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