MediaWiki  master
RevisionStore.php
Go to the documentation of this file.
1 <?php
27 namespace MediaWiki\Revision;
28 
29 use ActorMigration;
30 use CommentStore;
32 use Content;
34 use FallbackContent;
35 use IDBAccessObject;
36 use InvalidArgumentException;
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 
84  public const ORDER_OLDEST_TO_NEWEST = 'ASC';
85  public const ORDER_NEWEST_TO_OLDEST = 'DESC';
86 
87  // Constants for get(...)Between methods
88  public const INCLUDE_OLD = 'include_old';
89  public const INCLUDE_NEW = 'include_new';
90  public const INCLUDE_BOTH = 'include_both';
91 
95  private $blobStore;
96 
100  private $dbDomain;
101 
105  private $loadBalancer;
106 
110  private $cache;
111 
115  private $commentStore;
116 
121 
125  private $logger;
126 
131 
135  private $slotRoleStore;
136 
139 
142 
144  private $hookContainer;
145 
147  private $hookRunner;
148 
169  public function __construct(
170  ILoadBalancer $loadBalancer,
171  SqlBlobStore $blobStore,
174  NameTableStore $contentModelStore,
175  NameTableStore $slotRoleStore,
178  IContentHandlerFactory $contentHandlerFactory,
179  HookContainer $hookContainer,
180  $dbDomain = false
181  ) {
182  Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
183 
184  $this->loadBalancer = $loadBalancer;
185  $this->blobStore = $blobStore;
186  $this->cache = $cache;
187  $this->commentStore = $commentStore;
188  $this->contentModelStore = $contentModelStore;
189  $this->slotRoleStore = $slotRoleStore;
190  $this->slotRoleRegistry = $slotRoleRegistry;
191  $this->actorMigration = $actorMigration;
192  $this->dbDomain = $dbDomain;
193  $this->logger = new NullLogger();
194  $this->contentHandlerFactory = $contentHandlerFactory;
195  $this->hookContainer = $hookContainer;
196  $this->hookRunner = new HookRunner( $hookContainer );
197  }
198 
199  public function setLogger( LoggerInterface $logger ) {
200  $this->logger = $logger;
201  }
202 
206  public function isReadOnly() {
207  return $this->blobStore->isReadOnly();
208  }
209 
213  private function getDBLoadBalancer() {
214  return $this->loadBalancer;
215  }
216 
222  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
223  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
224  return $this->getDBConnectionRef( $mode );
225  }
226 
233  private function getDBConnectionRef( $mode, $groups = [] ) {
234  $lb = $this->getDBLoadBalancer();
235  return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
236  }
237 
252  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
253  if ( !$pageId && !$revId ) {
254  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
255  }
256 
257  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
258  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
259  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
260  $queryFlags = self::READ_NORMAL;
261  }
262 
263  $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
264  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
265 
266  // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
267  if ( $canUseTitleNewFromId ) {
268  $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
269  // TODO: better foreign title handling (introduce TitleFactory)
270  $title = Title::newFromID( $pageId, $titleFlags );
271  if ( $title ) {
272  return $title;
273  }
274  }
275 
276  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
277  $canUseRevId = ( $revId !== null && $revId > 0 );
278 
279  if ( $canUseRevId ) {
280  $dbr = $this->getDBConnectionRef( $dbMode );
281  // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
282  $row = $dbr->selectRow(
283  [ 'revision', 'page' ],
284  [
285  'page_namespace',
286  'page_title',
287  'page_id',
288  'page_latest',
289  'page_is_redirect',
290  'page_len',
291  ],
292  [ 'rev_id' => $revId ],
293  __METHOD__,
294  $dbOptions,
295  [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
296  );
297  if ( $row ) {
298  // TODO: better foreign title handling (introduce TitleFactory)
299  return Title::newFromRow( $row );
300  }
301  }
302 
303  // If we still don't have a title, fallback to master if that wasn't already happening.
304  if ( $dbMode !== DB_MASTER ) {
305  $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
306  if ( $title ) {
307  $this->logger->info(
308  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
309  [ 'trace' => wfBacktrace() ]
310  );
311  return $title;
312  }
313  }
314 
315  throw new RevisionAccessException(
316  "Could not determine title for page ID $pageId and revision ID $revId"
317  );
318  }
319 
327  private function failOnNull( $value, $name ) {
328  if ( $value === null ) {
329  throw new IncompleteRevisionException(
330  "$name must not be " . var_export( $value, true ) . "!"
331  );
332  }
333 
334  return $value;
335  }
336 
344  private function failOnEmpty( $value, $name ) {
345  if ( $value === null || $value === 0 || $value === '' ) {
346  throw new IncompleteRevisionException(
347  "$name must not be " . var_export( $value, true ) . "!"
348  );
349  }
350 
351  return $value;
352  }
353 
365  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
366  // TODO: pass in a DBTransactionContext instead of a database connection.
367  $this->checkDatabaseDomain( $dbw );
368 
369  $slotRoles = $rev->getSlotRoles();
370 
371  // Make sure the main slot is always provided throughout migration
372  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
373  throw new IncompleteRevisionException(
374  'main slot must be provided'
375  );
376  }
377 
378  // Checks
379  $this->failOnNull( $rev->getSize(), 'size field' );
380  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
381  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
382  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
383  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
384  $this->failOnNull( $user->getId(), 'user field' );
385  $this->failOnEmpty( $user->getName(), 'user_text field' );
386 
387  if ( !$rev->isReadyForInsertion() ) {
388  // This is here for future-proofing. At the time this check being added, it
389  // was redundant to the individual checks above.
390  throw new IncompleteRevisionException( 'Revision is incomplete' );
391  }
392 
393  if ( $slotRoles == [ SlotRecord::MAIN ] ) {
394  // T239717: If the main slot is the only slot, make sure the revision's nominal size
395  // and hash match the main slot's nominal size and hash.
396  $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
397  Assert::precondition(
398  $mainSlot->getSize() === $rev->getSize(),
399  'The revisions\'s size must match the main slot\'s size (see T239717)'
400  );
401  Assert::precondition(
402  $mainSlot->getSha1() === $rev->getSha1(),
403  'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
404  );
405  }
406 
407  // TODO: we shouldn't need an actual Title here.
409  $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
410 
411  $parentId = $rev->getParentId() === null
412  ? $this->getPreviousRevisionId( $dbw, $rev )
413  : $rev->getParentId();
414 
416  $rev = $dbw->doAtomicSection(
417  __METHOD__,
418  function ( IDatabase $dbw, $fname ) use (
419  $rev,
420  $user,
421  $comment,
422  $title,
423  $pageId,
424  $parentId
425  ) {
426  return $this->insertRevisionInternal(
427  $rev,
428  $dbw,
429  $user,
430  $comment,
431  $title,
432  $pageId,
433  $parentId
434  );
435  }
436  );
437 
438  // sanity checks
439  Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
440  Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
441  Assert::postcondition(
442  $rev->getComment( RevisionRecord::RAW ) !== null,
443  'revision must have a comment'
444  );
445  Assert::postcondition(
446  $rev->getUser( RevisionRecord::RAW ) !== null,
447  'revision must have a user'
448  );
449 
450  // Trigger exception if the main slot is missing.
451  // Technically, this could go away after MCR migration: while
452  // calling code may require a main slot to exist, RevisionStore
453  // really should not know or care about that requirement.
455 
456  foreach ( $slotRoles as $role ) {
457  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
458  Assert::postcondition(
459  $slot->getContent() !== null,
460  $role . ' slot must have content'
461  );
462  Assert::postcondition(
463  $slot->hasRevision(),
464  $role . ' slot must have a revision associated'
465  );
466  }
467 
468  $this->hookRunner->onRevisionRecordInserted( $rev );
469 
470  // Soft deprecated in 1.31, hard deprecated in 1.35
471  if ( $this->hookContainer->isRegistered( 'RevisionInsertComplete' ) ) {
472  // Only create the Revision object if its needed
473  $legacyRevision = new Revision( $rev );
474  $this->hookRunner->onRevisionInsertComplete( $legacyRevision, null, null );
475  }
476 
477  return $rev;
478  }
479 
480  private function insertRevisionInternal(
481  RevisionRecord $rev,
482  IDatabase $dbw,
483  User $user,
484  CommentStoreComment $comment,
485  Title $title,
486  $pageId,
487  $parentId
488  ) {
489  $slotRoles = $rev->getSlotRoles();
490 
491  $revisionRow = $this->insertRevisionRowOn(
492  $dbw,
493  $rev,
494  $title,
495  $parentId
496  );
497 
498  $revisionId = $revisionRow['rev_id'];
499 
500  $blobHints = [
501  BlobStore::PAGE_HINT => $pageId,
502  BlobStore::REVISION_HINT => $revisionId,
503  BlobStore::PARENT_HINT => $parentId,
504  ];
505 
506  $newSlots = [];
507  foreach ( $slotRoles as $role ) {
508  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
509 
510  // If the SlotRecord already has a revision ID set, this means it already exists
511  // in the database, and should already belong to the current revision.
512  // However, a slot may already have a revision, but no content ID, if the slot
513  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
514  // mode, and the respective archive row was not yet migrated to the new schema.
515  // In that case, a new slot row (and content row) must be inserted even during
516  // undeletion.
517  if ( $slot->hasRevision() && $slot->hasContentId() ) {
518  // TODO: properly abort transaction if the assertion fails!
519  Assert::parameter(
520  $slot->getRevision() === $revisionId,
521  'slot role ' . $slot->getRole(),
522  'Existing slot should belong to revision '
523  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
524  );
525 
526  // Slot exists, nothing to do, move along.
527  // This happens when restoring archived revisions.
528 
529  $newSlots[$role] = $slot;
530  } else {
531  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
532  }
533  }
534 
535  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
536 
537  $rev = new RevisionStoreRecord(
538  $title,
539  $user,
540  $comment,
541  (object)$revisionRow,
542  new RevisionSlots( $newSlots ),
543  $this->dbDomain
544  );
545 
546  return $rev;
547  }
548 
557  private function insertSlotOn(
558  IDatabase $dbw,
559  $revisionId,
560  SlotRecord $protoSlot,
561  Title $title,
562  array $blobHints = []
563  ) {
564  if ( $protoSlot->hasAddress() ) {
565  $blobAddress = $protoSlot->getAddress();
566  } else {
567  $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
568  }
569 
570  $contentId = null;
571 
572  if ( $protoSlot->hasContentId() ) {
573  $contentId = $protoSlot->getContentId();
574  } else {
575  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
576  }
577 
578  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
579 
580  return SlotRecord::newSaved(
581  $revisionId,
582  $contentId,
583  $blobAddress,
584  $protoSlot
585  );
586  }
587 
595  private function insertIpChangesRow(
596  IDatabase $dbw,
597  User $user,
598  RevisionRecord $rev,
599  $revisionId
600  ) {
601  if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
602  $ipcRow = [
603  'ipc_rev_id' => $revisionId,
604  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
605  'ipc_hex' => IPUtils::toHex( $user->getName() ),
606  ];
607  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
608  }
609  }
610 
622  private function insertRevisionRowOn(
623  IDatabase $dbw,
624  RevisionRecord $rev,
625  Title $title,
626  $parentId
627  ) {
628  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
629 
630  list( $commentFields, $commentCallback ) =
631  $this->commentStore->insertWithTempTable(
632  $dbw,
633  'rev_comment',
635  );
636  $revisionRow += $commentFields;
637 
638  list( $actorFields, $actorCallback ) =
639  $this->actorMigration->getInsertValuesWithTempTable(
640  $dbw,
641  'rev_user',
643  );
644  $revisionRow += $actorFields;
645 
646  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
647 
648  if ( !isset( $revisionRow['rev_id'] ) ) {
649  // only if auto-increment was used
650  $revisionRow['rev_id'] = intval( $dbw->insertId() );
651 
652  if ( $dbw->getType() === 'mysql' ) {
653  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
654  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
655  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
656 
657  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
658  $table = 'archive';
659  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
660  if ( $maxRevId2 >= $maxRevId ) {
661  $maxRevId = $maxRevId2;
662  $table = 'slots';
663  }
664 
665  if ( $maxRevId >= $revisionRow['rev_id'] ) {
666  $this->logger->debug(
667  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
668  . ' Trying to fix it.',
669  [
670  'revid' => $revisionRow['rev_id'],
671  'table' => $table,
672  'maxrevid' => $maxRevId,
673  ]
674  );
675 
676  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
677  throw new MWException( 'Failed to get database lock for T202032' );
678  }
679  $fname = __METHOD__;
680  $dbw->onTransactionResolution(
681  function ( $trigger, IDatabase $dbw ) use ( $fname ) {
682  $dbw->unlock( 'fix-for-T202032', $fname );
683  },
684  __METHOD__
685  );
686 
687  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
688 
689  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
690  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
691  // inserts too, though, at least on MariaDB 10.1.29.
692  //
693  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
694  // transactions in this code path thanks to the row lock from the original ->insert() above.
695  //
696  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
697  // that's for non-MySQL DBs.
698  $row1 = $dbw->query(
699  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
700  __METHOD__
701  )->fetchObject();
702 
703  $row2 = $dbw->query(
704  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
705  . ' FOR UPDATE',
706  __METHOD__
707  )->fetchObject();
708 
709  $maxRevId = max(
710  $maxRevId,
711  $row1 ? intval( $row1->v ) : 0,
712  $row2 ? intval( $row2->v ) : 0
713  );
714 
715  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
716  // transactions will throw a duplicate key error here. It doesn't seem worth trying
717  // to avoid that.
718  $revisionRow['rev_id'] = $maxRevId + 1;
719  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
720  }
721  }
722  }
723 
724  $commentCallback( $revisionRow['rev_id'] );
725  $actorCallback( $revisionRow['rev_id'], $revisionRow );
726 
727  return $revisionRow;
728  }
729 
738  private function getBaseRevisionRow(
739  IDatabase $dbw,
740  RevisionRecord $rev,
741  Title $title,
742  $parentId
743  ) {
744  // Record the edit in revisions
745  $revisionRow = [
746  'rev_page' => $rev->getPageId(),
747  'rev_parent_id' => $parentId,
748  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
749  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
750  'rev_deleted' => $rev->getVisibility(),
751  'rev_len' => $rev->getSize(),
752  'rev_sha1' => $rev->getSha1(),
753  ];
754 
755  if ( $rev->getId() !== null ) {
756  // Needed to restore revisions with their original ID
757  $revisionRow['rev_id'] = $rev->getId();
758  }
759 
760  return $revisionRow;
761  }
762 
771  private function storeContentBlob(
772  SlotRecord $slot,
773  Title $title,
774  array $blobHints = []
775  ) {
776  $content = $slot->getContent();
777  $format = $content->getDefaultFormat();
778  $model = $content->getModel();
779 
780  $this->checkContent( $content, $title, $slot->getRole() );
781 
782  return $this->blobStore->storeBlob(
783  $content->serialize( $format ),
784  // These hints "leak" some information from the higher abstraction layer to
785  // low level storage to allow for optimization.
786  array_merge(
787  $blobHints,
788  [
789  BlobStore::DESIGNATION_HINT => 'page-content',
790  BlobStore::ROLE_HINT => $slot->getRole(),
791  BlobStore::SHA1_HINT => $slot->getSha1(),
792  BlobStore::MODEL_HINT => $model,
793  BlobStore::FORMAT_HINT => $format,
794  ]
795  )
796  );
797  }
798 
805  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
806  $slotRow = [
807  'slot_revision_id' => $revisionId,
808  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
809  'slot_content_id' => $contentId,
810  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
811  // that we just inserted.
812  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
813  ];
814  $dbw->insert( 'slots', $slotRow, __METHOD__ );
815  }
816 
823  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
824  $contentRow = [
825  'content_size' => $slot->getSize(),
826  'content_sha1' => $slot->getSha1(),
827  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
828  'content_address' => $blobAddress,
829  ];
830  $dbw->insert( 'content', $contentRow, __METHOD__ );
831  return intval( $dbw->insertId() );
832  }
833 
844  private function checkContent( Content $content, Title $title, $role ) {
845  // Note: may return null for revisions that have not yet been inserted
846 
847  $model = $content->getModel();
848  $format = $content->getDefaultFormat();
849  $handler = $content->getContentHandler();
850 
851  $name = "$title";
852 
853  if ( !$handler->isSupportedFormat( $format ) ) {
854  throw new MWException( "Can't use format $format with content model $model on $name" );
855  }
856 
857  if ( !$content->isValid() ) {
858  throw new MWException(
859  "New content for $name is not valid! Content model is $model"
860  );
861  }
862  }
863 
889  public function newNullRevision(
890  IDatabase $dbw,
891  Title $title,
892  CommentStoreComment $comment,
893  $minor,
894  User $user
895  ) {
896  $this->checkDatabaseDomain( $dbw );
897 
898  $pageId = $title->getArticleID();
899 
900  // T51581: Lock the page table row to ensure no other process
901  // is adding a revision to the page at the same time.
902  // Avoid locking extra tables, compare T191892.
903  $pageLatest = $dbw->selectField(
904  'page',
905  'page_latest',
906  [ 'page_id' => $pageId ],
907  __METHOD__,
908  [ 'FOR UPDATE' ]
909  );
910 
911  if ( !$pageLatest ) {
912  $id = $title->getArticleID( self::READ_EXCLUSIVE );
913  $msg = 'T235589: Failed to select table row during null revision creation' .
914  " Page id '$pageId' does not exist. Maybe it is different from '$id'?";
915  $this->logger->error(
916  $msg,
917  [ 'exception' => new RuntimeException( $msg ) ]
918  );
919 
920  return null;
921  }
922 
923  // Fetch the actual revision row from master, without locking all extra tables.
924  $oldRevision = $this->loadRevisionFromConds(
925  $dbw,
926  [ 'rev_id' => intval( $pageLatest ) ],
927  self::READ_LATEST,
928  $title
929  );
930 
931  if ( !$oldRevision ) {
932  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
933  $this->logger->error(
934  $msg,
935  [ 'exception' => new RuntimeException( $msg ) ]
936  );
937  return null;
938  }
939 
940  // Construct the new revision
941  $timestamp = MWTimestamp::now( TS_MW );
942  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
943 
944  $newRevision->setComment( $comment );
945  $newRevision->setUser( $user );
946  $newRevision->setTimestamp( $timestamp );
947  $newRevision->setMinorEdit( $minor );
948 
949  return $newRevision;
950  }
951 
961  public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
962  $rc = $this->getRecentChange( $rev );
963  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
964  return $rc->getAttribute( 'rc_id' );
965  } else {
966  return 0;
967  }
968  }
969 
983  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
984  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
985 
987  [ 'rc_this_oldid' => $rev->getId() ],
988  __METHOD__,
989  $dbType
990  );
991 
992  // XXX: cache this locally? Glue it to the RevisionRecord?
993  return $rc;
994  }
995 
1015  private function loadSlotContent(
1016  SlotRecord $slot,
1017  $blobData = null,
1018  $blobFlags = null,
1019  $blobFormat = null,
1020  $queryFlags = 0
1021  ) {
1022  if ( $blobData !== null ) {
1023  Assert::parameterType( 'string', $blobData, '$blobData' );
1024  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1025 
1026  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1027 
1028  if ( $blobFlags === null ) {
1029  // No blob flags, so use the blob verbatim.
1030  $data = $blobData;
1031  } else {
1032  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1033  if ( $data === false ) {
1034  throw new RevisionAccessException(
1035  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1036  );
1037  }
1038  }
1039 
1040  } else {
1041  $address = $slot->getAddress();
1042  try {
1043  $data = $this->blobStore->getBlob( $address, $queryFlags );
1044  } catch ( BlobAccessException $e ) {
1045  throw new RevisionAccessException(
1046  "Failed to load data blob from $address: " . $e->getMessage() . '. '
1047  . 'If this problem persist, use the findBadBlobs maintenance script '
1048  . 'to investigate the issue and mark bad blobs.',
1049  0, $e
1050  );
1051  }
1052  }
1053 
1054  $model = $slot->getModel();
1055 
1056  // If the content model is not known, don't fail here (T220594, T220793, T228921)
1057  if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1058  $this->logger->warning(
1059  "Undefined content model '$model', falling back to UnknownContent",
1060  [
1061  'content_address' => $slot->getAddress(),
1062  'rev_id' => $slot->getRevision(),
1063  'role_name' => $slot->getRole(),
1064  'model_name' => $model,
1065  'trace' => wfBacktrace()
1066  ]
1067  );
1068 
1069  return new FallbackContent( $data, $model );
1070  }
1071 
1072  return $this->contentHandlerFactory
1073  ->getContentHandler( $model )
1074  ->unserializeContent( $data, $blobFormat );
1075  }
1076 
1091  public function getRevisionById( $id, $flags = 0 ) {
1092  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1093  }
1094 
1111  public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1112  $conds = [
1113  'page_namespace' => $linkTarget->getNamespace(),
1114  'page_title' => $linkTarget->getDBkey()
1115  ];
1116 
1117  // Only resolve to a Title when operating in the context of the local wiki (T248756)
1118  // TODO should not require Title in future (T206498)
1119  $title = $this->dbDomain === false ? Title::newFromLinkTarget( $linkTarget ) : null;
1120 
1121  if ( $revId ) {
1122  // Use the specified revision ID.
1123  // Note that we use newRevisionFromConds here because we want to retry
1124  // and fall back to master if the page is not found on a replica.
1125  // Since the caller supplied a revision ID, we are pretty sure the revision is
1126  // supposed to exist, so we should try hard to find it.
1127  $conds['rev_id'] = $revId;
1128  return $this->newRevisionFromConds( $conds, $flags, $title );
1129  } else {
1130  // Use a join to get the latest revision.
1131  // Note that we don't use newRevisionFromConds here because we don't want to retry
1132  // and fall back to master. The assumption is that we only want to force the fallback
1133  // if we are quite sure the revision exists because the caller supplied a revision ID.
1134  // If the page isn't found at all on a replica, it probably simply does not exist.
1135  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1136 
1137  $conds[] = 'rev_id=page_latest';
1138 
1139  return $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1140  }
1141  }
1142 
1159  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1160  $conds = [ 'page_id' => $pageId ];
1161  if ( $revId ) {
1162  // Use the specified revision ID.
1163  // Note that we use newRevisionFromConds here because we want to retry
1164  // and fall back to master if the page is not found on a replica.
1165  // Since the caller supplied a revision ID, we are pretty sure the revision is
1166  // supposed to exist, so we should try hard to find it.
1167  $conds['rev_id'] = $revId;
1168  return $this->newRevisionFromConds( $conds, $flags );
1169  } else {
1170  // Use a join to get the latest revision.
1171  // Note that we don't use newRevisionFromConds here because we don't want to retry
1172  // and fall back to master. The assumption is that we only want to force the fallback
1173  // if we are quite sure the revision exists because the caller supplied a revision ID.
1174  // If the page isn't found at all on a replica, it probably simply does not exist.
1175  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1176 
1177  $conds[] = 'rev_id=page_latest';
1178 
1179  return $this->loadRevisionFromConds( $db, $conds, $flags );
1180  }
1181  }
1182 
1198  public function getRevisionByTimestamp(
1199  LinkTarget $title,
1200  string $timestamp,
1201  int $flags = IDBAccessObject::READ_NORMAL
1202  ): ?RevisionRecord {
1203  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1204  return $this->newRevisionFromConds(
1205  [
1206  'rev_timestamp' => $db->timestamp( $timestamp ),
1207  'page_namespace' => $title->getNamespace(),
1208  'page_title' => $title->getDBkey()
1209  ],
1210  $flags,
1212  );
1213  }
1214 
1222  private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
1223  $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1224 
1225  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1226  $db = $this->getDBConnectionRef( $dbMode );
1227 
1228  $res = $db->select(
1229  $revQuery['tables'],
1230  $revQuery['fields'],
1231  [
1232  'slot_revision_id' => $revId,
1233  ],
1234  __METHOD__,
1235  $dbOptions,
1236  $revQuery['joins']
1237  );
1238 
1239  if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1240  // If we found no slots, try looking on the master database (T212428, T252156)
1241  $this->logger->info(
1242  __METHOD__ . ' falling back to READ_LATEST.',
1243  [
1244  'revid' => $revId,
1245  'trace' => wfBacktrace( true )
1246  ]
1247  );
1248  return $this->loadSlotRecords(
1249  $revId,
1250  $queryFlags | self::READ_LATEST,
1251  $title
1252  );
1253  }
1254 
1255  return $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
1256  }
1257 
1270  private function constructSlotRecords(
1271  $revId,
1272  $slotRows,
1273  $queryFlags,
1274  Title $title,
1275  $slotContents = null
1276  ) {
1277  $slots = [];
1278 
1279  foreach ( $slotRows as $row ) {
1280  // Resolve role names and model names from in-memory cache, if they were not joined in.
1281  if ( !isset( $row->role_name ) ) {
1282  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1283  }
1284 
1285  if ( !isset( $row->model_name ) ) {
1286  if ( isset( $row->content_model ) ) {
1287  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1288  } else {
1289  // We may get here if $row->model_name is set but null, perhaps because it
1290  // came from rev_content_model, which is NULL for the default model.
1291  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1292  $row->model_name = $slotRoleHandler->getDefaultModel( $title );
1293  }
1294  }
1295 
1296  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1297  if ( isset( $row->blob_data ) ) {
1298  $slotContents[$row->content_address] = $row->blob_data;
1299  }
1300 
1301  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1302  $blob = null;
1303  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1304  $blob = $slotContents[$slot->getAddress()];
1305  if ( $blob instanceof Content ) {
1306  return $blob;
1307  }
1308  }
1309  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1310  };
1311 
1312  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1313  }
1314 
1315  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1316  $this->logger->error(
1317  __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1318  [
1319  'revid' => $revId,
1320  'queryFlags' => $queryFlags,
1321  'trace' => wfBacktrace( true )
1322  ]
1323  );
1324 
1325  throw new RevisionAccessException(
1326  'Main slot of revision not found in database. See T212428.'
1327  );
1328  }
1329 
1330  return $slots;
1331  }
1332 
1348  private function newRevisionSlots(
1349  $revId,
1350  $revisionRow,
1351  $slotRows,
1352  $queryFlags,
1353  Title $title
1354  ) {
1355  if ( $slotRows ) {
1356  $slots = new RevisionSlots(
1357  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
1358  );
1359  } else {
1360  // XXX: do we need the same kind of caching here
1361  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1362 
1363  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
1364  return $this->loadSlotRecords( $revId, $queryFlags, $title );
1365  } );
1366  }
1367 
1368  return $slots;
1369  }
1370 
1388  public function newRevisionFromArchiveRow(
1389  $row,
1390  $queryFlags = 0,
1391  Title $title = null,
1392  array $overrides = []
1393  ) {
1394  return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $title, $overrides );
1395  }
1396 
1410  public function newRevisionFromRow(
1411  $row,
1412  $queryFlags = 0,
1413  Title $title = null,
1414  $fromCache = false
1415  ) {
1416  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
1417  }
1418 
1439  $row,
1440  $slots,
1441  $queryFlags = 0,
1442  Title $title = null,
1443  array $overrides = []
1444  ) {
1445  Assert::parameterType( \stdClass::class, $row, '$row' );
1446 
1447  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1448  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1449 
1450  if ( !$title && isset( $overrides['title'] ) ) {
1451  if ( !( $overrides['title'] instanceof Title ) ) {
1452  throw new MWException( 'title field override must contain a Title object.' );
1453  }
1454 
1455  $title = $overrides['title'];
1456  }
1457 
1458  if ( !isset( $title ) ) {
1459  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1460  $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1461  } else {
1462  throw new InvalidArgumentException(
1463  'A Title or ar_namespace and ar_title must be given'
1464  );
1465  }
1466  }
1467 
1468  foreach ( $overrides as $key => $value ) {
1469  $field = "ar_$key";
1470  $row->$field = $value;
1471  }
1472 
1473  try {
1474  $user = User::newFromAnyId(
1475  $row->ar_user ?? null,
1476  $row->ar_user_text ?? null,
1477  $row->ar_actor ?? null,
1478  $this->dbDomain
1479  );
1480  } catch ( InvalidArgumentException $ex ) {
1481  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1482  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1483  }
1484 
1485  if ( $user->getName() === '' ) {
1486  // T236624: If the user name is empty, force 'Unknown user',
1487  // even if the actor table has an entry for the empty user name.
1488  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1489  }
1490 
1491  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1492  // Legacy because $row may have come from self::selectFields()
1493  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1494 
1495  if ( !( $slots instanceof RevisionSlots ) ) {
1496  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $title );
1497  }
1498 
1499  return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
1500  }
1501 
1520  $row,
1521  $slots,
1522  $queryFlags = 0,
1523  Title $title = null,
1524  $fromCache = false
1525  ) {
1526  Assert::parameterType( \stdClass::class, $row, '$row' );
1527 
1528  if ( !$title ) {
1529  $pageId = (int)( $row->rev_page ?? 0 ); // XXX: fall back to page_id?
1530  $revId = (int)( $row->rev_id ?? 0 );
1531 
1532  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1533  } else {
1534  $this->ensureRevisionRowMatchesTitle( $row, $title );
1535  }
1536 
1537  if ( !isset( $row->page_latest ) ) {
1538  $row->page_latest = $title->getLatestRevID();
1539  if ( $row->page_latest === 0 && $title->exists() ) {
1540  wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1541  }
1542  }
1543 
1544  try {
1545  $user = User::newFromAnyId(
1546  $row->rev_user ?? null,
1547  $row->rev_user_text ?? null,
1548  $row->rev_actor ?? null,
1549  $this->dbDomain
1550  );
1551  } catch ( InvalidArgumentException $ex ) {
1552  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1553  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1554  }
1555 
1556  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1557  // Legacy because $row may have come from self::selectFields()
1558  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1559 
1560  if ( !( $slots instanceof RevisionSlots ) ) {
1561  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
1562  }
1563 
1564  // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1565  if ( $fromCache ) {
1566  $rev = new RevisionStoreCacheRecord(
1567  function ( $revId ) use ( $queryFlags ) {
1568  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1569  $row = $this->fetchRevisionRowFromConds(
1570  $db,
1571  [ 'rev_id' => intval( $revId ) ]
1572  );
1573  if ( !$row && !( $queryFlags & self::READ_LATEST ) ) {
1574  // If we found no slots, try looking on the master database (T259738)
1575  $this->logger->info(
1576  'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1577  [
1578  'revid' => $revId,
1579  'trace' => wfBacktrace( true )
1580  ]
1581  );
1582  $dbw = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
1583  return $this->fetchRevisionRowFromConds(
1584  $dbw,
1585  [ 'rev_id' => intval( $revId ) ]
1586  );
1587  }
1588  return $row;
1589  },
1590  $title, $user, $comment, $row, $slots, $this->dbDomain
1591  );
1592  } else {
1593  $rev = new RevisionStoreRecord(
1594  $title, $user, $comment, $row, $slots, $this->dbDomain );
1595  }
1596  return $rev;
1597  }
1598 
1608  private function ensureRevisionRowMatchesTitle( $row, Title $title, $context = [] ) {
1609  $revId = (int)( $row->rev_id ?? 0 );
1610  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1611  $titlePageId = $title->getArticleID();
1612 
1613  // Avoid fatal error when the Title's ID changed, T246720
1614  if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1615  $masterPageId = $title->getArticleID( Title::READ_LATEST );
1616  $masterLatest = $title->getLatestRevID( Title::READ_LATEST );
1617 
1618  if ( $revPageId === $masterPageId ) {
1619  $this->logger->warning(
1620  "Encountered stale Title object",
1621  [
1622  'page_id_stale' => $titlePageId,
1623  'page_id_reloaded' => $masterPageId,
1624  'page_latest' => $masterLatest,
1625  'rev_id' => $revId,
1626  'trace' => wfBacktrace()
1627  ] + $context
1628  );
1629  } else {
1630  throw new InvalidArgumentException(
1631  "Revision $revId belongs to page ID $revPageId, "
1632  . "the provided Title object belongs to page ID $masterPageId"
1633  );
1634  }
1635  }
1636  }
1637 
1663  public function newRevisionsFromBatch(
1664  $rows,
1665  array $options = [],
1666  $queryFlags = 0,
1667  Title $title = null
1668  ) {
1669  $result = new StatusValue();
1670  $archiveMode = $options['archive'] ?? false;
1671 
1672  if ( $archiveMode ) {
1673  $revIdField = 'ar_rev_id';
1674  } else {
1675  $revIdField = 'rev_id';
1676  }
1677 
1678  $rowsByRevId = [];
1679  $pageIdsToFetchTitles = [];
1680  $titlesByPageKey = [];
1681  foreach ( $rows as $row ) {
1682  if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1683  $result->warning(
1684  'internalerror_info',
1685  "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1686  );
1687  }
1688 
1689  // Attach a page key to the row, so we can find and reuse Title objects easily.
1690  $row->_page_key =
1691  $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1692 
1693  if ( $title ) {
1694  if ( !$archiveMode && $row->rev_page != $title->getArticleID() ) {
1695  throw new InvalidArgumentException(
1696  "Revision {$row->$revIdField} doesn't belong to page "
1697  . $title->getArticleID()
1698  );
1699  }
1700 
1701  if ( $archiveMode
1702  && ( $row->ar_namespace != $title->getNamespace()
1703  || $row->ar_title !== $title->getDBkey() )
1704  ) {
1705  throw new InvalidArgumentException(
1706  "Revision {$row->$revIdField} doesn't belong to page "
1707  . $title->getPrefixedDBkey()
1708  );
1709  }
1710  } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1711  if ( isset( $row->page_namespace ) && isset( $row->page_title )
1712  // This should always be true, but just in case we don't have a page_id
1713  // set or it doesn't match rev_page, let's fetch the title again.
1714  && isset( $row->page_id ) && isset( $row->rev_page )
1715  && $row->rev_page === $row->page_id
1716  ) {
1717  $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1718  } elseif ( $archiveMode ) {
1719  // Can't look up deleted pages by ID, but we have namespace and title
1720  $titlesByPageKey[ $row->_page_key ] =
1721  Title::makeTitle( $row->ar_namespace, $row->ar_title );
1722  } else {
1723  $pageIdsToFetchTitles[] = $row->rev_page;
1724  }
1725  }
1726  $rowsByRevId[$row->$revIdField] = $row;
1727  }
1728 
1729  if ( empty( $rowsByRevId ) ) {
1730  $result->setResult( true, [] );
1731  return $result;
1732  }
1733 
1734  // If the title is not supplied, batch-fetch Title objects.
1735  if ( $title ) {
1736  // same logic as for $row->_page_key above
1737  $pageKey = $archiveMode
1738  ? $title->getNamespace() . ':' . $title->getDBkey()
1739  : $title->getArticleID();
1740 
1741  $titlesByPageKey[$pageKey] = $title;
1742  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1743  // Note: when we fetch titles by ID, the page key is also the ID.
1744  // We should never get here if $archiveMode is true.
1745  Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1746 
1747  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1748  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1749  $titlesByPageKey[$t->getArticleID()] = $t;
1750  }
1751  }
1752 
1753  // which method to use for creating RevisionRecords
1754  $newRevisionRecord = [
1755  $this,
1756  $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1757  ];
1758 
1759  if ( !isset( $options['slots'] ) ) {
1760  $result->setResult(
1761  true,
1762  array_map(
1763  function ( $row )
1764  use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1765  try {
1766  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1767  $result->warning(
1768  'internalerror_info',
1769  "Couldn't find title for rev {$row->$revIdField} "
1770  . "(page key {$row->_page_key})"
1771  );
1772  return null;
1773  }
1774  return $newRevisionRecord( $row, null, $queryFlags,
1775  $titlesByPageKey[ $row->_page_key ] );
1776  } catch ( MWException $e ) {
1777  $result->warning( 'internalerror_info', $e->getMessage() );
1778  return null;
1779  }
1780  },
1781  $rowsByRevId
1782  )
1783  );
1784  return $result;
1785  }
1786 
1787  $slotRowOptions = [
1788  'slots' => $options['slots'] ?? true,
1789  'blobs' => $options['content'] ?? false,
1790  ];
1791 
1792  if ( is_array( $slotRowOptions['slots'] )
1793  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
1794  ) {
1795  // Make sure the main slot is always loaded, RevisionRecord requires this.
1796  $slotRowOptions['slots'][] = SlotRecord::MAIN;
1797  }
1798 
1799  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
1800 
1801  $result->merge( $slotRowsStatus );
1802  $slotRowsByRevId = $slotRowsStatus->getValue();
1803 
1804  $result->setResult(
1805  true,
1806  array_map(
1807  function ( $row )
1808  use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
1809  $revIdField, $newRevisionRecord
1810  ) {
1811  if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
1812  $result->warning(
1813  'internalerror_info',
1814  "Couldn't find slots for rev {$row->$revIdField}"
1815  );
1816  return null;
1817  }
1818  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1819  $result->warning(
1820  'internalerror_info',
1821  "Couldn't find title for rev {$row->$revIdField} "
1822  . "(page key {$row->_page_key})"
1823  );
1824  return null;
1825  }
1826  try {
1827  return $newRevisionRecord(
1828  $row,
1829  new RevisionSlots(
1830  $this->constructSlotRecords(
1831  $row->$revIdField,
1832  $slotRowsByRevId[$row->$revIdField],
1833  $queryFlags,
1834  $titlesByPageKey[$row->_page_key]
1835  )
1836  ),
1837  $queryFlags,
1838  $titlesByPageKey[$row->_page_key]
1839  );
1840  } catch ( MWException $e ) {
1841  $result->warning( 'internalerror_info', $e->getMessage() );
1842  return null;
1843  }
1844  },
1845  $rowsByRevId
1846  )
1847  );
1848  return $result;
1849  }
1850 
1874  private function getSlotRowsForBatch(
1875  $rowsOrIds,
1876  array $options = [],
1877  $queryFlags = 0
1878  ) {
1879  $result = new StatusValue();
1880 
1881  $revIds = [];
1882  foreach ( $rowsOrIds as $row ) {
1883  if ( is_object( $row ) ) {
1884  $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
1885  } else {
1886  $revIds[] = (int)$row;
1887  }
1888  }
1889 
1890  // Nothing to do.
1891  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
1892  if ( empty( $revIds ) ) {
1893  $result->setResult( true, [] );
1894  return $result;
1895  }
1896 
1897  // We need to set the `content` flag to join in content meta-data
1898  $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
1899  $revIdField = $slotQueryInfo['keys']['rev_id'];
1900  $slotQueryConds = [ $revIdField => $revIds ];
1901 
1902  if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
1903  if ( empty( $options['slots'] ) ) {
1904  // Degenerate case: return no slots for each revision.
1905  $result->setResult( true, array_fill_keys( $revIds, [] ) );
1906  return $result;
1907  }
1908 
1909  $roleIdField = $slotQueryInfo['keys']['role_id'];
1910  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
1911  return $this->slotRoleStore->getId( $slot_name );
1912  }, $options['slots'] );
1913  }
1914 
1915  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1916  $slotRows = $db->select(
1917  $slotQueryInfo['tables'],
1918  $slotQueryInfo['fields'],
1919  $slotQueryConds,
1920  __METHOD__,
1921  [],
1922  $slotQueryInfo['joins']
1923  );
1924 
1925  $slotContents = null;
1926  if ( $options['blobs'] ?? false ) {
1927  $blobAddresses = [];
1928  foreach ( $slotRows as $slotRow ) {
1929  $blobAddresses[] = $slotRow->content_address;
1930  }
1931  $slotContentFetchStatus = $this->blobStore
1932  ->getBlobBatch( $blobAddresses, $queryFlags );
1933  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
1934  $result->warning( $error['message'], ...$error['params'] );
1935  }
1936  $slotContents = $slotContentFetchStatus->getValue();
1937  }
1938 
1939  $slotRowsByRevId = [];
1940  foreach ( $slotRows as $slotRow ) {
1941  if ( $slotContents === null ) {
1942  // nothing to do
1943  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
1944  $slotRow->blob_data = $slotContents[$slotRow->content_address];
1945  } else {
1946  $result->warning(
1947  'internalerror_info',
1948  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
1949  );
1950  $slotRow->blob_data = null;
1951  }
1952 
1953  // conditional needed for SCHEMA_COMPAT_READ_OLD
1954  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
1955  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
1956  }
1957 
1958  // conditional needed for SCHEMA_COMPAT_READ_OLD
1959  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
1960  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
1961  }
1962 
1963  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
1964  }
1965 
1966  $result->setResult( true, $slotRowsByRevId );
1967  return $result;
1968  }
1969 
1990  public function getContentBlobsForBatch(
1991  $rowsOrIds,
1992  $slots = null,
1993  $queryFlags = 0
1994  ) {
1995  $result = $this->getSlotRowsForBatch(
1996  $rowsOrIds,
1997  [ 'slots' => $slots, 'blobs' => true ],
1998  $queryFlags
1999  );
2000 
2001  if ( $result->isOK() ) {
2002  // strip out all internal meta data that we don't want to expose
2003  foreach ( $result->value as $revId => $rowsByRole ) {
2004  foreach ( $rowsByRole as $role => $slotRow ) {
2005  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2006  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2007  // if we didn't ask for it.
2008  unset( $result->value[$revId][$role] );
2009  continue;
2010  }
2011 
2012  $result->value[$revId][$role] = (object)[
2013  'blob_data' => $slotRow->blob_data,
2014  'model_name' => $slotRow->model_name,
2015  ];
2016  }
2017  }
2018  }
2019 
2020  return $result;
2021  }
2022 
2038  array $fields,
2039  $queryFlags = 0,
2040  Title $title = null
2041  ) {
2042  if ( !$title && isset( $fields['title'] ) ) {
2043  if ( !( $fields['title'] instanceof Title ) ) {
2044  throw new MWException( 'title field must contain a Title object.' );
2045  }
2046 
2047  $title = $fields['title'];
2048  }
2049 
2050  if ( !$title ) {
2051  $pageId = $fields['page'] ?? 0;
2052  $revId = $fields['id'] ?? 0;
2053 
2054  $title = $this->getTitle( $pageId, $revId, $queryFlags );
2055  }
2056 
2057  if ( !isset( $fields['page'] ) ) {
2058  $fields['page'] = $title->getArticleID( $queryFlags );
2059  }
2060 
2061  // if we have a content object, use it to set the model and type
2062  if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
2063  && !is_array( $fields['content'] )
2064  ) {
2065  throw new MWException(
2066  'content field must contain a Content object or an array of Content objects.'
2067  );
2068  }
2069 
2070  if ( !empty( $fields['text_id'] ) ) {
2071  throw new MWException( 'The text_id field can not be used in MediaWiki 1.35 and later' );
2072  }
2073 
2074  if (
2075  isset( $fields['comment'] )
2076  && !( $fields['comment'] instanceof CommentStoreComment )
2077  ) {
2078  $commentData = $fields['comment_data'] ?? null;
2079 
2080  if ( $fields['comment'] instanceof Message ) {
2081  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2082  $fields['comment'],
2083  $commentData
2084  );
2085  } else {
2086  $commentText = trim( strval( $fields['comment'] ) );
2087  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2088  $commentText,
2089  $commentData
2090  );
2091  }
2092  }
2093 
2094  $revision = new MutableRevisionRecord( $title, $this->dbDomain );
2095 
2097  if ( isset( $fields['content'] ) ) {
2098  if ( is_array( $fields['content'] ) ) {
2099  $slotContent = $fields['content'];
2100  } else {
2101  $slotContent = [ SlotRecord::MAIN => $fields['content'] ];
2102  }
2103  } elseif ( isset( $fields['text'] ) ) {
2104  if ( isset( $fields['content_model'] ) ) {
2105  $model = $fields['content_model'];
2106  } else {
2107  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( SlotRecord::MAIN );
2108  $model = $slotRoleHandler->getDefaultModel( $title );
2109  }
2110 
2111  $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
2112  $content = $contentHandler->unserializeContent( $fields['text'] );
2113  $slotContent = [ SlotRecord::MAIN => $content ];
2114  } else {
2115  $slotContent = [];
2116  }
2117 
2118  foreach ( $slotContent as $role => $content ) {
2119  $revision->setContent( $role, $content );
2120  }
2121 
2122  $this->initializeMutableRevisionFromArray( $revision, $fields );
2123 
2124  return $revision;
2125  }
2126 
2132  MutableRevisionRecord $record,
2133  array $fields
2134  ) {
2136  $user = null;
2137 
2138  // If a user is passed in, use it if possible. We cannot use a user from a
2139  // remote wiki with unsuppressed ids, due to issues described in T222212.
2140  if ( isset( $fields['user'] ) &&
2141  ( $fields['user'] instanceof UserIdentity ) &&
2142  ( $this->dbDomain === false ||
2143  ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
2144  ) {
2145  $user = $fields['user'];
2146  } else {
2147  $userID = isset( $fields['user'] ) && is_numeric( $fields['user'] ) ? $fields['user'] : null;
2148  try {
2149  $user = User::newFromAnyId(
2150  $userID,
2151  $fields['user_text'] ?? null,
2152  $fields['actor'] ?? null,
2153  $this->dbDomain
2154  );
2155  } catch ( InvalidArgumentException $ex ) {
2156  $user = null;
2157  }
2158  }
2159 
2160  if ( $user ) {
2161  $record->setUser( $user );
2162  }
2163 
2164  $timestamp = isset( $fields['timestamp'] )
2165  ? strval( $fields['timestamp'] )
2166  : MWTimestamp::now( TS_MW );
2167 
2168  $record->setTimestamp( $timestamp );
2169 
2170  if ( isset( $fields['page'] ) ) {
2171  $record->setPageId( intval( $fields['page'] ) );
2172  }
2173 
2174  if ( isset( $fields['id'] ) ) {
2175  $record->setId( intval( $fields['id'] ) );
2176  }
2177  if ( isset( $fields['parent_id'] ) ) {
2178  $record->setParentId( intval( $fields['parent_id'] ) );
2179  }
2180 
2181  if ( isset( $fields['sha1'] ) ) {
2182  $record->setSha1( $fields['sha1'] );
2183  }
2184 
2185  if ( isset( $fields['size'] ) ) {
2186  $record->setSize( intval( $fields['size'] ) );
2187  } elseif ( isset( $fields['len'] ) ) {
2188  $record->setSize( intval( $fields['len'] ) );
2189  }
2190 
2191  if ( isset( $fields['minor_edit'] ) ) {
2192  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2193  }
2194  if ( isset( $fields['deleted'] ) ) {
2195  $record->setVisibility( intval( $fields['deleted'] ) );
2196  }
2197 
2198  if ( isset( $fields['comment'] ) ) {
2199  Assert::parameterType(
2200  CommentStoreComment::class,
2201  $fields['comment'],
2202  '$row[\'comment\']'
2203  );
2204  $record->setComment( $fields['comment'] );
2205  }
2206  }
2207 
2222  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2223  wfDeprecated( __METHOD__, '1.35' );
2224  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2225  if ( $id ) {
2226  $conds['rev_id'] = intval( $id );
2227  } else {
2228  $conds[] = 'rev_id=page_latest';
2229  }
2230  return $this->loadRevisionFromConds( $db, $conds );
2231  }
2232 
2250  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2251  wfDeprecated( __METHOD__, '1.35' );
2252  if ( $id ) {
2253  $matchId = intval( $id );
2254  } else {
2255  $matchId = 'page_latest';
2256  }
2257 
2258  return $this->loadRevisionFromConds(
2259  $db,
2260  [
2261  "rev_id=$matchId",
2262  'page_namespace' => $title->getNamespace(),
2263  'page_title' => $title->getDBkey()
2264  ],
2265  0,
2266  $title
2267  );
2268  }
2269 
2284  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2285  wfDeprecated( __METHOD__, '1.35' );
2286  return $this->loadRevisionFromConds( $db,
2287  [
2288  'rev_timestamp' => $db->timestamp( $timestamp ),
2289  'page_namespace' => $title->getNamespace(),
2290  'page_title' => $title->getDBkey()
2291  ],
2292  0,
2293  $title
2294  );
2295  }
2296 
2313  private function newRevisionFromConds(
2314  array $conditions,
2315  int $flags = IDBAccessObject::READ_NORMAL,
2316  Title $title = null,
2317  array $options = []
2318  ) {
2319  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2320  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title, $options );
2321 
2322  $lb = $this->getDBLoadBalancer();
2323 
2324  // Make sure new pending/committed revision are visibile later on
2325  // within web requests to certain avoid bugs like T93866 and T94407.
2326  if ( !$rev
2327  && !( $flags & self::READ_LATEST )
2328  && $lb->hasStreamingReplicaServers()
2329  && $lb->hasOrMadeRecentMasterChanges()
2330  ) {
2331  $flags = self::READ_LATEST;
2332  $dbw = $this->getDBConnectionRef( DB_MASTER );
2333  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title, $options );
2334  }
2335 
2336  return $rev;
2337  }
2338 
2353  private function loadRevisionFromConds(
2354  IDatabase $db,
2355  array $conditions,
2356  int $flags = IDBAccessObject::READ_NORMAL,
2357  Title $title = null,
2358  array $options = []
2359  ) {
2360  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2361  if ( $row ) {
2362  return $this->newRevisionFromRow( $row, $flags, $title );
2363  }
2364 
2365  return null;
2366  }
2367 
2375  private function checkDatabaseDomain( IDatabase $db ) {
2376  $dbDomain = $db->getDomainID();
2377  $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2378  if ( $dbDomain === $storeDomain ) {
2379  return;
2380  }
2381 
2382  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2383  }
2384 
2398  private function fetchRevisionRowFromConds(
2399  IDatabase $db,
2400  array $conditions,
2401  int $flags = IDBAccessObject::READ_NORMAL,
2402  array $options = []
2403  ) {
2404  $this->checkDatabaseDomain( $db );
2405 
2406  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2407  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2408  $options[] = 'FOR UPDATE';
2409  }
2410  return $db->selectRow(
2411  $revQuery['tables'],
2412  $revQuery['fields'],
2413  $conditions,
2414  __METHOD__,
2415  $options,
2416  $revQuery['joins']
2417  );
2418  }
2419 
2441  public function getQueryInfo( $options = [] ) {
2442  $ret = [
2443  'tables' => [],
2444  'fields' => [],
2445  'joins' => [],
2446  ];
2447 
2448  $ret['tables'][] = 'revision';
2449  $ret['fields'] = array_merge( $ret['fields'], [
2450  'rev_id',
2451  'rev_page',
2452  'rev_timestamp',
2453  'rev_minor_edit',
2454  'rev_deleted',
2455  'rev_len',
2456  'rev_parent_id',
2457  'rev_sha1',
2458  ] );
2459 
2460  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2461  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2462  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2463  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2464 
2465  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2466  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2467  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2468  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2469 
2470  if ( in_array( 'page', $options, true ) ) {
2471  $ret['tables'][] = 'page';
2472  $ret['fields'] = array_merge( $ret['fields'], [
2473  'page_namespace',
2474  'page_title',
2475  'page_id',
2476  'page_latest',
2477  'page_is_redirect',
2478  'page_len',
2479  ] );
2480  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2481  }
2482 
2483  if ( in_array( 'user', $options, true ) ) {
2484  $ret['tables'][] = 'user';
2485  $ret['fields'] = array_merge( $ret['fields'], [
2486  'user_name',
2487  ] );
2488  $u = $actorQuery['fields']['rev_user'];
2489  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2490  }
2491 
2492  if ( in_array( 'text', $options, true ) ) {
2493  throw new InvalidArgumentException(
2494  'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2495  );
2496  }
2497 
2498  return $ret;
2499  }
2500 
2521  public function getSlotsQueryInfo( $options = [] ) {
2522  $ret = [
2523  'tables' => [],
2524  'fields' => [],
2525  'joins' => [],
2526  'keys' => [],
2527  ];
2528 
2529  $ret['keys']['rev_id'] = 'slot_revision_id';
2530  $ret['keys']['role_id'] = 'slot_role_id';
2531 
2532  $ret['tables'][] = 'slots';
2533  $ret['fields'] = array_merge( $ret['fields'], [
2534  'slot_revision_id',
2535  'slot_content_id',
2536  'slot_origin',
2537  'slot_role_id',
2538  ] );
2539 
2540  if ( in_array( 'role', $options, true ) ) {
2541  // Use left join to attach role name, so we still find the revision row even
2542  // if the role name is missing. This triggers a more obvious failure mode.
2543  $ret['tables'][] = 'slot_roles';
2544  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2545  $ret['fields'][] = 'role_name';
2546  }
2547 
2548  if ( in_array( 'content', $options, true ) ) {
2549  $ret['keys']['model_id'] = 'content_model';
2550 
2551  $ret['tables'][] = 'content';
2552  $ret['fields'] = array_merge( $ret['fields'], [
2553  'content_size',
2554  'content_sha1',
2555  'content_address',
2556  'content_model',
2557  ] );
2558  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2559 
2560  if ( in_array( 'model', $options, true ) ) {
2561  // Use left join to attach model name, so we still find the revision row even
2562  // if the model name is missing. This triggers a more obvious failure mode.
2563  $ret['tables'][] = 'content_models';
2564  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2565  $ret['fields'][] = 'model_name';
2566  }
2567 
2568  }
2569 
2570  return $ret;
2571  }
2572 
2586  public function getArchiveQueryInfo() {
2587  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2588  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2589  $ret = [
2590  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2591  'fields' => [
2592  'ar_id',
2593  'ar_page_id',
2594  'ar_namespace',
2595  'ar_title',
2596  'ar_rev_id',
2597  'ar_timestamp',
2598  'ar_minor_edit',
2599  'ar_deleted',
2600  'ar_len',
2601  'ar_parent_id',
2602  'ar_sha1',
2603  ] + $commentQuery['fields'] + $actorQuery['fields'],
2604  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2605  ];
2606 
2607  return $ret;
2608  }
2609 
2619  public function getRevisionSizes( array $revIds ) {
2620  $dbr = $this->getDBConnectionRef( DB_REPLICA );
2621  $revLens = [];
2622  if ( !$revIds ) {
2623  return $revLens; // empty
2624  }
2625 
2626  $res = $dbr->select(
2627  'revision',
2628  [ 'rev_id', 'rev_len' ],
2629  [ 'rev_id' => $revIds ],
2630  __METHOD__
2631  );
2632 
2633  foreach ( $res as $row ) {
2634  $revLens[$row->rev_id] = intval( $row->rev_len );
2635  }
2636 
2637  return $revLens;
2638  }
2639 
2652  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2653  wfDeprecated( __METHOD__, '1.35' );
2654  return $this->getRevisionSizes( $revIds );
2655  }
2656 
2665  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2666  $op = $dir === 'next' ? '>' : '<';
2667  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2668 
2669  if ( !$rev->getId() || !$rev->getPageId() ) {
2670  // revision is unsaved or otherwise incomplete
2671  return null;
2672  }
2673 
2674  if ( $rev instanceof RevisionArchiveRecord ) {
2675  // revision is deleted, so it's not part of the page history
2676  return null;
2677  }
2678 
2679  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2680  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2681 
2682  $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2683  if ( $ts === false ) {
2684  // XXX Should this be moved into getTimestampFromId?
2685  $ts = $db->selectField( 'archive', 'ar_timestamp',
2686  [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2687  if ( $ts === false ) {
2688  // XXX Is this reachable? How can we have a page id but no timestamp?
2689  return null;
2690  }
2691  }
2692  $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2693 
2694  $revId = $db->selectField( 'revision', 'rev_id',
2695  [
2696  'rev_page' => $rev->getPageId(),
2697  "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op {$rev->getId()})"
2698  ],
2699  __METHOD__,
2700  [
2701  'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2702  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2703  ]
2704  );
2705 
2706  if ( $revId === false ) {
2707  return null;
2708  }
2709 
2710  return $this->getRevisionById( intval( $revId ) );
2711  }
2712 
2728  public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
2729  if ( $flags instanceof Title ) {
2730  // Old calling convention, we don't use Title here anymore
2731  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2732  $flags = 0;
2733  }
2734 
2735  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2736  }
2737 
2751  public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
2752  if ( $flags instanceof Title ) {
2753  // Old calling convention, we don't use Title here anymore
2754  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2755  $flags = 0;
2756  }
2757 
2758  return $this->getRelativeRevision( $rev, $flags, 'next' );
2759  }
2760 
2772  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2773  $this->checkDatabaseDomain( $db );
2774 
2775  if ( $rev->getPageId() === null ) {
2776  return 0;
2777  }
2778  # Use page_latest if ID is not given
2779  if ( !$rev->getId() ) {
2780  $prevId = $db->selectField(
2781  'page', 'page_latest',
2782  [ 'page_id' => $rev->getPageId() ],
2783  __METHOD__
2784  );
2785  } else {
2786  $prevId = $db->selectField(
2787  'revision', 'rev_id',
2788  [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
2789  __METHOD__,
2790  [ 'ORDER BY' => 'rev_id DESC' ]
2791  );
2792  }
2793  return intval( $prevId );
2794  }
2795 
2808  public function getTimestampFromId( $id, $flags = 0 ) {
2809  if ( $id instanceof Title ) {
2810  // Old deprecated calling convention supported for backwards compatibility
2811  $id = $flags;
2812  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2813  }
2814  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2815 
2816  $timestamp =
2817  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2818 
2819  return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2820  }
2821 
2831  public function countRevisionsByPageId( IDatabase $db, $id ) {
2832  $this->checkDatabaseDomain( $db );
2833 
2834  $row = $db->selectRow( 'revision',
2835  [ 'revCount' => 'COUNT(*)' ],
2836  [ 'rev_page' => $id ],
2837  __METHOD__
2838  );
2839  if ( $row ) {
2840  return intval( $row->revCount );
2841  }
2842  return 0;
2843  }
2844 
2854  public function countRevisionsByTitle( IDatabase $db, $title ) {
2855  $id = $title->getArticleID();
2856  if ( $id ) {
2857  return $this->countRevisionsByPageId( $db, $id );
2858  }
2859  return 0;
2860  }
2861 
2880  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2881  $this->checkDatabaseDomain( $db );
2882 
2883  if ( !$userId ) {
2884  return false;
2885  }
2886 
2887  $revQuery = $this->getQueryInfo();
2888  $res = $db->select(
2889  $revQuery['tables'],
2890  [
2891  'rev_user' => $revQuery['fields']['rev_user'],
2892  ],
2893  [
2894  'rev_page' => $pageId,
2895  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2896  ],
2897  __METHOD__,
2898  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2899  $revQuery['joins']
2900  );
2901  foreach ( $res as $row ) {
2902  if ( $row->rev_user != $userId ) {
2903  return false;
2904  }
2905  }
2906  return true;
2907  }
2908 
2922  public function getKnownCurrentRevision( Title $title, $revId = 0 ) {
2923  $db = $this->getDBConnectionRef( DB_REPLICA );
2924 
2925  $revIdPassed = $revId;
2926  $pageId = $title->getArticleID();
2927 
2928  if ( !$pageId ) {
2929  return false;
2930  }
2931 
2932  if ( !$revId ) {
2933  $revId = $title->getLatestRevID();
2934  }
2935 
2936  if ( !$revId ) {
2937  wfWarn(
2938  'No latest revision known for page ' . $title->getPrefixedDBkey()
2939  . ' even though it exists with page ID ' . $pageId
2940  );
2941  return false;
2942  }
2943 
2944  // Load the row from cache if possible. If not possible, populate the cache.
2945  // As a minor optimization, remember if this was a cache hit or miss.
2946  // We can sometimes avoid a database query later if this is a cache miss.
2947  $fromCache = true;
2948  $row = $this->cache->getWithSetCallback(
2949  // Page/rev IDs passed in from DB to reflect history merges
2950  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2951  WANObjectCache::TTL_WEEK,
2952  function ( $curValue, &$ttl, array &$setOpts ) use (
2953  $db, $revId, &$fromCache
2954  ) {
2955  $setOpts += Database::getCacheSetOptions( $db );
2956  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2957  if ( $row ) {
2958  $fromCache = false;
2959  }
2960  return $row; // don't cache negatives
2961  }
2962  );
2963 
2964  // Reflect revision deletion and user renames.
2965  if ( $row ) {
2966  $this->ensureRevisionRowMatchesTitle( $row, $title, [
2967  'from_cache_flag' => $fromCache,
2968  'page_id_initial' => $pageId,
2969  'rev_id_used' => $revId,
2970  'rev_id_requested' => $revIdPassed,
2971  ] );
2972 
2973  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2974  } else {
2975  return false;
2976  }
2977  }
2978 
2987  public function getFirstRevision(
2988  LinkTarget $title,
2989  int $flags = IDBAccessObject::READ_NORMAL
2990  ): ?RevisionRecord {
2991  $titleObj = Title::newFromLinkTarget( $title ); // TODO: eventually we shouldn't need a title
2992  return $this->newRevisionFromConds(
2993  [
2994  'page_namespace' => $title->getNamespace(),
2995  'page_title' => $title->getDBkey()
2996  ],
2997  $flags,
2998  $titleObj,
2999  [
3000  'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
3001  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
3002  ]
3003  );
3004  }
3005 
3017  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3018  return $this->cache->makeGlobalKey(
3019  self::ROW_CACHE_KEY,
3020  $db->getDomainID(),
3021  $pageId,
3022  $revId
3023  );
3024  }
3025 
3033  private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
3034  if ( $rev ) {
3035  if ( $rev->getId() === null ) {
3036  throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
3037  }
3038  if ( $rev->getPageId() !== $pageId ) {
3039  throw new InvalidArgumentException(
3040  "Revision {$rev->getId()} doesn't belong to page {$pageId}"
3041  );
3042  }
3043  }
3044  }
3045 
3060  private function getRevisionLimitConditions(
3061  IDatabase $dbr,
3062  RevisionRecord $old = null,
3063  RevisionRecord $new = null,
3064  $options = []
3065  ) {
3066  $options = (array)$options;
3067  $oldCmp = '>';
3068  $newCmp = '<';
3069  if ( in_array( self::INCLUDE_OLD, $options ) ) {
3070  $oldCmp = '>=';
3071  }
3072  if ( in_array( self::INCLUDE_NEW, $options ) ) {
3073  $newCmp = '<=';
3074  }
3075  if ( in_array( self::INCLUDE_BOTH, $options ) ) {
3076  $oldCmp = '>=';
3077  $newCmp = '<=';
3078  }
3079 
3080  $conds = [];
3081  if ( $old ) {
3082  $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3083  $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId()}) " .
3084  "OR rev_timestamp > {$oldTs}";
3085  }
3086  if ( $new ) {
3087  $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3088  $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId()}) " .
3089  "OR rev_timestamp < {$newTs}";
3090  }
3091  return $conds;
3092  }
3093 
3120  public function getRevisionIdsBetween(
3121  int $pageId,
3122  RevisionRecord $old = null,
3123  RevisionRecord $new = null,
3124  ?int $max = null,
3125  $options = [],
3126  ?string $order = null,
3127  int $flags = IDBAccessObject::READ_NORMAL
3128  ) : array {
3129  $this->assertRevisionParameter( 'old', $pageId, $old );
3130  $this->assertRevisionParameter( 'new', $pageId, $new );
3131 
3132  $options = (array)$options;
3133  $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3134  in_array( self::INCLUDE_BOTH, $options );
3135  $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3136  in_array( self::INCLUDE_BOTH, $options );
3137 
3138  // No DB query needed if old and new are the same revision.
3139  // Can't check for consecutive revisions with 'getParentId' for a similar
3140  // optimization as edge cases exist when there are revisions between
3141  // a revision and it's parent. See T185167 for more details.
3142  if ( $old && $new && $new->getId() === $old->getId() ) {
3143  return $includeOld || $includeNew ? [ $new->getId() ] : [];
3144  }
3145 
3146  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3147  $conds = array_merge(
3148  [
3149  'rev_page' => $pageId,
3150  $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3151  ],
3152  $this->getRevisionLimitConditions( $db, $old, $new, $options )
3153  );
3154 
3155  $queryOptions = [];
3156  if ( $order !== null ) {
3157  $queryOptions['ORDER BY'] = [ "rev_timestamp $order", "rev_id $order" ];
3158  }
3159  if ( $max !== null ) {
3160  $queryOptions['LIMIT'] = $max + 1; // extra to detect truncation
3161  }
3162 
3163  $values = $db->selectFieldValues(
3164  'revision',
3165  'rev_id',
3166  $conds,
3167  __METHOD__,
3168  $queryOptions
3169  );
3170  return array_map( 'intval', $values );
3171  }
3172 
3194  public function getAuthorsBetween(
3195  $pageId,
3196  RevisionRecord $old = null,
3197  RevisionRecord $new = null,
3198  User $user = null,
3199  $max = null,
3200  $options = []
3201  ) {
3202  $this->assertRevisionParameter( 'old', $pageId, $old );
3203  $this->assertRevisionParameter( 'new', $pageId, $new );
3204  $options = (array)$options;
3205 
3206  // No DB query needed if old and new are the same revision.
3207  // Can't check for consecutive revisions with 'getParentId' for a similar
3208  // optimization as edge cases exist when there are revisions between
3209  //a revision and it's parent. See T185167 for more details.
3210  if ( $old && $new && $new->getId() === $old->getId() ) {
3211  if ( empty( $options ) ) {
3212  return [];
3213  } else {
3214  return $user ? [ $new->getUser( RevisionRecord::FOR_PUBLIC, $user ) ] : [ $new->getUser() ];
3215  }
3216  }
3217 
3218  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3219  $conds = array_merge(
3220  [
3221  'rev_page' => $pageId,
3222  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3223  ],
3224  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3225  );
3226 
3227  $queryOpts = [ 'DISTINCT' ];
3228  if ( $max !== null ) {
3229  $queryOpts['LIMIT'] = $max + 1;
3230  }
3231 
3232  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3233  return array_map( function ( $row ) {
3234  return new UserIdentityValue( (int)$row->rev_user, $row->rev_user_text, (int)$row->rev_actor );
3235  }, iterator_to_array( $dbr->select(
3236  array_merge( [ 'revision' ], $actorQuery['tables'] ),
3237  $actorQuery['fields'],
3238  $conds, __METHOD__,
3239  $queryOpts,
3240  $actorQuery['joins']
3241  ) ) );
3242  }
3243 
3265  public function countAuthorsBetween(
3266  $pageId,
3267  RevisionRecord $old = null,
3268  RevisionRecord $new = null,
3269  User $user = null,
3270  $max = null,
3271  $options = []
3272  ) {
3273  // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3274  // and creation of UserIdentity stuff.
3275  return count( $this->getAuthorsBetween( $pageId, $old, $new, $user, $max, $options ) );
3276  }
3277 
3298  public function countRevisionsBetween(
3299  $pageId,
3300  RevisionRecord $old = null,
3301  RevisionRecord $new = null,
3302  $max = null,
3303  $options = []
3304  ) {
3305  $this->assertRevisionParameter( 'old', $pageId, $old );
3306  $this->assertRevisionParameter( 'new', $pageId, $new );
3307 
3308  // No DB query needed if old and new are the same revision.
3309  // Can't check for consecutive revisions with 'getParentId' for a similar
3310  // optimization as edge cases exist when there are revisions between
3311  //a revision and it's parent. See T185167 for more details.
3312  if ( $old && $new && $new->getId() === $old->getId() ) {
3313  return 0;
3314  }
3315 
3316  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3317  $conds = array_merge(
3318  [
3319  'rev_page' => $pageId,
3320  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3321  ],
3322  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3323  );
3324  if ( $max !== null ) {
3325  return $dbr->selectRowCount( 'revision', '1',
3326  $conds,
3327  __METHOD__,
3328  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3329  );
3330  } else {
3331  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3332  }
3333  }
3334 
3335  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3336 }
3337 
3342 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Revision\RevisionStore\ORDER_OLDEST_TO_NEWEST
const ORDER_OLDEST_TO_NEWEST
Definition: RevisionStore.php:84
Revision\MutableRevisionRecord\setMinorEdit
setMinorEdit( $minorEdit)
Definition: MutableRevisionRecord.php:275
Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:115
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:34
Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:125
Revision\RevisionStore\ensureRevisionRowMatchesTitle
ensureRevisionRowMatchesTitle( $row, Title $title, $context=[])
Check that the given row matches the given Title object.
Definition: RevisionStore.php:1608
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:34
Revision\RevisionStore\$hookContainer
HookContainer $hookContainer
Definition: RevisionStore.php:144
MediaWiki\Storage\BlobStore\PAGE_HINT
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with.
Definition: BlobStore.php:48
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:50
Revision\RevisionStore\insertSlotOn
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:557
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:34
Revision\RevisionStore\checkDatabaseDomain
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
Definition: RevisionStore.php:2375
MediaWiki\Storage\BlobStore\DESIGNATION_HINT
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition: BlobStore.php:42
MediaWiki\Storage\BlobAccessException
Exception representing a failure to access a data blob.
Definition: BlobAccessException.php:33
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:43
Revision\RevisionStore\getRecentChange
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
Definition: RevisionStore.php:983
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:32
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:295
User\getId
getId()
Get the user's ID.
Definition: User.php:2024
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:428
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:2037
Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:222
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:2922
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:220
Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:344
Revision\MutableRevisionRecord\setSha1
setSha1( $sha1)
Set revision hash, for optimization.
Definition: MutableRevisionRecord.php:221
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:95
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:80
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:169
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:73
Revision\RevisionStore\initializeMutableRevisionFromArray
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
Definition: RevisionStore.php:2131
Revision\RevisionStoreCacheRecord
A cached RevisionStoreRecord.
Definition: RevisionStoreCacheRecord.php:38
Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, Title $title)
Definition: RevisionStore.php:1222
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:439
Revision\RevisionStore\getSlotRowsForBatch
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
Definition: RevisionStore.php:1874
Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2586
Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:56
Revision\MutableRevisionRecord\setPageId
setPageId( $pageId)
Definition: MutableRevisionRecord.php:319
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:434
Revision\RevisionStore\INCLUDE_NEW
const INCLUDE_NEW
Definition: RevisionStore.php:89
CommentStore
Handle database storage of comments such as edit summaries and log reasons.
Definition: CommentStore.php:42
Revision\RevisionStore\$cache
WANObjectCache $cache
Definition: RevisionStore.php:110
Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:2808
Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:183
Revision\RevisionStore\getRcIdIfUnpatrolled
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
Definition: RevisionStore.php:961
Revision\RevisionFactory
Service for constructing revision objects.
Definition: RevisionFactory.php:38
Revision\MutableRevisionRecord\setId
setId( $id)
Set the revision ID.
Definition: MutableRevisionRecord.php:295
Revision\RevisionStore\checkContent
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
Definition: RevisionStore.php:844
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
Revision\SlotRecord\getRevision
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:389
ActorMigration
This class handles the logic for the actor table migration and should always be used in lieu of direc...
Definition: ActorMigration.php:41
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:3033
Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2521
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:120
$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:595
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:233
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:2652
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:2880
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\Revision
Definition: ContributionsLookup.php:3
Revision
Definition: Revision.php:40
Revision\RevisionStore\getRevisionLimitConditions
getRevisionLimitConditions(IDatabase $dbr, RevisionRecord $old=null, RevisionRecord $new=null, $options=[])
Converts revision limits to query conditions.
Definition: RevisionStore.php:3060
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:1663
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:1015
Revision\RevisionStore\newRevisionFromArchiveRowAndSlots
newRevisionFromArchiveRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, array $overrides=[])
Definition: RevisionStore.php:1438
Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:398
Revision\RevisionStore\getQueryInfo
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
Definition: RevisionStore.php:2441
MWException
MediaWiki exception.
Definition: MWException.php:29
Revision\RevisionRecord\getSize
getSize()
Returns the nominal size of this revision, in bogo-bytes.
Revision\RevisionStore\ORDER_NEWEST_TO_OLDEST
const ORDER_NEWEST_TO_OLDEST
Definition: RevisionStore.php:85
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:309
Revision\RevisionRecord\getSha1
getSha1()
Returns the base36 sha1 of this revision.
Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:771
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1027
Revision\RevisionStore\INCLUDE_BOTH
const INCLUDE_BOTH
Definition: RevisionStore.php:90
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:365
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
Wikimedia\Rdbms\Database\getCacheSetOptions
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4923
Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1091
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:523
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:1159
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:2222
$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:363
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:482
Revision\MutableRevisionRecord\setVisibility
setVisibility( $visibility)
Definition: MutableRevisionRecord.php:251
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:462
MediaWiki\Storage\BlobStore\MODEL_HINT
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob.
Definition: BlobStore.php:78
Revision\RevisionStore\newRevisionSlots
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, Title $title)
Factory method for RevisionSlots based on a revision ID.
Definition: RevisionStore.php:1348
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:2772
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:401
Revision\RevisionStore\$dbDomain
bool string $dbDomain
Definition: RevisionStore.php:100
Revision\RevisionRecord\isReadyForInsertion
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
Definition: RevisionRecord.php:555
Revision\RevisionStore\insertSlotRowOn
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
Definition: RevisionStore.php:805
Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2619
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:61
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:559
$title
$title
Definition: testCompression.php:38
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:591
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:492
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:643
Revision\RevisionRecord\getSlotRoles
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
Definition: RevisionRecord.php:210
DB_MASTER
const DB_MASTER
Definition: defines.php:26
Revision\RevisionRecord\getPageId
getPageId()
Get the page ID.
Definition: RevisionRecord.php:323
FallbackContent
Content object implementation representing unknown content.
Definition: FallbackContent.php:38
Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:95
Revision\RevisionStore\getPreviousRevision
getPreviousRevision(RevisionRecord $rev, $flags=0)
Get the revision before $rev in the page's history, if any.
Definition: RevisionStore.php:2728
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
Revision\RevisionStore\setLogger
setLogger(LoggerInterface $logger)
Definition: RevisionStore.php:199
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:531
Revision\RevisionStore\constructSlotRecords
constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
Definition: RevisionStore.php:1270
Revision\RevisionStore\ROW_CACHE_KEY
const ROW_CACHE_KEY
Definition: RevisionStore.php:82
Revision\RevisionStore\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: RevisionStore.php:138
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:271
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:287
Revision\RevisionStore\getFirstRevision
getFirstRevision(LinkTarget $title, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
Definition: RevisionStore.php:2987
$content
$content
Definition: router.php:76
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:515
Revision\RevisionRecord\DELETED_USER
const DELETED_USER
Definition: RevisionRecord.php:52
DBAccessObjectUtils\hasFlags
static hasFlags( $bitfield, $flags)
Definition: DBAccessObjectUtils.php:35
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:45
Revision\RevisionStore\getNextRevision
getNextRevision(RevisionRecord $rev, $flags=0)
Get the revision after $rev in the page's history, if any.
Definition: RevisionStore.php:2751
Revision\RevisionStore\getRevisionIdsBetween
getRevisionIdsBetween(int $pageId, RevisionRecord $old=null, RevisionRecord $new=null, ?int $max=null, $options=[], ?string $order=null, int $flags=IDBAccessObject::READ_NORMAL)
Get IDs of revisions between the given revisions.
Definition: RevisionStore.php:3120
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:125
Revision\RevisionStore\$slotRoleStore
NameTableStore $slotRoleStore
Definition: RevisionStore.php:135
Revision\RevisionStore\getContentBlobsForBatch
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
Definition: RevisionStore.php:1990
Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2854
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:388
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:497
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:2665
Revision\RevisionStore\$hookRunner
HookRunner $hookRunner
Definition: RevisionStore.php:147
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
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:1111
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:280
MediaWiki\Storage\BlobStore
Service for loading and storing data blobs.
Definition: BlobStore.php:35
Content
Base interface for content objects.
Definition: Content.php:35
Revision\RevisionStore\newNullRevision
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page's history.
Definition: RevisionStore.php:889
Revision\RevisionRecord\FOR_PUBLIC
const FOR_PUBLIC
Definition: RevisionRecord.php:59
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:423
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:2398
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:343
Title
Represents a title within MediaWiki.
Definition: Title.php:41
Revision\RevisionStore\insertContentRowOn
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
Definition: RevisionStore.php:823
Revision\RevisionStore\countRevisionsByPageId
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2831
Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
Definition: RevisionStore.php:480
Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:130
Revision\SlotRecord\newSaved
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
Definition: SlotRecord.php:166
Revision\RevisionStore\INCLUDE_OLD
const INCLUDE_OLD
Definition: RevisionStore.php:88
Revision\RevisionStore\getDBLoadBalancer
getDBLoadBalancer()
Definition: RevisionStore.php:213
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:1388
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:82
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:50
Revision\RevisionSlots
Value object representing the set of slots belonging to a revision.
Definition: RevisionSlots.php:41
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:161
MediaWiki\Storage\BlobStore\REVISION_HINT
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition: BlobStore.php:60
wfBacktrace
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
Definition: GlobalFunctions.php:1360
Revision\RevisionStore\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: RevisionStore.php:141
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:3194
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:2313
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
MWUnknownContentModelException
Exception thrown when an unregistered content model is requested.
Definition: MWUnknownContentModelException.php:11
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1074
Revision\MutableRevisionRecord\setTimestamp
setTimestamp( $timestamp)
Definition: MutableRevisionRecord.php:263
Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:327
Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:622
Revision\MutableRevisionRecord\setSize
setSize( $size)
Set nominal revision size, for optimization.
Definition: MutableRevisionRecord.php:239
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:567
$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:1519
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:2284
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:506
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:3265
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
Title\newFromID
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:472
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:2353
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2053
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:3017
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
Revision\MutableRevisionRecord\setComment
setComment(CommentStoreComment $comment)
Definition: MutableRevisionRecord.php:205
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:1198
Revision\RevisionStore\getBaseRevisionRow
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:738
Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, Title $title=null, $fromCache=false)
Definition: RevisionStore.php:1410
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Revision\RevisionStore\loadRevisionFromTitle
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
Definition: RevisionStore.php:2250
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:3298
Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:105
Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:206
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:252