Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/Block.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Blocks and bans object
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
use MediaWiki\MediaWikiServices;
24
25
class Block {
26
	/** @var string */
27
	public $mReason;
28
29
	/** @var string */
30
	public $mTimestamp;
31
32
	/** @var bool */
33
	public $mAuto;
34
35
	/** @var string */
36
	public $mExpiry;
37
38
	/** @var bool */
39
	public $mHideName;
40
41
	/** @var int */
42
	public $mParentBlockId;
43
44
	/** @var int */
45
	protected $mId;
46
47
	/** @var bool */
48
	protected $mFromMaster;
49
50
	/** @var bool */
51
	protected $mBlockEmail;
52
53
	/** @var bool */
54
	protected $mDisableUsertalk;
55
56
	/** @var bool */
57
	protected $mCreateAccount;
58
59
	/** @var User|string */
60
	protected $target;
61
62
	/** @var int Hack for foreign blocking (CentralAuth) */
63
	protected $forcedTargetID;
64
65
	/** @var int Block::TYPE_ constant. Can only be USER, IP or RANGE internally */
66
	protected $type;
67
68
	/** @var User */
69
	protected $blocker;
70
71
	/** @var bool */
72
	protected $isHardblock;
73
74
	/** @var bool */
75
	protected $isAutoblocking;
76
77
	# TYPE constants
78
	const TYPE_USER = 1;
79
	const TYPE_IP = 2;
80
	const TYPE_RANGE = 3;
81
	const TYPE_AUTO = 4;
82
	const TYPE_ID = 5;
83
84
	/**
85
	 * Create a new block with specified parameters on a user, IP or IP range.
86
	 *
87
	 * @param array $options Parameters of the block:
88
	 *     address string|User  Target user name, User object, IP address or IP range
89
	 *     user int             Override target user ID (for foreign users)
90
	 *     by int               User ID of the blocker
91
	 *     reason string        Reason of the block
92
	 *     timestamp string     The time at which the block comes into effect
93
	 *     auto bool            Is this an automatic block?
94
	 *     expiry string        Timestamp of expiration of the block or 'infinity'
95
	 *     anonOnly bool        Only disallow anonymous actions
96
	 *     createAccount bool   Disallow creation of new accounts
97
	 *     enableAutoblock bool Enable automatic blocking
98
	 *     hideName bool        Hide the target user name
99
	 *     blockEmail bool      Disallow sending emails
100
	 *     allowUsertalk bool   Allow the target to edit its own talk page
101
	 *     byText string        Username of the blocker (for foreign users)
102
	 *
103
	 * @since 1.26 accepts $options array instead of individual parameters; order
104
	 * of parameters above reflects the original order
105
	 */
106
	function __construct( $options = [] ) {
107
		$defaults = [
108
			'address'         => '',
109
			'user'            => null,
110
			'by'              => null,
111
			'reason'          => '',
112
			'timestamp'       => '',
113
			'auto'            => false,
114
			'expiry'          => '',
115
			'anonOnly'        => false,
116
			'createAccount'   => false,
117
			'enableAutoblock' => false,
118
			'hideName'        => false,
119
			'blockEmail'      => false,
120
			'allowUsertalk'   => false,
121
			'byText'          => '',
122
		];
123
124
		if ( func_num_args() > 1 || !is_array( $options ) ) {
125
			$options = array_combine(
126
				array_slice( array_keys( $defaults ), 0, func_num_args() ),
127
				func_get_args()
128
			);
129
			wfDeprecated( __METHOD__ . ' with multiple arguments', '1.26' );
130
		}
131
132
		$options += $defaults;
133
134
		$this->setTarget( $options['address'] );
135
136
		if ( $this->target instanceof User && $options['user'] ) {
137
			# Needed for foreign users
138
			$this->forcedTargetID = $options['user'];
139
		}
140
141
		if ( $options['by'] ) {
142
			# Local user
143
			$this->setBlocker( User::newFromId( $options['by'] ) );
144
		} else {
145
			# Foreign user
146
			$this->setBlocker( $options['byText'] );
147
		}
148
149
		$this->mReason = $options['reason'];
150
		$this->mTimestamp = wfTimestamp( TS_MW, $options['timestamp'] );
151
		$this->mExpiry = wfGetDB( DB_REPLICA )->decodeExpiry( $options['expiry'] );
152
153
		# Boolean settings
154
		$this->mAuto = (bool)$options['auto'];
155
		$this->mHideName = (bool)$options['hideName'];
156
		$this->isHardblock( !$options['anonOnly'] );
157
		$this->isAutoblocking( (bool)$options['enableAutoblock'] );
158
159
		# Prevention measures
160
		$this->prevents( 'sendemail', (bool)$options['blockEmail'] );
161
		$this->prevents( 'editownusertalk', !$options['allowUsertalk'] );
162
		$this->prevents( 'createaccount', (bool)$options['createAccount'] );
163
164
		$this->mFromMaster = false;
165
	}
166
167
	/**
168
	 * Load a blocked user from their block id.
169
	 *
170
	 * @param int $id Block id to search for
171
	 * @return Block|null
172
	 */
173
	public static function newFromID( $id ) {
174
		$dbr = wfGetDB( DB_REPLICA );
175
		$res = $dbr->selectRow(
176
			'ipblocks',
177
			self::selectFields(),
178
			[ 'ipb_id' => $id ],
179
			__METHOD__
180
		);
181
		if ( $res ) {
182
			return self::newFromRow( $res );
183
		} else {
184
			return null;
185
		}
186
	}
187
188
	/**
189
	 * Return the list of ipblocks fields that should be selected to create
190
	 * a new block.
191
	 * @return array
192
	 */
193
	public static function selectFields() {
194
		return [
195
			'ipb_id',
196
			'ipb_address',
197
			'ipb_by',
198
			'ipb_by_text',
199
			'ipb_reason',
200
			'ipb_timestamp',
201
			'ipb_auto',
202
			'ipb_anon_only',
203
			'ipb_create_account',
204
			'ipb_enable_autoblock',
205
			'ipb_expiry',
206
			'ipb_deleted',
207
			'ipb_block_email',
208
			'ipb_allow_usertalk',
209
			'ipb_parent_block_id',
210
		];
211
	}
212
213
	/**
214
	 * Check if two blocks are effectively equal.  Doesn't check irrelevant things like
215
	 * the blocking user or the block timestamp, only things which affect the blocked user
216
	 *
217
	 * @param Block $block
218
	 *
219
	 * @return bool
220
	 */
221
	public function equals( Block $block ) {
222
		return (
223
			(string)$this->target == (string)$block->target
224
			&& $this->type == $block->type
225
			&& $this->mAuto == $block->mAuto
226
			&& $this->isHardblock() == $block->isHardblock()
227
			&& $this->prevents( 'createaccount' ) == $block->prevents( 'createaccount' )
228
			&& $this->mExpiry == $block->mExpiry
229
			&& $this->isAutoblocking() == $block->isAutoblocking()
230
			&& $this->mHideName == $block->mHideName
231
			&& $this->prevents( 'sendemail' ) == $block->prevents( 'sendemail' )
232
			&& $this->prevents( 'editownusertalk' ) == $block->prevents( 'editownusertalk' )
233
			&& $this->mReason == $block->mReason
234
		);
235
	}
236
237
	/**
238
	 * Load a block from the database which affects the already-set $this->target:
239
	 *     1) A block directly on the given user or IP
240
	 *     2) A rangeblock encompassing the given IP (smallest first)
241
	 *     3) An autoblock on the given IP
242
	 * @param User|string $vagueTarget Also search for blocks affecting this target.  Doesn't
243
	 *     make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups.
244
	 * @throws MWException
245
	 * @return bool Whether a relevant block was found
246
	 */
247
	protected function newLoad( $vagueTarget = null ) {
248
		$db = wfGetDB( $this->mFromMaster ? DB_MASTER : DB_REPLICA );
249
250
		if ( $this->type !== null ) {
251
			$conds = [
252
				'ipb_address' => [ (string)$this->target ],
253
			];
254
		} else {
255
			$conds = [ 'ipb_address' => [] ];
256
		}
257
258
		# Be aware that the != '' check is explicit, since empty values will be
259
		# passed by some callers (bug 29116)
260
		if ( $vagueTarget != '' ) {
261
			list( $target, $type ) = self::parseTarget( $vagueTarget );
262
			switch ( $type ) {
263
				case self::TYPE_USER:
264
					# Slightly weird, but who are we to argue?
265
					$conds['ipb_address'][] = (string)$target;
266
					break;
267
268
				case self::TYPE_IP:
269
					$conds['ipb_address'][] = (string)$target;
270
					$conds[] = self::getRangeCond( IP::toHex( $target ) );
271
					$conds = $db->makeList( $conds, LIST_OR );
272
					break;
273
274
				case self::TYPE_RANGE:
275
					list( $start, $end ) = IP::parseRange( $target );
276
					$conds['ipb_address'][] = (string)$target;
277
					$conds[] = self::getRangeCond( $start, $end );
278
					$conds = $db->makeList( $conds, LIST_OR );
279
					break;
280
281
				default:
282
					throw new MWException( "Tried to load block with invalid type" );
283
			}
284
		}
285
286
		$res = $db->select( 'ipblocks', self::selectFields(), $conds, __METHOD__ );
287
288
		# This result could contain a block on the user, a block on the IP, and a russian-doll
289
		# set of rangeblocks.  We want to choose the most specific one, so keep a leader board.
290
		$bestRow = null;
291
292
		# Lower will be better
293
		$bestBlockScore = 100;
294
295
		# This is begging for $this = $bestBlock, but that's not allowed in PHP :(
296
		$bestBlockPreventsEdit = null;
297
298
		foreach ( $res as $row ) {
299
			$block = self::newFromRow( $row );
300
301
			# Don't use expired blocks
302
			if ( $block->isExpired() ) {
303
				continue;
304
			}
305
306
			# Don't use anon only blocks on users
307
			if ( $this->type == self::TYPE_USER && !$block->isHardblock() ) {
308
				continue;
309
			}
310
311
			if ( $block->getType() == self::TYPE_RANGE ) {
312
				# This is the number of bits that are allowed to vary in the block, give
313
				# or take some floating point errors
314
				$end = Wikimedia\base_convert( $block->getRangeEnd(), 16, 10 );
315
				$start = Wikimedia\base_convert( $block->getRangeStart(), 16, 10 );
316
				$size = log( $end - $start + 1, 2 );
317
318
				# This has the nice property that a /32 block is ranked equally with a
319
				# single-IP block, which is exactly what it is...
320
				$score = self::TYPE_RANGE - 1 + ( $size / 128 );
321
322
			} else {
323
				$score = $block->getType();
324
			}
325
326
			if ( $score < $bestBlockScore ) {
327
				$bestBlockScore = $score;
328
				$bestRow = $row;
329
				$bestBlockPreventsEdit = $block->prevents( 'edit' );
330
			}
331
		}
332
333
		if ( $bestRow !== null ) {
334
			$this->initFromRow( $bestRow );
335
			$this->prevents( 'edit', $bestBlockPreventsEdit );
336
			return true;
337
		} else {
338
			return false;
339
		}
340
	}
341
342
	/**
343
	 * Get a set of SQL conditions which will select rangeblocks encompassing a given range
344
	 * @param string $start Hexadecimal IP representation
345
	 * @param string $end Hexadecimal IP representation, or null to use $start = $end
346
	 * @return string
347
	 */
348
	public static function getRangeCond( $start, $end = null ) {
349
		if ( $end === null ) {
350
			$end = $start;
351
		}
352
		# Per bug 14634, we want to include relevant active rangeblocks; for
353
		# rangeblocks, we want to include larger ranges which enclose the given
354
		# range. We know that all blocks must be smaller than $wgBlockCIDRLimit,
355
		# so we can improve performance by filtering on a LIKE clause
356
		$chunk = self::getIpFragment( $start );
357
		$dbr = wfGetDB( DB_REPLICA );
358
		$like = $dbr->buildLike( $chunk, $dbr->anyString() );
359
360
		# Fairly hard to make a malicious SQL statement out of hex characters,
361
		# but stranger things have happened...
362
		$safeStart = $dbr->addQuotes( $start );
363
		$safeEnd = $dbr->addQuotes( $end );
364
365
		return $dbr->makeList(
366
			[
367
				"ipb_range_start $like",
368
				"ipb_range_start <= $safeStart",
369
				"ipb_range_end >= $safeEnd",
370
			],
371
			LIST_AND
372
		);
373
	}
374
375
	/**
376
	 * Get the component of an IP address which is certain to be the same between an IP
377
	 * address and a rangeblock containing that IP address.
378
	 * @param string $hex Hexadecimal IP representation
379
	 * @return string
380
	 */
381
	protected static function getIpFragment( $hex ) {
382
		global $wgBlockCIDRLimit;
383
		if ( substr( $hex, 0, 3 ) == 'v6-' ) {
384
			return 'v6-' . substr( substr( $hex, 3 ), 0, floor( $wgBlockCIDRLimit['IPv6'] / 4 ) );
385
		} else {
386
			return substr( $hex, 0, floor( $wgBlockCIDRLimit['IPv4'] / 4 ) );
387
		}
388
	}
389
390
	/**
391
	 * Given a database row from the ipblocks table, initialize
392
	 * member variables
393
	 * @param stdClass $row A row from the ipblocks table
394
	 */
395
	protected function initFromRow( $row ) {
396
		$this->setTarget( $row->ipb_address );
397
		if ( $row->ipb_by ) { // local user
398
			$this->setBlocker( User::newFromId( $row->ipb_by ) );
399
		} else { // foreign user
400
			$this->setBlocker( $row->ipb_by_text );
401
		}
402
403
		$this->mReason = $row->ipb_reason;
404
		$this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp );
405
		$this->mAuto = $row->ipb_auto;
406
		$this->mHideName = $row->ipb_deleted;
407
		$this->mId = (int)$row->ipb_id;
408
		$this->mParentBlockId = $row->ipb_parent_block_id;
409
410
		// I wish I didn't have to do this
411
		$this->mExpiry = wfGetDB( DB_REPLICA )->decodeExpiry( $row->ipb_expiry );
412
413
		$this->isHardblock( !$row->ipb_anon_only );
414
		$this->isAutoblocking( $row->ipb_enable_autoblock );
415
416
		$this->prevents( 'createaccount', $row->ipb_create_account );
417
		$this->prevents( 'sendemail', $row->ipb_block_email );
418
		$this->prevents( 'editownusertalk', !$row->ipb_allow_usertalk );
419
	}
