MediaWiki master
RevDelList.php
Go to the documentation of this file.
1<?php
9
10use LogEntry;
19use UnexpectedValueException;
21
33abstract class RevDelList extends RevisionListBase {
34
36 protected const SUPPRESS_BIT = RevisionRecord::DELETED_RESTRICTED;
37
39 private $lbFactory;
40
47 public function __construct(
48 IContextSource $context,
50 array $ids,
51 LBFactory $lbFactory
52 ) {
53 parent::__construct( $context, $page );
54
55 // ids is a protected variable in RevisionListBase
56 $this->ids = $ids;
57 $this->lbFactory = $lbFactory;
58 }
59
66 public static function getRelationType() {
67 return null;
68 }
69
76 public static function getRestriction() {
77 return null;
78 }
79
86 public static function getRevdelConstant() {
87 return null;
88 }
89
98 public static function suggestTarget( $target, array $ids ) {
99 return $target;
100 }
101
107 public function areAnySuppressed() {
109 foreach ( $this as $item ) {
110 if ( $item->getBits() & self::SUPPRESS_BIT ) {
111 return true;
112 }
113 }
114
115 return false;
116 }
117
130 public function setVisibility( array $params ) {
131 $status = Status::newGood();
132
133 $bitPars = $params['value'];
134 $comment = $params['comment'];
135 $perItemStatus = $params['perItemStatus'] ?? false;
136
137 // T387638 - Always ensure ->value['itemStatuses'] is set if requested
138 if ( $perItemStatus ) {
139 $status->value['itemStatuses'] = [];
140 }
141
142 // CAS-style checks are done on the _deleted fields so the select
143 // does not need to use FOR UPDATE nor be in the atomic section
144 $dbw = $this->lbFactory->getPrimaryDatabase();
145 $this->res = $this->doQuery( $dbw );
146
147 $status->merge( $this->acquireItemLocks() );
148 if ( !$status->isGood() ) {
149 return $status;
150 }
151
152 $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
153 $dbw->onTransactionResolution(
154 function () {
155 // Release locks on commit or error
156 $this->releaseItemLocks();
157 },
158 __METHOD__
159 );
160
161 $missing = array_fill_keys( $this->ids, true );
162 $this->clearFileOps();
163 $idsForLog = [];
164 $authorActors = [];
165
166 // For multi-item deletions, set the old/new bitfields in log_params such that "hid X"
167 // shows in logs if field X was hidden from ANY item and likewise for "unhid Y". Note the
168 // form does not let the same field get hidden and unhidden in different items at once.
169 $virtualOldBits = 0;
170 $virtualNewBits = 0;
171 $logType = 'delete';
172 $useSuppressLog = false;
173
174 // Will be filled with id => [old, new bits] information and
175 // passed to doPostCommitUpdates().
176 $visibilityChangeMap = [];
177
179 foreach ( $this as $item ) {
180 unset( $missing[$item->getId()] );
181
182 if ( $perItemStatus ) {
183 $itemStatus = Status::newGood();
184 $status->value['itemStatuses'][$item->getId()] = $itemStatus;
185 } else {
186 $itemStatus = $status;
187 }
188
189 $oldBits = $item->getBits();
190 // Build the actual new rev_deleted bitfield
191 $newBits = RevisionDeleter::extractBitfield( $bitPars, $oldBits );
192
193 if ( $oldBits == $newBits ) {
194 $itemStatus->warning(
195 'revdelete-no-change', $item->formatDate(), $item->formatTime() );
196 $status->failCount++;
197 continue;
198 } elseif ( $oldBits == 0 && $newBits != 0 ) {
199 $opType = 'hide';
200 } elseif ( $oldBits != 0 && $newBits == 0 ) {
201 $opType = 'show';
202 } else {
203 $opType = 'modify';
204 }
205
206 if ( $item->isHideCurrentOp( $newBits ) ) {
207 // Cannot hide current version text
208 $itemStatus->error(
209 'revdelete-hide-current', $item->formatDate(), $item->formatTime() );
210 $status->failCount++;
211 continue;
212 } elseif ( !$item->canView() ) {
213 // Cannot access this revision
214 $msg = ( $opType == 'show' ) ?
215 'revdelete-show-no-access' : 'revdelete-modify-no-access';
216 $itemStatus->error( $msg, $item->formatDate(), $item->formatTime() );
217 $status->failCount++;
218 continue;
219 // Cannot just "hide from Sysops" without hiding any fields
220 } elseif ( $newBits == self::SUPPRESS_BIT ) {
221 $itemStatus->warning(
222 'revdelete-only-restricted', $item->formatDate(), $item->formatTime() );
223 $status->failCount++;
224 continue;
225 }
226
227 // Update the revision
228 $ok = $item->setBits( $newBits );
229
230 if ( $ok ) {
231 $idsForLog[] = $item->getId();
232 // If any item field was suppressed or unsuppressed
233 if ( ( $oldBits | $newBits ) & self::SUPPRESS_BIT ) {
234 $logType = 'suppress';
235 $useSuppressLog = true;
236 }
237 // Track which fields where (un)hidden for each item
238 $addedBits = ( $oldBits ^ $newBits ) & $newBits;
239 $removedBits = ( $oldBits ^ $newBits ) & $oldBits;
240 $virtualNewBits |= $addedBits;
241 $virtualOldBits |= $removedBits;
242
243 $status->successCount++;
244 $authorActors[] = $item->getAuthorActor();
245
246 // Save the old and new bits in $visibilityChangeMap for
247 // later use.
248 $visibilityChangeMap[$item->getId()] = [
249 'oldBits' => $oldBits,
250 'newBits' => $newBits,
251 ];
252 } else {
253 $itemStatus->error(
254 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() );
255 $status->failCount++;
256 }
257 }
258
259 // Handle missing revisions
260 foreach ( $missing as $id => $unused ) {
261 if ( $perItemStatus ) {
262 $status->value['itemStatuses'][$id] = Status::newFatal( 'revdelete-modify-missing', $id );
263 } else {
264 $status->error( 'revdelete-modify-missing', $id );
265 }
266 $status->failCount++;
267 }
268
269 if ( $status->successCount == 0 ) {
270 $dbw->endAtomic( __METHOD__ );
271 return $status;
272 }
273
274 // Save success count
275 $successCount = $status->successCount;
276
277 // Move files, if there are any
278 $status->merge( $this->doPreCommitUpdates() );
279 if ( !$status->isOK() ) {
280 // Fatal error, such as no configured archive directory or I/O failures
281 $dbw->cancelAtomic( __METHOD__ );
282 return $status;
283 }
284
285 // Log it
286 $tags = $params['tags'] ?? [];
287
288 $logEntry = $this->updateLog(
289 $logType,
290 [
291 'page' => $this->page,
292 'count' => $successCount,
293 'newBits' => $virtualNewBits,
294 'oldBits' => $virtualOldBits,
295 'comment' => $comment,
296 'ids' => $idsForLog,
297 'tags' => $tags,
298 'authorActors' => $authorActors,
299 ]
300 );
301
302 $this->emitEvents( $bitPars, $visibilityChangeMap, $tags, $logEntry, $useSuppressLog );
303
304 // Clear caches after commit
305 DeferredUpdates::addCallableUpdate(
306 function () use ( $visibilityChangeMap ) {
307 $this->doPostCommitUpdates( $visibilityChangeMap );
308 },
309 DeferredUpdates::PRESEND,
310 $dbw
311 );
312
313 $dbw->endAtomic( __METHOD__ );
314
315 return $status;
316 }
317
325 protected function emitEvents(
326 array $bitPars,
327 array $visibilityChangeMap,
328 array $tags,
329 LogEntry $logEntry,
330 bool $suppressed
331 ) {
332 // stub
333 }
334
335 final protected function acquireItemLocks(): Status {
336 $status = Status::newGood();
338 foreach ( $this as $item ) {
339 $status->merge( $item->lock() );
340 }
341
342 return $status;
343 }
344
345 final protected function releaseItemLocks(): Status {
346 $status = Status::newGood();
348 foreach ( $this as $item ) {
349 $status->merge( $item->unlock() );
350 }
351
352 return $status;
353 }
354
360 public function reloadFromPrimary() {
361 $dbw = $this->lbFactory->getPrimaryDatabase();
362 $this->res = $this->doQuery( $dbw );
363 }
364
377 private function updateLog( $logType, $params ): LogEntry {
378 // Get the URL param's corresponding DB field
379 $field = RevisionDeleter::getRelationType( $this->getType() );
380 if ( !$field ) {
381 throw new UnexpectedValueException( "Bad log URL param type!" );
382 }
383 // Add params for affected page and ids
384 $logParams = $this->getLogParams( $params );
385 // Actually add the deletion log entry
386 $logEntry = new ManualLogEntry( $logType, $this->getLogAction() );
387 $logEntry->setTarget( $params['page'] );
388 $logEntry->setComment( $params['comment'] );
389 $logEntry->setParameters( $logParams );
390 $logEntry->setPerformer( $this->getUser() );
391 // Allow for easy searching of deletion log items for revision/log items
392 $relations = [
393 $field => $params['ids'],
394 ];
395 if ( isset( $params['authorActors'] ) ) {
396 $relations += [
397 'target_author_actor' => $params['authorActors'],
398 ];
399 }
400 $logEntry->setRelations( $relations );
401 // Apply change tags to the log entry
402 $logEntry->addTags( $params['tags'] );
403 $logId = $logEntry->insert();
404 $logEntry->publish( $logId );
405
406 return $logEntry;
407 }
408
413 public function getLogAction() {
414 return 'revision';
415 }
416
422 public function getLogParams( $params ) {
423 return [
424 '4::type' => $this->getType(),
425 '5::ids' => $params['ids'],
426 '6::ofield' => $params['oldBits'],
427 '7::nfield' => $params['newBits'],
428 ];
429 }
430
435 public function clearFileOps() {
436 }
437
443 public function doPreCommitUpdates() {
444 return Status::newGood();
445 }
446
453 public function doPostCommitUpdates( array $visibilityChangeMap ) {
454 return Status::newGood();
455 }
456
457}
458
460class_alias( RevDelList::class, 'RevDelList' );
Defer callable updates to run later in the PHP process.
Class for creating new log entries and inserting them into the database.
Abstract base class for deletable items.
emitEvents(array $bitPars, array $visibilityChangeMap, array $tags, LogEntry $logEntry, bool $suppressed)
getLogParams( $params)
Get log parameter array.
setVisibility(array $params)
Set the visibility for the revisions in this list.
static getRestriction()
Get the user right required for this list type Override this function.
doPostCommitUpdates(array $visibilityChangeMap)
A hook for setVisibility(): do any necessary updates post-commit.
clearFileOps()
Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates() STUB.
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
const SUPPRESS_BIT
Flag used for suppression, depending on the type of log.
areAnySuppressed()
Indicate whether any item in this list is suppressed.
getLogAction()
Get the log action for this list type.
doPreCommitUpdates()
A hook for setVisibility(): do batch updates pre-commit.
__construct(IContextSource $context, PageIdentity $page, array $ids, LBFactory $lbFactory)
static getRelationType()
Get the DB field name associated with the ID list.
static getRevdelConstant()
Get the revision deletion constant for this list type Override this function.
reloadFromPrimary()
Reload the list data from the primary DB.
General controller for RevDel, used by both SpecialRevisiondelete and ApiRevisionDelete.
static extractBitfield(array $bitPars, $oldfield)
Put together a rev_deleted bitfield.
List for revision table items for a single page.
doQuery( $db)
Do the DB query to iterate through the objects.
Page revision base class.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Represents a title within MediaWiki.
Definition Title.php:69
merge( $other, $overwriteValue=false)
Merge another status object into this one.
Interface for objects which can provide a MediaWiki context on request.
Interface for objects (potentially) representing an editable wiki page.