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\Linker\LinkTarget
;
26 use MediaWiki\Session\SessionManager
;
27 use MediaWiki\Special\SpecialPageFactory
;
28 use MediaWiki\User\UserIdentity
;
38 * A service class for checking permissions
39 * To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager().
43 class PermissionManager
{
45 /** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */
46 const RIGOR_QUICK
= 'quick';
48 /** @var string Does cheap and expensive checks possibly from a replica DB */
49 const RIGOR_FULL
= 'full';
51 /** @var string Does cheap and expensive checks, using the master as needed */
52 const RIGOR_SECURE
= 'secure';
54 /** @var SpecialPageFactory */
55 private $specialPageFactory;
57 /** @var string[] List of pages names anonymous user may see */
58 private $whitelistRead;
60 /** @var string[] Whitelists publicly readable titles with regular expressions */
61 private $whitelistReadRegexp;
63 /** @var bool Require users to confirm email address before they can edit */
64 private $emailConfirmToEdit;
66 /** @var bool If set to true, blocked users will no longer be allowed to log in */
67 private $blockDisablesLogin;
69 /** @var NamespaceInfo */
72 /** @var string[][] Access rights for groups and users in these groups */
73 private $groupPermissions;
75 /** @var string[][] Permission keys revoked from users in each group */
76 private $revokePermissions;
78 /** @var string[] A list of available rights, in addition to the ones defined by the core */
79 private $availableRights;
81 /** @var string[] Cached results of getAllRights() */
82 private $allRights = false;
84 /** @var string[][] Cached user rights */
85 private $usersRights = null;
87 /** @var string[] Cached rights for isEveryoneAllowed */
88 private $cachedRights = [];
91 * Array of Strings Core rights.
92 * Each of these should have a corresponding message of the form
96 private $coreRights = [
144 'move-categorypages',
145 'move-rootuserpages',
149 'override-export-depth',
171 'userrights-interwiki',
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
189 public function __construct(
190 SpecialPageFactory
$specialPageFactory,
192 $whitelistReadRegexp,
198 NamespaceInfo
$nsInfo
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;
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 * Can $user perform $action on a page?
237 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
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.
249 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
251 public function getPermissionErrors(
255 $rigor = self
::RIGOR_SECURE
,
258 $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
260 // Remove the errors being ignored.
261 foreach ( $errors as $index => $error ) {
262 $errKey = is_array( $error ) ?
$error[0] : $error;
264 if ( in_array( $errKey, $ignoreErrors ) ) {
265 unset( $errors[$index] );
267 if ( $errKey instanceof MessageSpecifier
&& in_array( $errKey->getKey(), $ignoreErrors ) ) {
268 unset( $errors[$index] );
276 * Check if user is blocked from editing a particular article
279 * @param LinkTarget $page Title to check
280 * @param bool $fromReplica Whether to check the replica DB instead of the master
284 public function isBlockedFrom( User
$user, LinkTarget
$page, $fromReplica = false ) {
285 $blocked = $user->isHidden();
287 // TODO: remove upon further migration to LinkTarget
288 $page = Title
::newFromLinkTarget( $page );
291 $block = $user->getBlock( $fromReplica );
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 );
298 $blocked = $block->appliesToTitle( $page );
303 // only for the purpose of the hook. We really don't need this here.
304 $allowUsertalk = $user->isAllowUsertalk();
306 Hooks
::run( 'UserIsBlockedFrom', [ $user, $page, &$blocked, &$allowUsertalk ] );
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().
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.
325 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
328 private function getPermissionErrorsInternal(
332 $rigor = self
::RIGOR_SECURE
,
335 if ( !in_array( $rigor, [ self
::RIGOR_QUICK
, self
::RIGOR_FULL
, self
::RIGOR_SECURE
] ) ) {
336 throw new Exception( "Invalid rigor parameter '$rigor'." );
339 # Read has special handling
340 if ( $action == 'read' ) {
342 'checkPermissionHooks',
343 'checkReadPermissions',
344 'checkUserBlock', // for wgBlockDisablesLogin
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' ) {
352 'checkQuickPermissions',
353 'checkPermissionHooks',
354 'checkPageRestrictions',
355 'checkCascadingSourcesRestrictions',
356 'checkActionPermissions',
361 'checkQuickPermissions',
362 'checkPermissionHooks',
363 'checkSpecialsAndNSPermissions',
364 'checkSiteConfigPermissions',
365 'checkUserConfigPermissions',
366 'checkPageRestrictions',
367 'checkCascadingSourcesRestrictions',
368 'checkActionPermissions',
374 foreach ( $checks as $method ) {
375 $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
377 if ( $short && $errors !== [] ) {
386 * Check various permission hooks
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
397 * @param LinkTarget $page
399 * @return array List of errors
401 private function checkPermissionHooks(
409 // TODO: remove when LinkTarget usage will expand further
410 $page = Title
::newFromLinkTarget( $page );
411 // Use getUserPermissionsErrors instead
413 if ( !Hooks
::run( 'userCan', [ &$page, &$user, $action, &$result ] ) ) {
414 return $result ?
[] : [ [ 'badaccess-group0' ] ];
416 // Check getUserPermissionsErrors hook
417 if ( !Hooks
::run( 'getUserPermissionsErrors', [ &$page, &$user, $action, &$result ] ) ) {
418 $errors = $this->resultToError( $errors, $result );
420 // Check getUserPermissionsErrorsExpensive hook
422 $rigor !== self
::RIGOR_QUICK
423 && !( $short && count( $errors ) > 0 )
424 && !Hooks
::run( 'getUserPermissionsErrorsExpensive', [ &$page, &$user, $action, &$result ] )
426 $errors = $this->resultToError( $errors, $result );
433 * Add the resulting error code to the errors array
435 * @param array $errors List of current errors
436 * @param array|string|MessageSpecifier|false $result Result of errors
438 * @return array List of errors
440 private function resultToError( $errors, $result ) {
441 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
442 // A single array representing an error
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' ];
461 * Check that the user is allowed to read this page.
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
472 * @param LinkTarget $page
474 * @return array List of errors
476 private function checkReadPermissions(
484 // TODO: remove when LinkTarget usage will expand further
485 $page = Title
::newFromLinkTarget( $page );
487 $whitelisted = false;
488 if ( User
::isEveryoneAllowed( 'read' ) ) {
489 # Shortcut for public wikis, allows skipping quite a bit of code
491 } elseif ( $user->isAllowed( 'read' ) ) {
492 # If the user is allowed to read pages, he is allowed to read all pages
494 } elseif ( $this->isSameSpecialPage( 'Userlogin', $page )
495 ||
$this->isSameSpecialPage( 'PasswordReset', $page )
496 ||
$this->isSameSpecialPage( 'Userlogout', $page )
498 # Always grant access to the login page.
499 # Even anons need to be able to log in.
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();
507 // Check for explicit whitelisting with and without underscores
508 if ( in_array( $name, $this->whitelistRead
, true )
509 ||
in_array( $dbName, $this->whitelistRead
, 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
) ) {
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 );
523 $pure = SpecialPage
::getTitleFor( $name )->getPrefixedText();
524 if ( in_array( $pure, $this->whitelistRead
, true ) ) {
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 ) ) {
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 );
555 * Get a description array when the user doesn't have the right to perform
556 * $action (i.e. when User::isAllowed() returns false)
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
562 private function missingPermissionError( $action, $short ) {
563 // We avoid expensive display logic for quickUserCan's and such
565 return [ 'badaccess-group0' ];
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];
574 * Returns true if this title resolves to the named special page
576 * @param string $name The special page name
577 * @param LinkTarget $page
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 ) {
593 * Check that the user isn't blocked from editing.
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
604 * @param LinkTarget $page
606 * @return array List of errors
608 private function checkUserBlock(
616 // Account creation blocks handled at userlogin.
617 // Unblocking handled in SpecialUnblock
618 if ( $rigor === self
::RIGOR_QUICK ||
in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
622 // Optimize for a very common case
623 if ( $action === 'read' && !$this->blockDisablesLogin
) {
627 if ( $this->emailConfirmToEdit
628 && !$user->isEmailConfirmed()
629 && $action === 'edit'
631 $errors[] = [ 'confirmedittext' ];
634 $useReplica = ( $rigor !== self
::RIGOR_SECURE
);
635 $block = $user->getBlock( $useReplica );
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 ) {
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.
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 ) {
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() );
678 * Permissions checks that fail most often, and which are easiest to test.
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
689 * @param LinkTarget $page
691 * @return array List of errors
693 private function checkQuickPermissions(
701 // TODO: remove when LinkTarget usage will expand further
702 $page = Title
::newFromLinkTarget( $page );
704 if ( !Hooks
::run( 'TitleQuickPermissions',
705 [ $page, $user, $action, &$errors, ( $rigor !== self
::RIGOR_QUICK
), $short ] )
710 $isSubPage = $this->nsInfo
->hasSubpages( $page->getNamespace() ) ?
711 strpos( $page->getText(), '/' ) !== false : false;
713 if ( $action == 'create' ) {
715 ( $this->nsInfo
->isTalk( $page->getNamespace() ) &&
716 !$user->isAllowed( 'createtalk' ) ) ||
717 ( !$this->nsInfo
->isTalk( $page->getNamespace() ) &&
718 !$user->isAllowed( 'createpage' ) )
720 $errors[] = $user->isAnon() ?
[ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
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' ];
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' ];
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' ];
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' ];
747 $errors[] = [ 'movenotallowed' ];
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' ];
763 } elseif ( !$user->isAllowed( $action ) ) {
764 $errors[] = $this->missingPermissionError( $action, $short );
771 * Check against page_restrictions table requirements on this
772 * page. The user must possess all required rights for this
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
784 * @param LinkTarget $page
786 * @return array List of errors
788 private function checkPageRestrictions(
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';
803 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
804 if ( $right == 'autoconfirmed' ) {
805 $right = 'editsemiprotected';
807 if ( $right == '' ) {
810 if ( !$user->isAllowed( $right ) ) {
811 $errors[] = [ 'protectedpagetext', $right, $action ];
812 } elseif ( $page->areRestrictionsCascading() && !$user->isAllowed( 'protect' ) ) {
813 $errors[] = [ 'protectedpagetext', 'protect', $action ];
821 * Check restrictions on cascading pages.
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
832 * @param LinkTarget $page
834 * @return array List of errors
836 private function checkCascadingSourcesRestrictions(
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
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';
863 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
864 if ( $right == 'autoconfirmed' ) {
865 $right = 'editsemiprotected';
867 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
869 /** @var Title $wikiPage */
870 foreach ( $cascadingSources as $wikiPage ) {
871 $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
873 $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
883 * Check action permissions not already checked in checkQuickPermissions
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
894 * @param LinkTarget $page
896 * @return array List of errors
898 private function checkActionPermissions(
906 global $wgDeleteRevisionsLimit, $wgLang;
908 // TODO: remove & rework upon further use of LinkTarget
909 $page = Title
::newFromLinkTarget( $page );
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' ];
916 } elseif ( $action == 'create' ) {
917 $title_protection = $page->getTitleProtection();
918 if ( $title_protection ) {
919 if ( $title_protection['permission'] == ''
920 ||
!$user->isAllowed( $title_protection['permission'] )
924 // TODO: get rid of the User dependency
925 User
::whoIs( $title_protection['user'] ),
926 $title_protection['reason']
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' ];
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' ];
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 );
952 // If protection keeps them from editing, they shouldn't be able to delete.
953 $errors[] = [ 'deleteprotected' ];
955 if ( $rigor !== self
::RIGOR_QUICK
&& $wgDeleteRevisionsLimit
956 && !$this->userCan( 'bigdelete', $user, $page ) && $page->isBigDeletion()
958 $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
960 } elseif ( $action === 'undelete' ) {
961 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
962 // Undeleting implies editing
963 $errors[] = [ 'undelete-cantedit' ];
965 if ( !$page->exists()
966 && count( $this->getPermissionErrorsInternal( 'create', $user, $page, $rigor, true ) )
968 // Undeleting where nothing currently exists implies creating
969 $errors[] = [ 'undelete-cantcreate' ];
976 * Check permissions on special pages & namespaces
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
987 * @param LinkTarget $page
989 * @return array List of errors
991 private function checkSpecialsAndNSPermissions(
999 // TODO: remove & rework upon further use of LinkTarget
1000 $page = Title
::newFromLinkTarget( $page );
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' ];
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 ];
1020 * Check sitewide CSS/JSON/JS permissions
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
1031 * @param LinkTarget $page
1033 * @return array List of errors
1035 private function checkSiteConfigPermissions(
1043 // TODO: remove & rework upon further use of LinkTarget
1044 $page = Title
::newFromLinkTarget( $page );
1046 if ( $action != 'patrol' ) {
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 ];
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] ) ];
1080 * Check CSS/JSON/JS sub-page permissions
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
1091 * @param LinkTarget $page
1093 * @return array List of errors
1095 private function checkUserConfigPermissions(
1103 // TODO: remove & rework upon further use of LinkTarget
1104 $page = Title
::newFromLinkTarget( $page );
1106 # Protect css/json/js subpages of user pages
1107 # XXX: this might be better using restrictions
1109 if ( $action === 'patrol' ) {
1113 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $page->getText() ) ) {
1114 // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
1116 $page->isUserCssConfigPage()
1117 && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
1119 $errors[] = [ 'mycustomcssprotected', $action ];
1121 $page->isUserJsonConfigPage()
1122 && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
1124 $errors[] = [ 'mycustomjsonprotected', $action ];
1126 $page->isUserJsConfigPage()
1127 && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
1129 $errors[] = [ 'mycustomjsprotected', $action ];
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 ) ) {
1138 $page->isUserCssConfigPage()
1139 && !$user->isAllowed( 'editusercss' )
1141 $errors[] = [ 'customcssprotected', $action ];
1143 $page->isUserJsonConfigPage()
1144 && !$user->isAllowed( 'edituserjson' )
1146 $errors[] = [ 'customjsonprotected', $action ];
1148 $page->isUserJsConfigPage()
1149 && !$user->isAllowed( 'edituserjs' )
1151 $errors[] = [ 'customjsprotected', $action ];
1160 * Testing a permission
1164 * @param UserIdentity $user
1165 * @param string $action
1169 public function userHasRight( UserIdentity
$user, $action = '' ) {
1170 if ( $action === '' ) {
1171 return true; // In the spirit of DWIM
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 );
1179 * Get the permissions this user has.
1183 * @param UserIdentity $user
1185 * @return string[] permission names
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()
1193 Hooks
::run( 'UserGetRights', [ $user, &$this->usersRights
[ $user->getId() ] ] );
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() ],
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() ] )
1215 $user->isLoggedIn() &&
1216 $this->blockDisablesLogin
&&
1220 $this->usersRights
[ $user->getId() ] = array_intersect(
1221 $this->usersRights
[ $user->getId() ],
1222 $this->getUserPermissions( $anon )
1226 return $this->usersRights
[ $user->getId() ];
1230 * Clears users permissions cache, if specific user is provided it tries to clear
1231 * permissions cache only for provided user.
1235 * @param User|null $user
1237 public function invalidateUsersRightsCache( $user = null ) {
1238 if ( $user !== null ) {
1239 if ( isset( $this->usersRights
[ $user->getId() ] ) ) {
1240 unset( $this->usersRights
[$user->getId()] );
1243 $this->usersRights
= null;
1248 * Check, if the given group has the given permission
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
1256 * @param string $group Group to check
1257 * @param string $role Role to check
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] );
1269 * Get the permissions associated with a given list of groups
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
1276 public function getGroupPermissions( $groups ) {
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] ) ) );
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] ) ) );
1293 return array_unique( $rights );
1297 * Get all the groups who have a given permission
1301 * @param string $role Role to check
1302 * @return array Array of Strings List of internal group names with the given permission
1304 public function getGroupsWithPermission( $role ) {
1305 $allowedGroups = [];
1306 foreach ( array_keys( $this->groupPermissions
) as $group ) {
1307 if ( $this->groupHasPermission( $group, $role ) ) {
1308 $allowedGroups[] = $group;
1311 return $allowedGroups;
1315 * Check if all users may be assumed to have the given permission
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.
1324 * @param string $right Right to check
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];
1336 if ( !isset( $this->groupPermissions
['*'][$right] )
1337 ||
!$this->groupPermissions
['*'][$right] ) {
1338 $this->cachedRights
[$right] = false;
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;
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' ) ) {
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;
1362 // Allow extensions to say false
1363 if ( !Hooks
::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
1364 $this->cachedRights
[$right] = false;
1368 $this->cachedRights
[$right] = true;
1373 * Get a list of all available permissions.
1377 * @return string[] Array of permission names
1379 public function getAllPermissions() {
1380 if ( $this->allRights
=== false ) {
1381 if ( count( $this->availableRights
) ) {
1382 $this->allRights
= array_unique( array_merge(
1384 $this->availableRights
1387 $this->allRights
= $this->coreRights
;
1389 Hooks
::run( 'UserGetAllRights', [ &$this->allRights
] );
1391 return $this->allRights
;
1395 * Overrides user permissions cache
1400 * @param string[]|string $rights
1404 public function overrideUserRightsForTesting( $user, $rights = [] ) {
1405 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1406 throw new Exception( __METHOD__
. ' can not be called outside of tests' );
1408 $this->usersRights
[ $user->getId() ] = is_array( $rights ) ?
$rights : [ $rights ];