420
421
	/**
422
	 * Create a new Block object from a database row
423
	 * @param stdClass $row Row from the ipblocks table
424
	 * @return Block
425
	 */
426
	public static function newFromRow( $row ) {
427
		$block = new Block;
428
		$block->initFromRow( $row );
429
		return $block;
430
	}
431
432
	/**
433
	 * Delete the row from the IP blocks table.
434
	 *
435
	 * @throws MWException
436
	 * @return bool
437
	 */
438
	public function delete() {
439
		if ( wfReadOnly() ) {
440
			return false;
441
		}
442
443
		if ( !$this->getId() ) {
444
			throw new MWException( "Block::delete() requires that the mId member be filled\n" );
445
		}
446
447
		$dbw = wfGetDB( DB_MASTER );
448
		$dbw->delete( 'ipblocks', [ 'ipb_parent_block_id' => $this->getId() ], __METHOD__ );
449
		$dbw->delete( 'ipblocks', [ 'ipb_id' => $this->getId() ], __METHOD__ );
450
451
		return $dbw->affectedRows() > 0;
452
	}
453
454
	/**
455
	 * Insert a block into the block table. Will fail if there is a conflicting
456
	 * block (same name and options) already in the database.
457
	 *
458
	 * @param IDatabase $dbw If you have one available
459
	 * @return bool|array False on failure, assoc array on success:
460
	 *	('id' => block ID, 'autoIds' => array of autoblock IDs)
461
	 */
