DatabaseBlock.php 46 KB

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