Файловый менеджер - Редактировать - /var/www/html/Permissions.zip
Ðазад
PK ! ��5 5 UltimateAuthority.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Permissions; use InvalidArgumentException; use MediaWiki\Block\Block; use MediaWiki\Page\PageIdentity; use MediaWiki\User\UserIdentity; use Wikimedia\Rdbms\IDBAccessObject; /** * Represents an authority that has all permissions. * This is intended for use in maintenance scripts and tests. * * @newable * @since 1.36 */ class UltimateAuthority implements Authority { /** @var UserIdentity */ private $actor; /** @var bool */ private $isTemp; /** * @stable to call * @param UserIdentity $actor * @param bool $isTemp */ public function __construct( UserIdentity $actor, $isTemp = false ) { $this->actor = $actor; $this->isTemp = $isTemp; } /** @inheritDoc */ public function getUser(): UserIdentity { return $this->actor; } /** @inheritDoc */ public function getBlock( int $freshness = IDBAccessObject::READ_NORMAL ): ?Block { return null; } /** @inheritDoc */ public function isAllowed( string $permission, ?PermissionStatus $status = null ): bool { return true; } /** @inheritDoc */ public function isAllowedAny( ...$permissions ): bool { if ( !$permissions ) { throw new InvalidArgumentException( 'At least one permission must be specified' ); } return true; } /** @inheritDoc */ public function isAllowedAll( ...$permissions ): bool { if ( !$permissions ) { throw new InvalidArgumentException( 'At least one permission must be specified' ); } return true; } /** @inheritDoc */ public function probablyCan( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return true; } /** @inheritDoc */ public function definitelyCan( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return true; } /** @inheritDoc */ public function isDefinitelyAllowed( string $action, ?PermissionStatus $status = null ): bool { return true; } /** @inheritDoc */ public function authorizeAction( string $action, ?PermissionStatus $status = null ): bool { return true; } /** @inheritDoc */ public function authorizeRead( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return true; } /** @inheritDoc */ public function authorizeWrite( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return true; } public function isRegistered(): bool { return $this->actor->isRegistered(); } public function isTemp(): bool { return $this->isTemp; } public function isNamed(): bool { return $this->isRegistered() && !$this->isTemp(); } } PK ! �괼� � RateLimitSubject.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Permissions; use MediaWiki\User\UserIdentity; /** * Represents the subject that rate limits are applied to. * * @unstable * @since 1.39 */ class RateLimitSubject { /** * @var UserIdentity */ private $user; /** * @var string|null */ private $ip; /** * @var array */ private $flags; /** @var string Flag indicating the user is exempt from rate limits */ public const EXEMPT = 'exempt'; /** @var string Flag indicating the user is a newbie */ public const NEWBIE = 'newbie'; /** * @internal * * @param UserIdentity $user * @param string|null $ip * @param array<string,bool> $flags */ public function __construct( UserIdentity $user, ?string $ip, array $flags ) { $this->user = $user; $this->ip = $ip; $this->flags = $flags; } /** * @return UserIdentity */ public function getUser(): UserIdentity { return $this->user; } /** * @return string|null */ public function getIP(): ?string { return $this->ip; } /** * Checks whether the given flag applies. * * @param string $flag * * @return bool */ public function is( string $flag ) { return !empty( $this->flags[$flag] ); } } PK ! ˆ��B B GrantsInfo.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Permissions; use MediaWiki\Config\ServiceOptions; use MediaWiki\MainConfigNames; /** * Users can authorize applications to use their account via OAuth. Grants are used to * limit permissions for these application. This service allows application logic to * access grants. * * @since 1.38 */ class GrantsInfo { /** * Risk level classification for grants which aren't particularly risky. These grants might * be abused, e.g. for vandalism, but the effect is easy to undo and the efficiency of abusing * them isn't particularly different from registering new user accounts and using those for * abuse. * Note that risk levels depend on the use case; the default classification is meant for * "normal" (public, open registration) wikis. Classification for e.g. a private wiki holding * confidential information could be quite different. */ public const RISK_LOW = 'low'; /** * Risk level classification for grants which can be used for disruptive vandalism or other * kinds of abuse that couldn't be achieved just by registering new accounts, such as main * page vandalism, vandalism of popular templates, page merge vandalism, or blocks. */ public const RISK_VANDALISM = 'vandalism'; /** * Risk level classification for grants which can be used to cause damage that is hard or * impossible to undo, such as exfiltrating sensitive private data or creating security * vulnerabilities. */ public const RISK_SECURITY = 'security'; /** * Risk level classification for grants which are used for internal purposes and should not * be handed out. */ public const RISK_INTERNAL = 'internal'; /** * @internal For use by ServiceWiring */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::GrantPermissions, MainConfigNames::GrantPermissionGroups, MainConfigNames::GrantRiskGroups, ]; private ServiceOptions $options; public function __construct( ServiceOptions $options ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; } /** * List all known grants. * @return string[] */ public function getValidGrants(): array { return array_keys( $this->options->get( MainConfigNames::GrantPermissions ) ); } /** * Map all grants to corresponding user rights. * @return string[][] grant => array of rights in the grant */ public function getRightsByGrant(): array { $res = []; foreach ( $this->options->get( MainConfigNames::GrantPermissions ) as $grant => $rights ) { $res[$grant] = array_keys( array_filter( $rights ) ); } return $res; } /** * Fetch the rights allowed by a set of grants. * @param string[]|string $grants * @return string[] */ public function getGrantRights( $grants ): array { $rights = []; foreach ( (array)$grants as $grant ) { if ( isset( $this->options->get( MainConfigNames::GrantPermissions )[$grant] ) ) { $rights = array_merge( $rights, array_keys( array_filter( $this->options->get( MainConfigNames::GrantPermissions )[$grant] ) ) ); } } return array_unique( $rights ); } /** * Test that all grants in the list are known. * @param string[] $grants * @return bool */ public function grantsAreValid( array $grants ): bool { return array_diff( $grants, $this->getValidGrants() ) === []; } /** * Divide the grants into groups. * @param string[]|null $grantsFilter * @return string[][] Map of (group => (grant list)) */ public function getGrantGroups( ?array $grantsFilter = null ): array { if ( is_array( $grantsFilter ) ) { $grantsFilter = array_fill_keys( $grantsFilter, true ); } $groups = []; foreach ( $this->options->get( MainConfigNames::GrantPermissions ) as $grant => $rights ) { if ( $grantsFilter !== null && !isset( $grantsFilter[$grant] ) ) { continue; } if ( isset( $this->options->get( MainConfigNames::GrantPermissionGroups )[$grant] ) ) { $groups[$this->options->get( MainConfigNames::GrantPermissionGroups )[$grant]][] = $grant; } else { $groups['other'][] = $grant; } } return $groups; } /** * Get the list of grants that are hidden and should always be granted. * @return string[] */ public function getHiddenGrants(): array { $grants = []; foreach ( $this->options->get( MainConfigNames::GrantPermissionGroups ) as $grant => $group ) { if ( $group === 'hidden' ) { $grants[] = $grant; } } return $grants; } /** * Returns a map of grant name => risk group. The risk groups are the GrantsInfo::RISK_* * constants, plus $default for grants where the risk level is not defined. * @param string $default Default risk group to assign to grants for which no risk group * is configured. $default does not have to be one of the RISK_* constants. * @return string[] * @since 1.42 */ public function getRiskGroupsByGrant( string $default = 'unknown' ): array { $res = []; $grantRiskGroups = $this->options->get( MainConfigNames::GrantRiskGroups ); foreach ( $this->options->get( MainConfigNames::GrantPermissions ) as $grant => $_ ) { $res[$grant] = $grantRiskGroups[$grant] ?? $default; } return $res; } } PK ! �aKX X RestrictionStore.phpnu �Iw�� <?php namespace MediaWiki\Permissions; use MediaWiki\Cache\CacheKeyHelper; use MediaWiki\Cache\LinkCache; use MediaWiki\CommentStore\CommentStore; use MediaWiki\Config\ServiceOptions; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Linker\LinksMigration; use MediaWiki\MainConfigNames; use MediaWiki\Page\PageIdentity; use MediaWiki\Page\PageIdentityValue; use MediaWiki\Page\PageStore; use MediaWiki\Title\Title; use MediaWiki\Title\TitleValue; use stdClass; use Wikimedia\ObjectCache\WANObjectCache; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DBAccessObjectUtils; use Wikimedia\Rdbms\IDBAccessObject; use Wikimedia\Rdbms\ILoadBalancer; use Wikimedia\Rdbms\IReadableDatabase; /** * @since 1.37 */ class RestrictionStore { /** @internal */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::NamespaceProtection, MainConfigNames::RestrictionLevels, MainConfigNames::RestrictionTypes, MainConfigNames::SemiprotectedRestrictionLevels, ]; private ServiceOptions $options; private WANObjectCache $wanCache; private ILoadBalancer $loadBalancer; private LinkCache $linkCache; private LinksMigration $linksMigration; private CommentStore $commentStore; private HookContainer $hookContainer; private HookRunner $hookRunner; private PageStore $pageStore; /** * @var array[] Caching various restrictions data in the following format: * cache key => [ * string[] `restrictions` => restrictions loaded for pages * ?string `expiry` => restrictions expiry data for pages * ?array `create_protection` => value for getCreateProtection * bool `cascade` => cascade restrictions on this page to included templates and images? * array[] `cascade_sources` => the results of getCascadeProtectionSources * ] */ private $cache = []; public function __construct( ServiceOptions $options, WANObjectCache $wanCache, ILoadBalancer $loadBalancer, LinkCache $linkCache, LinksMigration $linksMigration, CommentStore $commentStore, HookContainer $hookContainer, PageStore $pageStore ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->wanCache = $wanCache; $this->loadBalancer = $loadBalancer; $this->linkCache = $linkCache; $this->linksMigration = $linksMigration; $this->commentStore = $commentStore; $this->hookContainer = $hookContainer; $this->hookRunner = new HookRunner( $hookContainer ); $this->pageStore = $pageStore; } /** * Returns list of restrictions for specified page * * @param PageIdentity $page Must be local * @param string $action Action that restrictions need to be checked for * @return string[] Restriction levels needed to take the action. All levels are required. Note * that restriction levels are normally user rights, but 'sysop' and 'autoconfirmed' are also * allowed for backwards compatibility. These should be mapped to 'editprotected' and * 'editsemiprotected' respectively. Returns an empty array if there are no restrictions set * for this action (including for unrecognized actions). */ public function getRestrictions( PageIdentity $page, string $action ): array { $page->assertWiki( PageIdentity::LOCAL ); // Optimization: Avoid repeatedly fetching page restrictions (from cache or DB) // for repeated PermissionManager::userCan calls, if this action cannot be restricted // in the first place. This is primarily to improve batch rendering on RecentChanges, // where as of writing this will save 0.5s on a 8.0s response. (T341319) $restrictionTypes = $this->listApplicableRestrictionTypes( $page ); if ( !in_array( $action, $restrictionTypes ) ) { return []; } $restrictions = $this->getAllRestrictions( $page ); return $restrictions[$action] ?? []; } /** * Returns the restricted actions and their restrictions for the specified page * * @param PageIdentity $page Must be local * @return string[][] Keys are actions, values are arrays as returned by * RestrictionStore::getRestrictions(). Empty if no restrictions are in place. */ public function getAllRestrictions( PageIdentity $page ): array { $page->assertWiki( PageIdentity::LOCAL ); if ( !$this->areRestrictionsLoaded( $page ) ) { $this->loadRestrictions( $page ); } return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] ?? []; } /** * Get the expiry time for the restriction against a given action * * @param PageIdentity $page Must be local * @param string $action * @return ?string 14-char timestamp, or 'infinity' if the page is protected forever or not * protected at all, or null if the action is not recognized. */ public function getRestrictionExpiry( PageIdentity $page, string $action ): ?string { $page->assertWiki( PageIdentity::LOCAL ); if ( !$this->areRestrictionsLoaded( $page ) ) { $this->loadRestrictions( $page ); } return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['expiry'][$action] ?? null; } /** * Is this title subject to protection against creation? * * @param PageIdentity $page Must be local * @return ?array Null if no restrictions. Otherwise an array with the following keys: * - user: user id * - expiry: 14-digit timestamp or 'infinity' * - permission: string (pt_create_perm) * - reason: string * @internal Only to be called by Title::getTitleProtection. When that is discontinued, this * will be too, in favor of getRestrictions( $page, 'create' ). If someone wants to know who * protected it or the reason, there should be a method that exposes that for all restriction * types. */ public function getCreateProtection( PageIdentity $page ): ?array { $page->assertWiki( PageIdentity::LOCAL ); $protection = $this->getCreateProtectionInternal( $page ); // TODO: the remapping below probably need to be migrated into other method one day if ( $protection ) { if ( $protection['permission'] == 'sysop' ) { $protection['permission'] = 'editprotected'; // B/C } if ( $protection['permission'] == 'autoconfirmed' ) { $protection['permission'] = 'editsemiprotected'; // B/C } } return $protection; } /** * Remove any title creation protection due to page existing * * @param PageIdentity $page Must be local * @internal Only to be called by WikiPage::onArticleCreate. */ public function deleteCreateProtection( PageIdentity $page ): void { $page->assertWiki( PageIdentity::LOCAL ); $dbw = $this->loadBalancer->getConnection( DB_PRIMARY ); $dbw->newDeleteQueryBuilder() ->deleteFrom( 'protected_titles' ) ->where( [ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ] ) ->caller( __METHOD__ )->execute(); $this->cache[CacheKeyHelper::getKeyForPage( $page )]['create_protection'] = null; } /** * Is this page "semi-protected" - the *only* protection levels are listed in * $wgSemiprotectedRestrictionLevels? * * @param PageIdentity $page Must be local * @param string $action Action to check (default: edit) * @return bool */ public function isSemiProtected( PageIdentity $page, string $action = 'edit' ): bool { $page->assertWiki( PageIdentity::LOCAL ); $restrictions = $this->getRestrictions( $page, $action ); $semi = $this->options->get( MainConfigNames::SemiprotectedRestrictionLevels ); if ( !$restrictions || !$semi ) { // Not protected, or all protection is full protection return false; } // Remap autoconfirmed to editsemiprotected for BC foreach ( array_keys( $semi, 'editsemiprotected' ) as $key ) { $semi[$key] = 'autoconfirmed'; } foreach ( array_keys( $restrictions, 'editsemiprotected' ) as $key ) { $restrictions[$key] = 'autoconfirmed'; } return !array_diff( $restrictions, $semi ); } /** * Does the title correspond to a protected article? * * @param PageIdentity $page Must be local * @param string $action The action the page is protected from, by default checks all actions. * @return bool */ public function isProtected( PageIdentity $page, string $action = '' ): bool { $page->assertWiki( PageIdentity::LOCAL ); // Special pages have inherent protection (TODO: remove after switch to ProperPageIdentity) if ( $page->getNamespace() === NS_SPECIAL ) { return true; } // Check regular protection levels $applicableTypes = $this->listApplicableRestrictionTypes( $page ); if ( $action === '' ) { foreach ( $applicableTypes as $type ) { if ( $this->isProtected( $page, $type ) ) { return true; } } return false; } if ( !in_array( $action, $applicableTypes ) ) { return false; } return (bool)array_diff( array_intersect( $this->getRestrictions( $page, $action ), $this->options->get( MainConfigNames::RestrictionLevels ) ), [ '' ] ); } /** * Cascading protection: Return true if cascading restrictions apply to this page, false if not. * * @param PageIdentity $page Must be local * @return bool If the page is subject to cascading restrictions. */ public function isCascadeProtected( PageIdentity $page ): bool { $page->assertWiki( PageIdentity::LOCAL ); return $this->getCascadeProtectionSourcesInternal( $page )[0] !== []; } /** * Returns restriction types for the current page * * @param PageIdentity $page Must be local * @return string[] Applicable restriction types */ public function listApplicableRestrictionTypes( PageIdentity $page ): array { $page->assertWiki( PageIdentity::LOCAL ); if ( !$page->canExist() ) { return []; } $types = $this->listAllRestrictionTypes( $page->exists() ); if ( $page->getNamespace() !== NS_FILE ) { // Remove the upload restriction for non-file titles $types = array_values( array_diff( $types, [ 'upload' ] ) ); } if ( $this->hookContainer->isRegistered( 'TitleGetRestrictionTypes' ) ) { $this->hookRunner->onTitleGetRestrictionTypes( Title::newFromPageIdentity( $page ), $types ); } return $types; } /** * Get a filtered list of all restriction types supported by this wiki. * * @param bool $exists True to get all restriction types that apply to titles that do exist, * false for all restriction types that apply to titles that do not exist * @return string[] */ public function listAllRestrictionTypes( bool $exists = true ): array { $types = $this->options->get( MainConfigNames::RestrictionTypes ); if ( $exists ) { // Remove the create restriction for existing titles return array_values( array_diff( $types, [ 'create' ] ) ); } // Only the create restrictions apply to non-existing titles return array_values( array_intersect( $types, [ 'create' ] ) ); } /** * Load restrictions from page.page_restrictions and the page_restrictions table * * @param PageIdentity $page Must be local * @param int $flags IDBAccessObject::READ_XXX constants (e.g., READ_LATEST to read from * primary DB) * @internal Public for use in WikiPage only */ public function loadRestrictions( PageIdentity $page, int $flags = IDBAccessObject::READ_NORMAL ): void { $page->assertWiki( PageIdentity::LOCAL ); if ( !$page->canExist() ) { return; } $readLatest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST ); if ( $this->areRestrictionsLoaded( $page ) && !$readLatest ) { return; } $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )]; $cacheEntry['restrictions'] = []; // XXX Work around https://phabricator.wikimedia.org/T287575 if ( $readLatest ) { $page = $this->pageStore->getPageByReference( $page, $flags ) ?? $page; } $id = $page->getId(); if ( $id ) { $fname = __METHOD__; $loadRestrictionsFromDb = static function ( IReadableDatabase $dbr ) use ( $fname, $id ) { return iterator_to_array( $dbr->newSelectQueryBuilder() ->select( [ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ] ) ->from( 'page_restrictions' ) ->where( [ 'pr_page' => $id ] ) ->caller( $fname )->fetchResultSet() ); }; if ( $readLatest ) { $dbr = $this->loadBalancer->getConnection( DB_PRIMARY ); $rows = $loadRestrictionsFromDb( $dbr ); } else { $this->pageStore->getPageForLink( TitleValue::newFromPage( $page ) )->getId(); $latestRev = $this->linkCache->getGoodLinkFieldObj( $page, 'revision' ); if ( !$latestRev ) { // This method can get called in the middle of page creation // (WikiPage::doUserEditContent) where a page might have an // id but no revisions, while checking the "autopatrol" permission. $rows = []; } else { $rows = $this->wanCache->getWithSetCallback( // Page protections always leave a new null revision $this->wanCache->makeKey( 'page-restrictions', 'v1', $id, $latestRev ), $this->wanCache::TTL_DAY, function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) { $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); $setOpts += Database::getCacheSetOptions( $dbr ); if ( $this->loadBalancer->hasOrMadeRecentPrimaryChanges() ) { // TODO: cleanup Title cache and caller assumption mess in general $ttl = WANObjectCache::TTL_UNCACHEABLE; } return $loadRestrictionsFromDb( $dbr ); } ); } } $this->loadRestrictionsFromRows( $page, $rows ); } else { $titleProtection = $this->getCreateProtectionInternal( $page ); if ( $titleProtection ) { $now = wfTimestampNow(); $expiry = $titleProtection['expiry']; if ( !$expiry || $expiry > $now ) { // Apply the restrictions $cacheEntry['expiry']['create'] = $expiry ?: null; $cacheEntry['restrictions']['create'] = explode( ',', trim( $titleProtection['permission'] ) ); } else { // Get rid of the old restrictions $cacheEntry['create_protection'] = null; } } else { $cacheEntry['expiry']['create'] = 'infinity'; } } } /** * Compiles list of active page restrictions for this existing page. * Public for usage by LiquidThreads. * * @param PageIdentity $page Must be local * @param stdClass[] $rows Array of db result objects */ public function loadRestrictionsFromRows( PageIdentity $page, array $rows ): void { $page->assertWiki( PageIdentity::LOCAL ); $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )]; $restrictionTypes = $this->listApplicableRestrictionTypes( $page ); foreach ( $restrictionTypes as $type ) { $cacheEntry['restrictions'][$type] = []; $cacheEntry['expiry'][$type] = 'infinity'; } $cacheEntry['cascade'] = false; if ( !$rows ) { return; } // New restriction format -- load second to make them override old-style restrictions. $now = wfTimestampNow(); // Cycle through all the restrictions. foreach ( $rows as $row ) { // Don't take care of restrictions types that aren't allowed if ( !in_array( $row->pr_type, $restrictionTypes ) ) { continue; } $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); $expiry = $dbr->decodeExpiry( $row->pr_expiry ); // Only apply the restrictions if they haven't expired! // XXX Why would !$expiry ever be true? It should always be either 'infinity' or a // string consisting of 14 digits. Likewise for the ?: below. if ( !$expiry || $expiry > $now ) { $cacheEntry['expiry'][$row->pr_type] = $expiry ?: null; $cacheEntry['restrictions'][$row->pr_type] = explode( ',', trim( $row->pr_level ) ); if ( $row->pr_cascade ) { $cacheEntry['cascade'] = true; } } } } /** * Fetch title protection settings * * To work correctly, $this->loadRestrictions() needs to have access to the actual protections * in the database without munging 'sysop' => 'editprotected' and 'autoconfirmed' => * 'editsemiprotected'. * * @param PageIdentity $page Must be local * @return ?array Same format as getCreateProtection(). */ private function getCreateProtectionInternal( PageIdentity $page ): ?array { // Can't protect pages in special namespaces if ( !$page->canExist() ) { return null; } // Can't apply this type of protection to pages that exist. if ( $page->exists() ) { return null; } $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )]; if ( !$cacheEntry || !array_key_exists( 'create_protection', $cacheEntry ) ) { $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); $commentQuery = $this->commentStore->getJoin( 'pt_reason' ); $row = $dbr->newSelectQueryBuilder() ->select( [ 'pt_user', 'pt_expiry', 'pt_create_perm' ] ) ->from( 'protected_titles' ) ->where( [ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ] ) ->queryInfo( $commentQuery ) ->caller( __METHOD__ ) ->fetchRow(); if ( $row ) { $cacheEntry['create_protection'] = [ 'user' => $row->pt_user, 'expiry' => $dbr->decodeExpiry( $row->pt_expiry ), 'permission' => $row->pt_create_perm, 'reason' => $this->commentStore->getComment( 'pt_reason', $row )->text, ]; } else { $cacheEntry['create_protection'] = null; } } return $cacheEntry['create_protection']; } /** * Cascading protection: Get the source of any cascading restrictions on this page. * * @param PageIdentity $page Must be local * @return array[] Four elements: First is an array of PageIdentity objects combining the * third and fourth elements of this array, which may be empty. * Second is an array like that returned by getAllRestrictions(). * Third is an array of PageIdentity objects of the pages from * which cascading restrictions have come, orginating via templatelinks, which may be empty. * Fourth is an array of PageIdentity objects of the pages from * which cascading restrictions have come, orginating via imagelinks, which may be empty. */ public function getCascadeProtectionSources( PageIdentity $page ): array { $page->assertWiki( PageIdentity::LOCAL ); return $this->getCascadeProtectionSourcesInternal( $page ); } /** * Cascading protection: Get the source of any cascading restrictions on this page. * * @param PageIdentity $page Must be local * @return array[] Same as getCascadeProtectionSources(). */ private function getCascadeProtectionSourcesInternal( PageIdentity $page ): array { if ( !$page->canExist() ) { return [ [], [], [], [] ]; } $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )]; if ( isset( $cacheEntry['cascade_sources'] ) ) { return $cacheEntry['cascade_sources']; } $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); $baseQuery = $dbr->newSelectQueryBuilder() ->select( [ 'pr_expiry', 'pr_page', 'page_namespace', 'page_title', 'pr_type', 'pr_level' ] ) ->from( 'page_restrictions' ) ->join( 'page', null, 'page_id=pr_page' ) ->where( [ 'pr_cascade' => 1 ] ); $imageQuery = clone $baseQuery; $imageQuery->join( 'imagelinks', null, 'il_from=pr_page' ) ->fields( [ 'type' => $dbr->addQuotes( 'il' ), ] ) ->andWhere( [ 'il_to' => $page->getDBkey() ] ); $templateQuery = clone $baseQuery; $templateQuery->join( 'templatelinks', null, 'tl_from=pr_page' ) ->fields( [ 'type' => $dbr->addQuotes( 'tl' ), ] ) ->andWhere( $this->linksMigration->getLinksConditions( 'templatelinks', TitleValue::newFromPage( $page ) ) ); if ( $page->getNamespace() === NS_FILE ) { $unionQuery = $dbr->newUnionQueryBuilder() ->add( $imageQuery ) ->add( $templateQuery ) ->all(); $res = $unionQuery->caller( __METHOD__ )->fetchResultSet(); } else { $res = $templateQuery->caller( __METHOD__ )->fetchResultSet(); } $tlSources = []; $ilSources = []; $pageRestrictions = []; $now = wfTimestampNow(); foreach ( $res as $row ) { $expiry = $dbr->decodeExpiry( $row->pr_expiry ); if ( $expiry > $now ) { if ( $row->type === 'il' ) { $ilSources[$row->pr_page] = new PageIdentityValue( $row->pr_page, $row->page_namespace, $row->page_title, PageIdentity::LOCAL ); } elseif ( $row->type === 'tl' ) { $tlSources[$row->pr_page] = new PageIdentityValue( $row->pr_page, $row->page_namespace, $row->page_title, PageIdentity::LOCAL ); } // Add groups needed for each restriction type if its not already there // Make sure this restriction type still exists if ( !isset( $pageRestrictions[$row->pr_type] ) ) { $pageRestrictions[$row->pr_type] = []; } if ( !in_array( $row->pr_level, $pageRestrictions[$row->pr_type] ) ) { $pageRestrictions[$row->pr_type][] = $row->pr_level; } } } $sources = array_replace( $tlSources, $ilSources ); $cacheEntry['cascade_sources'] = [ $sources, $pageRestrictions, $tlSources, $ilSources ]; return $cacheEntry['cascade_sources']; } /** * @param PageIdentity $page Must be local * @return bool Whether or not the page's restrictions have already been loaded from the * database */ public function areRestrictionsLoaded( PageIdentity $page ): bool { $page->assertWiki( PageIdentity::LOCAL ); return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] ); } /** * Determines whether cascading protection sources have already been loaded from the database. * * @param PageIdentity $page Must be local * @return bool */ public function areCascadeProtectionSourcesLoaded( PageIdentity $page ): bool { $page->assertWiki( PageIdentity::LOCAL ); return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade_sources'] ); } /** * Checks if restrictions are cascading for the current page * * @param PageIdentity $page Must be local * @return bool */ public function areRestrictionsCascading( PageIdentity $page ): bool { $page->assertWiki( PageIdentity::LOCAL ); if ( !$this->areRestrictionsLoaded( $page ) ) { $this->loadRestrictions( $page ); } return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade'] ?? false; } /** * Flush the protection cache in this object and force reload from the database. This is used * when updating protection from WikiPage::doUpdateRestrictions(). * * @param PageIdentity $page Must be local * @internal */ public function flushRestrictions( PageIdentity $page ): void { $page->assertWiki( PageIdentity::LOCAL ); unset( $this->cache[CacheKeyHelper::getKeyForPage( $page )] ); } } PK ! 7[Q� � GrantsLocalization.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Permissions; use HtmlArmor; use MediaWiki\Html\Html; use MediaWiki\Language\Language; use MediaWiki\Languages\LanguageFactory; use MediaWiki\Linker\LinkRenderer; use MediaWiki\SpecialPage\SpecialPage; /** * This separate service is needed because the ::getGrantsLink method requires a LinkRenderer * and if we tried to inject a LinkRenderer into the GrantsInfo service, it would result in * recursive service instantiation for sessions using the BotPasswordSessionProvider, as a * result of injecting the LinkRenderer when trying to use a GrantsInfo method that doesn't * even need it. * * @since 1.38 */ class GrantsLocalization { private GrantsInfo $grantsInfo; private LinkRenderer $linkRenderer; private LanguageFactory $languageFactory; private Language $contentLanguage; public function __construct( GrantsInfo $grantsInfo, LinkRenderer $linkRenderer, LanguageFactory $languageFactory, Language $contentLanguage ) { $this->grantsInfo = $grantsInfo; $this->linkRenderer = $linkRenderer; $this->languageFactory = $languageFactory; $this->contentLanguage = $contentLanguage; } /** * Fetch the description of the grant. * @param string $grant * @param Language|string|null $lang * @return string Grant description */ public function getGrantDescription( string $grant, $lang = null ): string { // Give grep a chance to find the usages: // grant-blockusers, grant-createeditmovepage, grant-delete, // grant-editinterface, grant-editmycssjs, grant-editmywatchlist, // grant-editsiteconfig, grant-editpage, grant-editprotected, // grant-highvolume, grant-oversight, grant-patrol, grant-protect, // grant-rollback, grant-sendemail, grant-uploadeditmovefile, // grant-uploadfile, grant-basic, grant-viewdeleted, // grant-viewmywatchlist, grant-createaccount, grant-mergehistory, // grant-import // TODO: replace wfMessage with something that can be injected like TextFormatter $msg = wfMessage( "grant-$grant" ); if ( $lang ) { $msg->inLanguage( $lang ); } if ( !$msg->exists() ) { $msg = $lang ? wfMessage( 'grant-generic', $grant )->inLanguage( $lang ) : wfMessage( 'grant-generic', $grant ); } return $msg->text(); } /** * Fetch the descriptions for the grants. * @param string[] $grants * @param Language|string|null $lang * @return string[] Corresponding grant descriptions, keyed by grant name */ public function getGrantDescriptions( array $grants, $lang = null ): array { $ret = []; foreach ( $grants as $grant ) { $ret[$grant] = $this->getGrantDescription( $grant, $lang ); } return $ret; } /** * Fetch the descriptions for the grants, like getGrantDescriptions, but with HTML classes * for styling. The HTML is wikitext-compatible. * @param string[] $grants * @param Language|string|null $lang * @return string[] Grant description HTML for each grant, in the same order */ public function getGrantDescriptionsWithClasses( array $grants, $lang = null ): array { $riskGroupsByGrant = $this->grantsInfo->getRiskGroupsByGrant( 'unknown' ); $grantDescriptions = $this->getGrantDescriptions( $grants, $lang ); $results = []; foreach ( $grantDescriptions as $grant => $description ) { $riskGroup = $riskGroupsByGrant[$grant] ?? 'unknown'; // Messages used here: grantriskgroup-vandalism, grantriskgroup-security, // grantriskgroup-internal $riskGroupMsg = wfMessage( "grantriskgroup-$riskGroup" ); if ( $lang ) { $riskGroupMsg->inLanguage( $lang ); } if ( $riskGroupMsg->exists() ) { $riskDescription = $riskGroupMsg->text(); } else { $riskDescription = ''; } $results[] = htmlspecialchars( $description ) . ' ' . Html::element( 'span', [ 'class' => "mw-grant mw-grantriskgroup-$riskGroup" ], $riskDescription ); } return $results; } /** * Generate a link to Special:ListGrants for a particular grant name. * * This can be used to link end users to a full description of what * rights they are giving when they authorize a grant. * * @param string $grant the grant name * @param Language|string|null $lang * @return string (proto-relative) HTML link */ public function getGrantsLink( string $grant, $lang = null ): string { $riskGroupsByGrant = $this->grantsInfo->getRiskGroupsByGrant( 'unknown' ); $riskGroup = $riskGroupsByGrant[$grant] ?? 'unknown'; return $this->linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Listgrants', false, $grant ), new HtmlArmor( $this->getGrantDescriptionsWithClasses( [ $grant ], $lang )[ 0 ] ) ); } /** * Generate wikitext to display a list of grants. It will be in the format * * <grant-group-$group> * : <grant-$grant>; <grant-$grant>; ... * * ... * with some HTML classes for styling. * @param string[]|null $grantsFilter If non-null, only display these grants. * @param Language|string|null $lang * @return string Wikitext */ public function getGrantsWikiText( $grantsFilter, $lang = null ): string { if ( is_string( $lang ) ) { $lang = $this->languageFactory->getLanguage( $lang ); } elseif ( $lang === null ) { $lang = $this->contentLanguage; } $s = ''; foreach ( $this->grantsInfo->getGrantGroups( $grantsFilter ) as $group => $grants ) { if ( $group === 'hidden' ) { continue; // implicitly granted } $grantDescriptionsWithClasses = $this->getGrantDescriptionsWithClasses( $grants, $lang ); // Give grep a chance to find the usages: // grant-group-page-interaction, grant-group-file-interaction // grant-group-watchlist-interaction, grant-group-email, // grant-group-high-volume, grant-group-customization, // grant-group-administration, grant-group-private-information, // grant-group-other $s .= "*<span class=\"mw-grantgroup\">" . // TODO: replace wfMessage with something that can be injected like TextFormatter wfMessage( "grant-group-$group" )->inLanguage( $lang )->text() . "</span>\n"; $s .= ":" . $lang->semicolonList( $grantDescriptionsWithClasses ) . "\n"; } return "$s\n"; } } PK ! �^�6 �6 UserAuthority.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Permissions; use InvalidArgumentException; use MediaWiki\Block\Block; use MediaWiki\Block\BlockErrorFormatter; use MediaWiki\Context\IContextSource; use MediaWiki\Linker\LinkTarget; use MediaWiki\Page\PageIdentity; use MediaWiki\Request\WebRequest; use MediaWiki\Title\TitleValue; use MediaWiki\User\User; use MediaWiki\User\UserIdentity; use Wikimedia\Assert\Assert; use Wikimedia\DebugInfo\DebugInfoTrait; use Wikimedia\Rdbms\IDBAccessObject; /** * Represents the authority of a given User. For anonymous visitors, this will typically * allow only basic permissions. For logged in users, permissions are generally based on group * membership, but may be adjusted based on things like IP range blocks, OAuth grants, or * rate limits. * * @note This is intended as an intermediate step towards an implementation of Authority that * contains much of the logic currently in PermissionManager, and is based directly on * WebRequest and Session, rather than a User object. However, for now, code that needs an * Authority that reflects the current user and web request should use a User object directly. * * @unstable * @since 1.36 */ class UserAuthority implements Authority { use DebugInfoTrait; /** * @var PermissionManager * @noVarDump */ private $permissionManager; /** * @var RateLimiter * @noVarDump */ private $rateLimiter; /** * @var User * @noVarDump */ private $actor; /** * Local cache for user block information. False is used to indicate that there is no block, * while null indicates that we don't know and have to check. * @var Block|false|null */ private $userBlock = null; /** * Cache for the outcomes of rate limit checks. * We cache the outcomes primarily so we don't bump the counter multiple times * per request. * @var array<string,array> Map of actions to [ int, bool ] pairs. * The first element is the increment performed so far (typically 1). * The second element is the cached outcome of the check (whether the limit was reached) */ private $limitCache = []; /** * Whether the limit cache should be used. Generally, the limit cache should be used in web * requests, since we don't want to bump the same limit more than once per request. It * should not be used during testing, so limits can easily be tested without knowledge * about the caching mechanism. * * @var bool */ private bool $useLimitCache; private WebRequest $request; private IContextSource $uiContext; private BlockErrorFormatter $blockErrorFormatter; /** * @param User $user * @param WebRequest $request * @param IContextSource $uiContext * @param PermissionManager $permissionManager * @param RateLimiter $rateLimiter * @param BlockErrorFormatter $blockErrorFormatter */ public function __construct( User $user, WebRequest $request, IContextSource $uiContext, PermissionManager $permissionManager, RateLimiter $rateLimiter, BlockErrorFormatter $blockErrorFormatter ) { $this->actor = $user; $this->request = $request; $this->uiContext = $uiContext; $this->permissionManager = $permissionManager; $this->rateLimiter = $rateLimiter; $this->blockErrorFormatter = $blockErrorFormatter; $this->useLimitCache = !defined( 'MW_PHPUNIT_TEST' ); } /** * @internal * @param bool $useLimitCache */ public function setUseLimitCache( bool $useLimitCache ) { $this->useLimitCache = $useLimitCache; } /** @inheritDoc */ public function getUser(): UserIdentity { return $this->actor; } /** @inheritDoc */ public function isAllowed( string $permission, ?PermissionStatus $status = null ): bool { return $this->internalAllowed( $permission, $status, false, null ); } /** @inheritDoc */ public function isAllowedAny( ...$permissions ): bool { if ( !$permissions ) { throw new InvalidArgumentException( 'At least one permission must be specified' ); } return $this->permissionManager->userHasAnyRight( $this->actor, ...$permissions ); } /** @inheritDoc */ public function isAllowedAll( ...$permissions ): bool { if ( !$permissions ) { throw new InvalidArgumentException( 'At least one permission must be specified' ); } return $this->permissionManager->userHasAllRights( $this->actor, ...$permissions ); } /** @inheritDoc */ public function probablyCan( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return $this->internalCan( PermissionManager::RIGOR_QUICK, $action, $target, $status, false // do not check the rate limit ); } /** @inheritDoc */ public function definitelyCan( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { // Note that we do not use RIGOR_SECURE to avoid hitting the primary // database for read operations. RIGOR_FULL performs the same checks, // but is subject to replication lag. return $this->internalCan( PermissionManager::RIGOR_FULL, $action, $target, $status, 0 // only check the rate limit, don't count it as a hit ); } /** @inheritDoc */ public function isDefinitelyAllowed( string $action, ?PermissionStatus $status = null ): bool { $userBlock = $this->getApplicableBlock( PermissionManager::RIGOR_FULL, $action ); return $this->internalAllowed( $action, $status, 0, $userBlock ); } /** @inheritDoc */ public function authorizeAction( string $action, ?PermissionStatus $status = null ): bool { // Any side-effects can be added here. $userBlock = $this->getApplicableBlock( PermissionManager::RIGOR_SECURE, $action ); return $this->internalAllowed( $action, $status, 1, $userBlock ); } /** @inheritDoc */ public function authorizeRead( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { // Any side-effects can be added here. // Note that we do not use RIGOR_SECURE to avoid hitting the primary // database for read operations. RIGOR_FULL performs the same checks, // but is subject to replication lag. return $this->internalCan( PermissionManager::RIGOR_FULL, $action, $target, $status, 1 // count a hit towards the rate limit ); } /** @inheritDoc */ public function authorizeWrite( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { // Any side-effects can be added here. // Note that we need to use RIGOR_SECURE here to ensure that we do not // miss a user block or page protection due to replication lag. return $this->internalCan( PermissionManager::RIGOR_SECURE, $action, $target, $status, 1 // count a hit towards the rate limit ); } /** * Check whether the user is allowed to perform the action, taking into account * the user's block status as well as any rate limits. * * @param string $action * @param PermissionStatus|null $status * @param int|false $limitRate False means no check, 0 means check only, * and 1 means check and increment * @param ?Block $userBlock * * @return bool */ private function internalAllowed( string $action, ?PermissionStatus $status, $limitRate, ?Block $userBlock ): bool { if ( $status ) { Assert::precondition( $status->isGood(), 'The PermissionStatus passed as $status parameter must still be good' ); } if ( !$this->permissionManager->userHasRight( $this->actor, $action ) ) { if ( !$status ) { return false; } $status->setPermission( $action ); $status->merge( $this->permissionManager->newFatalPermissionDeniedStatus( $action, $this->uiContext ) ); } if ( $userBlock ) { if ( !$status ) { return false; } $messages = $this->blockErrorFormatter->getMessages( $userBlock, $this->actor, $this->request->getIP() ); $status->setPermission( $action ); foreach ( $messages as $message ) { $status->fatal( $message ); } } // Check and bump the rate limit. if ( $limitRate !== false ) { $isLimited = $this->limit( $action, $limitRate, $status ); if ( $isLimited && !$status ) { return false; } } return !$status || $status->isOK(); } // See ApiBase::BLOCK_CODE_MAP private const BLOCK_CODES = [ 'blockedtext', 'blockedtext-partial', 'autoblockedtext', 'systemblockedtext', 'blockedtext-composite', 'blockedtext-tempuser', 'autoblockedtext-tempuser', ]; /** * @param string $rigor * @param string $action * @param PageIdentity $target * @param ?PermissionStatus $status * @param int|false $limitRate False means no check, 0 means check only, * a non-zero values means check and increment * * @return bool */ private function internalCan( string $rigor, string $action, PageIdentity $target, ?PermissionStatus $status, $limitRate ): bool { // Check and bump the rate limit. if ( $limitRate !== false ) { $isLimited = $this->limit( $action, $limitRate, $status ); if ( $isLimited && !$status ) { // bail early if we don't have a status object return false; } } if ( !( $target instanceof LinkTarget ) ) { // TODO: PermissionManager should accept PageIdentity! $target = TitleValue::newFromPage( $target ); } if ( $status ) { $status->setPermission( $action ); $tempStatus = $this->permissionManager->getPermissionStatus( $action, $this->actor, $target, $rigor ); if ( $tempStatus->isGood() ) { // Nothing to merge, return early return $status->isOK(); } // Instead of `$status->merge( $tempStatus )`, process the messages like this to ensure that // the resulting status contains Message objects instead of strings+arrays, and thus does not // trigger wikitext escaping in a legacy code path. See T368821 for more information about // that behavior, and see T306494 for the specific bug this fixes. foreach ( $tempStatus->getMessages() as $msg ) { $status->fatal( $msg ); } foreach ( self::BLOCK_CODES as $code ) { // HACK: Detect whether the permission was denied because the user is blocked. // A similar hack exists in ApiBase::BLOCK_CODE_MAP. // When permission checking logic is moved out of PermissionManager, // we can record the block info directly when first checking the block, // rather than doing that here. if ( $tempStatus->hasMessage( $code ) ) { $block = $this->getBlock(); if ( $block ) { $status->setBlock( $block ); } break; } } return $status->isOK(); } else { // allow PermissionManager to short-circuit return $this->permissionManager->userCan( $action, $this->actor, $target, $rigor ); } } /** * Check whether a rate limit has been exceeded for the given action. * * @see RateLimiter::limit * @internal For use by User::pingLimiter only. * * @param string $action * @param int $incrBy * @param PermissionStatus|null $status * * @return bool */ public function limit( string $action, int $incrBy, ?PermissionStatus $status ): bool { $isLimited = null; if ( $this->useLimitCache && isset( $this->limitCache[ $action ] ) ) { // subtract the increment that was already applied earlier $incrRemaining = $incrBy - $this->limitCache[ $action ][ 0 ]; // if no increment is left to apply, return the cached outcome if ( $incrRemaining < 1 ) { $isLimited = $this->limitCache[ $action ][ 1 ]; } } else { $incrRemaining = $incrBy; } if ( $isLimited === null ) { // NOTE: Avoid toRateLimitSubject() if possible, for performance if ( $this->rateLimiter->isLimitable( $action ) ) { $isLimited = $this->rateLimiter->limit( $this->actor->toRateLimitSubject(), $action, $incrRemaining ); } else { $isLimited = false; } // Cache the outcome, so we don't bump the counter twice during the same request. $this->limitCache[ $action ] = [ $incrBy, $isLimited ]; } if ( $isLimited && $status ) { $status->setRateLimitExceeded(); } return $isLimited; } /** @inheritDoc */ public function getBlock( int $freshness = IDBAccessObject::READ_NORMAL ): ?Block { // Cache block info, so we don't have to fetch it again unnecessarily. if ( $this->userBlock === null || $freshness === IDBAccessObject::READ_LATEST ) { $this->userBlock = $this->actor->getBlock( $freshness ); // if we got null back, remember this as "false" $this->userBlock = $this->userBlock ?: false; } // if we remembered "false", return null return $this->userBlock ?: null; } private function getApplicableBlock( string $rigor, string $action, ?PageIdentity $target = null ): ?Block { // NOTE: We follow the parameter order of internalCan here. // It doesn't match the one in PermissionManager. return $this->permissionManager->getApplicableBlock( $action, $this->actor, $rigor, $target, $this->request ); } public function isRegistered(): bool { return $this->actor->isRegistered(); } public function isTemp(): bool { return $this->actor->isTemp(); } public function isNamed(): bool { return $this->actor->isNamed(); } } PK ! ���� SimpleAuthority.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Permissions; use InvalidArgumentException; use MediaWiki\Block\Block; use MediaWiki\Page\PageIdentity; use MediaWiki\User\UserIdentity; use Wikimedia\Rdbms\IDBAccessObject; /** * Represents an authority that has a specific set of permissions * which are specified explicitly. This is useful for testing, but * may also be used to represent a fixed set of permissions to be * used in some context, e.g. in an asynchronous job. * * @since 1.36 * @newable */ class SimpleAuthority implements Authority { /** @var UserIdentity */ private $actor; /** @var bool */ private $isTemp; /** @var true[] permissions (stored in the keys, values are ignored) */ private $permissions; /** * @stable to call * @param UserIdentity $actor * @param string[] $permissions A list of permissions to grant to the actor * @param bool $isTemp Whether the user is auto-created (since 1.39) */ public function __construct( UserIdentity $actor, array $permissions, bool $isTemp = false ) { $this->actor = $actor; $this->isTemp = $isTemp; $this->permissions = array_fill_keys( $permissions, true ); } /** @inheritDoc */ public function getUser(): UserIdentity { return $this->actor; } /** @inheritDoc */ public function getBlock( int $freshness = IDBAccessObject::READ_NORMAL ): ?Block { return null; } /** @inheritDoc */ public function isAllowed( string $permission, ?PermissionStatus $status = null ): bool { return isset( $this->permissions[ $permission ] ); } /** @inheritDoc */ public function isAllowedAny( ...$permissions ): bool { if ( !$permissions ) { throw new InvalidArgumentException( 'At least one permission must be specified' ); } foreach ( $permissions as $perm ) { if ( $this->isAllowed( $perm ) ) { return true; } } return false; } /** @inheritDoc */ public function isAllowedAll( ...$permissions ): bool { if ( !$permissions ) { throw new InvalidArgumentException( 'At least one permission must be specified' ); } foreach ( $permissions as $perm ) { if ( !$this->isAllowed( $perm ) ) { return false; } } return true; } private function checkPermission( string $permission, ?PermissionStatus $status ): bool { $ok = $this->isAllowed( $permission ); if ( !$ok && $status ) { // TODO: use a message that at includes the permission name $status->fatal( 'permissionserrors' ); $status->setPermission( $permission ); } return $ok; } /** @inheritDoc */ public function probablyCan( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return $this->checkPermission( $action, $status ); } /** @inheritDoc */ public function definitelyCan( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return $this->checkPermission( $action, $status ); } /** @inheritDoc */ public function isDefinitelyAllowed( string $action, ?PermissionStatus $status = null ): bool { return $this->checkPermission( $action, $status ); } /** @inheritDoc */ public function authorizeAction( string $action, ?PermissionStatus $status = null ): bool { return $this->checkPermission( $action, $status ); } /** @inheritDoc */ public function authorizeRead( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return $this->checkPermission( $action, $status ); } /** @inheritDoc */ public function authorizeWrite( string $action, PageIdentity $target, ?PermissionStatus $status = null ): bool { return $this->checkPermission( $action, $status ); } public function isRegistered(): bool { return $this->actor->isRegistered(); } public function isTemp(): bool { return $this->isTemp; } public function isNamed(): bool { return $this->isRegistered() && !$this->isTemp(); } } PK ! �Q��i� i� PermissionManager.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Permissions; use InvalidArgumentException; use LogicException; use MediaWiki\Actions\ActionFactory; use MediaWiki\Block\AbstractBlock; use MediaWiki\Block\Block; use MediaWiki\Block\BlockErrorFormatter; use MediaWiki\Block\BlockManager; use MediaWiki\Config\ServiceOptions; use MediaWiki\Context\IContextSource; use MediaWiki\Context\RequestContext; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Linker\LinkTarget; use MediaWiki\MainConfigNames; use MediaWiki\Message\Message; use MediaWiki\Page\PageIdentity; use MediaWiki\Page\PageReference; use MediaWiki\Page\RedirectLookup; use MediaWiki\Request\WebRequest; use MediaWiki\Session\SessionManager; use MediaWiki\SpecialPage\SpecialPage; use MediaWiki\SpecialPage\SpecialPageFactory; use MediaWiki\Title\NamespaceInfo; use MediaWiki\Title\Title; use MediaWiki\Title\TitleFormatter; use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\User; use MediaWiki\User\UserFactory; use MediaWiki\User\UserGroupManager; use MediaWiki\User\UserGroupMembership; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserIdentityLookup; use PermissionsError; use StatusValue; use Wikimedia\Message\MessageSpecifier; use Wikimedia\ScopedCallback; /** * A service class for checking permissions * To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager(). * * @since 1.33 */ class PermissionManager { /** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */ public const RIGOR_QUICK = 'quick'; /** @var string Does cheap and expensive checks possibly from a replica DB */ public const RIGOR_FULL = 'full'; /** @var string Does cheap and expensive checks, using the primary DB as needed */ public const RIGOR_SECURE = 'secure'; /** * @internal For use by ServiceWiring */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::WhitelistRead, MainConfigNames::WhitelistReadRegexp, MainConfigNames::EmailConfirmToEdit, MainConfigNames::BlockDisablesLogin, MainConfigNames::EnablePartialActionBlocks, MainConfigNames::GroupPermissions, MainConfigNames::RevokePermissions, MainConfigNames::AvailableRights, MainConfigNames::NamespaceProtection, MainConfigNames::RestrictionLevels, MainConfigNames::DeleteRevisionsLimit, MainConfigNames::RateLimits, MainConfigNames::ImplicitRights, ]; private ServiceOptions $options; private SpecialPageFactory $specialPageFactory; private NamespaceInfo $nsInfo; private GroupPermissionsLookup $groupPermissionsLookup; private UserGroupManager $userGroupManager; private BlockManager $blockManager; private BlockErrorFormatter $blockErrorFormatter; private HookRunner $hookRunner; private UserIdentityLookup $userIdentityLookup; private RedirectLookup $redirectLookup; private RestrictionStore $restrictionStore; private TitleFormatter $titleFormatter; private TempUserConfig $tempUserConfig; private UserFactory $userFactory; private ActionFactory $actionFactory; /** @var string[]|null Cached results of getAllPermissions() */ private $allRights; /** @var string[]|null Cached results of getImplicitRights() */ private $implicitRights; /** @var string[][] Cached user rights */ private $usersRights = []; /** * Temporary user rights, valid for the current request only. * @var string[][][] userid => override group => rights */ private $temporaryUserRights = []; /** @var bool[] Cached rights for isEveryoneAllowed, [ right => allowed ] */ private $cachedRights = []; /** * Array of core rights. * Each of these should have a corresponding message of the form * "right-$right". * @showinitializer */ private const CORE_RIGHTS = [ 'apihighlimits', 'applychangetags', 'autoconfirmed', 'autocreateaccount', 'autopatrol', 'bigdelete', 'block', 'blockemail', 'bot', 'browsearchive', 'changetags', 'createaccount', 'createpage', 'createtalk', 'delete', 'delete-redirect', 'deletechangetags', 'deletedhistory', 'deletedtext', 'deletelogentry', 'deleterevision', 'edit', 'editcontentmodel', 'editinterface', 'editprotected', 'editmyoptions', 'editmyprivateinfo', 'editmyusercss', 'editmyuserjson', 'editmyuserjs', 'editmyuserjsredirect', 'editmywatchlist', 'editsemiprotected', 'editsitecss', 'editsitejson', 'editsitejs', 'editusercss', 'edituserjson', 'edituserjs', 'hideuser', 'import', 'importupload', 'ipblock-exempt', 'managechangetags', 'markbotedits', 'mergehistory', 'minoredit', 'move', 'movefile', 'move-categorypages', 'move-rootuserpages', 'move-subpages', 'nominornewtalk', 'noratelimit', 'override-export-depth', 'pagelang', 'patrol', 'patrolmarks', 'protect', 'read', 'renameuser', 'reupload', 'reupload-own', 'reupload-shared', 'rollback', 'sendemail', 'siteadmin', 'suppressionlog', 'suppressredirect', 'suppressrevision', 'unblockself', 'undelete', 'unwatchedpages', 'upload', 'upload_by_url', 'userrights', 'userrights-interwiki', 'viewmyprivateinfo', 'viewmywatchlist', 'viewsuppressed', ]; /** * List of implicit rights. * These should not have a corresponding message of the form * "right-$right". * @showinitializer */ private const CORE_IMPLICIT_RIGHTS = [ 'renderfile', 'renderfile-nonstandard', 'stashedit', 'stashbasehtml', 'mailpassword', 'changeemail', 'confirmemail', 'linkpurge', 'purge', ]; public function __construct( ServiceOptions $options, SpecialPageFactory $specialPageFactory, NamespaceInfo $nsInfo, GroupPermissionsLookup $groupPermissionsLookup, UserGroupManager $userGroupManager, BlockManager $blockManager, BlockErrorFormatter $blockErrorFormatter, HookContainer $hookContainer, UserIdentityLookup $userIdentityLookup, RedirectLookup $redirectLookup, RestrictionStore $restrictionStore, TitleFormatter $titleFormatter, TempUserConfig $tempUserConfig, UserFactory $userFactory, ActionFactory $actionFactory ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->specialPageFactory = $specialPageFactory; $this->nsInfo = $nsInfo; $this->groupPermissionsLookup = $groupPermissionsLookup; $this->userGroupManager = $userGroupManager; $this->blockManager = $blockManager; $this->blockErrorFormatter = $blockErrorFormatter; $this->hookRunner = new HookRunner( $hookContainer ); $this->userIdentityLookup = $userIdentityLookup; $this->redirectLookup = $redirectLookup; $this->restrictionStore = $restrictionStore; $this->titleFormatter = $titleFormatter; $this->tempUserConfig = $tempUserConfig; $this->userFactory = $userFactory; $this->actionFactory = $actionFactory; } /** * Can $user perform $action on a page? * * The method replaced 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 * * @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 primary DB as needed * * @return bool */ public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ): bool { return $this->getPermissionStatus( $action, $user, $page, $rigor, true )->isGood(); } /** * A convenience method for calling PermissionManager::userCan * with PermissionManager::RIGOR_QUICK * * Suitable for use for nonessential UI controls in common cases, but * _not_ for functional access control. * May provide false positives, but should never provide a false negative. * * @see PermissionManager::userCan() * * @param string $action * @param User $user * @param LinkTarget $page * @return bool */ public function quickUserCan( $action, User $user, LinkTarget $page ): bool { return $this->userCan( $action, $user, $page, self::RIGOR_QUICK ); } /** * Can $user perform $action on a page? * * This *does not* check throttles (User::pingLimiter()). If that's desired, use the Authority * interface methods instead. * * @deprecated since 1.43 Use getPermissionStatus() instead. * * @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 primary DB as needed * @param string[] $ignoreErrors Set this to a list of message keys * whose corresponding errors may be ignored. * * @return array[] Permission errors. * Each entry contains valid arguments for wfMessage() / MessageLocalizer::msg(). * The format is *different* from the normal "legacy error array", as used by * Status::getErrorsArray() or PermissionStatus::toLegacyErrorArray(): * the first element of each entry can be a MessageSpecifier, not just a string. * @phan-return non-empty-array[] */ public function getPermissionErrors( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE, $ignoreErrors = [] ): array { $status = $this->getPermissionStatus( $action, $user, $page, $rigor ); $result = []; // Produce a result in the weird format used by this function foreach ( $status->getErrors() as [ 'message' => $keyOrMsg, 'params' => $params ] ) { $key = $keyOrMsg instanceof MessageSpecifier ? $keyOrMsg->getKey() : $keyOrMsg; // Remove the errors being ignored. if ( !in_array( $key, $ignoreErrors ) ) { $result[] = [ $keyOrMsg, ...$params ]; } } return $result; } /** * Like {@link getPermissionErrors}, but immediately throw if there are any errors. * * @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 primary DB as needed * @param string[] $ignoreErrors Set this to a list of message keys * whose corresponding errors may be ignored. * * @throws PermissionsError */ public function throwPermissionErrors( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE, $ignoreErrors = [] ): void { $status = $this->getPermissionStatus( $action, $user, $page, $rigor ); if ( $status->hasMessagesExcept( ...$ignoreErrors ) ) { throw new PermissionsError( $action, $status ); } } /** * Check if user is blocked from editing a particular article. If the user does not * have a block, this will return false. * * @param User $user * @param PageIdentity|LinkTarget $page Title to check * @param bool $fromReplica Whether to check the replica DB instead of the primary DB * @return bool */ public function isBlockedFrom( User $user, $page, $fromReplica = false ): bool { return (bool)$this->getApplicableBlock( 'edit', $user, $fromReplica ? self::RIGOR_FULL : self::RIGOR_SECURE, $page, $user->getRequest() ); } /** * Can $user perform $action on a page? * * This *does not* check throttles (User::pingLimiter()). If that's desired, use the Authority * interface methods instead. * * @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 primary DB as needed * @param bool $short Set this to true to stop after the first permission error. * @return PermissionStatus Permission errors as a status. * Check `$status->isGood()` to tell if the user can perform the action. * Use `$status->getMessages()` to display errors if the status is not good. */ public function getPermissionStatus( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE, $short = false ): PermissionStatus { if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) { throw new InvalidArgumentException( "Invalid rigor parameter '$rigor'." ); } // With RIGOR_QUICK we can assume automatic account creation will // occur. At a higher rigor level, the caller is required to opt // in by either passing in a temp placeholder user or by actually // creating the account. if ( $rigor === self::RIGOR_QUICK && !$user->isRegistered() && $this->tempUserConfig->isAutoCreateAction( $action ) ) { $user = $this->userFactory->newTempPlaceholder(); } # Read has special handling if ( $action === 'read' ) { $checks = [ [ $this, 'checkPermissionHooks' ], [ $this, 'checkReadPermissions' ], [ $this, 'checkUserBlock' ], // for wgBlockDisablesLogin ]; } elseif ( $action === 'create' ) { # 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. $checks = [ [ $this, 'checkQuickPermissions' ], [ $this, 'checkPermissionHooks' ], [ $this, 'checkPageRestrictions' ], [ $this, 'checkCascadingSourcesRestrictions' ], [ $this, 'checkActionPermissions' ], [ $this, 'checkUserBlock' ], ]; } else { // Exclude checkUserConfigPermissions on actions that cannot change the // content of the configuration pages. $skipUserConfigActions = [ // Allow patrolling per T21818 'patrol', // Allow admins and oversighters to delete. For user pages 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. // See T200176. 'delete', 'deleterevision', 'suppressrevision', // Allow admins and oversighters to view deleted content, even if they // cannot restore it. See T202989 'deletedhistory', 'deletedtext', 'viewsuppressed', ]; $checks = [ [ $this, 'checkQuickPermissions' ], [ $this, 'checkPermissionHooks' ], [ $this, 'checkSpecialsAndNSPermissions' ], [ $this, 'checkSiteConfigPermissions' ], ]; if ( !in_array( $action, $skipUserConfigActions, true ) ) { $checks[] = [ $this, 'checkUserConfigPermissions' ]; } $checks = [ ...$checks, [ $this, 'checkPageRestrictions' ], [ $this, 'checkCascadingSourcesRestrictions' ], [ $this, 'checkActionPermissions' ], [ $this, 'checkUserBlock' ] ]; } $status = PermissionStatus::newEmpty(); foreach ( $checks as $method ) { $method( $action, $user, $status, $rigor, $short, $page ); if ( $short && !$status->isGood() ) { break; } } if ( !$status->isGood() ) { $errors = $status->toLegacyErrorArray(); $this->hookRunner->onPermissionErrorAudit( $page, $user, $action, $rigor, $errors ); } return $status; } /** * Check various permission hooks * * @param string $action The action to check * @param User $user User to check * @param PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkPermissionHooks( $action, User $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove when LinkTarget usage will expand further $title = Title::newFromLinkTarget( $page ); // Use getUserPermissionsErrors instead $result = ''; if ( !$this->hookRunner->onUserCan( $title, $user, $action, $result ) ) { if ( !$result ) { $status->fatal( 'badaccess-group0' ); } return; } // Check getUserPermissionsErrors hook if ( !$this->hookRunner->onGetUserPermissionsErrors( $title, $user, $action, $result ) ) { $this->resultToStatus( $status, $result ); } // Check getUserPermissionsErrorsExpensive hook if ( $rigor !== self::RIGOR_QUICK && !( $short && !$status->isGood() ) && !$this->hookRunner->onGetUserPermissionsErrorsExpensive( $title, $user, $action, $result ) ) { $this->resultToStatus( $status, $result ); } } /** * Add the resulting error code to the errors array * * @param PermissionStatus $status Current errors * @param array|string|MessageSpecifier|false $result Result of errors */ private function resultToStatus( PermissionStatus $status, $result ): void { if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { // A single array representing an error $status->fatal( ...$result ); } elseif ( is_array( $result ) && count( $result ) && is_array( $result[0] ) ) { // A nested array representing multiple errors foreach ( $result as $result1 ) { $this->resultToStatus( $status, $result1 ); } } elseif ( is_string( $result ) && $result !== '' ) { // A string representing a message-id $status->fatal( $result ); } elseif ( $result instanceof MessageSpecifier ) { // A message specifier representing an error $status->fatal( $result ); } elseif ( $result === false ) { // a generic "We don't want them to do that" $status->fatal( 'badaccess-group0' ); } // If we got here, $results is the empty array or empty string, which mean no 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 PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkReadPermissions( $action, User $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove when LinkTarget usage will expand further $title = Title::newFromLinkTarget( $page ); $whiteListRead = $this->options->get( MainConfigNames::WhitelistRead ); $allowed = false; if ( $this->isEveryoneAllowed( 'read' ) ) { // Shortcut for public wikis, allows skipping quite a bit of code $allowed = true; } elseif ( $this->userHasRight( $user, 'read' ) ) { // If the user is allowed to read pages, he is allowed to read all pages $allowed = 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. $allowed = true; } elseif ( $this->isSameSpecialPage( 'RunJobs', $page ) ) { // relies on HMAC key signature alone $allowed = true; } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) { // Time to check the whitelist // Only do these checks if there's something to check against $name = $title->getPrefixedText(); $dbName = $title->getPrefixedDBkey(); // Check for explicit whitelisting with and without underscores if ( in_array( $name, $whiteListRead, true ) || in_array( $dbName, $whiteListRead, true ) ) { $allowed = true; } elseif ( $page->getNamespace() === NS_MAIN ) { // Old settings might have the title prefixed with // a colon for main-namespace pages if ( in_array( ':' . $name, $whiteListRead ) ) { $allowed = true; } } elseif ( $title->isSpecialPage() ) { // If it's a special page, ditch the subpage bit and check again $name = $title->getDBkey(); [ $name, /* $subpage */ ] = $this->specialPageFactory->resolveAlias( $name ); if ( $name ) { $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); if ( in_array( $pure, $whiteListRead, true ) ) { $allowed = true; } } } } $whitelistReadRegexp = $this->options->get( MainConfigNames::WhitelistReadRegexp ); if ( !$allowed && is_array( $whitelistReadRegexp ) && $whitelistReadRegexp ) { $name = $title->getPrefixedText(); // Check for regex whitelisting foreach ( $whitelistReadRegexp as $listItem ) { if ( preg_match( $listItem, $name ) ) { $allowed = true; break; } } } if ( !$allowed ) { # If the title is not whitelisted, give extensions a chance to do so... $this->hookRunner->onTitleReadWhitelist( $title, $user, $allowed ); if ( !$allowed ) { $this->missingPermissionError( $action, $short, $status ); } } } /** * Add an error to the status when an action isn't allowed to be performed. * * @param string $action The action to check * @param bool $short Short circuit on first error * @param PermissionStatus $status */ private function missingPermissionError( string $action, bool $short, PermissionStatus $status ): void { // We avoid expensive display logic for quickUserCan's and such if ( $short ) { $status->fatal( 'badaccess-group0' ); } // TODO: it would be a good idea to replace the method below with something else like // maybe callback injection $context = RequestContext::getMain(); $fatalStatus = $this->newFatalPermissionDeniedStatus( $action, $context ); $status->merge( $fatalStatus ); $statusPermission = $fatalStatus->getPermission(); if ( $statusPermission ) { $status->setPermission( $statusPermission ); } } /** * Factory function for fatal permission-denied errors * * @internal for use by UserAuthority * * @param string $permission User right required * @param IContextSource $context * * @return PermissionStatus */ public function newFatalPermissionDeniedStatus( $permission, IContextSource $context ): StatusValue { $groups = []; foreach ( $this->groupPermissionsLookup->getGroupsWithPermission( $permission ) as $group ) { $groups[] = UserGroupMembership::getLinkWiki( $group, $context ); } if ( $groups ) { return PermissionStatus::newFatal( 'badaccess-groups', Message::listParam( $groups, 'comma' ), count( $groups ) ); } $status = PermissionStatus::newFatal( 'badaccess-group0' ); $status->setPermission( $permission ); return $status; } /** * Whether a 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 ): bool { if ( $page->getNamespace() === NS_SPECIAL ) { [ $pageName ] = $this->specialPageFactory->resolveAlias( $page->getDBkey() ); if ( $name === $pageName ) { 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 PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkUserBlock( $action, User $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { $block = $this->getApplicableBlock( $action, $user, $rigor, $page, $user->getRequest() ); if ( $block ) { // @todo FIXME: Pass the relevant context into this function. $context = RequestContext::getMain(); $messages = $this->blockErrorFormatter->getMessages( $block, $user, $context->getRequest()->getIP() ); foreach ( $messages as $message ) { // TODO: We can pass $message directly once getPermissionErrors() is removed. // For now we store the message key as a string here out of overabundance of caution, // because there is a test case verifying that block messages use strings in that format. $status->fatal( $message->getKey(), ...$message->getParams() ); } } } /** * Return the Block object applicable for the given permission check, if any. * * @internal for use by UserAuthority only * * @param string $action The action to check * @param User $user User to check * @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 primary DB as needed * @param LinkTarget|PageReference|null $page * @param WebRequest|null $request The request to get the IP and cookies * from. If this is null, IP and cookie blocks will not be checked. * @return ?Block */ public function getApplicableBlock( string $action, User $user, string $rigor, $page, ?WebRequest $request ): ?Block { // Unblocking handled in SpecialUnblock if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'unblock' ] ) ) { return null; } // Optimize for a very common case if ( $action === 'read' && !$this->options->get( MainConfigNames::BlockDisablesLogin ) ) { return null; } // Implicit rights aren't blockable (T350117, T350202). if ( in_array( $action, $this->getImplicitRights(), true ) ) { return null; } $useReplica = $rigor !== self::RIGOR_SECURE; $isExempt = $this->userHasRight( $user, 'ipblock-exempt' ); $requestIfNotExempt = $isExempt ? null : $request; // Create account blocks are implemented separately due to weird IP exemption rules if ( in_array( $action, [ 'createaccount', 'autocreateaccount' ], true ) ) { return $this->blockManager->getCreateAccountBlock( $user, $requestIfNotExempt, $useReplica ); } $block = $this->blockManager->getBlock( $user, $requestIfNotExempt, $useReplica ); if ( !$block ) { return null; } $userIsHidden = $block->getHideName(); // Remove elements from the block that explicitly allow the action // (like "read" or "upload"). $block = $this->blockManager->filter( $block, static function ( AbstractBlock $originalBlock ) use ( $action ) { // Remove the block if it explicitly allows the action return $originalBlock->appliesToRight( $action ) !== false; } ); if ( !$block ) { return null; } // Convert the input page to a Title $targetTitle = null; if ( $page ) { $targetTitle = $page instanceof PageReference ? Title::castFromPageReference( $page ) : Title::castFromLinkTarget( $page ); if ( !$targetTitle->canExist() ) { $targetTitle = null; } } // 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. $actionInfo = $this->actionFactory->getActionInfo( $action, $targetTitle ); // Ensure that the retrieved action matches the restriction. if ( $actionInfo && $actionInfo->getRestriction() !== $action ) { $actionInfo = null; } // Return null if the action does not require an unblocked user. // If no ActionInfo is returned, assume that the action requires unblock // which is the default. // NOTE: We may get null here even for known actions, if a wiki's main page // is set to a special page, e.g. Special:MyLanguage/Main_Page (T348451, T346036). if ( $actionInfo && !$actionInfo->requiresUnblock() ) { return null; } // Remove elements from the block that do not apply to the specific page if ( $targetTitle ) { $targetIsUserTalk = !$userIsHidden && $targetTitle->equals( $user->getTalkPage() ); $block = $this->blockManager->filter( $block, static function ( AbstractBlock $originalBlock ) use ( $action, $targetTitle, $targetIsUserTalk ) { if ( $originalBlock->appliesToRight( $action ) ) { // An action block takes precedence over appliesToTitle(). // Block::appliesToRight('edit') always returns null, // allowing title-based exemptions to take effect. return true; } elseif ( $targetIsUserTalk ) { // Special handling for a user's own talk page. The block is not aware // of the user, so this must be done here. return $originalBlock->appliesToUsertalk( $targetTitle ); } else { return $originalBlock->appliesToTitle( $targetTitle ); } } ); } if ( $targetTitle && $block && $block instanceof AbstractBlock // for phan ) { // Allow extensions to let a blocked user access a particular page $allowUsertalk = $block->isUsertalkEditAllowed(); $blocked = true; $this->hookRunner->onUserIsBlockedFrom( $user, $targetTitle, $blocked, $allowUsertalk ); if ( !$blocked ) { $block = null; } } return $block; } /** * Run easy-to-test (or "quick") permissions checks for a given action. * * @param string $action The action to check * @param User $user User to check * @param PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkQuickPermissions( $action, User $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove when LinkTarget usage will expand further $title = Title::newFromLinkTarget( $page ); // This method is always called first, so $status is guaranteed to be empty, so we can // just pass an empty $errors array, instead of converting it to the legacy format and back. $errors = []; if ( !$this->hookRunner->onTitleQuickPermissions( $title, $user, $action, $errors, $rigor !== self::RIGOR_QUICK, $short ) ) { // $errors is an array of results, not a result, but resultToStatus() handles // arrays of arrays with recursion so this will work $this->resultToStatus( $status, $errors ); return; } $isSubPage = $this->nsInfo->hasSubpages( $title->getNamespace() ) && strpos( $title->getText(), '/' ) !== false; if ( $action === 'create' ) { if ( ( $this->nsInfo->isTalk( $title->getNamespace() ) && !$this->userHasRight( $user, 'createtalk' ) ) || ( !$this->nsInfo->isTalk( $title->getNamespace() ) && !$this->userHasRight( $user, 'createpage' ) ) ) { $status->fatal( $user->isNamed() ? 'nocreate-loggedin' : 'nocreatetext' ); } } elseif ( $action === 'move' ) { if ( !$this->userHasRight( $user, 'move-rootuserpages' ) && $title->getNamespace() === NS_USER && !$isSubPage ) { // Show user page-specific message only if the user can move other pages $status->fatal( 'cant-move-user-page' ); } // Check if user is allowed to move files if it's a file if ( $title->getNamespace() === NS_FILE && !$this->userHasRight( $user, 'movefile' ) ) { $status->fatal( 'movenotallowedfile' ); } // Check if user is allowed to move category pages if it's a category page if ( $title->getNamespace() === NS_CATEGORY && !$this->userHasRight( $user, 'move-categorypages' ) ) { $status->fatal( 'cant-move-category-page' ); } if ( !$this->userHasRight( $user, 'move' ) ) { // User can't move anything $userCanMove = $this->groupPermissionsLookup ->groupHasPermission( 'user', 'move' ); $autoconfirmedCanMove = $this->groupPermissionsLookup ->groupHasPermission( 'autoconfirmed', 'move' ); if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { // custom message if logged-in users without any special rights can move $status->fatal( 'movenologintext' ); } elseif ( $user->isTemp() && $autoconfirmedCanMove ) { // Temp user may be able to move if they log in as a proper account $status->fatal( 'movenologintext' ); } else { $status->fatal( 'movenotallowed' ); } } } elseif ( $action === 'move-target' ) { if ( !$this->userHasRight( $user, 'move' ) ) { // User can't move anything $status->fatal( 'movenotallowed' ); } elseif ( !$this->userHasRight( $user, 'move-rootuserpages' ) && $title->getNamespace() === NS_USER && !$isSubPage ) { // Show user page-specific message only if the user can move other pages $status->fatal( 'cant-move-to-user-page' ); } elseif ( !$this->userHasRight( $user, 'move-categorypages' ) && $title->getNamespace() === NS_CATEGORY ) { // Show category page-specific message only if the user can move other pages $status->fatal( 'cant-move-to-category-page' ); } } elseif ( $action === 'autocreateaccount' ) { // createaccount implies autocreateaccount if ( !$this->userHasAnyRight( $user, 'autocreateaccount', 'createaccount' ) ) { $this->missingPermissionError( $action, $short, $status ); } } elseif ( !$this->userHasRight( $user, $action ) ) { $this->missingPermissionError( $action, $short, $status ); } } /** * Check for any page_restrictions table requirements on this page. * * If the page has multiple restrictions, the user must have * all of those rights to perform the action in question. * * @param string $action The action to check * @param UserIdentity $user User to check * @param PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkPageRestrictions( $action, UserIdentity $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove & rework upon further use of LinkTarget $title = Title::newFromLinkTarget( $page ); foreach ( $this->restrictionStore->getRestrictions( $title, $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 ( !$this->userHasRight( $user, $right ) ) { $status->fatal( 'protectedpagetext', $right, $action ); } elseif ( $this->restrictionStore->areRestrictionsCascading( $title ) && !$this->userHasRight( $user, 'protect' ) ) { $status->fatal( 'protectedpagetext', 'protect', $action ); } } } /** * Check restrictions on cascading pages. * * @param string $action The action to check * @param UserIdentity $user User to check * @param PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkCascadingSourcesRestrictions( $action, UserIdentity $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove & rework upon further use of LinkTarget $title = Title::newFromLinkTarget( $page ); if ( $rigor !== self::RIGOR_QUICK && !$title->isUserConfigPage() ) { [ $sources, $restrictions, $tlSources, $ilSources ] = $this->restrictionStore ->getCascadeProtectionSources( $title ); // If the file Wikitext isn't transcluded then we // don't care about edit cascade restrictions for edit action if ( $action === 'edit' && $page->getNamespace() === NS_FILE && !$tlSources ) { return; } // For the purposes of cascading protection, edit restrictions should apply to uploads or moves // Thus remap upload and move to edit // Unless the file content itself is not transcluded if ( $ilSources && ( $action === 'upload' || $action === 'move' ) ) { $restrictedAction = 'edit'; } else { $restrictedAction = $action; } // 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[$restrictedAction] ) ) { foreach ( $restrictions[$restrictedAction] as $right ) { // Backwards compatibility, rewrite sysop -> editprotected if ( $right === 'sysop' ) { $right = 'editprotected'; } // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected if ( $right === 'autoconfirmed' ) { $right = 'editsemiprotected'; } if ( $right != '' && !$this->userHasAllRights( $user, 'protect', $right ) ) { $wikiPages = ''; foreach ( $sources as $pageIdentity ) { $wikiPages .= '* [[:' . $this->titleFormatter->getPrefixedText( $pageIdentity ) . "]]\n"; } $status->fatal( 'cascadeprotected', count( $sources ), $wikiPages, $action ); } } } } } /** * Check action permissions not already checked in checkQuickPermissions * * @param string $action The action to check * @param User $user User to check * @param PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkActionPermissions( $action, User $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove & rework upon further use of LinkTarget $title = Title::newFromLinkTarget( $page ); if ( $rigor !== self::RIGOR_QUICK && !defined( 'MW_NO_SESSION' ) ) { $sessionRestrictions = $user->getRequest()->getSession()->getRestrictions(); if ( $sessionRestrictions ) { $userCan = $sessionRestrictions->userCan( $title ); if ( !$userCan->isOK() ) { $status->merge( $userCan ); } } } if ( $action === 'protect' ) { if ( !$this->getPermissionStatus( 'edit', $user, $title, $rigor, true )->isGood() ) { // If they can't edit, they shouldn't protect. $status->fatal( 'protect-cantedit' ); } } elseif ( $action === 'create' ) { $createProtection = $this->restrictionStore->getCreateProtection( $title ); if ( $createProtection ) { if ( $createProtection['permission'] == '' || !$this->userHasRight( $user, $createProtection['permission'] ) ) { $protectUserIdentity = $this->userIdentityLookup ->getUserIdentityByUserId( $createProtection['user'] ); $status->fatal( 'titleprotected', $protectUserIdentity ? $protectUserIdentity->getName() : '', $createProtection['reason'] ); } } } elseif ( $action === 'move' ) { // Check for immobile pages if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) { // Specific message for this case $nsText = $title->getNsText(); if ( $nsText === '' ) { $nsText = wfMessage( 'blanknamespace' )->text(); } $status->fatal( 'immobile-source-namespace', $nsText ); } elseif ( !$title->isMovable() ) { // Less specific message for rarer cases $status->fatal( 'immobile-source-page' ); } } elseif ( $action === 'move-target' ) { if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) { $nsText = $title->getNsText(); if ( $nsText === '' ) { $nsText = wfMessage( 'blanknamespace' )->text(); } $status->fatal( 'immobile-target-namespace', $nsText ); } elseif ( !$title->isMovable() ) { $status->fatal( 'immobile-target-page' ); } } elseif ( $action === 'delete' || $action === 'delete-redirect' ) { $tempStatus = PermissionStatus::newEmpty(); $this->checkPageRestrictions( 'edit', $user, $tempStatus, $rigor, true, $title ); if ( $tempStatus->isGood() ) { $this->checkCascadingSourcesRestrictions( 'edit', $user, $tempStatus, $rigor, true, $title ); } if ( !$tempStatus->isGood() ) { // If protection keeps them from editing, they shouldn't be able to delete. $status->fatal( 'deleteprotected' ); } if ( $rigor !== self::RIGOR_QUICK && $action === 'delete' && $this->options->get( MainConfigNames::DeleteRevisionsLimit ) && !$this->userCan( 'bigdelete', $user, $title ) && $title->isBigDeletion() ) { // NOTE: This check is deprecated since 1.37, see T288759 $status->fatal( 'delete-toobig', Message::numParam( $this->options->get( MainConfigNames::DeleteRevisionsLimit ) ) ); } } elseif ( $action === 'undelete' ) { if ( !$this->getPermissionStatus( 'edit', $user, $title, $rigor, true )->isGood() ) { // Undeleting implies editing $status->fatal( 'undelete-cantedit' ); } if ( !$title->exists() && !$this->getPermissionStatus( 'create', $user, $title, $rigor, true )->isGood() ) { // Undeleting where nothing currently exists implies creating $status->fatal( 'undelete-cantcreate' ); } } elseif ( $action === 'edit' ) { if ( $this->options->get( MainConfigNames::EmailConfirmToEdit ) && !$user->isEmailConfirmed() ) { $status->fatal( 'confirmedittext' ); } if ( !$title->exists() ) { $status->merge( $this->getPermissionStatus( 'create', $user, $title, $rigor, true ) ); } } } /** * Check permissions on special pages & namespaces * * @param string $action The action to check * @param UserIdentity $user User to check * @param PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkSpecialsAndNSPermissions( $action, UserIdentity $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove & rework upon further use of LinkTarget $title = Title::newFromLinkTarget( $page ); // Only 'createaccount' can be performed on special pages, // which don't actually exist in the DB. if ( $title->getNamespace() === NS_SPECIAL && !in_array( $action, [ 'createaccount', 'autocreateaccount' ], true ) ) { $status->fatal( 'ns-specialprotected' ); } // Check $wgNamespaceProtection for restricted namespaces if ( $this->isNamespaceProtected( $title->getNamespace(), $user ) // Allow admins and oversighters to view deleted content, even if they // cannot restore it. See T362536. && !in_array( $action, [ 'deletedhistory', 'deletedtext', 'viewsuppressed' ], true ) ) { $ns = $title->getNamespace() === NS_MAIN ? wfMessage( 'nstab-main' )->text() : $title->getNsText(); if ( $title->getNamespace() === NS_MEDIAWIKI ) { $status->fatal( 'protectedinterface', $action ); } else { $status->fatal( 'namespaceprotected', $ns, $action ); } } } /** * Check sitewide CSS/JSON/JS permissions * * @param string $action The action to check * @param UserIdentity $user User to check * @param PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkSiteConfigPermissions( $action, UserIdentity $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove & rework upon further use of LinkTarget $title = Title::newFromLinkTarget( $page ); if ( $action === 'patrol' ) { return; } if ( in_array( $action, [ 'deletedhistory', 'deletedtext', 'viewsuppressed' ], true ) ) { // Allow admins and oversighters to view deleted content, even if they // cannot restore it. See T202989 // Not using the same handling in `getPermissionStatus` as the checks // for skipping `checkUserConfigPermissions` since normal admins can delete // user scripts, but not sitewide scripts return; } // Sitewide CSS/JSON/JS/RawHTML changes, like all NS_MEDIAWIKI changes, also require the // editinterface right. That's implemented as a restriction so no check needed here. if ( $title->isSiteCssConfigPage() && !$this->userHasRight( $user, 'editsitecss' ) ) { $status->fatal( 'sitecssprotected', $action ); } elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) { $status->fatal( 'sitejsonprotected', $action ); } elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) { $status->fatal( 'sitejsprotected', $action ); } if ( $title->isRawHtmlMessage() && !$this->userCanEditRawHtmlPage( $user ) ) { $status->fatal( 'siterawhtmlprotected', $action ); } } /** * Check CSS/JSON/JS subpage permissions * * @param string $action The action to check * @param UserIdentity $user User to check * @param PermissionStatus $status 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 primary DB as needed * @param bool $short Short circuit on first error * @param LinkTarget $page */ private function checkUserConfigPermissions( $action, UserIdentity $user, PermissionStatus $status, $rigor, $short, LinkTarget $page ): void { // TODO: remove & rework upon further use of LinkTarget $title = Title::newFromLinkTarget( $page ); // Protect css/json/js subpages of user pages // XXX: this might be better using restrictions if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $title->getText() ) ) { // Users need editmyuser* to edit their own CSS/JSON/JS subpages. if ( $title->isUserCssConfigPage() && !$this->userHasAnyRight( $user, 'editmyusercss', 'editusercss' ) ) { $status->fatal( 'mycustomcssprotected', $action ); } elseif ( $title->isUserJsonConfigPage() && !$this->userHasAnyRight( $user, 'editmyuserjson', 'edituserjson' ) ) { $status->fatal( 'mycustomjsonprotected', $action ); } elseif ( $title->isUserJsConfigPage() && !$this->userHasAnyRight( $user, 'editmyuserjs', 'edituserjs' ) ) { $status->fatal( 'mycustomjsprotected', $action ); } elseif ( $title->isUserJsConfigPage() && !$this->userHasAnyRight( $user, 'edituserjs', 'editmyuserjsredirect' ) ) { // T207750 - do not allow users to edit a redirect if they couldn't edit the target $target = $this->redirectLookup->getRedirectTarget( $title ); if ( $target && ( !$target->inNamespace( NS_USER ) || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() ) ) ) { $status->fatal( 'mycustomjsredirectprotected', $action ); } } } else { // Users need edituser* to edit others' CSS/JSON/JS subpages. // The checks to exclude deletion/suppression, which cannot be used for // attacks and should be excluded to avoid the situation where an // unprivileged user can post abusive content on their subpages // and only very highly privileged users could remove it, // are now a part of `getPermissionStatus` and this method isn't called. if ( $title->isUserCssConfigPage() && !$this->userHasRight( $user, 'editusercss' ) ) { $status->fatal( 'customcssprotected', $action ); } elseif ( $title->isUserJsonConfigPage() && !$this->userHasRight( $user, 'edituserjson' ) ) { $status->fatal( 'customjsonprotected', $action ); } elseif ( $title->isUserJsConfigPage() && !$this->userHasRight( $user, 'edituserjs' ) ) { $status->fatal( 'customjsprotected', $action ); } } } /** * Whether the user is generally allowed to perform the given action. * * @since 1.34 * @param UserIdentity $user * @param string $action * @return bool True if allowed */ public function userHasRight( UserIdentity $user, $action = '' ): bool { if ( $action === '' ) { // In the spirit of DWIM return true; } // Use strict parameter to avoid matching numeric 0 accidentally inserted // by misconfiguration: 0 == 'foo' return in_array( $action, $this->getImplicitRights(), true ) || in_array( $action, $this->getUserPermissions( $user ), true ); } /** * Whether the user is generally allowed to perform at least one of the actions. * * @since 1.34 * @param UserIdentity $user * @param string ...$actions * @return bool True if user is allowed to perform *any* of the actions */ public function userHasAnyRight( UserIdentity $user, ...$actions ): bool { foreach ( $actions as $action ) { if ( $this->userHasRight( $user, $action ) ) { return true; } } return false; } /** * Whether the user is allowed to perform all of the given actions. * * @since 1.34 * @param UserIdentity $user * @param string ...$actions * @return bool True if user is allowed to perform *all* of the given actions */ public function userHasAllRights( UserIdentity $user, ...$actions ): bool { foreach ( $actions as $action ) { if ( !$this->userHasRight( $user, $action ) ) { return false; } } return true; } /** * Get the permissions this user has. * * @since 1.34 * @param UserIdentity $user * @return string[] permission names */ public function getUserPermissions( UserIdentity $user ): array { $rightsCacheKey = $this->getRightsCacheKey( $user ); if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) { $userObj = $this->userFactory->newFromUserIdentity( $user ); $rights = $this->groupPermissionsLookup->getGroupPermissions( $this->userGroupManager->getUserEffectiveGroups( $user ) ); // Hook requires a full User object $this->hookRunner->onUserGetRights( $userObj, $rights ); // Deny any rights denied by the user's session, unless this // endpoint has no sessions. if ( !defined( 'MW_NO_SESSION' ) ) { // FIXME: $userObj->getRequest().. need to be replaced with something else $allowedRights = $userObj->getRequest()->getSession()->getAllowedUserRights(); if ( $allowedRights !== null ) { $rights = array_intersect( $rights, $allowedRights ); } } // Hook requires a full User object $this->hookRunner->onUserGetRightsRemove( $userObj, $rights ); // Force reindexation of rights when a hook has unset one of them $rights = array_values( array_unique( $rights ) ); // If BlockDisablesLogin is true, remove rights that anonymous // users don't have. This has to be done after the hooks so that // we know whether the user is exempt. (T129738) if ( $userObj->isRegistered() && $this->options->get( MainConfigNames::BlockDisablesLogin ) ) { // Stash the permissions as they are before triggering any block checks for BlockDisablesLogin // to avoid a potential infinite loop, since GetUserBlock handlers may themselves check // permissions on this user. (T384197) $this->usersRights[ $rightsCacheKey ] = $rights; $isExempt = in_array( 'ipblock-exempt', $rights, true ); if ( $this->blockManager->getBlock( $userObj, $isExempt ? null : $userObj->getRequest() ) ) { $anon = $this->userFactory->newAnonymous(); $rights = array_intersect( $rights, $this->getUserPermissions( $anon ) ); } } $this->usersRights[ $rightsCacheKey ] = $rights; } else { $rights = $this->usersRights[ $rightsCacheKey ]; } foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) { $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) ); } return $rights; } /** * Clear the in-process permission cache for one or all users. * * @since 1.34 * @param UserIdentity|null $user If a specific user is provided it will clear * the permission cache only for that user. */ public function invalidateUsersRightsCache( $user = null ): void { if ( $user !== null ) { $rightsCacheKey = $this->getRightsCacheKey( $user ); unset( $this->usersRights[ $rightsCacheKey ] ); } else { $this->usersRights = []; } } /** * Get a unique key for user rights cache. * * @param UserIdentity $user * @return string */ private function getRightsCacheKey( UserIdentity $user ): string { return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}"; } /** * Check if all users may be assumed to have the given permission * * We generally assume so if the right is granted to '*' and isn't revoked * on any group. It doesn't attempt to take grants or other extension * limitations on rights into account in the general case, though, as that * would require it to always return false and defeat the purpose. * Specifically, session-based rights restrictions (such as OAuth or bot * passwords) are applied based on the current session. * * @since 1.34 * @param string $right Right to check * @return bool */ public function isEveryoneAllowed( $right ): bool { // Use the cached results, except in unit tests which rely on // being able change the permission mid-request if ( isset( $this->cachedRights[$right] ) ) { return $this->cachedRights[$right]; } if ( !isset( $this->options->get( MainConfigNames::GroupPermissions )['*'][$right] ) || !$this->options->get( MainConfigNames::GroupPermissions )['*'][$right] ) { $this->cachedRights[$right] = false; return false; } // If it's revoked anywhere, then everyone doesn't have it foreach ( $this->options->get( MainConfigNames::RevokePermissions ) as $rights ) { if ( isset( $rights[$right] ) && $rights[$right] ) { $this->cachedRights[$right] = false; return false; } } // Remove any rights that aren't allowed to the global-session user, // unless there are no sessions for this endpoint. if ( !defined( 'MW_NO_SESSION' ) ) { // XXX: think what could be done with the below $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights(); if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) { $this->cachedRights[$right] = false; return false; } } // Allow extensions to say false if ( !$this->hookRunner->onUserIsEveryoneAllowed( $right ) ) { $this->cachedRights[$right] = false; return false; } $this->cachedRights[$right] = true; return true; } /** * Get a list of all permissions that can be managed through group permissions. * This does not include implicit rights which are granted to all users automatically. * * @see getImplicitRights() * * @since 1.34 * @return string[] Array of permission names */ public function getAllPermissions(): array { if ( $this->allRights === null ) { if ( count( $this->options->get( MainConfigNames::AvailableRights ) ) ) { $this->allRights = array_unique( array_merge( self::CORE_RIGHTS, $this->options->get( MainConfigNames::AvailableRights ) ) ); } else { $this->allRights = self::CORE_RIGHTS; } $this->hookRunner->onUserGetAllRights( $this->allRights ); } return $this->allRights; } /** * Get a list of implicit rights. * * Rights in this list should be granted to all users implicitly. * * Implicit rights are defined to allow rate limits to be imposed * on permissions * * @since 1.41 * @return string[] Array of permission names */ public function getImplicitRights(): array { if ( $this->implicitRights === null ) { $rights = array_unique( array_merge( self::CORE_IMPLICIT_RIGHTS, $this->options->get( MainConfigNames::ImplicitRights ) ) ); $this->implicitRights = array_diff( $rights, $this->getAllPermissions() ); } return $this->implicitRights; } /** * Determine if $user is unable to edit pages in namespace because it has been protected. * * @param int $index * @param UserIdentity $user * @return bool */ private function isNamespaceProtected( $index, UserIdentity $user ): bool { $namespaceProtection = $this->options->get( MainConfigNames::NamespaceProtection ); if ( isset( $namespaceProtection[$index] ) ) { return !$this->userHasAllRights( $user, ...(array)$namespaceProtection[$index] ); } return false; } /** * Determine which restriction levels it makes sense to use in a namespace, * optionally filtered by a user's rights. * * @param int $index Namespace ID (index) to check * @param UserIdentity|null $user User to check * @return string[] */ public function getNamespaceRestrictionLevels( $index, ?UserIdentity $user = null ): array { if ( !isset( $this->options->get( MainConfigNames::NamespaceProtection )[$index] ) ) { // All levels are valid if there's no namespace restriction. // But still filter by user, if necessary $levels = $this->options->get( MainConfigNames::RestrictionLevels ); if ( $user ) { $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) { $right = $level; if ( $right === 'sysop' ) { $right = 'editprotected'; // BC } if ( $right === 'autoconfirmed' ) { $right = 'editsemiprotected'; // BC } return $this->userHasRight( $user, $right ); } ) ); } return $levels; } // $wgNamespaceProtection can require one or more rights to edit the namespace, which // may be satisfied by membership in multiple groups each giving a subset of those rights. // A restriction level is redundant if, for any one of the namespace rights, all groups // giving that right also give the restriction level's right. Or, conversely, a // restriction level is not redundant if, for every namespace right, there's at least one // group giving that right without the restriction level's right. // // First, for each right, get a list of groups with that right. $namespaceRightGroups = []; foreach ( (array)$this->options->get( MainConfigNames::NamespaceProtection )[$index] as $right ) { if ( $right === 'sysop' ) { $right = 'editprotected'; // BC } if ( $right === 'autoconfirmed' ) { $right = 'editsemiprotected'; // BC } if ( $right != '' ) { $namespaceRightGroups[$right] = $this->groupPermissionsLookup->getGroupsWithPermission( $right ); } } // Now, go through the protection levels one by one. $usableLevels = [ '' ]; foreach ( $this->options->get( MainConfigNames::RestrictionLevels ) as $level ) { $right = $level; if ( $right === 'sysop' ) { $right = 'editprotected'; // BC } if ( $right === 'autoconfirmed' ) { $right = 'editsemiprotected'; // BC } if ( $right != '' && !isset( $namespaceRightGroups[$right] ) && ( !$user || $this->userHasRight( $user, $right ) ) ) { // Do any of the namespace rights imply the restriction right? (see explanation above) foreach ( $namespaceRightGroups as $groups ) { if ( !array_diff( $groups, $this->groupPermissionsLookup->getGroupsWithPermission( $right ) ) ) { // Yes, this one does. continue 2; } } // No, keep the restriction level $usableLevels[] = $level; } } return $usableLevels; } /** * Check if user is allowed to edit sitewide pages that contain raw HTML. * * Pages listed in $wgRawHtmlMessages allow raw HTML which can be used to deploy CSS or JS * code to all users so both rights are required to edit them. * * @param UserIdentity $user * @return bool True if user has both rights */ private function userCanEditRawHtmlPage( UserIdentity $user ): bool { return $this->userHasAllRights( $user, 'editsitecss', 'editsitejs' ); } /** * Add temporary user rights, only valid for the current function scope. * * This is meant for making it possible to programatically trigger certain actions that * the user wouldn't be able to trigger themselves; e.g. allow users without the bot right * to make bot-flagged actions through certain special pages. * * This returns a "scope guard" variable. Its only purpose is to be stored in a variable * by the caller, which is automatically closed at the end of the function, at which point * the rights are revoked again. Alternatively, you can close it earlier by consuming it * via ScopedCallback::consume(). * * @since 1.34 * @param UserIdentity $user * @param string|string[] $rights * @return ScopedCallback */ public function addTemporaryUserRights( UserIdentity $user, $rights ) { $userId = $user->getId(); $nextKey = count( $this->temporaryUserRights[$userId] ?? [] ); $this->temporaryUserRights[$userId][$nextKey] = (array)$rights; return new ScopedCallback( function () use ( $userId, $nextKey ) { unset( $this->temporaryUserRights[$userId][$nextKey] ); } ); } /** * Override the user permissions cache * * @internal For testing only * @since 1.34 * @param UserIdentity $user * @param string[]|string $rights */ public function overrideUserRightsForTesting( $user, $rights = [] ) { if ( !defined( 'MW_PHPUNIT_TEST' ) ) { throw new LogicException( __METHOD__ . ' can not be called outside of tests' ); } $this->usersRights[ $this->getRightsCacheKey( $user ) ] = is_array( $rights ) ? $rights : [ $rights ]; } } PK ! � B� � Hook/UserCanHook.phpnu �Iw�� <?php namespace MediaWiki\Permissions\Hook; use MediaWiki\Title\Title; use MediaWiki\User\User; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "userCan" to register handlers implementing this interface. * * @deprecated since 1.37 use getUserPermissionsErrors or getUserPermissionsErrorsExpensive instead. * @ingroup Hooks */ interface UserCanHook { /** * Use this hook to interrupt or advise the "user can do X to Y article" check. * If you want to display an error message, try getUserPermissionsErrors. * * @since 1.35 * * @param Title $title Title being checked against * @param User $user Current user * @param string $action Action being checked * @param string &$result Pointer to result returned if hook returns false. * If null is returned, userCan checks are continued by internal code. * @return bool|void True or no return value to continue or false to abort */ public function onUserCan( $title, $user, $action, &$result ); } PK ! s��E E Hook/UserGetAllRightsHook.phpnu �Iw�� <?php namespace MediaWiki\Permissions\Hook; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "UserGetAllRights" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface UserGetAllRightsHook { /** * This hook is called after calculating a list of all available rights. * * @since 1.35 * * @param string[] &$rights Array of rights, which may be added to * @return bool|void True or no return value to continue or false to abort */ public function onUserGetAllRights( &$rights ); } PK ! ܜ�)� � Hook/UserIsBlockedFromHook.phpnu �Iw�� <?php namespace MediaWiki\Permissions\Hook; use MediaWiki\Title\Title; use MediaWiki\User\User; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "UserIsBlockedFrom" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface UserIsBlockedFromHook { /** * Use this hook to check if a user is blocked from a specific page (for * specific block exemptions if a user is already blocked). * * @since 1.35 * * @param User $user User in question * @param Title $title Title of the page in question * @param bool &$blocked Out-param, whether or not the user is blocked from that page * @param bool &$allowUsertalk If the user is blocked, whether or not the block allows users * to edit their own user talk pages * @return bool|void True or no return value to continue or false to abort */ public function onUserIsBlockedFrom( $user, $title, &$blocked, &$allowUsertalk ); } PK ! �:��U U Hook/TitleReadWhitelistHook.phpnu �Iw�� <?php namespace MediaWiki\Permissions\Hook; use MediaWiki\Title\Title; use MediaWiki\User\User; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "TitleReadWhitelist" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface TitleReadWhitelistHook { /** * This hook is called at the end of read permissions checks, just before * adding the default error message if nothing allows the user to read the page. * * @since 1.35 * * @param Title $title Title being checked against * @param User $user Current user * @param bool &$whitelisted Whether this title is whitelisted * @return bool|void True or no return value to continue, or false to *not* whitelist * the title */ public function onTitleReadWhitelist( $title, $user, &$whitelisted ); } PK ! 9:k�� � "