462
	public function insert( $dbw = null ) {
463
		global $wgBlockDisablesLogin;
464
		wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" );
465
466
		if ( $dbw === null ) {
467
			$dbw = wfGetDB( DB_MASTER );
468
		}
469
470
		# Periodic purge via commit hooks
471
		if ( mt_rand( 0, 9 ) == 0 ) {
472
			Block::purgeExpired();
473
		}
474
475
		$row = $this->getDatabaseArray();
476
		$row['ipb_id'] = $dbw->nextSequenceValue( "ipblocks_ipb_id_seq" );
477
478
		$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
479
		$affected = $dbw->affectedRows();
480
		$this->mId = $dbw->insertId();
481
482
		# Don't collide with expired blocks.
483
		# Do this after trying to insert to avoid locking.
484
		if ( !$affected ) {
485
			# T96428: The ipb_address index uses a prefix on a field, so
486
			# use a standard SELECT + DELETE to avoid annoying gap locks.
487
			$ids = $dbw->selectFieldValues( 'ipblocks',
488
				'ipb_id',
489
				[
490
					'ipb_address' => $row['ipb_address'],
491
					'ipb_user' => $row['ipb_user'],
492
					'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() )
493
				],
494
				__METHOD__
495
			);
496
			if ( $ids ) {
497
				$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ );
498
				$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
499
				$affected = $dbw->affectedRows();
500
				$this->mId = $dbw->insertId();
501
			}
502
		}
503
504
		if ( $affected ) {
505
			$auto_ipd_ids = $this->doRetroactiveAutoblock();
506
507
			if ( $wgBlockDisablesLogin && $this->target instanceof User ) {
508
				// Change user login token to force them to be logged out.
509
				$this->target->setToken();
510
				$this->target->saveSettings();
511
			}
512
513
			return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
514
		}
515
516
		return false;
517
	}
518
519
	/**
520
	 * Update a block in the DB with new parameters.
521
	 * The ID field needs to be loaded first.
522
	 *
523
	 * @return bool|array False on failure, array on success:
524
	 *   ('id' => block ID, 'autoIds' => array of autoblock IDs)
525
	 */
526
	public function update() {
527
		wfDebug( "Block::update; timestamp {$this->mTimestamp}\n" );
528
		$dbw = wfGetDB( DB_MASTER );
529
530
		$dbw->startAtomic( __METHOD__ );
531
532
		$dbw->update(
533
			'ipblocks',
534
			$this->getDatabaseArray( $dbw ),
535
			[ 'ipb_id' => $this->getId() ],
536
			__METHOD__
537
		);
538
539
		$affected = $dbw->affectedRows();
540
541
		if ( $this->isAutoblocking() ) {
542
			// update corresponding autoblock(s) (bug 48813)
543
			$dbw->update(
544
				'ipblocks',
545
				$this->getAutoblockUpdateArray(),
546
				[ 'ipb_parent_block_id' => $this->getId() ],
547
				__METHOD__
548
			);
549
		} else {
550
			// autoblock no longer required, delete corresponding autoblock(s)
551
			$dbw->delete(
552
				'ipblocks',
553
				[ 'ipb_parent_block_id' => $this->getId() ],
554
				__METHOD__
555
			);
556
		}
557
558
		$dbw->endAtomic( __METHOD__ );
559
560
		if ( $affected ) {
561
			$auto_ipd_ids = $this->doRetroactiveAutoblock();
562
			return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
563
		}
564
565
		return false;
566
	}
567
568
	/**
569
	 * Get an array suitable for passing to $dbw->insert() or $dbw->update()
570
	 * @param IDatabase $db
571
	 * @return array
572
	 */
