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