BlockRestrictionStore.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <?php
  2. /**
  3. * Block restriction interface.
  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 MediaWiki\Block\Restriction\NamespaceRestriction;
  24. use MediaWiki\Block\Restriction\PageRestriction;
  25. use MediaWiki\Block\Restriction\Restriction;
  26. use MWException;
  27. use stdClass;
  28. use Wikimedia\Rdbms\IResultWrapper;
  29. use Wikimedia\Rdbms\IDatabase;
  30. use Wikimedia\Rdbms\ILoadBalancer;
  31. class BlockRestrictionStore {
  32. /**
  33. * Map of all of the restriction types.
  34. */
  35. private $types = [
  36. PageRestriction::TYPE_ID => PageRestriction::class,
  37. NamespaceRestriction::TYPE_ID => NamespaceRestriction::class,
  38. ];
  39. /**
  40. * @var ILoadBalancer
  41. */
  42. private $loadBalancer;
  43. /**
  44. * @param ILoadBalancer $loadBalancer load balancer for acquiring database connections
  45. */
  46. public function __construct( ILoadBalancer $loadBalancer ) {
  47. $this->loadBalancer = $loadBalancer;
  48. }
  49. /**
  50. * Retrieves the restrictions from the database by block id.
  51. *
  52. * @since 1.33
  53. * @param int|array $blockId
  54. * @param IDatabase|null $db
  55. * @return Restriction[]
  56. */
  57. public function loadByBlockId( $blockId, IDatabase $db = null ) {
  58. if ( $blockId === null || $blockId === [] ) {
  59. return [];
  60. }
  61. $db = $db ?: $this->loadBalancer->getConnectionRef( DB_REPLICA );
  62. $result = $db->select(
  63. [ 'ipblocks_restrictions', 'page' ],
  64. [ 'ir_ipb_id', 'ir_type', 'ir_value', 'page_namespace', 'page_title' ],
  65. [ 'ir_ipb_id' => $blockId ],
  66. __METHOD__,
  67. [],
  68. [ 'page' => [ 'LEFT JOIN', [ 'ir_type' => PageRestriction::TYPE_ID, 'ir_value=page_id' ] ] ]
  69. );
  70. return $this->resultToRestrictions( $result );
  71. }
  72. /**
  73. * Inserts the restrictions into the database.
  74. *
  75. * @since 1.33
  76. * @param Restriction[] $restrictions
  77. * @return bool
  78. */
  79. public function insert( array $restrictions ) {
  80. if ( !$restrictions ) {
  81. return false;
  82. }
  83. $rows = [];
  84. foreach ( $restrictions as $restriction ) {
  85. if ( !$restriction instanceof Restriction ) {
  86. continue;
  87. }
  88. $rows[] = $restriction->toRow();
  89. }
  90. if ( !$rows ) {
  91. return false;
  92. }
  93. $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
  94. $dbw->insert(
  95. 'ipblocks_restrictions',
  96. $rows,
  97. __METHOD__,
  98. [ 'IGNORE' ]
  99. );
  100. return true;
  101. }
  102. /**
  103. * Updates the list of restrictions. This method does not allow removing all
  104. * of the restrictions. To do that, use ::deleteByBlockId().
  105. *
  106. * @since 1.33
  107. * @param Restriction[] $restrictions
  108. * @return bool
  109. */
  110. public function update( array $restrictions ) {
  111. $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
  112. $dbw->startAtomic( __METHOD__ );
  113. // Organize the restrictions by blockid.
  114. $restrictionList = $this->restrictionsByBlockId( $restrictions );
  115. // Load the existing restrictions and organize by block id. Any block ids
  116. // that were passed into this function will be used to load all of the
  117. // existing restrictions. This list might be the same, or may be completely
  118. // different.
  119. $existingList = [];
  120. $blockIds = array_keys( $restrictionList );
  121. if ( !empty( $blockIds ) ) {
  122. $result = $dbw->select(
  123. [ 'ipblocks_restrictions' ],
  124. [ 'ir_ipb_id', 'ir_type', 'ir_value' ],
  125. [ 'ir_ipb_id' => $blockIds ],
  126. __METHOD__,
  127. [ 'FOR UPDATE' ]
  128. );
  129. $existingList = $this->restrictionsByBlockId(
  130. $this->resultToRestrictions( $result )
  131. );
  132. }
  133. $result = true;
  134. // Perform the actions on a per block-id basis.
  135. foreach ( $restrictionList as $blockId => $blockRestrictions ) {
  136. // Insert all of the restrictions first, ignoring ones that already exist.
  137. $success = $this->insert( $blockRestrictions );
  138. // Update the result. The first false is the result, otherwise, true.
  139. $result = $success && $result;
  140. $restrictionsToRemove = $this->restrictionsToRemove(
  141. $existingList[$blockId] ?? [],
  142. $restrictions
  143. );
  144. if ( empty( $restrictionsToRemove ) ) {
  145. continue;
  146. }
  147. $success = $this->delete( $restrictionsToRemove );
  148. // Update the result. The first false is the result, otherwise, true.
  149. $result = $success && $result;
  150. }
  151. $dbw->endAtomic( __METHOD__ );
  152. return $result;
  153. }
  154. /**
  155. * Updates the list of restrictions by parent id.
  156. *
  157. * @since 1.33
  158. * @param int $parentBlockId
  159. * @param Restriction[] $restrictions
  160. * @return bool
  161. */
  162. public function updateByParentBlockId( $parentBlockId, array $restrictions ) {
  163. // If removing all of the restrictions, then just delete them all.
  164. if ( empty( $restrictions ) ) {
  165. return $this->deleteByParentBlockId( $parentBlockId );
  166. }
  167. $parentBlockId = (int)$parentBlockId;
  168. $db = $this->loadBalancer->getConnectionRef( DB_MASTER );
  169. $db->startAtomic( __METHOD__ );
  170. $blockIds = $db->selectFieldValues(
  171. 'ipblocks',
  172. 'ipb_id',
  173. [ 'ipb_parent_block_id' => $parentBlockId ],
  174. __METHOD__,
  175. [ 'FOR UPDATE' ]
  176. );
  177. $result = true;
  178. foreach ( $blockIds as $id ) {
  179. $success = $this->update( $this->setBlockId( $id, $restrictions ) );
  180. // Update the result. The first false is the result, otherwise, true.
  181. $result = $success && $result;
  182. }
  183. $db->endAtomic( __METHOD__ );
  184. return $result;
  185. }
  186. /**
  187. * Delete the restrictions.
  188. *
  189. * @since 1.33
  190. * @param Restriction[] $restrictions
  191. * @throws MWException
  192. * @return bool
  193. */
  194. public function delete( array $restrictions ) {
  195. $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
  196. $result = true;
  197. foreach ( $restrictions as $restriction ) {
  198. if ( !$restriction instanceof Restriction ) {
  199. continue;
  200. }
  201. $success = $dbw->delete(
  202. 'ipblocks_restrictions',
  203. // The restriction row is made up of a compound primary key. Therefore,
  204. // the row and the delete conditions are the same.
  205. $restriction->toRow(),
  206. __METHOD__
  207. );
  208. // Update the result. The first false is the result, otherwise, true.
  209. $result = $success && $result;
  210. }
  211. return $result;
  212. }
  213. /**
  214. * Delete the restrictions by block ID.
  215. *
  216. * @since 1.33
  217. * @param int|array $blockId
  218. * @throws MWException
  219. * @return bool
  220. */
  221. public function deleteByBlockId( $blockId ) {
  222. $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
  223. return $dbw->delete(
  224. 'ipblocks_restrictions',
  225. [ 'ir_ipb_id' => $blockId ],
  226. __METHOD__
  227. );
  228. }
  229. /**
  230. * Delete the restrictions by parent block ID.
  231. *
  232. * @since 1.33
  233. * @param int|array $parentBlockId
  234. * @throws MWException
  235. * @return bool
  236. */
  237. public function deleteByParentBlockId( $parentBlockId ) {
  238. $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
  239. return $dbw->deleteJoin(
  240. 'ipblocks_restrictions',
  241. 'ipblocks',
  242. 'ir_ipb_id',
  243. 'ipb_id',
  244. [ 'ipb_parent_block_id' => $parentBlockId ],
  245. __METHOD__
  246. );
  247. }
  248. /**
  249. * Checks if two arrays of Restrictions are effectively equal. This is a loose
  250. * equality check as the restrictions do not have to contain the same block
  251. * ids.
  252. *
  253. * @since 1.33
  254. * @param Restriction[] $a
  255. * @param Restriction[] $b
  256. * @return bool
  257. */
  258. public function equals( array $a, array $b ) {
  259. $filter = function ( $restriction ) {
  260. return $restriction instanceof Restriction;
  261. };
  262. // Ensure that every item in the array is a Restriction. This prevents a
  263. // fatal error from calling Restriction::getHash if something in the array
  264. // is not a restriction.
  265. $a = array_filter( $a, $filter );
  266. $b = array_filter( $b, $filter );
  267. $aCount = count( $a );
  268. $bCount = count( $b );
  269. // If the count is different, then they are obviously a different set.
  270. if ( $aCount !== $bCount ) {
  271. return false;
  272. }
  273. // If both sets contain no items, then they are the same set.
  274. if ( $aCount === 0 && $bCount === 0 ) {
  275. return true;
  276. }
  277. $hasher = function ( $r ) {
  278. return $r->getHash();
  279. };
  280. $aHashes = array_map( $hasher, $a );
  281. $bHashes = array_map( $hasher, $b );
  282. sort( $aHashes );
  283. sort( $bHashes );
  284. return $aHashes === $bHashes;
  285. }
  286. /**
  287. * Set the blockId on a set of restrictions and return a new set.
  288. *
  289. * @since 1.33
  290. * @param int $blockId
  291. * @param Restriction[] $restrictions
  292. * @return Restriction[]
  293. */
  294. public function setBlockId( $blockId, array $restrictions ) {
  295. $blockRestrictions = [];
  296. foreach ( $restrictions as $restriction ) {
  297. if ( !$restriction instanceof Restriction ) {
  298. continue;
  299. }
  300. // Clone the restriction so any references to the current restriction are
  301. // not suddenly changed to a different blockId.
  302. $restriction = clone $restriction;
  303. $restriction->setBlockId( $blockId );
  304. $blockRestrictions[] = $restriction;
  305. }
  306. return $blockRestrictions;
  307. }
  308. /**
  309. * Get the restrictions that should be removed, which are existing
  310. * restrictions that are not in the new list of restrictions.
  311. *
  312. * @param Restriction[] $existing
  313. * @param Restriction[] $new
  314. * @return array
  315. */
  316. private function restrictionsToRemove( array $existing, array $new ) {
  317. return array_filter( $existing, function ( $e ) use ( $new ) {
  318. foreach ( $new as $restriction ) {
  319. if ( !$restriction instanceof Restriction ) {
  320. continue;
  321. }
  322. if ( $restriction->equals( $e ) ) {
  323. return false;
  324. }
  325. }
  326. return true;
  327. } );
  328. }
  329. /**
  330. * Converts an array of restrictions to an associative array of restrictions
  331. * where the keys are the block ids.
  332. *
  333. * @param Restriction[] $restrictions
  334. * @return array
  335. */
  336. private function restrictionsByBlockId( array $restrictions ) {
  337. $blockRestrictions = [];
  338. foreach ( $restrictions as $restriction ) {
  339. // Ensure that all of the items in the array are restrictions.
  340. if ( !$restriction instanceof Restriction ) {
  341. continue;
  342. }
  343. if ( !isset( $blockRestrictions[$restriction->getBlockId()] ) ) {
  344. $blockRestrictions[$restriction->getBlockId()] = [];
  345. }
  346. $blockRestrictions[$restriction->getBlockId()][] = $restriction;
  347. }
  348. return $blockRestrictions;
  349. }
  350. /**
  351. * Convert an Result Wrapper to an array of restrictions.
  352. *
  353. * @param IResultWrapper $result
  354. * @return Restriction[]
  355. */
  356. private function resultToRestrictions( IResultWrapper $result ) {
  357. $restrictions = [];
  358. foreach ( $result as $row ) {
  359. $restriction = $this->rowToRestriction( $row );
  360. if ( !$restriction ) {
  361. continue;
  362. }
  363. $restrictions[] = $restriction;
  364. }
  365. return $restrictions;
  366. }
  367. /**
  368. * Convert a result row from the database into a restriction object.
  369. *
  370. * @param stdClass $row
  371. * @return Restriction|null
  372. */
  373. private function rowToRestriction( stdClass $row ) {
  374. if ( array_key_exists( (int)$row->ir_type, $this->types ) ) {
  375. $class = $this->types[ (int)$row->ir_type ];
  376. return call_user_func( [ $class, 'newFromRow' ], $row );
  377. }
  378. return null;
  379. }
  380. }