Merge "Change php extract() to explicit code"
[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 * various archive table fields.
175 *
176 * @return ResultWrapper
177 */
178 public function listRevisions() {
179 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
180 $queryInfo = $revisionStore->getArchiveQueryInfo();
181
182 $conds = [
183 'ar_namespace' => $this->title->getNamespace(),
184 'ar_title' => $this->title->getDBkey(),
185 ];
186 $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
187
188 ChangeTags::modifyDisplayQuery(
189 $queryInfo['tables'],
190 $queryInfo['fields'],
191 $conds,
192 $queryInfo['joins'],
193 $options,
194 ''
195 );
196
197 $dbr = wfGetDB( DB_REPLICA );
198 return $dbr->select(
199 $queryInfo['tables'],
200 $queryInfo['fields'],
201 $conds,
202 __METHOD__,
203 $options,
204 $queryInfo['joins']
205 );
206 }
207
208 /**
209 * List the deleted file revisions for this page, if it's a file page.
210 * Returns a result wrapper with various filearchive fields, or null
211 * if not a file page.
212 *
213 * @return ResultWrapper
214 * @todo Does this belong in Image for fuller encapsulation?
215 */
216 public function listFiles() {
217 if ( $this->title->getNamespace() != NS_FILE ) {
218 return null;
219 }
220
221 $dbr = wfGetDB( DB_REPLICA );
222 $fileQuery = ArchivedFile::getQueryInfo();
223 return $dbr->select(
224 $fileQuery['tables'],
225 $fileQuery['fields'],
226 [ 'fa_name' => $this->title->getDBkey() ],
227 __METHOD__,
228 [ 'ORDER BY' => 'fa_timestamp DESC' ],
229 $fileQuery['joins']
230 );
231 }
232
233 /**
234 * Return a Revision object containing data for the deleted revision.
235 * Note that the result *may* or *may not* have a null page ID.
236 *
237 * @param string $timestamp
238 * @return Revision|null
239 */
240 public function getRevision( $timestamp ) {
241 $dbr = wfGetDB( DB_REPLICA );
242 $arQuery = Revision::getArchiveQueryInfo();
243
244 $row = $dbr->selectRow(
245 $arQuery['tables'],
246 $arQuery['fields'],
247 [
248 'ar_namespace' => $this->title->getNamespace(),
249 'ar_title' => $this->title->getDBkey(),
250 'ar_timestamp' => $dbr->timestamp( $timestamp )
251 ],
252 __METHOD__,
253 [],
254 $arQuery['joins']
255 );
256
257 if ( $row ) {
258 return Revision::newFromArchiveRow(
259 $row,
260 [ 'title' => $this->title ],
261 $this->title
262 );
263 }
264
265 return null;
266 }
267
268 /**
269 * Return the most-previous revision, either live or deleted, against
270 * the deleted revision given by timestamp.
271 *
272 * May produce unexpected results in case of history merges or other
273 * unusual time issues.
274 *
275 * @param string $timestamp
276 * @return Revision|null Null when there is no previous revision
277 */
278 public function getPreviousRevision( $timestamp ) {
279 $dbr = wfGetDB( DB_REPLICA );
280
281 // Check the previous deleted revision...
282 $row = $dbr->selectRow( 'archive',
283 'ar_timestamp',
284 [ 'ar_namespace' => $this->title->getNamespace(),
285 'ar_title' => $this->title->getDBkey(),
286 'ar_timestamp < ' .
287 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
288 __METHOD__,
289 [
290 'ORDER BY' => 'ar_timestamp DESC',
291 'LIMIT' => 1 ] );
292 $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
293
294 $row = $dbr->selectRow( [ 'page', 'revision' ],
295 [ 'rev_id', 'rev_timestamp' ],
296 [
297 'page_namespace' => $this->title->getNamespace(),
298 'page_title' => $this->title->getDBkey(),
299 'page_id = rev_page',
300 'rev_timestamp < ' .
301 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
302 __METHOD__,
303 [
304 'ORDER BY' => 'rev_timestamp DESC',
305 'LIMIT' => 1 ] );
306 $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
307 $prevLiveId = $row ? intval( $row->rev_id ) : null;
308
309 if ( $prevLive && $prevLive > $prevDeleted ) {
310 // Most prior revision was live
311 return Revision::newFromId( $prevLiveId );
312 } elseif ( $prevDeleted ) {
313 // Most prior revision was deleted
314 return $this->getRevision( $prevDeleted );
315 }
316
317 // No prior revision on this page.
318 return null;
319 }
320
321 /**
322 * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
323 *
324 * @param object $row Database row
325 * @return string
326 */
327 public function getTextFromRow( $row ) {
328 if ( is_null( $row->ar_text_id ) ) {
329 // An old row from MediaWiki 1.4 or previous.
330 // Text is embedded in this row in classic compression format.
331 return Revision::getRevisionText( $row, 'ar_' );
332 }
333
334 // New-style: keyed to the text storage backend.
335 $dbr = wfGetDB( DB_REPLICA );
336 $text = $dbr->selectRow( 'text',
337 [ 'old_text', 'old_flags' ],
338 [ 'old_id' => $row->ar_text_id ],
339 __METHOD__ );
340
341 return Revision::getRevisionText( $text );
342 }
343
344 /**
345 * Fetch (and decompress if necessary) the stored text of the most
346 * recently edited deleted revision of the page.
347 *
348 * If there are no archived revisions for the page, returns NULL.
349 *
350 * @return string|null
351 */
352 public function getLastRevisionText() {
353 $dbr = wfGetDB( DB_REPLICA );
354 $row = $dbr->selectRow( 'archive',
355 [ 'ar_text', 'ar_flags', 'ar_text_id' ],
356 [ 'ar_namespace' => $this->title->getNamespace(),
357 'ar_title' => $this->title->getDBkey() ],
358 __METHOD__,
359 [ 'ORDER BY' => 'ar_timestamp DESC' ] );
360
361 if ( $row ) {
362 return $this->getTextFromRow( $row );
363 }
364
365 return null;
366 }
367
368 /**
369 * Quick check if any archived revisions are present for the page.
370 *
371 * @return bool
372 */
373 public function isDeleted() {
374 $dbr = wfGetDB( DB_REPLICA );
375 $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
376 [ 'ar_namespace' => $this->title->getNamespace(),
377 'ar_title' => $this->title->getDBkey() ],
378 __METHOD__
379 );
380
381 return ( $n > 0 );
382 }
383
384 /**
385 * Restore the given (or all) text and file revisions for the page.
386 * Once restored, the items will be removed from the archive tables.
387 * The deletion log will be updated with an undeletion notice.
388 *
389 * This also sets Status objects, $this->fileStatus and $this->revisionStatus
390 * (depending what operations are attempted).
391 *
392 * @param array $timestamps Pass an empty array to restore all revisions,
393 * otherwise list the ones to undelete.
394 * @param string $comment
395 * @param array $fileVersions
396 * @param bool $unsuppress
397 * @param User $user User performing the action, or null to use $wgUser
398 * @param string|string[] $tags Change tags to add to log entry
399 * ($user should be able to add the specified tags before this is called)
400 * @return array|bool array(number of file revisions restored, number of image revisions
401 * restored, log message) on success, false on failure.
402 */
403 public function undelete( $timestamps, $comment = '', $fileVersions = [],
404 $unsuppress = false, User $user = null, $tags = null
405 ) {
406 // If both the set of text revisions and file revisions are empty,
407 // restore everything. Otherwise, just restore the requested items.
408 $restoreAll = empty( $timestamps ) && empty( $fileVersions );
409
410 $restoreText = $restoreAll || !empty( $timestamps );
411 $restoreFiles = $restoreAll || !empty( $fileVersions );
412
413 if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
414 $img = wfLocalFile( $this->title );
415 $img->load( File::READ_LATEST );
416 $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
417 if ( !$this->fileStatus->isOK() ) {
418 return false;
419 }
420 $filesRestored = $this->fileStatus->successCount;
421 } else {
422 $filesRestored = 0;
423 }
424
425 if ( $restoreText ) {
426 $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
427 if ( !$this->revisionStatus->isOK() ) {
428 return false;
429 }
430
431 $textRestored = $this->revisionStatus->getValue();
432 } else {
433 $textRestored = 0;
434 }
435
436 // Touch the log!
437
438 if ( !$textRestored && !$filesRestored ) {
439 wfDebug( "Undelete: nothing undeleted...\n" );
440
441 return false;
442 }
443
444 if ( $user === null ) {
445 global $wgUser;
446 $user = $wgUser;
447 }
448
449 $logEntry = new ManualLogEntry( 'delete', 'restore' );
450 $logEntry->setPerformer( $user );
451 $logEntry->setTarget( $this->title );
452 $logEntry->setComment( $comment );
453 $logEntry->setTags( $tags );
454 $logEntry->setParameters( [
455 ':assoc:count' => [
456 'revisions' => $textRestored,
457 'files' => $filesRestored,
458 ],
459 ] );
460
461 Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
462
463 $logid = $logEntry->insert();
464 $logEntry->publish( $logid );
465
466 return [ $textRestored, $filesRestored, $comment ];
467 }
468
469 /**
470 * This is the meaty bit -- It restores archived revisions of the given page
471 * to the revision table.
472 *
473 * @param array $timestamps Pass an empty array to restore all revisions,
474 * otherwise list the ones to undelete.
475 * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
476 * @param string $comment
477 * @throws ReadOnlyError
478 * @return Status Status object containing the number of revisions restored on success
479 */
480 private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
481 if ( wfReadOnly() ) {
482 throw new ReadOnlyError();
483 }
484
485 $dbw = wfGetDB( DB_MASTER );
486 $dbw->startAtomic( __METHOD__ );
487
488 $restoreAll = empty( $timestamps );
489
490 # Does this page already exist? We'll have to update it...
491 $article = WikiPage::factory( $this->title );
492 # Load latest data for the current page (T33179)
493 $article->loadPageData( 'fromdbmaster' );
494 $oldcountable = $article->isCountable();
495
496 $page = $dbw->selectRow( 'page',
497 [ 'page_id', 'page_latest' ],
498 [ 'page_namespace' => $this->title->getNamespace(),
499 'page_title' => $this->title->getDBkey() ],
500 __METHOD__,
501 [ 'FOR UPDATE' ] // lock page
502 );
503
504 if ( $page ) {
505 $makepage = false;
506 # Page already exists. Import the history, and if necessary
507 # we'll update the latest revision field in the record.
508
509 # Get the time span of this page
510 $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
511 [ 'rev_id' => $page->page_latest ],
512 __METHOD__ );
513
514 if ( $previousTimestamp === false ) {
515 wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
516
517 $status = Status::newGood( 0 );
518 $status->warning( 'undeleterevision-missing' );
519 $dbw->endAtomic( __METHOD__ );
520
521 return $status;
522 }
523 } else {
524 # Have to create a new article...
525 $makepage = true;
526 $previousTimestamp = 0;
527 }
528
529 $oldWhere = [
530 'ar_namespace' => $this->title->getNamespace(),
531 'ar_title' => $this->title->getDBkey(),
532 ];
533 if ( !$restoreAll ) {
534 $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
535 }
536
537 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
538 $queryInfo = $revisionStore->getArchiveQueryInfo();
539 $queryInfo['tables'][] = 'revision';
540 $queryInfo['fields'][] = 'rev_id';
541 $queryInfo['joins']['revision'] = [ 'LEFT JOIN', 'ar_rev_id=rev_id' ];
542
543 /**
544 * Select each archived revision...
545 */
546 $result = $dbw->select(
547 $queryInfo['tables'],
548 $queryInfo['fields'],
549 $oldWhere,
550 __METHOD__,
551 /* options */
552 [ 'ORDER BY' => 'ar_timestamp' ],
553 $queryInfo['joins']
554 );
555
556 $rev_count = $result->numRows();
557 if ( !$rev_count ) {
558 wfDebug( __METHOD__ . ": no revisions to restore\n" );
559
560 $status = Status::newGood( 0 );
561 $status->warning( "undelete-no-results" );
562 $dbw->endAtomic( __METHOD__ );
563
564 return $status;
565 }
566
567 // We use ar_id because there can be duplicate ar_rev_id even for the same
568 // page. In this case, we may be able to restore the first one.
569 $restoreFailedArIds = [];
570
571 // Map rev_id to the ar_id that is allowed to use it. When checking later,
572 // if it doesn't match, the current ar_id can not be restored.
573
574 // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
575 // rev_id is taken before we even start the restore).
576 $allowedRevIdToArIdMap = [];
577
578 $latestRestorableRow = null;
579
580 foreach ( $result as $row ) {
581 if ( $row->ar_rev_id ) {
582 // rev_id is taken even before we start restoring.
583 if ( $row->ar_rev_id === $row->rev_id ) {
584 $restoreFailedArIds[] = $row->ar_id;
585 $allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
586 } else {
587 // rev_id is not taken yet in the DB, but it might be taken
588 // by a prior revision in the same restore operation. If
589 // not, we need to reserve it.
590 if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
591 $restoreFailedArIds[] = $row->ar_id;
592 } else {
593 $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
594 $latestRestorableRow = $row;
595 }
596 }
597 } else {
598 // If ar_rev_id is null, there can't be a collision, and a
599 // rev_id will be chosen automatically.
600 $latestRestorableRow = $row;
601 }
602 }
603
604 $result->seek( 0 ); // move back
605
606 $oldPageId = 0;
607 if ( $latestRestorableRow !== null ) {
608 $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
609
610 // grab the content to check consistency with global state before restoring the page.
611 $revision = Revision::newFromArchiveRow(
612 $latestRestorableRow,
613 [
614 'title' => $article->getTitle(), // used to derive default content model
615 ],
616 $article->getTitle()
617 );
618 $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
619 $content = $revision->getContent( Revision::RAW );
620
621 // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
622 $status = $content->prepareSave( $article, 0, -1, $user );
623 if ( !$status->isOK() ) {
624 $dbw->endAtomic( __METHOD__ );
625
626 return $status;
627 }
628 }
629
630 $newid = false; // newly created page ID
631 $restored = 0; // number of revisions restored
632 /** @var Revision $revision */
633 $revision = null;
634 $restoredPages = [];
635 // If there are no restorable revisions, we can skip most of the steps.
636 if ( $latestRestorableRow === null ) {
637 $failedRevisionCount = $rev_count;
638 } else {
639 if ( $makepage ) {
640 // Check the state of the newest to-be version...
641 if ( !$unsuppress
642 && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
643 ) {
644 $dbw->endAtomic( __METHOD__ );
645
646 return Status::newFatal( "undeleterevdel" );
647 }
648 // Safe to insert now...
649 $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
650 if ( $newid === false ) {
651 // The old ID is reserved; let's pick another
652 $newid = $article->insertOn( $dbw );
653 }
654 $pageId = $newid;
655 } else {
656 // Check if a deleted revision will become the current revision...
657 if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
658 // Check the state of the newest to-be version...
659 if ( !$unsuppress
660 && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
661 ) {
662 $dbw->endAtomic( __METHOD__ );
663
664 return Status::newFatal( "undeleterevdel" );
665 }
666 }
667
668 $newid = false;
669 $pageId = $article->getId();
670 }
671
672 foreach ( $result as $row ) {
673 // Check for key dupes due to needed archive integrity.
674 if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
675 continue;
676 }
677 // Insert one revision at a time...maintaining deletion status
678 // unless we are specifically removing all restrictions...
679 $revision = Revision::newFromArchiveRow(
680 $row,
681 [
682 'page' => $pageId,
683 'title' => $this->title,
684 'deleted' => $unsuppress ? 0 : $row->ar_deleted
685 ],
686 $this->title
687 );
688
689 // This will also copy the revision to ip_changes if it was an IP edit.
690 $revision->insertOn( $dbw );
691
692 $restored++;
693
694 Hooks::run( 'ArticleRevisionUndeleted',
695 [ &$this->title, $revision, $row->ar_page_id ] );
696 $restoredPages[$row->ar_page_id] = true;
697 }
698
699 // Now that it's safely stored, take it out of the archive
700 // Don't delete rows that we failed to restore
701 $toDeleteConds = $oldWhere;
702 $failedRevisionCount = count( $restoreFailedArIds );
703 if ( $failedRevisionCount > 0 ) {
704 $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
705 }
706
707 $dbw->delete( 'archive',
708 $toDeleteConds,
709 __METHOD__ );
710 }
711
712 $status = Status::newGood( $restored );
713
714 if ( $failedRevisionCount > 0 ) {
715 $status->warning(
716 wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
717 }
718
719 // Was anything restored at all?
720 if ( $restored ) {
721 $created = (bool)$newid;
722 // Attach the latest revision to the page...
723 $wasnew = $article->updateIfNewerOn( $dbw, $revision );
724 if ( $created || $wasnew ) {
725 // Update site stats, link tables, etc
726 $article->doEditUpdates(
727 $revision,
728 User::newFromName( $revision->getUserText( Revision::RAW ), false ),
729 [
730 'created' => $created,
731 'oldcountable' => $oldcountable,
732 'restored' => true
733 ]
734 );
735 }
736
737 Hooks::run( 'ArticleUndelete',
738 [ &$this->title, $created, $comment, $oldPageId, $restoredPages ] );
739 if ( $this->title->getNamespace() == NS_FILE ) {
740 DeferredUpdates::addUpdate(
741 new HTMLCacheUpdate( $this->title, 'imagelinks', 'file-restore' )
742 );
743 }
744 }
745
746 $dbw->endAtomic( __METHOD__ );
747
748 return $status;
749 }
750
751 /**
752 * @return Status
753 */
754 public function getFileStatus() {
755 return $this->fileStatus;
756 }
757
758 /**
759 * @return Status
760 */
761 public function getRevisionStatus() {
762 return $this->revisionStatus;
763 }
764 }