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