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.
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.
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
20 namespace MediaWiki\Permissions
;
25 use MediaWiki\Config\ServiceOptions
;
26 use MediaWiki\Linker\LinkTarget
;
27 use MediaWiki\Revision\RevisionLookup
;
28 use MediaWiki\Revision\RevisionRecord
;
29 use MediaWiki\Session\SessionManager
;
30 use MediaWiki\Special\SpecialPageFactory
;
31 use MediaWiki\User\UserIdentity
;
38 use Wikimedia\ScopedCallback
;
42 * A service class for checking permissions
43 * To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager().
47 class PermissionManager
{
49 /** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */
50 const RIGOR_QUICK
= 'quick';
52 /** @var string Does cheap and expensive checks possibly from a replica DB */
53 const RIGOR_FULL
= 'full';
55 /** @var string Does cheap and expensive checks, using the master as needed */
56 const RIGOR_SECURE
= 'secure';
59 * TODO Make this const when HHVM support is dropped (T192166)
64 public static $constructorOptions = [
66 'WhitelistReadRegexp',
72 'NamespaceProtection',
76 /** @var ServiceOptions */
79 /** @var SpecialPageFactory */
80 private $specialPageFactory;
82 /** @var RevisionLookup */
83 private $revisionLookup;
85 /** @var NamespaceInfo */
88 /** @var string[]|null Cached results of getAllRights() */
91 /** @var string[][] Cached user rights */
92 private $usersRights = null;
95 * Temporary user rights, valid for the current request only.
96 * @var string[][][] userid => override group => rights
98 private $temporaryUserRights = [];
100 /** @var string[] Cached rights for isEveryoneAllowed */
101 private $cachedRights = [];
104 * Array of Strings Core rights.
105 * Each of these should have a corresponding message of the form
109 private $coreRights = [
139 'editmyuserjsredirect',
158 'move-categorypages',
159 'move-rootuserpages',
163 'override-export-depth',
185 'userrights-interwiki',
193 * @param ServiceOptions $options
194 * @param SpecialPageFactory $specialPageFactory
195 * @param RevisionLookup $revisionLookup
196 * @param NamespaceInfo $nsInfo
198 public function __construct(
199 ServiceOptions
$options,
200 SpecialPageFactory
$specialPageFactory,
201 RevisionLookup
$revisionLookup,
202 NamespaceInfo
$nsInfo
204 $options->assertRequiredOptions( self
::$constructorOptions );
205 $this->options
= $options;
206 $this->specialPageFactory
= $specialPageFactory;
207 $this->revisionLookup
= $revisionLookup;
208 $this->nsInfo
= $nsInfo;
212 * Can $user perform $action on a page?
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
218 * @see Title::userCan()
220 * @param string $action
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
230 public function userCan( $action, User
$user, LinkTarget
$page, $rigor = self
::RIGOR_SECURE
) {
231 return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
235 * A convenience method for calling PermissionManager::userCan
236 * with PermissionManager::RIGOR_QUICK
238 * Suitable for use for nonessential UI controls in common cases, but
239 * _not_ for functional access control.
240 * May provide false positives, but should never provide a false negative.
242 * @see PermissionManager::userCan()
244 * @param string $action
246 * @param LinkTarget $page
249 public function quickUserCan( $action, User
$user, LinkTarget
$page ) {
250 return $this->userCan( $action, $user, $page, self
::RIGOR_QUICK
);
254 * Can $user perform $action on a page?
256 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
258 * @param string $action Action that permission needs to be checked for
259 * @param User $user User to check
260 * @param LinkTarget $page
261 * @param string $rigor One of PermissionManager::RIGOR_ constants
262 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
263 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
264 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
265 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
266 * whose corresponding errors may be ignored.
268 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
270 public function getPermissionErrors(
274 $rigor = self
::RIGOR_SECURE
,
277 $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
279 // Remove the errors being ignored.
280 foreach ( $errors as $index => $error ) {
281 $errKey = is_array( $error ) ?
$error[0] : $error;
283 if ( in_array( $errKey, $ignoreErrors ) ) {
284 unset( $errors[$index] );
286 if ( $errKey instanceof MessageSpecifier
&& in_array( $errKey->getKey(), $ignoreErrors ) ) {
287 unset( $errors[$index] );
295 * Check if user is blocked from editing a particular article. If the user does not
296 * have a block, this will return false.
299 * @param LinkTarget $page Title to check
300 * @param bool $fromReplica Whether to check the replica DB instead of the master
304 public function isBlockedFrom( User
$user, LinkTarget
$page, $fromReplica = false ) {
305 $block = $user->getBlock( $fromReplica );
310 // TODO: remove upon further migration to LinkTarget
311 $title = Title
::newFromLinkTarget( $page );
313 $blocked = $user->isHidden();
315 // Special handling for a user's own talk page. The block is not aware
316 // of the user, so this must be done here.
317 if ( $title->equals( $user->getTalkPage() ) ) {
318 $blocked = $block->appliesToUsertalk( $title );
320 $blocked = $block->appliesToTitle( $title );
324 // only for the purpose of the hook. We really don't need this here.
325 $allowUsertalk = $user->isAllowUsertalk();
327 // Allow extensions to let a blocked user access a particular page
328 Hooks
::run( 'UserIsBlockedFrom', [ $user, $title, &$blocked, &$allowUsertalk ] );
334 * Can $user perform $action on a page? This is an internal function,
335 * with multiple levels of checks depending on performance needs; see $rigor below.
336 * It does not check wfReadOnly().
338 * @param string $action Action that permission needs to be checked for
339 * @param User $user User to check
340 * @param LinkTarget $page
341 * @param string $rigor One of PermissionManager::RIGOR_ constants
342 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
343 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
344 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
345 * @param bool $short Set this to true to stop after the first permission error.
347 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
350 private function getPermissionErrorsInternal(
354 $rigor = self
::RIGOR_SECURE
,
357 if ( !in_array( $rigor, [ self
::RIGOR_QUICK
, self
::RIGOR_FULL
, self
::RIGOR_SECURE
] ) ) {
358 throw new Exception( "Invalid rigor parameter '$rigor'." );
361 # Read has special handling
362 if ( $action == 'read' ) {
364 'checkPermissionHooks',
365 'checkReadPermissions',
366 'checkUserBlock', // for wgBlockDisablesLogin
368 # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
369 # or checkUserConfigPermissions here as it will lead to duplicate
370 # error messages. This is okay to do since anywhere that checks for
371 # create will also check for edit, and those checks are called for edit.
372 } elseif ( $action == 'create' ) {
374 'checkQuickPermissions',
375 'checkPermissionHooks',
376 'checkPageRestrictions',
377 'checkCascadingSourcesRestrictions',
378 'checkActionPermissions',
383 'checkQuickPermissions',
384 'checkPermissionHooks',
385 'checkSpecialsAndNSPermissions',
386 'checkSiteConfigPermissions',
387 'checkUserConfigPermissions',
388 'checkPageRestrictions',
389 'checkCascadingSourcesRestrictions',
390 'checkActionPermissions',
396 foreach ( $checks as $method ) {
397 $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
399 if ( $short && $errors !== [] ) {
408 * Check various permission hooks
410 * @param string $action The action to check
411 * @param User $user User to check
412 * @param array $errors List of current errors
413 * @param string $rigor One of PermissionManager::RIGOR_ constants
414 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
415 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
416 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
417 * @param bool $short Short circuit on first error
419 * @param LinkTarget $page
421 * @return array List of errors
423 private function checkPermissionHooks(
431 // TODO: remove when LinkTarget usage will expand further
432 $title = Title
::newFromLinkTarget( $page );
433 // Use getUserPermissionsErrors instead
435 if ( !Hooks
::run( 'userCan', [ &$title, &$user, $action, &$result ] ) ) {
436 return $result ?
[] : [ [ 'badaccess-group0' ] ];
438 // Check getUserPermissionsErrors hook
439 if ( !Hooks
::run( 'getUserPermissionsErrors', [ &$title, &$user, $action, &$result ] ) ) {
440 $errors = $this->resultToError( $errors, $result );
442 // Check getUserPermissionsErrorsExpensive hook
444 $rigor !== self
::RIGOR_QUICK
445 && !( $short && count( $errors ) > 0 )
446 && !Hooks
::run( 'getUserPermissionsErrorsExpensive', [ &$title, &$user, $action, &$result ] )
448 $errors = $this->resultToError( $errors, $result );
455 * Add the resulting error code to the errors array
457 * @param array $errors List of current errors
458 * @param array|string|MessageSpecifier|false $result Result of errors
460 * @return array List of errors
462 private function resultToError( $errors, $result ) {
463 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
464 // A single array representing an error
466 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
467 // A nested array representing multiple errors
468 $errors = array_merge( $errors, $result );
469 } elseif ( $result !== '' && is_string( $result ) ) {
470 // A string representing a message-id
471 $errors[] = [ $result ];
472 } elseif ( $result instanceof MessageSpecifier
) {
473 // A message specifier representing an error
474 $errors[] = [ $result ];
475 } elseif ( $result === false ) {
476 // a generic "We don't want them to do that"
477 $errors[] = [ 'badaccess-group0' ];
483 * Check that the user is allowed to read this page.
485 * @param string $action The action to check
486 * @param User $user User to check
487 * @param array $errors List of current errors
488 * @param string $rigor One of PermissionManager::RIGOR_ constants
489 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
490 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
491 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
492 * @param bool $short Short circuit on first error
494 * @param LinkTarget $page
496 * @return array List of errors
498 private function checkReadPermissions(
506 // TODO: remove when LinkTarget usage will expand further
507 $title = Title
::newFromLinkTarget( $page );
509 $whiteListRead = $this->options
->get( 'WhitelistRead' );
510 $whitelisted = false;
511 if ( $this->isEveryoneAllowed( 'read' ) ) {
512 # Shortcut for public wikis, allows skipping quite a bit of code
514 } elseif ( $this->userHasRight( $user, 'read' ) ) {
515 # If the user is allowed to read pages, he is allowed to read all pages
517 } elseif ( $this->isSameSpecialPage( 'Userlogin', $title )
518 ||
$this->isSameSpecialPage( 'PasswordReset', $title )
519 ||
$this->isSameSpecialPage( 'Userlogout', $title )
521 # Always grant access to the login page.
522 # Even anons need to be able to log in.
524 } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) {
525 # Time to check the whitelist
526 # Only do these checks is there's something to check against
527 $name = $title->getPrefixedText();
528 $dbName = $title->getPrefixedDBkey();
530 // Check for explicit whitelisting with and without underscores
531 if ( in_array( $name, $whiteListRead, true )
532 ||
in_array( $dbName, $whiteListRead, true ) ) {
534 } elseif ( $title->getNamespace() == NS_MAIN
) {
535 # Old settings might have the title prefixed with
536 # a colon for main-namespace pages
537 if ( in_array( ':' . $name, $whiteListRead ) ) {
540 } elseif ( $title->isSpecialPage() ) {
541 # If it's a special page, ditch the subpage bit and check again
542 $name = $title->getDBkey();
543 list( $name, /* $subpage */ ) =
544 $this->specialPageFactory
->resolveAlias( $name );
546 $pure = SpecialPage
::getTitleFor( $name )->getPrefixedText();
547 if ( in_array( $pure, $whiteListRead, true ) ) {
554 $whitelistReadRegexp = $this->options
->get( 'WhitelistReadRegexp' );
555 if ( !$whitelisted && is_array( $whitelistReadRegexp )
556 && !empty( $whitelistReadRegexp ) ) {
557 $name = $title->getPrefixedText();
558 // Check for regex whitelisting
559 foreach ( $whitelistReadRegexp as $listItem ) {
560 if ( preg_match( $listItem, $name ) ) {
567 if ( !$whitelisted ) {
568 # If the title is not whitelisted, give extensions a chance to do so...
569 Hooks
::run( 'TitleReadWhitelist', [ $title, $user, &$whitelisted ] );
570 if ( !$whitelisted ) {
571 $errors[] = $this->missingPermissionError( $action, $short );
579 * Get a description array when the user doesn't have the right to perform
580 * $action (i.e. when User::isAllowed() returns false)
582 * @param string $action The action to check
583 * @param bool $short Short circuit on first error
584 * @return array Array containing an error message key and any parameters
586 private function missingPermissionError( $action, $short ) {
587 // We avoid expensive display logic for quickUserCan's and such
589 return [ 'badaccess-group0' ];
592 // TODO: it would be a good idea to replace the method below with something else like
593 // maybe callback injection
594 return User
::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
598 * Returns true if this title resolves to the named special page
600 * @param string $name The special page name
601 * @param LinkTarget $page
605 private function isSameSpecialPage( $name, LinkTarget
$page ) {
606 if ( $page->getNamespace() == NS_SPECIAL
) {
607 list( $thisName, /* $subpage */ ) =
608 $this->specialPageFactory
->resolveAlias( $page->getDBkey() );
609 if ( $name == $thisName ) {
617 * Check that the user isn't blocked from editing.
619 * @param string $action The action to check
620 * @param User $user User to check
621 * @param array $errors List of current errors
622 * @param string $rigor One of PermissionManager::RIGOR_ constants
623 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
624 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
625 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
626 * @param bool $short Short circuit on first error
628 * @param LinkTarget $page
630 * @return array List of errors
632 private function checkUserBlock(
640 // Account creation blocks handled at userlogin.
641 // Unblocking handled in SpecialUnblock
642 if ( $rigor === self
::RIGOR_QUICK ||
in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
646 // Optimize for a very common case
647 if ( $action === 'read' && !$this->options
->get( 'BlockDisablesLogin' ) ) {
651 if ( $this->options
->get( 'EmailConfirmToEdit' )
652 && !$user->isEmailConfirmed()
653 && $action === 'edit'
655 $errors[] = [ 'confirmedittext' ];
658 $useReplica = ( $rigor !== self
::RIGOR_SECURE
);
659 $block = $user->getBlock( $useReplica );
661 // If the user does not have a block, or the block they do have explicitly
662 // allows the action (like "read" or "upload").
663 if ( !$block ||
$block->appliesToRight( $action ) === false ) {
667 // Determine if the user is blocked from this action on this page.
668 // What gets passed into this method is a user right, not an action name.
669 // There is no way to instantiate an action by restriction. However, this
670 // will get the action where the restriction is the same. This may result
671 // in actions being blocked that shouldn't be.
673 if ( Action
::exists( $action ) ) {
674 // TODO: this drags a ton of dependencies in, would be good to avoid WikiPage
675 // instantiation and decouple it creating an ActionPermissionChecker interface
676 $wikiPage = WikiPage
::factory( Title
::newFromLinkTarget( $page, 'clone' ) );
677 // Creating an action will perform several database queries to ensure that
678 // the action has not been overridden by the content type.
679 // FIXME: avoid use of RequestContext since it drags in User and Title dependencies
680 // probably we may use fake context object since it's unlikely that Action uses it
681 // anyway. It would be nice if we could avoid instantiating the Action at all.
682 $actionObj = Action
::factory( $action, $wikiPage, RequestContext
::getMain() );
683 // Ensure that the retrieved action matches the restriction.
684 if ( $actionObj && $actionObj->getRestriction() !== $action ) {
689 // If no action object is returned, assume that the action requires unblock
690 // which is the default.
691 if ( !$actionObj ||
$actionObj->requiresUnblock() ) {
692 if ( $this->isBlockedFrom( $user, $page, $useReplica ) ) {
693 // @todo FIXME: Pass the relevant context into this function.
694 $errors[] = $block->getPermissionsError( RequestContext
::getMain() );
702 * Permissions checks that fail most often, and which are easiest to test.
704 * @param string $action The action to check
705 * @param User $user User to check
706 * @param array $errors List of current errors
707 * @param string $rigor One of PermissionManager::RIGOR_ constants
708 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
709 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
710 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
711 * @param bool $short Short circuit on first error
713 * @param LinkTarget $page
715 * @return array List of errors
717 private function checkQuickPermissions(
725 // TODO: remove when LinkTarget usage will expand further
726 $title = Title
::newFromLinkTarget( $page );
728 if ( !Hooks
::run( 'TitleQuickPermissions',
729 [ $title, $user, $action, &$errors, ( $rigor !== self
::RIGOR_QUICK
), $short ] )
734 $isSubPage = $this->nsInfo
->hasSubpages( $title->getNamespace() ) ?
735 strpos( $title->getText(), '/' ) !== false : false;
737 if ( $action == 'create' ) {
739 ( $this->nsInfo
->isTalk( $title->getNamespace() ) &&
740 !$this->userHasRight( $user, 'createtalk' ) ) ||
741 ( !$this->nsInfo
->isTalk( $title->getNamespace() ) &&
742 !$this->userHasRight( $user, 'createpage' ) )
744 $errors[] = $user->isAnon() ?
[ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
746 } elseif ( $action == 'move' ) {
747 if ( !$this->userHasRight( $user, 'move-rootuserpages' )
748 && $title->getNamespace() == NS_USER
&& !$isSubPage ) {
749 // Show user page-specific message only if the user can move other pages
750 $errors[] = [ 'cant-move-user-page' ];
753 // Check if user is allowed to move files if it's a file
754 if ( $title->getNamespace() == NS_FILE
&&
755 !$this->userHasRight( $user, 'movefile' ) ) {
756 $errors[] = [ 'movenotallowedfile' ];
759 // Check if user is allowed to move category pages if it's a category page
760 if ( $title->getNamespace() == NS_CATEGORY
&&
761 !$this->userHasRight( $user, 'move-categorypages' ) ) {
762 $errors[] = [ 'cant-move-category-page' ];
765 if ( !$this->userHasRight( $user, 'move' ) ) {
766 // User can't move anything
767 $userCanMove = $this->groupHasPermission( 'user', 'move' );
768 $autoconfirmedCanMove = $this->groupHasPermission( 'autoconfirmed', 'move' );
769 if ( $user->isAnon() && ( $userCanMove ||
$autoconfirmedCanMove ) ) {
770 // custom message if logged-in users without any special rights can move
771 $errors[] = [ 'movenologintext' ];
773 $errors[] = [ 'movenotallowed' ];
776 } elseif ( $action == 'move-target' ) {
777 if ( !$this->userHasRight( $user, 'move' ) ) {
778 // User can't move anything
779 $errors[] = [ 'movenotallowed' ];
780 } elseif ( !$this->userHasRight( $user, 'move-rootuserpages' )
781 && $title->getNamespace() == NS_USER
&& !$isSubPage ) {
782 // Show user page-specific message only if the user can move other pages
783 $errors[] = [ 'cant-move-to-user-page' ];
784 } elseif ( !$this->userHasRight( $user, 'move-categorypages' )
785 && $title->getNamespace() == NS_CATEGORY
) {
786 // Show category page-specific message only if the user can move other pages
787 $errors[] = [ 'cant-move-to-category-page' ];
789 } elseif ( !$this->userHasRight( $user, $action ) ) {
790 $errors[] = $this->missingPermissionError( $action, $short );
797 * Check against page_restrictions table requirements on this
798 * page. The user must possess all required rights for this
801 * @param string $action The action to check
802 * @param User $user User to check
803 * @param array $errors List of current errors
804 * @param string $rigor One of PermissionManager::RIGOR_ constants
805 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
806 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
807 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
808 * @param bool $short Short circuit on first error
810 * @param LinkTarget $page
812 * @return array List of errors
814 private function checkPageRestrictions(
822 // TODO: remove & rework upon further use of LinkTarget
823 $title = Title
::newFromLinkTarget( $page );
824 foreach ( $title->getRestrictions( $action ) as $right ) {
825 // Backwards compatibility, rewrite sysop -> editprotected
826 if ( $right == 'sysop' ) {
827 $right = 'editprotected';
829 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
830 if ( $right == 'autoconfirmed' ) {
831 $right = 'editsemiprotected';
833 if ( $right == '' ) {
836 if ( !$this->userHasRight( $user, $right ) ) {
837 $errors[] = [ 'protectedpagetext', $right, $action ];
838 } elseif ( $title->areRestrictionsCascading() &&
839 !$this->userHasRight( $user, 'protect' ) ) {
840 $errors[] = [ 'protectedpagetext', 'protect', $action ];
848 * Check restrictions on cascading pages.
850 * @param string $action The action to check
851 * @param UserIdentity $user User to check
852 * @param array $errors List of current errors
853 * @param string $rigor One of PermissionManager::RIGOR_ constants
854 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
855 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
856 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
857 * @param bool $short Short circuit on first error
859 * @param LinkTarget $page
861 * @return array List of errors
863 private function checkCascadingSourcesRestrictions(
871 // TODO: remove & rework upon further use of LinkTarget
872 $title = Title
::newFromLinkTarget( $page );
873 if ( $rigor !== self
::RIGOR_QUICK
&& !$title->isUserConfigPage() ) {
874 # We /could/ use the protection level on the source page, but it's
875 # fairly ugly as we have to establish a precedence hierarchy for pages
876 # included by multiple cascade-protected pages. So just restrict
877 # it to people with 'protect' permission, as they could remove the
879 list( $cascadingSources, $restrictions ) = $title->getCascadeProtectionSources();
880 # Cascading protection depends on more than this page...
881 # Several cascading protected pages may include this page...
882 # Check each cascading level
883 # This is only for protection restrictions, not for all actions
884 if ( isset( $restrictions[$action] ) ) {
885 foreach ( $restrictions[$action] as $right ) {
886 // Backwards compatibility, rewrite sysop -> editprotected
887 if ( $right == 'sysop' ) {
888 $right = 'editprotected';
890 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
891 if ( $right == 'autoconfirmed' ) {
892 $right = 'editsemiprotected';
894 if ( $right != '' && !$this->userHasAllRights( $user, 'protect', $right ) ) {
896 /** @var Title $wikiPage */
897 foreach ( $cascadingSources as $wikiPage ) {
898 $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
900 $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
910 * Check action permissions not already checked in checkQuickPermissions
912 * @param string $action The action to check
913 * @param User $user User to check
914 * @param array $errors List of current errors
915 * @param string $rigor One of PermissionManager::RIGOR_ constants
916 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
917 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
918 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
919 * @param bool $short Short circuit on first error
921 * @param LinkTarget $page
923 * @return array List of errors
925 private function checkActionPermissions(
933 global $wgDeleteRevisionsLimit, $wgLang;
935 // TODO: remove & rework upon further use of LinkTarget
936 $title = Title
::newFromLinkTarget( $page );
938 if ( $action == 'protect' ) {
939 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
940 // If they can't edit, they shouldn't protect.
941 $errors[] = [ 'protect-cantedit' ];
943 } elseif ( $action == 'create' ) {
944 $title_protection = $title->getTitleProtection();
945 if ( $title_protection ) {
946 if ( $title_protection['permission'] == ''
947 ||
!$this->userHasRight( $user, $title_protection['permission'] )
951 // TODO: get rid of the User dependency
952 User
::whoIs( $title_protection['user'] ),
953 $title_protection['reason']
957 } elseif ( $action == 'move' ) {
958 // Check for immobile pages
959 if ( !$this->nsInfo
->isMovable( $title->getNamespace() ) ) {
960 // Specific message for this case
961 $errors[] = [ 'immobile-source-namespace', $title->getNsText() ];
962 } elseif ( !$title->isMovable() ) {
963 // Less specific message for rarer cases
964 $errors[] = [ 'immobile-source-page' ];
966 } elseif ( $action == 'move-target' ) {
967 if ( !$this->nsInfo
->isMovable( $title->getNamespace() ) ) {
968 $errors[] = [ 'immobile-target-namespace', $title->getNsText() ];
969 } elseif ( !$title->isMovable() ) {
970 $errors[] = [ 'immobile-target-page' ];
972 } elseif ( $action == 'delete' ) {
973 $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $title );
974 if ( !$tempErrors ) {
975 $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
976 $user, $tempErrors, $rigor, true, $title );
979 // If protection keeps them from editing, they shouldn't be able to delete.
980 $errors[] = [ 'deleteprotected' ];
982 if ( $rigor !== self
::RIGOR_QUICK
&& $wgDeleteRevisionsLimit
983 && !$this->userCan( 'bigdelete', $user, $title ) && $title->isBigDeletion()
985 $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
987 } elseif ( $action === 'undelete' ) {
988 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
989 // Undeleting implies editing
990 $errors[] = [ 'undelete-cantedit' ];
992 if ( !$title->exists()
993 && count( $this->getPermissionErrorsInternal( 'create', $user, $title, $rigor, true ) )
995 // Undeleting where nothing currently exists implies creating
996 $errors[] = [ 'undelete-cantcreate' ];
1003 * Check permissions on special pages & namespaces
1005 * @param string $action The action to check
1006 * @param UserIdentity $user User to check
1007 * @param array $errors List of current errors
1008 * @param string $rigor One of PermissionManager::RIGOR_ constants
1009 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1010 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1011 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1012 * @param bool $short Short circuit on first error
1014 * @param LinkTarget $page
1016 * @return array List of errors
1018 private function checkSpecialsAndNSPermissions(
1026 // TODO: remove & rework upon further use of LinkTarget
1027 $title = Title
::newFromLinkTarget( $page );
1029 # Only 'createaccount' can be performed on special pages,
1030 # which don't actually exist in the DB.
1031 if ( $title->getNamespace() == NS_SPECIAL
&& $action !== 'createaccount' ) {
1032 $errors[] = [ 'ns-specialprotected' ];
1035 # Check $wgNamespaceProtection for restricted namespaces
1036 if ( $this->isNamespaceProtected( $title->getNamespace(), $user ) ) {
1037 $ns = $title->getNamespace() == NS_MAIN ?
1038 wfMessage( 'nstab-main' )->text() : $title->getNsText();
1039 $errors[] = $title->getNamespace() == NS_MEDIAWIKI ?
1040 [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
1047 * Check sitewide CSS/JSON/JS permissions
1049 * @param string $action The action to check
1050 * @param User $user User to check
1051 * @param array $errors List of current errors
1052 * @param string $rigor One of PermissionManager::RIGOR_ constants
1053 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1054 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1055 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1056 * @param bool $short Short circuit on first error
1058 * @param LinkTarget $page
1060 * @return array List of errors
1062 private function checkSiteConfigPermissions(
1070 // TODO: remove & rework upon further use of LinkTarget
1071 $title = Title
::newFromLinkTarget( $page );
1073 if ( $action != 'patrol' ) {
1075 // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
1076 // editinterface right. That's implemented as a restriction so no check needed here.
1077 if ( $title->isSiteCssConfigPage() && !$this->userHasRight( $user, 'editsitecss' ) ) {
1078 $error = [ 'sitecssprotected', $action ];
1079 } elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) {
1080 $error = [ 'sitejsonprotected', $action ];
1081 } elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) {
1082 $error = [ 'sitejsprotected', $action ];
1083 } elseif ( $title->isRawHtmlMessage() ) {
1084 // Raw HTML can be used to deploy CSS or JS so require rights for both.
1085 if ( !$this->userHasRight( $user, 'editsitejs' ) ) {
1086 $error = [ 'sitejsprotected', $action ];
1087 } elseif ( !$this->userHasRight( $user, 'editsitecss' ) ) {
1088 $error = [ 'sitecssprotected', $action ];
1093 if ( $this->userHasRight( $user, 'editinterface' ) ) {
1094 // Most users / site admins will probably find out about the new, more restrictive
1095 // permissions by failing to edit something. Give them more info.
1096 // TODO remove this a few release cycles after 1.32
1097 $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
1107 * Check CSS/JSON/JS sub-page permissions
1109 * @param string $action The action to check
1110 * @param UserIdentity $user User to check
1111 * @param array $errors List of current errors
1112 * @param string $rigor One of PermissionManager::RIGOR_ constants
1113 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1114 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1115 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1116 * @param bool $short Short circuit on first error
1118 * @param LinkTarget $page
1120 * @return array List of errors
1122 private function checkUserConfigPermissions(
1130 // TODO: remove & rework upon further use of LinkTarget
1131 $title = Title
::newFromLinkTarget( $page );
1133 # Protect css/json/js subpages of user pages
1134 # XXX: this might be better using restrictions
1136 if ( $action === 'patrol' ) {
1140 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $title->getText() ) ) {
1141 // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
1143 $title->isUserCssConfigPage()
1144 && !$this->userHasAnyRight( $user, 'editmyusercss', 'editusercss' )
1146 $errors[] = [ 'mycustomcssprotected', $action ];
1148 $title->isUserJsonConfigPage()
1149 && !$this->userHasAnyRight( $user, 'editmyuserjson', 'edituserjson' )
1151 $errors[] = [ 'mycustomjsonprotected', $action ];
1153 $title->isUserJsConfigPage()
1154 && !$this->userHasAnyRight( $user, 'editmyuserjs', 'edituserjs' )
1156 $errors[] = [ 'mycustomjsprotected', $action ];
1158 $title->isUserJsConfigPage()
1159 && !$this->userHasAnyRight( $user, 'edituserjs', 'editmyuserjsredirect' )
1161 // T207750 - do not allow users to edit a redirect if they couldn't edit the target
1162 $rev = $this->revisionLookup
->getRevisionByTitle( $title );
1163 $content = $rev ?
$rev->getContent( 'main', RevisionRecord
::RAW
) : null;
1164 $target = $content ?
$content->getUltimateRedirectTarget() : null;
1166 !$target->inNamespace( NS_USER
)
1167 ||
!preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
1169 $errors[] = [ 'mycustomjsredirectprotected', $action ];
1173 // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
1174 // deletion/suppression which cannot be used for attacks and we want to avoid the
1175 // situation where an unprivileged user can post abusive content on their subpages
1176 // and only very highly privileged users could remove it.
1177 if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
1179 $title->isUserCssConfigPage()
1180 && !$this->userHasRight( $user, 'editusercss' )
1182 $errors[] = [ 'customcssprotected', $action ];
1184 $title->isUserJsonConfigPage()
1185 && !$this->userHasRight( $user, 'edituserjson' )
1187 $errors[] = [ 'customjsonprotected', $action ];
1189 $title->isUserJsConfigPage()
1190 && !$this->userHasRight( $user, 'edituserjs' )
1192 $errors[] = [ 'customjsprotected', $action ];
1201 * Testing a permission
1205 * @param UserIdentity $user
1206 * @param string $action
1210 public function userHasRight( UserIdentity
$user, $action = '' ) {
1211 if ( $action === '' ) {
1212 return true; // In the spirit of DWIM
1214 // Use strict parameter to avoid matching numeric 0 accidentally inserted
1215 // by misconfiguration: 0 == 'foo'
1216 return in_array( $action, $this->getUserPermissions( $user ), true );
1220 * Check if user is allowed to make any action
1222 * @param UserIdentity $user
1223 * // TODO: HHVM bug T228695#5450847 @param string ...$actions
1224 * @suppress PhanCommentParamWithoutRealParam
1225 * @return bool True if user is allowed to perform *any* of the given actions
1228 public function userHasAnyRight( UserIdentity
$user ) {
1229 $actions = array_slice( func_get_args(), 1 );
1230 foreach ( $actions as $action ) {
1231 if ( $this->userHasRight( $user, $action ) ) {
1239 * Check if user is allowed to make all actions
1241 * @param UserIdentity $user
1242 * // TODO: HHVM bug T228695#5450847 @param string ...$actions
1243 * @suppress PhanCommentParamWithoutRealParam
1244 * @return bool True if user is allowed to perform *all* of the given actions
1247 public function userHasAllRights( UserIdentity
$user ) {
1248 $actions = array_slice( func_get_args(), 1 );
1249 foreach ( $actions as $action ) {
1250 if ( !$this->userHasRight( $user, $action ) ) {
1258 * Get the permissions this user has.
1262 * @param UserIdentity $user
1264 * @return string[] permission names
1266 public function getUserPermissions( UserIdentity
$user ) {
1267 $user = User
::newFromIdentity( $user );
1268 $rightsCacheKey = $this->getRightsCacheKey( $user );
1269 if ( !isset( $this->usersRights
[ $rightsCacheKey ] ) ) {
1270 $this->usersRights
[ $rightsCacheKey ] = $this->getGroupPermissions(
1271 $user->getEffectiveGroups()
1273 Hooks
::run( 'UserGetRights', [ $user, &$this->usersRights
[ $rightsCacheKey ] ] );
1275 // Deny any rights denied by the user's session, unless this
1276 // endpoint has no sessions.
1277 if ( !defined( 'MW_NO_SESSION' ) ) {
1278 // FIXME: $user->getRequest().. need to be replaced with something else
1279 $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
1280 if ( $allowedRights !== null ) {
1281 $this->usersRights
[ $rightsCacheKey ] = array_intersect(
1282 $this->usersRights
[ $rightsCacheKey ],
1288 Hooks
::run( 'UserGetRightsRemove', [ $user, &$this->usersRights
[ $rightsCacheKey ] ] );
1289 // Force reindexation of rights when a hook has unset one of them
1290 $this->usersRights
[ $rightsCacheKey ] = array_values(
1291 array_unique( $this->usersRights
[ $rightsCacheKey ] )
1295 $user->isLoggedIn() &&
1296 $this->options
->get( 'BlockDisablesLogin' ) &&
1300 $this->usersRights
[ $rightsCacheKey ] = array_intersect(
1301 $this->usersRights
[ $rightsCacheKey ],
1302 $this->getUserPermissions( $anon )
1306 $rights = $this->usersRights
[ $rightsCacheKey ];
1307 foreach ( $this->temporaryUserRights
[ $user->getId() ] ??
[] as $overrides ) {
1308 $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
1314 * Clears users permissions cache, if specific user is provided it tries to clear
1315 * permissions cache only for provided user.
1319 * @param User|null $user
1321 public function invalidateUsersRightsCache( $user = null ) {
1322 if ( $user !== null ) {
1323 $rightsCacheKey = $this->getRightsCacheKey( $user );
1324 if ( isset( $this->usersRights
[ $rightsCacheKey ] ) ) {
1325 unset( $this->usersRights
[ $rightsCacheKey ] );
1328 $this->usersRights
= null;
1333 * Gets a unique key for user rights cache.
1334 * @param UserIdentity $user
1337 private function getRightsCacheKey( UserIdentity
$user ) {
1338 return $user->isRegistered() ?
"u:{$user->getId()}" : "anon:{$user->getName()}";
1342 * Check, if the given group has the given permission
1344 * If you're wanting to check whether all users have a permission, use
1345 * PermissionManager::isEveryoneAllowed() instead. That properly checks if it's revoked
1350 * @param string $group Group to check
1351 * @param string $role Role to check
1355 public function groupHasPermission( $group, $role ) {
1356 $groupPermissions = $this->options
->get( 'GroupPermissions' );
1357 $revokePermissions = $this->options
->get( 'RevokePermissions' );
1358 return isset( $groupPermissions[$group][$role] ) && $groupPermissions[$group][$role] &&
1359 !( isset( $revokePermissions[$group][$role] ) && $revokePermissions[$group][$role] );
1363 * Get the permissions associated with a given list of groups
1367 * @param array $groups Array of Strings List of internal group names
1368 * @return array Array of Strings List of permission key names for given groups combined
1370 public function getGroupPermissions( $groups ) {
1372 // grant every granted permission first
1373 foreach ( $groups as $group ) {
1374 if ( isset( $this->options
->get( 'GroupPermissions' )[$group] ) ) {
1375 $rights = array_merge( $rights,
1376 // array_filter removes empty items
1377 array_keys( array_filter( $this->options
->get( 'GroupPermissions' )[$group] ) ) );
1380 // now revoke the revoked permissions
1381 foreach ( $groups as $group ) {
1382 if ( isset( $this->options
->get( 'RevokePermissions' )[$group] ) ) {
1383 $rights = array_diff( $rights,
1384 array_keys( array_filter( $this->options
->get( 'RevokePermissions' )[$group] ) ) );
1387 return array_unique( $rights );
1391 * Get all the groups who have a given permission
1395 * @param string $role Role to check
1396 * @return array Array of Strings List of internal group names with the given permission
1398 public function getGroupsWithPermission( $role ) {
1399 $allowedGroups = [];
1400 foreach ( array_keys( $this->options
->get( 'GroupPermissions' ) ) as $group ) {
1401 if ( $this->groupHasPermission( $group, $role ) ) {
1402 $allowedGroups[] = $group;
1405 return $allowedGroups;
1409 * Check if all users may be assumed to have the given permission
1411 * We generally assume so if the right is granted to '*' and isn't revoked
1412 * on any group. It doesn't attempt to take grants or other extension
1413 * limitations on rights into account in the general case, though, as that
1414 * would require it to always return false and defeat the purpose.
1415 * Specifically, session-based rights restrictions (such as OAuth or bot
1416 * passwords) are applied based on the current session.
1418 * @param string $right Right to check
1423 public function isEveryoneAllowed( $right ) {
1424 // Use the cached results, except in unit tests which rely on
1425 // being able change the permission mid-request
1426 if ( isset( $this->cachedRights
[$right] ) ) {
1427 return $this->cachedRights
[$right];
1430 if ( !isset( $this->options
->get( 'GroupPermissions' )['*'][$right] )
1431 ||
!$this->options
->get( 'GroupPermissions' )['*'][$right] ) {
1432 $this->cachedRights
[$right] = false;
1436 // If it's revoked anywhere, then everyone doesn't have it
1437 foreach ( $this->options
->get( 'RevokePermissions' ) as $rights ) {
1438 if ( isset( $rights[$right] ) && $rights[$right] ) {
1439 $this->cachedRights
[$right] = false;
1444 // Remove any rights that aren't allowed to the global-session user,
1445 // unless there are no sessions for this endpoint.
1446 if ( !defined( 'MW_NO_SESSION' ) ) {
1448 // XXX: think what could be done with the below
1449 $allowedRights = SessionManager
::getGlobalSession()->getAllowedUserRights();
1450 if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
1451 $this->cachedRights
[$right] = false;
1456 // Allow extensions to say false
1457 if ( !Hooks
::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
1458 $this->cachedRights
[$right] = false;
1462 $this->cachedRights
[$right] = true;
1467 * Get a list of all available permissions.
1471 * @return string[] Array of permission names
1473 public function getAllPermissions() {
1474 if ( $this->allRights
=== null ) {
1475 if ( count( $this->options
->get( 'AvailableRights' ) ) ) {
1476 $this->allRights
= array_unique( array_merge(
1478 $this->options
->get( 'AvailableRights' )
1481 $this->allRights
= $this->coreRights
;
1483 Hooks
::run( 'UserGetAllRights', [ &$this->allRights
] );
1485 return $this->allRights
;
1489 * Determines if $user is unable to edit pages in namespace because it has been protected.
1491 * @param UserIdentity $user
1494 private function isNamespaceProtected( $index, UserIdentity
$user ) {
1495 $namespaceProtection = $this->options
->get( 'NamespaceProtection' );
1496 if ( isset( $namespaceProtection[$index] ) ) {
1497 return !$this->userHasAllRights( $user, ...(array)$namespaceProtection[$index] );
1503 * Determine which restriction levels it makes sense to use in a namespace,
1504 * optionally filtered by a user's rights.
1506 * @param int $index Index to check
1507 * @param UserIdentity|null $user User to check
1510 public function getNamespaceRestrictionLevels( $index, UserIdentity
$user = null ) {
1511 if ( !isset( $this->options
->get( 'NamespaceProtection' )[$index] ) ) {
1512 // All levels are valid if there's no namespace restriction.
1513 // But still filter by user, if necessary
1514 $levels = $this->options
->get( 'RestrictionLevels' );
1516 $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
1518 if ( $right == 'sysop' ) {
1519 $right = 'editprotected'; // BC
1521 if ( $right == 'autoconfirmed' ) {
1522 $right = 'editsemiprotected'; // BC
1524 return $this->userHasRight( $user, $right );
1530 // $wgNamespaceProtection can require one or more rights to edit the namespace, which
1531 // may be satisfied by membership in multiple groups each giving a subset of those rights.
1532 // A restriction level is redundant if, for any one of the namespace rights, all groups
1533 // giving that right also give the restriction level's right. Or, conversely, a
1534 // restriction level is not redundant if, for every namespace right, there's at least one
1535 // group giving that right without the restriction level's right.
1537 // First, for each right, get a list of groups with that right.
1538 $namespaceRightGroups = [];
1539 foreach ( (array)$this->options
->get( 'NamespaceProtection' )[$index] as $right ) {
1540 if ( $right == 'sysop' ) {
1541 $right = 'editprotected'; // BC
1543 if ( $right == 'autoconfirmed' ) {
1544 $right = 'editsemiprotected'; // BC
1546 if ( $right != '' ) {
1547 $namespaceRightGroups[$right] = $this->getGroupsWithPermission( $right );
1551 // Now, go through the protection levels one by one.
1552 $usableLevels = [ '' ];
1553 foreach ( $this->options
->get( 'RestrictionLevels' ) as $level ) {
1555 if ( $right == 'sysop' ) {
1556 $right = 'editprotected'; // BC
1558 if ( $right == 'autoconfirmed' ) {
1559 $right = 'editsemiprotected'; // BC
1562 if ( $right != '' &&
1563 !isset( $namespaceRightGroups[$right] ) &&
1564 ( !$user ||
$this->userHasRight( $user, $right ) )
1566 // Do any of the namespace rights imply the restriction right? (see explanation above)
1567 foreach ( $namespaceRightGroups as $groups ) {
1568 if ( !array_diff( $groups, $this->getGroupsWithPermission( $right ) ) ) {
1569 // Yes, this one does.
1573 // No, keep the restriction level
1574 $usableLevels[] = $level;
1578 return $usableLevels;
1582 * Add temporary user rights, only valid for the current scope.
1583 * This is meant for making it possible to programatically trigger certain actions that
1584 * the user wouldn't be able to trigger themselves; e.g. allow users without the bot right
1585 * to make bot-flagged actions through certain special pages.
1586 * Returns a "scope guard" variable; whenever that variable goes out of scope or is consumed
1587 * via ScopedCallback::consume(), the temporary rights are revoked.
1591 * @param UserIdentity $user
1592 * @param string|string[] $rights
1593 * @return ScopedCallback
1595 public function addTemporaryUserRights( UserIdentity
$user, $rights ) {
1596 $userId = $user->getId();
1597 $nextKey = count( $this->temporaryUserRights
[$userId] ??
[] );
1598 $this->temporaryUserRights
[$userId][$nextKey] = (array)$rights;
1599 return new ScopedCallback( function () use ( $userId, $nextKey ) {
1600 unset( $this->temporaryUserRights
[$userId][$nextKey] );
1605 * Overrides user permissions cache
1610 * @param string[]|string $rights
1614 public function overrideUserRightsForTesting( $user, $rights = [] ) {
1615 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1616 throw new Exception( __METHOD__
. ' can not be called outside of tests' );
1618 $this->usersRights
[ $this->getRightsCacheKey( $user ) ] =
1619 is_array( $rights ) ?
$rights : [ $rights ];