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