Merge "objectcache: move MemcachedClient class to /utils subdir"
[lhc/web/wiklou.git] / includes / Permissions / PermissionManager.php
index d256e9b..ec0157b 100644 (file)
@@ -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',
@@ -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.
         *
@@ -1243,7 +1271,7 @@ class PermissionManager {
 
                        if (
                                $user->isLoggedIn() &&
-                               $this->blockDisablesLogin &&
+                               $this->options->get( 'BlockDisablesLogin' ) &&
                                $user->getBlock()
                        ) {
                                $anon = new User;
@@ -1293,10 +1321,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 +1339,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 +1365,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 +1395,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 +1440,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 +1453,85 @@ class PermissionManager {
                return $this->allRights;
        }
 
+       /**
+        * 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