3 * Provide things related to namespaces.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
23 use MediaWiki\Config\ServiceOptions
;
26 * This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
27 * them based on index. The textual names of the namespaces are handled by Language.php.
34 * These namespaces should always be first-letter capitalized, now and
35 * forevermore. Historically, they could've probably been lowercased too,
36 * but some things are just too ingrained now. :)
38 private $alwaysCapitalizedNamespaces = [ NS_SPECIAL
, NS_USER
, NS_MEDIAWIKI
];
40 /** @var string[]|null Canonical namespaces cache */
41 private $canonicalNamespaces = null;
43 /** @var array|false Canonical namespaces index cache */
44 private $namespaceIndexes = false;
46 /** @var int[]|null Valid namespaces cache */
47 private $validNamespaces = null;
49 /** @var ServiceOptions */
53 * TODO Make this const when HHVM support is dropped (T192166)
58 public static $constructorOptions = [
60 'CanonicalNamespaceNames',
61 'CapitalLinkOverrides',
65 'ExtraSignatureNamespaces',
66 'NamespaceContentModels',
67 'NamespaceProtection',
68 'NamespacesWithSubpages',
69 'NonincludableNamespaces',
74 * @param ServiceOptions $options
76 public function __construct( ServiceOptions
$options ) {
77 $options->assertRequiredOptions( self
::$constructorOptions );
78 $this->options
= $options;
82 * Throw an exception when trying to get the subject or talk page
83 * for a given namespace where it does not make sense.
84 * Special namespaces are defined in includes/Defines.php and have
85 * a value below 0 (ex: NS_SPECIAL = -1 , NS_MEDIA = -2)
88 * @param string $method
93 private function isMethodValidFor( $index, $method ) {
94 if ( $index < NS_MAIN
) {
95 throw new MWException( "$method does not make any sense for given namespace $index" );
101 * Can pages in the given namespace be moved?
103 * @param int $index Namespace index
106 public function isMovable( $index ) {
107 $result = !( $index < NS_MAIN ||
108 ( $index == NS_FILE
&& !$this->options
->get( 'AllowImageMoving' ) ) );
113 Hooks
::run( 'NamespaceIsMovable', [ $index, &$result ] );
119 * Is the given namespace is a subject (non-talk) namespace?
121 * @param int $index Namespace index
124 public function isSubject( $index ) {
125 return !$this->isTalk( $index );
129 * Is the given namespace a talk namespace?
131 * @param int $index Namespace index
134 public function isTalk( $index ) {
135 return $index > NS_MAIN
140 * Get the talk namespace index for a given namespace
142 * @param int $index Namespace index
145 public function getTalk( $index ) {
146 $this->isMethodValidFor( $index, __METHOD__
);
147 return $this->isTalk( $index )
153 * Get the subject namespace index for a given namespace
154 * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
156 * @param int $index Namespace index
159 public function getSubject( $index ) {
160 # Handle special namespaces
161 if ( $index < NS_MAIN
) {
165 return $this->isTalk( $index )
171 * Get the associated namespace.
172 * For talk namespaces, returns the subject (non-talk) namespace
173 * For subject (non-talk) namespaces, returns the talk namespace
175 * @param int $index Namespace index
176 * @return int|null If no associated namespace could be found
178 public function getAssociated( $index ) {
179 $this->isMethodValidFor( $index, __METHOD__
);
181 if ( $this->isSubject( $index ) ) {
182 return $this->getTalk( $index );
183 } elseif ( $this->isTalk( $index ) ) {
184 return $this->getSubject( $index );
191 * Returns whether the specified namespace exists
197 public function exists( $index ) {
198 $nslist = $this->getCanonicalNamespaces();
199 return isset( $nslist[$index] );
203 * Returns whether the specified namespaces are the same namespace
205 * @note It's possible that in the future we may start using something
206 * other than just namespace indexes. Under that circumstance making use
207 * of this function rather than directly doing comparison will make
208 * sure that code will not potentially break.
210 * @param int $ns1 The first namespace index
211 * @param int $ns2 The second namespace index
215 public function equals( $ns1, $ns2 ) {
220 * Returns whether the specified namespaces share the same subject.
221 * eg: NS_USER and NS_USER wil return true, as well
222 * NS_USER and NS_USER_TALK will return true.
224 * @param int $ns1 The first namespace index
225 * @param int $ns2 The second namespace index
229 public function subjectEquals( $ns1, $ns2 ) {
230 return $this->getSubject( $ns1 ) == $this->getSubject( $ns2 );
234 * Returns array of all defined namespaces with their canonical
239 public function getCanonicalNamespaces() {
240 if ( $this->canonicalNamespaces
=== null ) {
241 $this->canonicalNamespaces
=
242 [ NS_MAIN
=> '' ] +
$this->options
->get( 'CanonicalNamespaceNames' );
243 $this->canonicalNamespaces +
=
244 ExtensionRegistry
::getInstance()->getAttribute( 'ExtensionNamespaces' );
245 if ( is_array( $this->options
->get( 'ExtraNamespaces' ) ) ) {
246 $this->canonicalNamespaces +
= $this->options
->get( 'ExtraNamespaces' );
248 Hooks
::run( 'CanonicalNamespaces', [ &$this->canonicalNamespaces
] );
250 return $this->canonicalNamespaces
;
254 * Returns the canonical (English) name for a given index
256 * @param int $index Namespace index
257 * @return string|bool If no canonical definition.
259 public function getCanonicalName( $index ) {
260 $nslist = $this->getCanonicalNamespaces();
261 return $nslist[$index] ??
false;
265 * Returns the index for a given canonical name, or NULL
266 * The input *must* be converted to lower case first
268 * @param string $name Namespace name
271 public function getCanonicalIndex( $name ) {
272 if ( $this->namespaceIndexes
=== false ) {
273 $this->namespaceIndexes
= [];
274 foreach ( $this->getCanonicalNamespaces() as $i => $text ) {
275 $this->namespaceIndexes
[strtolower( $text )] = $i;
278 if ( array_key_exists( $name, $this->namespaceIndexes
) ) {
279 return $this->namespaceIndexes
[$name];
286 * Returns an array of the namespaces (by integer id) that exist on the
287 * wiki. Used primarily by the api in help documentation.
290 public function getValidNamespaces() {
291 if ( is_null( $this->validNamespaces
) ) {
292 foreach ( array_keys( $this->getCanonicalNamespaces() ) as $ns ) {
294 $this->validNamespaces
[] = $ns;
297 // T109137: sort numerically
298 sort( $this->validNamespaces
, SORT_NUMERIC
);
301 return $this->validNamespaces
;
307 * Does this namespace ever have a talk namespace?
309 * @param int $index Namespace ID
310 * @return bool True if this namespace either is or has a corresponding talk namespace.
312 public function hasTalkNamespace( $index ) {
313 return $index >= NS_MAIN
;
317 * Does this namespace contain content, for the purposes of calculating
320 * @param int $index Index to check
323 public function isContent( $index ) {
324 return $index == NS_MAIN ||
in_array( $index, $this->options
->get( 'ContentNamespaces' ) );
328 * Might pages in this namespace require the use of the Signature button on
331 * @param int $index Index to check
334 public function wantSignatures( $index ) {
335 return $this->isTalk( $index ) ||
336 in_array( $index, $this->options
->get( 'ExtraSignatureNamespaces' ) );
340 * Can pages in a namespace be watched?
345 public function isWatchable( $index ) {
346 return $index >= NS_MAIN
;
350 * Does the namespace allow subpages?
352 * @param int $index Index to check
355 public function hasSubpages( $index ) {
356 return !empty( $this->options
->get( 'NamespacesWithSubpages' )[$index] );
360 * Get a list of all namespace indices which are considered to contain content
361 * @return array Array of namespace indices
363 public function getContentNamespaces() {
364 $contentNamespaces = $this->options
->get( 'ContentNamespaces' );
365 if ( !is_array( $contentNamespaces ) ||
$contentNamespaces === [] ) {
367 } elseif ( !in_array( NS_MAIN
, $contentNamespaces ) ) {
368 // always force NS_MAIN to be part of array (to match the algorithm used by isContent)
369 return array_merge( [ NS_MAIN
], $contentNamespaces );
371 return $contentNamespaces;
376 * List all namespace indices which are considered subject, aka not a talk
377 * or special namespace. See also NamespaceInfo::isSubject
379 * @return array Array of namespace indices
381 public function getSubjectNamespaces() {
383 $this->getValidNamespaces(),
384 [ $this, 'isSubject' ]
389 * List all namespace indices which are considered talks, aka not a subject
390 * or special namespace. See also NamespaceInfo::isTalk
392 * @return array Array of namespace indices
394 public function getTalkNamespaces() {
396 $this->getValidNamespaces(),
402 * Is the namespace first-letter capitalized?
404 * @param int $index Index to check
407 public function isCapitalized( $index ) {
408 // Turn NS_MEDIA into NS_FILE
409 $index = $index === NS_MEDIA ? NS_FILE
: $index;
411 // Make sure to get the subject of our namespace
412 $index = $this->getSubject( $index );
414 // Some namespaces are special and should always be upper case
415 if ( in_array( $index, $this->alwaysCapitalizedNamespaces
) ) {
418 $overrides = $this->options
->get( 'CapitalLinkOverrides' );
419 if ( isset( $overrides[$index] ) ) {
420 // CapitalLinkOverrides is explicitly set
421 return $overrides[$index];
423 // Default to the global setting
424 return $this->options
->get( 'CapitalLinks' );
428 * Does the namespace (potentially) have different aliases for different
429 * genders. Not all languages make a distinction here.
431 * @param int $index Index to check
434 public function hasGenderDistinction( $index ) {
435 return $index == NS_USER ||
$index == NS_USER_TALK
;
439 * It is not possible to use pages from this namespace as template?
441 * @param int $index Index to check
444 public function isNonincludable( $index ) {
445 $namespaces = $this->options
->get( 'NonincludableNamespaces' );
446 return $namespaces && in_array( $index, $namespaces );
450 * Get the default content model for a namespace
451 * This does not mean that all pages in that namespace have the model
453 * @note To determine the default model for a new page's main slot, or any slot in general,
454 * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler().
456 * @param int $index Index to check
457 * @return null|string Default model name for the given namespace, if set
459 public function getNamespaceContentModel( $index ) {
460 return $this->options
->get( 'NamespaceContentModels' )[$index] ??
null;
464 * Determine which restriction levels it makes sense to use in a namespace,
465 * optionally filtered by a user's rights.
467 * @param int $index Index to check
468 * @param User|null $user User to check
471 public function getRestrictionLevels( $index, User
$user = null ) {
472 if ( !isset( $this->options
->get( 'NamespaceProtection' )[$index] ) ) {
473 // All levels are valid if there's no namespace restriction.
474 // But still filter by user, if necessary
475 $levels = $this->options
->get( 'RestrictionLevels' );
477 $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
479 if ( $right == 'sysop' ) {
480 $right = 'editprotected'; // BC
482 if ( $right == 'autoconfirmed' ) {
483 $right = 'editsemiprotected'; // BC
485 return ( $right == '' ||
$user->isAllowed( $right ) );
491 // First, get the list of groups that can edit this namespace.
492 $namespaceGroups = [];
493 $combine = 'array_merge';
494 foreach ( (array)$this->options
->get( 'NamespaceProtection' )[$index] as $right ) {
495 if ( $right == 'sysop' ) {
496 $right = 'editprotected'; // BC
498 if ( $right == 'autoconfirmed' ) {
499 $right = 'editsemiprotected'; // BC
501 if ( $right != '' ) {
502 $namespaceGroups = call_user_func( $combine, $namespaceGroups,
503 User
::getGroupsWithPermission( $right ) );
504 $combine = 'array_intersect';
508 // Now, keep only those restriction levels where there is at least one
509 // group that can edit the namespace but would be blocked by the
511 $usableLevels = [ '' ];
512 foreach ( $this->options
->get( 'RestrictionLevels' ) as $level ) {
514 if ( $right == 'sysop' ) {
515 $right = 'editprotected'; // BC
517 if ( $right == 'autoconfirmed' ) {
518 $right = 'editsemiprotected'; // BC
520 if ( $right != '' && ( !$user ||
$user->isAllowed( $right ) ) &&
521 array_diff( $namespaceGroups, User
::getGroupsWithPermission( $right ) )
523 $usableLevels[] = $level;
527 return $usableLevels;
531 * Returns the link type to be used for categories.
533 * This determines which section of a category page titles
534 * in the namespace will appear within.
536 * @param int $index Namespace index
537 * @return string One of 'subcat', 'file', 'page'
539 public function getCategoryLinkType( $index ) {
540 $this->isMethodValidFor( $index, __METHOD__
);
542 if ( $index == NS_CATEGORY
) {
544 } elseif ( $index == NS_FILE
) {