Fix AbstractBlock param types in documentation
[lhc/web/wiklou.git] / includes / block / BlockManager.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21 namespace MediaWiki\Block;
22
23 use Block;
24 use IP;
25 use MediaWiki\User\UserIdentity;
26 use User;
27 use WebRequest;
28 use Wikimedia\IPSet;
29
30 /**
31 * A service class for checking blocks.
32 * To obtain an instance, use MediaWikiServices::getInstance()->getBlockManager().
33 *
34 * @since 1.34 Refactored from User and Block.
35 */
36 class BlockManager {
37 // TODO: This should be UserIdentity instead of User
38 /** @var User */
39 private $currentUser;
40
41 /** @var WebRequest */
42 private $currentRequest;
43
44 /** @var bool */
45 private $applyIpBlocksToXff;
46
47 /** @var bool */
48 private $cookieSetOnAutoblock;
49
50 /** @var bool */
51 private $cookieSetOnIpBlock;
52
53 /** @var array */
54 private $dnsBlacklistUrls;
55
56 /** @var bool */
57 private $enableDnsBlacklist;
58
59 /** @var array */
60 private $proxyList;
61
62 /** @var array */
63 private $proxyWhitelist;
64
65 /** @var array */
66 private $softBlockRanges;
67
68 /**
69 * @param User $currentUser
70 * @param WebRequest $currentRequest
71 * @param bool $applyIpBlocksToXff
72 * @param bool $cookieSetOnAutoblock
73 * @param bool $cookieSetOnIpBlock
74 * @param array $dnsBlacklistUrls
75 * @param bool $enableDnsBlacklist
76 * @param array $proxyList
77 * @param array $proxyWhitelist
78 * @param array $softBlockRanges
79 */
80 public function __construct(
81 $currentUser,
82 $currentRequest,
83 $applyIpBlocksToXff,
84 $cookieSetOnAutoblock,
85 $cookieSetOnIpBlock,
86 $dnsBlacklistUrls,
87 $enableDnsBlacklist,
88 $proxyList,
89 $proxyWhitelist,
90 $softBlockRanges
91 ) {
92 $this->currentUser = $currentUser;
93 $this->currentRequest = $currentRequest;
94 $this->applyIpBlocksToXff = $applyIpBlocksToXff;
95 $this->cookieSetOnAutoblock = $cookieSetOnAutoblock;
96 $this->cookieSetOnIpBlock = $cookieSetOnIpBlock;
97 $this->dnsBlacklistUrls = $dnsBlacklistUrls;
98 $this->enableDnsBlacklist = $enableDnsBlacklist;
99 $this->proxyList = $proxyList;
100 $this->proxyWhitelist = $proxyWhitelist;
101 $this->softBlockRanges = $softBlockRanges;
102 }
103
104 /**
105 * Get the blocks that apply to a user and return the most relevant one.
106 *
107 * TODO: $user should be UserIdentity instead of User
108 *
109 * @internal This should only be called by User::getBlockedStatus
110 * @param User $user
111 * @param bool $fromReplica Whether to check the replica DB first.
112 * To improve performance, non-critical checks are done against replica DBs.
113 * Check when actually saving should be done against master.
114 * @return AbstractBlock|null The most relevant block, or null if there is no block.
115 */
116 public function getUserBlock( User $user, $fromReplica ) {
117 $isAnon = $user->getId() === 0;
118
119 // TODO: If $user is the current user, we should use the current request. Otherwise,
120 // we should not look for XFF or cookie blocks.
121 $request = $user->getRequest();
122
123 # We only need to worry about passing the IP address to the Block generator if the
124 # user is not immune to autoblocks/hardblocks, and they are the current user so we
125 # know which IP address they're actually coming from
126 $ip = null;
127 $sessionUser = $this->currentUser;
128 // the session user is set up towards the end of Setup.php. Until then,
129 // assume it's a logged-out user.
130 $globalUserName = $sessionUser->isSafeToLoad()
131 ? $sessionUser->getName()
132 : IP::sanitizeIP( $this->currentRequest->getIP() );
133 if ( $user->getName() === $globalUserName && !$user->isAllowed( 'ipblock-exempt' ) ) {
134 $ip = $this->currentRequest->getIP();
135 }
136
137 // User/IP blocking
138 // TODO: remove dependency on Block
139 $block = Block::newFromTarget( $user, $ip, !$fromReplica );
140
141 // Cookie blocking
142 if ( !$block instanceof AbstractBlock ) {
143 $block = $this->getBlockFromCookieValue( $user, $request );
144 }
145
146 // Proxy blocking
147 if ( !$block instanceof AbstractBlock
148 && $ip !== null
149 && !in_array( $ip, $this->proxyWhitelist )
150 ) {
151 // Local list
152 if ( $this->isLocallyBlockedProxy( $ip ) ) {
153 $block = new SystemBlock( [
154 'byText' => wfMessage( 'proxyblocker' )->text(),
155 'reason' => wfMessage( 'proxyblockreason' )->plain(),
156 'address' => $ip,
157 'systemBlock' => 'proxy',
158 ] );
159 } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
160 $block = new SystemBlock( [
161 'byText' => wfMessage( 'sorbs' )->text(),
162 'reason' => wfMessage( 'sorbsreason' )->plain(),
163 'address' => $ip,
164 'systemBlock' => 'dnsbl',
165 ] );
166 }
167 }
168
169 // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
170 if ( !$block instanceof AbstractBlock
171 && $this->applyIpBlocksToXff
172 && $ip !== null
173 && !in_array( $ip, $this->proxyWhitelist )
174 ) {
175 $xff = $request->getHeader( 'X-Forwarded-For' );
176 $xff = array_map( 'trim', explode( ',', $xff ) );
177 $xff = array_diff( $xff, [ $ip ] );
178 // TODO: remove dependency on Block
179 $xffblocks = Block::getBlocksForIPList( $xff, $isAnon, !$fromReplica );
180 // TODO: remove dependency on Block
181 $block = Block::chooseBlock( $xffblocks, $xff );
182 if ( $block instanceof AbstractBlock ) {
183 # Mangle the reason to alert the user that the block
184 # originated from matching the X-Forwarded-For header.
185 $block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() );
186 }
187 }
188
189 if ( !$block instanceof AbstractBlock
190 && $ip !== null
191 && $isAnon
192 && IP::isInRanges( $ip, $this->softBlockRanges )
193 ) {
194 $block = new SystemBlock( [
195 'address' => $ip,
196 'byText' => 'MediaWiki default',
197 'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
198 'anonOnly' => true,
199 'systemBlock' => 'wgSoftBlockRanges',
200 ] );
201 }
202
203 return $block;
204 }
205
206 /**
207 * Try to load a Block from an ID given in a cookie value.
208 *
209 * @param UserIdentity $user
210 * @param WebRequest $request
211 * @return Block|bool The Block object, or false if none could be loaded.
212 */
213 private function getBlockFromCookieValue(
214 UserIdentity $user,
215 WebRequest $request
216 ) {
217 $blockCookieVal = $request->getCookie( 'BlockID' );
218 $response = $request->response();
219
220 // Make sure there's something to check. The cookie value must start with a number.
221 if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
222 return false;
223 }
224 // Load the Block from the ID in the cookie.
225 // TODO: remove dependency on Block
226 $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
227 if ( $blockCookieId !== null ) {
228 // An ID was found in the cookie.
229 // TODO: remove dependency on Block
230 $tmpBlock = Block::newFromID( $blockCookieId );
231 if ( $tmpBlock instanceof Block ) {
232 switch ( $tmpBlock->getType() ) {
233 case Block::TYPE_USER:
234 $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking();
235 $useBlockCookie = ( $this->cookieSetOnAutoblock === true );
236 break;
237 case Block::TYPE_IP:
238 case Block::TYPE_RANGE:
239 // If block is type IP or IP range, load only if user is not logged in (T152462)
240 $blockIsValid = !$tmpBlock->isExpired() && $user->getId() === 0;
241 $useBlockCookie = ( $this->cookieSetOnIpBlock === true );
242 break;
243 default:
244 $blockIsValid = false;
245 $useBlockCookie = false;
246 }
247
248 if ( $blockIsValid && $useBlockCookie ) {
249 // Use the block.
250 return $tmpBlock;
251 }
252
253 // If the block is not valid, remove the cookie.
254 // TODO: remove dependency on Block
255 Block::clearCookie( $response );
256 } else {
257 // If the block doesn't exist, remove the cookie.
258 // TODO: remove dependency on Block
259 Block::clearCookie( $response );
260 }
261 }
262 return false;
263 }
264
265 /**
266 * Check if an IP address is in the local proxy list
267 *
268 * @param string $ip
269 * @return bool
270 */
271 private function isLocallyBlockedProxy( $ip ) {
272 if ( !$this->proxyList ) {
273 return false;
274 }
275
276 if ( !is_array( $this->proxyList ) ) {
277 // Load values from the specified file
278 $this->proxyList = array_map( 'trim', file( $this->proxyList ) );
279 }
280
281 $resultProxyList = [];
282 $deprecatedIPEntries = [];
283
284 // backward compatibility: move all ip addresses in keys to values
285 foreach ( $this->proxyList as $key => $value ) {
286 $keyIsIP = IP::isIPAddress( $key );
287 $valueIsIP = IP::isIPAddress( $value );
288 if ( $keyIsIP && !$valueIsIP ) {
289 $deprecatedIPEntries[] = $key;
290 $resultProxyList[] = $key;
291 } elseif ( $keyIsIP && $valueIsIP ) {
292 $deprecatedIPEntries[] = $key;
293 $resultProxyList[] = $key;
294 $resultProxyList[] = $value;
295 } else {
296 $resultProxyList[] = $value;
297 }
298 }
299
300 if ( $deprecatedIPEntries ) {
301 wfDeprecated(
302 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
303 implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
304 }
305
306 $proxyListIPSet = new IPSet( $resultProxyList );
307 return $proxyListIPSet->match( $ip );
308 }
309
310 /**
311 * Whether the given IP is in a DNS blacklist.
312 *
313 * @param string $ip IP to check
314 * @param bool $checkWhitelist Whether to check the whitelist first
315 * @return bool True if blacklisted.
316 */
317 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
318 if ( !$this->enableDnsBlacklist ||
319 ( $checkWhitelist && in_array( $ip, $this->proxyWhitelist ) )
320 ) {
321 return false;
322 }
323
324 return $this->inDnsBlacklist( $ip, $this->dnsBlacklistUrls );
325 }
326
327 /**
328 * Whether the given IP is in a given DNS blacklist.
329 *
330 * @param string $ip IP to check
331 * @param array $bases Array of Strings: URL of the DNS blacklist
332 * @return bool True if blacklisted.
333 */
334 private function inDnsBlacklist( $ip, array $bases ) {
335 $found = false;
336 // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
337 if ( IP::isIPv4( $ip ) ) {
338 // Reverse IP, T23255
339 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
340
341 foreach ( $bases as $base ) {
342 // Make hostname
343 // If we have an access key, use that too (ProjectHoneypot, etc.)
344 $basename = $base;
345 if ( is_array( $base ) ) {
346 if ( count( $base ) >= 2 ) {
347 // Access key is 1, base URL is 0
348 $host = "{$base[1]}.$ipReversed.{$base[0]}";
349 } else {
350 $host = "$ipReversed.{$base[0]}";
351 }
352 $basename = $base[0];
353 } else {
354 $host = "$ipReversed.$base";
355 }
356
357 // Send query
358 $ipList = gethostbynamel( $host );
359
360 if ( $ipList ) {
361 wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
362 $found = true;
363 break;
364 }
365
366 wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
367 }
368 }
369
370 return $found;
371 }
372
373 }