wiki.techinc.nl/includes/block/BlockManager.php

684 lines
22 KiB
PHP
Raw Normal View History

<?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\Block;
block: Allow cookie-block tracking from any uncached web request This was previously hardcoded from three places: 1) Upon viewing EditPage, 2) Upon viewing SpecialCreateAccount, 3) For any url if the user is logged-in (User::loadFromSession/isLoggedIn). == User::loadFromSession Performing cookie blocks from here created a circular dependency because Block may need the user language for localisation, which is determined by asking the User object. This was previously worked around by using a DeferredUpdate (T180050, T226777). Moving this logic explicitly to the end of the pre-send cycle in MediaWiki::preOutputCommit breaks the cycle. This is also where other request-specific handling resides already. == Limited effect on unregistered users When an unregistered user performs an edit, and gets blocked, the cookie block is not applied until they open built-in editor or CreateAccount page. This makes it more likely for a user's IP to change meanwhile. Either intentionally, or simply due to IPs varying naturally (e.g. between mobile locations, or when going on/off WiFi). By applying it throughout sessioned page views for unregistered users, it is more likely to get set. Similar to what was already done for logged-in users. This commit also makes the intent of not caching EditPage and SpecialCreateAccount explicit. This was previously implicit through nothing having called setCdnMaxage() and/or due to Session::persist being checked for by OutputPage::sendCacheControl. Bug: T233594 Change-Id: Icf5a00f9b41d31bb6d4742c049feca0039d0c9d9
2019-09-07 23:44:46 +00:00
use LogicException;
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;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use Message;
use MWCryptHash;
use Psr\Log\LoggerInterface;
use User;
use WebRequest;
use WebResponse;
use Wikimedia\IPSet;
use Wikimedia\IPUtils;
/**
* A service class for checking blocks.
* To obtain an instance, use MediaWikiServices::getInstance()->getBlockManager().
*
* @since 1.34 Refactored from User and Block.
*/
class BlockManager {
/** @var PermissionManager */
private $permissionManager;
/** @var UserFactory */
private $userFactory;
/** @var ServiceOptions */
private $options;
/**
* @internal For use by ServiceWiring
*/
public const CONSTRUCTOR_OPTIONS = [
'ApplyIpBlocksToXff',
'CookieSetOnAutoblock',
'CookieSetOnIpBlock',
'DnsBlacklistUrls',
'EnableDnsBlacklist',
'ProxyList',
'ProxyWhitelist',
'SecretKey',
'SoftBlockRanges',
];
/** @var LoggerInterface */
private $logger;
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;
/**
* @param ServiceOptions $options
* @param PermissionManager $permissionManager
* @param UserFactory $userFactory
* @param LoggerInterface $logger
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
*/
public function __construct(
ServiceOptions $options,
PermissionManager $permissionManager,
UserFactory $userFactory,
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
LoggerInterface $logger,
HookContainer $hookContainer
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
$this->permissionManager = $permissionManager;
$this->userFactory = $userFactory;
$this->logger = $logger;
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 );
}
/**
* Get the blocks that apply to a user. If there is only one, return that, otherwise
* return a composite block that combines the strictest features of the applicable
* blocks.
*
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
* Different blocks may be sought, depending on the user and their permissions. The
* user may be:
* (1) The global user (and can be affected by IP blocks). The global request object
* is needed for checking the IP address, the XFF header and the cookies.
* (2) The global user (and exempt from IP blocks). The global request object is
* available.
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
* (3) Another user (not the global user). No request object is available or needed;
* just look for a block against the user account.
*
* Cases #1 and #2 check whether the global user is blocked in practice; the block
* may due to their user account being blocked or to an IP address block or cookie
* block (or multiple of these). Case #3 simply checks whether a user's account is
* blocked, and does not determine whether the person using that account is affected
* in practice by any IP address or cookie blocks.
*
* @internal This should only be called by User::getBlockedStatus
* @param UserIdentity $user
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
* @param WebRequest|null $request The global request object if the user is the
* global user (cases #1 and #2), otherwise null (case #3). The IP address and
* information from the request header are needed to find some types of blocks.
* @param bool $fromReplica Whether to check the replica DB first.
* To improve performance, non-critical checks are done against replica DBs.
* Check when actually saving should be done against primary.
* @param bool $disableIpBlockExemptChecking This is used internally to prevent
* a infinite recursion with autopromote. See T270145.
* @return AbstractBlock|null The most relevant block, or null if there is no block.
*/
public function getUserBlock(
UserIdentity $user,
$request,
$fromReplica,
$disableIpBlockExemptChecking = false
) {
$fromPrimary = !$fromReplica;
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
$ip = null;
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
// If this is the global user, they may be affected by IP blocks (case #1),
// or they may be exempt (case #2). If affected, look for additional blocks
// against the IP address and referenced in a cookie.
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
$checkIpBlocks = $request &&
// Because calling getBlock within Autopromote leads back to here,
// thus causing a infinite recursion. We fix this by not checking for
// ipblock-exempt when calling getBlock within Autopromote.
// See T270145.
!$disableIpBlockExemptChecking &&
!$this->permissionManager->userHasRight( $user, 'ipblock-exempt' );
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
if ( $request && $checkIpBlocks ) {
// Case #1: checking the global user, including IP blocks
$ip = $request->getIP();
$isAnon = !$user->isRegistered();
$xff = $request->getHeader( 'X-Forwarded-For' );
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
// TODO: remove dependency on DatabaseBlock (T221075)
$blocks = array_merge(
DatabaseBlock::newListFromTarget( $user, $ip, $fromPrimary ),
$this->getSystemIpBlocks( $ip, $isAnon ),
$this->getXffBlocks( $ip, $xff, $isAnon, $fromPrimary ),
$this->getCookieBlock( $user, $request )
);
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
} else {
// Case #2: checking the global user, but they are exempt from IP blocks
// and cookie blocks, so we only check for a user account block.
// Case #3: checking whether another user's account is blocked.
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
// TODO: remove dependency on DatabaseBlock (T221075)
$blocks = DatabaseBlock::newListFromTarget( $user, null, $fromPrimary );
}
$block = $this->createGetBlockResult( $ip, $blocks );
$legacyUser = $this->userFactory->newFromUserIdentity( $user );
$this->hookRunner->onGetUserBlock( clone $legacyUser, $ip, $block );
return $block;
}
/**
* @param string|null $ip
* @param AbstractBlock[] $blocks
* @return AbstractBlock|null
*/
private function createGetBlockResult( ?string $ip, array $blocks ): ?AbstractBlock {
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
// Filter out any duplicated blocks, e.g. from the cookie
$blocks = $this->getUniqueBlocks( $blocks );
if ( count( $blocks ) === 0 ) {
return null;
} elseif ( count( $blocks ) === 1 ) {
return $blocks[ 0 ];
} else {
// @phan-suppress-next-line SecurityCheck-DoubleEscaped
return new CompositeBlock( [
'address' => $ip,
'reason' => new Message( 'blockedtext-composite-reason' ),
'originalBlocks' => $blocks,
] );
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
}
}
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
/**
* Get the blocks that apply to an IP address. If there is only one, return that, otherwise
* return a composite block that combines the strictest features of the applicable blocks.
*
* @since 1.38
* @param string $ip
* @param bool $fromReplica
* @return AbstractBlock|null
*/
public function getIpBlock( string $ip, bool $fromReplica ): ?AbstractBlock {
if ( !IPUtils::isValid( $ip ) ) {
return null;
}
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
$blocks = array_merge(
DatabaseBlock::newListFromTarget( $ip, $ip, !$fromReplica ),
$this->getSystemIpBlocks( $ip, true )
);
return $this->createGetBlockResult( $ip, $blocks );
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
}
/**
* Get the cookie block, if there is one.
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
*
* @param UserIdentity $user
* @param WebRequest $request
* @return AbstractBlock[]
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
*/
private function getCookieBlock( UserIdentity $user, WebRequest $request ): array {
$cookieBlock = $this->getBlockFromCookieValue( $user, $request );
return $cookieBlock instanceof DatabaseBlock ? [ $cookieBlock ] : [];
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
}
/**
* Get any system blocks against the IP address.
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
*
* @param string $ip
* @param bool $isAnon Whether the user accessing the wiki from the IP address is logged out
* @return AbstractBlock[]
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
*/
private function getSystemIpBlocks( string $ip, bool $isAnon ): array {
$blocks = [];
// Proxy blocking
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
if ( !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) ) {
// Local list
if ( $this->isLocallyBlockedProxy( $ip ) ) {
// @phan-suppress-next-line SecurityCheck-DoubleEscaped
$blocks[] = new SystemBlock( [
'reason' => new Message( 'proxyblockreason' ),
'address' => $ip,
'systemBlock' => 'proxy',
] );
} elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
// @phan-suppress-next-line SecurityCheck-DoubleEscaped
$blocks[] = new SystemBlock( [
'reason' => new Message( 'sorbsreason' ),
'address' => $ip,
'anonOnly' => true,
'systemBlock' => 'dnsbl',
] );
}
}
// Soft blocking
if ( $isAnon && IPUtils::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) ) ) {
// @phan-suppress-next-line SecurityCheck-DoubleEscaped
$blocks[] = new SystemBlock( [
'address' => $ip,
'reason' => new Message( 'softblockrangesreason', [ $ip ] ),
'anonOnly' => true,
'systemBlock' => 'wgSoftBlockRanges',
] );
}
return $blocks;
}
/**
* If `$wgApplyIpBlocksToXff` is truthy and the IP that the user is accessing the wiki from is not in
* `$wgProxyWhitelist`, then get the blocks that apply to the IP(s) in the X-Forwarded-For HTTP
* header.
*
* @param string $ip
* @param string $xff
* @param bool $isAnon
* @param bool $fromPrimary
* @return AbstractBlock[]
*/
private function getXffBlocks( string $ip, string $xff, bool $isAnon, bool $fromPrimary ): array {
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
// (T25343) Apply IP blocks to the contents of XFF headers, if enabled
if ( $this->options->get( 'ApplyIpBlocksToXff' )
&& !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) )
) {
$xff = array_map( 'trim', explode( ',', $xff ) );
$xff = array_diff( $xff, [ $ip ] );
// TODO: remove dependency on DatabaseBlock (T221075)
return DatabaseBlock::getBlocksForIPList( $xff, $isAnon, $fromPrimary );
}
return [];
}
/**
* Given a list of blocks, return a list of unique blocks.
*
* This usually means that each block has a unique ID. For a block with ID null,
* if it's an autoblock, it will be filtered out if the parent block is present;
* if not, it is assumed to be a unique system block, and kept.
*
* @param AbstractBlock[] $blocks
* @return AbstractBlock[]
*/
private function getUniqueBlocks( array $blocks ) {
$systemBlocks = [];
$databaseBlocks = [];
foreach ( $blocks as $block ) {
if ( $block instanceof SystemBlock ) {
$systemBlocks[] = $block;
} elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
/** @var DatabaseBlock $block */
'@phan-var DatabaseBlock $block';
if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
$databaseBlocks[$block->getParentBlockId()] = $block;
}
} else {
$databaseBlocks[$block->getId()] = $block;
}
}
return array_values( array_merge( $systemBlocks, $databaseBlocks ) );
}
/**
* Try to load a block from an ID given in a cookie value.
*
* If the block is invalid, doesn't exist, or the cookie value is malformed, no
* block will be loaded. In these cases the cookie will either (1) be replaced
* with a valid cookie or (2) removed, next time trackBlockWithCookie is called.
*
* @param UserIdentity $user
* @param WebRequest $request
* @return DatabaseBlock|bool The block object, or false if none could be loaded.
*/
private function getBlockFromCookieValue(
UserIdentity $user,
WebRequest $request
) {
$cookieValue = $request->getCookie( 'BlockID' );
if ( $cookieValue === null ) {
return false;
}
$blockCookieId = $this->getIdFromCookieValue( $cookieValue );
if ( $blockCookieId !== null ) {
Pass the user and request into BlockManager::getUserBlock Blocks are checked from the User object. Specifically, User::getBlockedStatus instantiates a BlockManager and calls BlockManager::getUserBlock. However, checking the block often depends on knowing more about the state than the User should know. As a result, the global user and request objects were passed into the block manager on construction. Whether the global request object should be passed into a service constructor is still up for debate, so this moves the check for the global state back to User::getBlockedStatus for now. (Note that it reintroduces the problem of the User knowing more about state than it should.) This change also makes clearer the cases in which BlockManager::getUserBlock is called from the User. Different blocks may be sought, depending on the user and their permissions. The user may be: (1) The global user (and can be affected by IP blocks). The global request object is needed for checking the IP address, the XFF header and the cookies. (2) The global user (and exempt from IP blocks). The global request object is needed for checking the cookies. (3) Another user (not the global user). No request object is available or needed; just look for a block against the user account. Cases #1 and #2 check whether the global user is blocked in practice; the block may due to their user account being blocked or to an IP address block or cookie block (or multiple of these). Case #3 simply checks whether a user's account is blocked, and does not determine whether the person using that account is affected in practice by any IP address or cookie blocks. Bug: T231919 Change-Id: I3f51fd3579514b83b567dfe20926df2f0930dc85
2019-08-23 16:11:45 +00:00
// TODO: remove dependency on DatabaseBlock (T221075)
$block = DatabaseBlock::newFromID( $blockCookieId );
if (
$block instanceof DatabaseBlock &&
$this->shouldApplyCookieBlock( $block, !$user->isRegistered() )
) {
return $block;
}
}
return false;
}
/**
* Check if the block loaded from the cookie should be applied.
*
* @param DatabaseBlock $block
* @param bool $isAnon The user is logged out
* @return bool The block should be applied
*/
private function shouldApplyCookieBlock( DatabaseBlock $block, $isAnon ) {
if ( !$block->isExpired() ) {
switch ( $block->getType() ) {
case DatabaseBlock::TYPE_IP:
case DatabaseBlock::TYPE_RANGE:
// If block is type IP or IP range, load only
// if user is not logged in (T152462)
return $isAnon &&
$this->options->get( 'CookieSetOnIpBlock' );
case DatabaseBlock::TYPE_USER:
return $block->isAutoblocking() &&
$this->options->get( 'CookieSetOnAutoblock' );
default:
return false;
}
}
return false;
}
/**
* Check if an IP address is in the local proxy list
*
* @param string $ip
* @return bool
*/
private function isLocallyBlockedProxy( $ip ) {
$proxyList = $this->options->get( 'ProxyList' );
if ( !$proxyList ) {
return false;
}
if ( !is_array( $proxyList ) ) {
// Load values from the specified file
$proxyList = array_map( 'trim', file( $proxyList ) );
}
$proxyListIPSet = new IPSet( $proxyList );
return $proxyListIPSet->match( $ip );
}
/**
* Whether the given IP is in a DNS blacklist.
*
* @param string $ip IP to check
* @param bool $checkAllowed Whether to check $wgProxyWhitelist first
* @return bool True if blacklisted.
*/
public function isDnsBlacklisted( $ip, $checkAllowed = false ) {
if ( !$this->options->get( 'EnableDnsBlacklist' ) ||
( $checkAllowed && in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) )
) {
return false;
}
return $this->inDnsBlacklist( $ip, $this->options->get( 'DnsBlacklistUrls' ) );
}
/**
* Whether the given IP is in a given DNS blacklist.
*
* @param string $ip IP to check
* @param string[] $bases URL of the DNS blacklist
* @return bool True if blacklisted.
*/
private function inDnsBlacklist( $ip, array $bases ) {
$found = false;
// @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
if ( IPUtils::isIPv4( $ip ) ) {
// Reverse IP, T23255
$ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
foreach ( $bases as $base ) {
// Make hostname
// If we have an access key, use that too (ProjectHoneypot, etc.)
$basename = $base;
if ( is_array( $base ) ) {
if ( count( $base ) >= 2 ) {
// Access key is 1, base URL is 0
$hostname = "{$base[1]}.$ipReversed.{$base[0]}";
} else {
$hostname = "$ipReversed.{$base[0]}";
}
$basename = $base[0];
} else {
$hostname = "$ipReversed.$base";
}
// Send query
$ipList = $this->checkHost( $hostname );
if ( $ipList ) {
$this->logger->info(
'Hostname {hostname} is {ipList}, it\'s a proxy says {basename}!',
[
'hostname' => $hostname,
'ipList' => $ipList[0],
'basename' => $basename,
]
);
$found = true;
break;
}
$this->logger->debug( "Requested $hostname, not found in $basename." );
}
}
return $found;
}
/**
* Wrapper for mocking in tests.
*
* @param string $hostname DNSBL query
* @return string[]|bool IPv4 array, or false if the IP is not blacklisted
*/
protected function checkHost( $hostname ) {
return gethostbynamel( $hostname );
}
/**
* Set the 'BlockID' cookie depending on block type and user authentication status.
*
* If a block cookie is already set, this will check the block that the cookie references
* and do the following:
* - If the block is a valid block that should be applied, do nothing and return early.
* This ensures that the cookie's expiry time is based on the time of the first page
* load or attempt. (See discussion on T233595.)
* - If the block is invalid (e.g. has expired), clear the cookie and continue to check
* whether there is another block that should be tracked.
* - If the block is a valid block, but should not be tracked by a cookie, clear the
* cookie and continue to check whether there is another block that should be tracked.
*
* @since 1.34
* @param User $user
block: Allow cookie-block tracking from any uncached web request This was previously hardcoded from three places: 1) Upon viewing EditPage, 2) Upon viewing SpecialCreateAccount, 3) For any url if the user is logged-in (User::loadFromSession/isLoggedIn). == User::loadFromSession Performing cookie blocks from here created a circular dependency because Block may need the user language for localisation, which is determined by asking the User object. This was previously worked around by using a DeferredUpdate (T180050, T226777). Moving this logic explicitly to the end of the pre-send cycle in MediaWiki::preOutputCommit breaks the cycle. This is also where other request-specific handling resides already. == Limited effect on unregistered users When an unregistered user performs an edit, and gets blocked, the cookie block is not applied until they open built-in editor or CreateAccount page. This makes it more likely for a user's IP to change meanwhile. Either intentionally, or simply due to IPs varying naturally (e.g. between mobile locations, or when going on/off WiFi). By applying it throughout sessioned page views for unregistered users, it is more likely to get set. Similar to what was already done for logged-in users. This commit also makes the intent of not caching EditPage and SpecialCreateAccount explicit. This was previously implicit through nothing having called setCdnMaxage() and/or due to Session::persist being checked for by OutputPage::sendCacheControl. Bug: T233594 Change-Id: Icf5a00f9b41d31bb6d4742c049feca0039d0c9d9
2019-09-07 23:44:46 +00:00
* @param WebResponse $response The response on which to set the cookie.
* @throws LogicException If called before the User object was loaded.
* @throws LogicException If not called pre-send.
*/
block: Allow cookie-block tracking from any uncached web request This was previously hardcoded from three places: 1) Upon viewing EditPage, 2) Upon viewing SpecialCreateAccount, 3) For any url if the user is logged-in (User::loadFromSession/isLoggedIn). == User::loadFromSession Performing cookie blocks from here created a circular dependency because Block may need the user language for localisation, which is determined by asking the User object. This was previously worked around by using a DeferredUpdate (T180050, T226777). Moving this logic explicitly to the end of the pre-send cycle in MediaWiki::preOutputCommit breaks the cycle. This is also where other request-specific handling resides already. == Limited effect on unregistered users When an unregistered user performs an edit, and gets blocked, the cookie block is not applied until they open built-in editor or CreateAccount page. This makes it more likely for a user's IP to change meanwhile. Either intentionally, or simply due to IPs varying naturally (e.g. between mobile locations, or when going on/off WiFi). By applying it throughout sessioned page views for unregistered users, it is more likely to get set. Similar to what was already done for logged-in users. This commit also makes the intent of not caching EditPage and SpecialCreateAccount explicit. This was previously implicit through nothing having called setCdnMaxage() and/or due to Session::persist being checked for by OutputPage::sendCacheControl. Bug: T233594 Change-Id: Icf5a00f9b41d31bb6d4742c049feca0039d0c9d9
2019-09-07 23:44:46 +00:00
public function trackBlockWithCookie( User $user, WebResponse $response ) {
$request = $user->getRequest();
if ( $request->getCookie( 'BlockID' ) !== null ) {
$cookieBlock = $this->getBlockFromCookieValue( $user, $request );
if ( $cookieBlock && $this->shouldApplyCookieBlock( $cookieBlock, $user->isAnon() ) ) {
return;
}
// The block pointed to by the cookie is invalid or should not be tracked.
$this->clearBlockCookie( $response );
}
block: Allow cookie-block tracking from any uncached web request This was previously hardcoded from three places: 1) Upon viewing EditPage, 2) Upon viewing SpecialCreateAccount, 3) For any url if the user is logged-in (User::loadFromSession/isLoggedIn). == User::loadFromSession Performing cookie blocks from here created a circular dependency because Block may need the user language for localisation, which is determined by asking the User object. This was previously worked around by using a DeferredUpdate (T180050, T226777). Moving this logic explicitly to the end of the pre-send cycle in MediaWiki::preOutputCommit breaks the cycle. This is also where other request-specific handling resides already. == Limited effect on unregistered users When an unregistered user performs an edit, and gets blocked, the cookie block is not applied until they open built-in editor or CreateAccount page. This makes it more likely for a user's IP to change meanwhile. Either intentionally, or simply due to IPs varying naturally (e.g. between mobile locations, or when going on/off WiFi). By applying it throughout sessioned page views for unregistered users, it is more likely to get set. Similar to what was already done for logged-in users. This commit also makes the intent of not caching EditPage and SpecialCreateAccount explicit. This was previously implicit through nothing having called setCdnMaxage() and/or due to Session::persist being checked for by OutputPage::sendCacheControl. Bug: T233594 Change-Id: Icf5a00f9b41d31bb6d4742c049feca0039d0c9d9
2019-09-07 23:44:46 +00:00
if ( !$user->isSafeToLoad() ) {
// Prevent a circular dependency by not allowing this method to be called
// before or while the user is being loaded.
// E.g. User > BlockManager > Block > Message > getLanguage > User.
// See also T180050 and T226777.
throw new LogicException( __METHOD__ . ' requires a loaded User object' );
}
if ( $response->headersSent() ) {
throw new LogicException( __METHOD__ . ' must be called pre-send' );
}
$block = $user->getBlock();
$isAnon = $user->isAnon();
if ( $block ) {
if ( $block instanceof CompositeBlock ) {
// TODO: Improve on simply tracking the first trackable block (T225654)
foreach ( $block->getOriginalBlocks() as $originalBlock ) {
if ( $this->shouldTrackBlockWithCookie( $originalBlock, $isAnon ) ) {
'@phan-var DatabaseBlock $originalBlock';
$this->setBlockCookie( $originalBlock, $response );
return;
}
}
block: Allow cookie-block tracking from any uncached web request This was previously hardcoded from three places: 1) Upon viewing EditPage, 2) Upon viewing SpecialCreateAccount, 3) For any url if the user is logged-in (User::loadFromSession/isLoggedIn). == User::loadFromSession Performing cookie blocks from here created a circular dependency because Block may need the user language for localisation, which is determined by asking the User object. This was previously worked around by using a DeferredUpdate (T180050, T226777). Moving this logic explicitly to the end of the pre-send cycle in MediaWiki::preOutputCommit breaks the cycle. This is also where other request-specific handling resides already. == Limited effect on unregistered users When an unregistered user performs an edit, and gets blocked, the cookie block is not applied until they open built-in editor or CreateAccount page. This makes it more likely for a user's IP to change meanwhile. Either intentionally, or simply due to IPs varying naturally (e.g. between mobile locations, or when going on/off WiFi). By applying it throughout sessioned page views for unregistered users, it is more likely to get set. Similar to what was already done for logged-in users. This commit also makes the intent of not caching EditPage and SpecialCreateAccount explicit. This was previously implicit through nothing having called setCdnMaxage() and/or due to Session::persist being checked for by OutputPage::sendCacheControl. Bug: T233594 Change-Id: Icf5a00f9b41d31bb6d4742c049feca0039d0c9d9
2019-09-07 23:44:46 +00:00
} else {
if ( $this->shouldTrackBlockWithCookie( $block, $isAnon ) ) {
'@phan-var DatabaseBlock $block';
$this->setBlockCookie( $block, $response );
}
}
}
}
/**
* Set the 'BlockID' cookie to this block's ID and expiry time. The cookie's expiry will be
* the same as the block's, to a maximum of 24 hours.
*
* @since 1.34
* @internal Should be private.
* Left public for backwards compatibility, until DatabaseBlock::setCookie is removed.
* @param DatabaseBlock $block
* @param WebResponse $response The response on which to set the cookie.
*/
public function setBlockCookie( DatabaseBlock $block, WebResponse $response ) {
// Calculate the default expiry time.
$maxExpiryTime = wfTimestamp( TS_MW, (int)wfTimestamp() + ( 24 * 60 * 60 ) );
// Use the block's expiry time only if it's less than the default.
$expiryTime = $block->getExpiry();
if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
$expiryTime = $maxExpiryTime;
}
// Set the cookie
$expiryValue = (int)wfTimestamp( TS_UNIX, $expiryTime );
$cookieOptions = [ 'httpOnly' => false ];
$cookieValue = $this->getCookieValue( $block );
$response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
}
/**
* Check if the block should be tracked with a cookie.
*
* @param AbstractBlock $block
* @param bool $isAnon The user is logged out
* @return bool The block should be tracked with a cookie
*/
private function shouldTrackBlockWithCookie( AbstractBlock $block, $isAnon ) {
if ( $block instanceof DatabaseBlock ) {
switch ( $block->getType() ) {
case DatabaseBlock::TYPE_IP:
case DatabaseBlock::TYPE_RANGE:
return $isAnon && $this->options->get( 'CookieSetOnIpBlock' );
case DatabaseBlock::TYPE_USER:
return !$isAnon &&
$this->options->get( 'CookieSetOnAutoblock' ) &&
$block->isAutoblocking();
default:
return false;
}
}
return false;
}
/**
* Unset the 'BlockID' cookie.
*
* @since 1.34
* @param WebResponse $response
*/
public static function clearBlockCookie( WebResponse $response ) {
$response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
}
/**
* Get the stored ID from the 'BlockID' cookie. The cookie's value is usually a combination of
* the ID and a HMAC (see DatabaseBlock::setCookie), but will sometimes only be the ID.
*
* @since 1.34
* @internal Should be private.
* Left public for backwards compatibility, until DatabaseBlock::getIdFromCookieValue is removed.
* @param string $cookieValue The string in which to find the ID.
* @return int|null The block ID, or null if the HMAC is present and invalid.
*/
public function getIdFromCookieValue( $cookieValue ) {
// The cookie value must start with a number
if ( !is_numeric( substr( $cookieValue, 0, 1 ) ) ) {
return null;
}
// Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
$bangPos = strpos( $cookieValue, '!' );
$id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
if ( !$this->options->get( 'SecretKey' ) ) {
// If there's no secret key, just use the ID as given.
return (int)$id;
}
$storedHmac = substr( $cookieValue, $bangPos + 1 );
$calculatedHmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
if ( $calculatedHmac === $storedHmac ) {
return (int)$id;
} else {
return null;
}
}
/**
* Get the BlockID cookie's value for this block. This is usually the block ID concatenated
* with an HMAC in order to avoid spoofing (T152951), but if wgSecretKey is not set will just
* be the block ID.
*
* @since 1.34
* @internal Should be private.
* Left public for backwards compatibility, until DatabaseBlock::getCookieValue is removed.
* @param DatabaseBlock $block
* @return string The block ID, probably concatenated with "!" and the HMAC.
*/
public function getCookieValue( DatabaseBlock $block ) {
$id = $block->getId();
if ( !$this->options->get( 'SecretKey' ) ) {
// If there's no secret key, don't append a HMAC.
return $id;
}
$hmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
$cookieValue = $id . '!' . $hmac;
return $cookieValue;
}
}