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;
33 use ContentHandler;
35 use FallbackContent;
36 use IDBAccessObject;
37 use InvalidArgumentException;
48 use Message;
49 use MWException;
50 use MWTimestamp;
52 use Psr\Log\LoggerAwareInterface;
53 use Psr\Log\LoggerInterface;
54 use Psr\Log\NullLogger;
55 use RecentChange;
56 use Revision;
57 use RuntimeException;
58 use StatusValue;
59 use Title;
60 use Traversable;
61 use User;
62 use WANObjectCache;
63 use Wikimedia\Assert\Assert;
64 use Wikimedia\IPUtils;
70 
81  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
82 
83  public const ROW_CACHE_KEY = 'revision-row-1.29';
84 
85  public const ORDER_OLDEST_TO_NEWEST = 'ASC';
86  public const ORDER_NEWEST_TO_OLDEST = 'DESC';
87 
91  private $blobStore;
92 
96  private $dbDomain;
97 
101  private $loadBalancer;
102 
106  private $cache;
107 
111  private $commentStore;
112 
117 
121  private $logger;
122 
127 
131  private $slotRoleStore;
132 
135 
138 
140  private $hookContainer;
141 
143  private $hookRunner;
144 
165  public function __construct(
166  ILoadBalancer $loadBalancer,
167  SqlBlobStore $blobStore,
170  NameTableStore $contentModelStore,
171  NameTableStore $slotRoleStore,
174  IContentHandlerFactory $contentHandlerFactory,
175  HookContainer $hookContainer,
176  $dbDomain = false
177  ) {
178  Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
179 
180  $this->loadBalancer = $loadBalancer;
181  $this->blobStore = $blobStore;
182  $this->cache = $cache;
183  $this->commentStore = $commentStore;
184  $this->contentModelStore = $contentModelStore;
185  $this->slotRoleStore = $slotRoleStore;
186  $this->slotRoleRegistry = $slotRoleRegistry;
187  $this->actorMigration = $actorMigration;
188  $this->dbDomain = $dbDomain;
189  $this->logger = new NullLogger();
190  $this->contentHandlerFactory = $contentHandlerFactory;
191  $this->hookContainer = $hookContainer;
192  $this->hookRunner = new HookRunner( $hookContainer );
193  }
194 
195  public function setLogger( LoggerInterface $logger ) {
196  $this->logger = $logger;
197  }
198 
202  public function isReadOnly() {
203  return $this->blobStore->isReadOnly();
204  }
205 
209  private function getDBLoadBalancer() {
210  return $this->loadBalancer;
211  }
212 
218  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
219  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
220  return $this->getDBConnectionRef( $mode );
221  }
222 
229  private function getDBConnectionRef( $mode, $groups = [] ) {
230  $lb = $this->getDBLoadBalancer();
231  return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
232  }
233 
248  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
249  if ( !$pageId && !$revId ) {
250  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
251  }
252 
253  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
254  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
255  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
256  $queryFlags = self::READ_NORMAL;
257  }
258 
259  $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
260  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
261 
262  // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
263  if ( $canUseTitleNewFromId ) {
264  $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
265  // TODO: better foreign title handling (introduce TitleFactory)
266  $title = Title::newFromID( $pageId, $titleFlags );
267  if ( $title ) {
268  return $title;
269  }
270  }
271 
272  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
273  $canUseRevId = ( $revId !== null && $revId > 0 );
274 
275  if ( $canUseRevId ) {
276  $dbr = $this->getDBConnectionRef( $dbMode );
277  // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
278  $row = $dbr->selectRow(
279  [ 'revision', 'page' ],
280  [
281  'page_namespace',
282  'page_title',
283  'page_id',
284  'page_latest',
285  'page_is_redirect',
286  'page_len',
287  ],
288  [ 'rev_id' => $revId ],
289  __METHOD__,
290  $dbOptions,
291  [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
292  );
293  if ( $row ) {
294  // TODO: better foreign title handling (introduce TitleFactory)
295  return Title::newFromRow( $row );
296  }
297  }
298 
299  // If we still don't have a title, fallback to master if that wasn't already happening.
300  if ( $dbMode !== DB_MASTER ) {
301  $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
302  if ( $title ) {
303  $this->logger->info(
304  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
305  [ 'trace' => wfBacktrace() ]
306  );
307  return $title;
308  }
309  }
310 
311  throw new RevisionAccessException(
312  "Could not determine title for page ID $pageId and revision ID $revId"
313  );
314  }
315 
323  private function failOnNull( $value, $name ) {
324  if ( $value === null ) {
325  throw new IncompleteRevisionException(
326  "$name must not be " . var_export( $value, true ) . "!"
327  );
328  }
329 
330  return $value;
331  }
332 
340  private function failOnEmpty( $value, $name ) {
341  if ( $value === null || $value === 0 || $value === '' ) {
342  throw new IncompleteRevisionException(
343  "$name must not be " . var_export( $value, true ) . "!"
344  );
345  }
346 
347  return $value;
348  }
349 
361  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
362  // TODO: pass in a DBTransactionContext instead of a database connection.
363  $this->checkDatabaseDomain( $dbw );
364 
365  $slotRoles = $rev->getSlotRoles();
366 
367  // Make sure the main slot is always provided throughout migration
368  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
369  throw new IncompleteRevisionException(
370  'main slot must be provided'
371  );
372  }
373 
374  // Checks
375  $this->failOnNull( $rev->getSize(), 'size field' );
376  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
377  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
378  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
379  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
380  $this->failOnNull( $user->getId(), 'user field' );
381  $this->failOnEmpty( $user->getName(), 'user_text field' );
382 
383  if ( !$rev->isReadyForInsertion() ) {
384  // This is here for future-proofing. At the time this check being added, it
385  // was redundant to the individual checks above.
386  throw new IncompleteRevisionException( 'Revision is incomplete' );
387  }
388 
389  if ( $slotRoles == [ SlotRecord::MAIN ] ) {
390  // T239717: If the main slot is the only slot, make sure the revision's nominal size
391  // and hash match the main slot's nominal size and hash.
392  $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
393  Assert::precondition(
394  $mainSlot->getSize() === $rev->getSize(),
395  'The revisions\'s size must match the main slot\'s size (see T239717)'
396  );
397  Assert::precondition(
398  $mainSlot->getSha1() === $rev->getSha1(),
399  'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
400  );
401  }
402 
403  // TODO: we shouldn't need an actual Title here.
405  $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
406 
407  $parentId = $rev->getParentId() === null
408  ? $this->getPreviousRevisionId( $dbw, $rev )
409  : $rev->getParentId();
410 
412  $rev = $dbw->doAtomicSection(
413  __METHOD__,
414  function ( IDatabase $dbw, $fname ) use (
415  $rev,
416  $user,
417  $comment,
418  $title,
419  $pageId,
420  $parentId
421  ) {
422  return $this->insertRevisionInternal(
423  $rev,
424  $dbw,
425  $user,
426  $comment,
427  $title,
428  $pageId,
429  $parentId
430  );
431  }
432  );
433 
434  // sanity checks
435  Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
436  Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
437  Assert::postcondition(
438  $rev->getComment( RevisionRecord::RAW ) !== null,
439  'revision must have a comment'
440  );
441  Assert::postcondition(
442  $rev->getUser( RevisionRecord::RAW ) !== null,
443  'revision must have a user'
444  );
445 
446  // Trigger exception if the main slot is missing.
447  // Technically, this could go away after MCR migration: while
448  // calling code may require a main slot to exist, RevisionStore
449  // really should not know or care about that requirement.
451 
452  foreach ( $slotRoles as $role ) {
453  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
454  Assert::postcondition(
455  $slot->getContent() !== null,
456  $role . ' slot must have content'
457  );
458  Assert::postcondition(
459  $slot->hasRevision(),
460  $role . ' slot must have a revision associated'
461  );
462  }
463 
464  $this->hookRunner->onRevisionRecordInserted( $rev );
465 
466  // Soft deprecated in 1.31, hard deprecated in 1.35
467  if ( $this->hookContainer->isRegistered( 'RevisionInsertComplete' ) ) {
468  // Only create the Revision object if its needed
469  $legacyRevision = new Revision( $rev );
470  $this->hookRunner->onRevisionInsertComplete( $legacyRevision, null, null );
471  }
472 
473  return $rev;
474  }
475 
476  private function insertRevisionInternal(
477  RevisionRecord $rev,
478  IDatabase $dbw,
479  User $user,
480  CommentStoreComment $comment,
481  Title $title,
482  $pageId,
483  $parentId
484  ) {
485  $slotRoles = $rev->getSlotRoles();
486 
487  $revisionRow = $this->insertRevisionRowOn(
488  $dbw,
489  $rev,
490  $title,
491  $parentId
492  );
493 
494  $revisionId = $revisionRow['rev_id'];
495 
496  $blobHints = [
497  BlobStore::PAGE_HINT => $pageId,
498  BlobStore::REVISION_HINT => $revisionId,
499  BlobStore::PARENT_HINT => $parentId,
500  ];
501 
502  $newSlots = [];
503  foreach ( $slotRoles as $role ) {
504  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
505 
506  // If the SlotRecord already has a revision ID set, this means it already exists
507  // in the database, and should already belong to the current revision.
508  // However, a slot may already have a revision, but no content ID, if the slot
509  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
510  // mode, and the respective archive row was not yet migrated to the new schema.
511  // In that case, a new slot row (and content row) must be inserted even during
512  // undeletion.
513  if ( $slot->hasRevision() && $slot->hasContentId() ) {
514  // TODO: properly abort transaction if the assertion fails!
515  Assert::parameter(
516  $slot->getRevision() === $revisionId,
517  'slot role ' . $slot->getRole(),
518  'Existing slot should belong to revision '
519  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
520  );
521 
522  // Slot exists, nothing to do, move along.
523  // This happens when restoring archived revisions.
524 
525  $newSlots[$role] = $slot;
526  } else {
527  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
528  }
529  }
530 
531  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
532 
533  $rev = new RevisionStoreRecord(
534  $title,
535  $user,
536  $comment,
537  (object)$revisionRow,
538  new RevisionSlots( $newSlots ),
539  $this->dbDomain
540  );
541 
542  return $rev;
543  }
544 
553  private function insertSlotOn(
554  IDatabase $dbw,
555  $revisionId,
556  SlotRecord $protoSlot,
557  Title $title,
558  array $blobHints = []
559  ) {
560  if ( $protoSlot->hasAddress() ) {
561  $blobAddress = $protoSlot->getAddress();
562  } else {
563  $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
564  }
565 
566  $contentId = null;
567 
568  if ( $protoSlot->hasContentId() ) {
569  $contentId = $protoSlot->getContentId();
570  } else {
571  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
572  }
573 
574  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
575 
576  $savedSlot = SlotRecord::newSaved(
577  $revisionId,
578  $contentId,
579  $blobAddress,
580  $protoSlot
581  );
582 
583  return $savedSlot;
584  }
585 
593  private function insertIpChangesRow(
594  IDatabase $dbw,
595  User $user,
596  RevisionRecord $rev,
597  $revisionId
598  ) {
599  if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
600  $ipcRow = [
601  'ipc_rev_id' => $revisionId,
602  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
603  'ipc_hex' => IPUtils::toHex( $user->getName() ),
604  ];
605  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
606  }
607  }
608 
620  private function insertRevisionRowOn(
621  IDatabase $dbw,
622  RevisionRecord $rev,
623  Title $title,
624  $parentId
625  ) {
626  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
627 
628  list( $commentFields, $commentCallback ) =
629  $this->commentStore->insertWithTempTable(
630  $dbw,
631  'rev_comment',
633  );
634  $revisionRow += $commentFields;
635 
636  list( $actorFields, $actorCallback ) =
637  $this->actorMigration->getInsertValuesWithTempTable(
638  $dbw,
639  'rev_user',
641  );
642  $revisionRow += $actorFields;
643 
644  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
645 
646  if ( !isset( $revisionRow['rev_id'] ) ) {
647  // only if auto-increment was used
648  $revisionRow['rev_id'] = intval( $dbw->insertId() );
649 
650  if ( $dbw->getType() === 'mysql' ) {
651  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
652  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
653  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
654 
655  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
656  $table = 'archive';
657  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
658  if ( $maxRevId2 >= $maxRevId ) {
659  $maxRevId = $maxRevId2;
660  $table = 'slots';
661  }
662 
663  if ( $maxRevId >= $revisionRow['rev_id'] ) {
664  $this->logger->debug(
665  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
666  . ' Trying to fix it.',
667  [
668  'revid' => $revisionRow['rev_id'],
669  'table' => $table,
670  'maxrevid' => $maxRevId,
671  ]
672  );
673 
674  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
675  throw new MWException( 'Failed to get database lock for T202032' );
676  }
677  $fname = __METHOD__;
678  $dbw->onTransactionResolution(
679  function ( $trigger, IDatabase $dbw ) use ( $fname ) {
680  $dbw->unlock( 'fix-for-T202032', $fname );
681  },
682  __METHOD__
683  );
684 
685  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
686 
687  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
688  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
689  // inserts too, though, at least on MariaDB 10.1.29.
690  //
691  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
692  // transactions in this code path thanks to the row lock from the original ->insert() above.
693  //
694  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
695  // that's for non-MySQL DBs.
696  $row1 = $dbw->query(
697  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
698  __METHOD__
699  )->fetchObject();
700 
701  $row2 = $dbw->query(
702  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
703  . ' FOR UPDATE',
704  __METHOD__
705  )->fetchObject();
706 
707  $maxRevId = max(
708  $maxRevId,
709  $row1 ? intval( $row1->v ) : 0,
710  $row2 ? intval( $row2->v ) : 0
711  );
712 
713  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
714  // transactions will throw a duplicate key error here. It doesn't seem worth trying
715  // to avoid that.
716  $revisionRow['rev_id'] = $maxRevId + 1;
717  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
718  }
719  }
720  }
721 
722  $commentCallback( $revisionRow['rev_id'] );
723  $actorCallback( $revisionRow['rev_id'], $revisionRow );
724 
725  return $revisionRow;
726  }
727 
738  private function getBaseRevisionRow(
739  IDatabase $dbw,
740  RevisionRecord $rev,
741  Title $title,
742  $parentId
743  ) {
744  // Record the edit in revisions
745  $revisionRow = [
746  'rev_page' => $rev->getPageId(),
747  'rev_parent_id' => $parentId,
748  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
749  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
750  'rev_deleted' => $rev->getVisibility(),
751  'rev_len' => $rev->getSize(),
752  'rev_sha1' => $rev->getSha1(),
753  ];
754 
755  if ( $rev->getId() !== null ) {
756  // Needed to restore revisions with their original ID
757  $revisionRow['rev_id'] = $rev->getId();
758  }
759 
760  return $revisionRow;
761  }
762 
771  private function storeContentBlob(
772  SlotRecord $slot,
773  Title $title,
774  array $blobHints = []
775  ) {
776  $content = $slot->getContent();
777  $format = $content->getDefaultFormat();
778  $model = $content->getModel();
779 
780  $this->checkContent( $content, $title, $slot->getRole() );
781 
782  return $this->blobStore->storeBlob(
783  $content->serialize( $format ),
784  // These hints "leak" some information from the higher abstraction layer to
785  // low level storage to allow for optimization.
786  array_merge(
787  $blobHints,
788  [
789  BlobStore::DESIGNATION_HINT => 'page-content',
790  BlobStore::ROLE_HINT => $slot->getRole(),
791  BlobStore::SHA1_HINT => $slot->getSha1(),
792  BlobStore::MODEL_HINT => $model,
793  BlobStore::FORMAT_HINT => $format,
794  ]
795  )
796  );
797  }
798 
805  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
806  $slotRow = [
807  'slot_revision_id' => $revisionId,
808  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
809  'slot_content_id' => $contentId,
810  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
811  // that we just inserted.
812  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
813  ];
814  $dbw->insert( 'slots', $slotRow, __METHOD__ );
815  }
816 
823  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
824  $contentRow = [
825  'content_size' => $slot->getSize(),
826  'content_sha1' => $slot->getSha1(),
827  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
828  'content_address' => $blobAddress,
829  ];
830  $dbw->insert( 'content', $contentRow, __METHOD__ );
831  return intval( $dbw->insertId() );
832  }
833 
844  private function checkContent( Content $content, Title $title, $role ) {
845  // Note: may return null for revisions that have not yet been inserted
846 
847  $model = $content->getModel();
848  $format = $content->getDefaultFormat();
849  $handler = $content->getContentHandler();
850 
851  $name = "$title";
852 
853  if ( !$handler->isSupportedFormat( $format ) ) {
854  throw new MWException( "Can't use format $format with content model $model on $name" );
855  }
856 
857  if ( !$content->isValid() ) {
858  throw new MWException(
859  "New content for $name is not valid! Content model is $model"
860  );
861  }
862  }
863 
889  public function newNullRevision(
890  IDatabase $dbw,
891  Title $title,
892  CommentStoreComment $comment,
893  $minor,
894  User $user
895  ) {
896  $this->checkDatabaseDomain( $dbw );
897 
898  $pageId = $title->getArticleID();
899 
900  // T51581: Lock the page table row to ensure no other process
901  // is adding a revision to the page at the same time.
902  // Avoid locking extra tables, compare T191892.
903  $pageLatest = $dbw->selectField(
904  'page',
905  'page_latest',
906  [ 'page_id' => $pageId ],
907  __METHOD__,
908  [ 'FOR UPDATE' ]
909  );
910 
911  if ( !$pageLatest ) {
912  return null;
913  }
914 
915  // Fetch the actual revision row from master, without locking all extra tables.
916  $oldRevision = $this->loadRevisionFromConds(
917  $dbw,
918  [ 'rev_id' => intval( $pageLatest ) ],
919  self::READ_LATEST,
920  $title
921  );
922 
923  if ( !$oldRevision ) {
924  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
925  $this->logger->error(
926  $msg,
927  [ 'exception' => new RuntimeException( $msg ) ]
928  );
929  return null;
930  }
931 
932  // Construct the new revision
933  $timestamp = MWTimestamp::now( TS_MW );
934  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
935 
936  $newRevision->setComment( $comment );
937  $newRevision->setUser( $user );
938  $newRevision->setTimestamp( $timestamp );
939  $newRevision->setMinorEdit( $minor );
940 
941  return $newRevision;
942  }
943 
953  public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
954  $rc = $this->getRecentChange( $rev );
955  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
956  return $rc->getAttribute( 'rc_id' );
957  } else {
958  return 0;
959  }
960  }
961 
975  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
976  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
977 
979  [ 'rc_this_oldid' => $rev->getId() ],
980  __METHOD__,
981  $dbType
982  );
983 
984  // XXX: cache this locally? Glue it to the RevisionRecord?
985  return $rc;
986  }
987 
1007  private function loadSlotContent(
1008  SlotRecord $slot,
1009  $blobData = null,
1010  $blobFlags = null,
1011  $blobFormat = null,
1012  $queryFlags = 0
1013  ) {
1014  if ( $blobData !== null ) {
1015  Assert::parameterType( 'string', $blobData, '$blobData' );
1016  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1017 
1018  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1019 
1020  if ( $blobFlags === null ) {
1021  // No blob flags, so use the blob verbatim.
1022  $data = $blobData;
1023  } else {
1024  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1025  if ( $data === false ) {
1026  throw new RevisionAccessException(
1027  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1028  );
1029  }
1030  }
1031 
1032  } else {
1033  $address = $slot->getAddress();
1034  try {
1035  $data = $this->blobStore->getBlob( $address, $queryFlags );
1036  } catch ( BlobAccessException $e ) {
1037  throw new RevisionAccessException(
1038  "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1039  );
1040  }
1041  }
1042 
1043  $model = $slot->getModel();
1044 
1045  // If the content model is not known, don't fail here (T220594, T220793, T228921)
1046  if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1047  $this->logger->warning(
1048  "Undefined content model '$model', falling back to UnknownContent",
1049  [
1050  'content_address' => $slot->getAddress(),
1051  'rev_id' => $slot->getRevision(),
1052  'role_name' => $slot->getRole(),
1053  'model_name' => $model,
1054  'trace' => wfBacktrace()
1055  ]
1056  );
1057 
1058  return new FallbackContent( $data, $model );
1059  }
1060 
1061  return $this->contentHandlerFactory
1062  ->getContentHandler( $model )
1063  ->unserializeContent( $data, $blobFormat );
1064  }
1065 
1080  public function getRevisionById( $id, $flags = 0 ) {
1081  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1082  }
1083 
1100  public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1101  $conds = [
1102  'page_namespace' => $linkTarget->getNamespace(),
1103  'page_title' => $linkTarget->getDBkey()
1104  ];
1105 
1106  // Only resolve to a Title when operating in the context of the local wiki (T248756)
1107  // TODO should not require Title in future (T206498)
1108  $title = $this->dbDomain === false ? Title::newFromLinkTarget( $linkTarget ) : null;
1109 
1110  if ( $revId ) {
1111  // Use the specified revision ID.
1112  // Note that we use newRevisionFromConds here because we want to retry
1113  // and fall back to master if the page is not found on a replica.
1114  // Since the caller supplied a revision ID, we are pretty sure the revision is
1115  // supposed to exist, so we should try hard to find it.
1116  $conds['rev_id'] = $revId;
1117  return $this->newRevisionFromConds( $conds, $flags, $title );
1118  } else {
1119  // Use a join to get the latest revision.
1120  // Note that we don't use newRevisionFromConds here because we don't want to retry
1121  // and fall back to master. The assumption is that we only want to force the fallback
1122  // if we are quite sure the revision exists because the caller supplied a revision ID.
1123  // If the page isn't found at all on a replica, it probably simply does not exist.
1124  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1125 
1126  $conds[] = 'rev_id=page_latest';
1127  $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1128 
1129  return $rev;
1130  }
1131  }
1132 
1149  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1150  $conds = [ 'page_id' => $pageId ];
1151  if ( $revId ) {
1152  // Use the specified revision ID.
1153  // Note that we use newRevisionFromConds here because we want to retry
1154  // and fall back to master if the page is not found on a replica.
1155  // Since the caller supplied a revision ID, we are pretty sure the revision is
1156  // supposed to exist, so we should try hard to find it.
1157  $conds['rev_id'] = $revId;
1158  return $this->newRevisionFromConds( $conds, $flags );
1159  } else {
1160  // Use a join to get the latest revision.
1161  // Note that we don't use newRevisionFromConds here because we don't want to retry
1162  // and fall back to master. The assumption is that we only want to force the fallback
1163  // if we are quite sure the revision exists because the caller supplied a revision ID.
1164  // If the page isn't found at all on a replica, it probably simply does not exist.
1165  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1166 
1167  $conds[] = 'rev_id=page_latest';
1168  $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1169 
1170  return $rev;
1171  }
1172  }
1173 
1189  public function getRevisionByTimestamp(
1190  LinkTarget $title,
1191  string $timestamp,
1192  int $flags = IDBAccessObject::READ_NORMAL
1193  ): ?RevisionRecord {
1194  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1195  return $this->newRevisionFromConds(
1196  [
1197  'rev_timestamp' => $db->timestamp( $timestamp ),
1198  'page_namespace' => $title->getNamespace(),
1199  'page_title' => $title->getDBkey()
1200  ],
1201  $flags,
1203  );
1204  }
1205 
1213  private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
1214  $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1215 
1216  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1217  $db = $this->getDBConnectionRef( $dbMode );
1218 
1219  $res = $db->select(
1220  $revQuery['tables'],
1221  $revQuery['fields'],
1222  [
1223  'slot_revision_id' => $revId,
1224  ],
1225  __METHOD__,
1226  $dbOptions,
1227  $revQuery['joins']
1228  );
1229 
1230  if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1231  // If we found no slots, try looking on the master database (T212428, T252156)
1232  $this->logger->info(
1233  __METHOD__ . ' falling back to READ_LATEST.',
1234  [
1235  'revid' => $revId,
1236  'trace' => wfBacktrace( true )
1237  ]
1238  );
1239  return $this->loadSlotRecords(
1240  $revId,
1241  $queryFlags | self::READ_LATEST,
1242  $title
1243  );
1244  }
1245 
1246  $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
1247 
1248  return $slots;
1249  }
1250 
1263  private function constructSlotRecords(
1264  $revId,
1265  $slotRows,
1266  $queryFlags,
1267  Title $title,
1268  $slotContents = null
1269  ) {
1270  $slots = [];
1271 
1272  foreach ( $slotRows as $row ) {
1273  // Resolve role names and model names from in-memory cache, if they were not joined in.
1274  if ( !isset( $row->role_name ) ) {
1275  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1276  }
1277 
1278  if ( !isset( $row->model_name ) ) {
1279  if ( isset( $row->content_model ) ) {
1280  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1281  } else {
1282  // We may get here if $row->model_name is set but null, perhaps because it
1283  // came from rev_content_model, which is NULL for the default model.
1284  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1285  $row->model_name = $slotRoleHandler->getDefaultModel( $title );
1286  }
1287  }
1288 
1289  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1290  if ( isset( $row->blob_data ) ) {
1291  $slotContents[$row->content_address] = $row->blob_data;
1292  }
1293 
1294  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1295  $blob = null;
1296  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1297  $blob = $slotContents[$slot->getAddress()];
1298  if ( $blob instanceof Content ) {
1299  return $blob;
1300  }
1301  }
1302  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1303  };
1304 
1305  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1306  }
1307 
1308  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1309  $this->logger->error(
1310  __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1311  [
1312  'revid' => $revId,
1313  'queryFlags' => $queryFlags,
1314  'trace' => wfBacktrace( true )
1315  ]
1316  );
1317 
1318  throw new RevisionAccessException(
1319  'Main slot of revision not found in database. See T212428.'
1320  );
1321  }
1322 
1323  return $slots;
1324  }
1325 
1341  private function newRevisionSlots(
1342  $revId,
1343  $revisionRow,
1344  $slotRows,
1345  $queryFlags,
1346  Title $title
1347  ) {
1348  if ( $slotRows ) {
1349  $slots = new RevisionSlots(
1350  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
1351  );
1352  } else {
1353  // XXX: do we need the same kind of caching here
1354  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1355 
1356  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
1357  return $this->loadSlotRecords( $revId, $queryFlags, $title );
1358  } );
1359  }
1360 
1361  return $slots;
1362  }
1363 
1381  public function newRevisionFromArchiveRow(
1382  $row,
1383  $queryFlags = 0,
1384  Title $title = null,
1385  array $overrides = []
1386  ) {
1387  return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $title, $overrides );
1388  }
1389 
1403  public function newRevisionFromRow(
1404  $row,
1405  $queryFlags = 0,
1406  Title $title = null,
1407  $fromCache = false
1408  ) {
1409  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
1410  }
1411 
1432  $row,
1433  $slots,
1434  $queryFlags = 0,
1435  Title $title = null,
1436  array $overrides = []
1437  ) {
1438  Assert::parameterType( 'object', $row, '$row' );
1439 
1440  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1441  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1442 
1443  if ( !$title && isset( $overrides['title'] ) ) {
1444  if ( !( $overrides['title'] instanceof Title ) ) {
1445  throw new MWException( 'title field override must contain a Title object.' );
1446  }
1447 
1448  $title = $overrides['title'];
1449  }
1450 
1451  if ( !isset( $title ) ) {
1452  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1453  $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1454  } else {
1455  throw new InvalidArgumentException(
1456  'A Title or ar_namespace and ar_title must be given'
1457  );
1458  }
1459  }
1460 
1461  foreach ( $overrides as $key => $value ) {
1462  $field = "ar_$key";
1463  $row->$field = $value;
1464  }
1465 
1466  try {
1467  $user = User::newFromAnyId(
1468  $row->ar_user ?? null,
1469  $row->ar_user_text ?? null,
1470  $row->ar_actor ?? null,
1471  $this->dbDomain
1472  );
1473  } catch ( InvalidArgumentException $ex ) {
1474  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1475  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1476  }
1477 
1478  if ( $user->getName() === '' ) {
1479  // T236624: If the user name is empty, force 'Unknown user',
1480  // even if the actor table has an entry for the empty user name.
1481  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1482  }
1483 
1484  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1485  // Legacy because $row may have come from self::selectFields()
1486  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1487 
1488  if ( !( $slots instanceof RevisionSlots ) ) {
1489  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $title );
1490  }
1491 
1492  return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
1493  }
1494 
1513  $row,
1514  $slots,
1515  $queryFlags = 0,
1516  Title $title = null,
1517  $fromCache = false
1518  ) {
1519  Assert::parameterType( 'object', $row, '$row' );
1520 
1521  if ( !$title ) {
1522  $pageId = (int)( $row->rev_page ?? 0 ); // XXX: fall back to page_id?
1523  $revId = (int)( $row->rev_id ?? 0 );
1524 
1525  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1526  } else {
1527  $this->ensureRevisionRowMatchesTitle( $row, $title );
1528  }
1529 
1530  if ( !isset( $row->page_latest ) ) {
1531  $row->page_latest = $title->getLatestRevID();
1532  if ( $row->page_latest === 0 && $title->exists() ) {
1533  wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1534  }
1535  }
1536 
1537  try {
1538  $user = User::newFromAnyId(
1539  $row->rev_user ?? null,
1540  $row->rev_user_text ?? null,
1541  $row->rev_actor ?? null,
1542  $this->dbDomain
1543  );
1544  } catch ( InvalidArgumentException $ex ) {
1545  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1546  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1547  }
1548 
1549  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1550  // Legacy because $row may have come from self::selectFields()
1551  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1552 
1553  if ( !( $slots instanceof RevisionSlots ) ) {
1554  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
1555  }
1556 
1557  // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1558  if ( $fromCache ) {
1559  $rev = new RevisionStoreCacheRecord(
1560  function ( $revId ) use ( $queryFlags ) {
1561  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1562  return $this->fetchRevisionRowFromConds(
1563  $db,
1564  [ 'rev_id' => intval( $revId ) ]
1565  );
1566  },
1567  $title, $user, $comment, $row, $slots, $this->dbDomain
1568  );
1569  } else {
1570  $rev = new RevisionStoreRecord(
1571  $title, $user, $comment, $row, $slots, $this->dbDomain );
1572  }
1573  return $rev;
1574  }
1575 
1585  private function ensureRevisionRowMatchesTitle( $row, Title $title, $context = [] ) {
1586  $revId = (int)( $row->rev_id ?? 0 );
1587  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1588  $titlePageId = $title->getArticleID();
1589 
1590  // Avoid fatal error when the Title's ID changed, T246720
1591  if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1592  $masterPageId = $title->getArticleID( Title::READ_LATEST );
1593  $masterLatest = $title->getLatestRevID( Title::READ_LATEST );
1594 
1595  if ( $revPageId === $masterPageId ) {
1596  $this->logger->warning(
1597  "Encountered stale Title object",
1598  [
1599  'page_id_stale' => $titlePageId,
1600  'page_id_reloaded' => $masterPageId,
1601  'page_latest' => $masterLatest,
1602  'rev_id' => $revId,
1603  'trace' => wfBacktrace()
1604  ] + $context
1605  );
1606  } else {
1607  throw new InvalidArgumentException(
1608  "Revision $revId belongs to page ID $revPageId, "
1609  . "the provided Title object belongs to page ID $masterPageId"
1610  );
1611  }
1612  }
1613  }
1614 
1640  public function newRevisionsFromBatch(
1641  $rows,
1642  array $options = [],
1643  $queryFlags = 0,
1644  Title $title = null
1645  ) {
1646  $result = new StatusValue();
1647  $archiveMode = $options['archive'] ?? false;
1648 
1649  if ( $archiveMode ) {
1650  $revIdField = 'ar_rev_id';
1651  } else {
1652  $revIdField = 'rev_id';
1653  }
1654 
1655  $rowsByRevId = [];
1656  $pageIdsToFetchTitles = [];
1657  $titlesByPageKey = [];
1658  foreach ( $rows as $row ) {
1659  if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1660  $result->warning(
1661  'internalerror_info',
1662  "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1663  );
1664  }
1665 
1666  // Attach a page key to the row, so we can find and reuse Title objects easily.
1667  $row->_page_key =
1668  $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1669 
1670  if ( $title ) {
1671  if ( !$archiveMode && $row->rev_page != $title->getArticleID() ) {
1672  throw new InvalidArgumentException(
1673  "Revision {$row->$revIdField} doesn't belong to page "
1674  . $title->getArticleID()
1675  );
1676  }
1677 
1678  if ( $archiveMode
1679  && ( $row->ar_namespace != $title->getNamespace()
1680  || $row->ar_title !== $title->getDBkey() )
1681  ) {
1682  throw new InvalidArgumentException(
1683  "Revision {$row->$revIdField} doesn't belong to page "
1684  . $title->getPrefixedDBkey()
1685  );
1686  }
1687  } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1688  if ( isset( $row->page_namespace ) && isset( $row->page_title )
1689  // This should always be true, but just in case we don't have a page_id
1690  // set or it doesn't match rev_page, let's fetch the title again.
1691  && isset( $row->page_id ) && isset( $row->rev_page )
1692  && $row->rev_page === $row->page_id
1693  ) {
1694  $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1695  } elseif ( $archiveMode ) {
1696  // Can't look up deleted pages by ID, but we have namespace and title
1697  $titlesByPageKey[ $row->_page_key ] =
1698  Title::makeTitle( $row->ar_namespace, $row->ar_title );
1699  } else {
1700  $pageIdsToFetchTitles[] = $row->rev_page;
1701  }
1702  }
1703  $rowsByRevId[$row->$revIdField] = $row;
1704  }
1705 
1706  if ( empty( $rowsByRevId ) ) {
1707  $result->setResult( true, [] );
1708  return $result;
1709  }
1710 
1711  // If the title is not supplied, batch-fetch Title objects.
1712  if ( $title ) {
1713  // same logic as for $row->_page_key above
1714  $pageKey = $archiveMode
1715  ? $title->getNamespace() . ':' . $title->getDBkey()
1716  : $title->getArticleID();
1717 
1718  $titlesByPageKey[$pageKey] = $title;
1719  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1720  // Note: when we fetch titles by ID, the page key is also the ID.
1721  // We should never get here if $archiveMode is true.
1722  Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1723 
1724  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1725  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1726  $titlesByPageKey[$t->getArticleID()] = $t;
1727  }
1728  }
1729 
1730  // which method to use for creating RevisionRecords
1731  $newRevisionRecord = [
1732  $this,
1733  $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1734  ];
1735 
1736  if ( !isset( $options['slots'] ) ) {
1737  $result->setResult(
1738  true,
1739  array_map(
1740  function ( $row )
1741  use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1742  try {
1743  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1744  $result->warning(
1745  'internalerror_info',
1746  "Couldn't find title for rev {$row->$revIdField} "
1747  . "(page key {$row->_page_key})"
1748  );
1749  return null;
1750  }
1751  return $newRevisionRecord( $row, null, $queryFlags,
1752  $titlesByPageKey[ $row->_page_key ] );
1753  } catch ( MWException $e ) {
1754  $result->warning( 'internalerror_info', $e->getMessage() );
1755  return null;
1756  }
1757  },
1758  $rowsByRevId
1759  )
1760  );
1761  return $result;
1762  }
1763 
1764  $slotRowOptions = [
1765  'slots' => $options['slots'] ?? true,
1766  'blobs' => $options['content'] ?? false,
1767  ];
1768 
1769  if ( is_array( $slotRowOptions['slots'] )
1770  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
1771  ) {
1772  // Make sure the main slot is always loaded, RevisionRecord requires this.
1773  $slotRowOptions['slots'][] = SlotRecord::MAIN;
1774  }
1775 
1776  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
1777 
1778  $result->merge( $slotRowsStatus );
1779  $slotRowsByRevId = $slotRowsStatus->getValue();
1780 
1781  $result->setResult(
1782  true,
1783  array_map(
1784  function ( $row )
1785  use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
1786  $revIdField, $newRevisionRecord
1787  ) {
1788  if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
1789  $result->warning(
1790  'internalerror_info',
1791  "Couldn't find slots for rev {$row->$revIdField}"
1792  );
1793  return null;
1794  }
1795  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1796  $result->warning(
1797  'internalerror_info',
1798  "Couldn't find title for rev {$row->$revIdField} "
1799  . "(page key {$row->_page_key})"
1800  );
1801  return null;
1802  }
1803  try {
1804  return $newRevisionRecord(
1805  $row,
1806  new RevisionSlots(
1807  $this->constructSlotRecords(
1808  $row->$revIdField,
1809  $slotRowsByRevId[$row->$revIdField],
1810  $queryFlags,
1811  $titlesByPageKey[$row->_page_key]
1812  )
1813  ),
1814  $queryFlags,
1815  $titlesByPageKey[$row->_page_key]
1816  );
1817  } catch ( MWException $e ) {
1818  $result->warning( 'internalerror_info', $e->getMessage() );
1819  return null;
1820  }
1821  },
1822  $rowsByRevId
1823  )
1824  );
1825  return $result;
1826  }
1827 
1851  private function getSlotRowsForBatch(
1852  $rowsOrIds,
1853  array $options = [],
1854  $queryFlags = 0
1855  ) {
1856  $result = new StatusValue();
1857 
1858  $revIds = [];
1859  foreach ( $rowsOrIds as $row ) {
1860  if ( is_object( $row ) ) {
1861  $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
1862  } else {
1863  $revIds[] = (int)$row;
1864  }
1865  }
1866 
1867  // Nothing to do.
1868  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
1869  if ( empty( $revIds ) ) {
1870  $result->setResult( true, [] );
1871  return $result;
1872  }
1873 
1874  // We need to set the `content` flag to join in content meta-data
1875  $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
1876  $revIdField = $slotQueryInfo['keys']['rev_id'];
1877  $slotQueryConds = [ $revIdField => $revIds ];
1878 
1879  if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
1880  if ( empty( $options['slots'] ) ) {
1881  // Degenerate case: return no slots for each revision.
1882  $result->setResult( true, array_fill_keys( $revIds, [] ) );
1883  return $result;
1884  }
1885 
1886  $roleIdField = $slotQueryInfo['keys']['role_id'];
1887  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
1888  return $this->slotRoleStore->getId( $slot_name );
1889  }, $options['slots'] );
1890  }
1891 
1892  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1893  $slotRows = $db->select(
1894  $slotQueryInfo['tables'],
1895  $slotQueryInfo['fields'],
1896  $slotQueryConds,
1897  __METHOD__,
1898  [],
1899  $slotQueryInfo['joins']
1900  );
1901 
1902  $slotContents = null;
1903  if ( $options['blobs'] ?? false ) {
1904  $blobAddresses = [];
1905  foreach ( $slotRows as $slotRow ) {
1906  $blobAddresses[] = $slotRow->content_address;
1907  }
1908  $slotContentFetchStatus = $this->blobStore
1909  ->getBlobBatch( $blobAddresses, $queryFlags );
1910  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
1911  $result->warning( $error['message'], ...$error['params'] );
1912  }
1913  $slotContents = $slotContentFetchStatus->getValue();
1914  }
1915 
1916  $slotRowsByRevId = [];
1917  foreach ( $slotRows as $slotRow ) {
1918  if ( $slotContents === null ) {
1919  // nothing to do
1920  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
1921  $slotRow->blob_data = $slotContents[$slotRow->content_address];
1922  } else {
1923  $result->warning(
1924  'internalerror_info',
1925  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
1926  );
1927  $slotRow->blob_data = null;
1928  }
1929 
1930  // conditional needed for SCHEMA_COMPAT_READ_OLD
1931  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
1932  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
1933  }
1934 
1935  // conditional needed for SCHEMA_COMPAT_READ_OLD
1936  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
1937  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
1938  }
1939 
1940  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
1941  }
1942 
1943  $result->setResult( true, $slotRowsByRevId );
1944  return $result;
1945  }
1946 
1967  public function getContentBlobsForBatch(
1968  $rowsOrIds,
1969  $slots = null,
1970  $queryFlags = 0
1971  ) {
1972  $result = $this->getSlotRowsForBatch(
1973  $rowsOrIds,
1974  [ 'slots' => $slots, 'blobs' => true ],
1975  $queryFlags
1976  );
1977 
1978  if ( $result->isOK() ) {
1979  // strip out all internal meta data that we don't want to expose
1980  foreach ( $result->value as $revId => $rowsByRole ) {
1981  foreach ( $rowsByRole as $role => $slotRow ) {
1982  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
1983  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
1984  // if we didn't ask for it.
1985  unset( $result->value[$revId][$role] );
1986  continue;
1987  }
1988 
1989  $result->value[$revId][$role] = (object)[
1990  'blob_data' => $slotRow->blob_data,
1991  'model_name' => $slotRow->model_name,
1992  ];
1993  }
1994  }
1995  }
1996 
1997  return $result;
1998  }
1999 
2015  array $fields,
2016  $queryFlags = 0,
2017  Title $title = null
2018  ) {
2019  if ( !$title && isset( $fields['title'] ) ) {
2020  if ( !( $fields['title'] instanceof Title ) ) {
2021  throw new MWException( 'title field must contain a Title object.' );
2022  }
2023 
2024  $title = $fields['title'];
2025  }
2026 
2027  if ( !$title ) {
2028  $pageId = $fields['page'] ?? 0;
2029  $revId = $fields['id'] ?? 0;
2030 
2031  $title = $this->getTitle( $pageId, $revId, $queryFlags );
2032  }
2033 
2034  if ( !isset( $fields['page'] ) ) {
2035  $fields['page'] = $title->getArticleID( $queryFlags );
2036  }
2037 
2038  // if we have a content object, use it to set the model and type
2039  if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
2040  && !is_array( $fields['content'] )
2041  ) {
2042  throw new MWException(
2043  'content field must contain a Content object or an array of Content objects.'
2044  );
2045  }
2046 
2047  if ( !empty( $fields['text_id'] ) ) {
2048  throw new MWException( 'The text_id field can not be used in MediaWiki 1.35 and later' );
2049  }
2050 
2051  if (
2052  isset( $fields['comment'] )
2053  && !( $fields['comment'] instanceof CommentStoreComment )
2054  ) {
2055  $commentData = $fields['comment_data'] ?? null;
2056 
2057  if ( $fields['comment'] instanceof Message ) {
2058  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2059  $fields['comment'],
2060  $commentData
2061  );
2062  } else {
2063  $commentText = trim( strval( $fields['comment'] ) );
2064  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2065  $commentText,
2066  $commentData
2067  );
2068  }
2069  }
2070 
2071  $revision = new MutableRevisionRecord( $title, $this->dbDomain );
2072 
2074  if ( isset( $fields['content'] ) ) {
2075  if ( is_array( $fields['content'] ) ) {
2076  $slotContent = $fields['content'];
2077  } else {
2078  $slotContent = [ SlotRecord::MAIN => $fields['content'] ];
2079  }
2080  } elseif ( isset( $fields['text'] ) ) {
2081  if ( isset( $fields['content_model'] ) ) {
2082  $model = $fields['content_model'];
2083  } else {
2084  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( SlotRecord::MAIN );
2085  $model = $slotRoleHandler->getDefaultModel( $title );
2086  }
2087 
2088  $contentHandler = ContentHandler::getForModelID( $model );
2089  $content = $contentHandler->unserializeContent( $fields['text'] );
2090  $slotContent = [ SlotRecord::MAIN => $content ];
2091  } else {
2092  $slotContent = [];
2093  }
2094 
2095  foreach ( $slotContent as $role => $content ) {
2096  $revision->setContent( $role, $content );
2097  }
2098 
2099  $this->initializeMutableRevisionFromArray( $revision, $fields );
2100 
2101  return $revision;
2102  }
2103 
2109  MutableRevisionRecord $record,
2110  array $fields
2111  ) {
2113  $user = null;
2114 
2115  // If a user is passed in, use it if possible. We cannot use a user from a
2116  // remote wiki with unsuppressed ids, due to issues described in T222212.
2117  if ( isset( $fields['user'] ) &&
2118  ( $fields['user'] instanceof UserIdentity ) &&
2119  ( $this->dbDomain === false ||
2120  ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
2121  ) {
2122  $user = $fields['user'];
2123  } else {
2124  $userID = isset( $fields['user'] ) && is_numeric( $fields['user'] ) ? $fields['user'] : null;
2125  try {
2126  $user = User::newFromAnyId(
2127  $userID,
2128  $fields['user_text'] ?? null,
2129  $fields['actor'] ?? null,
2130  $this->dbDomain
2131  );
2132  } catch ( InvalidArgumentException $ex ) {
2133  $user = null;
2134  }
2135  }
2136 
2137  if ( $user ) {
2138  $record->setUser( $user );
2139  }
2140 
2141  $timestamp = isset( $fields['timestamp'] )
2142  ? strval( $fields['timestamp'] )
2143  : MWTimestamp::now( TS_MW );
2144 
2145  $record->setTimestamp( $timestamp );
2146 
2147  if ( isset( $fields['page'] ) ) {
2148  $record->setPageId( intval( $fields['page'] ) );
2149  }
2150 
2151  if ( isset( $fields['id'] ) ) {
2152  $record->setId( intval( $fields['id'] ) );
2153  }
2154  if ( isset( $fields['parent_id'] ) ) {
2155  $record->setParentId( intval( $fields['parent_id'] ) );
2156  }
2157 
2158  if ( isset( $fields['sha1'] ) ) {
2159  $record->setSha1( $fields['sha1'] );
2160  }
2161 
2162  if ( isset( $fields['size'] ) ) {
2163  $record->setSize( intval( $fields['size'] ) );
2164  } elseif ( isset( $fields['len'] ) ) {
2165  $record->setSize( intval( $fields['len'] ) );
2166  }
2167 
2168  if ( isset( $fields['minor_edit'] ) ) {
2169  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2170  }
2171  if ( isset( $fields['deleted'] ) ) {
2172  $record->setVisibility( intval( $fields['deleted'] ) );
2173  }
2174 
2175  if ( isset( $fields['comment'] ) ) {
2176  Assert::parameterType(
2177  CommentStoreComment::class,
2178  $fields['comment'],
2179  '$row[\'comment\']'
2180  );
2181  $record->setComment( $fields['comment'] );
2182  }
2183  }
2184 
2199  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2200  wfDeprecated( __METHOD__, '1.35' );
2201  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2202  if ( $id ) {
2203  $conds['rev_id'] = intval( $id );
2204  } else {
2205  $conds[] = 'rev_id=page_latest';
2206  }
2207  return $this->loadRevisionFromConds( $db, $conds );
2208  }
2209 
2227  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2228  wfDeprecated( __METHOD__, '1.35' );
2229  if ( $id ) {
2230  $matchId = intval( $id );
2231  } else {
2232  $matchId = 'page_latest';
2233  }
2234 
2235  return $this->loadRevisionFromConds(
2236  $db,
2237  [
2238  "rev_id=$matchId",
2239  'page_namespace' => $title->getNamespace(),
2240  'page_title' => $title->getDBkey()
2241  ],
2242  0,
2243  $title
2244  );
2245  }
2246 
2261  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2262  wfDeprecated( __METHOD__, '1.35' );
2263  return $this->loadRevisionFromConds( $db,
2264  [
2265  'rev_timestamp' => $db->timestamp( $timestamp ),
2266  'page_namespace' => $title->getNamespace(),
2267  'page_title' => $title->getDBkey()
2268  ],
2269  0,
2270  $title
2271  );
2272  }
2273 
2290  private function newRevisionFromConds(
2291  array $conditions,
2292  int $flags = IDBAccessObject::READ_NORMAL,
2293  Title $title = null,
2294  array $options = []
2295  ) {
2296  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2297  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title, $options );
2298 
2299  $lb = $this->getDBLoadBalancer();
2300 
2301  // Make sure new pending/committed revision are visibile later on
2302  // within web requests to certain avoid bugs like T93866 and T94407.
2303  if ( !$rev
2304  && !( $flags & self::READ_LATEST )
2305  && $lb->hasStreamingReplicaServers()
2306  && $lb->hasOrMadeRecentMasterChanges()
2307  ) {
2308  $flags = self::READ_LATEST;
2309  $dbw = $this->getDBConnectionRef( DB_MASTER );
2310  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title, $options );
2311  }
2312 
2313  return $rev;
2314  }
2315 
2330  private function loadRevisionFromConds(
2331  IDatabase $db,
2332  array $conditions,
2333  int $flags = IDBAccessObject::READ_NORMAL,
2334  Title $title = null,
2335  array $options = []
2336  ) {
2337  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2338  if ( $row ) {
2339  $rev = $this->newRevisionFromRow( $row, $flags, $title );
2340 
2341  return $rev;
2342  }
2343 
2344  return null;
2345  }
2346 
2354  private function checkDatabaseDomain( IDatabase $db ) {
2355  $dbDomain = $db->getDomainID();
2356  $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2357  if ( $dbDomain === $storeDomain ) {
2358  return;
2359  }
2360 
2361  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2362  }
2363 
2377  private function fetchRevisionRowFromConds(
2378  IDatabase $db,
2379  array $conditions,
2380  int $flags = IDBAccessObject::READ_NORMAL,
2381  array $options = []
2382  ) {
2383  $this->checkDatabaseDomain( $db );
2384 
2385  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2386  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2387  $options[] = 'FOR UPDATE';
2388  }
2389  return $db->selectRow(
2390  $revQuery['tables'],
2391  $revQuery['fields'],
2392  $conditions,
2393  __METHOD__,
2394  $options,
2395  $revQuery['joins']
2396  );
2397  }
2398 
2420  public function getQueryInfo( $options = [] ) {
2421  $ret = [
2422  'tables' => [],
2423  'fields' => [],
2424  'joins' => [],
2425  ];
2426 
2427  $ret['tables'][] = 'revision';
2428  $ret['fields'] = array_merge( $ret['fields'], [
2429  'rev_id',
2430  'rev_page',
2431  'rev_timestamp',
2432  'rev_minor_edit',
2433  'rev_deleted',
2434  'rev_len',
2435  'rev_parent_id',
2436  'rev_sha1',
2437  ] );
2438 
2439  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2440  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2441  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2442  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2443 
2444  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2445  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2446  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2447  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2448 
2449  if ( in_array( 'page', $options, true ) ) {
2450  $ret['tables'][] = 'page';
2451  $ret['fields'] = array_merge( $ret['fields'], [
2452  'page_namespace',
2453  'page_title',
2454  'page_id',
2455  'page_latest',
2456  'page_is_redirect',
2457  'page_len',
2458  ] );
2459  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2460  }
2461 
2462  if ( in_array( 'user', $options, true ) ) {
2463  $ret['tables'][] = 'user';
2464  $ret['fields'] = array_merge( $ret['fields'], [
2465  'user_name',
2466  ] );
2467  $u = $actorQuery['fields']['rev_user'];
2468  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2469  }
2470 
2471  if ( in_array( 'text', $options, true ) ) {
2472  throw new InvalidArgumentException(
2473  'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2474  );
2475  }
2476 
2477  return $ret;
2478  }
2479 
2500  public function getSlotsQueryInfo( $options = [] ) {
2501  $ret = [
2502  'tables' => [],
2503  'fields' => [],
2504  'joins' => [],
2505  'keys' => [],
2506  ];
2507 
2508  $ret['keys']['rev_id'] = 'slot_revision_id';
2509  $ret['keys']['role_id'] = 'slot_role_id';
2510 
2511  $ret['tables'][] = 'slots';
2512  $ret['fields'] = array_merge( $ret['fields'], [
2513  'slot_revision_id',
2514  'slot_content_id',
2515  'slot_origin',
2516  'slot_role_id',
2517  ] );
2518 
2519  if ( in_array( 'role', $options, true ) ) {
2520  // Use left join to attach role name, so we still find the revision row even
2521  // if the role name is missing. This triggers a more obvious failure mode.
2522  $ret['tables'][] = 'slot_roles';
2523  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2524  $ret['fields'][] = 'role_name';
2525  }
2526 
2527  if ( in_array( 'content', $options, true ) ) {
2528  $ret['keys']['model_id'] = 'content_model';
2529 
2530  $ret['tables'][] = 'content';
2531  $ret['fields'] = array_merge( $ret['fields'], [
2532  'content_size',
2533  'content_sha1',
2534  'content_address',
2535  'content_model',
2536  ] );
2537  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2538 
2539  if ( in_array( 'model', $options, true ) ) {
2540  // Use left join to attach model name, so we still find the revision row even
2541  // if the model name is missing. This triggers a more obvious failure mode.
2542  $ret['tables'][] = 'content_models';
2543  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2544  $ret['fields'][] = 'model_name';
2545  }
2546 
2547  }
2548 
2549  return $ret;
2550  }
2551 
2565  public function getArchiveQueryInfo() {
2566  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2567  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2568  $ret = [
2569  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2570  'fields' => [
2571  'ar_id',
2572  'ar_page_id',
2573  'ar_namespace',
2574  'ar_title',
2575  'ar_rev_id',
2576  'ar_timestamp',
2577  'ar_minor_edit',
2578  'ar_deleted',
2579  'ar_len',
2580  'ar_parent_id',
2581  'ar_sha1',
2582  ] + $commentQuery['fields'] + $actorQuery['fields'],
2583  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2584  ];
2585 
2586  return $ret;
2587  }
2588 
2598  public function getRevisionSizes( array $revIds ) {
2599  $dbr = $this->getDBConnectionRef( DB_REPLICA );
2600  $revLens = [];
2601  if ( !$revIds ) {
2602  return $revLens; // empty
2603  }
2604 
2605  $res = $dbr->select(
2606  'revision',
2607  [ 'rev_id', 'rev_len' ],
2608  [ 'rev_id' => $revIds ],
2609  __METHOD__
2610  );
2611 
2612  foreach ( $res as $row ) {
2613  $revLens[$row->rev_id] = intval( $row->rev_len );
2614  }
2615 
2616  return $revLens;
2617  }
2618 
2631  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2632  wfDeprecated( __METHOD__, '1.35' );
2633  return $this->getRevisionSizes( $revIds );
2634  }
2635 
2644  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2645  $op = $dir === 'next' ? '>' : '<';
2646  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2647 
2648  if ( !$rev->getId() || !$rev->getPageId() ) {
2649  // revision is unsaved or otherwise incomplete
2650  return null;
2651  }
2652 
2653  if ( $rev instanceof RevisionArchiveRecord ) {
2654  // revision is deleted, so it's not part of the page history
2655  return null;
2656  }
2657 
2658  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2659  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2660 
2661  $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2662  if ( $ts === false ) {
2663  // XXX Should this be moved into getTimestampFromId?
2664  $ts = $db->selectField( 'archive', 'ar_timestamp',
2665  [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2666  if ( $ts === false ) {
2667  // XXX Is this reachable? How can we have a page id but no timestamp?
2668  return null;
2669  }
2670  }
2671  $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2672 
2673  $revId = $db->selectField( 'revision', 'rev_id',
2674  [
2675  'rev_page' => $rev->getPageId(),
2676  "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op {$rev->getId()})"
2677  ],
2678  __METHOD__,
2679  [
2680  'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2681  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2682  ]
2683  );
2684 
2685  if ( $revId === false ) {
2686  return null;
2687  }
2688 
2689  return $this->getRevisionById( intval( $revId ) );
2690  }
2691 
2707  public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
2708  if ( $flags instanceof Title ) {
2709  // Old calling convention, we don't use Title here anymore
2710  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2711  $flags = 0;
2712  }
2713 
2714  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2715  }
2716 
2730  public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
2731  if ( $flags instanceof Title ) {
2732  // Old calling convention, we don't use Title here anymore
2733  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2734  $flags = 0;
2735  }
2736 
2737  return $this->getRelativeRevision( $rev, $flags, 'next' );
2738  }
2739 
2751  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2752  $this->checkDatabaseDomain( $db );
2753 
2754  if ( $rev->getPageId() === null ) {
2755  return 0;
2756  }
2757  # Use page_latest if ID is not given
2758  if ( !$rev->getId() ) {
2759  $prevId = $db->selectField(
2760  'page', 'page_latest',
2761  [ 'page_id' => $rev->getPageId() ],
2762  __METHOD__
2763  );
2764  } else {
2765  $prevId = $db->selectField(
2766  'revision', 'rev_id',
2767  [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
2768  __METHOD__,
2769  [ 'ORDER BY' => 'rev_id DESC' ]
2770  );
2771  }
2772  return intval( $prevId );
2773  }
2774 
2787  public function getTimestampFromId( $id, $flags = 0 ) {
2788  if ( $id instanceof Title ) {
2789  // Old deprecated calling convention supported for backwards compatibility
2790  $id = $flags;
2791  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2792  }
2793  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2794 
2795  $timestamp =
2796  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2797 
2798  return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2799  }
2800 
2810  public function countRevisionsByPageId( IDatabase $db, $id ) {
2811  $this->checkDatabaseDomain( $db );
2812 
2813  $row = $db->selectRow( 'revision',
2814  [ 'revCount' => 'COUNT(*)' ],
2815  [ 'rev_page' => $id ],
2816  __METHOD__
2817  );
2818  if ( $row ) {
2819  return intval( $row->revCount );
2820  }
2821  return 0;
2822  }
2823 
2833  public function countRevisionsByTitle( IDatabase $db, $title ) {
2834  $id = $title->getArticleID();
2835  if ( $id ) {
2836  return $this->countRevisionsByPageId( $db, $id );
2837  }
2838  return 0;
2839  }
2840 
2859  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2860  $this->checkDatabaseDomain( $db );
2861 
2862  if ( !$userId ) {
2863  return false;
2864  }
2865 
2866  $revQuery = $this->getQueryInfo();
2867  $res = $db->select(
2868  $revQuery['tables'],
2869  [
2870  'rev_user' => $revQuery['fields']['rev_user'],
2871  ],
2872  [
2873  'rev_page' => $pageId,
2874  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2875  ],
2876  __METHOD__,
2877  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2878  $revQuery['joins']
2879  );
2880  foreach ( $res as $row ) {
2881  if ( $row->rev_user != $userId ) {
2882  return false;
2883  }
2884  }
2885  return true;
2886  }
2887 
2901  public function getKnownCurrentRevision( Title $title, $revId = 0 ) {
2902  $db = $this->getDBConnectionRef( DB_REPLICA );
2903 
2904  $revIdPassed = $revId;
2905  $pageId = $title->getArticleID();
2906 
2907  if ( !$pageId ) {
2908  return false;
2909  }
2910 
2911  if ( !$revId ) {
2912  $revId = $title->getLatestRevID();
2913  }
2914 
2915  if ( !$revId ) {
2916  wfWarn(
2917  'No latest revision known for page ' . $title->getPrefixedDBkey()
2918  . ' even though it exists with page ID ' . $pageId
2919  );
2920  return false;
2921  }
2922 
2923  // Load the row from cache if possible. If not possible, populate the cache.
2924  // As a minor optimization, remember if this was a cache hit or miss.
2925  // We can sometimes avoid a database query later if this is a cache miss.
2926  $fromCache = true;
2927  $row = $this->cache->getWithSetCallback(
2928  // Page/rev IDs passed in from DB to reflect history merges
2929  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2930  WANObjectCache::TTL_WEEK,
2931  function ( $curValue, &$ttl, array &$setOpts ) use (
2932  $db, $revId, &$fromCache
2933  ) {
2934  $setOpts += Database::getCacheSetOptions( $db );
2935  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2936  if ( $row ) {
2937  $fromCache = false;
2938  }
2939  return $row; // don't cache negatives
2940  }
2941  );
2942 
2943  // Reflect revision deletion and user renames.
2944  if ( $row ) {
2945  $this->ensureRevisionRowMatchesTitle( $row, $title, [
2946  'from_cache_flag' => $fromCache,
2947  'page_id_initial' => $pageId,
2948  'rev_id_used' => $revId,
2949  'rev_id_requested' => $revIdPassed,
2950  ] );
2951 
2952  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2953  } else {
2954  return false;
2955  }
2956  }
2957 
2966  public function getFirstRevision(
2967  LinkTarget $title,
2968  int $flags = IDBAccessObject::READ_NORMAL
2969  ): ?RevisionRecord {
2970  $titleObj = Title::newFromLinkTarget( $title ); // TODO: eventually we shouldn't need a title
2971  return $this->newRevisionFromConds(
2972  [
2973  'page_namespace' => $title->getNamespace(),
2974  'page_title' => $title->getDBkey()
2975  ],
2976  $flags,
2977  $titleObj,
2978  [
2979  'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
2980  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
2981  ]
2982  );
2983  }
2984 
2996  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2997  return $this->cache->makeGlobalKey(
2998  self::ROW_CACHE_KEY,
2999  $db->getDomainID(),
3000  $pageId,
3001  $revId
3002  );
3003  }
3004 
3012  private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
3013  if ( $rev ) {
3014  if ( $rev->getId() === null ) {
3015  throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
3016  }
3017  if ( $rev->getPageId() !== $pageId ) {
3018  throw new InvalidArgumentException(
3019  "Revision {$rev->getId()} doesn't belong to page {$pageId}"
3020  );
3021  }
3022  }
3023  }
3024 
3039  private function getRevisionLimitConditions(
3040  IDatabase $dbr,
3041  RevisionRecord $old = null,
3042  RevisionRecord $new = null,
3043  $options = []
3044  ) {
3045  $options = (array)$options;
3046  $oldCmp = '>';
3047  $newCmp = '<';
3048  if ( in_array( 'include_old', $options ) ) {
3049  $oldCmp = '>=';
3050  }
3051  if ( in_array( 'include_new', $options ) ) {
3052  $newCmp = '<=';
3053  }
3054  if ( in_array( 'include_both', $options ) ) {
3055  $oldCmp = '>=';
3056  $newCmp = '<=';
3057  }
3058 
3059  $conds = [];
3060  if ( $old ) {
3061  $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3062  $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId()}) " .
3063  "OR rev_timestamp > {$oldTs}";
3064  }
3065  if ( $new ) {
3066  $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3067  $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId()}) " .
3068  "OR rev_timestamp < {$newTs}";
3069  }
3070  return $conds;
3071  }
3072 
3099  public function getRevisionIdsBetween(
3100  int $pageId,
3101  RevisionRecord $old = null,
3102  RevisionRecord $new = null,
3103  ?int $max = null,
3104  $options = [],
3105  ?string $order = null,
3106  int $flags = IDBAccessObject::READ_NORMAL
3107  ) : array {
3108  $this->assertRevisionParameter( 'old', $pageId, $old );
3109  $this->assertRevisionParameter( 'new', $pageId, $new );
3110 
3111  $options = (array)$options;
3112  $includeOld = in_array( 'include_old', $options ) ||
3113  in_array( 'include_both', $options );
3114  $includeNew = in_array( 'include_new', $options ) ||
3115  in_array( 'include_both', $options );
3116 
3117  // No DB query needed if old and new are the same revision.
3118  // Can't check for consecutive revisions with 'getParentId' for a similar
3119  // optimization as edge cases exist when there are revisions between
3120  // a revision and it's parent. See T185167 for more details.
3121  if ( $old && $new && $new->getId() === $old->getId() ) {
3122  return $includeOld || $includeNew ? [ $new->getId() ] : [];
3123  }
3124 
3125  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3126  $conds = array_merge(
3127  [
3128  'rev_page' => $pageId,
3129  $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3130  ],
3131  $this->getRevisionLimitConditions( $db, $old, $new, $options )
3132  );
3133 
3134  $queryOptions = [];
3135  if ( $order !== null ) {
3136  $queryOptions['ORDER BY'] = [ "rev_timestamp $order", "rev_id $order" ];
3137  }
3138  if ( $max !== null ) {
3139  $queryOptions['LIMIT'] = $max + 1; // extra to detect truncation
3140  }
3141 
3142  $values = $db->selectFieldValues(
3143  'revision',
3144  'rev_id',
3145  $conds,
3146  __METHOD__,
3147  $queryOptions
3148  );
3149  return array_map( 'intval', $values );
3150  }
3151 
3173  public function getAuthorsBetween(
3174  $pageId,
3175  RevisionRecord $old = null,
3176  RevisionRecord $new = null,
3177  User $user = null,
3178  $max = null,
3179  $options = []
3180  ) {
3181  $this->assertRevisionParameter( 'old', $pageId, $old );
3182  $this->assertRevisionParameter( 'new', $pageId, $new );
3183  $options = (array)$options;
3184 
3185  // No DB query needed if old and new are the same revision.
3186  // Can't check for consecutive revisions with 'getParentId' for a similar
3187  // optimization as edge cases exist when there are revisions between
3188  //a revision and it's parent. See T185167 for more details.
3189  if ( $old && $new && $new->getId() === $old->getId() ) {
3190  if ( empty( $options ) ) {
3191  return [];
3192  } else {
3193  return $user ? [ $new->getUser( RevisionRecord::FOR_PUBLIC, $user ) ] : [ $new->getUser() ];
3194  }
3195  }
3196 
3197  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3198  $conds = array_merge(
3199  [
3200  'rev_page' => $pageId,
3201  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3202  ],
3203  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3204  );
3205 
3206  $queryOpts = [ 'DISTINCT' ];
3207  if ( $max !== null ) {
3208  $queryOpts['LIMIT'] = $max + 1;
3209  }
3210 
3211  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3212  return array_map( function ( $row ) {
3213  return new UserIdentityValue( (int)$row->rev_user, $row->rev_user_text, (int)$row->rev_actor );
3214  }, iterator_to_array( $dbr->select(
3215  array_merge( [ 'revision' ], $actorQuery['tables'] ),
3216  $actorQuery['fields'],
3217  $conds, __METHOD__,
3218  $queryOpts,
3219  $actorQuery['joins']
3220  ) ) );
3221  }
3222 
3244  public function countAuthorsBetween(
3245  $pageId,
3246  RevisionRecord $old = null,
3247  RevisionRecord $new = null,
3248  User $user = null,
3249  $max = null,
3250  $options = []
3251  ) {
3252  // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3253  // and creation of UserIdentity stuff.
3254  return count( $this->getAuthorsBetween( $pageId, $old, $new, $user, $max, $options ) );
3255  }
3256 
3277  public function countRevisionsBetween(
3278  $pageId,
3279  RevisionRecord $old = null,
3280  RevisionRecord $new = null,
3281  $max = null,
3282  $options = []
3283  ) {
3284  $this->assertRevisionParameter( 'old', $pageId, $old );
3285  $this->assertRevisionParameter( 'new', $pageId, $new );
3286 
3287  // No DB query needed if old and new are the same revision.
3288  // Can't check for consecutive revisions with 'getParentId' for a similar
3289  // optimization as edge cases exist when there are revisions between
3290  //a revision and it's parent. See T185167 for more details.
3291  if ( $old && $new && $new->getId() === $old->getId() ) {
3292  return 0;
3293  }
3294 
3295  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3296  $conds = array_merge(
3297  [
3298  'rev_page' => $pageId,
3299  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3300  ],
3301  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3302  );
3303  if ( $max !== null ) {
3304  return $dbr->selectRowCount( 'revision', '1',
3305  $conds,
3306  __METHOD__,
3307  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3308  );
3309  } else {
3310  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3311  }
3312  }
3313 
3314  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3315 }
3316 
3321 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Revision\RevisionStore\ORDER_OLDEST_TO_NEWEST
const ORDER_OLDEST_TO_NEWEST
Definition: RevisionStore.php:85
Revision\MutableRevisionRecord\setMinorEdit
setMinorEdit( $minorEdit)
Definition: MutableRevisionRecord.php:241
Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:111
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:34
Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:121
Revision\RevisionStore\ensureRevisionRowMatchesTitle
ensureRevisionRowMatchesTitle( $row, Title $title, $context=[])
Check that the given row matches the given Title object.
Definition: RevisionStore.php:1585
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:34
Revision\RevisionStore\$hookContainer
HookContainer $hookContainer
Definition: RevisionStore.php:140
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
ContentHandler
A content handler knows how do deal with a specific type of content on a wiki page.
Definition: ContentHandler.php:59
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:269
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:50
Revision\RevisionStore\insertSlotOn
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:553
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:2354
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
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:975
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:32
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
User\getId
getId()
Get the user's ID.
Definition: User.php:2025
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:435
Revision\RevisionStore\newMutableRevisionFromArray
newMutableRevisionFromArray(array $fields, $queryFlags=0, Title $title=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1....
Definition: RevisionStore.php:2014
Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:218
Revision\RevisionStore\getKnownCurrentRevision
getKnownCurrentRevision(Title $title, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: RevisionStore.php:2901
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:214
Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:340
Revision\MutableRevisionRecord\setSha1
setSha1( $sha1)
Set revision hash, for optimization.
Definition: MutableRevisionRecord.php:199
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:85
Revision\MutableRevisionRecord\setParentId
setParentId( $parentId)
Definition: MutableRevisionRecord.php:94
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:81
MediaWiki\Storage\SqlBlobStore
Service for storing and loading Content objects.
Definition: SqlBlobStore.php:51
Revision\RevisionStore\__construct
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorMigration $actorMigration, IContentHandlerFactory $contentHandlerFactory, HookContainer $hookContainer, $dbDomain=false)
Definition: RevisionStore.php:165
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:72
Revision\RevisionStore\initializeMutableRevisionFromArray
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
Definition: RevisionStore.php:2108
Revision\RevisionStoreCacheRecord
A cached RevisionStoreRecord.
Definition: RevisionStoreCacheRecord.php:38
Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, Title $title)
Definition: RevisionStore.php:1213
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:446
Revision\RevisionStore\getSlotRowsForBatch
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
Definition: RevisionStore.php:1851
Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2565
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:276
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:442
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:106
Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:2787
Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:191
Revision\RevisionStore\getRcIdIfUnpatrolled
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
Definition: RevisionStore.php:953
Revision\RevisionFactory
Service for constructing revision objects.
Definition: RevisionFactory.php:38
Revision\MutableRevisionRecord\setId
setId( $id)
Set the revision ID.
Definition: MutableRevisionRecord.php:258
Revision\RevisionStore\checkContent
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
Definition: RevisionStore.php:844
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:396
ActorMigration
This class handles the logic for the actor table migration and should always be used in lieu of direc...
Definition: ActorMigration.php:39
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:3012
Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2500
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:116
$revQuery
$revQuery
Definition: testCompression.php:56
Revision\RevisionStore\insertIpChangesRow
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
Definition: RevisionStore.php:593
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Revision\RevisionStore\getDBConnectionRef
getDBConnectionRef( $mode, $groups=[])
Definition: RevisionStore.php:229
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:2631
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:2859
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\Revision
Definition: ContributionsLookup.php:3
Revision
Definition: Revision.php:40
Revision\RevisionStore\getRevisionLimitConditions
getRevisionLimitConditions(IDatabase $dbr, RevisionRecord $old=null, RevisionRecord $new=null, $options=[])
Converts revision limits to query conditions.
Definition: RevisionStore.php:3039
Revision\RevisionStore\newRevisionsFromBatch
newRevisionsFromBatch( $rows, array $options=[], $queryFlags=0, Title $title=null)
Construct a RevisionRecord instance for each row in $rows, and return them as an associative array in...
Definition: RevisionStore.php:1640
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:1007
Revision\RevisionStore\newRevisionFromArchiveRowAndSlots
newRevisionFromArchiveRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, array $overrides=[])
Definition: RevisionStore.php:1431
Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:405
Revision\RevisionStore\getQueryInfo
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
Definition: RevisionStore.php:2420
MWException
MediaWiki exception.
Definition: MWException.php:29
Revision\RevisionRecord\getSize
getSize()
Returns the nominal size of this revision, in bogo-bytes.
Revision\RevisionStore\ORDER_NEWEST_TO_OLDEST
const ORDER_NEWEST_TO_OLDEST
Definition: RevisionStore.php:86
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:269
Revision\RevisionRecord\getSha1
getSha1()
Returns the base36 sha1 of this revision.
Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:771
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1026
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:361
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
Wikimedia\Rdbms\Database\getCacheSetOptions
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4923
Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1080
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:524
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:1149
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:2199
$blob
$blob
Definition: testCompression.php:70
Revision\RevisionRecord\getUser
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Definition: RevisionRecord.php:371
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:489
Revision\MutableRevisionRecord\setVisibility
setVisibility( $visibility)
Definition: MutableRevisionRecord.php:223
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:469
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\newRevisionSlots
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, Title $title)
Factory method for RevisionSlots based on a revision ID.
Definition: RevisionStore.php:1341
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:2751
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:409
Revision\RevisionStore\$dbDomain
bool string $dbDomain
Definition: RevisionStore.php:96
Revision\RevisionRecord\isReadyForInsertion
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
Definition: RevisionRecord.php:563
Revision\RevisionStore\insertSlotRowOn
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
Definition: RevisionStore.php:805
Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2598
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:60
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:566
$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:592
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:499
User\newFromAnyId
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:614
Revision\RevisionRecord\getSlotRoles
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
Definition: RevisionRecord.php:218
DB_MASTER
const DB_MASTER
Definition: defines.php:26
Revision\RevisionRecord\getPageId
getPageId()
Get the page ID.
Definition: RevisionRecord.php:331
FallbackContent
Content object implementation representing unknown content.
Definition: FallbackContent.php:38
Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:91
Revision\RevisionStore\getPreviousRevision
getPreviousRevision(RevisionRecord $rev, $flags=0)
Get the revision before $rev in the page's history, if any.
Definition: RevisionStore.php:2707
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
Revision\RevisionStore\setLogger
setLogger(LoggerInterface $logger)
Definition: RevisionStore.php:195
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:538
Revision\RevisionStore\constructSlotRecords
constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
Definition: RevisionStore.php:1263
Revision\RevisionStore\ROW_CACHE_KEY
const ROW_CACHE_KEY
Definition: RevisionStore.php:83
Revision\RevisionStore\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: RevisionStore.php:134
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:279
Revision\RevisionArchiveRecord
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Definition: RevisionArchiveRecord.php:41
Revision\RevisionRecord\getParentId
getParentId()
Get parent revision ID (the original previous page revision).
Definition: RevisionRecord.php:295
Revision\RevisionStore\getFirstRevision
getFirstRevision(LinkTarget $title, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
Definition: RevisionStore.php:2966
$content
$content
Definition: router.php:76
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:522
Revision\RevisionRecord\DELETED_USER
const DELETED_USER
Definition: RevisionRecord.php:51
DBAccessObjectUtils\hasFlags
static hasFlags( $bitfield, $flags)
Definition: DBAccessObjectUtils.php:35
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:45
Revision\RevisionStore\getNextRevision
getNextRevision(RevisionRecord $rev, $flags=0)
Get the revision after $rev in the page's history, if any.
Definition: RevisionStore.php:2730
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:3099
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:125
Revision\RevisionStore\$slotRoleStore
NameTableStore $slotRoleStore
Definition: RevisionStore.php:131
Revision\RevisionStore\getContentBlobsForBatch
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
Definition: RevisionStore.php:1967
Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2833
Revision\RevisionRecord\getComment
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
Definition: RevisionRecord.php:396
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:498
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:2644
Revision\RevisionStore\$hookRunner
HookRunner $hookRunner
Definition: RevisionStore.php:143
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
Revision\RevisionStore\getRevisionByTitle
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: RevisionStore.php:1100
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:281
MediaWiki\Storage\BlobStore
Service for loading and storing data blobs.
Definition: BlobStore.php:35
Content
Base interface for content objects.
Definition: Content.php:35
Revision\RevisionStore\newNullRevision
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page's history.
Definition: RevisionStore.php:889
Revision\RevisionRecord\FOR_PUBLIC
const FOR_PUBLIC
Definition: RevisionRecord.php:58
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:431
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:2377
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:351
Title
Represents a title within MediaWiki.
Definition: Title.php:42
Revision\RevisionStore\insertContentRowOn
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
Definition: RevisionStore.php:823
Revision\RevisionStore\countRevisionsByPageId
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2810
Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
Definition: RevisionStore.php:476
Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:126
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:164
Revision\RevisionStore\getDBLoadBalancer
getDBLoadBalancer()
Definition: RevisionStore.php:209
Revision\RevisionStore\newRevisionFromArchiveRow
newRevisionFromArchiveRow( $row, $queryFlags=0, Title $title=null, array $overrides=[])
Make a fake revision object from an archive table row.
Definition: RevisionStore.php:1381
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:81
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:49
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
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:1353
Revision\RevisionStore\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: RevisionStore.php:137
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\getAuthorsBetween
getAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, User $user=null, $max=null, $options=[])
Get the authors between the given revisions or revisions.
Definition: RevisionStore.php:3173
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\newRevisionFromConds
newRevisionFromConds(array $conditions, int $flags=IDBAccessObject::READ_NORMAL, Title $title=null, array $options=[])
Given a set of conditions, fetch a revision.
Definition: RevisionStore.php:2290
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:44
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:1073
Revision\MutableRevisionRecord\setTimestamp
setTimestamp( $timestamp)
Definition: MutableRevisionRecord.php:232
Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:323
Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:620
Revision\MutableRevisionRecord\setSize
setSize( $size)
Set nominal revision size, for optimization.
Definition: MutableRevisionRecord.php:214
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:570
$t
$t
Definition: testCompression.php:74
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Revision\RevisionStore\newRevisionFromRowAndSlots
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, $fromCache=false)
Definition: RevisionStore.php:1512
MediaWiki\$context
IContextSource $context
Definition: MediaWiki.php:40
Revision\RevisionStore\loadRevisionFromTimestamp
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:2261
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:513
Revision\RevisionStore\countAuthorsBetween
countAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, User $user=null, $max=null, $options=[])
Get the number of authors between the given revisions.
Definition: RevisionStore.php:3244
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
Title\newFromID
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:473
Revision\RevisionStore\loadRevisionFromConds
loadRevisionFromConds(IDatabase $db, array $conditions, int $flags=IDBAccessObject::READ_NORMAL, Title $title=null, array $options=[])
Given a set of conditions, fetch a revision from the given database connection.
Definition: RevisionStore.php:2330
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2054
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:2996
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
Revision\MutableRevisionRecord\setComment
setComment(CommentStoreComment $comment)
Definition: MutableRevisionRecord.php:186
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
Revision\RevisionStore\getRevisionByTimestamp
getRevisionByTimestamp(LinkTarget $title, string $timestamp, int $flags=IDBAccessObject::READ_NORMAL)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:1189
Revision\RevisionStore\getBaseRevisionRow
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:738
Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, Title $title=null, $fromCache=false)
Definition: RevisionStore.php:1403
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
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:2227
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:3277
Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:101
Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:202
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:248