Merge "interwiki: Fix-up for I5a979f047031e"
[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 /**
25 * Used to show archived pages and eventually restore them.
26 *
27 * @ingroup SpecialPage
28 */
29 class PageArchive {
30 /** @var Title */
31 protected $title;
32
33 /** @var Status */
34 protected $fileStatus;
35
36 /** @var Status */
37 protected $revisionStatus;
38
39 /** @var Config */
40 protected $config;
41
42 function __construct( $title, Config $config = null ) {
43 if ( is_null( $title ) ) {
44 throw new MWException( __METHOD__ . ' given a null title.' );
45 }
46 $this->title = $title;
47 if ( $config === null ) {
48 wfDebug( __METHOD__ . ' did not have a Config object passed to it' );
49 $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
50 }
51 $this->config = $config;
52 }
53
54 public function doesWrites() {
55 return true;
56 }
57
58 /**
59 * List all deleted pages recorded in the archive table. Returns result
60 * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
61 * namespace/title.
62 *
63 * @return ResultWrapper
64 */
65 public static function listAllPages() {
66 $dbr = wfGetDB( DB_SLAVE );
67
68 return self::listPages( $dbr, '' );
69 }
70
71 /**
72 * List deleted pages recorded in the archive table matching the
73 * given title prefix.
74 * Returns result wrapper with (ar_namespace, ar_title, count) fields.
75 *
76 * @param string $prefix Title prefix
77 * @return ResultWrapper
78 */
79 public static function listPagesByPrefix( $prefix ) {
80 $dbr = wfGetDB( DB_SLAVE );
81
82 $title = Title::newFromText( $prefix );
83 if ( $title ) {
84 $ns = $title->getNamespace();
85 $prefix = $title->getDBkey();
86 } else {
87 // Prolly won't work too good
88 // @todo handle bare namespace names cleanly?
89 $ns = 0;
90 }
91
92 $conds = array(
93 'ar_namespace' => $ns,
94 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
95 );
96
97 return self::listPages( $dbr, $conds );
98 }
99
100 /**
101 * @param IDatabase $dbr
102 * @param string|array $condition
103 * @return bool|ResultWrapper
104 */
105 protected static function listPages( $dbr, $condition ) {
106 return $dbr->select(
107 array( 'archive' ),
108 array(
109 'ar_namespace',
110 'ar_title',
111 'count' => 'COUNT(*)'
112 ),
113 $condition,
114 __METHOD__,
115 array(
116 'GROUP BY' => array( 'ar_namespace', 'ar_title' ),
117 'ORDER BY' => array( 'ar_namespace', 'ar_title' ),
118 'LIMIT' => 100,
119 )
120 );
121 }
122
123 /**
124 * List the revisions of the given page. Returns result wrapper with
125 * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
126 *
127 * @return ResultWrapper
128 */
129 function listRevisions() {
130 $dbr = wfGetDB( DB_SLAVE );
131
132 $tables = array( 'archive' );
133
134 $fields = array(
135 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
136 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
137 );
138
139 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
140 $fields[] = 'ar_content_format';
141 $fields[] = 'ar_content_model';
142 }
143
144 $conds = array( 'ar_namespace' => $this->title->getNamespace(),
145 'ar_title' => $this->title->getDBkey() );
146
147 $options = array( 'ORDER BY' => 'ar_timestamp DESC' );
148
149 $join_conds = array();
150
151 ChangeTags::modifyDisplayQuery(
152 $tables,
153 $fields,
154 $conds,
155 $join_conds,
156 $options,
157 ''
158 );
159
160 return $dbr->select( $tables,
161 $fields,
162 $conds,
163 __METHOD__,
164 $options,
165 $join_conds
166 );
167 }
168
169 /**
170 * List the deleted file revisions for this page, if it's a file page.
171 * Returns a result wrapper with various filearchive fields, or null
172 * if not a file page.
173 *
174 * @return ResultWrapper
175 * @todo Does this belong in Image for fuller encapsulation?
176 */
177 function listFiles() {
178 if ( $this->title->getNamespace() != NS_FILE ) {
179 return null;
180 }
181
182 $dbr = wfGetDB( DB_SLAVE );
183 return $dbr->select(
184 'filearchive',
185 ArchivedFile::selectFields(),
186 array( 'fa_name' => $this->title->getDBkey() ),
187 __METHOD__,
188 array( 'ORDER BY' => 'fa_timestamp DESC' )
189 );
190 }
191
192 /**
193 * Return a Revision object containing data for the deleted revision.
194 * Note that the result *may* or *may not* have a null page ID.
195 *
196 * @param string $timestamp
197 * @return Revision|null
198 */
199 function getRevision( $timestamp ) {
200 $dbr = wfGetDB( DB_SLAVE );
201
202 $fields = array(
203 'ar_rev_id',
204 'ar_text',
205 'ar_comment',
206 'ar_user',
207 'ar_user_text',
208 'ar_timestamp',
209 'ar_minor_edit',
210 'ar_flags',
211 'ar_text_id',
212 'ar_deleted',
213 'ar_len',
214 'ar_sha1',
215 );
216
217 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
218 $fields[] = 'ar_content_format';
219 $fields[] = 'ar_content_model';
220 }
221
222 $row = $dbr->selectRow( 'archive',
223 $fields,
224 array( 'ar_namespace' => $this->title->getNamespace(),
225 'ar_title' => $this->title->getDBkey(),
226 'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
227 __METHOD__ );
228
229 if ( $row ) {
230 return Revision::newFromArchiveRow( $row, array( 'title' => $this->title ) );
231 }
232
233 return null;
234 }
235
236 /**
237 * Return the most-previous revision, either live or deleted, against
238 * the deleted revision given by timestamp.
239 *
240 * May produce unexpected results in case of history merges or other
241 * unusual time issues.
242 *
243 * @param string $timestamp
244 * @return Revision|null Null when there is no previous revision
245 */
246 function getPreviousRevision( $timestamp ) {
247 $dbr = wfGetDB( DB_SLAVE );
248
249 // Check the previous deleted revision...
250 $row = $dbr->selectRow( 'archive',
251 'ar_timestamp',
252 array( 'ar_namespace' => $this->title->getNamespace(),
253 'ar_title' => $this->title->getDBkey(),
254 'ar_timestamp < ' .
255 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
256 __METHOD__,
257 array(
258 'ORDER BY' => 'ar_timestamp DESC',
259 'LIMIT' => 1 ) );
260 $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
261
262 $row = $dbr->selectRow( array( 'page', 'revision' ),
263 array( 'rev_id', 'rev_timestamp' ),
264 array(
265 'page_namespace' => $this->title->getNamespace(),
266 'page_title' => $this->title->getDBkey(),
267 'page_id = rev_page',
268 'rev_timestamp < ' .
269 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
270 __METHOD__,
271 array(
272 'ORDER BY' => 'rev_timestamp DESC',
273 'LIMIT' => 1 ) );
274 $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
275 $prevLiveId = $row ? intval( $row->rev_id ) : null;
276
277 if ( $prevLive && $prevLive > $prevDeleted ) {
278 // Most prior revision was live
279 return Revision::newFromId( $prevLiveId );
280 } elseif ( $prevDeleted ) {
281 // Most prior revision was deleted
282 return $this->getRevision( $prevDeleted );
283 }
284
285 // No prior revision on this page.
286 return null;
287 }
288
289 /**
290 * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
291 *
292 * @param object $row Database row
293 * @return string
294 */
295 function getTextFromRow( $row ) {
296 if ( is_null( $row->ar_text_id ) ) {
297 // An old row from MediaWiki 1.4 or previous.
298 // Text is embedded in this row in classic compression format.
299 return Revision::getRevisionText( $row, 'ar_' );
300 }
301
302 // New-style: keyed to the text storage backend.
303 $dbr = wfGetDB( DB_SLAVE );
304 $text = $dbr->selectRow( 'text',
305 array( 'old_text', 'old_flags' ),
306 array( 'old_id' => $row->ar_text_id ),
307 __METHOD__ );
308
309 return Revision::getRevisionText( $text );
310 }
311
312 /**
313 * Fetch (and decompress if necessary) the stored text of the most
314 * recently edited deleted revision of the page.
315 *
316 * If there are no archived revisions for the page, returns NULL.
317 *
318 * @return string|null
319 */
320 function getLastRevisionText() {
321 $dbr = wfGetDB( DB_SLAVE );
322 $row = $dbr->selectRow( 'archive',
323 array( 'ar_text', 'ar_flags', 'ar_text_id' ),
324 array( 'ar_namespace' => $this->title->getNamespace(),
325 'ar_title' => $this->title->getDBkey() ),
326 __METHOD__,
327 array( 'ORDER BY' => 'ar_timestamp DESC' ) );
328
329 if ( $row ) {
330 return $this->getTextFromRow( $row );
331 }
332
333 return null;
334 }
335
336 /**
337 * Quick check if any archived revisions are present for the page.
338 *
339 * @return bool
340 */
341 function isDeleted() {
342 $dbr = wfGetDB( DB_SLAVE );
343 $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
344 array( 'ar_namespace' => $this->title->getNamespace(),
345 'ar_title' => $this->title->getDBkey() ),
346 __METHOD__
347 );
348
349 return ( $n > 0 );
350 }
351
352 /**
353 * Restore the given (or all) text and file revisions for the page.
354 * Once restored, the items will be removed from the archive tables.
355 * The deletion log will be updated with an undeletion notice.
356 *
357 * @param array $timestamps Pass an empty array to restore all revisions,
358 * otherwise list the ones to undelete.
359 * @param string $comment
360 * @param array $fileVersions
361 * @param bool $unsuppress
362 * @param User $user User performing the action, or null to use $wgUser
363 * @return array(number of file revisions restored, number of image revisions
364 * restored, log message) on success, false on failure.
365 */
366 function undelete( $timestamps, $comment = '', $fileVersions = array(),
367 $unsuppress = false, User $user = null
368 ) {
369 // If both the set of text revisions and file revisions are empty,
370 // restore everything. Otherwise, just restore the requested items.
371 $restoreAll = empty( $timestamps ) && empty( $fileVersions );
372
373 $restoreText = $restoreAll || !empty( $timestamps );
374 $restoreFiles = $restoreAll || !empty( $fileVersions );
375
376 if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
377 $img = wfLocalFile( $this->title );
378 $img->load( File::READ_LATEST );
379 $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
380 if ( !$this->fileStatus->isOK() ) {
381 return false;
382 }
383 $filesRestored = $this->fileStatus->successCount;
384 } else {
385 $filesRestored = 0;
386 }
387
388 if ( $restoreText ) {
389 $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
390 if ( !$this->revisionStatus->isOK() ) {
391 return false;
392 }
393
394 $textRestored = $this->revisionStatus->getValue();
395 } else {
396 $textRestored = 0;
397 }
398
399 // Touch the log!
400
401 if ( $textRestored && $filesRestored ) {
402 $reason = wfMessage( 'undeletedrevisions-files' )
403 ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
404 } elseif ( $textRestored ) {
405 $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
406 ->inContentLanguage()->text();
407 } elseif ( $filesRestored ) {
408 $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
409 ->inContentLanguage()->text();
410 } else {
411 wfDebug( "Undelete: nothing undeleted...\n" );
412
413 return false;
414 }
415
416 if ( trim( $comment ) != '' ) {
417 $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
418 }
419
420 if ( $user === null ) {
421 global $wgUser;
422 $user = $wgUser;
423 }
424
425 $logEntry = new ManualLogEntry( 'delete', 'restore' );
426 $logEntry->setPerformer( $user );
427 $logEntry->setTarget( $this->title );
428 $logEntry->setComment( $reason );
429
430 Hooks::run( 'ArticleUndeleteLogEntry', array( $this, &$logEntry, $user ) );
431
432 $logid = $logEntry->insert();
433 $logEntry->publish( $logid );
434
435 return array( $textRestored, $filesRestored, $reason );
436 }
437
438 /**
439 * This is the meaty bit -- restores archived revisions of the given page
440 * to the cur/old tables. If the page currently exists, all revisions will
441 * be stuffed into old, otherwise the most recent will go into cur.
442 *
443 * @param array $timestamps Pass an empty array to restore all revisions,
444 * otherwise list the ones to undelete.
445 * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
446 * @param string $comment
447 * @throws ReadOnlyError
448 * @return Status Status object containing the number of revisions restored on success
449 */
450 private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
451 if ( wfReadOnly() ) {
452 throw new ReadOnlyError();
453 }
454
455 $restoreAll = empty( $timestamps );
456 $dbw = wfGetDB( DB_MASTER );
457
458 # Does this page already exist? We'll have to update it...
459 $article = WikiPage::factory( $this->title );
460 # Load latest data for the current page (bug 31179)
461 $article->loadPageData( 'fromdbmaster' );
462 $oldcountable = $article->isCountable();
463
464 $page = $dbw->selectRow( 'page',
465 array( 'page_id', 'page_latest' ),
466 array( 'page_namespace' => $this->title->getNamespace(),
467 'page_title' => $this->title->getDBkey() ),
468 __METHOD__,
469 array( 'FOR UPDATE' ) // lock page
470 );
471
472 if ( $page ) {
473 $makepage = false;
474 # Page already exists. Import the history, and if necessary
475 # we'll update the latest revision field in the record.
476
477 $previousRevId = $page->page_latest;
478
479 # Get the time span of this page
480 $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
481 array( 'rev_id' => $previousRevId ),
482 __METHOD__ );
483
484 if ( $previousTimestamp === false ) {
485 wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
486
487 $status = Status::newGood( 0 );
488 $status->warning( 'undeleterevision-missing' );
489
490 return $status;
491 }
492 } else {
493 # Have to create a new article...
494 $makepage = true;
495 $previousRevId = 0;
496 $previousTimestamp = 0;
497 }
498
499 $oldWhere = array(
500 'ar_namespace' => $this->title->getNamespace(),
501 'ar_title' => $this->title->getDBkey(),
502 );
503 if ( !$restoreAll ) {
504 $oldWhere['ar_timestamp'] = array_map( array( &$dbw, 'timestamp' ), $timestamps );
505 }
506
507 $fields = array(
508 'ar_rev_id',
509 'ar_text',
510 'ar_comment',
511 'ar_user',
512 'ar_user_text',
513 'ar_timestamp',
514 'ar_minor_edit',
515 'ar_flags',
516 'ar_text_id',
517 'ar_deleted',
518 'ar_page_id',
519 'ar_len',
520 'ar_sha1'
521 );
522
523 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
524 $fields[] = 'ar_content_format';
525 $fields[] = 'ar_content_model';
526 }
527
528 /**
529 * Select each archived revision...
530 */
531 $result = $dbw->select( 'archive',
532 $fields,
533 $oldWhere,
534 __METHOD__,
535 /* options */ array( 'ORDER BY' => 'ar_timestamp' )
536 );
537
538 $rev_count = $result->numRows();
539 if ( !$rev_count ) {
540 wfDebug( __METHOD__ . ": no revisions to restore\n" );
541
542 $status = Status::newGood( 0 );
543 $status->warning( "undelete-no-results" );
544
545 return $status;
546 }
547
548 $result->seek( $rev_count - 1 ); // move to last
549 $row = $result->fetchObject(); // get newest archived rev
550 $oldPageId = (int)$row->ar_page_id; // pass this to ArticleUndelete hook
551 $result->seek( 0 ); // move back
552
553 // grab the content to check consistency with global state before restoring the page.
554 $revision = Revision::newFromArchiveRow( $row,
555 array(
556 'title' => $article->getTitle(), // used to derive default content model
557 )
558 );
559 $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
560 $content = $revision->getContent( Revision::RAW );
561
562 // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
563 $status = $content->prepareSave( $article, 0, -1, $user );
564
565 if ( !$status->isOK() ) {
566 return $status;
567 }
568
569 if ( $makepage ) {
570 // Check the state of the newest to-be version...
571 if ( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
572 return Status::newFatal( "undeleterevdel" );
573 }
574 // Safe to insert now...
575 $newid = $article->insertOn( $dbw, $row->ar_page_id );
576 if ( $newid === false ) {
577 // The old ID is reserved; let's pick another
578 $newid = $article->insertOn( $dbw );
579 }
580 $pageId = $newid;
581 } else {
582 // Check if a deleted revision will become the current revision...
583 if ( $row->ar_timestamp > $previousTimestamp ) {
584 // Check the state of the newest to-be version...
585 if ( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
586 return Status::newFatal( "undeleterevdel" );
587 }
588 }
589
590 $newid = false;
591 $pageId = $article->getId();
592 }
593
594 $revision = null;
595 $restored = 0;
596
597 foreach ( $result as $row ) {
598 // Check for key dupes due to needed archive integrity.
599 if ( $row->ar_rev_id ) {
600 $exists = $dbw->selectField( 'revision', '1',
601 array( 'rev_id' => $row->ar_rev_id ), __METHOD__ );
602 if ( $exists ) {
603 continue; // don't throw DB errors
604 }
605 }
606 // Insert one revision at a time...maintaining deletion status
607 // unless we are specifically removing all restrictions...
608 $revision = Revision::newFromArchiveRow( $row,
609 array(
610 'page' => $pageId,
611 'title' => $this->title,
612 'deleted' => $unsuppress ? 0 : $row->ar_deleted
613 ) );
614
615 $revision->insertOn( $dbw );
616 $restored++;
617
618 Hooks::run( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) );
619 }
620 # Now that it's safely stored, take it out of the archive
621 $dbw->delete( 'archive',
622 $oldWhere,
623 __METHOD__ );
624
625 // Was anything restored at all?
626 if ( $restored == 0 ) {
627 return Status::newGood( 0 );
628 }
629
630 $created = (bool)$newid;
631
632 // Attach the latest revision to the page...
633 $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
634 if ( $created || $wasnew ) {
635 // Update site stats, link tables, etc
636 $article->doEditUpdates(
637 $revision,
638 User::newFromName( $revision->getUserText( Revision::RAW ), false ),
639 array(
640 'created' => $created,
641 'oldcountable' => $oldcountable,
642 'restored' => true
643 )
644 );
645 }
646
647 Hooks::run( 'ArticleUndelete', array( &$this->title, $created, $comment, $oldPageId ) );
648
649 if ( $this->title->getNamespace() == NS_FILE ) {
650 DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
651 }
652
653 return Status::newGood( $restored );
654 }
655
656 /**
657 * @return Status
658 */
659 function getFileStatus() {
660 return $this->fileStatus;
661 }
662
663 /**
664 * @return Status
665 */
666 function getRevisionStatus() {
667 return $this->revisionStatus;
668 }
669 }
670
671 /**
672 * Special page allowing users with the appropriate permissions to view
673 * and restore deleted content.
674 *
675 * @ingroup SpecialPage
676 */
677 class SpecialUndelete extends SpecialPage {
678 private $mAction;
679 private $mTarget;
680 private $mTimestamp;
681 private $mRestore;
682 private $mRevdel;
683 private $mInvert;
684 private $mFilename;
685 private $mTargetTimestamp;
686 private $mAllowed;
687 private $mCanView;
688 private $mComment;
689 private $mToken;
690
691 /** @var Title */
692 private $mTargetObj;
693
694 function __construct() {
695 parent::__construct( 'Undelete', 'deletedhistory' );
696 }
697
698 function loadRequest( $par ) {
699 $request = $this->getRequest();
700 $user = $this->getUser();
701
702 $this->mAction = $request->getVal( 'action' );
703 if ( $par !== null && $par !== '' ) {
704 $this->mTarget = $par;
705 } else {
706 $this->mTarget = $request->getVal( 'target' );
707 }
708
709 $this->mTargetObj = null;
710
711 if ( $this->mTarget !== null && $this->mTarget !== '' ) {
712 $this->mTargetObj = Title::newFromText( $this->mTarget );
713 }
714
715 $this->mSearchPrefix = $request->getText( 'prefix' );
716 $time = $request->getVal( 'timestamp' );
717 $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
718 $this->mFilename = $request->getVal( 'file' );
719
720 $posted = $request->wasPosted() &&
721 $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
722 $this->mRestore = $request->getCheck( 'restore' ) && $posted;
723 $this->mRevdel = $request->getCheck( 'revdel' ) && $posted;
724 $this->mInvert = $request->getCheck( 'invert' ) && $posted;
725 $this->mPreview = $request->getCheck( 'preview' ) && $posted;
726 $this->mDiff = $request->getCheck( 'diff' );
727 $this->mDiffOnly = $request->getBool( 'diffonly', $this->getUser()->getOption( 'diffonly' ) );
728 $this->mComment = $request->getText( 'wpComment' );
729 $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $user->isAllowed( 'suppressrevision' );
730 $this->mToken = $request->getVal( 'token' );
731
732 if ( $this->isAllowed( 'undelete' ) && !$user->isBlocked() ) {
733 $this->mAllowed = true; // user can restore
734 $this->mCanView = true; // user can view content
735 } elseif ( $this->isAllowed( 'deletedtext' ) ) {
736 $this->mAllowed = false; // user cannot restore
737 $this->mCanView = true; // user can view content
738 $this->mRestore = false;
739 } else { // user can only view the list of revisions
740 $this->mAllowed = false;
741 $this->mCanView = false;
742 $this->mTimestamp = '';
743 $this->mRestore = false;
744 }
745
746 if ( $this->mRestore || $this->mInvert ) {
747 $timestamps = array();
748 $this->mFileVersions = array();
749 foreach ( $request->getValues() as $key => $val ) {
750 $matches = array();
751 if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
752 array_push( $timestamps, $matches[1] );
753 }
754
755 if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
756 $this->mFileVersions[] = intval( $matches[1] );
757 }
758 }
759 rsort( $timestamps );
760 $this->mTargetTimestamp = $timestamps;
761 }
762 }
763
764 /**
765 * Checks whether a user is allowed the permission for the
766 * specific title if one is set.
767 *
768 * @param string $permission
769 * @param User $user
770 * @return bool
771 */
772 protected function isAllowed( $permission, User $user = null ) {
773 $user = $user ?: $this->getUser();
774 if ( $this->mTargetObj !== null ) {
775 return $this->mTargetObj->userCan( $permission, $user );
776 } else {
777 return $user->isAllowed( $permission );
778 }
779 }
780
781 function userCanExecute( User $user ) {
782 return $this->isAllowed( $this->mRestriction, $user );
783 }
784
785 function execute( $par ) {
786 $this->useTransactionalTimeLimit();
787
788 $user = $this->getUser();
789
790 $this->setHeaders();
791 $this->outputHeader();
792
793 $this->loadRequest( $par );
794 $this->checkPermissions(); // Needs to be after mTargetObj is set
795
796 $out = $this->getOutput();
797
798 if ( is_null( $this->mTargetObj ) ) {
799 $out->addWikiMsg( 'undelete-header' );
800
801 # Not all users can just browse every deleted page from the list
802 if ( $user->isAllowed( 'browsearchive' ) ) {
803 $this->showSearchForm();
804 }
805
806 return;
807 }
808
809 $this->addHelpLink( 'Help:Undelete' );
810 if ( $this->mAllowed ) {
811 $out->setPageTitle( $this->msg( 'undeletepage' ) );
812 } else {
813 $out->setPageTitle( $this->msg( 'viewdeletedpage' ) );
814 }
815
816 $this->getSkin()->setRelevantTitle( $this->mTargetObj );
817
818 if ( $this->mTimestamp !== '' ) {
819 $this->showRevision( $this->mTimestamp );
820 } elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) {
821 $file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
822 // Check if user is allowed to see this file
823 if ( !$file->exists() ) {
824 $out->addWikiMsg( 'filedelete-nofile', $this->mFilename );
825 } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
826 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
827 throw new PermissionsError( 'suppressrevision' );
828 } else {
829 throw new PermissionsError( 'deletedtext' );
830 }
831 } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
832 $this->showFileConfirmationForm( $this->mFilename );
833 } else {
834 $this->showFile( $this->mFilename );
835 }
836 } elseif ( $this->mAction === "submit" ) {
837 if ( $this->mRestore ) {
838 $this->undelete();
839 } elseif ( $this->mRevdel ) {
840 $this->redirectToRevDel();
841 }
842
843 } else {
844 $this->showHistory();
845 }
846 }
847
848 /**
849 * Convert submitted form data to format expected by RevisionDelete and
850 * redirect the request
851 */
852 private function redirectToRevDel() {
853 $archive = new PageArchive( $this->mTargetObj );
854
855 $revisions = array();
856
857 foreach ( $this->getRequest()->getValues() as $key => $val ) {
858 $matches = array();
859 if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
860 $revisions[ $archive->getRevision( $matches[1] )->getId() ] = 1;
861 }
862 }
863 $query = array(
864 "type" => "revision",
865 "ids" => $revisions,
866 "target" => wfUrlencode( $this->mTargetObj->getPrefixedText() )
867 );
868 $url = SpecialPage::getTitleFor( "RevisionDelete" )->getFullURL( $query );
869 $this->getOutput()->redirect( $url );
870 }
871
872 function showSearchForm() {
873 $out = $this->getOutput();
874 $out->setPageTitle( $this->msg( 'undelete-search-title' ) );
875 $out->addHTML(
876 Xml::openElement( 'form', array( 'method' => 'get', 'action' => wfScript() ) ) .
877 Xml::fieldset( $this->msg( 'undelete-search-box' )->text() ) .
878 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
879 Html::rawElement(
880 'label',
881 array( 'for' => 'prefix' ),
882 $this->msg( 'undelete-search-prefix' )->parse()
883 ) .
884 Xml::input(
885 'prefix',
886 20,
887 $this->mSearchPrefix,
888 array( 'id' => 'prefix', 'autofocus' => '' )
889 ) . ' ' .
890 Xml::submitButton( $this->msg( 'undelete-search-submit' )->text() ) .
891 Xml::closeElement( 'fieldset' ) .
892 Xml::closeElement( 'form' )
893 );
894
895 # List undeletable articles
896 if ( $this->mSearchPrefix ) {
897 $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
898 $this->showList( $result );
899 }
900 }
901
902 /**
903 * Generic list of deleted pages
904 *
905 * @param ResultWrapper $result
906 * @return bool
907 */
908 private function showList( $result ) {
909 $out = $this->getOutput();
910
911 if ( $result->numRows() == 0 ) {
912 $out->addWikiMsg( 'undelete-no-results' );
913
914 return false;
915 }
916
917 $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
918
919 $undelete = $this->getPageTitle();
920 $out->addHTML( "<ul>\n" );
921 foreach ( $result as $row ) {
922 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
923 if ( $title !== null ) {
924 $item = Linker::linkKnown(
925 $undelete,
926 htmlspecialchars( $title->getPrefixedText() ),
927 array(),
928 array( 'target' => $title->getPrefixedText() )
929 );
930 } else {
931 // The title is no longer valid, show as text
932 $item = Html::element(
933 'span',
934 array( 'class' => 'mw-invalidtitle' ),
935 Linker::getInvalidTitleDescription(
936 $this->getContext(),
937 $row->ar_namespace,
938 $row->ar_title
939 )
940 );
941 }
942 $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
943 $out->addHTML( "<li>{$item} ({$revs})</li>\n" );
944 }
945 $result->free();
946 $out->addHTML( "</ul>\n" );
947
948 return true;
949 }
950
951 private function showRevision( $timestamp ) {
952 if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
953 return;
954 }
955
956 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
957 if ( !Hooks::run( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) {
958 return;
959 }
960 $rev = $archive->getRevision( $timestamp );
961
962 $out = $this->getOutput();
963 $user = $this->getUser();
964
965 if ( !$rev ) {
966 $out->addWikiMsg( 'undeleterevision-missing' );
967
968 return;
969 }
970
971 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
972 if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
973 $out->wrapWikiMsg(
974 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
975 $rev->isDeleted( Revision::DELETED_RESTRICTED ) ?
976 'rev-suppressed-text-permission' : 'rev-deleted-text-permission'
977 );
978
979 return;
980 }
981
982 $out->wrapWikiMsg(
983 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
984 $rev->isDeleted( Revision::DELETED_RESTRICTED ) ?
985 'rev-suppressed-text-view' : 'rev-deleted-text-view'
986 );
987 $out->addHTML( '<br />' );
988 // and we are allowed to see...
989 }
990
991 if ( $this->mDiff ) {
992 $previousRev = $archive->getPreviousRevision( $timestamp );
993 if ( $previousRev ) {
994 $this->showDiff( $previousRev, $rev );
995 if ( $this->mDiffOnly ) {
996 return;
997 }
998
999 $out->addHTML( '<hr />' );
1000 } else {
1001 $out->addWikiMsg( 'undelete-nodiff' );
1002 }
1003 }
1004
1005 $link = Linker::linkKnown(
1006 $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
1007 htmlspecialchars( $this->mTargetObj->getPrefixedText() )
1008 );
1009
1010 $lang = $this->getLanguage();
1011
1012 // date and time are separate parameters to facilitate localisation.
1013 // $time is kept for backward compat reasons.
1014 $time = $lang->userTimeAndDate( $timestamp, $user );
1015 $d = $lang->userDate( $timestamp, $user );
1016 $t = $lang->userTime( $timestamp, $user );
1017 $userLink = Linker::revUserTools( $rev );
1018
1019 $content = $rev->getContent( Revision::FOR_THIS_USER, $user );
1020
1021 $isText = ( $content instanceof TextContent );
1022
1023 if ( $this->mPreview || $isText ) {
1024 $openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
1025 } else {
1026 $openDiv = '<div id="mw-undelete-revision">';
1027 }
1028 $out->addHTML( $openDiv );
1029
1030 // Revision delete links
1031 if ( !$this->mDiff ) {
1032 $revdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
1033 if ( $revdel ) {
1034 $out->addHTML( "$revdel " );
1035 }
1036 }
1037
1038 $out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params(
1039 $time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' );
1040
1041 if ( !Hooks::run( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ) ) {
1042 return;
1043 }
1044
1045 if ( ( $this->mPreview || !$isText ) && $content ) {
1046 // NOTE: non-text content has no source view, so always use rendered preview
1047
1048 // Hide [edit]s
1049 $popts = $out->parserOptions();
1050 $popts->setEditSection( false );
1051
1052 $pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
1053 $out->addParserOutput( $pout );
1054 }
1055
1056 if ( $isText ) {
1057 // source view for textual content
1058 $sourceView = Xml::element(
1059 'textarea',
1060 array(
1061 'readonly' => 'readonly',
1062 'cols' => $user->getIntOption( 'cols' ),
1063 'rows' => $user->getIntOption( 'rows' )
1064 ),
1065 $content->getNativeData() . "\n"
1066 );
1067
1068 $previewButton = Xml::element( 'input', array(
1069 'type' => 'submit',
1070 'name' => 'preview',
1071 'value' => $this->msg( 'showpreview' )->text()
1072 ) );
1073 } else {
1074 $sourceView = '';
1075 $previewButton = '';
1076 }
1077
1078 $diffButton = Xml::element( 'input', array(
1079 'name' => 'diff',
1080 'type' => 'submit',
1081 'value' => $this->msg( 'showdiff' )->text() ) );
1082
1083 $out->addHTML(
1084 $sourceView .
1085 Xml::openElement( 'div', array(
1086 'style' => 'clear: both' ) ) .
1087 Xml::openElement( 'form', array(
1088 'method' => 'post',
1089 'action' => $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) .
1090 Xml::element( 'input', array(
1091 'type' => 'hidden',
1092 'name' => 'target',
1093 'value' => $this->mTargetObj->getPrefixedDBkey() ) ) .
1094 Xml::element( 'input', array(
1095 'type' => 'hidden',
1096 'name' => 'timestamp',
1097 'value' => $timestamp ) ) .
1098 Xml::element( 'input', array(
1099 'type' => 'hidden',
1100 'name' => 'wpEditToken',
1101 'value' => $user->getEditToken() ) ) .
1102 $previewButton .
1103 $diffButton .
1104 Xml::closeElement( 'form' ) .
1105 Xml::closeElement( 'div' )
1106 );
1107 }
1108
1109 /**
1110 * Build a diff display between this and the previous either deleted
1111 * or non-deleted edit.
1112 *
1113 * @param Revision $previousRev
1114 * @param Revision $currentRev
1115 * @return string HTML
1116 */
1117 function showDiff( $previousRev, $currentRev ) {
1118 $diffContext = clone $this->getContext();
1119 $diffContext->setTitle( $currentRev->getTitle() );
1120 $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
1121
1122 $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
1123 $diffEngine->showDiffStyle();
1124
1125 $formattedDiff = $diffEngine->generateContentDiffBody(
1126 $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
1127 $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
1128 );
1129
1130 $formattedDiff = $diffEngine->addHeader(
1131 $formattedDiff,
1132 $this->diffHeader( $previousRev, 'o' ),
1133 $this->diffHeader( $currentRev, 'n' )
1134 );
1135
1136 $this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" );
1137 }
1138
1139 /**
1140 * @param Revision $rev
1141 * @param string $prefix
1142 * @return string
1143 */
1144 private function diffHeader( $rev, $prefix ) {
1145 $isDeleted = !( $rev->getId() && $rev->getTitle() );
1146 if ( $isDeleted ) {
1147 /// @todo FIXME: $rev->getTitle() is null for deleted revs...?
1148 $targetPage = $this->getPageTitle();
1149 $targetQuery = array(
1150 'target' => $this->mTargetObj->getPrefixedText(),
1151 'timestamp' => wfTimestamp( TS_MW, $rev->getTimestamp() )
1152 );
1153 } else {
1154 /// @todo FIXME: getId() may return non-zero for deleted revs...
1155 $targetPage = $rev->getTitle();
1156 $targetQuery = array( 'oldid' => $rev->getId() );
1157 }
1158
1159 // Add show/hide deletion links if available
1160 $user = $this->getUser();
1161 $lang = $this->getLanguage();
1162 $rdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
1163
1164 if ( $rdel ) {
1165 $rdel = " $rdel";
1166 }
1167
1168 $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
1169
1170 $tags = wfGetDB( DB_SLAVE )->selectField(
1171 'tag_summary',
1172 'ts_tags',
1173 array( 'ts_rev_id' => $rev->getId() ),
1174 __METHOD__
1175 );
1176 $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff' );
1177
1178 // FIXME This is reimplementing DifferenceEngine#getRevisionHeader
1179 // and partially #showDiffPage, but worse
1180 return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
1181 Linker::link(
1182 $targetPage,
1183 $this->msg(
1184 'revisionasof',
1185 $lang->userTimeAndDate( $rev->getTimestamp(), $user ),
1186 $lang->userDate( $rev->getTimestamp(), $user ),
1187 $lang->userTime( $rev->getTimestamp(), $user )
1188 )->escaped(),
1189 array(),
1190 $targetQuery
1191 ) .
1192 '</strong></div>' .
1193 '<div id="mw-diff-' . $prefix . 'title2">' .
1194 Linker::revUserTools( $rev ) . '<br />' .
1195 '</div>' .
1196 '<div id="mw-diff-' . $prefix . 'title3">' .
1197 $minor . Linker::revComment( $rev ) . $rdel . '<br />' .
1198 '</div>' .
1199 '<div id="mw-diff-' . $prefix . 'title5">' .
1200 $tagSummary[0] . '<br />' .
1201 '</div>';
1202 }
1203
1204 /**
1205 * Show a form confirming whether a tokenless user really wants to see a file
1206 * @param string $key
1207 */
1208 private function showFileConfirmationForm( $key ) {
1209 $out = $this->getOutput();
1210 $lang = $this->getLanguage();
1211 $user = $this->getUser();
1212 $file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
1213 $out->addWikiMsg( 'undelete-show-file-confirm',
1214 $this->mTargetObj->getText(),
1215 $lang->userDate( $file->getTimestamp(), $user ),
1216 $lang->userTime( $file->getTimestamp(), $user ) );
1217 $out->addHTML(
1218 Xml::openElement( 'form', array(
1219 'method' => 'POST',
1220 'action' => $this->getPageTitle()->getLocalURL( array(
1221 'target' => $this->mTarget,
1222 'file' => $key,
1223 'token' => $user->getEditToken( $key ),
1224 ) ),
1225 )
1226 ) .
1227 Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) .
1228 '</form>'
1229 );
1230 }
1231
1232 /**
1233 * Show a deleted file version requested by the visitor.
1234 * @param string $key
1235 */
1236 private function showFile( $key ) {
1237 $this->getOutput()->disable();
1238
1239 # We mustn't allow the output to be CDN cached, otherwise
1240 # if an admin previews a deleted image, and it's cached, then
1241 # a user without appropriate permissions can toddle off and
1242 # nab the image, and CDN will serve it
1243 $response = $this->getRequest()->response();
1244 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
1245 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
1246 $response->header( 'Pragma: no-cache' );
1247
1248 $repo = RepoGroup::singleton()->getLocalRepo();
1249 $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
1250 $repo->streamFile( $path );
1251 }
1252
1253 protected function showHistory() {
1254 $this->checkReadOnly();
1255
1256 $out = $this->getOutput();
1257 if ( $this->mAllowed ) {
1258 $out->addModules( 'mediawiki.special.undelete' );
1259 }
1260 $out->wrapWikiMsg(
1261 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1262 array( 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) )
1263 );
1264
1265 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1266 Hooks::run( 'UndeleteForm::showHistory', array( &$archive, $this->mTargetObj ) );
1267 /*
1268 $text = $archive->getLastRevisionText();
1269 if( is_null( $text ) ) {
1270 $out->addWikiMsg( 'nohistory' );
1271 return;
1272 }
1273 */
1274 $out->addHTML( '<div class="mw-undelete-history">' );
1275 if ( $this->mAllowed ) {
1276 $out->addWikiMsg( 'undeletehistory' );
1277 $out->addWikiMsg( 'undeleterevdel' );
1278 } else {
1279 $out->addWikiMsg( 'undeletehistorynoadmin' );
1280 }
1281 $out->addHTML( '</div>' );
1282
1283 # List all stored revisions
1284 $revisions = $archive->listRevisions();
1285 $files = $archive->listFiles();
1286
1287 $haveRevisions = $revisions && $revisions->numRows() > 0;
1288 $haveFiles = $files && $files->numRows() > 0;
1289
1290 # Batch existence check on user and talk pages
1291 if ( $haveRevisions ) {
1292 $batch = new LinkBatch();
1293 foreach ( $revisions as $row ) {
1294 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
1295 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
1296 }
1297 $batch->execute();
1298 $revisions->seek( 0 );
1299 }
1300 if ( $haveFiles ) {
1301 $batch = new LinkBatch();
1302 foreach ( $files as $row ) {
1303 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
1304 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
1305 }
1306 $batch->execute();
1307 $files->seek( 0 );
1308 }
1309
1310 if ( $this->mAllowed ) {
1311 $action = $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) );
1312 # Start the form here
1313 $top = Xml::openElement(
1314 'form',
1315 array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' )
1316 );
1317 $out->addHTML( $top );
1318 }
1319
1320 # Show relevant lines from the deletion log:
1321 $deleteLogPage = new LogPage( 'delete' );
1322 $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
1323 LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
1324 # Show relevant lines from the suppression log:
1325 $suppressLogPage = new LogPage( 'suppress' );
1326 if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
1327 $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
1328 LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
1329 }
1330
1331 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1332 # Format the user-visible controls (comment field, submission button)
1333 # in a nice little table
1334 if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
1335 $unsuppressBox =
1336 "<tr>
1337 <td>&#160;</td>
1338 <td class='mw-input'>" .
1339 Xml::checkLabel( $this->msg( 'revdelete-unsuppress' )->text(),
1340 'wpUnsuppress', 'mw-undelete-unsuppress', $this->mUnsuppress ) .
1341 "</td>
1342 </tr>";
1343 } else {
1344 $unsuppressBox = '';
1345 }
1346
1347 $table = Xml::fieldset( $this->msg( 'undelete-fieldset-title' )->text() ) .
1348 Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
1349 "<tr>
1350 <td colspan='2' class='mw-undelete-extrahelp'>" .
1351 $this->msg( 'undeleteextrahelp' )->parseAsBlock() .
1352 "</td>
1353 </tr>
1354 <tr>
1355 <td class='mw-label'>" .
1356 Xml::label( $this->msg( 'undeletecomment' )->text(), 'wpComment' ) .
1357 "</td>
1358 <td class='mw-input'>" .
1359 Xml::input(
1360 'wpComment',
1361 50,
1362 $this->mComment,
1363 array( 'id' => 'wpComment', 'autofocus' => '' )
1364 ) .
1365 "</td>
1366 </tr>
1367 <tr>
1368 <td>&#160;</td>
1369 <td class='mw-submit'>" .
1370 Xml::submitButton(
1371 $this->msg( 'undeletebtn' )->text(),
1372 array( 'name' => 'restore', 'id' => 'mw-undelete-submit' )
1373 ) . ' ' .
1374 Xml::submitButton(
1375 $this->msg( 'undeleteinvert' )->text(),
1376 array( 'name' => 'invert', 'id' => 'mw-undelete-invert' )
1377 ) .
1378 "</td>
1379 </tr>" .
1380 $unsuppressBox .
1381 Xml::closeElement( 'table' ) .
1382 Xml::closeElement( 'fieldset' );
1383
1384 $out->addHTML( $table );
1385 }
1386
1387 $out->addHTML( Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n" );
1388
1389 if ( $haveRevisions ) {
1390 # Show the page's stored (deleted) history
1391
1392 if ( $this->getUser()->isAllowed( 'deleterevision' ) ) {
1393 $out->addHTML( Html::element(
1394 'button',
1395 array(
1396 'name' => 'revdel',
1397 'type' => 'submit',
1398 'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
1399 ),
1400 $this->msg( 'showhideselectedversions' )->text()
1401 ) . "\n" );
1402 }
1403
1404 $out->addHTML( '<ul>' );
1405 $remaining = $revisions->numRows();
1406 $earliestLiveTime = $this->mTargetObj->getEarliestRevTime();
1407
1408 foreach ( $revisions as $row ) {
1409 $remaining--;
1410 $out->addHTML( $this->formatRevisionRow( $row, $earliestLiveTime, $remaining ) );
1411 }
1412 $revisions->free();
1413 $out->addHTML( '</ul>' );
1414 } else {
1415 $out->addWikiMsg( 'nohistory' );
1416 }
1417
1418 if ( $haveFiles ) {
1419 $out->addHTML( Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n" );
1420 $out->addHTML( '<ul>' );
1421 foreach ( $files as $row ) {
1422 $out->addHTML( $this->formatFileRow( $row ) );
1423 }
1424 $files->free();
1425 $out->addHTML( '</ul>' );
1426 }
1427
1428 if ( $this->mAllowed ) {
1429 # Slip in the hidden controls here
1430 $misc = Html::hidden( 'target', $this->mTarget );
1431 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
1432 $misc .= Xml::closeElement( 'form' );
1433 $out->addHTML( $misc );
1434 }
1435
1436 return true;
1437 }
1438
1439 protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
1440 $rev = Revision::newFromArchiveRow( $row,
1441 array(
1442 'title' => $this->mTargetObj
1443 ) );
1444
1445 $revTextSize = '';
1446 $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
1447 // Build checkboxen...
1448 if ( $this->mAllowed ) {
1449 if ( $this->mInvert ) {
1450 if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1451 $checkBox = Xml::check( "ts$ts" );
1452 } else {
1453 $checkBox = Xml::check( "ts$ts", true );
1454 }
1455 } else {
1456 $checkBox = Xml::check( "ts$ts" );
1457 }
1458 } else {
1459 $checkBox = '';
1460 }
1461
1462 // Build page & diff links...
1463 $user = $this->getUser();
1464 if ( $this->mCanView ) {
1465 $titleObj = $this->getPageTitle();
1466 # Last link
1467 if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
1468 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1469 $last = $this->msg( 'diff' )->escaped();
1470 } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1471 $pageLink = $this->getPageLink( $rev, $titleObj, $ts );
1472 $last = Linker::linkKnown(
1473 $titleObj,
1474 $this->msg( 'diff' )->escaped(),
1475 array(),
1476 array(
1477 'target' => $this->mTargetObj->getPrefixedText(),
1478 'timestamp' => $ts,
1479 'diff' => 'prev'
1480 )
1481 );
1482 } else {
1483 $pageLink = $this->getPageLink( $rev, $titleObj, $ts );
1484 $last = $this->msg( 'diff' )->escaped();
1485 }
1486 } else {
1487 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1488 $last = $this->msg( 'diff' )->escaped();
1489 }
1490
1491 // User links
1492 $userLink = Linker::revUserTools( $rev );
1493
1494 // Minor edit
1495 $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
1496
1497 // Revision text size
1498 $size = $row->ar_len;
1499 if ( !is_null( $size ) ) {
1500 $revTextSize = Linker::formatRevisionSize( $size );
1501 }
1502
1503 // Edit summary
1504 $comment = Linker::revComment( $rev );
1505
1506 // Tags
1507 $attribs = array();
1508 list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'deletedhistory' );
1509 if ( $classes ) {
1510 $attribs['class'] = implode( ' ', $classes );
1511 }
1512
1513 $revisionRow = $this->msg( 'undelete-revision-row2' )
1514 ->rawParams(
1515 $checkBox,
1516 $last,
1517 $pageLink,
1518 $userLink,
1519 $minor,
1520 $revTextSize,
1521 $comment,
1522 $tagSummary
1523 )
1524 ->escaped();
1525
1526 return Xml::tags( 'li', $attribs, $revisionRow ) . "\n";
1527 }
1528
1529 private function formatFileRow( $row ) {
1530 $file = ArchivedFile::newFromRow( $row );
1531 $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
1532 $user = $this->getUser();
1533
1534 $checkBox = '';
1535 if ( $this->mCanView && $row->fa_storage_key ) {
1536 if ( $this->mAllowed ) {
1537 $checkBox = Xml::check( 'fileid' . $row->fa_id );
1538 }
1539 $key = urlencode( $row->fa_storage_key );
1540 $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
1541 } else {
1542 $pageLink = $this->getLanguage()->userTimeAndDate( $ts, $user );
1543 }
1544 $userLink = $this->getFileUser( $file );
1545 $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1546 $bytes = $this->msg( 'parentheses' )
1547 ->rawParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
1548 ->plain();
1549 $data = htmlspecialchars( $data . ' ' . $bytes );
1550 $comment = $this->getFileComment( $file );
1551
1552 // Add show/hide deletion links if available
1553 $canHide = $this->isAllowed( 'deleterevision' );
1554 if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) {
1555 if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1556 // Revision was hidden from sysops
1557 $revdlink = Linker::revDeleteLinkDisabled( $canHide );
1558 } else {
1559 $query = array(
1560 'type' => 'filearchive',
1561 'target' => $this->mTargetObj->getPrefixedDBkey(),
1562 'ids' => $row->fa_id
1563 );
1564 $revdlink = Linker::revDeleteLink( $query,
1565 $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1566 }
1567 } else {
1568 $revdlink = '';
1569 }
1570
1571 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1572 }
1573
1574 /**
1575 * Fetch revision text link if it's available to all users
1576 *
1577 * @param Revision $rev
1578 * @param Title $titleObj
1579 * @param string $ts Timestamp
1580 * @return string
1581 */
1582 function getPageLink( $rev, $titleObj, $ts ) {
1583 $user = $this->getUser();
1584 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1585
1586 if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1587 return '<span class="history-deleted">' . $time . '</span>';
1588 }
1589
1590 $link = Linker::linkKnown(
1591 $titleObj,
1592 htmlspecialchars( $time ),
1593 array(),
1594 array(
1595 'target' => $this->mTargetObj->getPrefixedText(),
1596 'timestamp' => $ts
1597 )
1598 );
1599
1600 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1601 $link = '<span class="history-deleted">' . $link . '</span>';
1602 }
1603
1604 return $link;
1605 }
1606
1607 /**
1608 * Fetch image view link if it's available to all users
1609 *
1610 * @param File|ArchivedFile $file
1611 * @param Title $titleObj
1612 * @param string $ts A timestamp
1613 * @param string $key A storage key
1614 *
1615 * @return string HTML fragment
1616 */
1617 function getFileLink( $file, $titleObj, $ts, $key ) {
1618 $user = $this->getUser();
1619 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1620
1621 if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1622 return '<span class="history-deleted">' . $time . '</span>';
1623 }
1624
1625 $link = Linker::linkKnown(
1626 $titleObj,
1627 htmlspecialchars( $time ),
1628 array(),
1629 array(
1630 'target' => $this->mTargetObj->getPrefixedText(),
1631 'file' => $key,
1632 'token' => $user->getEditToken( $key )
1633 )
1634 );
1635
1636 if ( $file->isDeleted( File::DELETED_FILE ) ) {
1637 $link = '<span class="history-deleted">' . $link . '</span>';
1638 }
1639
1640 return $link;
1641 }
1642
1643 /**
1644 * Fetch file's user id if it's available to this user
1645 *
1646 * @param File|ArchivedFile $file
1647 * @return string HTML fragment
1648 */
1649 function getFileUser( $file ) {
1650 if ( !$file->userCan( File::DELETED_USER, $this->getUser() ) ) {
1651 return '<span class="history-deleted">' .
1652 $this->msg( 'rev-deleted-user' )->escaped() .
1653 '</span>';
1654 }
1655
1656 $link = Linker::userLink( $file->getRawUser(), $file->getRawUserText() ) .
1657 Linker::userToolLinks( $file->getRawUser(), $file->getRawUserText() );
1658
1659 if ( $file->isDeleted( File::DELETED_USER ) ) {
1660 $link = '<span class="history-deleted">' . $link . '</span>';
1661 }
1662
1663 return $link;
1664 }
1665
1666 /**
1667 * Fetch file upload comment if it's available to this user
1668 *
1669 * @param File|ArchivedFile $file
1670 * @return string HTML fragment
1671 */
1672 function getFileComment( $file ) {
1673 if ( !$file->userCan( File::DELETED_COMMENT, $this->getUser() ) ) {
1674 return '<span class="history-deleted"><span class="comment">' .
1675 $this->msg( 'rev-deleted-comment' )->escaped() . '</span></span>';
1676 }
1677
1678 $link = Linker::commentBlock( $file->getRawDescription() );
1679
1680 if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1681 $link = '<span class="history-deleted">' . $link . '</span>';
1682 }
1683
1684 return $link;
1685 }
1686
1687 function undelete() {
1688 if ( $this->getConfig()->get( 'UploadMaintenance' )
1689 && $this->mTargetObj->getNamespace() == NS_FILE
1690 ) {
1691 throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
1692 }
1693
1694 $this->checkReadOnly();
1695
1696 $out = $this->getOutput();
1697 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1698 Hooks::run( 'UndeleteForm::undelete', array( &$archive, $this->mTargetObj ) );
1699 $ok = $archive->undelete(
1700 $this->mTargetTimestamp,
1701 $this->mComment,
1702 $this->mFileVersions,
1703 $this->mUnsuppress,
1704 $this->getUser()
1705 );
1706
1707 if ( is_array( $ok ) ) {
1708 if ( $ok[1] ) { // Undeleted file count
1709 Hooks::run( 'FileUndeleteComplete', array(
1710 $this->mTargetObj, $this->mFileVersions,
1711 $this->getUser(), $this->mComment ) );
1712 }
1713
1714 $link = Linker::linkKnown( $this->mTargetObj );
1715 $out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() );
1716 } else {
1717 $out->setPageTitle( $this->msg( 'undelete-error' ) );
1718 }
1719
1720 // Show revision undeletion warnings and errors
1721 $status = $archive->getRevisionStatus();
1722 if ( $status && !$status->isGood() ) {
1723 $out->addWikiText( '<div class="error">' .
1724 $status->getWikiText(
1725 'cannotundelete',
1726 'cannotundelete'
1727 ) . '</div>'
1728 );
1729 }
1730
1731 // Show file undeletion warnings and errors
1732 $status = $archive->getFileStatus();
1733 if ( $status && !$status->isGood() ) {
1734 $out->addWikiText( '<div class="error">' .
1735 $status->getWikiText(
1736 'undelete-error-short',
1737 'undelete-error-long'
1738 ) . '</div>'
1739 );
1740 }
1741 }
1742
1743 /**
1744 * Return an array of subpages beginning with $search that this special page will accept.
1745 *
1746 * @param string $search Prefix to search for
1747 * @param int $limit Maximum number of results to return (usually 10)
1748 * @param int $offset Number of results to skip (usually 0)
1749 * @return string[] Matching subpages
1750 */
1751 public function prefixSearchSubpages( $search, $limit, $offset ) {
1752 return $this->prefixSearchString( $search, $limit, $offset );
1753 }
1754
1755 protected function getGroupName() {
1756 return 'pagetools';
1757 }
1758 }