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 IDBAccessObject;
36 use InvalidArgumentException;
47 use Message;
48 use MWException;
49 use MWTimestamp;
51 use Psr\Log\LoggerAwareInterface;
52 use Psr\Log\LoggerInterface;
53 use Psr\Log\NullLogger;
54 use RecentChange;
55 use Revision;
56 use RuntimeException;
57 use StatusValue;
58 use Title;
59 use Traversable;
60 use User;
61 use WANObjectCache;
62 use Wikimedia\Assert\Assert;
63 use Wikimedia\IPUtils;
69 
80  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
81 
82  public const ROW_CACHE_KEY = 'revision-row-1.29';
83 
87  private $blobStore;
88 
92  private $dbDomain;
93 
97  private $loadBalancer;
98 
102  private $cache;
103 
107  private $commentStore;
108 
113 
117  private $logger;
118 
123 
127  private $slotRoleStore;
128 
131 
134 
136  private $hookRunner;
137 
158  public function __construct(
159  ILoadBalancer $loadBalancer,
160  SqlBlobStore $blobStore,
163  NameTableStore $contentModelStore,
164  NameTableStore $slotRoleStore,
167  IContentHandlerFactory $contentHandlerFactory,
168  HookContainer $hookContainer,
169  $dbDomain = false
170  ) {
171  Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
172 
173  $this->loadBalancer = $loadBalancer;
174  $this->blobStore = $blobStore;
175  $this->cache = $cache;
176  $this->commentStore = $commentStore;
177  $this->contentModelStore = $contentModelStore;
178  $this->slotRoleStore = $slotRoleStore;
179  $this->slotRoleRegistry = $slotRoleRegistry;
180  $this->actorMigration = $actorMigration;
181  $this->dbDomain = $dbDomain;
182  $this->logger = new NullLogger();
183  $this->contentHandlerFactory = $contentHandlerFactory;
184  $this->hookRunner = new HookRunner( $hookContainer );
185  }
186 
187  public function setLogger( LoggerInterface $logger ) {
188  $this->logger = $logger;
189  }
190 
194  public function isReadOnly() {
195  return $this->blobStore->isReadOnly();
196  }
197 
201  private function getDBLoadBalancer() {
202  return $this->loadBalancer;
203  }
204 
210  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
211  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
212  return $this->getDBConnectionRef( $mode );
213  }
214 
221  private function getDBConnectionRef( $mode, $groups = [] ) {
222  $lb = $this->getDBLoadBalancer();
223  return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
224  }
225 
240  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
241  if ( !$pageId && !$revId ) {
242  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
243  }
244 
245  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
246  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
247  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
248  $queryFlags = self::READ_NORMAL;
249  }
250 
251  $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
252  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
253 
254  // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
255  if ( $canUseTitleNewFromId ) {
256  $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
257  // TODO: better foreign title handling (introduce TitleFactory)
258  $title = Title::newFromID( $pageId, $titleFlags );
259  if ( $title ) {
260  return $title;
261  }
262  }
263 
264  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
265  $canUseRevId = ( $revId !== null && $revId > 0 );
266 
267  if ( $canUseRevId ) {
268  $dbr = $this->getDBConnectionRef( $dbMode );
269  // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
270  $row = $dbr->selectRow(
271  [ 'revision', 'page' ],
272  [
273  'page_namespace',
274  'page_title',
275  'page_id',
276  'page_latest',
277  'page_is_redirect',
278  'page_len',
279  ],
280  [ 'rev_id' => $revId ],
281  __METHOD__,
282  $dbOptions,
283  [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
284  );
285  if ( $row ) {
286  // TODO: better foreign title handling (introduce TitleFactory)
287  return Title::newFromRow( $row );
288  }
289  }
290 
291  // If we still don't have a title, fallback to master if that wasn't already happening.
292  if ( $dbMode !== DB_MASTER ) {
293  $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
294  if ( $title ) {
295  $this->logger->info(
296  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
297  [ 'trace' => wfBacktrace() ]
298  );
299  return $title;
300  }
301  }
302 
303  throw new RevisionAccessException(
304  "Could not determine title for page ID $pageId and revision ID $revId"
305  );
306  }
307 
315  private function failOnNull( $value, $name ) {
316  if ( $value === null ) {
317  throw new IncompleteRevisionException(
318  "$name must not be " . var_export( $value, true ) . "!"
319  );
320  }
321 
322  return $value;
323  }
324 
332  private function failOnEmpty( $value, $name ) {
333  if ( $value === null || $value === 0 || $value === '' ) {
334  throw new IncompleteRevisionException(
335  "$name must not be " . var_export( $value, true ) . "!"
336  );
337  }
338 
339  return $value;
340  }
341 
353  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
354  // TODO: pass in a DBTransactionContext instead of a database connection.
355  $this->checkDatabaseDomain( $dbw );
356 
357  $slotRoles = $rev->getSlotRoles();
358 
359  // Make sure the main slot is always provided throughout migration
360  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
361  throw new IncompleteRevisionException(
362  'main slot must be provided'
363  );
364  }
365 
366  // Checks
367  $this->failOnNull( $rev->getSize(), 'size field' );
368  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
369  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
370  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
371  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
372  $this->failOnNull( $user->getId(), 'user field' );
373  $this->failOnEmpty( $user->getName(), 'user_text field' );
374 
375  if ( !$rev->isReadyForInsertion() ) {
376  // This is here for future-proofing. At the time this check being added, it
377  // was redundant to the individual checks above.
378  throw new IncompleteRevisionException( 'Revision is incomplete' );
379  }
380 
381  if ( $slotRoles == [ SlotRecord::MAIN ] ) {
382  // T239717: If the main slot is the only slot, make sure the revision's nominal size
383  // and hash match the main slot's nominal size and hash.
384  $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
385  Assert::precondition(
386  $mainSlot->getSize() === $rev->getSize(),
387  'The revisions\'s size must match the main slot\'s size (see T239717)'
388  );
389  Assert::precondition(
390  $mainSlot->getSha1() === $rev->getSha1(),
391  'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
392  );
393  }
394 
395  // TODO: we shouldn't need an actual Title here.
397  $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
398 
399  $parentId = $rev->getParentId() === null
400  ? $this->getPreviousRevisionId( $dbw, $rev )
401  : $rev->getParentId();
402 
404  $rev = $dbw->doAtomicSection(
405  __METHOD__,
406  function ( IDatabase $dbw, $fname ) use (
407  $rev,
408  $user,
409  $comment,
410  $title,
411  $pageId,
412  $parentId
413  ) {
414  return $this->insertRevisionInternal(
415  $rev,
416  $dbw,
417  $user,
418  $comment,
419  $title,
420  $pageId,
421  $parentId
422  );
423  }
424  );
425 
426  // sanity checks
427  Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
428  Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
429  Assert::postcondition(
430  $rev->getComment( RevisionRecord::RAW ) !== null,
431  'revision must have a comment'
432  );
433  Assert::postcondition(
434  $rev->getUser( RevisionRecord::RAW ) !== null,
435  'revision must have a user'
436  );
437 
438  // Trigger exception if the main slot is missing.
439  // Technically, this could go away after MCR migration: while
440  // calling code may require a main slot to exist, RevisionStore
441  // really should not know or care about that requirement.
443 
444  foreach ( $slotRoles as $role ) {
445  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
446  Assert::postcondition(
447  $slot->getContent() !== null,
448  $role . ' slot must have content'
449  );
450  Assert::postcondition(
451  $slot->hasRevision(),
452  $role . ' slot must have a revision associated'
453  );
454  }
455 
456  $this->hookRunner->onRevisionRecordInserted( $rev );
457 
458  // Soft deprecated in 1.31, hard deprecated in 1.35
459  $legacyRevision = new Revision( $rev );
460  $this->hookRunner->onRevisionInsertComplete( $legacyRevision, null, null );
461 
462  return $rev;
463  }
464 
465  private function insertRevisionInternal(
466  RevisionRecord $rev,
467  IDatabase $dbw,
468  User $user,
469  CommentStoreComment $comment,
470  Title $title,
471  $pageId,
472  $parentId
473  ) {
474  $slotRoles = $rev->getSlotRoles();
475 
476  $revisionRow = $this->insertRevisionRowOn(
477  $dbw,
478  $rev,
479  $title,
480  $parentId
481  );
482 
483  $revisionId = $revisionRow['rev_id'];
484 
485  $blobHints = [
486  BlobStore::PAGE_HINT => $pageId,
487  BlobStore::REVISION_HINT => $revisionId,
488  BlobStore::PARENT_HINT => $parentId,
489  ];
490 
491  $newSlots = [];
492  foreach ( $slotRoles as $role ) {
493  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
494 
495  // If the SlotRecord already has a revision ID set, this means it already exists
496  // in the database, and should already belong to the current revision.
497  // However, a slot may already have a revision, but no content ID, if the slot
498  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
499  // mode, and the respective archive row was not yet migrated to the new schema.
500  // In that case, a new slot row (and content row) must be inserted even during
501  // undeletion.
502  if ( $slot->hasRevision() && $slot->hasContentId() ) {
503  // TODO: properly abort transaction if the assertion fails!
504  Assert::parameter(
505  $slot->getRevision() === $revisionId,
506  'slot role ' . $slot->getRole(),
507  'Existing slot should belong to revision '
508  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
509  );
510 
511  // Slot exists, nothing to do, move along.
512  // This happens when restoring archived revisions.
513 
514  $newSlots[$role] = $slot;
515  } else {
516  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
517  }
518  }
519 
520  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
521 
522  $rev = new RevisionStoreRecord(
523  $title,
524  $user,
525  $comment,
526  (object)$revisionRow,
527  new RevisionSlots( $newSlots ),
528  $this->dbDomain
529  );
530 
531  return $rev;
532  }
533 
542  private function insertSlotOn(
543  IDatabase $dbw,
544  $revisionId,
545  SlotRecord $protoSlot,
546  Title $title,
547  array $blobHints = []
548  ) {
549  if ( $protoSlot->hasAddress() ) {
550  $blobAddress = $protoSlot->getAddress();
551  } else {
552  $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
553  }
554 
555  $contentId = null;
556 
557  if ( $protoSlot->hasContentId() ) {
558  $contentId = $protoSlot->getContentId();
559  } else {
560  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
561  }
562 
563  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
564 
565  $savedSlot = SlotRecord::newSaved(
566  $revisionId,
567  $contentId,
568  $blobAddress,
569  $protoSlot
570  );
571 
572  return $savedSlot;
573  }
574 
582  private function insertIpChangesRow(
583  IDatabase $dbw,
584  User $user,
585  RevisionRecord $rev,
586  $revisionId
587  ) {
588  if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
589  $ipcRow = [
590  'ipc_rev_id' => $revisionId,
591  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
592  'ipc_hex' => IPUtils::toHex( $user->getName() ),
593  ];
594  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
595  }
596  }
597 
609  private function insertRevisionRowOn(
610  IDatabase $dbw,
611  RevisionRecord $rev,
612  Title $title,
613  $parentId
614  ) {
615  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
616 
617  list( $commentFields, $commentCallback ) =
618  $this->commentStore->insertWithTempTable(
619  $dbw,
620  'rev_comment',
622  );
623  $revisionRow += $commentFields;
624 
625  list( $actorFields, $actorCallback ) =
626  $this->actorMigration->getInsertValuesWithTempTable(
627  $dbw,
628  'rev_user',
630  );
631  $revisionRow += $actorFields;
632 
633  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
634 
635  if ( !isset( $revisionRow['rev_id'] ) ) {
636  // only if auto-increment was used
637  $revisionRow['rev_id'] = intval( $dbw->insertId() );
638 
639  if ( $dbw->getType() === 'mysql' ) {
640  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
641  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
642  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
643 
644  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
645  $table = 'archive';
646  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
647  if ( $maxRevId2 >= $maxRevId ) {
648  $maxRevId = $maxRevId2;
649  $table = 'slots';
650  }
651 
652  if ( $maxRevId >= $revisionRow['rev_id'] ) {
653  $this->logger->debug(
654  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
655  . ' Trying to fix it.',
656  [
657  'revid' => $revisionRow['rev_id'],
658  'table' => $table,
659  'maxrevid' => $maxRevId,
660  ]
661  );
662 
663  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
664  throw new MWException( 'Failed to get database lock for T202032' );
665  }
666  $fname = __METHOD__;
667  $dbw->onTransactionResolution(
668  function ( $trigger, IDatabase $dbw ) use ( $fname ) {
669  $dbw->unlock( 'fix-for-T202032', $fname );
670  }
671  );
672 
673  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
674 
675  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
676  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
677  // inserts too, though, at least on MariaDB 10.1.29.
678  //
679  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
680  // transactions in this code path thanks to the row lock from the original ->insert() above.
681  //
682  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
683  // that's for non-MySQL DBs.
684  $row1 = $dbw->query(
685  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
686  )->fetchObject();
687 
688  $row2 = $dbw->query(
689  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
690  . ' FOR UPDATE'
691  )->fetchObject();
692 
693  $maxRevId = max(
694  $maxRevId,
695  $row1 ? intval( $row1->v ) : 0,
696  $row2 ? intval( $row2->v ) : 0
697  );
698 
699  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
700  // transactions will throw a duplicate key error here. It doesn't seem worth trying
701  // to avoid that.
702  $revisionRow['rev_id'] = $maxRevId + 1;
703  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
704  }
705  }
706  }
707 
708  $commentCallback( $revisionRow['rev_id'] );
709  $actorCallback( $revisionRow['rev_id'], $revisionRow );
710 
711  return $revisionRow;
712  }
713 
724  private function getBaseRevisionRow(
725  IDatabase $dbw,
726  RevisionRecord $rev,
727  Title $title,
728  $parentId
729  ) {
730  // Record the edit in revisions
731  $revisionRow = [
732  'rev_page' => $rev->getPageId(),
733  'rev_parent_id' => $parentId,
734  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
735  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
736  'rev_deleted' => $rev->getVisibility(),
737  'rev_len' => $rev->getSize(),
738  'rev_sha1' => $rev->getSha1(),
739  ];
740 
741  if ( $rev->getId() !== null ) {
742  // Needed to restore revisions with their original ID
743  $revisionRow['rev_id'] = $rev->getId();
744  }
745 
746  return $revisionRow;
747  }
748 
757  private function storeContentBlob(
758  SlotRecord $slot,
759  Title $title,
760  array $blobHints = []
761  ) {
762  $content = $slot->getContent();
763  $format = $content->getDefaultFormat();
764  $model = $content->getModel();
765 
766  $this->checkContent( $content, $title, $slot->getRole() );
767 
768  return $this->blobStore->storeBlob(
769  $content->serialize( $format ),
770  // These hints "leak" some information from the higher abstraction layer to
771  // low level storage to allow for optimization.
772  array_merge(
773  $blobHints,
774  [
775  BlobStore::DESIGNATION_HINT => 'page-content',
776  BlobStore::ROLE_HINT => $slot->getRole(),
777  BlobStore::SHA1_HINT => $slot->getSha1(),
778  BlobStore::MODEL_HINT => $model,
779  BlobStore::FORMAT_HINT => $format,
780  ]
781  )
782  );
783  }
784 
791  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
792  $slotRow = [
793  'slot_revision_id' => $revisionId,
794  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
795  'slot_content_id' => $contentId,
796  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
797  // that we just inserted.
798  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
799  ];
800  $dbw->insert( 'slots', $slotRow, __METHOD__ );
801  }
802 
809  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
810  $contentRow = [
811  'content_size' => $slot->getSize(),
812  'content_sha1' => $slot->getSha1(),
813  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
814  'content_address' => $blobAddress,
815  ];
816  $dbw->insert( 'content', $contentRow, __METHOD__ );
817  return intval( $dbw->insertId() );
818  }
819 
830  private function checkContent( Content $content, Title $title, $role ) {
831  // Note: may return null for revisions that have not yet been inserted
832 
833  $model = $content->getModel();
834  $format = $content->getDefaultFormat();
835  $handler = $content->getContentHandler();
836 
837  $name = "$title";
838 
839  if ( !$handler->isSupportedFormat( $format ) ) {
840  throw new MWException( "Can't use format $format with content model $model on $name" );
841  }
842 
843  if ( !$content->isValid() ) {
844  throw new MWException(
845  "New content for $name is not valid! Content model is $model"
846  );
847  }
848  }
849 
875  public function newNullRevision(
876  IDatabase $dbw,
877  Title $title,
878  CommentStoreComment $comment,
879  $minor,
880  User $user
881  ) {
882  $this->checkDatabaseDomain( $dbw );
883 
884  $pageId = $title->getArticleID();
885 
886  // T51581: Lock the page table row to ensure no other process
887  // is adding a revision to the page at the same time.
888  // Avoid locking extra tables, compare T191892.
889  $pageLatest = $dbw->selectField(
890  'page',
891  'page_latest',
892  [ 'page_id' => $pageId ],
893  __METHOD__,
894  [ 'FOR UPDATE' ]
895  );
896 
897  if ( !$pageLatest ) {
898  return null;
899  }
900 
901  // Fetch the actual revision row from master, without locking all extra tables.
902  $oldRevision = $this->loadRevisionFromConds(
903  $dbw,
904  [ 'rev_id' => intval( $pageLatest ) ],
905  self::READ_LATEST,
906  $title
907  );
908 
909  if ( !$oldRevision ) {
910  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
911  $this->logger->error(
912  $msg,
913  [ 'exception' => new RuntimeException( $msg ) ]
914  );
915  return null;
916  }
917 
918  // Construct the new revision
919  $timestamp = MWTimestamp::now( TS_MW );
920  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
921 
922  $newRevision->setComment( $comment );
923  $newRevision->setUser( $user );
924  $newRevision->setTimestamp( $timestamp );
925  $newRevision->setMinorEdit( $minor );
926 
927  return $newRevision;
928  }
929 
939  public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
940  $rc = $this->getRecentChange( $rev );
941  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
942  return $rc->getAttribute( 'rc_id' );
943  } else {
944  return 0;
945  }
946  }
947 
961  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
962  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
963 
965  [ 'rc_this_oldid' => $rev->getId() ],
966  __METHOD__,
967  $dbType
968  );
969 
970  // XXX: cache this locally? Glue it to the RevisionRecord?
971  return $rc;
972  }
973 
993  private function loadSlotContent(
994  SlotRecord $slot,
995  $blobData = null,
996  $blobFlags = null,
997  $blobFormat = null,
998  $queryFlags = 0
999  ) {
1000  if ( $blobData !== null ) {
1001  Assert::parameterType( 'string', $blobData, '$blobData' );
1002  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1003 
1004  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1005 
1006  if ( $blobFlags === null ) {
1007  // No blob flags, so use the blob verbatim.
1008  $data = $blobData;
1009  } else {
1010  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1011  if ( $data === false ) {
1012  throw new RevisionAccessException(
1013  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1014  );
1015  }
1016  }
1017 
1018  } else {
1019  $address = $slot->getAddress();
1020  try {
1021  $data = $this->blobStore->getBlob( $address, $queryFlags );
1022  } catch ( BlobAccessException $e ) {
1023  throw new RevisionAccessException(
1024  "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1025  );
1026  }
1027  }
1028 
1029  return $this->contentHandlerFactory
1030  ->getContentHandler( $slot->getModel() )
1031  ->unserializeContent( $data, $blobFormat );
1032  }
1033 
1048  public function getRevisionById( $id, $flags = 0 ) {
1049  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1050  }
1051 
1068  public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1069  $conds = [
1070  'page_namespace' => $linkTarget->getNamespace(),
1071  'page_title' => $linkTarget->getDBkey()
1072  ];
1073 
1074  // Only resolve to a Title when operating in the context of the local wiki (T248756)
1075  // TODO should not require Title in future (T206498)
1076  $title = $this->dbDomain === false ? Title::newFromLinkTarget( $linkTarget ) : null;
1077 
1078  if ( $revId ) {
1079  // Use the specified revision ID.
1080  // Note that we use newRevisionFromConds here because we want to retry
1081  // and fall back to master if the page is not found on a replica.
1082  // Since the caller supplied a revision ID, we are pretty sure the revision is
1083  // supposed to exist, so we should try hard to find it.
1084  $conds['rev_id'] = $revId;
1085  return $this->newRevisionFromConds( $conds, $flags, $title );
1086  } else {
1087  // Use a join to get the latest revision.
1088  // Note that we don't use newRevisionFromConds here because we don't want to retry
1089  // and fall back to master. The assumption is that we only want to force the fallback
1090  // if we are quite sure the revision exists because the caller supplied a revision ID.
1091  // If the page isn't found at all on a replica, it probably simply does not exist.
1092  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1093 
1094  $conds[] = 'rev_id=page_latest';
1095  $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1096 
1097  return $rev;
1098  }
1099  }
1100 
1117  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1118  $conds = [ 'page_id' => $pageId ];
1119  if ( $revId ) {
1120  // Use the specified revision ID.
1121  // Note that we use newRevisionFromConds here because we want to retry
1122  // and fall back to master if the page is not found on a replica.
1123  // Since the caller supplied a revision ID, we are pretty sure the revision is
1124  // supposed to exist, so we should try hard to find it.
1125  $conds['rev_id'] = $revId;
1126  return $this->newRevisionFromConds( $conds, $flags );
1127  } else {
1128  // Use a join to get the latest revision.
1129  // Note that we don't use newRevisionFromConds here because we don't want to retry
1130  // and fall back to master. The assumption is that we only want to force the fallback
1131  // if we are quite sure the revision exists because the caller supplied a revision ID.
1132  // If the page isn't found at all on a replica, it probably simply does not exist.
1133  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1134 
1135  $conds[] = 'rev_id=page_latest';
1136  $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1137 
1138  return $rev;
1139  }
1140  }
1141 
1157  public function getRevisionByTimestamp(
1158  LinkTarget $title,
1159  string $timestamp,
1160  int $flags = IDBAccessObject::READ_NORMAL
1161  ): ?RevisionRecord {
1162  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1163  return $this->newRevisionFromConds(
1164  [
1165  'rev_timestamp' => $db->timestamp( $timestamp ),
1166  'page_namespace' => $title->getNamespace(),
1167  'page_title' => $title->getDBkey()
1168  ],
1169  $flags,
1171  );
1172  }
1173 
1181  private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
1182  $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1183 
1184  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1185  $db = $this->getDBConnectionRef( $dbMode );
1186 
1187  $res = $db->select(
1188  $revQuery['tables'],
1189  $revQuery['fields'],
1190  [
1191  'slot_revision_id' => $revId,
1192  ],
1193  __METHOD__,
1194  $dbOptions,
1195  $revQuery['joins']
1196  );
1197 
1198  $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
1199 
1200  return $slots;
1201  }
1202 
1215  private function constructSlotRecords(
1216  $revId,
1217  $slotRows,
1218  $queryFlags,
1219  Title $title,
1220  $slotContents = null
1221  ) {
1222  $slots = [];
1223 
1224  foreach ( $slotRows as $row ) {
1225  // Resolve role names and model names from in-memory cache, if they were not joined in.
1226  if ( !isset( $row->role_name ) ) {
1227  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1228  }
1229 
1230  if ( !isset( $row->model_name ) ) {
1231  if ( isset( $row->content_model ) ) {
1232  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1233  } else {
1234  // We may get here if $row->model_name is set but null, perhaps because it
1235  // came from rev_content_model, which is NULL for the default model.
1236  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1237  $row->model_name = $slotRoleHandler->getDefaultModel( $title );
1238  }
1239  }
1240 
1241  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1242  if ( isset( $row->blob_data ) ) {
1243  $slotContents[$row->content_address] = $row->blob_data;
1244  }
1245 
1246  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1247  $blob = null;
1248  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1249  $blob = $slotContents[$slot->getAddress()];
1250  if ( $blob instanceof Content ) {
1251  return $blob;
1252  }
1253  }
1254  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1255  };
1256 
1257  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1258  }
1259 
1260  if ( !$slots && !( $queryFlags & self::READ_LATEST ) ) {
1261  // If we found no slots, try looking on the master database (T212428, T252156)
1262  $this->logger->info(
1263  __METHOD__ . ' falling back to READ_LATEST.',
1264  [ 'trace' => wfBacktrace() ]
1265  );
1266  return $this->constructSlotRecords(
1267  $revId,
1268  $slotRows,
1269  $queryFlags | self::READ_LATEST,
1270  $title,
1271  $slotContents
1272  );
1273  }
1274 
1275  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1276  throw new RevisionAccessException(
1277  'Main slot of revision ' . $revId . ' not found in database!'
1278  );
1279  }
1280 
1281  return $slots;
1282  }
1283 
1299  private function newRevisionSlots(
1300  $revId,
1301  $revisionRow,
1302  $slotRows,
1303  $queryFlags,
1304  Title $title
1305  ) {
1306  if ( $slotRows ) {
1307  $slots = new RevisionSlots(
1308  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
1309  );
1310  } else {
1311  // XXX: do we need the same kind of caching here
1312  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1313 
1314  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
1315  return $this->loadSlotRecords( $revId, $queryFlags, $title );
1316  } );
1317  }
1318 
1319  return $slots;
1320  }
1321 
1339  public function newRevisionFromArchiveRow(
1340  $row,
1341  $queryFlags = 0,
1342  Title $title = null,
1343  array $overrides = []
1344  ) {
1345  return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $title, $overrides );
1346  }
1347 
1361  public function newRevisionFromRow(
1362  $row,
1363  $queryFlags = 0,
1364  Title $title = null,
1365  $fromCache = false
1366  ) {
1367  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
1368  }
1369 
1390  $row,
1391  $slots,
1392  $queryFlags = 0,
1393  Title $title = null,
1394  array $overrides = []
1395  ) {
1396  Assert::parameterType( 'object', $row, '$row' );
1397 
1398  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1399  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1400 
1401  if ( !$title && isset( $overrides['title'] ) ) {
1402  if ( !( $overrides['title'] instanceof Title ) ) {
1403  throw new MWException( 'title field override must contain a Title object.' );
1404  }
1405 
1406  $title = $overrides['title'];
1407  }
1408 
1409  if ( !isset( $title ) ) {
1410  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1411  $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1412  } else {
1413  throw new InvalidArgumentException(
1414  'A Title or ar_namespace and ar_title must be given'
1415  );
1416  }
1417  }
1418 
1419  foreach ( $overrides as $key => $value ) {
1420  $field = "ar_$key";
1421  $row->$field = $value;
1422  }
1423 
1424  try {
1425  $user = User::newFromAnyId(
1426  $row->ar_user ?? null,
1427  $row->ar_user_text ?? null,
1428  $row->ar_actor ?? null,
1429  $this->dbDomain
1430  );
1431  } catch ( InvalidArgumentException $ex ) {
1432  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1433  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1434  }
1435 
1436  if ( $user->getName() === '' ) {
1437  // T236624: If the user name is empty, force 'Unknown user',
1438  // even if the actor table has an entry for the empty user name.
1439  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1440  }
1441 
1442  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1443  // Legacy because $row may have come from self::selectFields()
1444  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1445 
1446  if ( !( $slots instanceof RevisionSlots ) ) {
1447  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $title );
1448  }
1449 
1450  return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
1451  }
1452 
1471  $row,
1472  $slots,
1473  $queryFlags = 0,
1474  Title $title = null,
1475  $fromCache = false
1476  ) {
1477  Assert::parameterType( 'object', $row, '$row' );
1478 
1479  if ( !$title ) {
1480  $pageId = (int)( $row->rev_page ?? 0 ); // XXX: fall back to page_id?
1481  $revId = (int)( $row->rev_id ?? 0 );
1482 
1483  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1484  } else {
1485  $this->ensureRevisionRowMatchesTitle( $row, $title );
1486  }
1487 
1488  if ( !isset( $row->page_latest ) ) {
1489  $row->page_latest = $title->getLatestRevID();
1490  if ( $row->page_latest === 0 && $title->exists() ) {
1491  wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1492  }
1493  }
1494 
1495  try {
1496  $user = User::newFromAnyId(
1497  $row->rev_user ?? null,
1498  $row->rev_user_text ?? null,
1499  $row->rev_actor ?? null,
1500  $this->dbDomain
1501  );
1502  } catch ( InvalidArgumentException $ex ) {
1503  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1504  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1505  }
1506 
1507  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1508  // Legacy because $row may have come from self::selectFields()
1509  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1510 
1511  if ( !( $slots instanceof RevisionSlots ) ) {
1512  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
1513  }
1514 
1515  // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1516  if ( $fromCache ) {
1517  $rev = new RevisionStoreCacheRecord(
1518  function ( $revId ) use ( $queryFlags ) {
1519  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1520  return $this->fetchRevisionRowFromConds(
1521  $db,
1522  [ 'rev_id' => intval( $revId ) ]
1523  );
1524  },
1525  $title, $user, $comment, $row, $slots, $this->dbDomain
1526  );
1527  } else {
1528  $rev = new RevisionStoreRecord(
1529  $title, $user, $comment, $row, $slots, $this->dbDomain );
1530  }
1531  return $rev;
1532  }
1533 
1543  private function ensureRevisionRowMatchesTitle( $row, Title $title, $context = [] ) {
1544  $revId = (int)( $row->rev_id ?? 0 );
1545  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1546  $titlePageId = $title->getArticleID();
1547 
1548  // Avoid fatal error when the Title's ID changed, T246720
1549  if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1550  $masterPageId = $title->getArticleID( Title::READ_LATEST );
1551  $masterLatest = $title->getLatestRevID( Title::READ_LATEST );
1552 
1553  if ( $revPageId === $masterPageId ) {
1554  $this->logger->warning(
1555  "Encountered stale Title object",
1556  [
1557  'page_id_stale' => $titlePageId,
1558  'page_id_reloaded' => $masterPageId,
1559  'page_latest' => $masterLatest,
1560  'rev_id' => $revId,
1561  'trace' => wfBacktrace()
1562  ] + $context
1563  );
1564  } else {
1565  throw new InvalidArgumentException(
1566  "Revision $revId belongs to page ID $revPageId, "
1567  . "the provided Title object belongs to page ID $masterPageId"
1568  );
1569  }
1570  }
1571  }
1572 
1598  public function newRevisionsFromBatch(
1599  $rows,
1600  array $options = [],
1601  $queryFlags = 0,
1602  Title $title = null
1603  ) {
1604  $result = new StatusValue();
1605  $archiveMode = $options['archive'] ?? false;
1606 
1607  if ( $archiveMode ) {
1608  $revIdField = 'ar_rev_id';
1609  } else {
1610  $revIdField = 'rev_id';
1611  }
1612 
1613  $rowsByRevId = [];
1614  $pageIdsToFetchTitles = [];
1615  $titlesByPageKey = [];
1616  foreach ( $rows as $row ) {
1617  if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1618  $result->warning(
1619  'internalerror_info',
1620  "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1621  );
1622  }
1623 
1624  // Attach a page key to the row, so we can find and reuse Title objects easily.
1625  $row->_page_key =
1626  $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1627 
1628  if ( $title ) {
1629  if ( !$archiveMode && $row->rev_page != $title->getArticleID() ) {
1630  throw new InvalidArgumentException(
1631  "Revision {$row->$revIdField} doesn't belong to page "
1632  . $title->getArticleID()
1633  );
1634  }
1635 
1636  if ( $archiveMode
1637  && ( $row->ar_namespace != $title->getNamespace()
1638  || $row->ar_title !== $title->getDBkey() )
1639  ) {
1640  throw new InvalidArgumentException(
1641  "Revision {$row->$revIdField} doesn't belong to page "
1642  . $title->getPrefixedDBkey()
1643  );
1644  }
1645  } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1646  if ( isset( $row->page_namespace ) && isset( $row->page_title )
1647  // This should always be true, but just in case we don't have a page_id
1648  // set or it doesn't match rev_page, let's fetch the title again.
1649  && isset( $row->page_id ) && isset( $row->rev_page )
1650  && $row->rev_page === $row->page_id
1651  ) {
1652  $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1653  } elseif ( $archiveMode ) {
1654  // Can't look up deleted pages by ID, but we have namespace and title
1655  $titlesByPageKey[ $row->_page_key ] =
1656  Title::makeTitle( $row->ar_namespace, $row->ar_title );
1657  } else {
1658  $pageIdsToFetchTitles[] = $row->rev_page;
1659  }
1660  }
1661  $rowsByRevId[$row->$revIdField] = $row;
1662  }
1663 
1664  if ( empty( $rowsByRevId ) ) {
1665  $result->setResult( true, [] );
1666  return $result;
1667  }
1668 
1669  // If the title is not supplied, batch-fetch Title objects.
1670  if ( $title ) {
1671  // same logic as for $row->_page_key above
1672  $pageKey = $archiveMode
1673  ? $title->getNamespace() . ':' . $title->getDBkey()
1674  : $title->getArticleID();
1675 
1676  $titlesByPageKey[$pageKey] = $title;
1677  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1678  // Note: when we fetch titles by ID, the page key is also the ID.
1679  // We should never get here if $archiveMode is true.
1680  Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1681 
1682  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1683  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1684  $titlesByPageKey[$t->getArticleID()] = $t;
1685  }
1686  }
1687 
1688  // which method to use for creating RevisionRecords
1689  $newRevisionRecord = [
1690  $this,
1691  $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1692  ];
1693 
1694  if ( !isset( $options['slots'] ) ) {
1695  $result->setResult(
1696  true,
1697  array_map(
1698  function ( $row )
1699  use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1700  try {
1701  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1702  $result->warning(
1703  'internalerror_info',
1704  "Couldn't find title for rev {$row->$revIdField} "
1705  . "(page key {$row->_page_key})"
1706  );
1707  return null;
1708  }
1709  return $newRevisionRecord( $row, null, $queryFlags,
1710  $titlesByPageKey[ $row->_page_key ] );
1711  } catch ( MWException $e ) {
1712  $result->warning( 'internalerror_info', $e->getMessage() );
1713  return null;
1714  }
1715  },
1716  $rowsByRevId
1717  )
1718  );
1719  return $result;
1720  }
1721 
1722  $slotRowOptions = [
1723  'slots' => $options['slots'] ?? true,
1724  'blobs' => $options['content'] ?? false,
1725  ];
1726 
1727  if ( is_array( $slotRowOptions['slots'] )
1728  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
1729  ) {
1730  // Make sure the main slot is always loaded, RevisionRecord requires this.
1731  $slotRowOptions['slots'][] = SlotRecord::MAIN;
1732  }
1733 
1734  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
1735 
1736  $result->merge( $slotRowsStatus );
1737  $slotRowsByRevId = $slotRowsStatus->getValue();
1738 
1739  $result->setResult(
1740  true,
1741  array_map(
1742  function ( $row )
1743  use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
1744  $revIdField, $newRevisionRecord
1745  ) {
1746  if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
1747  $result->warning(
1748  'internalerror_info',
1749  "Couldn't find slots for rev {$row->$revIdField}"
1750  );
1751  return null;
1752  }
1753  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1754  $result->warning(
1755  'internalerror_info',
1756  "Couldn't find title for rev {$row->$revIdField} "
1757  . "(page key {$row->_page_key})"
1758  );
1759  return null;
1760  }
1761  try {
1762  return $newRevisionRecord(
1763  $row,
1764  new RevisionSlots(
1765  $this->constructSlotRecords(
1766  $row->$revIdField,
1767  $slotRowsByRevId[$row->$revIdField],
1768  $queryFlags,
1769  $titlesByPageKey[$row->_page_key]
1770  )
1771  ),
1772  $queryFlags,
1773  $titlesByPageKey[$row->_page_key]
1774  );
1775  } catch ( MWException $e ) {
1776  $result->warning( 'internalerror_info', $e->getMessage() );
1777  return null;
1778  }
1779  },
1780  $rowsByRevId
1781  )
1782  );
1783  return $result;
1784  }
1785 
1809  private function getSlotRowsForBatch(
1810  $rowsOrIds,
1811  array $options = [],
1812  $queryFlags = 0
1813  ) {
1814  $result = new StatusValue();
1815 
1816  $revIds = [];
1817  foreach ( $rowsOrIds as $row ) {
1818  if ( is_object( $row ) ) {
1819  $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
1820  } else {
1821  $revIds[] = (int)$row;
1822  }
1823  }
1824 
1825  // Nothing to do.
1826  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
1827  if ( empty( $revIds ) ) {
1828  $result->setResult( true, [] );
1829  return $result;
1830  }
1831 
1832  // We need to set the `content` flag to join in content meta-data
1833  $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
1834  $revIdField = $slotQueryInfo['keys']['rev_id'];
1835  $slotQueryConds = [ $revIdField => $revIds ];
1836 
1837  if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
1838  if ( empty( $options['slots'] ) ) {
1839  // Degenerate case: return no slots for each revision.
1840  $result->setResult( true, array_fill_keys( $revIds, [] ) );
1841  return $result;
1842  }
1843 
1844  $roleIdField = $slotQueryInfo['keys']['role_id'];
1845  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
1846  return $this->slotRoleStore->getId( $slot_name );
1847  }, $options['slots'] );
1848  }
1849 
1850  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1851  $slotRows = $db->select(
1852  $slotQueryInfo['tables'],
1853  $slotQueryInfo['fields'],
1854  $slotQueryConds,
1855  __METHOD__,
1856  [],
1857  $slotQueryInfo['joins']
1858  );
1859 
1860  $slotContents = null;
1861  if ( $options['blobs'] ?? false ) {
1862  $blobAddresses = [];
1863  foreach ( $slotRows as $slotRow ) {
1864  $blobAddresses[] = $slotRow->content_address;
1865  }
1866  $slotContentFetchStatus = $this->blobStore
1867  ->getBlobBatch( $blobAddresses, $queryFlags );
1868  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
1869  $result->warning( $error['message'], ...$error['params'] );
1870  }
1871  $slotContents = $slotContentFetchStatus->getValue();
1872  }
1873 
1874  $slotRowsByRevId = [];
1875  foreach ( $slotRows as $slotRow ) {
1876  if ( $slotContents === null ) {
1877  // nothing to do
1878  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
1879  $slotRow->blob_data = $slotContents[$slotRow->content_address];
1880  } else {
1881  $result->warning(
1882  'internalerror_info',
1883  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
1884  );
1885  $slotRow->blob_data = null;
1886  }
1887 
1888  // conditional needed for SCHEMA_COMPAT_READ_OLD
1889  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
1890  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
1891  }
1892 
1893  // conditional needed for SCHEMA_COMPAT_READ_OLD
1894  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
1895  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
1896  }
1897 
1898  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
1899  }
1900 
1901  $result->setResult( true, $slotRowsByRevId );
1902  return $result;
1903  }
1904 
1925  public function getContentBlobsForBatch(
1926  $rowsOrIds,
1927  $slots = null,
1928  $queryFlags = 0
1929  ) {
1930  $result = $this->getSlotRowsForBatch(
1931  $rowsOrIds,
1932  [ 'slots' => $slots, 'blobs' => true ],
1933  $queryFlags
1934  );
1935 
1936  if ( $result->isOK() ) {
1937  // strip out all internal meta data that we don't want to expose
1938  foreach ( $result->value as $revId => $rowsByRole ) {
1939  foreach ( $rowsByRole as $role => $slotRow ) {
1940  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
1941  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
1942  // if we didn't ask for it.
1943  unset( $result->value[$revId][$role] );
1944  continue;
1945  }
1946 
1947  $result->value[$revId][$role] = (object)[
1948  'blob_data' => $slotRow->blob_data,
1949  'model_name' => $slotRow->model_name,
1950  ];
1951  }
1952  }
1953  }
1954 
1955  return $result;
1956  }
1957 
1973  array $fields,
1974  $queryFlags = 0,
1975  Title $title = null
1976  ) {
1977  if ( !$title && isset( $fields['title'] ) ) {
1978  if ( !( $fields['title'] instanceof Title ) ) {
1979  throw new MWException( 'title field must contain a Title object.' );
1980  }
1981 
1982  $title = $fields['title'];
1983  }
1984 
1985  if ( !$title ) {
1986  $pageId = $fields['page'] ?? 0;
1987  $revId = $fields['id'] ?? 0;
1988 
1989  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1990  }
1991 
1992  if ( !isset( $fields['page'] ) ) {
1993  $fields['page'] = $title->getArticleID( $queryFlags );
1994  }
1995 
1996  // if we have a content object, use it to set the model and type
1997  if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
1998  && !is_array( $fields['content'] )
1999  ) {
2000  throw new MWException(
2001  'content field must contain a Content object or an array of Content objects.'
2002  );
2003  }
2004 
2005  if ( !empty( $fields['text_id'] ) ) {
2006  throw new MWException( 'The text_id field can not be used in MediaWiki 1.35 and later' );
2007  }
2008 
2009  if (
2010  isset( $fields['comment'] )
2011  && !( $fields['comment'] instanceof CommentStoreComment )
2012  ) {
2013  $commentData = $fields['comment_data'] ?? null;
2014 
2015  if ( $fields['comment'] instanceof Message ) {
2016  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2017  $fields['comment'],
2018  $commentData
2019  );
2020  } else {
2021  $commentText = trim( strval( $fields['comment'] ) );
2022  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2023  $commentText,
2024  $commentData
2025  );
2026  }
2027  }
2028 
2029  $revision = new MutableRevisionRecord( $title, $this->dbDomain );
2030 
2032  if ( isset( $fields['content'] ) ) {
2033  if ( is_array( $fields['content'] ) ) {
2034  $slotContent = $fields['content'];
2035  } else {
2036  $slotContent = [ SlotRecord::MAIN => $fields['content'] ];
2037  }
2038  } elseif ( isset( $fields['text'] ) ) {
2039  if ( isset( $fields['content_model'] ) ) {
2040  $model = $fields['content_model'];
2041  } else {
2042  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( SlotRecord::MAIN );
2043  $model = $slotRoleHandler->getDefaultModel( $title );
2044  }
2045 
2046  $contentHandler = ContentHandler::getForModelID( $model );
2047  $content = $contentHandler->unserializeContent( $fields['text'] );
2048  $slotContent = [ SlotRecord::MAIN => $content ];
2049  } else {
2050  $slotContent = [];
2051  }
2052 
2053  foreach ( $slotContent as $role => $content ) {
2054  $revision->setContent( $role, $content );
2055  }
2056 
2057  $this->initializeMutableRevisionFromArray( $revision, $fields );
2058 
2059  return $revision;
2060  }
2061 
2067  MutableRevisionRecord $record,
2068  array $fields
2069  ) {
2071  $user = null;
2072 
2073  // If a user is passed in, use it if possible. We cannot use a user from a
2074  // remote wiki with unsuppressed ids, due to issues described in T222212.
2075  if ( isset( $fields['user'] ) &&
2076  ( $fields['user'] instanceof UserIdentity ) &&
2077  ( $this->dbDomain === false ||
2078  ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
2079  ) {
2080  $user = $fields['user'];
2081  } else {
2082  $userID = isset( $fields['user'] ) && is_numeric( $fields['user'] ) ? $fields['user'] : null;
2083  try {
2084  $user = User::newFromAnyId(
2085  $userID,
2086  $fields['user_text'] ?? null,
2087  $fields['actor'] ?? null,
2088  $this->dbDomain
2089  );
2090  } catch ( InvalidArgumentException $ex ) {
2091  $user = null;
2092  }
2093  }
2094 
2095  if ( $user ) {
2096  $record->setUser( $user );
2097  }
2098 
2099  $timestamp = isset( $fields['timestamp'] )
2100  ? strval( $fields['timestamp'] )
2101  : MWTimestamp::now( TS_MW );
2102 
2103  $record->setTimestamp( $timestamp );
2104 
2105  if ( isset( $fields['page'] ) ) {
2106  $record->setPageId( intval( $fields['page'] ) );
2107  }
2108 
2109  if ( isset( $fields['id'] ) ) {
2110  $record->setId( intval( $fields['id'] ) );
2111  }
2112  if ( isset( $fields['parent_id'] ) ) {
2113  $record->setParentId( intval( $fields['parent_id'] ) );
2114  }
2115 
2116  if ( isset( $fields['sha1'] ) ) {
2117  $record->setSha1( $fields['sha1'] );
2118  }
2119 
2120  if ( isset( $fields['size'] ) ) {
2121  $record->setSize( intval( $fields['size'] ) );
2122  } elseif ( isset( $fields['len'] ) ) {
2123  $record->setSize( intval( $fields['len'] ) );
2124  }
2125 
2126  if ( isset( $fields['minor_edit'] ) ) {
2127  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2128  }
2129  if ( isset( $fields['deleted'] ) ) {
2130  $record->setVisibility( intval( $fields['deleted'] ) );
2131  }
2132 
2133  if ( isset( $fields['comment'] ) ) {
2134  Assert::parameterType(
2135  CommentStoreComment::class,
2136  $fields['comment'],
2137  '$row[\'comment\']'
2138  );
2139  $record->setComment( $fields['comment'] );
2140  }
2141  }
2142 
2157  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2158  wfDeprecated( __METHOD__, '1.35' );
2159  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2160  if ( $id ) {
2161  $conds['rev_id'] = intval( $id );
2162  } else {
2163  $conds[] = 'rev_id=page_latest';
2164  }
2165  return $this->loadRevisionFromConds( $db, $conds );
2166  }
2167 
2185  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2186  wfDeprecated( __METHOD__, '1.35' );
2187  if ( $id ) {
2188  $matchId = intval( $id );
2189  } else {
2190  $matchId = 'page_latest';
2191  }
2192 
2193  return $this->loadRevisionFromConds(
2194  $db,
2195  [
2196  "rev_id=$matchId",
2197  'page_namespace' => $title->getNamespace(),
2198  'page_title' => $title->getDBkey()
2199  ],
2200  0,
2201  $title
2202  );
2203  }
2204 
2219  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2220  wfDeprecated( __METHOD__, '1.35' );
2221  return $this->loadRevisionFromConds( $db,
2222  [
2223  'rev_timestamp' => $db->timestamp( $timestamp ),
2224  'page_namespace' => $title->getNamespace(),
2225  'page_title' => $title->getDBkey()
2226  ],
2227  0,
2228  $title
2229  );
2230  }
2231 
2248  private function newRevisionFromConds(
2249  array $conditions,
2250  int $flags = IDBAccessObject::READ_NORMAL,
2251  Title $title = null,
2252  array $options = []
2253  ) {
2254  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2255  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title, $options );
2256 
2257  $lb = $this->getDBLoadBalancer();
2258 
2259  // Make sure new pending/committed revision are visibile later on
2260  // within web requests to certain avoid bugs like T93866 and T94407.
2261  if ( !$rev
2262  && !( $flags & self::READ_LATEST )
2263  && $lb->hasStreamingReplicaServers()
2264  && $lb->hasOrMadeRecentMasterChanges()
2265  ) {
2266  $flags = self::READ_LATEST;
2267  $dbw = $this->getDBConnectionRef( DB_MASTER );
2268  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title, $options );
2269  }
2270 
2271  return $rev;
2272  }
2273 
2288  private function loadRevisionFromConds(
2289  IDatabase $db,
2290  array $conditions,
2291  int $flags = IDBAccessObject::READ_NORMAL,
2292  Title $title = null,
2293  array $options = []
2294  ) {
2295  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2296  if ( $row ) {
2297  $rev = $this->newRevisionFromRow( $row, $flags, $title );
2298 
2299  return $rev;
2300  }
2301 
2302  return null;
2303  }
2304 
2312  private function checkDatabaseDomain( IDatabase $db ) {
2313  $dbDomain = $db->getDomainID();
2314  $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2315  if ( $dbDomain === $storeDomain ) {
2316  return;
2317  }
2318 
2319  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2320  }
2321 
2335  private function fetchRevisionRowFromConds(
2336  IDatabase $db,
2337  array $conditions,
2338  int $flags = IDBAccessObject::READ_NORMAL,
2339  array $options = []
2340  ) {
2341  $this->checkDatabaseDomain( $db );
2342 
2343  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2344  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2345  $options[] = 'FOR UPDATE';
2346  }
2347  return $db->selectRow(
2348  $revQuery['tables'],
2349  $revQuery['fields'],
2350  $conditions,
2351  __METHOD__,
2352  $options,
2353  $revQuery['joins']
2354  );
2355  }
2356 
2378  public function getQueryInfo( $options = [] ) {
2379  $ret = [
2380  'tables' => [],
2381  'fields' => [],
2382  'joins' => [],
2383  ];
2384 
2385  $ret['tables'][] = 'revision';
2386  $ret['fields'] = array_merge( $ret['fields'], [
2387  'rev_id',
2388  'rev_page',
2389  'rev_timestamp',
2390  'rev_minor_edit',
2391  'rev_deleted',
2392  'rev_len',
2393  'rev_parent_id',
2394  'rev_sha1',
2395  ] );
2396 
2397  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2398  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2399  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2400  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2401 
2402  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2403  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2404  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2405  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2406 
2407  if ( in_array( 'page', $options, true ) ) {
2408  $ret['tables'][] = 'page';
2409  $ret['fields'] = array_merge( $ret['fields'], [
2410  'page_namespace',
2411  'page_title',
2412  'page_id',
2413  'page_latest',
2414  'page_is_redirect',
2415  'page_len',
2416  ] );
2417  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2418  }
2419 
2420  if ( in_array( 'user', $options, true ) ) {
2421  $ret['tables'][] = 'user';
2422  $ret['fields'] = array_merge( $ret['fields'], [
2423  'user_name',
2424  ] );
2425  $u = $actorQuery['fields']['rev_user'];
2426  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2427  }
2428 
2429  if ( in_array( 'text', $options, true ) ) {
2430  throw new InvalidArgumentException(
2431  'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2432  );
2433  }
2434 
2435  return $ret;
2436  }
2437 
2458  public function getSlotsQueryInfo( $options = [] ) {
2459  $ret = [
2460  'tables' => [],
2461  'fields' => [],
2462  'joins' => [],
2463  'keys' => [],
2464  ];
2465 
2466  $ret['keys']['rev_id'] = 'slot_revision_id';
2467  $ret['keys']['role_id'] = 'slot_role_id';
2468 
2469  $ret['tables'][] = 'slots';
2470  $ret['fields'] = array_merge( $ret['fields'], [
2471  'slot_revision_id',
2472  'slot_content_id',
2473  'slot_origin',
2474  'slot_role_id',
2475  ] );
2476 
2477  if ( in_array( 'role', $options, true ) ) {
2478  // Use left join to attach role name, so we still find the revision row even
2479  // if the role name is missing. This triggers a more obvious failure mode.
2480  $ret['tables'][] = 'slot_roles';
2481  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2482  $ret['fields'][] = 'role_name';
2483  }
2484 
2485  if ( in_array( 'content', $options, true ) ) {
2486  $ret['keys']['model_id'] = 'content_model';
2487 
2488  $ret['tables'][] = 'content';
2489  $ret['fields'] = array_merge( $ret['fields'], [
2490  'content_size',
2491  'content_sha1',
2492  'content_address',
2493  'content_model',
2494  ] );
2495  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2496 
2497  if ( in_array( 'model', $options, true ) ) {
2498  // Use left join to attach model name, so we still find the revision row even
2499  // if the model name is missing. This triggers a more obvious failure mode.
2500  $ret['tables'][] = 'content_models';
2501  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2502  $ret['fields'][] = 'model_name';
2503  }
2504 
2505  }
2506 
2507  return $ret;
2508  }
2509 
2523  public function getArchiveQueryInfo() {
2524  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2525  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2526  $ret = [
2527  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2528  'fields' => [
2529  'ar_id',
2530  'ar_page_id',
2531  'ar_namespace',
2532  'ar_title',
2533  'ar_rev_id',
2534  'ar_timestamp',
2535  'ar_minor_edit',
2536  'ar_deleted',
2537  'ar_len',
2538  'ar_parent_id',
2539  'ar_sha1',
2540  ] + $commentQuery['fields'] + $actorQuery['fields'],
2541  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2542  ];
2543 
2544  return $ret;
2545  }
2546 
2556  public function getRevisionSizes( array $revIds ) {
2557  $dbr = $this->getDBConnectionRef( DB_REPLICA );
2558  $revLens = [];
2559  if ( !$revIds ) {
2560  return $revLens; // empty
2561  }
2562 
2563  $res = $dbr->select(
2564  'revision',
2565  [ 'rev_id', 'rev_len' ],
2566  [ 'rev_id' => $revIds ],
2567  __METHOD__
2568  );
2569 
2570  foreach ( $res as $row ) {
2571  $revLens[$row->rev_id] = intval( $row->rev_len );
2572  }
2573 
2574  return $revLens;
2575  }
2576 
2589  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2590  wfDeprecated( __METHOD__, '1.35' );
2591  return $this->getRevisionSizes( $revIds );
2592  }
2593 
2602  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2603  $op = $dir === 'next' ? '>' : '<';
2604  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2605 
2606  if ( !$rev->getId() || !$rev->getPageId() ) {
2607  // revision is unsaved or otherwise incomplete
2608  return null;
2609  }
2610 
2611  if ( $rev instanceof RevisionArchiveRecord ) {
2612  // revision is deleted, so it's not part of the page history
2613  return null;
2614  }
2615 
2616  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2617  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2618 
2619  $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2620  if ( $ts === false ) {
2621  // XXX Should this be moved into getTimestampFromId?
2622  $ts = $db->selectField( 'archive', 'ar_timestamp',
2623  [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2624  if ( $ts === false ) {
2625  // XXX Is this reachable? How can we have a page id but no timestamp?
2626  return null;
2627  }
2628  }
2629  $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2630 
2631  $revId = $db->selectField( 'revision', 'rev_id',
2632  [
2633  'rev_page' => $rev->getPageId(),
2634  "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op {$rev->getId()})"
2635  ],
2636  __METHOD__,
2637  [
2638  'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2639  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2640  ]
2641  );
2642 
2643  if ( $revId === false ) {
2644  return null;
2645  }
2646 
2647  return $this->getRevisionById( intval( $revId ) );
2648  }
2649 
2665  public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
2666  if ( $flags instanceof Title ) {
2667  // Old calling convention, we don't use Title here anymore
2668  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2669  $flags = 0;
2670  }
2671 
2672  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2673  }
2674 
2688  public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
2689  if ( $flags instanceof Title ) {
2690  // Old calling convention, we don't use Title here anymore
2691  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2692  $flags = 0;
2693  }
2694 
2695  return $this->getRelativeRevision( $rev, $flags, 'next' );
2696  }
2697 
2709  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2710  $this->checkDatabaseDomain( $db );
2711 
2712  if ( $rev->getPageId() === null ) {
2713  return 0;
2714  }
2715  # Use page_latest if ID is not given
2716  if ( !$rev->getId() ) {
2717  $prevId = $db->selectField(
2718  'page', 'page_latest',
2719  [ 'page_id' => $rev->getPageId() ],
2720  __METHOD__
2721  );
2722  } else {
2723  $prevId = $db->selectField(
2724  'revision', 'rev_id',
2725  [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
2726  __METHOD__,
2727  [ 'ORDER BY' => 'rev_id DESC' ]
2728  );
2729  }
2730  return intval( $prevId );
2731  }
2732 
2745  public function getTimestampFromId( $id, $flags = 0 ) {
2746  if ( $id instanceof Title ) {
2747  // Old deprecated calling convention supported for backwards compatibility
2748  $id = $flags;
2749  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2750  }
2751  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2752 
2753  $timestamp =
2754  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2755 
2756  return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2757  }
2758 
2768  public function countRevisionsByPageId( IDatabase $db, $id ) {
2769  $this->checkDatabaseDomain( $db );
2770 
2771  $row = $db->selectRow( 'revision',
2772  [ 'revCount' => 'COUNT(*)' ],
2773  [ 'rev_page' => $id ],
2774  __METHOD__
2775  );
2776  if ( $row ) {
2777  return intval( $row->revCount );
2778  }
2779  return 0;
2780  }
2781 
2791  public function countRevisionsByTitle( IDatabase $db, $title ) {
2792  $id = $title->getArticleID();
2793  if ( $id ) {
2794  return $this->countRevisionsByPageId( $db, $id );
2795  }
2796  return 0;
2797  }
2798 
2817  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2818  $this->checkDatabaseDomain( $db );
2819 
2820  if ( !$userId ) {
2821  return false;
2822  }
2823 
2824  $revQuery = $this->getQueryInfo();
2825  $res = $db->select(
2826  $revQuery['tables'],
2827  [
2828  'rev_user' => $revQuery['fields']['rev_user'],
2829  ],
2830  [
2831  'rev_page' => $pageId,
2832  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2833  ],
2834  __METHOD__,
2835  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2836  $revQuery['joins']
2837  );
2838  foreach ( $res as $row ) {
2839  if ( $row->rev_user != $userId ) {
2840  return false;
2841  }
2842  }
2843  return true;
2844  }
2845 
2859  public function getKnownCurrentRevision( Title $title, $revId = 0 ) {
2860  $db = $this->getDBConnectionRef( DB_REPLICA );
2861 
2862  $revIdPassed = $revId;
2863  $pageId = $title->getArticleID();
2864 
2865  if ( !$pageId ) {
2866  return false;
2867  }
2868 
2869  if ( !$revId ) {
2870  $revId = $title->getLatestRevID();
2871  }
2872 
2873  if ( !$revId ) {
2874  wfWarn(
2875  'No latest revision known for page ' . $title->getPrefixedDBkey()
2876  . ' even though it exists with page ID ' . $pageId
2877  );
2878  return false;
2879  }
2880 
2881  // Load the row from cache if possible. If not possible, populate the cache.
2882  // As a minor optimization, remember if this was a cache hit or miss.
2883  // We can sometimes avoid a database query later if this is a cache miss.
2884  $fromCache = true;
2885  $row = $this->cache->getWithSetCallback(
2886  // Page/rev IDs passed in from DB to reflect history merges
2887  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2888  WANObjectCache::TTL_WEEK,
2889  function ( $curValue, &$ttl, array &$setOpts ) use (
2890  $db, $revId, &$fromCache
2891  ) {
2892  $setOpts += Database::getCacheSetOptions( $db );
2893  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2894  if ( $row ) {
2895  $fromCache = false;
2896  }
2897  return $row; // don't cache negatives
2898  }
2899  );
2900 
2901  // Reflect revision deletion and user renames.
2902  if ( $row ) {
2903  $this->ensureRevisionRowMatchesTitle( $row, $title, [
2904  'from_cache_flag' => $fromCache,
2905  'page_id_initial' => $pageId,
2906  'rev_id_used' => $revId,
2907  'rev_id_requested' => $revIdPassed,
2908  ] );
2909 
2910  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2911  } else {
2912  return false;
2913  }
2914  }
2915 
2924  public function getFirstRevision(
2925  LinkTarget $title,
2926  int $flags = IDBAccessObject::READ_NORMAL
2927  ): ?RevisionRecord {
2928  $titleObj = Title::newFromLinkTarget( $title ); // TODO: eventually we shouldn't need a title
2929  return $this->newRevisionFromConds(
2930  [
2931  'page_namespace' => $title->getNamespace(),
2932  'page_title' => $title->getDBkey()
2933  ],
2934  $flags,
2935  $titleObj,
2936  [
2937  'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
2938  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
2939  ]
2940  );
2941  }
2942 
2954  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2955  return $this->cache->makeGlobalKey(
2956  self::ROW_CACHE_KEY,
2957  $db->getDomainID(),
2958  $pageId,
2959  $revId
2960  );
2961  }
2962 
2970  private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
2971  if ( $rev ) {
2972  if ( $rev->getId() === null ) {
2973  throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
2974  }
2975  if ( $rev->getPageId() !== $pageId ) {
2976  throw new InvalidArgumentException(
2977  "Revision {$rev->getId()} doesn't belong to page {$pageId}"
2978  );
2979  }
2980  }
2981  }
2982 
2995  private function getRevisionLimitConditions(
2996  IDatabase $dbr,
2997  RevisionRecord $old = null,
2998  RevisionRecord $new = null,
2999  $options = []
3000  ) {
3001  $options = (array)$options;
3002  $oldCmp = '>';
3003  $newCmp = '<';
3004  if ( in_array( 'include_old', $options ) ) {
3005  $oldCmp = '>=';
3006  }
3007  if ( in_array( 'include_new', $options ) ) {
3008  $newCmp = '<=';
3009  }
3010  if ( in_array( 'include_both', $options ) ) {
3011  $oldCmp = '>=';
3012  $newCmp = '<=';
3013  }
3014 
3015  $conds = [];
3016  if ( $old ) {
3017  $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3018  $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId()}) " .
3019  "OR rev_timestamp > {$oldTs}";
3020  }
3021  if ( $new ) {
3022  $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3023  $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId()}) " .
3024  "OR rev_timestamp < {$newTs}";
3025  }
3026  return $conds;
3027  }
3028 
3050  public function getAuthorsBetween(
3051  $pageId,
3052  RevisionRecord $old = null,
3053  RevisionRecord $new = null,
3054  User $user = null,
3055  $max = null,
3056  $options = []
3057  ) {
3058  $this->assertRevisionParameter( 'old', $pageId, $old );
3059  $this->assertRevisionParameter( 'new', $pageId, $new );
3060  $options = (array)$options;
3061 
3062  // No DB query needed if old and new are the same revision.
3063  // Can't check for consecutive revisions with 'getParentId' for a similar
3064  // optimization as edge cases exist when there are revisions between
3065  //a revision and it's parent. See T185167 for more details.
3066  if ( $old && $new && $new->getId() === $old->getId() ) {
3067  if ( empty( $options ) ) {
3068  return [];
3069  } else {
3070  return $user ? [ $new->getUser( RevisionRecord::FOR_PUBLIC, $user ) ] : [ $new->getUser() ];
3071  }
3072  }
3073 
3074  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3075  $conds = array_merge(
3076  [
3077  'rev_page' => $pageId,
3078  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3079  ],
3080  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3081  );
3082 
3083  $queryOpts = [ 'DISTINCT' ];
3084  if ( $max !== null ) {
3085  $queryOpts['LIMIT'] = $max + 1;
3086  }
3087 
3088  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3089  return array_map( function ( $row ) {
3090  return new UserIdentityValue( (int)$row->rev_user, $row->rev_user_text, (int)$row->rev_actor );
3091  }, iterator_to_array( $dbr->select(
3092  array_merge( [ 'revision' ], $actorQuery['tables'] ),
3093  $actorQuery['fields'],
3094  $conds, __METHOD__,
3095  $queryOpts,
3096  $actorQuery['joins']
3097  ) ) );
3098  }
3099 
3121  public function countAuthorsBetween(
3122  $pageId,
3123  RevisionRecord $old = null,
3124  RevisionRecord $new = null,
3125  User $user = null,
3126  $max = null,
3127  $options = []
3128  ) {
3129  // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3130  // and creation of UserIdentity stuff.
3131  return count( $this->getAuthorsBetween( $pageId, $old, $new, $user, $max, $options ) );
3132  }
3133 
3154  public function countRevisionsBetween(
3155  $pageId,
3156  RevisionRecord $old = null,
3157  RevisionRecord $new = null,
3158  $max = null,
3159  $options = []
3160  ) {
3161  $this->assertRevisionParameter( 'old', $pageId, $old );
3162  $this->assertRevisionParameter( 'new', $pageId, $new );
3163 
3164  // No DB query needed if old and new are the same revision.
3165  // Can't check for consecutive revisions with 'getParentId' for a similar
3166  // optimization as edge cases exist when there are revisions between
3167  //a revision and it's parent. See T185167 for more details.
3168  if ( $old && $new && $new->getId() === $old->getId() ) {
3169  return 0;
3170  }
3171 
3172  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3173  $conds = array_merge(
3174  [
3175  'rev_page' => $pageId,
3176  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3177  ],
3178  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3179  );
3180  if ( $max !== null ) {
3181  return $dbr->selectRowCount( 'revision', '1',
3182  $conds,
3183  __METHOD__,
3184  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3185  );
3186  } else {
3187  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3188  }
3189  }
3190 
3191  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3192 }
3193 
3198 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Revision\MutableRevisionRecord\setMinorEdit
setMinorEdit( $minorEdit)
Definition: MutableRevisionRecord.php:237
Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:107
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:32
Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:117
Revision\RevisionStore\ensureRevisionRowMatchesTitle
ensureRevisionRowMatchesTitle( $row, Title $title, $context=[])
Check that the given row matches the given Title object.
Definition: RevisionStore.php:1543
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:32
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:57
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:267
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:49
Revision\RevisionStore\insertSlotOn
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:542
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:33
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:2312
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:32
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:42
Revision\RevisionStore\getRecentChange
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
Definition: RevisionStore.php:961
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:31
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:2108
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:1972
Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:210
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:2859
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:208
Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:332
Revision\MutableRevisionRecord\setSha1
setSha1( $sha1)
Set revision hash, for optimization.
Definition: MutableRevisionRecord.php:195
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:90
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:79
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:158
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:71
Revision\RevisionStore\initializeMutableRevisionFromArray
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
Definition: RevisionStore.php:2066
Revision\RevisionStoreCacheRecord
A cached RevisionStoreRecord.
Definition: RevisionStoreCacheRecord.php:38
Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, Title $title)
Definition: RevisionStore.php:1181
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:1809
Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2523
Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:54
Revision\MutableRevisionRecord\setPageId
setPageId( $pageId)
Definition: MutableRevisionRecord.php:272
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:442
CommentStore
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
Definition: CommentStore.php:31
Revision\RevisionStore\$cache
WANObjectCache $cache
Definition: RevisionStore.php:102
Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:2745
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:939
Revision\RevisionFactory
Service for constructing revision objects.
Definition: RevisionFactory.php:38
Revision\MutableRevisionRecord\setId
setId( $id)
Set the revision ID.
Definition: MutableRevisionRecord.php:254
Revision\RevisionStore\checkContent
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
Definition: RevisionStore.php:830
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
ActorMigration
This class handles the logic for the actor table migration.
Definition: ActorMigration.php:38
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:2970
Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2458
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:112
$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:582
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:221
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:2589
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:2817
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\Revision
Definition: FallbackSlotRoleHandler.php:23
Revision
Definition: Revision.php:39
Revision\RevisionStore\getRevisionLimitConditions
getRevisionLimitConditions(IDatabase $dbr, RevisionRecord $old=null, RevisionRecord $new=null, $options=[])
Converts revision limits to query conditions.
Definition: RevisionStore.php:2995
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:1598
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:993
Revision\RevisionStore\newRevisionFromArchiveRowAndSlots
newRevisionFromArchiveRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, array $overrides=[])
Definition: RevisionStore.php:1389
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:2378
MWException
MediaWiki exception.
Definition: MWException.php:26
Revision\RevisionRecord\getSize
getSize()
Returns the nominal size of this revision, in bogo-bytes.
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:265
Revision\RevisionRecord\getSha1
getSha1()
Returns the base36 sha1 of this revision.
Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:757
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
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:353
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:4703
Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1048
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:527
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:1117
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:2157
$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:219
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:1299
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:2709
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:409
Revision\RevisionStore\$dbDomain
bool string $dbDomain
Definition: RevisionStore.php:92
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:791
Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2556
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:595
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:616
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
Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:87
Revision\RevisionStore\getPreviousRevision
getPreviousRevision(RevisionRecord $rev, $flags=0)
Get the revision before $rev in the page's history, if any.
Definition: RevisionStore.php:2665
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
Revision\RevisionStore\setLogger
setLogger(LoggerInterface $logger)
Definition: RevisionStore.php:187
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:1215
Revision\RevisionStore\ROW_CACHE_KEY
const ROW_CACHE_KEY
Definition: RevisionStore.php:82
Revision\RevisionStore\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: RevisionStore.php:130
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:2924
$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:43
Revision\RevisionStore\getNextRevision
getNextRevision(RevisionRecord $rev, $flags=0)
Get the revision after $rev in the page's history, if any.
Definition: RevisionStore.php:2688
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:120
Revision\RevisionStore\$slotRoleStore
NameTableStore $slotRoleStore
Definition: RevisionStore.php:127
Revision\RevisionStore\getContentBlobsForBatch
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
Definition: RevisionStore.php:1925
Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2791
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:501
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:2602
Revision\RevisionStore\$hookRunner
HookRunner $hookRunner
Definition: RevisionStore.php:136
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:1068
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:284
MediaWiki\Storage\BlobStore
Service for loading and storing data blobs.
Definition: BlobStore.php:35
Content
Base interface for content objects.
Definition: Content.php:34
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:875
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:2335
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:809
Revision\RevisionStore\countRevisionsByPageId
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2768
Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
Definition: RevisionStore.php:465
Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:122
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:201
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:1339
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:80
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:39
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:160
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:1351
Revision\RevisionStore\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: RevisionStore.php:133
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:3050
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:2248
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:10
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:1051
Revision\MutableRevisionRecord\setTimestamp
setTimestamp( $timestamp)
Definition: MutableRevisionRecord.php:228
Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:315
Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:609
Revision\MutableRevisionRecord\setSize
setSize( $size)
Set nominal revision size, for optimization.
Definition: MutableRevisionRecord.php:210
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:23
$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:1470
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:2219
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:3121
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:55
Title\newFromID
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:476
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:2288
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2137
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:2954
CommentStoreComment
CommentStoreComment represents a comment stored by CommentStore.
Definition: CommentStoreComment.php:29
Revision\MutableRevisionRecord\setComment
setComment(CommentStoreComment $comment)
Definition: MutableRevisionRecord.php:182
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:1157
Revision\RevisionStore\getBaseRevisionRow
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:724
Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, Title $title=null, $fromCache=false)
Definition: RevisionStore.php:1361
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:2185
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:3154
Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:97
Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:194
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:240