Merge "Introduce PermissionManager service"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 5 Apr 2019 16:45:32 +0000 (16:45 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 5 Apr 2019 16:45:32 +0000 (16:45 +0000)
includes/AutoLoader.php
includes/MediaWikiServices.php
includes/Permissions/PermissionManager.php [new file with mode: 0644]
includes/ServiceWiring.php
includes/Title.php
includes/user/User.php
tests/phpunit/includes/Permissions/PermissionManagerTest.php [new file with mode: 0644]
tests/phpunit/includes/TitlePermissionTest.php
tests/phpunit/includes/TitleTest.php

index f8fbf83..fa11bcb 100644 (file)
@@ -134,6 +134,7 @@ class AutoLoader {
                        'MediaWiki\\Edit\\' => __DIR__ . '/edit/',
                        'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/',
                        'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
+                       'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/',
                        'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
                        'MediaWiki\\Revision\\' => __DIR__ . '/Revision/',
                        'MediaWiki\\Session\\' => __DIR__ . '/session/',
index 292e8df..6bf5d1d 100644 (file)
@@ -14,6 +14,7 @@ use Hooks;
 use IBufferingStatsdDataFactory;
 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
 use MediaWiki\Http\HttpRequestFactory;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Preferences\PreferencesFactory;
 use MediaWiki\Shell\CommandFactory;
 use MediaWiki\Revision\RevisionRenderer;
@@ -721,6 +722,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'PerDbNameStatsdDataFactory' );
        }
 
