MediaWiki  1.34.0
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;
37 use InvalidArgumentException;
38 use IP;
39 use LogicException;
48 use Message;
49 use MWException;
51 use Psr\Log\LoggerAwareInterface;
52 use Psr\Log\LoggerInterface;
53 use Psr\Log\NullLogger;
54 use RecentChange;
55 use Revision;
56 use RuntimeException;
57 use StatusValue;
58 use stdClass;
59 use Title;
60 use Traversable;
61 use User;
62 use WANObjectCache;
63 use Wikimedia\Assert\Assert;
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(
176  '$mcrMigrationStage',
177  'Reading from the old and the new schema at the same time is not supported.'
178  );
179  Assert::parameter(
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  // TODO: This should trigger a deprecation warning eventually (T200918), but not
2721  // before all known usages are removed (see T198341 and T201164).
2722  // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
2723  }
2724 
2725  $ret['tables'][] = 'text';
2726  $ret['fields'] = array_merge( $ret['fields'], [
2727  'old_text',
2728  'old_flags'
2729  ] );
2730  $ret['joins']['text'] = [ 'JOIN', [ 'rev_text_id=old_id' ] ];
2731  }
2732 
2733  return $ret;
2734  }
2735 
2756  public function getSlotsQueryInfo( $options = [] ) {
2757  $ret = [
2758  'tables' => [],
2759  'fields' => [],
2760  'joins' => [],
2761  'keys' => [],
2762  ];
2763 
2764  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2765  $db = $this->getDBConnectionRef( DB_REPLICA );
2766  $ret['keys']['rev_id'] = 'rev_id';
2767 
2768  $ret['tables'][] = 'revision';
2769 
2770  $ret['fields']['slot_revision_id'] = 'rev_id';
2771  $ret['fields']['slot_content_id'] = 'NULL';
2772  $ret['fields']['slot_origin'] = 'rev_id';
2773  $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
2774 
2775  if ( in_array( 'content', $options, true ) ) {
2776  $ret['fields']['content_size'] = 'rev_len';
2777  $ret['fields']['content_sha1'] = 'rev_sha1';
2778  $ret['fields']['content_address']
2779  = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'rev_text_id' ] );
2780 
2781  // Allow the content_id field to be emulated later
2782  $ret['fields']['rev_text_id'] = 'rev_text_id';
2783 
2784  if ( $this->contentHandlerUseDB ) {
2785  $ret['fields']['model_name'] = 'rev_content_model';
2786  } else {
2787  $ret['fields']['model_name'] = 'NULL';
2788  }
2789  }
2790  } else {
2791  $ret['keys']['rev_id'] = 'slot_revision_id';
2792  $ret['keys']['role_id'] = 'slot_role_id';
2793 
2794  $ret['tables'][] = 'slots';
2795  $ret['fields'] = array_merge( $ret['fields'], [
2796  'slot_revision_id',
2797  'slot_content_id',
2798  'slot_origin',
2799  'slot_role_id',
2800  ] );
2801 
2802  if ( in_array( 'role', $options, true ) ) {
2803  // Use left join to attach role name, so we still find the revision row even
2804  // if the role name is missing. This triggers a more obvious failure mode.
2805  $ret['tables'][] = 'slot_roles';
2806  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2807  $ret['fields'][] = 'role_name';
2808  }
2809 
2810  if ( in_array( 'content', $options, true ) ) {
2811  $ret['keys']['model_id'] = 'content_model';
2812 
2813  $ret['tables'][] = 'content';
2814  $ret['fields'] = array_merge( $ret['fields'], [
2815  'content_size',
2816  'content_sha1',
2817  'content_address',
2818  'content_model',
2819  ] );
2820  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2821 
2822  if ( in_array( 'model', $options, true ) ) {
2823  // Use left join to attach model name, so we still find the revision row even
2824  // if the model name is missing. This triggers a more obvious failure mode.
2825  $ret['tables'][] = 'content_models';
2826  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2827  $ret['fields'][] = 'model_name';
2828  }
2829 
2830  }
2831  }
2832 
2833  return $ret;
2834  }
2835 
2849  public function getArchiveQueryInfo() {
2850  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2851  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2852  $ret = [
2853  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2854  'fields' => [
2855  'ar_id',
2856  'ar_page_id',
2857  'ar_namespace',
2858  'ar_title',
2859  'ar_rev_id',
2860  'ar_timestamp',
2861  'ar_minor_edit',
2862  'ar_deleted',
2863  'ar_len',
2864  'ar_parent_id',
2865  'ar_sha1',
2866  ] + $commentQuery['fields'] + $actorQuery['fields'],
2867  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2868  ];
2869 
2870  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2871  $ret['fields'][] = 'ar_text_id';
2872 
2873  if ( $this->contentHandlerUseDB ) {
2874  $ret['fields'][] = 'ar_content_format';
2875  $ret['fields'][] = 'ar_content_model';
2876  }
2877  }
2878 
2879  return $ret;
2880  }
2881 
2891  public function getRevisionSizes( array $revIds ) {
2892  return $this->listRevisionSizes( $this->getDBConnectionRef( DB_REPLICA ), $revIds );
2893  }
2894 
2907  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2908  $this->checkDatabaseDomain( $db );
2909 
2910  $revLens = [];
2911  if ( !$revIds ) {
2912  return $revLens; // empty
2913  }
2914 
2915  $res = $db->select(
2916  'revision',
2917  [ 'rev_id', 'rev_len' ],
2918  [ 'rev_id' => $revIds ],
2919  __METHOD__
2920  );
2921 
2922  foreach ( $res as $row ) {
2923  $revLens[$row->rev_id] = intval( $row->rev_len );
2924  }
2925 
2926  return $revLens;
2927  }
2928 
2937  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2938  $op = $dir === 'next' ? '>' : '<';
2939  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2940 
2941  if ( !$rev->getId() || !$rev->getPageId() ) {
2942  // revision is unsaved or otherwise incomplete
2943  return null;
2944  }
2945 
2946  if ( $rev instanceof RevisionArchiveRecord ) {
2947  // revision is deleted, so it's not part of the page history
2948  return null;
2949  }
2950 
2951  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2952  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2953 
2954  $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2955  if ( $ts === false ) {
2956  // XXX Should this be moved into getTimestampFromId?
2957  $ts = $db->selectField( 'archive', 'ar_timestamp',
2958  [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2959  if ( $ts === false ) {
2960  // XXX Is this reachable? How can we have a page id but no timestamp?
2961  return null;
2962  }
2963  }
2964  $ts = $db->addQuotes( $db->timestamp( $ts ) );
2965 
2966  $revId = $db->selectField( 'revision', 'rev_id',
2967  [
2968  'rev_page' => $rev->getPageId(),
2969  "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})"
2970  ],
2971  __METHOD__,
2972  [
2973  'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
2974  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2975  ]
2976  );
2977 
2978  if ( $revId === false ) {
2979  return null;
2980  }
2981 
2982  return $this->getRevisionById( intval( $revId ) );
2983  }
2984 
3000  public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
3001  if ( $flags instanceof Title ) {
3002  // Old calling convention, we don't use Title here anymore
3003  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
3004  $flags = 0;
3005  }
3006 
3007  return $this->getRelativeRevision( $rev, $flags, 'prev' );
3008  }
3009 
3023  public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
3024  if ( $flags instanceof Title ) {
3025  // Old calling convention, we don't use Title here anymore
3026  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
3027  $flags = 0;
3028  }
3029 
3030  return $this->getRelativeRevision( $rev, $flags, 'next' );
3031  }
3032 
3044  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
3045  $this->checkDatabaseDomain( $db );
3046 
3047  if ( $rev->getPageId() === null ) {
3048  return 0;
3049  }
3050  # Use page_latest if ID is not given
3051  if ( !$rev->getId() ) {
3052  $prevId = $db->selectField(
3053  'page', 'page_latest',
3054  [ 'page_id' => $rev->getPageId() ],
3055  __METHOD__
3056  );
3057  } else {
3058  $prevId = $db->selectField(
3059  'revision', 'rev_id',
3060  [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
3061  __METHOD__,
3062  [ 'ORDER BY' => 'rev_id DESC' ]
3063  );
3064  }
3065  return intval( $prevId );
3066  }
3067 
3080  public function getTimestampFromId( $id, $flags = 0 ) {
3081  if ( $id instanceof Title ) {
3082  // Old deprecated calling convention supported for backwards compatibility
3083  $id = $flags;
3084  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
3085  }
3086  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3087 
3088  $timestamp =
3089  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
3090 
3091  return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
3092  }
3093 
3103  public function countRevisionsByPageId( IDatabase $db, $id ) {
3104  $this->checkDatabaseDomain( $db );
3105 
3106  $row = $db->selectRow( 'revision',
3107  [ 'revCount' => 'COUNT(*)' ],
3108  [ 'rev_page' => $id ],
3109  __METHOD__
3110  );
3111  if ( $row ) {
3112  return intval( $row->revCount );
3113  }
3114  return 0;
3115  }
3116 
3126  public function countRevisionsByTitle( IDatabase $db, $title ) {
3127  $id = $title->getArticleID();
3128  if ( $id ) {
3129  return $this->countRevisionsByPageId( $db, $id );
3130  }
3131  return 0;
3132  }
3133 
3152  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
3153  $this->checkDatabaseDomain( $db );
3154 
3155  if ( !$userId ) {
3156  return false;
3157  }
3158 
3159  $revQuery = $this->getQueryInfo();
3160  $res = $db->select(
3161  $revQuery['tables'],
3162  [
3163  'rev_user' => $revQuery['fields']['rev_user'],
3164  ],
3165  [
3166  'rev_page' => $pageId,
3167  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
3168  ],
3169  __METHOD__,
3170  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
3171  $revQuery['joins']
3172  );
3173  foreach ( $res as $row ) {
3174  if ( $row->rev_user != $userId ) {
3175  return false;
3176  }
3177  }
3178  return true;
3179  }
3180 
3194  public function getKnownCurrentRevision( Title $title, $revId ) {
3195  $db = $this->getDBConnectionRef( DB_REPLICA );
3196 
3197  $pageId = $title->getArticleID();
3198 
3199  if ( !$pageId ) {
3200  return false;
3201  }
3202 
3203  if ( !$revId ) {
3204  $revId = $title->getLatestRevID();
3205  }
3206 
3207  if ( !$revId ) {
3208  wfWarn(
3209  'No latest revision known for page ' . $title->getPrefixedDBkey()
3210  . ' even though it exists with page ID ' . $pageId
3211  );
3212  return false;
3213  }
3214 
3215  // Load the row from cache if possible. If not possible, populate the cache.
3216  // As a minor optimization, remember if this was a cache hit or miss.
3217  // We can sometimes avoid a database query later if this is a cache miss.
3218  $fromCache = true;
3219  $row = $this->cache->getWithSetCallback(
3220  // Page/rev IDs passed in from DB to reflect history merges
3221  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
3223  function ( $curValue, &$ttl, array &$setOpts ) use (
3224  $db, $pageId, $revId, &$fromCache
3225  ) {
3226  $setOpts += Database::getCacheSetOptions( $db );
3227  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
3228  if ( $row ) {
3229  $fromCache = false;
3230  }
3231  return $row; // don't cache negatives
3232  }
3233  );
3234 
3235  // Reflect revision deletion and user renames.
3236  if ( $row ) {
3237  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
3238  } else {
3239  return false;
3240  }
3241  }
3242 
3254  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3255  return $this->cache->makeGlobalKey(
3256  self::ROW_CACHE_KEY,
3257  $db->getDomainID(),
3258  $pageId,
3259  $revId
3260  );
3261  }
3262 
3263  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3264 
3265 }
3266 
3271 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Revision\MutableRevisionRecord\setMinorEdit
setMinorEdit( $minorEdit)
Definition: MutableRevisionRecord.php:238
Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:113
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:32
Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:123
IP\toHex
static toHex( $ip)
Return a zero-padded upper case hexadecimal representation of an IP address.
Definition: IP.php:404
MediaWiki\Storage\BlobStore\PAGE_HINT
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with.
Definition: BlobStore.php:48
ContentHandler
A content handler knows how do deal with a specific type of content on a wiki page.
Definition: ContentHandler.php:55
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:254
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:49
Revision\RevisionStore\insertSlotOn
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:654
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:33
Revision\RevisionStore\checkDatabaseDomain
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
Definition: RevisionStore.php:2553
MediaWiki\Storage\BlobStore\DESIGNATION_HINT
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition: BlobStore.php:42
MediaWiki\Storage\BlobAccessException
Exception representing a failure to access a data blob.
Definition: BlobAccessException.php:32
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:42
Revision\RevisionStore\getRecentChange
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
Definition: RevisionStore.php:1133
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
SCHEMA_COMPAT_READ_NEW
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:267
Revision\RevisionStore\newRevisionFromConds
newRevisionFromConds( $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision.
Definition: RevisionStore.php:2496
Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:31
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
Revision\RevisionStore\loadRevisionFromConds
loadRevisionFromConds(IDatabase $db, $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision from the given database connection.
Definition: RevisionStore.php:2530
User\getId
getId()
Get the user's ID.
Definition: User.php:2203
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:435
Revision\RevisionStore\newMutableRevisionFromArray
newMutableRevisionFromArray(array $fields, $queryFlags=0, Title $title=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1....
Definition: RevisionStore.php:2217
Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:280
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:207
Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:402
Revision\MutableRevisionRecord\setSha1
setSha1( $sha1)
Set revision hash, for optimization.
Definition: MutableRevisionRecord.php:196
Revision\RevisionStore\emulateMainSlot_1_29
emulateMainSlot_1_29( $row, $queryFlags, Title $title)
Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
Definition: RevisionStore.php:1218
Revision\MutableRevisionRecord\setParentId
setParentId( $parentId)
Definition: MutableRevisionRecord.php:87
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:79
MediaWiki\Storage\SqlBlobStore
Service for storing and loading Content objects.
Definition: SqlBlobStore.php:51
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:70
Revision\RevisionStore\initializeMutableRevisionFromArray
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
Definition: RevisionStore.php:2303
Revision\RevisionStoreCacheRecord
A cached RevisionStoreRecord.
Definition: RevisionStoreCacheRecord.php:38
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1869
Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, Title $title)
Definition: RevisionStore.php:1591
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:446
Revision\RevisionStore\getSlotRowsForBatch
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
Definition: RevisionStore.php:2057
Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2849
Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:53
Revision\MutableRevisionRecord\setPageId
setPageId( $pageId)
Definition: MutableRevisionRecord.php:273
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:436
IP
A collection of public static functions to play with IP address and IP ranges.
Definition: IP.php:67
CommentStore
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
Definition: CommentStore.php:31
Revision\RevisionStore\$cache
WANObjectCache $cache
Definition: RevisionStore.php:108
Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:3080
Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:191
Revision\RevisionStore\getRcIdIfUnpatrolled
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
Definition: RevisionStore.php:1111
Revision\RevisionFactory
Service for constructing revision objects.
Definition: RevisionFactory.php:37
Revision\MutableRevisionRecord\setId
setId( $id)
Set the revision ID.
Definition: MutableRevisionRecord.php:255
Revision\RevisionStore\checkContent
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
Definition: RevisionStore.php:976
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
ActorMigration
This class handles the logic for the actor table migration.
Definition: ActorMigration.php:38
Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2756
Message
$res
$res
Definition: testCompression.php:52
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:118
$revQuery
$revQuery
Definition: testCompression.php:51
Revision\RevisionStore\insertIpChangesRow
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
Definition: RevisionStore.php:706
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
Revision\RevisionStore\mapArchiveFields
static mapArchiveFields( $archiveRow)
Maps fields of the archive row to corresponding revision rows.
Definition: RevisionStore.php:1168
Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Revision\RevisionStore\getDBConnectionRef
getDBConnectionRef( $mode, $groups=[])
Definition: RevisionStore.php:291
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Revision\RevisionStore\listRevisionSizes
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2907
Revision\RevisionStore\$mcrMigrationStage
int $mcrMigrationStage
An appropriate combination of SCHEMA_COMPAT_XXX flags.
Definition: RevisionStore.php:136
Revision\RevisionStore\userWasLastToEdit
userWasLastToEdit(IDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
Definition: RevisionStore.php:3152
$dbr
$dbr
Definition: testCompression.php:50
MediaWiki\Revision
Created by PhpStorm.
Definition: FallbackSlotRoleHandler.php:23
Revision
Definition: Revision.php:40
Revision\RevisionStore\newRevisionsFromBatch
newRevisionsFromBatch( $rows, array $options=[], $queryFlags=0, Title $title=null)
Construct a RevisionRecord instance for each row in $rows, and return them as an associative array in...
Definition: RevisionStore.php:1918
Revision\RevisionStore\loadSlotContent
loadSlotContent(SlotRecord $slot, $blobData=null, $blobFlags=null, $blobFormat=null, $queryFlags=0)
Loads a Content object based on a slot row.
Definition: RevisionStore.php:1412
ContentHandler\getDefaultModelFor
static getDefaultModelFor(Title $title)
Returns the name of the default content model to be used for the page with the given title.
Definition: ContentHandler.php:186
Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:405
Revision\RevisionStore\getQueryInfo
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
Definition: RevisionStore.php:2653
MWException
MediaWiki exception.
Definition: MWException.php:26
Revision\RevisionRecord\getSize
getSize()
Returns the nominal size of this revision, in bogo-bytes.
MediaWiki\Storage\BlobStore\SHA1_HINT
const SHA1_HINT
Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the method.
Definition: BlobStore.php:72
Revision\RevisionStore\getContentHandlerUseDB
getContentHandlerUseDB()
Definition: RevisionStore.php:246
Revision\MutableRevisionRecord\setUser
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
Definition: MutableRevisionRecord.php:266
Revision\RevisionRecord\getSha1
getSha1()
Returns the base36 sha1 of this revision.
Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:903
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Definition: GlobalFunctions.php:1044
Revision\RevisionStore\insertRevisionOn
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
Definition: RevisionStore.php:424
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
Wikimedia\Rdbms\Database\getCacheSetOptions
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4404
Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1469
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:518
Revision\RevisionStore\getRevisionByPageId
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
Definition: RevisionStore.php:1535
Revision\RevisionStore\loadRevisionFromPageId
loadRevisionFromPageId(IDatabase $db, $pageid, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
Definition: RevisionStore.php:2409
$blob
$blob
Definition: testCompression.php:65
Revision\RevisionRecord\getUser
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Definition: RevisionRecord.php:365
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:489
Revision\RevisionStore\__construct
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, $mcrMigrationStage, ActorMigration $actorMigration, $dbDomain=false)
Definition: RevisionStore.php:160
Revision\MutableRevisionRecord\setVisibility
setVisibility( $visibility)
Definition: MutableRevisionRecord.php:220
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:469
MediaWiki\Storage\BlobStore\MODEL_HINT
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob.
Definition: BlobStore.php:78
Revision\RevisionStore\newRevisionSlots
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, Title $title)
Factory method for RevisionSlots based on a revision ID.
Definition: RevisionStore.php:1699
Revision\RevisionStore\getPreviousRevisionId
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save.
Definition: RevisionStore.php:3044
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:403
Revision\RevisionStore\$dbDomain
bool string $dbDomain
Definition: RevisionStore.php:92
Revision\RevisionRecord\isReadyForInsertion
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
Definition: RevisionRecord.php:557
MediaWiki\Storage\SqlBlobStore\makeAddressFromTextId
static makeAddressFromTextId( $id)
Returns an address referring to content stored in the text table row with the given ID.
Definition: SqlBlobStore.php:694
Revision\RevisionStore\insertSlotRowOn
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
Definition: RevisionStore.php:937
Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2891
$t
$t
Definition: make-normalization-table.php:143
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:60
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:566
Revision\RevisionStore\loadRevisionFromId
loadRevisionFromId(IDatabase $db, $id)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:2390
$title
$title
Definition: testCompression.php:34
Revision\RevisionStore\getKnownCurrentRevision
getKnownCurrentRevision(Title $title, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: RevisionStore.php:3194
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:499
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1898
User\newFromAnyId
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:596
Revision\RevisionRecord\getSlotRoles
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
Definition: RevisionRecord.php:218
DB_MASTER
const DB_MASTER
Definition: defines.php:26
Revision\RevisionRecord\getPageId
getPageId()
Get the page ID.
Definition: RevisionRecord.php:325
Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:87
Revision\RevisionStore\getPreviousRevision
getPreviousRevision(RevisionRecord $rev, $flags=0)
Get the revision before $rev in the page's history, if any.
Definition: RevisionStore.php:3000
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
$sort
$sort
Definition: profileinfo.php:331
Revision\RevisionStore\setLogger
setLogger(LoggerInterface $logger)
Definition: RevisionStore.php:232
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:538
Revision\RevisionStore\constructSlotRecords
constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
Definition: RevisionStore.php:1625
Revision\RevisionStore\ROW_CACHE_KEY
const ROW_CACHE_KEY
Definition: RevisionStore.php:82
Revision\RevisionStore\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: RevisionStore.php:139
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:273
Revision\RevisionArchiveRecord
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Definition: RevisionArchiveRecord.php:40
Revision\RevisionStore\emulateContentId
emulateContentId( $textId)
Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode, based on the revisi...
Definition: RevisionStore.php:1386
Revision\RevisionRecord\getParentId
getParentId()
Get parent revision ID (the original previous page revision).
Definition: RevisionRecord.php:289
$content
$content
Definition: router.php:78
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:522
DBAccessObjectUtils\hasFlags
static hasFlags( $bitfield, $flags)
Definition: DBAccessObjectUtils.php:35
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:42
SCHEMA_COMPAT_WRITE_OLD
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:264
Revision\RevisionStore\getNextRevision
getNextRevision(RevisionRecord $rev, $flags=0)
Get the revision after $rev in the page's history, if any.
Definition: RevisionStore.php:3023
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:116
Revision\RevisionStore\$slotRoleStore
NameTableStore $slotRoleStore
Definition: RevisionStore.php:133
SCHEMA_COMPAT_WRITE_NEW
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:266
Revision\RevisionStore\getContentBlobsForBatch
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
Definition: RevisionStore.php:2170
Revision\RevisionStore\setContentHandlerUseDB
setContentHandlerUseDB( $contentHandlerUseDB)
Definition: RevisionStore.php:255
Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:3126
Revision\RevisionStore\hasMcrSchemaFlags
hasMcrSchemaFlags( $flags)
Definition: RevisionStore.php:214
Revision\RevisionRecord\getComment
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
Definition: RevisionRecord.php:390
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:36
Title\newFromIDs
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition: Title.php:492
Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:39
Revision\RevisionStore\getRelativeRevision
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
Definition: RevisionStore.php:2937
IP\isValid
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
Revision\RevisionStore\getRevisionByTitle
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: RevisionStore.php:1489
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:268
Revision\RevisionStore\assertCrossWikiContentLoadingIsSafe
assertCrossWikiContentLoadingIsSafe()
Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading and still...
Definition: RevisionStore.php:224
MediaWiki\Storage\BlobStore
Service for loading and storing data blobs.
Definition: BlobStore.php:35
Content
Base interface for content objects.
Definition: Content.php:34
IExpiringStore\TTL_WEEK
const TTL_WEEK
Definition: IExpiringStore.php:36
Revision\RevisionStore\newNullRevision
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page's history.
Definition: RevisionStore.php:1047
Wikimedia\Rdbms\DBConnRef
Helper class used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition: DBConnRef.php:29
Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:425
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:345
Title
Represents a title within MediaWiki.
Definition: Title.php:42
Revision\RevisionStore\insertContentRowOn
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
Definition: RevisionStore.php:955
Revision\RevisionStore\getRevisionByTimestamp
getRevisionByTimestamp( $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:1571
Revision\RevisionStore\$contentHandlerUseDB
boolean $contentHandlerUseDB
Definition: RevisionStore.php:98
Revision\RevisionStore\countRevisionsByPageId
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:3103
Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
Definition: RevisionStore.php:540
Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:128
Revision\SlotRecord\newSaved
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
Definition: SlotRecord.php:164
Revision\RevisionStore\fetchRevisionRowFromConds
fetchRevisionRowFromConds(IDatabase $db, $conditions, $flags=0)
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects.
Definition: RevisionStore.php:2575
Revision\RevisionStore\getDBLoadBalancer
getDBLoadBalancer()
Definition: RevisionStore.php:271
Revision\RevisionStore\newRevisionFromArchiveRow
newRevisionFromArchiveRow( $row, $queryFlags=0, Title $title=null, array $overrides=[])
Make a fake revision object from an archive table row.
Definition: RevisionStore.php:1743
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:79
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:32
Revision\RevisionSlots
Value object representing the set of slots belonging to a revision.
Definition: RevisionSlots.php:35
MediaWiki\Storage\BlobStore\REVISION_HINT
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition: BlobStore.php:60
wfBacktrace
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
Definition: GlobalFunctions.php:1417
MediaWiki\Storage\BlobStore\PARENT_HINT
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition: BlobStore.php:66
MediaWiki\Storage\BlobStore\ROLE_HINT
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with.
Definition: BlobStore.php:54
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
MWUnknownContentModelException
Exception thrown when an unregistered content model is requested.
Definition: MWUnknownContentModelException.php:10
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1065
Revision\MutableRevisionRecord\setTimestamp
setTimestamp( $timestamp)
Definition: MutableRevisionRecord.php:229
Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:385
Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:733
Revision\MutableRevisionRecord\setSize
setSize( $size)
Set nominal revision size, for optimization.
Definition: MutableRevisionRecord.php:211
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Revision\RevisionStore\newRevisionFromRowAndSlots
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, $fromCache=false)
Definition: RevisionStore.php:1836
Revision\RevisionStore\loadRevisionFromTimestamp
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:2469
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:513
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
Revision\RevisionStore\updateRevisionTextId
updateRevisionTextId(IDatabase $dbw, $revisionId, &$blobAddress)
Definition: RevisionStore.php:624
Title\newFromID
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:467
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2232
SCHEMA_COMPAT_READ_BOTH
const SCHEMA_COMPAT_READ_BOTH
Definition: Defines.php:269
Revision\RevisionStore\getRevisionRowCacheKey
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) Caching rows w...
Definition: RevisionStore.php:3254
CommentStoreComment
CommentStoreComment represents a comment stored by CommentStore.
Definition: CommentStoreComment.php:29
Revision\MutableRevisionRecord\setComment
setComment(CommentStoreComment $comment)
Definition: MutableRevisionRecord.php:183
SCHEMA_COMPAT_READ_OLD
const SCHEMA_COMPAT_READ_OLD
Definition: Defines.php:265
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
Revision\RevisionStore\getBaseRevisionRow
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:852
Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, Title $title=null, $fromCache=false)
Definition: RevisionStore.php:1810
Revision\RevisionStore\findSlotContentId
findSlotContentId(IDatabase $db, $revId, $role)
Finds the ID of a content row for a given revision and slot role.
Definition: RevisionStore.php:2607
Hooks
Hooks class.
Definition: Hooks.php:34
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
Revision\RevisionStore\loadRevisionFromTitle
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
Definition: RevisionStore.php:2435
Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:103
Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:239
MediaWiki\Storage\BlobStore\FORMAT_HINT
const FORMAT_HINT
Hint key for use with storeBlob, indicating the serialization format used to create the blob,...
Definition: BlobStore.php:84
Revision\RevisionStore\getTitle
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
Definition: RevisionStore.php:310