Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 171 |
|
0.00% |
0 / 17 |
CRAP | |
0.00% |
0 / 1 |
| RevDelList | |
0.00% |
0 / 170 |
|
0.00% |
0 / 17 |
1806 | |
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 / 117 |
|
0.00% |
0 / 1 |
420 | |||
| emitEvents | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 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 / 21 |
|
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 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | * @ingroup RevisionDelete |
| 6 | */ |
| 7 | |
| 8 | namespace MediaWiki\RevisionDelete; |
| 9 | |
| 10 | use LogEntry; |
| 11 | use MediaWiki\Context\IContextSource; |
| 12 | use MediaWiki\Deferred\DeferredUpdates; |
| 13 | use MediaWiki\Logging\ManualLogEntry; |
| 14 | use MediaWiki\Page\PageIdentity; |
| 15 | use MediaWiki\Revision\RevisionRecord; |
| 16 | use MediaWiki\RevisionList\RevisionListBase; |
| 17 | use MediaWiki\Status\Status; |
| 18 | use MediaWiki\Title\Title; |
| 19 | use UnexpectedValueException; |
| 20 | use Wikimedia\Rdbms\LBFactory; |
| 21 | |
| 22 | /** |
| 23 | * Abstract base class for a list of deletable items. The list class |
| 24 | * needs to be able to make a query from a set of identifiers to pull |
| 25 | * relevant rows, to return RevDelItem subclasses wrapping them, and |
| 26 | * to wrap bulk update operations. |
| 27 | * |
| 28 | * @property RevDelItem $current |
| 29 | * @method RevDelItem next() |
| 30 | * @method RevDelItem reset() |
| 31 | * @method RevDelItem current() |
| 32 | */ |
| 33 | abstract class RevDelList extends RevisionListBase { |
| 34 | |
| 35 | /** Flag used for suppression, depending on the type of log */ |
| 36 | protected const SUPPRESS_BIT = RevisionRecord::DELETED_RESTRICTED; |
| 37 | |
| 38 | /** @var LBFactory */ |
| 39 | private $lbFactory; |
| 40 | |
| 41 | /** |
| 42 | * @param IContextSource $context |
| 43 | * @param PageIdentity $page |
| 44 | * @param array $ids |
| 45 | * @param LBFactory $lbFactory |
| 46 | */ |
| 47 | public function __construct( |
| 48 | IContextSource $context, |
| 49 | PageIdentity $page, |
| 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 | |
| 60 | /** |
| 61 | * Get the DB field name associated with the ID list. |
| 62 | * This used to populate the log_search table for finding log entries. |
| 63 | * Override this function. |
| 64 | * @return string|null |
| 65 | */ |
| 66 | public static function getRelationType() { |
| 67 | return null; |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Get the user right required for this list type |
| 72 | * Override this function. |
| 73 | * @since 1.22 |
| 74 | * @return string|null |
| 75 | */ |
| 76 | public static function getRestriction() { |
| 77 | return null; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Get the revision deletion constant for this list type |
| 82 | * Override this function. |
| 83 | * @since 1.22 |
| 84 | * @return int|null |
| 85 | */ |
| 86 | public static function getRevdelConstant() { |
| 87 | return null; |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * Suggest a target for the revision deletion |
| 92 | * Optionally override this function. |
| 93 | * @since 1.22 |
| 94 | * @param Title|null $target User-supplied target |
| 95 | * @param array $ids |
| 96 | * @return Title|null |
| 97 | */ |
| 98 | public static function suggestTarget( $target, array $ids ) { |
| 99 | return $target; |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Indicate whether any item in this list is suppressed |
| 104 | * @since 1.25 |
| 105 | * @return bool |
| 106 | */ |
| 107 | public function areAnySuppressed() { |
| 108 | /** @var RevDelItem $item */ |
| 109 | foreach ( $this as $item ) { |
| 110 | if ( $item->getBits() & self::SUPPRESS_BIT ) { |
| 111 | return true; |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | return false; |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Set the visibility for the revisions in this list. Logging and |
| 120 | * transactions are done here. |
| 121 | * |
| 122 | * @param array $params Associative array of parameters. Members are: |
| 123 | * value: ExtractBitParams() bitfield array |
| 124 | * comment: The log comment |
| 125 | * perItemStatus: Set if you want per-item status reports |
| 126 | * tags: The array of change tags to apply to the log entry |
| 127 | * @return Status |
| 128 | * @since 1.23 Added 'perItemStatus' param |
| 129 | */ |
| 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 | |
| 178 | /** @var RevDelItem $item */ |
| 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 | |
| 318 | /** |
| 319 | * @param array $bitPars See RevisionDeleter::extractBitfield |
| 320 | * @param array $visibilityChangeMap [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ] |
| 321 | * @param array $tags |
| 322 | * @param LogEntry $logEntry |
| 323 | * @param bool $suppressed |
| 324 | */ |
| 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(); |
| 337 | /** @var RevDelItem $item */ |
| 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(); |
| 347 | /** @var RevDelItem $item */ |
| 348 | foreach ( $this as $item ) { |
| 349 | $status->merge( $item->unlock() ); |
| 350 | } |
| 351 | |
| 352 | return $status; |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Reload the list data from the primary DB. This can be done after setVisibility() |
| 357 | * to allow $item->getHTML() to show the new data. |
| 358 | * @since 1.37 |
| 359 | */ |
| 360 | public function reloadFromPrimary() { |
| 361 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
| 362 | $this->res = $this->doQuery( $dbw ); |
| 363 | } |
| 364 | |
| 365 | /** |
| 366 | * Record a log entry on the action |
| 367 | * @param string $logType One of (delete,suppress) |
| 368 | * @param array $params Associative array of parameters: |
| 369 | * newBits: The new value of the *_deleted bitfield |
| 370 | * oldBits: The old value of the *_deleted bitfield. |
| 371 | * page: The target page reference |
| 372 | * ids: The ID list |
| 373 | * comment: The log comment |
| 374 | * authorActors: The array of the actor IDs of the offenders |
| 375 | * tags: The array of change tags to apply to the log entry |
| 376 | */ |
| 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 | |
| 409 | /** |
| 410 | * Get the log action for this list type |
| 411 | * @return string |
| 412 | */ |
| 413 | public function getLogAction() { |
| 414 | return 'revision'; |
| 415 | } |
| 416 | |
| 417 | /** |
| 418 | * Get log parameter array. |
| 419 | * @param array $params Associative array of log parameters, same as updateLog() |
| 420 | * @return array |
| 421 | */ |
| 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 | |
| 431 | /** |
| 432 | * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates() |
| 433 | * STUB |
| 434 | */ |
| 435 | public function clearFileOps() { |
| 436 | } |
| 437 | |
| 438 | /** |
| 439 | * A hook for setVisibility(): do batch updates pre-commit. |
| 440 | * STUB |
| 441 | * @return Status |
| 442 | */ |
| 443 | public function doPreCommitUpdates() { |
| 444 | return Status::newGood(); |
| 445 | } |
| 446 | |
| 447 | /** |
| 448 | * A hook for setVisibility(): do any necessary updates post-commit. |
| 449 | * STUB |
| 450 | * @param array $visibilityChangeMap [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ] |
| 451 | * @return Status |
| 452 | */ |
| 453 | public function doPostCommitUpdates( array $visibilityChangeMap ) { |
| 454 | return Status::newGood(); |
| 455 | } |
| 456 | |
| 457 | } |
| 458 | |
| 459 | /** @deprecated class alias since 1.46 */ |
| 460 | class_alias( RevDelList::class, 'RevDelList' ); |