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;
37 use LogicException;
55 use Message;
56 use MWException;
57 use MWTimestamp;
59 use Psr\Log\LoggerAwareInterface;
60 use Psr\Log\LoggerInterface;
61 use Psr\Log\NullLogger;
62 use RecentChange;
63 use Revision;
64 use RuntimeException;
65 use StatusValue;
66 use stdClass;
67 use Title;
68 use TitleFactory;
69 use Traversable;
70 use WANObjectCache;
71 use Wikimedia\Assert\Assert;
72 use Wikimedia\IPUtils;
78 
89  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
90 
92 
93  public const ROW_CACHE_KEY = 'revision-row-1.29';
94 
95  public const ORDER_OLDEST_TO_NEWEST = 'ASC';
96  public const ORDER_NEWEST_TO_OLDEST = 'DESC';
97 
98  // Constants for get(...)Between methods
99  public const INCLUDE_OLD = 'include_old';
100  public const INCLUDE_NEW = 'include_new';
101  public const INCLUDE_BOTH = 'include_both';
102 
106  private $blobStore;
107 
111  private $wikiId;
112 
116  private $loadBalancer;
117 
121  private $cache;
122 
126  private $commentStore;
127 
132 
134  private $actorStore;
135 
139  private $logger;
140 
145 
149  private $slotRoleStore;
150 
153 
156 
158  private $hookContainer;
159 
161  private $hookRunner;
162 
166  private $titleFactory;
167 
191  public function __construct(
192  ILoadBalancer $loadBalancer,
193  SqlBlobStore $blobStore,
196  NameTableStore $contentModelStore,
197  NameTableStore $slotRoleStore,
200  ActorStore $actorStore,
201  IContentHandlerFactory $contentHandlerFactory,
203  HookContainer $hookContainer,
204  $wikiId = WikiAwareEntity::LOCAL
205  ) {
206  Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
207 
208  $this->loadBalancer = $loadBalancer;
209  $this->blobStore = $blobStore;
210  $this->cache = $cache;
211  $this->commentStore = $commentStore;
212  $this->contentModelStore = $contentModelStore;
213  $this->slotRoleStore = $slotRoleStore;
214  $this->slotRoleRegistry = $slotRoleRegistry;
215  $this->actorMigration = $actorMigration;
216  $this->actorStore = $actorStore;
217  $this->wikiId = $wikiId;
218  $this->logger = new NullLogger();
219  $this->contentHandlerFactory = $contentHandlerFactory;
220  $this->titleFactory = $titleFactory;
221  $this->hookContainer = $hookContainer;
222  $this->hookRunner = new HookRunner( $hookContainer );
223  }
224 
225  public function setLogger( LoggerInterface $logger ) {
226  $this->logger = $logger;
227  }
228 
232  public function isReadOnly() {
233  return $this->blobStore->isReadOnly();
234  }
235 
239  private function getDBLoadBalancer() {
240  return $this->loadBalancer;
241  }
242 
248  public function getWikiId() {
249  return $this->wikiId;
250  }
251 
257  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
258  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
259  return $this->getDBConnectionRef( $mode );
260  }
261 
268  private function getDBConnectionRef( $mode, $groups = [] ) {
269  $lb = $this->getDBLoadBalancer();
270  return $lb->getConnectionRef( $mode, $groups, $this->wikiId );
271  }
272 
289  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
290  // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069
291  if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
292  wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' );
293  }
294 
295  $page = $this->getPage( $pageId, $revId, $queryFlags );
296  return $this->titleFactory->castFromPageIdentity( $page );
297  }
298 
309  private function getPage( ?int $pageId, ?int $revId, int $queryFlags = self::READ_NORMAL ) {
310  if ( !$pageId && !$revId ) {
311  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
312  }
313 
314  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
315  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
316  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
317  $queryFlags = self::READ_NORMAL;
318  }
319 
320  $canUsePageId = ( $pageId !== null && $pageId > 0 );
321  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
322 
323  // Loading by ID is best
324  if ( $canUsePageId ) {
325  // TODO: use PageStore once we have that, return a PageRecord! T195069
326  $dbr = $this->getDBConnectionRef( $dbMode );
327  $row = $dbr->selectRow(
328  'page',
329  [
330  'page_namespace',
331  'page_title',
332  'page_id',
333  'page_latest',
334  'page_is_redirect',
335  'page_len',
336  ],
337  [ 'page_id' => $pageId ],
338  __METHOD__,
339  $dbOptions
340  );
341  if ( $row ) {
342  return $this->newPageFromRow( $row );
343  }
344  }
345 
346  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
347  $canUseRevId = ( $revId !== null && $revId > 0 );
348 
349  if ( $canUseRevId ) {
350  // TODO: use PageStore once we have that, return a PageRecord! T195069
351  $dbr = $this->getDBConnectionRef( $dbMode );
352  $row = $dbr->selectRow(
353  [ 'revision', 'page' ],
354  [
355  'page_namespace',
356  'page_title',
357  'page_id',
358  'page_latest',
359  'page_is_redirect',
360  'page_len',
361  ],
362  [ 'rev_id' => $revId ],
363  __METHOD__,
364  $dbOptions,
365  [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
366  );
367  if ( $row ) {
368  return $this->newPageFromRow( $row );
369  }
370  }
371 
372  // If we still don't have a title, fallback to master if that wasn't already happening.
373  if ( $dbMode !== DB_MASTER ) {
374  $title = $this->getPage( $pageId, $revId, self::READ_LATEST );
375  if ( $title ) {
376  $this->logger->info(
377  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
378  [ 'trace' => wfBacktrace() ]
379  );
380  return $title;
381  }
382  }
383 
384  throw new RevisionAccessException(
385  "Could not determine title for page ID $pageId and revision ID $revId"
386  );
387  }
388 
394  private function newPageFromRow( stdClass $row ): PageIdentity {
395  if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
396  // NOTE: since there is still a lot of code that needs a full Title,
397  // and uses Title::castFromPageIdentity() to get one, it's beneficial
398  // to create a Title right away if we can, so we don't have to convert
399  // over and over later on.
400  // When there is less need to convert to Title, this special case can
401  // be removed.
402  return $this->titleFactory->newFromRow( $row );
403  } else {
404  return new PageIdentityValue(
405  (int)$row->page_id,
406  (int)$row->page_namespace,
407  $row->page_title,
408  $this->wikiId
409  );
410  }
411  }
412 
420  private function failOnNull( $value, $name ) {
421  if ( $value === null ) {
422  throw new IncompleteRevisionException(
423  "$name must not be " . var_export( $value, true ) . "!"
424  );
425  }
426 
427  return $value;
428  }
429 
437  private function failOnEmpty( $value, $name ) {
438  if ( $value === null || $value === 0 || $value === '' ) {
439  throw new IncompleteRevisionException(
440  "$name must not be " . var_export( $value, true ) . "!"
441  );
442  }
443 
444  return $value;
445  }
446 
458  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
459  // TODO: pass in a DBTransactionContext instead of a database connection.
460  $this->checkDatabaseDomain( $dbw );
461 
462  $slotRoles = $rev->getSlotRoles();
463 
464  // Make sure the main slot is always provided throughout migration
465  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
466  throw new IncompleteRevisionException(
467  'main slot must be provided'
468  );
469  }
470 
471  // Checks
472  $this->failOnNull( $rev->getSize(), 'size field' );
473  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
474  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
475  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
476  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
477  $this->failOnNull( $user->getId(), 'user field' );
478  $this->failOnEmpty( $user->getName(), 'user_text field' );
479 
480  if ( !$rev->isReadyForInsertion() ) {
481  // This is here for future-proofing. At the time this check being added, it
482  // was redundant to the individual checks above.
483  throw new IncompleteRevisionException( 'Revision is incomplete' );
484  }
485 
486  if ( $slotRoles == [ SlotRecord::MAIN ] ) {
487  // T239717: If the main slot is the only slot, make sure the revision's nominal size
488  // and hash match the main slot's nominal size and hash.
489  $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
490  Assert::precondition(
491  $mainSlot->getSize() === $rev->getSize(),
492  'The revisions\'s size must match the main slot\'s size (see T239717)'
493  );
494  Assert::precondition(
495  $mainSlot->getSha1() === $rev->getSha1(),
496  'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
497  );
498  }
499 
500  $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early
501 
502  $parentId = $rev->getParentId() === null
503  ? $this->getPreviousRevisionId( $dbw, $rev )
504  : $rev->getParentId();
505 
507  $rev = $dbw->doAtomicSection(
508  __METHOD__,
509  function ( IDatabase $dbw, $fname ) use (
510  $rev,
511  $user,
512  $comment,
513  $pageId,
514  $parentId
515  ) {
516  return $this->insertRevisionInternal(
517  $rev,
518  $dbw,
519  $user,
520  $comment,
521  $rev->getPage(),
522  $pageId,
523  $parentId
524  );
525  }
526  );
527 
528  // sanity checks
529  Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' );
530  Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' );
531  Assert::postcondition(
532  $rev->getComment( RevisionRecord::RAW ) !== null,
533  'revision must have a comment'
534  );
535  Assert::postcondition(
536  $rev->getUser( RevisionRecord::RAW ) !== null,
537  'revision must have a user'
538  );
539 
540  // Trigger exception if the main slot is missing.
541  // Technically, this could go away after MCR migration: while
542  // calling code may require a main slot to exist, RevisionStore
543  // really should not know or care about that requirement.
545 
546  foreach ( $slotRoles as $role ) {
547  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
548  Assert::postcondition(
549  $slot->getContent() !== null,
550  $role . ' slot must have content'
551  );
552  Assert::postcondition(
553  $slot->hasRevision(),
554  $role . ' slot must have a revision associated'
555  );
556  }
557 
558  $this->hookRunner->onRevisionRecordInserted( $rev );
559 
560  // Soft deprecated in 1.31, hard deprecated in 1.35
561  if ( $this->hookContainer->isRegistered( 'RevisionInsertComplete' ) ) {
562  // Only create the Revision object if its needed
563  $legacyRevision = new Revision( $rev );
564  $this->hookRunner->onRevisionInsertComplete( $legacyRevision, null, null );
565  }
566 
567  return $rev;
568  }
569 
582  public function updateSlotsOn(
583  RevisionRecord $revision,
584  RevisionSlotsUpdate $revisionSlotsUpdate,
585  IDatabase $dbw
586  ) : array {
587  $this->checkDatabaseDomain( $dbw );
588 
589  // Make sure all modified and removed slots are derived slots
590  foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
591  Assert::precondition(
592  $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
593  'Trying to modify a slot that is not derived'
594  );
595  }
596  foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) {
597  $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
598  Assert::precondition(
599  $isDerived,
600  'Trying to remove a slot that is not derived'
601  );
602  throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' );
603  }
604 
606  $slotRecords = $dbw->doAtomicSection(
607  __METHOD__,
608  function ( IDatabase $dbw, $fname ) use (
609  $revision,
610  $revisionSlotsUpdate
611  ) {
612  return $this->updateSlotsInternal(
613  $revision,
614  $revisionSlotsUpdate,
615  $dbw
616  );
617  }
618  );
619 
620  foreach ( $slotRecords as $role => $slot ) {
621  Assert::postcondition(
622  $slot->getContent() !== null,
623  $role . ' slot must have content'
624  );
625  Assert::postcondition(
626  $slot->hasRevision(),
627  $role . ' slot must have a revision associated'
628  );
629  }
630 
631  return $slotRecords;
632  }
633 
640  private function updateSlotsInternal(
641  RevisionRecord $revision,
642  RevisionSlotsUpdate $revisionSlotsUpdate,
643  IDatabase $dbw
644  ) : array {
645  $page = $revision->getPage();
646  $revId = $revision->getId( $this->wikiId );
647  $blobHints = [
648  BlobStore::PAGE_HINT => $page->getId( $this->wikiId ),
649  BlobStore::REVISION_HINT => $revId,
650  BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
651  ];
652 
653  $newSlots = [];
654  foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
655  $slot = $revisionSlotsUpdate->getModifiedSlot( $role );
656  $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
657  }
658 
659  return $newSlots;
660  }
661 
662  private function insertRevisionInternal(
663  RevisionRecord $rev,
664  IDatabase $dbw,
665  UserIdentity $user,
666  CommentStoreComment $comment,
667  PageIdentity $page,
668  $pageId,
669  $parentId
670  ) {
671  $slotRoles = $rev->getSlotRoles();
672 
673  $revisionRow = $this->insertRevisionRowOn(
674  $dbw,
675  $rev,
676  $parentId
677  );
678 
679  $revisionId = $revisionRow['rev_id'];
680 
681  $blobHints = [
682  BlobStore::PAGE_HINT => $pageId,
683  BlobStore::REVISION_HINT => $revisionId,
684  BlobStore::PARENT_HINT => $parentId,
685  ];
686 
687  $newSlots = [];
688  foreach ( $slotRoles as $role ) {
689  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
690 
691  // If the SlotRecord already has a revision ID set, this means it already exists
692  // in the database, and should already belong to the current revision.
693  // However, a slot may already have a revision, but no content ID, if the slot
694  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
695  // mode, and the respective archive row was not yet migrated to the new schema.
696  // In that case, a new slot row (and content row) must be inserted even during
697  // undeletion.
698  if ( $slot->hasRevision() && $slot->hasContentId() ) {
699  // TODO: properly abort transaction if the assertion fails!
700  Assert::parameter(
701  $slot->getRevision() === $revisionId,
702  'slot role ' . $slot->getRole(),
703  'Existing slot should belong to revision '
704  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
705  );
706 
707  // Slot exists, nothing to do, move along.
708  // This happens when restoring archived revisions.
709 
710  $newSlots[$role] = $slot;
711  } else {
712  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
713  }
714  }
715 
716  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
717 
718  $rev = new RevisionStoreRecord(
719  $page,
720  $user,
721  $comment,
722  (object)$revisionRow,
723  new RevisionSlots( $newSlots ),
724  $this->wikiId
725  );
726 
727  return $rev;
728  }
729 
738  private function insertSlotOn(
739  IDatabase $dbw,
740  $revisionId,
741  SlotRecord $protoSlot,
742  PageIdentity $page,
743  array $blobHints = []
744  ) {
745  if ( $protoSlot->hasAddress() ) {
746  $blobAddress = $protoSlot->getAddress();
747  } else {
748  $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
749  }
750 
751  $contentId = null;
752 
753  if ( $protoSlot->hasContentId() ) {
754  $contentId = $protoSlot->getContentId();
755  } else {
756  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
757  }
758 
759  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
760 
761  return SlotRecord::newSaved(
762  $revisionId,
763  $contentId,
764  $blobAddress,
765  $protoSlot
766  );
767  }
768 
776  private function insertIpChangesRow(
777  IDatabase $dbw,
778  UserIdentity $user,
779  RevisionRecord $rev,
780  $revisionId
781  ) {
782  if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
783  $ipcRow = [
784  'ipc_rev_id' => $revisionId,
785  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
786  'ipc_hex' => IPUtils::toHex( $user->getName() ),
787  ];
788  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
789  }
790  }
791 
802  private function insertRevisionRowOn(
803  IDatabase $dbw,
804  RevisionRecord $rev,
805  $parentId
806  ) {
807  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
808 
809  list( $commentFields, $commentCallback ) =
810  $this->commentStore->insertWithTempTable(
811  $dbw,
812  'rev_comment',
814  );
815  $revisionRow += $commentFields;
816 
817  list( $actorFields, $actorCallback ) =
818  $this->actorMigration->getInsertValuesWithTempTable(
819  $dbw,
820  'rev_user',
822  );
823  $revisionRow += $actorFields;
824 
825  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
826 
827  if ( !isset( $revisionRow['rev_id'] ) ) {
828  // only if auto-increment was used
829  $revisionRow['rev_id'] = intval( $dbw->insertId() );
830 
831  if ( $dbw->getType() === 'mysql' ) {
832  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
833  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
834  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
835 
836  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
837  $table = 'archive';
838  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
839  if ( $maxRevId2 >= $maxRevId ) {
840  $maxRevId = $maxRevId2;
841  $table = 'slots';
842  }
843 
844  if ( $maxRevId >= $revisionRow['rev_id'] ) {
845  $this->logger->debug(
846  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
847  . ' Trying to fix it.',
848  [
849  'revid' => $revisionRow['rev_id'],
850  'table' => $table,
851  'maxrevid' => $maxRevId,
852  ]
853  );
854 
855  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
856  throw new MWException( 'Failed to get database lock for T202032' );
857  }
858  $fname = __METHOD__;
859  $dbw->onTransactionResolution(
860  static function ( $trigger, IDatabase $dbw ) use ( $fname ) {
861  $dbw->unlock( 'fix-for-T202032', $fname );
862  },
863  __METHOD__
864  );
865 
866  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
867 
868  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
869  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
870  // inserts too, though, at least on MariaDB 10.1.29.
871  //
872  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
873  // transactions in this code path thanks to the row lock from the original ->insert() above.
874  //
875  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
876  // that's for non-MySQL DBs.
877  $row1 = $dbw->query(
878  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
879  __METHOD__
880  )->fetchObject();
881 
882  $row2 = $dbw->query(
883  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
884  . ' FOR UPDATE',
885  __METHOD__
886  )->fetchObject();
887 
888  $maxRevId = max(
889  $maxRevId,
890  $row1 ? intval( $row1->v ) : 0,
891  $row2 ? intval( $row2->v ) : 0
892  );
893 
894  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
895  // transactions will throw a duplicate key error here. It doesn't seem worth trying
896  // to avoid that.
897  $revisionRow['rev_id'] = $maxRevId + 1;
898  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
899  }
900  }
901  }
902 
903  $commentCallback( $revisionRow['rev_id'] );
904  $actorCallback( $revisionRow['rev_id'], $revisionRow );
905 
906  return $revisionRow;
907  }
908 
916  private function getBaseRevisionRow(
917  IDatabase $dbw,
918  RevisionRecord $rev,
919  $parentId
920  ) {
921  // Record the edit in revisions
922  $revisionRow = [
923  'rev_page' => $rev->getPageId( $this->wikiId ),
924  'rev_parent_id' => $parentId,
925  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
926  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
927  'rev_deleted' => $rev->getVisibility(),
928  'rev_len' => $rev->getSize(),
929  'rev_sha1' => $rev->getSha1(),
930  ];
931 
932  if ( $rev->getId( $this->wikiId ) !== null ) {
933  // Needed to restore revisions with their original ID
934  $revisionRow['rev_id'] = $rev->getId( $this->wikiId );
935  }
936 
937  return $revisionRow;
938  }
939 
948  private function storeContentBlob(
949  SlotRecord $slot,
950  PageIdentity $page,
951  array $blobHints = []
952  ) {
953  $content = $slot->getContent();
954  $format = $content->getDefaultFormat();
955  $model = $content->getModel();
956 
957  $this->checkContent( $content, $page, $slot->getRole() );
958 
959  return $this->blobStore->storeBlob(
960  $content->serialize( $format ),
961  // These hints "leak" some information from the higher abstraction layer to
962  // low level storage to allow for optimization.
963  array_merge(
964  $blobHints,
965  [
966  BlobStore::DESIGNATION_HINT => 'page-content',
967  BlobStore::ROLE_HINT => $slot->getRole(),
968  BlobStore::SHA1_HINT => $slot->getSha1(),
969  BlobStore::MODEL_HINT => $model,
970  BlobStore::FORMAT_HINT => $format,
971  ]
972  )
973  );
974  }
975 
982  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
983  $slotRow = [
984  'slot_revision_id' => $revisionId,
985  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
986  'slot_content_id' => $contentId,
987  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
988  // that we just inserted.
989  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
990  ];
991  $dbw->insert( 'slots', $slotRow, __METHOD__ );
992  }
993 
1000  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
1001  $contentRow = [
1002  'content_size' => $slot->getSize(),
1003  'content_sha1' => $slot->getSha1(),
1004  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
1005  'content_address' => $blobAddress,
1006  ];
1007  $dbw->insert( 'content', $contentRow, __METHOD__ );
1008  return intval( $dbw->insertId() );
1009  }
1010 
1021  private function checkContent( Content $content, PageIdentity $page, string $role ) {
1022  // Note: may return null for revisions that have not yet been inserted
1023 
1024  $model = $content->getModel();
1025  $format = $content->getDefaultFormat();
1026  $handler = $content->getContentHandler();
1027 
1028  if ( !$handler->isSupportedFormat( $format ) ) {
1029  throw new MWException(
1030  "Can't use format $format with content model $model on $page role $role"
1031  );
1032  }
1033 
1034  if ( !$content->isValid() ) {
1035  throw new MWException(
1036  "New content for $page role $role is not valid! Content model is $model"
1037  );
1038  }
1039  }
1040 
1066  public function newNullRevision(
1067  IDatabase $dbw,
1068  PageIdentity $page,
1069  CommentStoreComment $comment,
1070  $minor,
1071  UserIdentity $user
1072  ) {
1073  $this->checkDatabaseDomain( $dbw );
1074 
1075  $pageId = $this->getArticleId( $page );
1076 
1077  // T51581: Lock the page table row to ensure no other process
1078  // is adding a revision to the page at the same time.
1079  // Avoid locking extra tables, compare T191892.
1080  $pageLatest = $dbw->selectField(
1081  'page',
1082  'page_latest',
1083  [ 'page_id' => $pageId ],
1084  __METHOD__,
1085  [ 'FOR UPDATE' ]
1086  );
1087 
1088  if ( !$pageLatest ) {
1089  $msg = 'T235589: Failed to select table row during null revision creation' .
1090  " Page id '$pageId' does not exist.";
1091  $this->logger->error(
1092  $msg,
1093  [ 'exception' => new RuntimeException( $msg ) ]
1094  );
1095 
1096  return null;
1097  }
1098 
1099  // Fetch the actual revision row from master, without locking all extra tables.
1100  $oldRevision = $this->loadRevisionFromConds(
1101  $dbw,
1102  [ 'rev_id' => intval( $pageLatest ) ],
1103  self::READ_LATEST,
1104  $page
1105  );
1106 
1107  if ( !$oldRevision ) {
1108  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1109  $this->logger->error(
1110  $msg,
1111  [ 'exception' => new RuntimeException( $msg ) ]
1112  );
1113  return null;
1114  }
1115 
1116  // Construct the new revision
1117  $timestamp = MWTimestamp::now( TS_MW );
1118  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1119 
1120  $newRevision->setComment( $comment );
1121  $newRevision->setUser( $user );
1122  $newRevision->setTimestamp( $timestamp );
1123  $newRevision->setMinorEdit( $minor );
1124 
1125  return $newRevision;
1126  }
1127 
1137  public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1138  $rc = $this->getRecentChange( $rev );
1139  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1140  return $rc->getAttribute( 'rc_id' );
1141  } else {
1142  return 0;
1143  }
1144  }
1145 
1159  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1160  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1161 
1163  [ 'rc_this_oldid' => $rev->getId( $this->wikiId ) ],
1164  __METHOD__,
1165  $dbType
1166  );
1167 
1168  // XXX: cache this locally? Glue it to the RevisionRecord?
1169  return $rc;
1170  }
1171 
1191  private function loadSlotContent(
1192  SlotRecord $slot,
1193  $blobData = null,
1194  $blobFlags = null,
1195  $blobFormat = null,
1196  $queryFlags = 0
1197  ) {
1198  if ( $blobData !== null ) {
1199  Assert::parameterType( 'string', $blobData, '$blobData' );
1200  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1201 
1202  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1203 
1204  if ( $blobFlags === null ) {
1205  // No blob flags, so use the blob verbatim.
1206  $data = $blobData;
1207  } else {
1208  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1209  if ( $data === false ) {
1210  throw new RevisionAccessException(
1211  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1212  );
1213  }
1214  }
1215 
1216  } else {
1217  $address = $slot->getAddress();
1218  try {
1219  $data = $this->blobStore->getBlob( $address, $queryFlags );
1220  } catch ( BlobAccessException $e ) {
1221  throw new RevisionAccessException(
1222  "Failed to load data blob from $address: " . $e->getMessage() . '. '
1223  . 'If this problem persist, use the findBadBlobs maintenance script '
1224  . 'to investigate the issue and mark bad blobs.',
1225  0, $e
1226  );
1227  }
1228  }
1229 
1230  $model = $slot->getModel();
1231 
1232  // If the content model is not known, don't fail here (T220594, T220793, T228921)
1233  if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1234  $this->logger->warning(
1235  "Undefined content model '$model', falling back to UnknownContent",
1236  [
1237  'content_address' => $slot->getAddress(),
1238  'rev_id' => $slot->getRevision(),
1239  'role_name' => $slot->getRole(),
1240  'model_name' => $model,
1241  'trace' => wfBacktrace()
1242  ]
1243  );
1244 
1245  return new FallbackContent( $data, $model );
1246  }
1247 
1248  return $this->contentHandlerFactory
1249  ->getContentHandler( $model )
1250  ->unserializeContent( $data, $blobFormat );
1251  }
1252 
1270  public function getRevisionById( $id, $flags = 0, PageIdentity $page = null ) {
1271  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags, $page );
1272  }
1273 
1290  public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) {
1291  $conds = [
1292  'page_namespace' => $page->getNamespace(),
1293  'page_title' => $page->getDBkey()
1294  ];
1295 
1296  if ( $page instanceof LinkTarget ) {
1297  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1298  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1299  }
1300 
1301  if ( $revId ) {
1302  // Use the specified revision ID.
1303  // Note that we use newRevisionFromConds here because we want to retry
1304  // and fall back to master if the page is not found on a replica.
1305  // Since the caller supplied a revision ID, we are pretty sure the revision is
1306  // supposed to exist, so we should try hard to find it.
1307  $conds['rev_id'] = $revId;
1308  return $this->newRevisionFromConds( $conds, $flags, $page );
1309  } else {
1310  // Use a join to get the latest revision.
1311  // Note that we don't use newRevisionFromConds here because we don't want to retry
1312  // and fall back to master. The assumption is that we only want to force the fallback
1313  // if we are quite sure the revision exists because the caller supplied a revision ID.
1314  // If the page isn't found at all on a replica, it probably simply does not exist.
1315  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1316  $conds[] = 'rev_id=page_latest';
1317  return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1318  }
1319  }
1320 
1337  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1338  $conds = [ 'page_id' => $pageId ];
1339  if ( $revId ) {
1340  // Use the specified revision ID.
1341  // Note that we use newRevisionFromConds here because we want to retry
1342  // and fall back to master if the page is not found on a replica.
1343  // Since the caller supplied a revision ID, we are pretty sure the revision is
1344  // supposed to exist, so we should try hard to find it.
1345  $conds['rev_id'] = $revId;
1346  return $this->newRevisionFromConds( $conds, $flags );
1347  } else {
1348  // Use a join to get the latest revision.
1349  // Note that we don't use newRevisionFromConds here because we don't want to retry
1350  // and fall back to master. The assumption is that we only want to force the fallback
1351  // if we are quite sure the revision exists because the caller supplied a revision ID.
1352  // If the page isn't found at all on a replica, it probably simply does not exist.
1353  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1354 
1355  $conds[] = 'rev_id=page_latest';
1356 
1357  return $this->loadRevisionFromConds( $db, $conds, $flags );
1358  }
1359  }
1360 
1376  public function getRevisionByTimestamp(
1377  $page,
1378  string $timestamp,
1379  int $flags = IDBAccessObject::READ_NORMAL
1380  ): ?RevisionRecord {
1381  if ( $page instanceof LinkTarget ) {
1382  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1383  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1384  }
1385  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1386  return $this->newRevisionFromConds(
1387  [
1388  'rev_timestamp' => $db->timestamp( $timestamp ),
1389  'page_namespace' => $page->getNamespace(),
1390  'page_title' => $page->getDBkey()
1391  ],
1392  $flags,
1393  $page
1394  );
1395  }
1396 
1404  private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) {
1405  $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1406 
1407  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1408  $db = $this->getDBConnectionRef( $dbMode );
1409 
1410  $res = $db->select(
1411  $revQuery['tables'],
1412  $revQuery['fields'],
1413  [
1414  'slot_revision_id' => $revId,
1415  ],
1416  __METHOD__,
1417  $dbOptions,
1418  $revQuery['joins']
1419  );
1420 
1421  if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1422  // If we found no slots, try looking on the master database (T212428, T252156)
1423  $this->logger->info(
1424  __METHOD__ . ' falling back to READ_LATEST.',
1425  [
1426  'revid' => $revId,
1427  'trace' => wfBacktrace( true )
1428  ]
1429  );
1430  return $this->loadSlotRecords(
1431  $revId,
1432  $queryFlags | self::READ_LATEST,
1433  $page
1434  );
1435  }
1436 
1437  return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1438  }
1439 
1452  private function constructSlotRecords(
1453  $revId,
1454  $slotRows,
1455  $queryFlags,
1456  PageIdentity $page,
1457  $slotContents = null
1458  ) {
1459  $slots = [];
1460 
1461  foreach ( $slotRows as $row ) {
1462  // Resolve role names and model names from in-memory cache, if they were not joined in.
1463  if ( !isset( $row->role_name ) ) {
1464  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1465  }
1466 
1467  if ( !isset( $row->model_name ) ) {
1468  if ( isset( $row->content_model ) ) {
1469  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1470  } else {
1471  // We may get here if $row->model_name is set but null, perhaps because it
1472  // came from rev_content_model, which is NULL for the default model.
1473  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1474  $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1475  }
1476  }
1477 
1478  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1479  if ( isset( $row->blob_data ) ) {
1480  $slotContents[$row->content_address] = $row->blob_data;
1481  }
1482 
1483  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1484  $blob = null;
1485  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1486  $blob = $slotContents[$slot->getAddress()];
1487  if ( $blob instanceof Content ) {
1488  return $blob;
1489  }
1490  }
1491  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1492  };
1493 
1494  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1495  }
1496 
1497  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1498  $this->logger->error(
1499  __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1500  [
1501  'revid' => $revId,
1502  'queryFlags' => $queryFlags,
1503  'trace' => wfBacktrace( true )
1504  ]
1505  );
1506 
1507  throw new RevisionAccessException(
1508  'Main slot of revision not found in database. See T212428.'
1509  );
1510  }
1511 
1512  return $slots;
1513  }
1514 
1530  private function newRevisionSlots(
1531  $revId,
1532  $revisionRow,
1533  $slotRows,
1534  $queryFlags,
1535  PageIdentity $page
1536  ) {
1537  if ( $slotRows ) {
1538  $slots = new RevisionSlots(
1539  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1540  );
1541  } else {
1542  // XXX: do we need the same kind of caching here
1543  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1544 
1545  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) {
1546  return $this->loadSlotRecords( $revId, $queryFlags, $page );
1547  } );
1548  }
1549 
1550  return $slots;
1551  }
1552 
1570  public function newRevisionFromArchiveRow(
1571  $row,
1572  $queryFlags = 0,
1573  PageIdentity $page = null,
1574  array $overrides = []
1575  ) {
1576  return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides );
1577  }
1578 
1591  public function newRevisionFromRow(
1592  $row,
1593  $queryFlags = 0,
1594  PageIdentity $page = null,
1595  $fromCache = false
1596  ) {
1597  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache );
1598  }
1599 
1620  $row,
1621  $slots,
1622  $queryFlags = 0,
1623  PageIdentity $page = null,
1624  array $overrides = []
1625  ) {
1626  Assert::parameterType( \stdClass::class, $row, '$row' );
1627 
1628  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1629  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1630 
1631  if ( !$page && isset( $overrides['title'] ) ) {
1632  if ( !( $overrides['title'] instanceof PageIdentity ) ) {
1633  throw new MWException( 'title field override must contain a PageIdentity object.' );
1634  }
1635 
1636  $page = $overrides['title'];
1637  }
1638 
1639  if ( !isset( $page ) ) {
1640  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1641  $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1642  } else {
1643  throw new InvalidArgumentException(
1644  'A Title or ar_namespace and ar_title must be given'
1645  );
1646  }
1647  }
1648 
1649  foreach ( $overrides as $key => $value ) {
1650  $field = "ar_$key";
1651  $row->$field = $value;
1652  }
1653 
1654  try {
1655  $user = $this->actorStore->newActorFromRowFields(
1656  $row->ar_user ?? null,
1657  $row->ar_user_text ?? null,
1658  $row->ar_actor ?? null
1659  );
1660  } catch ( InvalidArgumentException $ex ) {
1661  $this->logger->warning( 'Could not load user for archive revision {rev_id}', [
1662  'ar_rev_id' => $row->ar_rev_id,
1663  'ar_actor' => $row->ar_actor ?? 'null',
1664  'ar_user_text' => $row->ar_user_text ?? 'null',
1665  'ar_user' => $row->ar_user ?? 'null',
1666  'exception' => $ex
1667  ] );
1668  $user = $this->actorStore->getUnknownActor();
1669  }
1670 
1671  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1672  // Legacy because $row may have come from self::selectFields()
1673  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1674 
1675  if ( !( $slots instanceof RevisionSlots ) ) {
1676  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $page );
1677  }
1678  return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId );
1679  }
1680 
1699  $row,
1700  $slots,
1701  $queryFlags = 0,
1702  PageIdentity $page = null,
1703  $fromCache = false
1704  ) {
1705  Assert::parameterType( \stdClass::class, $row, '$row' );
1706 
1707  if ( !$page ) {
1708  if ( isset( $row->page_id )
1709  && isset( $row->page_namespace )
1710  && isset( $row->page_title )
1711  ) {
1712  $page = $this->newPageFromRow( $row );
1713  } else {
1714  $pageId = (int)( $row->rev_page ?? 0 );
1715  $revId = (int)( $row->rev_id ?? 0 );
1716 
1717  $page = $this->getPage( $pageId, $revId, $queryFlags );
1718  }
1719  } else {
1720  $this->ensureRevisionRowMatchesPage( $row, $page );
1721  }
1722 
1723  try {
1724  $user = $this->actorStore->newActorFromRowFields(
1725  $row->rev_user ?? null,
1726  $row->rev_user_text ?? null,
1727  $row->rev_actor ?? null
1728  );
1729  } catch ( InvalidArgumentException $ex ) {
1730  $this->logger->warning( 'Could not load user for revision {rev_id}', [
1731  'rev_id' => $row->rev_id,
1732  'rev_actor' => $row->rev_actor ?? 'null',
1733  'rev_user_text' => $row->rev_user_text ?? 'null',
1734  'rev_user' => $row->rev_user ?? 'null',
1735  'exception' => $ex
1736  ] );
1737  $user = $this->actorStore->getUnknownActor();
1738  }
1739 
1740  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1741  // Legacy because $row may have come from self::selectFields()
1742  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1743 
1744  if ( !( $slots instanceof RevisionSlots ) ) {
1745  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $page );
1746  }
1747 
1748  // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1749  if ( $fromCache ) {
1750  $rev = new RevisionStoreCacheRecord(
1751  function ( $revId ) use ( $queryFlags ) {
1752  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1753  $row = $this->fetchRevisionRowFromConds(
1754  $db,
1755  [ 'rev_id' => intval( $revId ) ]
1756  );
1757  if ( !$row && !( $queryFlags & self::READ_LATEST ) ) {
1758  // If we found no slots, try looking on the master database (T259738)
1759  $this->logger->info(
1760  'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1761  [
1762  'revid' => $revId,
1763  'trace' => wfBacktrace( true )
1764  ]
1765  );
1766  $dbw = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
1767  $row = $this->fetchRevisionRowFromConds(
1768  $dbw,
1769  [ 'rev_id' => intval( $revId ) ]
1770  );
1771  }
1772  if ( !$row ) {
1773  return [ null, null ];
1774  }
1775  return [
1776  $row->rev_deleted,
1777  $this->actorStore->newActorFromRowFields(
1778  $row->rev_user ?? null,
1779  $row->rev_user_text ?? null,
1780  $row->rev_actor ?? null
1781  )
1782  ];
1783  },
1784  $page, $user, $comment, $row, $slots, $this->wikiId
1785  );
1786  } else {
1787  $rev = new RevisionStoreRecord(
1788  $page, $user, $comment, $row, $slots, $this->wikiId );
1789  }
1790  return $rev;
1791  }
1792 
1802  private function ensureRevisionRowMatchesTitle( $row, Title $title, $context = [] ) {
1803  $revId = (int)( $row->rev_id ?? 0 );
1804  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1805  $titlePageId = $title->getArticleID();
1806 
1807  // Avoid fatal error when the Title's ID changed, T246720
1808  if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1809  $masterPageId = $title->getArticleID( Title::READ_LATEST );
1810  $masterLatest = $title->getLatestRevID( Title::READ_LATEST );
1811 
1812  if ( $revPageId === $masterPageId ) {
1813  $this->logger->warning(
1814  "Encountered stale Title object",
1815  [
1816  'page_id_stale' => $titlePageId,
1817  'page_id_reloaded' => $masterPageId,
1818  'page_latest' => $masterLatest,
1819  'rev_id' => $revId,
1820  'trace' => wfBacktrace()
1821  ] + $context
1822  );
1823  } else {
1824  throw new RevisionAccessException(
1825  "Revision $revId belongs to page ID $revPageId, "
1826  . "the provided Title object belongs to page ID $masterPageId"
1827  );
1828  }
1829  }
1830  }
1831 
1841  private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) {
1842  if ( $page instanceof Title ) {
1844  return;
1845  }
1846 
1847  $revId = (int)( $row->rev_id ?? 0 );
1848  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1849  $titlePageId = $this->getArticleId( $page );
1850 
1851  // Raise fatal error when the Title's ID changed, T246720
1852  if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1853  throw new RevisionAccessException(
1854  "Revision $revId belongs to page ID $revPageId, "
1855  . "the provided Title object belongs to page ID $titlePageId"
1856  );
1857  }
1858  }
1859 
1885  public function newRevisionsFromBatch(
1886  $rows,
1887  array $options = [],
1888  $queryFlags = 0,
1889  PageIdentity $page = null
1890  ) {
1891  $result = new StatusValue();
1892  $archiveMode = $options['archive'] ?? false;
1893 
1894  if ( $archiveMode ) {
1895  $revIdField = 'ar_rev_id';
1896  } else {
1897  $revIdField = 'rev_id';
1898  }
1899 
1900  $rowsByRevId = [];
1901  $pageIdsToFetchTitles = [];
1902  $titlesByPageKey = [];
1903  foreach ( $rows as $row ) {
1904  if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1905  $result->warning(
1906  'internalerror_info',
1907  "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1908  );
1909  }
1910 
1911  // Attach a page key to the row, so we can find and reuse Title objects easily.
1912  $row->_page_key =
1913  $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1914 
1915  if ( $page ) {
1916  if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1917  throw new InvalidArgumentException(
1918  "Revision {$row->$revIdField} doesn't belong to page "
1919  . $this->getArticleId( $page )
1920  );
1921  }
1922 
1923  if ( $archiveMode
1924  && ( $row->ar_namespace != $page->getNamespace()
1925  || $row->ar_title !== $page->getDBkey() )
1926  ) {
1927  throw new InvalidArgumentException(
1928  "Revision {$row->$revIdField} doesn't belong to page "
1929  . $page
1930  );
1931  }
1932  } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1933  if ( isset( $row->page_namespace ) && isset( $row->page_title )
1934  // This should always be true, but just in case we don't have a page_id
1935  // set or it doesn't match rev_page, let's fetch the title again.
1936  && isset( $row->page_id ) && isset( $row->rev_page )
1937  && $row->rev_page === $row->page_id
1938  ) {
1939  $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1940  } elseif ( $archiveMode ) {
1941  // Can't look up deleted pages by ID, but we have namespace and title
1942  $titlesByPageKey[ $row->_page_key ] =
1943  Title::makeTitle( $row->ar_namespace, $row->ar_title );
1944  } else {
1945  $pageIdsToFetchTitles[] = $row->rev_page;
1946  }
1947  }
1948  $rowsByRevId[$row->$revIdField] = $row;
1949  }
1950 
1951  if ( empty( $rowsByRevId ) ) {
1952  $result->setResult( true, [] );
1953  return $result;
1954  }
1955 
1956  // If the page is not supplied, batch-fetch Title objects.
1957  if ( $page ) {
1958  // same logic as for $row->_page_key above
1959  $pageKey = $archiveMode
1960  ? $page->getNamespace() . ':' . $page->getDBkey()
1961  : $this->getArticleId( $page );
1962 
1963  $titlesByPageKey[$pageKey] = $page;
1964  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1965  // Note: when we fetch titles by ID, the page key is also the ID.
1966  // We should never get here if $archiveMode is true.
1967  Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1968 
1969  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1970  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1971  $titlesByPageKey[$t->getArticleID()] = $t;
1972  }
1973  }
1974 
1975  // which method to use for creating RevisionRecords
1976  $newRevisionRecord = [
1977  $this,
1978  $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1979  ];
1980 
1981  if ( !isset( $options['slots'] ) ) {
1982  $result->setResult(
1983  true,
1984  array_map(
1985  static function ( $row )
1986  use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1987  try {
1988  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1989  $result->warning(
1990  'internalerror_info',
1991  "Couldn't find title for rev {$row->$revIdField} "
1992  . "(page key {$row->_page_key})"
1993  );
1994  return null;
1995  }
1996  return $newRevisionRecord( $row, null, $queryFlags,
1997  $titlesByPageKey[ $row->_page_key ] );
1998  } catch ( MWException $e ) {
1999  $result->warning( 'internalerror_info', $e->getMessage() );
2000  return null;
2001  }
2002  },
2003  $rowsByRevId
2004  )
2005  );
2006  return $result;
2007  }
2008 
2009  $slotRowOptions = [
2010  'slots' => $options['slots'] ?? true,
2011  'blobs' => $options['content'] ?? false,
2012  ];
2013 
2014  if ( is_array( $slotRowOptions['slots'] )
2015  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
2016  ) {
2017  // Make sure the main slot is always loaded, RevisionRecord requires this.
2018  $slotRowOptions['slots'][] = SlotRecord::MAIN;
2019  }
2020 
2021  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2022 
2023  $result->merge( $slotRowsStatus );
2024  $slotRowsByRevId = $slotRowsStatus->getValue();
2025 
2026  $result->setResult(
2027  true,
2028  array_map(
2029  function ( $row )
2030  use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
2031  $revIdField, $newRevisionRecord
2032  ) {
2033  if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
2034  $result->warning(
2035  'internalerror_info',
2036  "Couldn't find slots for rev {$row->$revIdField}"
2037  );
2038  return null;
2039  }
2040  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2041  $result->warning(
2042  'internalerror_info',
2043  "Couldn't find title for rev {$row->$revIdField} "
2044  . "(page key {$row->_page_key})"
2045  );
2046  return null;
2047  }
2048  try {
2049  return $newRevisionRecord(
2050  $row,
2051  new RevisionSlots(
2052  $this->constructSlotRecords(
2053  $row->$revIdField,
2054  $slotRowsByRevId[$row->$revIdField],
2055  $queryFlags,
2056  $titlesByPageKey[$row->_page_key]
2057  )
2058  ),
2059  $queryFlags,
2060  $titlesByPageKey[$row->_page_key]
2061  );
2062  } catch ( MWException $e ) {
2063  $result->warning( 'internalerror_info', $e->getMessage() );
2064  return null;
2065  }
2066  },
2067  $rowsByRevId
2068  )
2069  );
2070  return $result;
2071  }
2072 
2096  private function getSlotRowsForBatch(
2097  $rowsOrIds,
2098  array $options = [],
2099  $queryFlags = 0
2100  ) {
2101  $result = new StatusValue();
2102 
2103  $revIds = [];
2104  foreach ( $rowsOrIds as $row ) {
2105  if ( is_object( $row ) ) {
2106  $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
2107  } else {
2108  $revIds[] = (int)$row;
2109  }
2110  }
2111 
2112  // Nothing to do.
2113  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2114  if ( empty( $revIds ) ) {
2115  $result->setResult( true, [] );
2116  return $result;
2117  }
2118 
2119  // We need to set the `content` flag to join in content meta-data
2120  $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
2121  $revIdField = $slotQueryInfo['keys']['rev_id'];
2122  $slotQueryConds = [ $revIdField => $revIds ];
2123 
2124  if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2125  if ( empty( $options['slots'] ) ) {
2126  // Degenerate case: return no slots for each revision.
2127  $result->setResult( true, array_fill_keys( $revIds, [] ) );
2128  return $result;
2129  }
2130 
2131  $roleIdField = $slotQueryInfo['keys']['role_id'];
2132  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
2133  return $this->slotRoleStore->getId( $slot_name );
2134  }, $options['slots'] );
2135  }
2136 
2137  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2138  $slotRows = $db->select(
2139  $slotQueryInfo['tables'],
2140  $slotQueryInfo['fields'],
2141  $slotQueryConds,
2142  __METHOD__,
2143  [],
2144  $slotQueryInfo['joins']
2145  );
2146 
2147  $slotContents = null;
2148  if ( $options['blobs'] ?? false ) {
2149  $blobAddresses = [];
2150  foreach ( $slotRows as $slotRow ) {
2151  $blobAddresses[] = $slotRow->content_address;
2152  }
2153  $slotContentFetchStatus = $this->blobStore
2154  ->getBlobBatch( $blobAddresses, $queryFlags );
2155  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2156  $result->warning( $error['message'], ...$error['params'] );
2157  }
2158  $slotContents = $slotContentFetchStatus->getValue();
2159  }
2160 
2161  $slotRowsByRevId = [];
2162  foreach ( $slotRows as $slotRow ) {
2163  if ( $slotContents === null ) {
2164  // nothing to do
2165  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2166  $slotRow->blob_data = $slotContents[$slotRow->content_address];
2167  } else {
2168  $result->warning(
2169  'internalerror_info',
2170  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2171  );
2172  $slotRow->blob_data = null;
2173  }
2174 
2175  // conditional needed for SCHEMA_COMPAT_READ_OLD
2176  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2177  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2178  }
2179 
2180  // conditional needed for SCHEMA_COMPAT_READ_OLD
2181  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2182  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2183  }
2184 
2185  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2186  }
2187 
2188  $result->setResult( true, $slotRowsByRevId );
2189  return $result;
2190  }
2191 
2212  public function getContentBlobsForBatch(
2213  $rowsOrIds,
2214  $slots = null,
2215  $queryFlags = 0
2216  ) {
2217  $result = $this->getSlotRowsForBatch(
2218  $rowsOrIds,
2219  [ 'slots' => $slots, 'blobs' => true ],
2220  $queryFlags
2221  );
2222 
2223  if ( $result->isOK() ) {
2224  // strip out all internal meta data that we don't want to expose
2225  foreach ( $result->value as $revId => $rowsByRole ) {
2226  foreach ( $rowsByRole as $role => $slotRow ) {
2227  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2228  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2229  // if we didn't ask for it.
2230  unset( $result->value[$revId][$role] );
2231  continue;
2232  }
2233 
2234  $result->value[$revId][$role] = (object)[
2235  'blob_data' => $slotRow->blob_data,
2236  'model_name' => $slotRow->model_name,
2237  ];
2238  }
2239  }
2240  }
2241 
2242  return $result;
2243  }
2244 
2262  array $fields,
2263  $queryFlags = 0,
2264  PageIdentity $page = null
2265  ) {
2266  wfDeprecated( __METHOD__, '1.31' );
2267 
2268  if ( !$page && isset( $fields['title'] ) ) {
2269  if ( !( $fields['title'] instanceof PageIdentity ) ) {
2270  throw new MWException( 'title field must contain a Title object.' );
2271  }
2272 
2273  $page = $fields['title'];
2274  }
2275 
2276  if ( !$page ) {
2277  $pageId = $fields['page'] ?? 0;
2278  $revId = $fields['id'] ?? 0;
2279 
2280  $page = $this->getPage( $pageId, $revId, $queryFlags );
2281  }
2282 
2283  if ( !isset( $fields['page'] ) ) {
2284  $fields['page'] = $this->getArticleId( $page );
2285  }
2286 
2287  // if we have a content object, use it to set the model and type
2288  if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
2289  && !is_array( $fields['content'] )
2290  ) {
2291  throw new MWException(
2292  'content field must contain a Content object or an array of Content objects.'
2293  );
2294  }
2295 
2296  if ( !empty( $fields['text_id'] ) ) {
2297  throw new MWException( 'The text_id field can not be used in MediaWiki 1.35 and later' );
2298  }
2299 
2300  if (
2301  isset( $fields['comment'] )
2302  && !( $fields['comment'] instanceof CommentStoreComment )
2303  ) {
2304  $commentData = $fields['comment_data'] ?? null;
2305 
2306  if ( $fields['comment'] instanceof Message ) {
2307  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2308  $fields['comment'],
2309  $commentData
2310  );
2311  } else {
2312  $commentText = trim( strval( $fields['comment'] ) );
2313  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2314  $commentText,
2315  $commentData
2316  );
2317  }
2318  }
2319 
2320  $revision = new MutableRevisionRecord( $page, $this->wikiId );
2321 
2323  if ( isset( $fields['content'] ) ) {
2324  if ( is_array( $fields['content'] ) ) {
2325  $slotContent = $fields['content'];
2326  } else {
2327  $slotContent = [ SlotRecord::MAIN => $fields['content'] ];
2328  }
2329  } elseif ( isset( $fields['text'] ) ) {
2330  if ( isset( $fields['content_model'] ) ) {
2331  $model = $fields['content_model'];
2332  } else {
2333  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( SlotRecord::MAIN );
2334  $model = $slotRoleHandler->getDefaultModel( $page );
2335  }
2336 
2337  $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
2338  $content = $contentHandler->unserializeContent( $fields['text'] );
2339  $slotContent = [ SlotRecord::MAIN => $content ];
2340  } else {
2341  $slotContent = [];
2342  }
2343 
2344  foreach ( $slotContent as $role => $content ) {
2345  $revision->setContent( $role, $content );
2346  }
2347 
2348  $this->initializeMutableRevisionFromArray( $revision, $fields );
2349 
2350  return $revision;
2351  }
2352 
2358  MutableRevisionRecord $record,
2359  array $fields
2360  ) {
2362  $user = null;
2363  if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
2364  $fields['user']->assertWiki( $this->wikiId );
2365  $user = $fields['user'];
2366  } else {
2367  $actorID = isset( $fields['actor'] ) && is_numeric( $fields['actor'] ) ? (int)$fields['actor'] : null;
2368  $userID = isset( $fields['user'] ) && is_numeric( $fields['user'] ) ? (int)$fields['user'] : null;
2369  try {
2370  $user = $this->actorStore->newActorFromRowFields(
2371  $userID,
2372  $fields['user_text'] ?? null,
2373  $actorID
2374  );
2375  } catch ( InvalidArgumentException $ex ) {
2376  $user = null;
2377  }
2378  if ( !$user && $actorID ) {
2379  try {
2380  $user = $this->actorStore->getActorById(
2381  $actorID,
2382  $this->getDBConnectionRefForQueryFlags( self::READ_NORMAL )
2383  );
2384  } catch ( InvalidArgumentException $ex ) {
2385  $user = null;
2386  }
2387  }
2388  if ( !$user ) {
2389  try {
2390  if ( $userID ) {
2391  $fromUserId = $this->actorStore->getUserIdentityByUserId( $userID );
2392  if ( $fromUserId ) {
2393  $user = $fromUserId;
2394  } elseif ( $fields['user_text'] ?? null ) {
2395  $fromName = $this->actorStore
2396  ->getUserIdentityByName( $fields['user_text'] ?? null );
2397  if ( $fromName ) {
2398  $user = $fromName;
2399  }
2400  }
2401  }
2402  } catch ( InvalidArgumentException $ex ) {
2403  $user = null;
2404  }
2405  }
2406  // Could not initialize the user, maybe it doesn't exist?
2407  if ( isset( $fields['user_text'] ) ) {
2408  $user = new UserIdentityValue(
2409  $userID === null ? 0 : $userID,
2410  $fields['user_text'],
2411  $fields['actor'] ?? 0,
2412  $this->wikiId
2413  );
2414  }
2415  }
2416 
2417  if ( $user ) {
2418  $record->setUser( $user );
2419  }
2420 
2421  $timestamp = isset( $fields['timestamp'] )
2422  ? strval( $fields['timestamp'] )
2423  : MWTimestamp::now( TS_MW );
2424 
2425  $record->setTimestamp( $timestamp );
2426 
2427  if ( isset( $fields['page'] ) ) {
2428  $record->setPageId( intval( $fields['page'] ) );
2429  }
2430 
2431  if ( isset( $fields['id'] ) ) {
2432  $record->setId( intval( $fields['id'] ) );
2433  }
2434  if ( isset( $fields['parent_id'] ) ) {
2435  $record->setParentId( intval( $fields['parent_id'] ) );
2436  }
2437 
2438  if ( isset( $fields['sha1'] ) ) {
2439  $record->setSha1( $fields['sha1'] );
2440  }
2441 
2442  if ( isset( $fields['size'] ) ) {
2443  $record->setSize( intval( $fields['size'] ) );
2444  } elseif ( isset( $fields['len'] ) ) {
2445  $record->setSize( intval( $fields['len'] ) );
2446  }
2447 
2448  if ( isset( $fields['minor_edit'] ) ) {
2449  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2450  }
2451  if ( isset( $fields['deleted'] ) ) {
2452  $record->setVisibility( intval( $fields['deleted'] ) );
2453  }
2454 
2455  if ( isset( $fields['comment'] ) ) {
2456  Assert::parameterType(
2457  CommentStoreComment::class,
2458  $fields['comment'],
2459  '$row[\'comment\']'
2460  );
2461  $record->setComment( $fields['comment'] );
2462  }
2463  }
2464 
2479  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2480  wfDeprecated( __METHOD__, '1.35' );
2481  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2482  if ( $id ) {
2483  $conds['rev_id'] = intval( $id );
2484  } else {
2485  $conds[] = 'rev_id=page_latest';
2486  }
2487  return $this->loadRevisionFromConds( $db, $conds );
2488  }
2489 
2507  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2508  wfDeprecated( __METHOD__, '1.35' );
2509  if ( $id ) {
2510  $matchId = intval( $id );
2511  } else {
2512  $matchId = 'page_latest';
2513  }
2514 
2515  return $this->loadRevisionFromConds(
2516  $db,
2517  [
2518  "rev_id=$matchId",
2519  'page_namespace' => $title->getNamespace(),
2520  'page_title' => $title->getDBkey()
2521  ],
2522  0,
2523  $title
2524  );
2525  }
2526 
2541  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2542  wfDeprecated( __METHOD__, '1.35' );
2543  return $this->loadRevisionFromConds( $db,
2544  [
2545  'rev_timestamp' => $db->timestamp( $timestamp ),
2546  'page_namespace' => $title->getNamespace(),
2547  'page_title' => $title->getDBkey()
2548  ],
2549  0,
2550  $title
2551  );
2552  }
2553 
2570  private function newRevisionFromConds(
2571  array $conditions,
2572  int $flags = IDBAccessObject::READ_NORMAL,
2573  PageIdentity $page = null,
2574  array $options = []
2575  ) {
2576  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2577  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2578 
2579  $lb = $this->getDBLoadBalancer();
2580 
2581  // Make sure new pending/committed revision are visibile later on
2582  // within web requests to certain avoid bugs like T93866 and T94407.
2583  if ( !$rev
2584  && !( $flags & self::READ_LATEST )
2585  && $lb->hasStreamingReplicaServers()
2586  && $lb->hasOrMadeRecentMasterChanges()
2587  ) {
2588  $flags = self::READ_LATEST;
2589  $dbw = $this->getDBConnectionRef( DB_MASTER );
2590  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2591  }
2592 
2593  return $rev;
2594  }
2595 
2610  private function loadRevisionFromConds(
2611  IDatabase $db,
2612  array $conditions,
2613  int $flags = IDBAccessObject::READ_NORMAL,
2614  PageIdentity $page = null,
2615  array $options = []
2616  ) {
2617  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2618  if ( $row ) {
2619  return $this->newRevisionFromRow( $row, $flags, $page );
2620  }
2621 
2622  return null;
2623  }
2624 
2632  private function checkDatabaseDomain( IDatabase $db ) {
2633  $dbDomain = $db->getDomainID();
2634  $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2635  if ( $dbDomain === $storeDomain ) {
2636  return;
2637  }
2638 
2639  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2640  }
2641 
2655  private function fetchRevisionRowFromConds(
2656  IDatabase $db,
2657  array $conditions,
2658  int $flags = IDBAccessObject::READ_NORMAL,
2659  array $options = []
2660  ) {
2661  $this->checkDatabaseDomain( $db );
2662 
2663  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2664  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2665  $options[] = 'FOR UPDATE';
2666  }
2667  return $db->selectRow(
2668  $revQuery['tables'],
2669  $revQuery['fields'],
2670  $conditions,
2671  __METHOD__,
2672  $options,
2673  $revQuery['joins']
2674  );
2675  }
2676 
2698  public function getQueryInfo( $options = [] ) {
2699  $ret = [
2700  'tables' => [],
2701  'fields' => [],
2702  'joins' => [],
2703  ];
2704 
2705  $ret['tables'][] = 'revision';
2706  $ret['fields'] = array_merge( $ret['fields'], [
2707  'rev_id',
2708  'rev_page',
2709  'rev_timestamp',
2710  'rev_minor_edit',
2711  'rev_deleted',
2712  'rev_len',
2713  'rev_parent_id',
2714  'rev_sha1',
2715  ] );
2716 
2717  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2718  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2719  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2720  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2721 
2722  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2723  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2724  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2725  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2726 
2727  if ( in_array( 'page', $options, true ) ) {
2728  $ret['tables'][] = 'page';
2729  $ret['fields'] = array_merge( $ret['fields'], [
2730  'page_namespace',
2731  'page_title',
2732  'page_id',
2733  'page_latest',
2734  'page_is_redirect',
2735  'page_len',
2736  ] );
2737  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2738  }
2739 
2740  if ( in_array( 'user', $options, true ) ) {
2741  $ret['tables'][] = 'user';
2742  $ret['fields'] = array_merge( $ret['fields'], [
2743  'user_name',
2744  ] );
2745  $u = $actorQuery['fields']['rev_user'];
2746  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2747  }
2748 
2749  if ( in_array( 'text', $options, true ) ) {
2750  throw new InvalidArgumentException(
2751  'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2752  );
2753  }
2754 
2755  return $ret;
2756  }
2757 
2778  public function getSlotsQueryInfo( $options = [] ) {
2779  $ret = [
2780  'tables' => [],
2781  'fields' => [],
2782  'joins' => [],
2783  'keys' => [],
2784  ];
2785 
2786  $ret['keys']['rev_id'] = 'slot_revision_id';
2787  $ret['keys']['role_id'] = 'slot_role_id';
2788 
2789  $ret['tables'][] = 'slots';
2790  $ret['fields'] = array_merge( $ret['fields'], [
2791  'slot_revision_id',
2792  'slot_content_id',
2793  'slot_origin',
2794  'slot_role_id',
2795  ] );
2796 
2797  if ( in_array( 'role', $options, true ) ) {
2798  // Use left join to attach role name, so we still find the revision row even
2799  // if the role name is missing. This triggers a more obvious failure mode.
2800  $ret['tables'][] = 'slot_roles';
2801  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2802  $ret['fields'][] = 'role_name';
2803  }
2804 
2805  if ( in_array( 'content', $options, true ) ) {
2806  $ret['keys']['model_id'] = 'content_model';
2807 
2808  $ret['tables'][] = 'content';
2809  $ret['fields'] = array_merge( $ret['fields'], [
2810  'content_size',
2811  'content_sha1',
2812  'content_address',
2813  'content_model',
2814  ] );
2815  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2816 
2817  if ( in_array( 'model', $options, true ) ) {
2818  // Use left join to attach model name, so we still find the revision row even
2819  // if the model name is missing. This triggers a more obvious failure mode.
2820  $ret['tables'][] = 'content_models';
2821  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2822  $ret['fields'][] = 'model_name';
2823  }
2824 
2825  }
2826 
2827  return $ret;
2828  }
2829 
2843  public function getArchiveQueryInfo() {
2844  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2845  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2846  $ret = [
2847  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2848  'fields' => [
2849  'ar_id',
2850  'ar_page_id',
2851  'ar_namespace',
2852  'ar_title',
2853  'ar_rev_id',
2854  'ar_timestamp',
2855  'ar_minor_edit',
2856  'ar_deleted',
2857  'ar_len',
2858  'ar_parent_id',
2859  'ar_sha1',
2860  ] + $commentQuery['fields'] + $actorQuery['fields'],
2861  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2862  ];
2863 
2864  return $ret;
2865  }
2866 
2876  public function getRevisionSizes( array $revIds ) {
2877  $dbr = $this->getDBConnectionRef( DB_REPLICA );
2878  $revLens = [];
2879  if ( !$revIds ) {
2880  return $revLens; // empty
2881  }
2882 
2883  $res = $dbr->select(
2884  'revision',
2885  [ 'rev_id', 'rev_len' ],
2886  [ 'rev_id' => $revIds ],
2887  __METHOD__
2888  );
2889 
2890  foreach ( $res as $row ) {
2891  $revLens[$row->rev_id] = intval( $row->rev_len );
2892  }
2893 
2894  return $revLens;
2895  }
2896 
2909  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2910  wfDeprecated( __METHOD__, '1.35' );
2911  return $this->getRevisionSizes( $revIds );
2912  }
2913 
2922  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2923  $op = $dir === 'next' ? '>' : '<';
2924  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2925 
2926  $revisionIdValue = $rev->getId( $this->wikiId );
2927 
2928  if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) {
2929  // revision is unsaved or otherwise incomplete
2930  return null;
2931  }
2932 
2933  if ( $rev instanceof RevisionArchiveRecord ) {
2934  // revision is deleted, so it's not part of the page history
2935  return null;
2936  }
2937 
2938  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2939  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2940 
2941  $ts = $this->getTimestampFromId( $revisionIdValue, $flags );
2942  if ( $ts === false ) {
2943  // XXX Should this be moved into getTimestampFromId?
2944  $ts = $db->selectField( 'archive', 'ar_timestamp',
2945  [ 'ar_rev_id' => $revisionIdValue ], __METHOD__ );
2946  if ( $ts === false ) {
2947  // XXX Is this reachable? How can we have a page id but no timestamp?
2948  return null;
2949  }
2950  }
2951  $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2952 
2953  $revId = $db->selectField( 'revision', 'rev_id',
2954  [
2955  'rev_page' => $rev->getPageId( $this->wikiId ),
2956  "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op $revisionIdValue )"
2957  ],
2958  __METHOD__,
2959  [
2960  'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2961  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2962  ]
2963  );
2964 
2965  if ( $revId === false ) {
2966  return null;
2967  }
2968 
2969  return $this->getRevisionById( intval( $revId ) );
2970  }
2971 
2987  public function getPreviousRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2988  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2989  }
2990 
3004  public function getNextRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
3005  return $this->getRelativeRevision( $rev, $flags, 'next' );
3006  }
3007 
3019  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
3020  $this->checkDatabaseDomain( $db );
3021 
3022  if ( $rev->getPageId( $this->wikiId ) === null ) {
3023  return 0;
3024  }
3025  # Use page_latest if ID is not given
3026  if ( !$rev->getId( $this->wikiId ) ) {
3027  $prevId = $db->selectField(
3028  'page', 'page_latest',
3029  [ 'page_id' => $rev->getPageId( $this->wikiId ) ],
3030  __METHOD__
3031  );
3032  } else {
3033  $prevId = $db->selectField(
3034  'revision', 'rev_id',
3035  [ 'rev_page' => $rev->getPageId( $this->wikiId ), 'rev_id < ' . $rev->getId( $this->wikiId ) ],
3036  __METHOD__,
3037  [ 'ORDER BY' => 'rev_id DESC' ]
3038  );
3039  }
3040  return intval( $prevId );
3041  }
3042 
3055  public function getTimestampFromId( $id, $flags = 0 ) {
3056  if ( $id instanceof Title ) {
3057  // Old deprecated calling convention supported for backwards compatibility
3058  $id = $flags;
3059  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
3060  }
3061 
3062  // T270149: Bail out if we know the query will definitely return false. Some callers are
3063  // passing RevisionRecord::getId() call directly as $id which can possibly return null.
3064  // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL'
3065  // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null.
3066  // @todo typehint $id and remove the null check
3067  if ( $id === null || $id <= 0 ) {
3068  return false;
3069  }
3070 
3071  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3072 
3073  $timestamp =
3074  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
3075 
3076  return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
3077  }
3078 
3088  public function countRevisionsByPageId( IDatabase $db, $id ) {
3089  $this->checkDatabaseDomain( $db );
3090 
3091  $row = $db->selectRow( 'revision',
3092  [ 'revCount' => 'COUNT(*)' ],
3093  [ 'rev_page' => $id ],
3094  __METHOD__
3095  );
3096  if ( $row ) {
3097  return intval( $row->revCount );
3098  }
3099  return 0;
3100  }
3101 
3111  public function countRevisionsByTitle( IDatabase $db, PageIdentity $page ) {
3112  $id = $this->getArticleId( $page );
3113  if ( $id ) {
3114  return $this->countRevisionsByPageId( $db, $id );
3115  }
3116  return 0;
3117  }
3118 
3137  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
3138  $this->checkDatabaseDomain( $db );
3139 
3140  if ( !$userId ) {
3141  return false;
3142  }
3143 
3144  $revQuery = $this->getQueryInfo();
3145  $res = $db->select(
3146  $revQuery['tables'],
3147  [
3148  'rev_user' => $revQuery['fields']['rev_user'],
3149  ],
3150  [
3151  'rev_page' => $pageId,
3152  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
3153  ],
3154  __METHOD__,
3155  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
3156  $revQuery['joins']
3157  );
3158  foreach ( $res as $row ) {
3159  if ( $row->rev_user != $userId ) {
3160  return false;
3161  }
3162  }
3163  return true;
3164  }
3165 
3179  public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) {
3180  $db = $this->getDBConnectionRef( DB_REPLICA );
3181  if ( !$page instanceof Title ) {
3182 
3183  // TODO: For foreign wikis we can not cast from PageIdentityValue to Title,
3184  // since getLatestRevID will fetch from local database. To be fixed with cross-wiki
3185  // aware PageStore. T274067
3186  $page->assertWiki( PageIdentity::LOCAL );
3188  } else {
3189  $title = $page;
3190  }
3191  $revIdPassed = $revId;
3192  $pageId = $this->getArticleID( $title );
3193 
3194  if ( !$pageId ) {
3195  return false;
3196  }
3197 
3198  if ( !$revId ) {
3199  $revId = $title->getLatestRevID();
3200  }
3201 
3202  if ( !$revId ) {
3203  wfWarn(
3204  'No latest revision known for page ' . $title->getPrefixedDBkey()
3205  . ' even though it exists with page ID ' . $pageId
3206  );
3207  return false;
3208  }
3209 
3210  // Load the row from cache if possible. If not possible, populate the cache.
3211  // As a minor optimization, remember if this was a cache hit or miss.
3212  // We can sometimes avoid a database query later if this is a cache miss.
3213  $fromCache = true;
3214  $row = $this->cache->getWithSetCallback(
3215  // Page/rev IDs passed in from DB to reflect history merges
3216  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
3217  WANObjectCache::TTL_WEEK,
3218  function ( $curValue, &$ttl, array &$setOpts ) use (
3219  $db, $revId, &$fromCache
3220  ) {
3221  $setOpts += Database::getCacheSetOptions( $db );
3222  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
3223  if ( $row ) {
3224  $fromCache = false;
3225  }
3226  return $row; // don't cache negatives
3227  }
3228  );
3229 
3230  // Reflect revision deletion and user renames.
3231  if ( $row ) {
3232  $this->ensureRevisionRowMatchesTitle( $row, $title, [
3233  'from_cache_flag' => $fromCache,
3234  'page_id_initial' => $pageId,
3235  'rev_id_used' => $revId,
3236  'rev_id_requested' => $revIdPassed,
3237  ] );
3238 
3239  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
3240  } else {
3241  return false;
3242  }
3243  }
3244 
3253  public function getFirstRevision(
3254  $page,
3255  int $flags = IDBAccessObject::READ_NORMAL
3256  ): ?RevisionRecord {
3257  if ( $page instanceof LinkTarget ) {
3258  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
3259  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
3260  }
3261  return $this->newRevisionFromConds(
3262  [
3263  'page_namespace' => $page->getNamespace(),
3264  'page_title' => $page->getDBkey()
3265  ],
3266  $flags,
3267  $page,
3268  [
3269  'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
3270  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
3271  ]
3272  );
3273  }
3274 
3286  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3287  return $this->cache->makeGlobalKey(
3288  self::ROW_CACHE_KEY,
3289  $db->getDomainID(),
3290  $pageId,
3291  $revId
3292  );
3293  }
3294 
3302  private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
3303  if ( $rev ) {
3304  if ( $rev->getId( $this->wikiId ) === null ) {
3305  throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
3306  }
3307  if ( $rev->getPageId( $this->wikiId ) !== $pageId ) {
3308  throw new InvalidArgumentException(
3309  "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
3310  );
3311  }
3312  }
3313  }
3314 
3329  private function getRevisionLimitConditions(
3330  IDatabase $dbr,
3331  RevisionRecord $old = null,
3332  RevisionRecord $new = null,
3333  $options = []
3334  ) {
3335  $options = (array)$options;
3336  $oldCmp = '>';
3337  $newCmp = '<';
3338  if ( in_array( self::INCLUDE_OLD, $options ) ) {
3339  $oldCmp = '>=';
3340  }
3341  if ( in_array( self::INCLUDE_NEW, $options ) ) {
3342  $newCmp = '<=';
3343  }
3344  if ( in_array( self::INCLUDE_BOTH, $options ) ) {
3345  $oldCmp = '>=';
3346  $newCmp = '<=';
3347  }
3348 
3349  $conds = [];
3350  if ( $old ) {
3351  $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3352  $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId( $this->wikiId )}) " .
3353  "OR rev_timestamp > {$oldTs}";
3354  }
3355  if ( $new ) {
3356  $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3357  $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId( $this->wikiId )}) " .
3358  "OR rev_timestamp < {$newTs}";
3359  }
3360  return $conds;
3361  }
3362 
3389  public function getRevisionIdsBetween(
3390  int $pageId,
3391  RevisionRecord $old = null,
3392  RevisionRecord $new = null,
3393  ?int $max = null,
3394  $options = [],
3395  ?string $order = null,
3396  int $flags = IDBAccessObject::READ_NORMAL
3397  ) : array {
3398  $this->assertRevisionParameter( 'old', $pageId, $old );
3399  $this->assertRevisionParameter( 'new', $pageId, $new );
3400 
3401  $options = (array)$options;
3402  $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3403  in_array( self::INCLUDE_BOTH, $options );
3404  $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3405  in_array( self::INCLUDE_BOTH, $options );
3406 
3407  // No DB query needed if old and new are the same revision.
3408  // Can't check for consecutive revisions with 'getParentId' for a similar
3409  // optimization as edge cases exist when there are revisions between
3410  // a revision and it's parent. See T185167 for more details.
3411  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3412  return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3413  }
3414 
3415  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3416  $conds = array_merge(
3417  [
3418  'rev_page' => $pageId,
3419  $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3420  ],
3421  $this->getRevisionLimitConditions( $db, $old, $new, $options )
3422  );
3423 
3424  $queryOptions = [];
3425  if ( $order !== null ) {
3426  $queryOptions['ORDER BY'] = [ "rev_timestamp $order", "rev_id $order" ];
3427  }
3428  if ( $max !== null ) {
3429  $queryOptions['LIMIT'] = $max + 1; // extra to detect truncation
3430  }
3431 
3432  $values = $db->selectFieldValues(
3433  'revision',
3434  'rev_id',
3435  $conds,
3436  __METHOD__,
3437  $queryOptions
3438  );
3439  return array_map( 'intval', $values );
3440  }
3441 
3463  public function getAuthorsBetween(
3464  $pageId,
3465  RevisionRecord $old = null,
3466  RevisionRecord $new = null,
3467  Authority $performer = null,
3468  $max = null,
3469  $options = []
3470  ) {
3471  $this->assertRevisionParameter( 'old', $pageId, $old );
3472  $this->assertRevisionParameter( 'new', $pageId, $new );
3473  $options = (array)$options;
3474 
3475  // No DB query needed if old and new are the same revision.
3476  // Can't check for consecutive revisions with 'getParentId' for a similar
3477  // optimization as edge cases exist when there are revisions between
3478  //a revision and it's parent. See T185167 for more details.
3479  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3480  if ( empty( $options ) ) {
3481  return [];
3482  } elseif ( $performer ) {
3483  return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3484  } else {
3485  return [ $new->getUser() ];
3486  }
3487  }
3488 
3489  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3490  $conds = array_merge(
3491  [
3492  'rev_page' => $pageId,
3493  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3494  ],
3495  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3496  );
3497 
3498  $queryOpts = [ 'DISTINCT' ];
3499  if ( $max !== null ) {
3500  $queryOpts['LIMIT'] = $max + 1;
3501  }
3502 
3503  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3504  return array_map( function ( $row ) {
3505  return $this->actorStore->newActorFromRowFields(
3506  $row->rev_user,
3507  $row->rev_user_text,
3508  $row->rev_actor
3509  );
3510  }, iterator_to_array( $dbr->select(
3511  array_merge( [ 'revision' ], $actorQuery['tables'] ),
3512  $actorQuery['fields'],
3513  $conds, __METHOD__,
3514  $queryOpts,
3515  $actorQuery['joins']
3516  ) ) );
3517  }
3518 
3540  public function countAuthorsBetween(
3541  $pageId,
3542  RevisionRecord $old = null,
3543  RevisionRecord $new = null,
3544  Authority $performer = null,
3545  $max = null,
3546  $options = []
3547  ) {
3548  // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3549  // and creation of UserIdentity stuff.
3550  return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3551  }
3552 
3573  public function countRevisionsBetween(
3574  $pageId,
3575  RevisionRecord $old = null,
3576  RevisionRecord $new = null,
3577  $max = null,
3578  $options = []
3579  ) {
3580  $this->assertRevisionParameter( 'old', $pageId, $old );
3581  $this->assertRevisionParameter( 'new', $pageId, $new );
3582 
3583  // No DB query needed if old and new are the same revision.
3584  // Can't check for consecutive revisions with 'getParentId' for a similar
3585  // optimization as edge cases exist when there are revisions between
3586  //a revision and it's parent. See T185167 for more details.
3587  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3588  return 0;
3589  }
3590 
3591  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3592  $conds = array_merge(
3593  [
3594  'rev_page' => $pageId,
3595  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3596  ],
3597  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3598  );
3599  if ( $max !== null ) {
3600  return $dbr->selectRowCount( 'revision', '1',
3601  $conds,
3602  __METHOD__,
3603  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3604  );
3605  } else {
3606  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3607  }
3608  }
3609 
3610  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3611 }
3612 
3617 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Revision\RevisionStore\ORDER_OLDEST_TO_NEWEST
const ORDER_OLDEST_TO_NEWEST
Definition: RevisionStore.php:95
Revision\RevisionStore\getWikiId
getWikiId()
Get the ID of the wiki this revision belongs to.
Definition: RevisionStore.php:248
Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, PageIdentity $page)
Definition: RevisionStore.php:1404
Revision\MutableRevisionRecord\setMinorEdit
setMinorEdit( $minorEdit)
Definition: MutableRevisionRecord.php:303
Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:126
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:37
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:65
Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:139
Revision\RevisionStore\ensureRevisionRowMatchesTitle
ensureRevisionRowMatchesTitle( $row, Title $title, $context=[])
Check that the given row matches the given Title object.
Definition: RevisionStore.php:1802
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:37
Revision\RevisionStore\$hookContainer
HookContainer $hookContainer
Definition: RevisionStore.php:158
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
Revision\RevisionStore\getPage
getPage(?int $pageId, ?int $revId, int $queryFlags=self::READ_NORMAL)
Determines the page based on the available information.
Definition: RevisionStore.php:309
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:50
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:34
Revision\RevisionStore\checkDatabaseDomain
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
Definition: RevisionStore.php:2632
MediaWiki\Storage\BlobStore\DESIGNATION_HINT
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition: BlobStore.php:42
MediaWiki\Storage\BlobAccessException
Exception representing a failure to access a data blob.
Definition: BlobAccessException.php:33
Revision\RevisionStore\newNullRevision
newNullRevision(IDatabase $dbw, PageIdentity $page, CommentStoreComment $comment, $minor, UserIdentity $user)
Create a new null-revision for insertion into a page's history.
Definition: RevisionStore.php:1066
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:1159
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:32
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:319
Revision\RevisionRecord\getPageId
getPageId( $wikiId=self::LOCAL)
Get the page ID.
Definition: RevisionRecord.php:351
Revision\RevisionStore\getAuthorsBetween
getAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, Authority $performer=null, $max=null, $options=[])
Get the authors between the given revisions or revisions.
Definition: RevisionStore.php:3463
Revision\RevisionStore\$actorStore
ActorStore $actorStore
Definition: RevisionStore.php:134
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:452
Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:257
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:223
Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:437
Revision\RevisionStore\loadRevisionFromConds
loadRevisionFromConds(IDatabase $db, array $conditions, int $flags=IDBAccessObject::READ_NORMAL, PageIdentity $page=null, array $options=[])
Given a set of conditions, fetch a revision from the given database connection.
Definition: RevisionStore.php:2610
Revision\MutableRevisionRecord\setSha1
setSha1( $sha1)
Set revision hash, for optimization.
Definition: MutableRevisionRecord.php:249
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:87
Revision\MutableRevisionRecord\setParentId
setParentId( $parentId)
Definition: MutableRevisionRecord.php:123
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:89
MediaWiki\Storage\SqlBlobStore
Service for storing and loading Content objects.
Definition: SqlBlobStore.php:51
Revision\RevisionStore\newRevisionFromRowAndSlots
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, PageIdentity $page=null, $fromCache=false)
Definition: RevisionStore.php:1698
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:76
Revision\RevisionStore\initializeMutableRevisionFromArray
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
Definition: RevisionStore.php:2357
Revision\RevisionRecord\getPage
getPage()
Returns the page this revision belongs to.
Definition: RevisionRecord.php:386
Revision\RevisionStoreCacheRecord
A cached RevisionStoreRecord.
Definition: RevisionStoreCacheRecord.php:37
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:463
Revision\RevisionStore\getSlotRowsForBatch
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
Definition: RevisionStore.php:2096
Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2843
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:347
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:475
Revision\RevisionStore\getPreviousRevision
getPreviousRevision(RevisionRecord $rev, $flags=self::READ_NORMAL)
Get the revision before $rev in the page's history, if any.
Definition: RevisionStore.php:2987
Revision\RevisionStore\INCLUDE_NEW
const INCLUDE_NEW
Definition: RevisionStore.php:100
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:121
Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:3055
Revision\RevisionStore\getRcIdIfUnpatrolled
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
Definition: RevisionStore.php:1137
Revision\RevisionFactory
Service for constructing revision objects.
Definition: RevisionFactory.php:38
Revision\MutableRevisionRecord\setId
setId( $id)
Set the revision ID.
Definition: MutableRevisionRecord.php:323
Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, UserIdentity $user, CommentStoreComment $comment, PageIdentity $page, $pageId, $parentId)
Definition: RevisionStore.php:662
Revision\RevisionStore\newRevisionSlots
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, PageIdentity $page)
Factory method for RevisionSlots based on a revision ID.
Definition: RevisionStore.php:1530
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:413
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:3302
Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2778
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0, PageIdentity $page=null)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1270
Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:131
$revQuery
$revQuery
Definition: testCompression.php:56
Revision\RevisionRecord\getParentId
getParentId( $wikiId=self::LOCAL)
Get parent revision ID (the original previous page revision).
Definition: RevisionRecord.php:313
MediaWiki\DAO\WikiAwareEntity
Marker interface for entities aware of the wiki they belong to.
Definition: WikiAwareEntity.php:34
Revision\RevisionStore\getRevisionByTimestamp
getRevisionByTimestamp( $page, string $timestamp, int $flags=IDBAccessObject::READ_NORMAL)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:1376
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
Revision\RevisionStore\__construct
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorMigration $actorMigration, ActorStore $actorStore, IContentHandlerFactory $contentHandlerFactory, TitleFactory $titleFactory, HookContainer $hookContainer, $wikiId=WikiAwareEntity::LOCAL)
Definition: RevisionStore.php:191
Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Revision\RevisionStore\$titleFactory
TitleFactory $titleFactory
Definition: RevisionStore.php:166
Revision\RevisionStore\getDBConnectionRef
getDBConnectionRef( $mode, $groups=[])
Definition: RevisionStore.php:268
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:2909
Title\castFromPageIdentity
static castFromPageIdentity(?PageIdentity $pageIdentity)
Return a Title for a given PageIdentity.
Definition: Title.php:328
Revision\RevisionStore\userWasLastToEdit
userWasLastToEdit(IDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
Definition: RevisionStore.php:3137
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\Revision
Definition: ContributionsLookup.php:3
MediaWiki\Page\LegacyArticleIdAccess
trait LegacyArticleIdAccess
Definition: LegacyArticleIdAccess.php:26
Revision\RevisionRecord\getUser
getUser( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Definition: RevisionRecord.php:405
Revision
Definition: Revision.php:40
Revision\RevisionStore\getNextRevision
getNextRevision(RevisionRecord $rev, $flags=self::READ_NORMAL)
Get the revision after $rev in the page's history, if any.
Definition: RevisionStore.php:3004
Revision\RevisionStore\getRevisionLimitConditions
getRevisionLimitConditions(IDatabase $dbr, RevisionRecord $old=null, RevisionRecord $new=null, $options=[])
Converts revision limits to query conditions.
Definition: RevisionStore.php:3329
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:1191
Revision\RevisionStore\insertIpChangesRow
insertIpChangesRow(IDatabase $dbw, UserIdentity $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
Definition: RevisionStore.php:776
Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, PageIdentity $page=null, $fromCache=false)
Definition: RevisionStore.php:1591
Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:422
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1066
Revision\RevisionStore\getQueryInfo
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
Definition: RevisionStore.php:2698
MWException
MediaWiki exception.
Definition: MWException.php:29
Revision\RevisionRecord\getSize
getSize()
Returns the nominal size of this revision, in bogo-bytes.
Wikimedia\Rdbms\Database\getCacheSetOptions
static getCacheSetOptions(?IDatabase ... $dbs)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4984
Revision\RevisionStore\ORDER_NEWEST_TO_OLDEST
const ORDER_NEWEST_TO_OLDEST
Definition: RevisionStore.php:96
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:337
Revision\RevisionRecord\getSha1
getSha1()
Returns the base36 sha1 of this revision.
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1034
Revision\RevisionStore\newRevisionFromArchiveRowAndSlots
newRevisionFromArchiveRowAndSlots( $row, $slots, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Definition: RevisionStore.php:1619
Revision\RevisionStore\INCLUDE_BOTH
const INCLUDE_BOTH
Definition: RevisionStore.php:101
Revision\RevisionStore\insertSlotOn
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, PageIdentity $page, array $blobHints=[])
Definition: RevisionStore.php:738
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:458
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:558
Revision\RevisionStore\getRevisionByPageId
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
Definition: RevisionStore.php:1337
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:2479
$blob
$blob
Definition: testCompression.php:70
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:506
Revision\RevisionStore\newRevisionFromArchiveRow
newRevisionFromArchiveRow( $row, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Make a fake revision object from an archive table row.
Definition: RevisionStore.php:1570
Revision\RevisionStore\constructSlotRecords
constructSlotRecords( $revId, $slotRows, $queryFlags, PageIdentity $page, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
Definition: RevisionStore.php:1452
Revision\MutableRevisionRecord\setVisibility
setVisibility( $visibility)
Definition: MutableRevisionRecord.php:279
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:486
MediaWiki\Storage\BlobStore\MODEL_HINT
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob.
Definition: BlobStore.php:78
Revision\RevisionStore\getPreviousRevisionId
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save.
Definition: RevisionStore.php:3019
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:442
Revision\RevisionRecord\isReadyForInsertion
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
Definition: RevisionRecord.php:584
Revision\RevisionStore\updateSlotsInternal
updateSlotsInternal(RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw)
Definition: RevisionStore.php:640
Revision\RevisionStore\insertSlotRowOn
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
Definition: RevisionStore.php:982
Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2876
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:64
Revision\RevisionStore\newRevisionFromConds
newRevisionFromConds(array $conditions, int $flags=IDBAccessObject::READ_NORMAL, PageIdentity $page=null, array $options=[])
Given a set of conditions, fetch a revision.
Definition: RevisionStore.php:2570
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:583
$title
$title
Definition: testCompression.php:38
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:626
Revision\RevisionStore\countAuthorsBetween
countAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, Authority $performer=null, $max=null, $options=[])
Get the number of authors between the given revisions.
Definition: RevisionStore.php:3540
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:516
Revision\RevisionRecord\getSlotRoles
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
Definition: RevisionRecord.php:223
DB_MASTER
const DB_MASTER
Definition: defines.php:26
FallbackContent
Content object implementation representing unknown content.
Definition: FallbackContent.php:38
Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:106
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
Revision\RevisionStore\setLogger
setLogger(LoggerInterface $logger)
Definition: RevisionStore.php:225
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:555
Revision\RevisionStore\ROW_CACHE_KEY
const ROW_CACHE_KEY
Definition: RevisionStore.php:93
Revision\RevisionStore\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: RevisionStore.php:152
MediaWiki\Permissions\Authority
Definition: Authority.php:30
Revision\RevisionArchiveRecord
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Definition: RevisionArchiveRecord.php:41
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, PageIdentity $page)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:3111
Revision\RevisionStore\ensureRevisionRowMatchesPage
ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context=[])
Check that the given row matches the given PageIdentity object.
Definition: RevisionStore.php:1841
$content
$content
Definition: router.php:76
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:539
Revision\RevisionStore\newMutableRevisionFromArray
newMutableRevisionFromArray(array $fields, $queryFlags=0, PageIdentity $page=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1....
Definition: RevisionStore.php:2261
Revision\RevisionRecord\DELETED_USER
const DELETED_USER
Definition: RevisionRecord.php:55
DBAccessObjectUtils\hasFlags
static hasFlags( $bitfield, $flags)
Definition: DBAccessObjectUtils.php:35
Revision\RevisionStore\getRevisionByTitle
getRevisionByTitle( $page, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: RevisionStore.php:1290
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:45
Revision\RevisionStore\getKnownCurrentRevision
getKnownCurrentRevision(PageIdentity $page, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: RevisionStore.php:3179
Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:295
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:3389
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:125
Revision\RevisionStore\$slotRoleStore
NameTableStore $slotRoleStore
Definition: RevisionStore.php:149
Revision\RevisionStore\getContentBlobsForBatch
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
Definition: RevisionStore.php:2212
Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, $parentId)
Definition: RevisionStore.php:802
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:36
Title\newFromIDs
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition: Title.php:532
Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:40
Revision\RevisionStore\getRelativeRevision
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
Definition: RevisionStore.php:2922
Revision\RevisionStore\$hookRunner
HookRunner $hookRunner
Definition: RevisionStore.php:161
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
MediaWiki\Storage\BlobStore
Service for loading and storing data blobs.
Definition: BlobStore.php:35
Content
Base interface for content objects.
Definition: Content.php:35
Wikimedia\Rdbms\DBConnRef
Helper class used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition: DBConnRef.php:29
Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:464
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:2655
Title
Represents a title within MediaWiki.
Definition: Title.php:46
Revision\RevisionStore\insertContentRowOn
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
Definition: RevisionStore.php:1000
Revision\RevisionStore\countRevisionsByPageId
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:3088
Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, PageIdentity $page, array $blobHints=[])
Definition: RevisionStore.php:948
Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:144
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:184
Revision\RevisionStore\INCLUDE_OLD
const INCLUDE_OLD
Definition: RevisionStore.php:99
Revision\RevisionStore\getDBLoadBalancer
getDBLoadBalancer()
Definition: RevisionStore.php:239
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:85
Revision\RevisionStore\newPageFromRow
newPageFromRow(stdClass $row)
Definition: RevisionStore.php:394
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:53
TitleFactory
Creates Title objects.
Definition: TitleFactory.php:34
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
Page\PageIdentityValue
Immutable value object representing a page identity.
Definition: PageIdentityValue.php:43
Revision\RevisionRecord\getComment
getComment( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision comment, if it's available to the specified audience.
Definition: RevisionRecord.php:429
MediaWiki\Storage\BlobStore\REVISION_HINT
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition: BlobStore.php:60
wfBacktrace
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
Definition: GlobalFunctions.php:1371
Revision\RevisionStore\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: RevisionStore.php:155
MediaWiki\Storage\BlobStore\PARENT_HINT
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition: BlobStore.php:66
Revision\RevisionStore\$wikiId
bool string $wikiId
Definition: RevisionStore.php:111
MediaWiki\Storage\BlobStore\ROLE_HINT
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with.
Definition: BlobStore.php:54
Revision\RevisionStore\checkContent
checkContent(Content $content, PageIdentity $page, string $role)
MCR migration note: this corresponds to Revision::checkContentModel.
Definition: RevisionStore.php:1021
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
MWUnknownContentModelException
Exception thrown when an unregistered content model is requested.
Definition: MWUnknownContentModelException.php:11
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1081
Revision\MutableRevisionRecord\setTimestamp
setTimestamp( $timestamp)
Definition: MutableRevisionRecord.php:291
Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:420
Revision\MutableRevisionRecord\setSize
setSize( $size)
Set nominal revision size, for optimization.
Definition: MutableRevisionRecord.php:267
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:576
$t
$t
Definition: testCompression.php:74
Title\castFromLinkTarget
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:315
Revision\RevisionStore\getFirstRevision
getFirstRevision( $page, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
Definition: RevisionStore.php:3253
Revision\RevisionStore\updateSlotsOn
updateSlotsOn(RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw)
Update derived slots in an existing revision into the database, returning the modified slots on succe...
Definition: RevisionStore.php:582
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
MediaWiki\$context
IContextSource $context
Definition: MediaWiki.php:40
Revision\RevisionStore\newRevisionsFromBatch
newRevisionsFromBatch( $rows, array $options=[], $queryFlags=0, PageIdentity $page=null)
Construct a RevisionRecord instance for each row in $rows, and return them as an associative array in...
Definition: RevisionStore.php:1885
Revision\RevisionStore\loadRevisionFromTimestamp
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:2541
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:530
Revision\RevisionRecord\FOR_THIS_USER
const FOR_THIS_USER
Definition: RevisionRecord.php:63
Revision\RevisionStore\getRevisionRowCacheKey
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) Caching rows w...
Definition: RevisionStore.php:3286
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
Revision\MutableRevisionRecord\setComment
setComment(CommentStoreComment $comment)
Definition: MutableRevisionRecord.php:233
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\User\ActorStore
Definition: ActorStore.php:43
Revision\RevisionStore\getBaseRevisionRow
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, $parentId)
Definition: RevisionStore.php:916
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:2507
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:3573
Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:196
Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:116
Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:232
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:289