39 private $submitClicked;
74 private const UI_LABELS = [
76 'check-label' =>
'revdelete-hide-text',
77 'success' =>
'revdelete-success',
78 'failure' =>
'revdelete-failure',
79 'text' =>
'revdelete-text-text',
80 'selected' =>
'revdelete-selected-text',
83 'check-label' =>
'revdelete-hide-text',
84 'success' =>
'revdelete-success',
85 'failure' =>
'revdelete-failure',
86 'text' =>
'revdelete-text-text',
87 'selected' =>
'revdelete-selected-text',
90 'check-label' =>
'revdelete-hide-image',
91 'success' =>
'revdelete-success',
92 'failure' =>
'revdelete-failure',
93 'text' =>
'revdelete-text-file',
94 'selected' =>
'revdelete-selected-file',
97 'check-label' =>
'revdelete-hide-image',
98 'success' =>
'revdelete-success',
99 'failure' =>
'revdelete-failure',
100 'text' =>
'revdelete-text-file',
101 'selected' =>
'revdelete-selected-file',
104 'check-label' =>
'revdelete-hide-name',
105 'success' =>
'logdelete-success',
106 'failure' =>
'logdelete-failure',
107 'text' =>
'logdelete-text',
108 'selected' =>
'logdelete-selected',
116 parent::__construct(
'Revisiondelete' );
137 $this->submitClicked = $request->wasPosted() && $request->getBool(
'wpSubmit' );
138 # Handle our many different possible input types.
139 $ids = $request->getVal(
'ids' );
140 if ( $ids !==
null ) {
141 # Allow CSV, for backwards compatibility, or a single ID for show/hide links
142 $this->ids = explode(
',', $ids );
145 $this->ids = array_keys( $request->getArray(
'ids', [] ) );
148 $this->ids = array_unique( array_filter( $this->ids ) );
150 $this->typeName = $request->getVal(
'type' ) ??
'';
151 $this->targetObj = Title::newFromText( $request->getText(
'target' ) );
153 # For reviewing deleted files...
154 $this->archiveName = $request->getVal(
'file' );
155 $this->token = $request->getVal(
'token' );
156 if ( $this->archiveName && $this->targetObj ) {
162 $this->typeName = RevisionDeleter::getCanonicalTypeName( $this->typeName );
165 if ( !$this->typeName || count( $this->ids ) == 0 ) {
166 throw new ErrorPageError(
'revdelete-nooldid-title',
'revdelete-nooldid-text' );
169 $restriction = RevisionDeleter::getRestriction( $this->typeName );
171 if ( !$this->
getAuthority()->isAllowedAny( $restriction,
'deletedhistory' ) ) {
175 # Allow the list type to adjust the passed target
176 $this->targetObj = RevisionDeleter::suggestTarget(
182 # We need a target page!
183 if ( $this->targetObj ===
null ||
184 ( $this->typeName !==
'logging' && !$this->targetObj->canExist() )
186 $output->addWikiMsg(
'undelete-header' );
192 $checkReplica = !$this->submitClicked;
194 $this->permissionManager->isBlockedFrom(
204 $this->getLanguage(),
209 $this->typeLabels = self::UI_LABELS[$this->typeName];
212 $this->mIsAllowed = $this->permissionManager->userHasRight( $user, $restriction );
213 $canViewSuppressedOnly = $this->permissionManager->userHasRight( $user,
'viewsuppressed' ) &&
214 !$this->permissionManager->userHasRight( $user,
'suppressrevision' );
215 $pageIsSuppressed = $list->areAnySuppressed();
216 $this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed );
218 $this->otherReason = $request->getVal(
'wpReason',
'' );
219 # Give a link to the logs/hist for this page
222 # Initialise checkboxes
224 # Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name
225 [ $this->typeLabels[
'check-label'],
'wpHidePrimary',
226 RevisionDeleter::getRevdelConstant( $this->typeName )
228 [
'revdelete-hide-comment',
'wpHideComment', RevisionRecord::DELETED_COMMENT ],
229 [
'revdelete-hide-user',
'wpHideUser', RevisionRecord::DELETED_USER ]
231 if ( $this->permissionManager->userHasRight( $user,
'suppressrevision' ) ) {
232 $this->checks[] = [
'revdelete-hide-restricted',
233 'wpHideRestricted', RevisionRecord::DELETED_RESTRICTED ];
236 # Either submit or create our form
237 if ( $this->mIsAllowed && $this->submitClicked ) {
243 if ( $this->permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
244 # Show relevant lines from the deletion log
245 $deleteLogPage =
new LogPage(
'delete' );
246 $output->addHTML(
"<h2>" . $deleteLogPage->getName()->escaped() .
"</h2>\n" );
247 LogEventsList::showLogExtract(
252 [
'lim' => 25,
'conds' => $this->
getLogQueryCond(),
'useMaster' => $this->wasSaved ]
255 # Show relevant lines from the suppression log
256 if ( $this->permissionManager->userHasRight( $user,
'suppressionlog' ) ) {
257 $suppressLogPage =
new LogPage(
'suppress' );
258 $output->addHTML(
"<h2>" . $suppressLogPage->getName()->escaped() .
"</h2>\n" );
259 LogEventsList::showLogExtract(
264 [
'lim' => 25,
'conds' => $this->
getLogQueryCond(),
'useMaster' => $this->wasSaved ]
274 # Give a link to the logs/hist for this page
275 if ( $this->targetObj ) {
277 $this->
getSkin()->setRelevantTitle( $this->targetObj );
280 $links[] = $linkRenderer->makeKnownLink(
282 $this->
msg(
'viewpagelogs' )->text(),
284 [
'page' => $this->targetObj->getPrefixedText() ]
286 if ( !$this->targetObj->isSpecialPage() ) {
287 # Give a link to the page history
288 $links[] = $linkRenderer->makeKnownLink(
290 $this->
msg(
'pagehist' )->text(),
292 [
'action' =>
'history' ]
294 # Link to deleted edits
295 if ( $this->permissionManager->userHasRight( $this->getUser(),
'undelete' ) ) {
297 $links[] = $linkRenderer->makeKnownLink(
299 $this->
msg(
'deletedhist' )->text(),
301 [
'target' => $this->targetObj->getPrefixedDBkey() ]
305 # Logs themselves don't have histories or archived revisions
317 $conds[
'log_type'] = [
'delete',
'suppress' ];
318 $conds[
'log_action'] = $this->
getList()->getLogAction();
319 $conds[
'ls_field'] = RevisionDeleter::getRelationType( $this->typeName );
322 $conds[
'ls_value'] = array_map(
'strval', $this->ids );
333 if ( $this->targetObj->getNamespace() !==
NS_FILE ) {
334 $this->
getOutput()->addWikiMsg(
'revdelete-no-file' );
338 $repo = $this->repoGroup->getLocalRepo();
339 $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName );
342 if ( !$oimage->exists() ) {
343 $this->
getOutput()->addWikiMsg(
'revdelete-no-file' );
348 if ( !$oimage->userCan( File::DELETED_FILE, $user ) ) {
349 if ( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
355 if ( !$user->matchEditToken( $this->token, $archiveName ) ) {
357 $this->
getOutput()->addWikiMsg(
'revdelete-show-file-confirm',
358 $this->targetObj->getText(),
359 $lang->userDate( $oimage->getTimestamp(), $user ),
360 $lang->userTime( $oimage->getTimestamp(), $user ) );
362 Html::rawElement(
'form', [
365 'target' => $this->targetObj->getPrefixedDBkey(),
366 'file' => $archiveName,
367 'token' => $user->getEditToken( $archiveName ),
370 Html::submitButton( $this->msg(
'revdelete-show-file-submit' )->text() )
377 # We mustn't allow the output to be CDN cached, otherwise
378 # if an admin previews a deleted image, and it's cached, then
379 # a user without appropriate permissions can toddle off and
380 # nab the image, and CDN will serve it
381 $this->
getRequest()->response()->header(
'Expires: ' . gmdate(
'D, d M Y H:i:s', 0 ) .
' GMT' );
383 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate'
386 $key = $oimage->getStorageKey();
387 $path = $repo->getZonePath(
'deleted' ) .
'/' . $repo->getDeletedHashPath( $key ) . $key;
388 $repo->streamFileWithStatus(
$path );
396 if ( $this->revDelList ===
null ) {
397 $this->revDelList = RevisionDeleter::createList(
398 $this->typeName, $this->
getContext(), $this->targetObj, $this->ids
402 return $this->revDelList;
414 $out->wrapWikiMsg(
"<strong>$1</strong>", [ $this->typeLabels[
'selected'],
415 $this->
getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ] );
418 $out->addHTML(
"<ul>" );
423 foreach ( $list as $item ) {
424 if ( !$item->canView() ) {
425 if ( !$this->submitClicked ) {
428 $userAllowed =
false;
432 $out->addHTML( $item->getHTML() );
435 if ( !$numRevisions ) {
436 throw new ErrorPageError(
'revdelete-nooldid-title',
'revdelete-nooldid-text' );
439 $out->addHTML(
"</ul>" );
444 if ( !$userAllowed ) {
449 if ( $this->mIsAllowed ) {
450 $suppressAllowed = $this->permissionManager
451 ->userHasRight( $this->
getUser(),
'suppressrevision' );
452 $out->addModules( [
'mediawiki.misc-authed-ooui' ] );
453 $out->addModuleStyles( [
'mediawiki.special',
454 'mediawiki.interface.helpers.styles' ] );
456 $dropdownReason = $this->
msg(
'revdelete-reason-dropdown' )
457 ->page( $this->targetObj )->inContentLanguage()->text();
459 if ( $suppressAllowed ) {
460 $dropdownReason .=
"\n" . $this->
msg(
'revdelete-reason-dropdown-suppress' )
461 ->page( $this->targetObj )->inContentLanguage()->text();
468 'label' => $this->
msg(
'revdelete-log' )->text(),
469 'cssclass' =>
'wpReasonDropDown',
470 'id' =>
'wpRevDeleteReasonList',
471 'name' =>
'wpRevDeleteReasonList',
472 'options' => Html::listDropdownOptions(
474 [
'other' => $this->
msg(
'revdelete-reasonotherlist' )->text() ]
476 'default' => $this->
getRequest()->getText(
'wpRevDeleteReasonList',
'other' )
481 'label' => $this->
msg(
'revdelete-otherreason' )->text(),
482 'name' =>
'wpReason',
488 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155,
493 'name' =>
'wpEditToken',
494 'default' => $this->
getUser()->getEditToken()
500 'default' => $this->targetObj->getPrefixedText()
506 'default' => $this->typeName
512 'default' => implode(
',', $this->ids )
515 $htmlForm = HTMLForm::factory(
'ooui', $fields, $this->
getContext() );
517 ->setSubmitText( $this->
msg(
'revdelete-submit', $numRevisions )->text() )
518 ->setSubmitName(
'wpSubmit' )
519 ->setWrapperLegend( $this->
msg(
'revdelete-legend' )->text() )
520 ->setAction( $this->
getPageTitle()->getLocalURL( [
'action' =>
'submit' ] ) )
523 if ( $this->permissionManager->userHasRight( $this->getUser(),
'editinterface' ) ) {
526 if ( $suppressAllowed ) {
527 $link .= $linkRenderer->makeKnownLink(
528 $this->
msg(
'revdelete-reason-dropdown-suppress' )->inContentLanguage()->getTitle(),
529 $this->
msg(
'revdelete-edit-reasonlist-suppress' )->text(),
531 [
'action' =>
'edit' ]
533 $link .= $this->
msg(
'pipe-separator' )->escaped();
535 $link .= $linkRenderer->makeKnownLink(
536 $this->
msg(
'revdelete-reason-dropdown' )->inContentLanguage()->getTitle(),
537 $this->
msg(
'revdelete-edit-reasonlist' )->text(),
539 [
'action' =>
'edit' ]
541 $htmlForm->setPostHtml( Html::rawElement(
'p', [
'class' =>
'mw-revdel-editreasons' ], $link ) );
543 $out->addHTML( $htmlForm->getHTML(
false ) );
554 "<strong>$1</strong>\n$2", $this->typeLabels[
'text'],
555 'revdelete-text-others'
558 if ( $this->permissionManager->userHasRight( $this->getUser(),
'suppressrevision' ) ) {
559 $this->
getOutput()->addWikiMsg(
'revdelete-suppress-text' );
562 if ( $this->mIsAllowed ) {
563 $this->
getOutput()->addWikiMsg(
'revdelete-confirm' );
578 if ( $list->length() == 1 ) {
584 foreach ( $this->checks as $item ) {
587 [ $message, $name, $bitField ] = $item;
591 'label-raw' => $this->
msg( $message )->escaped(),
595 'default' => $list->length() == 1 ? $list->current()->getBits() & $bitField : null
598 if ( $bitField == RevisionRecord::DELETED_RESTRICTED ) {
599 $field[
'label-raw'] =
"<b>" . $field[
'label-raw'] .
"</b>";
600 if ( $type ===
'radio' ) {
601 $field[
'options-messages'] = [
602 'revdelete-radio-same' => -1,
603 'revdelete-radio-unset-suppress' => 0,
604 'revdelete-radio-set-suppress' => 1
607 } elseif ( $type ===
'radio' ) {
608 $field[
'options-messages'] = [
609 'revdelete-radio-same' => -1,
610 'revdelete-radio-unset' => 0,
611 'revdelete-radio-set' => 1
627 # Check edit token on submission
628 $token = $this->
getRequest()->getVal(
'wpEditToken' );
629 if ( $this->submitClicked && !$this->
getUser()->matchEditToken( $token ) ) {
630 $this->
getOutput()->addWikiMsg(
'sessionfailure' );
636 $listReason = $this->
getRequest()->getText(
'wpRevDeleteReasonList',
'other' );
637 $comment = $listReason;
638 if ( $comment ===
'other' ) {
639 $comment = $this->otherReason;
640 } elseif ( $this->otherReason !==
'' ) {
642 $comment .= $this->
msg(
'colon-separator' )->inContentLanguage()->text()
643 . $this->otherReason;
645 # Can the user set this field?
646 if ( $bitParams[RevisionRecord::DELETED_RESTRICTED] == 1
647 && !$this->permissionManager->userHasRight( $this->getUser(),
'suppressrevision' )
651 # If the save went through, go to success message...
652 $status = $this->
save( $bitParams, $comment );
653 if ( $status->isGood() ) {
658 # ...otherwise, bounce back to form...
671 $out->setPageTitleMsg( $this->
msg(
'actioncomplete' ) );
674 $out->msg( $this->typeLabels[
'success'] )->parse()
677 $this->wasSaved =
true;
678 $this->revDelList->reloadFromPrimary();
689 $out->setPageTitleMsg( $this->
msg(
'actionfailed' ) );
690 $out->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
693 $out->parseAsContent(
694 $status->getWikiText( $this->typeLabels[
'failure'],
false, $this->getLanguage() )
708 foreach ( $this->checks as [ , $name, $field ] ) {
709 $val = $this->
getRequest()->getInt( $name, 0 );
710 if ( $val < -1 || $val > 1 ) {
713 $bitfield[$field] = $val;
715 if ( !isset( $bitfield[RevisionRecord::DELETED_RESTRICTED] ) ) {
716 $bitfield[RevisionRecord::DELETED_RESTRICTED] = 0;
728 protected function save( array $bitPars, $reason ) {
729 return $this->
getList()->setVisibility(
730 [
'value' => $bitPars,
'comment' => $reason ]
744class_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.