MediaWiki  master
RevisionStore.php
Go to the documentation of this file.
1 <?php
27 namespace MediaWiki\Revision;
28 
29 use ActorMigration;
30 use CommentStore;
32 use Content;
33 use ContentHandler;
35 use Hooks;
36 use IDBAccessObject;
38 use IP;
39 use LogicException;
48 use Message;
49 use MWException;
54 use RecentChange;
55 use Revision;
57 use StatusValue;
58 use stdClass;
59 use Title;
60 use Traversable;
61 use User;
62 use WANObjectCache;
69 
80  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
81 
82  const ROW_CACHE_KEY = 'revision-row-1.29';
83 
87  private $blobStore;
88 
92  private $dbDomain;
93 
98  private $contentHandlerUseDB = true;
99 
103  private $loadBalancer;
104 
108  private $cache;
109 
113  private $commentStore;
114 
119 
123  private $logger;
124 
129 
133  private $slotRoleStore;
134 
137 
140 
160  public function __construct(
161  ILoadBalancer $loadBalancer,
162  SqlBlobStore $blobStore,
165  NameTableStore $contentModelStore,
166  NameTableStore $slotRoleStore,
170  $dbDomain = false
171  ) {
172  Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
173  Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
174  Assert::parameter(
175  ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH,
176  '$mcrMigrationStage',
177  'Reading from the old and the new schema at the same time is not supported.'
178  );
179  Assert::parameter(
180  ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== 0,
181  '$mcrMigrationStage',
182  'Reading needs to be enabled for the old or the new schema.'
183  );
184  Assert::parameter(
186  '$mcrMigrationStage',
187  'Writing needs to be enabled for the new schema.'
188  );
189  Assert::parameter(
192  '$mcrMigrationStage',
193  'Cannot read the old schema when not also writing it.'
194  );
195 
196  $this->loadBalancer = $loadBalancer;
197  $this->blobStore = $blobStore;
198  $this->cache = $cache;
199  $this->commentStore = $commentStore;
200  $this->contentModelStore = $contentModelStore;
201  $this->slotRoleStore = $slotRoleStore;
202  $this->slotRoleRegistry = $slotRoleRegistry;
203  $this->mcrMigrationStage = $mcrMigrationStage;
204  $this->actorMigration = $actorMigration;
205  $this->dbDomain = $dbDomain;
206  $this->logger = new NullLogger();
207  }
208 
214  private function hasMcrSchemaFlags( $flags ) {
215  return ( $this->mcrMigrationStage & $flags ) === $flags;
216  }
217 
225  if ( $this->dbDomain !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
226  throw new RevisionAccessException(
227  "Cross-wiki content loading is not supported by the pre-MCR schema"
228  );
229  }
230  }
231 
232  public function setLogger( LoggerInterface $logger ) {
233  $this->logger = $logger;
234  }
235 
239  public function isReadOnly() {
240  return $this->blobStore->isReadOnly();
241  }
242 
246  public function getContentHandlerUseDB() {
248  }
249 
258  ) {
259  if ( !$contentHandlerUseDB ) {
260  throw new MWException(
261  'Content model must be stored in the database for multi content revision migration.'
262  );
263  }
264  }
265  $this->contentHandlerUseDB = $contentHandlerUseDB;
266  }
267 
271  private function getDBLoadBalancer() {
272  return $this->loadBalancer;
273  }
274 
280  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
281  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
282  return $this->getDBConnectionRef( $mode );
283  }
284 
291  private function getDBConnectionRef( $mode, $groups = [] ) {
292  $lb = $this->getDBLoadBalancer();
293  return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
294  }
295 
310  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
311  if ( !$pageId && !$revId ) {
312  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
313  }
314 
315  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
316  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
317  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
318  $queryFlags = self::READ_NORMAL;
319  }
320 
321  $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
322  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
323 
324  // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
325  if ( $canUseTitleNewFromId ) {
326  $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
327  // TODO: better foreign title handling (introduce TitleFactory)
328  $title = Title::newFromID( $pageId, $titleFlags );
329  if ( $title ) {
330  return $title;
331  }
332  }
333 
334  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
335  $canUseRevId = ( $revId !== null && $revId > 0 );
336 
337  if ( $canUseRevId ) {
338  $dbr = $this->getDBConnectionRef( $dbMode );
339  // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
340  $row = $dbr->selectRow(
341  [ 'revision', 'page' ],
342  [
343  'page_namespace',
344  'page_title',
345  'page_id',
346  'page_latest',
347  'page_is_redirect',
348  'page_len',
349  ],
350  [ 'rev_id' => $revId ],
351  __METHOD__,
352  $dbOptions,
353  [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
354  );
355  if ( $row ) {
356  // TODO: better foreign title handling (introduce TitleFactory)
357  return Title::newFromRow( $row );
358  }
359  }
360 
361  // If we still don't have a title, fallback to master if that wasn't already happening.
362  if ( $dbMode !== DB_MASTER ) {
363  $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
364  if ( $title ) {
365  $this->logger->info(
366  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
367  [ 'trace' => wfBacktrace() ]
368  );
369  return $title;
370  }
371  }
372 
373  throw new RevisionAccessException(
374  "Could not determine title for page ID $pageId and revision ID $revId"
375  );
376  }
377 
385  private function failOnNull( $value, $name ) {
386  if ( $value === null ) {
387  throw new IncompleteRevisionException(
388  "$name must not be " . var_export( $value, true ) . "!"
389  );
390  }
391 
392  return $value;
393  }
394 
402  private function failOnEmpty( $value, $name ) {
403  if ( $value === null || $value === 0 || $value === '' ) {
404  throw new IncompleteRevisionException(
405  "$name must not be " . var_export( $value, true ) . "!"
406  );
407  }
408 
409  return $value;
410  }
411 
424  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
425  // TODO: pass in a DBTransactionContext instead of a database connection.
426  $this->checkDatabaseDomain( $dbw );
427 
428  $slotRoles = $rev->getSlotRoles();
429 
430  // Make sure the main slot is always provided throughout migration
431  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
432  throw new InvalidArgumentException(
433  'main slot must be provided'
434  );
435  }
436 
437  // If we are not writing into the new schema, we can't support extra slots.
439  && $slotRoles !== [ SlotRecord::MAIN ]
440  ) {
441  throw new InvalidArgumentException(
442  'Only the main slot is supported when not writing to the MCR enabled schema!'
443  );
444  }
445 
446  // As long as we are not reading from the new schema, we don't want to write extra slots.
448  && $slotRoles !== [ SlotRecord::MAIN ]
449  ) {
450  throw new InvalidArgumentException(
451  'Only the main slot is supported when not reading from the MCR enabled schema!'
452  );
453  }
454 
455  // Checks
456  $this->failOnNull( $rev->getSize(), 'size field' );
457  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
458  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
459  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
460  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
461  $this->failOnNull( $user->getId(), 'user field' );
462  $this->failOnEmpty( $user->getName(), 'user_text field' );
463 
464  if ( !$rev->isReadyForInsertion() ) {
465  // This is here for future-proofing. At the time this check being added, it
466  // was redundant to the individual checks above.
467  throw new IncompleteRevisionException( 'Revision is incomplete' );
468  }
469 
470  // TODO: we shouldn't need an actual Title here.
472  $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
473 
474  $parentId = $rev->getParentId() === null
475  ? $this->getPreviousRevisionId( $dbw, $rev )
476  : $rev->getParentId();
477 
479  $rev = $dbw->doAtomicSection(
480  __METHOD__,
481  function ( IDatabase $dbw, $fname ) use (
482  $rev,
483  $user,
484  $comment,
485  $title,
486  $pageId,
487  $parentId
488  ) {
489  return $this->insertRevisionInternal(
490  $rev,
491  $dbw,
492  $user,
493  $comment,
494  $title,
495  $pageId,
496  $parentId
497  );
498  }
499  );
500 
501  // sanity checks
502  Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
503  Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
504  Assert::postcondition(
505  $rev->getComment( RevisionRecord::RAW ) !== null,
506  'revision must have a comment'
507  );
508  Assert::postcondition(
509  $rev->getUser( RevisionRecord::RAW ) !== null,
510  'revision must have a user'
511  );
512 
513  // Trigger exception if the main slot is missing.
514  // Technically, this could go away after MCR migration: while
515  // calling code may require a main slot to exist, RevisionStore
516  // really should not know or care about that requirement.
518 
519  foreach ( $slotRoles as $role ) {
520  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
521  Assert::postcondition(
522  $slot->getContent() !== null,
523  $role . ' slot must have content'
524  );
525  Assert::postcondition(
526  $slot->hasRevision(),
527  $role . ' slot must have a revision associated'
528  );
529  }
530 
531  Hooks::run( 'RevisionRecordInserted', [ $rev ] );
532 
533  // TODO: deprecate in 1.32!
534  $legacyRevision = new Revision( $rev );
535  Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
536 
537  return $rev;
538  }
539 
540  private function insertRevisionInternal(
541  RevisionRecord $rev,
542  IDatabase $dbw,
543  User $user,
544  CommentStoreComment $comment,
545  Title $title,
546  $pageId,
547  $parentId
548  ) {
549  $slotRoles = $rev->getSlotRoles();
550 
551  $revisionRow = $this->insertRevisionRowOn(
552  $dbw,
553  $rev,
554  $title,
555  $parentId
556  );
557 
558  $revisionId = $revisionRow['rev_id'];
559 
560  $blobHints = [
561  BlobStore::PAGE_HINT => $pageId,
562  BlobStore::REVISION_HINT => $revisionId,
563  BlobStore::PARENT_HINT => $parentId,
564  ];
565 
566  $newSlots = [];
567  foreach ( $slotRoles as $role ) {
568  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
569 
570  // If the SlotRecord already has a revision ID set, this means it already exists
571  // in the database, and should already belong to the current revision.
572  // However, a slot may already have a revision, but no content ID, if the slot
573  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
574  // mode, and the respective archive row was not yet migrated to the new schema.
575  // In that case, a new slot row (and content row) must be inserted even during
576  // undeletion.
577  if ( $slot->hasRevision() && $slot->hasContentId() ) {
578  // TODO: properly abort transaction if the assertion fails!
579  Assert::parameter(
580  $slot->getRevision() === $revisionId,
581  'slot role ' . $slot->getRole(),
582  'Existing slot should belong to revision '
583  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
584  );
585 
586  // Slot exists, nothing to do, move along.
587  // This happens when restoring archived revisions.
588 
589  $newSlots[$role] = $slot;
590 
591  // Write the main slot's text ID to the revision table for backwards compatibility
592  if ( $slot->getRole() === SlotRecord::MAIN
594  ) {
595  $blobAddress = $slot->getAddress();
596  $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
597  }
598  } else {
599  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
600  }
601  }
602 
603  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
604 
605  $rev = new RevisionStoreRecord(
606  $title,
607  $user,
608  $comment,
609  (object)$revisionRow,
610  new RevisionSlots( $newSlots ),
611  $this->dbDomain
612  );
613 
614  return $rev;
615  }
616 
624  private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
625  $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
626  if ( !$textId ) {
627  throw new LogicException(
628  'Blob address not supported in 1.29 database schema: ' . $blobAddress
629  );
630  }
631 
632  // getTextIdFromAddress() is free to insert something into the text table, so $textId
633  // may be a new value, not anything already contained in $blobAddress.
634  $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
635 
636  $dbw->update(
637  'revision',
638  [ 'rev_text_id' => $textId ],
639  [ 'rev_id' => $revisionId ],
640  __METHOD__
641  );
642 
643  return $textId;
644  }
645 
654  private function insertSlotOn(
655  IDatabase $dbw,
656  $revisionId,
657  SlotRecord $protoSlot,
658  Title $title,
659  array $blobHints = []
660  ) {
661  if ( $protoSlot->hasAddress() ) {
662  $blobAddress = $protoSlot->getAddress();
663  } else {
664  $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
665  }
666 
667  $contentId = null;
668 
669  // Write the main slot's text ID to the revision table for backwards compatibility
670  if ( $protoSlot->getRole() === SlotRecord::MAIN
672  ) {
673  // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
674  // with the real content ID below.
675  $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
676  $contentId = $this->emulateContentId( $textId );
677  }
678 
679  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
680  if ( $protoSlot->hasContentId() ) {
681  $contentId = $protoSlot->getContentId();
682  } else {
683  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
684  }
685 
686  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
687  }
688 
689  $savedSlot = SlotRecord::newSaved(
690  $revisionId,
691  $contentId,
692  $blobAddress,
693  $protoSlot
694  );
695 
696  return $savedSlot;
697  }
698 
706  private function insertIpChangesRow(
707  IDatabase $dbw,
708  User $user,
709  RevisionRecord $rev,
710  $revisionId
711  ) {
712  if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
713  $ipcRow = [
714  'ipc_rev_id' => $revisionId,
715  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
716  'ipc_hex' => IP::toHex( $user->getName() ),
717  ];
718  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
719  }
720  }
721 
733  private function insertRevisionRowOn(
734  IDatabase $dbw,
735  RevisionRecord $rev,
736  Title $title,
737  $parentId
738  ) {
739  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
740 
741  list( $commentFields, $commentCallback ) =
742  $this->commentStore->insertWithTempTable(
743  $dbw,
744  'rev_comment',
746  );
747  $revisionRow += $commentFields;
748 
749  list( $actorFields, $actorCallback ) =
750  $this->actorMigration->getInsertValuesWithTempTable(
751  $dbw,
752  'rev_user',
754  );
755  $revisionRow += $actorFields;
756 
757  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
758 
759  if ( !isset( $revisionRow['rev_id'] ) ) {
760  // only if auto-increment was used
761  $revisionRow['rev_id'] = intval( $dbw->insertId() );
762 
763  if ( $dbw->getType() === 'mysql' ) {
764  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
765  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
766  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
767 
768  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
769  $table = 'archive';
770  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
771  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
772  if ( $maxRevId2 >= $maxRevId ) {
773  $maxRevId = $maxRevId2;
774  $table = 'slots';
775  }
776  }
777 
778  if ( $maxRevId >= $revisionRow['rev_id'] ) {
779  $this->logger->debug(
780  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
781  . ' Trying to fix it.',
782  [
783  'revid' => $revisionRow['rev_id'],
784  'table' => $table,
785  'maxrevid' => $maxRevId,
786  ]
787  );
788 
789  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
790  throw new MWException( 'Failed to get database lock for T202032' );
791  }
792  $fname = __METHOD__;
793  $dbw->onTransactionResolution(
794  function ( $trigger, IDatabase $dbw ) use ( $fname ) {
795  $dbw->unlock( 'fix-for-T202032', $fname );
796  }
797  );
798 
799  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
800 
801  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
802  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
803  // inserts too, though, at least on MariaDB 10.1.29.
804  //
805  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
806  // transactions in this code path thanks to the row lock from the original ->insert() above.
807  //
808  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
809  // that's for non-MySQL DBs.
810  $row1 = $dbw->query(
811  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
812  )->fetchObject();
813  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
814  $row2 = $dbw->query(
815  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
816  . ' FOR UPDATE'
817  )->fetchObject();
818  } else {
819  $row2 = null;
820  }
821  $maxRevId = max(
822  $maxRevId,
823  $row1 ? intval( $row1->v ) : 0,
824  $row2 ? intval( $row2->v ) : 0
825  );
826 
827  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
828  // transactions will throw a duplicate key error here. It doesn't seem worth trying
829  // to avoid that.
830  $revisionRow['rev_id'] = $maxRevId + 1;
831  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
832  }
833  }
834  }
835 
836  $commentCallback( $revisionRow['rev_id'] );
837  $actorCallback( $revisionRow['rev_id'], $revisionRow );
838 
839  return $revisionRow;
840  }
841 
852  private function getBaseRevisionRow(
853  IDatabase $dbw,
854  RevisionRecord $rev,
855  Title $title,
856  $parentId
857  ) {
858  // Record the edit in revisions
859  $revisionRow = [
860  'rev_page' => $rev->getPageId(),
861  'rev_parent_id' => $parentId,
862  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
863  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
864  'rev_deleted' => $rev->getVisibility(),
865  'rev_len' => $rev->getSize(),
866  'rev_sha1' => $rev->getSha1(),
867  ];
868 
869  if ( $rev->getId() !== null ) {
870  // Needed to restore revisions with their original ID
871  $revisionRow['rev_id'] = $rev->getId();
872  }
873 
874  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
875  // In non MCR mode this IF section will relate to the main slot
876  $mainSlot = $rev->getSlot( SlotRecord::MAIN );
877  $model = $mainSlot->getModel();
878  $format = $mainSlot->getFormat();
879 
880  // MCR migration note: rev_content_model and rev_content_format will go away
881  if ( $this->contentHandlerUseDB ) {
883 
884  $defaultModel = ContentHandler::getDefaultModelFor( $title );
885  $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
886 
887  $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
888  $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
889  }
890  }
891 
892  return $revisionRow;
893  }
894 
903  private function storeContentBlob(
904  SlotRecord $slot,
905  Title $title,
906  array $blobHints = []
907  ) {
908  $content = $slot->getContent();
909  $format = $content->getDefaultFormat();
910  $model = $content->getModel();
911 
912  $this->checkContent( $content, $title, $slot->getRole() );
913 
914  return $this->blobStore->storeBlob(
915  $content->serialize( $format ),
916  // These hints "leak" some information from the higher abstraction layer to
917  // low level storage to allow for optimization.
918  array_merge(
919  $blobHints,
920  [
921  BlobStore::DESIGNATION_HINT => 'page-content',
922  BlobStore::ROLE_HINT => $slot->getRole(),
923  BlobStore::SHA1_HINT => $slot->getSha1(),
924  BlobStore::MODEL_HINT => $model,
925  BlobStore::FORMAT_HINT => $format,
926  ]
927  )
928  );
929  }
930 
937  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
938  $slotRow = [
939  'slot_revision_id' => $revisionId,
940  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
941  'slot_content_id' => $contentId,
942  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
943  // that we just inserted.
944  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
945  ];
946  $dbw->insert( 'slots', $slotRow, __METHOD__ );
947  }
948 
955  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
956  $contentRow = [
957  'content_size' => $slot->getSize(),
958  'content_sha1' => $slot->getSha1(),
959  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
960  'content_address' => $blobAddress,
961  ];
962  $dbw->insert( 'content', $contentRow, __METHOD__ );
963  return intval( $dbw->insertId() );
964  }
965 
976  private function checkContent( Content $content, Title $title, $role ) {
977  // Note: may return null for revisions that have not yet been inserted
978 
979  $model = $content->getModel();
980  $format = $content->getDefaultFormat();
981  $handler = $content->getContentHandler();
982 
983  $name = "$title";
984 
985  if ( !$handler->isSupportedFormat( $format ) ) {
986  throw new MWException( "Can't use format $format with content model $model on $name" );
987  }
988 
989  if ( !$this->contentHandlerUseDB ) {
990  // if $wgContentHandlerUseDB is not set,
991  // all revisions must use the default content model and format.
992 
994 
995  $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
996  $defaultModel = $roleHandler->getDefaultModel( $title );
997  $defaultHandler = ContentHandler::getForModelID( $defaultModel );
998  $defaultFormat = $defaultHandler->getDefaultFormat();
999 
1000  if ( $model != $defaultModel ) {
1001  throw new MWException( "Can't save non-default content model with "
1002  . "\$wgContentHandlerUseDB disabled: model is $model, "
1003  . "default for $name is $defaultModel"
1004  );
1005  }
1006 
1007  if ( $format != $defaultFormat ) {
1008  throw new MWException( "Can't use non-default content format with "
1009  . "\$wgContentHandlerUseDB disabled: format is $format, "
1010  . "default for $name is $defaultFormat"
1011  );
1012  }
1013  }
1014 
1015  if ( !$content->isValid() ) {
1016  throw new MWException(
1017  "New content for $name is not valid! Content model is $model"
1018  );
1019  }
1020  }
1021 
1047  public function newNullRevision(
1048  IDatabase $dbw,
1049  Title $title,
1050  CommentStoreComment $comment,
1051  $minor,
1052  User $user
1053  ) {
1054  $this->checkDatabaseDomain( $dbw );
1055 
1056  $pageId = $title->getArticleID();
1057 
1058  // T51581: Lock the page table row to ensure no other process
1059  // is adding a revision to the page at the same time.
1060  // Avoid locking extra tables, compare T191892.
1061  $pageLatest = $dbw->selectField(
1062  'page',
1063  'page_latest',
1064  [ 'page_id' => $pageId ],
1065  __METHOD__,
1066  [ 'FOR UPDATE' ]
1067  );
1068 
1069  if ( !$pageLatest ) {
1070  return null;
1071  }
1072 
1073  // Fetch the actual revision row from master, without locking all extra tables.
1074  $oldRevision = $this->loadRevisionFromConds(
1075  $dbw,
1076  [ 'rev_id' => intval( $pageLatest ) ],
1077  self::READ_LATEST,
1078  $title
1079  );
1080 
1081  if ( !$oldRevision ) {
1082  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1083  $this->logger->error(
1084  $msg,
1085  [ 'exception' => new RuntimeException( $msg ) ]
1086  );
1087  return null;
1088  }
1089 
1090  // Construct the new revision
1091  $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1092  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1093 
1094  $newRevision->setComment( $comment );
1095  $newRevision->setUser( $user );
1096  $newRevision->setTimestamp( $timestamp );
1097  $newRevision->setMinorEdit( $minor );
1098 
1099  return $newRevision;
1100  }
1101 
1111  public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1112  $rc = $this->getRecentChange( $rev );
1113  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1114  return $rc->getAttribute( 'rc_id' );
1115  } else {
1116  return 0;
1117  }
1118  }
1119 
1133  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1134  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1135  $db = $this->getDBConnectionRef( $dbType );
1136 
1137  $userIdentity = $rev->getUser( RevisionRecord::RAW );
1138 
1139  if ( !$userIdentity ) {
1140  // If the revision has no user identity, chances are it never went
1141  // into the database, and doesn't have an RC entry.
1142  return null;
1143  }
1144 
1145  // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
1146  $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
1148  [
1149  $actorWhere['conds'],
1150  'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
1151  'rc_this_oldid' => $rev->getId()
1152  ],
1153  __METHOD__,
1154  $dbType
1155  );
1156 
1157  // XXX: cache this locally? Glue it to the RevisionRecord?
1158  return $rc;
1159  }
1160 
1168  private static function mapArchiveFields( $archiveRow ) {
1169  $fieldMap = [
1170  // keep with ar prefix:
1171  'ar_id' => 'ar_id',
1172 
1173  // not the same suffix:
1174  'ar_page_id' => 'rev_page',
1175  'ar_rev_id' => 'rev_id',
1176 
1177  // same suffix:
1178  'ar_text_id' => 'rev_text_id',
1179  'ar_timestamp' => 'rev_timestamp',
1180  'ar_user_text' => 'rev_user_text',
1181  'ar_user' => 'rev_user',
1182  'ar_actor' => 'rev_actor',
1183  'ar_minor_edit' => 'rev_minor_edit',
1184  'ar_deleted' => 'rev_deleted',
1185  'ar_len' => 'rev_len',
1186  'ar_parent_id' => 'rev_parent_id',
1187  'ar_sha1' => 'rev_sha1',
1188  'ar_comment' => 'rev_comment',
1189  'ar_comment_cid' => 'rev_comment_cid',
1190  'ar_comment_id' => 'rev_comment_id',
1191  'ar_comment_text' => 'rev_comment_text',
1192  'ar_comment_data' => 'rev_comment_data',
1193  'ar_comment_old' => 'rev_comment_old',
1194  'ar_content_format' => 'rev_content_format',
1195  'ar_content_model' => 'rev_content_model',
1196  ];
1197 
1198  $revRow = new stdClass();
1199  foreach ( $fieldMap as $arKey => $revKey ) {
1200  if ( property_exists( $archiveRow, $arKey ) ) {
1201  $revRow->$revKey = $archiveRow->$arKey;
1202  }
1203  }
1204 
1205  return $revRow;
1206  }
1207 
1218  private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
1219  $mainSlotRow = new stdClass();
1220  $mainSlotRow->role_name = SlotRecord::MAIN;
1221  $mainSlotRow->model_name = null;
1222  $mainSlotRow->slot_revision_id = null;
1223  $mainSlotRow->slot_content_id = null;
1224  $mainSlotRow->content_address = null;
1225 
1226  $content = null;
1227  $blobData = null;
1228  $blobFlags = null;
1229 
1230  if ( is_object( $row ) ) {
1231  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1232  // Don't emulate from a row when using the new schema.
1233  // Emulating from an array is still OK.
1234  throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
1235  }
1236 
1237  // archive row
1238  if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
1239  $row = $this->mapArchiveFields( $row );
1240  }
1241 
1242  if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
1243  $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
1244  $row->rev_text_id
1245  );
1246  }
1247 
1248  // This is used by null-revisions
1249  $mainSlotRow->slot_origin = isset( $row->slot_origin )
1250  ? intval( $row->slot_origin )
1251  : null;
1252 
1253  if ( isset( $row->old_text ) ) {
1254  // this happens when the text-table gets joined directly, in the pre-1.30 schema
1255  $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
1256  // Check against selects that might have not included old_flags
1257  if ( !property_exists( $row, 'old_flags' ) ) {
1258  throw new InvalidArgumentException( 'old_flags was not set in $row' );
1259  }
1260  $blobFlags = $row->old_flags ?? '';
1261  }
1262 
1263  $mainSlotRow->slot_revision_id = intval( $row->rev_id );
1264 
1265  $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
1266  $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
1267  $mainSlotRow->model_name = isset( $row->rev_content_model )
1268  ? strval( $row->rev_content_model )
1269  : null;
1270  // XXX: in the future, we'll probably always use the default format, and drop content_format
1271  $mainSlotRow->format_name = isset( $row->rev_content_format )
1272  ? strval( $row->rev_content_format )
1273  : null;
1274 
1275  if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
1276  // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1277  $mainSlotRow->slot_content_id
1278  = $this->emulateContentId( intval( $row->rev_text_id ) );
1279  }
1280  } elseif ( is_array( $row ) ) {
1281  $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
1282 
1283  $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
1284  ? intval( $row['slot_origin'] )
1285  : null;
1286  $mainSlotRow->content_address = isset( $row['text_id'] )
1287  ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
1288  : null;
1289  $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
1290  $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
1291 
1292  $mainSlotRow->model_name = isset( $row['content_model'] )
1293  ? strval( $row['content_model'] ) : null; // XXX: must be a string!
1294  // XXX: in the future, we'll probably always use the default format, and drop content_format
1295  $mainSlotRow->format_name = isset( $row['content_format'] )
1296  ? strval( $row['content_format'] ) : null;
1297  $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
1298  // XXX: If the flags field is not set then $blobFlags should be null so that no
1299  // decoding will happen. An empty string will result in default decodings.
1300  $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
1301 
1302  // if we have a Content object, override mText and mContentModel
1303  if ( !empty( $row['content'] ) ) {
1304  if ( !( $row['content'] instanceof Content ) ) {
1305  throw new MWException( 'content field must contain a Content object.' );
1306  }
1307 
1309  $content = $row['content'];
1310  $handler = $content->getContentHandler();
1311 
1312  $mainSlotRow->model_name = $content->getModel();
1313 
1314  // XXX: in the future, we'll probably always use the default format.
1315  if ( $mainSlotRow->format_name === null ) {
1316  $mainSlotRow->format_name = $handler->getDefaultFormat();
1317  }
1318  }
1319 
1320  if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
1321  // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1322  $mainSlotRow->slot_content_id
1323  = $this->emulateContentId( intval( $row['text_id'] ) );
1324  }
1325  } else {
1326  throw new MWException( 'Revision constructor passed invalid row format.' );
1327  }
1328 
1329  // With the old schema, the content changes with every revision,
1330  // except for null-revisions.
1331  if ( !isset( $mainSlotRow->slot_origin ) ) {
1332  $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
1333  }
1334 
1335  if ( $mainSlotRow->model_name === null ) {
1336  $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
1338 
1339  return $this->slotRoleRegistry->getRoleHandler( $slot->getRole() )
1340  ->getDefaultModel( $title );
1341  };
1342  }
1343 
1344  if ( !$content ) {
1345  // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
1346  // is missing, but "empty revisions" with no content are used in some edge cases.
1347 
1348  $content = function ( SlotRecord $slot )
1349  use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
1350  {
1351  return $this->loadSlotContent(
1352  $slot,
1353  $blobData,
1354  $blobFlags,
1355  $mainSlotRow->format_name,
1356  $queryFlags
1357  );
1358  };
1359  }
1360 
1361  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
1362  // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
1363  // the inherited slot to have the same content_id as the original slot. In that case,
1364  // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
1365  $mainSlotRow->slot_content_id =
1366  function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
1367  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1368  return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
1369  };
1370  }
1371 
1372  return new SlotRecord( $mainSlotRow, $content );
1373  }
1374 
1386  private function emulateContentId( $textId ) {
1387  // Return a negative number to ensure the ID is distinct from any real content IDs
1388  // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
1389  // mode.
1390  return -$textId;
1391  }
1392 
1412  private function loadSlotContent(
1413  SlotRecord $slot,
1414  $blobData = null,
1415  $blobFlags = null,
1416  $blobFormat = null,
1417  $queryFlags = 0
1418  ) {
1419  if ( $blobData !== null ) {
1420  Assert::parameterType( 'string', $blobData, '$blobData' );
1421  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1422 
1423  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1424 
1425  if ( $blobFlags === null ) {
1426  // No blob flags, so use the blob verbatim.
1427  $data = $blobData;
1428  } else {
1429  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1430  if ( $data === false ) {
1431  throw new RevisionAccessException(
1432  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1433  );
1434  }
1435  }
1436 
1437  } else {
1438  $address = $slot->getAddress();
1439  try {
1440  $data = $this->blobStore->getBlob( $address, $queryFlags );
1441  } catch ( BlobAccessException $e ) {
1442  throw new RevisionAccessException(
1443  "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1444  );
1445  }
1446  }
1447 
1448  // Unserialize content
1449  $handler = ContentHandler::getForModelID( $slot->getModel() );
1450 
1451  $content = $handler->unserializeContent( $data, $blobFormat );
1452  return $content;
1453  }
1454 
1469  public function getRevisionById( $id, $flags = 0 ) {
1470  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1471  }
1472 
1489  public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1490  // TODO should not require Title in future (T206498)
1491  $title = Title::newFromLinkTarget( $linkTarget );
1492  $conds = [
1493  'page_namespace' => $title->getNamespace(),
1494  'page_title' => $title->getDBkey()
1495  ];
1496  if ( $revId ) {
1497  // Use the specified revision ID.
1498  // Note that we use newRevisionFromConds here because we want to retry
1499  // and fall back to master if the page is not found on a replica.
1500  // Since the caller supplied a revision ID, we are pretty sure the revision is
1501  // supposed to exist, so we should try hard to find it.
1502  $conds['rev_id'] = $revId;
1503  return $this->newRevisionFromConds( $conds, $flags, $title );
1504  } else {
1505  // Use a join to get the latest revision.
1506  // Note that we don't use newRevisionFromConds here because we don't want to retry
1507  // and fall back to master. The assumption is that we only want to force the fallback
1508  // if we are quite sure the revision exists because the caller supplied a revision ID.
1509  // If the page isn't found at all on a replica, it probably simply does not exist.
1510  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1511 
1512  $conds[] = 'rev_id=page_latest';
1513  $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1514 
1515  return $rev;
1516  }
1517  }
1518 
1535  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1536  $conds = [ 'page_id' => $pageId ];
1537  if ( $revId ) {
1538  // Use the specified revision ID.
1539  // Note that we use newRevisionFromConds here because we want to retry
1540  // and fall back to master if the page is not found on a replica.
1541  // Since the caller supplied a revision ID, we are pretty sure the revision is
1542  // supposed to exist, so we should try hard to find it.
1543  $conds['rev_id'] = $revId;
1544  return $this->newRevisionFromConds( $conds, $flags );
1545  } else {
1546  // Use a join to get the latest revision.
1547  // Note that we don't use newRevisionFromConds here because we don't want to retry
1548  // and fall back to master. The assumption is that we only want to force the fallback
1549  // if we are quite sure the revision exists because the caller supplied a revision ID.
1550  // If the page isn't found at all on a replica, it probably simply does not exist.
1551  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1552 
1553  $conds[] = 'rev_id=page_latest';
1554  $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1555 
1556  return $rev;
1557  }
1558  }
1559 
1571  public function getRevisionByTimestamp( $title, $timestamp ) {
1572  $db = $this->getDBConnectionRef( DB_REPLICA );
1573  return $this->newRevisionFromConds(
1574  [
1575  'rev_timestamp' => $db->timestamp( $timestamp ),
1576  'page_namespace' => $title->getNamespace(),
1577  'page_title' => $title->getDBkey()
1578  ],
1579  0,
1580  $title
1581  );
1582  }
1583 
1591  private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
1592  $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1593 
1594  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1595  $db = $this->getDBConnectionRef( $dbMode );
1596 
1597  $res = $db->select(
1598  $revQuery['tables'],
1599  $revQuery['fields'],
1600  [
1601  'slot_revision_id' => $revId,
1602  ],
1603  __METHOD__,
1604  $dbOptions,
1605  $revQuery['joins']
1606  );
1607 
1608  $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
1609 
1610  return $slots;
1611  }
1612 
1625  private function constructSlotRecords(
1626  $revId,
1627  $slotRows,
1628  $queryFlags,
1629  Title $title,
1630  $slotContents = null
1631  ) {
1632  $slots = [];
1633 
1634  foreach ( $slotRows as $row ) {
1635  // Resolve role names and model names from in-memory cache, if they were not joined in.
1636  if ( !isset( $row->role_name ) ) {
1637  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1638  }
1639 
1640  if ( !isset( $row->model_name ) ) {
1641  if ( isset( $row->content_model ) ) {
1642  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1643  } else {
1644  // We may get here if $row->model_name is set but null, perhaps because it
1645  // came from rev_content_model, which is NULL for the default model.
1646  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1647  $row->model_name = $slotRoleHandler->getDefaultModel( $title );
1648  }
1649  }
1650 
1651  if ( !isset( $row->content_id ) && isset( $row->rev_text_id ) ) {
1652  $row->slot_content_id
1653  = $this->emulateContentId( intval( $row->rev_text_id ) );
1654  }
1655 
1656  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1657  if ( isset( $row->blob_data ) ) {
1658  $slotContents[$row->content_address] = $row->blob_data;
1659  }
1660 
1661  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1662  $blob = null;
1663  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1664  $blob = $slotContents[$slot->getAddress()];
1665  if ( $blob instanceof Content ) {
1666  return $blob;
1667  }
1668  }
1669  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1670  };
1671 
1672  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1673  }
1674 
1675  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1676  throw new RevisionAccessException(
1677  'Main slot of revision ' . $revId . ' not found in database!'
1678  );
1679  }
1680 
1681  return $slots;
1682  }
1683 
1699  private function newRevisionSlots(
1700  $revId,
1701  $revisionRow,
1702  $slotRows,
1703  $queryFlags,
1704  Title $title
1705  ) {
1706  if ( $slotRows ) {
1707  $slots = new RevisionSlots(
1708  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
1709  );
1710  } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1711  $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
1712  // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive
1713  $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
1714  } else {
1715  // XXX: do we need the same kind of caching here
1716  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1717 
1718  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
1719  return $this->loadSlotRecords( $revId, $queryFlags, $title );
1720  } );
1721  }
1722 
1723  return $slots;
1724  }
1725 
1743  public function newRevisionFromArchiveRow(
1744  $row,
1745  $queryFlags = 0,
1746  Title $title = null,
1747  array $overrides = []
1748  ) {
1749  Assert::parameterType( 'object', $row, '$row' );
1750 
1751  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1752  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1753 
1754  if ( !$title && isset( $overrides['title'] ) ) {
1755  if ( !( $overrides['title'] instanceof Title ) ) {
1756  throw new MWException( 'title field override must contain a Title object.' );
1757  }
1758 
1759  $title = $overrides['title'];
1760  }
1761 
1762  if ( !isset( $title ) ) {
1763  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1764  $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1765  } else {
1766  throw new InvalidArgumentException(
1767  'A Title or ar_namespace and ar_title must be given'
1768  );
1769  }
1770  }
1771 
1772  foreach ( $overrides as $key => $value ) {
1773  $field = "ar_$key";
1774  $row->$field = $value;
1775  }
1776 
1777  try {
1778  $user = User::newFromAnyId(
1779  $row->ar_user ?? null,
1780  $row->ar_user_text ?? null,
1781  $row->ar_actor ?? null,
1782  $this->dbDomain
1783  );
1784  } catch ( InvalidArgumentException $ex ) {
1785  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1786  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1787  }
1788 
1789  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1790  // Legacy because $row may have come from self::selectFields()
1791  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1792 
1793  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, null, $queryFlags, $title );
1794 
1795  return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
1796  }
1797 
1810  public function newRevisionFromRow(
1811  $row,
1812  $queryFlags = 0,
1813  Title $title = null,
1814  $fromCache = false
1815  ) {
1816  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
1817  }
1818 
1837  $row,
1838  $slots,
1839  $queryFlags = 0,
1840  Title $title = null,
1841  $fromCache = false
1842  ) {
1843  Assert::parameterType( 'object', $row, '$row' );
1844 
1845  if ( !$title ) {
1846  $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
1847  $revId = $row->rev_id ?? 0;
1848 
1849  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1850  }
1851 
1852  if ( !isset( $row->page_latest ) ) {
1853  $row->page_latest = $title->getLatestRevID();
1854  if ( $row->page_latest === 0 && $title->exists() ) {
1855  wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1856  }
1857  }
1858 
1859  try {
1860  $user = User::newFromAnyId(
1861  $row->rev_user ?? null,
1862  $row->rev_user_text ?? null,
1863  $row->rev_actor ?? null,
1864  $this->dbDomain
1865  );
1866  } catch ( InvalidArgumentException $ex ) {
1867  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1868  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1869  }
1870 
1871  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1872  // Legacy because $row may have come from self::selectFields()
1873  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1874 
1875  if ( !( $slots instanceof RevisionSlots ) ) {
1876  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
1877  }
1878 
1879  // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1880  if ( $fromCache ) {
1881  $rev = new RevisionStoreCacheRecord(
1882  function ( $revId ) use ( $queryFlags ) {
1883  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1884  return $this->fetchRevisionRowFromConds(
1885  $db,
1886  [ 'rev_id' => intval( $revId ) ]
1887  );
1888  },
1889  $title, $user, $comment, $row, $slots, $this->dbDomain
1890  );
1891  } else {
1892  $rev = new RevisionStoreRecord(
1893  $title, $user, $comment, $row, $slots, $this->dbDomain );
1894  }
1895  return $rev;
1896  }
1897 
1918  public function newRevisionsFromBatch(
1919  $rows,
1920  array $options = [],
1921  $queryFlags = 0,
1922  Title $title = null
1923  ) {
1924  $result = new StatusValue();
1925 
1926  $rowsByRevId = [];
1927  $pageIdsToFetchTitles = [];
1928  $titlesByPageId = [];
1929  foreach ( $rows as $row ) {
1930  if ( isset( $rowsByRevId[$row->rev_id] ) ) {
1931  $result->warning(
1932  'internalerror',
1933  "Duplicate rows in newRevisionsFromBatch, rev_id {$row->rev_id}"
1934  );
1935  }
1936  if ( $title && $row->rev_page != $title->getArticleID() ) {
1937  throw new InvalidArgumentException(
1938  "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}"
1939  );
1940  } elseif ( !$title && !isset( $titlesByPageId[ $row->rev_page ] ) ) {
1941  if ( isset( $row->page_namespace ) && isset( $row->page_title ) &&
1942  // This should not happen, but just in case we don't have a page_id
1943  // set or it doesn't match rev_page, let's fetch the title again.
1944  isset( $row->page_id ) && $row->rev_page === $row->page_id
1945  ) {
1946  $titlesByPageId[ $row->rev_page ] = Title::newFromRow( $row );
1947  } else {
1948  $pageIdsToFetchTitles[] = $row->rev_page;
1949  }
1950  }
1951  $rowsByRevId[$row->rev_id] = $row;
1952  }
1953 
1954  if ( empty( $rowsByRevId ) ) {
1955  $result->setResult( true, [] );
1956  return $result;
1957  }
1958 
1959  // If the title is not supplied, batch-fetch Title objects.
1960  if ( $title ) {
1961  $titlesByPageId[$title->getArticleID()] = $title;
1962  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1963  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1964  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1965  $titlesByPageId[$t->getArticleID()] = $t;
1966  }
1967  }
1968 
1969  if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
1970  $result->setResult( true,
1971  array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) {
1972  try {
1973  return $this->newRevisionFromRow(
1974  $row,
1975  $queryFlags,
1976  $titlesByPageId[$row->rev_page]
1977  );
1978  } catch ( MWException $e ) {
1979  $result->warning( 'internalerror', $e->getMessage() );
1980  return null;
1981  }
1982  }, $rowsByRevId )
1983  );
1984  return $result;
1985  }
1986 
1987  $slotRowOptions = [
1988  'slots' => $options['slots'] ?? true,
1989  'blobs' => $options['content'] ?? false,
1990  ];
1991 
1992  if ( is_array( $slotRowOptions['slots'] )
1993  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
1994  ) {
1995  // Make sure the main slot is always loaded, RevisionRecord requires this.
1996  $slotRowOptions['slots'][] = SlotRecord::MAIN;
1997  }
1998 
1999  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2000 
2001  $result->merge( $slotRowsStatus );
2002  $slotRowsByRevId = $slotRowsStatus->getValue();
2003 
2004  $result->setResult( true, array_map( function ( $row ) use
2005  ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $result ) {
2006  if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) {
2007  $result->warning(
2008  'internalerror',
2009  "Couldn't find slots for rev {$row->rev_id}"
2010  );
2011  return null;
2012  }
2013  try {
2014  return $this->newRevisionFromRowAndSlots(
2015  $row,
2016  new RevisionSlots(
2017  $this->constructSlotRecords(
2018  $row->rev_id,
2019  $slotRowsByRevId[$row->rev_id],
2020  $queryFlags,
2021  $titlesByPageId[$row->rev_page]
2022  )
2023  ),
2024  $queryFlags,
2025  $titlesByPageId[$row->rev_page]
2026  );
2027  } catch ( MWException $e ) {
2028  $result->warning( 'internalerror', $e->getMessage() );
2029  return null;
2030  }
2031  }, $rowsByRevId ) );
2032  return $result;
2033  }
2034 
2057  private function getSlotRowsForBatch(
2058  $rowsOrIds,
2059  array $options = [],
2060  $queryFlags = 0
2061  ) {
2062  $readNew = $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW );
2063  $result = new StatusValue();
2064 
2065  $revIds = [];
2066  foreach ( $rowsOrIds as $row ) {
2067  $revIds[] = is_object( $row ) ? (int)$row->rev_id : (int)$row;
2068  }
2069 
2070  // Nothing to do.
2071  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2072  if ( empty( $revIds ) ) {
2073  $result->setResult( true, [] );
2074  return $result;
2075  }
2076 
2077  // We need to set the `content` flag to join in content meta-data
2078  $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
2079  $revIdField = $slotQueryInfo['keys']['rev_id'];
2080  $slotQueryConds = [ $revIdField => $revIds ];
2081 
2082  if ( $readNew && isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2083  if ( empty( $options['slots'] ) ) {
2084  // Degenerate case: return no slots for each revision.
2085  $result->setResult( true, array_fill_keys( $revIds, [] ) );
2086  return $result;
2087  }
2088 
2089  $roleIdField = $slotQueryInfo['keys']['role_id'];
2090  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
2091  return $this->slotRoleStore->getId( $slot_name );
2092  }, $options['slots'] );
2093  }
2094 
2095  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2096  $slotRows = $db->select(
2097  $slotQueryInfo['tables'],
2098  $slotQueryInfo['fields'],
2099  $slotQueryConds,
2100  __METHOD__,
2101  [],
2102  $slotQueryInfo['joins']
2103  );
2104 
2105  $slotContents = null;
2106  if ( $options['blobs'] ?? false ) {
2107  $blobAddresses = [];
2108  foreach ( $slotRows as $slotRow ) {
2109  $blobAddresses[] = $slotRow->content_address;
2110  }
2111  $slotContentFetchStatus = $this->blobStore
2112  ->getBlobBatch( $blobAddresses, $queryFlags );
2113  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2114  $result->warning( $error['message'], ...$error['params'] );
2115  }
2116  $slotContents = $slotContentFetchStatus->getValue();
2117  }
2118 
2119  $slotRowsByRevId = [];
2120  foreach ( $slotRows as $slotRow ) {
2121  if ( $slotContents === null ) {
2122  // nothing to do
2123  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2124  $slotRow->blob_data = $slotContents[$slotRow->content_address];
2125  } else {
2126  $result->warning(
2127  'internalerror',
2128  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2129  );
2130  $slotRow->blob_data = null;
2131  }
2132 
2133  // conditional needed for SCHEMA_COMPAT_READ_OLD
2134  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2135  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2136  }
2137 
2138  // conditional needed for SCHEMA_COMPAT_READ_OLD
2139  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2140  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2141  }
2142 
2143  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2144  }
2145 
2146  $result->setResult( true, $slotRowsByRevId );
2147  return $result;
2148  }
2149 
2170  public function getContentBlobsForBatch(
2171  $rowsOrIds,
2172  $slots = null,
2173  $queryFlags = 0
2174  ) {
2175  $result = $this->getSlotRowsForBatch(
2176  $rowsOrIds,
2177  [ 'slots' => $slots, 'blobs' => true ],
2178  $queryFlags
2179  );
2180 
2181  if ( $result->isOK() ) {
2182  // strip out all internal meta data that we don't want to expose
2183  foreach ( $result->value as $revId => $rowsByRole ) {
2184  foreach ( $rowsByRole as $role => $slotRow ) {
2185  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2186  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2187  // if we didn't ask for it.
2188  unset( $result->value[$revId][$role] );
2189  continue;
2190  }
2191 
2192  $result->value[$revId][$role] = (object)[
2193  'blob_data' => $slotRow->blob_data,
2194  'model_name' => $slotRow->model_name,
2195  ];
2196  }
2197  }
2198  }
2199 
2200  return $result;
2201  }
2202 
2218  array $fields,
2219  $queryFlags = 0,
2220  Title $title = null
2221  ) {
2222  if ( !$title && isset( $fields['title'] ) ) {
2223  if ( !( $fields['title'] instanceof Title ) ) {
2224  throw new MWException( 'title field must contain a Title object.' );
2225  }
2226 
2227  $title = $fields['title'];
2228  }
2229 
2230  if ( !$title ) {
2231  $pageId = $fields['page'] ?? 0;
2232  $revId = $fields['id'] ?? 0;
2233 
2234  $title = $this->getTitle( $pageId, $revId, $queryFlags );
2235  }
2236 
2237  if ( !isset( $fields['page'] ) ) {
2238  $fields['page'] = $title->getArticleID( $queryFlags );
2239  }
2240 
2241  // if we have a content object, use it to set the model and type
2242  if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
2243  && !is_array( $fields['content'] )
2244  ) {
2245  throw new MWException(
2246  'content field must contain a Content object or an array of Content objects.'
2247  );
2248  }
2249 
2250  if ( !empty( $fields['text_id'] ) ) {
2251  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2252  throw new MWException( "The text_id field is only available in the pre-MCR schema" );
2253  }
2254 
2255  if ( !empty( $fields['content'] ) ) {
2256  throw new MWException(
2257  "Text already stored in external store (id {$fields['text_id']}), " .
2258  "can't specify content object"
2259  );
2260  }
2261  }
2262 
2263  if (
2264  isset( $fields['comment'] )
2265  && !( $fields['comment'] instanceof CommentStoreComment )
2266  ) {
2267  $commentData = $fields['comment_data'] ?? null;
2268 
2269  if ( $fields['comment'] instanceof Message ) {
2270  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2271  $fields['comment'],
2272  $commentData
2273  );
2274  } else {
2275  $commentText = trim( strval( $fields['comment'] ) );
2276  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2277  $commentText,
2278  $commentData
2279  );
2280  }
2281  }
2282 
2283  $revision = new MutableRevisionRecord( $title, $this->dbDomain );
2284  $this->initializeMutableRevisionFromArray( $revision, $fields );
2285 
2286  if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
2287  // @phan-suppress-next-line PhanTypeNoPropertiesForeach
2288  foreach ( $fields['content'] as $role => $content ) {
2289  $revision->setContent( $role, $content );
2290  }
2291  } else {
2292  $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
2293  $revision->setSlot( $mainSlot );
2294  }
2295 
2296  return $revision;
2297  }
2298 
2304  MutableRevisionRecord $record,
2305  array $fields
2306  ) {
2308  $user = null;
2309 
2310  // If a user is passed in, use it if possible. We cannot use a user from a
2311  // remote wiki with unsuppressed ids, due to issues described in T222212.
2312  if ( isset( $fields['user'] ) &&
2313  ( $fields['user'] instanceof UserIdentity ) &&
2314  ( $this->dbDomain === false ||
2315  ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
2316  ) {
2317  $user = $fields['user'];
2318  } else {
2319  try {
2320  $user = User::newFromAnyId(
2321  $fields['user'] ?? null,
2322  $fields['user_text'] ?? null,
2323  $fields['actor'] ?? null,
2324  $this->dbDomain
2325  );
2326  } catch ( InvalidArgumentException $ex ) {
2327  $user = null;
2328  }
2329  }
2330 
2331  if ( $user ) {
2332  $record->setUser( $user );
2333  }
2334 
2335  $timestamp = isset( $fields['timestamp'] )
2336  ? strval( $fields['timestamp'] )
2337  : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
2338 
2339  $record->setTimestamp( $timestamp );
2340 
2341  if ( isset( $fields['page'] ) ) {
2342  $record->setPageId( intval( $fields['page'] ) );
2343  }
2344 
2345  if ( isset( $fields['id'] ) ) {
2346  $record->setId( intval( $fields['id'] ) );
2347  }
2348  if ( isset( $fields['parent_id'] ) ) {
2349  $record->setParentId( intval( $fields['parent_id'] ) );
2350  }
2351 
2352  if ( isset( $fields['sha1'] ) ) {
2353  $record->setSha1( $fields['sha1'] );
2354  }
2355  if ( isset( $fields['size'] ) ) {
2356  $record->setSize( intval( $fields['size'] ) );
2357  }
2358 
2359  if ( isset( $fields['minor_edit'] ) ) {
2360  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2361  }
2362  if ( isset( $fields['deleted'] ) ) {
2363  $record->setVisibility( intval( $fields['deleted'] ) );
2364  }
2365 
2366  if ( isset( $fields['comment'] ) ) {
2367  Assert::parameterType(
2368  CommentStoreComment::class,
2369  $fields['comment'],
2370  '$row[\'comment\']'
2371  );
2372  $record->setComment( $fields['comment'] );
2373  }
2374  }
2375 
2390  public function loadRevisionFromId( IDatabase $db, $id ) {
2391  return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
2392  }
2393 
2409  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2410  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2411  if ( $id ) {
2412  $conds['rev_id'] = intval( $id );
2413  } else {
2414  $conds[] = 'rev_id=page_latest';
2415  }
2416  return $this->loadRevisionFromConds( $db, $conds );
2417  }
2418 
2435  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2436  if ( $id ) {
2437  $matchId = intval( $id );
2438  } else {
2439  $matchId = 'page_latest';
2440  }
2441 
2442  return $this->loadRevisionFromConds(
2443  $db,
2444  [
2445  "rev_id=$matchId",
2446  'page_namespace' => $title->getNamespace(),
2447  'page_title' => $title->getDBkey()
2448  ],
2449  0,
2450  $title
2451  );
2452  }
2453 
2469  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2470  return $this->loadRevisionFromConds( $db,
2471  [
2472  'rev_timestamp' => $db->timestamp( $timestamp ),
2473  'page_namespace' => $title->getNamespace(),
2474  'page_title' => $title->getDBkey()
2475  ],
2476  0,
2477  $title
2478  );
2479  }
2480 
2496  private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
2497  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2498  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
2499 
2500  $lb = $this->getDBLoadBalancer();
2501 
2502  // Make sure new pending/committed revision are visibile later on
2503  // within web requests to certain avoid bugs like T93866 and T94407.
2504  if ( !$rev
2505  && !( $flags & self::READ_LATEST )
2506  && $lb->hasStreamingReplicaServers()
2507  && $lb->hasOrMadeRecentMasterChanges()
2508  ) {
2509  $flags = self::READ_LATEST;
2510  $dbw = $this->getDBConnectionRef( DB_MASTER );
2511  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
2512  }
2513 
2514  return $rev;
2515  }
2516 
2530  private function loadRevisionFromConds(
2531  IDatabase $db,
2532  $conditions,
2533  $flags = 0,
2534  Title $title = null
2535  ) {
2536  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
2537  if ( $row ) {
2538  $rev = $this->newRevisionFromRow( $row, $flags, $title );
2539 
2540  return $rev;
2541  }
2542 
2543  return null;
2544  }
2545 
2553  private function checkDatabaseDomain( IDatabase $db ) {
2554  $dbDomain = $db->getDomainID();
2555  $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2556  if ( $dbDomain === $storeDomain ) {
2557  return;
2558  }
2559 
2560  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2561  }
2562 
2575  private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
2576  $this->checkDatabaseDomain( $db );
2577 
2578  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2579  $options = [];
2580  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2581  $options[] = 'FOR UPDATE';
2582  }
2583  return $db->selectRow(
2584  $revQuery['tables'],
2585  $revQuery['fields'],
2586  $conditions,
2587  __METHOD__,
2588  $options,
2589  $revQuery['joins']
2590  );
2591  }
2592 
2607  private function findSlotContentId( IDatabase $db, $revId, $role ) {
2608  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
2609  return null;
2610  }
2611 
2612  try {
2613  $roleId = $this->slotRoleStore->getId( $role );
2614  $conditions = [
2615  'slot_revision_id' => $revId,
2616  'slot_role_id' => $roleId,
2617  ];
2618 
2619  $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
2620 
2621  return $contentId ?: null;
2622  } catch ( NameTableAccessException $ex ) {
2623  // If the role is missing from the slot_roles table,
2624  // the corresponding row in slots cannot exist.
2625  return null;
2626  }
2627  }
2628 
2653  public function getQueryInfo( $options = [] ) {
2654  $ret = [
2655  'tables' => [],
2656  'fields' => [],
2657  'joins' => [],
2658  ];
2659 
2660  $ret['tables'][] = 'revision';
2661  $ret['fields'] = array_merge( $ret['fields'], [
2662  'rev_id',
2663  'rev_page',
2664  'rev_timestamp',
2665  'rev_minor_edit',
2666  'rev_deleted',
2667  'rev_len',
2668  'rev_parent_id',
2669  'rev_sha1',
2670  ] );
2671 
2672  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2673  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2674  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2675  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2676 
2677  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2678  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2679  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2680  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2681 
2682  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2683  $ret['fields'][] = 'rev_text_id';
2684 
2685  if ( $this->contentHandlerUseDB ) {
2686  $ret['fields'][] = 'rev_content_format';
2687  $ret['fields'][] = 'rev_content_model';
2688  }
2689  }
2690 
2691  if ( in_array( 'page', $options, true ) ) {
2692  $ret['tables'][] = 'page';
2693  $ret['fields'] = array_merge( $ret['fields'], [
2694  'page_namespace',
2695  'page_title',
2696  'page_id',
2697  'page_latest',
2698  'page_is_redirect',
2699  'page_len',
2700  ] );
2701  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2702  }
2703 
2704  if ( in_array( 'user', $options, true ) ) {
2705  $ret['tables'][] = 'user';
2706  $ret['fields'] = array_merge( $ret['fields'], [
2707  'user_name',
2708  ] );
2709  $u = $actorQuery['fields']['rev_user'];
2710  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2711  }
2712 
2713  if ( in_array( 'text', $options, true ) ) {
2714  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
2715  throw new InvalidArgumentException( 'text table can no longer be joined directly' );
2716  } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2717  // NOTE: even when this class is set to not read from the old schema, callers
2718  // should still be able to join against the text table, as long as we are still
2719  // writing the old schema for compatibility.
2720  wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
2721  }
2722 
2723  $ret['tables'][] = 'text';
2724  $ret['fields'] = array_merge( $ret['fields'], [
2725  'old_text',
2726  'old_flags'
2727  ] );
2728  $ret['joins']['text'] = [ 'JOIN', [ 'rev_text_id=old_id' ] ];
2729  }
2730 
2731  return $ret;
2732  }
2733 
2754  public function getSlotsQueryInfo( $options = [] ) {
2755  $ret = [
2756  'tables' => [],
2757  'fields' => [],
2758  'joins' => [],
2759  'keys' => [],
2760  ];
2761 
2762  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2763  $db = $this->getDBConnectionRef( DB_REPLICA );
2764  $ret['keys']['rev_id'] = 'rev_id';
2765 
2766  $ret['tables'][] = 'revision';
2767 
2768  $ret['fields']['slot_revision_id'] = 'rev_id';
2769  $ret['fields']['slot_content_id'] = 'NULL';
2770  $ret['fields']['slot_origin'] = 'rev_id';
2771  $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
2772 
2773  if ( in_array( 'content', $options, true ) ) {
2774  $ret['fields']['content_size'] = 'rev_len';
2775  $ret['fields']['content_sha1'] = 'rev_sha1';
2776  $ret['fields']['content_address']
2777  = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'rev_text_id' ] );
2778 
2779  // Allow the content_id field to be emulated later
2780  $ret['fields']['rev_text_id'] = 'rev_text_id';
2781 
2782  if ( $this->contentHandlerUseDB ) {
2783  $ret['fields']['model_name'] = 'rev_content_model';
2784  } else {
2785  $ret['fields']['model_name'] = 'NULL';
2786  }
2787  }
2788  } else {
2789  $ret['keys']['rev_id'] = 'slot_revision_id';
2790  $ret['keys']['role_id'] = 'slot_role_id';
2791 
2792  $ret['tables'][] = 'slots';
2793  $ret['fields'] = array_merge( $ret['fields'], [
2794  'slot_revision_id',
2795  'slot_content_id',
2796  'slot_origin',
2797  'slot_role_id',
2798  ] );
2799 
2800  if ( in_array( 'role', $options, true ) ) {
2801  // Use left join to attach role name, so we still find the revision row even
2802  // if the role name is missing. This triggers a more obvious failure mode.
2803  $ret['tables'][] = 'slot_roles';
2804  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2805  $ret['fields'][] = 'role_name';
2806  }
2807 
2808  if ( in_array( 'content', $options, true ) ) {
2809  $ret['keys']['model_id'] = 'content_model';
2810 
2811  $ret['tables'][] = 'content';
2812  $ret['fields'] = array_merge( $ret['fields'], [
2813  'content_size',
2814  'content_sha1',
2815  'content_address',
2816  'content_model',
2817  ] );
2818  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2819 
2820  if ( in_array( 'model', $options, true ) ) {
2821  // Use left join to attach model name, so we still find the revision row even
2822  // if the model name is missing. This triggers a more obvious failure mode.
2823  $ret['tables'][] = 'content_models';
2824  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2825  $ret['fields'][] = 'model_name';
2826  }
2827 
2828  }
2829  }
2830 
2831  return $ret;
2832  }
2833 
2847  public function getArchiveQueryInfo() {
2848  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2849  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2850  $ret = [
2851  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2852  'fields' => [
2853  'ar_id',
2854  'ar_page_id',
2855  'ar_namespace',
2856  'ar_title',
2857  'ar_rev_id',
2858  'ar_timestamp',
2859  'ar_minor_edit',
2860  'ar_deleted',
2861  'ar_len',
2862  'ar_parent_id',
2863  'ar_sha1',
2864  ] + $commentQuery['fields'] + $actorQuery['fields'],
2865  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2866  ];
2867 
2868  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2869  $ret['fields'][] = 'ar_text_id';
2870 
2871  if ( $this->contentHandlerUseDB ) {
2872  $ret['fields'][] = 'ar_content_format';
2873  $ret['fields'][] = 'ar_content_model';
2874  }
2875  }
2876 
2877  return $ret;
2878  }
2879 
2889  public function getRevisionSizes( array $revIds ) {
2890  return $this->listRevisionSizes( $this->getDBConnectionRef( DB_REPLICA ), $revIds );
2891  }
2892 
2905  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2906  $this->checkDatabaseDomain( $db );
2907 
2908  $revLens = [];
2909  if ( !$revIds ) {
2910  return $revLens; // empty
2911  }
2912 
2913  $res = $db->select(
2914  'revision',
2915  [ 'rev_id', 'rev_len' ],
2916  [ 'rev_id' => $revIds ],
2917  __METHOD__
2918  );
2919 
2920  foreach ( $res as $row ) {
2921  $revLens[$row->rev_id] = intval( $row->rev_len );
2922  }
2923 
2924  return $revLens;
2925  }
2926 
2935  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2936  $op = $dir === 'next' ? '>' : '<';
2937  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2938 
2939  if ( !$rev->getId() || !$rev->getPageId() ) {
2940  // revision is unsaved or otherwise incomplete
2941  return null;
2942  }
2943 
2944  if ( $rev instanceof RevisionArchiveRecord ) {
2945  // revision is deleted, so it's not part of the page history
2946  return null;
2947  }
2948 
2949  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2950  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2951 
2952  $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2953  if ( $ts === false ) {
2954  // XXX Should this be moved into getTimestampFromId?
2955  $ts = $db->selectField( 'archive', 'ar_timestamp',
2956  [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2957  if ( $ts === false ) {
2958  // XXX Is this reachable? How can we have a page id but no timestamp?
2959  return null;
2960  }
2961  }
2962  $ts = $db->addQuotes( $db->timestamp( $ts ) );
2963 
2964  $revId = $db->selectField( 'revision', 'rev_id',
2965  [
2966  'rev_page' => $rev->getPageId(),
2967  "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})"
2968  ],
2969  __METHOD__,
2970  [
2971  'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
2972  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2973  ]
2974  );
2975 
2976  if ( $revId === false ) {
2977  return null;
2978  }
2979 
2980  return $this->getRevisionById( intval( $revId ) );
2981  }
2982 
2998  public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
2999  if ( $flags instanceof Title ) {
3000  // Old calling convention, we don't use Title here anymore
3001  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
3002  $flags = 0;
3003  }
3004 
3005  return $this->getRelativeRevision( $rev, $flags, 'prev' );
3006  }
3007 
3021  public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
3022  if ( $flags instanceof Title ) {
3023  // Old calling convention, we don't use Title here anymore
3024  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
3025  $flags = 0;
3026  }
3027 
3028  return $this->getRelativeRevision( $rev, $flags, 'next' );
3029  }
3030 
3042  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
3043  $this->checkDatabaseDomain( $db );
3044 
3045  if ( $rev->getPageId() === null ) {
3046  return 0;
3047  }
3048  # Use page_latest if ID is not given
3049  if ( !$rev->getId() ) {
3050  $prevId = $db->selectField(
3051  'page', 'page_latest',
3052  [ 'page_id' => $rev->getPageId() ],
3053  __METHOD__
3054  );
3055  } else {
3056  $prevId = $db->selectField(
3057  'revision', 'rev_id',
3058  [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
3059  __METHOD__,
3060  [ 'ORDER BY' => 'rev_id DESC' ]
3061  );
3062  }
3063  return intval( $prevId );
3064  }
3065 
3078  public function getTimestampFromId( $id, $flags = 0 ) {
3079  if ( $id instanceof Title ) {
3080  // Old deprecated calling convention supported for backwards compatibility
3081  $id = $flags;
3082  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
3083  }
3084  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3085 
3086  $timestamp =
3087  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
3088 
3089  return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
3090  }
3091 
3101  public function countRevisionsByPageId( IDatabase $db, $id ) {
3102  $this->checkDatabaseDomain( $db );
3103 
3104  $row = $db->selectRow( 'revision',
3105  [ 'revCount' => 'COUNT(*)' ],
3106  [ 'rev_page' => $id ],
3107  __METHOD__
3108  );
3109  if ( $row ) {
3110  return intval( $row->revCount );
3111  }
3112  return 0;
3113  }
3114 
3124  public function countRevisionsByTitle( IDatabase $db, $title ) {
3125  $id = $title->getArticleID();
3126  if ( $id ) {
3127  return $this->countRevisionsByPageId( $db, $id );
3128  }
3129  return 0;
3130  }
3131 
3150  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
3151  $this->checkDatabaseDomain( $db );
3152 
3153  if ( !$userId ) {
3154  return false;
3155  }
3156 
3157  $revQuery = $this->getQueryInfo();
3158  $res = $db->select(
3159  $revQuery['tables'],
3160  [
3161  'rev_user' => $revQuery['fields']['rev_user'],
3162  ],
3163  [
3164  'rev_page' => $pageId,
3165  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
3166  ],
3167  __METHOD__,
3168  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
3169  $revQuery['joins']
3170  );
3171  foreach ( $res as $row ) {
3172  if ( $row->rev_user != $userId ) {
3173  return false;
3174  }
3175  }
3176  return true;
3177  }
3178 
3192  public function getKnownCurrentRevision( Title $title, $revId ) {
3193  $db = $this->getDBConnectionRef( DB_REPLICA );
3194 
3195  $pageId = $title->getArticleID();
3196 
3197  if ( !$pageId ) {
3198  return false;
3199  }
3200 
3201  if ( !$revId ) {
3202  $revId = $title->getLatestRevID();
3203  }
3204 
3205  if ( !$revId ) {
3206  wfWarn(
3207  'No latest revision known for page ' . $title->getPrefixedDBkey()
3208  . ' even though it exists with page ID ' . $pageId
3209  );
3210  return false;
3211  }
3212 
3213  // Load the row from cache if possible. If not possible, populate the cache.
3214  // As a minor optimization, remember if this was a cache hit or miss.
3215  // We can sometimes avoid a database query later if this is a cache miss.
3216  $fromCache = true;
3217  $row = $this->cache->getWithSetCallback(
3218  // Page/rev IDs passed in from DB to reflect history merges
3219  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
3221  function ( $curValue, &$ttl, array &$setOpts ) use (
3222  $db, $pageId, $revId, &$fromCache
3223  ) {
3224  $setOpts += Database::getCacheSetOptions( $db );
3225  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
3226  if ( $row ) {
3227  $fromCache = false;
3228  }
3229  return $row; // don't cache negatives
3230  }
3231  );
3232 
3233  // Reflect revision deletion and user renames.
3234  if ( $row ) {
3235  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
3236  } else {
3237  return false;
3238  }
3239  }
3240 
3252  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3253  return $this->cache->makeGlobalKey(
3254  self::ROW_CACHE_KEY,
3255  $db->getDomainID(),
3256  $pageId,
3257  $revId
3258  );
3259  }
3260 
3261  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3262 
3263 }
3264 
3269 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:264
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
ActorMigration $actorMigration
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
failOnEmpty( $value, $name)
const SCHEMA_COMPAT_READ_BOTH
Definition: Defines.php:269
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot...
Definition: SlotRecord.php:164
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save...
isValid()
Returns whether the content is valid.
isMinor()
MCR migration note: this replaces Revision::isMinor.
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3166
static toHex( $ip)
Return a zero-padded upper case hexadecimal representation of an IP address.
Definition: IP.php:404
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob...
Definition: BlobStore.php:78
A RevisionRecord representing an existing revision persisted in the revision table.
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:467
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there&#39;s one.
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
NameTableStore $slotRoleStore
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
NameTableStore $contentModelStore
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ &#39;page&#39;, &#39;user&#39; ] ) Caching rows w...
userWasLastToEdit(IDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page...
The Message class provides methods which fulfil two basic services:
Definition: Message.php:162
getKnownCurrentRevision(Title $title, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
failOnNull( $value, $name)
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
static makeAddressFromTextId( $id)
Returns an address referring to content stored in the text table row with the given ID...
setLogger(LoggerInterface $logger)
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:596
static mapArchiveFields( $archiveRow)
Maps fields of the archive row to corresponding revision rows.
$sort
static getDefaultModelFor(Title $title)
Returns the name of the default content model to be used for the page with the given title...
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page...
assertCrossWikiContentLoadingIsSafe()
Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading and still...
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
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:267
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with. ...
Definition: BlobStore.php:48
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
newRevisionFromRow( $row, $queryFlags=0, Title $title=null, $fromCache=false)
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
getDBConnectionRefForQueryFlags( $queryFlags)
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, $fromCache=false)
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:518
CommentStore $commentStore
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
getDBConnectionRef( $mode, $groups=[])
const DB_MASTER
Definition: defines.php:26
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2229
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
getModel()
Returns the content model.
Definition: SlotRecord.php:566
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
Created by PhpStorm.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
newMutableRevisionFromArray(array $fields, $queryFlags=0, Title $title=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1...
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, Title $title)
Factory method for RevisionSlots based on a revision ID.
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
setSize( $size)
Set nominal revision size, for optimization.
emulateMainSlot_1_29( $row, $queryFlags, Title $title)
Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page&#39;s history.
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
getVisibility()
Get the deletion bitfield of the revision.
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition: BlobStore.php:66
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot&#39;s content.
Definition: SlotRecord.php:446
setComment(CommentStoreComment $comment)
loadRevisionFromId(IDatabase $db, $id)
Load a page revision from a given revision ID number.
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given page ID...
loadSlotRecords( $revId, $queryFlags, Title $title)
Service for looking up page revisions.
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:266
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
fetchRevisionRowFromConds(IDatabase $db, $conditions, $flags=0)
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects...
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with. ...
Definition: BlobStore.php:54
newRevisionFromArchiveRow( $row, $queryFlags=0, Title $title=null, array $overrides=[])
Make a fake revision object from an archive table row.
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
SlotRoleRegistry $slotRoleRegistry
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition: Title.php:492
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Service for looking up page revisions.
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
static hasFlags( $bitfield, $flags)
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
getNextRevision(RevisionRecord $rev, $flags=0)
Get the revision after $rev in the page&#39;s history, if any.
A RevisionRecord representing a revision of a deleted page persisted in the archive table...
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
newRevisionFromConds( $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision.
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:469
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision, and inherits all slots form it.
loadRevisionFromPageId(IDatabase $db, $pageid, $id=0)
Load either the current, or a specified, revision that&#39;s attached to a given page.
setSha1( $sha1)
Set revision hash, for optimization.
getOrigin()
Returns the revision ID of the revision that originated the slot&#39;s content.
Definition: SlotRecord.php:405
updateRevisionTextId(IDatabase $dbw, $revisionId, &$blobAddress)
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
const PRC_UNPATROLLED
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:271
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, $mcrMigrationStage, ActorMigration $actorMigration, $dbDomain=false)
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
Service for constructing revision objects.
getRevisionByTimestamp( $title, $timestamp)
Load the revision for the given title with the given timestamp.
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
getId()
Get revision ID.
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:489
getSha1()
Returns the content size.
Definition: SlotRecord.php:538
getId()
Get the user&#39;s ID.
Definition: User.php:2200
setId( $id)
Set the revision ID.
getSha1()
Returns the base36 sha1 of this revision.
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:513
setContentHandlerUseDB( $contentHandlerUseDB)
getSize()
Returns the content size.
Definition: SlotRecord.php:522
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
$revQuery
getAddress()
Returns the address of this slot&#39;s content.
Definition: SlotRecord.php:499
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
Exception representing a failure to look up a revision.
ILoadBalancer $loadBalancer
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
getLatestRevID( $flags=0)
What is the page_latest field for this page?
Definition: Title.php:3247
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that&#39;s attached to a given page.
findSlotContentId(IDatabase $db, $revId, $role)
Finds the ID of a content row for a given revision and slot role.
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
getPreviousRevision(RevisionRecord $rev, $flags=0)
Get the revision before $rev in the page&#39;s history, if any.
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4432
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application...
Definition: BlobStore.php:42
getSize()
Returns the nominal size of this revision, in bogo-bytes.
Page revision base class.
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:435
const DB_REPLICA
Definition: defines.php:25
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision&#39;s author&#39;s user identity, if it&#39;s available to the specified audience.
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
$content
Definition: router.php:78
const SCHEMA_COMPAT_READ_OLD
Definition: Defines.php:265
Value object representing the set of slots belonging to a revision.
getModel()
Returns the ID of the content model used by this Content object.
loadRevisionFromConds(IDatabase $db, $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision from the given database connection.
loadSlotContent(SlotRecord $slot, $blobData=null, $blobFlags=null, $blobFormat=null, $queryFlags=0)
Loads a Content object based on a slot row.
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it&#39;s available to the specified audience.
emulateContentId( $textId)
Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode, based on the revisi...
int $mcrMigrationStage
An appropriate combination of SCHEMA_COMPAT_XXX flags.
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition: BlobStore.php:60
getParentId()
Get parent revision ID (the original previous page revision).
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
LoggerInterface $logger
getDefaultFormat()
Convenience method that returns the default serialization format for the content model that this Cont...
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
getPageId()
Get the page ID.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
newRevisionsFromBatch( $rows, array $options=[], $queryFlags=0, Title $title=null)
Construct a RevisionRecord instance for each row in $rows, and return them as an associative array in...
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1846
const FORMAT_HINT
Hint key for use with storeBlob, indicating the serialization format used to create the blob...
Definition: BlobStore.php:84