573
	protected function getDatabaseArray( $db = null ) {
574
		if ( !$db ) {
575
			$db = wfGetDB( DB_REPLICA );
576
		}
577
		$expiry = $db->encodeExpiry( $this->mExpiry );
578
579
		if ( $this->forcedTargetID ) {
580
			$uid = $this->forcedTargetID;
581
		} else {
582
			$uid = $this->target instanceof User ? $this->target->getId() : 0;
583
		}
584
585
		$a = [
586
			'ipb_address'          => (string)$this->target,
587
			'ipb_user'             => $uid,
588
			'ipb_by'               => $this->getBy(),
589
			'ipb_by_text'          => $this->getByName(),
590
			'ipb_reason'           => $this->mReason,
591
			'ipb_timestamp'        => $db->timestamp( $this->mTimestamp ),
592
			'ipb_auto'             => $this->mAuto,
593
			'ipb_anon_only'        => !$this->isHardblock(),
594
			'ipb_create_account'   => $this->prevents( 'createaccount' ),
595
			'ipb_enable_autoblock' => $this->isAutoblocking(),
596
			'ipb_expiry'           => $expiry,
597
			'ipb_range_start'      => $this->getRangeStart(),
598
			'ipb_range_end'        => $this->getRangeEnd(),
599
			'ipb_deleted'          => intval( $this->mHideName ), // typecast required for SQLite
600
			'ipb_block_email'      => $this->prevents( 'sendemail' ),
601
			'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
602
			'ipb_parent_block_id'  => $this->mParentBlockId
603
		];
604
605
		return $a;
606
	}
607
608
	/**
609
	 * @return array
610
	 */
611
	protected function getAutoblockUpdateArray() {
612
		return [
613
			'ipb_by'               => $this->getBy(),
614
			'ipb_by_text'          => $this->getByName(),
615
			'ipb_reason'           => $this->mReason,
616
			'ipb_create_account'   => $this->prevents( 'createaccount' ),
617
			'ipb_deleted'          => (int)$this->mHideName, // typecast required for SQLite
618
			'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
619
		];
620
	}
621
622
	/**
623
	 * Retroactively autoblocks the last IP used by the user (if it is a user)
624
	 * blocked by this Block.
625
	 *
626
	 * @return array Block IDs of retroactive autoblocks made
627
	 */
628
	protected function doRetroactiveAutoblock() {
629
		$blockIds = [];
630
		# If autoblock is enabled, autoblock the LAST IP(s) used
631
		if ( $this->isAutoblocking() && $this->getType() == self::TYPE_USER ) {
632
			wfDebug( "Doing retroactive autoblocks for " . $this->getTarget() . "\n" );
633
634
			$continue = Hooks::run(
635
				'PerformRetroactiveAutoblock', [ $this, &$blockIds ] );
636
637
			if ( $continue ) {
638
				self::defaultRetroactiveAutoblock( $this, $blockIds );
639
			}
640
		}
641
		return $blockIds;
642
	}
643
644
	/**
645
	 * Retroactively autoblocks the last IP used by the user (if it is a user)
646
	 * blocked by this Block. This will use the recentchanges table.
647
	 *
648
	 * @param Block $block
649
	 * @param array &$blockIds
650
	 */
651
	protected static function defaultRetroactiveAutoblock( Block $block, array &$blockIds ) {
652
		global $wgPutIPinRC;
653
654
		// No IPs are in recentchanges table, so nothing to select
655
		if ( !$wgPutIPinRC ) {
656
			return;
657
		}
658
659
		$dbr = wfGetDB( DB_REPLICA );
660
661
		$options = [ 'ORDER BY' => 'rc_timestamp DESC' ];
662
		$conds = [ 'rc_user_text' => (string)$block->getTarget() ];
663
664
		// Just the last IP used.
665
		$options['LIMIT'] = 1;
666
667
		$res = $dbr->select( 'recentchanges', [ 'rc_ip' ], $conds,
668
			__METHOD__, $options );
669
670
		if ( !$res->numRows() ) {
671
			# No results, don't autoblock anything
672
			wfDebug( "No IP found to retroactively autoblock\n" );
673
		} else {
674
			foreach ( $res as $row ) {
675
				if ( $row->rc_ip ) {
676
					$id = $block->doAutoblock( $row->rc_ip );
677
					if ( $id ) {
678
						$blockIds[] = $id;
679
					}
680
				}
681
			}
682
		}
683
	}
684
685
	/**
686
	 * Checks whether a given IP is on the autoblock whitelist.
687
	 * TODO: this probably belongs somewhere else, but not sure where...
688
	 *
689
	 * @param string $ip The IP to check
690
	 * @return bool
691
	 */
692
	public static function isWhitelistedFromAutoblocks( $ip ) {
693
		// Try to get the autoblock_whitelist from the cache, as it's faster
694
		// than getting the msg raw and explode()'ing it.
695
		$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
696
		$lines = $cache->getWithSetCallback(
697
			wfMemcKey( 'ipb', 'autoblock', 'whitelist' ),
698
			$cache::TTL_DAY,
699
			function ( $curValue, &$ttl, array &$setOpts ) {
700
				$setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
701
702
				return explode( "\n",
703
					wfMessage( 'autoblock_whitelist' )->inContentLanguage()->plain() );
704
			}
705
		);
706
707
		wfDebug( "Checking the autoblock whitelist..\n" );
708
709
		foreach ( $lines as $line ) {
710
			# List items only
711
			if ( substr( $line, 0, 1 ) !== '*' ) {
712
				continue;
713
			}
714
715
			$wlEntry = substr( $line, 1 );
716
			$wlEntry = trim( $wlEntry );
717
718
			wfDebug( "Checking $ip against $wlEntry..." );
719
720
			# Is the IP in this range?
721
			if ( IP::isInRange( $ip, $wlEntry ) ) {
722
				wfDebug( " IP $ip matches $wlEntry, not autoblocking\n" );
723
				return true;
724
			} else {
725
				wfDebug( " No match\n" );
726
			}
727
		}
728
729
		return false;
730
	}
731
732
	/**
733
	 * Autoblocks the given IP, referring to this Block.
734
	 *
735
	 * @param string $autoblockIP The IP to autoblock.
736
	 * @return int|bool Block ID if an autoblock was inserted, false if not.
737
	 */
