'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/',
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;
return $this->getService( 'PerDbNameStatsdDataFactory' );
}
+ /**
+ * @since 1.33
+ * @return PermissionManager
+ */
+ public function getPermissionManager() {
+ return $this->getService( 'PermissionManager' );
+ }
+
/**
* @since 1.31
* @return PreferencesFactory
--- /dev/null
+<?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;
+ }
+
+}
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;
);
},
+ '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(),
* @file
*/
+use MediaWiki\Permissions\PermissionManager;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
*
* @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 );
* @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 );
}
/**
* - 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 );
}
/**
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
* @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 );
}
/**
// 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;
+ }
+
}
--- /dev/null
+<?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,
+ ]
+ ],
+ ];
+ }
+
+}
/**
* @group Database
*
- * @covers Title::getUserPermissionsErrors
- * @covers Title::getUserPermissionsErrorsInternal
+ * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrors
+ * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrorsInternal
*/
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()->
/**
* @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;
/**
* @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 );
/**
* @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()->
/**
* @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 );
/**
* @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 );
/**
* @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 );
/**
* @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 );
/**
* @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 );
/**
* @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' );
* This test is failing per T201776.
*
* @group Broken
- * @covers Title::checkPageRestrictions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkPageRestrictions
*/
public function testPageRestrictions() {
$prefix = MediaWikiServices::getInstance()->getContentLanguage()->
}
/**
- * @covers Title::checkCascadingSourcesRestrictions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions
*/
public function testCascadingSourcesRestrictions() {
$this->setTitle( NS_MAIN, "test page" );
/**
* @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" ] );
}
/**
- * @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" );
$this->title->getUserPermissionsErrors( 'edit', $this->user ) );
$this->setMwGlobals( 'wgEmailConfirmToEdit', false );
+ $this->overrideMwServices();
+
$this->assertNotContains( [ 'confirmedittext' ],
$this->title->getUserPermissionsErrors( 'edit', $this->user ) );
}
/**
- * @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.
* @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 ) {