X-Git-Url: https://git.cyclocoop.org/%27.WWW_URL.%27admin/?a=blobdiff_plain;f=includes%2FPermissions%2FPermissionManager.php;h=f9ad3ebb93639e46ef98c8e753dde60d4ecd53e6;hb=7be62046f8d04847b0c5bf5876756107c209d0eb;hp=d256e9b18eb098548e65b845bf1b89eaa648b3eb;hpb=31dfe22853bf9ccd0d4e8f6d29ec9148e63fb980;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php index d256e9b18e..f9ad3ebb93 100644 --- a/includes/Permissions/PermissionManager.php +++ b/includes/Permissions/PermissionManager.php @@ -22,6 +22,7 @@ namespace MediaWiki\Permissions; use Action; use Exception; use Hooks; +use MediaWiki\Config\ServiceOptions; use MediaWiki\Linker\LinkTarget; use MediaWiki\Revision\RevisionLookup; use MediaWiki\Revision\RevisionRecord; @@ -54,36 +55,36 @@ class PermissionManager { /** @var string Does cheap and expensive checks, using the master as needed */ const RIGOR_SECURE = 'secure'; + /** + * TODO Make this const when HHVM support is dropped (T192166) + * + * @since 1.34 + * @var array + */ + public static $constructorOptions = [ + 'WhitelistRead', + 'WhitelistReadRegexp', + 'EmailConfirmToEdit', + 'BlockDisablesLogin', + 'GroupPermissions', + 'RevokePermissions', + 'AvailableRights', + 'NamespaceProtection', + 'RestrictionLevels' + ]; + + /** @var ServiceOptions */ + private $options; + /** @var SpecialPageFactory */ private $specialPageFactory; /** @var RevisionLookup */ private $revisionLookup; - /** @var string[] List of pages names anonymous user may see */ - private $whitelistRead; - - /** @var string[] Whitelists publicly readable titles with regular expressions */ - private $whitelistReadRegexp; - - /** @var bool Require users to confirm email address before they can edit */ - private $emailConfirmToEdit; - - /** @var bool If set to true, blocked users will no longer be allowed to log in */ - private $blockDisablesLogin; - /** @var NamespaceInfo */ private $nsInfo; - /** @var string[][] Access rights for groups and users in these groups */ - private $groupPermissions; - - /** @var string[][] Permission keys revoked from users in each group */ - private $revokePermissions; - - /** @var string[] A list of available rights, in addition to the ones defined by the core */ - private $availableRights; - /** @var string[] Cached results of getAllRights() */ private $allRights = false; @@ -189,38 +190,21 @@ class PermissionManager { ]; /** + * @param ServiceOptions $options * @param SpecialPageFactory $specialPageFactory * @param RevisionLookup $revisionLookup - * @param string[] $whitelistRead - * @param string[] $whitelistReadRegexp - * @param bool $emailConfirmToEdit - * @param bool $blockDisablesLogin - * @param string[][] $groupPermissions - * @param string[][] $revokePermissions - * @param string[] $availableRights * @param NamespaceInfo $nsInfo */ public function __construct( + ServiceOptions $options, SpecialPageFactory $specialPageFactory, RevisionLookup $revisionLookup, - $whitelistRead, - $whitelistReadRegexp, - $emailConfirmToEdit, - $blockDisablesLogin, - $groupPermissions, - $revokePermissions, - $availableRights, NamespaceInfo $nsInfo ) { + $options->assertRequiredOptions( self::$constructorOptions ); + $this->options = $options; $this->specialPageFactory = $specialPageFactory; $this->revisionLookup = $revisionLookup; - $this->whitelistRead = $whitelistRead; - $this->whitelistReadRegexp = $whitelistReadRegexp; - $this->emailConfirmToEdit = $emailConfirmToEdit; - $this->blockDisablesLogin = $blockDisablesLogin; - $this->groupPermissions = $groupPermissions; - $this->revokePermissions = $revokePermissions; - $this->availableRights = $availableRights; $this->nsInfo = $nsInfo; } @@ -289,7 +273,8 @@ class PermissionManager { } /** - * Check if user is blocked from editing a particular article + * Check if user is blocked from editing a particular article. If the user does not + * have a block, this will return false. * * @param User $user * @param LinkTarget $page Title to check @@ -298,27 +283,29 @@ class PermissionManager { * @return bool */ public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) { - $blocked = $user->isHidden(); + $block = $user->getBlock( $fromReplica ); + if ( !$block ) { + return false; + } // TODO: remove upon further migration to LinkTarget $title = Title::newFromLinkTarget( $page ); + $blocked = $user->isHidden(); if ( !$blocked ) { - $block = $user->getBlock( $fromReplica ); - if ( $block ) { - // Special handling for a user's own talk page. The block is not aware - // of the user, so this must be done here. - if ( $title->equals( $user->getTalkPage() ) ) { - $blocked = $block->appliesToUsertalk( $title ); - } else { - $blocked = $block->appliesToTitle( $title ); - } + // Special handling for a user's own talk page. The block is not aware + // of the user, so this must be done here. + if ( $title->equals( $user->getTalkPage() ) ) { + $blocked = $block->appliesToUsertalk( $title ); + } else { + $blocked = $block->appliesToTitle( $title ); } } // only for the purpose of the hook. We really don't need this here. $allowUsertalk = $user->isAllowUsertalk(); + // Allow extensions to let a blocked user access a particular page Hooks::run( 'UserIsBlockedFrom', [ $user, $title, &$blocked, &$allowUsertalk ] ); return $blocked; @@ -500,11 +487,12 @@ class PermissionManager { // TODO: remove when LinkTarget usage will expand further $title = Title::newFromLinkTarget( $page ); + $whiteListRead = $this->options->get( 'WhitelistRead' ); $whitelisted = false; - if ( User::isEveryoneAllowed( 'read' ) ) { + if ( $this->isEveryoneAllowed( 'read' ) ) { # Shortcut for public wikis, allows skipping quite a bit of code $whitelisted = true; - } elseif ( $user->isAllowed( 'read' ) ) { + } elseif ( $this->userHasRight( $user, 'read' ) ) { # If the user is allowed to read pages, he is allowed to read all pages $whitelisted = true; } elseif ( $this->isSameSpecialPage( 'Userlogin', $title ) @@ -514,20 +502,20 @@ class PermissionManager { # Always grant access to the login page. # Even anons need to be able to log in. $whitelisted = true; - } elseif ( is_array( $this->whitelistRead ) && count( $this->whitelistRead ) ) { + } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) { # Time to check the whitelist # Only do these checks is there's something to check against $name = $title->getPrefixedText(); $dbName = $title->getPrefixedDBkey(); // Check for explicit whitelisting with and without underscores - if ( in_array( $name, $this->whitelistRead, true ) - || in_array( $dbName, $this->whitelistRead, true ) ) { + if ( in_array( $name, $whiteListRead, true ) + || in_array( $dbName, $whiteListRead, true ) ) { $whitelisted = true; } elseif ( $title->getNamespace() == NS_MAIN ) { # Old settings might have the title prefixed with # a colon for main-namespace pages - if ( in_array( ':' . $name, $this->whitelistRead ) ) { + if ( in_array( ':' . $name, $whiteListRead ) ) { $whitelisted = true; } } elseif ( $title->isSpecialPage() ) { @@ -537,18 +525,19 @@ class PermissionManager { $this->specialPageFactory->resolveAlias( $name ); if ( $name ) { $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); - if ( in_array( $pure, $this->whitelistRead, true ) ) { + if ( in_array( $pure, $whiteListRead, true ) ) { $whitelisted = true; } } } } - if ( !$whitelisted && is_array( $this->whitelistReadRegexp ) - && !empty( $this->whitelistReadRegexp ) ) { + $whitelistReadRegexp = $this->options->get( 'WhitelistReadRegexp' ); + if ( !$whitelisted && is_array( $whitelistReadRegexp ) + && !empty( $whitelistReadRegexp ) ) { $name = $title->getPrefixedText(); // Check for regex whitelisting - foreach ( $this->whitelistReadRegexp as $listItem ) { + foreach ( $whitelistReadRegexp as $listItem ) { if ( preg_match( $listItem, $name ) ) { $whitelisted = true; break; @@ -636,11 +625,11 @@ class PermissionManager { } // Optimize for a very common case - if ( $action === 'read' && !$this->blockDisablesLogin ) { + if ( $action === 'read' && !$this->options->get( 'BlockDisablesLogin' ) ) { return $errors; } - if ( $this->emailConfirmToEdit + if ( $this->options->get( 'EmailConfirmToEdit' ) && !$user->isEmailConfirmed() && $action === 'edit' ) { @@ -729,33 +718,35 @@ class PermissionManager { if ( $action == 'create' ) { if ( ( $this->nsInfo->isTalk( $title->getNamespace() ) && - !$user->isAllowed( 'createtalk' ) ) || + !$this->userHasRight( $user, 'createtalk' ) ) || ( !$this->nsInfo->isTalk( $title->getNamespace() ) && - !$user->isAllowed( 'createpage' ) ) + !$this->userHasRight( $user, 'createpage' ) ) ) { $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ]; } } elseif ( $action == 'move' ) { - if ( !$user->isAllowed( 'move-rootuserpages' ) + if ( !$this->userHasRight( $user, 'move-rootuserpages' ) && $title->getNamespace() == NS_USER && !$isSubPage ) { // Show user page-specific message only if the user can move other pages $errors[] = [ 'cant-move-user-page' ]; } // Check if user is allowed to move files if it's a file - if ( $title->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) { + if ( $title->getNamespace() == NS_FILE && + !$this->userHasRight( $user, 'movefile' ) ) { $errors[] = [ 'movenotallowedfile' ]; } // Check if user is allowed to move category pages if it's a category page - if ( $title->getNamespace() == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) { + if ( $title->getNamespace() == NS_CATEGORY && + !$this->userHasRight( $user, 'move-categorypages' ) ) { $errors[] = [ 'cant-move-category-page' ]; } - if ( !$user->isAllowed( 'move' ) ) { + if ( !$this->userHasRight( $user, 'move' ) ) { // User can't move anything - $userCanMove = User::groupHasPermission( 'user', 'move' ); - $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); + $userCanMove = $this->groupHasPermission( 'user', 'move' ); + $autoconfirmedCanMove = $this->groupHasPermission( 'autoconfirmed', 'move' ); if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { // custom message if logged-in users without any special rights can move $errors[] = [ 'movenologintext' ]; @@ -764,19 +755,19 @@ class PermissionManager { } } } elseif ( $action == 'move-target' ) { - if ( !$user->isAllowed( 'move' ) ) { + if ( !$this->userHasRight( $user, 'move' ) ) { // User can't move anything $errors[] = [ 'movenotallowed' ]; - } elseif ( !$user->isAllowed( 'move-rootuserpages' ) + } elseif ( !$this->userHasRight( $user, 'move-rootuserpages' ) && $title->getNamespace() == NS_USER && !$isSubPage ) { // Show user page-specific message only if the user can move other pages $errors[] = [ 'cant-move-to-user-page' ]; - } elseif ( !$user->isAllowed( 'move-categorypages' ) + } elseif ( !$this->userHasRight( $user, 'move-categorypages' ) && $title->getNamespace() == NS_CATEGORY ) { // Show category page-specific message only if the user can move other pages $errors[] = [ 'cant-move-to-category-page' ]; } - } elseif ( !$user->isAllowed( $action ) ) { + } elseif ( !$this->userHasRight( $user, $action ) ) { $errors[] = $this->missingPermissionError( $action, $short ); } @@ -823,9 +814,10 @@ class PermissionManager { if ( $right == '' ) { continue; } - if ( !$user->isAllowed( $right ) ) { + if ( !$this->userHasRight( $user, $right ) ) { $errors[] = [ 'protectedpagetext', $right, $action ]; - } elseif ( $title->areRestrictionsCascading() && !$user->isAllowed( 'protect' ) ) { + } elseif ( $title->areRestrictionsCascading() && + !$this->userHasRight( $user, 'protect' ) ) { $errors[] = [ 'protectedpagetext', 'protect', $action ]; } } @@ -837,7 +829,7 @@ class PermissionManager { * Check restrictions on cascading pages. * * @param string $action The action to check - * @param User $user User to check + * @param UserIdentity $user User to check * @param array $errors List of current errors * @param string $rigor One of PermissionManager::RIGOR_ constants * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation) @@ -851,7 +843,7 @@ class PermissionManager { */ private function checkCascadingSourcesRestrictions( $action, - User $user, + UserIdentity $user, $errors, $rigor, $short, @@ -880,7 +872,7 @@ class PermissionManager { if ( $right == 'autoconfirmed' ) { $right = 'editsemiprotected'; } - if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { + if ( $right != '' && !$this->userHasAllRights( $user, 'protect', $right ) ) { $wikiPages = ''; /** @var Title $wikiPage */ foreach ( $cascadingSources as $wikiPage ) { @@ -933,7 +925,7 @@ class PermissionManager { $title_protection = $title->getTitleProtection(); if ( $title_protection ) { if ( $title_protection['permission'] == '' - || !$user->isAllowed( $title_protection['permission'] ) + || !$this->userHasRight( $user, $title_protection['permission'] ) ) { $errors[] = [ 'titleprotected', @@ -992,7 +984,7 @@ class PermissionManager { * Check permissions on special pages & namespaces * * @param string $action The action to check - * @param User $user User to check + * @param UserIdentity $user User to check * @param array $errors List of current errors * @param string $rigor One of PermissionManager::RIGOR_ constants * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation) @@ -1006,7 +998,7 @@ class PermissionManager { */ private function checkSpecialsAndNSPermissions( $action, - User $user, + UserIdentity $user, $errors, $rigor, $short, @@ -1022,7 +1014,7 @@ class PermissionManager { } # Check $wgNamespaceProtection for restricted namespaces - if ( $title->isNamespaceProtected( $user ) ) { + if ( $this->isNamespaceProtected( $title->getNamespace(), $user ) ) { $ns = $title->getNamespace() == NS_MAIN ? wfMessage( 'nstab-main' )->text() : $title->getNsText(); $errors[] = $title->getNamespace() == NS_MEDIAWIKI ? @@ -1063,23 +1055,23 @@ class PermissionManager { $error = null; // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the // editinterface right. That's implemented as a restriction so no check needed here. - if ( $title->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) { + if ( $title->isSiteCssConfigPage() && !$this->userHasRight( $user, 'editsitecss' ) ) { $error = [ 'sitecssprotected', $action ]; - } elseif ( $title->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) { + } elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) { $error = [ 'sitejsonprotected', $action ]; - } elseif ( $title->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) { + } elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) { $error = [ 'sitejsprotected', $action ]; } elseif ( $title->isRawHtmlMessage() ) { // Raw HTML can be used to deploy CSS or JS so require rights for both. - if ( !$user->isAllowed( 'editsitejs' ) ) { + if ( !$this->userHasRight( $user, 'editsitejs' ) ) { $error = [ 'sitejsprotected', $action ]; - } elseif ( !$user->isAllowed( 'editsitecss' ) ) { + } elseif ( !$this->userHasRight( $user, 'editsitecss' ) ) { $error = [ 'sitecssprotected', $action ]; } } if ( $error ) { - if ( $user->isAllowed( 'editinterface' ) ) { + if ( $this->userHasRight( $user, 'editinterface' ) ) { // Most users / site admins will probably find out about the new, more restrictive // permissions by failing to edit something. Give them more info. // TODO remove this a few release cycles after 1.32 @@ -1096,7 +1088,7 @@ class PermissionManager { * Check CSS/JSON/JS sub-page permissions * * @param string $action The action to check - * @param User $user User to check + * @param UserIdentity $user User to check * @param array $errors List of current errors * @param string $rigor One of PermissionManager::RIGOR_ constants * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation) @@ -1110,7 +1102,7 @@ class PermissionManager { */ private function checkUserConfigPermissions( $action, - User $user, + UserIdentity $user, $errors, $rigor, $short, @@ -1130,22 +1122,22 @@ class PermissionManager { // Users need editmyuser* to edit their own CSS/JSON/JS subpages. if ( $title->isUserCssConfigPage() - && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) + && !$this->userHasAnyRight( $user, 'editmyusercss', 'editusercss' ) ) { $errors[] = [ 'mycustomcssprotected', $action ]; } elseif ( $title->isUserJsonConfigPage() - && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' ) + && !$this->userHasAnyRight( $user, 'editmyuserjson', 'edituserjson' ) ) { $errors[] = [ 'mycustomjsonprotected', $action ]; } elseif ( $title->isUserJsConfigPage() - && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) + && !$this->userHasAnyRight( $user, 'editmyuserjs', 'edituserjs' ) ) { $errors[] = [ 'mycustomjsprotected', $action ]; } elseif ( $title->isUserJsConfigPage() - && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' ) + && !$this->userHasAnyRight( $user, 'edituserjs', 'editmyuserjsredirect' ) ) { // T207750 - do not allow users to edit a redirect if they couldn't edit the target $rev = $this->revisionLookup->getRevisionByTitle( $title ); @@ -1166,17 +1158,17 @@ class PermissionManager { if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) { if ( $title->isUserCssConfigPage() - && !$user->isAllowed( 'editusercss' ) + && !$this->userHasRight( $user, 'editusercss' ) ) { $errors[] = [ 'customcssprotected', $action ]; } elseif ( $title->isUserJsonConfigPage() - && !$user->isAllowed( 'edituserjson' ) + && !$this->userHasRight( $user, 'edituserjson' ) ) { $errors[] = [ 'customjsonprotected', $action ]; } elseif ( $title->isUserJsConfigPage() - && !$user->isAllowed( 'edituserjs' ) + && !$this->userHasRight( $user, 'edituserjs' ) ) { $errors[] = [ 'customjsprotected', $action ]; } @@ -1205,6 +1197,42 @@ class PermissionManager { return in_array( $action, $this->getUserPermissions( $user ), true ); } + /** + * Check if user is allowed to make any action + * + * @param UserIdentity $user + * // TODO: HHVM can't create mocks with variable params @param string ...$actions + * @return bool True if user is allowed to perform *any* of the given actions + * @since 1.34 + */ + public function userHasAnyRight( UserIdentity $user ) { + $actions = array_slice( func_get_args(), 1 ); + foreach ( $actions as $action ) { + if ( $this->userHasRight( $user, $action ) ) { + return true; + } + } + return false; + } + + /** + * Check if user is allowed to make all actions + * + * @param UserIdentity $user + * // TODO: HHVM can't create mocks with variable params @param string ...$actions + * @return bool True if user is allowed to perform *all* of the given actions + * @since 1.34 + */ + public function userHasAllRights( UserIdentity $user ) { + $actions = array_slice( func_get_args(), 1 ); + foreach ( $actions as $action ) { + if ( !$this->userHasRight( $user, $action ) ) { + return false; + } + } + return true; + } + /** * Get the permissions this user has. * @@ -1216,11 +1244,12 @@ class PermissionManager { */ public function getUserPermissions( UserIdentity $user ) { $user = User::newFromIdentity( $user ); - if ( !isset( $this->usersRights[ $user->getId() ] ) ) { - $this->usersRights[ $user->getId() ] = $this->getGroupPermissions( + $rightsCacheKey = $this->getRightsCacheKey( $user ); + if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) { + $this->usersRights[ $rightsCacheKey ] = $this->getGroupPermissions( $user->getEffectiveGroups() ); - Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] ); + Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $rightsCacheKey ] ] ); // Deny any rights denied by the user's session, unless this // endpoint has no sessions. @@ -1228,32 +1257,32 @@ class PermissionManager { // FIXME: $user->getRequest().. need to be replaced with something else $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights(); if ( $allowedRights !== null ) { - $this->usersRights[ $user->getId() ] = array_intersect( - $this->usersRights[ $user->getId() ], + $this->usersRights[ $rightsCacheKey ] = array_intersect( + $this->usersRights[ $rightsCacheKey ], $allowedRights ); } } - Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] ); + Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $rightsCacheKey ] ] ); // Force reindexation of rights when a hook has unset one of them - $this->usersRights[ $user->getId() ] = array_values( - array_unique( $this->usersRights[ $user->getId() ] ) + $this->usersRights[ $rightsCacheKey ] = array_values( + array_unique( $this->usersRights[ $rightsCacheKey ] ) ); if ( $user->isLoggedIn() && - $this->blockDisablesLogin && + $this->options->get( 'BlockDisablesLogin' ) && $user->getBlock() ) { $anon = new User; - $this->usersRights[ $user->getId() ] = array_intersect( - $this->usersRights[ $user->getId() ], + $this->usersRights[ $rightsCacheKey ] = array_intersect( + $this->usersRights[ $rightsCacheKey ], $this->getUserPermissions( $anon ) ); } } - $rights = $this->usersRights[ $user->getId() ]; + $rights = $this->usersRights[ $rightsCacheKey ]; foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) { $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) ); } @@ -1270,14 +1299,24 @@ class PermissionManager { */ public function invalidateUsersRightsCache( $user = null ) { if ( $user !== null ) { - if ( isset( $this->usersRights[ $user->getId() ] ) ) { - unset( $this->usersRights[$user->getId()] ); + $rightsCacheKey = $this->getRightsCacheKey( $user ); + if ( isset( $this->usersRights[ $rightsCacheKey ] ) ) { + unset( $this->usersRights[ $rightsCacheKey ] ); } } else { $this->usersRights = null; } } + /** + * Gets a unique key for user rights cache. + * @param UserIdentity $user + * @return string + */ + private function getRightsCacheKey( UserIdentity $user ) { + return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}"; + } + /** * Check, if the given group has the given permission * @@ -1293,10 +1332,10 @@ class PermissionManager { * @return bool */ public function groupHasPermission( $group, $role ) { - return isset( $this->groupPermissions[$group][$role] ) && - $this->groupPermissions[$group][$role] && - !( isset( $this->revokePermissions[$group][$role] ) && - $this->revokePermissions[$group][$role] ); + $groupPermissions = $this->options->get( 'GroupPermissions' ); + $revokePermissions = $this->options->get( 'RevokePermissions' ); + return isset( $groupPermissions[$group][$role] ) && $groupPermissions[$group][$role] && + !( isset( $revokePermissions[$group][$role] ) && $revokePermissions[$group][$role] ); } /** @@ -1311,17 +1350,17 @@ class PermissionManager { $rights = []; // grant every granted permission first foreach ( $groups as $group ) { - if ( isset( $this->groupPermissions[$group] ) ) { + if ( isset( $this->options->get( 'GroupPermissions' )[$group] ) ) { $rights = array_merge( $rights, // array_filter removes empty items - array_keys( array_filter( $this->groupPermissions[$group] ) ) ); + array_keys( array_filter( $this->options->get( 'GroupPermissions' )[$group] ) ) ); } } // now revoke the revoked permissions foreach ( $groups as $group ) { - if ( isset( $this->revokePermissions[$group] ) ) { + if ( isset( $this->options->get( 'RevokePermissions' )[$group] ) ) { $rights = array_diff( $rights, - array_keys( array_filter( $this->revokePermissions[$group] ) ) ); + array_keys( array_filter( $this->options->get( 'RevokePermissions' )[$group] ) ) ); } } return array_unique( $rights ); @@ -1337,7 +1376,7 @@ class PermissionManager { */ public function getGroupsWithPermission( $role ) { $allowedGroups = []; - foreach ( array_keys( $this->groupPermissions ) as $group ) { + foreach ( array_keys( $this->options->get( 'GroupPermissions' ) ) as $group ) { if ( $this->groupHasPermission( $group, $role ) ) { $allowedGroups[] = $group; } @@ -1367,14 +1406,14 @@ class PermissionManager { return $this->cachedRights[$right]; } - if ( !isset( $this->groupPermissions['*'][$right] ) - || !$this->groupPermissions['*'][$right] ) { + if ( !isset( $this->options->get( 'GroupPermissions' )['*'][$right] ) + || !$this->options->get( 'GroupPermissions' )['*'][$right] ) { $this->cachedRights[$right] = false; return false; } // If it's revoked anywhere, then everyone doesn't have it - foreach ( $this->revokePermissions as $rights ) { + foreach ( $this->options->get( 'RevokePermissions' ) as $rights ) { if ( isset( $rights[$right] ) && $rights[$right] ) { $this->cachedRights[$right] = false; return false; @@ -1412,10 +1451,10 @@ class PermissionManager { */ public function getAllPermissions() { if ( $this->allRights === false ) { - if ( count( $this->availableRights ) ) { + if ( count( $this->options->get( 'AvailableRights' ) ) ) { $this->allRights = array_unique( array_merge( $this->coreRights, - $this->availableRights + $this->options->get( 'AvailableRights' ) ) ); } else { $this->allRights = $this->coreRights; @@ -1425,6 +1464,99 @@ class PermissionManager { return $this->allRights; } + /** + * Determines if $user is unable to edit pages in namespace because it has been protected. + * @param $index + * @param UserIdentity $user + * @return bool + */ + private function isNamespaceProtected( $index, UserIdentity $user ) { + $namespaceProtection = $this->options->get( 'NamespaceProtection' ); + if ( isset( $namespaceProtection[$index] ) ) { + return !$this->userHasAllRights( $user, ...(array)$namespaceProtection[$index] ); + } + return false; + } + + /** + * Determine which restriction levels it makes sense to use in a namespace, + * optionally filtered by a user's rights. + * + * @param int $index Index to check + * @param UserIdentity|null $user User to check + * @return array + */ + public function getNamespaceRestrictionLevels( $index, UserIdentity $user = null ) { + if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) { + // All levels are valid if there's no namespace restriction. + // But still filter by user, if necessary + $levels = $this->options->get( 'RestrictionLevels' ); + if ( $user ) { + $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) { + $right = $level; + if ( $right == 'sysop' ) { + $right = 'editprotected'; // BC + } + if ( $right == 'autoconfirmed' ) { + $right = 'editsemiprotected'; // BC + } + return $this->userHasRight( $user, $right ); + } ) ); + } + return $levels; + } + + // $wgNamespaceProtection can require one or more rights to edit the namespace, which + // may be satisfied by membership in multiple groups each giving a subset of those rights. + // A restriction level is redundant if, for any one of the namespace rights, all groups + // giving that right also give the restriction level's right. Or, conversely, a + // restriction level is not redundant if, for every namespace right, there's at least one + // group giving that right without the restriction level's right. + // + // First, for each right, get a list of groups with that right. + $namespaceRightGroups = []; + foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) { + if ( $right == 'sysop' ) { + $right = 'editprotected'; // BC + } + if ( $right == 'autoconfirmed' ) { + $right = 'editsemiprotected'; // BC + } + if ( $right != '' ) { + $namespaceRightGroups[$right] = $this->getGroupsWithPermission( $right ); + } + } + + // Now, go through the protection levels one by one. + $usableLevels = [ '' ]; + foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) { + $right = $level; + if ( $right == 'sysop' ) { + $right = 'editprotected'; // BC + } + if ( $right == 'autoconfirmed' ) { + $right = 'editsemiprotected'; // BC + } + + if ( $right != '' && + !isset( $namespaceRightGroups[$right] ) && + ( !$user || $this->userHasRight( $user, $right ) ) + ) { + // Do any of the namespace rights imply the restriction right? (see explanation above) + foreach ( $namespaceRightGroups as $groups ) { + if ( !array_diff( $groups, $this->getGroupsWithPermission( $right ) ) ) { + // Yes, this one does. + continue 2; + } + } + // No, keep the restriction level + $usableLevels[] = $level; + } + } + + return $usableLevels; + } + /** * Add temporary user rights, only valid for the current scope. * This is meant for making it possible to programatically trigger certain actions that @@ -1462,7 +1594,8 @@ class PermissionManager { if ( !defined( 'MW_PHPUNIT_TEST' ) ) { throw new Exception( __METHOD__ . ' can not be called outside of tests' ); } - $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ]; + $this->usersRights[ $this->getRightsCacheKey( $user ) ] = + is_array( $rights ) ? $rights : [ $rights ]; } }