738
	public function doAutoblock( $autoblockIP ) {
739
		# If autoblocks are disabled, go away.
740
		if ( !$this->isAutoblocking() ) {
741
			return false;
742
		}
743
744
		# Check for presence on the autoblock whitelist.
745
		if ( self::isWhitelistedFromAutoblocks( $autoblockIP ) ) {
746
			return false;
747
		}
748
749
		# Allow hooks to cancel the autoblock.
750
		if ( !Hooks::run( 'AbortAutoblock', [ $autoblockIP, &$this ] ) ) {
751
			wfDebug( "Autoblock aborted by hook.\n" );
752
			return false;
753
		}
754
755
		# It's okay to autoblock. Go ahead and insert/update the block...
756
757
		# Do not add a *new* block if the IP is already blocked.
758
		$ipblock = Block::newFromTarget( $autoblockIP );
759
		if ( $ipblock ) {
760
			# Check if the block is an autoblock and would exceed the user block
761
			# if renewed. If so, do nothing, otherwise prolong the block time...
762
			if ( $ipblock->mAuto && // @todo Why not compare $ipblock->mExpiry?
763
				$this->mExpiry > Block::getAutoblockExpiry( $ipblock->mTimestamp )
764
			) {
765
				# Reset block timestamp to now and its expiry to
766
				# $wgAutoblockExpiry in the future
767
				$ipblock->updateTimestamp();
768
			}
769
			return false;
770
		}
771
772
		# Make a new block object with the desired properties.
773
		$autoblock = new Block;
774
		wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" );
775
		$autoblock->setTarget( $autoblockIP );
776
		$autoblock->setBlocker( $this->getBlocker() );
777
		$autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason )
778
			->inContentLanguage()->plain();
779
		$timestamp = wfTimestampNow();
780
		$autoblock->mTimestamp = $timestamp;
781
		$autoblock->mAuto = 1;
782
		$autoblock->prevents( 'createaccount', $this->prevents( 'createaccount' ) );
783
		# Continue suppressing the name if needed
784
		$autoblock->mHideName = $this->mHideName;
785
		$autoblock->prevents( 'editownusertalk', $this->prevents( 'editownusertalk' ) );
786
		$autoblock->mParentBlockId = $this->mId;
787
788
		if ( $this->mExpiry == 'infinity' ) {
789
			# Original block was indefinite, start an autoblock now
790
			$autoblock->mExpiry = Block::getAutoblockExpiry( $timestamp );
791
		} else {
792
			# If the user is already blocked with an expiry date, we don't
793
			# want to pile on top of that.
794
			$autoblock->mExpiry = min( $this->mExpiry, Block::getAutoblockExpiry( $timestamp ) );
795
		}
796
797
		# Insert the block...
798
		$status = $autoblock->insert();
799
		return $status
800
			? $status['id']
801
			: false;
802
	}
803
804
	/**
805
	 * Check if a block has expired. Delete it if it is.
806
	 * @return bool
807
	 */
808
	public function deleteIfExpired() {
809
810
		if ( $this->isExpired() ) {
811
			wfDebug( "Block::deleteIfExpired() -- deleting\n" );
812
			$this->delete();
813
			$retVal = true;
814
		} else {
815
			wfDebug( "Block::deleteIfExpired() -- not expired\n" );
816
			$retVal = false;
817
		}
818
819
		return $retVal;
820
	}
821
822
	/**
823
	 * Has the block expired?
824
	 * @return bool
825
	 */
826
	public function isExpired() {
827
		$timestamp = wfTimestampNow();
828
		wfDebug( "Block::isExpired() checking current " . $timestamp . " vs $this->mExpiry\n" );
829
830
		if ( !$this->mExpiry ) {
831
			return false;
832
		} else {
833
			return $timestamp > $this->mExpiry;
834
		}
835
	}
836
837
	/**
838
	 * Is the block address valid (i.e. not a null string?)
839
	 * @return bool
840
	 */
841
	public function isValid() {
842
		return $this->getTarget() != null;
843
	}
844
845
	/**
846
	 * Update the timestamp on autoblocks.
847
	 */
848
	public function updateTimestamp() {
849
		if ( $this->mAuto ) {
850
			$this->mTimestamp = wfTimestamp();
851
			$this->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp );
0 ignored issues
show
It seems like $this->mTimestamp can also be of type false; however, Block::getAutoblockExpiry() does only seem to accept string|integer, did you maybe forget to handle an error condition?
Loading history...
Documentation Bug introduced by
It seems like \Block::getAutoblockExpiry($this->mTimestamp) can also be of type boolean. However, the property $mExpiry is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
852
853
			$dbw = wfGetDB( DB_MASTER );
854
			$dbw->update( 'ipblocks',
855
				[ /* SET */
856
					'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ),
857
					'ipb_expiry' => $dbw->timestamp( $this->mExpiry ),
858
				],
859
				[ /* WHERE */
860
					'ipb_id' => $this->getId(),
861
				],
862
				__METHOD__
863
			);
864
		}
865
	}
866
867
	/**
868
	 * Get the IP address at the start of the range in Hex form
869
	 * @throws MWException
870
	 * @return string IP in Hex form
871
	 */
872 View Code Duplication
	public function getRangeStart() {
873
		switch ( $this->type ) {
874
			case self::TYPE_USER:
875
				return '';
876
			case self::TYPE_IP:
877
				return IP::toHex( $this->target );
878
			case self::TYPE_RANGE:
879
				list( $start, /*...*/ ) = IP::parseRange( $this->target );
880
				return $start;
881
			default:
882
				throw new MWException( "Block with invalid type" );
883
		}
884
	}
885
886
	/**
887
	 * Get the IP address at the end of the range in Hex form
888
	 * @throws MWException
889
	 * @return string IP in Hex form
890
	 */
891 View Code Duplication
	public function getRangeEnd() {
892
		switch ( $this->type ) {
893
			case self::TYPE_USER:
894
				return '';
895
			case self::TYPE_IP:
896
				return IP::toHex( $this->target );
897
			case self::TYPE_RANGE:
898
				list( /*...*/, $end ) = IP::parseRange( $this->target );
899
				return $end;
900
			default:
901
				throw new MWException( "Block with invalid type" );
902
		}
903
	}
904
905
	/**
906
	 * Get the user id of the blocking sysop
907
	 *
908
	 * @return int (0 for foreign users)
909
	 */
910
	public function getBy() {
911
		$blocker = $this->getBlocker();
912
		return ( $blocker instanceof User )
913
			? $blocker->getId()
914
			: 0;
915
	}
916
917
	/**
918
	 * Get the username of the blocking sysop
919
	 *
920
	 * @return string
921
	 */
922
	public function getByName() {
923
		$blocker = $this->getBlocker();
924
		return ( $blocker instanceof User )
925
			? $blocker->getName()
926
			: (string)$blocker; // username
927
	}
928
929
	/**
930
	 * Get the block ID
931
	 * @return int
932
	 */
933
	public function getId() {
934
		return $this->mId;
935
	}
936
937
	/**
938
	 * Get/set a flag determining whether the master is used for reads
939
	 *
940
	 * @param bool|null $x
941
	 * @return bool
942
	 */
943
	public function fromMaster( $x = null ) {
944
		return wfSetVar( $this->mFromMaster, $x );
945
	}
946
947
	/**
948
	 * Get/set whether the Block is a hardblock (affects logged-in users on a given IP/range)
949
	 * @param bool|null $x
950
	 * @return bool
951
	 */
