38 use Wikimedia\ScopedCallback;
50 const RIGOR_QUICK =
'quick';
53 const RIGOR_FULL =
'full';
56 const RIGOR_SECURE =
'secure';
62 public const CONSTRUCTOR_OPTIONS = [
64 'WhitelistReadRegexp',
70 'NamespaceProtection',
137 'editmyuserjsredirect',
156 'move-categorypages',
157 'move-rootuserpages',
161 'override-export-depth',
183 'userrights-interwiki',
248 return $this->
userCan(
$action, $user, $page, self::RIGOR_QUICK );
272 $rigor = self::RIGOR_SECURE,
278 foreach ( $errors as $index => $error ) {
279 $errKey = is_array( $error ) ? $error[0] : $error;
281 if ( in_array( $errKey, $ignoreErrors ) ) {
282 unset( $errors[$index] );
284 if ( $errKey instanceof
MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
285 unset( $errors[$index] );
303 $block = $user->
getBlock( $fromReplica );
316 $blocked = $block->appliesToUsertalk(
$title );
318 $blocked = $block->appliesToTitle(
$title );
326 Hooks::run(
'UserIsBlockedFrom', [ $user,
$title, &$blocked, &$allowUsertalk ] );
352 $rigor = self::RIGOR_SECURE,
355 if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
356 throw new Exception(
"Invalid rigor parameter '$rigor'." );
359 # Read has special handling
362 'checkPermissionHooks',
363 'checkReadPermissions',
366 # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
367 # or checkUserConfigPermissions here as it will lead to duplicate
368 # error messages. This is okay to do since anywhere that checks for
369 # create will also check for edit, and those checks are called for edit.
370 } elseif (
$action ==
'create' ) {
372 'checkQuickPermissions',
373 'checkPermissionHooks',
374 'checkPageRestrictions',
375 'checkCascadingSourcesRestrictions',
376 'checkActionPermissions',
381 'checkQuickPermissions',
382 'checkPermissionHooks',
383 'checkSpecialsAndNSPermissions',
384 'checkSiteConfigPermissions',
385 'checkUserConfigPermissions',
386 'checkPageRestrictions',
387 'checkCascadingSourcesRestrictions',
388 'checkActionPermissions',
394 foreach ( $checks as $method ) {
395 $errors = $this->$method(
$action, $user, $errors, $rigor, $short, $page );
397 if ( $short && $errors !== [] ) {
434 return $result ? [] : [ [
'badaccess-group0' ] ];
442 $rigor !== self::RIGOR_QUICK
443 && !( $short && count( $errors ) > 0 )
461 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
464 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
466 $errors = array_merge( $errors, $result );
467 } elseif ( $result !==
'' && is_string( $result ) ) {
469 $errors[] = [ $result ];
472 $errors[] = [ $result ];
473 } elseif ( $result ===
false ) {
475 $errors[] = [
'badaccess-group0' ];
507 $whiteListRead = $this->options->get(
'WhitelistRead' );
508 $whitelisted =
false;
510 # Shortcut for public wikis, allows skipping quite a bit of code
513 # If the user is allowed to read pages, he is allowed to read all pages
519 # Always grant access to the login page.
520 # Even anons need to be able to log in.
522 } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) {
523 # Time to check the whitelist
524 # Only do these checks is there's something to check against
525 $name =
$title->getPrefixedText();
526 $dbName =
$title->getPrefixedDBkey();
529 if ( in_array( $name, $whiteListRead,
true )
530 || in_array( $dbName, $whiteListRead,
true ) ) {
533 # Old settings might have the title prefixed with
534 # a colon for main-namespace pages
535 if ( in_array(
':' . $name, $whiteListRead ) ) {
538 } elseif (
$title->isSpecialPage() ) {
539 # If it's a special page, ditch the subpage bit and check again
540 $name =
$title->getDBkey();
542 $this->specialPageFactory->resolveAlias( $name );
545 if ( in_array( $pure, $whiteListRead,
true ) ) {
552 $whitelistReadRegexp = $this->options->get(
'WhitelistReadRegexp' );
553 if ( !$whitelisted && is_array( $whitelistReadRegexp )
554 && !empty( $whitelistReadRegexp ) ) {
555 $name =
$title->getPrefixedText();
557 foreach ( $whitelistReadRegexp as $listItem ) {
558 if ( preg_match( $listItem, $name ) ) {
565 if ( !$whitelisted ) {
566 # If the title is not whitelisted, give extensions a chance to do so...
568 if ( !$whitelisted ) {
587 return [
'badaccess-group0' ];
606 $this->specialPageFactory->resolveAlias( $page->
getDBkey() );
607 if ( $name == $thisName ) {
640 if ( $rigor === self::RIGOR_QUICK || in_array(
$action, [
'createaccount',
'unblock' ] ) ) {
645 if (
$action ===
'read' && !$this->options->get(
'BlockDisablesLogin' ) ) {
649 if ( $this->options->get(
'EmailConfirmToEdit' )
653 $errors[] = [
'confirmedittext' ];
656 $useReplica = ( $rigor !== self::RIGOR_SECURE );
657 $block = $user->
getBlock( $useReplica );
661 if ( !$block || $block->appliesToRight(
$action ) === false ) {
682 if ( $actionObj && $actionObj->getRestriction() !==
$action ) {
689 if ( !$actionObj || $actionObj->requiresUnblock() ) {
727 [
$title, $user,
$action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] )
732 $isSubPage = $this->nsInfo->hasSubpages(
$title->getNamespace() ) ?
733 strpos(
$title->getText(),
'/' ) !==
false :
false;
737 ( $this->nsInfo->isTalk(
$title->getNamespace() ) &&
739 ( !$this->nsInfo->isTalk(
$title->getNamespace() ) &&
742 $errors[] = $user->
isAnon() ? [
'nocreatetext' ] : [
'nocreate-loggedin' ];
744 } elseif (
$action ==
'move' ) {
745 if ( !$this->
userHasRight( $user,
'move-rootuserpages' )
748 $errors[] = [
'cant-move-user-page' ];
754 $errors[] = [
'movenotallowedfile' ];
760 $errors[] = [
'cant-move-category-page' ];
767 if ( $user->
isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
769 $errors[] = [
'movenologintext' ];
771 $errors[] = [
'movenotallowed' ];
774 } elseif (
$action ==
'move-target' ) {
777 $errors[] = [
'movenotallowed' ];
778 } elseif ( !$this->
userHasRight( $user,
'move-rootuserpages' )
781 $errors[] = [
'cant-move-to-user-page' ];
782 } elseif ( !$this->
userHasRight( $user,
'move-categorypages' )
785 $errors[] = [
'cant-move-to-category-page' ];
824 if ( $right ==
'sysop' ) {
825 $right =
'editprotected';
828 if ( $right ==
'autoconfirmed' ) {
829 $right =
'editsemiprotected';
831 if ( $right ==
'' ) {
835 $errors[] = [
'protectedpagetext', $right,
$action ];
836 } elseif (
$title->areRestrictionsCascading() &&
838 $errors[] = [
'protectedpagetext',
'protect',
$action ];
871 if ( $rigor !== self::RIGOR_QUICK && !
$title->isUserConfigPage() ) {
872 # We /could/ use the protection level on the source page, but it's
873 # fairly ugly as we have to establish a precedence hierarchy for pages
874 # included by multiple cascade-protected pages. So just restrict
875 # it to people with 'protect' permission, as they could remove the
877 list( $cascadingSources, $restrictions ) =
$title->getCascadeProtectionSources();
878 # Cascading protection depends on more than this page...
879 # Several cascading protected pages may include this page...
880 # Check each cascading level
881 # This is only for protection restrictions, not for all actions
882 if ( isset( $restrictions[
$action] ) ) {
883 foreach ( $restrictions[
$action] as $right ) {
885 if ( $right ==
'sysop' ) {
886 $right =
'editprotected';
889 if ( $right ==
'autoconfirmed' ) {
890 $right =
'editsemiprotected';
892 if ( $right !=
'' && !$this->
userHasAllRights( $user,
'protect', $right ) ) {
895 foreach ( $cascadingSources as $wikiPage ) {
896 $wikiPages .=
'* [[:' . $wikiPage->getPrefixedText() .
"]]\n";
898 $errors[] = [
'cascadeprotected', count( $cascadingSources ), $wikiPages,
$action ];
939 $errors[] = [
'protect-cantedit' ];
941 } elseif (
$action ==
'create' ) {
942 $title_protection =
$title->getTitleProtection();
943 if ( $title_protection ) {
944 if ( $title_protection[
'permission'] ==
''
945 || !$this->
userHasRight( $user, $title_protection[
'permission'] )
951 $title_protection[
'reason']
955 } elseif (
$action ==
'move' ) {
957 if ( !$this->nsInfo->isMovable(
$title->getNamespace() ) ) {
959 $errors[] = [
'immobile-source-namespace',
$title->getNsText() ];
960 } elseif ( !
$title->isMovable() ) {
962 $errors[] = [
'immobile-source-page' ];
964 } elseif (
$action ==
'move-target' ) {
965 if ( !$this->nsInfo->isMovable(
$title->getNamespace() ) ) {
966 $errors[] = [
'immobile-target-namespace',
$title->getNsText() ];
967 } elseif ( !
$title->isMovable() ) {
968 $errors[] = [
'immobile-target-page' ];
970 } elseif (
$action ==
'delete' ) {
972 if ( !$tempErrors ) {
974 $user, $tempErrors, $rigor,
true,
$title );
978 $errors[] = [
'deleteprotected' ];
985 } elseif (
$action ===
'undelete' ) {
988 $errors[] = [
'undelete-cantedit' ];
994 $errors[] = [
'undelete-cantcreate' ];
1027 # Only 'createaccount' can be performed on special pages,
1028 # which don't actually exist in the DB.
1030 $errors[] = [
'ns-specialprotected' ];
1033 # Check $wgNamespaceProtection for restricted namespaces
1038 [
'protectedinterface',
$action ] : [
'namespaceprotected', $ns,
$action ];
1075 if (
$title->isSiteCssConfigPage() && !$this->
userHasRight( $user,
'editsitecss' ) ) {
1076 $error = [
'sitecssprotected',
$action ];
1077 } elseif (
$title->isSiteJsonConfigPage() && !$this->
userHasRight( $user,
'editsitejson' ) ) {
1078 $error = [
'sitejsonprotected',
$action ];
1079 } elseif (
$title->isSiteJsConfigPage() && !$this->
userHasRight( $user,
'editsitejs' ) ) {
1080 $error = [
'sitejsprotected',
$action ];
1081 } elseif (
$title->isRawHtmlMessage() ) {
1084 $error = [
'sitejsprotected',
$action ];
1085 } elseif ( !$this->
userHasRight( $user,
'editsitecss' ) ) {
1086 $error = [
'sitecssprotected',
$action ];
1095 $error = [
'interfaceadmin-info',
wfMessage( $error[0], $error[1] ) ];
1131 # Protect css/json/js subpages of user pages
1132 # XXX: this might be better using restrictions
1138 if ( preg_match(
'/^' . preg_quote( $user->
getName(),
'/' ) .
'\//',
$title->getText() ) ) {
1141 $title->isUserCssConfigPage()
1144 $errors[] = [
'mycustomcssprotected',
$action ];
1146 $title->isUserJsonConfigPage()
1149 $errors[] = [
'mycustomjsonprotected',
$action ];
1151 $title->isUserJsConfigPage()
1154 $errors[] = [
'mycustomjsprotected',
$action ];
1156 $title->isUserJsConfigPage()
1157 && !$this->
userHasAnyRight( $user,
'edituserjs',
'editmyuserjsredirect' )
1160 $rev = $this->revisionLookup->getRevisionByTitle(
$title );
1164 !$target->inNamespace(
NS_USER )
1165 || !preg_match(
'/^' . preg_quote( $user->
getName(),
'/' ) .
'\//', $target->getText() )
1167 $errors[] = [
'mycustomjsredirectprotected',
$action ];
1175 if ( !in_array(
$action, [
'delete',
'deleterevision',
'suppressrevision' ],
true ) ) {
1177 $title->isUserCssConfigPage()
1180 $errors[] = [
'customcssprotected',
$action ];
1182 $title->isUserJsonConfigPage()
1185 $errors[] = [
'customjsonprotected',
$action ];
1187 $title->isUserJsConfigPage()
1190 $errors[] = [
'customjsprotected',
$action ];
1227 $actions = array_slice( func_get_args(), 1 );
1228 foreach ( $actions as
$action ) {
1246 $actions = array_slice( func_get_args(), 1 );
1247 foreach ( $actions as
$action ) {
1267 if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) {
1269 $user->getEffectiveGroups()
1271 Hooks::run(
'UserGetRights', [ $user, &$this->usersRights[ $rightsCacheKey ] ] );
1275 if ( !defined(
'MW_NO_SESSION' ) ) {
1277 $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
1278 if ( $allowedRights !==
null ) {
1279 $this->usersRights[ $rightsCacheKey ] = array_intersect(
1280 $this->usersRights[ $rightsCacheKey ],
1286 Hooks::run(
'UserGetRightsRemove', [ $user, &$this->usersRights[ $rightsCacheKey ] ] );
1288 $this->usersRights[ $rightsCacheKey ] = array_values(
1289 array_unique( $this->usersRights[ $rightsCacheKey ] )
1293 $user->isLoggedIn() &&
1294 $this->options->get(
'BlockDisablesLogin' ) &&
1298 $this->usersRights[ $rightsCacheKey ] = array_intersect(
1299 $this->usersRights[ $rightsCacheKey ],
1304 $rights = $this->usersRights[ $rightsCacheKey ];
1305 foreach ( $this->temporaryUserRights[ $user->
getId() ] ?? [] as $overrides ) {
1306 $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
1320 if ( $user !==
null ) {
1322 if ( isset( $this->usersRights[ $rightsCacheKey ] ) ) {
1323 unset( $this->usersRights[ $rightsCacheKey ] );
1326 $this->usersRights =
null;
1336 return $user->
isRegistered() ?
"u:{$user->getId()}" :
"anon:{$user->getName()}";
1354 $groupPermissions = $this->options->get(
'GroupPermissions' );
1355 $revokePermissions = $this->options->get(
'RevokePermissions' );
1356 return isset( $groupPermissions[$group][$role] ) && $groupPermissions[$group][$role] &&
1357 !( isset( $revokePermissions[$group][$role] ) && $revokePermissions[$group][$role] );
1371 foreach ( $groups as $group ) {
1372 if ( isset( $this->options->get(
'GroupPermissions' )[$group] ) ) {
1373 $rights = array_merge( $rights,
1375 array_keys( array_filter( $this->options->get(
'GroupPermissions' )[$group] ) ) );
1379 foreach ( $groups as $group ) {
1380 if ( isset( $this->options->get(
'RevokePermissions' )[$group] ) ) {
1381 $rights = array_diff( $rights,
1382 array_keys( array_filter( $this->options->get(
'RevokePermissions' )[$group] ) ) );
1385 return array_unique( $rights );
1397 $allowedGroups = [];
1398 foreach ( array_keys( $this->options->get(
'GroupPermissions' ) ) as $group ) {
1400 $allowedGroups[] = $group;
1403 return $allowedGroups;
1424 if ( isset( $this->cachedRights[$right] ) ) {
1425 return $this->cachedRights[$right];
1428 if ( !isset( $this->options->get(
'GroupPermissions' )[
'*'][$right] )
1429 || !$this->options->get(
'GroupPermissions' )[
'*'][$right] ) {
1430 $this->cachedRights[$right] =
false;
1435 foreach ( $this->options->get(
'RevokePermissions' ) as $rights ) {
1436 if ( isset( $rights[$right] ) && $rights[$right] ) {
1437 $this->cachedRights[$right] =
false;
1444 if ( !defined(
'MW_NO_SESSION' ) ) {
1448 if ( $allowedRights !==
null && !in_array( $right, $allowedRights,
true ) ) {
1449 $this->cachedRights[$right] =
false;
1455 if ( !
Hooks::run(
'UserIsEveryoneAllowed', [ $right ] ) ) {
1456 $this->cachedRights[$right] =
false;
1460 $this->cachedRights[$right] =
true;
1472 if ( $this->allRights ===
null ) {
1473 if ( count( $this->options->get(
'AvailableRights' ) ) ) {
1474 $this->allRights = array_unique( array_merge(
1476 $this->options->get(
'AvailableRights' )
1481 Hooks::run(
'UserGetAllRights', [ &$this->allRights ] );
1493 $namespaceProtection = $this->options->get(
'NamespaceProtection' );
1494 if ( isset( $namespaceProtection[$index] ) ) {
1495 return !$this->
userHasAllRights( $user, ...(array)$namespaceProtection[$index] );
1509 if ( !isset( $this->options->get(
'NamespaceProtection' )[$index] ) ) {
1512 $levels = $this->options->get(
'RestrictionLevels' );
1514 $levels = array_values( array_filter( $levels,
function ( $level ) use ( $user ) {
1516 if ( $right ==
'sysop' ) {
1517 $right =
'editprotected';
1519 if ( $right ==
'autoconfirmed' ) {
1520 $right =
'editsemiprotected';
1536 $namespaceRightGroups = [];
1537 foreach ( (array)$this->options->get(
'NamespaceProtection' )[$index] as $right ) {
1538 if ( $right ==
'sysop' ) {
1539 $right =
'editprotected';
1541 if ( $right ==
'autoconfirmed' ) {
1542 $right =
'editsemiprotected';
1544 if ( $right !=
'' ) {
1550 $usableLevels = [
'' ];
1551 foreach ( $this->options->get(
'RestrictionLevels' ) as $level ) {
1553 if ( $right ==
'sysop' ) {
1554 $right =
'editprotected';
1556 if ( $right ==
'autoconfirmed' ) {
1557 $right =
'editsemiprotected';
1560 if ( $right !=
'' &&
1561 !isset( $namespaceRightGroups[$right] ) &&
1565 foreach ( $namespaceRightGroups as $groups ) {
1572 $usableLevels[] = $level;
1576 return $usableLevels;
1594 $userId = $user->
getId();
1595 $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
1596 $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
1597 return new ScopedCallback(
function () use ( $userId, $nextKey ) {
1598 unset( $this->temporaryUserRights[$userId][$nextKey] );
1613 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
1614 throw new Exception( __METHOD__ .
' can not be called outside of tests' );
1617 is_array( $rights ) ? $rights : [ $rights ];