Merge "maintenance: Script to rename titles for Unicode uppercasing changes"
[lhc/web/wiklou.git] / includes / Permissions / PermissionManager.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20 namespace MediaWiki\Permissions;
21
22 use Action;
23 use Exception;
24 use Hooks;
25 use MediaWiki\Linker\LinkTarget;
26 use MediaWiki\Session\SessionManager;
27 use MediaWiki\Special\SpecialPageFactory;
28 use MediaWiki\User\UserIdentity;
29 use MessageSpecifier;
30 use NamespaceInfo;
31 use RequestContext;
32 use SpecialPage;
33 use Title;
34 use User;
35 use WikiPage;
36
37 /**
38 * A service class for checking permissions
39 * To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager().
40 *
41 * @since 1.33
42 */
43 class PermissionManager {
44
45 /** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */
46 const RIGOR_QUICK = 'quick';
47
48 /** @var string Does cheap and expensive checks possibly from a replica DB */
49 const RIGOR_FULL = 'full';
50
51 /** @var string Does cheap and expensive checks, using the master as needed */
52 const RIGOR_SECURE = 'secure';
53
54 /** @var SpecialPageFactory */
55 private $specialPageFactory;
56
57 /** @var string[] List of pages names anonymous user may see */
58 private $whitelistRead;
59
60 /** @var string[] Whitelists publicly readable titles with regular expressions */
61 private $whitelistReadRegexp;
62
63 /** @var bool Require users to confirm email address before they can edit */
64 private $emailConfirmToEdit;
65
66 /** @var bool If set to true, blocked users will no longer be allowed to log in */
67 private $blockDisablesLogin;
68
69 /** @var NamespaceInfo */
70 private $nsInfo;
71
72 /** @var string[][] Access rights for groups and users in these groups */
73 private $groupPermissions;
74
75 /** @var string[][] Permission keys revoked from users in each group */
76 private $revokePermissions;
77
78 /** @var string[] A list of available rights, in addition to the ones defined by the core */
79 private $availableRights;
80
81 /** @var string[] Cached results of getAllRights() */
82 private $allRights = false;
83
84 /** @var string[][] Cached user rights */
85 private $usersRights = null;
86
87 /** @var string[] Cached rights for isEveryoneAllowed */
88 private $cachedRights = [];
89
90 /**
91 * Array of Strings Core rights.
92 * Each of these should have a corresponding message of the form
93 * "right-$right".
94 * @showinitializer
95 */
96 private $coreRights = [
97 'apihighlimits',
98 'applychangetags',
99 'autoconfirmed',
100 'autocreateaccount',
101 'autopatrol',
102 'bigdelete',
103 'block',
104 'blockemail',
105 'bot',
106 'browsearchive',
107 'changetags',
108 'createaccount',
109 'createpage',
110 'createtalk',
111 'delete',
112 'deletechangetags',
113 'deletedhistory',
114 'deletedtext',
115 'deletelogentry',
116 'deleterevision',
117 'edit',
118 'editcontentmodel',
119 'editinterface',
120 'editprotected',
121 'editmyoptions',
122 'editmyprivateinfo',
123 'editmyusercss',
124 'editmyuserjson',
125 'editmyuserjs',
126 'editmywatchlist',
127 'editsemiprotected',
128 'editsitecss',
129 'editsitejson',
130 'editsitejs',
131 'editusercss',
132 'edituserjson',
133 'edituserjs',
134 'hideuser',
135 'import',
136 'importupload',
137 'ipblock-exempt',
138 'managechangetags',
139 'markbotedits',
140 'mergehistory',
141 'minoredit',
142 'move',
143 'movefile',
144 'move-categorypages',
145 'move-rootuserpages',
146 'move-subpages',
147 'nominornewtalk',
148 'noratelimit',
149 'override-export-depth',
150 'pagelang',
151 'patrol',
152 'patrolmarks',
153 'protect',
154 'purge',
155 'read',
156 'reupload',
157 'reupload-own',
158 'reupload-shared',
159 'rollback',
160 'sendemail',
161 'siteadmin',
162 'suppressionlog',
163 'suppressredirect',
164 'suppressrevision',
165 'unblockself',
166 'undelete',
167 'unwatchedpages',
168 'upload',
169 'upload_by_url',
170 'userrights',
171 'userrights-interwiki',
172 'viewmyprivateinfo',
173 'viewmywatchlist',
174 'viewsuppressed',
175 'writeapi',
176 ];
177
178 /**
179 * @param SpecialPageFactory $specialPageFactory
180 * @param string[] $whitelistRead
181 * @param string[] $whitelistReadRegexp
182 * @param bool $emailConfirmToEdit
183 * @param bool $blockDisablesLogin
184 * @param string[][] $groupPermissions
185 * @param string[][] $revokePermissions
186 * @param string[] $availableRights
187 * @param NamespaceInfo $nsInfo
188 */
189 public function __construct(
190 SpecialPageFactory $specialPageFactory,
191 $whitelistRead,
192 $whitelistReadRegexp,
193 $emailConfirmToEdit,
194 $blockDisablesLogin,
195 $groupPermissions,
196 $revokePermissions,
197 $availableRights,
198 NamespaceInfo $nsInfo
199 ) {
200 $this->specialPageFactory = $specialPageFactory;
201 $this->whitelistRead = $whitelistRead;
202 $this->whitelistReadRegexp = $whitelistReadRegexp;
203 $this->emailConfirmToEdit = $emailConfirmToEdit;
204 $this->blockDisablesLogin = $blockDisablesLogin;
205 $this->groupPermissions = $groupPermissions;
206 $this->revokePermissions = $revokePermissions;
207 $this->availableRights = $availableRights;
208 $this->nsInfo = $nsInfo;
209 }
210
211 /**
212 * Can $user perform $action on a page?
213 *
214 * The method is intended to replace Title::userCan()
215 * The $user parameter need to be superseded by UserIdentity value in future
216 * The $title parameter need to be superseded by PageIdentity value in future
217 *
218 * @see Title::userCan()
219 *
220 * @param string $action
221 * @param User $user
222 * @param LinkTarget $page
223 * @param string $rigor One of PermissionManager::RIGOR_ constants
224 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
225 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
226 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
227 *
228 * @return bool
229 */
230 public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) {
231 return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
232 }
233
234 /**
235 * Can $user perform $action on a page?
236 *
237 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
238 *
239 * @param string $action Action that permission needs to be checked for
240 * @param User $user User to check
241 * @param LinkTarget $page
242 * @param string $rigor One of PermissionManager::RIGOR_ constants
243 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
244 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
245 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
246 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
247 * whose corresponding errors may be ignored.
248 *
249 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
250 */
251 public function getPermissionErrors(
252 $action,
253 User $user,
254 LinkTarget $page,
255 $rigor = self::RIGOR_SECURE,
256 $ignoreErrors = []
257 ) {
258 $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
259
260 // Remove the errors being ignored.
261 foreach ( $errors as $index => $error ) {
262 $errKey = is_array( $error ) ? $error[0] : $error;
263
264 if ( in_array( $errKey, $ignoreErrors ) ) {
265 unset( $errors[$index] );
266 }
267 if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
268 unset( $errors[$index] );
269 }
270 }
271
272 return $errors;
273 }
274
275 /**
276 * Check if user is blocked from editing a particular article
277 *
278 * @param User $user
279 * @param LinkTarget $page Title to check
280 * @param bool $fromReplica Whether to check the replica DB instead of the master
281 *
282 * @return bool
283 */
284 public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
285 $blocked = $user->isHidden();
286
287 // TODO: remove upon further migration to LinkTarget
288 $page = Title::newFromLinkTarget( $page );
289
290 if ( !$blocked ) {
291 $block = $user->getBlock( $fromReplica );
292 if ( $block ) {
293 // Special handling for a user's own talk page. The block is not aware
294 // of the user, so this must be done here.
295 if ( $page->equals( $user->getTalkPage() ) ) {
296 $blocked = $block->appliesToUsertalk( $page );
297 } else {
298 $blocked = $block->appliesToTitle( $page );
299 }
300 }
301 }
302
303 // only for the purpose of the hook. We really don't need this here.
304 $allowUsertalk = $user->isAllowUsertalk();
305
306 Hooks::run( 'UserIsBlockedFrom', [ $user, $page, &$blocked, &$allowUsertalk ] );
307
308 return $blocked;
309 }
310
311 /**
312 * Can $user perform $action on a page? This is an internal function,
313 * with multiple levels of checks depending on performance needs; see $rigor below.
314 * It does not check wfReadOnly().
315 *
316 * @param string $action Action that permission needs to be checked for
317 * @param User $user User to check
318 * @param LinkTarget $page
319 * @param string $rigor One of PermissionManager::RIGOR_ constants
320 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
321 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
322 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
323 * @param bool $short Set this to true to stop after the first permission error.
324 *
325 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
326 * @throws Exception
327 */
328 private function getPermissionErrorsInternal(
329 $action,
330 User $user,
331 LinkTarget $page,
332 $rigor = self::RIGOR_SECURE,
333 $short = false
334 ) {
335 if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
336 throw new Exception( "Invalid rigor parameter '$rigor'." );
337 }
338
339 # Read has special handling
340 if ( $action == 'read' ) {
341 $checks = [
342 'checkPermissionHooks',
343 'checkReadPermissions',
344 'checkUserBlock', // for wgBlockDisablesLogin
345 ];
346 # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
347 # or checkUserConfigPermissions here as it will lead to duplicate
348 # error messages. This is okay to do since anywhere that checks for
349 # create will also check for edit, and those checks are called for edit.
350 } elseif ( $action == 'create' ) {
351 $checks = [
352 'checkQuickPermissions',
353 'checkPermissionHooks',
354 'checkPageRestrictions',
355 'checkCascadingSourcesRestrictions',
356 'checkActionPermissions',
357 'checkUserBlock'
358 ];
359 } else {
360 $checks = [
361 'checkQuickPermissions',
362 'checkPermissionHooks',
363 'checkSpecialsAndNSPermissions',
364 'checkSiteConfigPermissions',
365 'checkUserConfigPermissions',
366 'checkPageRestrictions',
367 'checkCascadingSourcesRestrictions',
368 'checkActionPermissions',
369 'checkUserBlock'
370 ];
371 }
372
373 $errors = [];
374 foreach ( $checks as $method ) {
375 $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
376
377 if ( $short && $errors !== [] ) {
378 break;
379 }
380 }
381
382 return $errors;
383 }
384
385 /**
386 * Check various permission hooks
387 *
388 * @param string $action The action to check
389 * @param User $user User to check
390 * @param array $errors List of current errors
391 * @param string $rigor One of PermissionManager::RIGOR_ constants
392 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
393 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
394 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
395 * @param bool $short Short circuit on first error
396 *
397 * @param LinkTarget $page
398 *
399 * @return array List of errors
400 */
401 private function checkPermissionHooks(
402 $action,
403 User $user,
404 $errors,
405 $rigor,
406 $short,
407 LinkTarget $page
408 ) {
409 // TODO: remove when LinkTarget usage will expand further
410 $page = Title::newFromLinkTarget( $page );
411 // Use getUserPermissionsErrors instead
412 $result = '';
413 if ( !Hooks::run( 'userCan', [ &$page, &$user, $action, &$result ] ) ) {
414 return $result ? [] : [ [ 'badaccess-group0' ] ];
415 }
416 // Check getUserPermissionsErrors hook
417 if ( !Hooks::run( 'getUserPermissionsErrors', [ &$page, &$user, $action, &$result ] ) ) {
418 $errors = $this->resultToError( $errors, $result );
419 }
420 // Check getUserPermissionsErrorsExpensive hook
421 if (
422 $rigor !== self::RIGOR_QUICK
423 && !( $short && count( $errors ) > 0 )
424 && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$page, &$user, $action, &$result ] )
425 ) {
426 $errors = $this->resultToError( $errors, $result );
427 }
428
429 return $errors;
430 }
431
432 /**
433 * Add the resulting error code to the errors array
434 *
435 * @param array $errors List of current errors
436 * @param array|string|MessageSpecifier|false $result Result of errors
437 *
438 * @return array List of errors
439 */
440 private function resultToError( $errors, $result ) {
441 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
442 // A single array representing an error
443 $errors[] = $result;
444 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
445 // A nested array representing multiple errors
446 $errors = array_merge( $errors, $result );
447 } elseif ( $result !== '' && is_string( $result ) ) {
448 // A string representing a message-id
449 $errors[] = [ $result ];
450 } elseif ( $result instanceof MessageSpecifier ) {
451 // A message specifier representing an error
452 $errors[] = [ $result ];
453 } elseif ( $result === false ) {
454 // a generic "We don't want them to do that"
455 $errors[] = [ 'badaccess-group0' ];
456 }
457 return $errors;
458 }
459
460 /**
461 * Check that the user is allowed to read this page.
462 *
463 * @param string $action The action to check
464 * @param User $user User to check
465 * @param array $errors List of current errors
466 * @param string $rigor One of PermissionManager::RIGOR_ constants
467 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
468 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
469 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
470 * @param bool $short Short circuit on first error
471 *
472 * @param LinkTarget $page
473 *
474 * @return array List of errors
475 */
476 private function checkReadPermissions(
477 $action,
478 User $user,
479 $errors,
480 $rigor,
481 $short,
482 LinkTarget $page
483 ) {
484 // TODO: remove when LinkTarget usage will expand further
485 $page = Title::newFromLinkTarget( $page );
486
487 $whitelisted = false;
488 if ( User::isEveryoneAllowed( 'read' ) ) {
489 # Shortcut for public wikis, allows skipping quite a bit of code
490 $whitelisted = true;
491 } elseif ( $user->isAllowed( 'read' ) ) {
492 # If the user is allowed to read pages, he is allowed to read all pages
493 $whitelisted = true;
494 } elseif ( $this->isSameSpecialPage( 'Userlogin', $page )
495 || $this->isSameSpecialPage( 'PasswordReset', $page )
496 || $this->isSameSpecialPage( 'Userlogout', $page )
497 ) {
498 # Always grant access to the login page.
499 # Even anons need to be able to log in.
500 $whitelisted = true;
501 } elseif ( is_array( $this->whitelistRead ) && count( $this->whitelistRead ) ) {
502 # Time to check the whitelist
503 # Only do these checks is there's something to check against
504 $name = $page->getPrefixedText();
505 $dbName = $page->getPrefixedDBkey();
506
507 // Check for explicit whitelisting with and without underscores
508 if ( in_array( $name, $this->whitelistRead, true )
509 || in_array( $dbName, $this->whitelistRead, true ) ) {
510 $whitelisted = true;
511 } elseif ( $page->getNamespace() == NS_MAIN ) {
512 # Old settings might have the title prefixed with
513 # a colon for main-namespace pages
514 if ( in_array( ':' . $name, $this->whitelistRead ) ) {
515 $whitelisted = true;
516 }
517 } elseif ( $page->isSpecialPage() ) {
518 # If it's a special page, ditch the subpage bit and check again
519 $name = $page->getDBkey();
520 list( $name, /* $subpage */ ) =
521 $this->specialPageFactory->resolveAlias( $name );
522 if ( $name ) {
523 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
524 if ( in_array( $pure, $this->whitelistRead, true ) ) {
525 $whitelisted = true;
526 }
527 }
528 }
529 }
530
531 if ( !$whitelisted && is_array( $this->whitelistReadRegexp )
532 && !empty( $this->whitelistReadRegexp ) ) {
533 $name = $page->getPrefixedText();
534 // Check for regex whitelisting
535 foreach ( $this->whitelistReadRegexp as $listItem ) {
536 if ( preg_match( $listItem, $name ) ) {
537 $whitelisted = true;
538 break;
539 }
540 }
541 }
542
543 if ( !$whitelisted ) {
544 # If the title is not whitelisted, give extensions a chance to do so...
545 Hooks::run( 'TitleReadWhitelist', [ $page, $user, &$whitelisted ] );
546 if ( !$whitelisted ) {
547 $errors[] = $this->missingPermissionError( $action, $short );
548 }
549 }
550
551 return $errors;
552 }
553
554 /**
555 * Get a description array when the user doesn't have the right to perform
556 * $action (i.e. when User::isAllowed() returns false)
557 *
558 * @param string $action The action to check
559 * @param bool $short Short circuit on first error
560 * @return array Array containing an error message key and any parameters
561 */
562 private function missingPermissionError( $action, $short ) {
563 // We avoid expensive display logic for quickUserCan's and such
564 if ( $short ) {
565 return [ 'badaccess-group0' ];
566 }
567
568 // TODO: it would be a good idea to replace the method below with something else like
569 // maybe callback injection
570 return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
571 }
572
573 /**
574 * Returns true if this title resolves to the named special page
575 *
576 * @param string $name The special page name
577 * @param LinkTarget $page
578 *
579 * @return bool
580 */
581 private function isSameSpecialPage( $name, LinkTarget $page ) {
582 if ( $page->getNamespace() == NS_SPECIAL ) {
583 list( $thisName, /* $subpage */ ) =
584 $this->specialPageFactory->resolveAlias( $page->getDBkey() );
585 if ( $name == $thisName ) {
586 return true;
587 }
588 }
589 return false;
590 }
591
592 /**
593 * Check that the user isn't blocked from editing.
594 *
595 * @param string $action The action to check
596 * @param User $user User to check
597 * @param array $errors List of current errors
598 * @param string $rigor One of PermissionManager::RIGOR_ constants
599 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
600 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
601 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
602 * @param bool $short Short circuit on first error
603 *
604 * @param LinkTarget $page
605 *
606 * @return array List of errors
607 */
608 private function checkUserBlock(
609 $action,
610 User $user,
611 $errors,
612 $rigor,
613 $short,
614 LinkTarget $page
615 ) {
616 // Account creation blocks handled at userlogin.
617 // Unblocking handled in SpecialUnblock
618 if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
619 return $errors;
620 }
621
622 // Optimize for a very common case
623 if ( $action === 'read' && !$this->blockDisablesLogin ) {
624 return $errors;
625 }
626
627 if ( $this->emailConfirmToEdit
628 && !$user->isEmailConfirmed()
629 && $action === 'edit'
630 ) {
631 $errors[] = [ 'confirmedittext' ];
632 }
633
634 $useReplica = ( $rigor !== self::RIGOR_SECURE );
635 $block = $user->getBlock( $useReplica );
636
637 // If the user does not have a block, or the block they do have explicitly
638 // allows the action (like "read" or "upload").
639 if ( !$block || $block->appliesToRight( $action ) === false ) {
640 return $errors;
641 }
642
643 // Determine if the user is blocked from this action on this page.
644 // What gets passed into this method is a user right, not an action name.
645 // There is no way to instantiate an action by restriction. However, this
646 // will get the action where the restriction is the same. This may result
647 // in actions being blocked that shouldn't be.
648 $actionObj = null;
649 if ( Action::exists( $action ) ) {
650 // TODO: this drags a ton of dependencies in, would be good to avoid WikiPage
651 // instantiation and decouple it creating an ActionPermissionChecker interface
652 $wikiPage = WikiPage::factory( Title::newFromLinkTarget( $page, 'clone' ) );
653 // Creating an action will perform several database queries to ensure that
654 // the action has not been overridden by the content type.
655 // FIXME: avoid use of RequestContext since it drags in User and Title dependencies
656 // probably we may use fake context object since it's unlikely that Action uses it
657 // anyway. It would be nice if we could avoid instantiating the Action at all.
658 $actionObj = Action::factory( $action, $wikiPage, RequestContext::getMain() );
659 // Ensure that the retrieved action matches the restriction.
660 if ( $actionObj && $actionObj->getRestriction() !== $action ) {
661 $actionObj = null;
662 }
663 }
664
665 // If no action object is returned, assume that the action requires unblock
666 // which is the default.
667 if ( !$actionObj || $actionObj->requiresUnblock() ) {
668 if ( $this->isBlockedFrom( $user, $page, $useReplica ) ) {
669 // @todo FIXME: Pass the relevant context into this function.
670 $errors[] = $block->getPermissionsError( RequestContext::getMain() );
671 }
672 }
673
674 return $errors;
675 }
676
677 /**
678 * Permissions checks that fail most often, and which are easiest to test.
679 *
680 * @param string $action The action to check
681 * @param User $user User to check
682 * @param array $errors List of current errors
683 * @param string $rigor One of PermissionManager::RIGOR_ constants
684 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
685 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
686 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
687 * @param bool $short Short circuit on first error
688 *
689 * @param LinkTarget $page
690 *
691 * @return array List of errors
692 */
693 private function checkQuickPermissions(
694 $action,
695 User $user,
696 $errors,
697 $rigor,
698 $short,
699 LinkTarget $page
700 ) {
701 // TODO: remove when LinkTarget usage will expand further
702 $page = Title::newFromLinkTarget( $page );
703
704 if ( !Hooks::run( 'TitleQuickPermissions',
705 [ $page, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] )
706 ) {
707 return $errors;
708 }
709
710 $isSubPage = $this->nsInfo->hasSubpages( $page->getNamespace() ) ?
711 strpos( $page->getText(), '/' ) !== false : false;
712
713 if ( $action == 'create' ) {
714 if (
715 ( $this->nsInfo->isTalk( $page->getNamespace() ) &&
716 !$user->isAllowed( 'createtalk' ) ) ||
717 ( !$this->nsInfo->isTalk( $page->getNamespace() ) &&
718 !$user->isAllowed( 'createpage' ) )
719 ) {
720 $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
721 }
722 } elseif ( $action == 'move' ) {
723 if ( !$user->isAllowed( 'move-rootuserpages' )
724 && $page->getNamespace() == NS_USER && !$isSubPage ) {
725 // Show user page-specific message only if the user can move other pages
726 $errors[] = [ 'cant-move-user-page' ];
727 }
728
729 // Check if user is allowed to move files if it's a file
730 if ( $page->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
731 $errors[] = [ 'movenotallowedfile' ];
732 }
733
734 // Check if user is allowed to move category pages if it's a category page
735 if ( $page->getNamespace() == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
736 $errors[] = [ 'cant-move-category-page' ];
737 }
738
739 if ( !$user->isAllowed( 'move' ) ) {
740 // User can't move anything
741 $userCanMove = User::groupHasPermission( 'user', 'move' );
742 $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
743 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
744 // custom message if logged-in users without any special rights can move
745 $errors[] = [ 'movenologintext' ];
746 } else {
747 $errors[] = [ 'movenotallowed' ];
748 }
749 }
750 } elseif ( $action == 'move-target' ) {
751 if ( !$user->isAllowed( 'move' ) ) {
752 // User can't move anything
753 $errors[] = [ 'movenotallowed' ];
754 } elseif ( !$user->isAllowed( 'move-rootuserpages' )
755 && $page->getNamespace() == NS_USER && !$isSubPage ) {
756 // Show user page-specific message only if the user can move other pages
757 $errors[] = [ 'cant-move-to-user-page' ];
758 } elseif ( !$user->isAllowed( 'move-categorypages' )
759 && $page->getNamespace() == NS_CATEGORY ) {
760 // Show category page-specific message only if the user can move other pages
761 $errors[] = [ 'cant-move-to-category-page' ];
762 }
763 } elseif ( !$user->isAllowed( $action ) ) {
764 $errors[] = $this->missingPermissionError( $action, $short );
765 }
766
767 return $errors;
768 }
769
770 /**
771 * Check against page_restrictions table requirements on this
772 * page. The user must possess all required rights for this
773 * action.
774 *
775 * @param string $action The action to check
776 * @param User $user User to check
777 * @param array $errors List of current errors
778 * @param string $rigor One of PermissionManager::RIGOR_ constants
779 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
780 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
781 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
782 * @param bool $short Short circuit on first error
783 *
784 * @param LinkTarget $page
785 *
786 * @return array List of errors
787 */
788 private function checkPageRestrictions(
789 $action,
790 User $user,
791 $errors,
792 $rigor,
793 $short,
794 LinkTarget $page
795 ) {
796 // TODO: remove & rework upon further use of LinkTarget
797 $page = Title::newFromLinkTarget( $page );
798 foreach ( $page->getRestrictions( $action ) as $right ) {
799 // Backwards compatibility, rewrite sysop -> editprotected
800 if ( $right == 'sysop' ) {
801 $right = 'editprotected';
802 }
803 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
804 if ( $right == 'autoconfirmed' ) {
805 $right = 'editsemiprotected';
806 }
807 if ( $right == '' ) {
808 continue;
809 }
810 if ( !$user->isAllowed( $right ) ) {
811 $errors[] = [ 'protectedpagetext', $right, $action ];
812 } elseif ( $page->areRestrictionsCascading() && !$user->isAllowed( 'protect' ) ) {
813 $errors[] = [ 'protectedpagetext', 'protect', $action ];
814 }
815 }
816
817 return $errors;
818 }
819
820 /**
821 * Check restrictions on cascading pages.
822 *
823 * @param string $action The action to check
824 * @param User $user User to check
825 * @param array $errors List of current errors
826 * @param string $rigor One of PermissionManager::RIGOR_ constants
827 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
828 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
829 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
830 * @param bool $short Short circuit on first error
831 *
832 * @param LinkTarget $page
833 *
834 * @return array List of errors
835 */
836 private function checkCascadingSourcesRestrictions(
837 $action,
838 User $user,
839 $errors,
840 $rigor,
841 $short,
842 LinkTarget $page
843 ) {
844 // TODO: remove & rework upon further use of LinkTarget
845 $page = Title::newFromLinkTarget( $page );
846 if ( $rigor !== self::RIGOR_QUICK && !$page->isUserConfigPage() ) {
847 # We /could/ use the protection level on the source page, but it's
848 # fairly ugly as we have to establish a precedence hierarchy for pages
849 # included by multiple cascade-protected pages. So just restrict
850 # it to people with 'protect' permission, as they could remove the
851 # protection anyway.
852 list( $cascadingSources, $restrictions ) = $page->getCascadeProtectionSources();
853 # Cascading protection depends on more than this page...
854 # Several cascading protected pages may include this page...
855 # Check each cascading level
856 # This is only for protection restrictions, not for all actions
857 if ( isset( $restrictions[$action] ) ) {
858 foreach ( $restrictions[$action] as $right ) {
859 // Backwards compatibility, rewrite sysop -> editprotected
860 if ( $right == 'sysop' ) {
861 $right = 'editprotected';
862 }
863 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
864 if ( $right == 'autoconfirmed' ) {
865 $right = 'editsemiprotected';
866 }
867 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
868 $wikiPages = '';
869 /** @var Title $wikiPage */
870 foreach ( $cascadingSources as $wikiPage ) {
871 $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
872 }
873 $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
874 }
875 }
876 }
877 }
878
879 return $errors;
880 }
881
882 /**
883 * Check action permissions not already checked in checkQuickPermissions
884 *
885 * @param string $action The action to check
886 * @param User $user User to check
887 * @param array $errors List of current errors
888 * @param string $rigor One of PermissionManager::RIGOR_ constants
889 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
890 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
891 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
892 * @param bool $short Short circuit on first error
893 *
894 * @param LinkTarget $page
895 *
896 * @return array List of errors
897 */
898 private function checkActionPermissions(
899 $action,
900 User $user,
901 $errors,
902 $rigor,
903 $short,
904 LinkTarget $page
905 ) {
906 global $wgDeleteRevisionsLimit, $wgLang;
907
908 // TODO: remove & rework upon further use of LinkTarget
909 $page = Title::newFromLinkTarget( $page );
910
911 if ( $action == 'protect' ) {
912 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
913 // If they can't edit, they shouldn't protect.
914 $errors[] = [ 'protect-cantedit' ];
915 }
916 } elseif ( $action == 'create' ) {
917 $title_protection = $page->getTitleProtection();
918 if ( $title_protection ) {
919 if ( $title_protection['permission'] == ''
920 || !$user->isAllowed( $title_protection['permission'] )
921 ) {
922 $errors[] = [
923 'titleprotected',
924 // TODO: get rid of the User dependency
925 User::whoIs( $title_protection['user'] ),
926 $title_protection['reason']
927 ];
928 }
929 }
930 } elseif ( $action == 'move' ) {
931 // Check for immobile pages
932 if ( !$this->nsInfo->isMovable( $page->getNamespace() ) ) {
933 // Specific message for this case
934 $errors[] = [ 'immobile-source-namespace', $page->getNsText() ];
935 } elseif ( !$page->isMovable() ) {
936 // Less specific message for rarer cases
937 $errors[] = [ 'immobile-source-page' ];
938 }
939 } elseif ( $action == 'move-target' ) {
940 if ( !$this->nsInfo->isMovable( $page->getNamespace() ) ) {
941 $errors[] = [ 'immobile-target-namespace', $page->getNsText() ];
942 } elseif ( !$page->isMovable() ) {
943 $errors[] = [ 'immobile-target-page' ];
944 }
945 } elseif ( $action == 'delete' ) {
946 $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $page );
947 if ( !$tempErrors ) {
948 $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
949 $user, $tempErrors, $rigor, true, $page );
950 }
951 if ( $tempErrors ) {
952 // If protection keeps them from editing, they shouldn't be able to delete.
953 $errors[] = [ 'deleteprotected' ];
954 }
955 if ( $rigor !== self::RIGOR_QUICK && $wgDeleteRevisionsLimit
956 && !$this->userCan( 'bigdelete', $user, $page ) && $page->isBigDeletion()
957 ) {
958 $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
959 }
960 } elseif ( $action === 'undelete' ) {
961 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
962 // Undeleting implies editing
963 $errors[] = [ 'undelete-cantedit' ];
964 }
965 if ( !$page->exists()
966 && count( $this->getPermissionErrorsInternal( 'create', $user, $page, $rigor, true ) )
967 ) {
968 // Undeleting where nothing currently exists implies creating
969 $errors[] = [ 'undelete-cantcreate' ];
970 }
971 }
972 return $errors;
973 }
974
975 /**
976 * Check permissions on special pages & namespaces
977 *
978 * @param string $action The action to check
979 * @param User $user User to check
980 * @param array $errors List of current errors
981 * @param string $rigor One of PermissionManager::RIGOR_ constants
982 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
983 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
984 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
985 * @param bool $short Short circuit on first error
986 *
987 * @param LinkTarget $page
988 *
989 * @return array List of errors
990 */
991 private function checkSpecialsAndNSPermissions(
992 $action,
993 User $user,
994 $errors,
995 $rigor,
996 $short,
997 LinkTarget $page
998 ) {
999 // TODO: remove & rework upon further use of LinkTarget
1000 $page = Title::newFromLinkTarget( $page );
1001
1002 # Only 'createaccount' can be performed on special pages,
1003 # which don't actually exist in the DB.
1004 if ( $page->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) {
1005 $errors[] = [ 'ns-specialprotected' ];
1006 }
1007
1008 # Check $wgNamespaceProtection for restricted namespaces
1009 if ( $page->isNamespaceProtected( $user ) ) {
1010 $ns = $page->getNamespace() == NS_MAIN ?
1011 wfMessage( 'nstab-main' )->text() : $page->getNsText();
1012 $errors[] = $page->getNamespace() == NS_MEDIAWIKI ?
1013 [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
1014 }
1015
1016 return $errors;
1017 }
1018
1019 /**
1020 * Check sitewide CSS/JSON/JS permissions
1021 *
1022 * @param string $action The action to check
1023 * @param User $user User to check
1024 * @param array $errors List of current errors
1025 * @param string $rigor One of PermissionManager::RIGOR_ constants
1026 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1027 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1028 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1029 * @param bool $short Short circuit on first error
1030 *
1031 * @param LinkTarget $page
1032 *
1033 * @return array List of errors
1034 */
1035 private function checkSiteConfigPermissions(
1036 $action,
1037 User $user,
1038 $errors,
1039 $rigor,
1040 $short,
1041 LinkTarget $page
1042 ) {
1043 // TODO: remove & rework upon further use of LinkTarget
1044 $page = Title::newFromLinkTarget( $page );
1045
1046 if ( $action != 'patrol' ) {
1047 $error = null;
1048 // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
1049 // editinterface right. That's implemented as a restriction so no check needed here.
1050 if ( $page->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
1051 $error = [ 'sitecssprotected', $action ];
1052 } elseif ( $page->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
1053 $error = [ 'sitejsonprotected', $action ];
1054 } elseif ( $page->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
1055 $error = [ 'sitejsprotected', $action ];
1056 } elseif ( $page->isRawHtmlMessage() ) {
1057 // Raw HTML can be used to deploy CSS or JS so require rights for both.
1058 if ( !$user->isAllowed( 'editsitejs' ) ) {
1059 $error = [ 'sitejsprotected', $action ];
1060 } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
1061 $error = [ 'sitecssprotected', $action ];
1062 }
1063 }
1064
1065 if ( $error ) {
1066 if ( $user->isAllowed( 'editinterface' ) ) {
1067 // Most users / site admins will probably find out about the new, more restrictive
1068 // permissions by failing to edit something. Give them more info.
1069 // TODO remove this a few release cycles after 1.32
1070 $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
1071 }
1072 $errors[] = $error;
1073 }
1074 }
1075
1076 return $errors;
1077 }
1078
1079 /**
1080 * Check CSS/JSON/JS sub-page permissions
1081 *
1082 * @param string $action The action to check
1083 * @param User $user User to check
1084 * @param array $errors List of current errors
1085 * @param string $rigor One of PermissionManager::RIGOR_ constants
1086 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1087 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1088 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1089 * @param bool $short Short circuit on first error
1090 *
1091 * @param LinkTarget $page
1092 *
1093 * @return array List of errors
1094 */
1095 private function checkUserConfigPermissions(
1096 $action,
1097 User $user,
1098 $errors,
1099 $rigor,
1100 $short,
1101 LinkTarget $page
1102 ) {
1103 // TODO: remove & rework upon further use of LinkTarget
1104 $page = Title::newFromLinkTarget( $page );
1105
1106 # Protect css/json/js subpages of user pages
1107 # XXX: this might be better using restrictions
1108
1109 if ( $action === 'patrol' ) {
1110 return $errors;
1111 }
1112
1113 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $page->getText() ) ) {
1114 // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
1115 if (
1116 $page->isUserCssConfigPage()
1117 && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
1118 ) {
1119 $errors[] = [ 'mycustomcssprotected', $action ];
1120 } elseif (
1121 $page->isUserJsonConfigPage()
1122 && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
1123 ) {
1124 $errors[] = [ 'mycustomjsonprotected', $action ];
1125 } elseif (
1126 $page->isUserJsConfigPage()
1127 && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
1128 ) {
1129 $errors[] = [ 'mycustomjsprotected', $action ];
1130 }
1131 } else {
1132 // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
1133 // deletion/suppression which cannot be used for attacks and we want to avoid the
1134 // situation where an unprivileged user can post abusive content on their subpages
1135 // and only very highly privileged users could remove it.
1136 if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
1137 if (
1138 $page->isUserCssConfigPage()
1139 && !$user->isAllowed( 'editusercss' )
1140 ) {
1141 $errors[] = [ 'customcssprotected', $action ];
1142 } elseif (
1143 $page->isUserJsonConfigPage()
1144 && !$user->isAllowed( 'edituserjson' )
1145 ) {
1146 $errors[] = [ 'customjsonprotected', $action ];
1147 } elseif (
1148 $page->isUserJsConfigPage()
1149 && !$user->isAllowed( 'edituserjs' )
1150 ) {
1151 $errors[] = [ 'customjsprotected', $action ];
1152 }
1153 }
1154 }
1155
1156 return $errors;
1157 }
1158
1159 /**
1160 * Testing a permission
1161 *
1162 * @since 1.34
1163 *
1164 * @param UserIdentity $user
1165 * @param string $action
1166 *
1167 * @return bool
1168 */
1169 public function userHasRight( UserIdentity $user, $action = '' ) {
1170 if ( $action === '' ) {
1171 return true; // In the spirit of DWIM
1172 }
1173 // Use strict parameter to avoid matching numeric 0 accidentally inserted
1174 // by misconfiguration: 0 == 'foo'
1175 return in_array( $action, $this->getUserPermissions( $user ), true );
1176 }
1177
1178 /**
1179 * Get the permissions this user has.
1180 *
1181 * @since 1.34
1182 *
1183 * @param UserIdentity $user
1184 *
1185 * @return string[] permission names
1186 */
1187 public function getUserPermissions( UserIdentity $user ) {
1188 $user = User::newFromIdentity( $user );
1189 if ( !isset( $this->usersRights[ $user->getId() ] ) ) {
1190 $this->usersRights[ $user->getId() ] = $this->getGroupPermissions(
1191 $user->getEffectiveGroups()
1192 );
1193 Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] );
1194
1195 // Deny any rights denied by the user's session, unless this
1196 // endpoint has no sessions.
1197 if ( !defined( 'MW_NO_SESSION' ) ) {
1198 // FIXME: $user->getRequest().. need to be replaced with something else
1199 $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
1200 if ( $allowedRights !== null ) {
1201 $this->usersRights[ $user->getId() ] = array_intersect(
1202 $this->usersRights[ $user->getId() ],
1203 $allowedRights
1204 );
1205 }
1206 }
1207
1208 Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] );
1209 // Force reindexation of rights when a hook has unset one of them
1210 $this->usersRights[ $user->getId() ] = array_values(
1211 array_unique( $this->usersRights[ $user->getId() ] )
1212 );
1213
1214 if (
1215 $user->isLoggedIn() &&
1216 $this->blockDisablesLogin &&
1217 $user->getBlock()
1218 ) {
1219 $anon = new User;
1220 $this->usersRights[ $user->getId() ] = array_intersect(
1221 $this->usersRights[ $user->getId() ],
1222 $this->getUserPermissions( $anon )
1223 );
1224 }
1225 }
1226 return $this->usersRights[ $user->getId() ];
1227 }
1228
1229 /**
1230 * Clears users permissions cache, if specific user is provided it tries to clear
1231 * permissions cache only for provided user.
1232 *
1233 * @since 1.34
1234 *
1235 * @param User|null $user
1236 */
1237 public function invalidateUsersRightsCache( $user = null ) {
1238 if ( $user !== null ) {
1239 if ( isset( $this->usersRights[ $user->getId() ] ) ) {
1240 unset( $this->usersRights[$user->getId()] );
1241 }
1242 } else {
1243 $this->usersRights = null;
1244 }
1245 }
1246
1247 /**
1248 * Check, if the given group has the given permission
1249 *
1250 * If you're wanting to check whether all users have a permission, use
1251 * PermissionManager::isEveryoneAllowed() instead. That properly checks if it's revoked
1252 * from anyone.
1253 *
1254 * @since 1.34
1255 *
1256 * @param string $group Group to check
1257 * @param string $role Role to check
1258 *
1259 * @return bool
1260 */
1261 public function groupHasPermission( $group, $role ) {
1262 return isset( $this->groupPermissions[$group][$role] ) &&
1263 $this->groupPermissions[$group][$role] &&
1264 !( isset( $this->revokePermissions[$group][$role] ) &&
1265 $this->revokePermissions[$group][$role] );
1266 }
1267
1268 /**
1269 * Get the permissions associated with a given list of groups
1270 *
1271 * @since 1.34
1272 *
1273 * @param array $groups Array of Strings List of internal group names
1274 * @return array Array of Strings List of permission key names for given groups combined
1275 */
1276 public function getGroupPermissions( $groups ) {
1277 $rights = [];
1278 // grant every granted permission first
1279 foreach ( $groups as $group ) {
1280 if ( isset( $this->groupPermissions[$group] ) ) {
1281 $rights = array_merge( $rights,
1282 // array_filter removes empty items
1283 array_keys( array_filter( $this->groupPermissions[$group] ) ) );
1284 }
1285 }
1286 // now revoke the revoked permissions
1287 foreach ( $groups as $group ) {
1288 if ( isset( $this->revokePermissions[$group] ) ) {
1289 $rights = array_diff( $rights,
1290 array_keys( array_filter( $this->revokePermissions[$group] ) ) );
1291 }
1292 }
1293 return array_unique( $rights );
1294 }
1295
1296 /**
1297 * Get all the groups who have a given permission
1298 *
1299 * @since 1.34
1300 *
1301 * @param string $role Role to check
1302 * @return array Array of Strings List of internal group names with the given permission
1303 */
1304 public function getGroupsWithPermission( $role ) {
1305 $allowedGroups = [];
1306 foreach ( array_keys( $this->groupPermissions ) as $group ) {
1307 if ( $this->groupHasPermission( $group, $role ) ) {
1308 $allowedGroups[] = $group;
1309 }
1310 }
1311 return $allowedGroups;
1312 }
1313
1314 /**
1315 * Check if all users may be assumed to have the given permission
1316 *
1317 * We generally assume so if the right is granted to '*' and isn't revoked
1318 * on any group. It doesn't attempt to take grants or other extension
1319 * limitations on rights into account in the general case, though, as that
1320 * would require it to always return false and defeat the purpose.
1321 * Specifically, session-based rights restrictions (such as OAuth or bot
1322 * passwords) are applied based on the current session.
1323 *
1324 * @param string $right Right to check
1325 *
1326 * @return bool
1327 * @since 1.34
1328 */
1329 public function isEveryoneAllowed( $right ) {
1330 // Use the cached results, except in unit tests which rely on
1331 // being able change the permission mid-request
1332 if ( isset( $this->cachedRights[$right] ) ) {
1333 return $this->cachedRights[$right];
1334 }
1335
1336 if ( !isset( $this->groupPermissions['*'][$right] )
1337 || !$this->groupPermissions['*'][$right] ) {
1338 $this->cachedRights[$right] = false;
1339 return false;
1340 }
1341
1342 // If it's revoked anywhere, then everyone doesn't have it
1343 foreach ( $this->revokePermissions as $rights ) {
1344 if ( isset( $rights[$right] ) && $rights[$right] ) {
1345 $this->cachedRights[$right] = false;
1346 return false;
1347 }
1348 }
1349
1350 // Remove any rights that aren't allowed to the global-session user,
1351 // unless there are no sessions for this endpoint.
1352 if ( !defined( 'MW_NO_SESSION' ) ) {
1353
1354 // XXX: think what could be done with the below
1355 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
1356 if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
1357 $this->cachedRights[$right] = false;
1358 return false;
1359 }
1360 }
1361
1362 // Allow extensions to say false
1363 if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
1364 $this->cachedRights[$right] = false;
1365 return false;
1366 }
1367
1368 $this->cachedRights[$right] = true;
1369 return true;
1370 }
1371
1372 /**
1373 * Get a list of all available permissions.
1374 *
1375 * @since 1.34
1376 *
1377 * @return string[] Array of permission names
1378 */
1379 public function getAllPermissions() {
1380 if ( $this->allRights === false ) {
1381 if ( count( $this->availableRights ) ) {
1382 $this->allRights = array_unique( array_merge(
1383 $this->coreRights,
1384 $this->availableRights
1385 ) );
1386 } else {
1387 $this->allRights = $this->coreRights;
1388 }
1389 Hooks::run( 'UserGetAllRights', [ &$this->allRights ] );
1390 }
1391 return $this->allRights;
1392 }
1393
1394 /**
1395 * Overrides user permissions cache
1396 *
1397 * @since 1.34
1398 *
1399 * @param User $user
1400 * @param string[]|string $rights
1401 *
1402 * @throws Exception
1403 */
1404 public function overrideUserRightsForTesting( $user, $rights = [] ) {
1405 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1406 throw new Exception( __METHOD__ . ' can not be called outside of tests' );
1407 }
1408 $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ];
1409 }
1410
1411 }