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