Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 165 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
RevDelList | |
0.00% |
0 / 165 |
|
0.00% |
0 / 16 |
1722 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getRelationType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRestriction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRevdelConstant | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
suggestTarget | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
areAnySuppressed | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
setVisibility | |
0.00% |
0 / 114 |
|
0.00% |
0 / 1 |
420 | |||
acquireItemLocks | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
releaseItemLocks | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
reloadFromPrimary | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
updateLog | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
getLogAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLogParams | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
clearFileOps | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doPreCommitUpdates | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doPostCommitUpdates | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | * @ingroup RevisionDelete |
20 | */ |
21 | |
22 | use MediaWiki\Context\IContextSource; |
23 | use MediaWiki\Deferred\DeferredUpdates; |
24 | use MediaWiki\Page\PageIdentity; |
25 | use MediaWiki\Revision\RevisionRecord; |
26 | use MediaWiki\Status\Status; |
27 | use MediaWiki\Title\Title; |
28 | use Wikimedia\Rdbms\LBFactory; |
29 | |
30 | /** |
31 | * Abstract base class for a list of deletable items. The list class |
32 | * needs to be able to make a query from a set of identifiers to pull |
33 | * relevant rows, to return RevDelItem subclasses wrapping them, and |
34 | * to wrap bulk update operations. |
35 | * |
36 | * @property RevDelItem $current |
37 | * @method RevDelItem next() |
38 | * @method RevDelItem reset() |
39 | * @method RevDelItem current() |
40 | */ |
41 | abstract class RevDelList extends RevisionListBase { |
42 | |
43 | /** Flag used for suppression, depending on the type of log */ |
44 | protected const SUPPRESS_BIT = RevisionRecord::DELETED_RESTRICTED; |
45 | |
46 | /** @var LBFactory */ |
47 | private $lbFactory; |
48 | |
49 | /** |
50 | * @param IContextSource $context |
51 | * @param PageIdentity $page |
52 | * @param array $ids |
53 | * @param LBFactory $lbFactory |
54 | */ |
55 | public function __construct( |
56 | IContextSource $context, |
57 | PageIdentity $page, |
58 | array $ids, |
59 | LBFactory $lbFactory |
60 | ) { |
61 | parent::__construct( $context, $page ); |
62 | |
63 | // ids is a protected variable in RevisionListBase |
64 | $this->ids = $ids; |
65 | $this->lbFactory = $lbFactory; |
66 | } |
67 | |
68 | /** |
69 | * Get the DB field name associated with the ID list. |
70 | * This used to populate the log_search table for finding log entries. |
71 | * Override this function. |
72 | * @return string|null |
73 | */ |
74 | public static function getRelationType() { |
75 | return null; |
76 | } |
77 | |
78 | /** |
79 | * Get the user right required for this list type |
80 | * Override this function. |
81 | * @since 1.22 |
82 | * @return string|null |
83 | */ |
84 | public static function getRestriction() { |
85 | return null; |
86 | } |
87 | |
88 | /** |
89 | * Get the revision deletion constant for this list type |
90 | * Override this function. |
91 | * @since 1.22 |
92 | * @return int|null |
93 | */ |
94 | public static function getRevdelConstant() { |
95 | return null; |
96 | } |
97 | |
98 | /** |
99 | * Suggest a target for the revision deletion |
100 | * Optionally override this function. |
101 | * @since 1.22 |
102 | * @param Title|null $target User-supplied target |
103 | * @param array $ids |
104 | * @return Title|null |
105 | */ |
106 | public static function suggestTarget( $target, array $ids ) { |
107 | return $target; |
108 | } |
109 | |
110 | /** |
111 | * Indicate whether any item in this list is suppressed |
112 | * @since 1.25 |
113 | * @return bool |
114 | */ |
115 | public function areAnySuppressed() { |
116 | /** @var RevDelItem $item */ |
117 | foreach ( $this as $item ) { |
118 | if ( $item->getBits() & self::SUPPRESS_BIT ) { |
119 | return true; |
120 | } |
121 | } |
122 | |
123 | return false; |
124 | } |
125 | |
126 | /** |
127 | * Set the visibility for the revisions in this list. Logging and |
128 | * transactions are done here. |
129 | * |
130 | * @param array $params Associative array of parameters. Members are: |
131 | * value: ExtractBitParams() bitfield array |
132 | * comment: The log comment |
133 | * perItemStatus: Set if you want per-item status reports |
134 | * tags: The array of change tags to apply to the log entry |
135 | * @return Status |
136 | * @since 1.23 Added 'perItemStatus' param |
137 | */ |
138 | public function setVisibility( array $params ) { |
139 | $status = Status::newGood(); |
140 | |
141 | $bitPars = $params['value']; |
142 | $comment = $params['comment']; |
143 | $perItemStatus = $params['perItemStatus'] ?? false; |
144 | |
145 | // CAS-style checks are done on the _deleted fields so the select |
146 | // does not need to use FOR UPDATE nor be in the atomic section |
147 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
148 | $this->res = $this->doQuery( $dbw ); |
149 | |
150 | $status->merge( $this->acquireItemLocks() ); |
151 | if ( !$status->isGood() ) { |
152 | return $status; |
153 | } |
154 | |
155 | $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE ); |
156 | $dbw->onTransactionResolution( |
157 | function () { |
158 | // Release locks on commit or error |
159 | $this->releaseItemLocks(); |
160 | }, |
161 | __METHOD__ |
162 | ); |
163 | |
164 | $missing = array_fill_keys( $this->ids, true ); |
165 | $this->clearFileOps(); |
166 | $idsForLog = []; |
167 | $authorActors = []; |
168 | |
169 | if ( $perItemStatus ) { |
170 | $status->value['itemStatuses'] = []; |
171 | } |
172 | |
173 | // For multi-item deletions, set the old/new bitfields in log_params such that "hid X" |
174 | // shows in logs if field X was hidden from ANY item and likewise for "unhid Y". Note the |
175 | // form does not let the same field get hidden and unhidden in different items at once. |
176 | $virtualOldBits = 0; |
177 | $virtualNewBits = 0; |
178 | $logType = 'delete'; |
179 | |
180 | // Will be filled with id => [old, new bits] information and |
181 | // passed to doPostCommitUpdates(). |
182 | $visibilityChangeMap = []; |
183 | |
184 | /** @var RevDelItem $item */ |
185 | foreach ( $this as $item ) { |
186 | unset( $missing[$item->getId()] ); |
187 | |
188 | if ( $perItemStatus ) { |
189 | $itemStatus = Status::newGood(); |
190 | $status->value['itemStatuses'][$item->getId()] = $itemStatus; |
191 | } else { |
192 | $itemStatus = $status; |
193 | } |
194 | |
195 | $oldBits = $item->getBits(); |
196 | // Build the actual new rev_deleted bitfield |
197 | $newBits = RevisionDeleter::extractBitfield( $bitPars, $oldBits ); |
198 | |
199 | if ( $oldBits == $newBits ) { |
200 | $itemStatus->warning( |
201 | 'revdelete-no-change', $item->formatDate(), $item->formatTime() ); |
202 | $status->failCount++; |
203 | continue; |
204 | } elseif ( $oldBits == 0 && $newBits != 0 ) { |
205 | $opType = 'hide'; |
206 | } elseif ( $oldBits != 0 && $newBits == 0 ) { |
207 | $opType = 'show'; |
208 | } else { |
209 | $opType = 'modify'; |
210 | } |
211 | |
212 | if ( $item->isHideCurrentOp( $newBits ) ) { |
213 | // Cannot hide current version text |
214 | $itemStatus->error( |
215 | 'revdelete-hide-current', $item->formatDate(), $item->formatTime() ); |
216 | $status->failCount++; |
217 | continue; |
218 | } elseif ( !$item->canView() ) { |
219 | // Cannot access this revision |
220 | $msg = ( $opType == 'show' ) ? |
221 | 'revdelete-show-no-access' : 'revdelete-modify-no-access'; |
222 | $itemStatus->error( $msg, $item->formatDate(), $item->formatTime() ); |
223 | $status->failCount++; |
224 | continue; |
225 | // Cannot just "hide from Sysops" without hiding any fields |
226 | } elseif ( $newBits == self::SUPPRESS_BIT ) { |
227 | $itemStatus->warning( |
228 | 'revdelete-only-restricted', $item->formatDate(), $item->formatTime() ); |
229 | $status->failCount++; |
230 | continue; |
231 | } |
232 | |
233 | // Update the revision |
234 | $ok = $item->setBits( $newBits ); |
235 | |
236 | if ( $ok ) { |
237 | $idsForLog[] = $item->getId(); |
238 | // If any item field was suppressed or unsuppressed |
239 | if ( ( $oldBits | $newBits ) & self::SUPPRESS_BIT ) { |
240 | $logType = 'suppress'; |
241 | } |
242 | // Track which fields where (un)hidden for each item |
243 | $addedBits = ( $oldBits ^ $newBits ) & $newBits; |
244 | $removedBits = ( $oldBits ^ $newBits ) & $oldBits; |
245 | $virtualNewBits |= $addedBits; |
246 | $virtualOldBits |= $removedBits; |
247 | |
248 | $status->successCount++; |
249 | $authorActors[] = $item->getAuthorActor(); |
250 | |
251 | // Save the old and new bits in $visibilityChangeMap for |
252 | // later use. |
253 | $visibilityChangeMap[$item->getId()] = [ |
254 | 'oldBits' => $oldBits, |
255 | 'newBits' => $newBits, |
256 | ]; |
257 | } else { |
258 | $itemStatus->error( |
259 | 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() ); |
260 | $status->failCount++; |
261 | } |
262 | } |
263 | |
264 | // Handle missing revisions |
265 | foreach ( $missing as $id => $unused ) { |
266 | if ( $perItemStatus ) { |
267 | $status->value['itemStatuses'][$id] = Status::newFatal( 'revdelete-modify-missing', $id ); |
268 | } else { |
269 | $status->error( 'revdelete-modify-missing', $id ); |
270 | } |
271 | $status->failCount++; |
272 | } |
273 | |
274 | if ( $status->successCount == 0 ) { |
275 | $dbw->endAtomic( __METHOD__ ); |
276 | return $status; |
277 | } |
278 | |
279 | // Save success count |
280 | $successCount = $status->successCount; |
281 | |
282 | // Move files, if there are any |
283 | $status->merge( $this->doPreCommitUpdates() ); |
284 | if ( !$status->isOK() ) { |
285 | // Fatal error, such as no configured archive directory or I/O failures |
286 | $dbw->cancelAtomic( __METHOD__ ); |
287 | return $status; |
288 | } |
289 | |
290 | // Log it |
291 | $authorFields = []; |
292 | $authorFields['authorActors'] = $authorActors; |
293 | $this->updateLog( |
294 | $logType, |
295 | [ |
296 | 'page' => $this->page, |
297 | 'count' => $successCount, |
298 | 'newBits' => $virtualNewBits, |
299 | 'oldBits' => $virtualOldBits, |
300 | 'comment' => $comment, |
301 | 'ids' => $idsForLog, |
302 | 'tags' => $params['tags'] ?? [], |
303 | ] + $authorFields |
304 | ); |
305 | |
306 | // Clear caches after commit |
307 | DeferredUpdates::addCallableUpdate( |
308 | function () use ( $visibilityChangeMap ) { |
309 | $this->doPostCommitUpdates( $visibilityChangeMap ); |
310 | }, |
311 | DeferredUpdates::PRESEND, |
312 | $dbw |
313 | ); |
314 | |
315 | $dbw->endAtomic( __METHOD__ ); |
316 | |
317 | return $status; |
318 | } |
319 | |
320 | final protected function acquireItemLocks() { |
321 | $status = Status::newGood(); |
322 | /** @var RevDelItem $item */ |
323 | foreach ( $this as $item ) { |
324 | $status->merge( $item->lock() ); |
325 | } |
326 | |
327 | return $status; |
328 | } |
329 | |
330 | final protected function releaseItemLocks() { |
331 | $status = Status::newGood(); |
332 | /** @var RevDelItem $item */ |
333 | foreach ( $this as $item ) { |
334 | $status->merge( $item->unlock() ); |
335 | } |
336 | |
337 | return $status; |
338 | } |
339 | |
340 | /** |
341 | * Reload the list data from the primary DB. This can be done after setVisibility() |
342 | * to allow $item->getHTML() to show the new data. |
343 | * @since 1.37 |
344 | */ |
345 | public function reloadFromPrimary() { |
346 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
347 | $this->res = $this->doQuery( $dbw ); |
348 | } |
349 | |
350 | /** |
351 | * Record a log entry on the action |
352 | * @param string $logType One of (delete,suppress) |
353 | * @param array $params Associative array of parameters: |
354 | * newBits: The new value of the *_deleted bitfield |
355 | * oldBits: The old value of the *_deleted bitfield. |
356 | * page: The target page reference |
357 | * ids: The ID list |
358 | * comment: The log comment |
359 | * authorActors: The array of the actor IDs of the offenders |
360 | * tags: The array of change tags to apply to the log entry |
361 | */ |
362 | private function updateLog( $logType, $params ) { |
363 | // Get the URL param's corresponding DB field |
364 | $field = RevisionDeleter::getRelationType( $this->getType() ); |
365 | if ( !$field ) { |
366 | throw new UnexpectedValueException( "Bad log URL param type!" ); |
367 | } |
368 | // Add params for affected page and ids |
369 | $logParams = $this->getLogParams( $params ); |
370 | // Actually add the deletion log entry |
371 | $logEntry = new ManualLogEntry( $logType, $this->getLogAction() ); |
372 | $logEntry->setTarget( $params['page'] ); |
373 | $logEntry->setComment( $params['comment'] ); |
374 | $logEntry->setParameters( $logParams ); |
375 | $logEntry->setPerformer( $this->getUser() ); |
376 | // Allow for easy searching of deletion log items for revision/log items |
377 | $relations = [ |
378 | $field => $params['ids'], |
379 | ]; |
380 | if ( isset( $params['authorActors'] ) ) { |
381 | $relations += [ |
382 | 'target_author_actor' => $params['authorActors'], |
383 | ]; |
384 | } |
385 | $logEntry->setRelations( $relations ); |
386 | // Apply change tags to the log entry |
387 | $logEntry->addTags( $params['tags'] ); |
388 | $logId = $logEntry->insert(); |
389 | $logEntry->publish( $logId ); |
390 | } |
391 | |
392 | /** |
393 | * Get the log action for this list type |
394 | * @return string |
395 | */ |
396 | public function getLogAction() { |
397 | return 'revision'; |
398 | } |
399 | |
400 | /** |
401 | * Get log parameter array. |
402 | * @param array $params Associative array of log parameters, same as updateLog() |
403 | * @return array |
404 | */ |
405 | public function getLogParams( $params ) { |
406 | return [ |
407 | '4::type' => $this->getType(), |
408 | '5::ids' => $params['ids'], |
409 | '6::ofield' => $params['oldBits'], |
410 | '7::nfield' => $params['newBits'], |
411 | ]; |
412 | } |
413 | |
414 | /** |
415 | * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates() |
416 | * STUB |
417 | */ |
418 | public function clearFileOps() { |
419 | } |
420 | |
421 | /** |
422 | * A hook for setVisibility(): do batch updates pre-commit. |
423 | * STUB |
424 | * @return Status |
425 | */ |
426 | public function doPreCommitUpdates() { |
427 | return Status::newGood(); |
428 | } |
429 | |
430 | /** |
431 | * A hook for setVisibility(): do any necessary updates post-commit. |
432 | * STUB |
433 | * @param array $visibilityChangeMap [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ] |
434 | * @return Status |
435 | */ |
436 | public function doPostCommitUpdates( array $visibilityChangeMap ) { |
437 | return Status::newGood(); |
438 | } |
439 | |
440 | } |