MediaWiki  master
RevisionStore.php
Go to the documentation of this file.
1 <?php
27 namespace MediaWiki\Revision;
28 
29 use ActorMigration;
30 use CommentStore;
32 use Content;
34 use FallbackContent;
35 use IDBAccessObject;
36 use InvalidArgumentException;
52 use Message;
53 use MWException;
54 use MWTimestamp;
56 use Psr\Log\LoggerAwareInterface;
57 use Psr\Log\LoggerInterface;
58 use Psr\Log\NullLogger;
59 use RecentChange;
60 use Revision;
61 use RuntimeException;
62 use StatusValue;
63 use Title;
64 use Traversable;
65 use WANObjectCache;
66 use Wikimedia\Assert\Assert;
67 use Wikimedia\IPUtils;
73 
84  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
85 
87 
88  public const ROW_CACHE_KEY = 'revision-row-1.29';
89 
90  public const ORDER_OLDEST_TO_NEWEST = 'ASC';
91  public const ORDER_NEWEST_TO_OLDEST = 'DESC';
92 
93  // Constants for get(...)Between methods
94  public const INCLUDE_OLD = 'include_old';
95  public const INCLUDE_NEW = 'include_new';
96  public const INCLUDE_BOTH = 'include_both';
97 
101  private $blobStore;
102 
106  private $wikiId;
107 
111  private $loadBalancer;
112 
116  private $cache;
117 
121  private $commentStore;
122 
127 
129  private $actorStore;
130 
134  private $logger;
135 
140 
144  private $slotRoleStore;
145 
148 
151 
153  private $hookContainer;
154 
156  private $hookRunner;
157 
179  public function __construct(
180  ILoadBalancer $loadBalancer,
181  SqlBlobStore $blobStore,
184  NameTableStore $contentModelStore,
185  NameTableStore $slotRoleStore,
188  ActorStore $actorStore,
189  IContentHandlerFactory $contentHandlerFactory,
190  HookContainer $hookContainer,
191  $wikiId = WikiAwareEntity::LOCAL
192  ) {
193  Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
194 
195  $this->loadBalancer = $loadBalancer;
196  $this->blobStore = $blobStore;
197  $this->cache = $cache;
198  $this->commentStore = $commentStore;
199  $this->contentModelStore = $contentModelStore;
200  $this->slotRoleStore = $slotRoleStore;
201  $this->slotRoleRegistry = $slotRoleRegistry;
202  $this->actorMigration = $actorMigration;
203  $this->actorStore = $actorStore;
204  $this->wikiId = $wikiId;
205  $this->logger = new NullLogger();
206  $this->contentHandlerFactory = $contentHandlerFactory;
207  $this->hookContainer = $hookContainer;
208  $this->hookRunner = new HookRunner( $hookContainer );
209  }
210 
211  public function setLogger( LoggerInterface $logger ) {
212  $this->logger = $logger;
213  }
214 
218  public function isReadOnly() {
219  return $this->blobStore->isReadOnly();
220  }
221 
225  private function getDBLoadBalancer() {
226  return $this->loadBalancer;
227  }
228 
234  public function getWikiId() {
235  return $this->wikiId;
236  }
237 
243  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
244  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
245  return $this->getDBConnectionRef( $mode );
246  }
247 
254  private function getDBConnectionRef( $mode, $groups = [] ) {
255  $lb = $this->getDBLoadBalancer();
256  return $lb->getConnectionRef( $mode, $groups, $this->wikiId );
257  }
258 
273  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
274  if ( !$pageId && !$revId ) {
275  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
276  }
277 
278  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
279  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
280  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
281  $queryFlags = self::READ_NORMAL;
282  }
283 
284  $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
285  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
286 
287  // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
288  if ( $canUseTitleNewFromId ) {
289  $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
290  // TODO: better foreign title handling (introduce TitleFactory)
291  $title = Title::newFromID( $pageId, $titleFlags );
292  if ( $title ) {
293  return $title;
294  }
295  }
296 
297  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
298  $canUseRevId = ( $revId !== null && $revId > 0 );
299 
300  if ( $canUseRevId ) {
301  $dbr = $this->getDBConnectionRef( $dbMode );
302  // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
303  $row = $dbr->selectRow(
304  [ 'revision', 'page' ],
305  [
306  'page_namespace',
307  'page_title',
308  'page_id',
309  'page_latest',
310  'page_is_redirect',
311  'page_len',
312  ],
313  [ 'rev_id' => $revId ],
314  __METHOD__,
315  $dbOptions,
316  [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
317  );
318  if ( $row ) {
319  // TODO: better foreign title handling (introduce TitleFactory)
320  return Title::newFromRow( $row );
321  }
322  }
323 
324  // If we still don't have a title, fallback to master if that wasn't already happening.
325  if ( $dbMode !== DB_MASTER ) {
326  $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
327  if ( $title ) {
328  $this->logger->info(
329  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
330  [ 'trace' => wfBacktrace() ]
331  );
332  return $title;
333  }
334  }
335 
336  throw new RevisionAccessException(
337  "Could not determine title for page ID $pageId and revision ID $revId"
338  );
339  }
340 
348  private function failOnNull( $value, $name ) {
349  if ( $value === null ) {
350  throw new IncompleteRevisionException(
351  "$name must not be " . var_export( $value, true ) . "!"
352  );
353  }
354 
355  return $value;
356  }
357 
365  private function failOnEmpty( $value, $name ) {
366  if ( $value === null || $value === 0 || $value === '' ) {
367  throw new IncompleteRevisionException(
368  "$name must not be " . var_export( $value, true ) . "!"
369  );
370  }
371 
372  return $value;
373  }
374 
386  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
387  // TODO: pass in a DBTransactionContext instead of a database connection.
388  $this->checkDatabaseDomain( $dbw );
389 
390  $slotRoles = $rev->getSlotRoles();
391 
392  // Make sure the main slot is always provided throughout migration
393  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
394  throw new IncompleteRevisionException(
395  'main slot must be provided'
396  );
397  }
398 
399  // Checks
400  $this->failOnNull( $rev->getSize(), 'size field' );
401  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
402  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
403  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
404  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
405  $this->failOnNull( $user->getId(), 'user field' );
406  $this->failOnEmpty( $user->getName(), 'user_text field' );
407 
408  if ( !$rev->isReadyForInsertion() ) {
409  // This is here for future-proofing. At the time this check being added, it
410  // was redundant to the individual checks above.
411  throw new IncompleteRevisionException( 'Revision is incomplete' );
412  }
413 
414  if ( $slotRoles == [ SlotRecord::MAIN ] ) {
415  // T239717: If the main slot is the only slot, make sure the revision's nominal size
416  // and hash match the main slot's nominal size and hash.
417  $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
418  Assert::precondition(
419  $mainSlot->getSize() === $rev->getSize(),
420  'The revisions\'s size must match the main slot\'s size (see T239717)'
421  );
422  Assert::precondition(
423  $mainSlot->getSha1() === $rev->getSha1(),
424  'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
425  );
426  }
427 
428  $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early
429 
430  $parentId = $rev->getParentId() === null
431  ? $this->getPreviousRevisionId( $dbw, $rev )
432  : $rev->getParentId();
433 
435  $rev = $dbw->doAtomicSection(
436  __METHOD__,
437  function ( IDatabase $dbw, $fname ) use (
438  $rev,
439  $user,
440  $comment,
441  $pageId,
442  $parentId
443  ) {
444  return $this->insertRevisionInternal(
445  $rev,
446  $dbw,
447  $user,
448  $comment,
449  $rev->getPage(),
450  $pageId,
451  $parentId
452  );
453  }
454  );
455 
456  // sanity checks
457  Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' );
458  Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' );
459  Assert::postcondition(
460  $rev->getComment( RevisionRecord::RAW ) !== null,
461  'revision must have a comment'
462  );
463  Assert::postcondition(
464  $rev->getUser( RevisionRecord::RAW ) !== null,
465  'revision must have a user'
466  );
467 
468  // Trigger exception if the main slot is missing.
469  // Technically, this could go away after MCR migration: while
470  // calling code may require a main slot to exist, RevisionStore
471  // really should not know or care about that requirement.
473 
474  foreach ( $slotRoles as $role ) {
475  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
476  Assert::postcondition(
477  $slot->getContent() !== null,
478  $role . ' slot must have content'
479  );
480  Assert::postcondition(
481  $slot->hasRevision(),
482  $role . ' slot must have a revision associated'
483  );
484  }
485 
486  $this->hookRunner->onRevisionRecordInserted( $rev );
487 
488  // Soft deprecated in 1.31, hard deprecated in 1.35
489  if ( $this->hookContainer->isRegistered( 'RevisionInsertComplete' ) ) {
490  // Only create the Revision object if its needed
491  $legacyRevision = new Revision( $rev );
492  $this->hookRunner->onRevisionInsertComplete( $legacyRevision, null, null );
493  }
494 
495  return $rev;
496  }
497 
498  private function insertRevisionInternal(
499  RevisionRecord $rev,
500  IDatabase $dbw,
501  UserIdentity $user,
502  CommentStoreComment $comment,
503  PageIdentity $page,
504  $pageId,
505  $parentId
506  ) {
507  $slotRoles = $rev->getSlotRoles();
508 
509  $revisionRow = $this->insertRevisionRowOn(
510  $dbw,
511  $rev,
512  $parentId
513  );
514 
515  $revisionId = $revisionRow['rev_id'];
516 
517  $blobHints = [
518  BlobStore::PAGE_HINT => $pageId,
519  BlobStore::REVISION_HINT => $revisionId,
520  BlobStore::PARENT_HINT => $parentId,
521  ];
522 
523  $newSlots = [];
524  foreach ( $slotRoles as $role ) {
525  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
526 
527  // If the SlotRecord already has a revision ID set, this means it already exists
528  // in the database, and should already belong to the current revision.
529  // However, a slot may already have a revision, but no content ID, if the slot
530  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
531  // mode, and the respective archive row was not yet migrated to the new schema.
532  // In that case, a new slot row (and content row) must be inserted even during
533  // undeletion.
534  if ( $slot->hasRevision() && $slot->hasContentId() ) {
535  // TODO: properly abort transaction if the assertion fails!
536  Assert::parameter(
537  $slot->getRevision() === $revisionId,
538  'slot role ' . $slot->getRole(),
539  'Existing slot should belong to revision '
540  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
541  );
542 
543  // Slot exists, nothing to do, move along.
544  // This happens when restoring archived revisions.
545 
546  $newSlots[$role] = $slot;
547  } else {
548  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
549  }
550  }
551 
552  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
553 
554  $rev = new RevisionStoreRecord(
555  $page,
556  $user,
557  $comment,
558  (object)$revisionRow,
559  new RevisionSlots( $newSlots ),
560  $this->wikiId
561  );
562 
563  return $rev;
564  }
565 
574  private function insertSlotOn(
575  IDatabase $dbw,
576  $revisionId,
577  SlotRecord $protoSlot,
578  PageIdentity $page,
579  array $blobHints = []
580  ) {
581  if ( $protoSlot->hasAddress() ) {
582  $blobAddress = $protoSlot->getAddress();
583  } else {
584  $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
585  }
586 
587  $contentId = null;
588 
589  if ( $protoSlot->hasContentId() ) {
590  $contentId = $protoSlot->getContentId();
591  } else {
592  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
593  }
594 
595  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
596 
597  return SlotRecord::newSaved(
598  $revisionId,
599  $contentId,
600  $blobAddress,
601  $protoSlot
602  );
603  }
604 
612  private function insertIpChangesRow(
613  IDatabase $dbw,
614  UserIdentity $user,
615  RevisionRecord $rev,
616  $revisionId
617  ) {
618  if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
619  $ipcRow = [
620  'ipc_rev_id' => $revisionId,
621  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
622  'ipc_hex' => IPUtils::toHex( $user->getName() ),
623  ];
624  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
625  }
626  }
627 
638  private function insertRevisionRowOn(
639  IDatabase $dbw,
640  RevisionRecord $rev,
641  $parentId
642  ) {
643  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
644 
645  list( $commentFields, $commentCallback ) =
646  $this->commentStore->insertWithTempTable(
647  $dbw,
648  'rev_comment',
650  );
651  $revisionRow += $commentFields;
652 
653  list( $actorFields, $actorCallback ) =
654  $this->actorMigration->getInsertValuesWithTempTable(
655  $dbw,
656  'rev_user',
658  );
659  $revisionRow += $actorFields;
660 
661  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
662 
663  if ( !isset( $revisionRow['rev_id'] ) ) {
664  // only if auto-increment was used
665  $revisionRow['rev_id'] = intval( $dbw->insertId() );
666 
667  if ( $dbw->getType() === 'mysql' ) {
668  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
669  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
670  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
671 
672  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
673  $table = 'archive';
674  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
675  if ( $maxRevId2 >= $maxRevId ) {
676  $maxRevId = $maxRevId2;
677  $table = 'slots';
678  }
679 
680  if ( $maxRevId >= $revisionRow['rev_id'] ) {
681  $this->logger->debug(
682  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
683  . ' Trying to fix it.',
684  [
685  'revid' => $revisionRow['rev_id'],
686  'table' => $table,
687  'maxrevid' => $maxRevId,
688  ]
689  );
690 
691  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
692  throw new MWException( 'Failed to get database lock for T202032' );
693  }
694  $fname = __METHOD__;
695  $dbw->onTransactionResolution(
696  static function ( $trigger, IDatabase $dbw ) use ( $fname ) {
697  $dbw->unlock( 'fix-for-T202032', $fname );
698  },
699  __METHOD__
700  );
701 
702  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
703 
704  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
705  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
706  // inserts too, though, at least on MariaDB 10.1.29.
707  //
708  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
709  // transactions in this code path thanks to the row lock from the original ->insert() above.
710  //
711  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
712  // that's for non-MySQL DBs.
713  $row1 = $dbw->query(
714  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
715  __METHOD__
716  )->fetchObject();
717 
718  $row2 = $dbw->query(
719  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
720  . ' FOR UPDATE',
721  __METHOD__
722  )->fetchObject();
723 
724  $maxRevId = max(
725  $maxRevId,
726  $row1 ? intval( $row1->v ) : 0,
727  $row2 ? intval( $row2->v ) : 0
728  );
729 
730  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
731  // transactions will throw a duplicate key error here. It doesn't seem worth trying
732  // to avoid that.
733  $revisionRow['rev_id'] = $maxRevId + 1;
734  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
735  }
736  }
737  }
738 
739  $commentCallback( $revisionRow['rev_id'] );
740  $actorCallback( $revisionRow['rev_id'], $revisionRow );
741 
742  return $revisionRow;
743  }
744 
752  private function getBaseRevisionRow(
753  IDatabase $dbw,
754  RevisionRecord $rev,
755  $parentId
756  ) {
757  // Record the edit in revisions
758  $revisionRow = [
759  'rev_page' => $rev->getPageId( $this->wikiId ),
760  'rev_parent_id' => $parentId,
761  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
762  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
763  'rev_deleted' => $rev->getVisibility(),
764  'rev_len' => $rev->getSize(),
765  'rev_sha1' => $rev->getSha1(),
766  ];
767 
768  if ( $rev->getId( $this->wikiId ) !== null ) {
769  // Needed to restore revisions with their original ID
770  $revisionRow['rev_id'] = $rev->getId( $this->wikiId );
771  }
772 
773  return $revisionRow;
774  }
775 
784  private function storeContentBlob(
785  SlotRecord $slot,
786  PageIdentity $page,
787  array $blobHints = []
788  ) {
789  $content = $slot->getContent();
790  $format = $content->getDefaultFormat();
791  $model = $content->getModel();
792 
793  $this->checkContent( $content, $page, $slot->getRole() );
794 
795  return $this->blobStore->storeBlob(
796  $content->serialize( $format ),
797  // These hints "leak" some information from the higher abstraction layer to
798  // low level storage to allow for optimization.
799  array_merge(
800  $blobHints,
801  [
802  BlobStore::DESIGNATION_HINT => 'page-content',
803  BlobStore::ROLE_HINT => $slot->getRole(),
804  BlobStore::SHA1_HINT => $slot->getSha1(),
805  BlobStore::MODEL_HINT => $model,
806  BlobStore::FORMAT_HINT => $format,
807  ]
808  )
809  );
810  }
811 
818  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
819  $slotRow = [
820  'slot_revision_id' => $revisionId,
821  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
822  'slot_content_id' => $contentId,
823  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
824  // that we just inserted.
825  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
826  ];
827  $dbw->insert( 'slots', $slotRow, __METHOD__ );
828  }
829 
836  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
837  $contentRow = [
838  'content_size' => $slot->getSize(),
839  'content_sha1' => $slot->getSha1(),
840  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
841  'content_address' => $blobAddress,
842  ];
843  $dbw->insert( 'content', $contentRow, __METHOD__ );
844  return intval( $dbw->insertId() );
845  }
846 
857  private function checkContent( Content $content, PageIdentity $page, string $role ) {
858  // Note: may return null for revisions that have not yet been inserted
859 
860  $model = $content->getModel();
861  $format = $content->getDefaultFormat();
862  $handler = $content->getContentHandler();
863 
864  if ( !$handler->isSupportedFormat( $format ) ) {
865  throw new MWException(
866  "Can't use format $format with content model $model on $page role $role"
867  );
868  }
869 
870  if ( !$content->isValid() ) {
871  throw new MWException(
872  "New content for $page role $role is not valid! Content model is $model"
873  );
874  }
875  }
876 
902  public function newNullRevision(
903  IDatabase $dbw,
904  PageIdentity $page,
905  CommentStoreComment $comment,
906  $minor,
907  UserIdentity $user
908  ) {
909  $this->checkDatabaseDomain( $dbw );
910 
911  $pageId = $this->getArticleId( $page );
912 
913  // T51581: Lock the page table row to ensure no other process
914  // is adding a revision to the page at the same time.
915  // Avoid locking extra tables, compare T191892.
916  $pageLatest = $dbw->selectField(
917  'page',
918  'page_latest',
919  [ 'page_id' => $pageId ],
920  __METHOD__,
921  [ 'FOR UPDATE' ]
922  );
923 
924  if ( !$pageLatest ) {
925  $msg = 'T235589: Failed to select table row during null revision creation' .
926  " Page id '$pageId' does not exist.";
927  $this->logger->error(
928  $msg,
929  [ 'exception' => new RuntimeException( $msg ) ]
930  );
931 
932  return null;
933  }
934 
935  // Fetch the actual revision row from master, without locking all extra tables.
936  $oldRevision = $this->loadRevisionFromConds(
937  $dbw,
938  [ 'rev_id' => intval( $pageLatest ) ],
939  self::READ_LATEST,
940  $page
941  );
942 
943  if ( !$oldRevision ) {
944  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
945  $this->logger->error(
946  $msg,
947  [ 'exception' => new RuntimeException( $msg ) ]
948  );
949  return null;
950  }
951 
952  // Construct the new revision
953  $timestamp = MWTimestamp::now( TS_MW );
954  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
955 
956  $newRevision->setComment( $comment );
957  $newRevision->setUser( $user );
958  $newRevision->setTimestamp( $timestamp );
959  $newRevision->setMinorEdit( $minor );
960 
961  return $newRevision;
962  }
963 
973  public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
974  $rc = $this->getRecentChange( $rev );
975  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
976  return $rc->getAttribute( 'rc_id' );
977  } else {
978  return 0;
979  }
980  }
981 
995  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
996  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
997 
999  [ 'rc_this_oldid' => $rev->getId( $this->wikiId ) ],
1000  __METHOD__,
1001  $dbType
1002  );
1003 
1004  // XXX: cache this locally? Glue it to the RevisionRecord?
1005  return $rc;
1006  }
1007 
1027  private function loadSlotContent(
1028  SlotRecord $slot,
1029  $blobData = null,
1030  $blobFlags = null,
1031  $blobFormat = null,
1032  $queryFlags = 0
1033  ) {
1034  if ( $blobData !== null ) {
1035  Assert::parameterType( 'string', $blobData, '$blobData' );
1036  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1037 
1038  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1039 
1040  if ( $blobFlags === null ) {
1041  // No blob flags, so use the blob verbatim.
1042  $data = $blobData;
1043  } else {
1044  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1045  if ( $data === false ) {
1046  throw new RevisionAccessException(
1047  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1048  );
1049  }
1050  }
1051 
1052  } else {
1053  $address = $slot->getAddress();
1054  try {
1055  $data = $this->blobStore->getBlob( $address, $queryFlags );
1056  } catch ( BlobAccessException $e ) {
1057  throw new RevisionAccessException(
1058  "Failed to load data blob from $address: " . $e->getMessage() . '. '
1059  . 'If this problem persist, use the findBadBlobs maintenance script '
1060  . 'to investigate the issue and mark bad blobs.',
1061  0, $e
1062  );
1063  }
1064  }
1065 
1066  $model = $slot->getModel();
1067 
1068  // If the content model is not known, don't fail here (T220594, T220793, T228921)
1069  if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1070  $this->logger->warning(
1071  "Undefined content model '$model', falling back to UnknownContent",
1072  [
1073  'content_address' => $slot->getAddress(),
1074  'rev_id' => $slot->getRevision(),
1075  'role_name' => $slot->getRole(),
1076  'model_name' => $model,
1077  'trace' => wfBacktrace()
1078  ]
1079  );
1080 
1081  return new FallbackContent( $data, $model );
1082  }
1083 
1084  return $this->contentHandlerFactory
1085  ->getContentHandler( $model )
1086  ->unserializeContent( $data, $blobFormat );
1087  }
1088 
1103  public function getRevisionById( $id, $flags = 0 ) {
1104  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1105  }
1106 
1123  public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) {
1124  $conds = [
1125  'page_namespace' => $page->getNamespace(),
1126  'page_title' => $page->getDBkey()
1127  ];
1128 
1129  if ( $page instanceof LinkTarget ) {
1130  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1131  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1132  }
1133 
1134  if ( $revId ) {
1135  // Use the specified revision ID.
1136  // Note that we use newRevisionFromConds here because we want to retry
1137  // and fall back to master if the page is not found on a replica.
1138  // Since the caller supplied a revision ID, we are pretty sure the revision is
1139  // supposed to exist, so we should try hard to find it.
1140  $conds['rev_id'] = $revId;
1141  return $this->newRevisionFromConds( $conds, $flags, $page );
1142  } else {
1143  // Use a join to get the latest revision.
1144  // Note that we don't use newRevisionFromConds here because we don't want to retry
1145  // and fall back to master. The assumption is that we only want to force the fallback
1146  // if we are quite sure the revision exists because the caller supplied a revision ID.
1147  // If the page isn't found at all on a replica, it probably simply does not exist.
1148  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1149  $conds[] = 'rev_id=page_latest';
1150  return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1151  }
1152  }
1153 
1170  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1171  $conds = [ 'page_id' => $pageId ];
1172  if ( $revId ) {
1173  // Use the specified revision ID.
1174  // Note that we use newRevisionFromConds here because we want to retry
1175  // and fall back to master if the page is not found on a replica.
1176  // Since the caller supplied a revision ID, we are pretty sure the revision is
1177  // supposed to exist, so we should try hard to find it.
1178  $conds['rev_id'] = $revId;
1179  return $this->newRevisionFromConds( $conds, $flags );
1180  } else {
1181  // Use a join to get the latest revision.
1182  // Note that we don't use newRevisionFromConds here because we don't want to retry
1183  // and fall back to master. The assumption is that we only want to force the fallback
1184  // if we are quite sure the revision exists because the caller supplied a revision ID.
1185  // If the page isn't found at all on a replica, it probably simply does not exist.
1186  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1187 
1188  $conds[] = 'rev_id=page_latest';
1189 
1190  return $this->loadRevisionFromConds( $db, $conds, $flags );
1191  }
1192  }
1193 
1209  public function getRevisionByTimestamp(
1210  $page,
1211  string $timestamp,
1212  int $flags = IDBAccessObject::READ_NORMAL
1213  ): ?RevisionRecord {
1214  if ( $page instanceof LinkTarget ) {
1215  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1216  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1217  }
1218  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1219  return $this->newRevisionFromConds(
1220  [
1221  'rev_timestamp' => $db->timestamp( $timestamp ),
1222  'page_namespace' => $page->getNamespace(),
1223  'page_title' => $page->getDBkey()
1224  ],
1225  $flags,
1226  $page
1227  );
1228  }
1229 
1237  private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) {
1238  $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1239 
1240  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1241  $db = $this->getDBConnectionRef( $dbMode );
1242 
1243  $res = $db->select(
1244  $revQuery['tables'],
1245  $revQuery['fields'],
1246  [
1247  'slot_revision_id' => $revId,
1248  ],
1249  __METHOD__,
1250  $dbOptions,
1251  $revQuery['joins']
1252  );
1253 
1254  if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1255  // If we found no slots, try looking on the master database (T212428, T252156)
1256  $this->logger->info(
1257  __METHOD__ . ' falling back to READ_LATEST.',
1258  [
1259  'revid' => $revId,
1260  'trace' => wfBacktrace( true )
1261  ]
1262  );
1263  return $this->loadSlotRecords(
1264  $revId,
1265  $queryFlags | self::READ_LATEST,
1266  $page
1267  );
1268  }
1269 
1270  return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1271  }
1272 
1285  private function constructSlotRecords(
1286  $revId,
1287  $slotRows,
1288  $queryFlags,
1289  PageIdentity $page,
1290  $slotContents = null
1291  ) {
1292  $slots = [];
1293 
1294  foreach ( $slotRows as $row ) {
1295  // Resolve role names and model names from in-memory cache, if they were not joined in.
1296  if ( !isset( $row->role_name ) ) {
1297  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1298  }
1299 
1300  if ( !isset( $row->model_name ) ) {
1301  if ( isset( $row->content_model ) ) {
1302  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1303  } else {
1304  // We may get here if $row->model_name is set but null, perhaps because it
1305  // came from rev_content_model, which is NULL for the default model.
1306  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1307  $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1308  }
1309  }
1310 
1311  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1312  if ( isset( $row->blob_data ) ) {
1313  $slotContents[$row->content_address] = $row->blob_data;
1314  }
1315 
1316  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1317  $blob = null;
1318  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1319  $blob = $slotContents[$slot->getAddress()];
1320  if ( $blob instanceof Content ) {
1321  return $blob;
1322  }
1323  }
1324  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1325  };
1326 
1327  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1328  }
1329 
1330  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1331  $this->logger->error(
1332  __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1333  [
1334  'revid' => $revId,
1335  'queryFlags' => $queryFlags,
1336  'trace' => wfBacktrace( true )
1337  ]
1338  );
1339 
1340  throw new RevisionAccessException(
1341  'Main slot of revision not found in database. See T212428.'
1342  );
1343  }
1344 
1345  return $slots;
1346  }
1347 
1363  private function newRevisionSlots(
1364  $revId,
1365  $revisionRow,
1366  $slotRows,
1367  $queryFlags,
1368  PageIdentity $page
1369  ) {
1370  if ( $slotRows ) {
1371  $slots = new RevisionSlots(
1372  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1373  );
1374  } else {
1375  // XXX: do we need the same kind of caching here
1376  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1377 
1378  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) {
1379  return $this->loadSlotRecords( $revId, $queryFlags, $page );
1380  } );
1381  }
1382 
1383  return $slots;
1384  }
1385 
1403  public function newRevisionFromArchiveRow(
1404  $row,
1405  $queryFlags = 0,
1406  PageIdentity $page = null,
1407  array $overrides = []
1408  ) {
1409  return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides );
1410  }
1411 
1424  public function newRevisionFromRow(
1425  $row,
1426  $queryFlags = 0,
1427  PageIdentity $page = null,
1428  $fromCache = false
1429  ) {
1430  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache );
1431  }
1432 
1453  $row,
1454  $slots,
1455  $queryFlags = 0,
1456  PageIdentity $page = null,
1457  array $overrides = []
1458  ) {
1459  Assert::parameterType( \stdClass::class, $row, '$row' );
1460 
1461  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1462  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1463 
1464  if ( !$page && isset( $overrides['title'] ) ) {
1465  if ( !( $overrides['title'] instanceof PageIdentity ) ) {
1466  throw new MWException( 'title field override must contain a PageIdentity object.' );
1467  }
1468 
1469  $page = $overrides['title'];
1470  }
1471 
1472  if ( !isset( $page ) ) {
1473  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1474  $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1475  } else {
1476  throw new InvalidArgumentException(
1477  'A Title or ar_namespace and ar_title must be given'
1478  );
1479  }
1480  }
1481 
1482  foreach ( $overrides as $key => $value ) {
1483  $field = "ar_$key";
1484  $row->$field = $value;
1485  }
1486 
1487  try {
1488  $user = $this->actorStore->newActorFromRowFields(
1489  $row->ar_user ?? null,
1490  $row->ar_user_text ?? null,
1491  $row->ar_actor ?? null
1492  );
1493  } catch ( InvalidArgumentException $ex ) {
1494  $this->logger->warning( 'Could not load user for archive revision {rev_id}', [
1495  'ar_rev_id' => $row->ar_rev_id,
1496  'ar_actor' => $row->ar_actor ?? 'null',
1497  'ar_user_text' => $row->ar_user_text ?? 'null',
1498  'ar_user' => $row->ar_user ?? 'null',
1499  'exception' => $ex
1500  ] );
1501  $user = $this->actorStore->getUnknownActor();
1502  }
1503 
1504  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1505  // Legacy because $row may have come from self::selectFields()
1506  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1507 
1508  if ( !( $slots instanceof RevisionSlots ) ) {
1509  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $page );
1510  }
1511  return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId );
1512  }
1513 
1532  $row,
1533  $slots,
1534  $queryFlags = 0,
1535  PageIdentity $page = null,
1536  $fromCache = false
1537  ) {
1538  Assert::parameterType( \stdClass::class, $row, '$row' );
1539 
1540  if ( !$page ) {
1541  $pageId = (int)( $row->rev_page ?? 0 ); // XXX: fall back to page_id?
1542  $revId = (int)( $row->rev_id ?? 0 );
1543 
1544  $page = $this->getTitle( $pageId, $revId, $queryFlags );
1545  } else {
1546  $this->ensureRevisionRowMatchesPage( $row, $page );
1547  }
1548 
1549  try {
1550  $user = $this->actorStore->newActorFromRowFields(
1551  $row->rev_user ?? null,
1552  $row->rev_user_text ?? null,
1553  $row->rev_actor ?? null
1554  );
1555  } catch ( InvalidArgumentException $ex ) {
1556  $this->logger->warning( 'Could not load user for revision {rev_id}', [
1557  'rev_id' => $row->rev_id,
1558  'rev_actor' => $row->rev_actor ?? 'null',
1559  'rev_user_text' => $row->rev_user_text ?? 'null',
1560  'rev_user' => $row->rev_user ?? 'null',
1561  'exception' => $ex
1562  ] );
1563  $user = $this->actorStore->getUnknownActor();
1564  }
1565 
1566  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1567  // Legacy because $row may have come from self::selectFields()
1568  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1569 
1570  if ( !( $slots instanceof RevisionSlots ) ) {
1571  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $page );
1572  }
1573 
1574  // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1575  if ( $fromCache ) {
1576  $rev = new RevisionStoreCacheRecord(
1577  function ( $revId ) use ( $queryFlags ) {
1578  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1579  $row = $this->fetchRevisionRowFromConds(
1580  $db,
1581  [ 'rev_id' => intval( $revId ) ]
1582  );
1583  if ( !$row && !( $queryFlags & self::READ_LATEST ) ) {
1584  // If we found no slots, try looking on the master database (T259738)
1585  $this->logger->info(
1586  'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1587  [
1588  'revid' => $revId,
1589  'trace' => wfBacktrace( true )
1590  ]
1591  );
1592  $dbw = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
1593  $row = $this->fetchRevisionRowFromConds(
1594  $dbw,
1595  [ 'rev_id' => intval( $revId ) ]
1596  );
1597  }
1598  if ( !$row ) {
1599  return [ null, null ];
1600  }
1601  return [
1602  $row->rev_deleted,
1603  $this->actorStore->newActorFromRowFields(
1604  $row->rev_user ?? null,
1605  $row->rev_user_text ?? null,
1606  $row->rev_actor ?? null
1607  )
1608  ];
1609  },
1610  $page, $user, $comment, $row, $slots, $this->wikiId
1611  );
1612  } else {
1613  $rev = new RevisionStoreRecord(
1614  $page, $user, $comment, $row, $slots, $this->wikiId );
1615  }
1616  return $rev;
1617  }
1618 
1628  private function ensureRevisionRowMatchesTitle( $row, Title $title, $context = [] ) {
1629  $revId = (int)( $row->rev_id ?? 0 );
1630  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1631  $titlePageId = $title->getArticleID();
1632 
1633  // Avoid fatal error when the Title's ID changed, T246720
1634  if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1635  $masterPageId = $title->getArticleID( Title::READ_LATEST );
1636  $masterLatest = $title->getLatestRevID( Title::READ_LATEST );
1637 
1638  if ( $revPageId === $masterPageId ) {
1639  $this->logger->warning(
1640  "Encountered stale Title object",
1641  [
1642  'page_id_stale' => $titlePageId,
1643  'page_id_reloaded' => $masterPageId,
1644  'page_latest' => $masterLatest,
1645  'rev_id' => $revId,
1646  'trace' => wfBacktrace()
1647  ] + $context
1648  );
1649  } else {
1650  throw new RevisionAccessException(
1651  "Revision $revId belongs to page ID $revPageId, "
1652  . "the provided Title object belongs to page ID $masterPageId"
1653  );
1654  }
1655  }
1656  }
1657 
1667  private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) {
1668  if ( $page instanceof Title ) {
1670  return;
1671  }
1672 
1673  $revId = (int)( $row->rev_id ?? 0 );
1674  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1675  $titlePageId = $this->getArticleId( $page );
1676 
1677  // Raise fatal error when the Title's ID changed, T246720
1678  if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1679  throw new RevisionAccessException(
1680  "Revision $revId belongs to page ID $revPageId, "
1681  . "the provided Title object belongs to page ID $titlePageId"
1682  );
1683  }
1684  }
1685 
1711  public function newRevisionsFromBatch(
1712  $rows,
1713  array $options = [],
1714  $queryFlags = 0,
1715  PageIdentity $page = null
1716  ) {
1717  $result = new StatusValue();
1718  $archiveMode = $options['archive'] ?? false;
1719 
1720  if ( $archiveMode ) {
1721  $revIdField = 'ar_rev_id';
1722  } else {
1723  $revIdField = 'rev_id';
1724  }
1725 
1726  $rowsByRevId = [];
1727  $pageIdsToFetchTitles = [];
1728  $titlesByPageKey = [];
1729  foreach ( $rows as $row ) {
1730  if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1731  $result->warning(
1732  'internalerror_info',
1733  "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1734  );
1735  }
1736 
1737  // Attach a page key to the row, so we can find and reuse Title objects easily.
1738  $row->_page_key =
1739  $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1740 
1741  if ( $page ) {
1742  if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1743  throw new InvalidArgumentException(
1744  "Revision {$row->$revIdField} doesn't belong to page "
1745  . $this->getArticleId( $page )
1746  );
1747  }
1748 
1749  if ( $archiveMode
1750  && ( $row->ar_namespace != $page->getNamespace()
1751  || $row->ar_title !== $page->getDBkey() )
1752  ) {
1753  throw new InvalidArgumentException(
1754  "Revision {$row->$revIdField} doesn't belong to page "
1755  . $page
1756  );
1757  }
1758  } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1759  if ( isset( $row->page_namespace ) && isset( $row->page_title )
1760  // This should always be true, but just in case we don't have a page_id
1761  // set or it doesn't match rev_page, let's fetch the title again.
1762  && isset( $row->page_id ) && isset( $row->rev_page )
1763  && $row->rev_page === $row->page_id
1764  ) {
1765  $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1766  } elseif ( $archiveMode ) {
1767  // Can't look up deleted pages by ID, but we have namespace and title
1768  $titlesByPageKey[ $row->_page_key ] =
1769  Title::makeTitle( $row->ar_namespace, $row->ar_title );
1770  } else {
1771  $pageIdsToFetchTitles[] = $row->rev_page;
1772  }
1773  }
1774  $rowsByRevId[$row->$revIdField] = $row;
1775  }
1776 
1777  if ( empty( $rowsByRevId ) ) {
1778  $result->setResult( true, [] );
1779  return $result;
1780  }
1781 
1782  // If the page is not supplied, batch-fetch Title objects.
1783  if ( $page ) {
1784  // same logic as for $row->_page_key above
1785  $pageKey = $archiveMode
1786  ? $page->getNamespace() . ':' . $page->getDBkey()
1787  : $this->getArticleId( $page );
1788 
1789  $titlesByPageKey[$pageKey] = $page;
1790  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1791  // Note: when we fetch titles by ID, the page key is also the ID.
1792  // We should never get here if $archiveMode is true.
1793  Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1794 
1795  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1796  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1797  $titlesByPageKey[$t->getArticleID()] = $t;
1798  }
1799  }
1800 
1801  // which method to use for creating RevisionRecords
1802  $newRevisionRecord = [
1803  $this,
1804  $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1805  ];
1806 
1807  if ( !isset( $options['slots'] ) ) {
1808  $result->setResult(
1809  true,
1810  array_map(
1811  static function ( $row )
1812  use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1813  try {
1814  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1815  $result->warning(
1816  'internalerror_info',
1817  "Couldn't find title for rev {$row->$revIdField} "
1818  . "(page key {$row->_page_key})"
1819  );
1820  return null;
1821  }
1822  return $newRevisionRecord( $row, null, $queryFlags,
1823  $titlesByPageKey[ $row->_page_key ] );
1824  } catch ( MWException $e ) {
1825  $result->warning( 'internalerror_info', $e->getMessage() );
1826  return null;
1827  }
1828  },
1829  $rowsByRevId
1830  )
1831  );
1832  return $result;
1833  }
1834 
1835  $slotRowOptions = [
1836  'slots' => $options['slots'] ?? true,
1837  'blobs' => $options['content'] ?? false,
1838  ];
1839 
1840  if ( is_array( $slotRowOptions['slots'] )
1841  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
1842  ) {
1843  // Make sure the main slot is always loaded, RevisionRecord requires this.
1844  $slotRowOptions['slots'][] = SlotRecord::MAIN;
1845  }
1846 
1847  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
1848 
1849  $result->merge( $slotRowsStatus );
1850  $slotRowsByRevId = $slotRowsStatus->getValue();
1851 
1852  $result->setResult(
1853  true,
1854  array_map(
1855  function ( $row )
1856  use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
1857  $revIdField, $newRevisionRecord
1858  ) {
1859  if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
1860  $result->warning(
1861  'internalerror_info',
1862  "Couldn't find slots for rev {$row->$revIdField}"
1863  );
1864  return null;
1865  }
1866  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1867  $result->warning(
1868  'internalerror_info',
1869  "Couldn't find title for rev {$row->$revIdField} "
1870  . "(page key {$row->_page_key})"
1871  );
1872  return null;
1873  }
1874  try {
1875  return $newRevisionRecord(
1876  $row,
1877  new RevisionSlots(
1878  $this->constructSlotRecords(
1879  $row->$revIdField,
1880  $slotRowsByRevId[$row->$revIdField],
1881  $queryFlags,
1882  $titlesByPageKey[$row->_page_key]
1883  )
1884  ),
1885  $queryFlags,
1886  $titlesByPageKey[$row->_page_key]
1887  );
1888  } catch ( MWException $e ) {
1889  $result->warning( 'internalerror_info', $e->getMessage() );
1890  return null;
1891  }
1892  },
1893  $rowsByRevId
1894  )
1895  );
1896  return $result;
1897  }
1898 
1922  private function getSlotRowsForBatch(
1923  $rowsOrIds,
1924  array $options = [],
1925  $queryFlags = 0
1926  ) {
1927  $result = new StatusValue();
1928 
1929  $revIds = [];
1930  foreach ( $rowsOrIds as $row ) {
1931  if ( is_object( $row ) ) {
1932  $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
1933  } else {
1934  $revIds[] = (int)$row;
1935  }
1936  }
1937 
1938  // Nothing to do.
1939  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
1940  if ( empty( $revIds ) ) {
1941  $result->setResult( true, [] );
1942  return $result;
1943  }
1944 
1945  // We need to set the `content` flag to join in content meta-data
1946  $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
1947  $revIdField = $slotQueryInfo['keys']['rev_id'];
1948  $slotQueryConds = [ $revIdField => $revIds ];
1949 
1950  if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
1951  if ( empty( $options['slots'] ) ) {
1952  // Degenerate case: return no slots for each revision.
1953  $result->setResult( true, array_fill_keys( $revIds, [] ) );
1954  return $result;
1955  }
1956 
1957  $roleIdField = $slotQueryInfo['keys']['role_id'];
1958  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
1959  return $this->slotRoleStore->getId( $slot_name );
1960  }, $options['slots'] );
1961  }
1962 
1963  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1964  $slotRows = $db->select(
1965  $slotQueryInfo['tables'],
1966  $slotQueryInfo['fields'],
1967  $slotQueryConds,
1968  __METHOD__,
1969  [],
1970  $slotQueryInfo['joins']
1971  );
1972 
1973  $slotContents = null;
1974  if ( $options['blobs'] ?? false ) {
1975  $blobAddresses = [];
1976  foreach ( $slotRows as $slotRow ) {
1977  $blobAddresses[] = $slotRow->content_address;
1978  }
1979  $slotContentFetchStatus = $this->blobStore
1980  ->getBlobBatch( $blobAddresses, $queryFlags );
1981  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
1982  $result->warning( $error['message'], ...$error['params'] );
1983  }
1984  $slotContents = $slotContentFetchStatus->getValue();
1985  }
1986 
1987  $slotRowsByRevId = [];
1988  foreach ( $slotRows as $slotRow ) {
1989  if ( $slotContents === null ) {
1990  // nothing to do
1991  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
1992  $slotRow->blob_data = $slotContents[$slotRow->content_address];
1993  } else {
1994  $result->warning(
1995  'internalerror_info',
1996  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
1997  );
1998  $slotRow->blob_data = null;
1999  }
2000 
2001  // conditional needed for SCHEMA_COMPAT_READ_OLD
2002  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2003  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2004  }
2005 
2006  // conditional needed for SCHEMA_COMPAT_READ_OLD
2007  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2008  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2009  }
2010 
2011  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2012  }
2013 
2014  $result->setResult( true, $slotRowsByRevId );
2015  return $result;
2016  }
2017 
2038  public function getContentBlobsForBatch(
2039  $rowsOrIds,
2040  $slots = null,
2041  $queryFlags = 0
2042  ) {
2043  $result = $this->getSlotRowsForBatch(
2044  $rowsOrIds,
2045  [ 'slots' => $slots, 'blobs' => true ],
2046  $queryFlags
2047  );
2048 
2049  if ( $result->isOK() ) {
2050  // strip out all internal meta data that we don't want to expose
2051  foreach ( $result->value as $revId => $rowsByRole ) {
2052  foreach ( $rowsByRole as $role => $slotRow ) {
2053  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2054  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2055  // if we didn't ask for it.
2056  unset( $result->value[$revId][$role] );
2057  continue;
2058  }
2059 
2060  $result->value[$revId][$role] = (object)[
2061  'blob_data' => $slotRow->blob_data,
2062  'model_name' => $slotRow->model_name,
2063  ];
2064  }
2065  }
2066  }
2067 
2068  return $result;
2069  }
2070 
2088  array $fields,
2089  $queryFlags = 0,
2090  PageIdentity $page = null
2091  ) {
2092  wfDeprecated( __METHOD__, '1.31' );
2093 
2094  if ( !$page && isset( $fields['title'] ) ) {
2095  if ( !( $fields['title'] instanceof PageIdentity ) ) {
2096  throw new MWException( 'title field must contain a Title object.' );
2097  }
2098 
2099  $page = $fields['title'];
2100  }
2101 
2102  if ( !$page ) {
2103  $pageId = $fields['page'] ?? 0;
2104  $revId = $fields['id'] ?? 0;
2105 
2106  $page = $this->getTitle( $pageId, $revId, $queryFlags );
2107  }
2108 
2109  if ( !isset( $fields['page'] ) ) {
2110  $fields['page'] = $this->getArticleId( $page );
2111  }
2112 
2113  // if we have a content object, use it to set the model and type
2114  if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
2115  && !is_array( $fields['content'] )
2116  ) {
2117  throw new MWException(
2118  'content field must contain a Content object or an array of Content objects.'
2119  );
2120  }
2121 
2122  if ( !empty( $fields['text_id'] ) ) {
2123  throw new MWException( 'The text_id field can not be used in MediaWiki 1.35 and later' );
2124  }
2125 
2126  if (
2127  isset( $fields['comment'] )
2128  && !( $fields['comment'] instanceof CommentStoreComment )
2129  ) {
2130  $commentData = $fields['comment_data'] ?? null;
2131 
2132  if ( $fields['comment'] instanceof Message ) {
2133  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2134  $fields['comment'],
2135  $commentData
2136  );
2137  } else {
2138  $commentText = trim( strval( $fields['comment'] ) );
2139  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2140  $commentText,
2141  $commentData
2142  );
2143  }
2144  }
2145 
2146  $revision = new MutableRevisionRecord( $page, $this->wikiId );
2147 
2149  if ( isset( $fields['content'] ) ) {
2150  if ( is_array( $fields['content'] ) ) {
2151  $slotContent = $fields['content'];
2152  } else {
2153  $slotContent = [ SlotRecord::MAIN => $fields['content'] ];
2154  }
2155  } elseif ( isset( $fields['text'] ) ) {
2156  if ( isset( $fields['content_model'] ) ) {
2157  $model = $fields['content_model'];
2158  } else {
2159  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( SlotRecord::MAIN );
2160  $model = $slotRoleHandler->getDefaultModel( $page );
2161  }
2162 
2163  $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
2164  $content = $contentHandler->unserializeContent( $fields['text'] );
2165  $slotContent = [ SlotRecord::MAIN => $content ];
2166  } else {
2167  $slotContent = [];
2168  }
2169 
2170  foreach ( $slotContent as $role => $content ) {
2171  $revision->setContent( $role, $content );
2172  }
2173 
2174  $this->initializeMutableRevisionFromArray( $revision, $fields );
2175 
2176  return $revision;
2177  }
2178 
2184  MutableRevisionRecord $record,
2185  array $fields
2186  ) {
2188  $user = null;
2189  if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
2190  $fields['user']->assertWiki( $this->wikiId );
2191  $user = $fields['user'];
2192  } else {
2193  $actorID = isset( $fields['actor'] ) && is_numeric( $fields['actor'] ) ? (int)$fields['actor'] : null;
2194  $userID = isset( $fields['user'] ) && is_numeric( $fields['user'] ) ? (int)$fields['user'] : null;
2195  try {
2196  $user = $this->actorStore->newActorFromRowFields(
2197  $userID,
2198  $fields['user_text'] ?? null,
2199  $actorID
2200  );
2201  } catch ( InvalidArgumentException $ex ) {
2202  $user = null;
2203  }
2204  if ( !$user && $actorID ) {
2205  try {
2206  $user = $this->actorStore->getActorById( $actorID );
2207  } catch ( InvalidArgumentException $ex ) {
2208  $user = null;
2209  }
2210  }
2211  if ( !$user ) {
2212  try {
2213  if ( $userID ) {
2214  $fromUserId = $this->actorStore->getUserIdentityByUserId( $userID );
2215  if ( $fromUserId ) {
2216  $user = $fromUserId;
2217  } elseif ( $fields['user_text'] ?? null ) {
2218  $fromName = $this->actorStore
2219  ->getUserIdentityByName( $fields['user_text'] ?? null );
2220  if ( $fromName ) {
2221  $user = $fromName;
2222  }
2223  }
2224  }
2225  } catch ( InvalidArgumentException $ex ) {
2226  $user = null;
2227  }
2228  }
2229  // Could not initialize the user, maybe it doesn't exist?
2230  if ( isset( $fields['user_text'] ) ) {
2231  $user = new UserIdentityValue(
2232  $userID === null ? 0 : $userID,
2233  $fields['user_text'],
2234  $fields['actor'] ?? 0,
2235  $this->wikiId
2236  );
2237  }
2238  }
2239 
2240  if ( $user ) {
2241  $record->setUser( $user );
2242  }
2243 
2244  $timestamp = isset( $fields['timestamp'] )
2245  ? strval( $fields['timestamp'] )
2246  : MWTimestamp::now( TS_MW );
2247 
2248  $record->setTimestamp( $timestamp );
2249 
2250  if ( isset( $fields['page'] ) ) {
2251  $record->setPageId( intval( $fields['page'] ) );
2252  }
2253 
2254  if ( isset( $fields['id'] ) ) {
2255  $record->setId( intval( $fields['id'] ) );
2256  }
2257  if ( isset( $fields['parent_id'] ) ) {
2258  $record->setParentId( intval( $fields['parent_id'] ) );
2259  }
2260 
2261  if ( isset( $fields['sha1'] ) ) {
2262  $record->setSha1( $fields['sha1'] );
2263  }
2264 
2265  if ( isset( $fields['size'] ) ) {
2266  $record->setSize( intval( $fields['size'] ) );
2267  } elseif ( isset( $fields['len'] ) ) {
2268  $record->setSize( intval( $fields['len'] ) );
2269  }
2270 
2271  if ( isset( $fields['minor_edit'] ) ) {
2272  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2273  }
2274  if ( isset( $fields['deleted'] ) ) {
2275  $record->setVisibility( intval( $fields['deleted'] ) );
2276  }
2277 
2278  if ( isset( $fields['comment'] ) ) {
2279  Assert::parameterType(
2280  CommentStoreComment::class,
2281  $fields['comment'],
2282  '$row[\'comment\']'
2283  );
2284  $record->setComment( $fields['comment'] );
2285  }
2286  }
2287 
2302  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2303  wfDeprecated( __METHOD__, '1.35' );
2304  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2305  if ( $id ) {
2306  $conds['rev_id'] = intval( $id );
2307  } else {
2308  $conds[] = 'rev_id=page_latest';
2309  }
2310  return $this->loadRevisionFromConds( $db, $conds );
2311  }
2312 
2330  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2331  wfDeprecated( __METHOD__, '1.35' );
2332  if ( $id ) {
2333  $matchId = intval( $id );
2334  } else {
2335  $matchId = 'page_latest';
2336  }
2337 
2338  return $this->loadRevisionFromConds(
2339  $db,
2340  [
2341  "rev_id=$matchId",
2342  'page_namespace' => $title->getNamespace(),
2343  'page_title' => $title->getDBkey()
2344  ],
2345  0,
2346  $title
2347  );
2348  }
2349 
2364  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2365  wfDeprecated( __METHOD__, '1.35' );
2366  return $this->loadRevisionFromConds( $db,
2367  [
2368  'rev_timestamp' => $db->timestamp( $timestamp ),
2369  'page_namespace' => $title->getNamespace(),
2370  'page_title' => $title->getDBkey()
2371  ],
2372  0,
2373  $title
2374  );
2375  }
2376 
2393  private function newRevisionFromConds(
2394  array $conditions,
2395  int $flags = IDBAccessObject::READ_NORMAL,
2396  PageIdentity $page = null,
2397  array $options = []
2398  ) {
2399  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2400  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2401 
2402  $lb = $this->getDBLoadBalancer();
2403 
2404  // Make sure new pending/committed revision are visibile later on
2405  // within web requests to certain avoid bugs like T93866 and T94407.
2406  if ( !$rev
2407  && !( $flags & self::READ_LATEST )
2408  && $lb->hasStreamingReplicaServers()
2409  && $lb->hasOrMadeRecentMasterChanges()
2410  ) {
2411  $flags = self::READ_LATEST;
2412  $dbw = $this->getDBConnectionRef( DB_MASTER );
2413  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2414  }
2415 
2416  return $rev;
2417  }
2418 
2433  private function loadRevisionFromConds(
2434  IDatabase $db,
2435  array $conditions,
2436  int $flags = IDBAccessObject::READ_NORMAL,
2437  PageIdentity $page = null,
2438  array $options = []
2439  ) {
2440  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2441  if ( $row ) {
2442  return $this->newRevisionFromRow( $row, $flags, $page );
2443  }
2444 
2445  return null;
2446  }
2447 
2455  private function checkDatabaseDomain( IDatabase $db ) {
2456  $dbDomain = $db->getDomainID();
2457  $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2458  if ( $dbDomain === $storeDomain ) {
2459  return;
2460  }
2461 
2462  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2463  }
2464 
2478  private function fetchRevisionRowFromConds(
2479  IDatabase $db,
2480  array $conditions,
2481  int $flags = IDBAccessObject::READ_NORMAL,
2482  array $options = []
2483  ) {
2484  $this->checkDatabaseDomain( $db );
2485 
2486  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2487  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2488  $options[] = 'FOR UPDATE';
2489  }
2490  return $db->selectRow(
2491  $revQuery['tables'],
2492  $revQuery['fields'],
2493  $conditions,
2494  __METHOD__,
2495  $options,
2496  $revQuery['joins']
2497  );
2498  }
2499 
2521  public function getQueryInfo( $options = [] ) {
2522  $ret = [
2523  'tables' => [],
2524  'fields' => [],
2525  'joins' => [],
2526  ];
2527 
2528  $ret['tables'][] = 'revision';
2529  $ret['fields'] = array_merge( $ret['fields'], [
2530  'rev_id',
2531  'rev_page',
2532  'rev_timestamp',
2533  'rev_minor_edit',
2534  'rev_deleted',
2535  'rev_len',
2536  'rev_parent_id',
2537  'rev_sha1',
2538  ] );
2539 
2540  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2541  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2542  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2543  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2544 
2545  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2546  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2547  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2548  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2549 
2550  if ( in_array( 'page', $options, true ) ) {
2551  $ret['tables'][] = 'page';
2552  $ret['fields'] = array_merge( $ret['fields'], [
2553  'page_namespace',
2554  'page_title',
2555  'page_id',
2556  'page_latest',
2557  'page_is_redirect',
2558  'page_len',
2559  ] );
2560  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2561  }
2562 
2563  if ( in_array( 'user', $options, true ) ) {
2564  $ret['tables'][] = 'user';
2565  $ret['fields'] = array_merge( $ret['fields'], [
2566  'user_name',
2567  ] );
2568  $u = $actorQuery['fields']['rev_user'];
2569  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2570  }
2571 
2572  if ( in_array( 'text', $options, true ) ) {
2573  throw new InvalidArgumentException(
2574  'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2575  );
2576  }
2577 
2578  return $ret;
2579  }
2580 
2601  public function getSlotsQueryInfo( $options = [] ) {
2602  $ret = [
2603  'tables' => [],
2604  'fields' => [],
2605  'joins' => [],
2606  'keys' => [],
2607  ];
2608 
2609  $ret['keys']['rev_id'] = 'slot_revision_id';
2610  $ret['keys']['role_id'] = 'slot_role_id';
2611 
2612  $ret['tables'][] = 'slots';
2613  $ret['fields'] = array_merge( $ret['fields'], [
2614  'slot_revision_id',
2615  'slot_content_id',
2616  'slot_origin',
2617  'slot_role_id',
2618  ] );
2619 
2620  if ( in_array( 'role', $options, true ) ) {
2621  // Use left join to attach role name, so we still find the revision row even
2622  // if the role name is missing. This triggers a more obvious failure mode.
2623  $ret['tables'][] = 'slot_roles';
2624  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2625  $ret['fields'][] = 'role_name';
2626  }
2627 
2628  if ( in_array( 'content', $options, true ) ) {
2629  $ret['keys']['model_id'] = 'content_model';
2630 
2631  $ret['tables'][] = 'content';
2632  $ret['fields'] = array_merge( $ret['fields'], [
2633  'content_size',
2634  'content_sha1',
2635  'content_address',
2636  'content_model',
2637  ] );
2638  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2639 
2640  if ( in_array( 'model', $options, true ) ) {
2641  // Use left join to attach model name, so we still find the revision row even
2642  // if the model name is missing. This triggers a more obvious failure mode.
2643  $ret['tables'][] = 'content_models';
2644  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2645  $ret['fields'][] = 'model_name';
2646  }
2647 
2648  }
2649 
2650  return $ret;
2651  }
2652 
2666  public function getArchiveQueryInfo() {
2667  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2668  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2669  $ret = [
2670  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2671  'fields' => [
2672  'ar_id',
2673  'ar_page_id',
2674  'ar_namespace',
2675  'ar_title',
2676  'ar_rev_id',
2677  'ar_timestamp',
2678  'ar_minor_edit',
2679  'ar_deleted',
2680  'ar_len',
2681  'ar_parent_id',
2682  'ar_sha1',
2683  ] + $commentQuery['fields'] + $actorQuery['fields'],
2684  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2685  ];
2686 
2687  return $ret;
2688  }
2689 
2699  public function getRevisionSizes( array $revIds ) {
2700  $dbr = $this->getDBConnectionRef( DB_REPLICA );
2701  $revLens = [];
2702  if ( !$revIds ) {
2703  return $revLens; // empty
2704  }
2705 
2706  $res = $dbr->select(
2707  'revision',
2708  [ 'rev_id', 'rev_len' ],
2709  [ 'rev_id' => $revIds ],
2710  __METHOD__
2711  );
2712 
2713  foreach ( $res as $row ) {
2714  $revLens[$row->rev_id] = intval( $row->rev_len );
2715  }
2716 
2717  return $revLens;
2718  }
2719 
2732  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2733  wfDeprecated( __METHOD__, '1.35' );
2734  return $this->getRevisionSizes( $revIds );
2735  }
2736 
2745  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2746  $op = $dir === 'next' ? '>' : '<';
2747  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2748 
2749  $revisionIdValue = $rev->getId( $this->wikiId );
2750 
2751  if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) {
2752  // revision is unsaved or otherwise incomplete
2753  return null;
2754  }
2755 
2756  if ( $rev instanceof RevisionArchiveRecord ) {
2757  // revision is deleted, so it's not part of the page history
2758  return null;
2759  }
2760 
2761  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2762  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2763 
2764  $ts = $this->getTimestampFromId( $revisionIdValue, $flags );
2765  if ( $ts === false ) {
2766  // XXX Should this be moved into getTimestampFromId?
2767  $ts = $db->selectField( 'archive', 'ar_timestamp',
2768  [ 'ar_rev_id' => $revisionIdValue ], __METHOD__ );
2769  if ( $ts === false ) {
2770  // XXX Is this reachable? How can we have a page id but no timestamp?
2771  return null;
2772  }
2773  }
2774  $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2775 
2776  $revId = $db->selectField( 'revision', 'rev_id',
2777  [
2778  'rev_page' => $rev->getPageId( $this->wikiId ),
2779  "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op $revisionIdValue )"
2780  ],
2781  __METHOD__,
2782  [
2783  'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2784  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2785  ]
2786  );
2787 
2788  if ( $revId === false ) {
2789  return null;
2790  }
2791 
2792  return $this->getRevisionById( intval( $revId ) );
2793  }
2794 
2810  public function getPreviousRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2811  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2812  }
2813 
2827  public function getNextRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2828  return $this->getRelativeRevision( $rev, $flags, 'next' );
2829  }
2830 
2842  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2843  $this->checkDatabaseDomain( $db );
2844 
2845  if ( $rev->getPageId( $this->wikiId ) === null ) {
2846  return 0;
2847  }
2848  # Use page_latest if ID is not given
2849  if ( !$rev->getId( $this->wikiId ) ) {
2850  $prevId = $db->selectField(
2851  'page', 'page_latest',
2852  [ 'page_id' => $rev->getPageId( $this->wikiId ) ],
2853  __METHOD__
2854  );
2855  } else {
2856  $prevId = $db->selectField(
2857  'revision', 'rev_id',
2858  [ 'rev_page' => $rev->getPageId( $this->wikiId ), 'rev_id < ' . $rev->getId( $this->wikiId ) ],
2859  __METHOD__,
2860  [ 'ORDER BY' => 'rev_id DESC' ]
2861  );
2862  }
2863  return intval( $prevId );
2864  }
2865 
2878  public function getTimestampFromId( $id, $flags = 0 ) {
2879  if ( $id instanceof Title ) {
2880  // Old deprecated calling convention supported for backwards compatibility
2881  $id = $flags;
2882  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2883  }
2884 
2885  // T270149: Bail out if we know the query will definitely return false. Some callers are
2886  // passing RevisionRecord::getId() call directly as $id which can possibly return null.
2887  // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL'
2888  // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null.
2889  // @todo typehint $id and remove the null check
2890  if ( $id === null || $id <= 0 ) {
2891  return false;
2892  }
2893 
2894  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2895 
2896  $timestamp =
2897  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2898 
2899  return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2900  }
2901 
2911  public function countRevisionsByPageId( IDatabase $db, $id ) {
2912  $this->checkDatabaseDomain( $db );
2913 
2914  $row = $db->selectRow( 'revision',
2915  [ 'revCount' => 'COUNT(*)' ],
2916  [ 'rev_page' => $id ],
2917  __METHOD__
2918  );
2919  if ( $row ) {
2920  return intval( $row->revCount );
2921  }
2922  return 0;
2923  }
2924 
2934  public function countRevisionsByTitle( IDatabase $db, PageIdentity $page ) {
2935  $id = $this->getArticleId( $page );
2936  if ( $id ) {
2937  return $this->countRevisionsByPageId( $db, $id );
2938  }
2939  return 0;
2940  }
2941 
2960  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2961  $this->checkDatabaseDomain( $db );
2962 
2963  if ( !$userId ) {
2964  return false;
2965  }
2966 
2967  $revQuery = $this->getQueryInfo();
2968  $res = $db->select(
2969  $revQuery['tables'],
2970  [
2971  'rev_user' => $revQuery['fields']['rev_user'],
2972  ],
2973  [
2974  'rev_page' => $pageId,
2975  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2976  ],
2977  __METHOD__,
2978  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2979  $revQuery['joins']
2980  );
2981  foreach ( $res as $row ) {
2982  if ( $row->rev_user != $userId ) {
2983  return false;
2984  }
2985  }
2986  return true;
2987  }
2988 
3002  public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) {
3003  $db = $this->getDBConnectionRef( DB_REPLICA );
3004  if ( !$page instanceof Title ) {
3005 
3006  // TODO: For foreign wikis we can not cast from PageIdentityValue to Title,
3007  // since getLatestRevID will fetch from local database. To be fixed with cross-wiki
3008  // aware PageStore. T274067
3009  $page->assertWiki( PageIdentity::LOCAL );
3011  } else {
3012  $title = $page;
3013  }
3014  $revIdPassed = $revId;
3015  $pageId = $this->getArticleID( $title );
3016 
3017  if ( !$pageId ) {
3018  return false;
3019  }
3020 
3021  if ( !$revId ) {
3022  $revId = $title->getLatestRevID();
3023  }
3024 
3025  if ( !$revId ) {
3026  wfWarn(
3027  'No latest revision known for page ' . $title->getPrefixedDBkey()
3028  . ' even though it exists with page ID ' . $pageId
3029  );
3030  return false;
3031  }
3032 
3033  // Load the row from cache if possible. If not possible, populate the cache.
3034  // As a minor optimization, remember if this was a cache hit or miss.
3035  // We can sometimes avoid a database query later if this is a cache miss.
3036  $fromCache = true;
3037  $row = $this->cache->getWithSetCallback(
3038  // Page/rev IDs passed in from DB to reflect history merges
3039  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
3040  WANObjectCache::TTL_WEEK,
3041  function ( $curValue, &$ttl, array &$setOpts ) use (
3042  $db, $revId, &$fromCache
3043  ) {
3044  $setOpts += Database::getCacheSetOptions( $db );
3045  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
3046  if ( $row ) {
3047  $fromCache = false;
3048  }
3049  return $row; // don't cache negatives
3050  }
3051  );
3052 
3053  // Reflect revision deletion and user renames.
3054  if ( $row ) {
3055  $this->ensureRevisionRowMatchesTitle( $row, $title, [
3056  'from_cache_flag' => $fromCache,
3057  'page_id_initial' => $pageId,
3058  'rev_id_used' => $revId,
3059  'rev_id_requested' => $revIdPassed,
3060  ] );
3061 
3062  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
3063  } else {
3064  return false;
3065  }
3066  }
3067 
3076  public function getFirstRevision(
3077  $page,
3078  int $flags = IDBAccessObject::READ_NORMAL
3079  ): ?RevisionRecord {
3080  if ( $page instanceof LinkTarget ) {
3081  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
3082  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
3083  }
3084  return $this->newRevisionFromConds(
3085  [
3086  'page_namespace' => $page->getNamespace(),
3087  'page_title' => $page->getDBkey()
3088  ],
3089  $flags,
3090  $page,
3091  [
3092  'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
3093  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
3094  ]
3095  );
3096  }
3097 
3109  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3110  return $this->cache->makeGlobalKey(
3111  self::ROW_CACHE_KEY,
3112  $db->getDomainID(),
3113  $pageId,
3114  $revId
3115  );
3116  }
3117 
3125  private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
3126  if ( $rev ) {
3127  if ( $rev->getId( $this->wikiId ) === null ) {
3128  throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
3129  }
3130  if ( $rev->getPageId( $this->wikiId ) !== $pageId ) {
3131  throw new InvalidArgumentException(
3132  "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
3133  );
3134  }
3135  }
3136  }
3137 
3152  private function getRevisionLimitConditions(
3153  IDatabase $dbr,
3154  RevisionRecord $old = null,
3155  RevisionRecord $new = null,
3156  $options = []
3157  ) {
3158  $options = (array)$options;
3159  $oldCmp = '>';
3160  $newCmp = '<';
3161  if ( in_array( self::INCLUDE_OLD, $options ) ) {
3162  $oldCmp = '>=';
3163  }
3164  if ( in_array( self::INCLUDE_NEW, $options ) ) {
3165  $newCmp = '<=';
3166  }
3167  if ( in_array( self::INCLUDE_BOTH, $options ) ) {
3168  $oldCmp = '>=';
3169  $newCmp = '<=';
3170  }
3171 
3172  $conds = [];
3173  if ( $old ) {
3174  $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3175  $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId( $this->wikiId )}) " .
3176  "OR rev_timestamp > {$oldTs}";
3177  }
3178  if ( $new ) {
3179  $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3180  $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId( $this->wikiId )}) " .
3181  "OR rev_timestamp < {$newTs}";
3182  }
3183  return $conds;
3184  }
3185 
3212  public function getRevisionIdsBetween(
3213  int $pageId,
3214  RevisionRecord $old = null,
3215  RevisionRecord $new = null,
3216  ?int $max = null,
3217  $options = [],
3218  ?string $order = null,
3219  int $flags = IDBAccessObject::READ_NORMAL
3220  ) : array {
3221  $this->assertRevisionParameter( 'old', $pageId, $old );
3222  $this->assertRevisionParameter( 'new', $pageId, $new );
3223 
3224  $options = (array)$options;
3225  $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3226  in_array( self::INCLUDE_BOTH, $options );
3227  $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3228  in_array( self::INCLUDE_BOTH, $options );
3229 
3230  // No DB query needed if old and new are the same revision.
3231  // Can't check for consecutive revisions with 'getParentId' for a similar
3232  // optimization as edge cases exist when there are revisions between
3233  // a revision and it's parent. See T185167 for more details.
3234  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3235  return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3236  }
3237 
3238  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3239  $conds = array_merge(
3240  [
3241  'rev_page' => $pageId,
3242  $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3243  ],
3244  $this->getRevisionLimitConditions( $db, $old, $new, $options )
3245  );
3246 
3247  $queryOptions = [];
3248  if ( $order !== null ) {
3249  $queryOptions['ORDER BY'] = [ "rev_timestamp $order", "rev_id $order" ];
3250  }
3251  if ( $max !== null ) {
3252  $queryOptions['LIMIT'] = $max + 1; // extra to detect truncation
3253  }
3254 
3255  $values = $db->selectFieldValues(
3256  'revision',
3257  'rev_id',
3258  $conds,
3259  __METHOD__,
3260  $queryOptions
3261  );
3262  return array_map( 'intval', $values );
3263  }
3264 
3286  public function getAuthorsBetween(
3287  $pageId,
3288  RevisionRecord $old = null,
3289  RevisionRecord $new = null,
3290  Authority $performer = null,
3291  $max = null,
3292  $options = []
3293  ) {
3294  $this->assertRevisionParameter( 'old', $pageId, $old );
3295  $this->assertRevisionParameter( 'new', $pageId, $new );
3296  $options = (array)$options;
3297 
3298  // No DB query needed if old and new are the same revision.
3299  // Can't check for consecutive revisions with 'getParentId' for a similar
3300  // optimization as edge cases exist when there are revisions between
3301  //a revision and it's parent. See T185167 for more details.
3302  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3303  if ( empty( $options ) ) {
3304  return [];
3305  } elseif ( $performer ) {
3306  return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3307  } else {
3308  return [ $new->getUser() ];
3309  }
3310  }
3311 
3312  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3313  $conds = array_merge(
3314  [
3315  'rev_page' => $pageId,
3316  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3317  ],
3318  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3319  );
3320 
3321  $queryOpts = [ 'DISTINCT' ];
3322  if ( $max !== null ) {
3323  $queryOpts['LIMIT'] = $max + 1;
3324  }
3325 
3326  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3327  return array_map( function ( $row ) {
3328  return $this->actorStore->newActorFromRowFields(
3329  $row->rev_user,
3330  $row->rev_user_text,
3331  $row->rev_actor
3332  );
3333  }, iterator_to_array( $dbr->select(
3334  array_merge( [ 'revision' ], $actorQuery['tables'] ),
3335  $actorQuery['fields'],
3336  $conds, __METHOD__,
3337  $queryOpts,
3338  $actorQuery['joins']
3339  ) ) );
3340  }
3341 
3363  public function countAuthorsBetween(
3364  $pageId,
3365  RevisionRecord $old = null,
3366  RevisionRecord $new = null,
3367  Authority $performer = null,
3368  $max = null,
3369  $options = []
3370  ) {
3371  // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3372  // and creation of UserIdentity stuff.
3373  return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3374  }
3375 
3396  public function countRevisionsBetween(
3397  $pageId,
3398  RevisionRecord $old = null,
3399  RevisionRecord $new = null,
3400  $max = null,
3401  $options = []
3402  ) {
3403  $this->assertRevisionParameter( 'old', $pageId, $old );
3404  $this->assertRevisionParameter( 'new', $pageId, $new );
3405 
3406  // No DB query needed if old and new are the same revision.
3407  // Can't check for consecutive revisions with 'getParentId' for a similar
3408  // optimization as edge cases exist when there are revisions between
3409  //a revision and it's parent. See T185167 for more details.
3410  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3411  return 0;
3412  }
3413 
3414  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3415  $conds = array_merge(
3416  [
3417  'rev_page' => $pageId,
3418  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3419  ],
3420  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3421  );
3422  if ( $max !== null ) {
3423  return $dbr->selectRowCount( 'revision', '1',
3424  $conds,
3425  __METHOD__,
3426  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3427  );
3428  } else {
3429  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3430  }
3431  }
3432 
3433  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3434 }
3435 
3440 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Revision\RevisionStore\ORDER_OLDEST_TO_NEWEST
const ORDER_OLDEST_TO_NEWEST
Definition: RevisionStore.php:90
Revision\RevisionStore\getWikiId
getWikiId()
Get the ID of the wiki this revision belongs to.
Definition: RevisionStore.php:234
Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, PageIdentity $page)
Definition: RevisionStore.php:1237
Revision\MutableRevisionRecord\setMinorEdit
setMinorEdit( $minorEdit)
Definition: MutableRevisionRecord.php:270
Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:121
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:37
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:65
Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:134
Revision\RevisionStore\ensureRevisionRowMatchesTitle
ensureRevisionRowMatchesTitle( $row, Title $title, $context=[])
Check that the given row matches the given Title object.
Definition: RevisionStore.php:1628
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:34
Revision\RevisionStore\$hookContainer
HookContainer $hookContainer
Definition: RevisionStore.php:153
MediaWiki\Storage\BlobStore\PAGE_HINT
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with.
Definition: BlobStore.php:48
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:50
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:34
Revision\RevisionStore\checkDatabaseDomain
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
Definition: RevisionStore.php:2455
MediaWiki\Storage\BlobStore\DESIGNATION_HINT
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition: BlobStore.php:42
MediaWiki\Storage\BlobAccessException
Exception representing a failure to access a data blob.
Definition: BlobAccessException.php:33
Revision\RevisionStore\newNullRevision
newNullRevision(IDatabase $dbw, PageIdentity $page, CommentStoreComment $comment, $minor, UserIdentity $user)
Create a new null-revision for insertion into a page's history.
Definition: RevisionStore.php:902
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:43
Revision\RevisionStore\getRecentChange
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
Definition: RevisionStore.php:995
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:32
Revision\RevisionStore\__construct
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorMigration $actorMigration, ActorStore $actorStore, IContentHandlerFactory $contentHandlerFactory, HookContainer $hookContainer, $wikiId=WikiAwareEntity::LOCAL)
Definition: RevisionStore.php:179
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:295
Revision\RevisionRecord\getPageId
getPageId( $wikiId=self::LOCAL)
Get the page ID.
Definition: RevisionRecord.php:325
Revision\RevisionStore\getAuthorsBetween
getAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, Authority $performer=null, $max=null, $options=[])
Get the authors between the given revisions or revisions.
Definition: RevisionStore.php:3286
Revision\RevisionStore\$actorStore
ActorStore $actorStore
Definition: RevisionStore.php:129
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:428
Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:243
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:223
Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:365
Revision\RevisionStore\loadRevisionFromConds
loadRevisionFromConds(IDatabase $db, array $conditions, int $flags=IDBAccessObject::READ_NORMAL, PageIdentity $page=null, array $options=[])
Given a set of conditions, fetch a revision from the given database connection.
Definition: RevisionStore.php:2433
Revision\MutableRevisionRecord\setSha1
setSha1( $sha1)
Set revision hash, for optimization.
Definition: MutableRevisionRecord.php:216
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:87
Revision\MutableRevisionRecord\setParentId
setParentId( $parentId)
Definition: MutableRevisionRecord.php:90
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:84
MediaWiki\Storage\SqlBlobStore
Service for storing and loading Content objects.
Definition: SqlBlobStore.php:51
Revision\RevisionStore\newRevisionFromRowAndSlots
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, PageIdentity $page=null, $fromCache=false)
Definition: RevisionStore.php:1531
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:76
Revision\RevisionStore\initializeMutableRevisionFromArray
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
Definition: RevisionStore.php:2183
Revision\RevisionRecord\getPage
getPage()
Returns the page this revision belongs to.
Definition: RevisionRecord.php:360
Revision\RevisionStoreCacheRecord
A cached RevisionStoreRecord.
Definition: RevisionStoreCacheRecord.php:37
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:439
Revision\RevisionStore\getSlotRowsForBatch
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
Definition: RevisionStore.php:1922
Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2666
Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:56
Revision\MutableRevisionRecord\setPageId
setPageId( $pageId)
Definition: MutableRevisionRecord.php:314
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:449
Revision\RevisionStore\getPreviousRevision
getPreviousRevision(RevisionRecord $rev, $flags=self::READ_NORMAL)
Get the revision before $rev in the page's history, if any.
Definition: RevisionStore.php:2810
Revision\RevisionStore\INCLUDE_NEW
const INCLUDE_NEW
Definition: RevisionStore.php:95
CommentStore
Handle database storage of comments such as edit summaries and log reasons.
Definition: CommentStore.php:42
Revision\RevisionStore\$cache
WANObjectCache $cache
Definition: RevisionStore.php:116
Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:2878
Revision\RevisionStore\getRcIdIfUnpatrolled
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
Definition: RevisionStore.php:973
Revision\RevisionFactory
Service for constructing revision objects.
Definition: RevisionFactory.php:38
Revision\MutableRevisionRecord\setId
setId( $id)
Set the revision ID.
Definition: MutableRevisionRecord.php:290
Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, UserIdentity $user, CommentStoreComment $comment, PageIdentity $page, $pageId, $parentId)
Definition: RevisionStore.php:498
Revision\RevisionStore\newRevisionSlots
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, PageIdentity $page)
Factory method for RevisionSlots based on a revision ID.
Definition: RevisionStore.php:1363
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
Revision\SlotRecord\getRevision
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:389
ActorMigration
This class handles the logic for the actor table migration and should always be used in lieu of direc...
Definition: ActorMigration.php:42
Revision\RevisionStore\assertRevisionParameter
assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev=null)
Asserts that if revision is provided, it's saved and belongs to the page with provided pageId.
Definition: RevisionStore.php:3125
Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2601
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:126
$revQuery
$revQuery
Definition: testCompression.php:56
Revision\RevisionRecord\getParentId
getParentId( $wikiId=self::LOCAL)
Get parent revision ID (the original previous page revision).
Definition: RevisionRecord.php:287
MediaWiki\DAO\WikiAwareEntity
Marker interface for entities aware of the wiki they belong to.
Definition: WikiAwareEntity.php:34
Revision\RevisionStore\getRevisionByTimestamp
getRevisionByTimestamp( $page, string $timestamp, int $flags=IDBAccessObject::READ_NORMAL)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:1209
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:34
Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Revision\RevisionStore\getDBConnectionRef
getDBConnectionRef( $mode, $groups=[])
Definition: RevisionStore.php:254
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Revision\RevisionStore\listRevisionSizes
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2732
Title\castFromPageIdentity
static castFromPageIdentity(?PageIdentity $pageIdentity)
Return a Title for a given PageIdentity.
Definition: Title.php:328
Revision\RevisionStore\userWasLastToEdit
userWasLastToEdit(IDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
Definition: RevisionStore.php:2960
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\Revision
Definition: ContributionsLookup.php:3
MediaWiki\Page\LegacyArticleIdAccess
trait LegacyArticleIdAccess
Convenience trait for conversion to PageIdentity.
Definition: LegacyArticleIdAccess.php:26
Revision\RevisionRecord\getUser
getUser( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Definition: RevisionRecord.php:379
Revision
Definition: Revision.php:40
Revision\RevisionStore\getNextRevision
getNextRevision(RevisionRecord $rev, $flags=self::READ_NORMAL)
Get the revision after $rev in the page's history, if any.
Definition: RevisionStore.php:2827
Revision\RevisionStore\getRevisionLimitConditions
getRevisionLimitConditions(IDatabase $dbr, RevisionRecord $old=null, RevisionRecord $new=null, $options=[])
Converts revision limits to query conditions.
Definition: RevisionStore.php:3152
Revision\RevisionStore\loadSlotContent
loadSlotContent(SlotRecord $slot, $blobData=null, $blobFlags=null, $blobFormat=null, $queryFlags=0)
Loads a Content object based on a slot row.
Definition: RevisionStore.php:1027
Revision\RevisionStore\insertIpChangesRow
insertIpChangesRow(IDatabase $dbw, UserIdentity $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
Definition: RevisionStore.php:612
Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, PageIdentity $page=null, $fromCache=false)
Definition: RevisionStore.php:1424
Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:398
Revision\RevisionStore\getQueryInfo
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
Definition: RevisionStore.php:2521
MWException
MediaWiki exception.
Definition: MWException.php:29
Revision\RevisionRecord\getSize
getSize()
Returns the nominal size of this revision, in bogo-bytes.
Wikimedia\Rdbms\Database\getCacheSetOptions
static getCacheSetOptions(?IDatabase ... $dbs)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4968
Revision\RevisionStore\ORDER_NEWEST_TO_OLDEST
const ORDER_NEWEST_TO_OLDEST
Definition: RevisionStore.php:91
MediaWiki\Storage\BlobStore\SHA1_HINT
const SHA1_HINT
Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the method.
Definition: BlobStore.php:72
Revision\MutableRevisionRecord\setUser
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
Definition: MutableRevisionRecord.php:304
Revision\RevisionRecord\getSha1
getSha1()
Returns the base36 sha1 of this revision.
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1034
Revision\RevisionStore\newRevisionFromArchiveRowAndSlots
newRevisionFromArchiveRowAndSlots( $row, $slots, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Definition: RevisionStore.php:1452
Revision\RevisionStore\INCLUDE_BOTH
const INCLUDE_BOTH
Definition: RevisionStore.php:96
Revision\RevisionStore\insertSlotOn
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, PageIdentity $page, array $blobHints=[])
Definition: RevisionStore.php:574
Revision\RevisionStore\insertRevisionOn
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
Definition: RevisionStore.php:386
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1103
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:558
Revision\RevisionStore\getRevisionByPageId
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
Definition: RevisionStore.php:1170
Revision\RevisionStore\loadRevisionFromPageId
loadRevisionFromPageId(IDatabase $db, $pageid, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
Definition: RevisionStore.php:2302
$blob
$blob
Definition: testCompression.php:70
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:482
Revision\RevisionStore\newRevisionFromArchiveRow
newRevisionFromArchiveRow( $row, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Make a fake revision object from an archive table row.
Definition: RevisionStore.php:1403
Revision\RevisionStore\constructSlotRecords
constructSlotRecords( $revId, $slotRows, $queryFlags, PageIdentity $page, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
Definition: RevisionStore.php:1285
Revision\MutableRevisionRecord\setVisibility
setVisibility( $visibility)
Definition: MutableRevisionRecord.php:246
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:462
MediaWiki\Storage\BlobStore\MODEL_HINT
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob.
Definition: BlobStore.php:78
Revision\RevisionStore\getPreviousRevisionId
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save.
Definition: RevisionStore.php:2842
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:416
Revision\RevisionRecord\isReadyForInsertion
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
Definition: RevisionRecord.php:558
Revision\RevisionStore\insertSlotRowOn
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
Definition: RevisionStore.php:818
Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2699
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:64
Revision\RevisionStore\newRevisionFromConds
newRevisionFromConds(array $conditions, int $flags=IDBAccessObject::READ_NORMAL, PageIdentity $page=null, array $options=[])
Given a set of conditions, fetch a revision.
Definition: RevisionStore.php:2393
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:559
$title
$title
Definition: testCompression.php:38
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:626
Revision\RevisionStore\countAuthorsBetween
countAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, Authority $performer=null, $max=null, $options=[])
Get the number of authors between the given revisions.
Definition: RevisionStore.php:3363
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:492
Revision\RevisionRecord\getSlotRoles
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
Definition: RevisionRecord.php:207
DB_MASTER
const DB_MASTER
Definition: defines.php:26
FallbackContent
Content object implementation representing unknown content.
Definition: FallbackContent.php:38
Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:101
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
Revision\RevisionStore\setLogger
setLogger(LoggerInterface $logger)
Definition: RevisionStore.php:211
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:531
Revision\RevisionStore\ROW_CACHE_KEY
const ROW_CACHE_KEY
Definition: RevisionStore.php:88
Revision\RevisionStore\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: RevisionStore.php:147
MediaWiki\Permissions\Authority
@unstable
Definition: Authority.php:30
Revision\RevisionArchiveRecord
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Definition: RevisionArchiveRecord.php:41
Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, PageIdentity $page)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2934
Revision\RevisionStore\ensureRevisionRowMatchesPage
ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context=[])
Check that the given row matches the given PageIdentity object.
Definition: RevisionStore.php:1667
$content
$content
Definition: router.php:76
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:515
Revision\RevisionStore\newMutableRevisionFromArray
newMutableRevisionFromArray(array $fields, $queryFlags=0, PageIdentity $page=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1....
Definition: RevisionStore.php:2087
Revision\RevisionRecord\DELETED_USER
const DELETED_USER
Definition: RevisionRecord.php:55
DBAccessObjectUtils\hasFlags
static hasFlags( $bitfield, $flags)
Definition: DBAccessObjectUtils.php:35
Revision\RevisionStore\getRevisionByTitle
getRevisionByTitle( $page, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: RevisionStore.php:1123
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:45
Revision\RevisionStore\getKnownCurrentRevision
getKnownCurrentRevision(PageIdentity $page, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: RevisionStore.php:3002
Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:269
Revision\RevisionStore\getRevisionIdsBetween
getRevisionIdsBetween(int $pageId, RevisionRecord $old=null, RevisionRecord $new=null, ?int $max=null, $options=[], ?string $order=null, int $flags=IDBAccessObject::READ_NORMAL)
Get IDs of revisions between the given revisions.
Definition: RevisionStore.php:3212
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:125
Revision\RevisionStore\$slotRoleStore
NameTableStore $slotRoleStore
Definition: RevisionStore.php:144
Revision\RevisionStore\getContentBlobsForBatch
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
Definition: RevisionStore.php:2038
Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, $parentId)
Definition: RevisionStore.php:638
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:36
Title\newFromIDs
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition: Title.php:532
Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:40
Revision\RevisionStore\getRelativeRevision
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
Definition: RevisionStore.php:2745
Revision\RevisionStore\$hookRunner
HookRunner $hookRunner
Definition: RevisionStore.php:156
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
MediaWiki\Storage\BlobStore
Service for loading and storing data blobs.
Definition: BlobStore.php:35
Content
Base interface for content objects.
Definition: Content.php:35
Wikimedia\Rdbms\DBConnRef
Helper class used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition: DBConnRef.php:29
Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:438
Revision\RevisionStore\fetchRevisionRowFromConds
fetchRevisionRowFromConds(IDatabase $db, array $conditions, int $flags=IDBAccessObject::READ_NORMAL, array $options=[])
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects.
Definition: RevisionStore.php:2478
Title
Represents a title within MediaWiki.
Definition: Title.php:46
Revision\RevisionStore\insertContentRowOn
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
Definition: RevisionStore.php:836
Revision\RevisionStore\countRevisionsByPageId
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2911
Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, PageIdentity $page, array $blobHints=[])
Definition: RevisionStore.php:784
Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:139
Revision\SlotRecord\newSaved
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
Definition: SlotRecord.php:166
Revision\RevisionStore\INCLUDE_OLD
const INCLUDE_OLD
Definition: RevisionStore.php:94
Revision\RevisionStore\getDBLoadBalancer
getDBLoadBalancer()
Definition: RevisionStore.php:225
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:85
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:53
Revision\RevisionSlots
Value object representing the set of slots belonging to a revision.
Definition: RevisionSlots.php:41
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:161
Revision\RevisionRecord\getComment
getComment( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision comment, if it's available to the specified audience.
Definition: RevisionRecord.php:403
MediaWiki\Storage\BlobStore\REVISION_HINT
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition: BlobStore.php:60
wfBacktrace
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
Definition: GlobalFunctions.php:1371
Revision\RevisionStore\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: RevisionStore.php:150
MediaWiki\Storage\BlobStore\PARENT_HINT
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition: BlobStore.php:66
Revision\RevisionStore\$wikiId
bool string $wikiId
Definition: RevisionStore.php:106
MediaWiki\Storage\BlobStore\ROLE_HINT
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with.
Definition: BlobStore.php:54
Revision\RevisionStore\checkContent
checkContent(Content $content, PageIdentity $page, string $role)
MCR migration note: this corresponds to Revision::checkContentModel.
Definition: RevisionStore.php:857
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
MWUnknownContentModelException
Exception thrown when an unregistered content model is requested.
Definition: MWUnknownContentModelException.php:11
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1081
Revision\MutableRevisionRecord\setTimestamp
setTimestamp( $timestamp)
Definition: MutableRevisionRecord.php:258
Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:348
Revision\MutableRevisionRecord\setSize
setSize( $size)
Set nominal revision size, for optimization.
Definition: MutableRevisionRecord.php:234
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:575
$t
$t
Definition: testCompression.php:74
Title\castFromLinkTarget
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:315
Revision\RevisionStore\getFirstRevision
getFirstRevision( $page, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
Definition: RevisionStore.php:3076
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
MediaWiki\$context
IContextSource $context
Definition: MediaWiki.php:39
Revision\RevisionStore\newRevisionsFromBatch
newRevisionsFromBatch( $rows, array $options=[], $queryFlags=0, PageIdentity $page=null)
Construct a RevisionRecord instance for each row in $rows, and return them as an associative array in...
Definition: RevisionStore.php:1711
Revision\RevisionStore\loadRevisionFromTimestamp
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:2364
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:506
Title\newFromID
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:507
Revision\RevisionRecord\FOR_THIS_USER
const FOR_THIS_USER
Definition: RevisionRecord.php:63
Revision\RevisionStore\getRevisionRowCacheKey
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) Caching rows w...
Definition: RevisionStore.php:3109
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
Revision\MutableRevisionRecord\setComment
setComment(CommentStoreComment $comment)
Definition: MutableRevisionRecord.php:200
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\User\ActorStore
Definition: ActorStore.php:43
Revision\RevisionStore\getBaseRevisionRow
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, $parentId)
Definition: RevisionStore.php:752
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Revision\RevisionStore\loadRevisionFromTitle
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
Definition: RevisionStore.php:2330
Revision\RevisionStore\countRevisionsBetween
countRevisionsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, $max=null, $options=[])
Get the number of revisions between the given revisions.
Definition: RevisionStore.php:3396
Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:180
Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:111
Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:218
MediaWiki\Storage\BlobStore\FORMAT_HINT
const FORMAT_HINT
Hint key for use with storeBlob, indicating the serialization format used to create the blob,...
Definition: BlobStore.php:84
Revision\RevisionStore\getTitle
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
Definition: RevisionStore.php:273