MediaWiki  master
RevisionStore.php
Go to the documentation of this file.
1 <?php
28 namespace MediaWiki\Revision;
29 
30 use ActorMigration;
31 use CommentStore;
33 use Content;
35 use FallbackContent;
36 use IDBAccessObject;
37 use InvalidArgumentException;
38 use LogicException;
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 RuntimeException;
64 use StatusValue;
65 use Title;
66 use TitleFactory;
67 use Traversable;
68 use WANObjectCache;
69 use Wikimedia\Assert\Assert;
70 use Wikimedia\IPUtils;
76 
87  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
88 
90 
91  public const ROW_CACHE_KEY = 'revision-row-1.29';
92 
93  public const ORDER_OLDEST_TO_NEWEST = 'ASC';
94  public const ORDER_NEWEST_TO_OLDEST = 'DESC';
95 
96  // Constants for get(...)Between methods
97  public const INCLUDE_OLD = 'include_old';
98  public const INCLUDE_NEW = 'include_new';
99  public const INCLUDE_BOTH = 'include_both';
100 
104  private $blobStore;
105 
109  private $wikiId;
110 
114  private $loadBalancer;
115 
119  private $cache;
120 
124  private $commentStore;
125 
130 
132  private $actorStore;
133 
137  private $logger;
138 
143 
147  private $slotRoleStore;
148 
151 
154 
156  private $hookRunner;
157 
159  private $pageStore;
160 
162  private $titleFactory;
163 
188  public function __construct(
201  HookContainer $hookContainer,
202  $wikiId = WikiAwareEntity::LOCAL
203  ) {
204  Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
205 
206  $this->loadBalancer = $loadBalancer;
207  $this->blobStore = $blobStore;
208  $this->cache = $cache;
209  $this->commentStore = $commentStore;
210  $this->contentModelStore = $contentModelStore;
211  $this->slotRoleStore = $slotRoleStore;
212  $this->slotRoleRegistry = $slotRoleRegistry;
213  $this->actorMigration = $actorMigration;
214  $this->actorStore = $actorStore;
215  $this->wikiId = $wikiId;
216  $this->logger = new NullLogger();
217  $this->contentHandlerFactory = $contentHandlerFactory;
218  $this->pageStore = $pageStore;
219  $this->titleFactory = $titleFactory;
220  $this->hookRunner = new HookRunner( $hookContainer );
221  }
222 
223  public function setLogger( LoggerInterface $logger ) {
224  $this->logger = $logger;
225  }
226 
230  public function isReadOnly() {
231  return $this->blobStore->isReadOnly();
232  }
233 
237  private function getDBLoadBalancer() {
238  return $this->loadBalancer;
239  }
240 
246  public function getWikiId() {
247  return $this->wikiId;
248  }
249 
255  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
256  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
257  return $this->getDBConnectionRef( $mode );
258  }
259 
266  private function getDBConnectionRef( $mode, $groups = [] ) {
267  $lb = $this->getDBLoadBalancer();
268  return $lb->getConnectionRef( $mode, $groups, $this->wikiId );
269  }
270 
287  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
288  // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069
289  if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
290  wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' );
291  }
292 
293  $page = $this->getPage( $pageId, $revId, $queryFlags );
294  return $this->titleFactory->castFromPageIdentity( $page );
295  }
296 
307  private function getPage( ?int $pageId, ?int $revId, int $queryFlags = self::READ_NORMAL ) {
308  if ( !$pageId && !$revId ) {
309  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
310  }
311 
312  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
313  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
314  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
315  $queryFlags = self::READ_NORMAL;
316  }
317 
318  $canUsePageId = ( $pageId !== null && $pageId > 0 );
319 
320  // Loading by ID is best
321  if ( $canUsePageId ) {
322  $page = $this->pageStore->getPageById( $pageId, $queryFlags );
323  if ( $page ) {
324  return $this->wrapPage( $page );
325  }
326  }
327 
328  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
329  $canUseRevId = ( $revId !== null && $revId > 0 );
330 
331  if ( $canUseRevId ) {
332  $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags )
333  ->join( 'revision', null, 'page_id=rev_page' )
334  ->conds( [ 'rev_id' => $revId ] )
335  ->caller( __METHOD__ );
336 
337  $page = $pageQuery->fetchPageRecord();
338  if ( $page ) {
339  return $this->wrapPage( $page );
340  }
341  }
342 
343  // If we still don't have a title, fallback to master if that wasn't already happening.
344  if ( $queryFlags === self::READ_NORMAL ) {
345  $title = $this->getPage( $pageId, $revId, self::READ_LATEST );
346  if ( $title ) {
347  $this->logger->info(
348  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
349  [ 'trace' => wfBacktrace() ]
350  );
351  return $title;
352  }
353  }
354 
355  throw new RevisionAccessException(
356  "Could not determine title for page ID $pageId and revision ID $revId"
357  );
358  }
359 
365  private function wrapPage( PageIdentity $page ): PageIdentity {
366  if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
367  // NOTE: since there is still a lot of code that needs a full Title,
368  // and uses Title::castFromPageIdentity() to get one, it's beneficial
369  // to create a Title right away if we can, so we don't have to convert
370  // over and over later on.
371  // When there is less need to convert to Title, this special case can
372  // be removed.
373  return $this->titleFactory->castFromPageIdentity( $page );
374  } else {
375  return $page;
376  }
377  }
378 
386  private function failOnNull( $value, $name ) {
387  if ( $value === null ) {
388  throw new IncompleteRevisionException(
389  "$name must not be " . var_export( $value, true ) . "!"
390  );
391  }
392 
393  return $value;
394  }
395 
403  private function failOnEmpty( $value, $name ) {
404  if ( $value === null || $value === 0 || $value === '' ) {
405  throw new IncompleteRevisionException(
406  "$name must not be " . var_export( $value, true ) . "!"
407  );
408  }
409 
410  return $value;
411  }
412 
424  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
425  // TODO: pass in a DBTransactionContext instead of a database connection.
426  $this->checkDatabaseDomain( $dbw );
427 
428  $slotRoles = $rev->getSlotRoles();
429 
430  // Make sure the main slot is always provided throughout migration
431  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
432  throw new IncompleteRevisionException(
433  'main slot must be provided'
434  );
435  }
436 
437  // Checks
438  $this->failOnNull( $rev->getSize(), 'size field' );
439  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
440  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
441  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
442  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
443  $this->failOnNull( $user->getId(), 'user field' );
444  $this->failOnEmpty( $user->getName(), 'user_text field' );
445 
446  if ( !$rev->isReadyForInsertion() ) {
447  // This is here for future-proofing. At the time this check being added, it
448  // was redundant to the individual checks above.
449  throw new IncompleteRevisionException( 'Revision is incomplete' );
450  }
451 
452  if ( $slotRoles == [ SlotRecord::MAIN ] ) {
453  // T239717: If the main slot is the only slot, make sure the revision's nominal size
454  // and hash match the main slot's nominal size and hash.
455  $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
456  Assert::precondition(
457  $mainSlot->getSize() === $rev->getSize(),
458  'The revisions\'s size must match the main slot\'s size (see T239717)'
459  );
460  Assert::precondition(
461  $mainSlot->getSha1() === $rev->getSha1(),
462  'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
463  );
464  }
465 
466  $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early
467 
468  $parentId = $rev->getParentId() ?? $this->getPreviousRevisionId( $dbw, $rev );
469 
471  $rev = $dbw->doAtomicSection(
472  __METHOD__,
473  function ( IDatabase $dbw, $fname ) use (
474  $rev,
475  $user,
476  $comment,
477  $pageId,
478  $parentId
479  ) {
480  return $this->insertRevisionInternal(
481  $rev,
482  $dbw,
483  $user,
484  $comment,
485  $rev->getPage(),
486  $pageId,
487  $parentId
488  );
489  }
490  );
491 
492  // sanity checks
493  Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' );
494  Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' );
495  Assert::postcondition(
496  $rev->getComment( RevisionRecord::RAW ) !== null,
497  'revision must have a comment'
498  );
499  Assert::postcondition(
500  $rev->getUser( RevisionRecord::RAW ) !== null,
501  'revision must have a user'
502  );
503 
504  // Trigger exception if the main slot is missing.
505  // Technically, this could go away after MCR migration: while
506  // calling code may require a main slot to exist, RevisionStore
507  // really should not know or care about that requirement.
509 
510  foreach ( $slotRoles as $role ) {
511  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
512  Assert::postcondition(
513  $slot->getContent() !== null,
514  $role . ' slot must have content'
515  );
516  Assert::postcondition(
517  $slot->hasRevision(),
518  $role . ' slot must have a revision associated'
519  );
520  }
521 
522  $this->hookRunner->onRevisionRecordInserted( $rev );
523 
524  return $rev;
525  }
526 
539  public function updateSlotsOn(
540  RevisionRecord $revision,
541  RevisionSlotsUpdate $revisionSlotsUpdate,
542  IDatabase $dbw
543  ): array {
544  $this->checkDatabaseDomain( $dbw );
545 
546  // Make sure all modified and removed slots are derived slots
547  foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
548  Assert::precondition(
549  $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
550  'Trying to modify a slot that is not derived'
551  );
552  }
553  foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) {
554  $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
555  Assert::precondition(
556  $isDerived,
557  'Trying to remove a slot that is not derived'
558  );
559  throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' );
560  }
561 
563  $slotRecords = $dbw->doAtomicSection(
564  __METHOD__,
565  function ( IDatabase $dbw, $fname ) use (
566  $revision,
567  $revisionSlotsUpdate
568  ) {
569  return $this->updateSlotsInternal(
570  $revision,
571  $revisionSlotsUpdate,
572  $dbw
573  );
574  }
575  );
576 
577  foreach ( $slotRecords as $role => $slot ) {
578  Assert::postcondition(
579  $slot->getContent() !== null,
580  $role . ' slot must have content'
581  );
582  Assert::postcondition(
583  $slot->hasRevision(),
584  $role . ' slot must have a revision associated'
585  );
586  }
587 
588  return $slotRecords;
589  }
590 
597  private function updateSlotsInternal(
598  RevisionRecord $revision,
599  RevisionSlotsUpdate $revisionSlotsUpdate,
600  IDatabase $dbw
601  ): array {
602  $page = $revision->getPage();
603  $revId = $revision->getId( $this->wikiId );
604  $blobHints = [
605  BlobStore::PAGE_HINT => $page->getId( $this->wikiId ),
606  BlobStore::REVISION_HINT => $revId,
607  BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
608  ];
609 
610  $newSlots = [];
611  foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
612  $slot = $revisionSlotsUpdate->getModifiedSlot( $role );
613  $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
614  }
615 
616  return $newSlots;
617  }
618 
619  private function insertRevisionInternal(
620  RevisionRecord $rev,
621  IDatabase $dbw,
622  UserIdentity $user,
623  CommentStoreComment $comment,
624  PageIdentity $page,
625  $pageId,
626  $parentId
627  ) {
628  $slotRoles = $rev->getSlotRoles();
629 
630  $revisionRow = $this->insertRevisionRowOn(
631  $dbw,
632  $rev,
633  $parentId
634  );
635 
636  $revisionId = $revisionRow['rev_id'];
637 
638  $blobHints = [
639  BlobStore::PAGE_HINT => $pageId,
640  BlobStore::REVISION_HINT => $revisionId,
641  BlobStore::PARENT_HINT => $parentId,
642  ];
643 
644  $newSlots = [];
645  foreach ( $slotRoles as $role ) {
646  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
647 
648  // If the SlotRecord already has a revision ID set, this means it already exists
649  // in the database, and should already belong to the current revision.
650  // However, a slot may already have a revision, but no content ID, if the slot
651  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
652  // mode, and the respective archive row was not yet migrated to the new schema.
653  // In that case, a new slot row (and content row) must be inserted even during
654  // undeletion.
655  if ( $slot->hasRevision() && $slot->hasContentId() ) {
656  // TODO: properly abort transaction if the assertion fails!
657  Assert::parameter(
658  $slot->getRevision() === $revisionId,
659  'slot role ' . $slot->getRole(),
660  'Existing slot should belong to revision '
661  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
662  );
663 
664  // Slot exists, nothing to do, move along.
665  // This happens when restoring archived revisions.
666 
667  $newSlots[$role] = $slot;
668  } else {
669  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
670  }
671  }
672 
673  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
674 
675  $rev = new RevisionStoreRecord(
676  $page,
677  $user,
678  $comment,
679  (object)$revisionRow,
680  new RevisionSlots( $newSlots ),
681  $this->wikiId
682  );
683 
684  return $rev;
685  }
686 
695  private function insertSlotOn(
696  IDatabase $dbw,
697  $revisionId,
698  SlotRecord $protoSlot,
699  PageIdentity $page,
700  array $blobHints = []
701  ) {
702  if ( $protoSlot->hasAddress() ) {
703  $blobAddress = $protoSlot->getAddress();
704  } else {
705  $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
706  }
707 
708  $contentId = null;
709 
710  if ( $protoSlot->hasContentId() ) {
711  $contentId = $protoSlot->getContentId();
712  } else {
713  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
714  }
715 
716  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
717 
718  return SlotRecord::newSaved(
719  $revisionId,
720  $contentId,
721  $blobAddress,
722  $protoSlot
723  );
724  }
725 
733  private function insertIpChangesRow(
734  IDatabase $dbw,
735  UserIdentity $user,
736  RevisionRecord $rev,
737  $revisionId
738  ) {
739  if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
740  $ipcRow = [
741  'ipc_rev_id' => $revisionId,
742  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
743  'ipc_hex' => IPUtils::toHex( $user->getName() ),
744  ];
745  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
746  }
747  }
748 
759  private function insertRevisionRowOn(
760  IDatabase $dbw,
761  RevisionRecord $rev,
762  $parentId
763  ) {
764  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
765 
766  list( $commentFields, $commentCallback ) =
767  $this->commentStore->insertWithTempTable(
768  $dbw,
769  'rev_comment',
771  );
772  $revisionRow += $commentFields;
773 
774  list( $actorFields, $actorCallback ) =
775  $this->actorMigration->getInsertValuesWithTempTable(
776  $dbw,
777  'rev_user',
779  );
780  $revisionRow += $actorFields;
781 
782  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
783 
784  if ( !isset( $revisionRow['rev_id'] ) ) {
785  // only if auto-increment was used
786  $revisionRow['rev_id'] = intval( $dbw->insertId() );
787 
788  if ( $dbw->getType() === 'mysql' ) {
789  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
790  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
791  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
792 
793  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
794  $table = 'archive';
795  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
796  if ( $maxRevId2 >= $maxRevId ) {
797  $maxRevId = $maxRevId2;
798  $table = 'slots';
799  }
800 
801  if ( $maxRevId >= $revisionRow['rev_id'] ) {
802  $this->logger->debug(
803  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
804  . ' Trying to fix it.',
805  [
806  'revid' => $revisionRow['rev_id'],
807  'table' => $table,
808  'maxrevid' => $maxRevId,
809  ]
810  );
811 
812  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
813  throw new MWException( 'Failed to get database lock for T202032' );
814  }
815  $fname = __METHOD__;
817  static function ( $trigger, IDatabase $dbw ) use ( $fname ) {
818  $dbw->unlock( 'fix-for-T202032', $fname );
819  },
820  __METHOD__
821  );
822 
823  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
824 
825  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
826  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
827  // inserts too, though, at least on MariaDB 10.1.29.
828  //
829  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
830  // transactions in this code path thanks to the row lock from the original ->insert() above.
831  //
832  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
833  // that's for non-MySQL DBs.
834  $row1 = $dbw->query(
835  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
836  __METHOD__
837  )->fetchObject();
838 
839  $row2 = $dbw->query(
840  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
841  . ' FOR UPDATE',
842  __METHOD__
843  )->fetchObject();
844 
845  $maxRevId = max(
846  $maxRevId,
847  $row1 ? intval( $row1->v ) : 0,
848  $row2 ? intval( $row2->v ) : 0
849  );
850 
851  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
852  // transactions will throw a duplicate key error here. It doesn't seem worth trying
853  // to avoid that.
854  $revisionRow['rev_id'] = $maxRevId + 1;
855  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
856  }
857  }
858  }
859 
860  $commentCallback( $revisionRow['rev_id'] );
861  $actorCallback( $revisionRow['rev_id'], $revisionRow );
862 
863  return $revisionRow;
864  }
865 
873  private function getBaseRevisionRow(
874  IDatabase $dbw,
875  RevisionRecord $rev,
876  $parentId
877  ) {
878  // Record the edit in revisions
879  $revisionRow = [
880  'rev_page' => $rev->getPageId( $this->wikiId ),
881  'rev_parent_id' => $parentId,
882  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
883  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
884  'rev_deleted' => $rev->getVisibility(),
885  'rev_len' => $rev->getSize(),
886  'rev_sha1' => $rev->getSha1(),
887  ];
888 
889  if ( $rev->getId( $this->wikiId ) !== null ) {
890  // Needed to restore revisions with their original ID
891  $revisionRow['rev_id'] = $rev->getId( $this->wikiId );
892  }
893 
894  return $revisionRow;
895  }
896 
905  private function storeContentBlob(
906  SlotRecord $slot,
907  PageIdentity $page,
908  array $blobHints = []
909  ) {
910  $content = $slot->getContent();
911  $format = $content->getDefaultFormat();
912  $model = $content->getModel();
913 
914  $this->checkContent( $content, $page, $slot->getRole() );
915 
916  return $this->blobStore->storeBlob(
917  $content->serialize( $format ),
918  // These hints "leak" some information from the higher abstraction layer to
919  // low level storage to allow for optimization.
920  array_merge(
921  $blobHints,
922  [
923  BlobStore::DESIGNATION_HINT => 'page-content',
924  BlobStore::ROLE_HINT => $slot->getRole(),
925  BlobStore::SHA1_HINT => $slot->getSha1(),
926  BlobStore::MODEL_HINT => $model,
927  BlobStore::FORMAT_HINT => $format,
928  ]
929  )
930  );
931  }
932 
939  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
940  $slotRow = [
941  'slot_revision_id' => $revisionId,
942  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
943  'slot_content_id' => $contentId,
944  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
945  // that we just inserted.
946  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
947  ];
948  $dbw->insert( 'slots', $slotRow, __METHOD__ );
949  }
950 
957  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
958  $contentRow = [
959  'content_size' => $slot->getSize(),
960  'content_sha1' => $slot->getSha1(),
961  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
962  'content_address' => $blobAddress,
963  ];
964  $dbw->insert( 'content', $contentRow, __METHOD__ );
965  return intval( $dbw->insertId() );
966  }
967 
978  private function checkContent( Content $content, PageIdentity $page, string $role ) {
979  // Note: may return null for revisions that have not yet been inserted
980 
981  $model = $content->getModel();
982  $format = $content->getDefaultFormat();
983  $handler = $content->getContentHandler();
984 
985  if ( !$handler->isSupportedFormat( $format ) ) {
986  throw new MWException(
987  "Can't use format $format with content model $model on $page role $role"
988  );
989  }
990 
991  if ( !$content->isValid() ) {
992  throw new MWException(
993  "New content for $page role $role is not valid! Content model is $model"
994  );
995  }
996  }
997 
1023  public function newNullRevision(
1024  IDatabase $dbw,
1025  PageIdentity $page,
1026  CommentStoreComment $comment,
1027  $minor,
1028  UserIdentity $user
1029  ) {
1030  $this->checkDatabaseDomain( $dbw );
1031 
1032  $pageId = $this->getArticleId( $page );
1033 
1034  // T51581: Lock the page table row to ensure no other process
1035  // is adding a revision to the page at the same time.
1036  // Avoid locking extra tables, compare T191892.
1037  $pageLatest = $dbw->selectField(
1038  'page',
1039  'page_latest',
1040  [ 'page_id' => $pageId ],
1041  __METHOD__,
1042  [ 'FOR UPDATE' ]
1043  );
1044 
1045  if ( !$pageLatest ) {
1046  $msg = 'T235589: Failed to select table row during null revision creation' .
1047  " Page id '$pageId' does not exist.";
1048  $this->logger->error(
1049  $msg,
1050  [ 'exception' => new RuntimeException( $msg ) ]
1051  );
1052 
1053  return null;
1054  }
1055 
1056  // Fetch the actual revision row from master, without locking all extra tables.
1057  $oldRevision = $this->loadRevisionFromConds(
1058  $dbw,
1059  [ 'rev_id' => intval( $pageLatest ) ],
1060  self::READ_LATEST,
1061  $page
1062  );
1063 
1064  if ( !$oldRevision ) {
1065  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1066  $this->logger->error(
1067  $msg,
1068  [ 'exception' => new RuntimeException( $msg ) ]
1069  );
1070  return null;
1071  }
1072 
1073  // Construct the new revision
1074  $timestamp = MWTimestamp::now( TS_MW );
1075  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1076 
1077  $newRevision->setComment( $comment );
1078  $newRevision->setUser( $user );
1079  $newRevision->setTimestamp( $timestamp );
1080  $newRevision->setMinorEdit( $minor );
1081 
1082  return $newRevision;
1083  }
1084 
1094  public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1095  $rc = $this->getRecentChange( $rev );
1096  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1097  return $rc->getAttribute( 'rc_id' );
1098  } else {
1099  return 0;
1100  }
1101  }
1102 
1116  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1117  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1118 
1120  [ 'rc_this_oldid' => $rev->getId( $this->wikiId ) ],
1121  __METHOD__,
1122  $dbType
1123  );
1124 
1125  // XXX: cache this locally? Glue it to the RevisionRecord?
1126  return $rc;
1127  }
1128 
1148  private function loadSlotContent(
1149  SlotRecord $slot,
1150  $blobData = null,
1151  $blobFlags = null,
1152  $blobFormat = null,
1153  $queryFlags = 0
1154  ) {
1155  if ( $blobData !== null ) {
1156  Assert::parameterType( 'string', $blobData, '$blobData' );
1157  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1158 
1159  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1160 
1161  if ( $blobFlags === null ) {
1162  // No blob flags, so use the blob verbatim.
1163  $data = $blobData;
1164  } else {
1165  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1166  if ( $data === false ) {
1167  throw new RevisionAccessException(
1168  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1169  );
1170  }
1171  }
1172 
1173  } else {
1174  $address = $slot->getAddress();
1175  try {
1176  $data = $this->blobStore->getBlob( $address, $queryFlags );
1177  } catch ( BlobAccessException $e ) {
1178  throw new RevisionAccessException(
1179  "Failed to load data blob from $address: " . $e->getMessage() . '. '
1180  . 'If this problem persist, use the findBadBlobs maintenance script '
1181  . 'to investigate the issue and mark bad blobs.',
1182  0, $e
1183  );
1184  }
1185  }
1186 
1187  $model = $slot->getModel();
1188 
1189  // If the content model is not known, don't fail here (T220594, T220793, T228921)
1190  if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1191  $this->logger->warning(
1192  "Undefined content model '$model', falling back to UnknownContent",
1193  [
1194  'content_address' => $slot->getAddress(),
1195  'rev_id' => $slot->getRevision(),
1196  'role_name' => $slot->getRole(),
1197  'model_name' => $model,
1198  'trace' => wfBacktrace()
1199  ]
1200  );
1201 
1202  return new FallbackContent( $data, $model );
1203  }
1204 
1205  return $this->contentHandlerFactory
1206  ->getContentHandler( $model )
1207  ->unserializeContent( $data, $blobFormat );
1208  }
1209 
1227  public function getRevisionById( $id, $flags = 0, PageIdentity $page = null ) {
1228  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags, $page );
1229  }
1230 
1247  public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) {
1248  $conds = [
1249  'page_namespace' => $page->getNamespace(),
1250  'page_title' => $page->getDBkey()
1251  ];
1252 
1253  if ( $page instanceof LinkTarget ) {
1254  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1255  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1256  }
1257 
1258  if ( $revId ) {
1259  // Use the specified revision ID.
1260  // Note that we use newRevisionFromConds here because we want to retry
1261  // and fall back to master if the page is not found on a replica.
1262  // Since the caller supplied a revision ID, we are pretty sure the revision is
1263  // supposed to exist, so we should try hard to find it.
1264  $conds['rev_id'] = $revId;
1265  return $this->newRevisionFromConds( $conds, $flags, $page );
1266  } else {
1267  // Use a join to get the latest revision.
1268  // Note that we don't use newRevisionFromConds here because we don't want to retry
1269  // and fall back to master. The assumption is that we only want to force the fallback
1270  // if we are quite sure the revision exists because the caller supplied a revision ID.
1271  // If the page isn't found at all on a replica, it probably simply does not exist.
1272  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1273  $conds[] = 'rev_id=page_latest';
1274  return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1275  }
1276  }
1277 
1294  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1295  $conds = [ 'page_id' => $pageId ];
1296  if ( $revId ) {
1297  // Use the specified revision ID.
1298  // Note that we use newRevisionFromConds here because we want to retry
1299  // and fall back to master if the page is not found on a replica.
1300  // Since the caller supplied a revision ID, we are pretty sure the revision is
1301  // supposed to exist, so we should try hard to find it.
1302  $conds['rev_id'] = $revId;
1303  return $this->newRevisionFromConds( $conds, $flags );
1304  } else {
1305  // Use a join to get the latest revision.
1306  // Note that we don't use newRevisionFromConds here because we don't want to retry
1307  // and fall back to master. The assumption is that we only want to force the fallback
1308  // if we are quite sure the revision exists because the caller supplied a revision ID.
1309  // If the page isn't found at all on a replica, it probably simply does not exist.
1310  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1311 
1312  $conds[] = 'rev_id=page_latest';
1313 
1314  return $this->loadRevisionFromConds( $db, $conds, $flags );
1315  }
1316  }
1317 
1333  public function getRevisionByTimestamp(
1334  $page,
1335  string $timestamp,
1336  int $flags = IDBAccessObject::READ_NORMAL
1337  ): ?RevisionRecord {
1338  if ( $page instanceof LinkTarget ) {
1339  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1340  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1341  }
1342  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1343  return $this->newRevisionFromConds(
1344  [
1345  'rev_timestamp' => $db->timestamp( $timestamp ),
1346  'page_namespace' => $page->getNamespace(),
1347  'page_title' => $page->getDBkey()
1348  ],
1349  $flags,
1350  $page
1351  );
1352  }
1353 
1361  private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) {
1362  $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1363 
1364  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1365  $db = $this->getDBConnectionRef( $dbMode );
1366 
1367  $res = $db->select(
1368  $revQuery['tables'],
1369  $revQuery['fields'],
1370  [
1371  'slot_revision_id' => $revId,
1372  ],
1373  __METHOD__,
1374  $dbOptions,
1375  $revQuery['joins']
1376  );
1377 
1378  if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1379  // If we found no slots, try looking on the primary database (T212428, T252156)
1380  $this->logger->info(
1381  __METHOD__ . ' falling back to READ_LATEST.',
1382  [
1383  'revid' => $revId,
1384  'trace' => wfBacktrace( true )
1385  ]
1386  );
1387  return $this->loadSlotRecords(
1388  $revId,
1389  $queryFlags | self::READ_LATEST,
1390  $page
1391  );
1392  }
1393 
1394  return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1395  }
1396 
1409  private function constructSlotRecords(
1410  $revId,
1411  $slotRows,
1412  $queryFlags,
1413  PageIdentity $page,
1414  $slotContents = null
1415  ) {
1416  $slots = [];
1417 
1418  foreach ( $slotRows as $row ) {
1419  // Resolve role names and model names from in-memory cache, if they were not joined in.
1420  if ( !isset( $row->role_name ) ) {
1421  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1422  }
1423 
1424  if ( !isset( $row->model_name ) ) {
1425  if ( isset( $row->content_model ) ) {
1426  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1427  } else {
1428  // We may get here if $row->model_name is set but null, perhaps because it
1429  // came from rev_content_model, which is NULL for the default model.
1430  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1431  $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1432  }
1433  }
1434 
1435  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1436  if ( isset( $row->blob_data ) ) {
1437  $slotContents[$row->content_address] = $row->blob_data;
1438  }
1439 
1440  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1441  $blob = null;
1442  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1443  $blob = $slotContents[$slot->getAddress()];
1444  if ( $blob instanceof Content ) {
1445  return $blob;
1446  }
1447  }
1448  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1449  };
1450 
1451  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1452  }
1453 
1454  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1455  $this->logger->error(
1456  __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1457  [
1458  'revid' => $revId,
1459  'queryFlags' => $queryFlags,
1460  'trace' => wfBacktrace( true )
1461  ]
1462  );
1463 
1464  throw new RevisionAccessException(
1465  'Main slot of revision not found in database. See T212428.'
1466  );
1467  }
1468 
1469  return $slots;
1470  }
1471 
1487  private function newRevisionSlots(
1488  $revId,
1489  $revisionRow,
1490  $slotRows,
1491  $queryFlags,
1492  PageIdentity $page
1493  ) {
1494  if ( $slotRows ) {
1495  $slots = new RevisionSlots(
1496  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1497  );
1498  } else {
1499  // XXX: do we need the same kind of caching here
1500  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1501 
1502  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) {
1503  return $this->loadSlotRecords( $revId, $queryFlags, $page );
1504  } );
1505  }
1506 
1507  return $slots;
1508  }
1509 
1531  public function newRevisionFromArchiveRow(
1532  $row,
1533  $queryFlags = 0,
1534  PageIdentity $page = null,
1535  array $overrides = []
1536  ) {
1537  return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides );
1538  }
1539 
1552  public function newRevisionFromRow(
1553  $row,
1554  $queryFlags = 0,
1555  PageIdentity $page = null,
1556  $fromCache = false
1557  ) {
1558  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache );
1559  }
1560 
1581  $row,
1582  $slots,
1583  $queryFlags = 0,
1584  PageIdentity $page = null,
1585  array $overrides = []
1586  ) {
1587  Assert::parameterType( \stdClass::class, $row, '$row' );
1588 
1589  // check second argument, since the old Revision::newFromArchiveRow had $overrides in that spot.
1590  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1591 
1592  if ( !$page && isset( $overrides['title'] ) ) {
1593  if ( !( $overrides['title'] instanceof PageIdentity ) ) {
1594  throw new MWException( 'title field override must contain a PageIdentity object.' );
1595  }
1596 
1597  $page = $overrides['title'];
1598  }
1599 
1600  if ( !isset( $page ) ) {
1601  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1602  $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1603  } else {
1604  throw new InvalidArgumentException(
1605  'A Title or ar_namespace and ar_title must be given'
1606  );
1607  }
1608  }
1609 
1610  foreach ( $overrides as $key => $value ) {
1611  $field = "ar_$key";
1612  $row->$field = $value;
1613  }
1614 
1615  try {
1616  $user = $this->actorStore->newActorFromRowFields(
1617  $row->ar_user ?? null,
1618  $row->ar_user_text ?? null,
1619  $row->ar_actor ?? null
1620  );
1621  } catch ( InvalidArgumentException $ex ) {
1622  $this->logger->warning( 'Could not load user for archive revision {rev_id}', [
1623  'ar_rev_id' => $row->ar_rev_id,
1624  'ar_actor' => $row->ar_actor ?? 'null',
1625  'ar_user_text' => $row->ar_user_text ?? 'null',
1626  'ar_user' => $row->ar_user ?? 'null',
1627  'exception' => $ex
1628  ] );
1629  $user = $this->actorStore->getUnknownActor();
1630  }
1631 
1632  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1633  // Legacy because $row may have come from self::selectFields()
1634  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1635 
1636  if ( !( $slots instanceof RevisionSlots ) ) {
1637  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $page );
1638  }
1639  return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId );
1640  }
1641 
1661  $row,
1662  $slots,
1663  $queryFlags = 0,
1664  PageIdentity $page = null,
1665  $fromCache = false
1666  ) {
1667  Assert::parameterType( \stdClass::class, $row, '$row' );
1668 
1669  if ( !$page ) {
1670  if ( isset( $row->page_id )
1671  && isset( $row->page_namespace )
1672  && isset( $row->page_title )
1673  ) {
1674  $page = new PageIdentityValue(
1675  (int)$row->page_id,
1676  (int)$row->page_namespace,
1677  $row->page_title,
1678  $this->wikiId
1679  );
1680 
1681  $page = $this->wrapPage( $page );
1682  } else {
1683  $pageId = (int)( $row->rev_page ?? 0 );
1684  $revId = (int)( $row->rev_id ?? 0 );
1685 
1686  $page = $this->getPage( $pageId, $revId, $queryFlags );
1687  }
1688  } else {
1689  $page = $this->ensureRevisionRowMatchesPage( $row, $page );
1690  }
1691 
1692  if ( !$page ) {
1693  // This should already have been caught about, but apparently
1694  // it not always is, see T286877.
1695  throw new RevisionAccessException(
1696  "Failed to determine page associated with revision {$row->rev_id}"
1697  );
1698  }
1699 
1700  try {
1701  $user = $this->actorStore->newActorFromRowFields(
1702  $row->rev_user ?? null,
1703  $row->rev_user_text ?? null,
1704  $row->rev_actor ?? null
1705  );
1706  } catch ( InvalidArgumentException $ex ) {
1707  $this->logger->warning( 'Could not load user for revision {rev_id}', [
1708  'rev_id' => $row->rev_id,
1709  'rev_actor' => $row->rev_actor ?? 'null',
1710  'rev_user_text' => $row->rev_user_text ?? 'null',
1711  'rev_user' => $row->rev_user ?? 'null',
1712  'exception' => $ex
1713  ] );
1714  $user = $this->actorStore->getUnknownActor();
1715  }
1716 
1717  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1718  // Legacy because $row may have come from self::selectFields()
1719  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1720 
1721  if ( !( $slots instanceof RevisionSlots ) ) {
1722  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $page );
1723  }
1724 
1725  // If this is a cached row, instantiate a cache-aware RevisionRecord to avoid stale data.
1726  if ( $fromCache ) {
1727  $rev = new RevisionStoreCacheRecord(
1728  function ( $revId ) use ( $queryFlags ) {
1729  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1730  $row = $this->fetchRevisionRowFromConds(
1731  $db,
1732  [ 'rev_id' => intval( $revId ) ]
1733  );
1734  if ( !$row && !( $queryFlags & self::READ_LATEST ) ) {
1735  // If we found no slots, try looking on the primary database (T259738)
1736  $this->logger->info(
1737  'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1738  [
1739  'revid' => $revId,
1740  'trace' => wfBacktrace( true )
1741  ]
1742  );
1743  $dbw = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
1744  $row = $this->fetchRevisionRowFromConds(
1745  $dbw,
1746  [ 'rev_id' => intval( $revId ) ]
1747  );
1748  }
1749  if ( !$row ) {
1750  return [ null, null ];
1751  }
1752  return [
1753  $row->rev_deleted,
1754  $this->actorStore->newActorFromRowFields(
1755  $row->rev_user ?? null,
1756  $row->rev_user_text ?? null,
1757  $row->rev_actor ?? null
1758  )
1759  ];
1760  },
1761  $page, $user, $comment, $row, $slots, $this->wikiId
1762  );
1763  } else {
1764  $rev = new RevisionStoreRecord(
1765  $page, $user, $comment, $row, $slots, $this->wikiId );
1766  }
1767  return $rev;
1768  }
1769 
1781  private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) {
1782  $revId = (int)( $row->rev_id ?? 0 );
1783  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1784  $expectedPageId = $page->getId( $this->wikiId );
1785  // Avoid fatal error when the Title's ID changed, T246720
1786  if ( $revPageId && $expectedPageId && $revPageId !== $expectedPageId ) {
1787  // NOTE: PageStore::getPageByReference may use the page ID, which we don't want here.
1788  $pageRec = $this->pageStore->getPageByName(
1789  $page->getNamespace(),
1790  $page->getDBkey(),
1791  PageStore::READ_LATEST
1792  );
1793  $masterPageId = $pageRec->getId( $this->wikiId );
1794  $masterLatest = $pageRec->getLatest( $this->wikiId );
1795  if ( $revPageId === $masterPageId ) {
1796  if ( $page instanceof Title ) {
1797  // If we were using a Title object, keep using it, but update the page ID.
1798  // This way, we don't unexpectedly mix Titles with immutable value objects.
1799  $page->resetArticleID( $masterPageId );
1800 
1801  } else {
1802  $page = $pageRec;
1803  }
1804 
1805  $this->logger->info(
1806  "Encountered stale Title object",
1807  [
1808  'page_id_stale' => $expectedPageId,
1809  'page_id_reloaded' => $masterPageId,
1810  'page_latest' => $masterLatest,
1811  'rev_id' => $revId,
1812  'trace' => wfBacktrace()
1813  ] + $context
1814  );
1815  } else {
1816  $expectedTitle = (string)$page;
1817  if ( $page instanceof Title ) {
1818  // If we started with a Title, keep using a Title.
1819  $page = $this->titleFactory->newFromID( $revPageId );
1820  } else {
1821  $page = $pageRec;
1822  }
1823 
1824  // This could happen if a caller to e.g. getRevisionById supplied a Title that is
1825  // plain wrong. In this case, we should ideally throw an IllegalArgumentException.
1826  // However, it is more likely that we encountered a race condition during a page
1827  // move (T268910, T279832) or database corruption (T263340). That situation
1828  // should not be ignored, but we can allow the request to continue in a reasonable
1829  // manner without breaking things for the user.
1830  $this->logger->error(
1831  "Encountered mismatching Title object (see T259022, T268910, T279832, T263340)",
1832  [
1833  'expected_page_id' => $masterPageId,
1834  'expected_page_title' => $expectedTitle,
1835  'rev_page' => $revPageId,
1836  'rev_page_title' => (string)$page,
1837  'page_latest' => $masterLatest,
1838  'rev_id' => $revId,
1839  'trace' => wfBacktrace()
1840  ] + $context
1841  );
1842  }
1843  }
1844 
1845  return $page;
1846  }
1847 
1873  public function newRevisionsFromBatch(
1874  $rows,
1875  array $options = [],
1876  $queryFlags = 0,
1877  PageIdentity $page = null
1878  ) {
1879  $result = new StatusValue();
1880  $archiveMode = $options['archive'] ?? false;
1881 
1882  if ( $archiveMode ) {
1883  $revIdField = 'ar_rev_id';
1884  } else {
1885  $revIdField = 'rev_id';
1886  }
1887 
1888  $rowsByRevId = [];
1889  $pageIdsToFetchTitles = [];
1890  $titlesByPageKey = [];
1891  foreach ( $rows as $row ) {
1892  if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1893  $result->warning(
1894  'internalerror_info',
1895  "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1896  );
1897  }
1898 
1899  // Attach a page key to the row, so we can find and reuse Title objects easily.
1900  $row->_page_key =
1901  $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1902 
1903  if ( $page ) {
1904  if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1905  throw new InvalidArgumentException(
1906  "Revision {$row->$revIdField} doesn't belong to page "
1907  . $this->getArticleId( $page )
1908  );
1909  }
1910 
1911  if ( $archiveMode
1912  && ( $row->ar_namespace != $page->getNamespace()
1913  || $row->ar_title !== $page->getDBkey() )
1914  ) {
1915  throw new InvalidArgumentException(
1916  "Revision {$row->$revIdField} doesn't belong to page "
1917  . $page
1918  );
1919  }
1920  } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1921  if ( isset( $row->page_namespace ) && isset( $row->page_title )
1922  // This should always be true, but just in case we don't have a page_id
1923  // set or it doesn't match rev_page, let's fetch the title again.
1924  && isset( $row->page_id ) && isset( $row->rev_page )
1925  && $row->rev_page === $row->page_id
1926  ) {
1927  $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1928  } elseif ( $archiveMode ) {
1929  // Can't look up deleted pages by ID, but we have namespace and title
1930  $titlesByPageKey[ $row->_page_key ] =
1931  Title::makeTitle( $row->ar_namespace, $row->ar_title );
1932  } else {
1933  $pageIdsToFetchTitles[] = $row->rev_page;
1934  }
1935  }
1936  $rowsByRevId[$row->$revIdField] = $row;
1937  }
1938 
1939  if ( empty( $rowsByRevId ) ) {
1940  $result->setResult( true, [] );
1941  return $result;
1942  }
1943 
1944  // If the page is not supplied, batch-fetch Title objects.
1945  if ( $page ) {
1946  // same logic as for $row->_page_key above
1947  $pageKey = $archiveMode
1948  ? $page->getNamespace() . ':' . $page->getDBkey()
1949  : $this->getArticleId( $page );
1950 
1951  $titlesByPageKey[$pageKey] = $page;
1952  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1953  // Note: when we fetch titles by ID, the page key is also the ID.
1954  // We should never get here if $archiveMode is true.
1955  Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1956 
1957  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1958  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1959  $titlesByPageKey[$t->getArticleID()] = $t;
1960  }
1961  }
1962 
1963  // which method to use for creating RevisionRecords
1964  $newRevisionRecord = [
1965  $this,
1966  $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1967  ];
1968 
1969  if ( !isset( $options['slots'] ) ) {
1970  $result->setResult(
1971  true,
1972  array_map(
1973  static function ( $row )
1974  use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1975  try {
1976  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1977  $result->warning(
1978  'internalerror_info',
1979  "Couldn't find title for rev {$row->$revIdField} "
1980  . "(page key {$row->_page_key})"
1981  );
1982  return null;
1983  }
1984  return $newRevisionRecord( $row, null, $queryFlags,
1985  $titlesByPageKey[ $row->_page_key ] );
1986  } catch ( MWException $e ) {
1987  $result->warning( 'internalerror_info', $e->getMessage() );
1988  return null;
1989  }
1990  },
1991  $rowsByRevId
1992  )
1993  );
1994  return $result;
1995  }
1996 
1997  $slotRowOptions = [
1998  'slots' => $options['slots'] ?? true,
1999  'blobs' => $options['content'] ?? false,
2000  ];
2001 
2002  if ( is_array( $slotRowOptions['slots'] )
2003  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
2004  ) {
2005  // Make sure the main slot is always loaded, RevisionRecord requires this.
2006  $slotRowOptions['slots'][] = SlotRecord::MAIN;
2007  }
2008 
2009  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2010 
2011  $result->merge( $slotRowsStatus );
2012  $slotRowsByRevId = $slotRowsStatus->getValue();
2013 
2014  $result->setResult(
2015  true,
2016  array_map(
2017  function ( $row )
2018  use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
2019  $revIdField, $newRevisionRecord
2020  ) {
2021  if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
2022  $result->warning(
2023  'internalerror_info',
2024  "Couldn't find slots for rev {$row->$revIdField}"
2025  );
2026  return null;
2027  }
2028  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2029  $result->warning(
2030  'internalerror_info',
2031  "Couldn't find title for rev {$row->$revIdField} "
2032  . "(page key {$row->_page_key})"
2033  );
2034  return null;
2035  }
2036  try {
2037  return $newRevisionRecord(
2038  $row,
2039  new RevisionSlots(
2040  $this->constructSlotRecords(
2041  $row->$revIdField,
2042  $slotRowsByRevId[$row->$revIdField],
2043  $queryFlags,
2044  $titlesByPageKey[$row->_page_key]
2045  )
2046  ),
2047  $queryFlags,
2048  $titlesByPageKey[$row->_page_key]
2049  );
2050  } catch ( MWException $e ) {
2051  $result->warning( 'internalerror_info', $e->getMessage() );
2052  return null;
2053  }
2054  },
2055  $rowsByRevId
2056  )
2057  );
2058  return $result;
2059  }
2060 
2084  private function getSlotRowsForBatch(
2085  $rowsOrIds,
2086  array $options = [],
2087  $queryFlags = 0
2088  ) {
2089  $result = new StatusValue();
2090 
2091  $revIds = [];
2092  foreach ( $rowsOrIds as $row ) {
2093  if ( is_object( $row ) ) {
2094  $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
2095  } else {
2096  $revIds[] = (int)$row;
2097  }
2098  }
2099 
2100  // Nothing to do.
2101  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2102  if ( empty( $revIds ) ) {
2103  $result->setResult( true, [] );
2104  return $result;
2105  }
2106 
2107  // We need to set the `content` flag to join in content meta-data
2108  $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
2109  $revIdField = $slotQueryInfo['keys']['rev_id'];
2110  $slotQueryConds = [ $revIdField => $revIds ];
2111 
2112  if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2113  if ( empty( $options['slots'] ) ) {
2114  // Degenerate case: return no slots for each revision.
2115  $result->setResult( true, array_fill_keys( $revIds, [] ) );
2116  return $result;
2117  }
2118 
2119  $roleIdField = $slotQueryInfo['keys']['role_id'];
2120  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
2121  return $this->slotRoleStore->getId( $slot_name );
2122  }, $options['slots'] );
2123  }
2124 
2125  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2126  $slotRows = $db->select(
2127  $slotQueryInfo['tables'],
2128  $slotQueryInfo['fields'],
2129  $slotQueryConds,
2130  __METHOD__,
2131  [],
2132  $slotQueryInfo['joins']
2133  );
2134 
2135  $slotContents = null;
2136  if ( $options['blobs'] ?? false ) {
2137  $blobAddresses = [];
2138  foreach ( $slotRows as $slotRow ) {
2139  $blobAddresses[] = $slotRow->content_address;
2140  }
2141  $slotContentFetchStatus = $this->blobStore
2142  ->getBlobBatch( $blobAddresses, $queryFlags );
2143  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2144  $result->warning( $error['message'], ...$error['params'] );
2145  }
2146  $slotContents = $slotContentFetchStatus->getValue();
2147  }
2148 
2149  $slotRowsByRevId = [];
2150  foreach ( $slotRows as $slotRow ) {
2151  if ( $slotContents === null ) {
2152  // nothing to do
2153  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2154  $slotRow->blob_data = $slotContents[$slotRow->content_address];
2155  } else {
2156  $result->warning(
2157  'internalerror_info',
2158  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2159  );
2160  $slotRow->blob_data = null;
2161  }
2162 
2163  // conditional needed for SCHEMA_COMPAT_READ_OLD
2164  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2165  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2166  }
2167 
2168  // conditional needed for SCHEMA_COMPAT_READ_OLD
2169  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2170  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2171  }
2172 
2173  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2174  }
2175 
2176  $result->setResult( true, $slotRowsByRevId );
2177  return $result;
2178  }
2179 
2200  public function getContentBlobsForBatch(
2201  $rowsOrIds,
2202  $slots = null,
2203  $queryFlags = 0
2204  ) {
2205  $result = $this->getSlotRowsForBatch(
2206  $rowsOrIds,
2207  [ 'slots' => $slots, 'blobs' => true ],
2208  $queryFlags
2209  );
2210 
2211  if ( $result->isOK() ) {
2212  // strip out all internal meta data that we don't want to expose
2213  foreach ( $result->value as $revId => $rowsByRole ) {
2214  foreach ( $rowsByRole as $role => $slotRow ) {
2215  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2216  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2217  // if we didn't ask for it.
2218  unset( $result->value[$revId][$role] );
2219  continue;
2220  }
2221 
2222  $result->value[$revId][$role] = (object)[
2223  'blob_data' => $slotRow->blob_data,
2224  'model_name' => $slotRow->model_name,
2225  ];
2226  }
2227  }
2228  }
2229 
2230  return $result;
2231  }
2232 
2249  private function newRevisionFromConds(
2250  array $conditions,
2251  int $flags = IDBAccessObject::READ_NORMAL,
2252  PageIdentity $page = null,
2253  array $options = []
2254  ) {
2255  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2256  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2257 
2258  $lb = $this->getDBLoadBalancer();
2259 
2260  // Make sure new pending/committed revision are visibile later on
2261  // within web requests to certain avoid bugs like T93866 and T94407.
2262  if ( !$rev
2263  && !( $flags & self::READ_LATEST )
2264  && $lb->hasStreamingReplicaServers()
2265  && $lb->hasOrMadeRecentMasterChanges()
2266  ) {
2267  $flags = self::READ_LATEST;
2268  $dbw = $this->getDBConnectionRef( DB_PRIMARY );
2269  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2270  }
2271 
2272  return $rev;
2273  }
2274 
2289  private function loadRevisionFromConds(
2290  IDatabase $db,
2291  array $conditions,
2292  int $flags = IDBAccessObject::READ_NORMAL,
2293  PageIdentity $page = null,
2294  array $options = []
2295  ) {
2296  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2297  if ( $row ) {
2298  return $this->newRevisionFromRow( $row, $flags, $page );
2299  }
2300 
2301  return null;
2302  }
2303 
2311  private function checkDatabaseDomain( IDatabase $db ) {
2312  $dbDomain = $db->getDomainID();
2313  $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2314  if ( $dbDomain === $storeDomain ) {
2315  return;
2316  }
2317 
2318  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2319  }
2320 
2334  private function fetchRevisionRowFromConds(
2335  IDatabase $db,
2336  array $conditions,
2337  int $flags = IDBAccessObject::READ_NORMAL,
2338  array $options = []
2339  ) {
2340  $this->checkDatabaseDomain( $db );
2341 
2342  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2343  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2344  $options[] = 'FOR UPDATE';
2345  }
2346  return $db->selectRow(
2347  $revQuery['tables'],
2348  $revQuery['fields'],
2349  $conditions,
2350  __METHOD__,
2351  $options,
2352  $revQuery['joins']
2353  );
2354  }
2355 
2377  public function getQueryInfo( $options = [] ) {
2378  $ret = [
2379  'tables' => [],
2380  'fields' => [],
2381  'joins' => [],
2382  ];
2383 
2384  $ret['tables'][] = 'revision';
2385  $ret['fields'] = array_merge( $ret['fields'], [
2386  'rev_id',
2387  'rev_page',
2388  'rev_timestamp',
2389  'rev_minor_edit',
2390  'rev_deleted',
2391  'rev_len',
2392  'rev_parent_id',
2393  'rev_sha1',
2394  ] );
2395 
2396  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2397  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2398  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2399  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2400 
2401  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2402  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2403  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2404  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2405 
2406  if ( in_array( 'page', $options, true ) ) {
2407  $ret['tables'][] = 'page';
2408  $ret['fields'] = array_merge( $ret['fields'], [
2409  'page_namespace',
2410  'page_title',
2411  'page_id',
2412  'page_latest',
2413  'page_is_redirect',
2414  'page_len',
2415  ] );
2416  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2417  }
2418 
2419  if ( in_array( 'user', $options, true ) ) {
2420  $ret['tables'][] = 'user';
2421  $ret['fields'] = array_merge( $ret['fields'], [
2422  'user_name',
2423  ] );
2424  $u = $actorQuery['fields']['rev_user'];
2425  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2426  }
2427 
2428  if ( in_array( 'text', $options, true ) ) {
2429  throw new InvalidArgumentException(
2430  'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2431  );
2432  }
2433 
2434  return $ret;
2435  }
2436 
2457  public function getSlotsQueryInfo( $options = [] ) {
2458  $ret = [
2459  'tables' => [],
2460  'fields' => [],
2461  'joins' => [],
2462  'keys' => [],
2463  ];
2464 
2465  $ret['keys']['rev_id'] = 'slot_revision_id';
2466  $ret['keys']['role_id'] = 'slot_role_id';
2467 
2468  $ret['tables'][] = 'slots';
2469  $ret['fields'] = array_merge( $ret['fields'], [
2470  'slot_revision_id',
2471  'slot_content_id',
2472  'slot_origin',
2473  'slot_role_id',
2474  ] );
2475 
2476  if ( in_array( 'role', $options, true ) ) {
2477  // Use left join to attach role name, so we still find the revision row even
2478  // if the role name is missing. This triggers a more obvious failure mode.
2479  $ret['tables'][] = 'slot_roles';
2480  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2481  $ret['fields'][] = 'role_name';
2482  }
2483 
2484  if ( in_array( 'content', $options, true ) ) {
2485  $ret['keys']['model_id'] = 'content_model';
2486 
2487  $ret['tables'][] = 'content';
2488  $ret['fields'] = array_merge( $ret['fields'], [
2489  'content_size',
2490  'content_sha1',
2491  'content_address',
2492  'content_model',
2493  ] );
2494  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2495 
2496  if ( in_array( 'model', $options, true ) ) {
2497  // Use left join to attach model name, so we still find the revision row even
2498  // if the model name is missing. This triggers a more obvious failure mode.
2499  $ret['tables'][] = 'content_models';
2500  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2501  $ret['fields'][] = 'model_name';
2502  }
2503 
2504  }
2505 
2506  return $ret;
2507  }
2508 
2526  public function getArchiveQueryInfo() {
2527  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2528  $ret = [
2529  'tables' => [
2530  'archive',
2531  'archive_actor' => 'actor'
2532  ] + $commentQuery['tables'],
2533  'fields' => [
2534  'ar_id',
2535  'ar_page_id',
2536  'ar_namespace',
2537  'ar_title',
2538  'ar_rev_id',
2539  'ar_timestamp',
2540  'ar_minor_edit',
2541  'ar_deleted',
2542  'ar_len',
2543  'ar_parent_id',
2544  'ar_sha1',
2545  'ar_actor',
2546  'ar_user' => 'archive_actor.actor_user',
2547  'ar_user_text' => 'archive_actor.actor_name',
2548  ] + $commentQuery['fields'],
2549  'joins' => [
2550  'archive_actor' => [ 'JOIN', 'actor_id=ar_actor' ]
2551  ] + $commentQuery['joins'],
2552  ];
2553 
2554  return $ret;
2555  }
2556 
2566  public function getRevisionSizes( array $revIds ) {
2567  $dbr = $this->getDBConnectionRef( DB_REPLICA );
2568  $revLens = [];
2569  if ( !$revIds ) {
2570  return $revLens; // empty
2571  }
2572 
2573  $res = $dbr->select(
2574  'revision',
2575  [ 'rev_id', 'rev_len' ],
2576  [ 'rev_id' => $revIds ],
2577  __METHOD__
2578  );
2579 
2580  foreach ( $res as $row ) {
2581  $revLens[$row->rev_id] = intval( $row->rev_len );
2582  }
2583 
2584  return $revLens;
2585  }
2586 
2595  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2596  $op = $dir === 'next' ? '>' : '<';
2597  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2598 
2599  $revisionIdValue = $rev->getId( $this->wikiId );
2600 
2601  if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) {
2602  // revision is unsaved or otherwise incomplete
2603  return null;
2604  }
2605 
2606  if ( $rev instanceof RevisionArchiveRecord ) {
2607  // revision is deleted, so it's not part of the page history
2608  return null;
2609  }
2610 
2611  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2612  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2613 
2614  $ts = $this->getTimestampFromId( $revisionIdValue, $flags );
2615  if ( $ts === false ) {
2616  // XXX Should this be moved into getTimestampFromId?
2617  $ts = $db->selectField( 'archive', 'ar_timestamp',
2618  [ 'ar_rev_id' => $revisionIdValue ], __METHOD__ );
2619  if ( $ts === false ) {
2620  // XXX Is this reachable? How can we have a page id but no timestamp?
2621  return null;
2622  }
2623  }
2624  $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2625 
2626  $revId = $db->selectField( 'revision', 'rev_id',
2627  [
2628  'rev_page' => $rev->getPageId( $this->wikiId ),
2629  "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op $revisionIdValue )"
2630  ],
2631  __METHOD__,
2632  [
2633  'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2634  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2635  ]
2636  );
2637 
2638  if ( $revId === false ) {
2639  return null;
2640  }
2641 
2642  return $this->getRevisionById( intval( $revId ) );
2643  }
2644 
2659  public function getPreviousRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2660  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2661  }
2662 
2674  public function getNextRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2675  return $this->getRelativeRevision( $rev, $flags, 'next' );
2676  }
2677 
2689  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2690  $this->checkDatabaseDomain( $db );
2691 
2692  if ( $rev->getPageId( $this->wikiId ) === null ) {
2693  return 0;
2694  }
2695  # Use page_latest if ID is not given
2696  if ( !$rev->getId( $this->wikiId ) ) {
2697  $prevId = $db->selectField(
2698  'page', 'page_latest',
2699  [ 'page_id' => $rev->getPageId( $this->wikiId ) ],
2700  __METHOD__
2701  );
2702  } else {
2703  $prevId = $db->selectField(
2704  'revision', 'rev_id',
2705  [ 'rev_page' => $rev->getPageId( $this->wikiId ), 'rev_id < ' . $rev->getId( $this->wikiId ) ],
2706  __METHOD__,
2707  [ 'ORDER BY' => 'rev_id DESC' ]
2708  );
2709  }
2710  return intval( $prevId );
2711  }
2712 
2725  public function getTimestampFromId( $id, $flags = 0 ) {
2726  if ( $id instanceof Title ) {
2727  // Old deprecated calling convention supported for backwards compatibility
2728  $id = $flags;
2729  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2730  }
2731 
2732  // T270149: Bail out if we know the query will definitely return false. Some callers are
2733  // passing RevisionRecord::getId() call directly as $id which can possibly return null.
2734  // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL'
2735  // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null.
2736  // @todo typehint $id and remove the null check
2737  if ( $id === null || $id <= 0 ) {
2738  return false;
2739  }
2740 
2741  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2742 
2743  $timestamp =
2744  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2745 
2746  return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2747  }
2748 
2758  public function countRevisionsByPageId( IDatabase $db, $id ) {
2759  $this->checkDatabaseDomain( $db );
2760 
2761  $row = $db->selectRow( 'revision',
2762  [ 'revCount' => 'COUNT(*)' ],
2763  [ 'rev_page' => $id ],
2764  __METHOD__
2765  );
2766  if ( $row ) {
2767  return intval( $row->revCount );
2768  }
2769  return 0;
2770  }
2771 
2781  public function countRevisionsByTitle( IDatabase $db, PageIdentity $page ) {
2782  $id = $this->getArticleId( $page );
2783  if ( $id ) {
2784  return $this->countRevisionsByPageId( $db, $id );
2785  }
2786  return 0;
2787  }
2788 
2807  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2808  $this->checkDatabaseDomain( $db );
2809 
2810  if ( !$userId ) {
2811  return false;
2812  }
2813 
2814  $revQuery = $this->getQueryInfo();
2815  $res = $db->select(
2816  $revQuery['tables'],
2817  [
2818  'rev_user' => $revQuery['fields']['rev_user'],
2819  ],
2820  [
2821  'rev_page' => $pageId,
2822  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2823  ],
2824  __METHOD__,
2825  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2826  $revQuery['joins']
2827  );
2828  foreach ( $res as $row ) {
2829  if ( $row->rev_user != $userId ) {
2830  return false;
2831  }
2832  }
2833  return true;
2834  }
2835 
2849  public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) {
2850  $db = $this->getDBConnectionRef( DB_REPLICA );
2851  $revIdPassed = $revId;
2852  $pageId = $this->getArticleId( $page );
2853  if ( !$pageId ) {
2854  return false;
2855  }
2856 
2857  if ( !$revId ) {
2858  if ( $page instanceof Title ) {
2859  $revId = $page->getLatestRevID();
2860  } else {
2861  $pageRecord = $this->pageStore->getPageByReference( $page );
2862  if ( $pageRecord ) {
2863  $revId = $pageRecord->getLatest( $this->getWikiId() );
2864  }
2865  }
2866  }
2867 
2868  if ( !$revId ) {
2869  $this->logger->warning(
2870  'No latest revision known for page {page} even though it exists with page ID {page_id}', [
2871  'page' => $page->__toString(),
2872  'page_id' => $pageId,
2873  'wiki_id' => $this->getWikiId() ?: 'local',
2874  ] );
2875  return false;
2876  }
2877 
2878  // Load the row from cache if possible. If not possible, populate the cache.
2879  // As a minor optimization, remember if this was a cache hit or miss.
2880  // We can sometimes avoid a database query later if this is a cache miss.
2881  $fromCache = true;
2882  $row = $this->cache->getWithSetCallback(
2883  // Page/rev IDs passed in from DB to reflect history merges
2884  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2885  WANObjectCache::TTL_WEEK,
2886  function ( $curValue, &$ttl, array &$setOpts ) use (
2887  $db, $revId, &$fromCache
2888  ) {
2889  $setOpts += Database::getCacheSetOptions( $db );
2890  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2891  if ( $row ) {
2892  $fromCache = false;
2893  }
2894  return $row; // don't cache negatives
2895  }
2896  );
2897 
2898  // Reflect revision deletion and user renames.
2899  if ( $row ) {
2900  $title = $this->ensureRevisionRowMatchesPage( $row, $page, [
2901  'from_cache_flag' => $fromCache,
2902  'page_id_initial' => $pageId,
2903  'rev_id_used' => $revId,
2904  'rev_id_requested' => $revIdPassed,
2905  ] );
2906 
2907  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2908  } else {
2909  return false;
2910  }
2911  }
2912 
2921  public function getFirstRevision(
2922  $page,
2923  int $flags = IDBAccessObject::READ_NORMAL
2924  ): ?RevisionRecord {
2925  if ( $page instanceof LinkTarget ) {
2926  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
2927  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
2928  }
2929  return $this->newRevisionFromConds(
2930  [
2931  'page_namespace' => $page->getNamespace(),
2932  'page_title' => $page->getDBkey()
2933  ],
2934  $flags,
2935  $page,
2936  [
2937  'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
2938  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
2939  ]
2940  );
2941  }
2942 
2954  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2955  return $this->cache->makeGlobalKey(
2956  self::ROW_CACHE_KEY,
2957  $db->getDomainID(),
2958  $pageId,
2959  $revId
2960  );
2961  }
2962 
2970  private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
2971  if ( $rev ) {
2972  if ( $rev->getId( $this->wikiId ) === null ) {
2973  throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
2974  }
2975  if ( $rev->getPageId( $this->wikiId ) !== $pageId ) {
2976  throw new InvalidArgumentException(
2977  "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
2978  );
2979  }
2980  }
2981  }
2982 
2997  private function getRevisionLimitConditions(
2998  IDatabase $dbr,
2999  RevisionRecord $old = null,
3000  RevisionRecord $new = null,
3001  $options = []
3002  ) {
3003  $options = (array)$options;
3004  $oldCmp = '>';
3005  $newCmp = '<';
3006  if ( in_array( self::INCLUDE_OLD, $options ) ) {
3007  $oldCmp = '>=';
3008  }
3009  if ( in_array( self::INCLUDE_NEW, $options ) ) {
3010  $newCmp = '<=';
3011  }
3012  if ( in_array( self::INCLUDE_BOTH, $options ) ) {
3013  $oldCmp = '>=';
3014  $newCmp = '<=';
3015  }
3016 
3017  $conds = [];
3018  if ( $old ) {
3019  $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3020  $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId( $this->wikiId )}) " .
3021  "OR rev_timestamp > {$oldTs}";
3022  }
3023  if ( $new ) {
3024  $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3025  $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId( $this->wikiId )}) " .
3026  "OR rev_timestamp < {$newTs}";
3027  }
3028  return $conds;
3029  }
3030 
3057  public function getRevisionIdsBetween(
3058  int $pageId,
3059  RevisionRecord $old = null,
3060  RevisionRecord $new = null,
3061  ?int $max = null,
3062  $options = [],
3063  ?string $order = null,
3064  int $flags = IDBAccessObject::READ_NORMAL
3065  ): array {
3066  $this->assertRevisionParameter( 'old', $pageId, $old );
3067  $this->assertRevisionParameter( 'new', $pageId, $new );
3068 
3069  $options = (array)$options;
3070  $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3071  in_array( self::INCLUDE_BOTH, $options );
3072  $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3073  in_array( self::INCLUDE_BOTH, $options );
3074 
3075  // No DB query needed if old and new are the same revision.
3076  // Can't check for consecutive revisions with 'getParentId' for a similar
3077  // optimization as edge cases exist when there are revisions between
3078  // a revision and it's parent. See T185167 for more details.
3079  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3080  return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3081  }
3082 
3083  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3084  $conds = array_merge(
3085  [
3086  'rev_page' => $pageId,
3087  $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3088  ],
3089  $this->getRevisionLimitConditions( $db, $old, $new, $options )
3090  );
3091 
3092  $queryOptions = [];
3093  if ( $order !== null ) {
3094  $queryOptions['ORDER BY'] = [ "rev_timestamp $order", "rev_id $order" ];
3095  }
3096  if ( $max !== null ) {
3097  $queryOptions['LIMIT'] = $max + 1; // extra to detect truncation
3098  }
3099 
3100  $values = $db->selectFieldValues(
3101  'revision',
3102  'rev_id',
3103  $conds,
3104  __METHOD__,
3105  $queryOptions
3106  );
3107  return array_map( 'intval', $values );
3108  }
3109 
3131  public function getAuthorsBetween(
3132  $pageId,
3133  RevisionRecord $old = null,
3134  RevisionRecord $new = null,
3135  Authority $performer = null,
3136  $max = null,
3137  $options = []
3138  ) {
3139  $this->assertRevisionParameter( 'old', $pageId, $old );
3140  $this->assertRevisionParameter( 'new', $pageId, $new );
3141  $options = (array)$options;
3142 
3143  // No DB query needed if old and new are the same revision.
3144  // Can't check for consecutive revisions with 'getParentId' for a similar
3145  // optimization as edge cases exist when there are revisions between
3146  //a revision and it's parent. See T185167 for more details.
3147  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3148  if ( empty( $options ) ) {
3149  return [];
3150  } elseif ( $performer ) {
3151  return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3152  } else {
3153  return [ $new->getUser() ];
3154  }
3155  }
3156 
3157  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3158  $conds = array_merge(
3159  [
3160  'rev_page' => $pageId,
3161  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3162  ],
3163  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3164  );
3165 
3166  $queryOpts = [ 'DISTINCT' ];
3167  if ( $max !== null ) {
3168  $queryOpts['LIMIT'] = $max + 1;
3169  }
3170 
3171  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3172  return array_map( function ( $row ) {
3173  return $this->actorStore->newActorFromRowFields(
3174  $row->rev_user,
3175  $row->rev_user_text,
3176  $row->rev_actor
3177  );
3178  }, iterator_to_array( $dbr->select(
3179  array_merge( [ 'revision' ], $actorQuery['tables'] ),
3180  $actorQuery['fields'],
3181  $conds, __METHOD__,
3182  $queryOpts,
3183  $actorQuery['joins']
3184  ) ) );
3185  }
3186 
3208  public function countAuthorsBetween(
3209  $pageId,
3210  RevisionRecord $old = null,
3211  RevisionRecord $new = null,
3212  Authority $performer = null,
3213  $max = null,
3214  $options = []
3215  ) {
3216  // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3217  // and creation of UserIdentity stuff.
3218  return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3219  }
3220 
3241  public function countRevisionsBetween(
3242  $pageId,
3243  RevisionRecord $old = null,
3244  RevisionRecord $new = null,
3245  $max = null,
3246  $options = []
3247  ) {
3248  $this->assertRevisionParameter( 'old', $pageId, $old );
3249  $this->assertRevisionParameter( 'new', $pageId, $new );
3250 
3251  // No DB query needed if old and new are the same revision.
3252  // Can't check for consecutive revisions with 'getParentId' for a similar
3253  // optimization as edge cases exist when there are revisions between
3254  //a revision and it's parent. See T185167 for more details.
3255  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3256  return 0;
3257  }
3258 
3259  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3260  $conds = array_merge(
3261  [
3262  'rev_page' => $pageId,
3263  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3264  ],
3265  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3266  );
3267  if ( $max !== null ) {
3268  return $dbr->selectRowCount( 'revision', '1',
3269  $conds,
3270  __METHOD__,
3271  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3272  );
3273  } else {
3274  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3275  }
3276  }
3277 
3278  // TODO: move relevant methods from Title here, e.g. isBigDeletion, etc.
3279 }
3280 
3285 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
MediaWiki\Revision\RevisionStore\newRevisionSlots
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, PageIdentity $page)
Factory method for RevisionSlots based on a revision ID.
Definition: RevisionStore.php:1487
MediaWiki\Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:64
MediaWiki\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:2689
MediaWiki\Storage\RevisionSlotsUpdate\getModifiedRoles
getModifiedRoles()
Returns a list of modified slot roles, that is, roles modified by calling modifySlot(),...
Definition: RevisionSlotsUpdate.php:138
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
MediaWiki\Revision\RevisionStore\INCLUDE_OLD
const INCLUDE_OLD
Definition: RevisionStore.php:97
MediaWiki\Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, PageIdentity $page, array $blobHints=[])
Definition: RevisionStore.php:905
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:38
MediaWiki\Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:104
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
MediaWiki\Storage\RevisionSlotsUpdate\getModifiedSlot
getModifiedSlot( $role)
Returns the SlotRecord associated with the given role, if the slot with that role was modified (and n...
Definition: RevisionSlotsUpdate.php:218
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:52
MediaWiki\Revision\RevisionRecord\isReadyForInsertion
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
Definition: RevisionRecord.php:568
MediaWiki\Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:34
MediaWiki\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:389
MediaWiki\Storage\BlobStore\DESIGNATION_HINT
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition: BlobStore.php:42
MediaWiki\Storage\BlobAccessException
Exception representing a failure to access a data blob.
Definition: BlobAccessException.php:33
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:43
MediaWiki\Revision\RevisionStore\$actorStore
ActorStore $actorStore
Definition: RevisionStore.php:132
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
MediaWiki\Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:32
MediaWiki\Revision\RevisionRecord\DELETED_USER
const DELETED_USER
Definition: RevisionRecord.php:55
MediaWiki\Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:463
MediaWiki\Revision\RevisionStore\newRevisionFromArchiveRow
newRevisionFromArchiveRow( $row, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Make a fake RevisionRecord object from an archive table row.
Definition: RevisionStore.php:1531
MediaWiki\Revision\RevisionStore\getFirstRevision
getFirstRevision( $page, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
Definition: RevisionStore.php:2921
MediaWiki\Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, PageIdentity $page=null, $fromCache=false)
Definition: RevisionStore.php:1552
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:226
MediaWiki\Revision\RevisionStore\$wikiId
bool string $wikiId
Definition: RevisionStore.php:109
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:85
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:87
MediaWiki\Revision\RevisionStore\getRelativeRevision
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
Definition: RevisionStore.php:2595
MediaWiki\Storage\SqlBlobStore
Service for storing and loading Content objects.
Definition: SqlBlobStore.php:52
MediaWiki\Revision\RevisionStore\getTitle
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
Definition: RevisionStore.php:287
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:80
MediaWiki\Revision\RevisionRecord\getPage
getPage()
Returns the page this revision belongs to.
Definition: RevisionRecord.php:370
MediaWiki\Revision\RevisionStore\getWikiId
getWikiId()
Get the ID of the wiki this revision belongs to.
Definition: RevisionStore.php:246
Page\PageReference\__toString
__toString()
Returns an informative human readable unique representation of the page identity, for use as a cache ...
MediaWiki\Revision\RevisionStoreCacheRecord
A cached RevisionStoreRecord.
Definition: RevisionStoreCacheRecord.php:37
MediaWiki\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:3057
MediaWiki\Revision\RevisionStore\wrapPage
wrapPage(PageIdentity $page)
Definition: RevisionStore.php:365
MediaWiki\Revision\RevisionRecord\getPageId
getPageId( $wikiId=self::LOCAL)
Get the page ID.
Definition: RevisionRecord.php:335
MediaWiki\Revision\RevisionStore\INCLUDE_NEW
const INCLUDE_NEW
Definition: RevisionStore.php:98
MediaWiki\Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:2725
MediaWiki\Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaced Revision::isMinor.
Definition: RevisionRecord.php:426
MediaWiki\Revision\RevisionStore\$cache
WANObjectCache $cache
Definition: RevisionStore.php:119
CommentStore
Handle database storage of comments such as edit summaries and log reasons.
Definition: CommentStore.php:42
Page\PageIdentity\getId
getId( $wikiId=self::LOCAL)
Returns the page ID.
MediaWiki\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:2849
MediaWiki\Revision\RevisionFactory
Service for constructing RevisionRecord objects.
Definition: RevisionFactory.php:37
MediaWiki\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:1294
MediaWiki\Revision\RevisionRecord\getSha1
getSha1()
Returns the base36 sha1 of this revision.
MediaWiki\Revision\RevisionStore\assertRevisionParameter
assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev=null)
Asserts that if revision is provided, it's saved and belongs to the page with provided pageId.
Definition: RevisionStore.php:2970
MediaWiki\User\UserIdentity\getId
getId( $wikiId=self::LOCAL)
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
MediaWiki\Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:555
MediaWiki\Revision\RevisionStore\insertSlotRowOn
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
Definition: RevisionStore.php:939
ActorMigration
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Definition: ActorMigration.php:15
Wikimedia\Rdbms\IDatabase\selectField
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
MediaWiki\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:2289
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
$revQuery
$revQuery
Definition: testCompression.php:56
MediaWiki\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:3131
MediaWiki\DAO\WikiAwareEntity
Marker interface for entities aware of the wiki they belong to.
Definition: WikiAwareEntity.php:34
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
MediaWiki\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:1247
MediaWiki\Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
MediaWiki\Revision\RevisionStore\$hookRunner
HookRunner $hookRunner
Definition: RevisionStore.php:156
MediaWiki\Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, PageIdentity $page)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2781
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
MediaWiki\Revision\RevisionStore\updateSlotsInternal
updateSlotsInternal(RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw)
Definition: RevisionStore.php:597
MediaWiki\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:2674
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\Revision
Definition: ContributionsLookup.php:3
MediaWiki\Page\LegacyArticleIdAccess
trait LegacyArticleIdAccess
Definition: LegacyArticleIdAccess.php:26
MediaWiki\Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:53
MediaWiki\Revision\RevisionStore\ROW_CACHE_KEY
const ROW_CACHE_KEY
Definition: RevisionStore.php:91
MediaWiki\Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
Wikimedia\Rdbms\IDatabase\timestamp
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1028
MWException
MediaWiki exception.
Definition: MWException.php:29
MediaWiki\Revision\RevisionStore\getContentBlobsForBatch
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
Definition: RevisionStore.php:2200
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:5143
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
MediaWiki\Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:255
MediaWiki\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:1023
MediaWiki\Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:129
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
MediaWiki\Revision\RevisionStore\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: RevisionStore.php:153
MediaWiki\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:2334
Page\PageReference\getNamespace
getNamespace()
Returns the page's namespace number.
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:612
$blob
$blob
Definition: testCompression.php:70
MediaWiki\Revision\RevisionStore\getSlotRowsForBatch
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
Definition: RevisionStore.php:2084
MediaWiki\Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2566
MediaWiki\Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:114
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
MediaWiki\Revision\RevisionStore\constructSlotRecords
constructSlotRecords( $revId, $slotRows, $queryFlags, PageIdentity $page, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
Definition: RevisionStore.php:1409
MediaWiki\Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:137
MediaWiki\Revision\RevisionStore\insertContentRowOn
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
Definition: RevisionStore.php:957
MediaWiki\User\UserIdentity\getName
getName()
$title
$title
Definition: testCompression.php:38
MediaWiki\Revision\RevisionStore\countRevisionsByPageId
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2758
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:680
Wikimedia\Rdbms\IDatabase\query
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
MediaWiki\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:733
MediaWiki\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:413
MediaWiki\Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2457
FallbackContent
Content object implementation representing unknown content.
Definition: FallbackContent.php:38
MediaWiki\Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:56
MediaWiki\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:539
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
MediaWiki\Revision\RevisionArchiveRecord
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Definition: RevisionArchiveRecord.php:41
MediaWiki\Revision\RevisionRecord\getParentId
getParentId( $wikiId=self::LOCAL)
Get parent revision ID (the original previous page revision).
Definition: RevisionRecord.php:297
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
MediaWiki\Revision\RevisionStore\$slotRoleStore
NameTableStore $slotRoleStore
Definition: RevisionStore.php:147
MediaWiki\Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:516
$content
$content
Definition: router.php:76
MediaWiki\Revision\RevisionStore\getRevisionLimitConditions
getRevisionLimitConditions(IDatabase $dbr, RevisionRecord $old=null, RevisionRecord $new=null, $options=[])
Converts revision limits to query conditions.
Definition: RevisionStore.php:2997
MediaWiki\Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:452
DBAccessObjectUtils\hasFlags
static hasFlags( $bitfield, $flags)
Definition: DBAccessObjectUtils.php:35
Page\PageReference\getDBkey
getDBkey()
Get the page title in DB key form.
MediaWiki\Revision\RevisionStore\__construct
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorMigration $actorMigration, ActorStore $actorStore, IContentHandlerFactory $contentHandlerFactory, PageStore $pageStore, TitleFactory $titleFactory, HookContainer $hookContainer, $wikiId=WikiAwareEntity::LOCAL)
Definition: RevisionStore.php:188
MediaWiki\Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:230
MediaWiki\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:1148
MediaWiki\Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:422
MediaWiki\Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, PageIdentity $page)
Definition: RevisionStore.php:1361
MediaWiki\Revision\RevisionStore\getQueryInfo
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
Definition: RevisionStore.php:2377
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
Wikimedia\Rdbms\IDatabase\unlock
unlock( $lockName, $method)
Release a lock.
MediaWiki\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
Wikimedia\Rdbms\IDatabase\selectRow
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
Wikimedia\Rdbms\IDatabase\getDomainID
getDomainID()
Return the currently selected domain ID.
MediaWiki\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:2807
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:128
MediaWiki\Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:124
Wikimedia\Rdbms\IDatabase\lock
lock( $lockName, $method, $timeout=5, $flags=0)
Acquire a named lock.
MediaWiki\Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:486
MediaWiki\Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:142
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:36
MediaWiki\Revision\RevisionStore\getRcIdIfUnpatrolled
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaced Revision::isUnpatrolled.
Definition: RevisionStore.php:1094
Title\newFromIDs
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition: Title.php:586
MediaWiki\Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:40
MediaWiki\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:2659
MediaWiki\Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:530
MediaWiki\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:2249
MediaWiki\Revision\RevisionStore\getRevisionRowCacheKey
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) Caching rows w...
Definition: RevisionStore.php:2954
Wikimedia\Rdbms\IDatabase\insert
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert the given row(s) into a table.
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
MediaWiki\Revision\RevisionStore\getBaseRevisionRow
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, $parentId)
Definition: RevisionStore.php:873
MediaWiki\Revision\RevisionRecord\getSlotRoles
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
Definition: RevisionRecord.php:207
Title
Represents a title within MediaWiki.
Definition: Title.php:49
MediaWiki\Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:539
Wikimedia\Rdbms\IDatabase\doAtomicSection
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
Wikimedia\Rdbms\IDatabase\selectSQLText
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.
MediaWiki\Revision\RevisionStore\setLogger
setLogger(LoggerInterface $logger)
Definition: RevisionStore.php:223
MediaWiki\Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, $parentId)
Definition: RevisionStore.php:759
MediaWiki\Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:279
MediaWiki\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:3208
MediaWiki\Revision\RevisionStore\ORDER_OLDEST_TO_NEWEST
const ORDER_OLDEST_TO_NEWEST
Definition: RevisionStore.php:93
Wikimedia\Rdbms\IDatabase\addQuotes
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
MediaWiki\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:3241
MediaWiki\Revision\RevisionStore\INCLUDE_BOTH
const INCLUDE_BOTH
Definition: RevisionStore.php:99
MediaWiki\Storage\RevisionSlotsUpdate\getRemovedRoles
getRemovedRoles()
Returns a list of removed slot roles, that is, roles removed by calling removeSlot(),...
Definition: RevisionSlotsUpdate.php:148
MediaWiki\Revision\RevisionStore\ORDER_NEWEST_TO_OLDEST
const ORDER_NEWEST_TO_OLDEST
Definition: RevisionStore.php:94
MediaWiki\Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2526
MediaWiki\Revision\RevisionStore\$pageStore
PageStore $pageStore
Definition: RevisionStore.php:159
MediaWiki\Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, UserIdentity $user, CommentStoreComment $comment, PageIdentity $page, $pageId, $parentId)
Definition: RevisionStore.php:619
MediaWiki\Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:403
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:91
TitleFactory
Creates Title objects.
Definition: TitleFactory.php:35
MediaWiki\Revision\RevisionStore\$titleFactory
TitleFactory $titleFactory
Definition: RevisionStore.php:162
MediaWiki\Revision\RevisionSlots
Value object representing the set of slots belonging to a revision.
Definition: RevisionSlots.php:41
MediaWiki\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:1333
Page\PageIdentityValue
Immutable value object representing a page identity.
Definition: PageIdentityValue.php:41
MediaWiki\Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
Definition: RevisionRecord.php:459
Wikimedia\Rdbms\IDatabase\getType
getType()
Get the RDBMS type of the server (e.g.
MediaWiki\Revision\RevisionStore\getDBLoadBalancer
getDBLoadBalancer()
Definition: RevisionStore.php:237
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
Wikimedia\Rdbms\IDatabase\select
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
MediaWiki\Revision\RevisionStore\getRecentChange
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
Definition: RevisionStore.php:1116
wfBacktrace
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
Definition: GlobalFunctions.php:1322
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
MediaWiki\Revision\RevisionRecord\getSize
getSize()
Returns the nominal size of this revision, in bogo-bytes.
MediaWiki\Revision\RevisionStore\newRevisionFromRowAndSlots
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, PageIdentity $page=null, $fromCache=false)
Definition: RevisionStore.php:1660
MediaWiki\Revision\RevisionRecord\FOR_THIS_USER
const FOR_THIS_USER
Definition: RevisionRecord.php:63
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
MediaWiki\Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:506
MediaWiki\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
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:556
$t
$t
Definition: testCompression.php:74
Title\castFromLinkTarget
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:351
MediaWiki\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:424
Wikimedia\Rdbms\IDatabase\insertId
insertId()
Get the inserted value of an auto-increment row.
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Wikimedia\Rdbms\IDatabase\onTransactionResolution
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback when the current transaction commits or rolls back.
MediaWiki\$context
IContextSource $context
Definition: MediaWiki.php:40
MediaWiki\Revision\RevisionStore\insertSlotOn
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, PageIdentity $page, array $blobHints=[])
Definition: RevisionStore.php:695
Page\PageStore
Definition: PageStore.php:26
MediaWiki\Revision\RevisionStore\checkContent
checkContent(Content $content, PageIdentity $page, string $role)
MCR migration note: this corresponded to Revision::checkContentModel.
Definition: RevisionStore.php:978
MediaWiki\Revision\RevisionStore\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: RevisionStore.php:150
MediaWiki\Revision\RevisionStore\getDBConnectionRef
getDBConnectionRef( $mode, $groups=[])
Definition: RevisionStore.php:266
MediaWiki\Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:386
MediaWiki\Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:319
MediaWiki\Revision\RevisionStore\newRevisionFromArchiveRowAndSlots
newRevisionFromArchiveRowAndSlots( $row, $slots, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Definition: RevisionStore.php:1580
MediaWiki\Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:583
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
MediaWiki\Revision\RevisionStore\ensureRevisionRowMatchesPage
ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context=[])
Check that the given row matches the given Title object.
Definition: RevisionStore.php:1781
MediaWiki\Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:448
Wikimedia\Rdbms\IDatabase\delete
delete( $table, $conds, $fname=__METHOD__)
Delete all rows in a table that match a condition.
MediaWiki\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:2311
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0, PageIdentity $page=null)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1227
MediaWiki\Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:180
MediaWiki\Revision\RevisionStore\getPage
getPage(?int $pageId, ?int $revId, int $queryFlags=self::READ_NORMAL)
Determines the page based on the available information.
Definition: RevisionStore.php:307
MediaWiki\User\ActorStore
Definition: ActorStore.php:43
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
MediaWiki\Revision\SlotRecord\getRevision
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:413
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
MediaWiki\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:1873