Completed
Branch master (939199)
by
unknown
39:35
created

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" );
0 ignored issues
show
It seems like $dbw is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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 );
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