952
	public function isHardblock( $x = null ) {
953
		wfSetVar( $this->isHardblock, $x );
954
955
		# You can't *not* hardblock a user
956
		return $this->getType() == self::TYPE_USER
957
			? true
958
			: $this->isHardblock;
959
	}
960
961
	/**
962
	 * @param null|bool $x
963
	 * @return bool
964
	 */
965
	public function isAutoblocking( $x = null ) {
966
		wfSetVar( $this->isAutoblocking, $x );
967
968
		# You can't put an autoblock on an IP or range as we don't have any history to
969
		# look over to get more IPs from
970
		return $this->getType() == self::TYPE_USER
971
			? $this->isAutoblocking
972
			: false;
973
	}
974
975
	/**
976
	 * Get/set whether the Block prevents a given action
977
	 *
978
	 * @param string $action Action to check
979
	 * @param bool|null $x Value for set, or null to just get value
980
	 * @return bool|null Null for unrecognized rights.
981
	 */
982
	public function prevents( $action, $x = null ) {
983
		global $wgBlockDisablesLogin;
984
		$res = null;
985
		switch ( $action ) {
986
			case 'edit':
987
				# For now... <evil laugh>
988
				$res = true;
989
				break;
990
			case 'createaccount':
991
				$res = wfSetVar( $this->mCreateAccount, $x );
992
				break;
993
			case 'sendemail':
994
				$res = wfSetVar( $this->mBlockEmail, $x );
995
				break;
996
			case 'editownusertalk':
997
				$res = wfSetVar( $this->mDisableUsertalk, $x );
998
				break;
999
			case 'read':
1000
				$res = false;
1001
				break;
1002
		}
1003
		if ( !$res && $wgBlockDisablesLogin ) {
1004
			// If a block would disable login, then it should
1005
			// prevent any action that all users cannot do
1006
			$anon = new User;
1007
			$res = $anon->isAllowed( $action ) ? $res : true;
1008
		}
1009
1010
		return $res;
1011
	}
1012
1013
	/**
1014
	 * Get the block name, but with autoblocked IPs hidden as per standard privacy policy
1015
	 * @return string Text is escaped
1016
	 */
1017
	public function getRedactedName() {
1018
		if ( $this->mAuto ) {
1019
			return Html::rawElement(
1020
				'span',
1021
				[ 'class' => 'mw-autoblockid' ],
1022
				wfMessage( 'autoblockid', $this->mId )
1023
			);
1024
		} else {
1025
			return htmlspecialchars( $this->getTarget() );
1026
		}
1027
	}
1028
1029
	/**
1030
	 * Get a timestamp of the expiry for autoblocks
1031
	 *
1032
	 * @param string|int $timestamp
1033
	 * @return string
1034
	 */
1035
	public static function getAutoblockExpiry( $timestamp ) {
1036
		global $wgAutoblockExpiry;
1037
1038
		return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry );
1039
	}
1040
1041
	/**
1042
	 * Purge expired blocks from the ipblocks table
1043
	 */
1044
	public static function purgeExpired() {
1045
		if ( wfReadOnly() ) {
1046
			return;
1047
		}
1048
1049
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
1050
			wfGetDB( DB_MASTER ),
1051
			__METHOD__,
1052
			function ( IDatabase $dbw, $fname ) {
1053
				$dbw->delete(
1054
					'ipblocks',
1055
					[ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
1056
					$fname
1057
				);
1058
			}
1059
		) );
1060
	}
1061
1062
	/**
1063
	 * Given a target and the target's type, get an existing Block object if possible.
1064
	 * @param string|User|int $specificTarget A block target, which may be one of several types:
1065
	 *     * A user to block, in which case $target will be a User
1066
	 *     * An IP to block, in which case $target will be a User generated by using
1067
	 *       User::newFromName( $ip, false ) to turn off name validation
1068
	 *     * An IP range, in which case $target will be a String "123.123.123.123/18" etc
1069
	 *     * The ID of an existing block, in the format "#12345" (since pure numbers are valid
1070
	 *       usernames
1071
	 *     Calling this with a user, IP address or range will not select autoblocks, and will
1072
	 *     only select a block where the targets match exactly (so looking for blocks on
1073
	 *     1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32)
1074
	 * @param string|User|int $vagueTarget As above, but we will search for *any* block which
1075
	 *     affects that target (so for an IP address, get ranges containing that IP; and also
1076
	 *     get any relevant autoblocks). Leave empty or blank to skip IP-based lookups.
1077
	 * @param bool $fromMaster Whether to use the DB_MASTER database
1078
	 * @return Block|null (null if no relevant block could be found).  The target and type
1079
	 *     of the returned Block will refer to the actual block which was found, which might
1080
	 *     not be the same as the target you gave if you used $vagueTarget!
1081
	 */
1082
	public static function newFromTarget( $specificTarget, $vagueTarget = null, $fromMaster = false ) {
1083
1084
		list( $target, $type ) = self::parseTarget( $specificTarget );
1085
		if ( $type == Block::TYPE_ID || $type == Block::TYPE_AUTO ) {
1086
			return Block::newFromID( $target );
1087
1088
		} elseif ( $target === null && $vagueTarget == '' ) {
1089
			# We're not going to find anything useful here
1090
			# Be aware that the == '' check is explicit, since empty values will be
1091
			# passed by some callers (bug 29116)
1092
			return null;
1093
1094
		} elseif ( in_array(
1095
			$type,
1096
			[ Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ] )
1097
		) {
1098
			$block = new Block();
1099
			$block->fromMaster( $fromMaster );
1100
1101
			if ( $type !== null ) {
1102
				$block->setTarget( $target );
1103
			}
1104
1105
			if ( $block->newLoad( $vagueTarget ) ) {
1106
				return $block;
1107
			}
1108
		}
1109
		return null;
1110
	}
1111
1112
	/**
1113
	 * Get all blocks that match any IP from an array of IP addresses
1114
	 *
1115
	 * @param array $ipChain List of IPs (strings), usually retrieved from the
1116
	 *	   X-Forwarded-For header of the request
1117
	 * @param bool $isAnon Exclude anonymous-only blocks if false
1118
	 * @param bool $fromMaster Whether to query the master or replica DB
1119
	 * @return array Array of Blocks
1120
	 * @since 1.22
1121
	 */
