*Clean up deletion of revisions and remove some gaps
[lhc/web/wiklou.git] / includes / SpecialUndelete.php
1 <?php
2
3 /**
4 * Special page allowing users with the appropriate permissions to view
5 * and restore deleted content
6 *
7 * @addtogroup SpecialPage
8 */
9
10 /**
11 * Constructor
12 */
13 function wfSpecialUndelete( $par ) {
14 global $wgRequest;
15
16 $form = new UndeleteForm( $wgRequest, $par );
17 $form->execute();
18 }
19
20 /**
21 * Used to show archived pages and eventually restore them.
22 * @addtogroup SpecialPage
23 */
24 class PageArchive {
25 protected $title;
26 var $fileStatus;
27
28 function __construct( $title ) {
29 if( is_null( $title ) ) {
30 throw new MWException( 'Archiver() given a null title.');
31 }
32 $this->title = $title;
33 }
34
35 /**
36 * List all deleted pages recorded in the archive table. Returns result
37 * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
38 * namespace/title.
39 *
40 * @return ResultWrapper
41 */
42 public static function listAllPages() {
43 $dbr = wfGetDB( DB_SLAVE );
44 return self::listPages( $dbr, '' );
45 }
46
47 /**
48 * List deleted pages recorded in the archive table matching the
49 * given title prefix.
50 * Returns result wrapper with (ar_namespace, ar_title, count) fields.
51 *
52 * @return ResultWrapper
53 */
54 public static function listPagesByPrefix( $prefix ) {
55 $dbr = wfGetDB( DB_SLAVE );
56
57 $title = Title::newFromText( $prefix );
58 if( $title ) {
59 $ns = $title->getNamespace();
60 $encPrefix = $dbr->escapeLike( $title->getDbKey() );
61 } else {
62 // Prolly won't work too good
63 // @todo handle bare namespace names cleanly?
64 $ns = 0;
65 $encPrefix = $dbr->escapeLike( $prefix );
66 }
67 $conds = array(
68 'ar_namespace' => $ns,
69 "ar_title LIKE '$encPrefix%'",
70 );
71 return self::listPages( $dbr, $conds );
72 }
73
74 protected static function listPages( $dbr, $condition ) {
75 return $dbr->resultObject(
76 $dbr->select(
77 array( 'archive' ),
78 array(
79 'ar_namespace',
80 'ar_title',
81 'COUNT(*) AS count'
82 ),
83 $condition,
84 __METHOD__,
85 array(
86 'GROUP BY' => 'ar_namespace,ar_title',
87 'ORDER BY' => 'ar_namespace,ar_title',
88 'LIMIT' => 100,
89 )
90 )
91 );
92 }
93
94 /**
95 * List the deleted file revisions for this page, if it's a file page.
96 * Returns a result wrapper with various filearchive fields, or null
97 * if not a file page.
98 *
99 * @return ResultWrapper
100 * @todo Does this belong in Image for fuller encapsulation?
101 */
102 function listFiles() {
103 if( $this->title->getNamespace() == NS_IMAGE ) {
104 $dbr = wfGetDB( DB_SLAVE );
105 $res = $dbr->select( 'filearchive',
106 array(
107 'fa_id',
108 'fa_name',
109 'fa_archive_name',
110 'fa_storage_key',
111 'fa_storage_group',
112 'fa_size',
113 'fa_width',
114 'fa_height',
115 'fa_bits',
116 'fa_metadata',
117 'fa_media_type',
118 'fa_major_mime',
119 'fa_minor_mime',
120 'fa_description',
121 'fa_user',
122 'fa_user_text',
123 'fa_timestamp',
124 'fa_deleted' ),
125 array( 'fa_name' => $this->title->getDbKey() ),
126 __METHOD__,
127 array( 'ORDER BY' => 'fa_timestamp DESC' ) );
128 $ret = $dbr->resultObject( $res );
129 return $ret;
130 }
131 return null;
132 }
133
134 /**
135 * Fetch (and decompress if necessary) the stored text for the deleted
136 * revision of the page with the given timestamp.
137 *
138 * @return string
139 * @deprecated Use getRevision() for more flexible information
140 */
141 function getRevisionText( $timestamp ) {
142 $rev = $this->getRevision( $timestamp );
143 return $rev ? $rev->getText() : null;
144 }
145
146 function getRevisionConds( $timestamp, $id ) {
147 if( $id ) {
148 $id = intval($id);
149 return "ar_rev_id=$id";
150 } else if( $timestamp ) {
151 return "ar_timestamp=$timestamp";
152 } else {
153 return 'ar_rev_id=0';
154 }
155 }
156
157 /**
158 * Return a Revision object containing data for the deleted revision.
159 * Note that the result *may* have a null page ID.
160 * @param string $timestamp or $id
161 * @return Revision
162 */
163 function getRevision( $timestamp, $id=null ) {
164 $dbr = wfGetDB( DB_SLAVE );
165 $row = $dbr->selectRow( 'archive',
166 array(
167 'ar_rev_id',
168 'ar_text',
169 'ar_comment',
170 'ar_user',
171 'ar_user_text',
172 'ar_timestamp',
173 'ar_minor_edit',
174 'ar_flags',
175 'ar_text_id',
176 'ar_deleted',
177 'ar_len' ),
178 array( 'ar_namespace' => $this->title->getNamespace(),
179 'ar_title' => $this->title->getDbkey(),
180 $this->getRevisionConds( $dbr->timestamp($timestamp), $id ) ),
181 __METHOD__ );
182 if( $row ) {
183 return new Revision( array(
184 'page' => $this->title->getArticleId(),
185 'id' => $row->ar_rev_id,
186 'text' => ($row->ar_text_id
187 ? null
188 : Revision::getRevisionText( $row, 'ar_' ) ),
189 'comment' => $row->ar_comment,
190 'user' => $row->ar_user,
191 'user_text' => $row->ar_user_text,
192 'timestamp' => $row->ar_timestamp,
193 'minor_edit' => $row->ar_minor_edit,
194 'text_id' => $row->ar_text_id,
195 'deleted' => $row->ar_deleted,
196 'len' => $row->ar_len) );
197 } else {
198 return null;
199 }
200 }
201
202 /**
203 * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
204 */
205 function getTextFromRow( $row ) {
206 if( is_null( $row->ar_text_id ) ) {
207 // An old row from MediaWiki 1.4 or previous.
208 // Text is embedded in this row in classic compression format.
209 return Revision::getRevisionText( $row, "ar_" );
210 } else {
211 // New-style: keyed to the text storage backend.
212 $dbr = wfGetDB( DB_SLAVE );
213 $text = $dbr->selectRow( 'text',
214 array( 'old_text', 'old_flags' ),
215 array( 'old_id' => $row->ar_text_id ),
216 __METHOD__ );
217 return Revision::getRevisionText( $text );
218 }
219 }
220
221
222 /**
223 * Fetch (and decompress if necessary) the stored text of the most
224 * recently edited deleted revision of the page.
225 *
226 * If there are no archived revisions for the page, returns NULL.
227 *
228 * @return string
229 */
230 function getLastRevisionText() {
231 $dbr = wfGetDB( DB_SLAVE );
232 $row = $dbr->selectRow( 'archive',
233 array( 'ar_text', 'ar_flags', 'ar_text_id' ),
234 array( 'ar_namespace' => $this->title->getNamespace(),
235 'ar_title' => $this->title->getDBkey() ),
236 'PageArchive::getLastRevisionText',
237 array( 'ORDER BY' => 'ar_timestamp DESC' ) );
238 if( $row ) {
239 return $this->getTextFromRow( $row );
240 } else {
241 return NULL;
242 }
243 }
244
245 /**
246 * Quick check if any archived revisions are present for the page.
247 * @return bool
248 */
249 function isDeleted() {
250 $dbr = wfGetDB( DB_SLAVE );
251 $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
252 array( 'ar_namespace' => $this->title->getNamespace(),
253 'ar_title' => $this->title->getDBkey() ) );
254 return ($n > 0);
255 }
256
257 /**
258 * Restore the given (or all) text and file revisions for the page.
259 * Once restored, the items will be removed from the archive tables.
260 * The deletion log will be updated with an undeletion notice.
261 * Use -1 for the one of the timestamps to only restore files or text
262 *
263 * @param string $pagetimestamp, restore all revisions since this time
264 * @param string $comment
265 * @param string $filetimestamp, restore all revision from this time on
266 * @param bool $Unsuppress
267 *
268 * @return true on success.
269 */
270 function undelete( $pagetimestamp = 0, $comment = '', $filetimestamp = 0, $Unsuppress = false) {
271 // If both the set of text revisions and file revisions are empty,
272 // restore everything. Otherwise, just restore the requested items.
273 $restoreAll = ($pagetimestamp==0 && $filetimestamp==0);
274
275 $restoreText = ($restoreAll || $pagetimestamp );
276 $restoreFiles = ($restoreAll || $filetimestamp );
277
278 if( $restoreText && $pagetimestamp >= 0 ) {
279 $textRestored = $this->undeleteRevisions( $pagetimestamp, $Unsuppress );
280 } else {
281 $textRestored = 0;
282 }
283
284 if( $restoreFiles && $filetimestamp >= 0 && $this->title->getNamespace()==NS_IMAGE ) {
285 $img = wfLocalFile( $this->title );
286 $this->fileStatus = $img->restore( $filetimestamp, $Unsuppress );
287 $filesRestored = $this->fileStatus->successCount;
288 } else {
289 $filesRestored = 0;
290 }
291
292 // Touch the log!
293 global $wgContLang;
294 $log = new LogPage( 'delete' );
295
296 if( $textRestored && $filesRestored ) {
297 $reason = wfMsgExt( 'undeletedrevisions-files', array('parsemag'),
298 $wgContLang->formatNum( $textRestored ),
299 $wgContLang->formatNum( $filesRestored ) );
300 } elseif( $textRestored ) {
301 $reason = wfMsgExt( 'undeletedrevisions', array('parsemag'),
302 $wgContLang->formatNum( $textRestored ) );
303 } elseif( $filesRestored ) {
304 $reason = wfMsgExt( 'undeletedfiles', array('parsemag'),
305 $wgContLang->formatNum( $filesRestored ) );
306 } else {
307 wfDebug( "Undelete: nothing undeleted...\n" );
308 return false;
309 }
310
311 if( trim( $comment ) != '' )
312 $reason .= ": {$comment}";
313 $log->addEntry( 'restore', $this->title, $reason, array($pagetimestamp,$filetimestamp) );
314
315 if ( $this->fileStatus && !$this->fileStatus->ok ) {
316 return false;
317 } else {
318 return true;
319 }
320 }
321
322 /**
323 * This is the meaty bit -- restores archived revisions of the given page
324 * to the cur/old tables. If the page currently exists, all revisions will
325 * be stuffed into old, otherwise the most recent will go into cur.
326 *
327 * @param string $timestamps, restore all revisions since this time
328 * @param string $comment
329 * @param array $fileVersions
330 * @param bool $Unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
331 *
332 * @return int number of revisions restored
333 */
334 private function undeleteRevisions( $timestamp, $Unsuppress = false ) {
335 $restoreAll = ($timestamp==0);
336
337 $dbw = wfGetDB( DB_MASTER );
338 $makepage = false; // Do we need to make a new page?
339
340 # Does this page already exist? We'll have to update it...
341 $article = new Article( $this->title );
342 $options = 'FOR UPDATE';
343 $page = $dbw->selectRow( 'page',
344 array( 'page_id', 'page_latest' ),
345 array( 'page_namespace' => $this->title->getNamespace(),
346 'page_title' => $this->title->getDBkey() ),
347 __METHOD__,
348 $options );
349
350 if( $page ) {
351 # Page already exists. Import the history, and if necessary
352 # we'll update the latest revision field in the record.
353 $newid = 0;
354 $pageId = $page->page_id;
355 $previousRevId = $page->page_latest;
356 # Get the time span of this page
357 $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
358 array( 'rev_id' => $previousRevId ),
359 __METHOD__ );
360
361 if( $previousTimestamp === false ) {
362 wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
363 return false;
364 }
365 # Do not fuck up histories by merging them in annoying, unrevertable ways
366 # This page id should match any deleted ones (excepting NULL values)
367 # We can allow restoration into redirect pages with no edit history
368 $otherpages = $dbw->selectField( 'archive', 'COUNT(*)',
369 array( 'ar_namespace' => $this->title->getNamespace(),
370 'ar_title' => $this->title->getDBkey(),
371 'ar_page_id IS NOT NULL', "ar_page_id != $pageId" ),
372 __METHOD__,
373 array('LIMIT' => 1) );
374 if( $otherpages && !$this->title->isValidRestoreOverTarget() ) {
375 return false;
376 }
377
378 } else {
379 # Have to create a new article...
380 $makepage = true;
381 $previousRevId = 0;
382 $previousTimestamp = 0;
383 }
384
385 $conditions = array(
386 'ar_namespace' => $this->title->getNamespace(),
387 'ar_title' => $this->title->getDBkey() );
388 if( $timestamp ) {
389 $conditions[] = "ar_timestamp >= {$timestamp}";
390 }
391
392 /**
393 * Select each archived revision...
394 */
395 $result = $dbw->select( 'archive',
396 /* fields */ array(
397 'ar_rev_id',
398 'ar_text',
399 'ar_comment',
400 'ar_user',
401 'ar_user_text',
402 'ar_timestamp',
403 'ar_minor_edit',
404 'ar_flags',
405 'ar_text_id',
406 'ar_deleted',
407 'ar_len' ),
408 /* WHERE */
409 $conditions,
410 __METHOD__,
411 /* options */ array(
412 'ORDER BY' => 'ar_timestamp' )
413 );
414 $ret = $dbw->resultObject( $result );
415
416 $rev_count = $dbw->numRows( $result );
417 if( $rev_count ) {
418 # We need to seek around as just using DESC in the ORDER BY
419 # would leave the revisions inserted in the wrong order
420 $first = $ret->fetchObject();
421 $ret->seek( $rev_count - 1 );
422 $last = $ret->fetchObject();
423 // We don't handle well changing the top revision's settings
424 if( !$Unsuppress && $last->ar_deleted && $last->ar_timestamp > $previousTimestamp ) {
425 wfDebug( __METHOD__.": restoration would result in a deleted top revision\n" );
426 return false;
427 }
428 $ret->seek( 0 );
429 }
430
431 if( $makepage ) {
432 $newid = $article->insertOn( $dbw );
433 $pageId = $newid;
434 }
435
436 $revision = null;
437 $restored = 0;
438
439 while( $row = $ret->fetchObject() ) {
440 if( $row->ar_text_id ) {
441 // Revision was deleted in 1.5+; text is in
442 // the regular text table, use the reference.
443 // Specify null here so the so the text is
444 // dereferenced for page length info if needed.
445 $revText = null;
446 } else {
447 // Revision was deleted in 1.4 or earlier.
448 // Text is squashed into the archive row, and
449 // a new text table entry will be created for it.
450 $revText = Revision::getRevisionText( $row, 'ar_' );
451 }
452 $revision = new Revision( array(
453 'page' => $pageId,
454 'id' => $row->ar_rev_id,
455 'text' => $revText,
456 'comment' => $row->ar_comment,
457 'user' => $row->ar_user,
458 'user_text' => $row->ar_user_text,
459 'timestamp' => $row->ar_timestamp,
460 'minor_edit' => $row->ar_minor_edit,
461 'text_id' => $row->ar_text_id,
462 'deleted' => $Unsuppress ? 0 : $row->ar_deleted,
463 'len' => $row->ar_len
464 ) );
465 $revision->insertOn( $dbw );
466 $restored++;
467 }
468
469 # If there were any revisions restored...
470 if( $revision ) {
471 // Attach the latest revision to the page...
472 $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
473
474 if( $newid || $wasnew ) {
475 // Update site stats, link tables, etc
476 $article->createUpdates( $revision );
477 }
478
479 if( $newid ) {
480 wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
481 Article::onArticleCreate( $this->title );
482 } else {
483 wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
484 Article::onArticleEdit( $this->title );
485 }
486 }
487
488 # Now that it's safely stored, take it out of the archive
489 $dbw->delete( 'archive',
490 /* WHERE */
491 $conditions,
492 __METHOD__ );
493 # Update any revision left to reflect the page they belong to.
494 # If a page was deleted, and a new one created over it, then deleted,
495 # selective restore acts as a way to seperate the two. Nevertheless, we
496 # still want the rest to be restorable, in case some mistake was made.
497 $dbw->update( 'archive',
498 array( 'ar_page_id' => $newid ),
499 array( 'ar_namespace' => $this->title->getNamespace(),
500 'ar_title' => $this->title->getDBkey() ),
501 __METHOD__ );
502
503 return $restored;
504 }
505
506 function getFileStatus() { return $this->fileStatus; }
507 }
508
509 /**
510 * The HTML form for Special:Undelete, which allows users with the appropriate
511 * permissions to view and restore deleted content.
512 * @addtogroup SpecialPage
513 */
514 class UndeleteForm {
515 var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj;
516 var $mTargetTimestamp, $mAllowed, $mComment;
517
518 function UndeleteForm( $request, $par = "" ) {
519 global $wgUser;
520 $this->mAction = $request->getVal( 'action' );
521 $this->mTarget = $request->getVal( 'target' );
522 $this->mSearchPrefix = $request->getText( 'prefix' );
523 $time = $request->getVal( 'timestamp' );
524 $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
525 $this->mFile = $request->getVal( 'file' );
526 $this->mDiff = $request->getVal( 'diff' );
527 $this->mOldid = $request->getVal( 'oldid' );
528
529 $posted = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
530 $this->mRestore = $request->getCheck( 'restore' ) && $posted;
531 $this->mPreview = $request->getCheck( 'preview' ) && $posted;
532 $this->mComment = $request->getText( 'wpComment' );
533 $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'oversight' );
534
535 if( $par != "" ) {
536 $this->mTarget = $par;
537 $_GET['target'] = $par; // hack for Pager
538 }
539 if( $wgUser->isAllowed( 'delete' ) && !$wgUser->isBlocked() ) {
540 $this->mAllowed = true;
541 } else {
542 $this->mAllowed = false;
543 $this->mTimestamp = '';
544 $this->mRestore = false;
545 }
546 if( $this->mTarget !== "" ) {
547 $this->mTargetObj = Title::newFromURL( $this->mTarget );
548 } else {
549 $this->mTargetObj = NULL;
550 }
551 if( $this->mRestore ) {
552 $this->mFileTimestamp = $request->getVal('imgrestorepoint');
553 $this->mPageTimestamp = $request->getVal('restorepoint');
554 }
555 $this->preCacheMessages();
556 }
557
558 /**
559 * As we use the same small set of messages in various methods and that
560 * they are called often, we call them once and save them in $this->message
561 */
562 function preCacheMessages() {
563 // Precache various messages
564 if( !isset( $this->message ) ) {
565 foreach( explode(' ', 'last rev-delundel' ) as $msg ) {
566 $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
567 }
568 }
569 }
570
571 function execute() {
572 global $wgOut, $wgUser;
573 if( $this->mAllowed ) {
574 $wgOut->setPagetitle( wfMsgHtml( "undeletepage" ) );
575 } else {
576 $wgOut->setPagetitle( wfMsgHtml( "viewdeletedpage" ) );
577 }
578
579 if( is_null( $this->mTargetObj ) ) {
580 # Not all users can just browse every deleted page from the list
581 if( $wgUser->isAllowed( 'browsearchive' ) ) {
582 $this->showSearchForm();
583
584 # List undeletable articles
585 if( $this->mSearchPrefix ) {
586 $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
587 $this->showList( $result );
588 }
589 } else {
590 $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
591 }
592 return;
593 }
594 if( $this->mTimestamp !== '' ) {
595 return $this->showRevision( $this->mTimestamp );
596 }
597
598 if( $this->mDiff && $this->mOldid )
599 return $this->showDiff( $this->mDiff, $this->mOldid );
600
601 if( $this->mFile !== null ) {
602 $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
603 // Check if user is allowed to see this file
604 if( !$file->userCan( File::DELETED_FILE ) ) {
605 $wgOut->permissionRequired( 'hiderevision' );
606 return false;
607 } else {
608 return $this->showFile( $this->mFile );
609 }
610 }
611
612 if( $this->mRestore && $this->mAction == "submit" ) {
613 return $this->undelete();
614 }
615 return $this->showHistory();
616 }
617
618 function showSearchForm() {
619 global $wgOut, $wgScript;
620 $wgOut->addWikiText( wfMsg( 'undelete-header' ) );
621
622 $wgOut->addHtml(
623 Xml::openElement( 'form', array(
624 'method' => 'get',
625 'action' => $wgScript ) ) .
626 '<fieldset>' .
627 Xml::element( 'legend', array(),
628 wfMsg( 'undelete-search-box' ) ) .
629 Xml::hidden( 'title',
630 SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) .
631 Xml::inputLabel( wfMsg( 'undelete-search-prefix' ),
632 'prefix', 'prefix', 20,
633 $this->mSearchPrefix ) .
634 Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) .
635 '</fieldset>' .
636 '</form>' );
637 }
638
639 // Generic list of deleted pages
640 private function showList( $result ) {
641 global $wgLang, $wgContLang, $wgUser, $wgOut;
642
643 if( $result->numRows() == 0 ) {
644 $wgOut->addWikiText( wfMsg( 'undelete-no-results' ) );
645 return;
646 }
647
648 $wgOut->addWikiText( wfMsg( "undeletepagetext" ) );
649
650 $sk = $wgUser->getSkin();
651 $undelete = SpecialPage::getTitleFor( 'Undelete' );
652 $wgOut->addHTML( "<ul>\n" );
653 while( $row = $result->fetchObject() ) {
654 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
655 $link = $sk->makeKnownLinkObj( $undelete, htmlspecialchars( $title->getPrefixedText() ), 'target=' . $title->getPrefixedUrl() );
656 #$revs = wfMsgHtml( 'undeleterevisions', $wgLang->formatNum( $row->count ) );
657 $revs = wfMsgExt( 'undeleterevisions',
658 array( 'parseinline' ),
659 $wgLang->formatNum( $row->count ) );
660 $wgOut->addHtml( "<li>{$link} ({$revs})</li>\n" );
661 }
662 $result->free();
663 $wgOut->addHTML( "</ul>\n" );
664
665 return true;
666 }
667
668 private function showRevision( $timestamp ) {
669 global $wgLang, $wgUser, $wgOut;
670 $self = SpecialPage::getTitleFor( 'Undelete' );
671 $skin = $wgUser->getSkin();
672
673 if(!preg_match("/[0-9]{14}/",$timestamp)) return 0;
674
675 $archive = new PageArchive( $this->mTargetObj );
676 $rev = $archive->getRevision( $timestamp );
677
678 if( !$rev ) {
679 $wgOut->addWikiText( wfMsg( 'undeleterevision-missing' ) );
680 return;
681 }
682
683 if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
684 if( !$rev->userCan(Revision::DELETED_TEXT) ) {
685 $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
686 return;
687 } else {
688 $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
689 $wgOut->addHTML( '<br/>' );
690 // and we are allowed to see...
691 }
692 }
693
694 $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
695
696 $link = $skin->makeKnownLinkObj( $self,
697 htmlspecialchars( $this->mTargetObj->getPrefixedText() ),
698 'target=' . $this->mTargetObj->getPrefixedUrl()
699 );
700 $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp ) );
701 $user = $skin->userLink( $rev->getUser(), $rev->getUserText() )
702 . $skin->userToolLinks( $rev->getUser(), $rev->getUserText() );
703
704 $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' );
705
706 wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
707
708 if( $this->mPreview ) {
709 $wgOut->addHtml( "<hr />\n" );
710 $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, false );
711 }
712
713 $wgOut->addHtml(
714 wfElement( 'textarea', array(
715 'readonly' => 'readonly',
716 'cols' => intval( $wgUser->getOption( 'cols' ) ),
717 'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
718 $rev->revText() . "\n" ) .
719 wfOpenElement( 'div' ) .
720 wfOpenElement( 'form', array(
721 'method' => 'post',
722 'action' => $self->getLocalURL( "action=submit" ) ) ) .
723 wfElement( 'input', array(
724 'type' => 'hidden',
725 'name' => 'target',
726 'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
727 wfElement( 'input', array(
728 'type' => 'hidden',
729 'name' => 'timestamp',
730 'value' => $timestamp ) ) .
731 wfElement( 'input', array(
732 'type' => 'hidden',
733 'name' => 'wpEditToken',
734 'value' => $wgUser->editToken() ) ) .
735 wfElement( 'input', array(
736 'type' => 'hidden',
737 'name' => 'preview',
738 'value' => '1' ) ) .
739 wfElement( 'input', array(
740 'type' => 'submit',
741 'value' => wfMsg( 'showpreview' ) ) ) .
742 wfCloseElement( 'form' ) .
743 wfCloseElement( 'div' ) );
744 }
745
746 /**
747 * Show the changes between two deleted revisions
748 */
749 private function showDiff( $newid, $oldid ) {
750 global $wgOut, $wgUser, $wgLang;
751
752 if( is_null($this->mTargetObj) )
753 return;
754 $skin = $wgUser->getSkin();
755
756 $archive = new PageArchive( $this->mTargetObj );
757 $oldRev = $archive->getRevision( null, $oldid );
758 $newRev = $archive->getRevision( null, $newid );
759
760 if( !$oldRev || !$newRev )
761 return;
762
763 $oldTitle = $this->mTargetObj->getPrefixedText();
764 $wgOut->addHtml( "<center><h3>$oldTitle</h3></center>" );
765
766 $oldminor = $newminor = '';
767
768 if($oldRev->mMinorEdit == 1) {
769 $oldminor = wfElement( 'span', array( 'class' => 'minor' ),
770 wfMsg( 'minoreditletter') ) . ' ';
771 }
772
773 if($newRev->mMinorEdit == 1) {
774 $newminor = wfElement( 'span', array( 'class' => 'minor' ),
775 wfMsg( 'minoreditletter') ) . ' ';
776 }
777
778 $ot = $wgLang->timeanddate( $oldRev->getTimestamp(), true );
779 $nt = $wgLang->timeanddate( $newRev->getTimestamp(), true );
780 $oldHeader = htmlspecialchars( wfMsg( 'revisionasof', $ot ) ) . "<br />" .
781 $skin->revUserTools( $oldRev, true ) . "<br />" .
782 $oldminor . $skin->revComment( $oldRev, false ) . "<br />";
783 $newHeader = htmlspecialchars( wfMsg( 'revisionasof', $nt ) ) . "<br />" .
784 $skin->revUserTools( $newRev, true ) . " <br />" .
785 $newminor . $skin->revComment( $newRev, false ) . "<br />";
786
787 $otext = $oldRev->revText();
788 $ntext = $newRev->revText();
789
790 $wgOut->addStyle( 'common/diff.css' );
791 $wgOut->addHtml(
792 "<div>" .
793 "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
794 "<col class='diff-marker' />" .
795 "<col class='diff-content' />" .
796 "<col class='diff-marker' />" .
797 "<col class='diff-content' />" .
798 "<tr>" .
799 "<td colspan='2' width='50%' align='center' class='diff-otitle'>" . $oldHeader . "</td>" .
800 "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" . $newHeader . "</td>" .
801 "</tr>" .
802 DifferenceEngine::generateDiffBody( $otext, $ntext ) .
803 "</table>" .
804 "</div>\n" );
805
806 return true;
807 }
808
809 /**
810 * Show a deleted file version requested by the visitor.
811 */
812 private function showFile( $key ) {
813 global $wgOut, $wgRequest;
814 $wgOut->disable();
815
816 # We mustn't allow the output to be Squid cached, otherwise
817 # if an admin previews a deleted image, and it's cached, then
818 # a user without appropriate permissions can toddle off and
819 # nab the image, and Squid will serve it
820 $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
821 $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
822 $wgRequest->response()->header( 'Pragma: no-cache' );
823
824 $store = FileStore::get( 'deleted' );
825 $store->stream( $key );
826 }
827
828 private function showHistory() {
829 global $wgLang, $wgContLang, $wgUser, $wgOut;
830
831 $this->sk = $wgUser->getSkin();
832 if( $this->mAllowed ) {
833 $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
834 } else {
835 $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
836 }
837
838 $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
839
840 $archive = new PageArchive( $this->mTargetObj );
841
842 if( $this->mAllowed ) {
843 $wgOut->addWikiText( '<p>' . wfMsgHtml( "undeletehistory" ) . '</p>' );
844 $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
845 } else {
846 $wgOut->addWikiText( wfMsgHtml( "undeletehistorynoadmin" ) );
847 }
848
849 # List all stored revisions
850 $revisions = new UndeleteRevisionsPager( $this, array(), $this->mTargetObj );
851 $files = $archive->listFiles();
852
853 $haveRevisions = $revisions && $revisions->getNumRows() > 0;
854 $haveFiles = $files && $files->numRows() > 0;
855
856 # Batch existence check on user and talk pages
857 if( $haveFiles ) {
858 $batch = new LinkBatch();
859 while( $row = $files->fetchObject() ) {
860 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
861 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
862 }
863 $batch->execute();
864 $files->seek( 0 );
865 }
866
867 if( $this->mAllowed ) {
868 $titleObj = SpecialPage::getTitleFor( "Undelete" );
869 $action = $titleObj->getLocalURL( "action=submit" );
870 # Start the form here
871 $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
872 $wgOut->addHtml( $top );
873 }
874
875 if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
876 # Format the user-visible controls (comment field, submission button)
877 # in a nice little table
878 $align = $wgContLang->isRtl() ? 'left' : 'right';
879 $table =
880 Xml::openElement( 'fieldset' ) .
881 Xml::openElement( 'table' ) .
882 "<tr>
883 <td colspan='2'>" .
884 wfMsgWikiHtml( 'undeleteextrahelp' ) .
885 "</td>
886 </tr>
887 <tr>
888 <td align='$align'>" .
889 Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
890 "</td>
891 <td>" .
892 Xml::input( 'wpComment', 50, $this->mComment ) .
893 "</td>
894 </tr>
895 <tr>
896 <td>&nbsp;</td>
897 <td>" .
898 Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
899 Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
900 Xml::openElement( 'p' ) .
901 Xml::check( 'wpUnsuppress', $this->mUnsuppress, array('id' => 'mw-undelete-unsupress') ) . ' ' .
902 Xml::label( wfMsgHtml('revdelete-unsuppress'), 'mw-undelete-unsupress' ) .
903 Xml::closeElement( 'p' ) .
904 "</td>
905 </tr>" .
906 Xml::closeElement( 'table' ) .
907 Xml::closeElement( 'fieldset' );
908
909 $wgOut->addHtml( $table );
910 }
911
912 $wgOut->addHTML( "<h2 id=\"pagehistory\">" . wfMsgHtml( "history" ) . "</h2>\n" );
913
914 if( $haveRevisions ) {
915 $wgOut->addHTML( '<p>' . wfMsgHtml( "restorepoint" ) . '</p>' );
916 $wgOut->addHTML( $revisions->getNavigationBar() );
917 $wgOut->addHTML( "<ul>" );
918 $wgOut->addHTML( "<li>" . wfRadio( "restorepoint", -1, false ) . " " . wfMsgHtml('restorenone') . "</li>" );
919 $wgOut->addHTML( $revisions->getBody() );
920 $wgOut->addHTML( "</ul>" );
921 $wgOut->addHTML( $revisions->getNavigationBar() );
922 } else {
923 $wgOut->addWikiText( wfMsg( "nohistory" ) );
924 }
925 if( $haveFiles ) {
926 $wgOut->addHtml( "<h2 id=\"filehistory\">" . wfMsgHtml( 'filehist' ) . "</h2>\n" );
927 $wgOut->addHTML( wfMsgHtml( "restorepoint" ) );
928 $wgOut->addHtml( "<ul>" );
929 $wgOut->addHTML( "<li>" . wfRadio( "imgrestorepoint", -1, false ) . " " . wfMsgHtml('restorenone') . "</li>" );
930 while( $row = $files->fetchObject() ) {
931 $file = ArchivedFile::newFromRow( $row );
932
933 $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
934 if( $this->mAllowed && $row->fa_storage_key ) {
935 $checkBox = wfRadio( "imgrestorepoint", $ts, false );
936 $key = urlencode( $row->fa_storage_key );
937 $target = urlencode( $this->mTarget );
938 $pageLink = $this->getFileLink( $file, $titleObj, $ts, $target, $key );
939 } else {
940 $checkBox = '';
941 $pageLink = $wgLang->timeanddate( $ts, true );
942 }
943 $userLink = $this->getFileUser( $file );
944 $data =
945 wfMsgHtml( 'widthheight',
946 $wgLang->formatNum( $row->fa_width ),
947 $wgLang->formatNum( $row->fa_height ) ) .
948 ' (' .
949 wfMsgHtml( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
950 ')';
951 $comment = $this->getFileComment( $file );
952 $rd='';
953 if( $wgUser->isAllowed( 'deleterevision' ) ) {
954 $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
955 if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
956 // If revision was hidden from sysops
957 $del = $this->message['rev-delundel'];
958 } else {
959 $del = $this->sk->makeKnownLinkObj( $revdel,
960 $this->message['rev-delundel'],
961 'target=' . urlencode( $this->mTarget ) .
962 '&fileid=' . urlencode( $row->fa_id ) );
963 // Bolden oversighted content
964 if( $file->isDeleted( File::DELETED_RESTRICTED ) )
965 $del = "<strong>$del</strong>";
966 }
967 $rd = "<tt>(<small>$del</small>)</tt>";
968 }
969 $wgOut->addHTML( "<li>$checkBox $rd $pageLink . . $userLink $data $comment</li>\n" );
970 }
971 $files->free();
972 $wgOut->addHTML( "</ul>" );
973 }
974
975 # Show relevant lines from the deletion log:
976 $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
977 $logViewer = new LogViewer(
978 new LogReader(
979 new FauxRequest(
980 array( 'page' => $this->mTargetObj->getPrefixedText(),
981 'type' => 'delete' ) ) ) );
982 $logViewer->showList( $wgOut );
983
984 if( $this->mAllowed ) {
985 # Slip in the hidden controls here
986 $misc = Xml::hidden( 'target', $this->mTarget );
987 $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
988 $misc .= Xml::closeElement( 'form' );
989 $wgOut->addHtml( $misc );
990 }
991
992 return true;
993 }
994
995 function formatRevisionRow( $row ) {
996 global $wgUser, $wgLang;
997
998 $rev = new Revision( array(
999 'page' => $this->mTargetObj->getArticleId(),
1000 'id' => $row->ar_rev_id,
1001 'comment' => $row->ar_comment,
1002 'user' => $row->ar_user,
1003 'user_text' => $row->ar_user_text,
1004 'timestamp' => $row->ar_timestamp,
1005 'minor_edit' => $row->ar_minor_edit,
1006 'text_id' => $row->ar_text_id,
1007 'deleted' => $row->ar_deleted,
1008 'len' => $row->ar_len) );
1009
1010 $stxt = '';
1011 $last = $this->message['last'];
1012
1013 if( $this->mAllowed ) {
1014 $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
1015 $checkBox = wfRadio( "restorepoint", $ts, false );
1016 $titleObj = SpecialPage::getTitleFor( "Undelete" );
1017 $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $this->mTarget );
1018 # Last link
1019 if( !$rev->userCan( Revision::DELETED_TEXT ) )
1020 $last = $this->message['last'];
1021 else if( isset($this->prevId[$row->ar_rev_id]) )
1022 $last = $this->sk->makeKnownLinkObj( $titleObj, $this->message['last'], "target=" . $this->mTarget .
1023 "&diff=" . $row->ar_rev_id . "&oldid=" . $this->prevId[$row->ar_rev_id] );
1024 } else {
1025 $checkBox = '';
1026 $pageLink = $wgLang->timeanddate( $ts, true );
1027 }
1028 $userLink = $this->sk->revUserTools( $rev );
1029
1030 if(!is_null($size = $row->ar_len)) {
1031 if($size == 0)
1032 $stxt = wfMsgHtml('historyempty');
1033 else
1034 $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
1035 }
1036 $comment = $this->sk->revComment( $rev );
1037 $revd='';
1038 if( $wgUser->isAllowed( 'deleterevision' ) ) {
1039 $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
1040 if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
1041 // If revision was hidden from sysops
1042 $del = $this->message['rev-delundel'];
1043 } else {
1044 $del = $this->sk->makeKnownLinkObj( $revdel,
1045 $this->message['rev-delundel'],
1046 'target=' . urlencode( $this->mTarget ) .
1047 '&artimestamp=' . urlencode( $row->ar_timestamp ) );
1048 // Bolden oversighted content
1049 if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
1050 $del = "<strong>$del</strong>";
1051 }
1052 $revd = "<tt>(<small>$del</small>)</tt>";
1053 }
1054
1055 return "<li>$checkBox $revd ($last) $pageLink . . $userLink $stxt $comment</li>";
1056 }
1057
1058 /**
1059 * Fetch revision text link if it's available to all users
1060 * @return string
1061 */
1062 function getPageLink( $rev, $titleObj, $ts, $target ) {
1063 global $wgLang;
1064
1065 if( !$rev->userCan(Revision::DELETED_TEXT) ) {
1066 return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
1067 } else {
1068 $link = $this->sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );
1069 if( $rev->isDeleted(Revision::DELETED_TEXT) )
1070 $link = '<span class="history-deleted">' . $link . '</span>';
1071 return $link;
1072 }
1073 }
1074
1075 /**
1076 * Fetch image view link if it's available to all users
1077 * @return string
1078 */
1079 function getFileLink( $file, $titleObj, $ts, $target, $key ) {
1080 global $wgLang;
1081
1082 if( !$file->userCan(File::DELETED_FILE) ) {
1083 return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
1084 } else {
1085 $link = $this->sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target&file=$key" );
1086 if( $file->isDeleted(File::DELETED_FILE) )
1087 $link = '<span class="history-deleted">' . $link . '</span>';
1088 return $link;
1089 }
1090 }
1091
1092 /**
1093 * Fetch file's user id if it's available to this user
1094 * @return string
1095 */
1096 function getFileUser( $file ) {
1097 if( !$file->userCan(File::DELETED_USER) ) {
1098 return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
1099 } else {
1100 $link = $this->sk->userLink( $file->user, $file->userText ) .
1101 $this->sk->userToolLinks( $file->user, $file->userText );
1102 if( $file->isDeleted(File::DELETED_USER) )
1103 $link = '<span class="history-deleted">' . $link . '</span>';
1104 return $link;
1105 }
1106 }
1107
1108 /**
1109 * Fetch file upload comment if it's available to this user
1110 * @return string
1111 */
1112 function getFileComment( $file ) {
1113 if( !$file->userCan(File::DELETED_COMMENT) ) {
1114 return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
1115 } else {
1116 $link = $this->sk->commentBlock( $file->description );
1117 if( $file->isDeleted(File::DELETED_COMMENT) )
1118 $link = '<span class="history-deleted">' . $link . '</span>';
1119 return $link;
1120 }
1121 }
1122
1123 function undelete() {
1124 global $wgOut, $wgUser;
1125 if( !is_null( $this->mTargetObj ) ) {
1126 $archive = new PageArchive( $this->mTargetObj );
1127
1128 $ok = $archive->undelete(
1129 $this->mPageTimestamp,
1130 $this->mComment,
1131 $this->mFileTimestamp,
1132 $this->mUnsuppress );
1133 if( $ok ) {
1134 $skin = $wgUser->getSkin();
1135 $link = $skin->makeKnownLinkObj( $this->mTargetObj, $this->mTargetObj->getPrefixedText(), 'redirect=no' );
1136 $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
1137 } else {
1138 $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
1139 $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
1140 }
1141
1142 // Show file deletion warnings and errors
1143 $status = $archive->getFileStatus();
1144 if ( $status && !$status->isGood() ) {
1145 $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
1146 }
1147 } else {
1148 $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
1149 }
1150 return false;
1151 }
1152 }
1153
1154 class UndeleteRevisionsPager extends ReverseChronologicalPager {
1155 public $mForm, $mConds;
1156
1157 function __construct( $form, $conds = array(), $title ) {
1158 $this->mForm = $form;
1159 $this->mConds = $conds;
1160 $this->title = $title;
1161 parent::__construct();
1162 }
1163
1164 function getStartBody() {
1165 wfProfileIn( __METHOD__ );
1166 # Do a link batch query
1167 $this->mResult->seek( 0 );
1168 $batch = new LinkBatch();
1169 # Give some pointers to make (last) links
1170 $this->mForm->prevId = array();
1171 while( $row = $this->mResult->fetchObject() ) {
1172 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
1173 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
1174
1175 $rev_id = isset($rev_id) ? $rev_id : $row->ar_rev_id;
1176 if( $rev_id > $row->ar_rev_id )
1177 $this->mForm->prevId[$rev_id] = $row->ar_rev_id;
1178 else if( $rev_id < $row->ar_rev_id )
1179 $this->mForm->prevId[$row->ar_rev_id] = $rev_id;
1180
1181 $rev_id = $row->ar_rev_id;
1182 }
1183
1184 $batch->execute();
1185 $this->mResult->seek( 0 );
1186
1187 wfProfileOut( __METHOD__ );
1188 return '';
1189 }
1190
1191 function formatRow( $row ) {
1192 $block = new Block;
1193 return $this->mForm->formatRevisionRow( $row );
1194 }
1195
1196 function getQueryInfo() {
1197 $conds = $this->mConds;
1198 $conds['ar_namespace'] = $this->title->getNamespace();
1199 $conds['ar_title'] = $this->title->getDBkey();
1200 return array(
1201 'tables' => array('archive'),
1202 'fields' => array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment',
1203 'ar_rev_id', 'ar_text_id', 'ar_len', 'ar_deleted' ),
1204 'conds' => $conds
1205 );
1206 }
1207
1208 function getIndexField() {
1209 return 'ar_timestamp';
1210 }
1211 }