Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 367 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
| SpecialRevisionDelete | |
0.00% |
0 / 366 |
|
0.00% |
0 / 16 |
5256 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 91 |
|
0.00% |
0 / 1 |
420 | |||
| showConvenienceLinks | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
20 | |||
| getLogQueryCond | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| tryShowFile | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
42 | |||
| getList | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
| showForm | |
0.00% |
0 / 98 |
|
0.00% |
0 / 1 |
110 | |||
| addUsageText | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| buildCheckBoxes | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
56 | |||
| submit | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
72 | |||
| success | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| failure | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
| extractBitParams | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
| save | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\Specials; |
| 8 | |
| 9 | use MediaWiki\CommentStore\CommentStore; |
| 10 | use MediaWiki\Exception\ErrorPageError; |
| 11 | use MediaWiki\Exception\PermissionsError; |
| 12 | use MediaWiki\Exception\UserBlockedError; |
| 13 | use MediaWiki\FileRepo\File\File; |
| 14 | use MediaWiki\FileRepo\RepoGroup; |
| 15 | use MediaWiki\Html\Html; |
| 16 | use MediaWiki\HTMLForm\HTMLForm; |
| 17 | use MediaWiki\Logging\LogEventsList; |
| 18 | use MediaWiki\Logging\LogPage; |
| 19 | use MediaWiki\Permissions\PermissionManager; |
| 20 | use MediaWiki\Revision\RevisionRecord; |
| 21 | use MediaWiki\RevisionDelete\RevDelList; |
| 22 | use MediaWiki\RevisionDelete\RevisionDeleter; |
| 23 | use MediaWiki\SpecialPage\SpecialPage; |
| 24 | use MediaWiki\SpecialPage\UnlistedSpecialPage; |
| 25 | use MediaWiki\Status\Status; |
| 26 | use MediaWiki\Title\Title; |
| 27 | |
| 28 | /** |
| 29 | * Special page allowing users with the appropriate permissions to view |
| 30 | * and hide revisions. Log items can also be hidden. |
| 31 | * |
| 32 | * @ingroup SpecialPage |
| 33 | */ |
| 34 | class SpecialRevisionDelete extends UnlistedSpecialPage { |
| 35 | /** @var bool Was the DB modified in this request */ |
| 36 | protected $wasSaved = false; |
| 37 | |
| 38 | /** @var bool True if the submit button was clicked, and the form was posted */ |
| 39 | private $submitClicked; |
| 40 | |
| 41 | /** @var array Target ID list */ |
| 42 | private $ids; |
| 43 | |
| 44 | /** @var string Archive name, for reviewing deleted files */ |
| 45 | private $archiveName; |
| 46 | |
| 47 | /** @var string Edit token for securing image views against XSS */ |
| 48 | private $token; |
| 49 | |
| 50 | /** @var Title Title object for target parameter */ |
| 51 | private $targetObj; |
| 52 | |
| 53 | /** @var string Deletion type, may be revision, archive, oldimage, filearchive, logging. */ |
| 54 | private $typeName; |
| 55 | |
| 56 | /** @var array Array of checkbox specs (message, name, deletion bits) */ |
| 57 | private $checks; |
| 58 | |
| 59 | /** @var array UI Labels about the current type */ |
| 60 | private $typeLabels; |
| 61 | |
| 62 | /** @var RevDelList RevDelList object, storing the list of items to be deleted/undeleted */ |
| 63 | private $revDelList; |
| 64 | |
| 65 | /** @var bool Whether user is allowed to perform the action */ |
| 66 | private $mIsAllowed; |
| 67 | |
| 68 | /** @var string */ |
| 69 | private $otherReason; |
| 70 | |
| 71 | private PermissionManager $permissionManager; |
| 72 | private RepoGroup $repoGroup; |
| 73 | |
| 74 | /** |
| 75 | * UI labels for each type. |
| 76 | */ |
| 77 | private const UI_LABELS = [ |
| 78 | 'revision' => [ |
| 79 | 'check-label' => 'revdelete-hide-text', |
| 80 | 'success' => 'revdelete-success', |
| 81 | 'failure' => 'revdelete-failure', |
| 82 | 'text' => 'revdelete-text-text', |
| 83 | 'selected' => 'revdelete-selected-text', |
| 84 | ], |
| 85 | 'archive' => [ |
| 86 | 'check-label' => 'revdelete-hide-text', |
| 87 | 'success' => 'revdelete-success', |
| 88 | 'failure' => 'revdelete-failure', |
| 89 | 'text' => 'revdelete-text-text', |
| 90 | 'selected' => 'revdelete-selected-text', |
| 91 | ], |
| 92 | 'oldimage' => [ |
| 93 | 'check-label' => 'revdelete-hide-image', |
| 94 | 'success' => 'revdelete-success', |
| 95 | 'failure' => 'revdelete-failure', |
| 96 | 'text' => 'revdelete-text-file', |
| 97 | 'selected' => 'revdelete-selected-file', |
| 98 | ], |
| 99 | 'filearchive' => [ |
| 100 | 'check-label' => 'revdelete-hide-image', |
| 101 | 'success' => 'revdelete-success', |
| 102 | 'failure' => 'revdelete-failure', |
| 103 | 'text' => 'revdelete-text-file', |
| 104 | 'selected' => 'revdelete-selected-file', |
| 105 | ], |
| 106 | 'logging' => [ |
| 107 | 'check-label' => 'revdelete-hide-name', |
| 108 | 'success' => 'logdelete-success', |
| 109 | 'failure' => 'logdelete-failure', |
| 110 | 'text' => 'logdelete-text', |
| 111 | 'selected' => 'logdelete-selected', |
| 112 | ], |
| 113 | ]; |
| 114 | |
| 115 | public function __construct( PermissionManager $permissionManager, RepoGroup $repoGroup ) { |
| 116 | parent::__construct( 'Revisiondelete' ); |
| 117 | |
| 118 | $this->permissionManager = $permissionManager; |
| 119 | $this->repoGroup = $repoGroup; |
| 120 | } |
| 121 | |
| 122 | /** @inheritDoc */ |
| 123 | public function doesWrites() { |
| 124 | return true; |
| 125 | } |
| 126 | |
| 127 | /** @inheritDoc */ |
| 128 | public function execute( $par ) { |
| 129 | $this->useTransactionalTimeLimit(); |
| 130 | |
| 131 | $this->checkPermissions(); |
| 132 | $this->checkReadOnly(); |
| 133 | |
| 134 | $output = $this->getOutput(); |
| 135 | $user = $this->getUser(); |
| 136 | |
| 137 | $this->setHeaders(); |
| 138 | $this->outputHeader(); |
| 139 | $request = $this->getRequest(); |
| 140 | $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' ); |
| 141 | # Handle our many different possible input types. |
| 142 | $ids = $request->getVal( 'ids' ); |
| 143 | if ( $ids !== null ) { |
| 144 | # Allow CSV, for backwards compatibility, or a single ID for show/hide links |
| 145 | $this->ids = explode( ',', $ids ); |
| 146 | } else { |
| 147 | # Array input |
| 148 | $this->ids = array_keys( $request->getArray( 'ids', [] ) ); |
| 149 | } |
| 150 | // $this->ids = array_map( 'intval', $this->ids ); |
| 151 | $this->ids = array_unique( array_filter( $this->ids ) ); |
| 152 | |
| 153 | $this->typeName = $request->getVal( 'type' ) ?? ''; |
| 154 | $this->targetObj = Title::newFromText( $request->getText( 'target' ) ); |
| 155 | |
| 156 | # For reviewing deleted files... |
| 157 | $this->archiveName = $request->getVal( 'file' ); |
| 158 | $this->token = $request->getVal( 'token' ); |
| 159 | if ( $this->archiveName && $this->targetObj ) { |
| 160 | $this->tryShowFile( $this->archiveName ); |
| 161 | |
| 162 | return; |
| 163 | } |
| 164 | |
| 165 | $this->typeName = RevisionDeleter::getCanonicalTypeName( $this->typeName ); |
| 166 | |
| 167 | # No targets? |
| 168 | if ( !$this->typeName || count( $this->ids ) == 0 ) { |
| 169 | throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); |
| 170 | } |
| 171 | |
| 172 | $restriction = RevisionDeleter::getRestriction( $this->typeName ); |
| 173 | |
| 174 | if ( !$this->getAuthority()->isAllowedAny( $restriction, 'deletedhistory' ) ) { |
| 175 | throw new PermissionsError( $restriction ); |
| 176 | } |
| 177 | |
| 178 | # Allow the list type to adjust the passed target |
| 179 | $this->targetObj = RevisionDeleter::suggestTarget( |
| 180 | $this->typeName, |
| 181 | $this->targetObj, |
| 182 | $this->ids |
| 183 | ); |
| 184 | |
| 185 | # We need a target page! |
| 186 | if ( $this->targetObj === null || |
| 187 | ( $this->typeName !== 'logging' && !$this->targetObj->canExist() ) |
| 188 | ) { |
| 189 | $output->addWikiMsg( 'undelete-header' ); |
| 190 | |
| 191 | return; |
| 192 | } |
| 193 | |
| 194 | // Check blocks |
| 195 | $checkReplica = !$this->submitClicked; |
| 196 | if ( |
| 197 | $this->permissionManager->isBlockedFrom( |
| 198 | $user, |
| 199 | $this->targetObj, |
| 200 | $checkReplica |
| 201 | ) |
| 202 | ) { |
| 203 | throw new UserBlockedError( |
| 204 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null |
| 205 | $user->getBlock(), |
| 206 | $user, |
| 207 | $this->getLanguage(), |
| 208 | $request->getIP() |
| 209 | ); |
| 210 | } |
| 211 | |
| 212 | $this->typeLabels = self::UI_LABELS[$this->typeName]; |
| 213 | $list = $this->getList(); |
| 214 | $list->reset(); |
| 215 | $this->mIsAllowed = $this->permissionManager->userHasRight( $user, $restriction ); |
| 216 | $canViewSuppressedOnly = $this->permissionManager->userHasRight( $user, 'viewsuppressed' ) && |
| 217 | !$this->permissionManager->userHasRight( $user, 'suppressrevision' ); |
| 218 | $pageIsSuppressed = $list->areAnySuppressed(); |
| 219 | $this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed ); |
| 220 | |
| 221 | $this->otherReason = $request->getVal( 'wpReason', '' ); |
| 222 | # Give a link to the logs/hist for this page |
| 223 | $this->showConvenienceLinks(); |
| 224 | |
| 225 | # Initialise checkboxes |
| 226 | $this->checks = [ |
| 227 | # Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name |
| 228 | [ $this->typeLabels['check-label'], 'wpHidePrimary', |
| 229 | RevisionDeleter::getRevdelConstant( $this->typeName ) |
| 230 | ], |
| 231 | [ 'revdelete-hide-comment', 'wpHideComment', RevisionRecord::DELETED_COMMENT ], |
| 232 | [ 'revdelete-hide-user', 'wpHideUser', RevisionRecord::DELETED_USER ] |
| 233 | ]; |
| 234 | if ( $this->permissionManager->userHasRight( $user, 'suppressrevision' ) ) { |
| 235 | $this->checks[] = [ 'revdelete-hide-restricted', |
| 236 | 'wpHideRestricted', RevisionRecord::DELETED_RESTRICTED ]; |
| 237 | } |
| 238 | |
| 239 | # Either submit or create our form |
| 240 | if ( $this->mIsAllowed && $this->submitClicked ) { |
| 241 | $this->submit(); |
| 242 | } else { |
| 243 | $this->showForm(); |
| 244 | } |
| 245 | |
| 246 | if ( $this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) { |
| 247 | # Show relevant lines from the deletion log |
| 248 | $deleteLogPage = new LogPage( 'delete' ); |
| 249 | $output->addHTML( "<h2>" . $deleteLogPage->getName()->escaped() . "</h2>\n" ); |
| 250 | LogEventsList::showLogExtract( |
| 251 | $output, |
| 252 | 'delete', |
| 253 | $this->targetObj, |
| 254 | '', /* user */ |
| 255 | [ 'lim' => 25, 'conds' => $this->getLogQueryCond(), 'useMaster' => $this->wasSaved ] |
| 256 | ); |
| 257 | } |
| 258 | # Show relevant lines from the suppression log |
| 259 | if ( $this->permissionManager->userHasRight( $user, 'suppressionlog' ) ) { |
| 260 | $suppressLogPage = new LogPage( 'suppress' ); |
| 261 | $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" ); |
| 262 | LogEventsList::showLogExtract( |
| 263 | $output, |
| 264 | 'suppress', |
| 265 | $this->targetObj, |
| 266 | '', |
| 267 | [ 'lim' => 25, 'conds' => $this->getLogQueryCond(), 'useMaster' => $this->wasSaved ] |
| 268 | ); |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * Show some useful links in the subtitle |
| 274 | */ |
| 275 | protected function showConvenienceLinks() { |
| 276 | $linkRenderer = $this->getLinkRenderer(); |
| 277 | # Give a link to the logs/hist for this page |
| 278 | if ( $this->targetObj ) { |
| 279 | // Also set header tabs to be for the target. |
| 280 | $this->getSkin()->setRelevantTitle( $this->targetObj ); |
| 281 | |
| 282 | $links = []; |
| 283 | $links[] = $linkRenderer->makeKnownLink( |
| 284 | SpecialPage::getTitleFor( 'Log' ), |
| 285 | $this->msg( 'viewpagelogs' )->text(), |
| 286 | [], |
| 287 | [ 'page' => $this->targetObj->getPrefixedText() ] |
| 288 | ); |
| 289 | if ( !$this->targetObj->isSpecialPage() ) { |
| 290 | # Give a link to the page history |
| 291 | $links[] = $linkRenderer->makeKnownLink( |
| 292 | $this->targetObj, |
| 293 | $this->msg( 'pagehist' )->text(), |
| 294 | [], |
| 295 | [ 'action' => 'history' ] |
| 296 | ); |
| 297 | # Link to deleted edits |
| 298 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'undelete' ) ) { |
| 299 | $undelete = SpecialPage::getTitleFor( 'Undelete' ); |
| 300 | $links[] = $linkRenderer->makeKnownLink( |
| 301 | $undelete, |
| 302 | $this->msg( 'deletedhist' )->text(), |
| 303 | [], |
| 304 | [ 'target' => $this->targetObj->getPrefixedDBkey() ] |
| 305 | ); |
| 306 | } |
| 307 | } |
| 308 | # Logs themselves don't have histories or archived revisions |
| 309 | $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | /** |
| 314 | * Get the condition used for fetching log snippets |
| 315 | * @return array |
| 316 | */ |
| 317 | protected function getLogQueryCond() { |
| 318 | $conds = []; |
| 319 | // Revision delete logs for these item |
| 320 | $conds['log_type'] = [ 'delete', 'suppress' ]; |
| 321 | $conds['log_action'] = $this->getList()->getLogAction(); |
| 322 | $conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName ); |
| 323 | // Convert IDs to strings, since ls_value is a text field. This avoids |
| 324 | // a fatal error in PostgreSQL: "operator does not exist: text = integer". |
| 325 | $conds['ls_value'] = array_map( 'strval', $this->ids ); |
| 326 | |
| 327 | return $conds; |
| 328 | } |
| 329 | |
| 330 | /** |
| 331 | * Show a deleted file version requested by the visitor. |
| 332 | * @todo Mostly copied from Special:Undelete. Refactor. |
| 333 | * @param string $archiveName |
| 334 | */ |
| 335 | protected function tryShowFile( $archiveName ) { |
| 336 | if ( $this->targetObj->getNamespace() !== NS_FILE ) { |
| 337 | $this->getOutput()->addWikiMsg( 'revdelete-no-file' ); |
| 338 | |
| 339 | return; |
| 340 | } |
| 341 | $repo = $this->repoGroup->getLocalRepo(); |
| 342 | $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName ); |
| 343 | $oimage->load(); |
| 344 | // Check if user is allowed to see this file |
| 345 | if ( !$oimage->exists() ) { |
| 346 | $this->getOutput()->addWikiMsg( 'revdelete-no-file' ); |
| 347 | |
| 348 | return; |
| 349 | } |
| 350 | $user = $this->getUser(); |
| 351 | if ( !$oimage->userCan( File::DELETED_FILE, $user ) ) { |
| 352 | if ( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) { |
| 353 | throw new PermissionsError( 'suppressrevision' ); |
| 354 | } else { |
| 355 | throw new PermissionsError( 'deletedtext' ); |
| 356 | } |
| 357 | } |
| 358 | if ( !$user->matchEditToken( $this->token, $archiveName ) ) { |
| 359 | $lang = $this->getLanguage(); |
| 360 | $this->getOutput()->addWikiMsg( 'revdelete-show-file-confirm', |
| 361 | $this->targetObj->getText(), |
| 362 | $lang->userDate( $oimage->getTimestamp(), $user ), |
| 363 | $lang->userTime( $oimage->getTimestamp(), $user ) ); |
| 364 | $this->getOutput()->addHTML( |
| 365 | Html::rawElement( 'form', [ |
| 366 | 'method' => 'POST', |
| 367 | 'action' => $this->getPageTitle()->getLocalURL( [ |
| 368 | 'target' => $this->targetObj->getPrefixedDBkey(), |
| 369 | 'file' => $archiveName, |
| 370 | 'token' => $user->getEditToken( $archiveName ), |
| 371 | ] ) |
| 372 | ], |
| 373 | Html::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() ) |
| 374 | ) |
| 375 | ); |
| 376 | |
| 377 | return; |
| 378 | } |
| 379 | $this->getOutput()->disable(); |
| 380 | # We mustn't allow the output to be CDN cached, otherwise |
| 381 | # if an admin previews a deleted image, and it's cached, then |
| 382 | # a user without appropriate permissions can toddle off and |
| 383 | # nab the image, and CDN will serve it |
| 384 | $this->getRequest()->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); |
| 385 | $this->getRequest()->response()->header( |
| 386 | 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' |
| 387 | ); |
| 388 | |
| 389 | $key = $oimage->getStorageKey(); |
| 390 | $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key; |
| 391 | $repo->streamFileWithStatus( $path ); |
| 392 | } |
| 393 | |
| 394 | /** |
| 395 | * Get the list object for this request |
| 396 | * @return RevDelList |
| 397 | */ |
| 398 | protected function getList() { |
| 399 | if ( $this->revDelList === null ) { |
| 400 | $this->revDelList = RevisionDeleter::createList( |
| 401 | $this->typeName, $this->getContext(), $this->targetObj, $this->ids |
| 402 | ); |
| 403 | } |
| 404 | |
| 405 | return $this->revDelList; |
| 406 | } |
| 407 | |
| 408 | /** |
| 409 | * Show a list of items that we will operate on, and show a form with checkboxes |
| 410 | * which will allow the user to choose new visibility settings. |
| 411 | */ |
| 412 | protected function showForm() { |
| 413 | $userAllowed = true; |
| 414 | |
| 415 | // Messages: revdelete-selected-text, revdelete-selected-file, logdelete-selected |
| 416 | $out = $this->getOutput(); |
| 417 | $out->wrapWikiMsg( "<strong>$1</strong>", [ $this->typeLabels['selected'], |
| 418 | $this->getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ] ); |
| 419 | |
| 420 | $this->addHelpLink( 'Help:RevisionDelete' ); |
| 421 | $out->addHTML( "<ul>" ); |
| 422 | |
| 423 | $numRevisions = 0; |
| 424 | // Live revisions... |
| 425 | $list = $this->getList(); |
| 426 | foreach ( $list as $item ) { |
| 427 | if ( !$item->canView() ) { |
| 428 | if ( !$this->submitClicked ) { |
| 429 | throw new PermissionsError( 'suppressrevision' ); |
| 430 | } |
| 431 | $userAllowed = false; |
| 432 | } |
| 433 | |
| 434 | $numRevisions++; |
| 435 | $out->addHTML( $item->getHTML() ); |
| 436 | } |
| 437 | |
| 438 | if ( !$numRevisions ) { |
| 439 | throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); |
| 440 | } |
| 441 | |
| 442 | $out->addHTML( "</ul>" ); |
| 443 | // Explanation text |
| 444 | $this->addUsageText(); |
| 445 | |
| 446 | // Normal sysops can always see what they did, but can't always change it |
| 447 | if ( !$userAllowed ) { |
| 448 | return; |
| 449 | } |
| 450 | |
| 451 | // Show form if the user can submit |
| 452 | if ( $this->mIsAllowed ) { |
| 453 | $suppressAllowed = $this->permissionManager |
| 454 | ->userHasRight( $this->getUser(), 'suppressrevision' ); |
| 455 | $out->addModules( [ 'mediawiki.misc-authed-ooui' ] ); |
| 456 | $out->addModuleStyles( [ 'mediawiki.special', |
| 457 | 'mediawiki.interface.helpers.styles' ] ); |
| 458 | |
| 459 | $dropdownReason = $this->msg( 'revdelete-reason-dropdown' ) |
| 460 | ->page( $this->targetObj )->inContentLanguage()->text(); |
| 461 | // Add additional specific reasons for suppress |
| 462 | if ( $suppressAllowed ) { |
| 463 | $dropdownReason .= "\n" . $this->msg( 'revdelete-reason-dropdown-suppress' ) |
| 464 | ->page( $this->targetObj )->inContentLanguage()->text(); |
| 465 | } |
| 466 | |
| 467 | $fields = $this->buildCheckBoxes(); |
| 468 | |
| 469 | $fields[] = [ |
| 470 | 'type' => 'select', |
| 471 | 'label' => $this->msg( 'revdelete-log' )->text(), |
| 472 | 'cssclass' => 'wpReasonDropDown', |
| 473 | 'id' => 'wpRevDeleteReasonList', |
| 474 | 'name' => 'wpRevDeleteReasonList', |
| 475 | 'options' => Html::listDropdownOptions( |
| 476 | $dropdownReason, |
| 477 | [ 'other' => $this->msg( 'revdelete-reasonotherlist' )->text() ] |
| 478 | ), |
| 479 | 'default' => $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ) |
| 480 | ]; |
| 481 | |
| 482 | $fields[] = [ |
| 483 | 'type' => 'text', |
| 484 | 'label' => $this->msg( 'revdelete-otherreason' )->text(), |
| 485 | 'name' => 'wpReason', |
| 486 | 'id' => 'wpReason', |
| 487 | // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP |
| 488 | // (e.g. emojis) count for two each. This limit is overridden in JS to instead count |
| 489 | // Unicode codepoints. |
| 490 | // "- 155" is to leave room for the 'wpRevDeleteReasonList' value. |
| 491 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155, |
| 492 | ]; |
| 493 | |
| 494 | $fields[] = [ |
| 495 | 'type' => 'hidden', |
| 496 | 'name' => 'wpEditToken', |
| 497 | 'default' => $this->getUser()->getEditToken() |
| 498 | ]; |
| 499 | |
| 500 | $fields[] = [ |
| 501 | 'type' => 'hidden', |
| 502 | 'name' => 'target', |
| 503 | 'default' => $this->targetObj->getPrefixedText() |
| 504 | ]; |
| 505 | |
| 506 | $fields[] = [ |
| 507 | 'type' => 'hidden', |
| 508 | 'name' => 'type', |
| 509 | 'default' => $this->typeName |
| 510 | ]; |
| 511 | |
| 512 | $fields[] = [ |
| 513 | 'type' => 'hidden', |
| 514 | 'name' => 'ids', |
| 515 | 'default' => implode( ',', $this->ids ) |
| 516 | ]; |
| 517 | |
| 518 | $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() ); |
| 519 | $htmlForm |
| 520 | ->setSubmitText( $this->msg( 'revdelete-submit', $numRevisions )->text() ) |
| 521 | ->setSubmitName( 'wpSubmit' ) |
| 522 | ->setWrapperLegend( $this->msg( 'revdelete-legend' )->text() ) |
| 523 | ->setAction( $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ) |
| 524 | ->loadData(); |
| 525 | // Show link to edit the dropdown reasons |
| 526 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'editinterface' ) ) { |
| 527 | $link = ''; |
| 528 | $linkRenderer = $this->getLinkRenderer(); |
| 529 | if ( $suppressAllowed ) { |
| 530 | $link .= $linkRenderer->makeKnownLink( |
| 531 | $this->msg( 'revdelete-reason-dropdown-suppress' )->inContentLanguage()->getTitle(), |
| 532 | $this->msg( 'revdelete-edit-reasonlist-suppress' )->text(), |
| 533 | [], |
| 534 | [ 'action' => 'edit' ] |
| 535 | ); |
| 536 | $link .= $this->msg( 'pipe-separator' )->escaped(); |
| 537 | } |
| 538 | $link .= $linkRenderer->makeKnownLink( |
| 539 | $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->getTitle(), |
| 540 | $this->msg( 'revdelete-edit-reasonlist' )->text(), |
| 541 | [], |
| 542 | [ 'action' => 'edit' ] |
| 543 | ); |
| 544 | $htmlForm->setPostHtml( Html::rawElement( 'p', [ 'class' => 'mw-revdel-editreasons' ], $link ) ); |
| 545 | } |
| 546 | $out->addHTML( $htmlForm->getHTML( false ) ); |
| 547 | } |
| 548 | } |
| 549 | |
| 550 | /** |
| 551 | * Show some introductory text |
| 552 | * @todo FIXME: Wikimedia-specific policy text |
| 553 | */ |
| 554 | protected function addUsageText() { |
| 555 | // Messages: revdelete-text-text, revdelete-text-file, logdelete-text |
| 556 | $this->getOutput()->wrapWikiMsg( |
| 557 | "<strong>$1</strong>\n$2", $this->typeLabels['text'], |
| 558 | 'revdelete-text-others' |
| 559 | ); |
| 560 | |
| 561 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ) ) { |
| 562 | $this->getOutput()->addWikiMsg( 'revdelete-suppress-text' ); |
| 563 | } |
| 564 | |
| 565 | if ( $this->mIsAllowed ) { |
| 566 | $this->getOutput()->addWikiMsg( 'revdelete-confirm' ); |
| 567 | } |
| 568 | } |
| 569 | |
| 570 | /** |
| 571 | * @return array $fields |
| 572 | */ |
| 573 | protected function buildCheckBoxes() { |
| 574 | $fields = []; |
| 575 | |
| 576 | $type = 'radio'; |
| 577 | |
| 578 | $list = $this->getList(); |
| 579 | |
| 580 | // If there is just one item, use checkboxes |
| 581 | if ( $list->length() == 1 ) { |
| 582 | $list->reset(); |
| 583 | |
| 584 | $type = 'check'; |
| 585 | } |
| 586 | |
| 587 | foreach ( $this->checks as $item ) { |
| 588 | // Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name, |
| 589 | // revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted |
| 590 | [ $message, $name, $bitField ] = $item; |
| 591 | |
| 592 | $field = [ |
| 593 | 'type' => $type, |
| 594 | 'label-raw' => $this->msg( $message )->escaped(), |
| 595 | 'id' => $name, |
| 596 | 'flatlist' => true, |
| 597 | 'name' => $name, |
| 598 | 'default' => $list->length() == 1 ? $list->current()->getBits() & $bitField : null |
| 599 | ]; |
| 600 | |
| 601 | if ( $bitField == RevisionRecord::DELETED_RESTRICTED ) { |
| 602 | $field['label-raw'] = "<b>" . $field['label-raw'] . "</b>"; |
| 603 | if ( $type === 'radio' ) { |
| 604 | $field['options-messages'] = [ |
| 605 | 'revdelete-radio-same' => -1, |
| 606 | 'revdelete-radio-unset-suppress' => 0, |
| 607 | 'revdelete-radio-set-suppress' => 1 |
| 608 | ]; |
| 609 | } |
| 610 | } elseif ( $type === 'radio' ) { |
| 611 | $field['options-messages'] = [ |
| 612 | 'revdelete-radio-same' => -1, |
| 613 | 'revdelete-radio-unset' => 0, |
| 614 | 'revdelete-radio-set' => 1 |
| 615 | ]; |
| 616 | } |
| 617 | |
| 618 | $fields[] = $field; |
| 619 | } |
| 620 | |
| 621 | return $fields; |
| 622 | } |
| 623 | |
| 624 | /** |
| 625 | * UI entry point for form submission. |
| 626 | * @throws PermissionsError |
| 627 | * @return bool |
| 628 | */ |
| 629 | protected function submit() { |
| 630 | # Check edit token on submission |
| 631 | $token = $this->getRequest()->getVal( 'wpEditToken' ); |
| 632 | if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) { |
| 633 | $this->getOutput()->addWikiMsg( 'sessionfailure' ); |
| 634 | |
| 635 | return false; |
| 636 | } |
| 637 | $bitParams = $this->extractBitParams(); |
| 638 | // from dropdown |
| 639 | $listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ); |
| 640 | $comment = $listReason; |
| 641 | if ( $comment === 'other' ) { |
| 642 | $comment = $this->otherReason; |
| 643 | } elseif ( $this->otherReason !== '' ) { |
| 644 | // Entry from drop down menu + additional comment |
| 645 | $comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text() |
| 646 | . $this->otherReason; |
| 647 | } |
| 648 | # Can the user set this field? |
| 649 | if ( $bitParams[RevisionRecord::DELETED_RESTRICTED] == 1 |
| 650 | && !$this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ) |
| 651 | ) { |
| 652 | throw new PermissionsError( 'suppressrevision' ); |
| 653 | } |
| 654 | # If the save went through, go to success message... |
| 655 | $status = $this->save( $bitParams, $comment ); |
| 656 | if ( $status->isGood() ) { |
| 657 | $this->success(); |
| 658 | |
| 659 | return true; |
| 660 | } else { |
| 661 | # ...otherwise, bounce back to form... |
| 662 | $this->failure( $status ); |
| 663 | } |
| 664 | |
| 665 | return false; |
| 666 | } |
| 667 | |
| 668 | /** |
| 669 | * Report that the submit operation succeeded |
| 670 | */ |
| 671 | protected function success() { |
| 672 | // Messages: revdelete-success, logdelete-success |
| 673 | $out = $this->getOutput(); |
| 674 | $out->setPageTitleMsg( $this->msg( 'actioncomplete' ) ); |
| 675 | $out->addHTML( |
| 676 | Html::successBox( |
| 677 | $out->msg( $this->typeLabels['success'] )->parse() |
| 678 | ) |
| 679 | ); |
| 680 | $this->wasSaved = true; |
| 681 | $this->revDelList->reloadFromPrimary(); |
| 682 | $this->showForm(); |
| 683 | } |
| 684 | |
| 685 | /** |
| 686 | * Report that the submit operation failed |
| 687 | * @param Status $status |
| 688 | */ |
| 689 | protected function failure( $status ) { |
| 690 | // Messages: revdelete-failure, logdelete-failure |
| 691 | $out = $this->getOutput(); |
| 692 | $out->setPageTitleMsg( $this->msg( 'actionfailed' ) ); |
| 693 | $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' ); |
| 694 | $out->addHTML( |
| 695 | Html::errorBox( |
| 696 | $out->parseAsContent( |
| 697 | $status->getWikiText( $this->typeLabels['failure'], false, $this->getLanguage() ) |
| 698 | ) |
| 699 | ) |
| 700 | ); |
| 701 | $this->showForm(); |
| 702 | } |
| 703 | |
| 704 | /** |
| 705 | * Put together an array that contains -1, 0, or the *_deleted const for each bit |
| 706 | * |
| 707 | * @return array |
| 708 | */ |
| 709 | protected function extractBitParams() { |
| 710 | $bitfield = []; |
| 711 | foreach ( $this->checks as [ /* message */, $name, $field ] ) { |
| 712 | $val = $this->getRequest()->getInt( $name, 0 /* unchecked */ ); |
| 713 | if ( $val < -1 || $val > 1 ) { |
| 714 | $val = -1; // -1 for existing value |
| 715 | } |
| 716 | $bitfield[$field] = $val; |
| 717 | } |
| 718 | if ( !isset( $bitfield[RevisionRecord::DELETED_RESTRICTED] ) ) { |
| 719 | $bitfield[RevisionRecord::DELETED_RESTRICTED] = 0; |
| 720 | } |
| 721 | |
| 722 | return $bitfield; |
| 723 | } |
| 724 | |
| 725 | /** |
| 726 | * Do the write operations. Simple wrapper for RevDel*List::setVisibility(). |
| 727 | * @param array $bitPars ExtractBitParams() bitfield array |
| 728 | * @param string $reason |
| 729 | * @return Status |
| 730 | */ |
| 731 | protected function save( array $bitPars, $reason ) { |
| 732 | return $this->getList()->setVisibility( |
| 733 | [ 'value' => $bitPars, 'comment' => $reason ] |
| 734 | ); |
| 735 | } |
| 736 | |
| 737 | /** @inheritDoc */ |
| 738 | protected function getGroupName() { |
| 739 | return 'pagetools'; |
| 740 | } |
| 741 | } |
| 742 | |
| 743 | /** |
| 744 | * Retain the old class name for backwards compatibility. |
| 745 | * @deprecated since 1.41 |
| 746 | */ |
| 747 | class_alias( SpecialRevisionDelete::class, 'SpecialRevisionDelete' ); |