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