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