45use Wikimedia\ScopedCallback;
56 public const RIGOR_QUICK =
'quick';
59 public const RIGOR_FULL =
'full';
62 public const RIGOR_SECURE =
'secure';
69 'WhitelistReadRegexp',
72 'EnablePartialActionBlocks',
76 'NamespaceProtection',
78 'DeleteRevisionsLimit',
160 'editmyuserjsredirect',
179 'move-categorypages',
180 'move-rootuserpages',
184 'override-export-depth',
206 'userrights-interwiki',
243 $this->hookRunner =
new HookRunner( $hookContainer );
284 return $this->
userCan( $action, $user, $page, self::RIGOR_QUICK );
308 $rigor = self::RIGOR_SECURE,
314 foreach ( $errors as $index => $error ) {
315 $errKey = is_array( $error ) ? $error[0] : $error;
317 if ( in_array( $errKey, $ignoreErrors ) ) {
318 unset( $errors[$index] );
320 if ( $errKey instanceof
MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
321 unset( $errors[$index] );
339 $block = $user->
getBlock( $fromReplica );
345 $title = Title::castFromPageIdentity( $page );
347 $title = Title::castFromLinkTarget( $page );
355 $blocked = $block->appliesToUsertalk(
$title );
357 $blocked = $block->appliesToTitle(
$title );
362 $allowUsertalk = $block->isUsertalkEditAllowed();
365 $this->hookRunner->onUserIsBlockedFrom( $user,
$title, $blocked, $allowUsertalk );
391 $rigor = self::RIGOR_SECURE,
394 if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
395 throw new Exception(
"Invalid rigor parameter '$rigor'." );
398 # Read has special handling
399 if ( $action ==
'read' ) {
401 'checkPermissionHooks',
402 'checkReadPermissions',
405 # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
406 # or checkUserConfigPermissions here as it will lead to duplicate
407 # error messages. This is okay to do since anywhere that checks for
408 # create will also check for edit, and those checks are called for edit.
409 } elseif ( $action ==
'create' ) {
411 'checkQuickPermissions',
412 'checkPermissionHooks',
413 'checkPageRestrictions',
414 'checkCascadingSourcesRestrictions',
415 'checkActionPermissions',
420 'checkQuickPermissions',
421 'checkPermissionHooks',
422 'checkSpecialsAndNSPermissions',
423 'checkSiteConfigPermissions',
424 'checkUserConfigPermissions',
425 'checkPageRestrictions',
426 'checkCascadingSourcesRestrictions',
427 'checkActionPermissions',
433 $skipUserConfigActions = [
452 if ( in_array( $action, $skipUserConfigActions,
true ) ) {
453 $checks = array_diff(
455 [
'checkUserConfigPermissions' ]
458 $checks = array_values( $checks );
463 foreach ( $checks as $method ) {
464 $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
466 if ( $short && $errors !== [] ) {
471 $errors = array_unique( $errors, SORT_REGULAR );
501 $title = Title::newFromLinkTarget( $page );
504 if ( !$this->hookRunner->onUserCan(
$title, $user, $action, $result ) ) {
505 return $result ? [] : [ [
'badaccess-group0' ] ];
508 if ( !$this->hookRunner->onGetUserPermissionsErrors(
$title, $user, $action, $result ) ) {
513 $rigor !== self::RIGOR_QUICK
514 && !( $short && count( $errors ) > 0 )
515 && !$this->hookRunner->onGetUserPermissionsErrorsExpensive(
516 $title, $user, $action, $result )
533 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
536 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
538 $errors = array_merge( $errors, $result );
539 } elseif ( $result !==
'' && is_string( $result ) ) {
541 $errors[] = [ $result ];
544 $errors[] = [ $result ];
545 } elseif ( $result ===
false ) {
547 $errors[] = [
'badaccess-group0' ];
577 $title = Title::newFromLinkTarget( $page );
579 $whiteListRead = $this->options->get(
'WhitelistRead' );
582 # Shortcut for public wikis, allows skipping quite a bit of code
585 # If the user is allowed to read pages, he is allowed to read all pages
591 # Always grant access to the login page.
592 # Even anons need to be able to log in.
595 # relies on HMAC key signature alone
597 } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) {
598 # Time to check the whitelist
599 # Only do these checks is there's something to check against
600 $name =
$title->getPrefixedText();
601 $dbName =
$title->getPrefixedDBkey();
604 if ( in_array( $name, $whiteListRead,
true )
605 || in_array( $dbName, $whiteListRead,
true )
609 # Old settings might have the title prefixed with
610 # a colon for main-namespace pages
611 if ( in_array(
':' . $name, $whiteListRead ) ) {
614 } elseif (
$title->isSpecialPage() ) {
615 # If it's a special page, ditch the subpage bit and check again
616 $name =
$title->getDBkey();
618 $this->specialPageFactory->resolveAlias( $name );
621 if ( in_array( $pure, $whiteListRead,
true ) ) {
628 $whitelistReadRegexp = $this->options->get(
'WhitelistReadRegexp' );
629 if ( !$allowed && is_array( $whitelistReadRegexp )
630 && !empty( $whitelistReadRegexp )
632 $name =
$title->getPrefixedText();
634 foreach ( $whitelistReadRegexp as $listItem ) {
635 if ( preg_match( $listItem, $name ) ) {
643 # If the title is not whitelisted, give extensions a chance to do so...
644 $this->hookRunner->onTitleReadWhitelist(
$title, $user, $allowed );
664 return [
'badaccess-group0' ];
683 $this->specialPageFactory->resolveAlias( $page->
getDBkey() );
684 if ( $name == $thisName ) {
716 if ( $rigor === self::RIGOR_QUICK || in_array( $action, [
'unblock' ] ) ) {
721 if ( $action ===
'read' && !$this->options->get(
'BlockDisablesLogin' ) ) {
725 if ( $this->options->get(
'EmailConfirmToEdit' )
727 && $action ===
'edit'
729 $errors[] = [
'confirmedittext' ];
733 case self::RIGOR_SECURE:
734 $blockInfoFreshness = Authority::READ_LATEST;
737 case self::RIGOR_FULL:
738 $blockInfoFreshness = Authority::READ_NORMAL;
743 $blockInfoFreshness = Authority::READ_NORMAL;
746 $block = $user->
getBlock( $blockInfoFreshness );
748 if ( $action ===
'createaccount' ) {
749 $applicableBlock =
null;
750 if ( $block && $block->appliesToRight(
'createaccount' ) ) {
751 $applicableBlock = $block;
754 # T15611: if the IP address the user is trying to create an account from is
755 # blocked with createaccount disabled, prevent new account creation there even
756 # when the user is logged in
757 if ( !$this->
userHasRight( $user,
'ipblock-exempt' ) ) {
758 $ipBlock = DatabaseBlock::newFromTarget(
761 if ( $ipBlock && $ipBlock->appliesToRight(
'createaccount' ) ) {
762 $applicableBlock = $ipBlock;
766 if ( $applicableBlock ) {
767 $context = RequestContext::getMain();
768 $message = $this->blockErrorFormatter->getMessage(
771 $context->getLanguage(),
772 $context->getRequest()->getIP()
774 $errors[] = array_merge( [ $message->getKey() ], $message->getParams() );
781 if ( !$block || $block->appliesToRight( $action ) ===
false ) {
791 if ( Action::exists( $action ) ) {
799 $title = Title::newFromLinkTarget( $page,
'clone' );
800 $context = RequestContext::getMain();
801 $actionObj = Action::factory(
803 Article::newFromTitle(
$title, $context ),
807 if ( $actionObj && $actionObj->getRestriction() !== $action ) {
814 if ( !$actionObj || $actionObj->requiresUnblock() ) {
818 $this->options->get(
'EnablePartialActionBlocks' ) &&
819 $block->appliesToRight( $action )
823 $context = RequestContext::getMain();
824 $message = $this->blockErrorFormatter->getMessage(
827 $context->getLanguage(),
828 $context->getRequest()->getIP()
830 $errors[] = array_merge( [ $message->getKey() ], $message->getParams() );
862 $title = Title::newFromLinkTarget( $page );
864 if ( !$this->hookRunner->onTitleQuickPermissions(
$title, $user, $action,
865 $errors, $rigor !== self::RIGOR_QUICK, $short )
870 $isSubPage = $this->nsInfo->hasSubpages(
$title->getNamespace() ) ?
871 strpos(
$title->getText(),
'/' ) !== false :
false;
873 if ( $action ==
'create' ) {
875 ( $this->nsInfo->isTalk(
$title->getNamespace() ) &&
876 !$this->userHasRight( $user,
'createtalk' ) ) ||
877 ( !$this->nsInfo->isTalk(
$title->getNamespace() ) &&
878 !$this->userHasRight( $user,
'createpage' ) )
880 $errors[] = $user->
isAnon() ? [
'nocreatetext' ] : [
'nocreate-loggedin' ];
882 } elseif ( $action ==
'move' ) {
883 if ( !$this->
userHasRight( $user,
'move-rootuserpages' )
887 $errors[] = [
'cant-move-user-page' ];
892 !$this->userHasRight( $user,
'movefile' )
894 $errors[] = [
'movenotallowedfile' ];
899 !$this->userHasRight( $user,
'move-categorypages' )
901 $errors[] = [
'cant-move-category-page' ];
908 if ( $user->
isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
910 $errors[] = [
'movenologintext' ];
912 $errors[] = [
'movenotallowed' ];
915 } elseif ( $action ==
'move-target' ) {
918 $errors[] = [
'movenotallowed' ];
919 } elseif ( !$this->
userHasRight( $user,
'move-rootuserpages' )
924 $errors[] = [
'cant-move-to-user-page' ];
925 } elseif ( !$this->
userHasRight( $user,
'move-categorypages' )
929 $errors[] = [
'cant-move-to-category-page' ];
965 $title = Title::newFromLinkTarget( $page );
966 foreach (
$title->getRestrictions( $action ) as $right ) {
968 if ( $right ==
'sysop' ) {
969 $right =
'editprotected';
972 if ( $right ==
'autoconfirmed' ) {
973 $right =
'editsemiprotected';
975 if ( $right ==
'' ) {
979 $errors[] = [
'protectedpagetext', $right, $action ];
980 } elseif (
$title->areRestrictionsCascading() &&
981 !$this->userHasRight( $user,
'protect' )
983 $errors[] = [
'protectedpagetext',
'protect', $action ];
1015 $title = Title::newFromLinkTarget( $page );
1016 if ( $rigor !== self::RIGOR_QUICK && !
$title->isUserConfigPage() ) {
1017 list( $cascadingSources, $restrictions ) =
$title->getCascadeProtectionSources();
1018 # Cascading protection depends on more than this page...
1019 # Several cascading protected pages may include this page...
1020 # Check each cascading level
1021 # This is only for protection restrictions, not for all actions
1022 if ( isset( $restrictions[$action] ) ) {
1023 foreach ( $restrictions[$action] as $right ) {
1025 if ( $right ==
'sysop' ) {
1026 $right =
'editprotected';
1029 if ( $right ==
'autoconfirmed' ) {
1030 $right =
'editsemiprotected';
1032 if ( $right !=
'' && !$this->
userHasAllRights( $user,
'protect', $right ) ) {
1035 foreach ( $cascadingSources as $wikiPage ) {
1036 $wikiPages .=
'* [[:' . $wikiPage->getPrefixedText() .
"]]\n";
1038 $errors[] = [
'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
1074 $title = Title::newFromLinkTarget( $page );
1076 if ( $action ==
'protect' ) {
1079 $errors[] = [
'protect-cantedit' ];
1081 } elseif ( $action ==
'create' ) {
1082 $title_protection =
$title->getTitleProtection();
1083 if ( $title_protection ) {
1084 if ( $title_protection[
'permission'] ==
''
1085 || !$this->
userHasRight( $user, $title_protection[
'permission'] )
1089 $this->userCache->getProp( $title_protection[
'user'],
'name' ),
1090 $title_protection[
'reason']
1094 } elseif ( $action ==
'move' ) {
1096 if ( !$this->nsInfo->isMovable(
$title->getNamespace() ) ) {
1098 $nsText =
$title->getNsText();
1099 if ( $nsText ===
'' ) {
1100 $nsText =
wfMessage(
'blanknamespace' )->text();
1102 $errors[] = [
'immobile-source-namespace', $nsText ];
1103 } elseif ( !
$title->isMovable() ) {
1105 $errors[] = [
'immobile-source-page' ];
1107 } elseif ( $action ==
'move-target' ) {
1108 if ( !$this->nsInfo->isMovable(
$title->getNamespace() ) ) {
1109 $nsText =
$title->getNsText();
1110 if ( $nsText ===
'' ) {
1111 $nsText =
wfMessage(
'blanknamespace' )->text();
1113 $errors[] = [
'immobile-target-namespace', $nsText ];
1114 } elseif ( !
$title->isMovable() ) {
1115 $errors[] = [
'immobile-target-page' ];
1117 } elseif ( $action ==
'delete' || $action ==
'delete-redirect' ) {
1119 if ( !$tempErrors ) {
1121 $user, $tempErrors, $rigor,
true,
$title );
1123 if ( $tempErrors ) {
1125 $errors[] = [
'deleteprotected' ];
1127 if ( $rigor !== self::RIGOR_QUICK
1128 && $action ==
'delete'
1129 && $this->options->get(
'DeleteRevisionsLimit' )
1130 && !$this->userCan(
'bigdelete', $user,
$title )
1131 &&
$title->isBigDeletion()
1136 $wgLang->formatNum( $this->options->get(
'DeleteRevisionsLimit' ) )
1139 } elseif ( $action ===
'undelete' ) {
1142 $errors[] = [
'undelete-cantedit' ];
1148 $errors[] = [
'undelete-cantcreate' ];
1150 } elseif ( $action ===
'edit' ) {
1151 if ( !
$title->exists() ) {
1152 $errors = array_merge(
1192 $title = Title::newFromLinkTarget( $page );
1194 # Only 'createaccount' can be performed on special pages,
1195 # which don't actually exist in the DB.
1196 if (
$title->getNamespace() ===
NS_SPECIAL && $action !==
'createaccount' ) {
1197 $errors[] = [
'ns-specialprotected' ];
1200 # Check $wgNamespaceProtection for restricted namespaces
1205 [
'protectedinterface', $action ] : [
'namespaceprotected', $ns, $action ];
1236 $title = Title::newFromLinkTarget( $page );
1238 if ( $action ===
'patrol' ) {
1242 if ( in_array( $action, [
'deletedhistory',
'deletedtext',
'viewsuppressed' ],
true ) ) {
1253 if (
$title->isSiteCssConfigPage() && !$this->userHasRight( $user,
'editsitecss' ) ) {
1254 $errors[] = [
'sitecssprotected', $action ];
1255 } elseif (
$title->isSiteJsonConfigPage() && !$this->userHasRight( $user,
'editsitejson' ) ) {
1256 $errors[] = [
'sitejsonprotected', $action ];
1257 } elseif (
$title->isSiteJsConfigPage() && !$this->userHasRight( $user,
'editsitejs' ) ) {
1258 $errors[] = [
'sitejsprotected', $action ];
1260 if (
$title->isRawHtmlMessage() && !$this->userCanEditRawHtmlPage( $user ) ) {
1261 $errors[] = [
'siterawhtmlprotected', $action ];
1292 $title = Title::newFromLinkTarget( $page );
1294 # Protect css/json/js subpages of user pages
1295 # XXX: this might be better using restrictions
1296 if ( preg_match(
'/^' . preg_quote( $user->
getName(),
'/' ) .
'\//',
$title->getText() ) ) {
1299 $title->isUserCssConfigPage()
1300 && !$this->userHasAnyRight( $user,
'editmyusercss',
'editusercss' )
1302 $errors[] = [
'mycustomcssprotected', $action ];
1304 $title->isUserJsonConfigPage()
1305 && !$this->userHasAnyRight( $user,
'editmyuserjson',
'edituserjson' )
1307 $errors[] = [
'mycustomjsonprotected', $action ];
1309 $title->isUserJsConfigPage()
1310 && !$this->userHasAnyRight( $user,
'editmyuserjs',
'edituserjs' )
1312 $errors[] = [
'mycustomjsprotected', $action ];
1314 $title->isUserJsConfigPage()
1315 && !$this->userHasAnyRight( $user,
'edituserjs',
'editmyuserjsredirect' )
1318 $rev = $this->revisionLookup->getRevisionByTitle(
$title );
1319 $content = $rev ? $rev->getContent(
'main', RevisionRecord::RAW ) :
null;
1322 !$target->inNamespace(
NS_USER )
1323 || !preg_match(
'/^' . preg_quote( $user->
getName(),
'/' ) .
'\//', $target->getText() )
1325 $errors[] = [
'mycustomjsredirectprotected', $action ];
1336 $title->isUserCssConfigPage()
1337 && !$this->userHasRight( $user,
'editusercss' )
1339 $errors[] = [
'customcssprotected', $action ];
1341 $title->isUserJsonConfigPage()
1342 && !$this->userHasRight( $user,
'edituserjson' )
1344 $errors[] = [
'customjsonprotected', $action ];
1346 $title->isUserJsConfigPage()
1347 && !$this->userHasRight( $user,
'edituserjs' )
1349 $errors[] = [
'customjsprotected', $action ];
1367 if ( $action ===
'' ) {
1384 foreach ( $actions as $action ) {
1401 foreach ( $actions as $action ) {
1420 if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) {
1423 $this->userGroupManager->getUserEffectiveGroups( $user )
1426 $this->hookRunner->onUserGetRights( $userObj, $this->usersRights[ $rightsCacheKey ] );
1430 if ( !defined(
'MW_NO_SESSION' ) ) {
1432 $allowedRights = $userObj->getRequest()->getSession()->getAllowedUserRights();
1433 if ( $allowedRights !==
null ) {
1434 $this->usersRights[ $rightsCacheKey ] = array_intersect(
1435 $this->usersRights[ $rightsCacheKey ],
1442 $this->hookRunner->onUserGetRightsRemove(
1443 $userObj, $this->usersRights[ $rightsCacheKey ] );
1445 $this->usersRights[ $rightsCacheKey ] = array_values(
1446 array_unique( $this->usersRights[ $rightsCacheKey ] )
1450 $userObj->isRegistered() &&
1451 $this->options->get(
'BlockDisablesLogin' ) &&
1452 $userObj->getBlock()
1455 $this->usersRights[ $rightsCacheKey ] = array_intersect(
1456 $this->usersRights[ $rightsCacheKey ],
1461 $rights = $this->usersRights[ $rightsCacheKey ];
1462 foreach ( $this->temporaryUserRights[ $user->
getId() ] ?? [] as $overrides ) {
1463 $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
1477 if ( $user !==
null ) {
1479 unset( $this->usersRights[ $rightsCacheKey ] );
1481 $this->usersRights =
null;
1491 return $user->
isRegistered() ?
"u:{$user->getId()}" :
"anon:{$user->getName()}";
1510 return $this->groupPermissionsLookup->groupHasPermission( $group, $role );
1523 return $this->groupPermissionsLookup->getGroupPermissions( $groups );
1536 return $this->groupPermissionsLookup->getGroupsWithPermission( $role );
1557 if ( isset( $this->cachedRights[$right] ) ) {
1558 return $this->cachedRights[$right];
1561 if ( !isset( $this->options->get(
'GroupPermissions' )[
'*'][$right] )
1562 || !$this->options->get(
'GroupPermissions' )[
'*'][$right]
1564 $this->cachedRights[$right] =
false;
1569 foreach ( $this->options->get(
'RevokePermissions' ) as $rights ) {
1570 if ( isset( $rights[$right] ) && $rights[$right] ) {
1571 $this->cachedRights[$right] =
false;
1578 if ( !defined(
'MW_NO_SESSION' ) ) {
1581 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
1582 if ( $allowedRights !==
null && !in_array( $right, $allowedRights,
true ) ) {
1583 $this->cachedRights[$right] =
false;
1589 if ( !$this->hookRunner->onUserIsEveryoneAllowed( $right ) ) {
1590 $this->cachedRights[$right] =
false;
1594 $this->cachedRights[$right] =
true;
1606 if ( $this->allRights ===
null ) {
1607 if ( count( $this->options->get(
'AvailableRights' ) ) ) {
1608 $this->allRights = array_unique( array_merge(
1610 $this->options->get(
'AvailableRights' )
1615 $this->hookRunner->onUserGetAllRights( $this->allRights );
1627 $namespaceProtection = $this->options->get(
'NamespaceProtection' );
1628 if ( isset( $namespaceProtection[$index] ) ) {
1629 return !$this->
userHasAllRights( $user, ...(array)$namespaceProtection[$index] );
1643 if ( !isset( $this->options->get(
'NamespaceProtection' )[$index] ) ) {
1646 $levels = $this->options->get(
'RestrictionLevels' );
1648 $levels = array_values( array_filter( $levels,
function ( $level ) use ( $user ) {
1650 if ( $right ==
'sysop' ) {
1651 $right =
'editprotected';
1653 if ( $right ==
'autoconfirmed' ) {
1654 $right =
'editsemiprotected';
1670 $namespaceRightGroups = [];
1671 foreach ( (array)$this->options->get(
'NamespaceProtection' )[$index] as $right ) {
1672 if ( $right ==
'sysop' ) {
1673 $right =
'editprotected';
1675 if ( $right ==
'autoconfirmed' ) {
1676 $right =
'editsemiprotected';
1678 if ( $right !=
'' ) {
1684 $usableLevels = [
'' ];
1685 foreach ( $this->options->get(
'RestrictionLevels' ) as $level ) {
1687 if ( $right ==
'sysop' ) {
1688 $right =
'editprotected';
1690 if ( $right ==
'autoconfirmed' ) {
1691 $right =
'editsemiprotected';
1694 if ( $right !=
'' &&
1695 !isset( $namespaceRightGroups[$right] ) &&
1699 foreach ( $namespaceRightGroups as $groups ) {
1706 $usableLevels[] = $level;
1710 return $usableLevels;
1740 $userId = $user->
getId();
1741 $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
1742 $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
1743 return new ScopedCallback(
function () use ( $userId, $nextKey ) {
1744 unset( $this->temporaryUserRights[$userId][$nextKey] );
1759 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
1760 throw new Exception( __METHOD__ .
' can not be called outside of tests' );
1763 is_array( $rights ) ? $rights : [ $rights ];
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Actions are things which can be done to pages (edit, delete, rollback, etc).
Class for viewing MediaWiki article and history.
Factory for handling the special page list and generating SpecialPage objects.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Group all the pieces relevant to the context of a request into one instance @newable.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Represents a title within MediaWiki.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
getBlock( $freshness=self::READ_NORMAL, $disableIpBlockExemptChecking=false)
Get the block affecting the user, or null if the user is not blocked.
getRequest()
Get the WebRequest object to use with this object.
isEmailConfirmed()
Is this user's e-mail address valid-looking and confirmed within limits of the current site configura...
isHidden()
Check if user account is hidden.
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
getTalkPage()
Get this user's talk page title.
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
isAnon()
Get whether the user is anonymous.
Interface for objects (potentially) representing an editable wiki page.