Remove parameter 'options' from hook 'SkinEditSectionLinks'
[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 User;
26 use WebRequest;
27 use Wikimedia\IPSet;
28 use MediaWiki\User\UserIdentity;
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 Block|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 Block ) {
143 $block = $this->getBlockFromCookieValue( $user, $request );
144 }
145
146 // Proxy blocking
147 if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $this->proxyWhitelist ) ) {
148 // Local list
149 if ( $this->isLocallyBlockedProxy( $ip ) ) {
150 $block = new Block( [
151 'byText' => wfMessage( 'proxyblocker' )->text(),
152 'reason' => wfMessage( 'proxyblockreason' )->plain(),
153 'address' => $ip,
154 'systemBlock' => 'proxy',
155 ] );
156 } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
157 $block = new Block( [
158 'byText' => wfMessage( 'sorbs' )->text(),
159 'reason' => wfMessage( 'sorbsreason' )->plain(),
160 'address' => $ip,
161 'systemBlock' => 'dnsbl',
162 ] );
163 }
164 }
165
166 // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
167 if ( !$block instanceof Block
168 && $this->applyIpBlocksToXff
169 && $ip !== null
170 && !in_array( $ip, $this->proxyWhitelist )
171 ) {
172 $xff = $request->getHeader( 'X-Forwarded-For' );
173 $xff = array_map( 'trim', explode( ',', $xff ) );
174 $xff = array_diff( $xff, [ $ip ] );
175 // TODO: remove dependency on Block
176 $xffblocks = Block::getBlocksForIPList( $xff, $isAnon, !$fromReplica );
177 // TODO: remove dependency on Block
178 $block = Block::chooseBlock( $xffblocks, $xff );
179 if ( $block instanceof Block ) {
180 # Mangle the reason to alert the user that the block
181 # originated from matching the X-Forwarded-For header.
182 $block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() );
183 }
184 }
185
186 if ( !$block instanceof Block
187 && $ip !== null
188 && $isAnon
189 && IP::isInRanges( $ip, $this->softBlockRanges )
190 ) {
191 $block = new Block( [
192 'address' => $ip,
193 'byText' => 'MediaWiki default',
194 'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
195 'anonOnly' => true,
196 'systemBlock' => 'wgSoftBlockRanges',
197 ] );
198 }
199
200 return $block;
201 }
202
203 /**
204 * Try to load a Block from an ID given in a cookie value.
205 *
206 * @param UserIdentity $user
207 * @param WebRequest $request
208 * @return Block|bool The Block object, or false if none could be loaded.
209 */
210 private function getBlockFromCookieValue(
211 UserIdentity $user,
212 WebRequest $request
213 ) {
214 $blockCookieVal = $request->getCookie( 'BlockID' );
215 $response = $request->response();
216
217 // Make sure there's something to check. The cookie value must start with a number.
218 if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
219 return false;
220 }
221 // Load the Block from the ID in the cookie.
222 // TODO: remove dependency on Block
223 $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
224 if ( $blockCookieId !== null ) {
225 // An ID was found in the cookie.
226 // TODO: remove dependency on Block
227 $tmpBlock = Block::newFromID( $blockCookieId );
228 if ( $tmpBlock instanceof Block ) {
229 switch ( $tmpBlock->getType() ) {
230 case Block::TYPE_USER:
231 $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking();
232 $useBlockCookie = ( $this->cookieSetOnAutoblock === true );
233 break;
234 case Block::TYPE_IP:
235 case Block::TYPE_RANGE:
236 // If block is type IP or IP range, load only if user is not logged in (T152462)
237 $blockIsValid = !$tmpBlock->isExpired() && $user->getId() === 0;
238 $useBlockCookie = ( $this->cookieSetOnIpBlock === true );
239 break;
240 default:
241 $blockIsValid = false;
242 $useBlockCookie = false;
243 }
244
245 if ( $blockIsValid && $useBlockCookie ) {
246 // Use the block.
247 return $tmpBlock;
248 }
249
250 // If the block is not valid, remove the cookie.
251 // TODO: remove dependency on Block
252 Block::clearCookie( $response );
253 } else {
254 // If the block doesn't exist, remove the cookie.
255 // TODO: remove dependency on Block
256 Block::clearCookie( $response );
257 }
258 }
259 return false;
260 }
261
262 /**
263 * Check if an IP address is in the local proxy list
264 *
265 * @param string $ip
266 * @return bool
267 */
268 private function isLocallyBlockedProxy( $ip ) {
269 if ( !$this->proxyList ) {
270 return false;
271 }
272
273 if ( !is_array( $this->proxyList ) ) {
274 // Load values from the specified file
275 $this->proxyList = array_map( 'trim', file( $this->proxyList ) );
276 }
277
278 $resultProxyList = [];
279 $deprecatedIPEntries = [];
280
281 // backward compatibility: move all ip addresses in keys to values
282 foreach ( $this->proxyList as $key => $value ) {
283 $keyIsIP = IP::isIPAddress( $key );
284 $valueIsIP = IP::isIPAddress( $value );
285 if ( $keyIsIP && !$valueIsIP ) {
286 $deprecatedIPEntries[] = $key;
287 $resultProxyList[] = $key;
288 } elseif ( $keyIsIP && $valueIsIP ) {
289 $deprecatedIPEntries[] = $key;
290 $resultProxyList[] = $key;
291 $resultProxyList[] = $value;
292 } else {
293 $resultProxyList[] = $value;
294 }
295 }
296
297 if ( $deprecatedIPEntries ) {
298 wfDeprecated(
299 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
300 implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
301 }
302
303 $proxyListIPSet = new IPSet( $resultProxyList );
304 return $proxyListIPSet->match( $ip );
305 }
306
307 /**
308 * Whether the given IP is in a DNS blacklist.
309 *
310 * @param string $ip IP to check
311 * @param bool $checkWhitelist Whether to check the whitelist first
312 * @return bool True if blacklisted.
313 */
314 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
315 if ( !$this->enableDnsBlacklist ||
316 ( $checkWhitelist && in_array( $ip, $this->proxyWhitelist ) )
317 ) {
318 return false;
319 }
320
321 return $this->inDnsBlacklist( $ip, $this->dnsBlacklistUrls );
322 }
323
324 /**
325 * Whether the given IP is in a given DNS blacklist.
326 *
327 * @param string $ip IP to check
328 * @param array $bases Array of Strings: URL of the DNS blacklist
329 * @return bool True if blacklisted.
330 */
331 private function inDnsBlacklist( $ip, array $bases ) {
332 $found = false;
333 // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
334 if ( IP::isIPv4( $ip ) ) {
335 // Reverse IP, T23255
336 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
337
338 foreach ( $bases as $base ) {
339 // Make hostname
340 // If we have an access key, use that too (ProjectHoneypot, etc.)
341 $basename = $base;
342 if ( is_array( $base ) ) {
343 if ( count( $base ) >= 2 ) {
344 // Access key is 1, base URL is 0
345 $host = "{$base[1]}.$ipReversed.{$base[0]}";
346 } else {
347 $host = "$ipReversed.{$base[0]}";
348 }
349 $basename = $base[0];
350 } else {
351 $host = "$ipReversed.$base";
352 }
353
354 // Send query
355 $ipList = gethostbynamel( $host );
356
357 if ( $ipList ) {
358 wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
359 $found = true;
360 break;
361 }
362
363 wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
364 }
365 }
366
367 return $found;
368 }
369
370 }