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