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