ParsoidParser: Record ParserOptions watcher on ParserOutput object
[mediawiki.git] / includes / title / NamespaceInfo.php
blob0dab9c5005043b984589f98a3ed1e63139c63392
1 <?php
2 /**
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
20 * @file
23 use MediaWiki\Config\ServiceOptions;
24 use MediaWiki\HookContainer\HookContainer;
25 use MediaWiki\HookContainer\HookRunner;
26 use MediaWiki\Linker\LinkTarget;
27 use MediaWiki\MainConfigNames;
29 /**
30 * This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
31 * them based on index. The textual names of the namespaces are handled by Language.php.
33 * @since 1.34
35 class NamespaceInfo {
37 /**
38 * These namespaces should always be first-letter capitalized, now and
39 * forevermore. Historically, they could've probably been lowercased too,
40 * but some things are just too ingrained now. :)
42 private $alwaysCapitalizedNamespaces = [ NS_SPECIAL, NS_USER, NS_MEDIAWIKI ];
44 /** @var string[]|null Canonical namespaces cache */
45 private $canonicalNamespaces = null;
47 /** @var array|false Canonical namespaces index cache */
48 private $namespaceIndexes = false;
50 /** @var int[]|null Valid namespaces cache */
51 private $validNamespaces = null;
53 /** @var ServiceOptions */
54 private $options;
56 /** @var HookRunner */
57 private $hookRunner;
59 /**
60 * Definitions of the NS_ constants are in Defines.php
62 * @internal
64 public const CANONICAL_NAMES = [
65 NS_MEDIA => 'Media',
66 NS_SPECIAL => 'Special',
67 NS_MAIN => '',
68 NS_TALK => 'Talk',
69 NS_USER => 'User',
70 NS_USER_TALK => 'User_talk',
71 NS_PROJECT => 'Project',
72 NS_PROJECT_TALK => 'Project_talk',
73 NS_FILE => 'File',
74 NS_FILE_TALK => 'File_talk',
75 NS_MEDIAWIKI => 'MediaWiki',
76 NS_MEDIAWIKI_TALK => 'MediaWiki_talk',
77 NS_TEMPLATE => 'Template',
78 NS_TEMPLATE_TALK => 'Template_talk',
79 NS_HELP => 'Help',
80 NS_HELP_TALK => 'Help_talk',
81 NS_CATEGORY => 'Category',
82 NS_CATEGORY_TALK => 'Category_talk',
85 /**
86 * @internal For use by ServiceWiring
88 public const CONSTRUCTOR_OPTIONS = [
89 MainConfigNames::CanonicalNamespaceNames,
90 MainConfigNames::CapitalLinkOverrides,
91 MainConfigNames::CapitalLinks,
92 MainConfigNames::ContentNamespaces,
93 MainConfigNames::ExtraNamespaces,
94 MainConfigNames::ExtraSignatureNamespaces,
95 MainConfigNames::NamespaceContentModels,
96 MainConfigNames::NamespacesWithSubpages,
97 MainConfigNames::NonincludableNamespaces,
101 * @param ServiceOptions $options
102 * @param HookContainer $hookContainer
104 public function __construct( ServiceOptions $options, HookContainer $hookContainer ) {
105 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
106 $this->options = $options;
107 $this->hookRunner = new HookRunner( $hookContainer );
111 * Throw an exception when trying to get the subject or talk page
112 * for a given namespace where it does not make sense.
113 * Special namespaces are defined in includes/Defines.php and have
114 * a value below 0 (ex: NS_SPECIAL = -1 , NS_MEDIA = -2)
116 * @param int $index
117 * @param string $method
119 * @throws MWException
120 * @return bool
122 private function isMethodValidFor( $index, $method ) {
123 if ( $index < NS_MAIN ) {
124 throw new MWException( "$method does not make any sense for given namespace $index" );
126 return true;
130 * Throw if given index isn't an integer or integer-like string and so can't be a valid namespace.
132 * @param int|string $index
133 * @param string $method
135 * @throws InvalidArgumentException
136 * @return int Cleaned up namespace index
138 private function makeValidNamespace( $index, $method ) {
139 if ( !(
140 is_int( $index )
141 // Namespace index numbers as strings
142 || ctype_digit( $index )
143 // Negative numbers as strings
144 || ( $index[0] === '-' && ctype_digit( substr( $index, 1 ) ) )
145 ) ) {
146 throw new InvalidArgumentException(
147 "$method called with non-integer (" . gettype( $index ) . ") namespace '$index'"
151 return intval( $index );
155 * Can pages in the given namespace be moved?
157 * @param int $index Namespace index
158 * @return bool
160 public function isMovable( $index ) {
161 $extensionRegistry = ExtensionRegistry::getInstance();
162 $extNamespaces = $extensionRegistry->getAttribute( 'ImmovableNamespaces' );
164 $result = $index >= NS_MAIN && !in_array( $index, $extNamespaces );
167 * @since 1.20
169 $this->hookRunner->onNamespaceIsMovable( $index, $result );
171 return $result;
175 * Is the given namespace is a subject (non-talk) namespace?
177 * @param int $index Namespace index
178 * @return bool
180 public function isSubject( $index ) {
181 return !$this->isTalk( $index );
185 * Is the given namespace a talk namespace?
187 * @param int $index Namespace index
188 * @return bool
190 public function isTalk( $index ) {
191 $index = $this->makeValidNamespace( $index, __METHOD__ );
193 return $index > NS_MAIN
194 && $index % 2;
198 * Get the talk namespace index for a given namespace
200 * @param int $index Namespace index
201 * @return int
202 * @throws MWException if the given namespace doesn't have an associated talk namespace
203 * (e.g. NS_SPECIAL).
205 public function getTalk( $index ) {
206 $index = $this->makeValidNamespace( $index, __METHOD__ );
208 $this->isMethodValidFor( $index, __METHOD__ );
209 return $this->isTalk( $index )
210 ? $index
211 : $index + 1;
215 * Get a LinkTarget referring to the talk page of $target.
217 * @see canHaveTalkPage
218 * @param LinkTarget $target
219 * @return LinkTarget Talk page for $target
220 * @throws MWException if $target doesn't have talk pages, e.g. because it's in NS_SPECIAL,
221 * because it's a relative section-only link, or it's an interwiki link.
223 public function getTalkPage( LinkTarget $target ): LinkTarget {
224 if ( $target->getText() === '' ) {
225 throw new MWException( 'Can\'t determine talk page associated with relative section link' );
228 if ( $target->getInterwiki() !== '' ) {
229 throw new MWException( 'Can\'t determine talk page associated with interwiki link' );
232 if ( $this->isTalk( $target->getNamespace() ) ) {
233 return $target;
236 // NOTE: getTalk throws on bad namespaces!
237 return new TitleValue( $this->getTalk( $target->getNamespace() ), $target->getDBkey() );
241 * Can the title have a corresponding talk page?
243 * False for relative section-only links (with getText() === ''),
244 * interwiki links (with getInterwiki() !== ''), and pages in NS_SPECIAL.
246 * @see getTalkPage
248 * @param LinkTarget $target
249 * @return bool True if this title either is a talk page or can have a talk page associated.
251 public function canHaveTalkPage( LinkTarget $target ) {
252 return $target->getNamespace() >= NS_MAIN &&
253 !$target->isExternal() &&
254 $target->getText() !== '';
258 * Get the subject namespace index for a given namespace
259 * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
261 * @param int $index Namespace index
262 * @return int
264 public function getSubject( $index ) {
265 $index = $this->makeValidNamespace( $index, __METHOD__ );
267 # Handle special namespaces
268 if ( $index < NS_MAIN ) {
269 return $index;
272 return $this->isTalk( $index )
273 ? $index - 1
274 : $index;
278 * @param LinkTarget $target
279 * @return LinkTarget Subject page for $target
281 public function getSubjectPage( LinkTarget $target ): LinkTarget {
282 if ( $this->isSubject( $target->getNamespace() ) ) {
283 return $target;
285 return new TitleValue( $this->getSubject( $target->getNamespace() ), $target->getDBkey() );
289 * Get the associated namespace.
290 * For talk namespaces, returns the subject (non-talk) namespace
291 * For subject (non-talk) namespaces, returns the talk namespace
293 * @param int $index Namespace index
294 * @return int
295 * @throws MWException if called on a namespace that has no talk pages (e.g., NS_SPECIAL)
297 public function getAssociated( $index ) {
298 $this->isMethodValidFor( $index, __METHOD__ );
300 if ( $this->isSubject( $index ) ) {
301 return $this->getTalk( $index );
303 return $this->getSubject( $index );
307 * @param LinkTarget $target
308 * @return LinkTarget Talk page for $target if it's a subject page, subject page if it's a talk
309 * page
310 * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL)
312 public function getAssociatedPage( LinkTarget $target ): LinkTarget {
313 if ( $target->getText() === '' ) {
314 throw new MWException( 'Can\'t determine talk page associated with relative section link' );
317 if ( $target->getInterwiki() !== '' ) {
318 throw new MWException( 'Can\'t determine talk page associated with interwiki link' );
321 return new TitleValue(
322 $this->getAssociated( $target->getNamespace() ), $target->getDBkey() );
326 * Returns whether the specified namespace exists
328 * @param int $index
330 * @return bool
332 public function exists( $index ) {
333 $nslist = $this->getCanonicalNamespaces();
334 return isset( $nslist[$index] );
338 * Returns whether the specified namespaces are the same namespace
340 * @note It's possible that in the future we may start using something
341 * other than just namespace indexes. Under that circumstance making use
342 * of this function rather than directly doing comparison will make
343 * sure that code will not potentially break.
345 * @param int $ns1 The first namespace index
346 * @param int $ns2 The second namespace index
348 * @return bool
350 public function equals( $ns1, $ns2 ) {
351 return $ns1 == $ns2;
355 * Returns whether the specified namespaces share the same subject.
356 * eg: NS_USER and NS_USER wil return true, as well
357 * NS_USER and NS_USER_TALK will return true.
359 * @param int $ns1 The first namespace index
360 * @param int $ns2 The second namespace index
362 * @return bool
364 public function subjectEquals( $ns1, $ns2 ) {
365 return $this->getSubject( $ns1 ) == $this->getSubject( $ns2 );
369 * Returns array of all defined namespaces with their canonical
370 * (English) names.
372 * @return string[]
374 public function getCanonicalNamespaces() {
375 if ( $this->canonicalNamespaces === null ) {
376 $this->canonicalNamespaces =
377 [ NS_MAIN => '' ] + $this->options->get( MainConfigNames::CanonicalNamespaceNames );
378 $this->canonicalNamespaces +=
379 ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' );
380 if ( is_array( $this->options->get( MainConfigNames::ExtraNamespaces ) ) ) {
381 $this->canonicalNamespaces += $this->options->get( MainConfigNames::ExtraNamespaces );
383 $this->hookRunner->onCanonicalNamespaces( $this->canonicalNamespaces );
385 return $this->canonicalNamespaces;
389 * Returns the canonical (English) name for a given index
391 * @param int $index Namespace index
392 * @return string|false If no canonical definition.
394 public function getCanonicalName( $index ) {
395 $nslist = $this->getCanonicalNamespaces();
396 return $nslist[$index] ?? false;
400 * Returns the index for a given canonical name, or NULL
401 * The input *must* be converted to lower case first
403 * @param string $name Namespace name
404 * @return int|null
406 public function getCanonicalIndex( $name ) {
407 if ( $this->namespaceIndexes === false ) {
408 $this->namespaceIndexes = [];
409 foreach ( $this->getCanonicalNamespaces() as $i => $text ) {
410 $this->namespaceIndexes[strtolower( $text )] = $i;
413 if ( array_key_exists( $name, $this->namespaceIndexes ) ) {
414 return $this->namespaceIndexes[$name];
415 } else {
416 return null;
421 * Returns an array of the namespaces (by integer id) that exist on the wiki. Used primarily by
422 * the API in help documentation. The array is sorted numerically and omits negative namespaces.
423 * @return array
425 public function getValidNamespaces() {
426 if ( $this->validNamespaces === null ) {
427 $this->validNamespaces = [];
428 foreach ( array_keys( $this->getCanonicalNamespaces() ) as $ns ) {
429 if ( $ns >= 0 ) {
430 $this->validNamespaces[] = $ns;
433 // T109137: sort numerically
434 sort( $this->validNamespaces, SORT_NUMERIC );
437 return $this->validNamespaces;
441 * Does this namespace ever have a talk namespace?
443 * @param int $index Namespace ID
444 * @return bool True if this namespace either is or has a corresponding talk namespace.
446 public function hasTalkNamespace( $index ) {
447 return $index >= NS_MAIN;
451 * Does this namespace contain content, for the purposes of calculating
452 * statistics, etc?
454 * @param int $index Index to check
455 * @return bool
457 public function isContent( $index ) {
458 return $index == NS_MAIN ||
459 in_array( $index, $this->options->get( MainConfigNames::ContentNamespaces ) );
463 * Might pages in this namespace require the use of the Signature button on
464 * the edit toolbar?
466 * @param int $index Index to check
467 * @return bool
469 public function wantSignatures( $index ) {
470 return $this->isTalk( $index ) ||
471 in_array( $index, $this->options->get( MainConfigNames::ExtraSignatureNamespaces ) );
475 * Can pages in a namespace be watched?
477 * @param int $index
478 * @return bool
480 public function isWatchable( $index ) {
481 return $index >= NS_MAIN;
485 * Does the namespace allow subpages? Note that this refers to structured
486 * handling of subpages, and does not include SpecialPage subpage parameters.
488 * @param int $index Index to check
489 * @return bool
491 public function hasSubpages( $index ) {
492 return !empty( $this->options->get( MainConfigNames::NamespacesWithSubpages )[$index] );
496 * Get a list of all namespace indices which are considered to contain content
497 * @return int[] Array of namespace indices
499 public function getContentNamespaces() {
500 $contentNamespaces = $this->options->get( MainConfigNames::ContentNamespaces );
501 if ( !is_array( $contentNamespaces ) || $contentNamespaces === [] ) {
502 return [ NS_MAIN ];
503 } elseif ( !in_array( NS_MAIN, $contentNamespaces ) ) {
504 // always force NS_MAIN to be part of array (to match the algorithm used by isContent)
505 return array_merge( [ NS_MAIN ], $contentNamespaces );
506 } else {
507 return $contentNamespaces;
512 * List all namespace indices which are considered subject, aka not a talk
513 * or special namespace. See also NamespaceInfo::isSubject
515 * @return int[] Array of namespace indices
517 public function getSubjectNamespaces() {
518 return array_filter(
519 $this->getValidNamespaces(),
520 [ $this, 'isSubject' ]
525 * List all namespace indices which are considered talks, aka not a subject
526 * or special namespace. See also NamespaceInfo::isTalk
528 * @return int[] Array of namespace indices
530 public function getTalkNamespaces() {
531 return array_filter(
532 $this->getValidNamespaces(),
533 [ $this, 'isTalk' ]
538 * Is the namespace first-letter capitalized?
540 * @param int $index Index to check
541 * @return bool
543 public function isCapitalized( $index ) {
544 // Turn NS_MEDIA into NS_FILE
545 $index = $index === NS_MEDIA ? NS_FILE : $index;
547 // Make sure to get the subject of our namespace
548 $index = $this->getSubject( $index );
550 // Some namespaces are special and should always be upper case
551 if ( in_array( $index, $this->alwaysCapitalizedNamespaces ) ) {
552 return true;
554 $overrides = $this->options->get( MainConfigNames::CapitalLinkOverrides );
555 if ( isset( $overrides[$index] ) ) {
556 // CapitalLinkOverrides is explicitly set
557 return $overrides[$index];
559 // Default to the global setting
560 return $this->options->get( MainConfigNames::CapitalLinks );
564 * Does the namespace (potentially) have different aliases for different
565 * genders. Not all languages make a distinction here.
567 * @param int $index Index to check
568 * @return bool
570 public function hasGenderDistinction( $index ) {
571 return $index == NS_USER || $index == NS_USER_TALK;
575 * It is not possible to use pages from this namespace as template?
577 * @param int $index Index to check
578 * @return bool
580 public function isNonincludable( $index ) {
581 $namespaces = $this->options->get( MainConfigNames::NonincludableNamespaces );
582 return $namespaces && in_array( $index, $namespaces );
586 * Get the default content model for a namespace
587 * This does not mean that all pages in that namespace have the model
589 * @note To determine the default model for a new page's main slot, or any slot in general,
590 * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler().
592 * @param int $index Index to check
593 * @return null|string Default model name for the given namespace, if set
595 public function getNamespaceContentModel( $index ) {
596 return $this->options->get( MainConfigNames::NamespaceContentModels )[$index] ?? null;
600 * Returns the link type to be used for categories.
602 * This determines which section of a category page titles
603 * in the namespace will appear within.
605 * @param int $index Namespace index
606 * @return string One of 'subcat', 'file', 'page'
608 public function getCategoryLinkType( $index ) {
609 $this->isMethodValidFor( $index, __METHOD__ );
611 if ( $index == NS_CATEGORY ) {
612 return 'subcat';
613 } elseif ( $index == NS_FILE ) {
614 return 'file';
615 } else {
616 return 'page';
621 * Retrieve the indexes for the namespaces defined by core.
623 * @since 1.34
625 * @return int[]
627 public static function getCommonNamespaces() {
628 return array_keys( self::CANONICAL_NAMES );