From: Krinkle Date: Fri, 5 Apr 2019 23:38:51 +0000 (+0000) Subject: Merge "rdbms: rename DB server index parameter to LoadBalancer::getMaintenanceConnect... X-Git-Tag: 1.34.0-rc.0~2137 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/pie.php?a=commitdiff_plain;h=9f6f943b7a52d511100184e59b4fb77bdb8aa415;hp=1f5c0bb8ec4d2f2ce4c04618da94e3f7f9dd47a6;p=lhc%2Fweb%2Fwiklou.git Merge "rdbms: rename DB server index parameter to LoadBalancer::getMaintenanceConnectionRef()" --- diff --git a/.phan/config.php b/.phan/config.php index e4ba47f265..2c0035ea04 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -115,38 +115,28 @@ $cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [ "PhanTypeComparisonFromArray", // approximate error count: 2 "PhanTypeComparisonToArray", - // approximate error count: 1 - "PhanTypeConversionFromArray", // approximate error count: 2 "PhanTypeExpectedObjectOrClassName", // approximate error count: 7 "PhanTypeExpectedObjectPropAccess", // approximate error count: 3 "PhanTypeInstantiateAbstract", - // approximate error count: 1 - "PhanTypeInvalidCallableArraySize", // approximate error count: 62 "PhanTypeInvalidDimOffset", // approximate error count: 10 "PhanTypeInvalidExpressionArrayDestructuring", - // approximate error count: 1 - "PhanTypeInvalidLeftOperand", // approximate error count: 7 "PhanTypeInvalidLeftOperandOfIntegerOp", // approximate error count: 2 "PhanTypeInvalidRightOperand", // approximate error count: 2 "PhanTypeInvalidRightOperandOfIntegerOp", - // approximate error count: 1 - "PhanTypeMagicVoidWithReturn", // approximate error count: 152 "PhanTypeMismatchArgument", // approximate error count: 28 "PhanTypeMismatchArgumentInternal", // approximate error count: 1 "PhanTypeMismatchBitwiseBinaryOperands", - // approximate error count: 1 - "PhanTypeMismatchDeclaredParam", // approximate error count: 2 "PhanTypeMismatchDimEmpty", // approximate error count: 29 @@ -177,10 +167,6 @@ $cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [ "PhanUndeclaredMethod", // approximate error count: 847 "PhanUndeclaredProperty", - // approximate error count: 1 - "PhanUndeclaredTypeReturnType", - // approximate error count: 3 - "PhanUndeclaredTypeThrowsType", // approximate error count: 2 "PhanUndeclaredVariableAssignOp", // approximate error count: 55 diff --git a/composer.json b/composer.json index 3faf620e56..b6814c5569 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,7 @@ "jakub-onderka/php-console-highlighter": "0.3.2", "jakub-onderka/php-parallel-lint": "0.9.2", "justinrainbow/json-schema": "~5.2", - "mediawiki/mediawiki-codesniffer": "24.0.0", + "mediawiki/mediawiki-codesniffer": "25.0.0", "monolog/monolog": "~1.22.1", "nikic/php-parser": "3.1.5", "seld/jsonlint": "1.7.1", diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index f8fbf83bb4..fa11bcb1b5 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -134,6 +134,7 @@ class AutoLoader { 'MediaWiki\\Edit\\' => __DIR__ . '/edit/', 'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/', 'MediaWiki\\Linker\\' => __DIR__ . '/linker/', + 'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/', 'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/', 'MediaWiki\\Revision\\' => __DIR__ . '/Revision/', 'MediaWiki\\Session\\' => __DIR__ . '/session/', diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 24aca2ea7b..990ed4e358 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -935,7 +935,7 @@ class MediaWiki { ) { if ( $config->get( 'StatsdServer' ) && $stats->hasData() ) { try { - $statsdServer = explode( ':', $config->get( 'StatsdServer' ) ); + $statsdServer = explode( ':', $config->get( 'StatsdServer' ), 2 ); $statsdHost = $statsdServer[0]; $statsdPort = $statsdServer[1] ?? 8125; $statsdSender = new SocketSender( $statsdHost, $statsdPort ); diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 292e8dfa14..6bf5d1d408 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -14,6 +14,7 @@ use Hooks; use IBufferingStatsdDataFactory; use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; use MediaWiki\Http\HttpRequestFactory; +use MediaWiki\Permissions\PermissionManager; use MediaWiki\Preferences\PreferencesFactory; use MediaWiki\Shell\CommandFactory; use MediaWiki\Revision\RevisionRenderer; @@ -721,6 +722,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'PerDbNameStatsdDataFactory' ); } + /** + * @since 1.33 + * @return PermissionManager + */ + public function getPermissionManager() { + return $this->getService( 'PermissionManager' ); + } + /** * @since 1.31 * @return PreferencesFactory diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php new file mode 100644 index 0000000000..1d94e0e889 --- /dev/null +++ b/includes/Permissions/PermissionManager.php @@ -0,0 +1,1047 @@ +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; + } + +} diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index 2a54a9b187..0329bd1587 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -1670,6 +1670,7 @@ class RevisionStore ) { if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) { $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title ); + // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] ); } else { // XXX: do we need the same kind of caching here diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index e121898400..cccb5e733f 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -46,6 +46,7 @@ use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use MediaWiki\Permissions\PermissionManager; use MediaWiki\Preferences\PreferencesFactory; use MediaWiki\Preferences\DefaultPreferencesFactory; use MediaWiki\Revision\MainSlotRoleHandler; @@ -389,6 +390,16 @@ return [ ); }, + 'PermissionManager' => function ( MediaWikiServices $services ) : PermissionManager { + $config = $services->getMainConfig(); + return new PermissionManager( + $services->getSpecialPageFactory(), + $config->get( 'WhitelistRead' ), + $config->get( 'WhitelistReadRegexp' ), + $config->get( 'EmailConfirmToEdit' ), + $config->get( 'BlockDisablesLogin' ) ); + }, + 'PreferencesFactory' => function ( MediaWikiServices $services ) : PreferencesFactory { $factory = new DefaultPreferencesFactory( $services->getMainConfig(), diff --git a/includes/Title.php b/includes/Title.php index 6d86b22573..d517b851e5 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -22,6 +22,7 @@ * @file */ +use MediaWiki\Permissions\PermissionManager; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IDatabase; use MediaWiki\Linker\LinkTarget; @@ -53,6 +54,13 @@ class Title implements LinkTarget, IDBAccessObject { */ const GAID_FOR_UPDATE = 1; + /** + * Flag for use with factory methods like newFromLinkTarget() that have + * a $forceClone parameter. If set, the method must return a new instance. + * Without this flag, some factory methods may return existing instances. + */ + const NEW_CLONE = 'clone'; + /** * @name Private member variables * Please use the accessor functions instead. @@ -231,27 +239,39 @@ class Title implements LinkTarget, IDBAccessObject { } /** - * Create a new Title from a TitleValue + * Returns a Title given a TitleValue. + * If the given TitleValue is already a Title instance, that instance is returned, + * unless $forceClone is "clone". If $forceClone is "clone" and the given TitleValue + * is already a Title instance, that instance is copied using the clone operator. * * @param TitleValue $titleValue Assumed to be safe. + * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned. * * @return Title */ - public static function newFromTitleValue( TitleValue $titleValue ) { - return self::newFromLinkTarget( $titleValue ); + public static function newFromTitleValue( TitleValue $titleValue, $forceClone = '' ) { + return self::newFromLinkTarget( $titleValue, $forceClone ); } /** - * Create a new Title from a LinkTarget + * Returns a Title given a LinkTarget. + * If the given LinkTarget is already a Title instance, that instance is returned, + * unless $forceClone is "clone". If $forceClone is "clone" and the given LinkTarget + * is already a Title instance, that instance is copied using the clone operator. * * @param LinkTarget $linkTarget Assumed to be safe. + * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned. * * @return Title */ - public static function newFromLinkTarget( LinkTarget $linkTarget ) { + public static function newFromLinkTarget( LinkTarget $linkTarget, $forceClone = '' ) { if ( $linkTarget instanceof Title ) { // Special case if it's already a Title object - return $linkTarget; + if ( $forceClone === self::NEW_CLONE ) { + return clone $linkTarget; + } else { + return $linkTarget; + } } return self::makeTitle( $linkTarget->getNamespace(), @@ -268,6 +288,10 @@ class Title implements LinkTarget, IDBAccessObject { * Title objects returned by this method are guaranteed to be valid, and * thus return true from the isValid() method. * + * @note The Title instance returned by this method is not guaranteed to be a fresh instance. + * It may instead be a cached instance created previously, with references to it remaining + * elsewhere. + * * @param string|int|null $text The link text; spaces, prefixes, and an * initial ':' indicating the main namespace are accepted. * @param int $defaultNamespace The namespace to use if none is specified @@ -302,6 +326,10 @@ class Title implements LinkTarget, IDBAccessObject { * Title objects returned by this method are guaranteed to be valid, and * thus return true from the isValid() method. * + * @note The Title instance returned by this method is not guaranteed to be a fresh instance. + * It may instead be a cached instance created previously, with references to it remaining + * elsewhere. + * * @see Title::newFromText * * @since 1.25 @@ -593,6 +621,10 @@ class Title implements LinkTarget, IDBAccessObject { /** * Create a new Title for the Main Page * + * @note The Title instance returned by this method is not guaranteed to be a fresh instance. + * It may instead be a cached instance created previously, with references to it remaining + * elsewhere. + * * @return Title The new object */ public static function newMainPage() { @@ -2093,7 +2125,13 @@ class Title implements LinkTarget, IDBAccessObject { * * @param string $action Action that permission needs to be checked for * @param User|null $user User to check (since 1.19); $wgUser will be used if not provided. + * * @return bool + * @throws Exception + * + * @deprecated since 1.33, + * use MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(..) instead + * */ public function quickUserCan( $action, $user = null ) { return $this->userCan( $action, $user, false ); @@ -2106,15 +2144,29 @@ class Title implements LinkTarget, IDBAccessObject { * @param User|null $user User to check (since 1.19); $wgUser will be used if not * provided. * @param string $rigor Same format as Title::getUserPermissionsErrors() + * * @return bool + * @throws Exception + * + * @deprecated since 1.33, + * use MediaWikiServices::getInstance()->getPermissionManager()->userCan(..) instead + * */ - public function userCan( $action, $user = null, $rigor = 'secure' ) { + public function userCan( $action, $user = null, $rigor = PermissionManager::RIGOR_SECURE ) { if ( !$user instanceof User ) { global $wgUser; $user = $wgUser; } - return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) ); + // TODO: this is for b/c, eventually will be removed + if ( $rigor === true ) { + $rigor = PermissionManager::RIGOR_SECURE; // b/c + } elseif ( $rigor === false ) { + $rigor = PermissionManager::RIGOR_QUICK; // b/c + } + + return MediaWikiServices::getInstance()->getPermissionManager() + ->userCan( $action, $user, $this, $rigor ); } /** @@ -2130,99 +2182,26 @@ class Title implements LinkTarget, IDBAccessObject { * - secure : does cheap and expensive checks, using the master as needed * @param array $ignoreErrors Array of Strings Set this to a list of message keys * whose corresponding errors may be ignored. + * * @return array Array of arrays of the arguments to wfMessage to explain permissions problems. - */ - public function getUserPermissionsErrors( - $action, $user, $rigor = 'secure', $ignoreErrors = [] - ) { - $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor ); - - // Remove the errors being ignored. - foreach ( $errors as $index => $error ) { - $errKey = is_array( $error ) ? $error[0] : $error; - - if ( in_array( $errKey, $ignoreErrors ) ) { - unset( $errors[$index] ); - } - if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) { - unset( $errors[$index] ); - } - } - - return $errors; - } - - /** - * Permissions checks that fail most often, and which are easiest to test. + * @throws Exception * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error + * @deprecated since 1.33, + * use MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissionsErrors() * - * @return array List of errors */ - private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) { - if ( !Hooks::run( 'TitleQuickPermissions', - [ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] ) - ) { - return $errors; - } - - if ( $action == 'create' ) { - if ( - ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || - ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) - ) { - $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ]; - } - } elseif ( $action == 'move' ) { - if ( !$user->isAllowed( 'move-rootuserpages' ) - && $this->mNamespace == NS_USER && !$this->isSubpage() ) { - // Show user page-specific message only if the user can move other pages - $errors[] = [ 'cant-move-user-page' ]; - } - - // Check if user is allowed to move files if it's a file - if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { - $errors[] = [ 'movenotallowedfile' ]; - } - - // Check if user is allowed to move category pages if it's a category page - if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) { - $errors[] = [ 'cant-move-category-page' ]; - } - - if ( !$user->isAllowed( 'move' ) ) { - // User can't move anything - $userCanMove = User::groupHasPermission( 'user', 'move' ); - $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); - if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { - // custom message if logged-in users without any special rights can move - $errors[] = [ 'movenologintext' ]; - } else { - $errors[] = [ 'movenotallowed' ]; - } - } - } elseif ( $action == 'move-target' ) { - if ( !$user->isAllowed( 'move' ) ) { - // User can't move anything - $errors[] = [ 'movenotallowed' ]; - } elseif ( !$user->isAllowed( 'move-rootuserpages' ) - && $this->mNamespace == NS_USER && !$this->isSubpage() ) { - // Show user page-specific message only if the user can move other pages - $errors[] = [ 'cant-move-to-user-page' ]; - } elseif ( !$user->isAllowed( 'move-categorypages' ) - && $this->mNamespace == NS_CATEGORY ) { - // Show category page-specific message only if the user can move other pages - $errors[] = [ 'cant-move-to-category-page' ]; - } - } elseif ( !$user->isAllowed( $action ) ) { - $errors[] = $this->missingPermissionError( $action, $short ); + public function getUserPermissionsErrors( + $action, $user, $rigor = PermissionManager::RIGOR_SECURE, $ignoreErrors = [] + ) { + // TODO: this is for b/c, eventually will be removed + if ( $rigor === true ) { + $rigor = PermissionManager::RIGOR_SECURE; // b/c + } elseif ( $rigor === false ) { + $rigor = PermissionManager::RIGOR_QUICK; // b/c } - return $errors; + return MediaWikiServices::getInstance()->getPermissionManager() + ->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors ); } /** @@ -2253,582 +2232,6 @@ class Title implements LinkTarget, IDBAccessObject { return $errors; } - /** - * Check various permission hooks - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) { - // Use getUserPermissionsErrors instead - $result = ''; - // Avoid PHP 7.1 warning from passing $this by reference - $titleRef = $this; - if ( !Hooks::run( 'userCan', [ &$titleRef, &$user, $action, &$result ] ) ) { - return $result ? [] : [ [ 'badaccess-group0' ] ]; - } - // Check getUserPermissionsErrors hook - // Avoid PHP 7.1 warning from passing $this by reference - $titleRef = $this; - if ( !Hooks::run( 'getUserPermissionsErrors', [ &$titleRef, &$user, $action, &$result ] ) ) { - $errors = $this->resultToError( $errors, $result ); - } - // Check getUserPermissionsErrorsExpensive hook - if ( - $rigor !== 'quick' - && !( $short && count( $errors ) > 0 ) - && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$titleRef, &$user, $action, &$result ] ) - ) { - $errors = $this->resultToError( $errors, $result ); - } - - return $errors; - } - - /** - * Check permissions on special pages & namespaces - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) { - # Only 'createaccount' can be performed on special pages, - # which don't actually exist in the DB. - if ( $this->isSpecialPage() && $action !== 'createaccount' ) { - $errors[] = [ 'ns-specialprotected' ]; - } - - # Check $wgNamespaceProtection for restricted namespaces - if ( $this->isNamespaceProtected( $user ) ) { - $ns = $this->mNamespace == NS_MAIN ? - wfMessage( 'nstab-main' )->text() : $this->getNsText(); - $errors[] = $this->mNamespace == NS_MEDIAWIKI ? - [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ]; - } - - return $errors; - } - - /** - * Check sitewide CSS/JSON/JS permissions - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short ) { - if ( $action != 'patrol' ) { - $error = null; - // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the - // editinterface right. That's implemented as a restriction so no check needed here. - if ( $this->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) { - $error = [ 'sitecssprotected', $action ]; - } elseif ( $this->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) { - $error = [ 'sitejsonprotected', $action ]; - } elseif ( $this->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) { - $error = [ 'sitejsprotected', $action ]; - } elseif ( $this->isRawHtmlMessage() ) { - // Raw HTML can be used to deploy CSS or JS so require rights for both. - if ( !$user->isAllowed( 'editsitejs' ) ) { - $error = [ 'sitejsprotected', $action ]; - } elseif ( !$user->isAllowed( 'editsitecss' ) ) { - $error = [ 'sitecssprotected', $action ]; - } - } - - if ( $error ) { - if ( $user->isAllowed( 'editinterface' ) ) { - // Most users / site admins will probably find out about the new, more restrictive - // permissions by failing to edit something. Give them more info. - // TODO remove this a few release cycles after 1.32 - $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ]; - } - $errors[] = $error; - } - } - - return $errors; - } - - /** - * Check CSS/JSON/JS sub-page permissions - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) { - # Protect css/json/js subpages of user pages - # XXX: this might be better using restrictions - - if ( $action === 'patrol' ) { - return $errors; - } - - if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { - // Users need editmyuser* to edit their own CSS/JSON/JS subpages. - if ( - $this->isUserCssConfigPage() - && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) - ) { - $errors[] = [ 'mycustomcssprotected', $action ]; - } elseif ( - $this->isUserJsonConfigPage() - && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' ) - ) { - $errors[] = [ 'mycustomjsonprotected', $action ]; - } elseif ( - $this->isUserJsConfigPage() - && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) - ) { - $errors[] = [ 'mycustomjsprotected', $action ]; - } - } else { - // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for - // deletion/suppression which cannot be used for attacks and we want to avoid the - // situation where an unprivileged user can post abusive content on their subpages - // and only very highly privileged users could remove it. - if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) { - if ( - $this->isUserCssConfigPage() - && !$user->isAllowed( 'editusercss' ) - ) { - $errors[] = [ 'customcssprotected', $action ]; - } elseif ( - $this->isUserJsonConfigPage() - && !$user->isAllowed( 'edituserjson' ) - ) { - $errors[] = [ 'customjsonprotected', $action ]; - } elseif ( - $this->isUserJsConfigPage() - && !$user->isAllowed( 'edituserjs' ) - ) { - $errors[] = [ 'customjsprotected', $action ]; - } - } - } - - return $errors; - } - - /** - * Check against page_restrictions table requirements on this - * page. The user must possess all required rights for this - * action. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) { - foreach ( $this->getRestrictions( $action ) as $right ) { - // Backwards compatibility, rewrite sysop -> editprotected - if ( $right == 'sysop' ) { - $right = 'editprotected'; - } - // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected - if ( $right == 'autoconfirmed' ) { - $right = 'editsemiprotected'; - } - if ( $right == '' ) { - continue; - } - if ( !$user->isAllowed( $right ) ) { - $errors[] = [ 'protectedpagetext', $right, $action ]; - } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) { - $errors[] = [ 'protectedpagetext', 'protect', $action ]; - } - } - - return $errors; - } - - /** - * Check restrictions on cascading pages. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) { - if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) { - # We /could/ use the protection level on the source page, but it's - # fairly ugly as we have to establish a precedence hierarchy for pages - # included by multiple cascade-protected pages. So just restrict - # it to people with 'protect' permission, as they could remove the - # protection anyway. - list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); - # Cascading protection depends on more than this page... - # Several cascading protected pages may include this page... - # Check each cascading level - # This is only for protection restrictions, not for all actions - if ( isset( $restrictions[$action] ) ) { - foreach ( $restrictions[$action] as $right ) { - // Backwards compatibility, rewrite sysop -> editprotected - if ( $right == 'sysop' ) { - $right = 'editprotected'; - } - // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected - if ( $right == 'autoconfirmed' ) { - $right = 'editsemiprotected'; - } - if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { - $pages = ''; - foreach ( $cascadingSources as $page ) { - $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; - } - $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ]; - } - } - } - } - - return $errors; - } - - /** - * Check action permissions not already checked in checkQuickPermissions - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) { - global $wgDeleteRevisionsLimit, $wgLang; - - if ( $action == 'protect' ) { - if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) { - // If they can't edit, they shouldn't protect. - $errors[] = [ 'protect-cantedit' ]; - } - } elseif ( $action == 'create' ) { - $title_protection = $this->getTitleProtection(); - if ( $title_protection ) { - if ( $title_protection['permission'] == '' - || !$user->isAllowed( $title_protection['permission'] ) - ) { - $errors[] = [ - 'titleprotected', - User::whoIs( $title_protection['user'] ), - $title_protection['reason'] - ]; - } - } - } elseif ( $action == 'move' ) { - // Check for immobile pages - if ( !MWNamespace::isMovable( $this->mNamespace ) ) { - // Specific message for this case - $errors[] = [ 'immobile-source-namespace', $this->getNsText() ]; - } elseif ( !$this->isMovable() ) { - // Less specific message for rarer cases - $errors[] = [ 'immobile-source-page' ]; - } - } elseif ( $action == 'move-target' ) { - if ( !MWNamespace::isMovable( $this->mNamespace ) ) { - $errors[] = [ 'immobile-target-namespace', $this->getNsText() ]; - } elseif ( !$this->isMovable() ) { - $errors[] = [ 'immobile-target-page' ]; - } - } elseif ( $action == 'delete' ) { - $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true ); - if ( !$tempErrors ) { - $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit', - $user, $tempErrors, $rigor, true ); - } - if ( $tempErrors ) { - // If protection keeps them from editing, they shouldn't be able to delete. - $errors[] = [ 'deleteprotected' ]; - } - if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit - && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() - ) { - $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ]; - } - } elseif ( $action === 'undelete' ) { - if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) { - // Undeleting implies editing - $errors[] = [ 'undelete-cantedit' ]; - } - if ( !$this->exists() - && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) ) - ) { - // Undeleting where nothing currently exists implies creating - $errors[] = [ 'undelete-cantcreate' ]; - } - } - return $errors; - } - - /** - * Check that the user isn't blocked from editing. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkUserBlock( $action, $user, $errors, $rigor, $short ) { - global $wgEmailConfirmToEdit, $wgBlockDisablesLogin; - // Account creation blocks handled at userlogin. - // Unblocking handled in SpecialUnblock - if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) { - return $errors; - } - - // Optimize for a very common case - if ( $action === 'read' && !$wgBlockDisablesLogin ) { - return $errors; - } - - if ( $wgEmailConfirmToEdit - && !$user->isEmailConfirmed() - && $action === 'edit' - ) { - $errors[] = [ 'confirmedittext' ]; - } - - $useReplica = ( $rigor !== 'secure' ); - $block = $user->getBlock( $useReplica ); - - // If the user does not have a block, or the block they do have explicitly - // allows the action (like "read" or "upload"). - if ( !$block || $block->appliesToRight( $action ) === false ) { - return $errors; - } - - // Determine if the user is blocked from this action on this page. - // What gets passed into this method is a user right, not an action name. - // There is no way to instantiate an action by restriction. However, this - // will get the action where the restriction is the same. This may result - // in actions being blocked that shouldn't be. - $actionObj = null; - if ( Action::exists( $action ) ) { - // Clone the title to prevent mutations to this object which is done - // by Title::loadFromRow() in WikiPage::loadFromRow(). - $page = WikiPage::factory( clone $this ); - // Creating an action will perform several database queries to ensure that - // the action has not been overridden by the content type. - // @todo FIXME: Pass the relevant context into this function. - $actionObj = Action::factory( $action, $page, RequestContext::getMain() ); - // Ensure that the retrieved action matches the restriction. - if ( $actionObj && $actionObj->getRestriction() !== $action ) { - $actionObj = null; - } - } - - // If no action object is returned, assume that the action requires unblock - // which is the default. - if ( !$actionObj || $actionObj->requiresUnblock() ) { - if ( $user->isBlockedFrom( $this, $useReplica ) ) { - // @todo FIXME: Pass the relevant context into this function. - $errors[] = $block->getPermissionsError( RequestContext::getMain() ); - } - } - - return $errors; - } - - /** - * Check that the user is allowed to read this page. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param string $rigor Same format as Title::getUserPermissionsErrors() - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) { - global $wgWhitelistRead, $wgWhitelistReadRegexp; - - $whitelisted = false; - if ( User::isEveryoneAllowed( 'read' ) ) { - # Shortcut for public wikis, allows skipping quite a bit of code - $whitelisted = true; - } elseif ( $user->isAllowed( 'read' ) ) { - # If the user is allowed to read pages, he is allowed to read all pages - $whitelisted = true; - } elseif ( $this->isSpecial( 'Userlogin' ) - || $this->isSpecial( 'PasswordReset' ) - || $this->isSpecial( 'Userlogout' ) - ) { - # Always grant access to the login page. - # Even anons need to be able to log in. - $whitelisted = true; - } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { - # Time to check the whitelist - # Only do these checks is there's something to check against - $name = $this->getPrefixedText(); - $dbName = $this->getPrefixedDBkey(); - - // Check for explicit whitelisting with and without underscores - if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { - $whitelisted = true; - } elseif ( $this->mNamespace == NS_MAIN ) { - # Old settings might have the title prefixed with - # a colon for main-namespace pages - if ( in_array( ':' . $name, $wgWhitelistRead ) ) { - $whitelisted = true; - } - } elseif ( $this->isSpecialPage() ) { - # If it's a special page, ditch the subpage bit and check again - $name = $this->mDbkeyform; - list( $name, /* $subpage */ ) = - MediaWikiServices::getInstance()->getSpecialPageFactory()-> - resolveAlias( $name ); - if ( $name ) { - $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); - if ( in_array( $pure, $wgWhitelistRead, true ) ) { - $whitelisted = true; - } - } - } - } - - if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) { - $name = $this->getPrefixedText(); - // Check for regex whitelisting - foreach ( $wgWhitelistReadRegexp as $listItem ) { - if ( preg_match( $listItem, $name ) ) { - $whitelisted = true; - break; - } - } - } - - if ( !$whitelisted ) { - # If the title is not whitelisted, give extensions a chance to do so... - Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] ); - if ( !$whitelisted ) { - $errors[] = $this->missingPermissionError( $action, $short ); - } - } - - return $errors; - } - - /** - * Get a description array when the user doesn't have the right to perform - * $action (i.e. when User::isAllowed() returns false) - * - * @param string $action The action to check - * @param bool $short Short circuit on first error - * @return array Array containing an error message key and any parameters - */ - private function missingPermissionError( $action, $short ) { - // We avoid expensive display logic for quickUserCan's and such - if ( $short ) { - return [ 'badaccess-group0' ]; - } - - return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0]; - } - - /** - * Can $user perform $action on this page? This is an internal function, - * with multiple levels of checks depending on performance needs; see $rigor below. - * It does not check wfReadOnly(). - * - * @param string $action Action that permission needs to be checked for - * @param User $user User to check - * @param string $rigor One of (quick,full,secure) - * - quick : does cheap permission checks from replica DBs (usable for GUI creation) - * - full : does cheap and expensive checks possibly from a replica DB - * - secure : does cheap and expensive checks, using the master as needed - * @param bool $short Set this to true to stop after the first permission error. - * @return array Array of arrays of the arguments to wfMessage to explain permissions problems. - */ - protected function getUserPermissionsErrorsInternal( - $action, $user, $rigor = 'secure', $short = false - ) { - if ( $rigor === true ) { - $rigor = 'secure'; // b/c - } elseif ( $rigor === false ) { - $rigor = 'quick'; // b/c - } elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) { - throw new Exception( "Invalid rigor parameter '$rigor'." ); - } - - # Read has special handling - if ( $action == 'read' ) { - $checks = [ - 'checkPermissionHooks', - 'checkReadPermissions', - 'checkUserBlock', // for wgBlockDisablesLogin - ]; - # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions - # or checkUserConfigPermissions here as it will lead to duplicate - # error messages. This is okay to do since anywhere that checks for - # create will also check for edit, and those checks are called for edit. - } elseif ( $action == 'create' ) { - $checks = [ - 'checkQuickPermissions', - 'checkPermissionHooks', - 'checkPageRestrictions', - 'checkCascadingSourcesRestrictions', - 'checkActionPermissions', - 'checkUserBlock' - ]; - } else { - $checks = [ - 'checkQuickPermissions', - 'checkPermissionHooks', - 'checkSpecialsAndNSPermissions', - 'checkSiteConfigPermissions', - 'checkUserConfigPermissions', - 'checkPageRestrictions', - 'checkCascadingSourcesRestrictions', - 'checkActionPermissions', - 'checkUserBlock' - ]; - } - - $errors = []; - foreach ( $checks as $method ) { - $errors = $this->$method( $action, $user, $errors, $rigor, $short ); - - if ( $short && $errors !== [] ) { - break; - } - } - - return $errors; - } - /** * Get a filtered list of all restriction types supported by this wiki. * @param bool $exists True to get all restriction types that apply to diff --git a/includes/api/ApiContinuationManager.php b/includes/api/ApiContinuationManager.php index 7da8ed9a5b..e4c52dd381 100644 --- a/includes/api/ApiContinuationManager.php +++ b/includes/api/ApiContinuationManager.php @@ -217,6 +217,8 @@ class ApiContinuationManager { // Some modules are unfinished: include those params, and copy // the generator params. foreach ( $continuationData as $module => $kvp ) { + // XXX: Not sure why phan is complaining here... + // @phan-suppress-next-line PhanTypeInvalidLeftOperand $data += $kvp; } $generatorParams = []; diff --git a/includes/block/BlockRestriction.php b/includes/block/BlockRestriction.php index cbd30c2c6f..2e8752e450 100644 --- a/includes/block/BlockRestriction.php +++ b/includes/block/BlockRestriction.php @@ -25,6 +25,7 @@ namespace MediaWiki\Block; use MediaWiki\Block\Restriction\NamespaceRestriction; use MediaWiki\Block\Restriction\PageRestriction; use MediaWiki\Block\Restriction\Restriction; +use MWException; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\IDatabase; diff --git a/includes/composer/ComposerVersionNormalizer.php b/includes/composer/ComposerVersionNormalizer.php index 52bc0cd121..5071fdc808 100644 --- a/includes/composer/ComposerVersionNormalizer.php +++ b/includes/composer/ComposerVersionNormalizer.php @@ -55,7 +55,7 @@ class ComposerVersionNormalizer { $version = substr( $version, 0, $dashPosition ); } - $version = implode( '.', array_pad( explode( '.', $version ), 4, '0' ) ); + $version = implode( '.', array_pad( explode( '.', $version, 4 ), 4, '0' ) ); if ( $dashPosition !== false ) { $version .= $suffix; diff --git a/includes/diff/TextSlotDiffRenderer.php b/includes/diff/TextSlotDiffRenderer.php index e2cdd82b86..001944c5be 100644 --- a/includes/diff/TextSlotDiffRenderer.php +++ b/includes/diff/TextSlotDiffRenderer.php @@ -214,12 +214,6 @@ class TextSlotDiffRenderer extends SlotDiffRenderer { $newText, 2 ); - - // Log a warning in case the configuration value is set to not silently ignore it - if ( $this->wikiDiff2MovedParagraphDetectionCutoff > 0 ) { - wfLogWarning( '$wgWikiDiff2MovedParagraphDetectionCutoff is set but has no - effect since the used version of WikiDiff2 does not support it.' ); - } } return $text; diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php index 3a75720cdf..ab8ef2f8de 100644 --- a/includes/filerepo/file/ForeignAPIFile.php +++ b/includes/filerepo/file/ForeignAPIFile.php @@ -154,7 +154,7 @@ class ForeignAPIFile extends File { /** * @param int $page - * @return int|number + * @return int */ public function getWidth( $page = 1 ) { return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0; diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 134a1045c1..aa04faec8f 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -796,11 +796,14 @@ class LocalFile extends File { /** isVisible inherited */ /** + * Checks if this file exists in its parent repo, as referenced by its + * virtual URL. + * * @return bool */ function isMissing() { if ( $this->missing === null ) { - list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() ); + $fileExists = $this->repo->fileExists( $this->getVirtualUrl() ); $this->missing = !$fileExists; } diff --git a/includes/jobqueue/Job.php b/includes/jobqueue/Job.php index 0b5e62df89..060003b2ec 100644 --- a/includes/jobqueue/Job.php +++ b/includes/jobqueue/Job.php @@ -106,13 +106,13 @@ abstract class Job implements IJobSpecification { /** * @param string $command - * @param array $params + * @param array|Title|null $params */ - public function __construct( $command, $params = [] ) { + public function __construct( $command, $params = null ) { if ( $params instanceof Title ) { // Backwards compatibility for old signature ($command, $title, $params) $title = $params; - $params = func_num_args() >= 3 ? func_get_arg( 2 ) : []; + $params = func_get_arg( 2 ); } else { // Subclasses can override getTitle() to return something more meaningful $title = Title::makeTitle( NS_SPECIAL, 'Blankpage' ); @@ -120,7 +120,7 @@ abstract class Job implements IJobSpecification { $this->command = $command; $this->title = $title; - $this->params = is_array( $params ) ? $params : []; // sanity + $this->params = is_array( $params ) ? $params : []; if ( !isset( $this->params['requestId'] ) ) { $this->params['requestId'] = WebRequest::getRequestId(); } diff --git a/includes/libs/rdbms/ChronologyProtector.php b/includes/libs/rdbms/ChronologyProtector.php index 3e71e3626b..62a296827d 100644 --- a/includes/libs/rdbms/ChronologyProtector.php +++ b/includes/libs/rdbms/ChronologyProtector.php @@ -72,7 +72,7 @@ class ChronologyProtector implements LoggerAwareInterface { /** * @param BagOStuff $store - * @param array[] $client Map of (ip: , agent: [, clientId: ] ) + * @param array $client Map of (ip: , agent: [, clientId: ] ) * @param int|null $posIndex Write counter index [optional] * @since 1.27 */ diff --git a/includes/libs/rdbms/encasing/MssqlBlob.php b/includes/libs/rdbms/encasing/MssqlBlob.php index 8e68aba0d7..6ad934acc4 100644 --- a/includes/libs/rdbms/encasing/MssqlBlob.php +++ b/includes/libs/rdbms/encasing/MssqlBlob.php @@ -7,6 +7,7 @@ class MssqlBlob extends Blob { /** * @param string $data + * @suppress PhanTypeMagicVoidWithReturn */ public function __construct( $data ) { if ( $data instanceof MssqlBlob ) { diff --git a/includes/media/JpegHandler.php b/includes/media/JpegHandler.php index 4bcb53dd56..4aca5b3b80 100644 --- a/includes/media/JpegHandler.php +++ b/includes/media/JpegHandler.php @@ -21,6 +21,8 @@ * @ingroup Media */ +use MediaWiki\Shell\Shell; + /** * JPEG specific handler. * Inherits most stuff from BitmapHandler, just here to do the metadata handler differently. @@ -140,17 +142,23 @@ class JpegHandler extends ExifBitmapHandler { $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360; if ( $wgJpegTran && is_executable( $wgJpegTran ) ) { - $cmd = wfEscapeShellArg( $wgJpegTran ) . - " -rotate " . wfEscapeShellArg( $rotation ) . - " -outfile " . wfEscapeShellArg( $params['dstPath'] ) . - " " . wfEscapeShellArg( $params['srcPath'] ); - wfDebug( __METHOD__ . ": running jpgtran: $cmd\n" ); - $retval = 0; - $err = wfShellExecWithStderr( $cmd, $retval ); - if ( $retval !== 0 ) { - $this->logErrorForExternalProcess( $retval, $err, $cmd ); - - return new MediaTransformError( 'thumbnail_error', 0, 0, $err ); + $command = Shell::command( $wgJpegTran, + '-rotate', + $rotation, + '-outfile', + $params['dstPath'], + $params['srcPath'] + ); + $result = $command + ->includeStderr() + ->execute(); + if ( $result->getExitCode() !== 0 ) { + $this->logErrorForExternalProcess( $result->getExitCode(), + $result->getStdout(), + $command + ); + + return new MediaTransformError( 'thumbnail_error', 0, 0, $result->getStdout() ); } return false; @@ -240,20 +248,21 @@ class JpegHandler extends ExifBitmapHandler { return false; } - $cmd = wfEscapeShellArg( $wgExiftool, + $result = Shell::command( + $wgExiftool, '-EXIF:ColorSpace', '-ICC_Profile:ProfileDescription', '-S', '-T', $filepath - ); - - $output = wfShellExecWithStderr( $cmd, $retval ); + ) + ->includeStderr() + ->execute(); // Explode EXIF data into an array with [0 => Color Space, 1 => Device Model Desc] - $data = explode( "\t", trim( $output ) ); + $data = explode( "\t", trim( $result->getStdout() ) ); - if ( $retval !== 0 ) { + if ( $result->getExitCode() !== 0 ) { return false; } @@ -271,16 +280,20 @@ class JpegHandler extends ExifBitmapHandler { return false; } - $cmd = wfEscapeShellArg( $wgExiftool, + $command = Shell::command( $wgExiftool, '-overwrite_original', '-icc_profile<=' . $profileFilepath, $filepath ); - - $output = wfShellExecWithStderr( $cmd, $retval ); - - if ( $retval !== 0 ) { - $this->logErrorForExternalProcess( $retval, $output, $cmd ); + $result = $command + ->includeStderr() + ->execute(); + + if ( $result->getExitCode() !== 0 ) { + $this->logErrorForExternalProcess( $result->getExitCode(), + $result->getStdout(), + $command + ); return false; } diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 4b0e503ec4..655fa2711f 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -2997,7 +2997,7 @@ class WikiPage implements Page, IDBAccessObject { * @param Content|null $content Page content to be used when determining * the required updates. This may be needed because $this->getContent() * may already return null when the page proper was deleted. - * @param RevisionRecord|Revision|null $revision The current page revision at the time of + * @param Revision|null $revision The current page revision at the time of * deletion, used when determining the required updates. This may be needed because * $this->getRevision() may already return null when the page proper was deleted. * @param User|null $user The user that caused the deletion diff --git a/includes/session/Session.php b/includes/session/Session.php index 3dc8299be9..328958cf1b 100644 --- a/includes/session/Session.php +++ b/includes/session/Session.php @@ -537,7 +537,7 @@ final class Session implements \Countable, \Iterator, \ArrayAccess { // Extension::OATHAuth. // Unseal and check - $pieces = explode( '.', $encrypted ); + $pieces = explode( '.', $encrypted, 4 ); if ( count( $pieces ) !== 3 ) { $ex = new \Exception( 'Invalid sealed-secret format' ); $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] ); diff --git a/includes/user/User.php b/includes/user/User.php index c191c70a15..311cac225c 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -2291,29 +2291,15 @@ class User implements IDBAccessObject, UserIdentity { * @param Title $title Title to check * @param bool $fromReplica Whether to check the replica DB instead of the master * @return bool + * @throws MWException + * + * @deprecated since 1.33, + * use MediaWikiServices::getInstance()->getPermissionManager()->isBlockedFrom(..) + * */ public function isBlockedFrom( $title, $fromReplica = false ) { - $blocked = $this->isHidden(); - - if ( !$blocked ) { - $block = $this->getBlock( $fromReplica ); - if ( $block ) { - // Special handling for a user's own talk page. The block is not aware - // of the user, so this must be done here. - if ( $title->equals( $this->getTalkPage() ) ) { - $blocked = $block->appliesToUsertalk( $title ); - } else { - $blocked = $block->appliesToTitle( $title ); - } - } - } - - // only for the purpose of the hook. We really don't need this here. - $allowUsertalk = $this->mAllowUsertalk; - - Hooks::run( 'UserIsBlockedFrom', [ $this, $title, &$blocked, &$allowUsertalk ] ); - - return $blocked; + return MediaWikiServices::getInstance()->getPermissionManager() + ->isBlockedFrom( $this, $title, $fromReplica ); } /** @@ -5741,4 +5727,14 @@ class User implements IDBAccessObject, UserIdentity { // XXX it's not clear whether central ID providers are supposed to obey this return $this->getName() === $user->getName(); } + + /** + * Checks if usertalk is allowed + * + * @return bool + */ + public function isAllowUsertalk() { + return $this->mAllowUsertalk; + } + } diff --git a/languages/messages/MessagesCs.php b/languages/messages/MessagesCs.php index 6646122f0f..25bab210ad 100644 --- a/languages/messages/MessagesCs.php +++ b/languages/messages/MessagesCs.php @@ -54,6 +54,7 @@ $specialPageAliases = [ 'Ancientpages' => [ 'Nejstarší_stránky', 'Staré_stránky', 'Stare_stranky' ], 'ApiHelp' => [ 'Nápověda_k_API' ], 'ApiSandbox' => [ 'API_pískoviště' ], + 'AutoblockList' => [ 'Seznam_automatických_blokování' ], 'Badtitle' => [ 'Neplatný_název' ], 'Blankpage' => [ 'Prázdná_stránka' ], 'Block' => [ 'Blokování', 'Blokovani', 'Blokovat_uživatele', 'Blokovat_IP', 'Blokovat_uzivatele' ], @@ -62,6 +63,7 @@ $specialPageAliases = [ 'BrokenRedirects' => [ 'Přerušená_přesměrování', 'Prerusena_presmerovani' ], 'Categories' => [ 'Kategorie' ], 'ChangeContentModel' => [ 'Změnit_model_obsahu_stránky' ], + 'ChangeCredentials' => [ 'Změna_přihlašovacích_údajů' ], 'ChangeEmail' => [ 'Změna_emailu', 'Zmena_emailu' ], 'ChangePassword' => [ 'Změna_hesla', 'Zmena_hesla' ], 'ComparePages' => [ 'Porovnání_stránek', 'PorovnáníStránek', 'Porovnani_stranek', 'PorovnaniStranek' ], @@ -111,7 +113,7 @@ $specialPageAliases = [ 'Mypage' => [ 'Moje_stránka', 'Moje_stranka', 'Má_stránka' ], 'Mytalk' => [ 'Moje_diskuse', 'Má_diskuse' ], 'Myuploads' => [ 'Moje_soubory', 'Mé_soubory' ], - 'Newimages' => [ 'Nové_obrázky', 'Galerie_nových_obrázků', 'Nove_obrazky' ], + 'Newimages' => [ 'Nové_soubory', 'Nové_obrázky', 'Galerie_nových_obrázků', 'Nove_obrazky' ], 'Newpages' => [ 'Nové_stránky', 'Nove_stranky', 'Nejnovější_stránky', 'Nejnovejsi_stranky' ], 'PagesWithProp' => [ 'Stránky_s_vlastností', 'Stránky_s_vlastnostmi' ], 'PasswordReset' => [ 'Reset_hesla', 'Resetovat_heslo', 'Obnova_hesla', 'Obnovit_heslo' ], @@ -127,6 +129,8 @@ $specialPageAliases = [ 'Recentchanges' => [ 'Poslední_změny', 'Posledni_zmeny' ], 'Recentchangeslinked' => [ 'Související_změny', 'Souvisejici_zmeny' ], 'Redirect' => [ 'Přesměrování', 'Přesměrovat' ], + 'RemoveCredentials' => [ 'Odstranění_přihlašovacích_údajů' ], + 'ResetTokens' => [ 'Reinicializace_klíčů' ], 'Revisiondelete' => [ 'Smazat_revizi' ], 'Search' => [ 'Hledání', 'Hledani' ], 'Shortpages' => [ 'Nejkratší_stránky', 'Nejkratsi_stranky' ], diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index f43f0a9dd3..5119d737f2 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -1364,6 +1364,9 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { JobQueueGroup::singleton()->get( $type )->delete(); } + // T219673: close any connections from code that failed to call reuseConnection() + // or is still holding onto a DBConnRef instance (e.g. in a singleton). + MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->closeAll(); CloneDatabase::changePrefix( self::$oldTablePrefix ); self::$oldTablePrefix = false; diff --git a/tests/phpunit/includes/Permissions/PermissionManagerTest.php b/tests/phpunit/includes/Permissions/PermissionManagerTest.php new file mode 100644 index 0000000000..7f5ec40eb2 --- /dev/null +++ b/tests/phpunit/includes/Permissions/PermissionManagerTest.php @@ -0,0 +1,1410 @@ +'; + + 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, + ] + ], + ]; + } + +} diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php index 13def702a2..f7ffe8d434 100644 --- a/tests/phpunit/includes/TitlePermissionTest.php +++ b/tests/phpunit/includes/TitlePermissionTest.php @@ -6,8 +6,8 @@ use MediaWiki\MediaWikiServices; /** * @group Database * - * @covers Title::getUserPermissionsErrors - * @covers Title::getUserPermissionsErrorsInternal + * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrors + * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrorsInternal */ class TitlePermissionTest extends MediaWikiLangTestCase { @@ -104,7 +104,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { * This test is failing per T201776. * * @group Broken - * @covers Title::checkQuickPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkQuickPermissions */ public function testQuickPermissions() { $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> @@ -395,7 +395,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkSpecialsAndNSPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkSpecialsAndNSPermissions */ public function testSpecialsAndNSPermissions() { global $wgNamespaceProtection; @@ -452,7 +452,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testJsConfigEditPermissions() { $this->setUser( $this->userName ); @@ -475,7 +475,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testJsonConfigEditPermissions() { $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> @@ -500,7 +500,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testCssConfigEditPermissions() { $this->setUser( $this->userName ); @@ -523,7 +523,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testOtherJsConfigEditPermissions() { $this->setUser( $this->userName ); @@ -546,7 +546,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testOtherJsonConfigEditPermissions() { $this->setUser( $this->userName ); @@ -569,7 +569,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testOtherCssConfigEditPermissions() { $this->setUser( $this->userName ); @@ -592,7 +592,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testOtherNonConfigEditPermissions() { $this->setUser( $this->userName ); @@ -614,7 +614,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This should use data providers like the other methods here. - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testPatrolActionConfigEditPermissions() { $this->setUser( 'anon' ); @@ -687,7 +687,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { * This test is failing per T201776. * * @group Broken - * @covers Title::checkPageRestrictions + * @covers \MediaWiki\Permissions\PermissionManager::checkPageRestrictions */ public function testPageRestrictions() { $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> @@ -780,7 +780,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { } /** - * @covers Title::checkCascadingSourcesRestrictions + * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions */ public function testCascadingSourcesRestrictions() { $this->setTitle( NS_MAIN, "test page" ); @@ -811,7 +811,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkActionPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions */ public function testActionPermissions() { $this->setUserPerm( [ "createpage" ] ); @@ -885,13 +885,14 @@ class TitlePermissionTest extends MediaWikiLangTestCase { } /** - * @covers Title::checkUserBlock + * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock */ public function testUserBlock() { $this->setMwGlobals( [ 'wgEmailConfirmToEdit' => true, 'wgEmailAuthentication' => true, ] ); + $this->overrideMwServices(); $this->setUserPerm( [ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ] ); $this->setTitle( NS_HELP, "test page" ); @@ -903,6 +904,8 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->title->getUserPermissionsErrors( 'edit', $this->user ) ); $this->setMwGlobals( 'wgEmailConfirmToEdit', false ); + $this->overrideMwServices(); + $this->assertNotContains( [ 'confirmedittext' ], $this->title->getUserPermissionsErrors( 'edit', $this->user ) ); @@ -1039,7 +1042,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { } /** - * @covers Title::checkUserBlock + * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock * * Tests to determine that the passed in permission does not get mixed up with * an action of the same name. diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index 35b196a9ba..149c25bc5f 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -1,5 +1,6 @@ assertEquals( $value->getFragment(), $title->getFragment() ); } + /** + * @covers Title::newFromLinkTarget + * @dataProvider provideNewFromTitleValue + */ + public function testNewFromLinkTarget( LinkTarget $value ) { + $title = Title::newFromLinkTarget( $value ); + + $dbkey = str_replace( ' ', '_', $value->getText() ); + $this->assertEquals( $dbkey, $title->getDBkey() ); + $this->assertEquals( $value->getNamespace(), $title->getNamespace() ); + $this->assertEquals( $value->getFragment(), $title->getFragment() ); + } + + /** + * @covers Title::newFromLinkTarget + */ + public function testNewFromLinkTarget_clone() { + $title = Title::newFromText( __METHOD__ ); + $this->assertSame( $title, Title::newFromLinkTarget( $title ) ); + + // The Title::NEW_CLONE flag should ensure that a fresh instance is returned. + $clone = Title::newFromLinkTarget( $title, Title::NEW_CLONE ); + $this->assertNotSame( $title, $clone ); + $this->assertTrue( $clone->equals( $title ) ); + } + public static function provideGetTitleValue() { return [ [ 'Foo' ], diff --git a/tests/phpunit/includes/db/LoadBalancerTest.php b/tests/phpunit/includes/db/LoadBalancerTest.php index 2c4e6b421b..cc01c7dbb5 100644 --- a/tests/phpunit/includes/db/LoadBalancerTest.php +++ b/tests/phpunit/includes/db/LoadBalancerTest.php @@ -32,7 +32,7 @@ use Wikimedia\Rdbms\LoadMonitorNull; * @covers \Wikimedia\Rdbms\LoadBalancer */ class LoadBalancerTest extends MediaWikiTestCase { - private function makeServerConfig() { + private function makeServerConfig( $flags = DBO_DEFAULT ) { global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir; return [ @@ -44,7 +44,7 @@ class LoadBalancerTest extends MediaWikiTestCase { 'type' => $wgDBtype, 'dbDirectory' => $wgSQLiteDataDir, 'load' => 0, - 'flags' => DBO_TRX // REPEATABLE-READ for consistency + 'flags' => $flags ]; } @@ -57,7 +57,8 @@ class LoadBalancerTest extends MediaWikiTestCase { $called = false; $lb = new LoadBalancer( [ - 'servers' => [ $this->makeServerConfig() ], + // Simulate web request with DBO_TRX + 'servers' => [ $this->makeServerConfig( DBO_TRX ) ], 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ), 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ), 'chronologyCallback' => function () use ( &$called ) { @@ -71,8 +72,8 @@ class LoadBalancerTest extends MediaWikiTestCase { $this->assertSame( 'my_test_wiki', $lb->resolveDomainID( 'my_test_wiki' ) ); $this->assertSame( $ld->getId(), $lb->resolveDomainID( false ) ); $this->assertSame( $ld->getId(), $lb->resolveDomainID( $ld ) ); - $this->assertFalse( $called ); + $dbw = $lb->getConnection( DB_MASTER ); $this->assertTrue( $called ); $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' ); @@ -106,39 +107,10 @@ class LoadBalancerTest extends MediaWikiTestCase { } public function testWithReplica() { - global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir; + global $wgDBserver; - $servers = [ - [ // master - 'host' => $wgDBserver, - 'dbname' => $wgDBname, - 'tablePrefix' => $this->dbPrefix(), - 'user' => $wgDBuser, - 'password' => $wgDBpassword, - 'type' => $wgDBtype, - 'dbDirectory' => $wgSQLiteDataDir, - 'load' => 0, - 'flags' => DBO_TRX // REPEATABLE-READ for consistency - ], - [ // emulated replica - 'host' => $wgDBserver, - 'dbname' => $wgDBname, - 'tablePrefix' => $this->dbPrefix(), - 'user' => $wgDBuser, - 'password' => $wgDBpassword, - 'type' => $wgDBtype, - 'dbDirectory' => $wgSQLiteDataDir, - 'load' => 100, - 'flags' => DBO_TRX // REPEATABLE-READ for consistency - ] - ]; - - $lb = new LoadBalancer( [ - 'servers' => $servers, - 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ), - 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ), - 'loadMonitorClass' => LoadMonitorNull::class - ] ); + // Simulate web request with DBO_TRX + $lb = $this->newMultiServerLocalLoadBalancer( DBO_TRX ); $dbw = $lb->getConnection( DB_MASTER ); $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' ); @@ -180,6 +152,51 @@ class LoadBalancerTest extends MediaWikiTestCase { $lb->closeAll(); } + private function newSingleServerLocalLoadBalancer() { + global $wgDBname; + + return new LoadBalancer( [ + 'servers' => [ $this->makeServerConfig() ], + 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ) + ] ); + } + + private function newMultiServerLocalLoadBalancer( $flags = DBO_DEFAULT ) { + global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir; + + $servers = [ + [ // master + 'host' => $wgDBserver, + 'dbname' => $wgDBname, + 'tablePrefix' => $this->dbPrefix(), + 'user' => $wgDBuser, + 'password' => $wgDBpassword, + 'type' => $wgDBtype, + 'dbDirectory' => $wgSQLiteDataDir, + 'load' => 0, + 'flags' => $flags + ], + [ // emulated replica + 'host' => $wgDBserver, + 'dbname' => $wgDBname, + 'tablePrefix' => $this->dbPrefix(), + 'user' => $wgDBuser, + 'password' => $wgDBpassword, + 'type' => $wgDBtype, + 'dbDirectory' => $wgSQLiteDataDir, + 'load' => 100, + 'flags' => $flags + ] + ]; + + return new LoadBalancer( [ + 'servers' => $servers, + 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ), + 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ), + 'loadMonitorClass' => LoadMonitorNull::class + ] ); + } + private function assertWriteForbidden( Database $db ) { try { $db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ ); @@ -286,12 +303,7 @@ class LoadBalancerTest extends MediaWikiTestCase { * @covers LoadBalancer::getAnyOpenConnection() */ function testOpenConnection() { - global $wgDBname; - - $lb = new LoadBalancer( [ - 'servers' => [ $this->makeServerConfig() ], - 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ) - ] ); + $lb = $this->newSingleServerLocalLoadBalancer(); $i = $lb->getWriterIndex(); $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) ); diff --git a/tests/phpunit/includes/media/MediaWikiMediaTestCase.php b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php index 7a536df944..2e9acfade4 100644 --- a/tests/phpunit/includes/media/MediaWikiMediaTestCase.php +++ b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php @@ -70,16 +70,10 @@ abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { * * File must be in the path returned by getFilePath() * @param string $name File name - * @param string|null $type MIME type [optional] + * @param string|false $type MIME type [optional] * @return UnregisteredLocalFile */ - protected function dataFile( $name, $type = null ) { - if ( !$type ) { - // Autodetect by file extension for the lazy. - $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); - $parts = explode( $name, '.' ); - $type = $magic->guessTypesForExtension( $parts[count( $parts ) - 1] ); - } + protected function dataFile( $name, $type = false ) { return new UnregisteredLocalFile( false, $this->repo, "mwstore://localtesting/data/$name", $type ); }