1<?php 2/** 3 * Provide things related to namespaces. 4 * 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. 9 * 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. 14 * 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 19 * 20 * @file 21 */ 22 23use MediaWiki\Config\ServiceOptions; 24use MediaWiki\HookContainer\HookContainer; 25use MediaWiki\HookContainer\HookRunner; 26use MediaWiki\Linker\LinkTarget; 27use MediaWiki\MediaWikiServices; 28 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. 32 * 33 * @since 1.34 34 */ 35class NamespaceInfo { 36 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. :) 41 */ 42 private $alwaysCapitalizedNamespaces = [ NS_SPECIAL, NS_USER, NS_MEDIAWIKI ]; 43 44 /** @var string[]|null Canonical namespaces cache */ 45 private $canonicalNamespaces = null; 46 47 /** @var array|false Canonical namespaces index cache */ 48 private $namespaceIndexes = false; 49 50 /** @var int[]|null Valid namespaces cache */ 51 private $validNamespaces = null; 52 53 /** @var ServiceOptions */ 54 private $options; 55 56 /** @var HookRunner */ 57 private $hookRunner; 58 59 /** 60 * Definitions of the NS_ constants are in Defines.php 61 * 62 * @internal 63 */ 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', 83 ]; 84 85 /** 86 * @internal For use by ServiceWiring 87 */ 88 public const CONSTRUCTOR_OPTIONS = [ 89 'CanonicalNamespaceNames', 90 'CapitalLinkOverrides', 91 'CapitalLinks', 92 'ContentNamespaces', 93 'ExtraNamespaces', 94 'ExtraSignatureNamespaces', 95 'NamespaceContentModels', 96 'NamespacesWithSubpages', 97 'NonincludableNamespaces', 98 ]; 99 100 /** 101 * @param ServiceOptions $options 102 * @param HookContainer $hookContainer 103 */ 104 public function __construct( ServiceOptions $options, HookContainer $hookContainer ) { 105 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); 106 $this->options = $options; 107 $this->hookRunner = new HookRunner( $hookContainer ); 108 } 109 110 /** 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) 115 * 116 * @param int $index 117 * @param string $method 118 * 119 * @throws MWException 120 * @return bool 121 */ 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" ); 125 } 126 return true; 127 } 128 129 /** 130 * Throw if given index isn't an integer or integer-like string and so can't be a valid namespace. 131 * 132 * @param int|string $index 133 * @param string $method 134 * 135 * @throws InvalidArgumentException 136 * @return int Cleaned up namespace index 137 */ 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'" 148 ); 149 } 150 151 return intval( $index ); 152 } 153 154 /** 155 * Can pages in the given namespace be moved? 156 * 157 * @param int $index Namespace index 158 * @return bool 159 */ 160 public function isMovable( $index ) { 161 $extensionRegistry = ExtensionRegistry::getInstance(); 162 $extNamespaces = $extensionRegistry->getAttribute( 'ImmovableNamespaces' ); 163 164 $result = $index >= NS_MAIN && !in_array( $index, $extNamespaces ); 165 166 /** 167 * @since 1.20 168 */ 169 $this->hookRunner->onNamespaceIsMovable( $index, $result ); 170 171 return $result; 172 } 173 174 /** 175 * Is the given namespace is a subject (non-talk) namespace? 176 * 177 * @param int $index Namespace index 178 * @return bool 179 */ 180 public function isSubject( $index ) { 181 return !$this->isTalk( $index ); 182 } 183 184 /** 185 * Is the given namespace a talk namespace? 186 * 187 * @param int $index Namespace index 188 * @return bool 189 */ 190 public function isTalk( $index ) { 191 $index = $this->makeValidNamespace( $index, __METHOD__ ); 192 193 return $index > NS_MAIN 194 && $index % 2; 195 } 196 197 /** 198 * Get the talk namespace index for a given namespace 199 * 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). 204 */ 205 public function getTalk( $index ) { 206 $index = $this->makeValidNamespace( $index, __METHOD__ ); 207 208 $this->isMethodValidFor( $index, __METHOD__ ); 209 return $this->isTalk( $index ) 210 ? $index 211 : $index + 1; 212 } 213 214 /** 215 * Get a LinkTarget referring to the talk page of $target. 216 * 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. 222 */ 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' ); 226 } 227 228 if ( $target->getInterwiki() !== '' ) { 229 throw new MWException( 'Can\'t determine talk page associated with interwiki link' ); 230 } 231 232 if ( $this->isTalk( $target->getNamespace() ) ) { 233 return $target; 234 } 235 236 // NOTE: getTalk throws on bad namespaces! 237 return new TitleValue( $this->getTalk( $target->getNamespace() ), $target->getDBkey() ); 238 } 239 240 /** 241 * Can the title have a corresponding talk page? 242 * 243 * False for relative section-only links (with getText() === ''), 244 * interwiki links (with getInterwiki() !== ''), and pages in NS_SPECIAL. 245 * 246 * @see getTalkPage 247 * 248 * @param LinkTarget $target 249 * @return bool True if this title either is a talk page or can have a talk page associated. 250 */ 251 public function canHaveTalkPage( LinkTarget $target ) { 252 if ( $target->getText() === '' || $target->getInterwiki() !== '' ) { 253 return false; 254 } 255 256 if ( $target->getNamespace() < NS_MAIN ) { 257 return false; 258 } 259 260 return true; 261 } 262 263 /** 264 * Get the subject namespace index for a given namespace 265 * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject. 266 * 267 * @param int $index Namespace index 268 * @return int 269 */ 270 public function getSubject( $index ) { 271 $index = $this->makeValidNamespace( $index, __METHOD__ ); 272 273 # Handle special namespaces 274 if ( $index < NS_MAIN ) { 275 return $index; 276 } 277 278 return $this->isTalk( $index ) 279 ? $index - 1 280 : $index; 281 } 282 283 /** 284 * @param LinkTarget $target 285 * @return LinkTarget Subject page for $target 286 */ 287 public function getSubjectPage( LinkTarget $target ): LinkTarget { 288 if ( $this->isSubject( $target->getNamespace() ) ) { 289 return $target; 290 } 291 return new TitleValue( $this->getSubject( $target->getNamespace() ), $target->getDBkey() ); 292 } 293 294 /** 295 * Get the associated namespace. 296 * For talk namespaces, returns the subject (non-talk) namespace 297 * For subject (non-talk) namespaces, returns the talk namespace 298 * 299 * @param int $index Namespace index 300 * @return int 301 * @throws MWException if called on a namespace that has no talk pages (e.g., NS_SPECIAL) 302 */ 303 public function getAssociated( $index ) { 304 $this->isMethodValidFor( $index, __METHOD__ ); 305 306 if ( $this->isSubject( $index ) ) { 307 return $this->getTalk( $index ); 308 } 309 return $this->getSubject( $index ); 310 } 311 312 /** 313 * @param LinkTarget $target 314 * @return LinkTarget Talk page for $target if it's a subject page, subject page if it's a talk 315 * page 316 * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL) 317 */ 318 public function getAssociatedPage( LinkTarget $target ): LinkTarget { 319 if ( $target->getText() === '' ) { 320 throw new MWException( 'Can\'t determine talk page associated with relative section link' ); 321 } 322 323 if ( $target->getInterwiki() !== '' ) { 324 throw new MWException( 'Can\'t determine talk page associated with interwiki link' ); 325 } 326 327 return new TitleValue( 328 $this->getAssociated( $target->getNamespace() ), $target->getDBkey() ); 329 } 330 331 /** 332 * Returns whether the specified namespace exists 333 * 334 * @param int $index 335 * 336 * @return bool 337 */ 338 public function exists( $index ) { 339 $nslist = $this->getCanonicalNamespaces(); 340 return isset( $nslist[$index] ); 341 } 342 343 /** 344 * Returns whether the specified namespaces are the same namespace 345 * 346 * @note It's possible that in the future we may start using something 347 * other than just namespace indexes. Under that circumstance making use 348 * of this function rather than directly doing comparison will make 349 * sure that code will not potentially break. 350 * 351 * @param int $ns1 The first namespace index 352 * @param int $ns2 The second namespace index 353 * 354 * @return bool 355 */ 356 public function equals( $ns1, $ns2 ) { 357 return $ns1 == $ns2; 358 } 359 360 /** 361 * Returns whether the specified namespaces share the same subject. 362 * eg: NS_USER and NS_USER wil return true, as well 363 * NS_USER and NS_USER_TALK will return true. 364 * 365 * @param int $ns1 The first namespace index 366 * @param int $ns2 The second namespace index 367 * 368 * @return bool 369 */ 370 public function subjectEquals( $ns1, $ns2 ) { 371 return $this->getSubject( $ns1 ) == $this->getSubject( $ns2 ); 372 } 373 374 /** 375 * Returns array of all defined namespaces with their canonical 376 * (English) names. 377 * 378 * @return string[] 379 */ 380 public function getCanonicalNamespaces() { 381 if ( $this->canonicalNamespaces === null ) { 382 $this->canonicalNamespaces = 383 [ NS_MAIN => '' ] + $this->options->get( 'CanonicalNamespaceNames' ); 384 $this->canonicalNamespaces += 385 ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' ); 386 if ( is_array( $this->options->get( 'ExtraNamespaces' ) ) ) { 387 $this->canonicalNamespaces += $this->options->get( 'ExtraNamespaces' ); 388 } 389 $this->hookRunner->onCanonicalNamespaces( $this->canonicalNamespaces ); 390 } 391 return $this->canonicalNamespaces; 392 } 393 394 /** 395 * Returns the canonical (English) name for a given index 396 * 397 * @param int $index Namespace index 398 * @return string|bool If no canonical definition. 399 */ 400 public function getCanonicalName( $index ) { 401 $nslist = $this->getCanonicalNamespaces(); 402 return $nslist[$index] ?? false; 403 } 404 405 /** 406 * Returns the index for a given canonical name, or NULL 407 * The input *must* be converted to lower case first 408 * 409 * @param string $name Namespace name 410 * @return int|null 411 */ 412 public function getCanonicalIndex( $name ) { 413 if ( $this->namespaceIndexes === false ) { 414 $this->namespaceIndexes = []; 415 foreach ( $this->getCanonicalNamespaces() as $i => $text ) { 416 $this->namespaceIndexes[strtolower( $text )] = $i; 417 } 418 } 419 if ( array_key_exists( $name, $this->namespaceIndexes ) ) { 420 return $this->namespaceIndexes[$name]; 421 } else { 422 return null; 423 } 424 } 425 426 /** 427 * Returns an array of the namespaces (by integer id) that exist on the wiki. Used primarily by 428 * the API in help documentation. The array is sorted numerically and omits negative namespaces. 429 * @return array 430 */ 431 public function getValidNamespaces() { 432 if ( $this->validNamespaces === null ) { 433 $this->validNamespaces = []; 434 foreach ( array_keys( $this->getCanonicalNamespaces() ) as $ns ) { 435 if ( $ns >= 0 ) { 436 $this->validNamespaces[] = $ns; 437 } 438 } 439 // T109137: sort numerically 440 sort( $this->validNamespaces, SORT_NUMERIC ); 441 } 442 443 return $this->validNamespaces; 444 } 445 446 /** 447 * Does this namespace ever have a talk namespace? 448 * 449 * @param int $index Namespace ID 450 * @return bool True if this namespace either is or has a corresponding talk namespace. 451 */ 452 public function hasTalkNamespace( $index ) { 453 return $index >= NS_MAIN; 454 } 455 456 /** 457 * Does this namespace contain content, for the purposes of calculating 458 * statistics, etc? 459 * 460 * @param int $index Index to check 461 * @return bool 462 */ 463 public function isContent( $index ) { 464 return $index == NS_MAIN || in_array( $index, $this->options->get( 'ContentNamespaces' ) ); 465 } 466 467 /** 468 * Might pages in this namespace require the use of the Signature button on 469 * the edit toolbar? 470 * 471 * @param int $index Index to check 472 * @return bool 473 */ 474 public function wantSignatures( $index ) { 475 return $this->isTalk( $index ) || 476 in_array( $index, $this->options->get( 'ExtraSignatureNamespaces' ) ); 477 } 478 479 /** 480 * Can pages in a namespace be watched? 481 * 482 * @param int $index 483 * @return bool 484 */ 485 public function isWatchable( $index ) { 486 return $index >= NS_MAIN; 487 } 488 489 /** 490 * Does the namespace allow subpages? Note that this refers to structured 491 * handling of subpages, and does not include SpecialPage subpage parameters. 492 * 493 * @param int $index Index to check 494 * @return bool 495 */ 496 public function hasSubpages( $index ) { 497 return !empty( $this->options->get( 'NamespacesWithSubpages' )[$index] ); 498 } 499 500 /** 501 * Get a list of all namespace indices which are considered to contain content 502 * @return int[] Array of namespace indices 503 */ 504 public function getContentNamespaces() { 505 $contentNamespaces = $this->options->get( 'ContentNamespaces' ); 506 if ( !is_array( $contentNamespaces ) || $contentNamespaces === [] ) { 507 return [ NS_MAIN ]; 508 } elseif ( !in_array( NS_MAIN, $contentNamespaces ) ) { 509 // always force NS_MAIN to be part of array (to match the algorithm used by isContent) 510 return array_merge( [ NS_MAIN ], $contentNamespaces ); 511 } else { 512 return $contentNamespaces; 513 } 514 } 515 516 /** 517 * List all namespace indices which are considered subject, aka not a talk 518 * or special namespace. See also NamespaceInfo::isSubject 519 * 520 * @return int[] Array of namespace indices 521 */ 522 public function getSubjectNamespaces() { 523 return array_filter( 524 $this->getValidNamespaces(), 525 [ $this, 'isSubject' ] 526 ); 527 } 528 529 /** 530 * List all namespace indices which are considered talks, aka not a subject 531 * or special namespace. See also NamespaceInfo::isTalk 532 * 533 * @return int[] Array of namespace indices 534 */ 535 public function getTalkNamespaces() { 536 return array_filter( 537 $this->getValidNamespaces(), 538 [ $this, 'isTalk' ] 539 ); 540 } 541 542 /** 543 * Is the namespace first-letter capitalized? 544 * 545 * @param int $index Index to check 546 * @return bool 547 */ 548 public function isCapitalized( $index ) { 549 // Turn NS_MEDIA into NS_FILE 550 $index = $index === NS_MEDIA ? NS_FILE : $index; 551 552 // Make sure to get the subject of our namespace 553 $index = $this->getSubject( $index ); 554 555 // Some namespaces are special and should always be upper case 556 if ( in_array( $index, $this->alwaysCapitalizedNamespaces ) ) { 557 return true; 558 } 559 $overrides = $this->options->get( 'CapitalLinkOverrides' ); 560 if ( isset( $overrides[$index] ) ) { 561 // CapitalLinkOverrides is explicitly set 562 return $overrides[$index]; 563 } 564 // Default to the global setting 565 return $this->options->get( 'CapitalLinks' ); 566 } 567 568 /** 569 * Does the namespace (potentially) have different aliases for different 570 * genders. Not all languages make a distinction here. 571 * 572 * @param int $index Index to check 573 * @return bool 574 */ 575 public function hasGenderDistinction( $index ) { 576 return $index == NS_USER || $index == NS_USER_TALK; 577 } 578 579 /** 580 * It is not possible to use pages from this namespace as template? 581 * 582 * @param int $index Index to check 583 * @return bool 584 */ 585 public function isNonincludable( $index ) { 586 $namespaces = $this->options->get( 'NonincludableNamespaces' ); 587 return $namespaces && in_array( $index, $namespaces ); 588 } 589 590 /** 591 * Get the default content model for a namespace 592 * This does not mean that all pages in that namespace have the model 593 * 594 * @note To determine the default model for a new page's main slot, or any slot in general, 595 * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler(). 596 * 597 * @param int $index Index to check 598 * @return null|string Default model name for the given namespace, if set 599 */ 600 public function getNamespaceContentModel( $index ) { 601 return $this->options->get( 'NamespaceContentModels' )[$index] ?? null; 602 } 603 604 /** 605 * Determine which restriction levels it makes sense to use in a namespace, 606 * optionally filtered by a user's rights. 607 * 608 * @deprecated since 1.34 User PermissionManager::getNamespaceRestrictionLevels instead. 609 * @param int $index Index to check 610 * @param User|null $user User to check 611 * @return array 612 */ 613 public function getRestrictionLevels( $index, User $user = null ) { 614 // PermissionManager is not injected because adding an explicit dependency 615 // breaks MW installer by adding a dependency chain on the database before 616 // it was set up. Also, the method is deprecated and will be soon removed. 617 wfDeprecated( __METHOD__, '1.34' ); 618 return MediaWikiServices::getInstance() 619 ->getPermissionManager() 620 ->getNamespaceRestrictionLevels( $index, $user ); 621 } 622 623 /** 624 * Returns the link type to be used for categories. 625 * 626 * This determines which section of a category page titles 627 * in the namespace will appear within. 628 * 629 * @param int $index Namespace index 630 * @return string One of 'subcat', 'file', 'page' 631 */ 632 public function getCategoryLinkType( $index ) { 633 $this->isMethodValidFor( $index, __METHOD__ ); 634 635 if ( $index == NS_CATEGORY ) { 636 return 'subcat'; 637 } elseif ( $index == NS_FILE ) { 638 return 'file'; 639 } else { 640 return 'page'; 641 } 642 } 643 644 /** 645 * Retrieve the indexes for the namespaces defined by core. 646 * 647 * @since 1.34 648 * 649 * @return int[] 650 */ 651 public static function getCommonNamespaces() { 652 return array_keys( self::CANONICAL_NAMES ); 653 } 654} 655