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