MWNamespace.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  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. /**
  23. * This is a utility class with only static functions
  24. * for dealing with namespaces that encodes all the
  25. * "magic" behaviors of them based on index. The textual
  26. * names of the namespaces are handled by Language.php.
  27. *
  28. * These are synonyms for the names given in the language file
  29. * Users and translators should not change them
  30. */
  31. class MWNamespace {
  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 static $alwaysCapitalizedNamespaces = [ NS_SPECIAL, NS_USER, NS_MEDIAWIKI ];
  38. /** @var string[]|null Canonical namespaces cache */
  39. private static $canonicalNamespaces = null;
  40. /** @var array|false Canonical namespaces index cache */
  41. private static $namespaceIndexes = false;
  42. /** @var int[]|null Valid namespaces cache */
  43. private static $validNamespaces = null;
  44. /**
  45. * Throw an exception when trying to get the subject or talk page
  46. * for a given namespace where it does not make sense.
  47. * Special namespaces are defined in includes/Defines.php and have
  48. * a value below 0 (ex: NS_SPECIAL = -1 , NS_MEDIA = -2)
  49. *
  50. * @param int $index
  51. * @param string $method
  52. *
  53. * @throws MWException
  54. * @return bool
  55. */
  56. private static function isMethodValidFor( $index, $method ) {
  57. if ( $index < NS_MAIN ) {
  58. throw new MWException( "$method does not make any sense for given namespace $index" );
  59. }
  60. return true;
  61. }
  62. /**
  63. * Clear internal caches
  64. *
  65. * For use in unit testing when namespace configuration is changed.
  66. *
  67. * @since 1.31
  68. */
  69. public static function clearCaches() {
  70. self::$canonicalNamespaces = null;
  71. self::$namespaceIndexes = false;
  72. self::$validNamespaces = null;
  73. }
  74. /**
  75. * Can pages in the given namespace be moved?
  76. *
  77. * @param int $index Namespace index
  78. * @return bool
  79. */
  80. public static function isMovable( $index ) {
  81. global $wgAllowImageMoving;
  82. $result = !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) );
  83. /**
  84. * @since 1.20
  85. */
  86. Hooks::run( 'NamespaceIsMovable', [ $index, &$result ] );
  87. return $result;
  88. }
  89. /**
  90. * Is the given namespace is a subject (non-talk) namespace?
  91. *
  92. * @param int $index Namespace index
  93. * @return bool
  94. * @since 1.19
  95. */
  96. public static function isSubject( $index ) {
  97. return !self::isTalk( $index );
  98. }
  99. /**
  100. * Is the given namespace a talk namespace?
  101. *
  102. * @param int $index Namespace index
  103. * @return bool
  104. */
  105. public static function isTalk( $index ) {
  106. return $index > NS_MAIN
  107. && $index % 2;
  108. }
  109. /**
  110. * Get the talk namespace index for a given namespace
  111. *
  112. * @param int $index Namespace index
  113. * @return int
  114. */
  115. public static function getTalk( $index ) {
  116. self::isMethodValidFor( $index, __METHOD__ );
  117. return self::isTalk( $index )
  118. ? $index
  119. : $index + 1;
  120. }
  121. /**
  122. * Get the subject namespace index for a given namespace
  123. * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
  124. *
  125. * @param int $index Namespace index
  126. * @return int
  127. */
  128. public static function getSubject( $index ) {
  129. # Handle special namespaces
  130. if ( $index < NS_MAIN ) {
  131. return $index;
  132. }
  133. return self::isTalk( $index )
  134. ? $index - 1
  135. : $index;
  136. }
  137. /**
  138. * Get the associated namespace.
  139. * For talk namespaces, returns the subject (non-talk) namespace
  140. * For subject (non-talk) namespaces, returns the talk namespace
  141. *
  142. * @param int $index Namespace index
  143. * @return int|null If no associated namespace could be found
  144. */
  145. public static function getAssociated( $index ) {
  146. self::isMethodValidFor( $index, __METHOD__ );
  147. if ( self::isSubject( $index ) ) {
  148. return self::getTalk( $index );
  149. } elseif ( self::isTalk( $index ) ) {
  150. return self::getSubject( $index );
  151. } else {
  152. return null;
  153. }
  154. }
  155. /**
  156. * Returns whether the specified namespace exists
  157. *
  158. * @param int $index
  159. *
  160. * @return bool
  161. * @since 1.19
  162. */
  163. public static function exists( $index ) {
  164. $nslist = self::getCanonicalNamespaces();
  165. return isset( $nslist[$index] );
  166. }
  167. /**
  168. * Returns whether the specified namespaces are the same namespace
  169. *
  170. * @note It's possible that in the future we may start using something
  171. * other than just namespace indexes. Under that circumstance making use
  172. * of this function rather than directly doing comparison will make
  173. * sure that code will not potentially break.
  174. *
  175. * @param int $ns1 The first namespace index
  176. * @param int $ns2 The second namespace index
  177. *
  178. * @return bool
  179. * @since 1.19
  180. */
  181. public static function equals( $ns1, $ns2 ) {
  182. return $ns1 == $ns2;
  183. }
  184. /**
  185. * Returns whether the specified namespaces share the same subject.
  186. * eg: NS_USER and NS_USER wil return true, as well
  187. * NS_USER and NS_USER_TALK will return true.
  188. *
  189. * @param int $ns1 The first namespace index
  190. * @param int $ns2 The second namespace index
  191. *
  192. * @return bool
  193. * @since 1.19
  194. */
  195. public static function subjectEquals( $ns1, $ns2 ) {
  196. return self::getSubject( $ns1 ) == self::getSubject( $ns2 );
  197. }
  198. /**
  199. * Returns array of all defined namespaces with their canonical
  200. * (English) names.
  201. *
  202. * @param bool $rebuild Rebuild namespace list (default = false). Used for testing.
  203. * Deprecated since 1.31, use self::clearCaches() instead.
  204. *
  205. * @return array
  206. * @since 1.17
  207. */
  208. public static function getCanonicalNamespaces( $rebuild = false ) {
  209. if ( $rebuild ) {
  210. self::clearCaches();
  211. }
  212. if ( self::$canonicalNamespaces === null ) {
  213. global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
  214. self::$canonicalNamespaces = [ NS_MAIN => '' ] + $wgCanonicalNamespaceNames;
  215. // Add extension namespaces
  216. self::$canonicalNamespaces +=
  217. ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' );
  218. if ( is_array( $wgExtraNamespaces ) ) {
  219. self::$canonicalNamespaces += $wgExtraNamespaces;
  220. }
  221. Hooks::run( 'CanonicalNamespaces', [ &self::$canonicalNamespaces ] );
  222. }
  223. return self::$canonicalNamespaces;
  224. }
  225. /**
  226. * Returns the canonical (English) name for a given index
  227. *
  228. * @param int $index Namespace index
  229. * @return string|bool If no canonical definition.
  230. */
  231. public static function getCanonicalName( $index ) {
  232. $nslist = self::getCanonicalNamespaces();
  233. return $nslist[$index] ?? false;
  234. }
  235. /**
  236. * Returns the index for a given canonical name, or NULL
  237. * The input *must* be converted to lower case first
  238. *
  239. * @param string $name Namespace name
  240. * @return int
  241. */
  242. public static function getCanonicalIndex( $name ) {
  243. if ( self::$namespaceIndexes === false ) {
  244. self::$namespaceIndexes = [];
  245. foreach ( self::getCanonicalNamespaces() as $i => $text ) {
  246. self::$namespaceIndexes[strtolower( $text )] = $i;
  247. }
  248. }
  249. if ( array_key_exists( $name, self::$namespaceIndexes ) ) {
  250. return self::$namespaceIndexes[$name];
  251. } else {
  252. return null;
  253. }
  254. }
  255. /**
  256. * Returns an array of the namespaces (by integer id) that exist on the
  257. * wiki. Used primarily by the api in help documentation.
  258. * @return array
  259. */
  260. public static function getValidNamespaces() {
  261. if ( is_null( self::$validNamespaces ) ) {
  262. foreach ( array_keys( self::getCanonicalNamespaces() ) as $ns ) {
  263. if ( $ns >= 0 ) {
  264. self::$validNamespaces[] = $ns;
  265. }
  266. }
  267. // T109137: sort numerically
  268. sort( self::$validNamespaces, SORT_NUMERIC );
  269. }
  270. return self::$validNamespaces;
  271. }
  272. /**
  273. * Does this namespace ever have a talk namespace?
  274. *
  275. * @deprecated since 1.30, use hasTalkNamespace() instead.
  276. *
  277. * @param int $index Namespace index
  278. * @return bool True if this namespace either is or has a corresponding talk namespace.
  279. */
  280. public static function canTalk( $index ) {
  281. return self::hasTalkNamespace( $index );
  282. }
  283. /**
  284. * Does this namespace ever have a talk namespace?
  285. *
  286. * @since 1.30
  287. *
  288. * @param int $index Namespace ID
  289. * @return bool True if this namespace either is or has a corresponding talk namespace.
  290. */
  291. public static function hasTalkNamespace( $index ) {
  292. return $index >= NS_MAIN;
  293. }
  294. /**
  295. * Does this namespace contain content, for the purposes of calculating
  296. * statistics, etc?
  297. *
  298. * @param int $index Index to check
  299. * @return bool
  300. */
  301. public static function isContent( $index ) {
  302. global $wgContentNamespaces;
  303. return $index == NS_MAIN || in_array( $index, $wgContentNamespaces );
  304. }
  305. /**
  306. * Might pages in this namespace require the use of the Signature button on
  307. * the edit toolbar?
  308. *
  309. * @param int $index Index to check
  310. * @return bool
  311. */
  312. public static function wantSignatures( $index ) {
  313. global $wgExtraSignatureNamespaces;
  314. return self::isTalk( $index ) || in_array( $index, $wgExtraSignatureNamespaces );
  315. }
  316. /**
  317. * Can pages in a namespace be watched?
  318. *
  319. * @param int $index
  320. * @return bool
  321. */
  322. public static function isWatchable( $index ) {
  323. return $index >= NS_MAIN;
  324. }
  325. /**
  326. * Does the namespace allow subpages?
  327. *
  328. * @param int $index Index to check
  329. * @return bool
  330. */
  331. public static function hasSubpages( $index ) {
  332. global $wgNamespacesWithSubpages;
  333. return !empty( $wgNamespacesWithSubpages[$index] );
  334. }
  335. /**
  336. * Get a list of all namespace indices which are considered to contain content
  337. * @return array Array of namespace indices
  338. */
  339. public static function getContentNamespaces() {
  340. global $wgContentNamespaces;
  341. if ( !is_array( $wgContentNamespaces ) || $wgContentNamespaces === [] ) {
  342. return [ NS_MAIN ];
  343. } elseif ( !in_array( NS_MAIN, $wgContentNamespaces ) ) {
  344. // always force NS_MAIN to be part of array (to match the algorithm used by isContent)
  345. return array_merge( [ NS_MAIN ], $wgContentNamespaces );
  346. } else {
  347. return $wgContentNamespaces;
  348. }
  349. }
  350. /**
  351. * List all namespace indices which are considered subject, aka not a talk
  352. * or special namespace. See also MWNamespace::isSubject
  353. *
  354. * @return array Array of namespace indices
  355. */
  356. public static function getSubjectNamespaces() {
  357. return array_filter(
  358. self::getValidNamespaces(),
  359. 'MWNamespace::isSubject'
  360. );
  361. }
  362. /**
  363. * List all namespace indices which are considered talks, aka not a subject
  364. * or special namespace. See also MWNamespace::isTalk
  365. *
  366. * @return array Array of namespace indices
  367. */
  368. public static function getTalkNamespaces() {
  369. return array_filter(
  370. self::getValidNamespaces(),
  371. 'MWNamespace::isTalk'
  372. );
  373. }
  374. /**
  375. * Is the namespace first-letter capitalized?
  376. *
  377. * @param int $index Index to check
  378. * @return bool
  379. */
  380. public static function isCapitalized( $index ) {
  381. global $wgCapitalLinks, $wgCapitalLinkOverrides;
  382. // Turn NS_MEDIA into NS_FILE
  383. $index = $index === NS_MEDIA ? NS_FILE : $index;
  384. // Make sure to get the subject of our namespace
  385. $index = self::getSubject( $index );
  386. // Some namespaces are special and should always be upper case
  387. if ( in_array( $index, self::$alwaysCapitalizedNamespaces ) ) {
  388. return true;
  389. }
  390. if ( isset( $wgCapitalLinkOverrides[$index] ) ) {
  391. // $wgCapitalLinkOverrides is explicitly set
  392. return $wgCapitalLinkOverrides[$index];
  393. }
  394. // Default to the global setting
  395. return $wgCapitalLinks;
  396. }
  397. /**
  398. * Does the namespace (potentially) have different aliases for different
  399. * genders. Not all languages make a distinction here.
  400. *
  401. * @since 1.18
  402. * @param int $index Index to check
  403. * @return bool
  404. */
  405. public static function hasGenderDistinction( $index ) {
  406. return $index == NS_USER || $index == NS_USER_TALK;
  407. }
  408. /**
  409. * It is not possible to use pages from this namespace as template?
  410. *
  411. * @since 1.20
  412. * @param int $index Index to check
  413. * @return bool
  414. */
  415. public static function isNonincludable( $index ) {
  416. global $wgNonincludableNamespaces;
  417. return $wgNonincludableNamespaces && in_array( $index, $wgNonincludableNamespaces );
  418. }
  419. /**
  420. * Get the default content model for a namespace
  421. * This does not mean that all pages in that namespace have the model
  422. *
  423. * @since 1.21
  424. * @param int $index Index to check
  425. * @return null|string Default model name for the given namespace, if set
  426. */
  427. public static function getNamespaceContentModel( $index ) {
  428. global $wgNamespaceContentModels;
  429. return $wgNamespaceContentModels[$index] ?? null;
  430. }
  431. /**
  432. * Determine which restriction levels it makes sense to use in a namespace,
  433. * optionally filtered by a user's rights.
  434. *
  435. * @since 1.23
  436. * @param int $index Index to check
  437. * @param User|null $user User to check
  438. * @return array
  439. */
  440. public static function getRestrictionLevels( $index, User $user = null ) {
  441. global $wgNamespaceProtection, $wgRestrictionLevels;
  442. if ( !isset( $wgNamespaceProtection[$index] ) ) {
  443. // All levels are valid if there's no namespace restriction.
  444. // But still filter by user, if necessary
  445. $levels = $wgRestrictionLevels;
  446. if ( $user ) {
  447. $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
  448. $right = $level;
  449. if ( $right == 'sysop' ) {
  450. $right = 'editprotected'; // BC
  451. }
  452. if ( $right == 'autoconfirmed' ) {
  453. $right = 'editsemiprotected'; // BC
  454. }
  455. return ( $right == '' || $user->isAllowed( $right ) );
  456. } ) );
  457. }
  458. return $levels;
  459. }
  460. // First, get the list of groups that can edit this namespace.
  461. $namespaceGroups = [];
  462. $combine = 'array_merge';
  463. foreach ( (array)$wgNamespaceProtection[$index] as $right ) {
  464. if ( $right == 'sysop' ) {
  465. $right = 'editprotected'; // BC
  466. }
  467. if ( $right == 'autoconfirmed' ) {
  468. $right = 'editsemiprotected'; // BC
  469. }
  470. if ( $right != '' ) {
  471. $namespaceGroups = call_user_func( $combine, $namespaceGroups,
  472. User::getGroupsWithPermission( $right ) );
  473. $combine = 'array_intersect';
  474. }
  475. }
  476. // Now, keep only those restriction levels where there is at least one
  477. // group that can edit the namespace but would be blocked by the
  478. // restriction.
  479. $usableLevels = [ '' ];
  480. foreach ( $wgRestrictionLevels as $level ) {
  481. $right = $level;
  482. if ( $right == 'sysop' ) {
  483. $right = 'editprotected'; // BC
  484. }
  485. if ( $right == 'autoconfirmed' ) {
  486. $right = 'editsemiprotected'; // BC
  487. }
  488. if ( $right != '' && ( !$user || $user->isAllowed( $right ) ) &&
  489. array_diff( $namespaceGroups, User::getGroupsWithPermission( $right ) )
  490. ) {
  491. $usableLevels[] = $level;
  492. }
  493. }
  494. return $usableLevels;
  495. }
  496. /**
  497. * Returns the link type to be used for categories.
  498. *
  499. * This determines which section of a category page titles
  500. * in the namespace will appear within.
  501. *
  502. * @since 1.32
  503. * @param int $index Namespace index
  504. * @return string One of 'subcat', 'file', 'page'
  505. */
  506. public static function getCategoryLinkType( $index ) {
  507. self::isMethodValidFor( $index, __METHOD__ );
  508. if ( $index == NS_CATEGORY ) {
  509. return 'subcat';
  510. } elseif ( $index == NS_FILE ) {
  511. return 'file';
  512. } else {
  513. return 'page';
  514. }
  515. }
  516. }