NamespaceInfo.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  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. use MediaWiki\Config\ServiceOptions;
  23. use MediaWiki\Linker\LinkTarget;
  24. use MediaWiki\MediaWikiServices;
  25. /**
  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.
  28. *
  29. * @since 1.34
  30. */
  31. class NamespaceInfo {
  32. /**
  33. * These namespaces should always be first-letter capitalized, now and
  34. * forevermore. Historically, they could've probably been lowercased too,
  35. * but some things are just too ingrained now. :)
  36. */
  37. private $alwaysCapitalizedNamespaces = [ NS_SPECIAL, NS_USER, NS_MEDIAWIKI ];
  38. /** @var string[]|null Canonical namespaces cache */
  39. private $canonicalNamespaces = null;
  40. /** @var array|false Canonical namespaces index cache */
  41. private $namespaceIndexes = false;
  42. /** @var int[]|null Valid namespaces cache */
  43. private $validNamespaces = null;
  44. /** @var ServiceOptions */
  45. private $options;
  46. /**
  47. * Definitions of the NS_ constants are in Defines.php
  48. *
  49. * @todo Make this const when HHVM support is dropped (T192166)
  50. *
  51. * @var array
  52. * @internal
  53. */
  54. public static $canonicalNames = [
  55. NS_MEDIA => 'Media',
  56. NS_SPECIAL => 'Special',
  57. NS_MAIN => '',
  58. NS_TALK => 'Talk',
  59. NS_USER => 'User',
  60. NS_USER_TALK => 'User_talk',
  61. NS_PROJECT => 'Project',
  62. NS_PROJECT_TALK => 'Project_talk',
  63. NS_FILE => 'File',
  64. NS_FILE_TALK => 'File_talk',
  65. NS_MEDIAWIKI => 'MediaWiki',
  66. NS_MEDIAWIKI_TALK => 'MediaWiki_talk',
  67. NS_TEMPLATE => 'Template',
  68. NS_TEMPLATE_TALK => 'Template_talk',
  69. NS_HELP => 'Help',
  70. NS_HELP_TALK => 'Help_talk',
  71. NS_CATEGORY => 'Category',
  72. NS_CATEGORY_TALK => 'Category_talk',
  73. ];
  74. /**
  75. * TODO Make this const when HHVM support is dropped (T192166)
  76. *
  77. * @since 1.34
  78. * @var array
  79. */
  80. public static $constructorOptions = [
  81. 'AllowImageMoving',
  82. 'CanonicalNamespaceNames',
  83. 'CapitalLinkOverrides',
  84. 'CapitalLinks',
  85. 'ContentNamespaces',
  86. 'ExtraNamespaces',
  87. 'ExtraSignatureNamespaces',
  88. 'NamespaceContentModels',
  89. 'NamespacesWithSubpages',
  90. 'NonincludableNamespaces',
  91. ];
  92. /**
  93. * @param ServiceOptions $options
  94. */
  95. public function __construct( ServiceOptions $options ) {
  96. $options->assertRequiredOptions( self::$constructorOptions );
  97. $this->options = $options;
  98. }
  99. /**
  100. * Throw an exception when trying to get the subject or talk page
  101. * for a given namespace where it does not make sense.
  102. * Special namespaces are defined in includes/Defines.php and have
  103. * a value below 0 (ex: NS_SPECIAL = -1 , NS_MEDIA = -2)
  104. *
  105. * @param int $index
  106. * @param string $method
  107. *
  108. * @throws MWException
  109. * @return bool
  110. */
  111. private function isMethodValidFor( $index, $method ) {
  112. if ( $index < NS_MAIN ) {
  113. throw new MWException( "$method does not make any sense for given namespace $index" );
  114. }
  115. return true;
  116. }
  117. /**
  118. * Can pages in the given namespace be moved?
  119. *
  120. * @param int $index Namespace index
  121. * @return bool
  122. */
  123. public function isMovable( $index ) {
  124. $result = $index >= NS_MAIN &&
  125. ( $index != NS_FILE || $this->options->get( 'AllowImageMoving' ) );
  126. /**
  127. * @since 1.20
  128. */
  129. Hooks::run( 'NamespaceIsMovable', [ $index, &$result ] );
  130. return $result;
  131. }
  132. /**
  133. * Is the given namespace is a subject (non-talk) namespace?
  134. *
  135. * @param int $index Namespace index
  136. * @return bool
  137. */
  138. public function isSubject( $index ) {
  139. return !$this->isTalk( $index );
  140. }
  141. /**
  142. * Is the given namespace a talk namespace?
  143. *
  144. * @param int $index Namespace index
  145. * @return bool
  146. */
  147. public function isTalk( $index ) {
  148. return $index > NS_MAIN
  149. && $index % 2;
  150. }
  151. /**
  152. * Get the talk namespace index for a given namespace
  153. *
  154. * @param int $index Namespace index
  155. * @return int
  156. * @throws MWException if the given namespace doesn't have an associated talk namespace
  157. * (e.g. NS_SPECIAL).
  158. */
  159. public function getTalk( $index ) {
  160. $this->isMethodValidFor( $index, __METHOD__ );
  161. return $this->isTalk( $index )
  162. ? $index
  163. : $index + 1;
  164. }
  165. /**
  166. * Get a LinkTarget referring to the talk page of $target.
  167. *
  168. * @see canHaveTalkPage
  169. * @param LinkTarget $target
  170. * @return LinkTarget Talk page for $target
  171. * @throws MWException if $target doesn't have talk pages, e.g. because it's in NS_SPECIAL,
  172. * because it's a relative section-only link, or it's an an interwiki link.
  173. */
  174. public function getTalkPage( LinkTarget $target ) : LinkTarget {
  175. if ( $target->getText() === '' ) {
  176. throw new MWException( 'Can\'t determine talk page associated with relative section link' );
  177. }
  178. if ( $target->getInterwiki() !== '' ) {
  179. throw new MWException( 'Can\'t determine talk page associated with interwiki link' );
  180. }
  181. if ( $this->isTalk( $target->getNamespace() ) ) {
  182. return $target;
  183. }
  184. // NOTE: getTalk throws on bad namespaces!
  185. return new TitleValue( $this->getTalk( $target->getNamespace() ), $target->getDBkey() );
  186. }
  187. /**
  188. * Can the title have a corresponding talk page?
  189. *
  190. * False for relative section-only links (with getText() === ''),
  191. * interwiki links (with getInterwiki() !== ''), and pages in NS_SPECIAL.
  192. *
  193. * @see getTalkPage
  194. *
  195. * @param LinkTarget $target
  196. * @return bool True if this title either is a talk page or can have a talk page associated.
  197. */
  198. public function canHaveTalkPage( LinkTarget $target ) {
  199. if ( $target->getText() === '' || $target->getInterwiki() !== '' ) {
  200. return false;
  201. }
  202. if ( $target->getNamespace() < NS_MAIN ) {
  203. return false;
  204. }
  205. return true;
  206. }
  207. /**
  208. * Get the subject namespace index for a given namespace
  209. * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
  210. *
  211. * @param int $index Namespace index
  212. * @return int
  213. */
  214. public function getSubject( $index ) {
  215. # Handle special namespaces
  216. if ( $index < NS_MAIN ) {
  217. return $index;
  218. }
  219. return $this->isTalk( $index )
  220. ? $index - 1
  221. : $index;
  222. }
  223. /**
  224. * @param LinkTarget $target
  225. * @return LinkTarget Subject page for $target
  226. */
  227. public function getSubjectPage( LinkTarget $target ) : LinkTarget {
  228. if ( $this->isSubject( $target->getNamespace() ) ) {
  229. return $target;
  230. }
  231. return new TitleValue( $this->getSubject( $target->getNamespace() ), $target->getDBkey() );
  232. }
  233. /**
  234. * Get the associated namespace.
  235. * For talk namespaces, returns the subject (non-talk) namespace
  236. * For subject (non-talk) namespaces, returns the talk namespace
  237. *
  238. * @param int $index Namespace index
  239. * @return int
  240. * @throws MWException if called on a namespace that has no talk pages (e.g., NS_SPECIAL)
  241. */
  242. public function getAssociated( $index ) {
  243. $this->isMethodValidFor( $index, __METHOD__ );
  244. if ( $this->isSubject( $index ) ) {
  245. return $this->getTalk( $index );
  246. }
  247. return $this->getSubject( $index );
  248. }
  249. /**
  250. * @param LinkTarget $target
  251. * @return LinkTarget Talk page for $target if it's a subject page, subject page if it's a talk
  252. * page
  253. * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL)
  254. */
  255. public function getAssociatedPage( LinkTarget $target ) : LinkTarget {
  256. if ( $target->getText() === '' ) {
  257. throw new MWException( 'Can\'t determine talk page associated with relative section link' );
  258. }
  259. if ( $target->getInterwiki() !== '' ) {
  260. throw new MWException( 'Can\'t determine talk page associated with interwiki link' );
  261. }
  262. return new TitleValue(
  263. $this->getAssociated( $target->getNamespace() ), $target->getDBkey() );
  264. }
  265. /**
  266. * Returns whether the specified namespace exists
  267. *
  268. * @param int $index
  269. *
  270. * @return bool
  271. */
  272. public function exists( $index ) {
  273. $nslist = $this->getCanonicalNamespaces();
  274. return isset( $nslist[$index] );
  275. }
  276. /**
  277. * Returns whether the specified namespaces are the same namespace
  278. *
  279. * @note It's possible that in the future we may start using something
  280. * other than just namespace indexes. Under that circumstance making use
  281. * of this function rather than directly doing comparison will make
  282. * sure that code will not potentially break.
  283. *
  284. * @param int $ns1 The first namespace index
  285. * @param int $ns2 The second namespace index
  286. *
  287. * @return bool
  288. */
  289. public function equals( $ns1, $ns2 ) {
  290. return $ns1 == $ns2;
  291. }
  292. /**
  293. * Returns whether the specified namespaces share the same subject.
  294. * eg: NS_USER and NS_USER wil return true, as well
  295. * NS_USER and NS_USER_TALK will return true.
  296. *
  297. * @param int $ns1 The first namespace index
  298. * @param int $ns2 The second namespace index
  299. *
  300. * @return bool
  301. */
  302. public function subjectEquals( $ns1, $ns2 ) {
  303. return $this->getSubject( $ns1 ) == $this->getSubject( $ns2 );
  304. }
  305. /**
  306. * Returns array of all defined namespaces with their canonical
  307. * (English) names.
  308. *
  309. * @return string[]
  310. */
  311. public function getCanonicalNamespaces() {
  312. if ( $this->canonicalNamespaces === null ) {
  313. $this->canonicalNamespaces =
  314. [ NS_MAIN => '' ] + $this->options->get( 'CanonicalNamespaceNames' );
  315. $this->canonicalNamespaces +=
  316. ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' );
  317. if ( is_array( $this->options->get( 'ExtraNamespaces' ) ) ) {
  318. $this->canonicalNamespaces += $this->options->get( 'ExtraNamespaces' );
  319. }
  320. Hooks::run( 'CanonicalNamespaces', [ &$this->canonicalNamespaces ] );
  321. }
  322. return $this->canonicalNamespaces;
  323. }
  324. /**
  325. * Returns the canonical (English) name for a given index
  326. *
  327. * @param int $index Namespace index
  328. * @return string|bool If no canonical definition.
  329. */
  330. public function getCanonicalName( $index ) {
  331. $nslist = $this->getCanonicalNamespaces();
  332. return $nslist[$index] ?? false;
  333. }
  334. /**
  335. * Returns the index for a given canonical name, or NULL
  336. * The input *must* be converted to lower case first
  337. *
  338. * @param string $name Namespace name
  339. * @return int|null
  340. */
  341. public function getCanonicalIndex( $name ) {
  342. if ( $this->namespaceIndexes === false ) {
  343. $this->namespaceIndexes = [];
  344. foreach ( $this->getCanonicalNamespaces() as $i => $text ) {
  345. $this->namespaceIndexes[strtolower( $text )] = $i;
  346. }
  347. }
  348. if ( array_key_exists( $name, $this->namespaceIndexes ) ) {
  349. return $this->namespaceIndexes[$name];
  350. } else {
  351. return null;
  352. }
  353. }
  354. /**
  355. * Returns an array of the namespaces (by integer id) that exist on the wiki. Used primarily by
  356. * the API in help documentation. The array is sorted numerically and omits negative namespaces.
  357. * @return array
  358. */
  359. public function getValidNamespaces() {
  360. if ( is_null( $this->validNamespaces ) ) {
  361. $this->validNamespaces = [];
  362. foreach ( array_keys( $this->getCanonicalNamespaces() ) as $ns ) {
  363. if ( $ns >= 0 ) {
  364. $this->validNamespaces[] = $ns;
  365. }
  366. }
  367. // T109137: sort numerically
  368. sort( $this->validNamespaces, SORT_NUMERIC );
  369. }
  370. return $this->validNamespaces;
  371. }
  372. /*
  373. /**
  374. * Does this namespace ever have a talk namespace?
  375. *
  376. * @param int $index Namespace ID
  377. * @return bool True if this namespace either is or has a corresponding talk namespace.
  378. */
  379. public function hasTalkNamespace( $index ) {
  380. return $index >= NS_MAIN;
  381. }
  382. /**
  383. * Does this namespace contain content, for the purposes of calculating
  384. * statistics, etc?
  385. *
  386. * @param int $index Index to check
  387. * @return bool
  388. */
  389. public function isContent( $index ) {
  390. return $index == NS_MAIN || in_array( $index, $this->options->get( 'ContentNamespaces' ) );
  391. }
  392. /**
  393. * Might pages in this namespace require the use of the Signature button on
  394. * the edit toolbar?
  395. *
  396. * @param int $index Index to check
  397. * @return bool
  398. */
  399. public function wantSignatures( $index ) {
  400. return $this->isTalk( $index ) ||
  401. in_array( $index, $this->options->get( 'ExtraSignatureNamespaces' ) );
  402. }
  403. /**
  404. * Can pages in a namespace be watched?
  405. *
  406. * @param int $index
  407. * @return bool
  408. */
  409. public function isWatchable( $index ) {
  410. return $index >= NS_MAIN;
  411. }
  412. /**
  413. * Does the namespace allow subpages?
  414. *
  415. * @param int $index Index to check
  416. * @return bool
  417. */
  418. public function hasSubpages( $index ) {
  419. return !empty( $this->options->get( 'NamespacesWithSubpages' )[$index] );
  420. }
  421. /**
  422. * Get a list of all namespace indices which are considered to contain content
  423. * @return array Array of namespace indices
  424. */
  425. public function getContentNamespaces() {
  426. $contentNamespaces = $this->options->get( 'ContentNamespaces' );
  427. if ( !is_array( $contentNamespaces ) || $contentNamespaces === [] ) {
  428. return [ NS_MAIN ];
  429. } elseif ( !in_array( NS_MAIN, $contentNamespaces ) ) {
  430. // always force NS_MAIN to be part of array (to match the algorithm used by isContent)
  431. return array_merge( [ NS_MAIN ], $contentNamespaces );
  432. } else {
  433. return $contentNamespaces;
  434. }
  435. }
  436. /**
  437. * List all namespace indices which are considered subject, aka not a talk
  438. * or special namespace. See also NamespaceInfo::isSubject
  439. *
  440. * @return array Array of namespace indices
  441. */
  442. public function getSubjectNamespaces() {
  443. return array_filter(
  444. $this->getValidNamespaces(),
  445. [ $this, 'isSubject' ]
  446. );
  447. }
  448. /**
  449. * List all namespace indices which are considered talks, aka not a subject
  450. * or special namespace. See also NamespaceInfo::isTalk
  451. *
  452. * @return array Array of namespace indices
  453. */
  454. public function getTalkNamespaces() {
  455. return array_filter(
  456. $this->getValidNamespaces(),
  457. [ $this, 'isTalk' ]
  458. );
  459. }
  460. /**
  461. * Is the namespace first-letter capitalized?
  462. *
  463. * @param int $index Index to check
  464. * @return bool
  465. */
  466. public function isCapitalized( $index ) {
  467. // Turn NS_MEDIA into NS_FILE
  468. $index = $index === NS_MEDIA ? NS_FILE : $index;
  469. // Make sure to get the subject of our namespace
  470. $index = $this->getSubject( $index );
  471. // Some namespaces are special and should always be upper case
  472. if ( in_array( $index, $this->alwaysCapitalizedNamespaces ) ) {
  473. return true;
  474. }
  475. $overrides = $this->options->get( 'CapitalLinkOverrides' );
  476. if ( isset( $overrides[$index] ) ) {
  477. // CapitalLinkOverrides is explicitly set
  478. return $overrides[$index];
  479. }
  480. // Default to the global setting
  481. return $this->options->get( 'CapitalLinks' );
  482. }
  483. /**
  484. * Does the namespace (potentially) have different aliases for different
  485. * genders. Not all languages make a distinction here.
  486. *
  487. * @param int $index Index to check
  488. * @return bool
  489. */
  490. public function hasGenderDistinction( $index ) {
  491. return $index == NS_USER || $index == NS_USER_TALK;
  492. }
  493. /**
  494. * It is not possible to use pages from this namespace as template?
  495. *
  496. * @param int $index Index to check
  497. * @return bool
  498. */
  499. public function isNonincludable( $index ) {
  500. $namespaces = $this->options->get( 'NonincludableNamespaces' );
  501. return $namespaces && in_array( $index, $namespaces );
  502. }
  503. /**
  504. * Get the default content model for a namespace
  505. * This does not mean that all pages in that namespace have the model
  506. *
  507. * @note To determine the default model for a new page's main slot, or any slot in general,
  508. * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler().
  509. *
  510. * @param int $index Index to check
  511. * @return null|string Default model name for the given namespace, if set
  512. */
  513. public function getNamespaceContentModel( $index ) {
  514. return $this->options->get( 'NamespaceContentModels' )[$index] ?? null;
  515. }
  516. /**
  517. * Determine which restriction levels it makes sense to use in a namespace,
  518. * optionally filtered by a user's rights.
  519. *
  520. * @deprecated since 1.34 User PermissionManager::getNamespaceRestrictionLevels instead.
  521. * @param int $index Index to check
  522. * @param User|null $user User to check
  523. * @return array
  524. */
  525. public function getRestrictionLevels( $index, User $user = null ) {
  526. // PermissionManager is not injected because adding an explicit dependency
  527. // breaks MW installer by adding a dependency chain on the database before
  528. // it was set up. Also, the method is deprecated and will be soon removed.
  529. return MediaWikiServices::getInstance()
  530. ->getPermissionManager()
  531. ->getNamespaceRestrictionLevels( $index, $user );
  532. }
  533. /**
  534. * Returns the link type to be used for categories.
  535. *
  536. * This determines which section of a category page titles
  537. * in the namespace will appear within.
  538. *
  539. * @param int $index Namespace index
  540. * @return string One of 'subcat', 'file', 'page'
  541. */
  542. public function getCategoryLinkType( $index ) {
  543. $this->isMethodValidFor( $index, __METHOD__ );
  544. if ( $index == NS_CATEGORY ) {
  545. return 'subcat';
  546. } elseif ( $index == NS_FILE ) {
  547. return 'file';
  548. } else {
  549. return 'page';
  550. }
  551. }
  552. /**
  553. * Retrieve the indexes for the namespaces defined by core.
  554. *
  555. * @since 1.34
  556. *
  557. * @return int[]
  558. */
  559. public static function getCommonNamespaces() {
  560. return array_keys( self::$canonicalNames );
  561. }
  562. }