1122
	public static function getBlocksForIPList( array $ipChain, $isAnon, $fromMaster = false ) {
1123
		if ( !count( $ipChain ) ) {
1124
			return [];
1125
		}
1126
1127
		$conds = [];
1128
		$proxyLookup = MediaWikiServices::getInstance()->getProxyLookup();
1129
		foreach ( array_unique( $ipChain ) as $ipaddr ) {
1130
			# Discard invalid IP addresses. Since XFF can be spoofed and we do not
1131
			# necessarily trust the header given to us, make sure that we are only
1132
			# checking for blocks on well-formatted IP addresses (IPv4 and IPv6).
1133
			# Do not treat private IP spaces as special as it may be desirable for wikis
1134
			# to block those IP ranges in order to stop misbehaving proxies that spoof XFF.
1135
			if ( !IP::isValid( $ipaddr ) ) {
1136
				continue;
1137
			}
1138
			# Don't check trusted IPs (includes local squids which will be in every request)
1139
			if ( $proxyLookup->isTrustedProxy( $ipaddr ) ) {
1140
				continue;
1141
			}
1142
			# Check both the original IP (to check against single blocks), as well as build
1143
			# the clause to check for rangeblocks for the given IP.
1144
			$conds['ipb_address'][] = $ipaddr;
1145
			$conds[] = self::getRangeCond( IP::toHex( $ipaddr ) );
1146
		}
1147
1148
		if ( !count( $conds ) ) {
1149
			return [];
1150
		}
1151
1152
		if ( $fromMaster ) {
1153
			$db = wfGetDB( DB_MASTER );
1154
		} else {
1155
			$db = wfGetDB( DB_REPLICA );
1156
		}
1157
		$conds = $db->makeList( $conds, LIST_OR );
1158
		if ( !$isAnon ) {
1159
			$conds = [ $conds, 'ipb_anon_only' => 0 ];
1160
		}
1161
		$selectFields = array_merge(
1162
			[ 'ipb_range_start', 'ipb_range_end' ],
1163
			Block::selectFields()
1164
		);
1165
		$rows = $db->select( 'ipblocks',
1166
			$selectFields,
1167
			$conds,
1168
			__METHOD__
1169
		);
1170
1171
		$blocks = [];
1172
		foreach ( $rows as $row ) {
1173
			$block = self::newFromRow( $row );
1174
			if ( !$block->isExpired() ) {
1175
				$blocks[] = $block;
1176
			}
1177
		}
1178
1179
		return $blocks;
1180
	}
1181
1182
	/**
1183
	 * From a list of multiple blocks, find the most exact and strongest Block.
1184
	 *
1185
	 * The logic for finding the "best" block is:
1186
	 *  - Blocks that match the block's target IP are preferred over ones in a range
1187
	 *  - Hardblocks are chosen over softblocks that prevent account creation
1188
	 *  - Softblocks that prevent account creation are chosen over other softblocks
1189
	 *  - Other softblocks are chosen over autoblocks
1190
	 *  - If there are multiple exact or range blocks at the same level, the one chosen
1191
	 *    is random
1192
	 * This should be used when $blocks where retrieved from the user's IP address
1193
	 * and $ipChain is populated from the same IP address information.
1194
	 *
1195
	 * @param array $blocks Array of Block objects
1196
	 * @param array $ipChain List of IPs (strings). This is used to determine how "close"
1197
	 * 	  a block is to the server, and if a block matches exactly, or is in a range.
1198
	 *	  The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2,
1199
	 *	  local-squid, ...)
1200
	 * @throws MWException
1201
	 * @return Block|null The "best" block from the list
1202
	 */
1203
	public static function chooseBlock( array $blocks, array $ipChain ) {
1204
		if ( !count( $blocks ) ) {
1205
			return null;
1206
		} elseif ( count( $blocks ) == 1 ) {
1207
			return $blocks[0];
1208
		}
1209
1210
		// Sort hard blocks before soft ones and secondarily sort blocks
1211
		// that disable account creation before those that don't.
1212
		usort( $blocks, function ( Block $a, Block $b ) {
1213
			$aWeight = (int)$a->isHardblock() . (int)$a->prevents( 'createaccount' );
1214
			$bWeight = (int)$b->isHardblock() . (int)$b->prevents( 'createaccount' );
1215
			return strcmp( $bWeight, $aWeight ); // highest weight first
1216
		} );
1217
1218
		$blocksListExact = [
1219
			'hard' => false,
1220
			'disable_create' => false,
1221
			'other' => false,
1222
			'auto' => false
1223
		];
1224
		$blocksListRange = [
1225
			'hard' => false,
1226
			'disable_create' => false,
1227
			'other' => false,
1228
			'auto' => false
1229
		];
1230
		$ipChain = array_reverse( $ipChain );
1231
1232
		/** @var Block $block */
1233
		foreach ( $blocks as $block ) {
1234
			// Stop searching if we have already have a "better" block. This
1235
			// is why the order of the blocks matters
1236
			if ( !$block->isHardblock() && $blocksListExact['hard'] ) {
1237
				break;
1238
			} elseif ( !$block->prevents( 'createaccount' ) && $blocksListExact['disable_create'] ) {
1239
				break;
1240
			}
1241
1242
			foreach ( $ipChain as $checkip ) {
1243
				$checkipHex = IP::toHex( $checkip );
1244
				if ( (string)$block->getTarget() === $checkip ) {
1245 View Code Duplication
					if ( $block->isHardblock() ) {
1246
						$blocksListExact['hard'] = $blocksListExact['hard'] ?: $block;
1247
					} elseif ( $block->prevents( 'createaccount' ) ) {
1248
						$blocksListExact['disable_create'] = $blocksListExact['disable_create'] ?: $block;
1249
					} elseif ( $block->mAuto ) {
1250
						$blocksListExact['auto'] = $blocksListExact['auto'] ?: $block;
1251
					} else {
1252
						$blocksListExact['other'] = $blocksListExact['other'] ?: $block;
1253
					}
1254
					// We found closest exact match in the ip list, so go to the next Block
1255
					break;
1256
				} elseif ( array_filter( $blocksListExact ) == []
1257
					&& $block->getRangeStart() <= $checkipHex
1258
					&& $block->getRangeEnd() >= $checkipHex
1259
				) {
1260 View Code Duplication
					if ( $block->isHardblock() ) {
1261
						$blocksListRange['hard'] = $blocksListRange['hard'] ?: $block;
1262
					} elseif ( $block->prevents( 'createaccount' ) ) {
1263
						$blocksListRange['disable_create'] = $blocksListRange['disable_create'] ?: $block;
1264
					} elseif ( $block->mAuto ) {
1265
						$blocksListRange['auto'] = $blocksListRange['auto'] ?: $block;
1266
					} else {
1267
						$blocksListRange['other'] = $blocksListRange['other'] ?: $block;
1268
					}
1269
					break;
1270
				}
1271
			}
1272
		}
1273
1274
		if ( array_filter( $blocksListExact ) == [] ) {
1275
			$blocksList = &$blocksListRange;
1276
		} else {
1277
			$blocksList = &$blocksListExact;
1278
		}
1279
1280
		$chosenBlock = null;
1281
		if ( $blocksList['hard'] ) {
1282
			$chosenBlock = $blocksList['hard'];
1283
		} elseif ( $blocksList['disable_create'] ) {
1284
			$chosenBlock = $blocksList['disable_create'];
1285
		} elseif ( $blocksList['other'] ) {
1286
			$chosenBlock = $blocksList['other'];
1287
		} elseif ( $blocksList['auto'] ) {
1288
			$chosenBlock = $blocksList['auto'];
1289
		} else {
1290
			throw new MWException( "Proxy block found, but couldn't be classified." );
1291
		}
1292
1293
		return $chosenBlock;
1294
	}
