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