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 stdClass;
66 use Title;
67 use TitleFactory;
68 use Traversable;
69 use WANObjectCache;
70 use Wikimedia\Assert\Assert;
71 use Wikimedia\IPUtils;
77 
88  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
89 
91 
92  public const ROW_CACHE_KEY = 'revision-row-1.29';
93 
94  public const ORDER_OLDEST_TO_NEWEST = 'ASC';
95  public const ORDER_NEWEST_TO_OLDEST = 'DESC';
96 
97  // Constants for get(...)Between methods
98  public const INCLUDE_OLD = 'include_old';
99  public const INCLUDE_NEW = 'include_new';
100  public const INCLUDE_BOTH = 'include_both';
101 
105  private $blobStore;
106 
110  private $wikiId;
111 
115  private $loadBalancer;
116 
120  private $cache;
121 
125  private $commentStore;
126 
131 
133  private $actorStore;
134 
138  private $logger;
139 
144 
148  private $slotRoleStore;
149 
152 
155 
157  private $hookRunner;
158 
160  private $pageStore;
161 
163  private $titleFactory;
164 
189  public function __construct(
202  HookContainer $hookContainer,
203  $wikiId = WikiAwareEntity::LOCAL
204  ) {
205  Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
206 
207  $this->loadBalancer = $loadBalancer;
208  $this->blobStore = $blobStore;
209  $this->cache = $cache;
210  $this->commentStore = $commentStore;
211  $this->contentModelStore = $contentModelStore;
212  $this->slotRoleStore = $slotRoleStore;
213  $this->slotRoleRegistry = $slotRoleRegistry;
214  $this->actorMigration = $actorMigration;
215  $this->actorStore = $actorStore;
216  $this->wikiId = $wikiId;
217  $this->logger = new NullLogger();
218  $this->contentHandlerFactory = $contentHandlerFactory;
219  $this->pageStore = $pageStore;
220  $this->titleFactory = $titleFactory;
221  $this->hookRunner = new HookRunner( $hookContainer );
222  }
223 
224  public function setLogger( LoggerInterface $logger ) {
225  $this->logger = $logger;
226  }
227 
231  public function isReadOnly() {
232  return $this->blobStore->isReadOnly();
233  }
234 
238  private function getDBLoadBalancer() {
239  return $this->loadBalancer;
240  }
241 
247  public function getWikiId() {
248  return $this->wikiId;
249  }
250 
256  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
257  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
258  return $this->getDBConnectionRef( $mode );
259  }
260 
267  private function getDBConnectionRef( $mode, $groups = [] ) {
268  $lb = $this->getDBLoadBalancer();
269  return $lb->getConnectionRef( $mode, $groups, $this->wikiId );
270  }
271 
288  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
289  // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069
290  if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
291  wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' );
292  }
293 
294  $page = $this->getPage( $pageId, $revId, $queryFlags );
295  return $this->titleFactory->castFromPageIdentity( $page );
296  }
297 
308  private function getPage( ?int $pageId, ?int $revId, int $queryFlags = self::READ_NORMAL ) {
309  if ( !$pageId && !$revId ) {
310  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
311  }
312 
313  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
314  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
315  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
316  $queryFlags = self::READ_NORMAL;
317  }
318 
319  $canUsePageId = ( $pageId !== null && $pageId > 0 );
320 
321  // Loading by ID is best
322  if ( $canUsePageId ) {
323  $page = $this->pageStore->getPageById( $pageId, $queryFlags );
324  if ( $page ) {
325  return $this->wrapPage( $page );
326  }
327  }
328 
329  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
330  $canUseRevId = ( $revId !== null && $revId > 0 );
331 
332  if ( $canUseRevId ) {
333  $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags )
334  ->join( 'revision', null, 'page_id=rev_page' )
335  ->conds( [ 'rev_id' => $revId ] )
336  ->caller( __METHOD__ );
337 
338  $page = $pageQuery->fetchPageRecord();
339  if ( $page ) {
340  return $this->wrapPage( $page );
341  }
342  }
343 
344  // If we still don't have a title, fallback to primary DB if that wasn't already happening.
345  if ( $queryFlags === self::READ_NORMAL ) {
346  $title = $this->getPage( $pageId, $revId, self::READ_LATEST );
347  if ( $title ) {
348  $this->logger->info(
349  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
350  [ 'trace' => wfBacktrace() ]
351  );
352  return $title;
353  }
354  }
355 
356  throw new RevisionAccessException(
357  'Could not determine title for page ID {page_id} and revision ID {rev_id}',
358  [
359  'page_id' => $pageId,
360  'rev_id' => $revId,
361  ]
362  );
363  }
364 
370  private function wrapPage( PageIdentity $page ): PageIdentity {
371  if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
372  // NOTE: since there is still a lot of code that needs a full Title,
373  // and uses Title::castFromPageIdentity() to get one, it's beneficial
374  // to create a Title right away if we can, so we don't have to convert
375  // over and over later on.
376  // When there is less need to convert to Title, this special case can
377  // be removed.
378  return $this->titleFactory->castFromPageIdentity( $page );
379  } else {
380  return $page;
381  }
382  }
383 
391  private function failOnNull( $value, $name ) {
392  if ( $value === null ) {
393  throw new IncompleteRevisionException(
394  "$name must not be " . var_export( $value, true ) . "!"
395  );
396  }
397 
398  return $value;
399  }
400 
408  private function failOnEmpty( $value, $name ) {
409  if ( $value === null || $value === 0 || $value === '' ) {
410  throw new IncompleteRevisionException(
411  "$name must not be " . var_export( $value, true ) . "!"
412  );
413  }
414 
415  return $value;
416  }
417 
429  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
430  // TODO: pass in a DBTransactionContext instead of a database connection.
431  $this->checkDatabaseDomain( $dbw );
432 
433  $slotRoles = $rev->getSlotRoles();
434 
435  // Make sure the main slot is always provided throughout migration
436  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
437  throw new IncompleteRevisionException(
438  'main slot must be provided'
439  );
440  }
441 
442  // Checks
443  $this->failOnNull( $rev->getSize(), 'size field' );
444  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
445  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
446  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
447  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
448  $this->failOnNull( $user->getId(), 'user field' );
449  $this->failOnEmpty( $user->getName(), 'user_text field' );
450 
451  if ( !$rev->isReadyForInsertion() ) {
452  // This is here for future-proofing. At the time this check being added, it
453  // was redundant to the individual checks above.
454  throw new IncompleteRevisionException( 'Revision is incomplete' );
455  }
456 
457  if ( $slotRoles == [ SlotRecord::MAIN ] ) {
458  // T239717: If the main slot is the only slot, make sure the revision's nominal size
459  // and hash match the main slot's nominal size and hash.
460  $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
461  Assert::precondition(
462  $mainSlot->getSize() === $rev->getSize(),
463  'The revisions\'s size must match the main slot\'s size (see T239717)'
464  );
465  Assert::precondition(
466  $mainSlot->getSha1() === $rev->getSha1(),
467  'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
468  );
469  }
470 
471  $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early
472 
473  $parentId = $rev->getParentId() ?? $this->getPreviousRevisionId( $dbw, $rev );
474 
476  $rev = $dbw->doAtomicSection(
477  __METHOD__,
478  function ( IDatabase $dbw, $fname ) use (
479  $rev,
480  $user,
481  $comment,
482  $pageId,
483  $parentId
484  ) {
485  return $this->insertRevisionInternal(
486  $rev,
487  $dbw,
488  $user,
489  $comment,
490  $rev->getPage(),
491  $pageId,
492  $parentId
493  );
494  }
495  );
496 
497  // sanity checks
498  Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' );
499  Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' );
500  Assert::postcondition(
501  $rev->getComment( RevisionRecord::RAW ) !== null,
502  'revision must have a comment'
503  );
504  Assert::postcondition(
505  $rev->getUser( RevisionRecord::RAW ) !== null,
506  'revision must have a user'
507  );
508 
509  // Trigger exception if the main slot is missing.
510  // Technically, this could go away after MCR migration: while
511  // calling code may require a main slot to exist, RevisionStore
512  // really should not know or care about that requirement.
514 
515  foreach ( $slotRoles as $role ) {
516  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
517  Assert::postcondition(
518  $slot->getContent() !== null,
519  $role . ' slot must have content'
520  );
521  Assert::postcondition(
522  $slot->hasRevision(),
523  $role . ' slot must have a revision associated'
524  );
525  }
526 
527  $this->hookRunner->onRevisionRecordInserted( $rev );
528 
529  return $rev;
530  }
531 
544  public function updateSlotsOn(
545  RevisionRecord $revision,
546  RevisionSlotsUpdate $revisionSlotsUpdate,
547  IDatabase $dbw
548  ): array {
549  $this->checkDatabaseDomain( $dbw );
550 
551  // Make sure all modified and removed slots are derived slots
552  foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
553  Assert::precondition(
554  $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
555  'Trying to modify a slot that is not derived'
556  );
557  }
558  foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) {
559  $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
560  Assert::precondition(
561  $isDerived,
562  'Trying to remove a slot that is not derived'
563  );
564  throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' );
565  }
566 
568  $slotRecords = $dbw->doAtomicSection(
569  __METHOD__,
570  function ( IDatabase $dbw, $fname ) use (
571  $revision,
572  $revisionSlotsUpdate
573  ) {
574  return $this->updateSlotsInternal(
575  $revision,
576  $revisionSlotsUpdate,
577  $dbw
578  );
579  }
580  );
581 
582  foreach ( $slotRecords as $role => $slot ) {
583  Assert::postcondition(
584  $slot->getContent() !== null,
585  $role . ' slot must have content'
586  );
587  Assert::postcondition(
588  $slot->hasRevision(),
589  $role . ' slot must have a revision associated'
590  );
591  }
592 
593  return $slotRecords;
594  }
595 
602  private function updateSlotsInternal(
603  RevisionRecord $revision,
604  RevisionSlotsUpdate $revisionSlotsUpdate,
605  IDatabase $dbw
606  ): array {
607  $page = $revision->getPage();
608  $revId = $revision->getId( $this->wikiId );
609  $blobHints = [
610  BlobStore::PAGE_HINT => $page->getId( $this->wikiId ),
611  BlobStore::REVISION_HINT => $revId,
612  BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
613  ];
614 
615  $newSlots = [];
616  foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
617  $slot = $revisionSlotsUpdate->getModifiedSlot( $role );
618  $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
619  }
620 
621  return $newSlots;
622  }
623 
624  private function insertRevisionInternal(
625  RevisionRecord $rev,
626  IDatabase $dbw,
627  UserIdentity $user,
628  CommentStoreComment $comment,
629  PageIdentity $page,
630  $pageId,
631  $parentId
632  ) {
633  $slotRoles = $rev->getSlotRoles();
634 
635  $revisionRow = $this->insertRevisionRowOn(
636  $dbw,
637  $rev,
638  $parentId
639  );
640 
641  $revisionId = $revisionRow['rev_id'];
642 
643  $blobHints = [
644  BlobStore::PAGE_HINT => $pageId,
645  BlobStore::REVISION_HINT => $revisionId,
646  BlobStore::PARENT_HINT => $parentId,
647  ];
648 
649  $newSlots = [];
650  foreach ( $slotRoles as $role ) {
651  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
652 
653  // If the SlotRecord already has a revision ID set, this means it already exists
654  // in the database, and should already belong to the current revision.
655  // However, a slot may already have a revision, but no content ID, if the slot
656  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
657  // mode, and the respective archive row was not yet migrated to the new schema.
658  // In that case, a new slot row (and content row) must be inserted even during
659  // undeletion.
660  if ( $slot->hasRevision() && $slot->hasContentId() ) {
661  // TODO: properly abort transaction if the assertion fails!
662  Assert::parameter(
663  $slot->getRevision() === $revisionId,
664  'slot role ' . $slot->getRole(),
665  'Existing slot should belong to revision '
666  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
667  );
668 
669  // Slot exists, nothing to do, move along.
670  // This happens when restoring archived revisions.
671 
672  $newSlots[$role] = $slot;
673  } else {
674  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
675  }
676  }
677 
678  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
679 
680  $rev = new RevisionStoreRecord(
681  $page,
682  $user,
683  $comment,
684  (object)$revisionRow,
685  new RevisionSlots( $newSlots ),
686  $this->wikiId
687  );
688 
689  return $rev;
690  }
691 
700  private function insertSlotOn(
701  IDatabase $dbw,
702  $revisionId,
703  SlotRecord $protoSlot,
704  PageIdentity $page,
705  array $blobHints = []
706  ) {
707  if ( $protoSlot->hasAddress() ) {
708  $blobAddress = $protoSlot->getAddress();
709  } else {
710  $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
711  }
712 
713  $contentId = null;
714 
715  if ( $protoSlot->hasContentId() ) {
716  $contentId = $protoSlot->getContentId();
717  } else {
718  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
719  }
720 
721  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
722 
723  return SlotRecord::newSaved(
724  $revisionId,
725  $contentId,
726  $blobAddress,
727  $protoSlot
728  );
729  }
730 
738  private function insertIpChangesRow(
739  IDatabase $dbw,
740  UserIdentity $user,
741  RevisionRecord $rev,
742  $revisionId
743  ) {
744  if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
745  $ipcRow = [
746  'ipc_rev_id' => $revisionId,
747  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
748  'ipc_hex' => IPUtils::toHex( $user->getName() ),
749  ];
750  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
751  }
752  }
753 
764  private function insertRevisionRowOn(
765  IDatabase $dbw,
766  RevisionRecord $rev,
767  $parentId
768  ) {
769  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
770 
771  list( $commentFields, $commentCallback ) =
772  $this->commentStore->insertWithTempTable(
773  $dbw,
774  'rev_comment',
776  );
777  $revisionRow += $commentFields;
778 
779  list( $actorFields, $actorCallback ) =
780  $this->actorMigration->getInsertValuesWithTempTable(
781  $dbw,
782  'rev_user',
784  );
785  $revisionRow += $actorFields;
786 
787  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
788 
789  if ( !isset( $revisionRow['rev_id'] ) ) {
790  // only if auto-increment was used
791  $revisionRow['rev_id'] = intval( $dbw->insertId() );
792 
793  if ( $dbw->getType() === 'mysql' ) {
794  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
795  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
796  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
797 
798  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
799  $table = 'archive';
800  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
801  if ( $maxRevId2 >= $maxRevId ) {
802  $maxRevId = $maxRevId2;
803  $table = 'slots';
804  }
805 
806  if ( $maxRevId >= $revisionRow['rev_id'] ) {
807  $this->logger->debug(
808  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
809  . ' Trying to fix it.',
810  [
811  'revid' => $revisionRow['rev_id'],
812  'table' => $table,
813  'maxrevid' => $maxRevId,
814  ]
815  );
816 
817  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
818  throw new MWException( 'Failed to get database lock for T202032' );
819  }
820  $fname = __METHOD__;
822  static function ( $trigger, IDatabase $dbw ) use ( $fname ) {
823  $dbw->unlock( 'fix-for-T202032', $fname );
824  },
825  __METHOD__
826  );
827 
828  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
829 
830  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
831  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
832  // inserts too, though, at least on MariaDB 10.1.29.
833  //
834  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
835  // transactions in this code path thanks to the row lock from the original ->insert() above.
836  //
837  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
838  // that's for non-MySQL DBs.
839  $row1 = $dbw->query(
840  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
841  __METHOD__
842  )->fetchObject();
843 
844  $row2 = $dbw->query(
845  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
846  . ' FOR UPDATE',
847  __METHOD__
848  )->fetchObject();
849 
850  $maxRevId = max(
851  $maxRevId,
852  $row1 ? intval( $row1->v ) : 0,
853  $row2 ? intval( $row2->v ) : 0
854  );
855 
856  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
857  // transactions will throw a duplicate key error here. It doesn't seem worth trying
858  // to avoid that.
859  $revisionRow['rev_id'] = $maxRevId + 1;
860  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
861  }
862  }
863  }
864 
865  $commentCallback( $revisionRow['rev_id'] );
866  $actorCallback( $revisionRow['rev_id'], $revisionRow );
867 
868  return $revisionRow;
869  }
870 
878  private function getBaseRevisionRow(
879  IDatabase $dbw,
880  RevisionRecord $rev,
881  $parentId
882  ) {
883  // Record the edit in revisions
884  $revisionRow = [
885  'rev_page' => $rev->getPageId( $this->wikiId ),
886  'rev_parent_id' => $parentId,
887  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
888  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
889  'rev_deleted' => $rev->getVisibility(),
890  'rev_len' => $rev->getSize(),
891  'rev_sha1' => $rev->getSha1(),
892  ];
893 
894  if ( $rev->getId( $this->wikiId ) !== null ) {
895  // Needed to restore revisions with their original ID
896  $revisionRow['rev_id'] = $rev->getId( $this->wikiId );
897  }
898 
899  return $revisionRow;
900  }
901 
910  private function storeContentBlob(
911  SlotRecord $slot,
912  PageIdentity $page,
913  array $blobHints = []
914  ) {
915  $content = $slot->getContent();
916  $format = $content->getDefaultFormat();
917  $model = $content->getModel();
918 
919  $this->checkContent( $content, $page, $slot->getRole() );
920 
921  return $this->blobStore->storeBlob(
922  $content->serialize( $format ),
923  // These hints "leak" some information from the higher abstraction layer to
924  // low level storage to allow for optimization.
925  array_merge(
926  $blobHints,
927  [
928  BlobStore::DESIGNATION_HINT => 'page-content',
929  BlobStore::ROLE_HINT => $slot->getRole(),
930  BlobStore::SHA1_HINT => $slot->getSha1(),
931  BlobStore::MODEL_HINT => $model,
932  BlobStore::FORMAT_HINT => $format,
933  ]
934  )
935  );
936  }
937 
944  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
945  $slotRow = [
946  'slot_revision_id' => $revisionId,
947  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
948  'slot_content_id' => $contentId,
949  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
950  // that we just inserted.
951  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
952  ];
953  $dbw->insert( 'slots', $slotRow, __METHOD__ );
954  }
955 
962  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
963  $contentRow = [
964  'content_size' => $slot->getSize(),
965  'content_sha1' => $slot->getSha1(),
966  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
967  'content_address' => $blobAddress,
968  ];
969  $dbw->insert( 'content', $contentRow, __METHOD__ );
970  return intval( $dbw->insertId() );
971  }
972 
983  private function checkContent( Content $content, PageIdentity $page, string $role ) {
984  // Note: may return null for revisions that have not yet been inserted
985 
986  $model = $content->getModel();
987  $format = $content->getDefaultFormat();
988  $handler = $content->getContentHandler();
989 
990  if ( !$handler->isSupportedFormat( $format ) ) {
991  throw new MWException(
992  "Can't use format $format with content model $model on $page role $role"
993  );
994  }
995 
996  if ( !$content->isValid() ) {
997  throw new MWException(
998  "New content for $page role $role is not valid! Content model is $model"
999  );
1000  }
1001  }
1002 
1028  public function newNullRevision(
1029  IDatabase $dbw,
1030  PageIdentity $page,
1031  CommentStoreComment $comment,
1032  $minor,
1033  UserIdentity $user
1034  ) {
1035  $this->checkDatabaseDomain( $dbw );
1036 
1037  $pageId = $this->getArticleId( $page );
1038 
1039  // T51581: Lock the page table row to ensure no other process
1040  // is adding a revision to the page at the same time.
1041  // Avoid locking extra tables, compare T191892.
1042  $pageLatest = $dbw->selectField(
1043  'page',
1044  'page_latest',
1045  [ 'page_id' => $pageId ],
1046  __METHOD__,
1047  [ 'FOR UPDATE' ]
1048  );
1049 
1050  if ( !$pageLatest ) {
1051  $msg = 'T235589: Failed to select table row during null revision creation' .
1052  " Page id '$pageId' does not exist.";
1053  $this->logger->error(
1054  $msg,
1055  [ 'exception' => new RuntimeException( $msg ) ]
1056  );
1057 
1058  return null;
1059  }
1060 
1061  // Fetch the actual revision row from primary DB, without locking all extra tables.
1062  $oldRevision = $this->loadRevisionFromConds(
1063  $dbw,
1064  [ 'rev_id' => intval( $pageLatest ) ],
1065  self::READ_LATEST,
1066  $page
1067  );
1068 
1069  if ( !$oldRevision ) {
1070  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1071  $this->logger->error(
1072  $msg,
1073  [ 'exception' => new RuntimeException( $msg ) ]
1074  );
1075  return null;
1076  }
1077 
1078  // Construct the new revision
1079  $timestamp = MWTimestamp::now( TS_MW );
1080  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1081 
1082  $newRevision->setComment( $comment );
1083  $newRevision->setUser( $user );
1084  $newRevision->setTimestamp( $timestamp );
1085  $newRevision->setMinorEdit( $minor );
1086 
1087  return $newRevision;
1088  }
1089 
1099  public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1100  $rc = $this->getRecentChange( $rev );
1101  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1102  return $rc->getAttribute( 'rc_id' );
1103  } else {
1104  return 0;
1105  }
1106  }
1107 
1121  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1122  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1123 
1125  [ 'rc_this_oldid' => $rev->getId( $this->wikiId ) ],
1126  __METHOD__,
1127  $dbType
1128  );
1129 
1130  // XXX: cache this locally? Glue it to the RevisionRecord?
1131  return $rc;
1132  }
1133 
1153  private function loadSlotContent(
1154  SlotRecord $slot,
1155  ?string $blobData = null,
1156  ?string $blobFlags = null,
1157  ?string $blobFormat = null,
1158  int $queryFlags = 0
1159  ) {
1160  if ( $blobData !== null ) {
1161  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1162 
1163  if ( $blobFlags === null ) {
1164  // No blob flags, so use the blob verbatim.
1165  $data = $blobData;
1166  } else {
1167  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1168  if ( $data === false ) {
1169  throw new RevisionAccessException(
1170  'Failed to expand blob data using flags {flags} (key: {cache_key})',
1171  [
1172  'flags' => $blobFlags,
1173  'cache_key' => $cacheKey,
1174  ]
1175  );
1176  }
1177  }
1178 
1179  } else {
1180  $address = $slot->getAddress();
1181  try {
1182  $data = $this->blobStore->getBlob( $address, $queryFlags );
1183  } catch ( BlobAccessException $e ) {
1184  throw new RevisionAccessException(
1185  'Failed to load data blob from {address}'
1186  . 'If this problem persist, use the findBadBlobs maintenance script '
1187  . 'to investigate the issue and mark bad blobs.',
1188  [ 'address' => $e->getMessage() ],
1189  0,
1190  $e
1191  );
1192  }
1193  }
1194 
1195  $model = $slot->getModel();
1196 
1197  // If the content model is not known, don't fail here (T220594, T220793, T228921)
1198  if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1199  $this->logger->warning(
1200  "Undefined content model '$model', falling back to UnknownContent",
1201  [
1202  'content_address' => $slot->getAddress(),
1203  'rev_id' => $slot->getRevision(),
1204  'role_name' => $slot->getRole(),
1205  'model_name' => $model,
1206  'trace' => wfBacktrace()
1207  ]
1208  );
1209 
1210  return new FallbackContent( $data, $model );
1211  }
1212 
1213  return $this->contentHandlerFactory
1214  ->getContentHandler( $model )
1215  ->unserializeContent( $data, $blobFormat );
1216  }
1217 
1235  public function getRevisionById( $id, $flags = 0, PageIdentity $page = null ) {
1236  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags, $page );
1237  }
1238 
1255  public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) {
1256  $conds = [
1257  'page_namespace' => $page->getNamespace(),
1258  'page_title' => $page->getDBkey()
1259  ];
1260 
1261  if ( $page instanceof LinkTarget ) {
1262  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1263  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1264  }
1265 
1266  if ( $revId ) {
1267  // Use the specified revision ID.
1268  // Note that we use newRevisionFromConds here because we want to retry
1269  // and fall back to primary DB if the page is not found on a replica.
1270  // Since the caller supplied a revision ID, we are pretty sure the revision is
1271  // supposed to exist, so we should try hard to find it.
1272  $conds['rev_id'] = $revId;
1273  return $this->newRevisionFromConds( $conds, $flags, $page );
1274  } else {
1275  // Use a join to get the latest revision.
1276  // Note that we don't use newRevisionFromConds here because we don't want to retry
1277  // and fall back to primary DB. The assumption is that we only want to force the fallback
1278  // if we are quite sure the revision exists because the caller supplied a revision ID.
1279  // If the page isn't found at all on a replica, it probably simply does not exist.
1280  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1281  $conds[] = 'rev_id=page_latest';
1282  return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1283  }
1284  }
1285 
1302  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1303  $conds = [ 'page_id' => $pageId ];
1304  if ( $revId ) {
1305  // Use the specified revision ID.
1306  // Note that we use newRevisionFromConds here because we want to retry
1307  // and fall back to primary DB if the page is not found on a replica.
1308  // Since the caller supplied a revision ID, we are pretty sure the revision is
1309  // supposed to exist, so we should try hard to find it.
1310  $conds['rev_id'] = $revId;
1311  return $this->newRevisionFromConds( $conds, $flags );
1312  } else {
1313  // Use a join to get the latest revision.
1314  // Note that we don't use newRevisionFromConds here because we don't want to retry
1315  // and fall back to primary DB. The assumption is that we only want to force the fallback
1316  // if we are quite sure the revision exists because the caller supplied a revision ID.
1317  // If the page isn't found at all on a replica, it probably simply does not exist.
1318  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1319 
1320  $conds[] = 'rev_id=page_latest';
1321 
1322  return $this->loadRevisionFromConds( $db, $conds, $flags );
1323  }
1324  }
1325 
1341  public function getRevisionByTimestamp(
1342  $page,
1343  string $timestamp,
1344  int $flags = IDBAccessObject::READ_NORMAL
1345  ): ?RevisionRecord {
1346  if ( $page instanceof LinkTarget ) {
1347  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1348  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1349  }
1350  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1351  return $this->newRevisionFromConds(
1352  [
1353  'rev_timestamp' => $db->timestamp( $timestamp ),
1354  'page_namespace' => $page->getNamespace(),
1355  'page_title' => $page->getDBkey()
1356  ],
1357  $flags,
1358  $page
1359  );
1360  }
1361 
1369  private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) {
1370  $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1371 
1372  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1373  $db = $this->getDBConnectionRef( $dbMode );
1374 
1375  $res = $db->select(
1376  $revQuery['tables'],
1377  $revQuery['fields'],
1378  [
1379  'slot_revision_id' => $revId,
1380  ],
1381  __METHOD__,
1382  $dbOptions,
1383  $revQuery['joins']
1384  );
1385 
1386  if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1387  // If we found no slots, try looking on the primary database (T212428, T252156)
1388  $this->logger->info(
1389  __METHOD__ . ' falling back to READ_LATEST.',
1390  [
1391  'revid' => $revId,
1392  'trace' => wfBacktrace( true )
1393  ]
1394  );
1395  return $this->loadSlotRecords(
1396  $revId,
1397  $queryFlags | self::READ_LATEST,
1398  $page
1399  );
1400  }
1401 
1402  return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1403  }
1404 
1417  private function constructSlotRecords(
1418  $revId,
1419  $slotRows,
1420  $queryFlags,
1421  PageIdentity $page,
1422  $slotContents = null
1423  ) {
1424  $slots = [];
1425 
1426  foreach ( $slotRows as $row ) {
1427  // Resolve role names and model names from in-memory cache, if they were not joined in.
1428  if ( !isset( $row->role_name ) ) {
1429  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1430  }
1431 
1432  if ( !isset( $row->model_name ) ) {
1433  if ( isset( $row->content_model ) ) {
1434  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1435  } else {
1436  // We may get here if $row->model_name is set but null, perhaps because it
1437  // came from rev_content_model, which is NULL for the default model.
1438  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1439  $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1440  }
1441  }
1442 
1443  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1444  if ( isset( $row->blob_data ) ) {
1445  $slotContents[$row->content_address] = $row->blob_data;
1446  }
1447 
1448  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1449  $blob = null;
1450  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1451  $blob = $slotContents[$slot->getAddress()];
1452  if ( $blob instanceof Content ) {
1453  return $blob;
1454  }
1455  }
1456  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1457  };
1458 
1459  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1460  }
1461 
1462  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1463  $this->logger->error(
1464  __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1465  [
1466  'revid' => $revId,
1467  'queryFlags' => $queryFlags,
1468  'trace' => wfBacktrace( true )
1469  ]
1470  );
1471 
1472  throw new RevisionAccessException(
1473  'Main slot of revision not found in database. See T212428.'
1474  );
1475  }
1476 
1477  return $slots;
1478  }
1479 
1495  private function newRevisionSlots(
1496  $revId,
1497  $revisionRow,
1498  $slotRows,
1499  $queryFlags,
1500  PageIdentity $page
1501  ) {
1502  if ( $slotRows ) {
1503  $slots = new RevisionSlots(
1504  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1505  );
1506  } else {
1507  // XXX: do we need the same kind of caching here
1508  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1509 
1510  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) {
1511  return $this->loadSlotRecords( $revId, $queryFlags, $page );
1512  } );
1513  }
1514 
1515  return $slots;
1516  }
1517 
1539  public function newRevisionFromArchiveRow(
1540  $row,
1541  $queryFlags = 0,
1542  PageIdentity $page = null,
1543  array $overrides = []
1544  ) {
1545  return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides );
1546  }
1547 
1560  public function newRevisionFromRow(
1561  $row,
1562  $queryFlags = 0,
1563  PageIdentity $page = null,
1564  $fromCache = false
1565  ) {
1566  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache );
1567  }
1568 
1589  stdClass $row,
1590  $slots,
1591  int $queryFlags = 0,
1592  ?PageIdentity $page = null,
1593  array $overrides = []
1594  ) {
1595  if ( !$page && isset( $overrides['title'] ) ) {
1596  if ( !( $overrides['title'] instanceof PageIdentity ) ) {
1597  throw new MWException( 'title field override must contain a PageIdentity object.' );
1598  }
1599 
1600  $page = $overrides['title'];
1601  }
1602 
1603  if ( !isset( $page ) ) {
1604  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1605  $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1606  } else {
1607  throw new InvalidArgumentException(
1608  'A Title or ar_namespace and ar_title must be given'
1609  );
1610  }
1611  }
1612 
1613  foreach ( $overrides as $key => $value ) {
1614  $field = "ar_$key";
1615  $row->$field = $value;
1616  }
1617 
1618  try {
1619  $user = $this->actorStore->newActorFromRowFields(
1620  $row->ar_user ?? null,
1621  $row->ar_user_text ?? null,
1622  $row->ar_actor ?? null
1623  );
1624  } catch ( InvalidArgumentException $ex ) {
1625  $this->logger->warning( 'Could not load user for archive revision {rev_id}', [
1626  'ar_rev_id' => $row->ar_rev_id,
1627  'ar_actor' => $row->ar_actor ?? 'null',
1628  'ar_user_text' => $row->ar_user_text ?? 'null',
1629  'ar_user' => $row->ar_user ?? 'null',
1630  'exception' => $ex
1631  ] );
1632  $user = $this->actorStore->getUnknownActor();
1633  }
1634 
1635  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1636  // Legacy because $row may have come from self::selectFields()
1637  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1638 
1639  if ( !( $slots instanceof RevisionSlots ) ) {
1640  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $page );
1641  }
1642  return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId );
1643  }
1644 
1664  stdClass $row,
1665  $slots,
1666  int $queryFlags = 0,
1667  ?PageIdentity $page = null,
1668  bool $fromCache = false
1669  ) {
1670  if ( !$page ) {
1671  if ( isset( $row->page_id )
1672  && isset( $row->page_namespace )
1673  && isset( $row->page_title )
1674  ) {
1675  $page = new PageIdentityValue(
1676  (int)$row->page_id,
1677  (int)$row->page_namespace,
1678  $row->page_title,
1679  $this->wikiId
1680  );
1681 
1682  $page = $this->wrapPage( $page );
1683  } else {
1684  $pageId = (int)( $row->rev_page ?? 0 );
1685  $revId = (int)( $row->rev_id ?? 0 );
1686 
1687  $page = $this->getPage( $pageId, $revId, $queryFlags );
1688  }
1689  } else {
1690  $page = $this->ensureRevisionRowMatchesPage( $row, $page );
1691  }
1692 
1693  if ( !$page ) {
1694  // This should already have been caught about, but apparently
1695  // it not always is, see T286877.
1696  throw new RevisionAccessException(
1697  "Failed to determine page associated with revision {$row->rev_id}"
1698  );
1699  }
1700 
1701  try {
1702  $user = $this->actorStore->newActorFromRowFields(
1703  $row->rev_user ?? null,
1704  $row->rev_user_text ?? null,
1705  $row->rev_actor ?? null
1706  );
1707  } catch ( InvalidArgumentException $ex ) {
1708  $this->logger->warning( 'Could not load user for revision {rev_id}', [
1709  'rev_id' => $row->rev_id,
1710  'rev_actor' => $row->rev_actor ?? 'null',
1711  'rev_user_text' => $row->rev_user_text ?? 'null',
1712  'rev_user' => $row->rev_user ?? 'null',
1713  'exception' => $ex
1714  ] );
1715  $user = $this->actorStore->getUnknownActor();
1716  }
1717 
1718  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1719  // Legacy because $row may have come from self::selectFields()
1720  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1721 
1722  if ( !( $slots instanceof RevisionSlots ) ) {
1723  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $page );
1724  }
1725 
1726  // If this is a cached row, instantiate a cache-aware RevisionRecord to avoid stale data.
1727  if ( $fromCache ) {
1728  $rev = new RevisionStoreCacheRecord(
1729  function ( $revId ) use ( $queryFlags ) {
1730  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1731  $row = $this->fetchRevisionRowFromConds(
1732  $db,
1733  [ 'rev_id' => intval( $revId ) ]
1734  );
1735  if ( !$row && !( $queryFlags & self::READ_LATEST ) ) {
1736  // If we found no slots, try looking on the primary database (T259738)
1737  $this->logger->info(
1738  'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1739  [
1740  'revid' => $revId,
1741  'trace' => wfBacktrace( true )
1742  ]
1743  );
1744  $dbw = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
1745  $row = $this->fetchRevisionRowFromConds(
1746  $dbw,
1747  [ 'rev_id' => intval( $revId ) ]
1748  );
1749  }
1750  if ( !$row ) {
1751  return [ null, null ];
1752  }
1753  return [
1754  $row->rev_deleted,
1755  $this->actorStore->newActorFromRowFields(
1756  $row->rev_user ?? null,
1757  $row->rev_user_text ?? null,
1758  $row->rev_actor ?? null
1759  )
1760  ];
1761  },
1762  $page, $user, $comment, $row, $slots, $this->wikiId
1763  );
1764  } else {
1765  $rev = new RevisionStoreRecord(
1766  $page, $user, $comment, $row, $slots, $this->wikiId );
1767  }
1768  return $rev;
1769  }
1770 
1782  private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) {
1783  $revId = (int)( $row->rev_id ?? 0 );
1784  $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1785  $expectedPageId = $page->getId( $this->wikiId );
1786  // Avoid fatal error when the Title's ID changed, T246720
1787  if ( $revPageId && $expectedPageId && $revPageId !== $expectedPageId ) {
1788  // NOTE: PageStore::getPageByReference may use the page ID, which we don't want here.
1789  $pageRec = $this->pageStore->getPageByName(
1790  $page->getNamespace(),
1791  $page->getDBkey(),
1792  PageStore::READ_LATEST
1793  );
1794  $masterPageId = $pageRec->getId( $this->wikiId );
1795  $masterLatest = $pageRec->getLatest( $this->wikiId );
1796  if ( $revPageId === $masterPageId ) {
1797  if ( $page instanceof Title ) {
1798  // If we were using a Title object, keep using it, but update the page ID.
1799  // This way, we don't unexpectedly mix Titles with immutable value objects.
1800  $page->resetArticleID( $masterPageId );
1801 
1802  } else {
1803  $page = $pageRec;
1804  }
1805 
1806  $this->logger->info(
1807  "Encountered stale Title object",
1808  [
1809  'page_id_stale' => $expectedPageId,
1810  'page_id_reloaded' => $masterPageId,
1811  'page_latest' => $masterLatest,
1812  'rev_id' => $revId,
1813  'trace' => wfBacktrace()
1814  ] + $context
1815  );
1816  } else {
1817  $expectedTitle = (string)$page;
1818  if ( $page instanceof Title ) {
1819  // If we started with a Title, keep using a Title.
1820  $page = $this->titleFactory->newFromID( $revPageId );
1821  } else {
1822  $page = $pageRec;
1823  }
1824 
1825  // This could happen if a caller to e.g. getRevisionById supplied a Title that is
1826  // plain wrong. In this case, we should ideally throw an IllegalArgumentException.
1827  // However, it is more likely that we encountered a race condition during a page
1828  // move (T268910, T279832) or database corruption (T263340). That situation
1829  // should not be ignored, but we can allow the request to continue in a reasonable
1830  // manner without breaking things for the user.
1831  $this->logger->error(
1832  "Encountered mismatching Title object (see T259022, T268910, T279832, T263340)",
1833  [
1834  'expected_page_id' => $masterPageId,
1835  'expected_page_title' => $expectedTitle,
1836  'rev_page' => $revPageId,
1837  'rev_page_title' => (string)$page,
1838  'page_latest' => $masterLatest,
1839  'rev_id' => $revId,
1840  'trace' => wfBacktrace()
1841  ] + $context
1842  );
1843  }
1844  }
1845 
1846  return $page;
1847  }
1848 
1874  public function newRevisionsFromBatch(
1875  $rows,
1876  array $options = [],
1877  $queryFlags = 0,
1878  PageIdentity $page = null
1879  ) {
1880  $result = new StatusValue();
1881  $archiveMode = $options['archive'] ?? false;
1882 
1883  if ( $archiveMode ) {
1884  $revIdField = 'ar_rev_id';
1885  } else {
1886  $revIdField = 'rev_id';
1887  }
1888 
1889  $rowsByRevId = [];
1890  $pageIdsToFetchTitles = [];
1891  $titlesByPageKey = [];
1892  foreach ( $rows as $row ) {
1893  if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1894  $result->warning(
1895  'internalerror_info',
1896  "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1897  );
1898  }
1899 
1900  // Attach a page key to the row, so we can find and reuse Title objects easily.
1901  $row->_page_key =
1902  $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1903 
1904  if ( $page ) {
1905  if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1906  throw new InvalidArgumentException(
1907  "Revision {$row->$revIdField} doesn't belong to page "
1908  . $this->getArticleId( $page )
1909  );
1910  }
1911 
1912  if ( $archiveMode
1913  && ( $row->ar_namespace != $page->getNamespace()
1914  || $row->ar_title !== $page->getDBkey() )
1915  ) {
1916  throw new InvalidArgumentException(
1917  "Revision {$row->$revIdField} doesn't belong to page "
1918  . $page
1919  );
1920  }
1921  } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1922  if ( isset( $row->page_namespace ) && isset( $row->page_title )
1923  // This should always be true, but just in case we don't have a page_id
1924  // set or it doesn't match rev_page, let's fetch the title again.
1925  && isset( $row->page_id ) && isset( $row->rev_page )
1926  && $row->rev_page === $row->page_id
1927  ) {
1928  $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1929  } elseif ( $archiveMode ) {
1930  // Can't look up deleted pages by ID, but we have namespace and title
1931  $titlesByPageKey[ $row->_page_key ] =
1932  Title::makeTitle( $row->ar_namespace, $row->ar_title );
1933  } else {
1934  $pageIdsToFetchTitles[] = $row->rev_page;
1935  }
1936  }
1937  $rowsByRevId[$row->$revIdField] = $row;
1938  }
1939 
1940  if ( empty( $rowsByRevId ) ) {
1941  $result->setResult( true, [] );
1942  return $result;
1943  }
1944 
1945  // If the page is not supplied, batch-fetch Title objects.
1946  if ( $page ) {
1947  // same logic as for $row->_page_key above
1948  $pageKey = $archiveMode
1949  ? $page->getNamespace() . ':' . $page->getDBkey()
1950  : $this->getArticleId( $page );
1951 
1952  $titlesByPageKey[$pageKey] = $page;
1953  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1954  // Note: when we fetch titles by ID, the page key is also the ID.
1955  // We should never get here if $archiveMode is true.
1956  Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1957 
1958  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1959  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1960  $titlesByPageKey[$t->getArticleID()] = $t;
1961  }
1962  }
1963 
1964  // which method to use for creating RevisionRecords
1965  $newRevisionRecord = [
1966  $this,
1967  $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1968  ];
1969 
1970  if ( !isset( $options['slots'] ) ) {
1971  $result->setResult(
1972  true,
1973  array_map(
1974  static function ( $row )
1975  use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1976  try {
1977  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1978  $result->warning(
1979  'internalerror_info',
1980  "Couldn't find title for rev {$row->$revIdField} "
1981  . "(page key {$row->_page_key})"
1982  );
1983  return null;
1984  }
1985  return $newRevisionRecord( $row, null, $queryFlags,
1986  $titlesByPageKey[ $row->_page_key ] );
1987  } catch ( MWException $e ) {
1988  $result->warning( 'internalerror_info', $e->getMessage() );
1989  return null;
1990  }
1991  },
1992  $rowsByRevId
1993  )
1994  );
1995  return $result;
1996  }
1997 
1998  $slotRowOptions = [
1999  'slots' => $options['slots'] ?? true,
2000  'blobs' => $options['content'] ?? false,
2001  ];
2002 
2003  if ( is_array( $slotRowOptions['slots'] )
2004  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
2005  ) {
2006  // Make sure the main slot is always loaded, RevisionRecord requires this.
2007  $slotRowOptions['slots'][] = SlotRecord::MAIN;
2008  }
2009 
2010  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2011 
2012  $result->merge( $slotRowsStatus );
2013  $slotRowsByRevId = $slotRowsStatus->getValue();
2014 
2015  $result->setResult(
2016  true,
2017  array_map(
2018  function ( $row )
2019  use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
2020  $revIdField, $newRevisionRecord
2021  ) {
2022  if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
2023  $result->warning(
2024  'internalerror_info',
2025  "Couldn't find slots for rev {$row->$revIdField}"
2026  );
2027  return null;
2028  }
2029  if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2030  $result->warning(
2031  'internalerror_info',
2032  "Couldn't find title for rev {$row->$revIdField} "
2033  . "(page key {$row->_page_key})"
2034  );
2035  return null;
2036  }
2037  try {
2038  return $newRevisionRecord(
2039  $row,
2040  new RevisionSlots(
2041  $this->constructSlotRecords(
2042  $row->$revIdField,
2043  $slotRowsByRevId[$row->$revIdField],
2044  $queryFlags,
2045  $titlesByPageKey[$row->_page_key]
2046  )
2047  ),
2048  $queryFlags,
2049  $titlesByPageKey[$row->_page_key]
2050  );
2051  } catch ( MWException $e ) {
2052  $result->warning( 'internalerror_info', $e->getMessage() );
2053  return null;
2054  }
2055  },
2056  $rowsByRevId
2057  )
2058  );
2059  return $result;
2060  }
2061 
2085  private function getSlotRowsForBatch(
2086  $rowsOrIds,
2087  array $options = [],
2088  $queryFlags = 0
2089  ) {
2090  $result = new StatusValue();
2091 
2092  $revIds = [];
2093  foreach ( $rowsOrIds as $row ) {
2094  if ( is_object( $row ) ) {
2095  $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
2096  } else {
2097  $revIds[] = (int)$row;
2098  }
2099  }
2100 
2101  // Nothing to do.
2102  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2103  if ( empty( $revIds ) ) {
2104  $result->setResult( true, [] );
2105  return $result;
2106  }
2107 
2108  // We need to set the `content` flag to join in content meta-data
2109  $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
2110  $revIdField = $slotQueryInfo['keys']['rev_id'];
2111  $slotQueryConds = [ $revIdField => $revIds ];
2112 
2113  if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2114  if ( empty( $options['slots'] ) ) {
2115  // Degenerate case: return no slots for each revision.
2116  $result->setResult( true, array_fill_keys( $revIds, [] ) );
2117  return $result;
2118  }
2119 
2120  $roleIdField = $slotQueryInfo['keys']['role_id'];
2121  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
2122  return $this->slotRoleStore->getId( $slot_name );
2123  }, $options['slots'] );
2124  }
2125 
2126  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2127  $slotRows = $db->select(
2128  $slotQueryInfo['tables'],
2129  $slotQueryInfo['fields'],
2130  $slotQueryConds,
2131  __METHOD__,
2132  [],
2133  $slotQueryInfo['joins']
2134  );
2135 
2136  $slotContents = null;
2137  if ( $options['blobs'] ?? false ) {
2138  $blobAddresses = [];
2139  foreach ( $slotRows as $slotRow ) {
2140  $blobAddresses[] = $slotRow->content_address;
2141  }
2142  $slotContentFetchStatus = $this->blobStore
2143  ->getBlobBatch( $blobAddresses, $queryFlags );
2144  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2145  $result->warning( $error['message'], ...$error['params'] );
2146  }
2147  $slotContents = $slotContentFetchStatus->getValue();
2148  }
2149 
2150  $slotRowsByRevId = [];
2151  foreach ( $slotRows as $slotRow ) {
2152  if ( $slotContents === null ) {
2153  // nothing to do
2154  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2155  $slotRow->blob_data = $slotContents[$slotRow->content_address];
2156  } else {
2157  $result->warning(
2158  'internalerror_info',
2159  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2160  );
2161  $slotRow->blob_data = null;
2162  }
2163 
2164  // conditional needed for SCHEMA_COMPAT_READ_OLD
2165  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2166  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2167  }
2168 
2169  // conditional needed for SCHEMA_COMPAT_READ_OLD
2170  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2171  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2172  }
2173 
2174  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2175  }
2176 
2177  $result->setResult( true, $slotRowsByRevId );
2178  return $result;
2179  }
2180 
2201  public function getContentBlobsForBatch(
2202  $rowsOrIds,
2203  $slots = null,
2204  $queryFlags = 0
2205  ) {
2206  $result = $this->getSlotRowsForBatch(
2207  $rowsOrIds,
2208  [ 'slots' => $slots, 'blobs' => true ],
2209  $queryFlags
2210  );
2211 
2212  if ( $result->isOK() ) {
2213  // strip out all internal meta data that we don't want to expose
2214  foreach ( $result->value as $revId => $rowsByRole ) {
2215  foreach ( $rowsByRole as $role => $slotRow ) {
2216  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2217  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2218  // if we didn't ask for it.
2219  unset( $result->value[$revId][$role] );
2220  continue;
2221  }
2222 
2223  $result->value[$revId][$role] = (object)[
2224  'blob_data' => $slotRow->blob_data,
2225  'model_name' => $slotRow->model_name,
2226  ];
2227  }
2228  }
2229  }
2230 
2231  return $result;
2232  }
2233 
2250  private function newRevisionFromConds(
2251  array $conditions,
2252  int $flags = IDBAccessObject::READ_NORMAL,
2253  PageIdentity $page = null,
2254  array $options = []
2255  ) {
2256  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2257  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2258 
2259  $lb = $this->getDBLoadBalancer();
2260 
2261  // Make sure new pending/committed revision are visibile later on
2262  // within web requests to certain avoid bugs like T93866 and T94407.
2263  if ( !$rev
2264  && !( $flags & self::READ_LATEST )
2265  && $lb->hasStreamingReplicaServers()
2266  && $lb->hasOrMadeRecentPrimaryChanges()
2267  ) {
2268  $flags = self::READ_LATEST;
2269  $dbw = $this->getDBConnectionRef( DB_PRIMARY );
2270  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2271  }
2272 
2273  return $rev;
2274  }
2275 
2290  private function loadRevisionFromConds(
2291  IDatabase $db,
2292  array $conditions,
2293  int $flags = IDBAccessObject::READ_NORMAL,
2294  PageIdentity $page = null,
2295  array $options = []
2296  ) {
2297  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2298  if ( $row ) {
2299  return $this->newRevisionFromRow( $row, $flags, $page );
2300  }
2301 
2302  return null;
2303  }
2304 
2312  private function checkDatabaseDomain( IDatabase $db ) {
2313  $dbDomain = $db->getDomainID();
2314  $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2315  if ( $dbDomain === $storeDomain ) {
2316  return;
2317  }
2318 
2319  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2320  }
2321 
2335  private function fetchRevisionRowFromConds(
2336  IDatabase $db,
2337  array $conditions,
2338  int $flags = IDBAccessObject::READ_NORMAL,
2339  array $options = []
2340  ) {
2341  $this->checkDatabaseDomain( $db );
2342 
2343  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2344  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2345  $options[] = 'FOR UPDATE';
2346  }
2347  return $db->selectRow(
2348  $revQuery['tables'],
2349  $revQuery['fields'],
2350  $conditions,
2351  __METHOD__,
2352  $options,
2353  $revQuery['joins']
2354  );
2355  }
2356 
2378  public function getQueryInfo( $options = [] ) {
2379  $ret = [
2380  'tables' => [],
2381  'fields' => [],
2382  'joins' => [],
2383  ];
2384 
2385  $ret['tables'][] = 'revision';
2386  $ret['fields'] = array_merge( $ret['fields'], [
2387  'rev_id',
2388  'rev_page',
2389  'rev_timestamp',
2390  'rev_minor_edit',
2391  'rev_deleted',
2392  'rev_len',
2393  'rev_parent_id',
2394  'rev_sha1',
2395  ] );
2396 
2397  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2398  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2399  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2400  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2401 
2402  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2403  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2404  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2405  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2406 
2407  if ( in_array( 'page', $options, true ) ) {
2408  $ret['tables'][] = 'page';
2409  $ret['fields'] = array_merge( $ret['fields'], [
2410  'page_namespace',
2411  'page_title',
2412  'page_id',
2413  'page_latest',
2414  'page_is_redirect',
2415  'page_len',
2416  ] );
2417  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2418  }
2419 
2420  if ( in_array( 'user', $options, true ) ) {
2421  $ret['tables'][] = 'user';
2422  $ret['fields'] = array_merge( $ret['fields'], [
2423  'user_name',
2424  ] );
2425  $u = $actorQuery['fields']['rev_user'];
2426  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2427  }
2428 
2429  if ( in_array( 'text', $options, true ) ) {
2430  throw new InvalidArgumentException(
2431  'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2432  );
2433  }
2434 
2435  return $ret;
2436  }
2437 
2458  public function getSlotsQueryInfo( $options = [] ) {
2459  $ret = [
2460  'tables' => [],
2461  'fields' => [],
2462  'joins' => [],
2463  'keys' => [],
2464  ];
2465 
2466  $ret['keys']['rev_id'] = 'slot_revision_id';
2467  $ret['keys']['role_id'] = 'slot_role_id';
2468 
2469  $ret['tables'][] = 'slots';
2470  $ret['fields'] = array_merge( $ret['fields'], [
2471  'slot_revision_id',
2472  'slot_content_id',
2473  'slot_origin',
2474  'slot_role_id',
2475  ] );
2476 
2477  if ( in_array( 'role', $options, true ) ) {
2478  // Use left join to attach role name, so we still find the revision row even
2479  // if the role name is missing. This triggers a more obvious failure mode.
2480  $ret['tables'][] = 'slot_roles';
2481  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2482  $ret['fields'][] = 'role_name';
2483  }
2484 
2485  if ( in_array( 'content', $options, true ) ) {
2486  $ret['keys']['model_id'] = 'content_model';
2487 
2488  $ret['tables'][] = 'content';
2489  $ret['fields'] = array_merge( $ret['fields'], [
2490  'content_size',
2491  'content_sha1',
2492  'content_address',
2493  'content_model',
2494  ] );
2495  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2496 
2497  if ( in_array( 'model', $options, true ) ) {
2498  // Use left join to attach model name, so we still find the revision row even
2499  // if the model name is missing. This triggers a more obvious failure mode.
2500  $ret['tables'][] = 'content_models';
2501  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2502  $ret['fields'][] = 'model_name';
2503  }
2504 
2505  }
2506 
2507  return $ret;
2508  }
2509 
2518  public function isRevisionRow( $row, string $table = '' ) {
2519  if ( !( $row instanceof stdClass ) ) {
2520  return false;
2521  }
2522  $queryInfo = $table === 'archive' ? $this->getArchiveQueryInfo() : $this->getQueryInfo();
2523  foreach ( $queryInfo['fields'] as $alias => $field ) {
2524  $name = is_numeric( $alias ) ? $field : $alias;
2525  if ( !property_exists( $row, $name ) ) {
2526  return false;
2527  }
2528  }
2529  return true;
2530  }
2531 
2549  public function getArchiveQueryInfo() {
2550  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2551  $ret = [
2552  'tables' => [
2553  'archive',
2554  'archive_actor' => 'actor'
2555  ] + $commentQuery['tables'],
2556  'fields' => [
2557  'ar_id',
2558  'ar_page_id',
2559  'ar_namespace',
2560  'ar_title',
2561  'ar_rev_id',
2562  'ar_timestamp',
2563  'ar_minor_edit',
2564  'ar_deleted',
2565  'ar_len',
2566  'ar_parent_id',
2567  'ar_sha1',
2568  'ar_actor',
2569  'ar_user' => 'archive_actor.actor_user',
2570  'ar_user_text' => 'archive_actor.actor_name',
2571  ] + $commentQuery['fields'],
2572  'joins' => [
2573  'archive_actor' => [ 'JOIN', 'actor_id=ar_actor' ]
2574  ] + $commentQuery['joins'],
2575  ];
2576 
2577  return $ret;
2578  }
2579 
2589  public function getRevisionSizes( array $revIds ) {
2590  $dbr = $this->getDBConnectionRef( DB_REPLICA );
2591  $revLens = [];
2592  if ( !$revIds ) {
2593  return $revLens; // empty
2594  }
2595 
2596  $res = $dbr->select(
2597  'revision',
2598  [ 'rev_id', 'rev_len' ],
2599  [ 'rev_id' => $revIds ],
2600  __METHOD__
2601  );
2602 
2603  foreach ( $res as $row ) {
2604  $revLens[$row->rev_id] = intval( $row->rev_len );
2605  }
2606 
2607  return $revLens;
2608  }
2609 
2618  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2619  $op = $dir === 'next' ? '>' : '<';
2620  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2621 
2622  $revisionIdValue = $rev->getId( $this->wikiId );
2623 
2624  if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) {
2625  // revision is unsaved or otherwise incomplete
2626  return null;
2627  }
2628 
2629  if ( $rev instanceof RevisionArchiveRecord ) {
2630  // revision is deleted, so it's not part of the page history
2631  return null;
2632  }
2633 
2634  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2635  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2636 
2637  $ts = $this->getTimestampFromId( $revisionIdValue, $flags );
2638  if ( $ts === false ) {
2639  // XXX Should this be moved into getTimestampFromId?
2640  $ts = $db->selectField( 'archive', 'ar_timestamp',
2641  [ 'ar_rev_id' => $revisionIdValue ], __METHOD__ );
2642  if ( $ts === false ) {
2643  // XXX Is this reachable? How can we have a page id but no timestamp?
2644  return null;
2645  }
2646  }
2647  $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2648 
2649  $revId = $db->selectField( 'revision', 'rev_id',
2650  [
2651  'rev_page' => $rev->getPageId( $this->wikiId ),
2652  "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op $revisionIdValue )"
2653  ],
2654  __METHOD__,
2655  [
2656  'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2657  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2658  ]
2659  );
2660 
2661  if ( $revId === false ) {
2662  return null;
2663  }
2664 
2665  return $this->getRevisionById( intval( $revId ) );
2666  }
2667 
2682  public function getPreviousRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2683  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2684  }
2685 
2697  public function getNextRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2698  return $this->getRelativeRevision( $rev, $flags, 'next' );
2699  }
2700 
2712  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2713  $this->checkDatabaseDomain( $db );
2714 
2715  if ( $rev->getPageId( $this->wikiId ) === null ) {
2716  return 0;
2717  }
2718  # Use page_latest if ID is not given
2719  if ( !$rev->getId( $this->wikiId ) ) {
2720  $prevId = $db->selectField(
2721  'page', 'page_latest',
2722  [ 'page_id' => $rev->getPageId( $this->wikiId ) ],
2723  __METHOD__
2724  );
2725  } else {
2726  $prevId = $db->selectField(
2727  'revision', 'rev_id',
2728  [ 'rev_page' => $rev->getPageId( $this->wikiId ), 'rev_id < ' . $rev->getId( $this->wikiId ) ],
2729  __METHOD__,
2730  [ 'ORDER BY' => 'rev_id DESC' ]
2731  );
2732  }
2733  return intval( $prevId );
2734  }
2735 
2748  public function getTimestampFromId( $id, $flags = 0 ) {
2749  if ( $id instanceof Title ) {
2750  // Old deprecated calling convention supported for backwards compatibility
2751  $id = $flags;
2752  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2753  }
2754 
2755  // T270149: Bail out if we know the query will definitely return false. Some callers are
2756  // passing RevisionRecord::getId() call directly as $id which can possibly return null.
2757  // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL'
2758  // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null.
2759  // @todo typehint $id and remove the null check
2760  if ( $id === null || $id <= 0 ) {
2761  return false;
2762  }
2763 
2764  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2765 
2766  $timestamp =
2767  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2768 
2769  return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2770  }
2771 
2781  public function countRevisionsByPageId( IDatabase $db, $id ) {
2782  $this->checkDatabaseDomain( $db );
2783 
2784  $row = $db->selectRow( 'revision',
2785  [ 'revCount' => 'COUNT(*)' ],
2786  [ 'rev_page' => $id ],
2787  __METHOD__
2788  );
2789  if ( $row ) {
2790  return intval( $row->revCount );
2791  }
2792  return 0;
2793  }
2794 
2804  public function countRevisionsByTitle( IDatabase $db, PageIdentity $page ) {
2805  $id = $this->getArticleId( $page );
2806  if ( $id ) {
2807  return $this->countRevisionsByPageId( $db, $id );
2808  }
2809  return 0;
2810  }
2811 
2830  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2831  $this->checkDatabaseDomain( $db );
2832 
2833  if ( !$userId ) {
2834  return false;
2835  }
2836 
2837  $revQuery = $this->getQueryInfo();
2838  $res = $db->select(
2839  $revQuery['tables'],
2840  [
2841  'rev_user' => $revQuery['fields']['rev_user'],
2842  ],
2843  [
2844  'rev_page' => $pageId,
2845  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2846  ],
2847  __METHOD__,
2848  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2849  $revQuery['joins']
2850  );
2851  foreach ( $res as $row ) {
2852  if ( $row->rev_user != $userId ) {
2853  return false;
2854  }
2855  }
2856  return true;
2857  }
2858 
2872  public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) {
2873  $db = $this->getDBConnectionRef( DB_REPLICA );
2874  $revIdPassed = $revId;
2875  $pageId = $this->getArticleId( $page );
2876  if ( !$pageId ) {
2877  return false;
2878  }
2879 
2880  if ( !$revId ) {
2881  if ( $page instanceof Title ) {
2882  $revId = $page->getLatestRevID();
2883  } else {
2884  $pageRecord = $this->pageStore->getPageByReference( $page );
2885  if ( $pageRecord ) {
2886  $revId = $pageRecord->getLatest( $this->getWikiId() );
2887  }
2888  }
2889  }
2890 
2891  if ( !$revId ) {
2892  $this->logger->warning(
2893  'No latest revision known for page {page} even though it exists with page ID {page_id}', [
2894  'page' => $page->__toString(),
2895  'page_id' => $pageId,
2896  'wiki_id' => $this->getWikiId() ?: 'local',
2897  ] );
2898  return false;
2899  }
2900 
2901  // Load the row from cache if possible. If not possible, populate the cache.
2902  // As a minor optimization, remember if this was a cache hit or miss.
2903  // We can sometimes avoid a database query later if this is a cache miss.
2904  $fromCache = true;
2905  $row = $this->cache->getWithSetCallback(
2906  // Page/rev IDs passed in from DB to reflect history merges
2907  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2908  WANObjectCache::TTL_WEEK,
2909  function ( $curValue, &$ttl, array &$setOpts ) use (
2910  $db, $revId, &$fromCache
2911  ) {
2912  $setOpts += Database::getCacheSetOptions( $db );
2913  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2914  if ( $row ) {
2915  $fromCache = false;
2916  }
2917  return $row; // don't cache negatives
2918  }
2919  );
2920 
2921  // Reflect revision deletion and user renames.
2922  if ( $row ) {
2923  $title = $this->ensureRevisionRowMatchesPage( $row, $page, [
2924  'from_cache_flag' => $fromCache,
2925  'page_id_initial' => $pageId,
2926  'rev_id_used' => $revId,
2927  'rev_id_requested' => $revIdPassed,
2928  ] );
2929 
2930  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2931  } else {
2932  return false;
2933  }
2934  }
2935 
2944  public function getFirstRevision(
2945  $page,
2946  int $flags = IDBAccessObject::READ_NORMAL
2947  ): ?RevisionRecord {
2948  if ( $page instanceof LinkTarget ) {
2949  // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
2950  $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
2951  }
2952  return $this->newRevisionFromConds(
2953  [
2954  'page_namespace' => $page->getNamespace(),
2955  'page_title' => $page->getDBkey()
2956  ],
2957  $flags,
2958  $page,
2959  [
2960  'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
2961  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
2962  ]
2963  );
2964  }
2965 
2977  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2978  return $this->cache->makeGlobalKey(
2979  self::ROW_CACHE_KEY,
2980  $db->getDomainID(),
2981  $pageId,
2982  $revId
2983  );
2984  }
2985 
2993  private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
2994  if ( $rev ) {
2995  if ( $rev->getId( $this->wikiId ) === null ) {
2996  throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
2997  }
2998  if ( $rev->getPageId( $this->wikiId ) !== $pageId ) {
2999  throw new InvalidArgumentException(
3000  "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
3001  );
3002  }
3003  }
3004  }
3005 
3020  private function getRevisionLimitConditions(
3021  IDatabase $dbr,
3022  RevisionRecord $old = null,
3023  RevisionRecord $new = null,
3024  $options = []
3025  ) {
3026  $options = (array)$options;
3027  $oldCmp = '>';
3028  $newCmp = '<';
3029  if ( in_array( self::INCLUDE_OLD, $options ) ) {
3030  $oldCmp = '>=';
3031  }
3032  if ( in_array( self::INCLUDE_NEW, $options ) ) {
3033  $newCmp = '<=';
3034  }
3035  if ( in_array( self::INCLUDE_BOTH, $options ) ) {
3036  $oldCmp = '>=';
3037  $newCmp = '<=';
3038  }
3039 
3040  $conds = [];
3041  if ( $old ) {
3042  $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3043  $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId( $this->wikiId )}) " .
3044  "OR rev_timestamp > {$oldTs}";
3045  }
3046  if ( $new ) {
3047  $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3048  $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId( $this->wikiId )}) " .
3049  "OR rev_timestamp < {$newTs}";
3050  }
3051  return $conds;
3052  }
3053 
3080  public function getRevisionIdsBetween(
3081  int $pageId,
3082  RevisionRecord $old = null,
3083  RevisionRecord $new = null,
3084  ?int $max = null,
3085  $options = [],
3086  ?string $order = null,
3087  int $flags = IDBAccessObject::READ_NORMAL
3088  ): array {
3089  $this->assertRevisionParameter( 'old', $pageId, $old );
3090  $this->assertRevisionParameter( 'new', $pageId, $new );
3091 
3092  $options = (array)$options;
3093  $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3094  in_array( self::INCLUDE_BOTH, $options );
3095  $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3096  in_array( self::INCLUDE_BOTH, $options );
3097 
3098  // No DB query needed if old and new are the same revision.
3099  // Can't check for consecutive revisions with 'getParentId' for a similar
3100  // optimization as edge cases exist when there are revisions between
3101  // a revision and it's parent. See T185167 for more details.
3102  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3103  return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3104  }
3105 
3106  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3107  $conds = array_merge(
3108  [
3109  'rev_page' => $pageId,
3110  $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3111  ],
3112  $this->getRevisionLimitConditions( $db, $old, $new, $options )
3113  );
3114 
3115  $queryOptions = [];
3116  if ( $order !== null ) {
3117  $queryOptions['ORDER BY'] = [ "rev_timestamp $order", "rev_id $order" ];
3118  }
3119  if ( $max !== null ) {
3120  $queryOptions['LIMIT'] = $max + 1; // extra to detect truncation
3121  }
3122 
3123  $values = $db->selectFieldValues(
3124  'revision',
3125  'rev_id',
3126  $conds,
3127  __METHOD__,
3128  $queryOptions
3129  );
3130  return array_map( 'intval', $values );
3131  }
3132 
3154  public function getAuthorsBetween(
3155  $pageId,
3156  RevisionRecord $old = null,
3157  RevisionRecord $new = null,
3158  Authority $performer = null,
3159  $max = null,
3160  $options = []
3161  ) {
3162  $this->assertRevisionParameter( 'old', $pageId, $old );
3163  $this->assertRevisionParameter( 'new', $pageId, $new );
3164  $options = (array)$options;
3165 
3166  // No DB query needed if old and new are the same revision.
3167  // Can't check for consecutive revisions with 'getParentId' for a similar
3168  // optimization as edge cases exist when there are revisions between
3169  //a revision and it's parent. See T185167 for more details.
3170  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3171  if ( empty( $options ) ) {
3172  return [];
3173  } elseif ( $performer ) {
3174  return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3175  } else {
3176  return [ $new->getUser() ];
3177  }
3178  }
3179 
3180  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3181  $conds = array_merge(
3182  [
3183  'rev_page' => $pageId,
3184  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3185  ],
3186  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3187  );
3188 
3189  $queryOpts = [ 'DISTINCT' ];
3190  if ( $max !== null ) {
3191  $queryOpts['LIMIT'] = $max + 1;
3192  }
3193 
3194  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3195  return array_map( function ( $row ) {
3196  return $this->actorStore->newActorFromRowFields(
3197  $row->rev_user,
3198  $row->rev_user_text,
3199  $row->rev_actor
3200  );
3201  }, iterator_to_array( $dbr->select(
3202  array_merge( [ 'revision' ], $actorQuery['tables'] ),
3203  $actorQuery['fields'],
3204  $conds, __METHOD__,
3205  $queryOpts,
3206  $actorQuery['joins']
3207  ) ) );
3208  }
3209 
3231  public function countAuthorsBetween(
3232  $pageId,
3233  RevisionRecord $old = null,
3234  RevisionRecord $new = null,
3235  Authority $performer = null,
3236  $max = null,
3237  $options = []
3238  ) {
3239  // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3240  // and creation of UserIdentity stuff.
3241  return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3242  }
3243 
3264  public function countRevisionsBetween(
3265  $pageId,
3266  RevisionRecord $old = null,
3267  RevisionRecord $new = null,
3268  $max = null,
3269  $options = []
3270  ) {
3271  $this->assertRevisionParameter( 'old', $pageId, $old );
3272  $this->assertRevisionParameter( 'new', $pageId, $new );
3273 
3274  // No DB query needed if old and new are the same revision.
3275  // Can't check for consecutive revisions with 'getParentId' for a similar
3276  // optimization as edge cases exist when there are revisions between
3277  //a revision and it's parent. See T185167 for more details.
3278  if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3279  return 0;
3280  }
3281 
3282  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3283  $conds = array_merge(
3284  [
3285  'rev_page' => $pageId,
3286  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3287  ],
3288  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3289  );
3290  if ( $max !== null ) {
3291  return $dbr->selectRowCount( 'revision', '1',
3292  $conds,
3293  __METHOD__,
3294  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3295  );
3296  } else {
3297  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3298  }
3299  }
3300 
3312  public function findIdenticalRevision(
3313  RevisionRecord $revision,
3314  int $searchLimit
3315  ): ?RevisionRecord {
3316  $revision->assertWiki( $this->wikiId );
3317  $db = $this->getDBConnectionRef( DB_REPLICA );
3318  $revQuery = $this->getQueryInfo();
3319  $subquery = $db->buildSelectSubquery(
3320  $revQuery['tables'],
3321  $revQuery['fields'],
3322  [ 'rev_page' => $revision->getPageId( $this->wikiId ) ],
3323  __METHOD__,
3324  [
3325  'ORDER BY' => [
3326  'rev_timestamp DESC',
3327  // for cases where there are multiple revs with same timestamp
3328  'rev_id DESC'
3329  ],
3330  'LIMIT' => $searchLimit,
3331  // skip the most recent edit, we can't revert to it anyway
3332  'OFFSET' => 1
3333  ],
3334  $revQuery['joins']
3335  );
3336 
3337  // selectRow effectively uses LIMIT 1 clause, returning only the first result
3338  $revisionRow = $db->selectRow(
3339  [ 'recent_revs' => $subquery ],
3340  '*',
3341  [ 'rev_sha1' => $revision->getSha1() ],
3342  __METHOD__
3343  );
3344 
3345  return $revisionRow ? $this->newRevisionFromRow( $revisionRow ) : null;
3346  }
3347 
3348  // TODO: move relevant methods from Title here, e.g. isBigDeletion, etc.
3349 }
3350 
3355 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:1495
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:2712
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:98
MediaWiki\Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, PageIdentity $page, array $blobHints=[])
Definition: RevisionStore.php:910
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:38
MediaWiki\Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:105
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:37
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
MediaWiki\Revision\RevisionStore\isRevisionRow
isRevisionRow( $row, string $table='')
Determine whether the parameter is a row containing all the fields that RevisionStore needs to create...
Definition: RevisionStore.php:2518
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:133
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:464
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:1539
MediaWiki\Revision\RevisionStore\getFirstRevision
getFirstRevision( $page, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
Definition: RevisionStore.php:2944
MediaWiki\Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, PageIdentity $page=null, $fromCache=false)
Definition: RevisionStore.php:1560
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:110
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:88
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
MediaWiki\Revision\RevisionStore\getRelativeRevision
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
Definition: RevisionStore.php:2618
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:288
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:247
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:3080
MediaWiki\Revision\RevisionStore\wrapPage
wrapPage(PageIdentity $page)
Definition: RevisionStore.php:370
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:99
MediaWiki\Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:2748
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:120
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:2872
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:1302
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:2993
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\newSaved
static newSaved(int $revisionId, ?int $contentId, string $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
Definition: SlotRecord.php:188
MediaWiki\Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:556
MediaWiki\Revision\RevisionStore\insertSlotRowOn
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
Definition: RevisionStore.php:944
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:2290
$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:3154
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:1255
MediaWiki\Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
MediaWiki\Revision\RevisionStore\$hookRunner
HookRunner $hookRunner
Definition: RevisionStore.php:157
MediaWiki\Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, PageIdentity $page)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2804
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:602
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:2697
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\Revision\RevisionStore\newRevisionFromArchiveRowAndSlots
newRevisionFromArchiveRowAndSlots(stdClass $row, $slots, int $queryFlags=0, ?PageIdentity $page=null, array $overrides=[])
Definition: RevisionStore.php:1588
MediaWiki\Revision
Definition: ArchivedRevisionLookup.php:21
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:92
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:2201
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:5240
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:256
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:1028
MediaWiki\Revision\RevisionStore\loadSlotContent
loadSlotContent(SlotRecord $slot, ?string $blobData=null, ?string $blobFlags=null, ?string $blobFormat=null, int $queryFlags=0)
Loads a Content object based on a slot row.
Definition: RevisionStore.php:1153
MediaWiki\Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:130
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:26
MediaWiki\Revision\RevisionStore\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: RevisionStore.php:154
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:2335
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:579
$blob
$blob
Definition: testCompression.php:70
MediaWiki\Revision\RevisionStore\newRevisionFromRowAndSlots
newRevisionFromRowAndSlots(stdClass $row, $slots, int $queryFlags=0, ?PageIdentity $page=null, bool $fromCache=false)
Definition: RevisionStore.php:1663
MediaWiki\Revision\RevisionStore\getSlotRowsForBatch
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
Definition: RevisionStore.php:2085
MediaWiki\Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2589
MediaWiki\Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:115
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:1417
MediaWiki\Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:138
MediaWiki\Revision\RevisionStore\insertContentRowOn
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
Definition: RevisionStore.php:962
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:2781
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:650
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:738
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:2458
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:55
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:544
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
MediaWiki\Revision\RevisionStore\findIdenticalRevision
findIdenticalRevision(RevisionRecord $revision, int $searchLimit)
Tries to find a revision identical to $revision in $searchLimit most recent revisions of this page.
Definition: RevisionStore.php:3312
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:42
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:148
MediaWiki\Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:517
$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:3020
MediaWiki\DAO\WikiAwareEntity\assertWiki
assertWiki( $wikiId)
Throws if $wikiId is different from the return value of getWikiId().
MediaWiki\Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:453
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:189
MediaWiki\Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:231
MediaWiki\Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:423
MediaWiki\Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, PageIdentity $page)
Definition: RevisionStore.php:1369
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:2378
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
Wikimedia\Rdbms\IDatabase\unlock
unlock( $lockName, $method)
Release a lock.
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:2830
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:137
MediaWiki\Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:125
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:487
MediaWiki\Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:143
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:36
MediaWiki\Revision\RevisionStore\getRcIdIfUnpatrolled
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaced Revision::isUnpatrolled.
Definition: RevisionStore.php:1099
Title\newFromIDs
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition: Title.php:553
MediaWiki\Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:39
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:2682
MediaWiki\Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:531
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:2250
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:2977
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:878
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:47
MediaWiki\Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:540
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:224
MediaWiki\Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, $parentId)
Definition: RevisionStore.php:764
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:3231
MediaWiki\Revision\RevisionStore\ORDER_OLDEST_TO_NEWEST
const ORDER_OLDEST_TO_NEWEST
Definition: RevisionStore.php:94
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:3264
MediaWiki\Revision\RevisionStore\INCLUDE_BOTH
const INCLUDE_BOTH
Definition: RevisionStore.php:100
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:95
MediaWiki\Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2549
MediaWiki\Revision\RevisionStore\$pageStore
PageStore $pageStore
Definition: RevisionStore.php:160
MediaWiki\Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, UserIdentity $user, CommentStoreComment $comment, PageIdentity $page, $pageId, $parentId)
Definition: RevisionStore.php:624
MediaWiki\Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:408
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:163
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:1341
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:238
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:1121
wfBacktrace
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
Definition: GlobalFunctions.php:1323
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\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:507
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:554
$t
$t
Definition: testCompression.php:74
Title\castFromLinkTarget
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:318
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:429
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:700
Page\PageStore
Definition: PageStore.php:28
MediaWiki\Revision\RevisionStore\checkContent
checkContent(Content $content, PageIdentity $page, string $role)
MCR migration note: this corresponded to Revision::checkContentModel.
Definition: RevisionStore.php:983
MediaWiki\Revision\RevisionStore\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: RevisionStore.php:151
MediaWiki\Revision\RevisionStore\getDBConnectionRef
getDBConnectionRef( $mode, $groups=[])
Definition: RevisionStore.php:267
MediaWiki\Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:391
MediaWiki\Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:317
MediaWiki\Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:584
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:1782
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:2312
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:1235
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:308
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:414
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:1874