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