Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1426802
PermissionManager.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
63 KB
Referenced Files
None
Subscribers
None
PermissionManager.php
View Options
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace
MediaWiki\Permissions
;
use
InvalidArgumentException
;
use
LogicException
;
use
MediaWiki\Actions\ActionFactory
;
use
MediaWiki\Block\AbstractBlock
;
use
MediaWiki\Block\Block
;
use
MediaWiki\Block\BlockErrorFormatter
;
use
MediaWiki\Block\BlockManager
;
use
MediaWiki\Config\ServiceOptions
;
use
MediaWiki\Context\IContextSource
;
use
MediaWiki\Context\RequestContext
;
use
MediaWiki\HookContainer\HookContainer
;
use
MediaWiki\HookContainer\HookRunner
;
use
MediaWiki\Linker\LinkTarget
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\Message\Message
;
use
MediaWiki\Page\PageIdentity
;
use
MediaWiki\Page\PageReference
;
use
MediaWiki\Page\RedirectLookup
;
use
MediaWiki\Request\WebRequest
;
use
MediaWiki\Session\SessionManager
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\SpecialPage\SpecialPageFactory
;
use
MediaWiki\Title\NamespaceInfo
;
use
MediaWiki\Title\Title
;
use
MediaWiki\Title\TitleFormatter
;
use
MediaWiki\User\TempUser\TempUserConfig
;
use
MediaWiki\User\User
;
use
MediaWiki\User\UserFactory
;
use
MediaWiki\User\UserGroupManager
;
use
MediaWiki\User\UserGroupMembership
;
use
MediaWiki\User\UserIdentity
;
use
MediaWiki\User\UserIdentityLookup
;
use
PermissionsError
;
use
StatusValue
;
use
Wikimedia\Message\MessageSpecifier
;
use
Wikimedia\ScopedCallback
;
/**
* A service class for checking permissions
* To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager().
*
* @since 1.33
*/
class
PermissionManager
{
/** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */
public
const
RIGOR_QUICK
=
'quick'
;
/** @var string Does cheap and expensive checks possibly from a replica DB */
public
const
RIGOR_FULL
=
'full'
;
/** @var string Does cheap and expensive checks, using the primary DB as needed */
public
const
RIGOR_SECURE
=
'secure'
;
/**
* @internal For use by ServiceWiring
*/
public
const
CONSTRUCTOR_OPTIONS
=
[
MainConfigNames
::
WhitelistRead
,
MainConfigNames
::
WhitelistReadRegexp
,
MainConfigNames
::
EmailConfirmToEdit
,
MainConfigNames
::
BlockDisablesLogin
,
MainConfigNames
::
EnablePartialActionBlocks
,
MainConfigNames
::
GroupPermissions
,
MainConfigNames
::
RevokePermissions
,
MainConfigNames
::
AvailableRights
,
MainConfigNames
::
NamespaceProtection
,
MainConfigNames
::
RestrictionLevels
,
MainConfigNames
::
DeleteRevisionsLimit
,
MainConfigNames
::
RateLimits
,
MainConfigNames
::
ImplicitRights
,
];
private
ServiceOptions
$options
;
private
SpecialPageFactory
$specialPageFactory
;
private
NamespaceInfo
$nsInfo
;
private
GroupPermissionsLookup
$groupPermissionsLookup
;
private
UserGroupManager
$userGroupManager
;
private
BlockManager
$blockManager
;
private
BlockErrorFormatter
$blockErrorFormatter
;
private
HookRunner
$hookRunner
;
private
UserIdentityLookup
$userIdentityLookup
;
private
RedirectLookup
$redirectLookup
;
private
RestrictionStore
$restrictionStore
;
private
TitleFormatter
$titleFormatter
;
private
TempUserConfig
$tempUserConfig
;
private
UserFactory
$userFactory
;
private
ActionFactory
$actionFactory
;
/** @var string[]|null Cached results of getAllPermissions() */
private
$allRights
;
/** @var string[]|null Cached results of getImplicitRights() */
private
$implicitRights
;
/** @var string[][] Cached user rights */
private
$usersRights
=
[];
/**
* Temporary user rights, valid for the current request only.
* @var string[][][] userid => override group => rights
*/
private
$temporaryUserRights
=
[];
/** @var bool[] Cached rights for isEveryoneAllowed, [ right => allowed ] */
private
$cachedRights
=
[];
/**
* Array of core rights.
* Each of these should have a corresponding message of the form
* "right-$right".
* @showinitializer
*/
private
const
CORE_RIGHTS
=
[
'apihighlimits'
,
'applychangetags'
,
'autoconfirmed'
,
'autocreateaccount'
,
'autopatrol'
,
'bigdelete'
,
'block'
,
'blockemail'
,
'bot'
,
'browsearchive'
,
'changetags'
,
'createaccount'
,
'createpage'
,
'createtalk'
,
'delete'
,
'delete-redirect'
,
'deletechangetags'
,
'deletedhistory'
,
'deletedtext'
,
'deletelogentry'
,
'deleterevision'
,
'edit'
,
'editcontentmodel'
,
'editinterface'
,
'editprotected'
,
'editmyoptions'
,
'editmyprivateinfo'
,
'editmyusercss'
,
'editmyuserjson'
,
'editmyuserjs'
,
'editmyuserjsredirect'
,
'editmywatchlist'
,
'editsemiprotected'
,
'editsitecss'
,
'editsitejson'
,
'editsitejs'
,
'editusercss'
,
'edituserjson'
,
'edituserjs'
,
'hideuser'
,
'import'
,
'importupload'
,
'ipblock-exempt'
,
'managechangetags'
,
'markbotedits'
,
'mergehistory'
,
'minoredit'
,
'move'
,
'movefile'
,
'move-categorypages'
,
'move-rootuserpages'
,
'move-subpages'
,
'nominornewtalk'
,
'noratelimit'
,
'override-export-depth'
,
'pagelang'
,
'patrol'
,
'patrolmarks'
,
'protect'
,
'read'
,
'renameuser'
,
'reupload'
,
'reupload-own'
,
'reupload-shared'
,
'rollback'
,
'sendemail'
,
'siteadmin'
,
'suppressionlog'
,
'suppressredirect'
,
'suppressrevision'
,
'unblockself'
,
'undelete'
,
'unwatchedpages'
,
'upload'
,
'upload_by_url'
,
'userrights'
,
'userrights-interwiki'
,
'viewmyprivateinfo'
,
'viewmywatchlist'
,
'viewsuppressed'
,
];
/**
* List of implicit rights.
* These should not have a corresponding message of the form
* "right-$right".
* @showinitializer
*/
private
const
CORE_IMPLICIT_RIGHTS
=
[
'renderfile'
,
'renderfile-nonstandard'
,
'stashedit'
,
'stashbasehtml'
,
'mailpassword'
,
'changeemail'
,
'confirmemail'
,
'linkpurge'
,
'purge'
,
];
public
function
__construct
(
ServiceOptions
$options
,
SpecialPageFactory
$specialPageFactory
,
NamespaceInfo
$nsInfo
,
GroupPermissionsLookup
$groupPermissionsLookup
,
UserGroupManager
$userGroupManager
,
BlockManager
$blockManager
,
BlockErrorFormatter
$blockErrorFormatter
,
HookContainer
$hookContainer
,
UserIdentityLookup
$userIdentityLookup
,
RedirectLookup
$redirectLookup
,
RestrictionStore
$restrictionStore
,
TitleFormatter
$titleFormatter
,
TempUserConfig
$tempUserConfig
,
UserFactory
$userFactory
,
ActionFactory
$actionFactory
)
{
$options
->
assertRequiredOptions
(
self
::
CONSTRUCTOR_OPTIONS
);
$this
->
options
=
$options
;
$this
->
specialPageFactory
=
$specialPageFactory
;
$this
->
nsInfo
=
$nsInfo
;
$this
->
groupPermissionsLookup
=
$groupPermissionsLookup
;
$this
->
userGroupManager
=
$userGroupManager
;
$this
->
blockManager
=
$blockManager
;
$this
->
blockErrorFormatter
=
$blockErrorFormatter
;
$this
->
hookRunner
=
new
HookRunner
(
$hookContainer
);
$this
->
userIdentityLookup
=
$userIdentityLookup
;
$this
->
redirectLookup
=
$redirectLookup
;
$this
->
restrictionStore
=
$restrictionStore
;
$this
->
titleFormatter
=
$titleFormatter
;
$this
->
tempUserConfig
=
$tempUserConfig
;
$this
->
userFactory
=
$userFactory
;
$this
->
actionFactory
=
$actionFactory
;
}
/**
* Can $user perform $action on a page?
*
* The method replaced Title::userCan()
* The $user parameter need to be superseded by UserIdentity value in future
* The $title parameter need to be superseded by PageIdentity value in future
*
* @param string $action
* @param User $user
* @param LinkTarget $page
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
*
* @return bool
*/
public
function
userCan
(
$action
,
User
$user
,
LinkTarget
$page
,
$rigor
=
self
::
RIGOR_SECURE
):
bool
{
return
$this
->
getPermissionStatus
(
$action
,
$user
,
$page
,
$rigor
,
true
)->
isGood
();
}
/**
* A convenience method for calling PermissionManager::userCan
* with PermissionManager::RIGOR_QUICK
*
* Suitable for use for nonessential UI controls in common cases, but
* _not_ for functional access control.
* May provide false positives, but should never provide a false negative.
*
* @see PermissionManager::userCan()
*
* @param string $action
* @param User $user
* @param LinkTarget $page
* @return bool
*/
public
function
quickUserCan
(
$action
,
User
$user
,
LinkTarget
$page
):
bool
{
return
$this
->
userCan
(
$action
,
$user
,
$page
,
self
::
RIGOR_QUICK
);
}
/**
* Can $user perform $action on a page?
*
* This *does not* check throttles (User::pingLimiter()). If that's desired, use the Authority
* interface methods instead.
*
* @deprecated since 1.43 Use getPermissionStatus() instead.
*
* @param string $action Action that permission needs to be checked for
* @param User $user User to check
* @param LinkTarget $page
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param string[] $ignoreErrors Set this to a list of message keys
* whose corresponding errors may be ignored.
*
* @return array[] Permission errors.
* Each entry contains valid arguments for wfMessage() / MessageLocalizer::msg().
* The format is *different* from the normal "legacy error array", as used by
* Status::getErrorsArray() or PermissionStatus::toLegacyErrorArray():
* the first element of each entry can be a MessageSpecifier, not just a string.
* @phan-return non-empty-array[]
*/
public
function
getPermissionErrors
(
$action
,
User
$user
,
LinkTarget
$page
,
$rigor
=
self
::
RIGOR_SECURE
,
$ignoreErrors
=
[]
):
array
{
$status
=
$this
->
getPermissionStatus
(
$action
,
$user
,
$page
,
$rigor
);
$result
=
[];
// Produce a result in the weird format used by this function
foreach
(
$status
->
getErrors
()
as
[
'message'
=>
$keyOrMsg
,
'params'
=>
$params
]
)
{
$key
=
$keyOrMsg
instanceof
MessageSpecifier
?
$keyOrMsg
->
getKey
()
:
$keyOrMsg
;
// Remove the errors being ignored.
if
(
!
in_array
(
$key
,
$ignoreErrors
)
)
{
$result
[]
=
[
$keyOrMsg
,
...
$params
];
}
}
return
$result
;
}
/**
* Like {@link getPermissionErrors}, but immediately throw if there are any errors.
*
* @param string $action Action that permission needs to be checked for
* @param User $user User to check
* @param LinkTarget $page
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param string[] $ignoreErrors Set this to a list of message keys
* whose corresponding errors may be ignored.
*
* @throws PermissionsError
*/
public
function
throwPermissionErrors
(
$action
,
User
$user
,
LinkTarget
$page
,
$rigor
=
self
::
RIGOR_SECURE
,
$ignoreErrors
=
[]
):
void
{
$status
=
$this
->
getPermissionStatus
(
$action
,
$user
,
$page
,
$rigor
);
if
(
$status
->
hasMessagesExcept
(
...
$ignoreErrors
)
)
{
throw
new
PermissionsError
(
$action
,
$status
);
}
}
/**
* Check if user is blocked from editing a particular article. If the user does not
* have a block, this will return false.
*
* @param User $user
* @param PageIdentity|LinkTarget $page Title to check
* @param bool $fromReplica Whether to check the replica DB instead of the primary DB
* @return bool
*/
public
function
isBlockedFrom
(
User
$user
,
$page
,
$fromReplica
=
false
):
bool
{
return
(
bool
)
$this
->
getApplicableBlock
(
'edit'
,
$user
,
$fromReplica
?
self
::
RIGOR_FULL
:
self
::
RIGOR_SECURE
,
$page
,
$user
->
getRequest
()
);
}
/**
* Can $user perform $action on a page?
*
* This *does not* check throttles (User::pingLimiter()). If that's desired, use the Authority
* interface methods instead.
*
* @param string $action Action that permission needs to be checked for
* @param User $user User to check
* @param LinkTarget $page
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Set this to true to stop after the first permission error.
* @return PermissionStatus Permission errors as a status.
* Check `$status->isGood()` to tell if the user can perform the action.
* Use `$status->getMessages()` to display errors if the status is not good.
*/
public
function
getPermissionStatus
(
$action
,
User
$user
,
LinkTarget
$page
,
$rigor
=
self
::
RIGOR_SECURE
,
$short
=
false
):
PermissionStatus
{
if
(
!
in_array
(
$rigor
,
[
self
::
RIGOR_QUICK
,
self
::
RIGOR_FULL
,
self
::
RIGOR_SECURE
]
)
)
{
throw
new
InvalidArgumentException
(
"Invalid rigor parameter '$rigor'."
);
}
// With RIGOR_QUICK we can assume automatic account creation will
// occur. At a higher rigor level, the caller is required to opt
// in by either passing in a temp placeholder user or by actually
// creating the account.
if
(
$rigor
===
self
::
RIGOR_QUICK
&&
!
$user
->
isRegistered
()
&&
$this
->
tempUserConfig
->
isAutoCreateAction
(
$action
)
)
{
$user
=
$this
->
userFactory
->
newTempPlaceholder
();
}
# Read has special handling
if
(
$action
===
'read'
)
{
$checks
=
[
[
$this
,
'checkPermissionHooks'
],
[
$this
,
'checkReadPermissions'
],
[
$this
,
'checkUserBlock'
],
// for wgBlockDisablesLogin
];
}
elseif
(
$action
===
'create'
)
{
# Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
# or checkUserConfigPermissions here as it will lead to duplicate
# error messages. This is okay to do since anywhere that checks for
# create will also check for edit, and those checks are called for edit.
$checks
=
[
[
$this
,
'checkQuickPermissions'
],
[
$this
,
'checkPermissionHooks'
],
[
$this
,
'checkPageRestrictions'
],
[
$this
,
'checkCascadingSourcesRestrictions'
],
[
$this
,
'checkActionPermissions'
],
[
$this
,
'checkUserBlock'
],
];
}
else
{
// Exclude checkUserConfigPermissions on actions that cannot change the
// content of the configuration pages.
$skipUserConfigActions
=
[
// Allow patrolling per T21818
'patrol'
,
// Allow admins and oversighters to delete. For user pages we want to avoid the
// situation where an unprivileged user can post abusive content on
// their subpages and only very highly privileged users could remove it.
// See T200176.
'delete'
,
'deleterevision'
,
'suppressrevision'
,
// Allow admins and oversighters to view deleted content, even if they
// cannot restore it. See T202989
'deletedhistory'
,
'deletedtext'
,
'viewsuppressed'
,
];
$checks
=
[
[
$this
,
'checkQuickPermissions'
],
[
$this
,
'checkPermissionHooks'
],
[
$this
,
'checkSpecialsAndNSPermissions'
],
[
$this
,
'checkSiteConfigPermissions'
],
];
if
(
!
in_array
(
$action
,
$skipUserConfigActions
,
true
)
)
{
$checks
[]
=
[
$this
,
'checkUserConfigPermissions'
];
}
$checks
=
[
...
$checks
,
[
$this
,
'checkPageRestrictions'
],
[
$this
,
'checkCascadingSourcesRestrictions'
],
[
$this
,
'checkActionPermissions'
],
[
$this
,
'checkUserBlock'
]
];
}
$status
=
PermissionStatus
::
newEmpty
();
foreach
(
$checks
as
$method
)
{
$method
(
$action
,
$user
,
$status
,
$rigor
,
$short
,
$page
);
if
(
$short
&&
!
$status
->
isGood
()
)
{
break
;
}
}
if
(
!
$status
->
isGood
()
)
{
$errors
=
$status
->
toLegacyErrorArray
();
$this
->
hookRunner
->
onPermissionErrorAudit
(
$page
,
$user
,
$action
,
$rigor
,
$errors
);
}
return
$status
;
}
/**
* Check various permission hooks
*
* @param string $action The action to check
* @param User $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkPermissionHooks
(
$action
,
User
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove when LinkTarget usage will expand further
$title
=
Title
::
newFromLinkTarget
(
$page
);
// Use getUserPermissionsErrors instead
$result
=
''
;
if
(
!
$this
->
hookRunner
->
onUserCan
(
$title
,
$user
,
$action
,
$result
)
)
{
if
(
!
$result
)
{
$status
->
fatal
(
'badaccess-group0'
);
}
return
;
}
// Check getUserPermissionsErrors hook
if
(
!
$this
->
hookRunner
->
onGetUserPermissionsErrors
(
$title
,
$user
,
$action
,
$result
)
)
{
$this
->
resultToStatus
(
$status
,
$result
);
}
// Check getUserPermissionsErrorsExpensive hook
if
(
$rigor
!==
self
::
RIGOR_QUICK
&&
!(
$short
&&
!
$status
->
isGood
()
)
&&
!
$this
->
hookRunner
->
onGetUserPermissionsErrorsExpensive
(
$title
,
$user
,
$action
,
$result
)
)
{
$this
->
resultToStatus
(
$status
,
$result
);
}
}
/**
* Add the resulting error code to the errors array
*
* @param PermissionStatus $status Current errors
* @param array|string|MessageSpecifier|false $result Result of errors
*/
private
function
resultToStatus
(
PermissionStatus
$status
,
$result
):
void
{
if
(
is_array
(
$result
)
&&
count
(
$result
)
&&
!
is_array
(
$result
[
0
]
)
)
{
// A single array representing an error
$status
->
fatal
(
...
$result
);
}
elseif
(
is_array
(
$result
)
&&
count
(
$result
)
&&
is_array
(
$result
[
0
]
)
)
{
// A nested array representing multiple errors
foreach
(
$result
as
$result1
)
{
$this
->
resultToStatus
(
$status
,
$result1
);
}
}
elseif
(
is_string
(
$result
)
&&
$result
!==
''
)
{
// A string representing a message-id
$status
->
fatal
(
$result
);
}
elseif
(
$result
instanceof
MessageSpecifier
)
{
// A message specifier representing an error
$status
->
fatal
(
$result
);
}
elseif
(
$result
===
false
)
{
// a generic "We don't want them to do that"
$status
->
fatal
(
'badaccess-group0'
);
}
// If we got here, $results is the empty array or empty string, which mean no errors.
}
/**
* Check that the user is allowed to read this page.
*
* @param string $action The action to check
* @param User $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkReadPermissions
(
$action
,
User
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove when LinkTarget usage will expand further
$title
=
Title
::
newFromLinkTarget
(
$page
);
$whiteListRead
=
$this
->
options
->
get
(
MainConfigNames
::
WhitelistRead
);
$allowed
=
false
;
if
(
$this
->
isEveryoneAllowed
(
'read'
)
)
{
// Shortcut for public wikis, allows skipping quite a bit of code
$allowed
=
true
;
}
elseif
(
$this
->
userHasRight
(
$user
,
'read'
)
)
{
// If the user is allowed to read pages, he is allowed to read all pages
$allowed
=
true
;
}
elseif
(
$this
->
isSameSpecialPage
(
'Userlogin'
,
$page
)
||
$this
->
isSameSpecialPage
(
'PasswordReset'
,
$page
)
||
$this
->
isSameSpecialPage
(
'Userlogout'
,
$page
)
)
{
// Always grant access to the login page.
// Even anons need to be able to log in.
$allowed
=
true
;
}
elseif
(
$this
->
isSameSpecialPage
(
'RunJobs'
,
$page
)
)
{
// relies on HMAC key signature alone
$allowed
=
true
;
}
elseif
(
is_array
(
$whiteListRead
)
&&
count
(
$whiteListRead
)
)
{
// Time to check the whitelist
// Only do these checks if there's something to check against
$name
=
$title
->
getPrefixedText
();
$dbName
=
$title
->
getPrefixedDBkey
();
// Check for explicit whitelisting with and without underscores
if
(
in_array
(
$name
,
$whiteListRead
,
true
)
||
in_array
(
$dbName
,
$whiteListRead
,
true
)
)
{
$allowed
=
true
;
}
elseif
(
$page
->
getNamespace
()
===
NS_MAIN
)
{
// Old settings might have the title prefixed with
// a colon for main-namespace pages
if
(
in_array
(
':'
.
$name
,
$whiteListRead
)
)
{
$allowed
=
true
;
}
}
elseif
(
$title
->
isSpecialPage
()
)
{
// If it's a special page, ditch the subpage bit and check again
$name
=
$title
->
getDBkey
();
[
$name
,
/* $subpage */
]
=
$this
->
specialPageFactory
->
resolveAlias
(
$name
);
if
(
$name
)
{
$pure
=
SpecialPage
::
getTitleFor
(
$name
)->
getPrefixedText
();
if
(
in_array
(
$pure
,
$whiteListRead
,
true
)
)
{
$allowed
=
true
;
}
}
}
}
$whitelistReadRegexp
=
$this
->
options
->
get
(
MainConfigNames
::
WhitelistReadRegexp
);
if
(
!
$allowed
&&
is_array
(
$whitelistReadRegexp
)
&&
$whitelistReadRegexp
)
{
$name
=
$title
->
getPrefixedText
();
// Check for regex whitelisting
foreach
(
$whitelistReadRegexp
as
$listItem
)
{
if
(
preg_match
(
$listItem
,
$name
)
)
{
$allowed
=
true
;
break
;
}
}
}
if
(
!
$allowed
)
{
# If the title is not whitelisted, give extensions a chance to do so...
$this
->
hookRunner
->
onTitleReadWhitelist
(
$title
,
$user
,
$allowed
);
if
(
!
$allowed
)
{
$this
->
missingPermissionError
(
$action
,
$short
,
$status
);
}
}
}
/**
* Add an error to the status when an action isn't allowed to be performed.
*
* @param string $action The action to check
* @param bool $short Short circuit on first error
* @param PermissionStatus $status
*/
private
function
missingPermissionError
(
string
$action
,
bool
$short
,
PermissionStatus
$status
):
void
{
// We avoid expensive display logic for quickUserCan's and such
if
(
$short
)
{
$status
->
fatal
(
'badaccess-group0'
);
}
// TODO: it would be a good idea to replace the method below with something else like
// maybe callback injection
$context
=
RequestContext
::
getMain
();
$fatalStatus
=
$this
->
newFatalPermissionDeniedStatus
(
$action
,
$context
);
$status
->
merge
(
$fatalStatus
);
$statusPermission
=
$fatalStatus
->
getPermission
();
if
(
$statusPermission
)
{
$status
->
setPermission
(
$statusPermission
);
}
}
/**
* Factory function for fatal permission-denied errors
*
* @internal for use by UserAuthority
*
* @param string $permission User right required
* @param IContextSource $context
*
* @return PermissionStatus
*/
public
function
newFatalPermissionDeniedStatus
(
$permission
,
IContextSource
$context
):
StatusValue
{
$groups
=
[];
foreach
(
$this
->
groupPermissionsLookup
->
getGroupsWithPermission
(
$permission
)
as
$group
)
{
$groups
[]
=
UserGroupMembership
::
getLinkWiki
(
$group
,
$context
);
}
if
(
$groups
)
{
return
PermissionStatus
::
newFatal
(
'badaccess-groups'
,
Message
::
listParam
(
$groups
,
'comma'
),
count
(
$groups
)
);
}
$status
=
PermissionStatus
::
newFatal
(
'badaccess-group0'
);
$status
->
setPermission
(
$permission
);
return
$status
;
}
/**
* Whether a title resolves to the named special page.
*
* @param string $name The special page name
* @param LinkTarget $page
* @return bool
*/
private
function
isSameSpecialPage
(
$name
,
LinkTarget
$page
):
bool
{
if
(
$page
->
getNamespace
()
===
NS_SPECIAL
)
{
[
$pageName
]
=
$this
->
specialPageFactory
->
resolveAlias
(
$page
->
getDBkey
()
);
if
(
$name
===
$pageName
)
{
return
true
;
}
}
return
false
;
}
/**
* Check that the user isn't blocked from editing.
*
* @param string $action The action to check
* @param User $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkUserBlock
(
$action
,
User
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
$block
=
$this
->
getApplicableBlock
(
$action
,
$user
,
$rigor
,
$page
,
$user
->
getRequest
()
);
if
(
$block
)
{
// @todo FIXME: Pass the relevant context into this function.
$context
=
RequestContext
::
getMain
();
$messages
=
$this
->
blockErrorFormatter
->
getMessages
(
$block
,
$user
,
$context
->
getRequest
()->
getIP
()
);
foreach
(
$messages
as
$message
)
{
// TODO: We can pass $message directly once getPermissionErrors() is removed.
// For now we store the message key as a string here out of overabundance of caution,
// because there is a test case verifying that block messages use strings in that format.
$status
->
fatal
(
$message
->
getKey
(),
...
$message
->
getParams
()
);
}
}
}
/**
* Return the Block object applicable for the given permission check, if any.
*
* @internal for use by UserAuthority only
*
* @param string $action The action to check
* @param User $user User to check
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param LinkTarget|PageReference|null $page
* @param WebRequest|null $request The request to get the IP and cookies
* from. If this is null, IP and cookie blocks will not be checked.
* @return ?Block
*/
public
function
getApplicableBlock
(
string
$action
,
User
$user
,
string
$rigor
,
$page
,
?
WebRequest
$request
):
?
Block
{
// Unblocking handled in SpecialUnblock
if
(
$rigor
===
self
::
RIGOR_QUICK
||
in_array
(
$action
,
[
'unblock'
]
)
)
{
return
null
;
}
// Optimize for a very common case
if
(
$action
===
'read'
&&
!
$this
->
options
->
get
(
MainConfigNames
::
BlockDisablesLogin
)
)
{
return
null
;
}
// Implicit rights aren't blockable (T350117, T350202).
if
(
in_array
(
$action
,
$this
->
getImplicitRights
(),
true
)
)
{
return
null
;
}
$useReplica
=
$rigor
!==
self
::
RIGOR_SECURE
;
$isExempt
=
$this
->
userHasRight
(
$user
,
'ipblock-exempt'
);
$requestIfNotExempt
=
$isExempt
?
null
:
$request
;
// Create account blocks are implemented separately due to weird IP exemption rules
if
(
in_array
(
$action
,
[
'createaccount'
,
'autocreateaccount'
],
true
)
)
{
return
$this
->
blockManager
->
getCreateAccountBlock
(
$user
,
$requestIfNotExempt
,
$useReplica
);
}
$block
=
$this
->
blockManager
->
getBlock
(
$user
,
$requestIfNotExempt
,
$useReplica
);
if
(
!
$block
)
{
return
null
;
}
$userIsHidden
=
$block
->
getHideName
();
// Remove elements from the block that explicitly allow the action
// (like "read" or "upload").
$block
=
$this
->
blockManager
->
filter
(
$block
,
static
function
(
AbstractBlock
$originalBlock
)
use
(
$action
)
{
// Remove the block if it explicitly allows the action
return
$originalBlock
->
appliesToRight
(
$action
)
!==
false
;
}
);
if
(
!
$block
)
{
return
null
;
}
// Convert the input page to a Title
$targetTitle
=
null
;
if
(
$page
)
{
$targetTitle
=
$page
instanceof
PageReference
?
Title
::
castFromPageReference
(
$page
)
:
Title
::
castFromLinkTarget
(
$page
);
if
(
!
$targetTitle
->
canExist
()
)
{
$targetTitle
=
null
;
}
}
// What gets passed into this method is a user right, not an action name.
// There is no way to instantiate an action by restriction. However, this
// will get the action where the restriction is the same. This may result
// in actions being blocked that shouldn't be.
$actionInfo
=
$this
->
actionFactory
->
getActionInfo
(
$action
,
$targetTitle
);
// Ensure that the retrieved action matches the restriction.
if
(
$actionInfo
&&
$actionInfo
->
getRestriction
()
!==
$action
)
{
$actionInfo
=
null
;
}
// Return null if the action does not require an unblocked user.
// If no ActionInfo is returned, assume that the action requires unblock
// which is the default.
// NOTE: We may get null here even for known actions, if a wiki's main page
// is set to a special page, e.g. Special:MyLanguage/Main_Page (T348451, T346036).
if
(
$actionInfo
&&
!
$actionInfo
->
requiresUnblock
()
)
{
return
null
;
}
// Remove elements from the block that do not apply to the specific page
if
(
$targetTitle
)
{
$targetIsUserTalk
=
!
$userIsHidden
&&
$targetTitle
->
equals
(
$user
->
getTalkPage
()
);
$block
=
$this
->
blockManager
->
filter
(
$block
,
static
function
(
AbstractBlock
$originalBlock
)
use
(
$action
,
$targetTitle
,
$targetIsUserTalk
)
{
if
(
$originalBlock
->
appliesToRight
(
$action
)
)
{
// An action block takes precedence over appliesToTitle().
// Block::appliesToRight('edit') always returns null,
// allowing title-based exemptions to take effect.
return
true
;
}
elseif
(
$targetIsUserTalk
)
{
// Special handling for a user's own talk page. The block is not aware
// of the user, so this must be done here.
return
$originalBlock
->
appliesToUsertalk
(
$targetTitle
);
}
else
{
return
$originalBlock
->
appliesToTitle
(
$targetTitle
);
}
}
);
}
if
(
$targetTitle
&&
$block
&&
$block
instanceof
AbstractBlock
// for phan
)
{
// Allow extensions to let a blocked user access a particular page
$allowUsertalk
=
$block
->
isUsertalkEditAllowed
();
$blocked
=
true
;
$this
->
hookRunner
->
onUserIsBlockedFrom
(
$user
,
$targetTitle
,
$blocked
,
$allowUsertalk
);
if
(
!
$blocked
)
{
$block
=
null
;
}
}
return
$block
;
}
/**
* Run easy-to-test (or "quick") permissions checks for a given action.
*
* @param string $action The action to check
* @param User $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkQuickPermissions
(
$action
,
User
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove when LinkTarget usage will expand further
$title
=
Title
::
newFromLinkTarget
(
$page
);
// This method is always called first, so $status is guaranteed to be empty, so we can
// just pass an empty $errors array, instead of converting it to the legacy format and back.
$errors
=
[];
if
(
!
$this
->
hookRunner
->
onTitleQuickPermissions
(
$title
,
$user
,
$action
,
$errors
,
$rigor
!==
self
::
RIGOR_QUICK
,
$short
)
)
{
// $errors is an array of results, not a result, but resultToStatus() handles
// arrays of arrays with recursion so this will work
$this
->
resultToStatus
(
$status
,
$errors
);
return
;
}
$isSubPage
=
$this
->
nsInfo
->
hasSubpages
(
$title
->
getNamespace
()
)
&&
strpos
(
$title
->
getText
(),
'/'
)
!==
false
;
if
(
$action
===
'create'
)
{
if
(
(
$this
->
nsInfo
->
isTalk
(
$title
->
getNamespace
()
)
&&
!
$this
->
userHasRight
(
$user
,
'createtalk'
)
)
||
(
!
$this
->
nsInfo
->
isTalk
(
$title
->
getNamespace
()
)
&&
!
$this
->
userHasRight
(
$user
,
'createpage'
)
)
)
{
$status
->
fatal
(
$user
->
isNamed
()
?
'nocreate-loggedin'
:
'nocreatetext'
);
}
}
elseif
(
$action
===
'move'
)
{
if
(
!
$this
->
userHasRight
(
$user
,
'move-rootuserpages'
)
&&
$title
->
getNamespace
()
===
NS_USER
&&
!
$isSubPage
)
{
// Show user page-specific message only if the user can move other pages
$status
->
fatal
(
'cant-move-user-page'
);
}
// Check if user is allowed to move files if it's a file
if
(
$title
->
getNamespace
()
===
NS_FILE
&&
!
$this
->
userHasRight
(
$user
,
'movefile'
)
)
{
$status
->
fatal
(
'movenotallowedfile'
);
}
// Check if user is allowed to move category pages if it's a category page
if
(
$title
->
getNamespace
()
===
NS_CATEGORY
&&
!
$this
->
userHasRight
(
$user
,
'move-categorypages'
)
)
{
$status
->
fatal
(
'cant-move-category-page'
);
}
if
(
!
$this
->
userHasRight
(
$user
,
'move'
)
)
{
// User can't move anything
$userCanMove
=
$this
->
groupPermissionsLookup
->
groupHasPermission
(
'user'
,
'move'
);
$autoconfirmedCanMove
=
$this
->
groupPermissionsLookup
->
groupHasPermission
(
'autoconfirmed'
,
'move'
);
if
(
$user
->
isAnon
()
&&
(
$userCanMove
||
$autoconfirmedCanMove
)
)
{
// custom message if logged-in users without any special rights can move
$status
->
fatal
(
'movenologintext'
);
}
elseif
(
$user
->
isTemp
()
&&
$autoconfirmedCanMove
)
{
// Temp user may be able to move if they log in as a proper account
$status
->
fatal
(
'movenologintext'
);
}
else
{
$status
->
fatal
(
'movenotallowed'
);
}
}
}
elseif
(
$action
===
'move-target'
)
{
if
(
!
$this
->
userHasRight
(
$user
,
'move'
)
)
{
// User can't move anything
$status
->
fatal
(
'movenotallowed'
);
}
elseif
(
!
$this
->
userHasRight
(
$user
,
'move-rootuserpages'
)
&&
$title
->
getNamespace
()
===
NS_USER
&&
!
$isSubPage
)
{
// Show user page-specific message only if the user can move other pages
$status
->
fatal
(
'cant-move-to-user-page'
);
}
elseif
(
!
$this
->
userHasRight
(
$user
,
'move-categorypages'
)
&&
$title
->
getNamespace
()
===
NS_CATEGORY
)
{
// Show category page-specific message only if the user can move other pages
$status
->
fatal
(
'cant-move-to-category-page'
);
}
}
elseif
(
$action
===
'autocreateaccount'
)
{
// createaccount implies autocreateaccount
if
(
!
$this
->
userHasAnyRight
(
$user
,
'autocreateaccount'
,
'createaccount'
)
)
{
$this
->
missingPermissionError
(
$action
,
$short
,
$status
);
}
}
elseif
(
!
$this
->
userHasRight
(
$user
,
$action
)
)
{
$this
->
missingPermissionError
(
$action
,
$short
,
$status
);
}
}
/**
* Check for any page_restrictions table requirements on this page.
*
* If the page has multiple restrictions, the user must have
* all of those rights to perform the action in question.
*
* @param string $action The action to check
* @param UserIdentity $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkPageRestrictions
(
$action
,
UserIdentity
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove & rework upon further use of LinkTarget
$title
=
Title
::
newFromLinkTarget
(
$page
);
foreach
(
$this
->
restrictionStore
->
getRestrictions
(
$title
,
$action
)
as
$right
)
{
// Backwards compatibility, rewrite sysop -> editprotected
if
(
$right
===
'sysop'
)
{
$right
=
'editprotected'
;
}
// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
if
(
$right
===
'autoconfirmed'
)
{
$right
=
'editsemiprotected'
;
}
if
(
$right
==
''
)
{
continue
;
}
if
(
!
$this
->
userHasRight
(
$user
,
$right
)
)
{
$status
->
fatal
(
'protectedpagetext'
,
$right
,
$action
);
}
elseif
(
$this
->
restrictionStore
->
areRestrictionsCascading
(
$title
)
&&
!
$this
->
userHasRight
(
$user
,
'protect'
)
)
{
$status
->
fatal
(
'protectedpagetext'
,
'protect'
,
$action
);
}
}
}
/**
* Check restrictions on cascading pages.
*
* @param string $action The action to check
* @param UserIdentity $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkCascadingSourcesRestrictions
(
$action
,
UserIdentity
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove & rework upon further use of LinkTarget
$title
=
Title
::
newFromLinkTarget
(
$page
);
if
(
$rigor
!==
self
::
RIGOR_QUICK
&&
!
$title
->
isUserConfigPage
()
)
{
[
$sources
,
$restrictions
,
$tlSources
,
$ilSources
]
=
$this
->
restrictionStore
->
getCascadeProtectionSources
(
$title
);
// If the file Wikitext isn't transcluded then we
// don't care about edit cascade restrictions for edit action
if
(
$action
===
'edit'
&&
$page
->
getNamespace
()
===
NS_FILE
&&
!
$tlSources
)
{
return
;
}
// For the purposes of cascading protection, edit restrictions should apply to uploads or moves
// Thus remap upload and move to edit
// Unless the file content itself is not transcluded
if
(
$ilSources
&&
(
$action
===
'upload'
||
$action
===
'move'
)
)
{
$restrictedAction
=
'edit'
;
}
else
{
$restrictedAction
=
$action
;
}
// Cascading protection depends on more than this page...
// Several cascading protected pages may include this page...
// Check each cascading level
// This is only for protection restrictions, not for all actions
if
(
isset
(
$restrictions
[
$restrictedAction
]
)
)
{
foreach
(
$restrictions
[
$restrictedAction
]
as
$right
)
{
// Backwards compatibility, rewrite sysop -> editprotected
if
(
$right
===
'sysop'
)
{
$right
=
'editprotected'
;
}
// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
if
(
$right
===
'autoconfirmed'
)
{
$right
=
'editsemiprotected'
;
}
if
(
$right
!=
''
&&
!
$this
->
userHasAllRights
(
$user
,
'protect'
,
$right
)
)
{
$wikiPages
=
''
;
foreach
(
$sources
as
$pageIdentity
)
{
$wikiPages
.=
'* [[:'
.
$this
->
titleFormatter
->
getPrefixedText
(
$pageIdentity
)
.
"]]
\n
"
;
}
$status
->
fatal
(
'cascadeprotected'
,
count
(
$sources
),
$wikiPages
,
$action
);
}
}
}
}
}
/**
* Check action permissions not already checked in checkQuickPermissions
*
* @param string $action The action to check
* @param User $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkActionPermissions
(
$action
,
User
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove & rework upon further use of LinkTarget
$title
=
Title
::
newFromLinkTarget
(
$page
);
if
(
$rigor
!==
self
::
RIGOR_QUICK
&&
!
defined
(
'MW_NO_SESSION'
)
)
{
$sessionRestrictions
=
$user
->
getRequest
()->
getSession
()->
getRestrictions
();
if
(
$sessionRestrictions
)
{
$userCan
=
$sessionRestrictions
->
userCan
(
$title
);
if
(
!
$userCan
->
isOK
()
)
{
$status
->
merge
(
$userCan
);
}
}
}
if
(
$action
===
'protect'
)
{
if
(
!
$this
->
getPermissionStatus
(
'edit'
,
$user
,
$title
,
$rigor
,
true
)->
isGood
()
)
{
// If they can't edit, they shouldn't protect.
$status
->
fatal
(
'protect-cantedit'
);
}
}
elseif
(
$action
===
'create'
)
{
$createProtection
=
$this
->
restrictionStore
->
getCreateProtection
(
$title
);
if
(
$createProtection
)
{
if
(
$createProtection
[
'permission'
]
==
''
||
!
$this
->
userHasRight
(
$user
,
$createProtection
[
'permission'
]
)
)
{
$protectUserIdentity
=
$this
->
userIdentityLookup
->
getUserIdentityByUserId
(
$createProtection
[
'user'
]
);
$status
->
fatal
(
'titleprotected'
,
$protectUserIdentity
?
$protectUserIdentity
->
getName
()
:
''
,
$createProtection
[
'reason'
]
);
}
}
}
elseif
(
$action
===
'move'
)
{
// Check for immobile pages
if
(
!
$this
->
nsInfo
->
isMovable
(
$title
->
getNamespace
()
)
)
{
// Specific message for this case
$nsText
=
$title
->
getNsText
();
if
(
$nsText
===
''
)
{
$nsText
=
wfMessage
(
'blanknamespace'
)->
text
();
}
$status
->
fatal
(
'immobile-source-namespace'
,
$nsText
);
}
elseif
(
!
$title
->
isMovable
()
)
{
// Less specific message for rarer cases
$status
->
fatal
(
'immobile-source-page'
);
}
}
elseif
(
$action
===
'move-target'
)
{
if
(
!
$this
->
nsInfo
->
isMovable
(
$title
->
getNamespace
()
)
)
{
$nsText
=
$title
->
getNsText
();
if
(
$nsText
===
''
)
{
$nsText
=
wfMessage
(
'blanknamespace'
)->
text
();
}
$status
->
fatal
(
'immobile-target-namespace'
,
$nsText
);
}
elseif
(
!
$title
->
isMovable
()
)
{
$status
->
fatal
(
'immobile-target-page'
);
}
}
elseif
(
$action
===
'delete'
||
$action
===
'delete-redirect'
)
{
$tempStatus
=
PermissionStatus
::
newEmpty
();
$this
->
checkPageRestrictions
(
'edit'
,
$user
,
$tempStatus
,
$rigor
,
true
,
$title
);
if
(
$tempStatus
->
isGood
()
)
{
$this
->
checkCascadingSourcesRestrictions
(
'edit'
,
$user
,
$tempStatus
,
$rigor
,
true
,
$title
);
}
if
(
!
$tempStatus
->
isGood
()
)
{
// If protection keeps them from editing, they shouldn't be able to delete.
$status
->
fatal
(
'deleteprotected'
);
}
if
(
$rigor
!==
self
::
RIGOR_QUICK
&&
$action
===
'delete'
&&
$this
->
options
->
get
(
MainConfigNames
::
DeleteRevisionsLimit
)
&&
!
$this
->
userCan
(
'bigdelete'
,
$user
,
$title
)
&&
$title
->
isBigDeletion
()
)
{
// NOTE: This check is deprecated since 1.37, see T288759
$status
->
fatal
(
'delete-toobig'
,
Message
::
numParam
(
$this
->
options
->
get
(
MainConfigNames
::
DeleteRevisionsLimit
)
)
);
}
}
elseif
(
$action
===
'undelete'
)
{
if
(
!
$this
->
getPermissionStatus
(
'edit'
,
$user
,
$title
,
$rigor
,
true
)->
isGood
()
)
{
// Undeleting implies editing
$status
->
fatal
(
'undelete-cantedit'
);
}
if
(
!
$title
->
exists
()
&&
!
$this
->
getPermissionStatus
(
'create'
,
$user
,
$title
,
$rigor
,
true
)->
isGood
()
)
{
// Undeleting where nothing currently exists implies creating
$status
->
fatal
(
'undelete-cantcreate'
);
}
}
elseif
(
$action
===
'edit'
)
{
if
(
$this
->
options
->
get
(
MainConfigNames
::
EmailConfirmToEdit
)
&&
!
$user
->
isEmailConfirmed
()
)
{
$status
->
fatal
(
'confirmedittext'
);
}
if
(
!
$title
->
exists
()
)
{
$status
->
merge
(
$this
->
getPermissionStatus
(
'create'
,
$user
,
$title
,
$rigor
,
true
)
);
}
}
}
/**
* Check permissions on special pages & namespaces
*
* @param string $action The action to check
* @param UserIdentity $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkSpecialsAndNSPermissions
(
$action
,
UserIdentity
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove & rework upon further use of LinkTarget
$title
=
Title
::
newFromLinkTarget
(
$page
);
// Only 'createaccount' can be performed on special pages,
// which don't actually exist in the DB.
if
(
$title
->
getNamespace
()
===
NS_SPECIAL
&&
!
in_array
(
$action
,
[
'createaccount'
,
'autocreateaccount'
],
true
)
)
{
$status
->
fatal
(
'ns-specialprotected'
);
}
// Check $wgNamespaceProtection for restricted namespaces
if
(
$this
->
isNamespaceProtected
(
$title
->
getNamespace
(),
$user
)
// Allow admins and oversighters to view deleted content, even if they
// cannot restore it. See T362536.
&&
!
in_array
(
$action
,
[
'deletedhistory'
,
'deletedtext'
,
'viewsuppressed'
],
true
)
)
{
$ns
=
$title
->
getNamespace
()
===
NS_MAIN
?
wfMessage
(
'nstab-main'
)->
text
()
:
$title
->
getNsText
();
if
(
$title
->
getNamespace
()
===
NS_MEDIAWIKI
)
{
$status
->
fatal
(
'protectedinterface'
,
$action
);
}
else
{
$status
->
fatal
(
'namespaceprotected'
,
$ns
,
$action
);
}
}
}
/**
* Check sitewide CSS/JSON/JS permissions
*
* @param string $action The action to check
* @param UserIdentity $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkSiteConfigPermissions
(
$action
,
UserIdentity
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove & rework upon further use of LinkTarget
$title
=
Title
::
newFromLinkTarget
(
$page
);
if
(
$action
===
'patrol'
)
{
return
;
}
if
(
in_array
(
$action
,
[
'deletedhistory'
,
'deletedtext'
,
'viewsuppressed'
],
true
)
)
{
// Allow admins and oversighters to view deleted content, even if they
// cannot restore it. See T202989
// Not using the same handling in `getPermissionStatus` as the checks
// for skipping `checkUserConfigPermissions` since normal admins can delete
// user scripts, but not sitewide scripts
return
;
}
// Sitewide CSS/JSON/JS/RawHTML changes, like all NS_MEDIAWIKI changes, also require the
// editinterface right. That's implemented as a restriction so no check needed here.
if
(
$title
->
isSiteCssConfigPage
()
&&
!
$this
->
userHasRight
(
$user
,
'editsitecss'
)
)
{
$status
->
fatal
(
'sitecssprotected'
,
$action
);
}
elseif
(
$title
->
isSiteJsonConfigPage
()
&&
!
$this
->
userHasRight
(
$user
,
'editsitejson'
)
)
{
$status
->
fatal
(
'sitejsonprotected'
,
$action
);
}
elseif
(
$title
->
isSiteJsConfigPage
()
&&
!
$this
->
userHasRight
(
$user
,
'editsitejs'
)
)
{
$status
->
fatal
(
'sitejsprotected'
,
$action
);
}
if
(
$title
->
isRawHtmlMessage
()
&&
!
$this
->
userCanEditRawHtmlPage
(
$user
)
)
{
$status
->
fatal
(
'siterawhtmlprotected'
,
$action
);
}
}
/**
* Check CSS/JSON/JS subpage permissions
*
* @param string $action The action to check
* @param UserIdentity $user User to check
* @param PermissionStatus $status Current errors
* @param string $rigor One of PermissionManager::RIGOR_ constants
* - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
* - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
* - RIGOR_SECURE : does cheap and expensive checks, using the primary DB as needed
* @param bool $short Short circuit on first error
* @param LinkTarget $page
*/
private
function
checkUserConfigPermissions
(
$action
,
UserIdentity
$user
,
PermissionStatus
$status
,
$rigor
,
$short
,
LinkTarget
$page
):
void
{
// TODO: remove & rework upon further use of LinkTarget
$title
=
Title
::
newFromLinkTarget
(
$page
);
// Protect css/json/js subpages of user pages
// XXX: this might be better using restrictions
if
(
preg_match
(
'/^'
.
preg_quote
(
$user
->
getName
(),
'/'
)
.
'
\/
/'
,
$title
->
getText
()
)
)
{
// Users need editmyuser* to edit their own CSS/JSON/JS subpages.
if
(
$title
->
isUserCssConfigPage
()
&&
!
$this
->
userHasAnyRight
(
$user
,
'editmyusercss'
,
'editusercss'
)
)
{
$status
->
fatal
(
'mycustomcssprotected'
,
$action
);
}
elseif
(
$title
->
isUserJsonConfigPage
()
&&
!
$this
->
userHasAnyRight
(
$user
,
'editmyuserjson'
,
'edituserjson'
)
)
{
$status
->
fatal
(
'mycustomjsonprotected'
,
$action
);
}
elseif
(
$title
->
isUserJsConfigPage
()
&&
!
$this
->
userHasAnyRight
(
$user
,
'editmyuserjs'
,
'edituserjs'
)
)
{
$status
->
fatal
(
'mycustomjsprotected'
,
$action
);
}
elseif
(
$title
->
isUserJsConfigPage
()
&&
!
$this
->
userHasAnyRight
(
$user
,
'edituserjs'
,
'editmyuserjsredirect'
)
)
{
// T207750 - do not allow users to edit a redirect if they couldn't edit the target
$target
=
$this
->
redirectLookup
->
getRedirectTarget
(
$title
);
if
(
$target
&&
(
!
$target
->
inNamespace
(
NS_USER
)
||
!
preg_match
(
'/^'
.
preg_quote
(
$user
->
getName
(),
'/'
)
.
'
\/
/'
,
$target
->
getText
()
)
)
)
{
$status
->
fatal
(
'mycustomjsredirectprotected'
,
$action
);
}
}
}
else
{
// Users need edituser* to edit others' CSS/JSON/JS subpages.
// The checks to exclude deletion/suppression, which cannot be used for
// attacks and should be excluded to avoid the situation where an
// unprivileged user can post abusive content on their subpages
// and only very highly privileged users could remove it,
// are now a part of `getPermissionStatus` and this method isn't called.
if
(
$title
->
isUserCssConfigPage
()
&&
!
$this
->
userHasRight
(
$user
,
'editusercss'
)
)
{
$status
->
fatal
(
'customcssprotected'
,
$action
);
}
elseif
(
$title
->
isUserJsonConfigPage
()
&&
!
$this
->
userHasRight
(
$user
,
'edituserjson'
)
)
{
$status
->
fatal
(
'customjsonprotected'
,
$action
);
}
elseif
(
$title
->
isUserJsConfigPage
()
&&
!
$this
->
userHasRight
(
$user
,
'edituserjs'
)
)
{
$status
->
fatal
(
'customjsprotected'
,
$action
);
}
}
}
/**
* Whether the user is generally allowed to perform the given action.
*
* @since 1.34
* @param UserIdentity $user
* @param string $action
* @return bool True if allowed
*/
public
function
userHasRight
(
UserIdentity
$user
,
$action
=
''
):
bool
{
if
(
$action
===
''
)
{
// In the spirit of DWIM
return
true
;
}
// Use strict parameter to avoid matching numeric 0 accidentally inserted
// by misconfiguration: 0 == 'foo'
return
in_array
(
$action
,
$this
->
getImplicitRights
(),
true
)
||
in_array
(
$action
,
$this
->
getUserPermissions
(
$user
),
true
);
}
/**
* Whether the user is generally allowed to perform at least one of the actions.
*
* @since 1.34
* @param UserIdentity $user
* @param string ...$actions
* @return bool True if user is allowed to perform *any* of the actions
*/
public
function
userHasAnyRight
(
UserIdentity
$user
,
...
$actions
):
bool
{
foreach
(
$actions
as
$action
)
{
if
(
$this
->
userHasRight
(
$user
,
$action
)
)
{
return
true
;
}
}
return
false
;
}
/**
* Whether the user is allowed to perform all of the given actions.
*
* @since 1.34
* @param UserIdentity $user
* @param string ...$actions
* @return bool True if user is allowed to perform *all* of the given actions
*/
public
function
userHasAllRights
(
UserIdentity
$user
,
...
$actions
):
bool
{
foreach
(
$actions
as
$action
)
{
if
(
!
$this
->
userHasRight
(
$user
,
$action
)
)
{
return
false
;
}
}
return
true
;
}
/**
* Get the permissions this user has.
*
* @since 1.34
* @param UserIdentity $user
* @return string[] permission names
*/
public
function
getUserPermissions
(
UserIdentity
$user
):
array
{
$rightsCacheKey
=
$this
->
getRightsCacheKey
(
$user
);
if
(
!
isset
(
$this
->
usersRights
[
$rightsCacheKey
]
)
)
{
$userObj
=
$this
->
userFactory
->
newFromUserIdentity
(
$user
);
$rights
=
$this
->
groupPermissionsLookup
->
getGroupPermissions
(
$this
->
userGroupManager
->
getUserEffectiveGroups
(
$user
)
);
// Hook requires a full User object
$this
->
hookRunner
->
onUserGetRights
(
$userObj
,
$rights
);
// Deny any rights denied by the user's session, unless this
// endpoint has no sessions.
if
(
!
defined
(
'MW_NO_SESSION'
)
)
{
// FIXME: $userObj->getRequest().. need to be replaced with something else
$allowedRights
=
$userObj
->
getRequest
()->
getSession
()->
getAllowedUserRights
();
if
(
$allowedRights
!==
null
)
{
$rights
=
array_intersect
(
$rights
,
$allowedRights
);
}
}
// Hook requires a full User object
$this
->
hookRunner
->
onUserGetRightsRemove
(
$userObj
,
$rights
);
// Force reindexation of rights when a hook has unset one of them
$rights
=
array_values
(
array_unique
(
$rights
)
);
// If BlockDisablesLogin is true, remove rights that anonymous
// users don't have. This has to be done after the hooks so that
// we know whether the user is exempt. (T129738)
if
(
$userObj
->
isRegistered
()
&&
$this
->
options
->
get
(
MainConfigNames
::
BlockDisablesLogin
)
)
{
// Stash the permissions as they are before triggering any block checks for BlockDisablesLogin
// to avoid a potential infinite loop, since GetUserBlock handlers may themselves check
// permissions on this user. (T384197)
$this
->
usersRights
[
$rightsCacheKey
]
=
$rights
;
$isExempt
=
in_array
(
'ipblock-exempt'
,
$rights
,
true
);
if
(
$this
->
blockManager
->
getBlock
(
$userObj
,
$isExempt
?
null
:
$userObj
->
getRequest
()
)
)
{
$anon
=
$this
->
userFactory
->
newAnonymous
();
$rights
=
array_intersect
(
$rights
,
$this
->
getUserPermissions
(
$anon
)
);
}
}
$this
->
usersRights
[
$rightsCacheKey
]
=
$rights
;
}
else
{
$rights
=
$this
->
usersRights
[
$rightsCacheKey
];
}
foreach
(
$this
->
temporaryUserRights
[
$user
->
getId
()
]
??
[]
as
$overrides
)
{
$rights
=
array_values
(
array_unique
(
array_merge
(
$rights
,
$overrides
)
)
);
}
return
$rights
;
}
/**
* Clear the in-process permission cache for one or all users.
*
* @since 1.34
* @param UserIdentity|null $user If a specific user is provided it will clear
* the permission cache only for that user.
*/
public
function
invalidateUsersRightsCache
(
$user
=
null
):
void
{
if
(
$user
!==
null
)
{
$rightsCacheKey
=
$this
->
getRightsCacheKey
(
$user
);
unset
(
$this
->
usersRights
[
$rightsCacheKey
]
);
}
else
{
$this
->
usersRights
=
[];
}
}
/**
* Get a unique key for user rights cache.
*
* @param UserIdentity $user
* @return string
*/
private
function
getRightsCacheKey
(
UserIdentity
$user
):
string
{
return
$user
->
isRegistered
()
?
"u:{$user->getId()}"
:
"anon:{$user->getName()}"
;
}
/**
* Check if all users may be assumed to have the given permission
*
* We generally assume so if the right is granted to '*' and isn't revoked
* on any group. It doesn't attempt to take grants or other extension
* limitations on rights into account in the general case, though, as that
* would require it to always return false and defeat the purpose.
* Specifically, session-based rights restrictions (such as OAuth or bot
* passwords) are applied based on the current session.
*
* @since 1.34
* @param string $right Right to check
* @return bool
*/
public
function
isEveryoneAllowed
(
$right
):
bool
{
// Use the cached results, except in unit tests which rely on
// being able change the permission mid-request
if
(
isset
(
$this
->
cachedRights
[
$right
]
)
)
{
return
$this
->
cachedRights
[
$right
];
}
if
(
!
isset
(
$this
->
options
->
get
(
MainConfigNames
::
GroupPermissions
)[
'*'
][
$right
]
)
||
!
$this
->
options
->
get
(
MainConfigNames
::
GroupPermissions
)[
'*'
][
$right
]
)
{
$this
->
cachedRights
[
$right
]
=
false
;
return
false
;
}
// If it's revoked anywhere, then everyone doesn't have it
foreach
(
$this
->
options
->
get
(
MainConfigNames
::
RevokePermissions
)
as
$rights
)
{
if
(
isset
(
$rights
[
$right
]
)
&&
$rights
[
$right
]
)
{
$this
->
cachedRights
[
$right
]
=
false
;
return
false
;
}
}
// Remove any rights that aren't allowed to the global-session user,
// unless there are no sessions for this endpoint.
if
(
!
defined
(
'MW_NO_SESSION'
)
)
{
// XXX: think what could be done with the below
$allowedRights
=
SessionManager
::
getGlobalSession
()->
getAllowedUserRights
();
if
(
$allowedRights
!==
null
&&
!
in_array
(
$right
,
$allowedRights
,
true
)
)
{
$this
->
cachedRights
[
$right
]
=
false
;
return
false
;
}
}
// Allow extensions to say false
if
(
!
$this
->
hookRunner
->
onUserIsEveryoneAllowed
(
$right
)
)
{
$this
->
cachedRights
[
$right
]
=
false
;
return
false
;
}
$this
->
cachedRights
[
$right
]
=
true
;
return
true
;
}
/**
* Get a list of all permissions that can be managed through group permissions.
* This does not include implicit rights which are granted to all users automatically.
*
* @see getImplicitRights()
*
* @since 1.34
* @return string[] Array of permission names
*/
public
function
getAllPermissions
():
array
{
if
(
$this
->
allRights
===
null
)
{
if
(
count
(
$this
->
options
->
get
(
MainConfigNames
::
AvailableRights
)
)
)
{
$this
->
allRights
=
array_unique
(
array_merge
(
self
::
CORE_RIGHTS
,
$this
->
options
->
get
(
MainConfigNames
::
AvailableRights
)
)
);
}
else
{
$this
->
allRights
=
self
::
CORE_RIGHTS
;
}
$this
->
hookRunner
->
onUserGetAllRights
(
$this
->
allRights
);
}
return
$this
->
allRights
;
}
/**
* Get a list of implicit rights.
*
* Rights in this list should be granted to all users implicitly.
*
* Implicit rights are defined to allow rate limits to be imposed
* on permissions
*
* @since 1.41
* @return string[] Array of permission names
*/
public
function
getImplicitRights
():
array
{
if
(
$this
->
implicitRights
===
null
)
{
$rights
=
array_unique
(
array_merge
(
self
::
CORE_IMPLICIT_RIGHTS
,
$this
->
options
->
get
(
MainConfigNames
::
ImplicitRights
)
)
);
$this
->
implicitRights
=
array_diff
(
$rights
,
$this
->
getAllPermissions
()
);
}
return
$this
->
implicitRights
;
}
/**
* Determine if $user is unable to edit pages in namespace because it has been protected.
*
* @param int $index
* @param UserIdentity $user
* @return bool
*/
private
function
isNamespaceProtected
(
$index
,
UserIdentity
$user
):
bool
{
$namespaceProtection
=
$this
->
options
->
get
(
MainConfigNames
::
NamespaceProtection
);
if
(
isset
(
$namespaceProtection
[
$index
]
)
)
{
return
!
$this
->
userHasAllRights
(
$user
,
...(
array
)
$namespaceProtection
[
$index
]
);
}
return
false
;
}
/**
* Determine which restriction levels it makes sense to use in a namespace,
* optionally filtered by a user's rights.
*
* @param int $index Namespace ID (index) to check
* @param UserIdentity|null $user User to check
* @return string[]
*/
public
function
getNamespaceRestrictionLevels
(
$index
,
?
UserIdentity
$user
=
null
):
array
{
if
(
!
isset
(
$this
->
options
->
get
(
MainConfigNames
::
NamespaceProtection
)[
$index
]
)
)
{
// All levels are valid if there's no namespace restriction.
// But still filter by user, if necessary
$levels
=
$this
->
options
->
get
(
MainConfigNames
::
RestrictionLevels
);
if
(
$user
)
{
$levels
=
array_values
(
array_filter
(
$levels
,
function
(
$level
)
use
(
$user
)
{
$right
=
$level
;
if
(
$right
===
'sysop'
)
{
$right
=
'editprotected'
;
// BC
}
if
(
$right
===
'autoconfirmed'
)
{
$right
=
'editsemiprotected'
;
// BC
}
return
$this
->
userHasRight
(
$user
,
$right
);
}
)
);
}
return
$levels
;
}
// $wgNamespaceProtection can require one or more rights to edit the namespace, which
// may be satisfied by membership in multiple groups each giving a subset of those rights.
// A restriction level is redundant if, for any one of the namespace rights, all groups
// giving that right also give the restriction level's right. Or, conversely, a
// restriction level is not redundant if, for every namespace right, there's at least one
// group giving that right without the restriction level's right.
//
// First, for each right, get a list of groups with that right.
$namespaceRightGroups
=
[];
foreach
(
(
array
)
$this
->
options
->
get
(
MainConfigNames
::
NamespaceProtection
)[
$index
]
as
$right
)
{
if
(
$right
===
'sysop'
)
{
$right
=
'editprotected'
;
// BC
}
if
(
$right
===
'autoconfirmed'
)
{
$right
=
'editsemiprotected'
;
// BC
}
if
(
$right
!=
''
)
{
$namespaceRightGroups
[
$right
]
=
$this
->
groupPermissionsLookup
->
getGroupsWithPermission
(
$right
);
}
}
// Now, go through the protection levels one by one.
$usableLevels
=
[
''
];
foreach
(
$this
->
options
->
get
(
MainConfigNames
::
RestrictionLevels
)
as
$level
)
{
$right
=
$level
;
if
(
$right
===
'sysop'
)
{
$right
=
'editprotected'
;
// BC
}
if
(
$right
===
'autoconfirmed'
)
{
$right
=
'editsemiprotected'
;
// BC
}
if
(
$right
!=
''
&&
!
isset
(
$namespaceRightGroups
[
$right
]
)
&&
(
!
$user
||
$this
->
userHasRight
(
$user
,
$right
)
)
)
{
// Do any of the namespace rights imply the restriction right? (see explanation above)
foreach
(
$namespaceRightGroups
as
$groups
)
{
if
(
!
array_diff
(
$groups
,
$this
->
groupPermissionsLookup
->
getGroupsWithPermission
(
$right
)
)
)
{
// Yes, this one does.
continue
2
;
}
}
// No, keep the restriction level
$usableLevels
[]
=
$level
;
}
}
return
$usableLevels
;
}
/**
* Check if user is allowed to edit sitewide pages that contain raw HTML.
*
* Pages listed in $wgRawHtmlMessages allow raw HTML which can be used to deploy CSS or JS
* code to all users so both rights are required to edit them.
*
* @param UserIdentity $user
* @return bool True if user has both rights
*/
private
function
userCanEditRawHtmlPage
(
UserIdentity
$user
):
bool
{
return
$this
->
userHasAllRights
(
$user
,
'editsitecss'
,
'editsitejs'
);
}
/**
* Add temporary user rights, only valid for the current function scope.
*
* This is meant for making it possible to programatically trigger certain actions that
* the user wouldn't be able to trigger themselves; e.g. allow users without the bot right
* to make bot-flagged actions through certain special pages.
*
* This returns a "scope guard" variable. Its only purpose is to be stored in a variable
* by the caller, which is automatically closed at the end of the function, at which point
* the rights are revoked again. Alternatively, you can close it earlier by consuming it
* via ScopedCallback::consume().
*
* @since 1.34
* @param UserIdentity $user
* @param string|string[] $rights
* @return ScopedCallback
*/
public
function
addTemporaryUserRights
(
UserIdentity
$user
,
$rights
)
{
$userId
=
$user
->
getId
();
$nextKey
=
count
(
$this
->
temporaryUserRights
[
$userId
]
??
[]
);
$this
->
temporaryUserRights
[
$userId
][
$nextKey
]
=
(
array
)
$rights
;
return
new
ScopedCallback
(
function
()
use
(
$userId
,
$nextKey
)
{
unset
(
$this
->
temporaryUserRights
[
$userId
][
$nextKey
]
);
}
);
}
/**
* Override the user permissions cache
*
* @internal For testing only
* @since 1.34
* @param UserIdentity $user
* @param string[]|string $rights
*/
public
function
overrideUserRightsForTesting
(
$user
,
$rights
=
[]
)
{
if
(
!
defined
(
'MW_PHPUNIT_TEST'
)
)
{
throw
new
LogicException
(
__METHOD__
.
' can not be called outside of tests'
);
}
$this
->
usersRights
[
$this
->
getRightsCacheKey
(
$user
)
]
=
is_array
(
$rights
)
?
$rights
:
[
$rights
];
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 13:43 (1 d, 19 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
11/bf/78d0aabf8c7da0b6c6b576880525
Default Alt Text
PermissionManager.php (63 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment