]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/specials/SpecialUndelete.php
MediaWiki 1.30.2 renames
[autoinstallsdev/mediawiki.git] / includes / specials / SpecialUndelete.php
1 <?php
2 /**
3  * Implements Special:Undelete
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup SpecialPage
22  */
23
24 use Wikimedia\Rdbms\ResultWrapper;
25
26 /**
27  * Special page allowing users with the appropriate permissions to view
28  * and restore deleted content.
29  *
30  * @ingroup SpecialPage
31  */
32 class SpecialUndelete extends SpecialPage {
33         private $mAction;
34         private $mTarget;
35         private $mTimestamp;
36         private $mRestore;
37         private $mRevdel;
38         private $mInvert;
39         private $mFilename;
40         private $mTargetTimestamp;
41         private $mAllowed;
42         private $mCanView;
43         private $mComment;
44         private $mToken;
45
46         /** @var Title */
47         private $mTargetObj;
48         /**
49          * @var string Search prefix
50          */
51         private $mSearchPrefix;
52
53         function __construct() {
54                 parent::__construct( 'Undelete', 'deletedhistory' );
55         }
56
57         public function doesWrites() {
58                 return true;
59         }
60
61         function loadRequest( $par ) {
62                 $request = $this->getRequest();
63                 $user = $this->getUser();
64
65                 $this->mAction = $request->getVal( 'action' );
66                 if ( $par !== null && $par !== '' ) {
67                         $this->mTarget = $par;
68                 } else {
69                         $this->mTarget = $request->getVal( 'target' );
70                 }
71
72                 $this->mTargetObj = null;
73
74                 if ( $this->mTarget !== null && $this->mTarget !== '' ) {
75                         $this->mTargetObj = Title::newFromText( $this->mTarget );
76                 }
77
78                 $this->mSearchPrefix = $request->getText( 'prefix' );
79                 $time = $request->getVal( 'timestamp' );
80                 $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
81                 $this->mFilename = $request->getVal( 'file' );
82
83                 $posted = $request->wasPosted() &&
84                         $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
85                 $this->mRestore = $request->getCheck( 'restore' ) && $posted;
86                 $this->mRevdel = $request->getCheck( 'revdel' ) && $posted;
87                 $this->mInvert = $request->getCheck( 'invert' ) && $posted;
88                 $this->mPreview = $request->getCheck( 'preview' ) && $posted;
89                 $this->mDiff = $request->getCheck( 'diff' );
90                 $this->mDiffOnly = $request->getBool( 'diffonly', $this->getUser()->getOption( 'diffonly' ) );
91                 $this->mComment = $request->getText( 'wpComment' );
92                 $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $user->isAllowed( 'suppressrevision' );
93                 $this->mToken = $request->getVal( 'token' );
94
95                 if ( $this->isAllowed( 'undelete' ) && !$user->isBlocked() ) {
96                         $this->mAllowed = true; // user can restore
97                         $this->mCanView = true; // user can view content
98                 } elseif ( $this->isAllowed( 'deletedtext' ) ) {
99                         $this->mAllowed = false; // user cannot restore
100                         $this->mCanView = true; // user can view content
101                         $this->mRestore = false;
102                 } else { // user can only view the list of revisions
103                         $this->mAllowed = false;
104                         $this->mCanView = false;
105                         $this->mTimestamp = '';
106                         $this->mRestore = false;
107                 }
108
109                 if ( $this->mRestore || $this->mInvert ) {
110                         $timestamps = [];
111                         $this->mFileVersions = [];
112                         foreach ( $request->getValues() as $key => $val ) {
113                                 $matches = [];
114                                 if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
115                                         array_push( $timestamps, $matches[1] );
116                                 }
117
118                                 if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
119                                         $this->mFileVersions[] = intval( $matches[1] );
120                                 }
121                         }
122                         rsort( $timestamps );
123                         $this->mTargetTimestamp = $timestamps;
124                 }
125         }
126
127         /**
128          * Checks whether a user is allowed the permission for the
129          * specific title if one is set.
130          *
131          * @param string $permission
132          * @param User $user
133          * @return bool
134          */
135         protected function isAllowed( $permission, User $user = null ) {
136                 $user = $user ?: $this->getUser();
137                 if ( $this->mTargetObj !== null ) {
138                         return $this->mTargetObj->userCan( $permission, $user );
139                 } else {
140                         return $user->isAllowed( $permission );
141                 }
142         }
143
144         function userCanExecute( User $user ) {
145                 return $this->isAllowed( $this->mRestriction, $user );
146         }
147
148         function execute( $par ) {
149                 $this->useTransactionalTimeLimit();
150
151                 $user = $this->getUser();
152
153                 $this->setHeaders();
154                 $this->outputHeader();
155
156                 $this->loadRequest( $par );
157                 $this->checkPermissions(); // Needs to be after mTargetObj is set
158
159                 $out = $this->getOutput();
160
161                 if ( is_null( $this->mTargetObj ) ) {
162                         $out->addWikiMsg( 'undelete-header' );
163
164                         # Not all users can just browse every deleted page from the list
165                         if ( $user->isAllowed( 'browsearchive' ) ) {
166                                 $this->showSearchForm();
167                         }
168
169                         return;
170                 }
171
172                 $this->addHelpLink( 'Help:Undelete' );
173                 if ( $this->mAllowed ) {
174                         $out->setPageTitle( $this->msg( 'undeletepage' ) );
175                 } else {
176                         $out->setPageTitle( $this->msg( 'viewdeletedpage' ) );
177                 }
178
179                 $this->getSkin()->setRelevantTitle( $this->mTargetObj );
180
181                 if ( $this->mTimestamp !== '' ) {
182                         $this->showRevision( $this->mTimestamp );
183                 } elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) {
184                         $file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
185                         // Check if user is allowed to see this file
186                         if ( !$file->exists() ) {
187                                 $out->addWikiMsg( 'filedelete-nofile', $this->mFilename );
188                         } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
189                                 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
190                                         throw new PermissionsError( 'suppressrevision' );
191                                 } else {
192                                         throw new PermissionsError( 'deletedtext' );
193                                 }
194                         } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
195                                 $this->showFileConfirmationForm( $this->mFilename );
196                         } else {
197                                 $this->showFile( $this->mFilename );
198                         }
199                 } elseif ( $this->mAction === "submit" ) {
200                         if ( $this->mRestore ) {
201                                 $this->undelete();
202                         } elseif ( $this->mRevdel ) {
203                                 $this->redirectToRevDel();
204                         }
205
206                 } else {
207                         $this->showHistory();
208                 }
209         }
210
211         /**
212          * Convert submitted form data to format expected by RevisionDelete and
213          * redirect the request
214          */
215         private function redirectToRevDel() {
216                 $archive = new PageArchive( $this->mTargetObj );
217
218                 $revisions = [];
219
220                 foreach ( $this->getRequest()->getValues() as $key => $val ) {
221                         $matches = [];
222                         if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
223                                 $revisions[ $archive->getRevision( $matches[1] )->getId() ] = 1;
224                         }
225                 }
226                 $query = [
227                         "type" => "revision",
228                         "ids" => $revisions,
229                         "target" => $this->mTargetObj->getPrefixedText()
230                 ];
231                 $url = SpecialPage::getTitleFor( 'Revisiondelete' )->getFullURL( $query );
232                 $this->getOutput()->redirect( $url );
233         }
234
235         function showSearchForm() {
236                 $out = $this->getOutput();
237                 $out->setPageTitle( $this->msg( 'undelete-search-title' ) );
238                 $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', true );
239
240                 $out->enableOOUI();
241
242                 $fields[] = new OOUI\ActionFieldLayout(
243                         new OOUI\TextInputWidget( [
244                                 'name' => 'prefix',
245                                 'inputId' => 'prefix',
246                                 'infusable' => true,
247                                 'value' => $this->mSearchPrefix,
248                                 'autofocus' => true,
249                         ] ),
250                         new OOUI\ButtonInputWidget( [
251                                 'label' => $this->msg( 'undelete-search-submit' )->text(),
252                                 'flags' => [ 'primary', 'progressive' ],
253                                 'inputId' => 'searchUndelete',
254                                 'type' => 'submit',
255                         ] ),
256                         [
257                                 'label' => new OOUI\HtmlSnippet(
258                                         $this->msg(
259                                                 $fuzzySearch ? 'undelete-search-full' : 'undelete-search-prefix'
260                                         )->parse()
261                                 ),
262                                 'align' => 'left',
263                         ]
264                 );
265
266                 $fieldset = new OOUI\FieldsetLayout( [
267                         'label' => $this->msg( 'undelete-search-box' )->text(),
268                         'items' => $fields,
269                 ] );
270
271                 $form = new OOUI\FormLayout( [
272                         'method' => 'get',
273                         'action' => wfScript(),
274                 ] );
275
276                 $form->appendContent(
277                         $fieldset,
278                         new OOUI\HtmlSnippet(
279                                 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
280                                 Html::hidden( 'fuzzy', $fuzzySearch )
281                         )
282                 );
283
284                 $out->addHTML(
285                         new OOUI\PanelLayout( [
286                                 'expanded' => false,
287                                 'padded' => true,
288                                 'framed' => true,
289                                 'content' => $form,
290                         ] )
291                 );
292
293                 # List undeletable articles
294                 if ( $this->mSearchPrefix ) {
295                         // For now, we enable search engine match only when specifically asked to
296                         // by using fuzzy=1 parameter.
297                         if ( $fuzzySearch ) {
298                                 $result = PageArchive::listPagesBySearch( $this->mSearchPrefix );
299                         } else {
300                                 $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
301                         }
302                         $this->showList( $result );
303                 }
304         }
305
306         /**
307          * Generic list of deleted pages
308          *
309          * @param ResultWrapper $result
310          * @return bool
311          */
312         private function showList( $result ) {
313                 $out = $this->getOutput();
314
315                 if ( $result->numRows() == 0 ) {
316                         $out->addWikiMsg( 'undelete-no-results' );
317
318                         return false;
319                 }
320
321                 $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
322
323                 $linkRenderer = $this->getLinkRenderer();
324                 $undelete = $this->getPageTitle();
325                 $out->addHTML( "<ul id='undeleteResultsList'>\n" );
326                 foreach ( $result as $row ) {
327                         $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
328                         if ( $title !== null ) {
329                                 $item = $linkRenderer->makeKnownLink(
330                                         $undelete,
331                                         $title->getPrefixedText(),
332                                         [],
333                                         [ 'target' => $title->getPrefixedText() ]
334                                 );
335                         } else {
336                                 // The title is no longer valid, show as text
337                                 $item = Html::element(
338                                         'span',
339                                         [ 'class' => 'mw-invalidtitle' ],
340                                         Linker::getInvalidTitleDescription(
341                                                 $this->getContext(),
342                                                 $row->ar_namespace,
343                                                 $row->ar_title
344                                         )
345                                 );
346                         }
347                         $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
348                         $out->addHTML( "<li class='undeleteResult'>{$item} ({$revs})</li>\n" );
349                 }
350                 $result->free();
351                 $out->addHTML( "</ul>\n" );
352
353                 return true;
354         }
355
356         private function showRevision( $timestamp ) {
357                 if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
358                         return;
359                 }
360
361                 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
362                 if ( !Hooks::run( 'UndeleteForm::showRevision', [ &$archive, $this->mTargetObj ] ) ) {
363                         return;
364                 }
365                 $rev = $archive->getRevision( $timestamp );
366
367                 $out = $this->getOutput();
368                 $user = $this->getUser();
369
370                 if ( !$rev ) {
371                         $out->addWikiMsg( 'undeleterevision-missing' );
372
373                         return;
374                 }
375
376                 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
377                         if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
378                                 $out->wrapWikiMsg(
379                                         "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
380                                 $rev->isDeleted( Revision::DELETED_RESTRICTED ) ?
381                                         'rev-suppressed-text-permission' : 'rev-deleted-text-permission'
382                                 );
383
384                                 return;
385                         }
386
387                         $out->wrapWikiMsg(
388                                 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
389                                 $rev->isDeleted( Revision::DELETED_RESTRICTED ) ?
390                                         'rev-suppressed-text-view' : 'rev-deleted-text-view'
391                         );
392                         $out->addHTML( '<br />' );
393                         // and we are allowed to see...
394                 }
395
396                 if ( $this->mDiff ) {
397                         $previousRev = $archive->getPreviousRevision( $timestamp );
398                         if ( $previousRev ) {
399                                 $this->showDiff( $previousRev, $rev );
400                                 if ( $this->mDiffOnly ) {
401                                         return;
402                                 }
403
404                                 $out->addHTML( '<hr />' );
405                         } else {
406                                 $out->addWikiMsg( 'undelete-nodiff' );
407                         }
408                 }
409
410                 $link = $this->getLinkRenderer()->makeKnownLink(
411                         $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
412                         $this->mTargetObj->getPrefixedText()
413                 );
414
415                 $lang = $this->getLanguage();
416
417                 // date and time are separate parameters to facilitate localisation.
418                 // $time is kept for backward compat reasons.
419                 $time = $lang->userTimeAndDate( $timestamp, $user );
420                 $d = $lang->userDate( $timestamp, $user );
421                 $t = $lang->userTime( $timestamp, $user );
422                 $userLink = Linker::revUserTools( $rev );
423
424                 $content = $rev->getContent( Revision::FOR_THIS_USER, $user );
425
426                 $isText = ( $content instanceof TextContent );
427
428                 if ( $this->mPreview || $isText ) {
429                         $openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
430                 } else {
431                         $openDiv = '<div id="mw-undelete-revision">';
432                 }
433                 $out->addHTML( $openDiv );
434
435                 // Revision delete links
436                 if ( !$this->mDiff ) {
437                         $revdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
438                         if ( $revdel ) {
439                                 $out->addHTML( "$revdel " );
440                         }
441                 }
442
443                 $out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params(
444                         $time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' );
445
446                 if ( !Hooks::run( 'UndeleteShowRevision', [ $this->mTargetObj, $rev ] ) ) {
447                         return;
448                 }
449
450                 if ( ( $this->mPreview || !$isText ) && $content ) {
451                         // NOTE: non-text content has no source view, so always use rendered preview
452
453                         // Hide [edit]s
454                         $popts = $out->parserOptions();
455                         $popts->setEditSection( false );
456
457                         $pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
458                         $out->addParserOutput( $pout );
459                 }
460
461                 if ( $isText ) {
462                         // source view for textual content
463                         $sourceView = Xml::element(
464                                 'textarea',
465                                 [
466                                         'readonly' => 'readonly',
467                                         'cols' => 80,
468                                         'rows' => 25
469                                 ],
470                                 $content->getNativeData() . "\n"
471                         );
472
473                         $previewButton = Xml::element( 'input', [
474                                 'type' => 'submit',
475                                 'name' => 'preview',
476                                 'value' => $this->msg( 'showpreview' )->text()
477                         ] );
478                 } else {
479                         $sourceView = '';
480                         $previewButton = '';
481                 }
482
483                 $diffButton = Xml::element( 'input', [
484                         'name' => 'diff',
485                         'type' => 'submit',
486                         'value' => $this->msg( 'showdiff' )->text() ] );
487
488                 $out->addHTML(
489                         $sourceView .
490                                 Xml::openElement( 'div', [
491                                         'style' => 'clear: both' ] ) .
492                                 Xml::openElement( 'form', [
493                                         'method' => 'post',
494                                         'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) .
495                                 Xml::element( 'input', [
496                                         'type' => 'hidden',
497                                         'name' => 'target',
498                                         'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
499                                 Xml::element( 'input', [
500                                         'type' => 'hidden',
501                                         'name' => 'timestamp',
502                                         'value' => $timestamp ] ) .
503                                 Xml::element( 'input', [
504                                         'type' => 'hidden',
505                                         'name' => 'wpEditToken',
506                                         'value' => $user->getEditToken() ] ) .
507                                 $previewButton .
508                                 $diffButton .
509                                 Xml::closeElement( 'form' ) .
510                                 Xml::closeElement( 'div' )
511                 );
512         }
513
514         /**
515          * Build a diff display between this and the previous either deleted
516          * or non-deleted edit.
517          *
518          * @param Revision $previousRev
519          * @param Revision $currentRev
520          * @return string HTML
521          */
522         function showDiff( $previousRev, $currentRev ) {
523                 $diffContext = clone $this->getContext();
524                 $diffContext->setTitle( $currentRev->getTitle() );
525                 $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
526
527                 $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
528                 $diffEngine->showDiffStyle();
529
530                 $formattedDiff = $diffEngine->generateContentDiffBody(
531                         $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
532                         $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
533                 );
534
535                 $formattedDiff = $diffEngine->addHeader(
536                         $formattedDiff,
537                         $this->diffHeader( $previousRev, 'o' ),
538                         $this->diffHeader( $currentRev, 'n' )
539                 );
540
541                 $this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" );
542         }
543
544         /**
545          * @param Revision $rev
546          * @param string $prefix
547          * @return string
548          */
549         private function diffHeader( $rev, $prefix ) {
550                 $isDeleted = !( $rev->getId() && $rev->getTitle() );
551                 if ( $isDeleted ) {
552                         /// @todo FIXME: $rev->getTitle() is null for deleted revs...?
553                         $targetPage = $this->getPageTitle();
554                         $targetQuery = [
555                                 'target' => $this->mTargetObj->getPrefixedText(),
556                                 'timestamp' => wfTimestamp( TS_MW, $rev->getTimestamp() )
557                         ];
558                 } else {
559                         /// @todo FIXME: getId() may return non-zero for deleted revs...
560                         $targetPage = $rev->getTitle();
561                         $targetQuery = [ 'oldid' => $rev->getId() ];
562                 }
563
564                 // Add show/hide deletion links if available
565                 $user = $this->getUser();
566                 $lang = $this->getLanguage();
567                 $rdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
568
569                 if ( $rdel ) {
570                         $rdel = " $rdel";
571                 }
572
573                 $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
574
575                 $tags = wfGetDB( DB_REPLICA )->selectField(
576                         'tag_summary',
577                         'ts_tags',
578                         [ 'ts_rev_id' => $rev->getId() ],
579                         __METHOD__
580                 );
581                 $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() );
582
583                 // FIXME This is reimplementing DifferenceEngine#getRevisionHeader
584                 // and partially #showDiffPage, but worse
585                 return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
586                         $this->getLinkRenderer()->makeLink(
587                                 $targetPage,
588                                 $this->msg(
589                                         'revisionasof',
590                                         $lang->userTimeAndDate( $rev->getTimestamp(), $user ),
591                                         $lang->userDate( $rev->getTimestamp(), $user ),
592                                         $lang->userTime( $rev->getTimestamp(), $user )
593                                 )->text(),
594                                 [],
595                                 $targetQuery
596                         ) .
597                         '</strong></div>' .
598                         '<div id="mw-diff-' . $prefix . 'title2">' .
599                         Linker::revUserTools( $rev ) . '<br />' .
600                         '</div>' .
601                         '<div id="mw-diff-' . $prefix . 'title3">' .
602                         $minor . Linker::revComment( $rev ) . $rdel . '<br />' .
603                         '</div>' .
604                         '<div id="mw-diff-' . $prefix . 'title5">' .
605                         $tagSummary[0] . '<br />' .
606                         '</div>';
607         }
608
609         /**
610          * Show a form confirming whether a tokenless user really wants to see a file
611          * @param string $key
612          */
613         private function showFileConfirmationForm( $key ) {
614                 $out = $this->getOutput();
615                 $lang = $this->getLanguage();
616                 $user = $this->getUser();
617                 $file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
618                 $out->addWikiMsg( 'undelete-show-file-confirm',
619                         $this->mTargetObj->getText(),
620                         $lang->userDate( $file->getTimestamp(), $user ),
621                         $lang->userTime( $file->getTimestamp(), $user ) );
622                 $out->addHTML(
623                         Xml::openElement( 'form', [
624                                         'method' => 'POST',
625                                         'action' => $this->getPageTitle()->getLocalURL( [
626                                                 'target' => $this->mTarget,
627                                                 'file' => $key,
628                                                 'token' => $user->getEditToken( $key ),
629                                         ] ),
630                                 ]
631                         ) .
632                                 Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) .
633                                 '</form>'
634                 );
635         }
636
637         /**
638          * Show a deleted file version requested by the visitor.
639          * @param string $key
640          */
641         private function showFile( $key ) {
642                 $this->getOutput()->disable();
643
644                 # We mustn't allow the output to be CDN cached, otherwise
645                 # if an admin previews a deleted image, and it's cached, then
646                 # a user without appropriate permissions can toddle off and
647                 # nab the image, and CDN will serve it
648                 $response = $this->getRequest()->response();
649                 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
650                 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
651                 $response->header( 'Pragma: no-cache' );
652
653                 $repo = RepoGroup::singleton()->getLocalRepo();
654                 $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
655                 $repo->streamFile( $path );
656         }
657
658         protected function showHistory() {
659                 $this->checkReadOnly();
660
661                 $out = $this->getOutput();
662                 if ( $this->mAllowed ) {
663                         $out->addModules( 'mediawiki.special.undelete' );
664                 }
665                 $out->wrapWikiMsg(
666                         "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
667                         [ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
668                 );
669
670                 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
671                 Hooks::run( 'UndeleteForm::showHistory', [ &$archive, $this->mTargetObj ] );
672
673                 $out->addHTML( '<div class="mw-undelete-history">' );
674                 if ( $this->mAllowed ) {
675                         $out->addWikiMsg( 'undeletehistory' );
676                         $out->addWikiMsg( 'undeleterevdel' );
677                 } else {
678                         $out->addWikiMsg( 'undeletehistorynoadmin' );
679                 }
680                 $out->addHTML( '</div>' );
681
682                 # List all stored revisions
683                 $revisions = $archive->listRevisions();
684                 $files = $archive->listFiles();
685
686                 $haveRevisions = $revisions && $revisions->numRows() > 0;
687                 $haveFiles = $files && $files->numRows() > 0;
688
689                 # Batch existence check on user and talk pages
690                 if ( $haveRevisions ) {
691                         $batch = new LinkBatch();
692                         foreach ( $revisions as $row ) {
693                                 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
694                                 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
695                         }
696                         $batch->execute();
697                         $revisions->seek( 0 );
698                 }
699                 if ( $haveFiles ) {
700                         $batch = new LinkBatch();
701                         foreach ( $files as $row ) {
702                                 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
703                                 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
704                         }
705                         $batch->execute();
706                         $files->seek( 0 );
707                 }
708
709                 if ( $this->mAllowed ) {
710                         $out->enableOOUI();
711
712                         $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
713                         # Start the form here
714                         $form = new OOUI\FormLayout( [
715                                 'method' => 'post',
716                                 'action' => $action,
717                                 'id' => 'undelete',
718                         ] );
719                 }
720
721                 # Show relevant lines from the deletion log:
722                 $deleteLogPage = new LogPage( 'delete' );
723                 $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
724                 LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
725                 # Show relevant lines from the suppression log:
726                 $suppressLogPage = new LogPage( 'suppress' );
727                 if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
728                         $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
729                         LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
730                 }
731
732                 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
733                         $fields[] = new OOUI\Layout( [
734                                 'content' => new OOUI\HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() )
735                         ] );
736
737                         $fields[] = new OOUI\FieldLayout(
738                                 new OOUI\TextInputWidget( [
739                                         'name' => 'wpComment',
740                                         'inputId' => 'wpComment',
741                                         'infusable' => true,
742                                         'value' => $this->mComment,
743                                         'autofocus' => true,
744                                 ] ),
745                                 [
746                                         'label' => $this->msg( 'undeletecomment' )->text(),
747                                         'align' => 'top',
748                                 ]
749                         );
750
751                         $fields[] = new OOUI\FieldLayout(
752                                 new OOUI\Widget( [
753                                         'content' => new OOUI\HorizontalLayout( [
754                                                 'items' => [
755                                                         new OOUI\ButtonInputWidget( [
756                                                                 'name' => 'restore',
757                                                                 'inputId' => 'mw-undelete-submit',
758                                                                 'value' => '1',
759                                                                 'label' => $this->msg( 'undeletebtn' )->text(),
760                                                                 'flags' => [ 'primary', 'progressive' ],
761                                                                 'type' => 'submit',
762                                                         ] ),
763                                                         new OOUI\ButtonInputWidget( [
764                                                                 'name' => 'invert',
765                                                                 'inputId' => 'mw-undelete-invert',
766                                                                 'value' => '1',
767                                                                 'label' => $this->msg( 'undeleteinvert' )->text()
768                                                         ] ),
769                                                 ]
770                                         ] )
771                                 ] )
772                         );
773
774                         if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
775                                 $fields[] = new OOUI\FieldLayout(
776                                         new OOUI\CheckboxInputWidget( [
777                                                 'name' => 'wpUnsuppress',
778                                                 'inputId' => 'mw-undelete-unsuppress',
779                                                 'value' => '1',
780                                         ] ),
781                                         [
782                                                 'label' => $this->msg( 'revdelete-unsuppress' )->text(),
783                                                 'align' => 'inline',
784                                         ]
785                                 );
786                         }
787
788                         $fieldset = new OOUI\FieldsetLayout( [
789                                 'label' => $this->msg( 'undelete-fieldset-title' )->text(),
790                                 'id' => 'mw-undelete-table',
791                                 'items' => $fields,
792                         ] );
793
794                         $form->appendContent(
795                                 new OOUI\PanelLayout( [
796                                         'expanded' => false,
797                                         'padded' => true,
798                                         'framed' => true,
799                                         'content' => $fieldset,
800                                 ] ),
801                                 new OOUI\HtmlSnippet(
802                                         Html::hidden( 'target', $this->mTarget ) .
803                                         Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() )
804                                 )
805                         );
806                 }
807
808                 $history = '';
809                 $history .= Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n";
810
811                 if ( $haveRevisions ) {
812                         # Show the page's stored (deleted) history
813
814                         if ( $this->getUser()->isAllowed( 'deleterevision' ) ) {
815                                 $history .= Html::element(
816                                         'button',
817                                         [
818                                                 'name' => 'revdel',
819                                                 'type' => 'submit',
820                                                 'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
821                                         ],
822                                         $this->msg( 'showhideselectedversions' )->text()
823                                 ) . "\n";
824                         }
825
826                         $history .= '<ul class="mw-undelete-revlist">';
827                         $remaining = $revisions->numRows();
828                         $earliestLiveTime = $this->mTargetObj->getEarliestRevTime();
829
830                         foreach ( $revisions as $row ) {
831                                 $remaining--;
832                                 $history .= $this->formatRevisionRow( $row, $earliestLiveTime, $remaining );
833                         }
834                         $revisions->free();
835                         $history .= '</ul>';
836                 } else {
837                         $out->addWikiMsg( 'nohistory' );
838                 }
839
840                 if ( $haveFiles ) {
841                         $history .= Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n";
842                         $history .= '<ul class="mw-undelete-revlist">';
843                         foreach ( $files as $row ) {
844                                 $history .= $this->formatFileRow( $row );
845                         }
846                         $files->free();
847                         $history .= '</ul>';
848                 }
849
850                 if ( $this->mAllowed ) {
851                         # Slip in the hidden controls here
852                         $misc = Html::hidden( 'target', $this->mTarget );
853                         $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
854                         $history .= $misc;
855
856                         $form->appendContent( new OOUI\HtmlSnippet( $history ) );
857                         $out->addHTML( $form );
858                 } else {
859                         $out->addHTML( $history );
860                 }
861
862                 return true;
863         }
864
865         protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
866                 $rev = Revision::newFromArchiveRow( $row,
867                         [
868                                 'title' => $this->mTargetObj
869                         ] );
870
871                 $revTextSize = '';
872                 $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
873                 // Build checkboxen...
874                 if ( $this->mAllowed ) {
875                         if ( $this->mInvert ) {
876                                 if ( in_array( $ts, $this->mTargetTimestamp ) ) {
877                                         $checkBox = Xml::check( "ts$ts" );
878                                 } else {
879                                         $checkBox = Xml::check( "ts$ts", true );
880                                 }
881                         } else {
882                                 $checkBox = Xml::check( "ts$ts" );
883                         }
884                 } else {
885                         $checkBox = '';
886                 }
887
888                 // Build page & diff links...
889                 $user = $this->getUser();
890                 if ( $this->mCanView ) {
891                         $titleObj = $this->getPageTitle();
892                         # Last link
893                         if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
894                                 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
895                                 $last = $this->msg( 'diff' )->escaped();
896                         } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
897                                 $pageLink = $this->getPageLink( $rev, $titleObj, $ts );
898                                 $last = $this->getLinkRenderer()->makeKnownLink(
899                                         $titleObj,
900                                         $this->msg( 'diff' )->text(),
901                                         [],
902                                         [
903                                                 'target' => $this->mTargetObj->getPrefixedText(),
904                                                 'timestamp' => $ts,
905                                                 'diff' => 'prev'
906                                         ]
907                                 );
908                         } else {
909                                 $pageLink = $this->getPageLink( $rev, $titleObj, $ts );
910                                 $last = $this->msg( 'diff' )->escaped();
911                         }
912                 } else {
913                         $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
914                         $last = $this->msg( 'diff' )->escaped();
915                 }
916
917                 // User links
918                 $userLink = Linker::revUserTools( $rev );
919
920                 // Minor edit
921                 $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
922
923                 // Revision text size
924                 $size = $row->ar_len;
925                 if ( !is_null( $size ) ) {
926                         $revTextSize = Linker::formatRevisionSize( $size );
927                 }
928
929                 // Edit summary
930                 $comment = Linker::revComment( $rev );
931
932                 // Tags
933                 $attribs = [];
934                 list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow(
935                         $row->ts_tags,
936                         'deletedhistory',
937                         $this->getContext()
938                 );
939                 if ( $classes ) {
940                         $attribs['class'] = implode( ' ', $classes );
941                 }
942
943                 $revisionRow = $this->msg( 'undelete-revision-row2' )
944                         ->rawParams(
945                                 $checkBox,
946                                 $last,
947                                 $pageLink,
948                                 $userLink,
949                                 $minor,
950                                 $revTextSize,
951                                 $comment,
952                                 $tagSummary
953                         )
954                         ->escaped();
955
956                 return Xml::tags( 'li', $attribs, $revisionRow ) . "\n";
957         }
958
959         private function formatFileRow( $row ) {
960                 $file = ArchivedFile::newFromRow( $row );
961                 $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
962                 $user = $this->getUser();
963
964                 $checkBox = '';
965                 if ( $this->mCanView && $row->fa_storage_key ) {
966                         if ( $this->mAllowed ) {
967                                 $checkBox = Xml::check( 'fileid' . $row->fa_id );
968                         }
969                         $key = urlencode( $row->fa_storage_key );
970                         $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
971                 } else {
972                         $pageLink = $this->getLanguage()->userTimeAndDate( $ts, $user );
973                 }
974                 $userLink = $this->getFileUser( $file );
975                 $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
976                 $bytes = $this->msg( 'parentheses' )
977                         ->rawParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
978                         ->plain();
979                 $data = htmlspecialchars( $data . ' ' . $bytes );
980                 $comment = $this->getFileComment( $file );
981
982                 // Add show/hide deletion links if available
983                 $canHide = $this->isAllowed( 'deleterevision' );
984                 if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) {
985                         if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
986                                 // Revision was hidden from sysops
987                                 $revdlink = Linker::revDeleteLinkDisabled( $canHide );
988                         } else {
989                                 $query = [
990                                         'type' => 'filearchive',
991                                         'target' => $this->mTargetObj->getPrefixedDBkey(),
992                                         'ids' => $row->fa_id
993                                 ];
994                                 $revdlink = Linker::revDeleteLink( $query,
995                                         $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
996                         }
997                 } else {
998                         $revdlink = '';
999                 }
1000
1001                 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1002         }
1003
1004         /**
1005          * Fetch revision text link if it's available to all users
1006          *
1007          * @param Revision $rev
1008          * @param Title $titleObj
1009          * @param string $ts Timestamp
1010          * @return string
1011          */
1012         function getPageLink( $rev, $titleObj, $ts ) {
1013                 $user = $this->getUser();
1014                 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1015
1016                 if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1017                         return '<span class="history-deleted">' . $time . '</span>';
1018                 }
1019
1020                 $link = $this->getLinkRenderer()->makeKnownLink(
1021                         $titleObj,
1022                         $time,
1023                         [],
1024                         [
1025                                 'target' => $this->mTargetObj->getPrefixedText(),
1026                                 'timestamp' => $ts
1027                         ]
1028                 );
1029
1030                 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1031                         $link = '<span class="history-deleted">' . $link . '</span>';
1032                 }
1033
1034                 return $link;
1035         }
1036
1037         /**
1038          * Fetch image view link if it's available to all users
1039          *
1040          * @param File|ArchivedFile $file
1041          * @param Title $titleObj
1042          * @param string $ts A timestamp
1043          * @param string $key A storage key
1044          *
1045          * @return string HTML fragment
1046          */
1047         function getFileLink( $file, $titleObj, $ts, $key ) {
1048                 $user = $this->getUser();
1049                 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1050
1051                 if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1052                         return '<span class="history-deleted">' . $time . '</span>';
1053                 }
1054
1055                 $link = $this->getLinkRenderer()->makeKnownLink(
1056                         $titleObj,
1057                         $time,
1058                         [],
1059                         [
1060                                 'target' => $this->mTargetObj->getPrefixedText(),
1061                                 'file' => $key,
1062                                 'token' => $user->getEditToken( $key )
1063                         ]
1064                 );
1065
1066                 if ( $file->isDeleted( File::DELETED_FILE ) ) {
1067                         $link = '<span class="history-deleted">' . $link . '</span>';
1068                 }
1069
1070                 return $link;
1071         }
1072
1073         /**
1074          * Fetch file's user id if it's available to this user
1075          *
1076          * @param File|ArchivedFile $file
1077          * @return string HTML fragment
1078          */
1079         function getFileUser( $file ) {
1080                 if ( !$file->userCan( File::DELETED_USER, $this->getUser() ) ) {
1081                         return '<span class="history-deleted">' .
1082                                 $this->msg( 'rev-deleted-user' )->escaped() .
1083                                 '</span>';
1084                 }
1085
1086                 $link = Linker::userLink( $file->getRawUser(), $file->getRawUserText() ) .
1087                         Linker::userToolLinks( $file->getRawUser(), $file->getRawUserText() );
1088
1089                 if ( $file->isDeleted( File::DELETED_USER ) ) {
1090                         $link = '<span class="history-deleted">' . $link . '</span>';
1091                 }
1092
1093                 return $link;
1094         }
1095
1096         /**
1097          * Fetch file upload comment if it's available to this user
1098          *
1099          * @param File|ArchivedFile $file
1100          * @return string HTML fragment
1101          */
1102         function getFileComment( $file ) {
1103                 if ( !$file->userCan( File::DELETED_COMMENT, $this->getUser() ) ) {
1104                         return '<span class="history-deleted"><span class="comment">' .
1105                                 $this->msg( 'rev-deleted-comment' )->escaped() . '</span></span>';
1106                 }
1107
1108                 $link = Linker::commentBlock( $file->getRawDescription() );
1109
1110                 if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1111                         $link = '<span class="history-deleted">' . $link . '</span>';
1112                 }
1113
1114                 return $link;
1115         }
1116
1117         function undelete() {
1118                 if ( $this->getConfig()->get( 'UploadMaintenance' )
1119                         && $this->mTargetObj->getNamespace() == NS_FILE
1120                 ) {
1121                         throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
1122                 }
1123
1124                 $this->checkReadOnly();
1125
1126                 $out = $this->getOutput();
1127                 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1128                 Hooks::run( 'UndeleteForm::undelete', [ &$archive, $this->mTargetObj ] );
1129                 $ok = $archive->undelete(
1130                         $this->mTargetTimestamp,
1131                         $this->mComment,
1132                         $this->mFileVersions,
1133                         $this->mUnsuppress,
1134                         $this->getUser()
1135                 );
1136
1137                 if ( is_array( $ok ) ) {
1138                         if ( $ok[1] ) { // Undeleted file count
1139                                 Hooks::run( 'FileUndeleteComplete', [
1140                                         $this->mTargetObj, $this->mFileVersions,
1141                                         $this->getUser(), $this->mComment ] );
1142                         }
1143
1144                         $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj );
1145                         $out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() );
1146                 } else {
1147                         $out->setPageTitle( $this->msg( 'undelete-error' ) );
1148                 }
1149
1150                 // Show revision undeletion warnings and errors
1151                 $status = $archive->getRevisionStatus();
1152                 if ( $status && !$status->isGood() ) {
1153                         $out->addWikiText( '<div class="error" id="mw-error-cannotundelete">' .
1154                                 $status->getWikiText(
1155                                         'cannotundelete',
1156                                         'cannotundelete'
1157                                 ) . '</div>'
1158                         );
1159                 }
1160
1161                 // Show file undeletion warnings and errors
1162                 $status = $archive->getFileStatus();
1163                 if ( $status && !$status->isGood() ) {
1164                         $out->addWikiText( '<div class="error">' .
1165                                 $status->getWikiText(
1166                                         'undelete-error-short',
1167                                         'undelete-error-long'
1168                                 ) . '</div>'
1169                         );
1170                 }
1171         }
1172
1173         /**
1174          * Return an array of subpages beginning with $search that this special page will accept.
1175          *
1176          * @param string $search Prefix to search for
1177          * @param int $limit Maximum number of results to return (usually 10)
1178          * @param int $offset Number of results to skip (usually 0)
1179          * @return string[] Matching subpages
1180          */
1181         public function prefixSearchSubpages( $search, $limit, $offset ) {
1182                 return $this->prefixSearchString( $search, $limit, $offset );
1183         }
1184
1185         protected function getGroupName() {
1186                 return 'pagetools';
1187         }
1188 }