53 private $submitClicked;
91 private const UI_LABELS = [
93 'check-label' =>
'revdelete-hide-text',
94 'success' =>
'revdelete-success',
95 'failure' =>
'revdelete-failure',
96 'text' =>
'revdelete-text-text',
97 'selected' =>
'revdelete-selected-text',
100 'check-label' =>
'revdelete-hide-text',
101 'success' =>
'revdelete-success',
102 'failure' =>
'revdelete-failure',
103 'text' =>
'revdelete-text-text',
104 'selected' =>
'revdelete-selected-text',
107 'check-label' =>
'revdelete-hide-image',
108 'success' =>
'revdelete-success',
109 'failure' =>
'revdelete-failure',
110 'text' =>
'revdelete-text-file',
111 'selected' =>
'revdelete-selected-file',
114 'check-label' =>
'revdelete-hide-image',
115 'success' =>
'revdelete-success',
116 'failure' =>
'revdelete-failure',
117 'text' =>
'revdelete-text-file',
118 'selected' =>
'revdelete-selected-file',
121 'check-label' =>
'revdelete-hide-name',
122 'success' =>
'logdelete-success',
123 'failure' =>
'logdelete-failure',
124 'text' =>
'logdelete-text',
125 'selected' =>
'logdelete-selected',
130 parent::__construct(
'Revisiondelete' );
132 $this->permissionManager = $permissionManager;
133 $this->repoGroup = $repoGroup;
152 $this->submitClicked = $request->wasPosted() && $request->getBool(
'wpSubmit' );
153 # Handle our many different possible input types.
154 $ids = $request->getVal(
'ids' );
155 if ( $ids !==
null ) {
156 # Allow CSV, for backwards compatibility, or a single ID for show/hide links
157 $this->ids = explode(
',', $ids );
160 $this->ids = array_keys( $request->getArray(
'ids', [] ) );
163 $this->ids = array_unique( array_filter( $this->ids ) );
165 $this->typeName = $request->getVal(
'type' );
166 $this->targetObj = Title::newFromText( $request->getText(
'target' ) );
168 # For reviewing deleted files...
169 $this->archiveName = $request->getVal(
'file' );
170 $this->token = $request->getVal(
'token' );
171 if ( $this->archiveName && $this->targetObj ) {
177 $this->typeName = RevisionDeleter::getCanonicalTypeName( $this->typeName );
180 if ( !$this->typeName || count( $this->ids ) == 0 ) {
181 throw new ErrorPageError(
'revdelete-nooldid-title',
'revdelete-nooldid-text' );
184 $restriction = RevisionDeleter::getRestriction( $this->typeName );
186 if ( !$this->
getAuthority()->isAllowedAny( $restriction,
'deletedhistory' ) ) {
190 # Allow the list type to adjust the passed target
191 $this->targetObj = RevisionDeleter::suggestTarget(
197 # We need a target page!
198 if ( $this->targetObj ===
null ) {
199 $output->addWikiMsg(
'undelete-header' );
205 $checkReplica = !$this->submitClicked;
207 $this->permissionManager->isBlockedFrom(
217 $this->getLanguage(),
222 $this->typeLabels = self::UI_LABELS[$this->typeName];
225 $this->mIsAllowed = $this->permissionManager->userHasRight( $user, $restriction );
226 $canViewSuppressedOnly = $this->permissionManager->userHasRight( $user,
'viewsuppressed' ) &&
227 !$this->permissionManager->userHasRight( $user,
'suppressrevision' );
228 $pageIsSuppressed = $list->areAnySuppressed();
229 $this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed );
231 $this->otherReason = $request->getVal(
'wpReason',
'' );
232 # Give a link to the logs/hist for this page
235 # Initialise checkboxes
237 # Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name
238 [ $this->typeLabels[
'check-label'],
'wpHidePrimary',
239 RevisionDeleter::getRevdelConstant( $this->typeName )
241 [
'revdelete-hide-comment',
'wpHideComment', RevisionRecord::DELETED_COMMENT ],
242 [
'revdelete-hide-user',
'wpHideUser', RevisionRecord::DELETED_USER ]
244 if ( $this->permissionManager->userHasRight( $user,
'suppressrevision' ) ) {
245 $this->checks[] = [
'revdelete-hide-restricted',
246 'wpHideRestricted', RevisionRecord::DELETED_RESTRICTED ];
249 # Either submit or create our form
250 if ( $this->mIsAllowed && $this->submitClicked ) {
256 if ( $this->permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
257 # Show relevant lines from the deletion log
258 $deleteLogPage =
new LogPage(
'delete' );
259 $output->addHTML(
"<h2>" . $deleteLogPage->getName()->escaped() .
"</h2>\n" );
260 LogEventsList::showLogExtract(
265 [
'lim' => 25,
'conds' => $this->
getLogQueryCond(),
'useMaster' => $this->wasSaved ]
268 # Show relevant lines from the suppression log
269 if ( $this->permissionManager->userHasRight( $user,
'suppressionlog' ) ) {
270 $suppressLogPage =
new LogPage(
'suppress' );
271 $output->addHTML(
"<h2>" . $suppressLogPage->getName()->escaped() .
"</h2>\n" );
272 LogEventsList::showLogExtract(
277 [
'lim' => 25,
'conds' => $this->
getLogQueryCond(),
'useMaster' => $this->wasSaved ]
287 # Give a link to the logs/hist for this page
288 if ( $this->targetObj ) {
290 $this->
getSkin()->setRelevantTitle( $this->targetObj );
293 $links[] = $linkRenderer->makeKnownLink(
295 $this->
msg(
'viewpagelogs' )->text(),
297 [
'page' => $this->targetObj->getPrefixedText() ]
299 if ( !$this->targetObj->isSpecialPage() ) {
300 # Give a link to the page history
301 $links[] = $linkRenderer->makeKnownLink(
303 $this->
msg(
'pagehist' )->text(),
305 [
'action' =>
'history' ]
307 # Link to deleted edits
308 if ( $this->permissionManager->userHasRight( $this->getUser(),
'undelete' ) ) {
310 $links[] = $linkRenderer->makeKnownLink(
312 $this->
msg(
'deletedhist' )->text(),
314 [
'target' => $this->targetObj->getPrefixedDBkey() ]
318 # Logs themselves don't have histories or archived revisions
330 $conds[
'log_type'] = [
'delete',
'suppress' ];
331 $conds[
'log_action'] = $this->
getList()->getLogAction();
332 $conds[
'ls_field'] = RevisionDeleter::getRelationType( $this->typeName );
335 $conds[
'ls_value'] = array_map(
'strval', $this->ids );
346 $repo = $this->repoGroup->getLocalRepo();
347 $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName );
350 if ( !$oimage->exists() ) {
351 $this->
getOutput()->addWikiMsg(
'revdelete-no-file' );
356 if ( !$oimage->userCan( File::DELETED_FILE, $user ) ) {
357 if ( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
363 if ( !$user->matchEditToken( $this->token, $archiveName ) ) {
365 $this->
getOutput()->addWikiMsg(
'revdelete-show-file-confirm',
366 $this->targetObj->getText(),
367 $lang->userDate( $oimage->getTimestamp(), $user ),
368 $lang->userTime( $oimage->getTimestamp(), $user ) );
370 Html::rawElement(
'form', [
373 'target' => $this->targetObj->getPrefixedDBkey(),
374 'file' => $archiveName,
375 'token' => $user->getEditToken( $archiveName ),
378 Html::submitButton( $this->msg(
'revdelete-show-file-submit' )->text() )
385 # We mustn't allow the output to be CDN cached, otherwise
386 # if an admin previews a deleted image, and it's cached, then
387 # a user without appropriate permissions can toddle off and
388 # nab the image, and CDN will serve it
389 $this->
getRequest()->response()->header(
'Expires: ' . gmdate(
'D, d M Y H:i:s', 0 ) .
' GMT' );
391 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate'
394 $key = $oimage->getStorageKey();
395 $path = $repo->getZonePath(
'deleted' ) .
'/' . $repo->getDeletedHashPath( $key ) . $key;
396 $repo->streamFileWithStatus(
$path );
404 if ( $this->revDelList ===
null ) {
405 $this->revDelList = RevisionDeleter::createList(
406 $this->typeName, $this->
getContext(), $this->targetObj, $this->ids
410 return $this->revDelList;
422 $out->wrapWikiMsg(
"<strong>$1</strong>", [ $this->typeLabels[
'selected'],
423 $this->
getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ] );
426 $out->addHTML(
"<ul>" );
431 for ( $list->reset(); $list->current(); $list->next() ) {
432 $item = $list->current();
434 if ( !$item->canView() ) {
435 if ( !$this->submitClicked ) {
438 $userAllowed =
false;
442 $out->addHTML( $item->getHTML() );
445 if ( !$numRevisions ) {
446 throw new ErrorPageError(
'revdelete-nooldid-title',
'revdelete-nooldid-text' );
449 $out->addHTML(
"</ul>" );
454 if ( !$userAllowed ) {
459 if ( $this->mIsAllowed ) {
460 $suppressAllowed = $this->permissionManager
461 ->userHasRight( $this->
getUser(),
'suppressrevision' );
462 $out->addModules( [
'mediawiki.misc-authed-ooui' ] );
463 $out->addModuleStyles( [
'mediawiki.special',
464 'mediawiki.interface.helpers.styles' ] );
466 $dropdownReason = $this->
msg(
'revdelete-reason-dropdown' )
467 ->page( $this->targetObj )->inContentLanguage()->text();
469 if ( $suppressAllowed ) {
470 $dropdownReason .=
"\n" . $this->
msg(
'revdelete-reason-dropdown-suppress' )
471 ->page( $this->targetObj )->inContentLanguage()->text();
478 'label' => $this->
msg(
'revdelete-log' )->text(),
479 'cssclass' =>
'wpReasonDropDown',
480 'id' =>
'wpRevDeleteReasonList',
481 'name' =>
'wpRevDeleteReasonList',
482 'options' => Html::listDropdownOptions(
484 [
'other' => $this->
msg(
'revdelete-reasonotherlist' )->text() ]
486 'default' => $this->
getRequest()->getText(
'wpRevDeleteReasonList',
'other' )
491 'label' => $this->
msg(
'revdelete-otherreason' )->text(),
492 'name' =>
'wpReason',
498 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155,
503 'name' =>
'wpEditToken',
504 'default' => $this->
getUser()->getEditToken()
510 'default' => $this->targetObj->getPrefixedText()
516 'default' => $this->typeName
522 'default' => implode(
',', $this->ids )
525 $htmlForm = HTMLForm::factory(
'ooui', $fields, $this->
getContext() );
527 ->setSubmitText( $this->
msg(
'revdelete-submit', $numRevisions )->text() )
528 ->setSubmitName(
'wpSubmit' )
529 ->setWrapperLegend( $this->
msg(
'revdelete-legend' )->text() )
530 ->setAction( $this->
getPageTitle()->getLocalURL( [
'action' =>
'submit' ] ) )
533 if ( $this->permissionManager->userHasRight( $this->getUser(),
'editinterface' ) ) {
536 if ( $suppressAllowed ) {
537 $link .= $linkRenderer->makeKnownLink(
538 $this->
msg(
'revdelete-reason-dropdown-suppress' )->inContentLanguage()->
getTitle(),
539 $this->
msg(
'revdelete-edit-reasonlist-suppress' )->text(),
541 [
'action' =>
'edit' ]
543 $link .= $this->
msg(
'pipe-separator' )->escaped();
545 $link .= $linkRenderer->makeKnownLink(
546 $this->
msg(
'revdelete-reason-dropdown' )->inContentLanguage()->
getTitle(),
547 $this->
msg(
'revdelete-edit-reasonlist' )->text(),
549 [
'action' =>
'edit' ]
551 $htmlForm->setPostHtml( Html::rawElement(
'p', [
'class' =>
'mw-revdel-editreasons' ], $link ) );
553 $out->addHTML( $htmlForm->getHTML(
false ) );
564 "<strong>$1</strong>\n$2", $this->typeLabels[
'text'],
565 'revdelete-text-others'
568 if ( $this->permissionManager->userHasRight( $this->getUser(),
'suppressrevision' ) ) {
569 $this->
getOutput()->addWikiMsg(
'revdelete-suppress-text' );
572 if ( $this->mIsAllowed ) {
573 $this->
getOutput()->addWikiMsg(
'revdelete-confirm' );
588 if ( $list->length() == 1 ) {
594 foreach ( $this->checks as $item ) {
597 [ $message, $name, $bitField ] = $item;
601 'label-raw' => $this->
msg( $message )->escaped(),
605 'default' => $list->length() == 1 ? $list->current()->getBits() & $bitField : null
608 if ( $bitField == RevisionRecord::DELETED_RESTRICTED ) {
609 $field[
'label-raw'] =
"<b>" . $field[
'label-raw'] .
"</b>";
610 if ( $type ===
'radio' ) {
611 $field[
'options-messages'] = [
612 'revdelete-radio-same' => -1,
613 'revdelete-radio-unset-suppress' => 0,
614 'revdelete-radio-set-suppress' => 1
617 } elseif ( $type ===
'radio' ) {
618 $field[
'options-messages'] = [
619 'revdelete-radio-same' => -1,
620 'revdelete-radio-unset' => 0,
621 'revdelete-radio-set' => 1
637 # Check edit token on submission
638 $token = $this->
getRequest()->getVal(
'wpEditToken' );
639 if ( $this->submitClicked && !$this->
getUser()->matchEditToken( $token ) ) {
640 $this->
getOutput()->addWikiMsg(
'sessionfailure' );
646 $listReason = $this->
getRequest()->getText(
'wpRevDeleteReasonList',
'other' );
647 $comment = $listReason;
648 if ( $comment ===
'other' ) {
649 $comment = $this->otherReason;
650 } elseif ( $this->otherReason !==
'' ) {
652 $comment .= $this->
msg(
'colon-separator' )->inContentLanguage()->text()
653 . $this->otherReason;
655 # Can the user set this field?
656 if ( $bitParams[RevisionRecord::DELETED_RESTRICTED] == 1
657 && !$this->permissionManager->userHasRight( $this->getUser(),
'suppressrevision' )
661 # If the save went through, go to success message...
662 $status = $this->
save( $bitParams, $comment );
663 if ( $status->isGood() ) {
668 # ...otherwise, bounce back to form...
681 $out->setPageTitleMsg( $this->
msg(
'actioncomplete' ) );
684 $out->msg( $this->typeLabels[
'success'] )->parse()
687 $this->wasSaved =
true;
688 $this->revDelList->reloadFromPrimary();
699 $out->setPageTitleMsg( $this->
msg(
'actionfailed' ) );
700 $out->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
703 $out->parseAsContent(
704 $status->getWikiText( $this->typeLabels[
'failure'],
false, $this->getLanguage() )
718 foreach ( $this->checks as [ , $name, $field ] ) {
719 $val = $this->
getRequest()->getInt( $name, 0 );
720 if ( $val < -1 || $val > 1 ) {
723 $bitfield[$field] = $val;
725 if ( !isset( $bitfield[RevisionRecord::DELETED_RESTRICTED] ) ) {
726 $bitfield[RevisionRecord::DELETED_RESTRICTED] = 0;
738 protected function save( array $bitPars, $reason ) {
739 return $this->
getList()->setVisibility(
740 [
'value' => $bitPars,
'comment' => $reason ]
753class_alias( SpecialRevisionDelete::class,
'SpecialRevisionDelete' );
An error page which can definitely be safely rendered using the OutputPage.
Class to simplify the use of log pages.
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getSkin()
Shortcut to get the skin being used for this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getUser()
Shortcut to get the User executing this instance.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Shortcut to construct a special page which is unlisted by default.
General controller for RevDel, used by both SpecialRevisiondelete and ApiRevisionDelete.