2019-03-07 20:02:07 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
*/
|
|
|
|
|
namespace MediaWiki\Permissions;
|
|
|
|
|
|
|
|
|
|
use Action;
|
2020-02-10 20:38:45 +00:00
|
|
|
use Article;
|
2019-03-07 20:02:07 +00:00
|
|
|
use Exception;
|
2019-09-20 15:03:48 +00:00
|
|
|
use MediaWiki\Block\BlockErrorFormatter;
|
2021-04-21 16:25:17 +00:00
|
|
|
use MediaWiki\Block\DatabaseBlock;
|
2020-01-10 00:00:51 +00:00
|
|
|
use MediaWiki\Config\ServiceOptions;
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
|
|
|
use MediaWiki\HookContainer\HookRunner;
|
2019-03-07 20:02:07 +00:00
|
|
|
use MediaWiki\Linker\LinkTarget;
|
2018-11-01 23:29:22 +00:00
|
|
|
use MediaWiki\Revision\RevisionLookup;
|
|
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
2019-04-09 06:58:04 +00:00
|
|
|
use MediaWiki\Session\SessionManager;
|
2020-02-21 00:01:43 +00:00
|
|
|
use MediaWiki\SpecialPage\SpecialPageFactory;
|
2021-01-05 23:08:09 +00:00
|
|
|
use MediaWiki\User\UserGroupManager;
|
2019-04-09 06:58:04 +00:00
|
|
|
use MediaWiki\User\UserIdentity;
|
2019-03-07 20:02:07 +00:00
|
|
|
use MessageSpecifier;
|
2019-04-09 09:28:38 +00:00
|
|
|
use NamespaceInfo;
|
2019-03-07 20:02:07 +00:00
|
|
|
use RequestContext;
|
|
|
|
|
use SpecialPage;
|
|
|
|
|
use Title;
|
|
|
|
|
use User;
|
2020-10-20 17:34:25 +00:00
|
|
|
use UserCache;
|
2019-07-11 17:22:20 +00:00
|
|
|
use Wikimedia\ScopedCallback;
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) */
|
2019-12-06 20:39:40 +00:00
|
|
|
public const RIGOR_QUICK = 'quick';
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
/** @var string Does cheap and expensive checks possibly from a replica DB */
|
2019-12-06 20:39:40 +00:00
|
|
|
public const RIGOR_FULL = 'full';
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
/** @var string Does cheap and expensive checks, using the master as needed */
|
2019-12-06 20:39:40 +00:00
|
|
|
public const RIGOR_SECURE = 'secure';
|
2019-03-07 20:02:07 +00:00
|
|
|
|
2019-08-21 05:28:47 +00:00
|
|
|
/**
|
2019-10-25 08:07:22 +00:00
|
|
|
* @internal For use by ServiceWiring
|
2019-08-21 05:28:47 +00:00
|
|
|
*/
|
2019-10-08 18:23:08 +00:00
|
|
|
public const CONSTRUCTOR_OPTIONS = [
|
2019-08-21 05:28:47 +00:00
|
|
|
'WhitelistRead',
|
|
|
|
|
'WhitelistReadRegexp',
|
|
|
|
|
'EmailConfirmToEdit',
|
|
|
|
|
'BlockDisablesLogin',
|
Introduce infrastructure for partial blocks for actions
This adds a new type of block restriction for actions, which extends
AbstractRestriction. Like page and namespace restrictions, action
restrictions are stored in the ipblocks_restrictions table.
Blockable actions are defined in a BlockActionInfo service, with a
method for getting all the blockable actions, getAllBlockActions.
Action blocks are checked for in PermissionManager::checkUserBlock
using DatabaseBlock::appliesToRight. To make this work, this patch
also removes the 'edit' case from AbstractBlock::appliesToRight,
which always returned true. This was incorrect, as blocks do not
always apply to edit, so cases that called appliesToRight('edit')
were fixed before this commit. appliesToRight('edit') now returns
null (i.e. unsure), which is correct because it is not possible to
determine whether a block applies to editing a particular page
without knowing what that page is, and appliesToRight doesn't know
that page.
There are some flags on sitewide blocks that predate partial blocks,
which block particular actions: 'createaccount' and 'sendemail'.
These are still handled in AbstractBlock::appliesToRight, and are
still checked for separately in the peripheral components.
The feature flag $wgEnablePartialActionBlocks must set to true to
enable partial action blocks.
Bug: T279556
Bug: T6995
Change-Id: I17962bb7c4247a12c722e7bc6bcaf8c36efd8600
2021-04-26 23:07:17 +00:00
|
|
|
'EnablePartialActionBlocks',
|
2019-08-21 05:28:47 +00:00
|
|
|
'GroupPermissions',
|
|
|
|
|
'RevokePermissions',
|
2019-08-21 19:49:59 +00:00
|
|
|
'AvailableRights',
|
|
|
|
|
'NamespaceProtection',
|
2021-03-29 22:32:07 +00:00
|
|
|
'RestrictionLevels',
|
|
|
|
|
'DeleteRevisionsLimit',
|
2019-08-21 05:28:47 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** @var ServiceOptions */
|
|
|
|
|
private $options;
|
|
|
|
|
|
2019-03-07 20:02:07 +00:00
|
|
|
/** @var SpecialPageFactory */
|
|
|
|
|
private $specialPageFactory;
|
|
|
|
|
|
2018-11-01 23:29:22 +00:00
|
|
|
/** @var RevisionLookup */
|
|
|
|
|
private $revisionLookup;
|
|
|
|
|
|
2019-05-05 15:43:42 +00:00
|
|
|
/** @var NamespaceInfo */
|
|
|
|
|
private $nsInfo;
|
|
|
|
|
|
2021-01-05 23:08:09 +00:00
|
|
|
/** @var GroupPermissionsLookup */
|
|
|
|
|
private $groupPermissionLookup;
|
|
|
|
|
|
|
|
|
|
/** @var UserGroupManager */
|
|
|
|
|
private $userGroupManager;
|
|
|
|
|
|
2020-10-02 21:59:17 +00:00
|
|
|
/** @var string[]|null Cached results of getAllPermissions() */
|
2019-08-30 16:01:28 +00:00
|
|
|
private $allRights;
|
2019-04-09 06:58:04 +00:00
|
|
|
|
2019-09-20 15:03:48 +00:00
|
|
|
/** @var BlockErrorFormatter */
|
|
|
|
|
private $blockErrorFormatter;
|
|
|
|
|
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
/** @var HookRunner */
|
|
|
|
|
private $hookRunner;
|
|
|
|
|
|
2020-10-20 17:34:25 +00:00
|
|
|
/** @var UserCache */
|
|
|
|
|
private $userCache;
|
|
|
|
|
|
2019-04-09 06:58:04 +00:00
|
|
|
/** @var string[][] Cached user rights */
|
|
|
|
|
private $usersRights = null;
|
|
|
|
|
|
2019-07-11 17:22:20 +00:00
|
|
|
/**
|
|
|
|
|
* Temporary user rights, valid for the current request only.
|
|
|
|
|
* @var string[][][] userid => override group => rights
|
|
|
|
|
*/
|
|
|
|
|
private $temporaryUserRights = [];
|
|
|
|
|
|
2019-09-15 14:40:42 +00:00
|
|
|
/** @var bool[] Cached rights for isEveryoneAllowed, [ right => allowed ] */
|
2019-04-09 06:58:04 +00:00
|
|
|
private $cachedRights = [];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Array of Strings Core rights.
|
|
|
|
|
* Each of these should have a corresponding message of the form
|
|
|
|
|
* "right-$right".
|
|
|
|
|
* @showinitializer
|
|
|
|
|
*/
|
|
|
|
|
private $coreRights = [
|
|
|
|
|
'apihighlimits',
|
|
|
|
|
'applychangetags',
|
|
|
|
|
'autoconfirmed',
|
|
|
|
|
'autocreateaccount',
|
|
|
|
|
'autopatrol',
|
|
|
|
|
'bigdelete',
|
|
|
|
|
'block',
|
|
|
|
|
'blockemail',
|
|
|
|
|
'bot',
|
|
|
|
|
'browsearchive',
|
|
|
|
|
'changetags',
|
|
|
|
|
'createaccount',
|
|
|
|
|
'createpage',
|
|
|
|
|
'createtalk',
|
|
|
|
|
'delete',
|
Add `delete-redirect` for deleting single-rev redirects during moves
A new user right, `delete-redirect`, is added (not given to anyone
by default). At Special:MovePage, if attempting to move to a single
revision redirect that would otherwise be an invalid target (i.e.
doesn't point to the source page), the user is able to delete the
target.
Deletions are logged as `delete/delete_redir2`, and the move is
then logged normally as `move/move`, mirroring current delete and
move logging.
To allow for separate handling by Special:MovePage,
MovePage::isValidMove now returns a fatal status `redirectexists` if
the target isn't valid but passes Title::isSingleRevRedirect.
Otherwise, `articleexists` is returned (as previously). Other callers
that don't intend to treat single revision redirects differently
should treat `redirectexists` the same as `articleexists`.
Currently, this deletion (like normal delete and move) cannot be
done through the move api. Since the deletion is only valid when
moving a page, unlike for normal deletion, deleting redirects with
this right cannot be done via the delete api either.
Bug: T239277
Change-Id: I36c8df0a12d326ae07018046541bd00103936144
2019-12-19 23:13:31 +00:00
|
|
|
'delete-redirect',
|
2019-04-09 06:58:04 +00:00
|
|
|
'deletechangetags',
|
|
|
|
|
'deletedhistory',
|
|
|
|
|
'deletedtext',
|
|
|
|
|
'deletelogentry',
|
|
|
|
|
'deleterevision',
|
|
|
|
|
'edit',
|
|
|
|
|
'editcontentmodel',
|
|
|
|
|
'editinterface',
|
|
|
|
|
'editprotected',
|
|
|
|
|
'editmyoptions',
|
|
|
|
|
'editmyprivateinfo',
|
|
|
|
|
'editmyusercss',
|
|
|
|
|
'editmyuserjson',
|
|
|
|
|
'editmyuserjs',
|
2018-11-01 23:29:22 +00:00
|
|
|
'editmyuserjsredirect',
|
2019-04-09 06:58:04 +00:00
|
|
|
'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',
|
|
|
|
|
'purge',
|
|
|
|
|
'read',
|
|
|
|
|
'reupload',
|
|
|
|
|
'reupload-own',
|
|
|
|
|
'reupload-shared',
|
|
|
|
|
'rollback',
|
|
|
|
|
'sendemail',
|
|
|
|
|
'siteadmin',
|
|
|
|
|
'suppressionlog',
|
|
|
|
|
'suppressredirect',
|
|
|
|
|
'suppressrevision',
|
|
|
|
|
'unblockself',
|
|
|
|
|
'undelete',
|
|
|
|
|
'unwatchedpages',
|
|
|
|
|
'upload',
|
|
|
|
|
'upload_by_url',
|
|
|
|
|
'userrights',
|
|
|
|
|
'userrights-interwiki',
|
|
|
|
|
'viewmyprivateinfo',
|
|
|
|
|
'viewmywatchlist',
|
|
|
|
|
'viewsuppressed',
|
|
|
|
|
'writeapi',
|
|
|
|
|
];
|
|
|
|
|
|
2019-03-07 20:02:07 +00:00
|
|
|
/**
|
2019-08-21 05:28:47 +00:00
|
|
|
* @param ServiceOptions $options
|
2019-03-07 20:02:07 +00:00
|
|
|
* @param SpecialPageFactory $specialPageFactory
|
2018-11-01 23:29:22 +00:00
|
|
|
* @param RevisionLookup $revisionLookup
|
2019-05-05 15:43:42 +00:00
|
|
|
* @param NamespaceInfo $nsInfo
|
2021-01-05 23:08:09 +00:00
|
|
|
* @param GroupPermissionsLookup $groupPermissionLookup
|
|
|
|
|
* @param UserGroupManager $userGroupManager
|
2019-09-20 15:03:48 +00:00
|
|
|
* @param BlockErrorFormatter $blockErrorFormatter
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
* @param HookContainer $hookContainer
|
2020-10-20 17:34:25 +00:00
|
|
|
* @param UserCache $userCache
|
2019-03-07 20:02:07 +00:00
|
|
|
*/
|
|
|
|
|
public function __construct(
|
2019-08-21 05:28:47 +00:00
|
|
|
ServiceOptions $options,
|
2019-03-07 20:02:07 +00:00
|
|
|
SpecialPageFactory $specialPageFactory,
|
2018-11-01 23:29:22 +00:00
|
|
|
RevisionLookup $revisionLookup,
|
2019-09-20 15:03:48 +00:00
|
|
|
NamespaceInfo $nsInfo,
|
2021-01-05 23:08:09 +00:00
|
|
|
GroupPermissionsLookup $groupPermissionLookup,
|
|
|
|
|
UserGroupManager $userGroupManager,
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
BlockErrorFormatter $blockErrorFormatter,
|
2020-10-20 17:34:25 +00:00
|
|
|
HookContainer $hookContainer,
|
|
|
|
|
UserCache $userCache
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
2019-10-08 18:23:08 +00:00
|
|
|
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
|
2019-08-21 05:28:47 +00:00
|
|
|
$this->options = $options;
|
2019-03-07 20:02:07 +00:00
|
|
|
$this->specialPageFactory = $specialPageFactory;
|
2018-11-01 23:29:22 +00:00
|
|
|
$this->revisionLookup = $revisionLookup;
|
2019-04-09 09:28:38 +00:00
|
|
|
$this->nsInfo = $nsInfo;
|
2021-01-05 23:08:09 +00:00
|
|
|
$this->groupPermissionLookup = $groupPermissionLookup;
|
|
|
|
|
$this->userGroupManager = $userGroupManager;
|
2019-09-20 15:03:48 +00:00
|
|
|
$this->blockErrorFormatter = $blockErrorFormatter;
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
$this->hookRunner = new HookRunner( $hookContainer );
|
2020-10-20 17:34:25 +00:00
|
|
|
$this->userCache = $userCache;
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Can $user perform $action on a page?
|
|
|
|
|
*
|
2020-07-27 14:40:24 +00:00
|
|
|
* The method replaced Title::userCan()
|
2019-03-07 20:02:07 +00:00
|
|
|
* 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 master as needed
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) {
|
|
|
|
|
return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-23 23:53:15 +00:00
|
|
|
/**
|
|
|
|
|
* 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 ) {
|
|
|
|
|
return $this->userCan( $action, $user, $page, self::RIGOR_QUICK );
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-07 20:02:07 +00:00
|
|
|
/**
|
|
|
|
|
* Can $user perform $action on a page?
|
|
|
|
|
*
|
|
|
|
|
* @todo FIXME: This *does not* check throttles (User::pingLimiter()).
|
|
|
|
|
*
|
|
|
|
|
* @param string $action Action that permission needs to be checked for
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
2020-10-28 10:01:33 +00:00
|
|
|
* @param string[] $ignoreErrors Set this to a list of message keys
|
2019-03-07 20:02:07 +00:00
|
|
|
* whose corresponding errors may be ignored.
|
|
|
|
|
*
|
2019-10-11 13:23:48 +00:00
|
|
|
* @return array[] Array of arrays of the arguments to wfMessage to explain permissions problems.
|
2019-03-07 20:02:07 +00:00
|
|
|
*/
|
|
|
|
|
public function getPermissionErrors(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
LinkTarget $page,
|
|
|
|
|
$rigor = self::RIGOR_SECURE,
|
|
|
|
|
$ignoreErrors = []
|
|
|
|
|
) {
|
|
|
|
|
$errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
|
|
|
|
|
|
|
|
|
|
// Remove the errors being ignored.
|
|
|
|
|
foreach ( $errors as $index => $error ) {
|
|
|
|
|
$errKey = is_array( $error ) ? $error[0] : $error;
|
|
|
|
|
|
|
|
|
|
if ( in_array( $errKey, $ignoreErrors ) ) {
|
|
|
|
|
unset( $errors[$index] );
|
|
|
|
|
}
|
|
|
|
|
if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
|
|
|
|
|
unset( $errors[$index] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-07-24 15:27:52 +00:00
|
|
|
* Check if user is blocked from editing a particular article. If the user does not
|
|
|
|
|
* have a block, this will return false.
|
2019-03-07 20:02:07 +00:00
|
|
|
*
|
|
|
|
|
* @param User $user
|
|
|
|
|
* @param LinkTarget $page Title to check
|
|
|
|
|
* @param bool $fromReplica Whether to check the replica DB instead of the master
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
|
2019-07-24 15:27:52 +00:00
|
|
|
$block = $user->getBlock( $fromReplica );
|
|
|
|
|
if ( !$block ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
// TODO: remove upon further migration to LinkTarget
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
2019-03-07 20:02:07 +00:00
|
|
|
|
2019-07-24 15:27:52 +00:00
|
|
|
$blocked = $user->isHidden();
|
2019-03-07 20:02:07 +00:00
|
|
|
if ( !$blocked ) {
|
2019-07-24 15:27:52 +00:00
|
|
|
// Special handling for a user's own talk page. The block is not aware
|
|
|
|
|
// of the user, so this must be done here.
|
|
|
|
|
if ( $title->equals( $user->getTalkPage() ) ) {
|
|
|
|
|
$blocked = $block->appliesToUsertalk( $title );
|
|
|
|
|
} else {
|
|
|
|
|
$blocked = $block->appliesToTitle( $title );
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// only for the purpose of the hook. We really don't need this here.
|
|
|
|
|
$allowUsertalk = $user->isAllowUsertalk();
|
|
|
|
|
|
2019-07-24 15:27:52 +00:00
|
|
|
// Allow extensions to let a blocked user access a particular page
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
$this->hookRunner->onUserIsBlockedFrom( $user, $title, $blocked, $allowUsertalk );
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
return $blocked;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Can $user perform $action on a page? This is an internal function,
|
|
|
|
|
* with multiple levels of checks depending on performance needs; see $rigor below.
|
|
|
|
|
* It does not check wfReadOnly().
|
|
|
|
|
*
|
|
|
|
|
* @param string $action Action that permission needs to be checked for
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Set this to true to stop after the first permission error.
|
|
|
|
|
*
|
2019-10-11 13:23:48 +00:00
|
|
|
* @return array[] Array of arrays of the arguments to wfMessage to explain permissions problems.
|
2019-03-07 20:02:07 +00:00
|
|
|
* @throws Exception
|
|
|
|
|
*/
|
|
|
|
|
private function getPermissionErrorsInternal(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
LinkTarget $page,
|
|
|
|
|
$rigor = self::RIGOR_SECURE,
|
|
|
|
|
$short = false
|
|
|
|
|
) {
|
|
|
|
|
if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
|
|
|
|
|
throw new Exception( "Invalid rigor parameter '$rigor'." );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Read has special handling
|
|
|
|
|
if ( $action == 'read' ) {
|
|
|
|
|
$checks = [
|
|
|
|
|
'checkPermissionHooks',
|
|
|
|
|
'checkReadPermissions',
|
|
|
|
|
'checkUserBlock', // for wgBlockDisablesLogin
|
|
|
|
|
];
|
|
|
|
|
# Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
|
|
|
|
|
# or checkUserConfigPermissions here as it will lead to duplicate
|
|
|
|
|
# error messages. This is okay to do since anywhere that checks for
|
|
|
|
|
# create will also check for edit, and those checks are called for edit.
|
|
|
|
|
} elseif ( $action == 'create' ) {
|
|
|
|
|
$checks = [
|
|
|
|
|
'checkQuickPermissions',
|
|
|
|
|
'checkPermissionHooks',
|
|
|
|
|
'checkPageRestrictions',
|
|
|
|
|
'checkCascadingSourcesRestrictions',
|
|
|
|
|
'checkActionPermissions',
|
|
|
|
|
'checkUserBlock'
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
$checks = [
|
|
|
|
|
'checkQuickPermissions',
|
|
|
|
|
'checkPermissionHooks',
|
|
|
|
|
'checkSpecialsAndNSPermissions',
|
|
|
|
|
'checkSiteConfigPermissions',
|
|
|
|
|
'checkUserConfigPermissions',
|
|
|
|
|
'checkPageRestrictions',
|
|
|
|
|
'checkCascadingSourcesRestrictions',
|
|
|
|
|
'checkActionPermissions',
|
|
|
|
|
'checkUserBlock'
|
|
|
|
|
];
|
2020-10-15 21:37:09 +00:00
|
|
|
|
|
|
|
|
// 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',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ( in_array( $action, $skipUserConfigActions, true ) ) {
|
|
|
|
|
$checks = array_diff(
|
|
|
|
|
$checks,
|
|
|
|
|
[ 'checkUserConfigPermissions' ]
|
|
|
|
|
);
|
|
|
|
|
// Reset numbering
|
|
|
|
|
$checks = array_values( $checks );
|
|
|
|
|
}
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$errors = [];
|
|
|
|
|
foreach ( $checks as $method ) {
|
|
|
|
|
$errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
|
|
|
|
|
|
|
|
|
|
if ( $short && $errors !== [] ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check various permission hooks
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkPermissionHooks(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// TODO: remove when LinkTarget usage will expand further
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
2019-03-07 20:02:07 +00:00
|
|
|
// Use getUserPermissionsErrors instead
|
|
|
|
|
$result = '';
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
if ( !$this->hookRunner->onUserCan( $title, $user, $action, $result ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
return $result ? [] : [ [ 'badaccess-group0' ] ];
|
|
|
|
|
}
|
|
|
|
|
// Check getUserPermissionsErrors hook
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
if ( !$this->hookRunner->onGetUserPermissionsErrors( $title, $user, $action, $result ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors = $this->resultToError( $errors, $result );
|
|
|
|
|
}
|
|
|
|
|
// Check getUserPermissionsErrorsExpensive hook
|
|
|
|
|
if (
|
|
|
|
|
$rigor !== self::RIGOR_QUICK
|
|
|
|
|
&& !( $short && count( $errors ) > 0 )
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
&& !$this->hookRunner->onGetUserPermissionsErrorsExpensive(
|
|
|
|
|
$title, $user, $action, $result )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
$errors = $this->resultToError( $errors, $result );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the resulting error code to the errors array
|
|
|
|
|
*
|
|
|
|
|
* @param array $errors List of current errors
|
2019-06-07 15:23:50 +00:00
|
|
|
* @param array|string|MessageSpecifier|false $result Result of errors
|
2019-03-07 20:02:07 +00:00
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function resultToError( $errors, $result ) {
|
|
|
|
|
if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
|
|
|
|
|
// A single array representing an error
|
|
|
|
|
$errors[] = $result;
|
|
|
|
|
} elseif ( is_array( $result ) && is_array( $result[0] ) ) {
|
|
|
|
|
// A nested array representing multiple errors
|
|
|
|
|
$errors = array_merge( $errors, $result );
|
|
|
|
|
} elseif ( $result !== '' && is_string( $result ) ) {
|
|
|
|
|
// A string representing a message-id
|
|
|
|
|
$errors[] = [ $result ];
|
|
|
|
|
} elseif ( $result instanceof MessageSpecifier ) {
|
|
|
|
|
// A message specifier representing an error
|
|
|
|
|
$errors[] = [ $result ];
|
|
|
|
|
} elseif ( $result === false ) {
|
|
|
|
|
// a generic "We don't want them to do that"
|
|
|
|
|
$errors[] = [ 'badaccess-group0' ];
|
|
|
|
|
}
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check that the user is allowed to read this page.
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkReadPermissions(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// TODO: remove when LinkTarget usage will expand further
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
2019-03-07 20:02:07 +00:00
|
|
|
|
2019-08-21 05:28:47 +00:00
|
|
|
$whiteListRead = $this->options->get( 'WhitelistRead' );
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = false;
|
2019-08-16 18:13:56 +00:00
|
|
|
if ( $this->isEveryoneAllowed( 'read' ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
# Shortcut for public wikis, allows skipping quite a bit of code
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = true;
|
2019-08-16 18:13:56 +00:00
|
|
|
} elseif ( $this->userHasRight( $user, 'read' ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
# If the user is allowed to read pages, he is allowed to read all pages
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = true;
|
2019-08-08 14:01:01 +00:00
|
|
|
} elseif ( $this->isSameSpecialPage( 'Userlogin', $title )
|
2020-06-27 01:13:01 +00:00
|
|
|
|| $this->isSameSpecialPage( 'PasswordReset', $title )
|
|
|
|
|
|| $this->isSameSpecialPage( 'Userlogout', $title )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
# Always grant access to the login page.
|
|
|
|
|
# Even anons need to be able to log in.
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = true;
|
2021-03-16 17:58:45 +00:00
|
|
|
} elseif ( $this->isSameSpecialPage( 'RunJobs', $title ) ) {
|
|
|
|
|
# relies on HMAC key signature alone
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = true;
|
2019-08-21 05:28:47 +00:00
|
|
|
} elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
# Time to check the whitelist
|
|
|
|
|
# Only do these checks is there's something to check against
|
2019-08-08 14:01:01 +00:00
|
|
|
$name = $title->getPrefixedText();
|
|
|
|
|
$dbName = $title->getPrefixedDBkey();
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
// Check for explicit whitelisting with and without underscores
|
2019-08-21 05:28:47 +00:00
|
|
|
if ( in_array( $name, $whiteListRead, true )
|
|
|
|
|
|| in_array( $dbName, $whiteListRead, true ) ) {
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = true;
|
2020-07-22 17:29:48 +00:00
|
|
|
} elseif ( $title->getNamespace() === NS_MAIN ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
# Old settings might have the title prefixed with
|
|
|
|
|
# a colon for main-namespace pages
|
2019-08-21 05:28:47 +00:00
|
|
|
if ( in_array( ':' . $name, $whiteListRead ) ) {
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = true;
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
2019-08-08 14:01:01 +00:00
|
|
|
} elseif ( $title->isSpecialPage() ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
# If it's a special page, ditch the subpage bit and check again
|
2019-08-08 14:01:01 +00:00
|
|
|
$name = $title->getDBkey();
|
2019-03-07 20:02:07 +00:00
|
|
|
list( $name, /* $subpage */ ) =
|
|
|
|
|
$this->specialPageFactory->resolveAlias( $name );
|
|
|
|
|
if ( $name ) {
|
|
|
|
|
$pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
|
2019-08-21 05:28:47 +00:00
|
|
|
if ( in_array( $pure, $whiteListRead, true ) ) {
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = true;
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 05:28:47 +00:00
|
|
|
$whitelistReadRegexp = $this->options->get( 'WhitelistReadRegexp' );
|
2021-03-19 18:36:44 +00:00
|
|
|
if ( !$allowed && is_array( $whitelistReadRegexp )
|
2019-08-21 05:28:47 +00:00
|
|
|
&& !empty( $whitelistReadRegexp ) ) {
|
2019-08-08 14:01:01 +00:00
|
|
|
$name = $title->getPrefixedText();
|
2019-03-07 20:02:07 +00:00
|
|
|
// Check for regex whitelisting
|
2019-08-21 05:28:47 +00:00
|
|
|
foreach ( $whitelistReadRegexp as $listItem ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
if ( preg_match( $listItem, $name ) ) {
|
2021-03-19 18:36:44 +00:00
|
|
|
$allowed = true;
|
2019-03-07 20:02:07 +00:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-19 18:36:44 +00:00
|
|
|
if ( !$allowed ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
# If the title is not whitelisted, give extensions a chance to do so...
|
2021-03-19 18:36:44 +00:00
|
|
|
$this->hookRunner->onTitleReadWhitelist( $title, $user, $allowed );
|
|
|
|
|
if ( !$allowed ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors[] = $this->missingPermissionError( $action, $short );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a description array when the user doesn't have the right to perform
|
|
|
|
|
* $action (i.e. when User::isAllowed() returns false)
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
* @return array Array containing an error message key and any parameters
|
|
|
|
|
*/
|
|
|
|
|
private function missingPermissionError( $action, $short ) {
|
|
|
|
|
// We avoid expensive display logic for quickUserCan's and such
|
|
|
|
|
if ( $short ) {
|
|
|
|
|
return [ 'badaccess-group0' ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: it would be a good idea to replace the method below with something else like
|
|
|
|
|
// maybe callback injection
|
|
|
|
|
return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns true if this title resolves to the named special page
|
|
|
|
|
*
|
|
|
|
|
* @param string $name The special page name
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
private function isSameSpecialPage( $name, LinkTarget $page ) {
|
2020-07-22 17:29:48 +00:00
|
|
|
if ( $page->getNamespace() === NS_SPECIAL ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
list( $thisName, /* $subpage */ ) =
|
|
|
|
|
$this->specialPageFactory->resolveAlias( $page->getDBkey() );
|
|
|
|
|
if ( $name == $thisName ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check that the user isn't blocked from editing.
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkUserBlock(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// Unblocking handled in SpecialUnblock
|
2021-04-21 16:25:17 +00:00
|
|
|
if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'unblock' ] ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Optimize for a very common case
|
2019-08-21 05:28:47 +00:00
|
|
|
if ( $action === 'read' && !$this->options->get( 'BlockDisablesLogin' ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 05:28:47 +00:00
|
|
|
if ( $this->options->get( 'EmailConfirmToEdit' )
|
2019-03-07 20:02:07 +00:00
|
|
|
&& !$user->isEmailConfirmed()
|
|
|
|
|
&& $action === 'edit'
|
|
|
|
|
) {
|
|
|
|
|
$errors[] = [ 'confirmedittext' ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$useReplica = ( $rigor !== self::RIGOR_SECURE );
|
|
|
|
|
$block = $user->getBlock( $useReplica );
|
|
|
|
|
|
2021-04-21 16:25:17 +00:00
|
|
|
if ( $action === 'createaccount' ) {
|
|
|
|
|
$applicableBlock = null;
|
|
|
|
|
if ( $block && $block->appliesToRight( 'createaccount' ) ) {
|
|
|
|
|
$applicableBlock = $block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# T15611: if the IP address the user is trying to create an account from is
|
|
|
|
|
# blocked with createaccount disabled, prevent new account creation there even
|
|
|
|
|
# when the user is logged in
|
|
|
|
|
if ( !$this->userHasRight( $user, 'ipblock-exempt' ) ) {
|
|
|
|
|
$ipBlock = DatabaseBlock::newFromTarget(
|
|
|
|
|
null, $user->getRequest()->getIP()
|
|
|
|
|
);
|
|
|
|
|
if ( $ipBlock && $ipBlock->appliesToRight( 'createaccount' ) ) {
|
|
|
|
|
$applicableBlock = $ipBlock;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// @todo FIXME: Pass the relevant context into this function.
|
|
|
|
|
if ( $applicableBlock ) {
|
|
|
|
|
$context = RequestContext::getMain();
|
|
|
|
|
$message = $this->blockErrorFormatter->getMessage(
|
|
|
|
|
$applicableBlock,
|
|
|
|
|
$context->getUser(),
|
|
|
|
|
$context->getLanguage(),
|
|
|
|
|
$context->getRequest()->getIP()
|
|
|
|
|
);
|
|
|
|
|
$errors[] = array_merge( [ $message->getKey() ], $message->getParams() );
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-07 20:02:07 +00:00
|
|
|
// If the user does not have a block, or the block they do have explicitly
|
|
|
|
|
// allows the action (like "read" or "upload").
|
|
|
|
|
if ( !$block || $block->appliesToRight( $action ) === false ) {
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine if the user is blocked from this action on this page.
|
|
|
|
|
// What gets passed into this method is a user right, not an action name.
|
|
|
|
|
// There is no way to instantiate an action by restriction. However, this
|
|
|
|
|
// will get the action where the restriction is the same. This may result
|
|
|
|
|
// in actions being blocked that shouldn't be.
|
|
|
|
|
$actionObj = null;
|
|
|
|
|
if ( Action::exists( $action ) ) {
|
2020-02-10 20:38:45 +00:00
|
|
|
// TODO: this drags a ton of dependencies in, would be good to avoid Article
|
2019-03-07 20:02:07 +00:00
|
|
|
// instantiation and decouple it creating an ActionPermissionChecker interface
|
|
|
|
|
// Creating an action will perform several database queries to ensure that
|
|
|
|
|
// the action has not been overridden by the content type.
|
|
|
|
|
// FIXME: avoid use of RequestContext since it drags in User and Title dependencies
|
|
|
|
|
// probably we may use fake context object since it's unlikely that Action uses it
|
|
|
|
|
// anyway. It would be nice if we could avoid instantiating the Action at all.
|
2020-02-10 20:38:45 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page, 'clone' );
|
|
|
|
|
$context = RequestContext::getMain();
|
|
|
|
|
$actionObj = Action::factory(
|
|
|
|
|
$action,
|
|
|
|
|
Article::newFromTitle( $title, $context ),
|
|
|
|
|
$context
|
|
|
|
|
);
|
2019-03-07 20:02:07 +00:00
|
|
|
// Ensure that the retrieved action matches the restriction.
|
|
|
|
|
if ( $actionObj && $actionObj->getRestriction() !== $action ) {
|
|
|
|
|
$actionObj = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no action object is returned, assume that the action requires unblock
|
|
|
|
|
// which is the default.
|
|
|
|
|
if ( !$actionObj || $actionObj->requiresUnblock() ) {
|
Introduce infrastructure for partial blocks for actions
This adds a new type of block restriction for actions, which extends
AbstractRestriction. Like page and namespace restrictions, action
restrictions are stored in the ipblocks_restrictions table.
Blockable actions are defined in a BlockActionInfo service, with a
method for getting all the blockable actions, getAllBlockActions.
Action blocks are checked for in PermissionManager::checkUserBlock
using DatabaseBlock::appliesToRight. To make this work, this patch
also removes the 'edit' case from AbstractBlock::appliesToRight,
which always returned true. This was incorrect, as blocks do not
always apply to edit, so cases that called appliesToRight('edit')
were fixed before this commit. appliesToRight('edit') now returns
null (i.e. unsure), which is correct because it is not possible to
determine whether a block applies to editing a particular page
without knowing what that page is, and appliesToRight doesn't know
that page.
There are some flags on sitewide blocks that predate partial blocks,
which block particular actions: 'createaccount' and 'sendemail'.
These are still handled in AbstractBlock::appliesToRight, and are
still checked for separately in the peripheral components.
The feature flag $wgEnablePartialActionBlocks must set to true to
enable partial action blocks.
Bug: T279556
Bug: T6995
Change-Id: I17962bb7c4247a12c722e7bc6bcaf8c36efd8600
2021-04-26 23:07:17 +00:00
|
|
|
if (
|
|
|
|
|
$this->isBlockedFrom( $user, $page, $useReplica ) ||
|
|
|
|
|
(
|
|
|
|
|
$this->options->get( 'EnablePartialActionBlocks' ) &&
|
|
|
|
|
$block->appliesToRight( $action )
|
|
|
|
|
)
|
|
|
|
|
) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// @todo FIXME: Pass the relevant context into this function.
|
2019-09-20 15:03:48 +00:00
|
|
|
$context = RequestContext::getMain();
|
|
|
|
|
$message = $this->blockErrorFormatter->getMessage(
|
|
|
|
|
$block,
|
|
|
|
|
$context->getUser(),
|
|
|
|
|
$context->getLanguage(),
|
|
|
|
|
$context->getRequest()->getIP()
|
|
|
|
|
);
|
|
|
|
|
$errors[] = array_merge( [ $message->getKey() ], $message->getParams() );
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Permissions checks that fail most often, and which are easiest to test.
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkQuickPermissions(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// TODO: remove when LinkTarget usage will expand further
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
2019-03-07 20:02:07 +00:00
|
|
|
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
if ( !$this->hookRunner->onTitleQuickPermissions( $title, $user, $action,
|
|
|
|
|
$errors, $rigor !== self::RIGOR_QUICK, $short )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-08 14:01:01 +00:00
|
|
|
$isSubPage = $this->nsInfo->hasSubpages( $title->getNamespace() ) ?
|
|
|
|
|
strpos( $title->getText(), '/' ) !== false : false;
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
if ( $action == 'create' ) {
|
|
|
|
|
if (
|
2019-08-08 14:01:01 +00:00
|
|
|
( $this->nsInfo->isTalk( $title->getNamespace() ) &&
|
2019-08-16 18:13:56 +00:00
|
|
|
!$this->userHasRight( $user, 'createtalk' ) ) ||
|
2019-08-08 14:01:01 +00:00
|
|
|
( !$this->nsInfo->isTalk( $title->getNamespace() ) &&
|
2019-08-16 18:13:56 +00:00
|
|
|
!$this->userHasRight( $user, 'createpage' ) )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
$errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $action == 'move' ) {
|
2019-08-16 18:13:56 +00:00
|
|
|
if ( !$this->userHasRight( $user, 'move-rootuserpages' )
|
2020-07-22 17:29:48 +00:00
|
|
|
&& $title->getNamespace() === NS_USER && !$isSubPage ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// Show user page-specific message only if the user can move other pages
|
|
|
|
|
$errors[] = [ 'cant-move-user-page' ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if user is allowed to move files if it's a file
|
2020-07-22 17:29:48 +00:00
|
|
|
if ( $title->getNamespace() === NS_FILE &&
|
2019-08-16 18:13:56 +00:00
|
|
|
!$this->userHasRight( $user, 'movefile' ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors[] = [ 'movenotallowedfile' ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if user is allowed to move category pages if it's a category page
|
2020-07-22 17:29:48 +00:00
|
|
|
if ( $title->getNamespace() === NS_CATEGORY &&
|
2019-08-16 18:13:56 +00:00
|
|
|
!$this->userHasRight( $user, 'move-categorypages' ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors[] = [ 'cant-move-category-page' ];
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-16 18:13:56 +00:00
|
|
|
if ( !$this->userHasRight( $user, 'move' ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// User can't move anything
|
2021-01-12 04:48:49 +00:00
|
|
|
$userCanMove = $this->groupHasPermission( 'user', 'move' );
|
|
|
|
|
$autoconfirmedCanMove = $this->groupHasPermission( 'autoconfirmed', 'move' );
|
2019-03-07 20:02:07 +00:00
|
|
|
if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
|
|
|
|
|
// custom message if logged-in users without any special rights can move
|
|
|
|
|
$errors[] = [ 'movenologintext' ];
|
|
|
|
|
} else {
|
|
|
|
|
$errors[] = [ 'movenotallowed' ];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $action == 'move-target' ) {
|
2019-08-16 18:13:56 +00:00
|
|
|
if ( !$this->userHasRight( $user, 'move' ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// User can't move anything
|
|
|
|
|
$errors[] = [ 'movenotallowed' ];
|
2019-08-16 18:13:56 +00:00
|
|
|
} elseif ( !$this->userHasRight( $user, 'move-rootuserpages' )
|
2020-07-22 17:29:48 +00:00
|
|
|
&& $title->getNamespace() === NS_USER
|
2020-06-27 01:13:01 +00:00
|
|
|
&& !$isSubPage
|
|
|
|
|
) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// Show user page-specific message only if the user can move other pages
|
|
|
|
|
$errors[] = [ 'cant-move-to-user-page' ];
|
2019-08-16 18:13:56 +00:00
|
|
|
} elseif ( !$this->userHasRight( $user, 'move-categorypages' )
|
2020-07-22 17:29:48 +00:00
|
|
|
&& $title->getNamespace() === NS_CATEGORY
|
2020-06-27 01:13:01 +00:00
|
|
|
) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// Show category page-specific message only if the user can move other pages
|
|
|
|
|
$errors[] = [ 'cant-move-to-category-page' ];
|
|
|
|
|
}
|
2019-08-16 18:13:56 +00:00
|
|
|
} elseif ( !$this->userHasRight( $user, $action ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors[] = $this->missingPermissionError( $action, $short );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check against page_restrictions table requirements on this
|
|
|
|
|
* page. The user must possess all required rights for this
|
|
|
|
|
* action.
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkPageRestrictions(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// TODO: remove & rework upon further use of LinkTarget
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
|
|
|
|
foreach ( $title->getRestrictions( $action ) as $right ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// Backwards compatibility, rewrite sysop -> editprotected
|
|
|
|
|
if ( $right == 'sysop' ) {
|
|
|
|
|
$right = 'editprotected';
|
|
|
|
|
}
|
|
|
|
|
// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
|
|
|
|
|
if ( $right == 'autoconfirmed' ) {
|
|
|
|
|
$right = 'editsemiprotected';
|
|
|
|
|
}
|
|
|
|
|
if ( $right == '' ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2019-08-16 18:13:56 +00:00
|
|
|
if ( !$this->userHasRight( $user, $right ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors[] = [ 'protectedpagetext', $right, $action ];
|
2019-08-16 18:13:56 +00:00
|
|
|
} elseif ( $title->areRestrictionsCascading() &&
|
2020-06-27 01:13:01 +00:00
|
|
|
!$this->userHasRight( $user, 'protect' )
|
|
|
|
|
) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors[] = [ 'protectedpagetext', 'protect', $action ];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check restrictions on cascading pages.
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
2019-08-21 22:42:08 +00:00
|
|
|
* @param UserIdentity $user User to check
|
2019-03-07 20:02:07 +00:00
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkCascadingSourcesRestrictions(
|
|
|
|
|
$action,
|
2019-08-21 22:42:08 +00:00
|
|
|
UserIdentity $user,
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// TODO: remove & rework upon further use of LinkTarget
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
|
|
|
|
if ( $rigor !== self::RIGOR_QUICK && !$title->isUserConfigPage() ) {
|
|
|
|
|
list( $cascadingSources, $restrictions ) = $title->getCascadeProtectionSources();
|
2019-03-07 20:02:07 +00:00
|
|
|
# Cascading protection depends on more than this page...
|
|
|
|
|
# Several cascading protected pages may include this page...
|
|
|
|
|
# Check each cascading level
|
|
|
|
|
# This is only for protection restrictions, not for all actions
|
|
|
|
|
if ( isset( $restrictions[$action] ) ) {
|
|
|
|
|
foreach ( $restrictions[$action] as $right ) {
|
|
|
|
|
// Backwards compatibility, rewrite sysop -> editprotected
|
|
|
|
|
if ( $right == 'sysop' ) {
|
|
|
|
|
$right = 'editprotected';
|
|
|
|
|
}
|
|
|
|
|
// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
|
|
|
|
|
if ( $right == 'autoconfirmed' ) {
|
|
|
|
|
$right = 'editsemiprotected';
|
|
|
|
|
}
|
2019-08-21 22:42:08 +00:00
|
|
|
if ( $right != '' && !$this->userHasAllRights( $user, 'protect', $right ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$wikiPages = '';
|
2019-04-09 06:58:04 +00:00
|
|
|
/** @var Title $wikiPage */
|
2019-03-07 20:02:07 +00:00
|
|
|
foreach ( $cascadingSources as $wikiPage ) {
|
|
|
|
|
$wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
|
|
|
|
|
}
|
|
|
|
|
$errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check action permissions not already checked in checkQuickPermissions
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkActionPermissions(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
2021-03-29 22:32:07 +00:00
|
|
|
global $wgLang;
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
// TODO: remove & rework upon further use of LinkTarget
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
if ( $action == 'protect' ) {
|
2019-08-08 14:01:01 +00:00
|
|
|
if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// If they can't edit, they shouldn't protect.
|
|
|
|
|
$errors[] = [ 'protect-cantedit' ];
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $action == 'create' ) {
|
2019-08-08 14:01:01 +00:00
|
|
|
$title_protection = $title->getTitleProtection();
|
2019-03-07 20:02:07 +00:00
|
|
|
if ( $title_protection ) {
|
|
|
|
|
if ( $title_protection['permission'] == ''
|
2019-08-16 18:13:56 +00:00
|
|
|
|| !$this->userHasRight( $user, $title_protection['permission'] )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
$errors[] = [
|
|
|
|
|
'titleprotected',
|
2020-10-20 17:34:25 +00:00
|
|
|
$this->userCache->getProp( $title_protection['user'], 'name' ),
|
2019-03-07 20:02:07 +00:00
|
|
|
$title_protection['reason']
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $action == 'move' ) {
|
|
|
|
|
// Check for immobile pages
|
2019-08-08 14:01:01 +00:00
|
|
|
if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// Specific message for this case
|
2020-04-11 00:49:04 +00:00
|
|
|
$nsText = $title->getNsText();
|
|
|
|
|
if ( $nsText === '' ) {
|
|
|
|
|
$nsText = wfMessage( 'blanknamespace' )->text();
|
|
|
|
|
}
|
|
|
|
|
$errors[] = [ 'immobile-source-namespace', $nsText ];
|
2019-08-08 14:01:01 +00:00
|
|
|
} elseif ( !$title->isMovable() ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// Less specific message for rarer cases
|
|
|
|
|
$errors[] = [ 'immobile-source-page' ];
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $action == 'move-target' ) {
|
2019-08-08 14:01:01 +00:00
|
|
|
if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
|
2020-04-11 00:49:04 +00:00
|
|
|
$nsText = $title->getNsText();
|
|
|
|
|
if ( $nsText === '' ) {
|
|
|
|
|
$nsText = wfMessage( 'blanknamespace' )->text();
|
|
|
|
|
}
|
|
|
|
|
$errors[] = [ 'immobile-target-namespace', $nsText ];
|
2019-08-08 14:01:01 +00:00
|
|
|
} elseif ( !$title->isMovable() ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors[] = [ 'immobile-target-page' ];
|
|
|
|
|
}
|
Add `delete-redirect` for deleting single-rev redirects during moves
A new user right, `delete-redirect`, is added (not given to anyone
by default). At Special:MovePage, if attempting to move to a single
revision redirect that would otherwise be an invalid target (i.e.
doesn't point to the source page), the user is able to delete the
target.
Deletions are logged as `delete/delete_redir2`, and the move is
then logged normally as `move/move`, mirroring current delete and
move logging.
To allow for separate handling by Special:MovePage,
MovePage::isValidMove now returns a fatal status `redirectexists` if
the target isn't valid but passes Title::isSingleRevRedirect.
Otherwise, `articleexists` is returned (as previously). Other callers
that don't intend to treat single revision redirects differently
should treat `redirectexists` the same as `articleexists`.
Currently, this deletion (like normal delete and move) cannot be
done through the move api. Since the deletion is only valid when
moving a page, unlike for normal deletion, deleting redirects with
this right cannot be done via the delete api either.
Bug: T239277
Change-Id: I36c8df0a12d326ae07018046541bd00103936144
2019-12-19 23:13:31 +00:00
|
|
|
} elseif ( $action == 'delete' || $action == 'delete-redirect' ) {
|
2019-08-08 14:01:01 +00:00
|
|
|
$tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $title );
|
2019-03-07 20:02:07 +00:00
|
|
|
if ( !$tempErrors ) {
|
|
|
|
|
$tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
|
2019-08-08 14:01:01 +00:00
|
|
|
$user, $tempErrors, $rigor, true, $title );
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
if ( $tempErrors ) {
|
|
|
|
|
// If protection keeps them from editing, they shouldn't be able to delete.
|
|
|
|
|
$errors[] = [ 'deleteprotected' ];
|
|
|
|
|
}
|
2021-03-29 22:32:07 +00:00
|
|
|
if ( $rigor !== self::RIGOR_QUICK
|
|
|
|
|
&& $action == 'delete'
|
|
|
|
|
&& $this->options->get( 'DeleteRevisionsLimit' )
|
|
|
|
|
&& !$this->userCan( 'bigdelete', $user, $title )
|
|
|
|
|
&& $title->isBigDeletion()
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
2021-03-29 22:32:07 +00:00
|
|
|
$errors[] = [
|
|
|
|
|
'delete-toobig',
|
|
|
|
|
$wgLang->formatNum( $this->options->get( 'DeleteRevisionsLimit' ) )
|
|
|
|
|
];
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
} elseif ( $action === 'undelete' ) {
|
2019-08-08 14:01:01 +00:00
|
|
|
if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// Undeleting implies editing
|
|
|
|
|
$errors[] = [ 'undelete-cantedit' ];
|
|
|
|
|
}
|
2019-08-08 14:01:01 +00:00
|
|
|
if ( !$title->exists()
|
|
|
|
|
&& count( $this->getPermissionErrorsInternal( 'create', $user, $title, $rigor, true ) )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
// Undeleting where nothing currently exists implies creating
|
|
|
|
|
$errors[] = [ 'undelete-cantcreate' ];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check permissions on special pages & namespaces
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
2019-08-23 15:33:21 +00:00
|
|
|
* @param UserIdentity $user User to check
|
2019-03-07 20:02:07 +00:00
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkSpecialsAndNSPermissions(
|
|
|
|
|
$action,
|
2019-08-23 15:33:21 +00:00
|
|
|
UserIdentity $user,
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// TODO: remove & rework upon further use of LinkTarget
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
# Only 'createaccount' can be performed on special pages,
|
|
|
|
|
# which don't actually exist in the DB.
|
2020-07-22 17:29:48 +00:00
|
|
|
if ( $title->getNamespace() === NS_SPECIAL && $action !== 'createaccount' ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors[] = [ 'ns-specialprotected' ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Check $wgNamespaceProtection for restricted namespaces
|
2019-08-23 15:33:21 +00:00
|
|
|
if ( $this->isNamespaceProtected( $title->getNamespace(), $user ) ) {
|
2020-07-22 17:29:48 +00:00
|
|
|
$ns = $title->getNamespace() === NS_MAIN ?
|
2019-08-08 14:01:01 +00:00
|
|
|
wfMessage( 'nstab-main' )->text() : $title->getNsText();
|
2020-07-22 17:29:48 +00:00
|
|
|
$errors[] = $title->getNamespace() === NS_MEDIAWIKI ?
|
2019-03-07 20:02:07 +00:00
|
|
|
[ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check sitewide CSS/JSON/JS permissions
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
|
|
|
|
* @param User $user User to check
|
|
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkSiteConfigPermissions(
|
|
|
|
|
$action,
|
|
|
|
|
User $user,
|
|
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// TODO: remove & rework upon further use of LinkTarget
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
2019-03-07 20:02:07 +00:00
|
|
|
|
2020-10-15 21:37:09 +00:00
|
|
|
if ( $action === 'patrol' ) {
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 `getPermissionErrorsInternal` as the checks
|
|
|
|
|
// for skipping `checkUserConfigPermissions` since normal admins can delete
|
|
|
|
|
// user scripts, but not sitedwide scripts
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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' ) ) {
|
|
|
|
|
$errors[] = [ 'sitecssprotected', $action ];
|
|
|
|
|
} elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) {
|
|
|
|
|
$errors[] = [ 'sitejsonprotected', $action ];
|
|
|
|
|
} elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) {
|
|
|
|
|
$errors[] = [ 'sitejsprotected', $action ];
|
|
|
|
|
}
|
|
|
|
|
if ( $title->isRawHtmlMessage() && !$this->userCanEditRawHtmlPage( $user ) ) {
|
|
|
|
|
$errors[] = [ 'siterawhtmlprotected', $action ];
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check CSS/JSON/JS sub-page permissions
|
|
|
|
|
*
|
|
|
|
|
* @param string $action The action to check
|
2019-08-21 22:42:08 +00:00
|
|
|
* @param UserIdentity $user User to check
|
2019-03-07 20:02:07 +00:00
|
|
|
* @param array $errors List of current errors
|
|
|
|
|
* @param string $rigor One of PermissionManager::RIGOR_ constants
|
|
|
|
|
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
|
|
|
|
|
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
|
|
|
|
|
* - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
|
|
|
|
|
* @param bool $short Short circuit on first error
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $page
|
|
|
|
|
*
|
|
|
|
|
* @return array List of errors
|
|
|
|
|
*/
|
|
|
|
|
private function checkUserConfigPermissions(
|
|
|
|
|
$action,
|
2019-08-21 22:42:08 +00:00
|
|
|
UserIdentity $user,
|
2019-03-07 20:02:07 +00:00
|
|
|
$errors,
|
|
|
|
|
$rigor,
|
|
|
|
|
$short,
|
|
|
|
|
LinkTarget $page
|
|
|
|
|
) {
|
|
|
|
|
// TODO: remove & rework upon further use of LinkTarget
|
2019-08-08 14:01:01 +00:00
|
|
|
$title = Title::newFromLinkTarget( $page );
|
2019-03-07 20:02:07 +00:00
|
|
|
|
|
|
|
|
# Protect css/json/js subpages of user pages
|
|
|
|
|
# XXX: this might be better using restrictions
|
2019-08-08 14:01:01 +00:00
|
|
|
if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $title->getText() ) ) {
|
2019-03-07 20:02:07 +00:00
|
|
|
// Users need editmyuser* to edit their own CSS/JSON/JS subpages.
|
|
|
|
|
if (
|
2019-08-08 14:01:01 +00:00
|
|
|
$title->isUserCssConfigPage()
|
2019-08-21 22:42:08 +00:00
|
|
|
&& !$this->userHasAnyRight( $user, 'editmyusercss', 'editusercss' )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
$errors[] = [ 'mycustomcssprotected', $action ];
|
|
|
|
|
} elseif (
|
2019-08-08 14:01:01 +00:00
|
|
|
$title->isUserJsonConfigPage()
|
2019-08-21 22:42:08 +00:00
|
|
|
&& !$this->userHasAnyRight( $user, 'editmyuserjson', 'edituserjson' )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
$errors[] = [ 'mycustomjsonprotected', $action ];
|
|
|
|
|
} elseif (
|
2019-08-08 14:01:01 +00:00
|
|
|
$title->isUserJsConfigPage()
|
2019-08-21 22:42:08 +00:00
|
|
|
&& !$this->userHasAnyRight( $user, 'editmyuserjs', 'edituserjs' )
|
2019-03-07 20:02:07 +00:00
|
|
|
) {
|
|
|
|
|
$errors[] = [ 'mycustomjsprotected', $action ];
|
2018-11-01 23:29:22 +00:00
|
|
|
} elseif (
|
2019-08-08 14:01:01 +00:00
|
|
|
$title->isUserJsConfigPage()
|
2019-08-21 22:42:08 +00:00
|
|
|
&& !$this->userHasAnyRight( $user, 'edituserjs', 'editmyuserjsredirect' )
|
2018-11-01 23:29:22 +00:00
|
|
|
) {
|
|
|
|
|
// T207750 - do not allow users to edit a redirect if they couldn't edit the target
|
2019-08-08 14:01:01 +00:00
|
|
|
$rev = $this->revisionLookup->getRevisionByTitle( $title );
|
2018-11-01 23:29:22 +00:00
|
|
|
$content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null;
|
|
|
|
|
$target = $content ? $content->getUltimateRedirectTarget() : null;
|
|
|
|
|
if ( $target && (
|
|
|
|
|
!$target->inNamespace( NS_USER )
|
|
|
|
|
|| !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
|
|
|
|
|
) ) {
|
|
|
|
|
$errors[] = [ 'mycustomjsredirectprotected', $action ];
|
|
|
|
|
}
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
2020-10-15 21:37:09 +00:00
|
|
|
// 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 `getPermissionErrorsInternal` and this method isn't called.
|
|
|
|
|
if (
|
|
|
|
|
$title->isUserCssConfigPage()
|
|
|
|
|
&& !$this->userHasRight( $user, 'editusercss' )
|
|
|
|
|
) {
|
|
|
|
|
$errors[] = [ 'customcssprotected', $action ];
|
|
|
|
|
} elseif (
|
|
|
|
|
$title->isUserJsonConfigPage()
|
|
|
|
|
&& !$this->userHasRight( $user, 'edituserjson' )
|
|
|
|
|
) {
|
|
|
|
|
$errors[] = [ 'customjsonprotected', $action ];
|
|
|
|
|
} elseif (
|
|
|
|
|
$title->isUserJsConfigPage()
|
|
|
|
|
&& !$this->userHasRight( $user, 'edituserjs' )
|
|
|
|
|
) {
|
|
|
|
|
$errors[] = [ 'customjsprotected', $action ];
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-09 06:58:04 +00:00
|
|
|
/**
|
|
|
|
|
* Testing a permission
|
|
|
|
|
*
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*
|
|
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @param string $action
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function userHasRight( UserIdentity $user, $action = '' ) {
|
|
|
|
|
if ( $action === '' ) {
|
|
|
|
|
return true; // In the spirit of DWIM
|
|
|
|
|
}
|
|
|
|
|
// Use strict parameter to avoid matching numeric 0 accidentally inserted
|
|
|
|
|
// by misconfiguration: 0 == 'foo'
|
|
|
|
|
return in_array( $action, $this->getUserPermissions( $user ), true );
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 22:42:08 +00:00
|
|
|
/**
|
|
|
|
|
* Check if user is allowed to make any action
|
|
|
|
|
*
|
|
|
|
|
* @param UserIdentity $user
|
2019-10-05 16:03:24 +00:00
|
|
|
* @param string ...$actions
|
2019-08-21 22:42:08 +00:00
|
|
|
* @return bool True if user is allowed to perform *any* of the given actions
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*/
|
2019-10-05 16:03:24 +00:00
|
|
|
public function userHasAnyRight( UserIdentity $user, ...$actions ) {
|
2019-08-21 22:42:08 +00:00
|
|
|
foreach ( $actions as $action ) {
|
|
|
|
|
if ( $this->userHasRight( $user, $action ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if user is allowed to make all actions
|
|
|
|
|
*
|
|
|
|
|
* @param UserIdentity $user
|
2019-10-05 16:03:24 +00:00
|
|
|
* @param string ...$actions
|
2019-08-21 22:42:08 +00:00
|
|
|
* @return bool True if user is allowed to perform *all* of the given actions
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*/
|
2019-10-05 16:03:24 +00:00
|
|
|
public function userHasAllRights( UserIdentity $user, ...$actions ) {
|
2019-08-21 22:42:08 +00:00
|
|
|
foreach ( $actions as $action ) {
|
|
|
|
|
if ( !$this->userHasRight( $user, $action ) ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-09 06:58:04 +00:00
|
|
|
/**
|
|
|
|
|
* Get the permissions this user has.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*
|
|
|
|
|
* @param UserIdentity $user
|
|
|
|
|
*
|
|
|
|
|
* @return string[] permission names
|
|
|
|
|
*/
|
|
|
|
|
public function getUserPermissions( UserIdentity $user ) {
|
2019-08-20 20:59:49 +00:00
|
|
|
$rightsCacheKey = $this->getRightsCacheKey( $user );
|
|
|
|
|
if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) {
|
2021-05-22 03:34:24 +00:00
|
|
|
$userObj = User::newFromIdentity( $user );
|
2021-01-12 04:48:49 +00:00
|
|
|
$this->usersRights[ $rightsCacheKey ] = $this->getGroupPermissions(
|
|
|
|
|
$this->userGroupManager->getUserEffectiveGroups( $user )
|
2019-04-09 06:58:04 +00:00
|
|
|
);
|
2021-05-22 03:34:24 +00:00
|
|
|
// Hook requires a full User object
|
|
|
|
|
$this->hookRunner->onUserGetRights( $userObj, $this->usersRights[ $rightsCacheKey ] );
|
2019-04-09 06:58:04 +00:00
|
|
|
|
|
|
|
|
// Deny any rights denied by the user's session, unless this
|
|
|
|
|
// endpoint has no sessions.
|
|
|
|
|
if ( !defined( 'MW_NO_SESSION' ) ) {
|
2021-05-22 03:34:24 +00:00
|
|
|
// FIXME: $userObj->getRequest().. need to be replaced with something else
|
|
|
|
|
$allowedRights = $userObj->getRequest()->getSession()->getAllowedUserRights();
|
2019-04-09 06:58:04 +00:00
|
|
|
if ( $allowedRights !== null ) {
|
2019-08-20 20:59:49 +00:00
|
|
|
$this->usersRights[ $rightsCacheKey ] = array_intersect(
|
|
|
|
|
$this->usersRights[ $rightsCacheKey ],
|
2019-04-09 06:58:04 +00:00
|
|
|
$allowedRights
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-22 03:34:24 +00:00
|
|
|
// Hook requires a full User object
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
$this->hookRunner->onUserGetRightsRemove(
|
2021-05-22 03:34:24 +00:00
|
|
|
$userObj, $this->usersRights[ $rightsCacheKey ] );
|
2019-04-09 06:58:04 +00:00
|
|
|
// Force reindexation of rights when a hook has unset one of them
|
2019-08-20 20:59:49 +00:00
|
|
|
$this->usersRights[ $rightsCacheKey ] = array_values(
|
|
|
|
|
array_unique( $this->usersRights[ $rightsCacheKey ] )
|
2019-04-09 06:58:04 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
2021-05-22 03:34:24 +00:00
|
|
|
$userObj->isRegistered() &&
|
2019-08-21 05:28:47 +00:00
|
|
|
$this->options->get( 'BlockDisablesLogin' ) &&
|
2021-05-22 03:34:24 +00:00
|
|
|
$userObj->getBlock()
|
2019-04-09 06:58:04 +00:00
|
|
|
) {
|
|
|
|
|
$anon = new User;
|
2019-08-20 20:59:49 +00:00
|
|
|
$this->usersRights[ $rightsCacheKey ] = array_intersect(
|
|
|
|
|
$this->usersRights[ $rightsCacheKey ],
|
2019-04-09 06:58:04 +00:00
|
|
|
$this->getUserPermissions( $anon )
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-20 20:59:49 +00:00
|
|
|
$rights = $this->usersRights[ $rightsCacheKey ];
|
2019-07-11 17:22:20 +00:00
|
|
|
foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) {
|
|
|
|
|
$rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
|
|
|
|
|
}
|
|
|
|
|
return $rights;
|
2019-04-09 06:58:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clears users permissions cache, if specific user is provided it tries to clear
|
|
|
|
|
* permissions cache only for provided user.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*
|
2019-10-24 03:14:31 +00:00
|
|
|
* @param UserIdentity|null $user
|
2019-04-09 06:58:04 +00:00
|
|
|
*/
|
|
|
|
|
public function invalidateUsersRightsCache( $user = null ) {
|
|
|
|
|
if ( $user !== null ) {
|
2019-08-20 20:59:49 +00:00
|
|
|
$rightsCacheKey = $this->getRightsCacheKey( $user );
|
2020-11-10 15:52:14 +00:00
|
|
|
unset( $this->usersRights[ $rightsCacheKey ] );
|
2019-04-09 06:58:04 +00:00
|
|
|
} else {
|
|
|
|
|
$this->usersRights = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-20 20:59:49 +00:00
|
|
|
/**
|
|
|
|
|
* Gets a unique key for user rights cache.
|
|
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function getRightsCacheKey( UserIdentity $user ) {
|
|
|
|
|
return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-09 06:58:04 +00:00
|
|
|
/**
|
|
|
|
|
* Check, if the given group has the given permission
|
|
|
|
|
*
|
|
|
|
|
* If you're wanting to check whether all users have a permission, use
|
|
|
|
|
* PermissionManager::isEveryoneAllowed() instead. That properly checks if it's revoked
|
|
|
|
|
* from anyone.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.34
|
2021-01-05 23:08:09 +00:00
|
|
|
* @deprecated since 1.36 Use GroupPermissionLookup instead
|
2019-04-09 06:58:04 +00:00
|
|
|
*
|
|
|
|
|
* @param string $group Group to check
|
|
|
|
|
* @param string $role Role to check
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function groupHasPermission( $group, $role ) {
|
2021-01-05 23:08:09 +00:00
|
|
|
return $this->groupPermissionLookup->groupHasPermission( $group, $role );
|
2019-04-09 06:58:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the permissions associated with a given list of groups
|
|
|
|
|
*
|
|
|
|
|
* @since 1.34
|
2021-01-05 23:08:09 +00:00
|
|
|
* @deprecated since 1.36 Use GroupPermissionLookup instead
|
2019-04-09 06:58:04 +00:00
|
|
|
*
|
2020-10-28 10:01:33 +00:00
|
|
|
* @param string[] $groups internal group names
|
|
|
|
|
* @return string[] permission key names for given groups combined
|
2019-04-09 06:58:04 +00:00
|
|
|
*/
|
|
|
|
|
public function getGroupPermissions( $groups ) {
|
2021-01-12 04:48:49 +00:00
|
|
|
return $this->groupPermissionLookup->getGroupPermissions( $groups );
|
2019-04-09 06:58:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all the groups who have a given permission
|
|
|
|
|
*
|
|
|
|
|
* @since 1.34
|
2021-01-05 23:08:09 +00:00
|
|
|
* @deprecated since 1.36, use GroupPermissionLookup instead.
|
2019-04-09 06:58:04 +00:00
|
|
|
*
|
|
|
|
|
* @param string $role Role to check
|
2020-10-28 10:01:33 +00:00
|
|
|
* @return string[] internal group names with the given permission
|
2019-04-09 06:58:04 +00:00
|
|
|
*/
|
|
|
|
|
public function getGroupsWithPermission( $role ) {
|
2021-01-05 23:08:09 +00:00
|
|
|
return $this->groupPermissionLookup->getGroupsWithPermission( $role );
|
2019-04-09 06:58:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
|
|
|
|
* @param string $right Right to check
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*/
|
|
|
|
|
public function isEveryoneAllowed( $right ) {
|
|
|
|
|
// 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];
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 05:28:47 +00:00
|
|
|
if ( !isset( $this->options->get( 'GroupPermissions' )['*'][$right] )
|
|
|
|
|
|| !$this->options->get( 'GroupPermissions' )['*'][$right] ) {
|
2019-04-09 06:58:04 +00:00
|
|
|
$this->cachedRights[$right] = false;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If it's revoked anywhere, then everyone doesn't have it
|
2019-08-21 05:28:47 +00:00
|
|
|
foreach ( $this->options->get( 'RevokePermissions' ) as $rights ) {
|
2019-04-09 06:58:04 +00:00
|
|
|
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
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
if ( !$this->hookRunner->onUserIsEveryoneAllowed( $right ) ) {
|
2019-04-09 06:58:04 +00:00
|
|
|
$this->cachedRights[$right] = false;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->cachedRights[$right] = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a list of all available permissions.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*
|
|
|
|
|
* @return string[] Array of permission names
|
|
|
|
|
*/
|
|
|
|
|
public function getAllPermissions() {
|
2019-08-30 16:01:28 +00:00
|
|
|
if ( $this->allRights === null ) {
|
2019-08-21 05:28:47 +00:00
|
|
|
if ( count( $this->options->get( 'AvailableRights' ) ) ) {
|
2019-04-09 06:58:04 +00:00
|
|
|
$this->allRights = array_unique( array_merge(
|
|
|
|
|
$this->coreRights,
|
2019-08-21 05:28:47 +00:00
|
|
|
$this->options->get( 'AvailableRights' )
|
2019-04-09 06:58:04 +00:00
|
|
|
) );
|
|
|
|
|
} else {
|
|
|
|
|
$this->allRights = $this->coreRights;
|
|
|
|
|
}
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
$this->hookRunner->onUserGetAllRights( $this->allRights );
|
2019-04-09 06:58:04 +00:00
|
|
|
}
|
|
|
|
|
return $this->allRights;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-23 15:33:21 +00:00
|
|
|
/**
|
|
|
|
|
* Determines if $user is unable to edit pages in namespace because it has been protected.
|
2019-11-23 22:28:57 +00:00
|
|
|
* @param int $index
|
2019-08-23 15:33:21 +00:00
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
private function isNamespaceProtected( $index, UserIdentity $user ) {
|
|
|
|
|
$namespaceProtection = $this->options->get( 'NamespaceProtection' );
|
|
|
|
|
if ( isset( $namespaceProtection[$index] ) ) {
|
|
|
|
|
return !$this->userHasAllRights( $user, ...(array)$namespaceProtection[$index] );
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 19:49:59 +00:00
|
|
|
/**
|
|
|
|
|
* Determine which restriction levels it makes sense to use in a namespace,
|
|
|
|
|
* optionally filtered by a user's rights.
|
|
|
|
|
*
|
2021-04-08 19:18:27 +00:00
|
|
|
* @param int $index Namespace ID (index) to check
|
2019-08-21 19:49:59 +00:00
|
|
|
* @param UserIdentity|null $user User to check
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function getNamespaceRestrictionLevels( $index, UserIdentity $user = null ) {
|
|
|
|
|
if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) {
|
|
|
|
|
// All levels are valid if there's no namespace restriction.
|
|
|
|
|
// But still filter by user, if necessary
|
|
|
|
|
$levels = $this->options->get( 'RestrictionLevels' );
|
|
|
|
|
if ( $user ) {
|
|
|
|
|
$levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
|
|
|
|
|
$right = $level;
|
|
|
|
|
if ( $right == 'sysop' ) {
|
|
|
|
|
$right = 'editprotected'; // BC
|
|
|
|
|
}
|
|
|
|
|
if ( $right == 'autoconfirmed' ) {
|
|
|
|
|
$right = 'editsemiprotected'; // BC
|
|
|
|
|
}
|
|
|
|
|
return $this->userHasRight( $user, $right );
|
|
|
|
|
} ) );
|
|
|
|
|
}
|
|
|
|
|
return $levels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// $wgNamespaceProtection can require one or more rights to edit the namespace, which
|
|
|
|
|
// may be satisfied by membership in multiple groups each giving a subset of those rights.
|
|
|
|
|
// A restriction level is redundant if, for any one of the namespace rights, all groups
|
|
|
|
|
// giving that right also give the restriction level's right. Or, conversely, a
|
|
|
|
|
// restriction level is not redundant if, for every namespace right, there's at least one
|
|
|
|
|
// group giving that right without the restriction level's right.
|
|
|
|
|
//
|
|
|
|
|
// First, for each right, get a list of groups with that right.
|
|
|
|
|
$namespaceRightGroups = [];
|
|
|
|
|
foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) {
|
|
|
|
|
if ( $right == 'sysop' ) {
|
|
|
|
|
$right = 'editprotected'; // BC
|
|
|
|
|
}
|
|
|
|
|
if ( $right == 'autoconfirmed' ) {
|
|
|
|
|
$right = 'editsemiprotected'; // BC
|
|
|
|
|
}
|
|
|
|
|
if ( $right != '' ) {
|
2021-01-12 04:48:49 +00:00
|
|
|
$namespaceRightGroups[$right] = $this->getGroupsWithPermission( $right );
|
2019-08-21 19:49:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now, go through the protection levels one by one.
|
|
|
|
|
$usableLevels = [ '' ];
|
|
|
|
|
foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) {
|
|
|
|
|
$right = $level;
|
|
|
|
|
if ( $right == 'sysop' ) {
|
|
|
|
|
$right = 'editprotected'; // BC
|
|
|
|
|
}
|
|
|
|
|
if ( $right == 'autoconfirmed' ) {
|
|
|
|
|
$right = 'editsemiprotected'; // BC
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $right != '' &&
|
|
|
|
|
!isset( $namespaceRightGroups[$right] ) &&
|
|
|
|
|
( !$user || $this->userHasRight( $user, $right ) )
|
|
|
|
|
) {
|
|
|
|
|
// Do any of the namespace rights imply the restriction right? (see explanation above)
|
|
|
|
|
foreach ( $namespaceRightGroups as $groups ) {
|
2021-01-12 04:48:49 +00:00
|
|
|
if ( !array_diff( $groups, $this->getGroupsWithPermission( $right ) ) ) {
|
2019-08-21 19:49:59 +00:00
|
|
|
// Yes, this one does.
|
|
|
|
|
continue 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// No, keep the restriction level
|
|
|
|
|
$usableLevels[] = $level;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $usableLevels;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-17 22:30:20 +00:00
|
|
|
/**
|
|
|
|
|
* 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 ) {
|
|
|
|
|
return $this->userHasAllRights( $user, 'editsitecss', 'editsitejs' );
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-11 17:22:20 +00:00
|
|
|
/**
|
|
|
|
|
* Add temporary user rights, only valid for the current 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.
|
|
|
|
|
* Returns a "scope guard" variable; whenever that variable goes out of scope or is consumed
|
|
|
|
|
* via ScopedCallback::consume(), the temporary rights are revoked.
|
2019-07-17 12:00:21 +00:00
|
|
|
*
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*
|
2019-07-11 17:22:20 +00:00
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @param string|string[] $rights
|
|
|
|
|
* @return ScopedCallback
|
|
|
|
|
*/
|
|
|
|
|
public function addTemporaryUserRights( UserIdentity $user, $rights ) {
|
2019-07-17 12:00:21 +00:00
|
|
|
$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] );
|
|
|
|
|
} );
|
2019-07-11 17:22:20 +00:00
|
|
|
}
|
|
|
|
|
|
2019-04-09 06:58:04 +00:00
|
|
|
/**
|
|
|
|
|
* Overrides user permissions cache
|
|
|
|
|
*
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*
|
|
|
|
|
* @param User $user
|
|
|
|
|
* @param string[]|string $rights
|
|
|
|
|
*
|
|
|
|
|
* @throws Exception
|
|
|
|
|
*/
|
|
|
|
|
public function overrideUserRightsForTesting( $user, $rights = [] ) {
|
|
|
|
|
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
|
|
|
|
|
throw new Exception( __METHOD__ . ' can not be called outside of tests' );
|
|
|
|
|
}
|
2019-08-20 20:59:49 +00:00
|
|
|
$this->usersRights[ $this->getRightsCacheKey( $user ) ] =
|
|
|
|
|
is_array( $rights ) ? $rights : [ $rights ];
|
2019-04-09 06:58:04 +00:00
|
|
|
}
|
|
|
|
|
|
2019-03-07 20:02:07 +00:00
|
|
|
}
|