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
24 * This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
25 * them based on index. The textual names of the namespaces are handled by Language.php.
32 * These namespaces should always be first-letter capitalized, now and
33 * forevermore. Historically, they could've probably been lowercased too,
34 * but some things are just too ingrained now. :)
36 private $alwaysCapitalizedNamespaces = [ NS_SPECIAL
, NS_USER
, NS_MEDIAWIKI
];
38 /** @var string[]|null Canonical namespaces cache */
39 private $canonicalNamespaces = null;
41 /** @var array|false Canonical namespaces index cache */
42 private $namespaceIndexes = false;
44 /** @var int[]|null Valid namespaces cache */
45 private $validNamespaces = null;
51 * @param Config $config
53 public function __construct( Config
$config ) {
54 $this->config
= $config;
58 * Throw an exception when trying to get the subject or talk page
59 * for a given namespace where it does not make sense.
60 * Special namespaces are defined in includes/Defines.php and have
61 * a value below 0 (ex: NS_SPECIAL = -1 , NS_MEDIA = -2)
64 * @param string $method
69 private function isMethodValidFor( $index, $method ) {
70 if ( $index < NS_MAIN
) {
71 throw new MWException( "$method does not make any sense for given namespace $index" );
77 * Can pages in the given namespace be moved?
79 * @param int $index Namespace index
82 public function isMovable( $index ) {
83 $result = !( $index < NS_MAIN ||
84 ( $index == NS_FILE
&& !$this->config
->get( 'AllowImageMoving' ) ) );
89 Hooks
::run( 'NamespaceIsMovable', [ $index, &$result ] );
95 * Is the given namespace is a subject (non-talk) namespace?
97 * @param int $index Namespace index
100 public function isSubject( $index ) {
101 return !$this->isTalk( $index );
105 * Is the given namespace a talk namespace?
107 * @param int $index Namespace index
110 public function isTalk( $index ) {
111 return $index > NS_MAIN
116 * Get the talk namespace index for a given namespace
118 * @param int $index Namespace index
121 public function getTalk( $index ) {
122 $this->isMethodValidFor( $index, __METHOD__
);
123 return $this->isTalk( $index )
129 * Get the subject namespace index for a given namespace
130 * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
132 * @param int $index Namespace index
135 public function getSubject( $index ) {
136 # Handle special namespaces
137 if ( $index < NS_MAIN
) {
141 return $this->isTalk( $index )
147 * Get the associated namespace.
148 * For talk namespaces, returns the subject (non-talk) namespace
149 * For subject (non-talk) namespaces, returns the talk namespace
151 * @param int $index Namespace index
152 * @return int|null If no associated namespace could be found
154 public function getAssociated( $index ) {
155 $this->isMethodValidFor( $index, __METHOD__
);
157 if ( $this->isSubject( $index ) ) {
158 return $this->getTalk( $index );
159 } elseif ( $this->isTalk( $index ) ) {
160 return $this->getSubject( $index );
167 * Returns whether the specified namespace exists
173 public function exists( $index ) {
174 $nslist = $this->getCanonicalNamespaces();
175 return isset( $nslist[$index] );
179 * Returns whether the specified namespaces are the same namespace
181 * @note It's possible that in the future we may start using something
182 * other than just namespace indexes. Under that circumstance making use
183 * of this function rather than directly doing comparison will make
184 * sure that code will not potentially break.
186 * @param int $ns1 The first namespace index
187 * @param int $ns2 The second namespace index
191 public function equals( $ns1, $ns2 ) {
196 * Returns whether the specified namespaces share the same subject.
197 * eg: NS_USER and NS_USER wil return true, as well
198 * NS_USER and NS_USER_TALK will return true.
200 * @param int $ns1 The first namespace index
201 * @param int $ns2 The second namespace index
205 public function subjectEquals( $ns1, $ns2 ) {
206 return $this->getSubject( $ns1 ) == $this->getSubject( $ns2 );
210 * Returns array of all defined namespaces with their canonical
215 public function getCanonicalNamespaces() {
216 if ( $this->canonicalNamespaces
=== null ) {
217 $this->canonicalNamespaces
=
218 [ NS_MAIN
=> '' ] +
$this->config
->get( 'CanonicalNamespaceNames' );
219 $this->canonicalNamespaces +
=
220 ExtensionRegistry
::getInstance()->getAttribute( 'ExtensionNamespaces' );
221 if ( is_array( $this->config
->get( 'ExtraNamespaces' ) ) ) {
222 $this->canonicalNamespaces +
= $this->config
->get( 'ExtraNamespaces' );
224 Hooks
::run( 'CanonicalNamespaces', [ &$this->canonicalNamespaces
] );
226 return $this->canonicalNamespaces
;
230 * Returns the canonical (English) name for a given index
232 * @param int $index Namespace index
233 * @return string|bool If no canonical definition.
235 public function getCanonicalName( $index ) {
236 $nslist = $this->getCanonicalNamespaces();
237 return $nslist[$index] ??
false;
241 * Returns the index for a given canonical name, or NULL
242 * The input *must* be converted to lower case first
244 * @param string $name Namespace name
247 public function getCanonicalIndex( $name ) {
248 if ( $this->namespaceIndexes
=== false ) {
249 $this->namespaceIndexes
= [];
250 foreach ( $this->getCanonicalNamespaces() as $i => $text ) {
251 $this->namespaceIndexes
[strtolower( $text )] = $i;
254 if ( array_key_exists( $name, $this->namespaceIndexes
) ) {
255 return $this->namespaceIndexes
[$name];
262 * Returns an array of the namespaces (by integer id) that exist on the
263 * wiki. Used primarily by the api in help documentation.
266 public function getValidNamespaces() {
267 if ( is_null( $this->validNamespaces
) ) {
268 foreach ( array_keys( $this->getCanonicalNamespaces() ) as $ns ) {
270 $this->validNamespaces
[] = $ns;
273 // T109137: sort numerically
274 sort( $this->validNamespaces
, SORT_NUMERIC
);
277 return $this->validNamespaces
;
283 * Does this namespace ever have a talk namespace?
285 * @param int $index Namespace ID
286 * @return bool True if this namespace either is or has a corresponding talk namespace.
288 public function hasTalkNamespace( $index ) {
289 return $index >= NS_MAIN
;
293 * Does this namespace contain content, for the purposes of calculating
296 * @param int $index Index to check
299 public function isContent( $index ) {
300 return $index == NS_MAIN ||
in_array( $index, $this->config
->get( 'ContentNamespaces' ) );
304 * Might pages in this namespace require the use of the Signature button on
307 * @param int $index Index to check
310 public function wantSignatures( $index ) {
311 return $this->isTalk( $index ) ||
312 in_array( $index, $this->config
->get( 'ExtraSignatureNamespaces' ) );
316 * Can pages in a namespace be watched?
321 public function isWatchable( $index ) {
322 return $index >= NS_MAIN
;
326 * Does the namespace allow subpages?
328 * @param int $index Index to check
331 public function hasSubpages( $index ) {
332 return !empty( $this->config
->get( 'NamespacesWithSubpages' )[$index] );
336 * Get a list of all namespace indices which are considered to contain content
337 * @return array Array of namespace indices
339 public function getContentNamespaces() {
340 $contentNamespaces = $this->config
->get( 'ContentNamespaces' );
341 if ( !is_array( $contentNamespaces ) ||
$contentNamespaces === [] ) {
343 } elseif ( !in_array( NS_MAIN
, $contentNamespaces ) ) {
344 // always force NS_MAIN to be part of array (to match the algorithm used by isContent)
345 return array_merge( [ NS_MAIN
], $contentNamespaces );
347 return $contentNamespaces;
352 * List all namespace indices which are considered subject, aka not a talk
353 * or special namespace. See also NamespaceInfo::isSubject
355 * @return array Array of namespace indices
357 public function getSubjectNamespaces() {
359 $this->getValidNamespaces(),
360 [ $this, 'isSubject' ]
365 * List all namespace indices which are considered talks, aka not a subject
366 * or special namespace. See also NamespaceInfo::isTalk
368 * @return array Array of namespace indices
370 public function getTalkNamespaces() {
372 $this->getValidNamespaces(),
378 * Is the namespace first-letter capitalized?
380 * @param int $index Index to check
383 public function isCapitalized( $index ) {
384 // Turn NS_MEDIA into NS_FILE
385 $index = $index === NS_MEDIA ? NS_FILE
: $index;
387 // Make sure to get the subject of our namespace
388 $index = $this->getSubject( $index );
390 // Some namespaces are special and should always be upper case
391 if ( in_array( $index, $this->alwaysCapitalizedNamespaces
) ) {
394 $overrides = $this->config
->get( 'CapitalLinkOverrides' );
395 if ( isset( $overrides[$index] ) ) {
396 // CapitalLinkOverrides is explicitly set
397 return $overrides[$index];
399 // Default to the global setting
400 return $this->config
->get( 'CapitalLinks' );
404 * Does the namespace (potentially) have different aliases for different
405 * genders. Not all languages make a distinction here.
407 * @param int $index Index to check
410 public function hasGenderDistinction( $index ) {
411 return $index == NS_USER ||
$index == NS_USER_TALK
;
415 * It is not possible to use pages from this namespace as template?
417 * @param int $index Index to check
420 public function isNonincludable( $index ) {
421 $namespaces = $this->config
->get( 'NonincludableNamespaces' );
422 return $namespaces && in_array( $index, $namespaces );
426 * Get the default content model for a namespace
427 * This does not mean that all pages in that namespace have the model
429 * @note To determine the default model for a new page's main slot, or any slot in general,
430 * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler().
432 * @param int $index Index to check
433 * @return null|string Default model name for the given namespace, if set
435 public function getNamespaceContentModel( $index ) {
436 return $this->config
->get( 'NamespaceContentModels' )[$index] ??
null;
440 * Determine which restriction levels it makes sense to use in a namespace,
441 * optionally filtered by a user's rights.
443 * @param int $index Index to check
444 * @param User|null $user User to check
447 public function getRestrictionLevels( $index, User
$user = null ) {
448 if ( !isset( $this->config
->get( 'NamespaceProtection' )[$index] ) ) {
449 // All levels are valid if there's no namespace restriction.
450 // But still filter by user, if necessary
451 $levels = $this->config
->get( 'RestrictionLevels' );
453 $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
455 if ( $right == 'sysop' ) {
456 $right = 'editprotected'; // BC
458 if ( $right == 'autoconfirmed' ) {
459 $right = 'editsemiprotected'; // BC
461 return ( $right == '' ||
$user->isAllowed( $right ) );
467 // First, get the list of groups that can edit this namespace.
468 $namespaceGroups = [];
469 $combine = 'array_merge';
470 foreach ( (array)$this->config
->get( 'NamespaceProtection' )[$index] as $right ) {
471 if ( $right == 'sysop' ) {
472 $right = 'editprotected'; // BC
474 if ( $right == 'autoconfirmed' ) {
475 $right = 'editsemiprotected'; // BC
477 if ( $right != '' ) {
478 $namespaceGroups = call_user_func( $combine, $namespaceGroups,
479 User
::getGroupsWithPermission( $right ) );
480 $combine = 'array_intersect';
484 // Now, keep only those restriction levels where there is at least one
485 // group that can edit the namespace but would be blocked by the
487 $usableLevels = [ '' ];
488 foreach ( $this->config
->get( 'RestrictionLevels' ) as $level ) {
490 if ( $right == 'sysop' ) {
491 $right = 'editprotected'; // BC
493 if ( $right == 'autoconfirmed' ) {
494 $right = 'editsemiprotected'; // BC
496 if ( $right != '' && ( !$user ||
$user->isAllowed( $right ) ) &&
497 array_diff( $namespaceGroups, User
::getGroupsWithPermission( $right ) )
499 $usableLevels[] = $level;
503 return $usableLevels;
507 * Returns the link type to be used for categories.
509 * This determines which section of a category page titles
510 * in the namespace will appear within.
512 * @param int $index Namespace index
513 * @return string One of 'subcat', 'file', 'page'
515 public function getCategoryLinkType( $index ) {
516 $this->isMethodValidFor( $index, __METHOD__
);
518 if ( $index == NS_CATEGORY
) {
520 } elseif ( $index == NS_FILE
) {