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