MediaWiki REL1_37
PageArchive.php
Go to the documentation of this file.
1<?php
29use Psr\Log\LoggerInterface;
33
38
40 protected $title;
41
43 protected $fileStatus;
44
46 protected $revisionStatus;
47
49 protected $config;
50
52 private $hookRunner;
53
56
59
61 private $logger;
62
65
67 private $repoGroup;
68
71
73 private $userFactory;
74
77
82 public function __construct( Title $title, Config $config = null ) {
83 $this->title = $title;
84
85 $this->logger = LoggerFactory::getInstance( 'PageArchive' );
86
87 $services = MediaWikiServices::getInstance();
88 if ( $config === null ) {
89 // TODO deprecate not passing a Config object, though technically this class is
90 // not @newable / stable to create
91 $this->logger->debug( 'Constructor did not have a Config object passed to it' );
92 $config = $services->getMainConfig();
93 }
94 $this->config = $config;
95
96 $this->hookRunner = new HookRunner( $services->getHookContainer() );
97 $this->jobQueueGroup = $services->getJobQueueGroup();
98 $this->loadBalancer = $services->getDBLoadBalancer();
99 $this->readOnlyMode = $services->getReadOnlyMode();
100 $this->repoGroup = $services->getRepoGroup();
101
102 // TODO: Refactor: delete()/undeleteAsUser() should live in a PageStore service;
103 // Methods in PageArchive and RevisionStore that deal with archive revisions
104 // should move into an ArchiveStore service (but could still be implemented
105 // together with RevisionStore).
106 $this->revisionStore = $services->getRevisionStore();
107
108 $this->userFactory = $services->getUserFactory();
109 $this->wikiPageFactory = $services->getWikiPageFactory();
110 }
111
112 public function doesWrites() {
113 return true;
114 }
115
124 public static function listPagesBySearch( $term ) {
125 $title = Title::newFromText( $term );
126 if ( $title ) {
127 $ns = $title->getNamespace();
128 $termMain = $title->getText();
129 $termDb = $title->getDBkey();
130 } else {
131 // Prolly won't work too good
132 // @todo handle bare namespace names cleanly?
133 $ns = 0;
134 $termMain = $termDb = $term;
135 }
136
137 // Try search engine first
138 $engine = MediaWikiServices::getInstance()->newSearchEngine();
139 $engine->setLimitOffset( 100 );
140 $engine->setNamespaces( [ $ns ] );
141 $results = $engine->searchArchiveTitle( $termMain );
142 if ( !$results->isOK() ) {
143 $results = [];
144 } else {
145 $results = $results->getValue();
146 }
147
148 if ( !$results ) {
149 // Fall back to regular prefix search
150 return self::listPagesByPrefix( $term );
151 }
152
154 $condTitles = array_unique( array_map( static function ( Title $t ) {
155 return $t->getDBkey();
156 }, $results ) );
157 $conds = [
158 'ar_namespace' => $ns,
159 $dbr->makeList( [ 'ar_title' => $condTitles ], LIST_OR ) . " OR ar_title " .
160 $dbr->buildLike( $termDb, $dbr->anyString() )
161 ];
162
163 return self::listPages( $dbr, $conds );
164 }
165
174 public static function listPagesByPrefix( $prefix ) {
176
177 $title = Title::newFromText( $prefix );
178 if ( $title ) {
179 $ns = $title->getNamespace();
180 $prefix = $title->getDBkey();
181 } else {
182 // Prolly won't work too good
183 // @todo handle bare namespace names cleanly?
184 $ns = 0;
185 }
186
187 $conds = [
188 'ar_namespace' => $ns,
189 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
190 ];
191
192 return self::listPages( $dbr, $conds );
193 }
194
200 protected static function listPages( $dbr, $condition ) {
201 return $dbr->select(
202 [ 'archive' ],
203 [
204 'ar_namespace',
205 'ar_title',
206 'count' => 'COUNT(*)'
207 ],
208 $condition,
209 __METHOD__,
210 [
211 'GROUP BY' => [ 'ar_namespace', 'ar_title' ],
212 'ORDER BY' => [ 'ar_namespace', 'ar_title' ],
213 'LIMIT' => 100,
214 ]
215 );
216 }
217
224 public function listRevisions() {
225 $queryInfo = $this->revisionStore->getArchiveQueryInfo();
226
227 $conds = [
228 'ar_namespace' => $this->title->getNamespace(),
229 'ar_title' => $this->title->getDBkey(),
230 ];
231
232 // NOTE: ordering by ar_timestamp and ar_id, to remove ambiguity.
233 // XXX: Ideally, we would be ordering by ar_timestamp and ar_rev_id, but since we
234 // don't have an index on ar_rev_id, that causes a file sort.
235 $options = [ 'ORDER BY' => [ 'ar_timestamp DESC', 'ar_id DESC' ] ];
236
238 $queryInfo['tables'],
239 $queryInfo['fields'],
240 $conds,
241 $queryInfo['joins'],
242 $options,
243 ''
244 );
245
246 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
247 return $dbr->select(
248 $queryInfo['tables'],
249 $queryInfo['fields'],
250 $conds,
251 __METHOD__,
252 $options,
253 $queryInfo['joins']
254 );
255 }
256
265 public function listFiles() {
266 if ( $this->title->getNamespace() !== NS_FILE ) {
267 return null;
268 }
269
270 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
271 $fileQuery = ArchivedFile::getQueryInfo();
272 return $dbr->select(
273 $fileQuery['tables'],
274 $fileQuery['fields'],
275 [ 'fa_name' => $this->title->getDBkey() ],
276 __METHOD__,
277 [ 'ORDER BY' => 'fa_timestamp DESC' ],
278 $fileQuery['joins']
279 );
280 }
281
290 public function getRevisionRecordByTimestamp( $timestamp ) {
291 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
292 $rec = $this->getRevisionByConditions(
293 [ 'ar_timestamp' => $dbr->timestamp( $timestamp ) ]
294 );
295 return $rec;
296 }
297
306 public function getArchivedRevisionRecord( int $revId ) {
307 return $this->getRevisionByConditions( [ 'ar_rev_id' => $revId ] );
308 }
309
316 private function getRevisionByConditions( array $conditions, array $options = [] ) {
317 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
318 $arQuery = $this->revisionStore->getArchiveQueryInfo();
319
320 $conditions += [
321 'ar_namespace' => $this->title->getNamespace(),
322 'ar_title' => $this->title->getDBkey(),
323 ];
324
325 $row = $dbr->selectRow(
326 $arQuery['tables'],
327 $arQuery['fields'],
328 $conditions,
329 __METHOD__,
330 $options,
331 $arQuery['joins']
332 );
333
334 if ( $row ) {
335 return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $this->title );
336 }
337
338 return null;
339 }
340
353 public function getPreviousRevisionRecord( string $timestamp ) {
354 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
355
356 // Check the previous deleted revision...
357 $row = $dbr->selectRow( 'archive',
358 [ 'ar_rev_id', 'ar_timestamp' ],
359 [ 'ar_namespace' => $this->title->getNamespace(),
360 'ar_title' => $this->title->getDBkey(),
361 'ar_timestamp < ' .
362 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
363 __METHOD__,
364 [
365 'ORDER BY' => 'ar_timestamp DESC',
366 ] );
367 $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
368 $prevDeletedId = $row ? intval( $row->ar_rev_id ) : null;
369
370 $row = $dbr->selectRow( [ 'page', 'revision' ],
371 [ 'rev_id', 'rev_timestamp' ],
372 [
373 'page_namespace' => $this->title->getNamespace(),
374 'page_title' => $this->title->getDBkey(),
375 'page_id = rev_page',
376 'rev_timestamp < ' .
377 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
378 __METHOD__,
379 [
380 'ORDER BY' => 'rev_timestamp DESC',
381 ] );
382 $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
383 $prevLiveId = $row ? intval( $row->rev_id ) : null;
384
385 if ( $prevLive && $prevLive > $prevDeleted ) {
386 // Most prior revision was live
387 $rec = $this->revisionStore->getRevisionById( $prevLiveId );
388 } elseif ( $prevDeleted ) {
389 // Most prior revision was deleted
390 $rec = $this->getArchivedRevisionRecord( $prevDeletedId );
391 } else {
392 $rec = null;
393 }
394
395 return $rec;
396 }
397
403 public function getLastRevisionId() {
404 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
405 $revId = $dbr->selectField(
406 'archive',
407 'ar_rev_id',
408 [ 'ar_namespace' => $this->title->getNamespace(),
409 'ar_title' => $this->title->getDBkey() ],
410 __METHOD__,
411 [ 'ORDER BY' => [ 'ar_timestamp DESC', 'ar_id DESC' ] ]
412 );
413
414 return $revId ? intval( $revId ) : false;
415 }
416
423 public function isDeleted() {
424 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
425 $row = $dbr->selectRow(
426 [ 'archive' ],
427 '1', // We don't care about the value. Allow the database to optimize.
428 [ 'ar_namespace' => $this->title->getNamespace(),
429 'ar_title' => $this->title->getDBkey() ],
430 __METHOD__
431 );
432
433 return (bool)$row;
434 }
435
457 public function undeleteAsUser(
458 $timestamps,
459 UserIdentity $user,
460 $comment = '',
461 $fileVersions = [],
462 $unsuppress = false,
463 $tags = null
464 ) {
465 $hookStatus = StatusValue::newGood();
466 $hookRes = $this->hookRunner->onPageUndelete(
467 $this->wikiPageFactory->newFromTitle( $this->title ),
468 $this->userFactory->newFromUserIdentity( $user ),
469 $comment,
470 $unsuppress,
471 $timestamps,
472 $fileVersions ?: [],
473 $hookStatus
474 );
475 if ( !$hookRes && !$hookStatus->isGood() ) {
476 // Note: as per the PageUndeleteHook documentation, `return false` is ignored if $status is good.
477 return false;
478 }
479 // If both the set of text revisions and file revisions are empty,
480 // restore everything. Otherwise, just restore the requested items.
481 $restoreAll = empty( $timestamps ) && empty( $fileVersions );
482
483 $restoreText = $restoreAll || !empty( $timestamps );
484 $restoreFiles = $restoreAll || !empty( $fileVersions );
485
486 if ( $restoreFiles && $this->title->getNamespace() === NS_FILE ) {
488 $img = $this->repoGroup->getLocalRepo()->newFile( $this->title );
489 $img->load( File::READ_LATEST );
490 $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
491 if ( !$this->fileStatus->isOK() ) {
492 return false;
493 }
494 $filesRestored = $this->fileStatus->successCount;
495 } else {
496 $filesRestored = 0;
497 }
498
499 if ( $restoreText ) {
500 $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
501 if ( !$this->revisionStatus->isOK() ) {
502 return false;
503 }
504
505 $textRestored = $this->revisionStatus->getValue();
506 } else {
507 $textRestored = 0;
508 }
509
510 // Touch the log!
511
512 if ( !$textRestored && !$filesRestored ) {
513 $this->logger->debug( "Undelete: nothing undeleted..." );
514
515 return false;
516 }
517
518 $logEntry = new ManualLogEntry( 'delete', 'restore' );
519 $logEntry->setPerformer( $user );
520 $logEntry->setTarget( $this->title );
521 $logEntry->setComment( $comment );
522 $logEntry->addTags( $tags );
523 $logEntry->setParameters( [
524 ':assoc:count' => [
525 'revisions' => $textRestored,
526 'files' => $filesRestored,
527 ],
528 ] );
529
530 $legacyUser = $this->userFactory->newFromUserIdentity( $user );
531 $this->hookRunner->onArticleUndeleteLogEntry( $this, $logEntry, $legacyUser );
532
533 $logid = $logEntry->insert();
534 $logEntry->publish( $logid );
535
536 return [ $textRestored, $filesRestored, $comment ];
537 }
538
550 private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
551 if ( $this->readOnlyMode->isReadOnly() ) {
552 throw new ReadOnlyError();
553 }
554
555 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
556 $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
557
558 $restoreAll = empty( $timestamps );
559
560 $oldWhere = [
561 'ar_namespace' => $this->title->getNamespace(),
562 'ar_title' => $this->title->getDBkey(),
563 ];
564 if ( !$restoreAll ) {
565 $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
566 }
567
568 $revisionStore = $this->revisionStore;
569 $queryInfo = $revisionStore->getArchiveQueryInfo();
570 $queryInfo['tables'][] = 'revision';
571 $queryInfo['fields'][] = 'rev_id';
572 $queryInfo['joins']['revision'] = [ 'LEFT JOIN', 'ar_rev_id=rev_id' ];
573
577 $result = $dbw->select(
578 $queryInfo['tables'],
579 $queryInfo['fields'],
580 $oldWhere,
581 __METHOD__,
582 /* options */
583 [ 'ORDER BY' => 'ar_timestamp' ],
584 $queryInfo['joins']
585 );
586
587 $rev_count = $result->numRows();
588 if ( !$rev_count ) {
589 $this->logger->debug( __METHOD__ . ": no revisions to restore" );
590
591 $status = Status::newGood( 0 );
592 $status->warning( "undelete-no-results" );
593 $dbw->endAtomic( __METHOD__ );
594
595 return $status;
596 }
597
598 // We use ar_id because there can be duplicate ar_rev_id even for the same
599 // page. In this case, we may be able to restore the first one.
600 $restoreFailedArIds = [];
601
602 // Map rev_id to the ar_id that is allowed to use it. When checking later,
603 // if it doesn't match, the current ar_id can not be restored.
604
605 // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
606 // rev_id is taken before we even start the restore).
607 $allowedRevIdToArIdMap = [];
608
609 $latestRestorableRow = null;
610
611 foreach ( $result as $row ) {
612 if ( $row->ar_rev_id ) {
613 // rev_id is taken even before we start restoring.
614 if ( $row->ar_rev_id === $row->rev_id ) {
615 $restoreFailedArIds[] = $row->ar_id;
616 $allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
617 } else {
618 // rev_id is not taken yet in the DB, but it might be taken
619 // by a prior revision in the same restore operation. If
620 // not, we need to reserve it.
621 if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
622 $restoreFailedArIds[] = $row->ar_id;
623 } else {
624 $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
625 $latestRestorableRow = $row;
626 }
627 }
628 } else {
629 // If ar_rev_id is null, there can't be a collision, and a
630 // rev_id will be chosen automatically.
631 $latestRestorableRow = $row;
632 }
633 }
634
635 $result->seek( 0 ); // move back
636
637 $article = $this->wikiPageFactory->newFromTitle( $this->title );
638
639 $oldPageId = 0;
641 $revision = null;
642 $created = true;
643 $oldcountable = false;
644 $updatedCurrentRevision = false;
645 $restored = 0; // number of revisions restored
646 $restoredPages = [];
647
648 // If there are no restorable revisions, we can skip most of the steps.
649 if ( $latestRestorableRow === null ) {
650 $failedRevisionCount = $rev_count;
651 } else {
652 $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
653
654 // Grab the content to check consistency with global state before restoring the page.
655 // XXX: The only current use case is Wikibase, which tries to enforce uniqueness of
656 // certain things across all pages. There may be a better way to do that.
658 $latestRestorableRow,
659 0,
660 $this->title
661 );
662
663 // TODO: use UserFactory::newFromUserIdentity from If610c68f4912e
664 // TODO: The User isn't used for anything in prepareSave()! We should drop it.
665 $user = $this->userFactory->newFromName(
666 $revision->getUser( RevisionRecord::RAW )->getName(),
667 UserFactory::RIGOR_NONE
668 );
669
670 foreach ( $revision->getSlotRoles() as $role ) {
671 $content = $revision->getContent( $role, RevisionRecord::RAW );
672
673 // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
674 $status = $content->prepareSave( $article, 0, -1, $user );
675 if ( !$status->isOK() ) {
676 $dbw->endAtomic( __METHOD__ );
677
678 return $status;
679 }
680 }
681
682 $pageId = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
683 if ( $pageId === false ) {
684 // The page ID is reserved; let's pick another
685 $pageId = $article->insertOn( $dbw );
686 if ( $pageId === false ) {
687 // The page title must be already taken (race condition)
688 $created = false;
689 }
690 }
691
692 # Does this page already exist? We'll have to update it...
693 if ( !$created ) {
694 # Load latest data for the current page (T33179)
695 $article->loadPageData( WikiPage::READ_EXCLUSIVE );
696 $pageId = $article->getId();
697 $oldcountable = $article->isCountable();
698
699 $previousTimestamp = false;
700 $latestRevId = $article->getLatest();
701 if ( $latestRevId ) {
702 $previousTimestamp = $revisionStore->getTimestampFromId(
703 $latestRevId,
704 RevisionStore::READ_LATEST
705 );
706 }
707 if ( $previousTimestamp === false ) {
708 $this->logger->debug( __METHOD__ . ": existing page refers to a page_latest that does not exist" );
709
710 $status = Status::newGood( 0 );
711 $status->warning( 'undeleterevision-missing' );
712 $dbw->cancelAtomic( __METHOD__ );
713
714 return $status;
715 }
716 } else {
717 $previousTimestamp = 0;
718 }
719
720 // Check if a deleted revision will become the current revision...
721 if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
722 // Check the state of the newest to-be version...
723 if ( !$unsuppress
724 && ( $latestRestorableRow->ar_deleted & RevisionRecord::DELETED_TEXT )
725 ) {
726 $dbw->cancelAtomic( __METHOD__ );
727
728 return Status::newFatal( "undeleterevdel" );
729 }
730 $updatedCurrentRevision = true;
731 }
732
733 foreach ( $result as $row ) {
734 // Check for key dupes due to needed archive integrity.
735 if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
736 continue;
737 }
738 // Insert one revision at a time...maintaining deletion status
739 // unless we are specifically removing all restrictions...
741 $row,
742 0,
743 $this->title,
744 [
745 'page_id' => $pageId,
746 'deleted' => $unsuppress ? 0 : $row->ar_deleted
747 ]
748 );
749
750 // This will also copy the revision to ip_changes if it was an IP edit.
751 $revisionStore->insertRevisionOn( $revision, $dbw );
752
753 $restored++;
754
755 $this->hookRunner->onRevisionUndeleted( $revision, $row->ar_page_id );
756
757 $restoredPages[$row->ar_page_id] = true;
758 }
759
760 // Now that it's safely stored, take it out of the archive
761 // Don't delete rows that we failed to restore
762 $toDeleteConds = $oldWhere;
763 $failedRevisionCount = count( $restoreFailedArIds );
764 if ( $failedRevisionCount > 0 ) {
765 $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
766 }
767
768 $dbw->delete( 'archive',
769 $toDeleteConds,
770 __METHOD__ );
771 }
772
773 $status = Status::newGood( $restored );
774
775 if ( $failedRevisionCount > 0 ) {
776 $status->warning(
777 wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
778 }
779
780 // Was anything restored at all?
781 if ( $restored ) {
782
783 if ( $updatedCurrentRevision ) {
784 // Attach the latest revision to the page...
785 // XXX: updateRevisionOn should probably move into a PageStore service.
786 $wasnew = $article->updateRevisionOn(
787 $dbw,
788 $revision,
789 $created ? 0 : $article->getLatest()
790 );
791 } else {
792 $wasnew = false;
793 }
794
795 if ( $created || $wasnew ) {
796 // Update site stats, link tables, etc
797 // TODO: use DerivedPageDataUpdater from If610c68f4912e!
798 // TODO use UserFactory::newFromUserIdentity
799 $article->doEditUpdates(
800 $revision,
801 $revision->getUser( RevisionRecord::RAW ),
802 [
803 'created' => $created,
804 'oldcountable' => $oldcountable,
805 'restored' => true
806 ]
807 );
808 }
809
810 $this->hookRunner->onArticleUndelete(
811 $this->title, $created, $comment, $oldPageId, $restoredPages );
812
813 if ( $this->title->getNamespace() === NS_FILE ) {
815 $this->title,
816 'imagelinks',
817 [ 'causeAction' => 'file-restore' ]
818 );
819 $this->jobQueueGroup->lazyPush( $job );
820 }
821 }
822
823 $dbw->endAtomic( __METHOD__ );
824
825 return $status;
826 }
827
831 public function getFileStatus() {
832 return $this->fileStatus;
833 }
834
838 public function getRevisionStatus() {
839 return $this->revisionStatus;
840 }
841}
const NS_FILE
Definition Defines.php:70
const LIST_OR
Definition Defines.php:46
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archivedfile object.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
static newForBacklinks(PageReference $page, $table, $params=[])
Class to handle enqueueing of background jobs.
Class for creating new log entries and inserting them into the database.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
Service for looking up page revisions.
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
newRevisionFromArchiveRow( $row, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Make a fake RevisionRecord object from an archive table row.
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Creates User objects.
Used to show archived pages and eventually restore them.
Status null $revisionStatus
ReadOnlyMode $readOnlyMode
listFiles()
List the deleted file revisions for this page, if it's a file page.
HookRunner $hookRunner
getPreviousRevisionRecord(string $timestamp)
Return the most-previous revision, either live or deleted, against the deleted revision given by time...
Config $config
undeleteAsUser( $timestamps, UserIdentity $user, $comment='', $fileVersions=[], $unsuppress=false, $tags=null)
Restore the given (or all) text and file revisions for the page.
RepoGroup $repoGroup
static listPages( $dbr, $condition)
static listPagesBySearch( $term)
List deleted pages recorded in the archive matching the given term, using search engine archive.
getArchivedRevisionRecord(int $revId)
Return the archived revision with the given ID.
getRevisionRecordByTimestamp( $timestamp)
Return a RevisionRecord object containing data for the deleted revision.
getLastRevisionId()
Returns the ID of the latest deleted revision.
Status null $fileStatus
__construct(Title $title, Config $config=null)
listRevisions()
List the revisions of the given page.
undeleteRevisions( $timestamps, $unsuppress=false, $comment='')
This is the meaty bit – It restores archived revisions of the given page to the revision table.
UserFactory $userFactory
JobQueueGroup $jobQueueGroup
static listPagesByPrefix( $prefix)
List deleted pages recorded in the archive table matching the given title prefix.
WikiPageFactory $wikiPageFactory
ILoadBalancer $loadBalancer
RevisionStore $revisionStore
isDeleted()
Quick check if any archived revisions are present for the page.
LoggerInterface $logger
getRevisionByConditions(array $conditions, array $options=[])
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
A service class for fetching the wiki's current read-only mode.
Prioritized list of file repositories.
Definition RepoGroup.php:33
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Represents a title within MediaWiki.
Definition Title.php:48
Interface for configuration instances.
Definition Config.php:30
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
Database cluster connection, tracking, load balancing, and transaction manager interface.
Result wrapper for grabbing data queried from an IDatabase object.
const DB_REPLICA
Definition defines.php:25
const DB_PRIMARY
Definition defines.php:27
if(count( $args)< 1) $job
$content
Definition router.php:76
return true
Definition router.php:92