+       /**
+        * @since 1.33
+        * @return PermissionManager
+        */
+       public function getPermissionManager() {
+               return $this->getService( 'PermissionManager' );
+       }
+
        /**
         * @since 1.31
         * @return PreferencesFactory
diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php
new file mode 100644 (file)
index 0000000..1d94e0e
--- /dev/null
@@ -0,0 +1,1047 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+namespace MediaWiki\Permissions;
+
+use Action;
+use Exception;
+use FatalError;
+use Hooks;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Special\SpecialPageFactory;
+use MessageSpecifier;
+use MWException;
+use MWNamespace;
+use RequestContext;
+use SpecialPage;
+use Title;
+use User;
+use WikiPage;
+
+/**
+ * A service class for checking permissions
+ * To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager().
+ *
+ * @since 1.33
+ */
+class PermissionManager {
+
+       /** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */
+       const RIGOR_QUICK = 'quick';
+
+       /** @var string Does cheap and expensive checks possibly from a replica DB */
+       const RIGOR_FULL = 'full';
+
+       /** @var string Does cheap and expensive checks, using the master as needed */
+       const RIGOR_SECURE = 'secure';
+
+       /** @var SpecialPageFactory */
+       private $specialPageFactory;
+
+       /** @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;
+
+       /**
+        * @param SpecialPageFactory $specialPageFactory
+        * @param string[] $whitelistRead
+        * @param string[] $whitelistReadRegexp
+        * @param bool $emailConfirmToEdit
+        * @param bool $blockDisablesLogin
+        */
+       public function __construct(
+               SpecialPageFactory $specialPageFactory,
+               $whitelistRead,
+               $whitelistReadRegexp,
+               $emailConfirmToEdit,
+               $blockDisablesLogin
+       ) {
+               $this->specialPageFactory = $specialPageFactory;
+               $this->whitelistRead = $whitelistRead;
+               $this->whitelistReadRegexp = $whitelistReadRegexp;
+               $this->emailConfirmToEdit = $emailConfirmToEdit;
+               $this->blockDisablesLogin = $blockDisablesLogin;
+       }
+
+       /**
+        * Can $user perform $action on a page?
+        *
+        * The method is intended to replace Title::userCan()
+        * The $user parameter need to be superseded by UserIdentity value in future
+        * The $title parameter need to be superseded by PageIdentity value in future
+        *
+        * @see Title::userCan()
+        *
+        * @param string $action
+        * @param User $user
+        * @param LinkTarget $page
+        * @param string $rigor One of PermissionManager::RIGOR_ constants
+        *   - RIGOR_QUICK  : does cheap permission checks from replica DBs (usable for GUI creation)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        *
+        * @return bool
+        * @throws Exception
+        */
+       public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) {
+               return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
+       }
+
+       /**
+        * Can $user perform $action on a page?
+        *
+        * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
+        *
+        * @param string $action Action that permission needs to be checked for
+        * @param User $user User to check
+        * @param LinkTarget $page
+        * @param string $rigor One of PermissionManager::RIGOR_ constants
+        *   - RIGOR_QUICK  : does cheap permission checks from replica DBs (usable for GUI creation)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param array $ignoreErrors Array of Strings Set this to a list of message keys
+        *   whose corresponding errors may be ignored.
+        *
+        * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
+        * @throws Exception
+        */
+       public function getPermissionErrors(
+               $action,
+               User $user,
+               LinkTarget $page,
+               $rigor = self::RIGOR_SECURE,
+               $ignoreErrors = []
+       ) {
+               $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
+
+               // Remove the errors being ignored.
+               foreach ( $errors as $index => $error ) {
+                       $errKey = is_array( $error ) ? $error[0] : $error;
+
+                       if ( in_array( $errKey, $ignoreErrors ) ) {
+                               unset( $errors[$index] );
+                       }
+                       if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
+                               unset( $errors[$index] );
+                       }
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Check if user is blocked from editing a particular article
+        *
+        * @param User $user
+        * @param LinkTarget $page Title to check
+        * @param bool $fromReplica Whether to check the replica DB instead of the master
+        *
+        * @return bool
+        * @throws FatalError
+        * @throws MWException
+        */
+       public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
+               $blocked = $user->isHidden();
+
+               // TODO: remove upon further migration to LinkTarget
+               $page = Title::newFromLinkTarget( $page );
+
+               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 ( $page->equals( $user->getTalkPage() ) ) {
+                                       $blocked = $block->appliesToUsertalk( $page );
+                               } else {
+                                       $blocked = $block->appliesToTitle( $page );
+                               }
+                       }
+               }
+
+               // only for the purpose of the hook. We really don't need this here.
+               $allowUsertalk = $user->isAllowUsertalk();
+
+               Hooks::run( 'UserIsBlockedFrom', [ $user, $page, &$blocked, &$allowUsertalk ] );
+
+               return $blocked;
+       }
+
+       /**
+        * Can $user perform $action on a page? This is an internal function,
+        * with multiple levels of checks depending on performance needs; see $rigor below.
+        * It does not check wfReadOnly().
+        *
+        * @param string $action Action that permission needs to be checked for
+        * @param User $user User to check
+        * @param LinkTarget $page
+        * @param string $rigor One of PermissionManager::RIGOR_ constants
+        *   - RIGOR_QUICK  : does cheap permission checks from replica DBs (usable for GUI creation)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Set this to true to stop after the first permission error.
+        *
+        * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
+        * @throws Exception
+        */
+       private function getPermissionErrorsInternal(
+               $action,
+               User $user,
+               LinkTarget $page,
+               $rigor = self::RIGOR_SECURE,
+               $short = false
+       ) {
+               if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
+                       throw new Exception( "Invalid rigor parameter '$rigor'." );
+               }
+
+               # Read has special handling
+               if ( $action == 'read' ) {
+                       $checks = [
+                               'checkPermissionHooks',
+                               'checkReadPermissions',
+                               'checkUserBlock', // for wgBlockDisablesLogin
+                       ];
+                       # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
+                       # or checkUserConfigPermissions here as it will lead to duplicate
+                       # error messages. This is okay to do since anywhere that checks for
+                       # create will also check for edit, and those checks are called for edit.
+               } elseif ( $action == 'create' ) {
+                       $checks = [
+                               'checkQuickPermissions',
+                               'checkPermissionHooks',
+                               'checkPageRestrictions',
+                               'checkCascadingSourcesRestrictions',
+                               'checkActionPermissions',
+                               'checkUserBlock'
+                       ];
+               } else {
+                       $checks = [
+                               'checkQuickPermissions',
+                               'checkPermissionHooks',
+                               'checkSpecialsAndNSPermissions',
+                               'checkSiteConfigPermissions',
+                               'checkUserConfigPermissions',
+                               'checkPageRestrictions',
+                               'checkCascadingSourcesRestrictions',
+                               'checkActionPermissions',
+                               'checkUserBlock'
+                       ];
+               }
+
+               $errors = [];
+               foreach ( $checks as $method ) {
+                       $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
+
+                       if ( $short && $errors !== [] ) {
+                               break;
+                       }
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Check various permission hooks
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        * @throws FatalError
+        * @throws MWException
+        */
+       private function checkPermissionHooks(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // TODO: remove when LinkTarget usage will expand further
+               $page = Title::newFromLinkTarget( $page );
+               // Use getUserPermissionsErrors instead
+               $result = '';
+               if ( !Hooks::run( 'userCan', [ &$page, &$user, $action, &$result ] ) ) {
+                       return $result ? [] : [ [ 'badaccess-group0' ] ];
+               }
+               // Check getUserPermissionsErrors hook
+               if ( !Hooks::run( 'getUserPermissionsErrors', [ &$page, &$user, $action, &$result ] ) ) {
+                       $errors = $this->resultToError( $errors, $result );
+               }
+               // Check getUserPermissionsErrorsExpensive hook
+               if (
+                       $rigor !== self::RIGOR_QUICK
+                       && !( $short && count( $errors ) > 0 )
+                       && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$page, &$user, $action, &$result ] )
+               ) {
+                       $errors = $this->resultToError( $errors, $result );
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Add the resulting error code to the errors array
+        *
+        * @param array $errors List of current errors
+        * @param array $result Result of errors
+        *
+        * @return array List of errors
+        */
+       private function resultToError( $errors, $result ) {
+               if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
+                       // A single array representing an error
+                       $errors[] = $result;
+               } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
+                       // A nested array representing multiple errors
+                       $errors = array_merge( $errors, $result );
+               } elseif ( $result !== '' && is_string( $result ) ) {
+                       // A string representing a message-id
+                       $errors[] = [ $result ];
+               } elseif ( $result instanceof MessageSpecifier ) {
+                       // A message specifier representing an error
+                       $errors[] = [ $result ];
+               } elseif ( $result === false ) {
+                       // a generic "We don't want them to do that"
+                       $errors[] = [ 'badaccess-group0' ];
+               }
+               return $errors;
+       }
+
+       /**
+        * Check that the user is allowed to read this page.
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        * @throws FatalError
+        * @throws MWException
+        */
+       private function checkReadPermissions(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // TODO: remove when LinkTarget usage will expand further
+               $page = Title::newFromLinkTarget( $page );
+
+               $whitelisted = false;
+               if ( User::isEveryoneAllowed( 'read' ) ) {
+                       # Shortcut for public wikis, allows skipping quite a bit of code
+                       $whitelisted = true;
+               } elseif ( $user->isAllowed( 'read' ) ) {
+                       # If the user is allowed to read pages, he is allowed to read all pages
+                       $whitelisted = true;
+               } elseif ( $this->isSameSpecialPage( 'Userlogin', $page )
+                                  || $this->isSameSpecialPage( 'PasswordReset', $page )
+                                  || $this->isSameSpecialPage( 'Userlogout', $page )
+               ) {
+                       # 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 ) ) {
+                       # Time to check the whitelist
+                       # Only do these checks is there's something to check against
+                       $name = $page->getPrefixedText();
+                       $dbName = $page->getPrefixedDBkey();
+
+                       // Check for explicit whitelisting with and without underscores
+                       if ( in_array( $name, $this->whitelistRead, true )
+                                || in_array( $dbName, $this->whitelistRead, true ) ) {
+                               $whitelisted = true;
+                       } elseif ( $page->getNamespace() == NS_MAIN ) {
+                               # Old settings might have the title prefixed with
+                               # a colon for main-namespace pages
+                               if ( in_array( ':' . $name, $this->whitelistRead ) ) {
+                                       $whitelisted = true;
+                               }
+                       } elseif ( $page->isSpecialPage() ) {
+                               # If it's a special page, ditch the subpage bit and check again
+                               $name = $page->getDBkey();
+                               list( $name, /* $subpage */ ) =
+                                       $this->specialPageFactory->resolveAlias( $name );
+                               if ( $name ) {
+                                       $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
+                                       if ( in_array( $pure, $this->whitelistRead, true ) ) {
+                                               $whitelisted = true;
+                                       }
+                               }
+                       }
+               }
+
+               if ( !$whitelisted && is_array( $this->whitelistReadRegexp )
+                        && !empty( $this->whitelistReadRegexp ) ) {
+                       $name = $page->getPrefixedText();
+                       // Check for regex whitelisting
+                       foreach ( $this->whitelistReadRegexp as $listItem ) {
+                               if ( preg_match( $listItem, $name ) ) {
+                                       $whitelisted = true;
+                                       break;
+                               }
+                       }
+               }
+
+               if ( !$whitelisted ) {
+                       # If the title is not whitelisted, give extensions a chance to do so...
+                       Hooks::run( 'TitleReadWhitelist', [ $page, $user, &$whitelisted ] );
+                       if ( !$whitelisted ) {
+                               $errors[] = $this->missingPermissionError( $action, $short );
+                       }
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Get a description array when the user doesn't have the right to perform
+        * $action (i.e. when User::isAllowed() returns false)
+        *
+        * @param string $action The action to check
+        * @param bool $short Short circuit on first error
+        * @return array Array containing an error message key and any parameters
+        */
+       private function missingPermissionError( $action, $short ) {
+               // We avoid expensive display logic for quickUserCan's and such
+               if ( $short ) {
+                       return [ 'badaccess-group0' ];
+               }
+
+               // TODO: it would be a good idea to replace the method below with something else like
+               //  maybe callback injection
+               return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
+       }
+
+       /**
+        * Returns true if this title resolves to the named special page
+        *
+        * @param string $name The special page name
+        * @param LinkTarget $page
+        *
+        * @return bool
+        */
+       private function isSameSpecialPage( $name, LinkTarget $page ) {
+               if ( $page->getNamespace() == NS_SPECIAL ) {
+                       list( $thisName, /* $subpage */ ) =
+                               $this->specialPageFactory->resolveAlias( $page->getDBkey() );
+                       if ( $name == $thisName ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Check that the user isn't blocked from editing.
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        * @throws MWException
+        */
+       private function checkUserBlock(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // Account creation blocks handled at userlogin.
+               // Unblocking handled in SpecialUnblock
+               if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
+                       return $errors;
+               }
+
+               // Optimize for a very common case
+               if ( $action === 'read' && !$this->blockDisablesLogin ) {
+                       return $errors;
+               }
+
+               if ( $this->emailConfirmToEdit
+                        && !$user->isEmailConfirmed()
+                        && $action === 'edit'
+               ) {
+                       $errors[] = [ 'confirmedittext' ];
+               }
+
+               $useReplica = ( $rigor !== self::RIGOR_SECURE );
+               $block = $user->getBlock( $useReplica );
+
+               // If the user does not have a block, or the block they do have explicitly
+               // allows the action (like "read" or "upload").
+               if ( !$block || $block->appliesToRight( $action ) === false ) {
+                       return $errors;
+               }
+
+               // Determine if the user is blocked from this action on this page.
+               // What gets passed into this method is a user right, not an action name.
+               // There is no way to instantiate an action by restriction. However, this
+               // will get the action where the restriction is the same. This may result
+               // in actions being blocked that shouldn't be.
+               $actionObj = null;
+               if ( Action::exists( $action ) ) {
+                       // TODO: this drags a ton of dependencies in, would be good to avoid WikiPage
+                       //  instantiation and decouple it creating an ActionPermissionChecker interface
+                       $wikiPage = WikiPage::factory( Title::newFromLinkTarget( $page, 'clone' ) );
+                       // Creating an action will perform several database queries to ensure that
+                       // the action has not been overridden by the content type.
+                       // FIXME: avoid use of RequestContext since it drags in User and Title dependencies
+                       //  probably we may use fake context object since it's unlikely that Action uses it
+                       //  anyway. It would be nice if we could avoid instantiating the Action at all.
+                       $actionObj = Action::factory( $action, $wikiPage, RequestContext::getMain() );
+                       // Ensure that the retrieved action matches the restriction.
+                       if ( $actionObj && $actionObj->getRestriction() !== $action ) {
+                               $actionObj = null;
+                       }
+               }
+
+               // If no action object is returned, assume that the action requires unblock
+               // which is the default.
+               if ( !$actionObj || $actionObj->requiresUnblock() ) {
+                       if ( $this->isBlockedFrom( $user, $page, $useReplica ) ) {
+                               // @todo FIXME: Pass the relevant context into this function.
+                               $errors[] = $block->getPermissionsError( RequestContext::getMain() );
+                       }
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Permissions checks that fail most often, and which are easiest to test.
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        * @throws FatalError
+        * @throws MWException
+        */
+       private function checkQuickPermissions(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // TODO: remove when LinkTarget usage will expand further
+               $page = Title::newFromLinkTarget( $page );
+
+               if ( !Hooks::run( 'TitleQuickPermissions',
+                       [ $page, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] )
+               ) {
+                       return $errors;
+               }
+
+               $isSubPage = MWNamespace::hasSubpages( $page->getNamespace() ) ?
+                       strpos( $page->getText(), '/' ) !== false : false;
+
+               if ( $action == 'create' ) {
+                       if (
+                               ( MWNamespace::isTalk( $page->getNamespace() ) && !$user->isAllowed( 'createtalk' ) ) ||
+                               ( !MWNamespace::isTalk( $page->getNamespace() ) && !$user->isAllowed( 'createpage' ) )
+                       ) {
+                               $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
+                       }
+               } elseif ( $action == 'move' ) {
+                       if ( !$user->isAllowed( 'move-rootuserpages' )
+                                && $page->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 ( $page->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
+                               $errors[] = [ 'movenotallowedfile' ];
+                       }
+
+                       // Check if user is allowed to move category pages if it's a category page
+                       if ( $page->getNamespace() == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
+                               $errors[] = [ 'cant-move-category-page' ];
+                       }
+
+                       if ( !$user->isAllowed( 'move' ) ) {
+                               // User can't move anything
+                               $userCanMove = User::groupHasPermission( 'user', 'move' );
+                               $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
+                               if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
+                                       // custom message if logged-in users without any special rights can move
+                                       $errors[] = [ 'movenologintext' ];
+                               } else {
+                                       $errors[] = [ 'movenotallowed' ];
+                               }
+                       }
+               } elseif ( $action == 'move-target' ) {
+                       if ( !$user->isAllowed( 'move' ) ) {
+                               // User can't move anything
+                               $errors[] = [ 'movenotallowed' ];
+                       } elseif ( !$user->isAllowed( 'move-rootuserpages' )
+                                          && $page->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' )
+                                          && $page->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 ) ) {
+                       $errors[] = $this->missingPermissionError( $action, $short );
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Check against page_restrictions table requirements on this
+        * page. The user must possess all required rights for this
+        * action.
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        */
+       private function checkPageRestrictions(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // TODO: remove & rework upon further use of LinkTarget
+               $page = Title::newFromLinkTarget( $page );
+               foreach ( $page->getRestrictions( $action ) as $right ) {
+                       // Backwards compatibility, rewrite sysop -> editprotected
+                       if ( $right == 'sysop' ) {
+                               $right = 'editprotected';
+                       }
+                       // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
+                       if ( $right == 'autoconfirmed' ) {
+                               $right = 'editsemiprotected';
+                       }
+                       if ( $right == '' ) {
+                               continue;
+                       }
+                       if ( !$user->isAllowed( $right ) ) {
+                               $errors[] = [ 'protectedpagetext', $right, $action ];
+                       } elseif ( $page->areRestrictionsCascading() && !$user->isAllowed( 'protect' ) ) {
+                               $errors[] = [ 'protectedpagetext', 'protect', $action ];
+                       }
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Check restrictions on cascading pages.
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        */
+       private function checkCascadingSourcesRestrictions(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // TODO: remove & rework upon further use of LinkTarget
+               $page = Title::newFromLinkTarget( $page );
+               if ( $rigor !== self::RIGOR_QUICK && !$page->isUserConfigPage() ) {
+                       # We /could/ use the protection level on the source page, but it's
+                       # fairly ugly as we have to establish a precedence hierarchy for pages
+                       # included by multiple cascade-protected pages. So just restrict
+                       # it to people with 'protect' permission, as they could remove the
+                       # protection anyway.
+                       list( $cascadingSources, $restrictions ) = $page->getCascadeProtectionSources();
+                       # Cascading protection depends on more than this page...
+                       # Several cascading protected pages may include this page...
+                       # Check each cascading level
+                       # This is only for protection restrictions, not for all actions
+                       if ( isset( $restrictions[$action] ) ) {
+                               foreach ( $restrictions[$action] as $right ) {
+                                       // Backwards compatibility, rewrite sysop -> editprotected
+                                       if ( $right == 'sysop' ) {
+                                               $right = 'editprotected';
+                                       }
+                                       // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
+                                       if ( $right == 'autoconfirmed' ) {
+                                               $right = 'editsemiprotected';
+                                       }
+                                       if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
+                                               $wikiPages = '';
+                                               foreach ( $cascadingSources as $wikiPage ) {
+                                                       $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
+                                               }
+                                               $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
+                                       }
+                               }
+                       }
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Check action permissions not already checked in checkQuickPermissions
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        * @throws Exception
+        */
+       private function checkActionPermissions(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               global $wgDeleteRevisionsLimit, $wgLang;
+
+               // TODO: remove & rework upon further use of LinkTarget
+               $page = Title::newFromLinkTarget( $page );
+
+               if ( $action == 'protect' ) {
+                       if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
+                               // If they can't edit, they shouldn't protect.
+                               $errors[] = [ 'protect-cantedit' ];
+                       }
+               } elseif ( $action == 'create' ) {
+                       $title_protection = $page->getTitleProtection();
+                       if ( $title_protection ) {
+                               if ( $title_protection['permission'] == ''
+                                        || !$user->isAllowed( $title_protection['permission'] )
+                               ) {
+                                       $errors[] = [
+                                               'titleprotected',
+                                               // TODO: get rid of the User dependency
+                                               User::whoIs( $title_protection['user'] ),
+                                               $title_protection['reason']
+                                       ];
+                               }
+                       }
+               } elseif ( $action == 'move' ) {
+                       // Check for immobile pages
+                       if ( !MWNamespace::isMovable( $page->getNamespace() ) ) {
+                               // Specific message for this case
+                               $errors[] = [ 'immobile-source-namespace', $page->getNsText() ];
+                       } elseif ( !$page->isMovable() ) {
+                               // Less specific message for rarer cases
+                               $errors[] = [ 'immobile-source-page' ];
+                       }
+               } elseif ( $action == 'move-target' ) {
+                       if ( !MWNamespace::isMovable( $page->getNamespace() ) ) {
+                               $errors[] = [ 'immobile-target-namespace', $page->getNsText() ];
+                       } elseif ( !$page->isMovable() ) {
+                               $errors[] = [ 'immobile-target-page' ];
+                       }
+               } elseif ( $action == 'delete' ) {
+                       $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $page );
+                       if ( !$tempErrors ) {
+                               $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
+                                       $user, $tempErrors, $rigor, true, $page );
+                       }
+                       if ( $tempErrors ) {
+                               // If protection keeps them from editing, they shouldn't be able to delete.
+                               $errors[] = [ 'deleteprotected' ];
+                       }
+                       if ( $rigor !== self::RIGOR_QUICK && $wgDeleteRevisionsLimit
+                                && !$this->userCan( 'bigdelete', $user, $page ) && $page->isBigDeletion()
+                       ) {
+                               $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
+                       }
+               } elseif ( $action === 'undelete' ) {
+                       if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
+                               // Undeleting implies editing
+                               $errors[] = [ 'undelete-cantedit' ];
+                       }
+                       if ( !$page->exists()
+                                && count( $this->getPermissionErrorsInternal( 'create', $user, $page, $rigor, true ) )
+                       ) {
+                               // Undeleting where nothing currently exists implies creating
+                               $errors[] = [ 'undelete-cantcreate' ];
+                       }
+               }
+               return $errors;
+       }
+
+       /**
+        * Check permissions on special pages & namespaces
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        */
+       private function checkSpecialsAndNSPermissions(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // TODO: remove & rework upon further use of LinkTarget
+               $page = Title::newFromLinkTarget( $page );
+
+               # Only 'createaccount' can be performed on special pages,
+               # which don't actually exist in the DB.
+               if ( $page->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) {
+                       $errors[] = [ 'ns-specialprotected' ];
+               }
+
+               # Check $wgNamespaceProtection for restricted namespaces
+               if ( $page->isNamespaceProtected( $user ) ) {
+                       $ns = $page->getNamespace() == NS_MAIN ?
+                               wfMessage( 'nstab-main' )->text() : $page->getNsText();
+                       $errors[] = $page->getNamespace() == NS_MEDIAWIKI ?
+                               [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Check sitewide CSS/JSON/JS permissions
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        */
+       private function checkSiteConfigPermissions(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // TODO: remove & rework upon further use of LinkTarget
+               $page = Title::newFromLinkTarget( $page );
+
+               if ( $action != 'patrol' ) {
+                       $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 ( $page->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
+                               $error = [ 'sitecssprotected', $action ];
+                       } elseif ( $page->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
+                               $error = [ 'sitejsonprotected', $action ];
+                       } elseif ( $page->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
+                               $error = [ 'sitejsprotected', $action ];
+                       } elseif ( $page->isRawHtmlMessage() ) {
+                               // Raw HTML can be used to deploy CSS or JS so require rights for both.
+                               if ( !$user->isAllowed( 'editsitejs' ) ) {
+                                       $error = [ 'sitejsprotected', $action ];
+                               } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
+                                       $error = [ 'sitecssprotected', $action ];
+                               }
+                       }
+
+                       if ( $error ) {
+                               if ( $user->isAllowed( '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
+                                       $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
+                               }
+                               $errors[] = $error;
+                       }
+               }
+
+               return $errors;
+       }
+
+       /**
+        * Check CSS/JSON/JS sub-page permissions
+        *
+        * @param string $action The action to check
+        * @param User $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)
+        *   - RIGOR_FULL   : does cheap and expensive checks possibly from a replica DB
+        *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+        * @param bool $short Short circuit on first error
+        *
+        * @param LinkTarget $page
+        *
+        * @return array List of errors
+        */
+       private function checkUserConfigPermissions(
+               $action,
+               User $user,
+               $errors,
+               $rigor,
+               $short,
+               LinkTarget $page
+       ) {
+               // TODO: remove & rework upon further use of LinkTarget
+               $page = Title::newFromLinkTarget( $page );
+
+               # Protect css/json/js subpages of user pages
+               # XXX: this might be better using restrictions
+
+               if ( $action === 'patrol' ) {
+                       return $errors;
+               }
+
+               if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $page->getText() ) ) {
+                       // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
+                       if (
+                               $page->isUserCssConfigPage()
+                               && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
+                       ) {
+                               $errors[] = [ 'mycustomcssprotected', $action ];
+                       } elseif (
+                               $page->isUserJsonConfigPage()
+                               && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
+                       ) {
+                               $errors[] = [ 'mycustomjsonprotected', $action ];
+                       } elseif (
+                               $page->isUserJsConfigPage()
+                               && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
+                       ) {
+                               $errors[] = [ 'mycustomjsprotected', $action ];
+                       }
+               } else {
+                       // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
+                       // deletion/suppression which cannot be used for attacks and we want to avoid the
+                       // situation where an unprivileged user can post abusive content on their subpages
+                       // and only very highly privileged users could remove it.
+                       if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
+                               if (
+                                       $page->isUserCssConfigPage()
+                                       && !$user->isAllowed( 'editusercss' )
+                               ) {
+                                       $errors[] = [ 'customcssprotected', $action ];
+                               } elseif (
+                                       $page->isUserJsonConfigPage()
+                                       && !$user->isAllowed( 'edituserjson' )
+                               ) {
+                                       $errors[] = [ 'customjsonprotected', $action ];
+                               } elseif (
+                                       $page->isUserJsConfigPage()
+                                       && !$user->isAllowed( 'edituserjs' )
+                               ) {
+                                       $errors[] = [ 'customjsprotected', $action ];
+                               }
+                       }
+               }
+
+               return $errors;
+       }
+
+}
index e121898..cccb5e7 100644 (file)
@@ -46,6 +46,7 @@ use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Linker\LinkRendererFactory;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Preferences\PreferencesFactory;
 use MediaWiki\Preferences\DefaultPreferencesFactory;
 use MediaWiki\Revision\MainSlotRoleHandler;
@@ -389,6 +390,16 @@ return [
                );
        },
 
+       'PermissionManager' => function ( MediaWikiServices $services ) : PermissionManager {
+               $config = $services->getMainConfig();
+               return new PermissionManager(
+                       $services->getSpecialPageFactory(),
+                       $config->get( 'WhitelistRead' ),
+                       $config->get( 'WhitelistReadRegexp' ),
+                       $config->get( 'EmailConfirmToEdit' ),
+                       $config->get( 'BlockDisablesLogin' ) );
+       },
+
        'PreferencesFactory' => function ( MediaWikiServices $services ) : PreferencesFactory {
                $factory = new DefaultPreferencesFactory(
                        $services->getMainConfig(),
index f53a47e..d517b85 100644 (file)
@@ -22,6 +22,7 @@
  * @file
  */
 
+use MediaWiki\Permissions\PermissionManager;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
@@ -2124,7 +2125,13 @@ class Title implements LinkTarget, IDBAccessObject {
         *
         * @param string $action Action that permission needs to be checked for
         * @param User|null $user User to check (since 1.19); $wgUser will be used if not provided.
+        *
         * @return bool
+        * @throws Exception
+        *
+        * @deprecated since 1.33,
+        * use MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(..) instead
+        *
         */
        public function quickUserCan( $action, $user = null ) {
                return $this->userCan( $action, $user, false );
@@ -2137,15 +2144,29 @@ class Title implements LinkTarget, IDBAccessObject {
         * @param User|null $user User to check (since 1.19); $wgUser will be used if not
         *   provided.
         * @param string $rigor Same format as Title::getUserPermissionsErrors()
+        *
         * @return bool
+        * @throws Exception
+        *
+        * @deprecated since 1.33,
+        * use MediaWikiServices::getInstance()->getPermissionManager()->userCan(..) instead
+        *
         */
-       public function userCan( $action, $user = null, $rigor = 'secure' ) {
+       public function userCan( $action, $user = null, $rigor = PermissionManager::RIGOR_SECURE ) {
                if ( !$user instanceof User ) {
                        global $wgUser;
                        $user = $wgUser;
                }
 
-               return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
+               // TODO: this is for b/c, eventually will be removed
+               if ( $rigor === true ) {
+                       $rigor = PermissionManager::RIGOR_SECURE; // b/c
+               } elseif ( $rigor === false ) {
+                       $rigor = PermissionManager::RIGOR_QUICK; // b/c
+               }
+
+               return MediaWikiServices::getInstance()->getPermissionManager()
+                       ->userCan( $action, $user, $this, $rigor );
        }
 
        /**
@@ -2161,99 +2182,26 @@ class Title implements LinkTarget, IDBAccessObject {
         *   - secure : does cheap and expensive checks, using the master as needed
         * @param array $ignoreErrors Array of Strings Set this to a list of message keys
         *   whose corresponding errors may be ignored.
+        *
         * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
-        */
-       public function getUserPermissionsErrors(
-               $action, $user, $rigor = 'secure', $ignoreErrors = []
-       ) {
-               $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
-
-               // Remove the errors being ignored.
-               foreach ( $errors as $index => $error ) {
-                       $errKey = is_array( $error ) ? $error[0] : $error;
-
-                       if ( in_array( $errKey, $ignoreErrors ) ) {
-                               unset( $errors[$index] );
-                       }
-                       if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
-                               unset( $errors[$index] );
-                       }
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Permissions checks that fail most often, and which are easiest to test.
+        * @throws Exception
         *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
+        * @deprecated since 1.33,
+        * use MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissionsErrors()
         *
-        * @return array List of errors
         */
-       private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
-               if ( !Hooks::run( 'TitleQuickPermissions',
-                       [ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
-               ) {
-                       return $errors;
-               }
-
-               if ( $action == 'create' ) {
-                       if (
-                               ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
-                               ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
-                       ) {
-                               $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
-                       }
-               } elseif ( $action == 'move' ) {
-                       if ( !$user->isAllowed( 'move-rootuserpages' )
-                                       && $this->mNamespace == NS_USER && !$this->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 ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
-                               $errors[] = [ 'movenotallowedfile' ];
-                       }
-
-                       // Check if user is allowed to move category pages if it's a category page
-                       if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
-                               $errors[] = [ 'cant-move-category-page' ];
-                       }
-
-                       if ( !$user->isAllowed( 'move' ) ) {
-                               // User can't move anything
-                               $userCanMove = User::groupHasPermission( 'user', 'move' );
-                               $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
-                               if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
-                                       // custom message if logged-in users without any special rights can move
-                                       $errors[] = [ 'movenologintext' ];
-                               } else {
-                                       $errors[] = [ 'movenotallowed' ];
-                               }
-                       }
-               } elseif ( $action == 'move-target' ) {
-                       if ( !$user->isAllowed( 'move' ) ) {
-                               // User can't move anything
-                               $errors[] = [ 'movenotallowed' ];
-                       } elseif ( !$user->isAllowed( 'move-rootuserpages' )
-                                       && $this->mNamespace == NS_USER && !$this->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' )
-                                       && $this->mNamespace == 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 ) ) {
-                       $errors[] = $this->missingPermissionError( $action, $short );
+       public function getUserPermissionsErrors(
+               $action, $user, $rigor = PermissionManager::RIGOR_SECURE, $ignoreErrors = []
+       ) {
+               // TODO: this is for b/c, eventually will be removed
+               if ( $rigor === true ) {
+                       $rigor = PermissionManager::RIGOR_SECURE; // b/c
+               } elseif ( $rigor === false ) {
+                       $rigor = PermissionManager::RIGOR_QUICK; // b/c
                }
 
-               return $errors;
+               return MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors );
        }
 
        /**
@@ -2284,582 +2232,6 @@ class Title implements LinkTarget, IDBAccessObject {
                return $errors;
        }
 
-       /**
-        * Check various permission hooks
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
-               // Use getUserPermissionsErrors instead
-               $result = '';
-               // Avoid PHP 7.1 warning from passing $this by reference
-               $titleRef = $this;
-               if ( !Hooks::run( 'userCan', [ &$titleRef, &$user, $action, &$result ] ) ) {
-                       return $result ? [] : [ [ 'badaccess-group0' ] ];
-               }
-               // Check getUserPermissionsErrors hook
-               // Avoid PHP 7.1 warning from passing $this by reference
-               $titleRef = $this;
-               if ( !Hooks::run( 'getUserPermissionsErrors', [ &$titleRef, &$user, $action, &$result ] ) ) {
-                       $errors = $this->resultToError( $errors, $result );
-               }
-               // Check getUserPermissionsErrorsExpensive hook
-               if (
-                       $rigor !== 'quick'
-                       && !( $short && count( $errors ) > 0 )
-                       && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$titleRef, &$user, $action, &$result ] )
-               ) {
-                       $errors = $this->resultToError( $errors, $result );
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Check permissions on special pages & namespaces
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
-               # Only 'createaccount' can be performed on special pages,
-               # which don't actually exist in the DB.
-               if ( $this->isSpecialPage() && $action !== 'createaccount' ) {
-                       $errors[] = [ 'ns-specialprotected' ];
-               }
-
-               # Check $wgNamespaceProtection for restricted namespaces
-               if ( $this->isNamespaceProtected( $user ) ) {
-                       $ns = $this->mNamespace == NS_MAIN ?
-                               wfMessage( 'nstab-main' )->text() : $this->getNsText();
-                       $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
-                               [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Check sitewide CSS/JSON/JS permissions
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short ) {
-               if ( $action != 'patrol' ) {
-                       $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 ( $this->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
-                               $error = [ 'sitecssprotected', $action ];
-                       } elseif ( $this->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
-                               $error = [ 'sitejsonprotected', $action ];
-                       } elseif ( $this->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
-                               $error = [ 'sitejsprotected', $action ];
-                       } elseif ( $this->isRawHtmlMessage() ) {
-                               // Raw HTML can be used to deploy CSS or JS so require rights for both.
-                               if ( !$user->isAllowed( 'editsitejs' ) ) {
-                                       $error = [ 'sitejsprotected', $action ];
-                               } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
-                                       $error = [ 'sitecssprotected', $action ];
-                               }
-                       }
-
-                       if ( $error ) {
-                               if ( $user->isAllowed( '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
-                                       $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
-                               }
-                               $errors[] = $error;
-                       }
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Check CSS/JSON/JS sub-page permissions
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) {
-               # Protect css/json/js subpages of user pages
-               # XXX: this might be better using restrictions
-
-               if ( $action === 'patrol' ) {
-                       return $errors;
-               }
-
-               if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
-                       // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
-                       if (
-                               $this->isUserCssConfigPage()
-                               && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
-                       ) {
-                               $errors[] = [ 'mycustomcssprotected', $action ];
-                       } elseif (
-                               $this->isUserJsonConfigPage()
-                               && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
-                       ) {
-                               $errors[] = [ 'mycustomjsonprotected', $action ];
-                       } elseif (
-                               $this->isUserJsConfigPage()
-                               && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
-                       ) {
-                               $errors[] = [ 'mycustomjsprotected', $action ];
-                       }
-               } else {
-                       // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
-                       // deletion/suppression which cannot be used for attacks and we want to avoid the
-                       // situation where an unprivileged user can post abusive content on their subpages
-                       // and only very highly privileged users could remove it.
-                       if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
-                               if (
-                                       $this->isUserCssConfigPage()
-                                       && !$user->isAllowed( 'editusercss' )
-                               ) {
-                                       $errors[] = [ 'customcssprotected', $action ];
-                               } elseif (
-                                       $this->isUserJsonConfigPage()
-                                       && !$user->isAllowed( 'edituserjson' )
-                               ) {
-                                       $errors[] = [ 'customjsonprotected', $action ];
-                               } elseif (
-                                       $this->isUserJsConfigPage()
-                                       && !$user->isAllowed( 'edituserjs' )
-                               ) {
-                                       $errors[] = [ 'customjsprotected', $action ];
-                               }
-                       }
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Check against page_restrictions table requirements on this
-        * page. The user must possess all required rights for this
-        * action.
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
-               foreach ( $this->getRestrictions( $action ) as $right ) {
-                       // Backwards compatibility, rewrite sysop -> editprotected
-                       if ( $right == 'sysop' ) {
-                               $right = 'editprotected';
-                       }
-                       // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
-                       if ( $right == 'autoconfirmed' ) {
-                               $right = 'editsemiprotected';
-                       }
-                       if ( $right == '' ) {
-                               continue;
-                       }
-                       if ( !$user->isAllowed( $right ) ) {
-                               $errors[] = [ 'protectedpagetext', $right, $action ];
-                       } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
-                               $errors[] = [ 'protectedpagetext', 'protect', $action ];
-                       }
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Check restrictions on cascading pages.
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
-               if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) {
-                       # We /could/ use the protection level on the source page, but it's
-                       # fairly ugly as we have to establish a precedence hierarchy for pages
-                       # included by multiple cascade-protected pages. So just restrict
-                       # it to people with 'protect' permission, as they could remove the
-                       # protection anyway.
-                       list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
-                       # Cascading protection depends on more than this page...
-                       # Several cascading protected pages may include this page...
-                       # Check each cascading level
-                       # This is only for protection restrictions, not for all actions
-                       if ( isset( $restrictions[$action] ) ) {
-                               foreach ( $restrictions[$action] as $right ) {
-                                       // Backwards compatibility, rewrite sysop -> editprotected
-                                       if ( $right == 'sysop' ) {
-                                               $right = 'editprotected';
-                                       }
-                                       // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
-                                       if ( $right == 'autoconfirmed' ) {
-                                               $right = 'editsemiprotected';
-                                       }
-                                       if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
-                                               $pages = '';
-                                               foreach ( $cascadingSources as $page ) {
-                                                       $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
-                                               }
-                                               $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
-                                       }
-                               }
-                       }
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Check action permissions not already checked in checkQuickPermissions
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
-               global $wgDeleteRevisionsLimit, $wgLang;
-
-               if ( $action == 'protect' ) {
-                       if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
-                               // If they can't edit, they shouldn't protect.
-                               $errors[] = [ 'protect-cantedit' ];
-                       }
-               } elseif ( $action == 'create' ) {
-                       $title_protection = $this->getTitleProtection();
-                       if ( $title_protection ) {
-                               if ( $title_protection['permission'] == ''
-                                       || !$user->isAllowed( $title_protection['permission'] )
-                               ) {
-                                       $errors[] = [
-                                               'titleprotected',
-                                               User::whoIs( $title_protection['user'] ),
-                                               $title_protection['reason']
-                                       ];
-                               }
-                       }
-               } elseif ( $action == 'move' ) {
-                       // Check for immobile pages
-                       if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
-                               // Specific message for this case
-                               $errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
-                       } elseif ( !$this->isMovable() ) {
-                               // Less specific message for rarer cases
-                               $errors[] = [ 'immobile-source-page' ];
-                       }
-               } elseif ( $action == 'move-target' ) {
-                       if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
-                               $errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
-                       } elseif ( !$this->isMovable() ) {
-                               $errors[] = [ 'immobile-target-page' ];
-                       }
-               } elseif ( $action == 'delete' ) {
-                       $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
-                       if ( !$tempErrors ) {
-                               $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
-                                       $user, $tempErrors, $rigor, true );
-                       }
-                       if ( $tempErrors ) {
-                               // If protection keeps them from editing, they shouldn't be able to delete.
-                               $errors[] = [ 'deleteprotected' ];
-                       }
-                       if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
-                               && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
-                       ) {
-                               $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
-                       }
-               } elseif ( $action === 'undelete' ) {
-                       if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
-                               // Undeleting implies editing
-                               $errors[] = [ 'undelete-cantedit' ];
-                       }
-                       if ( !$this->exists()
-                               && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) )
-                       ) {
-                               // Undeleting where nothing currently exists implies creating
-                               $errors[] = [ 'undelete-cantcreate' ];
-                       }
-               }
-               return $errors;
-       }
-
-       /**
-        * Check that the user isn't blocked from editing.
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
-               global $wgEmailConfirmToEdit, $wgBlockDisablesLogin;
-               // Account creation blocks handled at userlogin.
-               // Unblocking handled in SpecialUnblock
-               if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
-                       return $errors;
-               }
-
-               // Optimize for a very common case
-               if ( $action === 'read' && !$wgBlockDisablesLogin ) {
-                       return $errors;
-               }
-
-               if ( $wgEmailConfirmToEdit
-                       && !$user->isEmailConfirmed()
-                       && $action === 'edit'
-               ) {
-                       $errors[] = [ 'confirmedittext' ];
-               }
-
-               $useReplica = ( $rigor !== 'secure' );
-               $block = $user->getBlock( $useReplica );
-
-               // If the user does not have a block, or the block they do have explicitly
-               // allows the action (like "read" or "upload").
-               if ( !$block || $block->appliesToRight( $action ) === false ) {
-                       return $errors;
-               }
-
-               // Determine if the user is blocked from this action on this page.
-               // What gets passed into this method is a user right, not an action name.
-               // There is no way to instantiate an action by restriction. However, this
-               // will get the action where the restriction is the same. This may result
-               // in actions being blocked that shouldn't be.
-               $actionObj = null;
-               if ( Action::exists( $action ) ) {
-                       // Clone the title to prevent mutations to this object which is done
-                       // by Title::loadFromRow() in WikiPage::loadFromRow().
-                       $page = WikiPage::factory( clone $this );
-                       // Creating an action will perform several database queries to ensure that
-                       // the action has not been overridden by the content type.
-                       // @todo FIXME: Pass the relevant context into this function.
-                       $actionObj = Action::factory( $action, $page, RequestContext::getMain() );
-                       // Ensure that the retrieved action matches the restriction.
-                       if ( $actionObj && $actionObj->getRestriction() !== $action ) {
-                               $actionObj = null;
-                       }
-               }
-
-               // If no action object is returned, assume that the action requires unblock
-               // which is the default.
-               if ( !$actionObj || $actionObj->requiresUnblock() ) {
-                       if ( $user->isBlockedFrom( $this, $useReplica ) ) {
-                               // @todo FIXME: Pass the relevant context into this function.
-                               $errors[] = $block->getPermissionsError( RequestContext::getMain() );
-                       }
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Check that the user is allowed to read this page.
-        *
-        * @param string $action The action to check
-        * @param User $user User to check
-        * @param array $errors List of current errors
-        * @param string $rigor Same format as Title::getUserPermissionsErrors()
-        * @param bool $short Short circuit on first error
-        *
-        * @return array List of errors
-        */
-       private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
-               global $wgWhitelistRead, $wgWhitelistReadRegexp;
-
-               $whitelisted = false;
-               if ( User::isEveryoneAllowed( 'read' ) ) {
-                       # Shortcut for public wikis, allows skipping quite a bit of code
-                       $whitelisted = true;
-               } elseif ( $user->isAllowed( 'read' ) ) {
-                       # If the user is allowed to read pages, he is allowed to read all pages
-                       $whitelisted = true;
-               } elseif ( $this->isSpecial( 'Userlogin' )
-                       || $this->isSpecial( 'PasswordReset' )
-                       || $this->isSpecial( 'Userlogout' )
-               ) {
-                       # Always grant access to the login page.
-                       # Even anons need to be able to log in.
-                       $whitelisted = true;
-               } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
-                       # Time to check the whitelist
-                       # Only do these checks is there's something to check against
-                       $name = $this->getPrefixedText();
-                       $dbName = $this->getPrefixedDBkey();
-
-                       // Check for explicit whitelisting with and without underscores
-                       if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
-                               $whitelisted = true;
-                       } elseif ( $this->mNamespace == NS_MAIN ) {
-                               # Old settings might have the title prefixed with
-                               # a colon for main-namespace pages
-                               if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
-                                       $whitelisted = true;
-                               }
-                       } elseif ( $this->isSpecialPage() ) {
-                               # If it's a special page, ditch the subpage bit and check again
-                               $name = $this->mDbkeyform;
-                               list( $name, /* $subpage */ ) =
-                                       MediaWikiServices::getInstance()->getSpecialPageFactory()->
-                                               resolveAlias( $name );
-                               if ( $name ) {
-                                       $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
-                                       if ( in_array( $pure, $wgWhitelistRead, true ) ) {
-                                               $whitelisted = true;
-                                       }
-                               }
-                       }
-               }
-
-               if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
-                       $name = $this->getPrefixedText();
-                       // Check for regex whitelisting
-                       foreach ( $wgWhitelistReadRegexp as $listItem ) {
-                               if ( preg_match( $listItem, $name ) ) {
-                                       $whitelisted = true;
-                                       break;
-                               }
-                       }
-               }
-
-               if ( !$whitelisted ) {
-                       # If the title is not whitelisted, give extensions a chance to do so...
-                       Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
-                       if ( !$whitelisted ) {
-                               $errors[] = $this->missingPermissionError( $action, $short );
-                       }
-               }
-
-               return $errors;
-       }
-
-       /**
-        * Get a description array when the user doesn't have the right to perform
-        * $action (i.e. when User::isAllowed() returns false)
-        *
-        * @param string $action The action to check
-        * @param bool $short Short circuit on first error
-        * @return array Array containing an error message key and any parameters
-        */
-       private function missingPermissionError( $action, $short ) {
-               // We avoid expensive display logic for quickUserCan's and such
-               if ( $short ) {
-                       return [ 'badaccess-group0' ];
-               }
-
-               return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
-       }
-
-       /**
-        * Can $user perform $action on this page? This is an internal function,
-        * with multiple levels of checks depending on performance needs; see $rigor below.
-        * It does not check wfReadOnly().
-        *
-        * @param string $action Action that permission needs to be checked for
-        * @param User $user User to check
-        * @param string $rigor One of (quick,full,secure)
-        *   - quick  : does cheap permission checks from replica DBs (usable for GUI creation)
-        *   - full   : does cheap and expensive checks possibly from a replica DB
-        *   - secure : does cheap and expensive checks, using the master as needed
-        * @param bool $short Set this to true to stop after the first permission error.
-        * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
-        */
-       protected function getUserPermissionsErrorsInternal(
-               $action, $user, $rigor = 'secure', $short = false
-       ) {
-               if ( $rigor === true ) {
-                       $rigor = 'secure'; // b/c
-               } elseif ( $rigor === false ) {
-                       $rigor = 'quick'; // b/c
-               } elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
-                       throw new Exception( "Invalid rigor parameter '$rigor'." );
-               }
-
-               # Read has special handling
-               if ( $action == 'read' ) {
-                       $checks = [
-                               'checkPermissionHooks',
-                               'checkReadPermissions',
-                               'checkUserBlock', // for wgBlockDisablesLogin
-                       ];
-               # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
-               # or checkUserConfigPermissions here as it will lead to duplicate
-               # error messages. This is okay to do since anywhere that checks for
-               # create will also check for edit, and those checks are called for edit.
-               } elseif ( $action == 'create' ) {
-                       $checks = [
-                               'checkQuickPermissions',
-                               'checkPermissionHooks',
-                               'checkPageRestrictions',
-                               'checkCascadingSourcesRestrictions',
-                               'checkActionPermissions',
-                               'checkUserBlock'
-                       ];
-               } else {
-                       $checks = [
-                               'checkQuickPermissions',
-                               'checkPermissionHooks',
-                               'checkSpecialsAndNSPermissions',
-                               'checkSiteConfigPermissions',
-                               'checkUserConfigPermissions',
-                               'checkPageRestrictions',
-                               'checkCascadingSourcesRestrictions',
-                               'checkActionPermissions',
-                               'checkUserBlock'
-                       ];
-               }
-
-               $errors = [];
-               foreach ( $checks as $method ) {
-                       $errors = $this->$method( $action, $user, $errors, $rigor, $short );
-
-                       if ( $short && $errors !== [] ) {
-                               break;
-                       }
-               }
-
-               return $errors;
-       }
-
        /**
         * Get a filtered list of all restriction types supported by this wiki.
         * @param bool $exists True to get all restriction types that apply to
index c191c70..311cac2 100644 (file)
@@ -2291,29 +2291,15 @@ class User implements IDBAccessObject, UserIdentity {
         * @param Title $title Title to check
         * @param bool $fromReplica Whether to check the replica DB instead of the master
         * @return bool
+        * @throws MWException
+        *
+        * @deprecated since 1.33,
+        * use MediaWikiServices::getInstance()->getPermissionManager()->isBlockedFrom(..)
+        *
         */
        public function isBlockedFrom( $title, $fromReplica = false ) {
-               $blocked = $this->isHidden();
-
-               if ( !$blocked ) {
-                       $block = $this->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( $this->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 = $this->mAllowUsertalk;
-
-               Hooks::run( 'UserIsBlockedFrom', [ $this, $title, &$blocked, &$allowUsertalk ] );
-
-               return $blocked;
+               return MediaWikiServices::getInstance()->getPermissionManager()
+                       ->isBlockedFrom( $this, $title, $fromReplica );
        }
 
        /**
@@ -5741,4 +5727,14 @@ class User implements IDBAccessObject, UserIdentity {
                // XXX it's not clear whether central ID providers are supposed to obey this
                return $this->getName() === $user->getName();
        }
+
+       /**
+        * Checks if usertalk is allowed
+        *
+        * @return bool
+        */
+       public function isAllowUsertalk() {
+               return $this->mAllowUsertalk;
+       }
+
 }
diff --git a/tests/phpunit/includes/Permissions/PermissionManagerTest.php b/tests/phpunit/includes/Permissions/PermissionManagerTest.php
new file mode 100644 (file)
index 0000000..7f5ec40
--- /dev/null
@@ -0,0 +1,1410 @@
+<?php
+
+namespace MediaWiki\Tests\Permissions;
+
+use Action;
+use Block;
+use MediaWikiLangTestCase;
+use RequestContext;
+use Title;
+use User;
+use MediaWiki\Block\Restriction\NamespaceRestriction;
+use MediaWiki\Block\Restriction\PageRestriction;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Permissions\PermissionManager;
+
+/**
+ * @group Database
+ *
+ * @covers \MediaWiki\Permissions\PermissionManager
+ */
+class PermissionManagerTest extends MediaWikiLangTestCase {
+
+       /**
+        * @var string
+        */
+       protected $userName, $altUserName;
+
+       /**
+        * @var Title
+        */
+       protected $title;
+
+       /**
+        * @var User
+        */
+       protected $user, $anonUser, $userUser, $altUser;
+
+       /**
+        * @var PermissionManager
+        */
+       protected $permissionManager;
+
+       /** Constant for self::testIsBlockedFrom */
+       const USER_TALK_PAGE = '<user talk page>';
+
+       protected function setUp() {
+               parent::setUp();
+
+               $localZone = 'UTC';
+               $localOffset = date( 'Z' ) / 60;
+
+               $this->setMwGlobals( [
+                       'wgLocaltimezone' => $localZone,
+                       'wgLocalTZoffset' => $localOffset,
+                       'wgNamespaceProtection' => [
+                               NS_MEDIAWIKI => 'editinterface',
+                       ],
+               ] );
+               // Without this testUserBlock will use a non-English context on non-English MediaWiki
+               // installations (because of how Title::checkUserBlock is implemented) and fail.
+               RequestContext::resetMain();
+
+               $this->userName = 'Useruser';
+               $this->altUserName = 'Altuseruser';
+               date_default_timezone_set( $localZone );
+
+               $this->title = Title::makeTitle( NS_MAIN, "Main Page" );
+               if ( !isset( $this->userUser ) || !( $this->userUser instanceof User ) ) {
+                       $this->userUser = User::newFromName( $this->userName );
+
+                       if ( !$this->userUser->getId() ) {
+                               $this->userUser = User::createNew( $this->userName, [
+                                       "email" => "test@example.com",
+                                       "real_name" => "Test User" ] );
+                               $this->userUser->load();
+                       }
+
+                       $this->altUser = User::newFromName( $this->altUserName );
+                       if ( !$this->altUser->getId() ) {
+                               $this->altUser = User::createNew( $this->altUserName, [
+                                       "email" => "alttest@example.com",
+                                       "real_name" => "Test User Alt" ] );
+                               $this->altUser->load();
+                       }
+
+                       $this->anonUser = User::newFromId( 0 );
+
+                       $this->user = $this->userUser;
+               }
+
+               $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+               $this->overrideMwServices();
+       }
+
+       protected function setUserPerm( $perm ) {
+               // Setting member variables is evil!!!
+
+               if ( is_array( $perm ) ) {
+                       $this->user->mRights = $perm;
+               } else {
+                       $this->user->mRights = [ $perm ];
+               }
+       }
+
+       protected function setTitle( $ns, $title = "Main_Page" ) {
+               $this->title = Title::makeTitle( $ns, $title );
+       }
+
+       protected function setUser( $userName = null ) {
+               if ( $userName === 'anon' ) {
+                       $this->user = $this->anonUser;
+               } elseif ( $userName === null || $userName === $this->userName ) {
+                       $this->user = $this->userUser;
+               } else {
+                       $this->user = $this->altUser;
+               }
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        *
+        * This test is failing per T201776.
+        *
+        * @group Broken
+        * @covers \MediaWiki\Permissions\PermissionManager::checkQuickPermissions
+        */
+       public function testQuickPermissions() {
+               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
+               getFormattedNsText( NS_PROJECT );
+
+               $this->setUser( 'anon' );
+               $this->setTitle( NS_TALK );
+               $this->setUserPerm( "createtalk" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [], $res );
+
+               $this->setTitle( NS_TALK );
+               $this->setUserPerm( "createpage" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [ [ "nocreatetext" ] ], $res );
+
+               $this->setTitle( NS_TALK );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [ [ 'nocreatetext' ] ], $res );
+
+               $this->setTitle( NS_MAIN );
+               $this->setUserPerm( "createpage" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [], $res );
+
+               $this->setTitle( NS_MAIN );
+               $this->setUserPerm( "createtalk" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [ [ 'nocreatetext' ] ], $res );
+
+               $this->setUser( $this->userName );
+               $this->setTitle( NS_TALK );
+               $this->setUserPerm( "createtalk" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [], $res );
+
+               $this->setTitle( NS_TALK );
+               $this->setUserPerm( "createpage" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
+
+               $this->setTitle( NS_TALK );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
+
+               $this->setTitle( NS_MAIN );
+               $this->setUserPerm( "createpage" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [], $res );
+
+               $this->setTitle( NS_MAIN );
+               $this->setUserPerm( "createtalk" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
+
+               $this->setTitle( NS_MAIN );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'create', $this->user, $this->title );
+               $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
+
+               $this->setUser( 'anon' );
+               $this->setTitle( NS_USER, $this->userName . '' );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res );
+
+               $this->setTitle( NS_USER, $this->userName . '/subpage' );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+               $this->setTitle( NS_USER, $this->userName . '' );
+               $this->setUserPerm( "move-rootuserpages" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+               $this->setTitle( NS_USER, $this->userName . '/subpage' );
+               $this->setUserPerm( "move-rootuserpages" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+               $this->setTitle( NS_USER, $this->userName . '' );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res );
+
+               $this->setTitle( NS_USER, $this->userName . '/subpage' );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+               $this->setTitle( NS_USER, $this->userName . '' );
+               $this->setUserPerm( "move-rootuserpages" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+               $this->setTitle( NS_USER, $this->userName . '/subpage' );
+               $this->setUserPerm( "move-rootuserpages" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+               $this->setUser( $this->userName );
+               $this->setTitle( NS_FILE, "img.png" );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ], $res );
+
+               $this->setTitle( NS_FILE, "img.png" );
+               $this->setUserPerm( "movefile" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenotallowed' ] ], $res );
+
+               $this->setUser( 'anon' );
+               $this->setTitle( NS_FILE, "img.png" );
+               $this->setUserPerm( "" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ], $res );
+
+               $this->setTitle( NS_FILE, "img.png" );
+               $this->setUserPerm( "movefile" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move', $this->user, $this->title );
+               $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+               $this->setUser( $this->userName );
+               $this->setUserPerm( "move" );
+               $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] );
+
+               $this->setUserPerm( "" );
+               $this->runGroupPermissions(
+                       'move',
+                       [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ]
+               );
+
+               $this->setUser( 'anon' );
+               $this->setUserPerm( "move" );
+               $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] );
+
+               $this->setUserPerm( "" );
+               $this->runGroupPermissions(
+                       'move',
+                       [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ],
+                       [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ]
+               );
+
+               if ( $this->isWikitextNS( NS_MAIN ) ) {
+                       // NOTE: some content models don't allow moving
+                       // @todo find a Wikitext namespace for testing
+
+                       $this->setTitle( NS_MAIN );
+                       $this->setUser( 'anon' );
+                       $this->setUserPerm( "move" );
+                       $this->runGroupPermissions( 'move', [] );
+
+                       $this->setUserPerm( "" );
+                       $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ],
+                               [ [ 'movenologintext' ] ] );
+
+                       $this->setUser( $this->userName );
+                       $this->setUserPerm( "" );
+                       $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ] );
+
+                       $this->setUserPerm( "move" );
+                       $this->runGroupPermissions( 'move', [] );
+
+                       $this->setUser( 'anon' );
+                       $this->setUserPerm( 'move' );
+                       $res = $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title );
+                       $this->assertEquals( [], $res );
+
+                       $this->setUserPerm( '' );
+                       $res = $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title );
+                       $this->assertEquals( [ [ 'movenotallowed' ] ], $res );
+               }
+
+               $this->setTitle( NS_USER );
+               $this->setUser( $this->userName );
+               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move-target', $this->user, $this->title );
+               $this->assertEquals( [], $res );
+
+               $this->setUserPerm( "move" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move-target', $this->user, $this->title );
+               $this->assertEquals( [ [ 'cant-move-to-user-page' ] ], $res );
+
+               $this->setUser( 'anon' );
+               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move-target', $this->user, $this->title );
+               $this->assertEquals( [], $res );
+
+               $this->setTitle( NS_USER, "User/subpage" );
+               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move-target', $this->user, $this->title );
+               $this->assertEquals( [], $res );
+
+               $this->setUserPerm( "move" );
+               $res = $this->permissionManager
+                       ->getPermissionErrors( 'move-target', $this->user, $this->title );
+               $this->assertEquals( [], $res );
+
+               $this->setUser( 'anon' );
+               $check = [
+                       'edit' => [
+                               [ [ 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ] ],
+                               [ [ 'badaccess-group0' ] ],
+                               [],
+                               true
+                       ],
+                       'protect' => [
+                               [ [
+                                       'badaccess-groups',
+                                       "[[$prefix:Administrators|Administrators]]", 1 ],
+                                       [ 'protect-cantedit'
+                                       ] ],
+                               [ [ 'badaccess-group0' ], [ 'protect-cantedit' ] ],
+                               [ [ 'protect-cantedit' ] ],
+                               false
+                       ],
+                       '' => [ [], [], [], true ]
+               ];
+
+               foreach ( [ "edit", "protect", "" ] as $action ) {
+                       $this->setUserPerm( null );
+                       $this->assertEquals( $check[$action][0],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, true ) );
+                       $this->assertEquals( $check[$action][0],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
+                       $this->assertEquals( $check[$action][0],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
+
+                       global $wgGroupPermissions;
+                       $old = $wgGroupPermissions;
+                       $wgGroupPermissions = [];
+
+                       $this->assertEquals( $check[$action][1],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, true ) );
+                       $this->assertEquals( $check[$action][1],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
+                       $this->assertEquals( $check[$action][1],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
+                       $wgGroupPermissions = $old;
+
+                       $this->setUserPerm( $action );
+                       $this->assertEquals( $check[$action][2],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, true ) );
+                       $this->assertEquals( $check[$action][2],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
+                       $this->assertEquals( $check[$action][2],
+                               $this->permissionManager
+                                       ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
+
+                       $this->setUserPerm( $action );
+                       $this->assertEquals( $check[$action][3],
+                               $this->permissionManager->userCan( $action, $this->user, $this->title, true ) );
+                       $this->assertEquals( $check[$action][3],
+                               $this->permissionManager->userCan( $action, $this->user, $this->title,
+                                       PermissionManager::RIGOR_QUICK ) );
+                       # count( User::getGroupsWithPermissions( $action ) ) < 1
+               }
+       }
+
+       protected function runGroupPermissions( $action, $result, $result2 = null ) {
+               global $wgGroupPermissions;
+
+               if ( $result2 === null ) {
+                       $result2 = $result;
+               }
+
+               $wgGroupPermissions['autoconfirmed']['move'] = false;
+               $wgGroupPermissions['user']['move'] = false;
+               $res = $this->permissionManager
+                       ->getPermissionErrors( $action, $this->user, $this->title );
+               $this->assertEquals( $result, $res );
+
+               $wgGroupPermissions['autoconfirmed']['move'] = true;
+               $wgGroupPermissions['user']['move'] = false;
+               $res = $this->permissionManager
+                       ->getPermissionErrors( $action, $this->user, $this->title );
+               $this->assertEquals( $result2, $res );
+
+               $wgGroupPermissions['autoconfirmed']['move'] = true;
+               $wgGroupPermissions['user']['move'] = true;
+               $res = $this->permissionManager
+                       ->getPermissionErrors( $action, $this->user, $this->title );
+               $this->assertEquals( $result2, $res );
+
+               $wgGroupPermissions['autoconfirmed']['move'] = false;
+               $wgGroupPermissions['user']['move'] = true;
+               $res = $this->permissionManager
+                       ->getPermissionErrors( $action, $this->user, $this->title );
+               $this->assertEquals( $result2, $res );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers MediaWiki\Permissions\PermissionManager::checkSpecialsAndNSPermissions
+        */
+       public function testSpecialsAndNSPermissions() {
+               global $wgNamespaceProtection;
+               $this->setUser( $this->userName );
+
+               $this->setTitle( NS_SPECIAL );
+
+               $this->assertEquals( [ [ 'badaccess-group0' ], [ 'ns-specialprotected' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+               $this->setTitle( NS_MAIN );
+               $this->setUserPerm( 'bogus' );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+               $this->setTitle( NS_MAIN );
+               $this->setUserPerm( '' );
+               $this->assertEquals( [ [ 'badaccess-group0' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+               $wgNamespaceProtection[NS_USER] = [ 'bogus' ];
+
+               $this->setTitle( NS_USER );
+               $this->setUserPerm( '' );
+               $this->assertEquals( [ [ 'badaccess-group0' ],
+                       [ 'namespaceprotected', 'User', 'bogus' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+               $this->setTitle( NS_MEDIAWIKI );
+               $this->setUserPerm( 'bogus' );
+               $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+               $this->setTitle( NS_MEDIAWIKI );
+               $this->setUserPerm( 'bogus' );
+               $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+               $wgNamespaceProtection = null;
+
+               $this->setUserPerm( 'bogus' );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+               $this->assertEquals( true,
+                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+
+               $this->setUserPerm( '' );
+               $this->assertEquals( [ [ 'badaccess-group0' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+        */
+       public function testJsConfigEditPermissions() {
+               $this->setUser( $this->userName );
+
+               $this->setTitle( NS_USER, $this->userName . '/test.js' );
+               $this->runConfigEditPermissions(
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-groups' ] ]
+               );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+        */
+       public function testJsonConfigEditPermissions() {
+               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
+               getFormattedNsText( NS_PROJECT );
+               $this->setUser( $this->userName );
+
+               $this->setTitle( NS_USER, $this->userName . '/test.json' );
+               $this->runConfigEditPermissions(
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+                       [ [ 'badaccess-groups' ] ]
+               );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+        */
+       public function testCssConfigEditPermissions() {
+               $this->setUser( $this->userName );
+
+               $this->setTitle( NS_USER, $this->userName . '/test.css' );
+               $this->runConfigEditPermissions(
+                       [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+                       [ [ 'badaccess-groups' ] ]
+               );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+        */
+       public function testOtherJsConfigEditPermissions() {
+               $this->setUser( $this->userName );
+
+               $this->setTitle( NS_USER, $this->altUserName . '/test.js' );
+               $this->runConfigEditPermissions(
+                       [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-groups' ] ]
+               );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+        */
+       public function testOtherJsonConfigEditPermissions() {
+               $this->setUser( $this->userName );
+
+               $this->setTitle( NS_USER, $this->altUserName . '/test.json' );
+               $this->runConfigEditPermissions(
+                       [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+                       [ [ 'badaccess-groups' ] ]
+               );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+        */
+       public function testOtherCssConfigEditPermissions() {
+               $this->setUser( $this->userName );
+
+               $this->setTitle( NS_USER, $this->altUserName . '/test.css' );
+               $this->runConfigEditPermissions(
+                       [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+                       [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+                       [ [ 'badaccess-groups' ] ]
+               );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+        */
+       public function testOtherNonConfigEditPermissions() {
+               $this->setUser( $this->userName );
+
+               $this->setTitle( NS_USER, $this->altUserName . '/tempo' );
+               $this->runConfigEditPermissions(
+                       [ [ 'badaccess-group0' ] ],
+
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ] ],
+
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-groups' ] ]
+               );
+       }
+
+       /**
+        * @todo This should use data providers like the other methods here.
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+        */
+       public function testPatrolActionConfigEditPermissions() {
+               $this->setUser( 'anon' );
+               $this->setTitle( NS_USER, 'ToPatrolOrNotToPatrol' );
+               $this->runConfigEditPermissions(
+                       [ [ 'badaccess-group0' ] ],
+
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ] ],
+
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-group0' ] ],
+                       [ [ 'badaccess-groups' ] ]
+               );
+       }
+
+       protected function runConfigEditPermissions(
+               $resultNone,
+               $resultMyCss,
+               $resultMyJson,
+               $resultMyJs,
+               $resultUserCss,
+               $resultUserJson,
+               $resultUserJs,
+               $resultPatrol
+       ) {
+               $this->setUserPerm( '' );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'bogus', $this->user, $this->title );
+               $this->assertEquals( $resultNone, $result );
+
+               $this->setUserPerm( 'editmyusercss' );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'bogus', $this->user, $this->title );
+               $this->assertEquals( $resultMyCss, $result );
+
+               $this->setUserPerm( 'editmyuserjson' );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'bogus', $this->user, $this->title );
+               $this->assertEquals( $resultMyJson, $result );
+
+               $this->setUserPerm( 'editmyuserjs' );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'bogus', $this->user, $this->title );
+               $this->assertEquals( $resultMyJs, $result );
+
+               $this->setUserPerm( 'editusercss' );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'bogus', $this->user, $this->title );
+               $this->assertEquals( $resultUserCss, $result );
+
+               $this->setUserPerm( 'edituserjson' );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'bogus', $this->user, $this->title );
+               $this->assertEquals( $resultUserJson, $result );
+
+               $this->setUserPerm( 'edituserjs' );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'bogus', $this->user, $this->title );
+               $this->assertEquals( $resultUserJs, $result );
+
+               $this->setUserPerm( '' );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'patrol', $this->user, $this->title );
+               $this->assertEquals( reset( $resultPatrol[0] ), reset( $result[0] ) );
+
+               $this->setUserPerm( [ 'edituserjs', 'edituserjson', 'editusercss' ] );
+               $result = $this->permissionManager
+                       ->getPermissionErrors( 'bogus', $this->user, $this->title );
+               $this->assertEquals( [ [ 'badaccess-group0' ] ], $result );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        *
+        * This test is failing per T201776.
+        *
+        * @group Broken
+        * @covers \MediaWiki\Permissions\PermissionManager::checkPageRestrictions
+        */
+       public function testPageRestrictions() {
+               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
+               getFormattedNsText( NS_PROJECT );
+
+               $this->setTitle( NS_MAIN );
+               $this->title->mRestrictionsLoaded = true;
+               $this->setUserPerm( "edit" );
+               $this->title->mRestrictions = [ "bogus" => [ 'bogus', "sysop", "protect", "" ] ];
+
+               $this->assertEquals( [],
+                       $this->permissionManager->getPermissionErrors( 'edit',
+                               $this->user, $this->title ) );
+
+               $this->assertEquals( true,
+                       $this->permissionManager->userCan( 'edit', $this->user, $this->title,
+                               PermissionManager::RIGOR_QUICK ) );
+
+               $this->title->mRestrictions = [ "edit" => [ 'bogus', "sysop", "protect", "" ],
+                       "bogus" => [ 'bogus', "sysop", "protect", "" ] ];
+
+               $this->assertEquals( [ [ 'badaccess-group0' ],
+                       [ 'protectedpagetext', 'bogus', 'bogus' ],
+                       [ 'protectedpagetext', 'editprotected', 'bogus' ],
+                       [ 'protectedpagetext', 'protect', 'bogus' ] ],
+                       $this->permissionManager->getPermissionErrors( 'bogus',
+                               $this->user, $this->title ) );
+               $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
+                       [ 'protectedpagetext', 'editprotected', 'edit' ],
+                       [ 'protectedpagetext', 'protect', 'edit' ] ],
+                       $this->permissionManager->getPermissionErrors( 'edit',
+                               $this->user, $this->title ) );
+               $this->setUserPerm( "" );
+               $this->assertEquals( [ [ 'badaccess-group0' ],
+                       [ 'protectedpagetext', 'bogus', 'bogus' ],
+                       [ 'protectedpagetext', 'editprotected', 'bogus' ],
+                       [ 'protectedpagetext', 'protect', 'bogus' ] ],
+                       $this->permissionManager->getPermissionErrors( 'bogus',
+                               $this->user, $this->title ) );
+               $this->assertEquals( [ [ 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ],
+                       [ 'protectedpagetext', 'bogus', 'edit' ],
+                       [ 'protectedpagetext', 'editprotected', 'edit' ],
+                       [ 'protectedpagetext', 'protect', 'edit' ] ],
+                       $this->permissionManager->getPermissionErrors( 'edit',
+                               $this->user, $this->title ) );
+               $this->setUserPerm( [ "edit", "editprotected" ] );
+               $this->assertEquals( [ [ 'badaccess-group0' ],
+                       [ 'protectedpagetext', 'bogus', 'bogus' ],
+                       [ 'protectedpagetext', 'protect', 'bogus' ] ],
+                       $this->permissionManager->getPermissionErrors( 'bogus',
+                               $this->user, $this->title ) );
+               $this->assertEquals( [
+                       [ 'protectedpagetext', 'bogus', 'edit' ],
+                       [ 'protectedpagetext', 'protect', 'edit' ] ],
+                       $this->permissionManager->getPermissionErrors( 'edit',
+                               $this->user, $this->title ) );
+
+               $this->title->mCascadeRestriction = true;
+               $this->setUserPerm( "edit" );
+
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title,
+                               PermissionManager::RIGOR_QUICK ) );
+
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'edit', $this->user, $this->title,
+                               PermissionManager::RIGOR_QUICK ) );
+
+               $this->assertEquals( [ [ 'badaccess-group0' ],
+                       [ 'protectedpagetext', 'bogus', 'bogus' ],
+                       [ 'protectedpagetext', 'editprotected', 'bogus' ],
+                       [ 'protectedpagetext', 'protect', 'bogus' ] ],
+                       $this->permissionManager->getPermissionErrors( 'bogus',
+                               $this->user, $this->title ) );
+               $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
+                       [ 'protectedpagetext', 'editprotected', 'edit' ],
+                       [ 'protectedpagetext', 'protect', 'edit' ] ],
+                       $this->permissionManager->getPermissionErrors( 'edit',
+                               $this->user, $this->title ) );
+
+               $this->setUserPerm( [ "edit", "editprotected" ] );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title,
+                               PermissionManager::RIGOR_QUICK ) );
+
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'edit', $this->user, $this->title,
+                               PermissionManager::RIGOR_QUICK ) );
+
+               $this->assertEquals( [ [ 'badaccess-group0' ],
+                       [ 'protectedpagetext', 'bogus', 'bogus' ],
+                       [ 'protectedpagetext', 'protect', 'bogus' ],
+                       [ 'protectedpagetext', 'protect', 'bogus' ] ],
+                       $this->permissionManager->getPermissionErrors( 'bogus',
+                               $this->user, $this->title ) );
+               $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
+                       [ 'protectedpagetext', 'protect', 'edit' ],
+                       [ 'protectedpagetext', 'protect', 'edit' ] ],
+                       $this->permissionManager->getPermissionErrors( 'edit',
+                               $this->user, $this->title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions
+        */
+       public function testCascadingSourcesRestrictions() {
+               $this->setTitle( NS_MAIN, "test page" );
+               $this->setUserPerm( [ "edit", "bogus" ] );
+
+               $this->title->mCascadeSources = [
+                       Title::makeTitle( NS_MAIN, "Bogus" ),
+                       Title::makeTitle( NS_MAIN, "UnBogus" )
+               ];
+               $this->title->mCascadingRestrictions = [
+                       "bogus" => [ 'bogus', "sysop", "protect", "" ]
+               ];
+
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+               $this->assertEquals( [
+                       [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ],
+                       [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ],
+                       [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ] ],
+                       $this->permissionManager->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+               $this->assertEquals( true,
+                       $this->permissionManager->userCan( 'edit', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager->getPermissionErrors( 'edit', $this->user, $this->title ) );
+       }
+
+       /**
+        * @todo This test method should be split up into separate test methods and
+        * data providers
+        * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions
+        */
+       public function testActionPermissions() {
+               $this->setUserPerm( [ "createpage" ] );
+               $this->setTitle( NS_MAIN, "test page" );
+               $this->title->mTitleProtection['permission'] = '';
+               $this->title->mTitleProtection['user'] = $this->user->getId();
+               $this->title->mTitleProtection['expiry'] = 'infinity';
+               $this->title->mTitleProtection['reason'] = 'test';
+               $this->title->mCascadeRestriction = false;
+
+               $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'create', $this->user, $this->title ) );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+
+               $this->title->mTitleProtection['permission'] = 'editprotected';
+               $this->setUserPerm( [ 'createpage', 'protect' ] );
+               $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'create', $this->user, $this->title ) );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+
+               $this->setUserPerm( [ 'createpage', 'editprotected' ] );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'create', $this->user, $this->title ) );
+               $this->assertEquals( true,
+                       $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+
+               $this->setUserPerm( [ 'createpage' ] );
+               $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'create', $this->user, $this->title ) );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+
+               $this->setTitle( NS_MEDIA, "test page" );
+               $this->setUserPerm( [ "move" ] );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+               $this->assertEquals( [ [ 'immobile-source-namespace', 'Media' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move', $this->user, $this->title ) );
+
+               $this->setTitle( NS_HELP, "test page" );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move', $this->user, $this->title ) );
+               $this->assertEquals( true,
+                       $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+
+               $this->title->mInterwiki = "no";
+               $this->assertEquals( [ [ 'immobile-source-page' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move', $this->user, $this->title ) );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+
+               $this->setTitle( NS_MEDIA, "test page" );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+               $this->assertEquals( [ [ 'immobile-target-namespace', 'Media' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+
+               $this->setTitle( NS_HELP, "test page" );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+               $this->assertEquals( true,
+                       $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+
+               $this->title->mInterwiki = "no";
+               $this->assertEquals( [ [ 'immobile-target-page' ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+               $this->assertEquals( false,
+                       $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
+        */
+       public function testUserBlock() {
+               $this->setMwGlobals( [
+                       'wgEmailConfirmToEdit' => true,
+                       'wgEmailAuthentication' => true,
+               ] );
+
+               $this->overrideMwServices();
+               $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+               $this->setUserPerm( [
+                       'createpage',
+                       'edit',
+                       'move',
+                       'rollback',
+                       'patrol',
+                       'upload',
+                       'purge'
+               ] );
+               $this->setTitle( NS_HELP, "test page" );
+
+               # $wgEmailConfirmToEdit only applies to 'edit' action
+               $this->assertEquals( [],
+                       $this->permissionManager->getPermissionErrors( 'move-target',
+                               $this->user, $this->title ) );
+               $this->assertContains( [ 'confirmedittext' ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+
+               $this->setMwGlobals( 'wgEmailConfirmToEdit', false );
+               $this->overrideMwServices();
+               $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+               $this->assertNotContains( [ 'confirmedittext' ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+
+               # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount'
+               $this->assertEquals( [],
+                       $this->permissionManager->getPermissionErrors( 'move-target',
+                               $this->user, $this->title ) );
+
+               global $wgLang;
+               $prev = time();
+               $now = time() + 120;
+               $this->user->mBlockedby = $this->user->getId();
+               $this->user->mBlock = new Block( [
+                       'address' => '127.0.8.1',
+                       'by' => $this->user->getId(),
+                       'reason' => 'no reason given',
+                       'timestamp' => $prev + 3600,
+                       'auto' => true,
+                       'expiry' => 0
+               ] );
+               $this->user->mBlock->mTimestamp = 0;
+               $this->assertEquals( [ [ 'autoblockedtext',
+                       '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+                       'Useruser', null, 'infinite', '127.0.8.1',
+                       $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ] ],
+                       $this->permissionManager->getPermissionErrors( 'move-target',
+                               $this->user, $this->title ) );
+
+               $this->assertEquals( false, $this->permissionManager
+                       ->userCan( 'move-target', $this->user, $this->title ) );
+               // quickUserCan should ignore user blocks
+               $this->assertEquals( true, $this->permissionManager
+                       ->userCan( 'move-target', $this->user, $this->title,
+                               PermissionManager::RIGOR_QUICK ) );
+
+               global $wgLocalTZoffset;
+               $wgLocalTZoffset = -60;
+               $this->user->mBlockedby = $this->user->getName();
+               $this->user->mBlock = new Block( [
+                       'address' => '127.0.8.1',
+                       'by' => $this->user->getId(),
+                       'reason' => 'no reason given',
+                       'timestamp' => $now,
+                       'auto' => false,
+                       'expiry' => 10,
+               ] );
+               $this->assertEquals( [ [ 'blockedtext',
+                       '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+                       'Useruser', null, '23:00, 31 December 1969', '127.0.8.1',
+                       $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+               # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this )
+               #   $user->blockedFor() == ''
+               #   $user->mBlock->mExpiry == 'infinity'
+
+               $this->user->mBlockedby = $this->user->getName();
+               $this->user->mBlock = new Block( [
+                       'address' => '127.0.8.1',
+                       'by' => $this->user->getId(),
+                       'reason' => 'no reason given',
+                       'timestamp' => $now,
+                       'auto' => false,
+                       'expiry' => 10,
+                       'systemBlock' => 'test',
+               ] );
+
+               $errors = [ [ 'systemblockedtext',
+                       '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+                       'Useruser', 'test', '23:00, 31 December 1969', '127.0.8.1',
+                       $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
+
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'upload', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'purge', $this->user, $this->title ) );
+
+               // partial block message test
+               $this->user->mBlockedby = $this->user->getName();
+               $this->user->mBlock = new Block( [
+                       'address' => '127.0.8.1',
+                       'by' => $this->user->getId(),
+                       'reason' => 'no reason given',
+                       'timestamp' => $now,
+                       'sitewide' => false,
+                       'expiry' => 10,
+               ] );
+
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'upload', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'purge', $this->user, $this->title ) );
+
+               $this->user->mBlock->setRestrictions( [
+                       ( new PageRestriction( 0, $this->title->getArticleID() ) )->setTitle( $this->title ),
+               ] );
+
+               $errors = [ [ 'blockedtext-partial',
+                       '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+                       'Useruser', null, '23:00, 31 December 1969', '127.0.8.1',
+                       $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
+
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'upload', $this->user, $this->title ) );
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'purge', $this->user, $this->title ) );
+
+               // Test no block.
+               $this->user->mBlockedby = null;
+               $this->user->mBlock = null;
+
+               $this->assertEquals( [],
+                       $this->permissionManager
+                               ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
+        *
+        * Tests to determine that the passed in permission does not get mixed up with
+        * an action of the same name.
+        */
+       public function testUserBlockAction() {
+               global $wgLang;
+
+               $tester = $this->getMockBuilder( Action::class )
+                                          ->disableOriginalConstructor()
+                                          ->getMock();
+               $tester->method( 'getName' )
+                          ->willReturn( 'tester' );
+               $tester->method( 'getRestriction' )
+                          ->willReturn( 'test' );
+               $tester->method( 'requiresUnblock' )
+                          ->willReturn( false );
+
+               $this->setMwGlobals( [
+                       'wgActions' => [
+                               'tester' => $tester,
+                       ],
+                       'wgGroupPermissions' => [
+                               '*' => [
+                                       'tester' => true,
+                               ],
+                       ],
+               ] );
+
+               $now = time();
+               $this->user->mBlockedby = $this->user->getName();
+               $this->user->mBlock = new Block( [
+                       'address' => '127.0.8.1',
+                       'by' => $this->user->getId(),
+                       'reason' => 'no reason given',
+                       'timestamp' => $now,
+                       'auto' => false,
+                       'expiry' => 'infinity',
+               ] );
+
+               $errors = [ [ 'blockedtext',
+                       '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+                       'Useruser', null, 'infinite', '127.0.8.1',
+                       $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
+
+               $this->assertEquals( $errors,
+                       $this->permissionManager
+                               ->getPermissionErrors( 'tester', $this->user, $this->title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::isBlockedFrom
+        */
+       public function testBlockInstanceCache() {
+               // First, check the user isn't blocked
+               $user = $this->getMutableTestUser()->getUser();
+               $ut = Title::makeTitle( NS_USER_TALK, $user->getName() );
+               $this->assertNull( $user->getBlock( false ), 'sanity check' );
+               //$this->assertSame( '', $user->blockedBy(), 'sanity check' );
+               //$this->assertSame( '', $user->blockedFor(), 'sanity check' );
+               //$this->assertFalse( (bool)$user->isHidden(), 'sanity check' );
+               $this->assertFalse( $this->permissionManager
+                       ->isBlockedFrom( $user, $ut ), 'sanity check' );
+
+               // Block the user
+               $blocker = $this->getTestSysop()->getUser();
+               $block = new Block( [
+                       'hideName' => true,
+                       'allowUsertalk' => false,
+                       'reason' => 'Because',
+               ] );
+               $block->setTarget( $user );
+               $block->setBlocker( $blocker );
+               $res = $block->insert();
+               $this->assertTrue( (bool)$res['id'], 'sanity check: Failed to insert block' );
+
+               // Clear cache and confirm it loaded the block properly
+               $user->clearInstanceCache();
+               $this->assertInstanceOf( Block::class, $user->getBlock( false ) );
+               //$this->assertSame( $blocker->getName(), $user->blockedBy() );
+               //$this->assertSame( 'Because', $user->blockedFor() );
+               //$this->assertTrue( (bool)$user->isHidden() );
+               $this->assertTrue( $this->permissionManager->isBlockedFrom( $user, $ut ) );
+
+               // Unblock
+               $block->delete();
+
+               // Clear cache and confirm it loaded the not-blocked properly
+               $user->clearInstanceCache();
+               $this->assertNull( $user->getBlock( false ) );
+               //$this->assertSame( '', $user->blockedBy() );
+               //$this->assertSame( '', $user->blockedFor() );
+               //$this->assertFalse( (bool)$user->isHidden() );
+               $this->assertFalse( $this->permissionManager->isBlockedFrom( $user, $ut ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::isBlockedFrom
+        * @dataProvider provideIsBlockedFrom
+        * @param string|null $title Title to test.
+        * @param bool $expect Expected result from User::isBlockedFrom()
+        * @param array $options Additional test options:
+        *  - 'blockAllowsUTEdit': (bool, default true) Value for $wgBlockAllowsUTEdit
+        *  - 'allowUsertalk': (bool, default false) Passed to Block::__construct()
+        *  - 'pageRestrictions': (array|null) If non-empty, page restriction titles for the block.
+        */
+       public function testIsBlockedFrom( $title, $expect, array $options = [] ) {
+               $this->setMwGlobals( [
+                       'wgBlockAllowsUTEdit' => $options['blockAllowsUTEdit'] ?? true,
+               ] );
+
+               $user = $this->getTestUser()->getUser();
+
+               if ( $title === self::USER_TALK_PAGE ) {
+                       $title = $user->getTalkPage();
+               } else {
+                       $title = Title::newFromText( $title );
+               }
+
+               $restrictions = [];
+               foreach ( $options['pageRestrictions'] ?? [] as $pagestr ) {
+                       $page = $this->getExistingTestPage(
+                               $pagestr === self::USER_TALK_PAGE ? $user->getTalkPage() : $pagestr
+                       );
+                       $restrictions[] = new PageRestriction( 0, $page->getId() );
+               }
+               foreach ( $options['namespaceRestrictions'] ?? [] as $ns ) {
+                       $restrictions[] = new NamespaceRestriction( 0, $ns );
+               }
+
+               $block = new Block( [
+                       'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
+                       'allowUsertalk' => $options['allowUsertalk'] ?? false,
+                       'sitewide' => !$restrictions,
+               ] );
+               $block->setTarget( $user );
+               $block->setBlocker( $this->getTestSysop()->getUser() );
+               if ( $restrictions ) {
+                       $block->setRestrictions( $restrictions );
+               }
+               $block->insert();
+
+               try {
+                       $this->assertSame( $expect, $this->permissionManager->isBlockedFrom( $user, $title ) );
+               } finally {
+                       $block->delete();
+               }
+       }
+
+       public static function provideIsBlockedFrom() {
+               return [
+                       'Sitewide block, basic operation' => [ 'Test page', true ],
+                       'Sitewide block, not allowing user talk' => [
+                               self::USER_TALK_PAGE, true, [
+                                       'allowUsertalk' => false,
+                               ]
+                       ],
+                       'Sitewide block, allowing user talk' => [
+                               self::USER_TALK_PAGE, false, [
+                                       'allowUsertalk' => true,
+                               ]
+                       ],
+                       'Sitewide block, allowing user talk but $wgBlockAllowsUTEdit is false' => [
+                               self::USER_TALK_PAGE, true, [
+                                       'allowUsertalk' => true,
+                                       'blockAllowsUTEdit' => false,
+                               ]
+                       ],
+                       'Partial block, blocking the page' => [
+                               'Test page', true, [
+                                       'pageRestrictions' => [ 'Test page' ],
+                               ]
+                       ],
+                       'Partial block, not blocking the page' => [
+                               'Test page 2', false, [
+                                       'pageRestrictions' => [ 'Test page' ],
+                               ]
+                       ],
+                       'Partial block, not allowing user talk but user talk page is not blocked' => [
+                               self::USER_TALK_PAGE, false, [
+                                       'allowUsertalk' => false,
+                                       'pageRestrictions' => [ 'Test page' ],
+                               ]
+                       ],
+                       'Partial block, allowing user talk but user talk page is blocked' => [
+                               self::USER_TALK_PAGE, true, [
+                                       'allowUsertalk' => true,
+                                       'pageRestrictions' => [ self::USER_TALK_PAGE ],
+                               ]
+                       ],
+                       'Partial block, user talk page is not blocked but $wgBlockAllowsUTEdit is false' => [
+                               self::USER_TALK_PAGE, false, [
+                                       'allowUsertalk' => false,
+                                       'pageRestrictions' => [ 'Test page' ],
+                                       'blockAllowsUTEdit' => false,
+                               ]
+                       ],
+                       'Partial block, user talk page is blocked and $wgBlockAllowsUTEdit is false' => [
+                               self::USER_TALK_PAGE, true, [
+                                       'allowUsertalk' => true,
+                                       'pageRestrictions' => [ self::USER_TALK_PAGE ],
+                                       'blockAllowsUTEdit' => false,
+                               ]
+                       ],
+                       'Partial user talk namespace block, not allowing user talk' => [
+                               self::USER_TALK_PAGE, true, [
+                                       'allowUsertalk' => false,
+                                       'namespaceRestrictions' => [ NS_USER_TALK ],
+                               ]
+                       ],
+                       'Partial user talk namespace block, allowing user talk' => [
+                               self::USER_TALK_PAGE, false, [
+                                       'allowUsertalk' => true,
+                                       'namespaceRestrictions' => [ NS_USER_TALK ],
+                               ]
+                       ],
+                       'Partial user talk namespace block, where $wgBlockAllowsUTEdit is false' => [
+                               self::USER_TALK_PAGE, true, [
+                                       'allowUsertalk' => true,
+                                       'namespaceRestrictions' => [ NS_USER_TALK ],
+                                       'blockAllowsUTEdit' => false,
+                               ]
+                       ],
+               ];
+       }
+
+}
index 13def70..f7ffe8d 100644 (file)
@@ -6,8 +6,8 @@ use MediaWiki\MediaWikiServices;
 /**
  * @group Database
  *
- * @covers Title::getUserPermissionsErrors
- * @covers Title::getUserPermissionsErrorsInternal
+ * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrors
+ * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrorsInternal
  */
 class TitlePermissionTest extends MediaWikiLangTestCase {
 
@@ -104,7 +104,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * This test is failing per T201776.
         *
         * @group Broken
-        * @covers Title::checkQuickPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkQuickPermissions
         */
        public function testQuickPermissions() {
                $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
@@ -395,7 +395,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkSpecialsAndNSPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkSpecialsAndNSPermissions
         */
        public function testSpecialsAndNSPermissions() {
                global $wgNamespaceProtection;
@@ -452,7 +452,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkUserConfigPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
         */
        public function testJsConfigEditPermissions() {
                $this->setUser( $this->userName );
@@ -475,7 +475,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkUserConfigPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
         */
        public function testJsonConfigEditPermissions() {
                $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
@@ -500,7 +500,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkUserConfigPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
         */
        public function testCssConfigEditPermissions() {
                $this->setUser( $this->userName );
@@ -523,7 +523,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkUserConfigPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
         */
        public function testOtherJsConfigEditPermissions() {
                $this->setUser( $this->userName );
@@ -546,7 +546,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkUserConfigPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
         */
        public function testOtherJsonConfigEditPermissions() {
                $this->setUser( $this->userName );
@@ -569,7 +569,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkUserConfigPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
         */
        public function testOtherCssConfigEditPermissions() {
                $this->setUser( $this->userName );
@@ -592,7 +592,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkUserConfigPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
         */
        public function testOtherNonConfigEditPermissions() {
                $this->setUser( $this->userName );
@@ -614,7 +614,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
 
        /**
         * @todo This should use data providers like the other methods here.
-        * @covers Title::checkUserConfigPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
         */
        public function testPatrolActionConfigEditPermissions() {
                $this->setUser( 'anon' );
@@ -687,7 +687,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * This test is failing per T201776.
         *
         * @group Broken
-        * @covers Title::checkPageRestrictions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkPageRestrictions
         */
        public function testPageRestrictions() {
                $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
@@ -780,7 +780,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        }
 
        /**
-        * @covers Title::checkCascadingSourcesRestrictions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions
         */
        public function testCascadingSourcesRestrictions() {
                $this->setTitle( NS_MAIN, "test page" );
@@ -811,7 +811,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        /**
         * @todo This test method should be split up into separate test methods and
         * data providers
-        * @covers Title::checkActionPermissions
+        * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions
         */
        public function testActionPermissions() {
                $this->setUserPerm( [ "createpage" ] );
@@ -885,13 +885,14 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        }
 
        /**
-        * @covers Title::checkUserBlock
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
         */
        public function testUserBlock() {
                $this->setMwGlobals( [
                        'wgEmailConfirmToEdit' => true,
                        'wgEmailAuthentication' => true,
                ] );
+               $this->overrideMwServices();
 
                $this->setUserPerm( [ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ] );
                $this->setTitle( NS_HELP, "test page" );
@@ -903,6 +904,8 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
 
                $this->setMwGlobals( 'wgEmailConfirmToEdit', false );
+               $this->overrideMwServices();
+
                $this->assertNotContains( [ 'confirmedittext' ],
                        $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
 
@@ -1039,7 +1042,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
        }
 
        /**
-        * @covers Title::checkUserBlock
+        * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
         *
         * Tests to determine that the passed in permission does not get mixed up with
         * an action of the same name.
index 03802a8..149c25b 100644 (file)
@@ -327,7 +327,7 @@ class TitleTest extends MediaWikiTestCase {
         * @param string $action
         * @param array|string|bool $expected Required error
         *
-        * @covers Title::checkReadPermissions
+        * @covers \Mediawiki\Permissions\PermissionManager::checkReadPermissions
         * @dataProvider dataWgWhitelistReadRegexp
         */
        public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) {