1295
1296
	/**
1297
	 * From an existing Block, get the target and the type of target.
1298
	 * Note that, except for null, it is always safe to treat the target
1299
	 * as a string; for User objects this will return User::__toString()
1300
	 * which in turn gives User::getName().
1301
	 *
1302
	 * @param string|int|User|null $target
1303
	 * @return array( User|String|null, Block::TYPE_ constant|null )
1304
	 */
1305
	public static function parseTarget( $target ) {
1306
		# We may have been through this before
1307
		if ( $target instanceof User ) {
1308
			if ( IP::isValid( $target->getName() ) ) {
1309
				return [ $target, self::TYPE_IP ];
1310
			} else {
1311
				return [ $target, self::TYPE_USER ];
1312
			}
1313
		} elseif ( $target === null ) {
1314
			return [ null, null ];
1315
		}
1316
1317
		$target = trim( $target );
1318
1319
		if ( IP::isValid( $target ) ) {
1320
			# We can still create a User if it's an IP address, but we need to turn
1321
			# off validation checking (which would exclude IP addresses)
1322
			return [
1323
				User::newFromName( IP::sanitizeIP( $target ), false ),
1324
				Block::TYPE_IP
1325
			];
1326
1327
		} elseif ( IP::isValidBlock( $target ) ) {
1328
			# Can't create a User from an IP range
1329
			return [ IP::sanitizeRange( $target ), Block::TYPE_RANGE ];
1330
		}
1331
1332
		# Consider the possibility that this is not a username at all
1333
		# but actually an old subpage (bug #29797)
1334
		if ( strpos( $target, '/' ) !== false ) {
1335
			# An old subpage, drill down to the user behind it
1336
			$target = explode( '/', $target )[0];
1337
		}
1338
1339
		$userObj = User::newFromName( $target );
1340
		if ( $userObj instanceof User ) {
1341
			# Note that since numbers are valid usernames, a $target of "12345" will be
1342
			# considered a User.  If you want to pass a block ID, prepend a hash "#12345",
1343
			# since hash characters are not valid in usernames or titles generally.
1344
			return [ $userObj, Block::TYPE_USER ];
1345
1346
		} elseif ( preg_match( '/^#\d+$/', $target ) ) {
1347
			# Autoblock reference in the form "#12345"
1348
			return [ substr( $target, 1 ), Block::TYPE_AUTO ];
1349
1350
		} else {
1351
			# WTF?
1352
			return [ null, null ];
1353
		}
1354
	}
1355
1356
	/**
1357
	 * Get the type of target for this particular block
1358
	 * @return int Block::TYPE_ constant, will never be TYPE_ID
1359
	 */
1360
	public function getType() {
1361
		return $this->mAuto
1362
			? self::TYPE_AUTO
1363
			: $this->type;
1364
	}
1365
1366
	/**
1367
	 * Get the target and target type for this particular Block.  Note that for autoblocks,
1368
	 * this returns the unredacted name; frontend functions need to call $block->getRedactedName()
1369
	 * in this situation.
1370
	 * @return array( User|String, Block::TYPE_ constant )
1371
	 * @todo FIXME: This should be an integral part of the Block member variables
1372
	 */
1373
	public function getTargetAndType() {
1374
		return [ $this->getTarget(), $this->getType() ];
1375
	}
1376
1377
	/**
1378
	 * Get the target for this particular Block.  Note that for autoblocks,
1379
	 * this returns the unredacted name; frontend functions need to call $block->getRedactedName()
1380
	 * in this situation.
1381
	 * @return User|string
1382
	 */
1383
	public function getTarget() {
1384
		return $this->target;
1385
	}
1386
1387
	/**
1388
	 * @since 1.19
1389
	 *
1390
	 * @return mixed|string
1391
	 */
1392
	public function getExpiry() {
1393
		return $this->mExpiry;
1394
	}
1395
1396
	/**
1397
	 * Set the target for this block, and update $this->type accordingly
1398
	 * @param mixed $target
1399
	 */
1400
	public function setTarget( $target ) {
1401
		list( $this->target, $this->type ) = self::parseTarget( $target );
1402
	}
1403
1404
	/**
1405
	 * Get the user who implemented this block
1406
	 * @return User|string Local User object or string for a foreign user
1407
	 */
1408
	public function getBlocker() {
1409
		return $this->blocker;
1410
	}
1411
1412
	/**
1413
	 * Set the user who implemented (or will implement) this block
1414
	 * @param User|string $user Local User object or username string for foreign users
1415
	 */
1416
	public function setBlocker( $user ) {
1417
		$this->blocker = $user;
1418
	}
1419
1420
	/**
1421
	 * Get the key and parameters for the corresponding error message.
1422
	 *
1423
	 * @since 1.22
1424
	 * @param IContextSource $context
1425
	 * @return array
1426
	 */
1427
	public function getPermissionsError( IContextSource $context ) {
1428
		$blocker = $this->getBlocker();
1429
		if ( $blocker instanceof User ) { // local user
1430
			$blockerUserpage = $blocker->getUserPage();
1431
			$link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerUserpage->getText()}]]";
1432
		} else { // foreign user
1433
			$link = $blocker;
1434
		}
1435
1436
		$reason = $this->mReason;
1437
		if ( $reason == '' ) {
1438
			$reason = $context->msg( 'blockednoreason' )->text();
1439
		}
1440
1441
		/* $ip returns who *is* being blocked, $intended contains who was meant to be blocked.
1442
		 * This could be a username, an IP range, or a single IP. */
1443
		$intended = $this->getTarget();
1444
1445
		$lang = $context->getLanguage();
1446
		return [
1447
			$this->mAuto ? 'autoblockedtext' : 'blockedtext',
1448
			$link,
1449
			$reason,
1450
			$context->getRequest()->getIP(),
1451
			$this->getByName(),
1452
			$this->getId(),
1453
			$lang->formatExpiry( $this->mExpiry ),
1454
			(string)$intended,
1455
			$lang->userTimeAndDate( $this->mTimestamp, $context->getUser() ),
1456
		];
1457
	}
1458
}
1459