Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.74% |
130 / 133 |
|
84.62% |
11 / 13 |
CRAP | |
0.00% |
0 / 1 |
BlockRestrictionStore | |
97.74% |
130 / 133 |
|
84.62% |
11 / 13 |
40 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
loadByBlockId | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
insert | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
update | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
6 | |||
updateByParentBlockId | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
5 | |||
delete | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
deleteByBlockId | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
equals | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
setBlockId | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
restrictionsToRemove | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
restrictionsByBlockId | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
resultToRestrictions | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
rowToRestriction | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
5.05 |
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\ActionRestriction; |
26 | use MediaWiki\Block\Restriction\NamespaceRestriction; |
27 | use MediaWiki\Block\Restriction\PageRestriction; |
28 | use MediaWiki\Block\Restriction\Restriction; |
29 | use MediaWiki\DAO\WikiAwareEntity; |
30 | use stdClass; |
31 | use Wikimedia\Rdbms\IConnectionProvider; |
32 | use Wikimedia\Rdbms\IResultWrapper; |
33 | |
34 | class BlockRestrictionStore { |
35 | |
36 | private IConnectionProvider $dbProvider; |
37 | |
38 | /** |
39 | * @var string|false |
40 | */ |
41 | private $wikiId; |
42 | |
43 | public function __construct( |
44 | IConnectionProvider $dbProvider, |
45 | /* string|false */ $wikiId = WikiAwareEntity::LOCAL |
46 | ) { |
47 | $this->dbProvider = $dbProvider; |
48 | $this->wikiId = $wikiId; |
49 | } |
50 | |
51 | /** |
52 | * Retrieve the restrictions from the database by block ID. |
53 | * |
54 | * @since 1.33 |
55 | * @param int|int[] $blockId |
56 | * @return Restriction[] |
57 | */ |
58 | public function loadByBlockId( $blockId ) { |
59 | if ( $blockId === null || $blockId === [] ) { |
60 | return []; |
61 | } |
62 | |
63 | $result = $this->dbProvider->getReplicaDatabase( $this->wikiId ) |
64 | ->newSelectQueryBuilder() |
65 | ->select( [ 'ir_ipb_id', 'ir_type', 'ir_value', 'page_namespace', 'page_title' ] ) |
66 | ->from( 'ipblocks_restrictions' ) |
67 | ->leftJoin( 'page', null, [ 'ir_type' => PageRestriction::TYPE_ID, 'ir_value=page_id' ] ) |
68 | ->where( [ 'ir_ipb_id' => $blockId ] ) |
69 | ->caller( __METHOD__ )->fetchResultSet(); |
70 | |
71 | return $this->resultToRestrictions( $result ); |
72 | } |
73 | |
74 | /** |
75 | * Insert the restrictions into the database. |
76 | * |
77 | * @since 1.33 |
78 | * @param Restriction[] $restrictions |
79 | * @return bool |
80 | */ |
81 | public function insert( array $restrictions ) { |
82 | if ( !$restrictions ) { |
83 | return false; |
84 | } |
85 | |
86 | $rows = []; |
87 | foreach ( $restrictions as $restriction ) { |
88 | $rows[] = $restriction->toRow(); |
89 | } |
90 | |
91 | $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); |
92 | |
93 | $dbw->newInsertQueryBuilder() |
94 | ->insertInto( 'ipblocks_restrictions' ) |
95 | ->ignore() |
96 | ->rows( $rows ) |
97 | ->caller( __METHOD__ )->execute(); |
98 | |
99 | return true; |
100 | } |
101 | |
102 | /** |
103 | * Update 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 Whether all operations were successful |
109 | */ |
110 | public function update( array $restrictions ) { |
111 | $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); |
112 | |
113 | $dbw->startAtomic( __METHOD__ ); |
114 | |
115 | // Organize the restrictions by block ID. |
116 | $restrictionList = $this->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 ( $blockIds ) { |
125 | $result = $dbw->newSelectQueryBuilder() |
126 | ->select( [ 'ir_ipb_id', 'ir_type', 'ir_value' ] ) |
127 | ->forUpdate() |
128 | ->from( 'ipblocks_restrictions' ) |
129 | ->where( [ 'ir_ipb_id' => $blockIds ] ) |
130 | ->caller( __METHOD__ )->fetchResultSet(); |
131 | |
132 | $existingList = $this->restrictionsByBlockId( |
133 | $this->resultToRestrictions( $result ) |
134 | ); |
135 | } |
136 | |
137 | $result = true; |
138 | // Perform the actions on a per block-ID basis. |
139 | foreach ( $restrictionList as $blockId => $blockRestrictions ) { |
140 | // Insert all of the restrictions first, ignoring ones that already exist. |
141 | $success = $this->insert( $blockRestrictions ); |
142 | |
143 | $result = $success && $result; |
144 | |
145 | $restrictionsToRemove = $this->restrictionsToRemove( |
146 | $existingList[$blockId] ?? [], |
147 | $restrictions |
148 | ); |
149 | |
150 | if ( !$restrictionsToRemove ) { |
151 | continue; |
152 | } |
153 | |
154 | $success = $this->delete( $restrictionsToRemove ); |
155 | |
156 | $result = $success && $result; |
157 | } |
158 | |
159 | $dbw->endAtomic( __METHOD__ ); |
160 | |
161 | return $result; |
162 | } |
163 | |
164 | /** |
165 | * Updates the list of restrictions by parent ID. |
166 | * |
167 | * @since 1.33 |
168 | * @param int $parentBlockId |
169 | * @param Restriction[] $restrictions |
170 | * @return bool Whether all updates were successful |
171 | */ |
172 | public function updateByParentBlockId( $parentBlockId, array $restrictions ) { |
173 | $parentBlockId = (int)$parentBlockId; |
174 | |
175 | $db = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); |
176 | |
177 | $blockIds = $db->newSelectQueryBuilder() |
178 | ->select( 'bl_id' ) |
179 | ->forUpdate() |
180 | ->from( 'block' ) |
181 | ->where( [ 'bl_parent_block_id' => $parentBlockId ] ) |
182 | ->caller( __METHOD__ )->fetchFieldValues(); |
183 | if ( !$blockIds ) { |
184 | return true; |
185 | } |
186 | |
187 | // If removing all of the restrictions, then just delete them all. |
188 | if ( !$restrictions ) { |
189 | $blockIds = array_map( 'intval', $blockIds ); |
190 | return $this->deleteByBlockId( $blockIds ); |
191 | } |
192 | |
193 | $db->startAtomic( __METHOD__ ); |
194 | |
195 | $result = true; |
196 | foreach ( $blockIds as $id ) { |
197 | $success = $this->update( $this->setBlockId( $id, $restrictions ) ); |
198 | $result = $success && $result; |
199 | } |
200 | |
201 | $db->endAtomic( __METHOD__ ); |
202 | |
203 | return $result; |
204 | } |
205 | |
206 | /** |
207 | * Delete the restrictions. |
208 | * |
209 | * @since 1.33 |
210 | * @param Restriction[] $restrictions |
211 | * @return bool |
212 | */ |
213 | public function delete( array $restrictions ) { |
214 | $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); |
215 | foreach ( $restrictions as $restriction ) { |
216 | $dbw->newDeleteQueryBuilder() |
217 | ->deleteFrom( 'ipblocks_restrictions' ) |
218 | // The restriction row is made up of a compound primary key. Therefore, |
219 | // the row and the delete conditions are the same. |
220 | ->where( $restriction->toRow() ) |
221 | ->caller( __METHOD__ )->execute(); |
222 | } |
223 | |
224 | return true; |
225 | } |
226 | |
227 | /** |
228 | * Delete the restrictions by block ID. |
229 | * |
230 | * @since 1.33 |
231 | * @param int|int[] $blockId |
232 | * @return bool |
233 | */ |
234 | public function deleteByBlockId( $blockId ) { |
235 | $this->dbProvider->getPrimaryDatabase( $this->wikiId ) |
236 | ->newDeleteQueryBuilder() |
237 | ->deleteFrom( 'ipblocks_restrictions' ) |
238 | ->where( [ 'ir_ipb_id' => $blockId ] ) |
239 | ->caller( __METHOD__ )->execute(); |
240 | return true; |
241 | } |
242 | |
243 | /** |
244 | * Check if two arrays of Restrictions are effectively equal. This is a loose |
245 | * equality check as the restrictions do not have to contain the same block |
246 | * IDs. |
247 | * |
248 | * @since 1.33 |
249 | * @param Restriction[] $a |
250 | * @param Restriction[] $b |
251 | * @return bool |
252 | */ |
253 | public function equals( array $a, array $b ) { |
254 | $aCount = count( $a ); |
255 | $bCount = count( $b ); |
256 | |
257 | // If the count is different, then they are obviously a different set. |
258 | if ( $aCount !== $bCount ) { |
259 | return false; |
260 | } |
261 | |
262 | // If both sets contain no items, then they are the same set. |
263 | if ( $aCount === 0 && $bCount === 0 ) { |
264 | return true; |
265 | } |
266 | |
267 | $hasher = static function ( Restriction $r ) { |
268 | return $r->getHash(); |
269 | }; |
270 | |
271 | $aHashes = array_map( $hasher, $a ); |
272 | $bHashes = array_map( $hasher, $b ); |
273 | |
274 | sort( $aHashes ); |
275 | sort( $bHashes ); |
276 | |
277 | return $aHashes === $bHashes; |
278 | } |
279 | |
280 | /** |
281 | * Set the blockId on a set of restrictions and return a new set. |
282 | * |
283 | * @since 1.33 |
284 | * @param int $blockId |
285 | * @param Restriction[] $restrictions |
286 | * @return Restriction[] |
287 | */ |
288 | public function setBlockId( $blockId, array $restrictions ) { |
289 | $blockRestrictions = []; |
290 | |
291 | foreach ( $restrictions as $restriction ) { |
292 | // Clone the restriction so any references to the current restriction are |
293 | // not suddenly changed to a different blockId. |
294 | $restriction = clone $restriction; |
295 | $restriction->setBlockId( $blockId ); |
296 | |
297 | $blockRestrictions[] = $restriction; |
298 | } |
299 | |
300 | return $blockRestrictions; |
301 | } |
302 | |
303 | /** |
304 | * Get the restrictions that should be removed, which are existing |
305 | * restrictions that are not in the new list of restrictions. |
306 | * |
307 | * @param Restriction[] $existing |
308 | * @param Restriction[] $new |
309 | * @return array |
310 | */ |
311 | private function restrictionsToRemove( array $existing, array $new ) { |
312 | $restrictionsByHash = []; |
313 | foreach ( $existing as $restriction ) { |
314 | $restrictionsByHash[$restriction->getHash()] = $restriction; |
315 | } |
316 | foreach ( $new as $restriction ) { |
317 | unset( $restrictionsByHash[$restriction->getHash()] ); |
318 | } |
319 | return array_values( $restrictionsByHash ); |
320 | } |
321 | |
322 | /** |
323 | * Converts an array of restrictions to an associative array of restrictions |
324 | * where the keys are the block IDs. |
325 | * |
326 | * @param Restriction[] $restrictions |
327 | * @return array |
328 | */ |
329 | private function restrictionsByBlockId( array $restrictions ) { |
330 | $blockRestrictions = []; |
331 | |
332 | foreach ( $restrictions as $restriction ) { |
333 | $blockRestrictions[$restriction->getBlockId()][] = $restriction; |
334 | } |
335 | |
336 | return $blockRestrictions; |
337 | } |
338 | |
339 | /** |
340 | * Convert a result wrapper to an array of restrictions. |
341 | * |
342 | * @param IResultWrapper $result |
343 | * @return Restriction[] |
344 | */ |
345 | private function resultToRestrictions( IResultWrapper $result ) { |
346 | $restrictions = []; |
347 | foreach ( $result as $row ) { |
348 | $restriction = $this->rowToRestriction( $row ); |
349 | |
350 | if ( !$restriction ) { |
351 | continue; |
352 | } |
353 | |
354 | $restrictions[] = $restriction; |
355 | } |
356 | |
357 | return $restrictions; |
358 | } |
359 | |
360 | /** |
361 | * Convert a result row from the database into a restriction object. |
362 | * |
363 | * @param stdClass $row |
364 | * @return Restriction|null |
365 | */ |
366 | private function rowToRestriction( stdClass $row ) { |
367 | switch ( (int)$row->ir_type ) { |
368 | case PageRestriction::TYPE_ID: |
369 | return PageRestriction::newFromRow( $row ); |
370 | case NamespaceRestriction::TYPE_ID: |
371 | return NamespaceRestriction::newFromRow( $row ); |
372 | case ActionRestriction::TYPE_ID: |
373 | return ActionRestriction::newFromRow( $row ); |
374 | default: |
375 | return null; |
376 | } |
377 | } |
378 | } |