]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/Block.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / Block.php
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 Wikimedia\Rdbms\Database;
24 use Wikimedia\Rdbms\IDatabase;
25 use MediaWiki\MediaWikiServices;
26
27 class Block {
28         /** @var string */
29         public $mReason;
30
31         /** @var string */
32         public $mTimestamp;
33
34         /** @var bool */
35         public $mAuto;
36
37         /** @var string */
38         public $mExpiry;
39
40         /** @var bool */
41         public $mHideName;
42
43         /** @var int */
44         public $mParentBlockId;
45
46         /** @var int */
47         private $mId;
48
49         /** @var bool */
50         private $mFromMaster;
51
52         /** @var bool */
53         private $mBlockEmail;
54
55         /** @var bool */
56         private $mDisableUsertalk;
57
58         /** @var bool */
59         private $mCreateAccount;
60
61         /** @var User|string */
62         private $target;
63
64         /** @var int Hack for foreign blocking (CentralAuth) */
65         private $forcedTargetID;
66
67         /** @var int Block::TYPE_ constant. Can only be USER, IP or RANGE internally */
68         private $type;
69
70         /** @var User */
71         private $blocker;
72
73         /** @var bool */
74         private $isHardblock;
75
76         /** @var bool */
77         private $isAutoblocking;
78
79         /** @var string|null */
80         private $systemBlockType;
81
82         # TYPE constants
83         const TYPE_USER = 1;
84         const TYPE_IP = 2;
85         const TYPE_RANGE = 3;
86         const TYPE_AUTO = 4;
87         const TYPE_ID = 5;
88
89         /**
90          * Create a new block with specified parameters on a user, IP or IP range.
91          *
92          * @param array $options Parameters of the block:
93          *     address string|User  Target user name, User object, IP address or IP range
94          *     user int             Override target user ID (for foreign users)
95          *     by int               User ID of the blocker
96          *     reason string        Reason of the block
97          *     timestamp string     The time at which the block comes into effect
98          *     auto bool            Is this an automatic block?
99          *     expiry string        Timestamp of expiration of the block or 'infinity'
100          *     anonOnly bool        Only disallow anonymous actions
101          *     createAccount bool   Disallow creation of new accounts
102          *     enableAutoblock bool Enable automatic blocking
103          *     hideName bool        Hide the target user name
104          *     blockEmail bool      Disallow sending emails
105          *     allowUsertalk bool   Allow the target to edit its own talk page
106          *     byText string        Username of the blocker (for foreign users)
107          *     systemBlock string   Indicate that this block is automatically
108          *                          created by MediaWiki rather than being stored
109          *                          in the database. Value is a string to return
110          *                          from self::getSystemBlockType().
111          *
112          * @since 1.26 accepts $options array instead of individual parameters; order
113          * of parameters above reflects the original order
114          */
115         function __construct( $options = [] ) {
116                 $defaults = [
117                         'address'         => '',
118                         'user'            => null,
119                         'by'              => null,
120                         'reason'          => '',
121                         'timestamp'       => '',
122                         'auto'            => false,
123                         'expiry'          => '',
124                         'anonOnly'        => false,
125                         'createAccount'   => false,
126                         'enableAutoblock' => false,
127                         'hideName'        => false,
128                         'blockEmail'      => false,
129                         'allowUsertalk'   => false,
130                         'byText'          => '',
131                         'systemBlock'     => null,
132                 ];
133
134                 if ( func_num_args() > 1 || !is_array( $options ) ) {
135                         $options = array_combine(
136                                 array_slice( array_keys( $defaults ), 0, func_num_args() ),
137                                 func_get_args()
138                         );
139                         wfDeprecated( __METHOD__ . ' with multiple arguments', '1.26' );
140                 }
141
142                 $options += $defaults;
143
144                 $this->setTarget( $options['address'] );
145
146                 if ( $this->target instanceof User && $options['user'] ) {
147                         # Needed for foreign users
148                         $this->forcedTargetID = $options['user'];
149                 }
150
151                 if ( $options['by'] ) {
152                         # Local user
153                         $this->setBlocker( User::newFromId( $options['by'] ) );
154                 } else {
155                         # Foreign user
156                         $this->setBlocker( $options['byText'] );
157                 }
158
159                 $this->mReason = $options['reason'];
160                 $this->mTimestamp = wfTimestamp( TS_MW, $options['timestamp'] );
161                 $this->mExpiry = wfGetDB( DB_REPLICA )->decodeExpiry( $options['expiry'] );
162
163                 # Boolean settings
164                 $this->mAuto = (bool)$options['auto'];
165                 $this->mHideName = (bool)$options['hideName'];
166                 $this->isHardblock( !$options['anonOnly'] );
167                 $this->isAutoblocking( (bool)$options['enableAutoblock'] );
168
169                 # Prevention measures
170                 $this->prevents( 'sendemail', (bool)$options['blockEmail'] );
171                 $this->prevents( 'editownusertalk', !$options['allowUsertalk'] );
172                 $this->prevents( 'createaccount', (bool)$options['createAccount'] );
173
174                 $this->mFromMaster = false;
175                 $this->systemBlockType = $options['systemBlock'];
176         }
177
178         /**
179          * Load a blocked user from their block id.
180          *
181          * @param int $id Block id to search for
182          * @return Block|null
183          */
184         public static function newFromID( $id ) {
185                 $dbr = wfGetDB( DB_REPLICA );
186                 $res = $dbr->selectRow(
187                         'ipblocks',
188                         self::selectFields(),
189                         [ 'ipb_id' => $id ],
190                         __METHOD__
191                 );
192                 if ( $res ) {
193                         return self::newFromRow( $res );
194                 } else {
195                         return null;
196                 }
197         }
198
199         /**
200          * Return the list of ipblocks fields that should be selected to create
201          * a new block.
202          * @todo Deprecate this in favor of a method that returns tables and joins
203          *  as well, and use CommentStore::getJoin().
204          * @return array
205          */
206         public static function selectFields() {
207                 return [
208                         'ipb_id',
209                         'ipb_address',
210                         'ipb_by',
211                         'ipb_by_text',
212                         'ipb_timestamp',
213                         'ipb_auto',
214                         'ipb_anon_only',
215                         'ipb_create_account',
216                         'ipb_enable_autoblock',
217                         'ipb_expiry',
218                         'ipb_deleted',
219                         'ipb_block_email',
220                         'ipb_allow_usertalk',
221                         'ipb_parent_block_id',
222                 ] + CommentStore::newKey( 'ipb_reason' )->getFields();
223         }
224
225         /**
226          * Check if two blocks are effectively equal.  Doesn't check irrelevant things like
227          * the blocking user or the block timestamp, only things which affect the blocked user
228          *
229          * @param Block $block
230          *
231          * @return bool
232          */
233         public function equals( Block $block ) {
234                 return (
235                         (string)$this->target == (string)$block->target
236                         && $this->type == $block->type
237                         && $this->mAuto == $block->mAuto
238                         && $this->isHardblock() == $block->isHardblock()
239                         && $this->prevents( 'createaccount' ) == $block->prevents( 'createaccount' )
240                         && $this->mExpiry == $block->mExpiry
241                         && $this->isAutoblocking() == $block->isAutoblocking()
242                         && $this->mHideName == $block->mHideName
243                         && $this->prevents( 'sendemail' ) == $block->prevents( 'sendemail' )
244                         && $this->prevents( 'editownusertalk' ) == $block->prevents( 'editownusertalk' )
245                         && $this->mReason == $block->mReason
246                 );
247         }
248
249         /**
250          * Load a block from the database which affects the already-set $this->target:
251          *     1) A block directly on the given user or IP
252          *     2) A rangeblock encompassing the given IP (smallest first)
253          *     3) An autoblock on the given IP
254          * @param User|string $vagueTarget Also search for blocks affecting this target.  Doesn't
255          *     make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups.
256          * @throws MWException
257          * @return bool Whether a relevant block was found
258          */
259         protected function newLoad( $vagueTarget = null ) {
260                 $db = wfGetDB( $this->mFromMaster ? DB_MASTER : DB_REPLICA );
261
262                 if ( $this->type !== null ) {
263                         $conds = [
264                                 'ipb_address' => [ (string)$this->target ],
265                         ];
266                 } else {
267                         $conds = [ 'ipb_address' => [] ];
268                 }
269
270                 # Be aware that the != '' check is explicit, since empty values will be
271                 # passed by some callers (T31116)
272                 if ( $vagueTarget != '' ) {
273                         list( $target, $type ) = self::parseTarget( $vagueTarget );
274                         switch ( $type ) {
275                                 case self::TYPE_USER:
276                                         # Slightly weird, but who are we to argue?
277                                         $conds['ipb_address'][] = (string)$target;
278                                         break;
279
280                                 case self::TYPE_IP:
281                                         $conds['ipb_address'][] = (string)$target;
282                                         $conds[] = self::getRangeCond( IP::toHex( $target ) );
283                                         $conds = $db->makeList( $conds, LIST_OR );
284                                         break;
285
286                                 case self::TYPE_RANGE:
287                                         list( $start, $end ) = IP::parseRange( $target );
288                                         $conds['ipb_address'][] = (string)$target;
289                                         $conds[] = self::getRangeCond( $start, $end );
290                                         $conds = $db->makeList( $conds, LIST_OR );
291                                         break;
292
293                                 default:
294                                         throw new MWException( "Tried to load block with invalid type" );
295                         }
296                 }
297
298                 $res = $db->select( 'ipblocks', self::selectFields(), $conds, __METHOD__ );
299
300                 # This result could contain a block on the user, a block on the IP, and a russian-doll
301                 # set of rangeblocks.  We want to choose the most specific one, so keep a leader board.
302                 $bestRow = null;
303
304                 # Lower will be better
305                 $bestBlockScore = 100;
306
307                 # This is begging for $this = $bestBlock, but that's not allowed in PHP :(
308                 $bestBlockPreventsEdit = null;
309
310                 foreach ( $res as $row ) {
311                         $block = self::newFromRow( $row );
312
313                         # Don't use expired blocks
314                         if ( $block->isExpired() ) {
315                                 continue;
316                         }
317
318                         # Don't use anon only blocks on users
319                         if ( $this->type == self::TYPE_USER && !$block->isHardblock() ) {
320                                 continue;
321                         }
322
323                         if ( $block->getType() == self::TYPE_RANGE ) {
324                                 # This is the number of bits that are allowed to vary in the block, give
325                                 # or take some floating point errors
326                                 $end = Wikimedia\base_convert( $block->getRangeEnd(), 16, 10 );
327                                 $start = Wikimedia\base_convert( $block->getRangeStart(), 16, 10 );
328                                 $size = log( $end - $start + 1, 2 );
329
330                                 # This has the nice property that a /32 block is ranked equally with a
331                                 # single-IP block, which is exactly what it is...
332                                 $score = self::TYPE_RANGE - 1 + ( $size / 128 );
333
334                         } else {
335                                 $score = $block->getType();
336                         }
337
338                         if ( $score < $bestBlockScore ) {
339                                 $bestBlockScore = $score;
340                                 $bestRow = $row;
341                                 $bestBlockPreventsEdit = $block->prevents( 'edit' );
342                         }
343                 }
344
345                 if ( $bestRow !== null ) {
346                         $this->initFromRow( $bestRow );
347                         $this->prevents( 'edit', $bestBlockPreventsEdit );
348                         return true;
349                 } else {
350                         return false;
351                 }
352         }
353
354         /**
355          * Get a set of SQL conditions which will select rangeblocks encompassing a given range
356          * @param string $start Hexadecimal IP representation
357          * @param string $end Hexadecimal IP representation, or null to use $start = $end
358          * @return string
359          */
360         public static function getRangeCond( $start, $end = null ) {
361                 if ( $end === null ) {
362                         $end = $start;
363                 }
364                 # Per T16634, we want to include relevant active rangeblocks; for
365                 # rangeblocks, we want to include larger ranges which enclose the given
366                 # range. We know that all blocks must be smaller than $wgBlockCIDRLimit,
367                 # so we can improve performance by filtering on a LIKE clause
368                 $chunk = self::getIpFragment( $start );
369                 $dbr = wfGetDB( DB_REPLICA );
370                 $like = $dbr->buildLike( $chunk, $dbr->anyString() );
371
372                 # Fairly hard to make a malicious SQL statement out of hex characters,
373                 # but stranger things have happened...
374                 $safeStart = $dbr->addQuotes( $start );
375                 $safeEnd = $dbr->addQuotes( $end );
376
377                 return $dbr->makeList(
378                         [
379                                 "ipb_range_start $like",
380                                 "ipb_range_start <= $safeStart",
381                                 "ipb_range_end >= $safeEnd",
382                         ],
383                         LIST_AND
384                 );
385         }
386
387         /**
388          * Get the component of an IP address which is certain to be the same between an IP
389          * address and a rangeblock containing that IP address.
390          * @param string $hex Hexadecimal IP representation
391          * @return string
392          */
393         protected static function getIpFragment( $hex ) {
394                 global $wgBlockCIDRLimit;
395                 if ( substr( $hex, 0, 3 ) == 'v6-' ) {
396                         return 'v6-' . substr( substr( $hex, 3 ), 0, floor( $wgBlockCIDRLimit['IPv6'] / 4 ) );
397                 } else {
398                         return substr( $hex, 0, floor( $wgBlockCIDRLimit['IPv4'] / 4 ) );
399                 }
400         }
401
402         /**
403          * Given a database row from the ipblocks table, initialize
404          * member variables
405          * @param stdClass $row A row from the ipblocks table
406          */
407         protected function initFromRow( $row ) {
408                 $this->setTarget( $row->ipb_address );
409                 if ( $row->ipb_by ) { // local user
410                         $this->setBlocker( User::newFromId( $row->ipb_by ) );
411                 } else { // foreign user
412                         $this->setBlocker( $row->ipb_by_text );
413                 }
414
415                 $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp );
416                 $this->mAuto = $row->ipb_auto;
417                 $this->mHideName = $row->ipb_deleted;
418                 $this->mId = (int)$row->ipb_id;
419                 $this->mParentBlockId = $row->ipb_parent_block_id;
420
421                 // I wish I didn't have to do this
422                 $db = wfGetDB( DB_REPLICA );
423                 $this->mExpiry = $db->decodeExpiry( $row->ipb_expiry );
424                 $this->mReason = CommentStore::newKey( 'ipb_reason' )
425                         // Legacy because $row probably came from self::selectFields()
426                         ->getCommentLegacy( $db, $row )->text;
427
428                 $this->isHardblock( !$row->ipb_anon_only );
429                 $this->isAutoblocking( $row->ipb_enable_autoblock );
430
431                 $this->prevents( 'createaccount', $row->ipb_create_account );
432                 $this->prevents( 'sendemail', $row->ipb_block_email );
433                 $this->prevents( 'editownusertalk', !$row->ipb_allow_usertalk );
434         }
435
436         /**
437          * Create a new Block object from a database row
438          * @param stdClass $row Row from the ipblocks table
439          * @return Block
440          */
441         public static function newFromRow( $row ) {
442                 $block = new Block;
443                 $block->initFromRow( $row );
444                 return $block;
445         }
446
447         /**
448          * Delete the row from the IP blocks table.
449          *
450          * @throws MWException
451          * @return bool
452          */
453         public function delete() {
454                 if ( wfReadOnly() ) {
455                         return false;
456                 }
457
458                 if ( !$this->getId() ) {
459                         throw new MWException( "Block::delete() requires that the mId member be filled\n" );
460                 }
461
462                 $dbw = wfGetDB( DB_MASTER );
463                 $dbw->delete( 'ipblocks', [ 'ipb_parent_block_id' => $this->getId() ], __METHOD__ );
464                 $dbw->delete( 'ipblocks', [ 'ipb_id' => $this->getId() ], __METHOD__ );
465
466                 return $dbw->affectedRows() > 0;
467         }
468
469         /**
470          * Insert a block into the block table. Will fail if there is a conflicting
471          * block (same name and options) already in the database.
472          *
473          * @param IDatabase $dbw If you have one available
474          * @return bool|array False on failure, assoc array on success:
475          *      ('id' => block ID, 'autoIds' => array of autoblock IDs)
476          */
477         public function insert( $dbw = null ) {
478                 global $wgBlockDisablesLogin;
479
480                 if ( $this->getSystemBlockType() !== null ) {
481                         throw new MWException( 'Cannot insert a system block into the database' );
482                 }
483
484                 wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" );
485
486                 if ( $dbw === null ) {
487                         $dbw = wfGetDB( DB_MASTER );
488                 }
489
490                 # Periodic purge via commit hooks
491                 if ( mt_rand( 0, 9 ) == 0 ) {
492                         self::purgeExpired();
493                 }
494
495                 $row = $this->getDatabaseArray( $dbw );
496
497                 $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
498                 $affected = $dbw->affectedRows();
499                 $this->mId = $dbw->insertId();
500
501                 # Don't collide with expired blocks.
502                 # Do this after trying to insert to avoid locking.
503                 if ( !$affected ) {
504                         # T96428: The ipb_address index uses a prefix on a field, so
505                         # use a standard SELECT + DELETE to avoid annoying gap locks.
506                         $ids = $dbw->selectFieldValues( 'ipblocks',
507                                 'ipb_id',
508                                 [
509                                         'ipb_address' => $row['ipb_address'],
510                                         'ipb_user' => $row['ipb_user'],
511                                         'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() )
512                                 ],
513                                 __METHOD__
514                         );
515                         if ( $ids ) {
516                                 $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ );
517                                 $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
518                                 $affected = $dbw->affectedRows();
519                                 $this->mId = $dbw->insertId();
520                         }
521                 }
522
523                 if ( $affected ) {
524                         $auto_ipd_ids = $this->doRetroactiveAutoblock();
525
526                         if ( $wgBlockDisablesLogin && $this->target instanceof User ) {
527                                 // Change user login token to force them to be logged out.
528                                 $this->target->setToken();
529                                 $this->target->saveSettings();
530                         }
531
532                         return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
533                 }
534
535                 return false;
536         }
537
538         /**
539          * Update a block in the DB with new parameters.
540          * The ID field needs to be loaded first.
541          *
542          * @return bool|array False on failure, array on success:
543          *   ('id' => block ID, 'autoIds' => array of autoblock IDs)
544          */
545         public function update() {
546                 wfDebug( "Block::update; timestamp {$this->mTimestamp}\n" );
547                 $dbw = wfGetDB( DB_MASTER );
548
549                 $dbw->startAtomic( __METHOD__ );
550
551                 $dbw->update(
552                         'ipblocks',
553                         $this->getDatabaseArray( $dbw ),
554                         [ 'ipb_id' => $this->getId() ],
555                         __METHOD__
556                 );
557
558                 $affected = $dbw->affectedRows();
559
560                 if ( $this->isAutoblocking() ) {
561                         // update corresponding autoblock(s) (T50813)
562                         $dbw->update(
563                                 'ipblocks',
564                                 $this->getAutoblockUpdateArray( $dbw ),
565                                 [ 'ipb_parent_block_id' => $this->getId() ],
566                                 __METHOD__
567                         );
568                 } else {
569                         // autoblock no longer required, delete corresponding autoblock(s)
570                         $dbw->delete(
571                                 'ipblocks',
572                                 [ 'ipb_parent_block_id' => $this->getId() ],
573                                 __METHOD__
574                         );
575                 }
576
577                 $dbw->endAtomic( __METHOD__ );
578
579                 if ( $affected ) {
580                         $auto_ipd_ids = $this->doRetroactiveAutoblock();
581                         return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
582                 }
583
584                 return false;
585         }
586
587         /**
588          * Get an array suitable for passing to $dbw->insert() or $dbw->update()
589          * @param IDatabase $dbw
590          * @return array
591          */
592         protected function getDatabaseArray( IDatabase $dbw ) {
593                 $expiry = $dbw->encodeExpiry( $this->mExpiry );
594
595                 if ( $this->forcedTargetID ) {
596                         $uid = $this->forcedTargetID;
597                 } else {
598                         $uid = $this->target instanceof User ? $this->target->getId() : 0;
599                 }
600
601                 $a = [
602                         'ipb_address'          => (string)$this->target,
603                         'ipb_user'             => $uid,
604                         'ipb_by'               => $this->getBy(),
605                         'ipb_by_text'          => $this->getByName(),
606                         'ipb_timestamp'        => $dbw->timestamp( $this->mTimestamp ),
607                         'ipb_auto'             => $this->mAuto,
608                         'ipb_anon_only'        => !$this->isHardblock(),
609                         'ipb_create_account'   => $this->prevents( 'createaccount' ),
610                         'ipb_enable_autoblock' => $this->isAutoblocking(),
611                         'ipb_expiry'           => $expiry,
612                         'ipb_range_start'      => $this->getRangeStart(),
613                         'ipb_range_end'        => $this->getRangeEnd(),
614                         'ipb_deleted'          => intval( $this->mHideName ), // typecast required for SQLite
615                         'ipb_block_email'      => $this->prevents( 'sendemail' ),
616                         'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
617                         'ipb_parent_block_id'  => $this->mParentBlockId
618                 ] + CommentStore::newKey( 'ipb_reason' )->insert( $dbw, $this->mReason );
619
620                 return $a;
621         }
622
623         /**
624          * @param IDatabase $dbw
625          * @return array
626          */
627         protected function getAutoblockUpdateArray( IDatabase $dbw ) {
628                 return [
629                         'ipb_by'               => $this->getBy(),
630                         'ipb_by_text'          => $this->getByName(),
631                         'ipb_create_account'   => $this->prevents( 'createaccount' ),
632                         'ipb_deleted'          => (int)$this->mHideName, // typecast required for SQLite
633                         'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
634                 ] + CommentStore::newKey( 'ipb_reason' )->insert( $dbw, $this->mReason );
635         }
636
637         /**
638          * Retroactively autoblocks the last IP used by the user (if it is a user)
639          * blocked by this Block.
640          *
641          * @return array Block IDs of retroactive autoblocks made
642          */
643         protected function doRetroactiveAutoblock() {
644                 $blockIds = [];
645                 # If autoblock is enabled, autoblock the LAST IP(s) used
646                 if ( $this->isAutoblocking() && $this->getType() == self::TYPE_USER ) {
647                         wfDebug( "Doing retroactive autoblocks for " . $this->getTarget() . "\n" );
648
649                         $continue = Hooks::run(
650                                 'PerformRetroactiveAutoblock', [ $this, &$blockIds ] );
651
652                         if ( $continue ) {
653                                 self::defaultRetroactiveAutoblock( $this, $blockIds );
654                         }
655                 }
656                 return $blockIds;
657         }
658
659         /**
660          * Retroactively autoblocks the last IP used by the user (if it is a user)
661          * blocked by this Block. This will use the recentchanges table.
662          *
663          * @param Block $block
664          * @param array &$blockIds
665          */
666         protected static function defaultRetroactiveAutoblock( Block $block, array &$blockIds ) {
667                 global $wgPutIPinRC;
668
669                 // No IPs are in recentchanges table, so nothing to select
670                 if ( !$wgPutIPinRC ) {
671                         return;
672                 }
673
674                 $dbr = wfGetDB( DB_REPLICA );
675
676                 $options = [ 'ORDER BY' => 'rc_timestamp DESC' ];
677                 $conds = [ 'rc_user_text' => (string)$block->getTarget() ];
678
679                 // Just the last IP used.
680                 $options['LIMIT'] = 1;
681
682                 $res = $dbr->select( 'recentchanges', [ 'rc_ip' ], $conds,
683                         __METHOD__, $options );
684
685                 if ( !$res->numRows() ) {
686                         # No results, don't autoblock anything
687                         wfDebug( "No IP found to retroactively autoblock\n" );
688                 } else {
689                         foreach ( $res as $row ) {
690                                 if ( $row->rc_ip ) {
691                                         $id = $block->doAutoblock( $row->rc_ip );
692                                         if ( $id ) {
693                                                 $blockIds[] = $id;
694                                         }
695                                 }
696                         }
697                 }
698         }
699
700         /**
701          * Checks whether a given IP is on the autoblock whitelist.
702          * TODO: this probably belongs somewhere else, but not sure where...
703          *
704          * @param string $ip The IP to check
705          * @return bool
706          */
707         public static function isWhitelistedFromAutoblocks( $ip ) {
708                 // Try to get the autoblock_whitelist from the cache, as it's faster
709                 // than getting the msg raw and explode()'ing it.
710                 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
711                 $lines = $cache->getWithSetCallback(
712                         $cache->makeKey( 'ipb', 'autoblock', 'whitelist' ),
713                         $cache::TTL_DAY,
714                         function ( $curValue, &$ttl, array &$setOpts ) {
715                                 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
716
717                                 return explode( "\n",
718                                         wfMessage( 'autoblock_whitelist' )->inContentLanguage()->plain() );
719                         }
720                 );
721
722                 wfDebug( "Checking the autoblock whitelist..\n" );
723
724                 foreach ( $lines as $line ) {
725                         # List items only
726                         if ( substr( $line, 0, 1 ) !== '*' ) {
727                                 continue;
728                         }
729
730                         $wlEntry = substr( $line, 1 );
731                         $wlEntry = trim( $wlEntry );
732
733                         wfDebug( "Checking $ip against $wlEntry..." );
734
735                         # Is the IP in this range?
736                         if ( IP::isInRange( $ip, $wlEntry ) ) {
737                                 wfDebug( " IP $ip matches $wlEntry, not autoblocking\n" );
738                                 return true;
739                         } else {
740                                 wfDebug( " No match\n" );
741                         }
742                 }
743
744                 return false;
745         }
746
747         /**
748          * Autoblocks the given IP, referring to this Block.
749          *
750          * @param string $autoblockIP The IP to autoblock.
751          * @return int|bool Block ID if an autoblock was inserted, false if not.
752          */
753         public function doAutoblock( $autoblockIP ) {
754                 # If autoblocks are disabled, go away.
755                 if ( !$this->isAutoblocking() ) {
756                         return false;
757                 }
758
759                 # Don't autoblock for system blocks
760                 if ( $this->getSystemBlockType() !== null ) {
761                         throw new MWException( 'Cannot autoblock from a system block' );
762                 }
763
764                 # Check for presence on the autoblock whitelist.
765                 if ( self::isWhitelistedFromAutoblocks( $autoblockIP ) ) {
766                         return false;
767                 }
768
769                 // Avoid PHP 7.1 warning of passing $this by reference
770                 $block = $this;
771                 # Allow hooks to cancel the autoblock.
772                 if ( !Hooks::run( 'AbortAutoblock', [ $autoblockIP, &$block ] ) ) {
773                         wfDebug( "Autoblock aborted by hook.\n" );
774                         return false;
775                 }
776
777                 # It's okay to autoblock. Go ahead and insert/update the block...
778
779                 # Do not add a *new* block if the IP is already blocked.
780                 $ipblock = self::newFromTarget( $autoblockIP );
781                 if ( $ipblock ) {
782                         # Check if the block is an autoblock and would exceed the user block
783                         # if renewed. If so, do nothing, otherwise prolong the block time...
784                         if ( $ipblock->mAuto && // @todo Why not compare $ipblock->mExpiry?
785                                 $this->mExpiry > self::getAutoblockExpiry( $ipblock->mTimestamp )
786                         ) {
787                                 # Reset block timestamp to now and its expiry to
788                                 # $wgAutoblockExpiry in the future
789                                 $ipblock->updateTimestamp();
790                         }
791                         return false;
792                 }
793
794                 # Make a new block object with the desired properties.
795                 $autoblock = new Block;
796                 wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" );
797                 $autoblock->setTarget( $autoblockIP );
798                 $autoblock->setBlocker( $this->getBlocker() );
799                 $autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason )
800                         ->inContentLanguage()->plain();
801                 $timestamp = wfTimestampNow();
802                 $autoblock->mTimestamp = $timestamp;
803                 $autoblock->mAuto = 1;
804                 $autoblock->prevents( 'createaccount', $this->prevents( 'createaccount' ) );
805                 # Continue suppressing the name if needed
806                 $autoblock->mHideName = $this->mHideName;
807                 $autoblock->prevents( 'editownusertalk', $this->prevents( 'editownusertalk' ) );
808                 $autoblock->mParentBlockId = $this->mId;
809
810                 if ( $this->mExpiry == 'infinity' ) {
811                         # Original block was indefinite, start an autoblock now
812                         $autoblock->mExpiry = self::getAutoblockExpiry( $timestamp );
813                 } else {
814                         # If the user is already blocked with an expiry date, we don't
815                         # want to pile on top of that.
816                         $autoblock->mExpiry = min( $this->mExpiry, self::getAutoblockExpiry( $timestamp ) );
817                 }
818
819                 # Insert the block...
820                 $status = $autoblock->insert();
821                 return $status
822                         ? $status['id']
823                         : false;
824         }
825
826         /**
827          * Check if a block has expired. Delete it if it is.
828          * @return bool
829          */
830         public function deleteIfExpired() {
831                 if ( $this->isExpired() ) {
832                         wfDebug( "Block::deleteIfExpired() -- deleting\n" );
833                         $this->delete();
834                         $retVal = true;
835                 } else {
836                         wfDebug( "Block::deleteIfExpired() -- not expired\n" );
837                         $retVal = false;
838                 }
839
840                 return $retVal;
841         }
842
843         /**
844          * Has the block expired?
845          * @return bool
846          */
847         public function isExpired() {
848                 $timestamp = wfTimestampNow();
849                 wfDebug( "Block::isExpired() checking current " . $timestamp . " vs $this->mExpiry\n" );
850
851                 if ( !$this->mExpiry ) {
852                         return false;
853                 } else {
854                         return $timestamp > $this->mExpiry;
855                 }
856         }
857
858         /**
859          * Is the block address valid (i.e. not a null string?)
860          * @return bool
861          */
862         public function isValid() {
863                 return $this->getTarget() != null;
864         }
865
866         /**
867          * Update the timestamp on autoblocks.
868          */
869         public function updateTimestamp() {
870                 if ( $this->mAuto ) {
871                         $this->mTimestamp = wfTimestamp();
872                         $this->mExpiry = self::getAutoblockExpiry( $this->mTimestamp );
873
874                         $dbw = wfGetDB( DB_MASTER );
875                         $dbw->update( 'ipblocks',
876                                 [ /* SET */
877                                         'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ),
878                                         'ipb_expiry' => $dbw->timestamp( $this->mExpiry ),
879                                 ],
880                                 [ /* WHERE */
881                                         'ipb_id' => $this->getId(),
882                                 ],
883                                 __METHOD__
884                         );
885                 }
886         }
887
888         /**
889          * Get the IP address at the start of the range in Hex form
890          * @throws MWException
891          * @return string IP in Hex form
892          */
893         public function getRangeStart() {
894                 switch ( $this->type ) {
895                         case self::TYPE_USER:
896                                 return '';
897                         case self::TYPE_IP:
898                                 return IP::toHex( $this->target );
899                         case self::TYPE_RANGE:
900                                 list( $start, /*...*/ ) = IP::parseRange( $this->target );
901                                 return $start;
902                         default:
903                                 throw new MWException( "Block with invalid type" );
904                 }
905         }
906
907         /**
908          * Get the IP address at the end of the range in Hex form
909          * @throws MWException
910          * @return string IP in Hex form
911          */
912         public function getRangeEnd() {
913                 switch ( $this->type ) {
914                         case self::TYPE_USER:
915                                 return '';
916                         case self::TYPE_IP:
917                                 return IP::toHex( $this->target );
918                         case self::TYPE_RANGE:
919                                 list( /*...*/, $end ) = IP::parseRange( $this->target );
920                                 return $end;
921                         default:
922                                 throw new MWException( "Block with invalid type" );
923                 }
924         }
925
926         /**
927          * Get the user id of the blocking sysop
928          *
929          * @return int (0 for foreign users)
930          */
931         public function getBy() {
932                 $blocker = $this->getBlocker();
933                 return ( $blocker instanceof User )
934                         ? $blocker->getId()
935                         : 0;
936         }
937
938         /**
939          * Get the username of the blocking sysop
940          *
941          * @return string
942          */
943         public function getByName() {
944                 $blocker = $this->getBlocker();
945                 return ( $blocker instanceof User )
946                         ? $blocker->getName()
947                         : (string)$blocker; // username
948         }
949
950         /**
951          * Get the block ID
952          * @return int
953          */
954         public function getId() {
955                 return $this->mId;
956         }
957
958         /**
959          * Get the system block type, if any
960          * @since 1.29
961          * @return string|null
962          */
963         public function getSystemBlockType() {
964                 return $this->systemBlockType;
965         }
966
967         /**
968          * Get/set a flag determining whether the master is used for reads
969          *
970          * @param bool|null $x
971          * @return bool
972          */
973         public function fromMaster( $x = null ) {
974                 return wfSetVar( $this->mFromMaster, $x );
975         }
976
977         /**
978          * Get/set whether the Block is a hardblock (affects logged-in users on a given IP/range)
979          * @param bool|null $x
980          * @return bool
981          */
982         public function isHardblock( $x = null ) {
983                 wfSetVar( $this->isHardblock, $x );
984
985                 # You can't *not* hardblock a user
986                 return $this->getType() == self::TYPE_USER
987                         ? true
988                         : $this->isHardblock;
989         }
990
991         /**
992          * @param null|bool $x
993          * @return bool
994          */
995         public function isAutoblocking( $x = null ) {
996                 wfSetVar( $this->isAutoblocking, $x );
997
998                 # You can't put an autoblock on an IP or range as we don't have any history to
999                 # look over to get more IPs from
1000                 return $this->getType() == self::TYPE_USER
1001                         ? $this->isAutoblocking
1002                         : false;
1003         }
1004
1005         /**
1006          * Get/set whether the Block prevents a given action
1007          *
1008          * @param string $action Action to check
1009          * @param bool|null $x Value for set, or null to just get value
1010          * @return bool|null Null for unrecognized rights.
1011          */
1012         public function prevents( $action, $x = null ) {
1013                 global $wgBlockDisablesLogin;
1014                 $res = null;
1015                 switch ( $action ) {
1016                         case 'edit':
1017                                 # For now... <evil laugh>
1018                                 $res = true;
1019                                 break;
1020                         case 'createaccount':
1021                                 $res = wfSetVar( $this->mCreateAccount, $x );
1022                                 break;
1023                         case 'sendemail':
1024                                 $res = wfSetVar( $this->mBlockEmail, $x );
1025                                 break;
1026                         case 'editownusertalk':
1027                                 $res = wfSetVar( $this->mDisableUsertalk, $x );
1028                                 break;
1029                         case 'read':
1030                                 $res = false;
1031                                 break;
1032                 }
1033                 if ( !$res && $wgBlockDisablesLogin ) {
1034                         // If a block would disable login, then it should
1035                         // prevent any action that all users cannot do
1036                         $anon = new User;
1037                         $res = $anon->isAllowed( $action ) ? $res : true;
1038                 }
1039
1040                 return $res;
1041         }
1042
1043         /**
1044          * Get the block name, but with autoblocked IPs hidden as per standard privacy policy
1045          * @return string Text is escaped
1046          */
1047         public function getRedactedName() {
1048                 if ( $this->mAuto ) {
1049                         return Html::rawElement(
1050                                 'span',
1051                                 [ 'class' => 'mw-autoblockid' ],
1052                                 wfMessage( 'autoblockid', $this->mId )
1053                         );
1054                 } else {
1055                         return htmlspecialchars( $this->getTarget() );
1056                 }
1057         }
1058
1059         /**
1060          * Get a timestamp of the expiry for autoblocks
1061          *
1062          * @param string|int $timestamp
1063          * @return string
1064          */
1065         public static function getAutoblockExpiry( $timestamp ) {
1066                 global $wgAutoblockExpiry;
1067
1068                 return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry );
1069         }
1070
1071         /**
1072          * Purge expired blocks from the ipblocks table
1073          */
1074         public static function purgeExpired() {
1075                 if ( wfReadOnly() ) {
1076                         return;
1077                 }
1078
1079                 DeferredUpdates::addUpdate( new AtomicSectionUpdate(
1080                         wfGetDB( DB_MASTER ),
1081                         __METHOD__,
1082                         function ( IDatabase $dbw, $fname ) {
1083                                 $dbw->delete(
1084                                         'ipblocks',
1085                                         [ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
1086                                         $fname
1087                                 );
1088                         }
1089                 ) );
1090         }
1091
1092         /**
1093          * Given a target and the target's type, get an existing Block object if possible.
1094          * @param string|User|int $specificTarget A block target, which may be one of several types:
1095          *     * A user to block, in which case $target will be a User
1096          *     * An IP to block, in which case $target will be a User generated by using
1097          *       User::newFromName( $ip, false ) to turn off name validation
1098          *     * An IP range, in which case $target will be a String "123.123.123.123/18" etc
1099          *     * The ID of an existing block, in the format "#12345" (since pure numbers are valid
1100          *       usernames
1101          *     Calling this with a user, IP address or range will not select autoblocks, and will
1102          *     only select a block where the targets match exactly (so looking for blocks on
1103          *     1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32)
1104          * @param string|User|int $vagueTarget As above, but we will search for *any* block which
1105          *     affects that target (so for an IP address, get ranges containing that IP; and also
1106          *     get any relevant autoblocks). Leave empty or blank to skip IP-based lookups.
1107          * @param bool $fromMaster Whether to use the DB_MASTER database
1108          * @return Block|null (null if no relevant block could be found).  The target and type
1109          *     of the returned Block will refer to the actual block which was found, which might
1110          *     not be the same as the target you gave if you used $vagueTarget!
1111          */
1112         public static function newFromTarget( $specificTarget, $vagueTarget = null, $fromMaster = false ) {
1113                 list( $target, $type ) = self::parseTarget( $specificTarget );
1114                 if ( $type == self::TYPE_ID || $type == self::TYPE_AUTO ) {
1115                         return self::newFromID( $target );
1116
1117                 } elseif ( $target === null && $vagueTarget == '' ) {
1118                         # We're not going to find anything useful here
1119                         # Be aware that the == '' check is explicit, since empty values will be
1120                         # passed by some callers (T31116)
1121                         return null;
1122
1123                 } elseif ( in_array(
1124                         $type,
1125                         [ self::TYPE_USER, self::TYPE_IP, self::TYPE_RANGE, null ] )
1126                 ) {
1127                         $block = new Block();
1128                         $block->fromMaster( $fromMaster );
1129
1130                         if ( $type !== null ) {
1131                                 $block->setTarget( $target );
1132                         }
1133
1134                         if ( $block->newLoad( $vagueTarget ) ) {
1135                                 return $block;
1136                         }
1137                 }
1138                 return null;
1139         }
1140
1141         /**
1142          * Get all blocks that match any IP from an array of IP addresses
1143          *
1144          * @param array $ipChain List of IPs (strings), usually retrieved from the
1145          *     X-Forwarded-For header of the request
1146          * @param bool $isAnon Exclude anonymous-only blocks if false
1147          * @param bool $fromMaster Whether to query the master or replica DB
1148          * @return array Array of Blocks
1149          * @since 1.22
1150          */
1151         public static function getBlocksForIPList( array $ipChain, $isAnon, $fromMaster = false ) {
1152                 if ( !count( $ipChain ) ) {
1153                         return [];
1154                 }
1155
1156                 $conds = [];
1157                 $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup();
1158                 foreach ( array_unique( $ipChain ) as $ipaddr ) {
1159                         # Discard invalid IP addresses. Since XFF can be spoofed and we do not
1160                         # necessarily trust the header given to us, make sure that we are only
1161                         # checking for blocks on well-formatted IP addresses (IPv4 and IPv6).
1162                         # Do not treat private IP spaces as special as it may be desirable for wikis
1163                         # to block those IP ranges in order to stop misbehaving proxies that spoof XFF.
1164                         if ( !IP::isValid( $ipaddr ) ) {
1165                                 continue;
1166                         }
1167                         # Don't check trusted IPs (includes local squids which will be in every request)
1168                         if ( $proxyLookup->isTrustedProxy( $ipaddr ) ) {
1169                                 continue;
1170                         }
1171                         # Check both the original IP (to check against single blocks), as well as build
1172                         # the clause to check for rangeblocks for the given IP.
1173                         $conds['ipb_address'][] = $ipaddr;
1174                         $conds[] = self::getRangeCond( IP::toHex( $ipaddr ) );
1175                 }
1176
1177                 if ( !count( $conds ) ) {
1178                         return [];
1179                 }
1180
1181                 if ( $fromMaster ) {
1182                         $db = wfGetDB( DB_MASTER );
1183                 } else {
1184                         $db = wfGetDB( DB_REPLICA );
1185                 }
1186                 $conds = $db->makeList( $conds, LIST_OR );
1187                 if ( !$isAnon ) {
1188                         $conds = [ $conds, 'ipb_anon_only' => 0 ];
1189                 }
1190                 $selectFields = array_merge(
1191                         [ 'ipb_range_start', 'ipb_range_end' ],
1192                         self::selectFields()
1193                 );
1194                 $rows = $db->select( 'ipblocks',
1195                         $selectFields,
1196                         $conds,
1197                         __METHOD__
1198                 );
1199
1200                 $blocks = [];
1201                 foreach ( $rows as $row ) {
1202                         $block = self::newFromRow( $row );
1203                         if ( !$block->isExpired() ) {
1204                                 $blocks[] = $block;
1205                         }
1206                 }
1207
1208                 return $blocks;
1209         }
1210
1211         /**
1212          * From a list of multiple blocks, find the most exact and strongest Block.
1213          *
1214          * The logic for finding the "best" block is:
1215          *  - Blocks that match the block's target IP are preferred over ones in a range
1216          *  - Hardblocks are chosen over softblocks that prevent account creation
1217          *  - Softblocks that prevent account creation are chosen over other softblocks
1218          *  - Other softblocks are chosen over autoblocks
1219          *  - If there are multiple exact or range blocks at the same level, the one chosen
1220          *    is random
1221          * This should be used when $blocks where retrieved from the user's IP address
1222          * and $ipChain is populated from the same IP address information.
1223          *
1224          * @param array $blocks Array of Block objects
1225          * @param array $ipChain List of IPs (strings). This is used to determine how "close"
1226          *     a block is to the server, and if a block matches exactly, or is in a range.
1227          *     The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2,
1228          *     local-squid, ...)
1229          * @throws MWException
1230          * @return Block|null The "best" block from the list
1231          */
1232         public static function chooseBlock( array $blocks, array $ipChain ) {
1233                 if ( !count( $blocks ) ) {
1234                         return null;
1235                 } elseif ( count( $blocks ) == 1 ) {
1236                         return $blocks[0];
1237                 }
1238
1239                 // Sort hard blocks before soft ones and secondarily sort blocks
1240                 // that disable account creation before those that don't.
1241                 usort( $blocks, function ( Block $a, Block $b ) {
1242                         $aWeight = (int)$a->isHardblock() . (int)$a->prevents( 'createaccount' );
1243                         $bWeight = (int)$b->isHardblock() . (int)$b->prevents( 'createaccount' );
1244                         return strcmp( $bWeight, $aWeight ); // highest weight first
1245                 } );
1246
1247                 $blocksListExact = [
1248                         'hard' => false,
1249                         'disable_create' => false,
1250                         'other' => false,
1251                         'auto' => false
1252                 ];
1253                 $blocksListRange = [
1254                         'hard' => false,
1255                         'disable_create' => false,
1256                         'other' => false,
1257                         'auto' => false
1258                 ];
1259                 $ipChain = array_reverse( $ipChain );
1260
1261                 /** @var Block $block */
1262                 foreach ( $blocks as $block ) {
1263                         // Stop searching if we have already have a "better" block. This
1264                         // is why the order of the blocks matters
1265                         if ( !$block->isHardblock() && $blocksListExact['hard'] ) {
1266                                 break;
1267                         } elseif ( !$block->prevents( 'createaccount' ) && $blocksListExact['disable_create'] ) {
1268                                 break;
1269                         }
1270
1271                         foreach ( $ipChain as $checkip ) {
1272                                 $checkipHex = IP::toHex( $checkip );
1273                                 if ( (string)$block->getTarget() === $checkip ) {
1274                                         if ( $block->isHardblock() ) {
1275                                                 $blocksListExact['hard'] = $blocksListExact['hard'] ?: $block;
1276                                         } elseif ( $block->prevents( 'createaccount' ) ) {
1277                                                 $blocksListExact['disable_create'] = $blocksListExact['disable_create'] ?: $block;
1278                                         } elseif ( $block->mAuto ) {
1279                                                 $blocksListExact['auto'] = $blocksListExact['auto'] ?: $block;
1280                                         } else {
1281                                                 $blocksListExact['other'] = $blocksListExact['other'] ?: $block;
1282                                         }
1283                                         // We found closest exact match in the ip list, so go to the next Block
1284                                         break;
1285                                 } elseif ( array_filter( $blocksListExact ) == []
1286                                         && $block->getRangeStart() <= $checkipHex
1287                                         && $block->getRangeEnd() >= $checkipHex
1288                                 ) {
1289                                         if ( $block->isHardblock() ) {
1290                                                 $blocksListRange['hard'] = $blocksListRange['hard'] ?: $block;
1291                                         } elseif ( $block->prevents( 'createaccount' ) ) {
1292                                                 $blocksListRange['disable_create'] = $blocksListRange['disable_create'] ?: $block;
1293                                         } elseif ( $block->mAuto ) {
1294                                                 $blocksListRange['auto'] = $blocksListRange['auto'] ?: $block;
1295                                         } else {
1296                                                 $blocksListRange['other'] = $blocksListRange['other'] ?: $block;
1297                                         }
1298                                         break;
1299                                 }
1300                         }
1301                 }
1302
1303                 if ( array_filter( $blocksListExact ) == [] ) {
1304                         $blocksList = &$blocksListRange;
1305                 } else {
1306                         $blocksList = &$blocksListExact;
1307                 }
1308
1309                 $chosenBlock = null;
1310                 if ( $blocksList['hard'] ) {
1311                         $chosenBlock = $blocksList['hard'];
1312                 } elseif ( $blocksList['disable_create'] ) {
1313                         $chosenBlock = $blocksList['disable_create'];
1314                 } elseif ( $blocksList['other'] ) {
1315                         $chosenBlock = $blocksList['other'];
1316                 } elseif ( $blocksList['auto'] ) {
1317                         $chosenBlock = $blocksList['auto'];
1318                 } else {
1319                         throw new MWException( "Proxy block found, but couldn't be classified." );
1320                 }
1321
1322                 return $chosenBlock;
1323         }
1324
1325         /**
1326          * From an existing Block, get the target and the type of target.
1327          * Note that, except for null, it is always safe to treat the target
1328          * as a string; for User objects this will return User::__toString()
1329          * which in turn gives User::getName().
1330          *
1331          * @param string|int|User|null $target
1332          * @return array [ User|String|null, Block::TYPE_ constant|null ]
1333          */
1334         public static function parseTarget( $target ) {
1335                 # We may have been through this before
1336                 if ( $target instanceof User ) {
1337                         if ( IP::isValid( $target->getName() ) ) {
1338                                 return [ $target, self::TYPE_IP ];
1339                         } else {
1340                                 return [ $target, self::TYPE_USER ];
1341                         }
1342                 } elseif ( $target === null ) {
1343                         return [ null, null ];
1344                 }
1345
1346                 $target = trim( $target );
1347
1348                 if ( IP::isValid( $target ) ) {
1349                         # We can still create a User if it's an IP address, but we need to turn
1350                         # off validation checking (which would exclude IP addresses)
1351                         return [
1352                                 User::newFromName( IP::sanitizeIP( $target ), false ),
1353                                 self::TYPE_IP
1354                         ];
1355
1356                 } elseif ( IP::isValidRange( $target ) ) {
1357                         # Can't create a User from an IP range
1358                         return [ IP::sanitizeRange( $target ), self::TYPE_RANGE ];
1359                 }
1360
1361                 # Consider the possibility that this is not a username at all
1362                 # but actually an old subpage (bug #29797)
1363                 if ( strpos( $target, '/' ) !== false ) {
1364                         # An old subpage, drill down to the user behind it
1365                         $target = explode( '/', $target )[0];
1366                 }
1367
1368                 $userObj = User::newFromName( $target );
1369                 if ( $userObj instanceof User ) {
1370                         # Note that since numbers are valid usernames, a $target of "12345" will be
1371                         # considered a User.  If you want to pass a block ID, prepend a hash "#12345",
1372                         # since hash characters are not valid in usernames or titles generally.
1373                         return [ $userObj, self::TYPE_USER ];
1374
1375                 } elseif ( preg_match( '/^#\d+$/', $target ) ) {
1376                         # Autoblock reference in the form "#12345"
1377                         return [ substr( $target, 1 ), self::TYPE_AUTO ];
1378
1379                 } else {
1380                         # WTF?
1381                         return [ null, null ];
1382                 }
1383         }
1384
1385         /**
1386          * Get the type of target for this particular block
1387          * @return int Block::TYPE_ constant, will never be TYPE_ID
1388          */
1389         public function getType() {
1390                 return $this->mAuto
1391                         ? self::TYPE_AUTO
1392                         : $this->type;
1393         }
1394
1395         /**
1396          * Get the target and target type for this particular Block.  Note that for autoblocks,
1397          * this returns the unredacted name; frontend functions need to call $block->getRedactedName()
1398          * in this situation.
1399          * @return array [ User|String, Block::TYPE_ constant ]
1400          * @todo FIXME: This should be an integral part of the Block member variables
1401          */
1402         public function getTargetAndType() {
1403                 return [ $this->getTarget(), $this->getType() ];
1404         }
1405
1406         /**
1407          * Get the target for this particular Block.  Note that for autoblocks,
1408          * this returns the unredacted name; frontend functions need to call $block->getRedactedName()
1409          * in this situation.
1410          * @return User|string
1411          */
1412         public function getTarget() {
1413                 return $this->target;
1414         }
1415
1416         /**
1417          * @since 1.19
1418          *
1419          * @return mixed|string
1420          */
1421         public function getExpiry() {
1422                 return $this->mExpiry;
1423         }
1424
1425         /**
1426          * Set the target for this block, and update $this->type accordingly
1427          * @param mixed $target
1428          */
1429         public function setTarget( $target ) {
1430                 list( $this->target, $this->type ) = self::parseTarget( $target );
1431         }
1432
1433         /**
1434          * Get the user who implemented this block
1435          * @return User|string Local User object or string for a foreign user
1436          */
1437         public function getBlocker() {
1438                 return $this->blocker;
1439         }
1440
1441         /**
1442          * Set the user who implemented (or will implement) this block
1443          * @param User|string $user Local User object or username string for foreign users
1444          */
1445         public function setBlocker( $user ) {
1446                 $this->blocker = $user;
1447         }
1448
1449         /**
1450          * Set the 'BlockID' cookie to this block's ID and expiry time. The cookie's expiry will be
1451          * the same as the block's, to a maximum of 24 hours.
1452          *
1453          * @since 1.29
1454          *
1455          * @param WebResponse $response The response on which to set the cookie.
1456          */
1457         public function setCookie( WebResponse $response ) {
1458                 // Calculate the default expiry time.
1459                 $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) );
1460
1461                 // Use the Block's expiry time only if it's less than the default.
1462                 $expiryTime = $this->getExpiry();
1463                 if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
1464                         $expiryTime = $maxExpiryTime;
1465                 }
1466
1467                 // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie.
1468                 $expiryValue = DateTime::createFromFormat( 'YmdHis', $expiryTime )->format( 'U' );
1469                 $cookieOptions = [ 'httpOnly' => false ];
1470                 $cookieValue = $this->getCookieValue();
1471                 $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
1472         }
1473
1474         /**
1475          * Unset the 'BlockID' cookie.
1476          *
1477          * @since 1.29
1478          *
1479          * @param WebResponse $response The response on which to unset the cookie.
1480          */
1481         public static function clearCookie( WebResponse $response ) {
1482                 $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
1483         }
1484
1485         /**
1486          * Get the BlockID cookie's value for this block. This is usually the block ID concatenated
1487          * with an HMAC in order to avoid spoofing (T152951), but if wgSecretKey is not set will just
1488          * be the block ID.
1489          *
1490          * @since 1.29
1491          *
1492          * @return string The block ID, probably concatenated with "!" and the HMAC.
1493          */
1494         public function getCookieValue() {
1495                 $config = RequestContext::getMain()->getConfig();
1496                 $id = $this->getId();
1497                 $secretKey = $config->get( 'SecretKey' );
1498                 if ( !$secretKey ) {
1499                         // If there's no secret key, don't append a HMAC.
1500                         return $id;
1501                 }
1502                 $hmac = MWCryptHash::hmac( $id, $secretKey, false );
1503                 $cookieValue = $id . '!' . $hmac;
1504                 return $cookieValue;
1505         }
1506
1507         /**
1508          * Get the stored ID from the 'BlockID' cookie. The cookie's value is usually a combination of
1509          * the ID and a HMAC (see Block::setCookie), but will sometimes only be the ID.
1510          *
1511          * @since 1.29
1512          *
1513          * @param string $cookieValue The string in which to find the ID.
1514          *
1515          * @return int|null The block ID, or null if the HMAC is present and invalid.
1516          */
1517         public static function getIdFromCookieValue( $cookieValue ) {
1518                 // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
1519                 $bangPos = strpos( $cookieValue, '!' );
1520                 $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
1521                 // Get the site-wide secret key.
1522                 $config = RequestContext::getMain()->getConfig();
1523                 $secretKey = $config->get( 'SecretKey' );
1524                 if ( !$secretKey ) {
1525                         // If there's no secret key, just use the ID as given.
1526                         return $id;
1527                 }
1528                 $storedHmac = substr( $cookieValue, $bangPos + 1 );
1529                 $calculatedHmac = MWCryptHash::hmac( $id, $secretKey, false );
1530                 if ( $calculatedHmac === $storedHmac ) {
1531                         return $id;
1532                 } else {
1533                         return null;
1534                 }
1535         }
1536
1537         /**
1538          * Get the key and parameters for the corresponding error message.
1539          *
1540          * @since 1.22
1541          * @param IContextSource $context
1542          * @return array
1543          */
1544         public function getPermissionsError( IContextSource $context ) {
1545                 $blocker = $this->getBlocker();
1546                 if ( $blocker instanceof User ) { // local user
1547                         $blockerUserpage = $blocker->getUserPage();
1548                         $link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerUserpage->getText()}]]";
1549                 } else { // foreign user
1550                         $link = $blocker;
1551                 }
1552
1553                 $reason = $this->mReason;
1554                 if ( $reason == '' ) {
1555                         $reason = $context->msg( 'blockednoreason' )->text();
1556                 }
1557
1558                 /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked.
1559                  * This could be a username, an IP range, or a single IP. */
1560                 $intended = $this->getTarget();
1561
1562                 $systemBlockType = $this->getSystemBlockType();
1563
1564                 $lang = $context->getLanguage();
1565                 return [
1566                         $systemBlockType !== null
1567                                 ? 'systemblockedtext'
1568                                 : ( $this->mAuto ? 'autoblockedtext' : 'blockedtext' ),
1569                         $link,
1570                         $reason,
1571                         $context->getRequest()->getIP(),
1572                         $this->getByName(),
1573                         $systemBlockType !== null ? $systemBlockType : $this->getId(),
1574                         $lang->formatExpiry( $this->mExpiry ),
1575                         (string)$intended,
1576                         $lang->userTimeAndDate( $this->mTimestamp, $context->getUser() ),
1577                 ];
1578         }
1579 }