MediaWiki REL1_34
SpecialUndelete.php
Go to the documentation of this file.
1<?php
28
36 private $mAction;
37 private $mTarget;
38 private $mTimestamp;
39 private $mRestore;
40 private $mRevdel;
41 private $mInvert;
42 private $mFilename;
44 private $mAllowed;
45 private $mCanView;
46 private $mComment;
47 private $mToken;
49 private $mPreview;
51 private $mDiff;
53 private $mDiffOnly;
55 private $mUnsuppress;
58
60 private $mTargetObj;
65
66 function __construct() {
67 parent::__construct( 'Undelete', 'deletedhistory' );
68 }
69
70 public function doesWrites() {
71 return true;
72 }
73
74 function loadRequest( $par ) {
75 $request = $this->getRequest();
76 $user = $this->getUser();
77
78 $this->mAction = $request->getVal( 'action' );
79 if ( $par !== null && $par !== '' ) {
80 $this->mTarget = $par;
81 } else {
82 $this->mTarget = $request->getVal( 'target' );
83 }
84
85 $this->mTargetObj = null;
86
87 if ( $this->mTarget !== null && $this->mTarget !== '' ) {
88 $this->mTargetObj = Title::newFromText( $this->mTarget );
89 }
90
91 $this->mSearchPrefix = $request->getText( 'prefix' );
92 $time = $request->getVal( 'timestamp' );
93 $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
94 $this->mFilename = $request->getVal( 'file' );
95
96 $posted = $request->wasPosted() &&
97 $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
98 $this->mRestore = $request->getCheck( 'restore' ) && $posted;
99 $this->mRevdel = $request->getCheck( 'revdel' ) && $posted;
100 $this->mInvert = $request->getCheck( 'invert' ) && $posted;
101 $this->mPreview = $request->getCheck( 'preview' ) && $posted;
102 $this->mDiff = $request->getCheck( 'diff' );
103 $this->mDiffOnly = $request->getBool( 'diffonly', $this->getUser()->getOption( 'diffonly' ) );
104 $this->mComment = $request->getText( 'wpComment' );
105 $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && MediaWikiServices::getInstance()
106 ->getPermissionManager()
107 ->userHasRight( $user, 'suppressrevision' );
108 $this->mToken = $request->getVal( 'token' );
109
110 $block = $user->getBlock();
111 if ( $this->isAllowed( 'undelete' ) && !( $block && $block->isSitewide() ) ) {
112 $this->mAllowed = true; // user can restore
113 $this->mCanView = true; // user can view content
114 } elseif ( $this->isAllowed( 'deletedtext' ) ) {
115 $this->mAllowed = false; // user cannot restore
116 $this->mCanView = true; // user can view content
117 $this->mRestore = false;
118 } else { // user can only view the list of revisions
119 $this->mAllowed = false;
120 $this->mCanView = false;
121 $this->mTimestamp = '';
122 $this->mRestore = false;
123 }
124
125 if ( $this->mRestore || $this->mInvert ) {
126 $timestamps = [];
127 $this->mFileVersions = [];
128 foreach ( $request->getValues() as $key => $val ) {
129 $matches = [];
130 if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
131 array_push( $timestamps, $matches[1] );
132 }
133
134 if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
135 $this->mFileVersions[] = intval( $matches[1] );
136 }
137 }
138 rsort( $timestamps );
139 $this->mTargetTimestamp = $timestamps;
140 }
141 }
142
151 protected function isAllowed( $permission, User $user = null ) {
152 $user = $user ?: $this->getUser();
153 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
154
155 if ( $this->mTargetObj !== null ) {
156 return $permissionManager->userCan( $permission, $user, $this->mTargetObj );
157 } else {
158 return $permissionManager->userHasRight( $user, $permission );
159 }
160 }
161
162 function userCanExecute( User $user ) {
163 return $this->isAllowed( $this->mRestriction, $user );
164 }
165
166 function execute( $par ) {
168
169 $user = $this->getUser();
170
171 $this->setHeaders();
172 $this->outputHeader();
173 $this->addHelpLink( 'Help:Deletion_and_undeletion' );
174
175 $this->loadRequest( $par );
176 $this->checkPermissions(); // Needs to be after mTargetObj is set
177
178 $out = $this->getOutput();
179
180 if ( is_null( $this->mTargetObj ) ) {
181 $out->addWikiMsg( 'undelete-header' );
182
183 # Not all users can just browse every deleted page from the list
184 if ( MediaWikiServices::getInstance()
186 ->userHasRight( $user, 'browsearchive' )
187 ) {
188 $this->showSearchForm();
189 }
190
191 return;
192 }
193
194 $this->addHelpLink( 'Help:Undelete' );
195 if ( $this->mAllowed ) {
196 $out->setPageTitle( $this->msg( 'undeletepage' ) );
197 } else {
198 $out->setPageTitle( $this->msg( 'viewdeletedpage' ) );
199 }
200
201 $this->getSkin()->setRelevantTitle( $this->mTargetObj );
202
203 if ( $this->mTimestamp !== '' ) {
204 $this->showRevision( $this->mTimestamp );
205 } elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) {
206 $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
207 // Check if user is allowed to see this file
208 if ( !$file->exists() ) {
209 $out->addWikiMsg( 'filedelete-nofile', $this->mFilename );
210 } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
211 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
212 throw new PermissionsError( 'suppressrevision' );
213 } else {
214 throw new PermissionsError( 'deletedtext' );
215 }
216 } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
217 $this->showFileConfirmationForm( $this->mFilename );
218 } else {
219 $this->showFile( $this->mFilename );
220 }
221 } elseif ( $this->mAction === 'submit' ) {
222 if ( $this->mRestore ) {
223 $this->undelete();
224 } elseif ( $this->mRevdel ) {
225 $this->redirectToRevDel();
226 }
227
228 } else {
229 $this->showHistory();
230 }
231 }
232
237 private function redirectToRevDel() {
238 $archive = new PageArchive( $this->mTargetObj );
239
240 $revisions = [];
241
242 foreach ( $this->getRequest()->getValues() as $key => $val ) {
243 $matches = [];
244 if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
245 $revisions[$archive->getRevision( $matches[1] )->getId()] = 1;
246 }
247 }
248
249 $query = [
250 'type' => 'revision',
251 'ids' => $revisions,
252 'target' => $this->mTargetObj->getPrefixedText()
253 ];
254 $url = SpecialPage::getTitleFor( 'Revisiondelete' )->getFullURL( $query );
255 $this->getOutput()->redirect( $url );
256 }
257
258 function showSearchForm() {
259 $out = $this->getOutput();
260 $out->setPageTitle( $this->msg( 'undelete-search-title' ) );
261 $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', true );
262
263 $out->enableOOUI();
264
265 $fields = [];
266 $fields[] = new OOUI\ActionFieldLayout(
267 new OOUI\TextInputWidget( [
268 'name' => 'prefix',
269 'inputId' => 'prefix',
270 'infusable' => true,
271 'value' => $this->mSearchPrefix,
272 'autofocus' => true,
273 ] ),
274 new OOUI\ButtonInputWidget( [
275 'label' => $this->msg( 'undelete-search-submit' )->text(),
276 'flags' => [ 'primary', 'progressive' ],
277 'inputId' => 'searchUndelete',
278 'type' => 'submit',
279 ] ),
280 [
281 'label' => new OOUI\HtmlSnippet(
282 $this->msg(
283 $fuzzySearch ? 'undelete-search-full' : 'undelete-search-prefix'
284 )->parse()
285 ),
286 'align' => 'left',
287 ]
288 );
289
290 $fieldset = new OOUI\FieldsetLayout( [
291 'label' => $this->msg( 'undelete-search-box' )->text(),
292 'items' => $fields,
293 ] );
294
295 $form = new OOUI\FormLayout( [
296 'method' => 'get',
297 'action' => wfScript(),
298 ] );
299
300 $form->appendContent(
301 $fieldset,
302 new OOUI\HtmlSnippet(
303 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
304 Html::hidden( 'fuzzy', $fuzzySearch )
305 )
306 );
307
308 $out->addHTML(
309 new OOUI\PanelLayout( [
310 'expanded' => false,
311 'padded' => true,
312 'framed' => true,
313 'content' => $form,
314 ] )
315 );
316
317 # List undeletable articles
318 if ( $this->mSearchPrefix ) {
319 // For now, we enable search engine match only when specifically asked to
320 // by using fuzzy=1 parameter.
321 if ( $fuzzySearch ) {
322 $result = PageArchive::listPagesBySearch( $this->mSearchPrefix );
323 } else {
324 $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
325 }
326 $this->showList( $result );
327 }
328 }
329
336 private function showList( $result ) {
337 $out = $this->getOutput();
338
339 if ( $result->numRows() == 0 ) {
340 $out->addWikiMsg( 'undelete-no-results' );
341
342 return false;
343 }
344
345 $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
346
348 $undelete = $this->getPageTitle();
349 $out->addHTML( "<ul id='undeleteResultsList'>\n" );
350 foreach ( $result as $row ) {
351 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
352 if ( $title !== null ) {
353 $item = $linkRenderer->makeKnownLink(
354 $undelete,
355 $title->getPrefixedText(),
356 [],
357 [ 'target' => $title->getPrefixedText() ]
358 );
359 } else {
360 // The title is no longer valid, show as text
361 $item = Html::element(
362 'span',
363 [ 'class' => 'mw-invalidtitle' ],
365 $this->getContext(),
366 $row->ar_namespace,
367 $row->ar_title
368 )
369 );
370 }
371 $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
372 $out->addHTML(
373 Html::rawElement(
374 'li',
375 [ 'class' => 'undeleteResult' ],
376 "{$item} ({$revs})"
377 )
378 );
379 }
380 $result->free();
381 $out->addHTML( "</ul>\n" );
382
383 return true;
384 }
385
386 private function showRevision( $timestamp ) {
387 if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
388 return;
389 }
390
391 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
392 if ( !Hooks::run( 'UndeleteForm::showRevision', [ &$archive, $this->mTargetObj ] ) ) {
393 return;
394 }
395 $rev = $archive->getRevision( $timestamp );
396
397 $out = $this->getOutput();
398 $user = $this->getUser();
399
400 if ( !$rev ) {
401 $out->addWikiMsg( 'undeleterevision-missing' );
402
403 return;
404 }
405
406 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
407 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
408 $out->wrapWikiMsg(
409 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
410 $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
411 'rev-suppressed-text-permission' : 'rev-deleted-text-permission'
412 );
413
414 return;
415 }
416
417 $out->wrapWikiMsg(
418 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
419 $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
420 'rev-suppressed-text-view' : 'rev-deleted-text-view'
421 );
422 $out->addHTML( '<br />' );
423 // and we are allowed to see...
424 }
425
426 if ( $this->mDiff ) {
427 $previousRev = $archive->getPreviousRevision( $timestamp );
428 if ( $previousRev ) {
429 $this->showDiff( $previousRev, $rev );
430 if ( $this->mDiffOnly ) {
431 return;
432 }
433
434 $out->addHTML( '<hr />' );
435 } else {
436 $out->addWikiMsg( 'undelete-nodiff' );
437 }
438 }
439
440 $link = $this->getLinkRenderer()->makeKnownLink(
441 $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
442 $this->mTargetObj->getPrefixedText()
443 );
444
445 $lang = $this->getLanguage();
446
447 // date and time are separate parameters to facilitate localisation.
448 // $time is kept for backward compat reasons.
449 $time = $lang->userTimeAndDate( $timestamp, $user );
450 $d = $lang->userDate( $timestamp, $user );
451 $t = $lang->userTime( $timestamp, $user );
452 $userLink = Linker::revUserTools( $rev );
453
454 $content = $rev->getContent( RevisionRecord::FOR_THIS_USER, $user );
455
456 // TODO: MCR: this will have to become something like $hasTextSlots and $hasNonTextSlots
457 $isText = ( $content instanceof TextContent );
458
459 if ( $this->mPreview || $isText ) {
460 $openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
461 } else {
462 $openDiv = '<div id="mw-undelete-revision">';
463 }
464 $out->addHTML( $openDiv );
465
466 // Revision delete links
467 if ( !$this->mDiff ) {
468 $revdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
469 if ( $revdel ) {
470 $out->addHTML( "$revdel " );
471 }
472 }
473
474 $out->addWikiMsg(
475 'undelete-revision',
476 Message::rawParam( $link ), $time,
477 Message::rawParam( $userLink ), $d, $t
478 );
479 $out->addHTML( '</div>' );
480
481 if ( !Hooks::run( 'UndeleteShowRevision', [ $this->mTargetObj, $rev ] ) ) {
482 return;
483 }
484
485 if ( $this->mPreview || !$isText ) {
486 // NOTE: non-text content has no source view, so always use rendered preview
487
488 $popts = $out->parserOptions();
489 $renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
490
491 $rendered = $renderer->getRenderedRevision(
492 $rev->getRevisionRecord(),
493 $popts,
494 $user,
495 [ 'audience' => RevisionRecord::FOR_THIS_USER ]
496 );
497
498 // Fail hard if the audience check fails, since we already checked
499 // at the beginning of this method.
500 $pout = $rendered->getRevisionParserOutput();
501
502 $out->addParserOutput( $pout, [
503 'enableSectionEditLinks' => false,
504 ] );
505 }
506
507 $out->enableOOUI();
508 $buttonFields = [];
509
510 if ( $isText ) {
511 '@phan-var TextContent $content';
512 // TODO: MCR: make this work for multiple slots
513 // source view for textual content
514 $sourceView = Xml::element( 'textarea', [
515 'readonly' => 'readonly',
516 'cols' => 80,
517 'rows' => 25
518 ], $content->getText() . "\n" );
519
520 $buttonFields[] = new OOUI\ButtonInputWidget( [
521 'type' => 'submit',
522 'name' => 'preview',
523 'label' => $this->msg( 'showpreview' )->text()
524 ] );
525 } else {
526 $sourceView = '';
527 }
528
529 $buttonFields[] = new OOUI\ButtonInputWidget( [
530 'name' => 'diff',
531 'type' => 'submit',
532 'label' => $this->msg( 'showdiff' )->text()
533 ] );
534
535 $out->addHTML(
536 $sourceView .
537 Xml::openElement( 'div', [
538 'style' => 'clear: both' ] ) .
539 Xml::openElement( 'form', [
540 'method' => 'post',
541 'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) .
542 Xml::element( 'input', [
543 'type' => 'hidden',
544 'name' => 'target',
545 'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
546 Xml::element( 'input', [
547 'type' => 'hidden',
548 'name' => 'timestamp',
549 'value' => $timestamp ] ) .
550 Xml::element( 'input', [
551 'type' => 'hidden',
552 'name' => 'wpEditToken',
553 'value' => $user->getEditToken() ] ) .
554 new OOUI\FieldLayout(
555 new OOUI\Widget( [
556 'content' => new OOUI\HorizontalLayout( [
557 'items' => $buttonFields
558 ] )
559 ] )
560 ) .
561 Xml::closeElement( 'form' ) .
562 Xml::closeElement( 'div' )
563 );
564 }
565
573 function showDiff( $previousRev, $currentRev ) {
574 $diffContext = clone $this->getContext();
575 $diffContext->setTitle( $currentRev->getTitle() );
576 $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
577
578 $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
579 $diffEngine->setRevisions( $previousRev->getRevisionRecord(), $currentRev->getRevisionRecord() );
580 $diffEngine->showDiffStyle();
581 $formattedDiff = $diffEngine->getDiff(
582 $this->diffHeader( $previousRev, 'o' ),
583 $this->diffHeader( $currentRev, 'n' )
584 );
585
586 $this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" );
587 }
588
594 private function diffHeader( $rev, $prefix ) {
595 $isDeleted = !( $rev->getId() && $rev->getTitle() );
596 if ( $isDeleted ) {
598 $targetPage = $this->getPageTitle();
599 $targetQuery = [
600 'target' => $this->mTargetObj->getPrefixedText(),
601 'timestamp' => wfTimestamp( TS_MW, $rev->getTimestamp() )
602 ];
603 } else {
605 $targetPage = $rev->getTitle();
606 $targetQuery = [ 'oldid' => $rev->getId() ];
607 }
608
609 // Add show/hide deletion links if available
610 $user = $this->getUser();
611 $lang = $this->getLanguage();
612 $rdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
613
614 if ( $rdel ) {
615 $rdel = " $rdel";
616 }
617
618 $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
619
620 $tagIds = wfGetDB( DB_REPLICA )->selectFieldValues(
621 'change_tag',
622 'ct_tag_id',
623 [ 'ct_rev_id' => $rev->getId() ],
624 __METHOD__
625 );
626 $tags = [];
627 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
628 foreach ( $tagIds as $tagId ) {
629 try {
630 $tags[] = $changeTagDefStore->getName( (int)$tagId );
631 } catch ( NameTableAccessException $exception ) {
632 continue;
633 }
634 }
635 $tags = implode( ',', $tags );
636 $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() );
637
638 // FIXME This is reimplementing DifferenceEngine#getRevisionHeader
639 // and partially #showDiffPage, but worse
640 return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
641 $this->getLinkRenderer()->makeLink(
642 $targetPage,
643 $this->msg(
644 'revisionasof',
645 $lang->userTimeAndDate( $rev->getTimestamp(), $user ),
646 $lang->userDate( $rev->getTimestamp(), $user ),
647 $lang->userTime( $rev->getTimestamp(), $user )
648 )->text(),
649 [],
650 $targetQuery
651 ) .
652 '</strong></div>' .
653 '<div id="mw-diff-' . $prefix . 'title2">' .
654 Linker::revUserTools( $rev ) . '<br />' .
655 '</div>' .
656 '<div id="mw-diff-' . $prefix . 'title3">' .
657 $minor . Linker::revComment( $rev ) . $rdel . '<br />' .
658 '</div>' .
659 '<div id="mw-diff-' . $prefix . 'title5">' .
660 $tagSummary[0] . '<br />' .
661 '</div>';
662 }
663
668 private function showFileConfirmationForm( $key ) {
669 $out = $this->getOutput();
670 $lang = $this->getLanguage();
671 $user = $this->getUser();
672 $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
673 $out->addWikiMsg( 'undelete-show-file-confirm',
674 $this->mTargetObj->getText(),
675 $lang->userDate( $file->getTimestamp(), $user ),
676 $lang->userTime( $file->getTimestamp(), $user ) );
677 $out->addHTML(
678 Xml::openElement( 'form', [
679 'method' => 'POST',
680 'action' => $this->getPageTitle()->getLocalURL( [
681 'target' => $this->mTarget,
682 'file' => $key,
683 'token' => $user->getEditToken( $key ),
684 ] ),
685 ]
686 ) .
687 Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) .
688 '</form>'
689 );
690 }
691
696 private function showFile( $key ) {
697 $this->getOutput()->disable();
698
699 # We mustn't allow the output to be CDN cached, otherwise
700 # if an admin previews a deleted image, and it's cached, then
701 # a user without appropriate permissions can toddle off and
702 # nab the image, and CDN will serve it
703 $response = $this->getRequest()->response();
704 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
705 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
706 $response->header( 'Pragma: no-cache' );
707
708 $repo = RepoGroup::singleton()->getLocalRepo();
709 $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
710 $repo->streamFileWithStatus( $path );
711 }
712
713 protected function showHistory() {
714 $this->checkReadOnly();
715
716 $out = $this->getOutput();
717 if ( $this->mAllowed ) {
718 $out->addModules( 'mediawiki.special.undelete' );
719 }
720 $out->wrapWikiMsg(
721 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
722 [ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
723 );
724
725 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
726 Hooks::run( 'UndeleteForm::showHistory', [ &$archive, $this->mTargetObj ] );
727
728 $out->addHTML( '<div class="mw-undelete-history">' );
729 if ( $this->mAllowed ) {
730 $out->addWikiMsg( 'undeletehistory' );
731 $out->addWikiMsg( 'undeleterevdel' );
732 } else {
733 $out->addWikiMsg( 'undeletehistorynoadmin' );
734 }
735 $out->addHTML( '</div>' );
736
737 # List all stored revisions
738 $revisions = $archive->listRevisions();
739 $files = $archive->listFiles();
740
741 $haveRevisions = $revisions && $revisions->numRows() > 0;
742 $haveFiles = $files && $files->numRows() > 0;
743
744 # Batch existence check on user and talk pages
745 if ( $haveRevisions ) {
746 $batch = new LinkBatch();
747 foreach ( $revisions as $row ) {
748 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
749 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
750 }
751 $batch->execute();
752 $revisions->seek( 0 );
753 }
754 if ( $haveFiles ) {
755 $batch = new LinkBatch();
756 foreach ( $files as $row ) {
757 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
758 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
759 }
760 $batch->execute();
761 $files->seek( 0 );
762 }
763
764 if ( $this->mAllowed ) {
765 $out->enableOOUI();
766
767 $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
768 # Start the form here
769 $form = new OOUI\FormLayout( [
770 'method' => 'post',
771 'action' => $action,
772 'id' => 'undelete',
773 ] );
774 }
775
776 # Show relevant lines from the deletion log:
777 $deleteLogPage = new LogPage( 'delete' );
778 $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
779 LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
780 # Show relevant lines from the suppression log:
781 $suppressLogPage = new LogPage( 'suppress' );
782 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
783 if ( $permissionManager->userHasRight( $this->getUser(), 'suppressionlog' ) ) {
784 $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
785 LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
786 }
787
788 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
789 $fields = [];
790 $fields[] = new OOUI\Layout( [
791 'content' => new OOUI\HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() )
792 ] );
793
794 $fields[] = new OOUI\FieldLayout(
795 new OOUI\TextInputWidget( [
796 'name' => 'wpComment',
797 'inputId' => 'wpComment',
798 'infusable' => true,
799 'value' => $this->mComment,
800 'autofocus' => true,
801 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
802 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
803 // Unicode codepoints.
804 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
805 ] ),
806 [
807 'label' => $this->msg( 'undeletecomment' )->text(),
808 'align' => 'top',
809 ]
810 );
811
812 $fields[] = new OOUI\FieldLayout(
813 new OOUI\Widget( [
814 'content' => new OOUI\HorizontalLayout( [
815 'items' => [
816 new OOUI\ButtonInputWidget( [
817 'name' => 'restore',
818 'inputId' => 'mw-undelete-submit',
819 'value' => '1',
820 'label' => $this->msg( 'undeletebtn' )->text(),
821 'flags' => [ 'primary', 'progressive' ],
822 'type' => 'submit',
823 ] ),
824 new OOUI\ButtonInputWidget( [
825 'name' => 'invert',
826 'inputId' => 'mw-undelete-invert',
827 'value' => '1',
828 'label' => $this->msg( 'undeleteinvert' )->text()
829 ] ),
830 ]
831 ] )
832 ] )
833 );
834
835 if ( $permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ) ) {
836 $fields[] = new OOUI\FieldLayout(
837 new OOUI\CheckboxInputWidget( [
838 'name' => 'wpUnsuppress',
839 'inputId' => 'mw-undelete-unsuppress',
840 'value' => '1',
841 ] ),
842 [
843 'label' => $this->msg( 'revdelete-unsuppress' )->text(),
844 'align' => 'inline',
845 ]
846 );
847 }
848
849 $fieldset = new OOUI\FieldsetLayout( [
850 'label' => $this->msg( 'undelete-fieldset-title' )->text(),
851 'id' => 'mw-undelete-table',
852 'items' => $fields,
853 ] );
854
855 $form->appendContent(
856 new OOUI\PanelLayout( [
857 'expanded' => false,
858 'padded' => true,
859 'framed' => true,
860 'content' => $fieldset,
861 ] ),
862 new OOUI\HtmlSnippet(
863 Html::hidden( 'target', $this->mTarget ) .
864 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() )
865 )
866 );
867 }
868
869 $history = '';
870 $history .= Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n";
871
872 if ( $haveRevisions ) {
873 # Show the page's stored (deleted) history
874
875 if ( $permissionManager->userHasRight( $this->getUser(), 'deleterevision' ) ) {
876 $history .= Html::element(
877 'button',
878 [
879 'name' => 'revdel',
880 'type' => 'submit',
881 'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
882 ],
883 $this->msg( 'showhideselectedversions' )->text()
884 ) . "\n";
885 }
886
887 $history .= '<ul class="mw-undelete-revlist">';
888 $remaining = $revisions->numRows();
889 $earliestLiveTime = $this->mTargetObj->getEarliestRevTime();
890
891 foreach ( $revisions as $row ) {
892 $remaining--;
893 $history .= $this->formatRevisionRow( $row, $earliestLiveTime, $remaining );
894 }
895 $revisions->free();
896 $history .= '</ul>';
897 } else {
898 $out->addWikiMsg( 'nohistory' );
899 }
900
901 if ( $haveFiles ) {
902 $history .= Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n";
903 $history .= '<ul class="mw-undelete-revlist">';
904 foreach ( $files as $row ) {
905 $history .= $this->formatFileRow( $row );
906 }
907 $files->free();
908 $history .= '</ul>';
909 }
910
911 if ( $this->mAllowed ) {
912 # Slip in the hidden controls here
913 $misc = Html::hidden( 'target', $this->mTarget );
914 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
915 $history .= $misc;
916
917 $form->appendContent( new OOUI\HtmlSnippet( $history ) );
918 $out->addHTML( $form );
919 } else {
920 $out->addHTML( $history );
921 }
922
923 return true;
924 }
925
926 protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
927 $rev = Revision::newFromArchiveRow( $row,
928 [
929 'title' => $this->mTargetObj
930 ] );
931
932 $revTextSize = '';
933 $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
934 // Build checkboxen...
935 if ( $this->mAllowed ) {
936 if ( $this->mInvert ) {
937 if ( in_array( $ts, $this->mTargetTimestamp ) ) {
938 $checkBox = Xml::check( "ts$ts" );
939 } else {
940 $checkBox = Xml::check( "ts$ts", true );
941 }
942 } else {
943 $checkBox = Xml::check( "ts$ts" );
944 }
945 } else {
946 $checkBox = '';
947 }
948
949 // Build page & diff links...
950 $user = $this->getUser();
951 if ( $this->mCanView ) {
952 $titleObj = $this->getPageTitle();
953 # Last link
954 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() ) ) {
955 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
956 $last = $this->msg( 'diff' )->escaped();
957 } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
958 $pageLink = $this->getPageLink( $rev, $titleObj, $ts );
959 $last = $this->getLinkRenderer()->makeKnownLink(
960 $titleObj,
961 $this->msg( 'diff' )->text(),
962 [],
963 [
964 'target' => $this->mTargetObj->getPrefixedText(),
965 'timestamp' => $ts,
966 'diff' => 'prev'
967 ]
968 );
969 } else {
970 $pageLink = $this->getPageLink( $rev, $titleObj, $ts );
971 $last = $this->msg( 'diff' )->escaped();
972 }
973 } else {
974 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
975 $last = $this->msg( 'diff' )->escaped();
976 }
977
978 // User links
979 $userLink = Linker::revUserTools( $rev );
980
981 // Minor edit
982 $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
983
984 // Revision text size
985 $size = $row->ar_len;
986 if ( !is_null( $size ) ) {
987 $revTextSize = Linker::formatRevisionSize( $size );
988 }
989
990 // Edit summary
991 $comment = Linker::revComment( $rev );
992
993 // Tags
994 $attribs = [];
995 list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow(
996 $row->ts_tags,
997 'deletedhistory',
998 $this->getContext()
999 );
1000 if ( $classes ) {
1001 $attribs['class'] = implode( ' ', $classes );
1002 }
1003
1004 $revisionRow = $this->msg( 'undelete-revision-row2' )
1005 ->rawParams(
1006 $checkBox,
1007 $last,
1008 $pageLink,
1009 $userLink,
1010 $minor,
1011 $revTextSize,
1012 $comment,
1013 $tagSummary
1014 )
1015 ->escaped();
1016
1017 return Xml::tags( 'li', $attribs, $revisionRow ) . "\n";
1018 }
1019
1020 private function formatFileRow( $row ) {
1022 $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
1023 $user = $this->getUser();
1024
1025 $checkBox = '';
1026 if ( $this->mCanView && $row->fa_storage_key ) {
1027 if ( $this->mAllowed ) {
1028 $checkBox = Xml::check( 'fileid' . $row->fa_id );
1029 }
1030 $key = urlencode( $row->fa_storage_key );
1031 $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
1032 } else {
1033 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1034 }
1035 $userLink = $this->getFileUser( $file );
1036 $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1037 $bytes = $this->msg( 'parentheses' )
1038 ->plaintextParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
1039 ->plain();
1040 $data = htmlspecialchars( $data . ' ' . $bytes );
1041 $comment = $this->getFileComment( $file );
1042
1043 // Add show/hide deletion links if available
1044 $canHide = $this->isAllowed( 'deleterevision' );
1045 if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) {
1046 if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1047 // Revision was hidden from sysops
1048 $revdlink = Linker::revDeleteLinkDisabled( $canHide );
1049 } else {
1050 $query = [
1051 'type' => 'filearchive',
1052 'target' => $this->mTargetObj->getPrefixedDBkey(),
1053 'ids' => $row->fa_id
1054 ];
1055 $revdlink = Linker::revDeleteLink( $query,
1056 $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1057 }
1058 } else {
1059 $revdlink = '';
1060 }
1061
1062 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1063 }
1064
1073 function getPageLink( $rev, $titleObj, $ts ) {
1074 $user = $this->getUser();
1075 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1076
1077 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
1078 return '<span class="history-deleted">' . $time . '</span>';
1079 }
1080
1081 $link = $this->getLinkRenderer()->makeKnownLink(
1082 $titleObj,
1083 $time,
1084 [],
1085 [
1086 'target' => $this->mTargetObj->getPrefixedText(),
1087 'timestamp' => $ts
1088 ]
1089 );
1090
1091 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1092 $link = '<span class="history-deleted">' . $link . '</span>';
1093 }
1094
1095 return $link;
1096 }
1097
1108 function getFileLink( $file, $titleObj, $ts, $key ) {
1109 $user = $this->getUser();
1110 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1111
1112 if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1113 return '<span class="history-deleted">' . htmlspecialchars( $time ) . '</span>';
1114 }
1115
1116 $link = $this->getLinkRenderer()->makeKnownLink(
1117 $titleObj,
1118 $time,
1119 [],
1120 [
1121 'target' => $this->mTargetObj->getPrefixedText(),
1122 'file' => $key,
1123 'token' => $user->getEditToken( $key )
1124 ]
1125 );
1126
1127 if ( $file->isDeleted( File::DELETED_FILE ) ) {
1128 $link = '<span class="history-deleted">' . $link . '</span>';
1129 }
1130
1131 return $link;
1132 }
1133
1140 function getFileUser( $file ) {
1141 if ( !$file->userCan( File::DELETED_USER, $this->getUser() ) ) {
1142 return '<span class="history-deleted">' .
1143 $this->msg( 'rev-deleted-user' )->escaped() .
1144 '</span>';
1145 }
1146
1147 $link = Linker::userLink( $file->getRawUser(), $file->getRawUserText() ) .
1148 Linker::userToolLinks( $file->getRawUser(), $file->getRawUserText() );
1149
1150 if ( $file->isDeleted( File::DELETED_USER ) ) {
1151 $link = '<span class="history-deleted">' . $link . '</span>';
1152 }
1153
1154 return $link;
1155 }
1156
1163 function getFileComment( $file ) {
1164 if ( !$file->userCan( File::DELETED_COMMENT, $this->getUser() ) ) {
1165 return '<span class="history-deleted"><span class="comment">' .
1166 $this->msg( 'rev-deleted-comment' )->escaped() . '</span></span>';
1167 }
1168
1169 $link = Linker::commentBlock( $file->getRawDescription() );
1170
1171 if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1172 $link = '<span class="history-deleted">' . $link . '</span>';
1173 }
1174
1175 return $link;
1176 }
1177
1178 function undelete() {
1179 if ( $this->getConfig()->get( 'UploadMaintenance' )
1180 && $this->mTargetObj->getNamespace() == NS_FILE
1181 ) {
1182 throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
1183 }
1184
1185 $this->checkReadOnly();
1186
1187 $out = $this->getOutput();
1188 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1189 Hooks::run( 'UndeleteForm::undelete', [ &$archive, $this->mTargetObj ] );
1190 $ok = $archive->undelete(
1191 $this->mTargetTimestamp,
1192 $this->mComment,
1193 $this->mFileVersions,
1194 $this->mUnsuppress,
1195 $this->getUser()
1196 );
1197
1198 if ( is_array( $ok ) ) {
1199 if ( $ok[1] ) { // Undeleted file count
1200 Hooks::run( 'FileUndeleteComplete', [
1201 $this->mTargetObj, $this->mFileVersions,
1202 $this->getUser(), $this->mComment ] );
1203 }
1204
1205 $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj );
1206 $out->addWikiMsg( 'undeletedpage', Message::rawParam( $link ) );
1207 } else {
1208 $out->setPageTitle( $this->msg( 'undelete-error' ) );
1209 }
1210
1211 // Show revision undeletion warnings and errors
1212 $status = $archive->getRevisionStatus();
1213 if ( $status && !$status->isGood() ) {
1214 $out->wrapWikiTextAsInterface(
1215 'error',
1216 '<div id="mw-error-cannotundelete">' .
1217 $status->getWikiText(
1218 'cannotundelete',
1219 'cannotundelete',
1220 $this->getLanguage()
1221 ) . '</div>'
1222 );
1223 }
1224
1225 // Show file undeletion warnings and errors
1226 $status = $archive->getFileStatus();
1227 if ( $status && !$status->isGood() ) {
1228 $out->wrapWikiTextAsInterface(
1229 'error',
1230 $status->getWikiText(
1231 'undelete-error-short',
1232 'undelete-error-long',
1233 $this->getLanguage()
1234 )
1235 );
1236 }
1237 }
1238
1247 public function prefixSearchSubpages( $search, $limit, $offset ) {
1248 return $this->prefixSearchString( $search, $limit, $offset );
1249 }
1250
1251 protected function getGroupName() {
1252 return 'pagetools';
1253 }
1254}
getPermissionManager()
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Class representing a row of the 'filearchive' table.
static newFromRow( $row)
Loads a file object from the filearchive table.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
An error page which can definitely be safely rendered using the OutputPage.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:34
static userLink( $userId, $userName, $altUserName=false)
Make user link (or user contributions for unregistered users)
Definition Linker.php:898
static getRevDeleteLink(User $user, Revision $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition Linker.php:2110
static revComment(Revision $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1577
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2176
static getInvalidTitleDescription(IContextSource $context, $namespace, $title)
Get a message saying that an invalid title was encountered.
Definition Linker.php:187
static formatRevisionSize( $size)
Definition Linker.php:1602
static commentBlock( $comment, $title=null, $local=false, $wikiId=null, $useParentheses=true)
Wrap a comment in standard punctuation and formatting if it's non-empty, otherwise return empty strin...
Definition Linker.php:1547
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1124
static userToolLinks( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null, $useParentheses=true)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:943
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2154
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition LogPage.php:33
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
Exception representing a failure to look up a row from a name table.
static rawParam( $raw)
Definition Message.php:1027
Used to show archived pages and eventually restore them.
static listPagesBySearch( $term)
List deleted pages recorded in the archive matching the given term, using search engine archive.
static listPagesByPrefix( $prefix)
List deleted pages recorded in the archive table matching the given title prefix.
Show an error when a user tries to do something they do not have the necessary permissions for.
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
getSkin()
Shortcut to get the skin being used for this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
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,...
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getPageTitle( $subpage=false)
Get a self-referential title object.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getLanguage()
Shortcut to get user's language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
prefixSearchString( $search, $limit, $offset)
Perform a regular substring search for prefixSearchSubpages.
MediaWiki Linker LinkRenderer null $linkRenderer
Special page allowing users with the appropriate permissions to view and restore deleted content.
redirectToRevDel()
Convert submitted form data to format expected by RevisionDelete and redirect the request.
showList( $result)
Generic list of deleted pages.
getFileComment( $file)
Fetch file upload comment if it's available to this user.
int[] null $mFileVersions
diffHeader( $rev, $prefix)
showFileConfirmationForm( $key)
Show a form confirming whether a tokenless user really wants to see a file.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
showFile( $key)
Show a deleted file version requested by the visitor.
getPageLink( $rev, $titleObj, $ts)
Fetch revision text link if it's available to all users.
getFileUser( $file)
Fetch file's user id if it's available to this user.
string $mSearchPrefix
Search prefix.
execute( $par)
Default execute method Checks user permissions.
showRevision( $timestamp)
getFileLink( $file, $titleObj, $ts, $key)
Fetch image view link if it's available to all users.
doesWrites()
Indicates whether this special page may perform database writes.
isAllowed( $permission, User $user=null)
Checks whether a user is allowed the permission for the specific title if one is set.
userCanExecute(User $user)
Checks if the given user (identified by an object) can execute this special page (as defined by $mRes...
formatRevisionRow( $row, $earliestLiveTime, $remaining)
showDiff( $previousRev, $currentRev)
Build a diff display between this and the previous either deleted or non-deleted edit.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Content object implementation for representing flat text.
Represents a title within MediaWiki.
Definition Title.php:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
const NS_USER
Definition Defines.php:71
const NS_FILE
Definition Defines.php:75
const NS_USER_TALK
Definition Defines.php:72
Result wrapper for grabbing data queried from an IDatabase object.
$last
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:78
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!isset( $args[0])) $lang