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