41use Wikimedia\ScopedCallback;
52 public const RIGOR_QUICK =
'quick';
55 public const RIGOR_FULL =
'full';
58 public const RIGOR_SECURE =
'secure';
64 public const CONSTRUCTOR_OPTIONS = [
66 'WhitelistReadRegexp',
72 'NamespaceProtection',
145 'editmyuserjsredirect',
164 'move-categorypages',
165 'move-rootuserpages',
169 'override-export-depth',
191 'userrights-interwiki',
220 $this->hookRunner =
new HookRunner( $hookContainer );
262 return $this->
userCan( $action, $user, $page, self::RIGOR_QUICK );
286 $rigor = self::RIGOR_SECURE,
292 foreach ( $errors as $index => $error ) {
293 $errKey = is_array( $error ) ? $error[0] : $error;
295 if ( in_array( $errKey, $ignoreErrors ) ) {
296 unset( $errors[$index] );
298 if ( $errKey instanceof
MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
299 unset( $errors[$index] );
317 $block = $user->
getBlock( $fromReplica );
323 $title = Title::newFromLinkTarget( $page );
330 $blocked = $block->appliesToUsertalk(
$title );
332 $blocked = $block->appliesToTitle(
$title );
340 $this->hookRunner->onUserIsBlockedFrom( $user,
$title, $blocked, $allowUsertalk );
366 $rigor = self::RIGOR_SECURE,
369 if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
370 throw new Exception(
"Invalid rigor parameter '$rigor'." );
373 # Read has special handling
374 if ( $action ==
'read' ) {
376 'checkPermissionHooks',
377 'checkReadPermissions',
380 # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
381 # or checkUserConfigPermissions here as it will lead to duplicate
382 # error messages. This is okay to do since anywhere that checks for
383 # create will also check for edit, and those checks are called for edit.
384 } elseif ( $action ==
'create' ) {
386 'checkQuickPermissions',
387 'checkPermissionHooks',
388 'checkPageRestrictions',
389 'checkCascadingSourcesRestrictions',
390 'checkActionPermissions',
395 'checkQuickPermissions',
396 'checkPermissionHooks',
397 'checkSpecialsAndNSPermissions',
398 'checkSiteConfigPermissions',
399 'checkUserConfigPermissions',
400 'checkPageRestrictions',
401 'checkCascadingSourcesRestrictions',
402 'checkActionPermissions',
408 foreach ( $checks as $method ) {
409 $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
411 if ( $short && $errors !== [] ) {
444 $title = Title::newFromLinkTarget( $page );
447 if ( !$this->hookRunner->onUserCan(
$title, $user, $action, $result ) ) {
448 return $result ? [] : [ [
'badaccess-group0' ] ];
451 if ( !$this->hookRunner->onGetUserPermissionsErrors(
$title, $user, $action, $result ) ) {
456 $rigor !== self::RIGOR_QUICK
457 && !( $short && count( $errors ) > 0 )
458 && !$this->hookRunner->onGetUserPermissionsErrorsExpensive(
459 $title, $user, $action, $result )
476 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
479 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
481 $errors = array_merge( $errors, $result );
482 } elseif ( $result !==
'' && is_string( $result ) ) {
484 $errors[] = [ $result ];
487 $errors[] = [ $result ];
488 } elseif ( $result ===
false ) {
490 $errors[] = [
'badaccess-group0' ];
520 $title = Title::newFromLinkTarget( $page );
522 $whiteListRead = $this->options->get(
'WhitelistRead' );
523 $whitelisted =
false;
525 # Shortcut for public wikis, allows skipping quite a bit of code
528 # If the user is allowed to read pages, he is allowed to read all pages
534 # Always grant access to the login page.
535 # Even anons need to be able to log in.
537 } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) {
538 # Time to check the whitelist
539 # Only do these checks is there's something to check against
540 $name =
$title->getPrefixedText();
541 $dbName =
$title->getPrefixedDBkey();
544 if ( in_array( $name, $whiteListRead,
true )
545 || in_array( $dbName, $whiteListRead,
true ) ) {
548 # Old settings might have the title prefixed with
549 # a colon for main-namespace pages
550 if ( in_array(
':' . $name, $whiteListRead ) ) {
553 } elseif (
$title->isSpecialPage() ) {
554 # If it's a special page, ditch the subpage bit and check again
555 $name =
$title->getDBkey();
557 $this->specialPageFactory->resolveAlias( $name );
560 if ( in_array( $pure, $whiteListRead,
true ) ) {
567 $whitelistReadRegexp = $this->options->get(
'WhitelistReadRegexp' );
568 if ( !$whitelisted && is_array( $whitelistReadRegexp )
569 && !empty( $whitelistReadRegexp ) ) {
570 $name =
$title->getPrefixedText();
572 foreach ( $whitelistReadRegexp as $listItem ) {
573 if ( preg_match( $listItem, $name ) ) {
580 if ( !$whitelisted ) {
581 # If the title is not whitelisted, give extensions a chance to do so...
582 $this->hookRunner->onTitleReadWhitelist(
$title, $user, $whitelisted );
583 if ( !$whitelisted ) {
602 return [
'badaccess-group0' ];
621 $this->specialPageFactory->resolveAlias( $page->
getDBkey() );
622 if ( $name == $thisName ) {
655 if ( $rigor === self::RIGOR_QUICK || in_array( $action, [
'createaccount',
'unblock' ] ) ) {
660 if ( $action ===
'read' && !$this->options->get(
'BlockDisablesLogin' ) ) {
664 if ( $this->options->get(
'EmailConfirmToEdit' )
666 && $action ===
'edit'
668 $errors[] = [
'confirmedittext' ];
671 $useReplica = ( $rigor !== self::RIGOR_SECURE );
672 $block = $user->
getBlock( $useReplica );
676 if ( !$block || $block->appliesToRight( $action ) === false ) {
694 $title = Title::newFromLinkTarget( $page,
'clone' );
695 $context = RequestContext::getMain();
698 Article::newFromTitle(
$title, $context ),
702 if ( $actionObj && $actionObj->getRestriction() !== $action ) {
709 if ( !$actionObj || $actionObj->requiresUnblock() ) {
712 $context = RequestContext::getMain();
713 $message = $this->blockErrorFormatter->getMessage(
716 $context->getLanguage(),
717 $context->getRequest()->getIP()
719 $errors[] = array_merge( [ $message->getKey() ], $message->getParams() );
751 $title = Title::newFromLinkTarget( $page );
753 if ( !$this->hookRunner->onTitleQuickPermissions(
$title, $user, $action,
754 $errors, $rigor !== self::RIGOR_QUICK, $short )
759 $isSubPage = $this->nsInfo->hasSubpages(
$title->getNamespace() ) ?
760 strpos(
$title->getText(),
'/' ) !== false :
false;
762 if ( $action ==
'create' ) {
764 ( $this->nsInfo->isTalk(
$title->getNamespace() ) &&
765 !$this->userHasRight( $user,
'createtalk' ) ) ||
766 ( !$this->nsInfo->isTalk(
$title->getNamespace() ) &&
767 !$this->userHasRight( $user,
'createpage' ) )
769 $errors[] = $user->
isAnon() ? [
'nocreatetext' ] : [
'nocreate-loggedin' ];
771 } elseif ( $action ==
'move' ) {
772 if ( !$this->
userHasRight( $user,
'move-rootuserpages' )
775 $errors[] = [
'cant-move-user-page' ];
780 !$this->userHasRight( $user,
'movefile' ) ) {
781 $errors[] = [
'movenotallowedfile' ];
786 !$this->userHasRight( $user,
'move-categorypages' ) ) {
787 $errors[] = [
'cant-move-category-page' ];
794 if ( $user->
isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
796 $errors[] = [
'movenologintext' ];
798 $errors[] = [
'movenotallowed' ];
801 } elseif ( $action ==
'move-target' ) {
804 $errors[] = [
'movenotallowed' ];
805 } elseif ( !$this->
userHasRight( $user,
'move-rootuserpages' )
810 $errors[] = [
'cant-move-to-user-page' ];
811 } elseif ( !$this->
userHasRight( $user,
'move-categorypages' )
815 $errors[] = [
'cant-move-to-category-page' ];
851 $title = Title::newFromLinkTarget( $page );
852 foreach (
$title->getRestrictions( $action ) as $right ) {
854 if ( $right ==
'sysop' ) {
855 $right =
'editprotected';
858 if ( $right ==
'autoconfirmed' ) {
859 $right =
'editsemiprotected';
861 if ( $right ==
'' ) {
865 $errors[] = [
'protectedpagetext', $right, $action ];
866 } elseif (
$title->areRestrictionsCascading() &&
867 !$this->userHasRight( $user,
'protect' )
869 $errors[] = [
'protectedpagetext',
'protect', $action ];
901 $title = Title::newFromLinkTarget( $page );
902 if ( $rigor !== self::RIGOR_QUICK && !
$title->isUserConfigPage() ) {
903 list( $cascadingSources, $restrictions ) =
$title->getCascadeProtectionSources();
904 # Cascading protection depends on more than this page...
905 # Several cascading protected pages may include this page...
906 # Check each cascading level
907 # This is only for protection restrictions, not for all actions
908 if ( isset( $restrictions[$action] ) ) {
909 foreach ( $restrictions[$action] as $right ) {
911 if ( $right ==
'sysop' ) {
912 $right =
'editprotected';
915 if ( $right ==
'autoconfirmed' ) {
916 $right =
'editsemiprotected';
918 if ( $right !=
'' && !$this->
userHasAllRights( $user,
'protect', $right ) ) {
921 foreach ( $cascadingSources as $wikiPage ) {
922 $wikiPages .=
'* [[:' . $wikiPage->getPrefixedText() .
"]]\n";
924 $errors[] = [
'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
960 $title = Title::newFromLinkTarget( $page );
962 if ( $action ==
'protect' ) {
965 $errors[] = [
'protect-cantedit' ];
967 } elseif ( $action ==
'create' ) {
968 $title_protection =
$title->getTitleProtection();
969 if ( $title_protection ) {
970 if ( $title_protection[
'permission'] ==
''
971 || !$this->
userHasRight( $user, $title_protection[
'permission'] )
977 $title_protection[
'reason']
981 } elseif ( $action ==
'move' ) {
983 if ( !$this->nsInfo->isMovable(
$title->getNamespace() ) ) {
985 $nsText =
$title->getNsText();
986 if ( $nsText ===
'' ) {
987 $nsText =
wfMessage(
'blanknamespace' )->text();
989 $errors[] = [
'immobile-source-namespace', $nsText ];
990 } elseif ( !
$title->isMovable() ) {
992 $errors[] = [
'immobile-source-page' ];
994 } elseif ( $action ==
'move-target' ) {
995 if ( !$this->nsInfo->isMovable(
$title->getNamespace() ) ) {
996 $nsText =
$title->getNsText();
997 if ( $nsText ===
'' ) {
998 $nsText =
wfMessage(
'blanknamespace' )->text();
1000 $errors[] = [
'immobile-target-namespace', $nsText ];
1001 } elseif ( !
$title->isMovable() ) {
1002 $errors[] = [
'immobile-target-page' ];
1004 } elseif ( $action ==
'delete' ) {
1006 if ( !$tempErrors ) {
1008 $user, $tempErrors, $rigor,
true,
$title );
1010 if ( $tempErrors ) {
1012 $errors[] = [
'deleteprotected' ];
1019 } elseif ( $action ===
'undelete' ) {
1022 $errors[] = [
'undelete-cantedit' ];
1028 $errors[] = [
'undelete-cantcreate' ];
1059 $title = Title::newFromLinkTarget( $page );
1061 # Only 'createaccount' can be performed on special pages,
1062 # which don't actually exist in the DB.
1063 if (
$title->getNamespace() ==
NS_SPECIAL && $action !==
'createaccount' ) {
1064 $errors[] = [
'ns-specialprotected' ];
1067 # Check $wgNamespaceProtection for restricted namespaces
1072 [
'protectedinterface', $action ] : [
'namespaceprotected', $ns, $action ];
1103 $title = Title::newFromLinkTarget( $page );
1105 if ( $action !=
'patrol' ) {
1109 if (
$title->isSiteCssConfigPage() && !$this->userHasRight( $user,
'editsitecss' ) ) {
1110 $error = [
'sitecssprotected', $action ];
1111 } elseif (
$title->isSiteJsonConfigPage() && !$this->userHasRight( $user,
'editsitejson' ) ) {
1112 $error = [
'sitejsonprotected', $action ];
1113 } elseif (
$title->isSiteJsConfigPage() && !$this->userHasRight( $user,
'editsitejs' ) ) {
1114 $error = [
'sitejsprotected', $action ];
1116 if (
$title->isRawHtmlMessage() && !$this->userCanEditRawHtmlPage( $user ) ) {
1117 $error = [
'siterawhtmlprotected', $action ];
1125 $error = [
'interfaceadmin-info',
wfMessage( $error[0], $error[1] ) ];
1159 $title = Title::newFromLinkTarget( $page );
1161 # Protect css/json/js subpages of user pages
1162 # XXX: this might be better using restrictions
1164 if ( $action ===
'patrol' ) {
1168 if ( preg_match(
'/^' . preg_quote( $user->
getName(),
'/' ) .
'\//',
$title->getText() ) ) {
1171 $title->isUserCssConfigPage()
1172 && !$this->userHasAnyRight( $user,
'editmyusercss',
'editusercss' )
1174 $errors[] = [
'mycustomcssprotected', $action ];
1176 $title->isUserJsonConfigPage()
1177 && !$this->userHasAnyRight( $user,
'editmyuserjson',
'edituserjson' )
1179 $errors[] = [
'mycustomjsonprotected', $action ];
1181 $title->isUserJsConfigPage()
1182 && !$this->userHasAnyRight( $user,
'editmyuserjs',
'edituserjs' )
1184 $errors[] = [
'mycustomjsprotected', $action ];
1186 $title->isUserJsConfigPage()
1187 && !$this->userHasAnyRight( $user,
'edituserjs',
'editmyuserjsredirect' )
1190 $rev = $this->revisionLookup->getRevisionByTitle(
$title );
1191 $content = $rev ? $rev->getContent(
'main', RevisionRecord::RAW ) :
null;
1194 !$target->inNamespace(
NS_USER )
1195 || !preg_match(
'/^' . preg_quote( $user->
getName(),
'/' ) .
'\//', $target->getText() )
1197 $errors[] = [
'mycustomjsredirectprotected', $action ];
1205 if ( !in_array( $action, [
'delete',
'deleterevision',
'suppressrevision' ],
true ) ) {
1207 $title->isUserCssConfigPage()
1208 && !$this->userHasRight( $user,
'editusercss' )
1210 $errors[] = [
'customcssprotected', $action ];
1212 $title->isUserJsonConfigPage()
1213 && !$this->userHasRight( $user,
'edituserjson' )
1215 $errors[] = [
'customjsonprotected', $action ];
1217 $title->isUserJsConfigPage()
1218 && !$this->userHasRight( $user,
'edituserjs' )
1220 $errors[] = [
'customjsprotected', $action ];
1239 if ( $action ===
'' ) {
1256 foreach ( $actions as $action ) {
1273 foreach ( $actions as $action ) {
1293 if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) {
1295 $user->getEffectiveGroups()
1297 $this->hookRunner->onUserGetRights( $user, $this->usersRights[ $rightsCacheKey ] );
1301 if ( !defined(
'MW_NO_SESSION' ) ) {
1303 $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
1304 if ( $allowedRights !==
null ) {
1305 $this->usersRights[ $rightsCacheKey ] = array_intersect(
1306 $this->usersRights[ $rightsCacheKey ],
1312 $this->hookRunner->onUserGetRightsRemove(
1313 $user, $this->usersRights[ $rightsCacheKey ] );
1315 $this->usersRights[ $rightsCacheKey ] = array_values(
1316 array_unique( $this->usersRights[ $rightsCacheKey ] )
1320 $user->isLoggedIn() &&
1321 $this->options->get(
'BlockDisablesLogin' ) &&
1325 $this->usersRights[ $rightsCacheKey ] = array_intersect(
1326 $this->usersRights[ $rightsCacheKey ],
1331 $rights = $this->usersRights[ $rightsCacheKey ];
1332 foreach ( $this->temporaryUserRights[ $user->
getId() ] ?? [] as $overrides ) {
1333 $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
1347 if ( $user !==
null ) {
1349 if ( isset( $this->usersRights[ $rightsCacheKey ] ) ) {
1350 unset( $this->usersRights[ $rightsCacheKey ] );
1353 $this->usersRights =
null;
1363 return $user->
isRegistered() ?
"u:{$user->getId()}" :
"anon:{$user->getName()}";
1381 $groupPermissions = $this->options->get(
'GroupPermissions' );
1382 $revokePermissions = $this->options->get(
'RevokePermissions' );
1383 return isset( $groupPermissions[$group][$role] ) && $groupPermissions[$group][$role] &&
1384 !( isset( $revokePermissions[$group][$role] ) && $revokePermissions[$group][$role] );
1398 foreach ( $groups as $group ) {
1399 if ( isset( $this->options->get(
'GroupPermissions' )[$group] ) ) {
1400 $rights = array_merge( $rights,
1402 array_keys( array_filter( $this->options->get(
'GroupPermissions' )[$group] ) ) );
1406 foreach ( $groups as $group ) {
1407 if ( isset( $this->options->get(
'RevokePermissions' )[$group] ) ) {
1408 $rights = array_diff( $rights,
1409 array_keys( array_filter( $this->options->get(
'RevokePermissions' )[$group] ) ) );
1412 return array_unique( $rights );
1424 $allowedGroups = [];
1425 foreach ( array_keys( $this->options->get(
'GroupPermissions' ) ) as $group ) {
1427 $allowedGroups[] = $group;
1430 return $allowedGroups;
1451 if ( isset( $this->cachedRights[$right] ) ) {
1452 return $this->cachedRights[$right];
1455 if ( !isset( $this->options->get(
'GroupPermissions' )[
'*'][$right] )
1456 || !$this->options->get(
'GroupPermissions' )[
'*'][$right] ) {
1457 $this->cachedRights[$right] =
false;
1462 foreach ( $this->options->get(
'RevokePermissions' ) as $rights ) {
1463 if ( isset( $rights[$right] ) && $rights[$right] ) {
1464 $this->cachedRights[$right] =
false;
1471 if ( !defined(
'MW_NO_SESSION' ) ) {
1474 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
1475 if ( $allowedRights !==
null && !in_array( $right, $allowedRights,
true ) ) {
1476 $this->cachedRights[$right] =
false;
1482 if ( !$this->hookRunner->onUserIsEveryoneAllowed( $right ) ) {
1483 $this->cachedRights[$right] =
false;
1487 $this->cachedRights[$right] =
true;
1499 if ( $this->allRights ===
null ) {
1500 if ( count( $this->options->get(
'AvailableRights' ) ) ) {
1501 $this->allRights = array_unique( array_merge(
1503 $this->options->get(
'AvailableRights' )
1508 $this->hookRunner->onUserGetAllRights( $this->allRights );
1520 $namespaceProtection = $this->options->get(
'NamespaceProtection' );
1521 if ( isset( $namespaceProtection[$index] ) ) {
1522 return !$this->
userHasAllRights( $user, ...(array)$namespaceProtection[$index] );
1536 if ( !isset( $this->options->get(
'NamespaceProtection' )[$index] ) ) {
1539 $levels = $this->options->get(
'RestrictionLevels' );
1541 $levels = array_values( array_filter( $levels,
function ( $level ) use ( $user ) {
1543 if ( $right ==
'sysop' ) {
1544 $right =
'editprotected';
1546 if ( $right ==
'autoconfirmed' ) {
1547 $right =
'editsemiprotected';
1563 $namespaceRightGroups = [];
1564 foreach ( (array)$this->options->get(
'NamespaceProtection' )[$index] as $right ) {
1565 if ( $right ==
'sysop' ) {
1566 $right =
'editprotected';
1568 if ( $right ==
'autoconfirmed' ) {
1569 $right =
'editsemiprotected';
1571 if ( $right !=
'' ) {
1577 $usableLevels = [
'' ];
1578 foreach ( $this->options->get(
'RestrictionLevels' ) as $level ) {
1580 if ( $right ==
'sysop' ) {
1581 $right =
'editprotected';
1583 if ( $right ==
'autoconfirmed' ) {
1584 $right =
'editsemiprotected';
1587 if ( $right !=
'' &&
1588 !isset( $namespaceRightGroups[$right] ) &&
1592 foreach ( $namespaceRightGroups as $groups ) {
1599 $usableLevels[] = $level;
1603 return $usableLevels;
1633 $userId = $user->
getId();
1634 $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
1635 $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
1636 return new ScopedCallback(
function () use ( $userId, $nextKey ) {
1637 unset( $this->temporaryUserRights[$userId][$nextKey] );
1652 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
1653 throw new Exception( __METHOD__ .
' can not be called outside of tests' );
1656 is_array( $rights ) ? $rights : [ $rights ];
$wgDeleteRevisionsLimit
Optional to restrict deletion of pages with higher revision counts to users with the 'bigdelete' perm...
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).
static factory(?string $action, Page $article, IContextSource $context=null)
Get an appropriate Action subclass for the given action.
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
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,...
isAllowUsertalk()
Checks if usertalk is allowed.
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 whoIs( $id)
Get the username corresponding to a given user ID.
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.
getBlock( $fromReplica=true, $disableIpBlockExemptChecking=false)
Get the block affecting the user, or null if the user is not blocked.
isAnon()
Get whether the user is anonymous.