Merge "Database: clean up lockTables() and add postgres support"
[lhc/web/wiklou.git] / includes / page / PageArchive.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21 use MediaWiki\MediaWikiServices;
22 use Wikimedia\Rdbms\ResultWrapper;
23 use Wikimedia\Rdbms\IDatabase;
24
25 /**
26 * Used to show archived pages and eventually restore them.
27 */
28 class PageArchive {
29 /** @var Title */
30 protected $title;
31
32 /** @var Status */
33 protected $fileStatus;
34
35 /** @var Status */
36 protected $revisionStatus;
37
38 /** @var Config */
39 protected $config;
40
41 public function __construct( $title, Config $config = null ) {
42 if ( is_null( $title ) ) {
43 throw new MWException( __METHOD__ . ' given a null title.' );
44 }
45 $this->title = $title;
46 if ( $config === null ) {
47 wfDebug( __METHOD__ . ' did not have a Config object passed to it' );
48 $config = MediaWikiServices::getInstance()->getMainConfig();
49 }
50 $this->config = $config;
51 }
52
53 public function doesWrites() {
54 return true;
55 }
56
57 /**
58 * List all deleted pages recorded in the archive table. Returns result
59 * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
60 * namespace/title.
61 *
62 * @return ResultWrapper
63 */
64 public static function listAllPages() {
65 $dbr = wfGetDB( DB_REPLICA );
66
67 return self::listPages( $dbr, '' );
68 }
69
70 /**
71 * List deleted pages recorded in the archive matching the
72 * given term, using search engine archive.
73 * Returns result wrapper with (ar_namespace, ar_title, count) fields.
74 *
75 * @param string $term Search term
76 * @return ResultWrapper
77 */
78 public static function listPagesBySearch( $term ) {
79 $title = Title::newFromText( $term );
80 if ( $title ) {
81 $ns = $title->getNamespace();
82 $termMain = $title->getText();
83 $termDb = $title->getDBkey();
84 } else {
85 // Prolly won't work too good
86 // @todo handle bare namespace names cleanly?
87 $ns = 0;
88 $termMain = $termDb = $term;
89 }
90
91 // Try search engine first
92 $engine = MediaWikiServices::getInstance()->newSearchEngine();
93 $engine->setLimitOffset( 100 );
94 $engine->setNamespaces( [ $ns ] );
95 $results = $engine->searchArchiveTitle( $termMain );
96 if ( !$results->isOK() ) {
97 $results = [];
98 } else {
99 $results = $results->getValue();
100 }
101
102 if ( !$results ) {
103 // Fall back to regular prefix search
104 return self::listPagesByPrefix( $term );
105 }
106
107 $dbr = wfGetDB( DB_REPLICA );
108 $condTitles = array_unique( array_map( function ( Title $t ) {
109 return $t->getDBkey();
110 }, $results ) );
111 $conds = [
112 'ar_namespace' => $ns,
113 $dbr->makeList( [ 'ar_title' => $condTitles ], LIST_OR ) . " OR ar_title " .
114 $dbr->buildLike( $termDb, $dbr->anyString() )
115 ];
116
117 return self::listPages( $dbr, $conds );
118 }
119
120 /**
121 * List deleted pages recorded in the archive table matching the
122 * given title prefix.
123 * Returns result wrapper with (ar_namespace, ar_title, count) fields.
124 *
125 * @param string $prefix Title prefix
126 * @return ResultWrapper
127 */
128 public static function listPagesByPrefix( $prefix ) {
129 $dbr = wfGetDB( DB_REPLICA );
130
131 $title = Title::newFromText( $prefix );
132 if ( $title ) {
133 $ns = $title->getNamespace();
134 $prefix = $title->getDBkey();
135 } else {
136 // Prolly won't work too good
137 // @todo handle bare namespace names cleanly?
138 $ns = 0;
139 }
140
141 $conds = [
142 'ar_namespace' => $ns,
143 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
144 ];
145
146 return self::listPages( $dbr, $conds );
147 }
148
149 /**
150 * @param IDatabase $dbr
151 * @param string|array $condition
152 * @return bool|ResultWrapper
153 */
154 protected static function listPages( $dbr, $condition ) {
155 return $dbr->select(
156 [ 'archive' ],
157 [
158 'ar_namespace',
159 'ar_title',
160 'count' => 'COUNT(*)'
161 ],
162 $condition,
163 __METHOD__,
164 [
165 'GROUP BY' => [ 'ar_namespace', 'ar_title' ],
166 'ORDER BY' => [ 'ar_namespace', 'ar_title' ],
167 'LIMIT' => 100,
168 ]
169 );
170 }
171
172 /**
173 * List the revisions of the given page. Returns result wrapper with
174 * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
175 *
176 * @return ResultWrapper
177 */
178 public function listRevisions() {
179 $dbr = wfGetDB( DB_REPLICA );
180
181 $tables = [ 'archive' ];
182
183 $fields = [
184 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
185 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
186 'ar_page_id'
187 ];
188
189 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
190 $fields[] = 'ar_content_format';
191 $fields[] = 'ar_content_model';
192 }
193
194 $conds = [ 'ar_namespace' => $this->title->getNamespace(),
195 'ar_title' => $this->title->getDBkey() ];
196
197 $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
198
199 $join_conds = [];
200
201 ChangeTags::modifyDisplayQuery(
202 $tables,
203 $fields,
204 $conds,
205 $join_conds,
206 $options,
207 ''
208 );
209
210 return $dbr->select( $tables,
211 $fields,
212 $conds,
213 __METHOD__,
214 $options,
215 $join_conds
216 );
217 }
218
219 /**
220 * List the deleted file revisions for this page, if it's a file page.
221 * Returns a result wrapper with various filearchive fields, or null
222 * if not a file page.
223 *
224 * @return ResultWrapper
225 * @todo Does this belong in Image for fuller encapsulation?
226 */
227 public function listFiles() {
228 if ( $this->title->getNamespace() != NS_FILE ) {
229 return null;
230 }
231
232 $dbr = wfGetDB( DB_REPLICA );
233 return $dbr->select(
234 'filearchive',
235 ArchivedFile::selectFields(),
236 [ 'fa_name' => $this->title->getDBkey() ],
237 __METHOD__,
238 [ 'ORDER BY' => 'fa_timestamp DESC' ]
239 );
240 }
241
242 /**
243 * Return a Revision object containing data for the deleted revision.
244 * Note that the result *may* or *may not* have a null page ID.
245 *
246 * @param string $timestamp
247 * @return Revision|null
248 */
249 public function getRevision( $timestamp ) {
250 $dbr = wfGetDB( DB_REPLICA );
251
252 $fields = [
253 'ar_rev_id',
254 'ar_text',
255 'ar_comment',
256 'ar_user',
257 'ar_user_text',
258 'ar_timestamp',
259 'ar_minor_edit',
260 'ar_flags',
261 'ar_text_id',
262 'ar_deleted',
263 'ar_len',
264 'ar_sha1',
265 ];
266
267 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
268 $fields[] = 'ar_content_format';
269 $fields[] = 'ar_content_model';
270 }
271
272 $row = $dbr->selectRow( 'archive',
273 $fields,
274 [ 'ar_namespace' => $this->title->getNamespace(),
275 'ar_title' => $this->title->getDBkey(),
276 'ar_timestamp' => $dbr->timestamp( $timestamp ) ],
277 __METHOD__ );
278
279 if ( $row ) {
280 return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
281 }
282
283 return null;
284 }
285
286 /**
287 * Return the most-previous revision, either live or deleted, against
288 * the deleted revision given by timestamp.
289 *
290 * May produce unexpected results in case of history merges or other
291 * unusual time issues.
292 *
293 * @param string $timestamp
294 * @return Revision|null Null when there is no previous revision
295 */
296 public function getPreviousRevision( $timestamp ) {
297 $dbr = wfGetDB( DB_REPLICA );
298
299 // Check the previous deleted revision...
300 $row = $dbr->selectRow( 'archive',
301 'ar_timestamp',
302 [ 'ar_namespace' => $this->title->getNamespace(),
303 'ar_title' => $this->title->getDBkey(),
304 'ar_timestamp < ' .
305 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
306 __METHOD__,
307 [
308 'ORDER BY' => 'ar_timestamp DESC',
309 'LIMIT' => 1 ] );
310 $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
311
312 $row = $dbr->selectRow( [ 'page', 'revision' ],
313 [ 'rev_id', 'rev_timestamp' ],
314 [
315 'page_namespace' => $this->title->getNamespace(),
316 'page_title' => $this->title->getDBkey(),
317 'page_id = rev_page',
318 'rev_timestamp < ' .
319 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
320 __METHOD__,
321 [
322 'ORDER BY' => 'rev_timestamp DESC',
323 'LIMIT' => 1 ] );
324 $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
325 $prevLiveId = $row ? intval( $row->rev_id ) : null;
326
327 if ( $prevLive && $prevLive > $prevDeleted ) {
328 // Most prior revision was live
329 return Revision::newFromId( $prevLiveId );
330 } elseif ( $prevDeleted ) {
331 // Most prior revision was deleted
332 return $this->getRevision( $prevDeleted );
333 }
334
335 // No prior revision on this page.
336 return null;
337 }
338
339 /**
340 * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
341 *
342 * @param object $row Database row
343 * @return string
344 */
345 public function getTextFromRow( $row ) {
346 if ( is_null( $row->ar_text_id ) ) {
347 // An old row from MediaWiki 1.4 or previous.
348 // Text is embedded in this row in classic compression format.
349 return Revision::getRevisionText( $row, 'ar_' );
350 }
351
352 // New-style: keyed to the text storage backend.
353 $dbr = wfGetDB( DB_REPLICA );
354 $text = $dbr->selectRow( 'text',
355 [ 'old_text', 'old_flags' ],
356 [ 'old_id' => $row->ar_text_id ],
357 __METHOD__ );
358
359 return Revision::getRevisionText( $text );
360 }
361
362 /**
363 * Fetch (and decompress if necessary) the stored text of the most
364 * recently edited deleted revision of the page.
365 *
366 * If there are no archived revisions for the page, returns NULL.
367 *
368 * @return string|null
369 */
370 public function getLastRevisionText() {
371 $dbr = wfGetDB( DB_REPLICA );
372 $row = $dbr->selectRow( 'archive',
373 [ 'ar_text', 'ar_flags', 'ar_text_id' ],
374 [ 'ar_namespace' => $this->title->getNamespace(),
375 'ar_title' => $this->title->getDBkey() ],
376 __METHOD__,
377 [ 'ORDER BY' => 'ar_timestamp DESC' ] );
378
379 if ( $row ) {
380 return $this->getTextFromRow( $row );
381 }
382
383 return null;
384 }
385
386 /**
387 * Quick check if any archived revisions are present for the page.
388 *
389 * @return bool
390 */
391 public function isDeleted() {
392 $dbr = wfGetDB( DB_REPLICA );
393 $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
394 [ 'ar_namespace' => $this->title->getNamespace(),
395 'ar_title' => $this->title->getDBkey() ],
396 __METHOD__
397 );
398
399 return ( $n > 0 );
400 }
401
402 /**
403 * Restore the given (or all) text and file revisions for the page.
404 * Once restored, the items will be removed from the archive tables.
405 * The deletion log will be updated with an undeletion notice.
406 *
407 * This also sets Status objects, $this->fileStatus and $this->revisionStatus
408 * (depending what operations are attempted).
409 *
410 * @param array $timestamps Pass an empty array to restore all revisions,
411 * otherwise list the ones to undelete.
412 * @param string $comment
413 * @param array $fileVersions
414 * @param bool $unsuppress
415 * @param User $user User performing the action, or null to use $wgUser
416 * @param string|string[] $tags Change tags to add to log entry
417 * ($user should be able to add the specified tags before this is called)
418 * @return array|bool array(number of file revisions restored, number of image revisions
419 * restored, log message) on success, false on failure.
420 */
421 public function undelete( $timestamps, $comment = '', $fileVersions = [],
422 $unsuppress = false, User $user = null, $tags = null
423 ) {
424 // If both the set of text revisions and file revisions are empty,
425 // restore everything. Otherwise, just restore the requested items.
426 $restoreAll = empty( $timestamps ) && empty( $fileVersions );
427
428 $restoreText = $restoreAll || !empty( $timestamps );
429 $restoreFiles = $restoreAll || !empty( $fileVersions );
430
431 if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
432 $img = wfLocalFile( $this->title );
433 $img->load( File::READ_LATEST );
434 $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
435 if ( !$this->fileStatus->isOK() ) {
436 return false;
437 }
438 $filesRestored = $this->fileStatus->successCount;
439 } else {
440 $filesRestored = 0;
441 }
442
443 if ( $restoreText ) {
444 $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
445 if ( !$this->revisionStatus->isOK() ) {
446 return false;
447 }
448
449 $textRestored = $this->revisionStatus->getValue();
450 } else {
451 $textRestored = 0;
452 }
453
454 // Touch the log!
455
456 if ( $textRestored && $filesRestored ) {
457 $reason = wfMessage( 'undeletedrevisions-files' )
458 ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
459 } elseif ( $textRestored ) {
460 $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
461 ->inContentLanguage()->text();
462 } elseif ( $filesRestored ) {
463 $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
464 ->inContentLanguage()->text();
465 } else {
466 wfDebug( "Undelete: nothing undeleted...\n" );
467
468 return false;
469 }
470
471 if ( trim( $comment ) != '' ) {
472 $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
473 }
474
475 if ( $user === null ) {
476 global $wgUser;
477 $user = $wgUser;
478 }
479
480 $logEntry = new ManualLogEntry( 'delete', 'restore' );
481 $logEntry->setPerformer( $user );
482 $logEntry->setTarget( $this->title );
483 $logEntry->setComment( $reason );
484 $logEntry->setTags( $tags );
485
486 Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
487
488 $logid = $logEntry->insert();
489 $logEntry->publish( $logid );
490
491 return [ $textRestored, $filesRestored, $reason ];
492 }
493
494 /**
495 * This is the meaty bit -- It restores archived revisions of the given page
496 * to the revision table.
497 *
498 * @param array $timestamps Pass an empty array to restore all revisions,
499 * otherwise list the ones to undelete.
500 * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
501 * @param string $comment
502 * @throws ReadOnlyError
503 * @return Status Status object containing the number of revisions restored on success
504 */
505 private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
506 if ( wfReadOnly() ) {
507 throw new ReadOnlyError();
508 }
509
510 $dbw = wfGetDB( DB_MASTER );
511 $dbw->startAtomic( __METHOD__ );
512
513 $restoreAll = empty( $timestamps );
514
515 # Does this page already exist? We'll have to update it...
516 $article = WikiPage::factory( $this->title );
517 # Load latest data for the current page (T33179)
518 $article->loadPageData( 'fromdbmaster' );
519 $oldcountable = $article->isCountable();
520
521 $page = $dbw->selectRow( 'page',
522 [ 'page_id', 'page_latest' ],
523 [ 'page_namespace' => $this->title->getNamespace(),
524 'page_title' => $this->title->getDBkey() ],
525 __METHOD__,
526 [ 'FOR UPDATE' ] // lock page
527 );
528
529 if ( $page ) {
530 $makepage = false;
531 # Page already exists. Import the history, and if necessary
532 # we'll update the latest revision field in the record.
533
534 # Get the time span of this page
535 $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
536 [ 'rev_id' => $page->page_latest ],
537 __METHOD__ );
538
539 if ( $previousTimestamp === false ) {
540 wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
541
542 $status = Status::newGood( 0 );
543 $status->warning( 'undeleterevision-missing' );
544 $dbw->endAtomic( __METHOD__ );
545
546 return $status;
547 }
548 } else {
549 # Have to create a new article...
550 $makepage = true;
551 $previousTimestamp = 0;
552 }
553
554 $oldWhere = [
555 'ar_namespace' => $this->title->getNamespace(),
556 'ar_title' => $this->title->getDBkey(),
557 ];
558 if ( !$restoreAll ) {
559 $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
560 }
561
562 $fields = [
563 'ar_id',
564 'ar_rev_id',
565 'rev_id',
566 'ar_text',
567 'ar_comment',
568 'ar_user',
569 'ar_user_text',
570 'ar_timestamp',
571 'ar_minor_edit',
572 'ar_flags',
573 'ar_text_id',
574 'ar_deleted',
575 'ar_page_id',
576 'ar_len',
577 'ar_sha1'
578 ];
579
580 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
581 $fields[] = 'ar_content_format';
582 $fields[] = 'ar_content_model';
583 }
584
585 /**
586 * Select each archived revision...
587 */
588 $result = $dbw->select(
589 [ 'archive', 'revision' ],
590 $fields,
591 $oldWhere,
592 __METHOD__,
593 /* options */
594 [ 'ORDER BY' => 'ar_timestamp' ],
595 [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ]
596 );
597
598 $rev_count = $result->numRows();
599 if ( !$rev_count ) {
600 wfDebug( __METHOD__ . ": no revisions to restore\n" );
601
602 $status = Status::newGood( 0 );
603 $status->warning( "undelete-no-results" );
604 $dbw->endAtomic( __METHOD__ );
605
606 return $status;
607 }
608
609 // We use ar_id because there can be duplicate ar_rev_id even for the same
610 // page. In this case, we may be able to restore the first one.
611 $restoreFailedArIds = [];
612
613 // Map rev_id to the ar_id that is allowed to use it. When checking later,
614 // if it doesn't match, the current ar_id can not be restored.
615
616 // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
617 // rev_id is taken before we even start the restore).
618 $allowedRevIdToArIdMap = [];
619
620 $latestRestorableRow = null;
621
622 foreach ( $result as $row ) {
623 if ( $row->ar_rev_id ) {
624 // rev_id is taken even before we start restoring.
625 if ( $row->ar_rev_id === $row->rev_id ) {
626 $restoreFailedArIds[] = $row->ar_id;
627 $allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
628 } else {
629 // rev_id is not taken yet in the DB, but it might be taken
630 // by a prior revision in the same restore operation. If
631 // not, we need to reserve it.
632 if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
633 $restoreFailedArIds[] = $row->ar_id;
634 } else {
635 $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
636 $latestRestorableRow = $row;
637 }
638 }
639 } else {
640 // If ar_rev_id is null, there can't be a collision, and a
641 // rev_id will be chosen automatically.
642 $latestRestorableRow = $row;
643 }
644 }
645
646 $result->seek( 0 ); // move back
647
648 $oldPageId = 0;
649 if ( $latestRestorableRow !== null ) {
650 $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
651
652 // grab the content to check consistency with global state before restoring the page.
653 $revision = Revision::newFromArchiveRow( $latestRestorableRow,
654 [
655 'title' => $article->getTitle(), // used to derive default content model
656 ]
657 );
658 $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
659 $content = $revision->getContent( Revision::RAW );
660
661 // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
662 $status = $content->prepareSave( $article, 0, -1, $user );
663 if ( !$status->isOK() ) {
664 $dbw->endAtomic( __METHOD__ );
665
666 return $status;
667 }
668 }
669
670 $newid = false; // newly created page ID
671 $restored = 0; // number of revisions restored
672 /** @var Revision $revision */
673 $revision = null;
674 $restoredPages = [];
675 // If there are no restorable revisions, we can skip most of the steps.
676 if ( $latestRestorableRow === null ) {
677 $failedRevisionCount = $rev_count;
678 } else {
679 if ( $makepage ) {
680 // Check the state of the newest to-be version...
681 if ( !$unsuppress
682 && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
683 ) {
684 $dbw->endAtomic( __METHOD__ );
685
686 return Status::newFatal( "undeleterevdel" );
687 }
688 // Safe to insert now...
689 $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
690 if ( $newid === false ) {
691 // The old ID is reserved; let's pick another
692 $newid = $article->insertOn( $dbw );
693 }
694 $pageId = $newid;
695 } else {
696 // Check if a deleted revision will become the current revision...
697 if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
698 // Check the state of the newest to-be version...
699 if ( !$unsuppress
700 && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
701 ) {
702 $dbw->endAtomic( __METHOD__ );
703
704 return Status::newFatal( "undeleterevdel" );
705 }
706 }
707
708 $newid = false;
709 $pageId = $article->getId();
710 }
711
712 foreach ( $result as $row ) {
713 // Check for key dupes due to needed archive integrity.
714 if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
715 continue;
716 }
717 // Insert one revision at a time...maintaining deletion status
718 // unless we are specifically removing all restrictions...
719 $revision = Revision::newFromArchiveRow( $row,
720 [
721 'page' => $pageId,
722 'title' => $this->title,
723 'deleted' => $unsuppress ? 0 : $row->ar_deleted
724 ] );
725
726 $revision->insertOn( $dbw );
727 $restored++;
728
729 Hooks::run( 'ArticleRevisionUndeleted',
730 [ &$this->title, $revision, $row->ar_page_id ] );
731 $restoredPages[$row->ar_page_id] = true;
732 }
733
734 // Now that it's safely stored, take it out of the archive
735 // Don't delete rows that we failed to restore
736 $toDeleteConds = $oldWhere;
737 $failedRevisionCount = count( $restoreFailedArIds );
738 if ( $failedRevisionCount > 0 ) {
739 $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
740 }
741
742 $dbw->delete( 'archive',
743 $toDeleteConds,
744 __METHOD__ );
745 }
746
747 $status = Status::newGood( $restored );
748
749 if ( $failedRevisionCount > 0 ) {
750 $status->warning(
751 wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
752 }
753
754 // Was anything restored at all?
755 if ( $restored ) {
756 $created = (bool)$newid;
757 // Attach the latest revision to the page...
758 $wasnew = $article->updateIfNewerOn( $dbw, $revision );
759 if ( $created || $wasnew ) {
760 // Update site stats, link tables, etc
761 $article->doEditUpdates(
762 $revision,
763 User::newFromName( $revision->getUserText( Revision::RAW ), false ),
764 [
765 'created' => $created,
766 'oldcountable' => $oldcountable,
767 'restored' => true
768 ]
769 );
770 }
771
772 Hooks::run( 'ArticleUndelete',
773 [ &$this->title, $created, $comment, $oldPageId, $restoredPages ] );
774 if ( $this->title->getNamespace() == NS_FILE ) {
775 DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
776 }
777 }
778
779 $dbw->endAtomic( __METHOD__ );
780
781 return $status;
782 }
783
784 /**
785 * @return Status
786 */
787 public function getFileStatus() {
788 return $this->fileStatus;
789 }
790
791 /**
792 * @return Status
793 */
794 public function getRevisionStatus() {
795 return $this->revisionStatus;
796 }
797 }