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