MediaWiki  master
RevisionStore.php
Go to the documentation of this file.
1 <?php
27 namespace MediaWiki\Revision;
28 
29 use ActorMigration;
30 use CommentStore;
32 use Content;
33 use ContentHandler;
35 use Hooks;
36 use IDBAccessObject;
37 use InvalidArgumentException;
38 use LogicException;
47 use Message;
48 use MWException;
50 use Psr\Log\LoggerAwareInterface;
51 use Psr\Log\LoggerInterface;
52 use Psr\Log\NullLogger;
53 use RecentChange;
54 use Revision;
55 use RuntimeException;
56 use StatusValue;
57 use stdClass;
58 use Title;
59 use Traversable;
60 use User;
61 use WANObjectCache;
62 use Wikimedia\Assert\Assert;
63 use Wikimedia\IPUtils;
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 && IPUtils::isValid( $user->getName() ) ) {
713  $ipcRow = [
714  'ipc_rev_id' => $revisionId,
715  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
716  'ipc_hex' => IPUtils::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 
1137  [ 'rc_this_oldid' => $rev->getId() ],
1138  __METHOD__,
1139  $dbType
1140  );
1141 
1142  // XXX: cache this locally? Glue it to the RevisionRecord?
1143  return $rc;
1144  }
1145 
1153  private static function mapArchiveFields( $archiveRow ) {
1154  $fieldMap = [
1155  // keep with ar prefix:
1156  'ar_id' => 'ar_id',
1157 
1158  // not the same suffix:
1159  'ar_page_id' => 'rev_page',
1160  'ar_rev_id' => 'rev_id',
1161 
1162  // same suffix:
1163  'ar_text_id' => 'rev_text_id',
1164  'ar_timestamp' => 'rev_timestamp',
1165  'ar_user_text' => 'rev_user_text',
1166  'ar_user' => 'rev_user',
1167  'ar_actor' => 'rev_actor',
1168  'ar_minor_edit' => 'rev_minor_edit',
1169  'ar_deleted' => 'rev_deleted',
1170  'ar_len' => 'rev_len',
1171  'ar_parent_id' => 'rev_parent_id',
1172  'ar_sha1' => 'rev_sha1',
1173  'ar_comment' => 'rev_comment',
1174  'ar_comment_cid' => 'rev_comment_cid',
1175  'ar_comment_id' => 'rev_comment_id',
1176  'ar_comment_text' => 'rev_comment_text',
1177  'ar_comment_data' => 'rev_comment_data',
1178  'ar_comment_old' => 'rev_comment_old',
1179  'ar_content_format' => 'rev_content_format',
1180  'ar_content_model' => 'rev_content_model',
1181  ];
1182 
1183  $revRow = new stdClass();
1184  foreach ( $fieldMap as $arKey => $revKey ) {
1185  if ( property_exists( $archiveRow, $arKey ) ) {
1186  $revRow->$revKey = $archiveRow->$arKey;
1187  }
1188  }
1189 
1190  return $revRow;
1191  }
1192 
1203  private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
1204  $mainSlotRow = new stdClass();
1205  $mainSlotRow->role_name = SlotRecord::MAIN;
1206  $mainSlotRow->model_name = null;
1207  $mainSlotRow->slot_revision_id = null;
1208  $mainSlotRow->slot_content_id = null;
1209  $mainSlotRow->content_address = null;
1210 
1211  $content = null;
1212  $blobData = null;
1213  $blobFlags = null;
1214 
1215  if ( is_object( $row ) ) {
1216  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1217  // Don't emulate from a row when using the new schema.
1218  // Emulating from an array is still OK.
1219  throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
1220  }
1221 
1222  // archive row
1223  if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
1224  $row = $this->mapArchiveFields( $row );
1225  }
1226 
1227  if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
1228  $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
1229  $row->rev_text_id
1230  );
1231  }
1232 
1233  // This is used by null-revisions
1234  $mainSlotRow->slot_origin = isset( $row->slot_origin )
1235  ? intval( $row->slot_origin )
1236  : null;
1237 
1238  if ( isset( $row->old_text ) ) {
1239  // this happens when the text-table gets joined directly, in the pre-1.30 schema
1240  $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
1241  // Check against selects that might have not included old_flags
1242  if ( !property_exists( $row, 'old_flags' ) ) {
1243  throw new InvalidArgumentException( 'old_flags was not set in $row' );
1244  }
1245  $blobFlags = $row->old_flags ?? '';
1246  }
1247 
1248  $mainSlotRow->slot_revision_id = intval( $row->rev_id );
1249 
1250  $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
1251  $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
1252  $mainSlotRow->model_name = isset( $row->rev_content_model )
1253  ? strval( $row->rev_content_model )
1254  : null;
1255  // XXX: in the future, we'll probably always use the default format, and drop content_format
1256  $mainSlotRow->format_name = isset( $row->rev_content_format )
1257  ? strval( $row->rev_content_format )
1258  : null;
1259 
1260  if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
1261  // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1262  $mainSlotRow->slot_content_id
1263  = $this->emulateContentId( intval( $row->rev_text_id ) );
1264  }
1265  } elseif ( is_array( $row ) ) {
1266  $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
1267 
1268  $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
1269  ? intval( $row['slot_origin'] )
1270  : null;
1271  $mainSlotRow->content_address = isset( $row['text_id'] )
1272  ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
1273  : null;
1274  $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
1275  $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
1276 
1277  $mainSlotRow->model_name = isset( $row['content_model'] )
1278  ? strval( $row['content_model'] ) : null; // XXX: must be a string!
1279  // XXX: in the future, we'll probably always use the default format, and drop content_format
1280  $mainSlotRow->format_name = isset( $row['content_format'] )
1281  ? strval( $row['content_format'] ) : null;
1282  $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
1283  // XXX: If the flags field is not set then $blobFlags should be null so that no
1284  // decoding will happen. An empty string will result in default decodings.
1285  $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
1286 
1287  // if we have a Content object, override mText and mContentModel
1288  if ( !empty( $row['content'] ) ) {
1289  if ( !( $row['content'] instanceof Content ) ) {
1290  throw new MWException( 'content field must contain a Content object.' );
1291  }
1292 
1294  $content = $row['content'];
1295  $handler = $content->getContentHandler();
1296 
1297  $mainSlotRow->model_name = $content->getModel();
1298 
1299  // XXX: in the future, we'll probably always use the default format.
1300  if ( $mainSlotRow->format_name === null ) {
1301  $mainSlotRow->format_name = $handler->getDefaultFormat();
1302  }
1303  }
1304 
1305  if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
1306  // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1307  $mainSlotRow->slot_content_id
1308  = $this->emulateContentId( intval( $row['text_id'] ) );
1309  }
1310  } else {
1311  throw new MWException( 'Revision constructor passed invalid row format.' );
1312  }
1313 
1314  // With the old schema, the content changes with every revision,
1315  // except for null-revisions.
1316  if ( !isset( $mainSlotRow->slot_origin ) ) {
1317  $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
1318  }
1319 
1320  if ( $mainSlotRow->model_name === null ) {
1321  $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
1323 
1324  return $this->slotRoleRegistry->getRoleHandler( $slot->getRole() )
1325  ->getDefaultModel( $title );
1326  };
1327  }
1328 
1329  if ( !$content ) {
1330  // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
1331  // is missing, but "empty revisions" with no content are used in some edge cases.
1332 
1333  $content = function ( SlotRecord $slot )
1334  use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
1335  {
1336  return $this->loadSlotContent(
1337  $slot,
1338  $blobData,
1339  $blobFlags,
1340  $mainSlotRow->format_name,
1341  $queryFlags
1342  );
1343  };
1344  }
1345 
1346  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
1347  // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
1348  // the inherited slot to have the same content_id as the original slot. In that case,
1349  // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
1350  $mainSlotRow->slot_content_id =
1351  function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
1352  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1353  return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
1354  };
1355  }
1356 
1357  return new SlotRecord( $mainSlotRow, $content );
1358  }
1359 
1371  private function emulateContentId( $textId ) {
1372  // Return a negative number to ensure the ID is distinct from any real content IDs
1373  // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
1374  // mode.
1375  return -$textId;
1376  }
1377 
1397  private function loadSlotContent(
1398  SlotRecord $slot,
1399  $blobData = null,
1400  $blobFlags = null,
1401  $blobFormat = null,
1402  $queryFlags = 0
1403  ) {
1404  if ( $blobData !== null ) {
1405  Assert::parameterType( 'string', $blobData, '$blobData' );
1406  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1407 
1408  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1409 
1410  if ( $blobFlags === null ) {
1411  // No blob flags, so use the blob verbatim.
1412  $data = $blobData;
1413  } else {
1414  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1415  if ( $data === false ) {
1416  throw new RevisionAccessException(
1417  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1418  );
1419  }
1420  }
1421 
1422  } else {
1423  $address = $slot->getAddress();
1424  try {
1425  $data = $this->blobStore->getBlob( $address, $queryFlags );
1426  } catch ( BlobAccessException $e ) {
1427  throw new RevisionAccessException(
1428  "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1429  );
1430  }
1431  }
1432 
1433  // Unserialize content
1434  $handler = ContentHandler::getForModelID( $slot->getModel() );
1435 
1436  $content = $handler->unserializeContent( $data, $blobFormat );
1437  return $content;
1438  }
1439 
1454  public function getRevisionById( $id, $flags = 0 ) {
1455  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1456  }
1457 
1474  public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1475  // TODO should not require Title in future (T206498)
1476  $title = Title::newFromLinkTarget( $linkTarget );
1477  $conds = [
1478  'page_namespace' => $title->getNamespace(),
1479  'page_title' => $title->getDBkey()
1480  ];
1481  if ( $revId ) {
1482  // Use the specified revision ID.
1483  // Note that we use newRevisionFromConds here because we want to retry
1484  // and fall back to master if the page is not found on a replica.
1485  // Since the caller supplied a revision ID, we are pretty sure the revision is
1486  // supposed to exist, so we should try hard to find it.
1487  $conds['rev_id'] = $revId;
1488  return $this->newRevisionFromConds( $conds, $flags, $title );
1489  } else {
1490  // Use a join to get the latest revision.
1491  // Note that we don't use newRevisionFromConds here because we don't want to retry
1492  // and fall back to master. The assumption is that we only want to force the fallback
1493  // if we are quite sure the revision exists because the caller supplied a revision ID.
1494  // If the page isn't found at all on a replica, it probably simply does not exist.
1495  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1496 
1497  $conds[] = 'rev_id=page_latest';
1498  $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1499 
1500  return $rev;
1501  }
1502  }
1503 
1520  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1521  $conds = [ 'page_id' => $pageId ];
1522  if ( $revId ) {
1523  // Use the specified revision ID.
1524  // Note that we use newRevisionFromConds here because we want to retry
1525  // and fall back to master if the page is not found on a replica.
1526  // Since the caller supplied a revision ID, we are pretty sure the revision is
1527  // supposed to exist, so we should try hard to find it.
1528  $conds['rev_id'] = $revId;
1529  return $this->newRevisionFromConds( $conds, $flags );
1530  } else {
1531  // Use a join to get the latest revision.
1532  // Note that we don't use newRevisionFromConds here because we don't want to retry
1533  // and fall back to master. The assumption is that we only want to force the fallback
1534  // if we are quite sure the revision exists because the caller supplied a revision ID.
1535  // If the page isn't found at all on a replica, it probably simply does not exist.
1536  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1537 
1538  $conds[] = 'rev_id=page_latest';
1539  $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1540 
1541  return $rev;
1542  }
1543  }
1544 
1556  public function getRevisionByTimestamp( $title, $timestamp ) {
1557  $db = $this->getDBConnectionRef( DB_REPLICA );
1558  return $this->newRevisionFromConds(
1559  [
1560  'rev_timestamp' => $db->timestamp( $timestamp ),
1561  'page_namespace' => $title->getNamespace(),
1562  'page_title' => $title->getDBkey()
1563  ],
1564  0,
1565  $title
1566  );
1567  }
1568 
1576  private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
1577  $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1578 
1579  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1580  $db = $this->getDBConnectionRef( $dbMode );
1581 
1582  $res = $db->select(
1583  $revQuery['tables'],
1584  $revQuery['fields'],
1585  [
1586  'slot_revision_id' => $revId,
1587  ],
1588  __METHOD__,
1589  $dbOptions,
1590  $revQuery['joins']
1591  );
1592 
1593  $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
1594 
1595  return $slots;
1596  }
1597 
1610  private function constructSlotRecords(
1611  $revId,
1612  $slotRows,
1613  $queryFlags,
1614  Title $title,
1615  $slotContents = null
1616  ) {
1617  $slots = [];
1618 
1619  foreach ( $slotRows as $row ) {
1620  // Resolve role names and model names from in-memory cache, if they were not joined in.
1621  if ( !isset( $row->role_name ) ) {
1622  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1623  }
1624 
1625  if ( !isset( $row->model_name ) ) {
1626  if ( isset( $row->content_model ) ) {
1627  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1628  } else {
1629  // We may get here if $row->model_name is set but null, perhaps because it
1630  // came from rev_content_model, which is NULL for the default model.
1631  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1632  $row->model_name = $slotRoleHandler->getDefaultModel( $title );
1633  }
1634  }
1635 
1636  if ( !isset( $row->content_id ) && isset( $row->rev_text_id ) ) {
1637  $row->slot_content_id
1638  = $this->emulateContentId( intval( $row->rev_text_id ) );
1639  }
1640 
1641  // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1642  if ( isset( $row->blob_data ) ) {
1643  $slotContents[$row->content_address] = $row->blob_data;
1644  }
1645 
1646  $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1647  $blob = null;
1648  if ( isset( $slotContents[$slot->getAddress()] ) ) {
1649  $blob = $slotContents[$slot->getAddress()];
1650  if ( $blob instanceof Content ) {
1651  return $blob;
1652  }
1653  }
1654  return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1655  };
1656 
1657  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1658  }
1659 
1660  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1661  throw new RevisionAccessException(
1662  'Main slot of revision ' . $revId . ' not found in database!'
1663  );
1664  }
1665 
1666  return $slots;
1667  }
1668 
1684  private function newRevisionSlots(
1685  $revId,
1686  $revisionRow,
1687  $slotRows,
1688  $queryFlags,
1689  Title $title
1690  ) {
1691  if ( $slotRows ) {
1692  $slots = new RevisionSlots(
1693  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
1694  );
1695  } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1696  $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
1697  // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive
1698  $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
1699  } else {
1700  // XXX: do we need the same kind of caching here
1701  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1702 
1703  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
1704  return $this->loadSlotRecords( $revId, $queryFlags, $title );
1705  } );
1706  }
1707 
1708  return $slots;
1709  }
1710 
1728  public function newRevisionFromArchiveRow(
1729  $row,
1730  $queryFlags = 0,
1731  Title $title = null,
1732  array $overrides = []
1733  ) {
1734  Assert::parameterType( 'object', $row, '$row' );
1735 
1736  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1737  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1738 
1739  if ( !$title && isset( $overrides['title'] ) ) {
1740  if ( !( $overrides['title'] instanceof Title ) ) {
1741  throw new MWException( 'title field override must contain a Title object.' );
1742  }
1743 
1744  $title = $overrides['title'];
1745  }
1746 
1747  if ( !isset( $title ) ) {
1748  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1749  $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1750  } else {
1751  throw new InvalidArgumentException(
1752  'A Title or ar_namespace and ar_title must be given'
1753  );
1754  }
1755  }
1756 
1757  foreach ( $overrides as $key => $value ) {
1758  $field = "ar_$key";
1759  $row->$field = $value;
1760  }
1761 
1762  try {
1763  $user = User::newFromAnyId(
1764  $row->ar_user ?? null,
1765  $row->ar_user_text ?? null,
1766  $row->ar_actor ?? null,
1767  $this->dbDomain
1768  );
1769  } catch ( InvalidArgumentException $ex ) {
1770  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1771  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1772  }
1773 
1774  if ( $user->getName() === '' ) {
1775  // T236624: If the user name is empty, force 'Unknown user',
1776  // even if the actor table has an entry for the empty user name.
1777  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1778  }
1779 
1780  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1781  // Legacy because $row may have come from self::selectFields()
1782  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1783 
1784  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, null, $queryFlags, $title );
1785 
1786  return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
1787  }
1788 
1802  public function newRevisionFromRow(
1803  $row,
1804  $queryFlags = 0,
1805  Title $title = null,
1806  $fromCache = false
1807  ) {
1808  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
1809  }
1810 
1829  $row,
1830  $slots,
1831  $queryFlags = 0,
1832  Title $title = null,
1833  $fromCache = false
1834  ) {
1835  Assert::parameterType( 'object', $row, '$row' );
1836 
1837  if ( !$title ) {
1838  $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
1839  $revId = $row->rev_id ?? 0;
1840 
1841  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1842  }
1843 
1844  if ( !isset( $row->page_latest ) ) {
1845  $row->page_latest = $title->getLatestRevID();
1846  if ( $row->page_latest === 0 && $title->exists() ) {
1847  wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1848  }
1849  }
1850 
1851  try {
1852  $user = User::newFromAnyId(
1853  $row->rev_user ?? null,
1854  $row->rev_user_text ?? null,
1855  $row->rev_actor ?? null,
1856  $this->dbDomain
1857  );
1858  } catch ( InvalidArgumentException $ex ) {
1859  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1860  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1861  }
1862 
1863  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1864  // Legacy because $row may have come from self::selectFields()
1865  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1866 
1867  if ( !( $slots instanceof RevisionSlots ) ) {
1868  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
1869  }
1870 
1871  // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1872  if ( $fromCache ) {
1873  $rev = new RevisionStoreCacheRecord(
1874  function ( $revId ) use ( $queryFlags ) {
1875  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1876  return $this->fetchRevisionRowFromConds(
1877  $db,
1878  [ 'rev_id' => intval( $revId ) ]
1879  );
1880  },
1881  $title, $user, $comment, $row, $slots, $this->dbDomain
1882  );
1883  } else {
1884  $rev = new RevisionStoreRecord(
1885  $title, $user, $comment, $row, $slots, $this->dbDomain );
1886  }
1887  return $rev;
1888  }
1889 
1910  public function newRevisionsFromBatch(
1911  $rows,
1912  array $options = [],
1913  $queryFlags = 0,
1914  Title $title = null
1915  ) {
1916  $result = new StatusValue();
1917 
1918  $rowsByRevId = [];
1919  $pageIdsToFetchTitles = [];
1920  $titlesByPageId = [];
1921  foreach ( $rows as $row ) {
1922  if ( isset( $rowsByRevId[$row->rev_id] ) ) {
1923  $result->warning(
1924  'internalerror',
1925  "Duplicate rows in newRevisionsFromBatch, rev_id {$row->rev_id}"
1926  );
1927  }
1928  if ( $title && $row->rev_page != $title->getArticleID() ) {
1929  throw new InvalidArgumentException(
1930  "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}"
1931  );
1932  } elseif ( !$title && !isset( $titlesByPageId[ $row->rev_page ] ) ) {
1933  if ( isset( $row->page_namespace ) && isset( $row->page_title ) &&
1934  // This should not happen, but just in case we don't have a page_id
1935  // set or it doesn't match rev_page, let's fetch the title again.
1936  isset( $row->page_id ) && $row->rev_page === $row->page_id
1937  ) {
1938  $titlesByPageId[ $row->rev_page ] = Title::newFromRow( $row );
1939  } else {
1940  $pageIdsToFetchTitles[] = $row->rev_page;
1941  }
1942  }
1943  $rowsByRevId[$row->rev_id] = $row;
1944  }
1945 
1946  if ( empty( $rowsByRevId ) ) {
1947  $result->setResult( true, [] );
1948  return $result;
1949  }
1950 
1951  // If the title is not supplied, batch-fetch Title objects.
1952  if ( $title ) {
1953  $titlesByPageId[$title->getArticleID()] = $title;
1954  } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1955  $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1956  foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1957  $titlesByPageId[$t->getArticleID()] = $t;
1958  }
1959  }
1960 
1961  if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
1962  $result->setResult( true,
1963  array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) {
1964  try {
1965  return $this->newRevisionFromRow(
1966  $row,
1967  $queryFlags,
1968  $titlesByPageId[$row->rev_page]
1969  );
1970  } catch ( MWException $e ) {
1971  $result->warning( 'internalerror', $e->getMessage() );
1972  return null;
1973  }
1974  }, $rowsByRevId )
1975  );
1976  return $result;
1977  }
1978 
1979  $slotRowOptions = [
1980  'slots' => $options['slots'] ?? true,
1981  'blobs' => $options['content'] ?? false,
1982  ];
1983 
1984  if ( is_array( $slotRowOptions['slots'] )
1985  && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
1986  ) {
1987  // Make sure the main slot is always loaded, RevisionRecord requires this.
1988  $slotRowOptions['slots'][] = SlotRecord::MAIN;
1989  }
1990 
1991  $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
1992 
1993  $result->merge( $slotRowsStatus );
1994  $slotRowsByRevId = $slotRowsStatus->getValue();
1995 
1996  $result->setResult( true, array_map( function ( $row ) use
1997  ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $result ) {
1998  if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) {
1999  $result->warning(
2000  'internalerror',
2001  "Couldn't find slots for rev {$row->rev_id}"
2002  );
2003  return null;
2004  }
2005  try {
2006  return $this->newRevisionFromRowAndSlots(
2007  $row,
2008  new RevisionSlots(
2009  $this->constructSlotRecords(
2010  $row->rev_id,
2011  $slotRowsByRevId[$row->rev_id],
2012  $queryFlags,
2013  $titlesByPageId[$row->rev_page]
2014  )
2015  ),
2016  $queryFlags,
2017  $titlesByPageId[$row->rev_page]
2018  );
2019  } catch ( MWException $e ) {
2020  $result->warning( 'internalerror', $e->getMessage() );
2021  return null;
2022  }
2023  }, $rowsByRevId ) );
2024  return $result;
2025  }
2026 
2049  private function getSlotRowsForBatch(
2050  $rowsOrIds,
2051  array $options = [],
2052  $queryFlags = 0
2053  ) {
2054  $readNew = $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW );
2055  $result = new StatusValue();
2056 
2057  $revIds = [];
2058  foreach ( $rowsOrIds as $row ) {
2059  $revIds[] = is_object( $row ) ? (int)$row->rev_id : (int)$row;
2060  }
2061 
2062  // Nothing to do.
2063  // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2064  if ( empty( $revIds ) ) {
2065  $result->setResult( true, [] );
2066  return $result;
2067  }
2068 
2069  // We need to set the `content` flag to join in content meta-data
2070  $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
2071  $revIdField = $slotQueryInfo['keys']['rev_id'];
2072  $slotQueryConds = [ $revIdField => $revIds ];
2073 
2074  if ( $readNew && isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2075  if ( empty( $options['slots'] ) ) {
2076  // Degenerate case: return no slots for each revision.
2077  $result->setResult( true, array_fill_keys( $revIds, [] ) );
2078  return $result;
2079  }
2080 
2081  $roleIdField = $slotQueryInfo['keys']['role_id'];
2082  $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
2083  return $this->slotRoleStore->getId( $slot_name );
2084  }, $options['slots'] );
2085  }
2086 
2087  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2088  $slotRows = $db->select(
2089  $slotQueryInfo['tables'],
2090  $slotQueryInfo['fields'],
2091  $slotQueryConds,
2092  __METHOD__,
2093  [],
2094  $slotQueryInfo['joins']
2095  );
2096 
2097  $slotContents = null;
2098  if ( $options['blobs'] ?? false ) {
2099  $blobAddresses = [];
2100  foreach ( $slotRows as $slotRow ) {
2101  $blobAddresses[] = $slotRow->content_address;
2102  }
2103  $slotContentFetchStatus = $this->blobStore
2104  ->getBlobBatch( $blobAddresses, $queryFlags );
2105  foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2106  $result->warning( $error['message'], ...$error['params'] );
2107  }
2108  $slotContents = $slotContentFetchStatus->getValue();
2109  }
2110 
2111  $slotRowsByRevId = [];
2112  foreach ( $slotRows as $slotRow ) {
2113  if ( $slotContents === null ) {
2114  // nothing to do
2115  } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2116  $slotRow->blob_data = $slotContents[$slotRow->content_address];
2117  } else {
2118  $result->warning(
2119  'internalerror',
2120  "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2121  );
2122  $slotRow->blob_data = null;
2123  }
2124 
2125  // conditional needed for SCHEMA_COMPAT_READ_OLD
2126  if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2127  $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2128  }
2129 
2130  // conditional needed for SCHEMA_COMPAT_READ_OLD
2131  if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2132  $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2133  }
2134 
2135  $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2136  }
2137 
2138  $result->setResult( true, $slotRowsByRevId );
2139  return $result;
2140  }
2141 
2162  public function getContentBlobsForBatch(
2163  $rowsOrIds,
2164  $slots = null,
2165  $queryFlags = 0
2166  ) {
2167  $result = $this->getSlotRowsForBatch(
2168  $rowsOrIds,
2169  [ 'slots' => $slots, 'blobs' => true ],
2170  $queryFlags
2171  );
2172 
2173  if ( $result->isOK() ) {
2174  // strip out all internal meta data that we don't want to expose
2175  foreach ( $result->value as $revId => $rowsByRole ) {
2176  foreach ( $rowsByRole as $role => $slotRow ) {
2177  if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2178  // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2179  // if we didn't ask for it.
2180  unset( $result->value[$revId][$role] );
2181  continue;
2182  }
2183 
2184  $result->value[$revId][$role] = (object)[
2185  'blob_data' => $slotRow->blob_data,
2186  'model_name' => $slotRow->model_name,
2187  ];
2188  }
2189  }
2190  }
2191 
2192  return $result;
2193  }
2194 
2210  array $fields,
2211  $queryFlags = 0,
2212  Title $title = null
2213  ) {
2214  if ( !$title && isset( $fields['title'] ) ) {
2215  if ( !( $fields['title'] instanceof Title ) ) {
2216  throw new MWException( 'title field must contain a Title object.' );
2217  }
2218 
2219  $title = $fields['title'];
2220  }
2221 
2222  if ( !$title ) {
2223  $pageId = $fields['page'] ?? 0;
2224  $revId = $fields['id'] ?? 0;
2225 
2226  $title = $this->getTitle( $pageId, $revId, $queryFlags );
2227  }
2228 
2229  if ( !isset( $fields['page'] ) ) {
2230  $fields['page'] = $title->getArticleID( $queryFlags );
2231  }
2232 
2233  // if we have a content object, use it to set the model and type
2234  if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
2235  && !is_array( $fields['content'] )
2236  ) {
2237  throw new MWException(
2238  'content field must contain a Content object or an array of Content objects.'
2239  );
2240  }
2241 
2242  if ( !empty( $fields['text_id'] ) ) {
2243  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2244  throw new MWException( "The text_id field is only available in the pre-MCR schema" );
2245  }
2246 
2247  if ( !empty( $fields['content'] ) ) {
2248  throw new MWException(
2249  "Text already stored in external store (id {$fields['text_id']}), " .
2250  "can't specify content object"
2251  );
2252  }
2253  }
2254 
2255  if (
2256  isset( $fields['comment'] )
2257  && !( $fields['comment'] instanceof CommentStoreComment )
2258  ) {
2259  $commentData = $fields['comment_data'] ?? null;
2260 
2261  if ( $fields['comment'] instanceof Message ) {
2262  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2263  $fields['comment'],
2264  $commentData
2265  );
2266  } else {
2267  $commentText = trim( strval( $fields['comment'] ) );
2268  $fields['comment'] = CommentStoreComment::newUnsavedComment(
2269  $commentText,
2270  $commentData
2271  );
2272  }
2273  }
2274 
2275  $revision = new MutableRevisionRecord( $title, $this->dbDomain );
2276  $this->initializeMutableRevisionFromArray( $revision, $fields );
2277 
2278  if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
2279  foreach ( $fields['content'] as $role => $content ) {
2280  $revision->setContent( $role, $content );
2281  }
2282  } else {
2283  $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
2284  $revision->setSlot( $mainSlot );
2285  }
2286 
2287  return $revision;
2288  }
2289 
2295  MutableRevisionRecord $record,
2296  array $fields
2297  ) {
2299  $user = null;
2300 
2301  // If a user is passed in, use it if possible. We cannot use a user from a
2302  // remote wiki with unsuppressed ids, due to issues described in T222212.
2303  if ( isset( $fields['user'] ) &&
2304  ( $fields['user'] instanceof UserIdentity ) &&
2305  ( $this->dbDomain === false ||
2306  ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
2307  ) {
2308  $user = $fields['user'];
2309  } else {
2310  $userID = isset( $fields['user'] ) && is_numeric( $fields['user'] ) ? $fields['user'] : null;
2311  try {
2312  $user = User::newFromAnyId(
2313  $userID,
2314  $fields['user_text'] ?? null,
2315  $fields['actor'] ?? null,
2316  $this->dbDomain
2317  );
2318  } catch ( InvalidArgumentException $ex ) {
2319  $user = null;
2320  }
2321  }
2322 
2323  if ( $user ) {
2324  $record->setUser( $user );
2325  }
2326 
2327  $timestamp = isset( $fields['timestamp'] )
2328  ? strval( $fields['timestamp'] )
2329  : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
2330 
2331  $record->setTimestamp( $timestamp );
2332 
2333  if ( isset( $fields['page'] ) ) {
2334  $record->setPageId( intval( $fields['page'] ) );
2335  }
2336 
2337  if ( isset( $fields['id'] ) ) {
2338  $record->setId( intval( $fields['id'] ) );
2339  }
2340  if ( isset( $fields['parent_id'] ) ) {
2341  $record->setParentId( intval( $fields['parent_id'] ) );
2342  }
2343 
2344  if ( isset( $fields['sha1'] ) ) {
2345  $record->setSha1( $fields['sha1'] );
2346  }
2347  if ( isset( $fields['size'] ) ) {
2348  $record->setSize( intval( $fields['size'] ) );
2349  }
2350 
2351  if ( isset( $fields['minor_edit'] ) ) {
2352  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2353  }
2354  if ( isset( $fields['deleted'] ) ) {
2355  $record->setVisibility( intval( $fields['deleted'] ) );
2356  }
2357 
2358  if ( isset( $fields['comment'] ) ) {
2359  Assert::parameterType(
2360  CommentStoreComment::class,
2361  $fields['comment'],
2362  '$row[\'comment\']'
2363  );
2364  $record->setComment( $fields['comment'] );
2365  }
2366  }
2367 
2382  public function loadRevisionFromId( IDatabase $db, $id ) {
2383  return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
2384  }
2385 
2401  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2402  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2403  if ( $id ) {
2404  $conds['rev_id'] = intval( $id );
2405  } else {
2406  $conds[] = 'rev_id=page_latest';
2407  }
2408  return $this->loadRevisionFromConds( $db, $conds );
2409  }
2410 
2427  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2428  if ( $id ) {
2429  $matchId = intval( $id );
2430  } else {
2431  $matchId = 'page_latest';
2432  }
2433 
2434  return $this->loadRevisionFromConds(
2435  $db,
2436  [
2437  "rev_id=$matchId",
2438  'page_namespace' => $title->getNamespace(),
2439  'page_title' => $title->getDBkey()
2440  ],
2441  0,
2442  $title
2443  );
2444  }
2445 
2461  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2462  return $this->loadRevisionFromConds( $db,
2463  [
2464  'rev_timestamp' => $db->timestamp( $timestamp ),
2465  'page_namespace' => $title->getNamespace(),
2466  'page_title' => $title->getDBkey()
2467  ],
2468  0,
2469  $title
2470  );
2471  }
2472 
2488  private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
2489  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2490  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
2491 
2492  $lb = $this->getDBLoadBalancer();
2493 
2494  // Make sure new pending/committed revision are visibile later on
2495  // within web requests to certain avoid bugs like T93866 and T94407.
2496  if ( !$rev
2497  && !( $flags & self::READ_LATEST )
2498  && $lb->hasStreamingReplicaServers()
2499  && $lb->hasOrMadeRecentMasterChanges()
2500  ) {
2501  $flags = self::READ_LATEST;
2502  $dbw = $this->getDBConnectionRef( DB_MASTER );
2503  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
2504  }
2505 
2506  return $rev;
2507  }
2508 
2522  private function loadRevisionFromConds(
2523  IDatabase $db,
2524  $conditions,
2525  $flags = 0,
2526  Title $title = null
2527  ) {
2528  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
2529  if ( $row ) {
2530  $rev = $this->newRevisionFromRow( $row, $flags, $title );
2531 
2532  return $rev;
2533  }
2534 
2535  return null;
2536  }
2537 
2545  private function checkDatabaseDomain( IDatabase $db ) {
2546  $dbDomain = $db->getDomainID();
2547  $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2548  if ( $dbDomain === $storeDomain ) {
2549  return;
2550  }
2551 
2552  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2553  }
2554 
2567  private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
2568  $this->checkDatabaseDomain( $db );
2569 
2570  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2571  $options = [];
2572  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2573  $options[] = 'FOR UPDATE';
2574  }
2575  return $db->selectRow(
2576  $revQuery['tables'],
2577  $revQuery['fields'],
2578  $conditions,
2579  __METHOD__,
2580  $options,
2581  $revQuery['joins']
2582  );
2583  }
2584 
2599  private function findSlotContentId( IDatabase $db, $revId, $role ) {
2600  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
2601  return null;
2602  }
2603 
2604  try {
2605  $roleId = $this->slotRoleStore->getId( $role );
2606  $conditions = [
2607  'slot_revision_id' => $revId,
2608  'slot_role_id' => $roleId,
2609  ];
2610 
2611  $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
2612 
2613  return $contentId ?: null;
2614  } catch ( NameTableAccessException $ex ) {
2615  // If the role is missing from the slot_roles table,
2616  // the corresponding row in slots cannot exist.
2617  return null;
2618  }
2619  }
2620 
2645  public function getQueryInfo( $options = [] ) {
2646  $ret = [
2647  'tables' => [],
2648  'fields' => [],
2649  'joins' => [],
2650  ];
2651 
2652  $ret['tables'][] = 'revision';
2653  $ret['fields'] = array_merge( $ret['fields'], [
2654  'rev_id',
2655  'rev_page',
2656  'rev_timestamp',
2657  'rev_minor_edit',
2658  'rev_deleted',
2659  'rev_len',
2660  'rev_parent_id',
2661  'rev_sha1',
2662  ] );
2663 
2664  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2665  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2666  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2667  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2668 
2669  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2670  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2671  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2672  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2673 
2674  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2675  $ret['fields'][] = 'rev_text_id';
2676 
2677  if ( $this->contentHandlerUseDB ) {
2678  $ret['fields'][] = 'rev_content_format';
2679  $ret['fields'][] = 'rev_content_model';
2680  }
2681  }
2682 
2683  if ( in_array( 'page', $options, true ) ) {
2684  $ret['tables'][] = 'page';
2685  $ret['fields'] = array_merge( $ret['fields'], [
2686  'page_namespace',
2687  'page_title',
2688  'page_id',
2689  'page_latest',
2690  'page_is_redirect',
2691  'page_len',
2692  ] );
2693  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2694  }
2695 
2696  if ( in_array( 'user', $options, true ) ) {
2697  $ret['tables'][] = 'user';
2698  $ret['fields'] = array_merge( $ret['fields'], [
2699  'user_name',
2700  ] );
2701  $u = $actorQuery['fields']['rev_user'];
2702  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2703  }
2704 
2705  if ( in_array( 'text', $options, true ) ) {
2706  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
2707  throw new InvalidArgumentException( 'text table can no longer be joined directly' );
2708  } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2709  // NOTE: even when this class is set to not read from the old schema, callers
2710  // should still be able to join against the text table, as long as we are still
2711  // writing the old schema for compatibility.
2712  wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
2713  }
2714 
2715  $ret['tables'][] = 'text';
2716  $ret['fields'] = array_merge( $ret['fields'], [
2717  'old_text',
2718  'old_flags'
2719  ] );
2720  $ret['joins']['text'] = [ 'JOIN', [ 'rev_text_id=old_id' ] ];
2721  }
2722 
2723  return $ret;
2724  }
2725 
2746  public function getSlotsQueryInfo( $options = [] ) {
2747  $ret = [
2748  'tables' => [],
2749  'fields' => [],
2750  'joins' => [],
2751  'keys' => [],
2752  ];
2753 
2754  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2755  $db = $this->getDBConnectionRef( DB_REPLICA );
2756  $ret['keys']['rev_id'] = 'rev_id';
2757 
2758  $ret['tables'][] = 'revision';
2759 
2760  $ret['fields']['slot_revision_id'] = 'rev_id';
2761  $ret['fields']['slot_content_id'] = 'NULL';
2762  $ret['fields']['slot_origin'] = 'rev_id';
2763  $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
2764 
2765  if ( in_array( 'content', $options, true ) ) {
2766  $ret['fields']['content_size'] = 'rev_len';
2767  $ret['fields']['content_sha1'] = 'rev_sha1';
2768  $ret['fields']['content_address']
2769  = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'rev_text_id' ] );
2770 
2771  // Allow the content_id field to be emulated later
2772  $ret['fields']['rev_text_id'] = 'rev_text_id';
2773 
2774  if ( $this->contentHandlerUseDB ) {
2775  $ret['fields']['model_name'] = 'rev_content_model';
2776  } else {
2777  $ret['fields']['model_name'] = 'NULL';
2778  }
2779  }
2780  } else {
2781  $ret['keys']['rev_id'] = 'slot_revision_id';
2782  $ret['keys']['role_id'] = 'slot_role_id';
2783 
2784  $ret['tables'][] = 'slots';
2785  $ret['fields'] = array_merge( $ret['fields'], [
2786  'slot_revision_id',
2787  'slot_content_id',
2788  'slot_origin',
2789  'slot_role_id',
2790  ] );
2791 
2792  if ( in_array( 'role', $options, true ) ) {
2793  // Use left join to attach role name, so we still find the revision row even
2794  // if the role name is missing. This triggers a more obvious failure mode.
2795  $ret['tables'][] = 'slot_roles';
2796  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2797  $ret['fields'][] = 'role_name';
2798  }
2799 
2800  if ( in_array( 'content', $options, true ) ) {
2801  $ret['keys']['model_id'] = 'content_model';
2802 
2803  $ret['tables'][] = 'content';
2804  $ret['fields'] = array_merge( $ret['fields'], [
2805  'content_size',
2806  'content_sha1',
2807  'content_address',
2808  'content_model',
2809  ] );
2810  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2811 
2812  if ( in_array( 'model', $options, true ) ) {
2813  // Use left join to attach model name, so we still find the revision row even
2814  // if the model name is missing. This triggers a more obvious failure mode.
2815  $ret['tables'][] = 'content_models';
2816  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2817  $ret['fields'][] = 'model_name';
2818  }
2819 
2820  }
2821  }
2822 
2823  return $ret;
2824  }
2825 
2839  public function getArchiveQueryInfo() {
2840  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2841  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2842  $ret = [
2843  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2844  'fields' => [
2845  'ar_id',
2846  'ar_page_id',
2847  'ar_namespace',
2848  'ar_title',
2849  'ar_rev_id',
2850  'ar_timestamp',
2851  'ar_minor_edit',
2852  'ar_deleted',
2853  'ar_len',
2854  'ar_parent_id',
2855  'ar_sha1',
2856  ] + $commentQuery['fields'] + $actorQuery['fields'],
2857  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2858  ];
2859 
2860  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2861  $ret['fields'][] = 'ar_text_id';
2862 
2863  if ( $this->contentHandlerUseDB ) {
2864  $ret['fields'][] = 'ar_content_format';
2865  $ret['fields'][] = 'ar_content_model';
2866  }
2867  }
2868 
2869  return $ret;
2870  }
2871 
2881  public function getRevisionSizes( array $revIds ) {
2882  return $this->listRevisionSizes( $this->getDBConnectionRef( DB_REPLICA ), $revIds );
2883  }
2884 
2897  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2898  $this->checkDatabaseDomain( $db );
2899 
2900  $revLens = [];
2901  if ( !$revIds ) {
2902  return $revLens; // empty
2903  }
2904 
2905  $res = $db->select(
2906  'revision',
2907  [ 'rev_id', 'rev_len' ],
2908  [ 'rev_id' => $revIds ],
2909  __METHOD__
2910  );
2911 
2912  foreach ( $res as $row ) {
2913  $revLens[$row->rev_id] = intval( $row->rev_len );
2914  }
2915 
2916  return $revLens;
2917  }
2918 
2927  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2928  $op = $dir === 'next' ? '>' : '<';
2929  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2930 
2931  if ( !$rev->getId() || !$rev->getPageId() ) {
2932  // revision is unsaved or otherwise incomplete
2933  return null;
2934  }
2935 
2936  if ( $rev instanceof RevisionArchiveRecord ) {
2937  // revision is deleted, so it's not part of the page history
2938  return null;
2939  }
2940 
2941  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2942  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2943 
2944  $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2945  if ( $ts === false ) {
2946  // XXX Should this be moved into getTimestampFromId?
2947  $ts = $db->selectField( 'archive', 'ar_timestamp',
2948  [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2949  if ( $ts === false ) {
2950  // XXX Is this reachable? How can we have a page id but no timestamp?
2951  return null;
2952  }
2953  }
2954  $ts = $db->addQuotes( $db->timestamp( $ts ) );
2955 
2956  $revId = $db->selectField( 'revision', 'rev_id',
2957  [
2958  'rev_page' => $rev->getPageId(),
2959  "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})"
2960  ],
2961  __METHOD__,
2962  [
2963  'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2964  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2965  ]
2966  );
2967 
2968  if ( $revId === false ) {
2969  return null;
2970  }
2971 
2972  return $this->getRevisionById( intval( $revId ) );
2973  }
2974 
2990  public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
2991  if ( $flags instanceof Title ) {
2992  // Old calling convention, we don't use Title here anymore
2993  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2994  $flags = 0;
2995  }
2996 
2997  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2998  }
2999 
3013  public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
3014  if ( $flags instanceof Title ) {
3015  // Old calling convention, we don't use Title here anymore
3016  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
3017  $flags = 0;
3018  }
3019 
3020  return $this->getRelativeRevision( $rev, $flags, 'next' );
3021  }
3022 
3034  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
3035  $this->checkDatabaseDomain( $db );
3036 
3037  if ( $rev->getPageId() === null ) {
3038  return 0;
3039  }
3040  # Use page_latest if ID is not given
3041  if ( !$rev->getId() ) {
3042  $prevId = $db->selectField(
3043  'page', 'page_latest',
3044  [ 'page_id' => $rev->getPageId() ],
3045  __METHOD__
3046  );
3047  } else {
3048  $prevId = $db->selectField(
3049  'revision', 'rev_id',
3050  [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
3051  __METHOD__,
3052  [ 'ORDER BY' => 'rev_id DESC' ]
3053  );
3054  }
3055  return intval( $prevId );
3056  }
3057 
3070  public function getTimestampFromId( $id, $flags = 0 ) {
3071  if ( $id instanceof Title ) {
3072  // Old deprecated calling convention supported for backwards compatibility
3073  $id = $flags;
3074  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
3075  }
3076  $db = $this->getDBConnectionRefForQueryFlags( $flags );
3077 
3078  $timestamp =
3079  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
3080 
3081  return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
3082  }
3083 
3093  public function countRevisionsByPageId( IDatabase $db, $id ) {
3094  $this->checkDatabaseDomain( $db );
3095 
3096  $row = $db->selectRow( 'revision',
3097  [ 'revCount' => 'COUNT(*)' ],
3098  [ 'rev_page' => $id ],
3099  __METHOD__
3100  );
3101  if ( $row ) {
3102  return intval( $row->revCount );
3103  }
3104  return 0;
3105  }
3106 
3116  public function countRevisionsByTitle( IDatabase $db, $title ) {
3117  $id = $title->getArticleID();
3118  if ( $id ) {
3119  return $this->countRevisionsByPageId( $db, $id );
3120  }
3121  return 0;
3122  }
3123 
3142  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
3143  $this->checkDatabaseDomain( $db );
3144 
3145  if ( !$userId ) {
3146  return false;
3147  }
3148 
3149  $revQuery = $this->getQueryInfo();
3150  $res = $db->select(
3151  $revQuery['tables'],
3152  [
3153  'rev_user' => $revQuery['fields']['rev_user'],
3154  ],
3155  [
3156  'rev_page' => $pageId,
3157  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
3158  ],
3159  __METHOD__,
3160  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
3161  $revQuery['joins']
3162  );
3163  foreach ( $res as $row ) {
3164  if ( $row->rev_user != $userId ) {
3165  return false;
3166  }
3167  }
3168  return true;
3169  }
3170 
3184  public function getKnownCurrentRevision( Title $title, $revId = 0 ) {
3185  $db = $this->getDBConnectionRef( DB_REPLICA );
3186 
3187  $pageId = $title->getArticleID();
3188 
3189  if ( !$pageId ) {
3190  return false;
3191  }
3192 
3193  if ( !$revId ) {
3194  $revId = $title->getLatestRevID();
3195  }
3196 
3197  if ( !$revId ) {
3198  wfWarn(
3199  'No latest revision known for page ' . $title->getPrefixedDBkey()
3200  . ' even though it exists with page ID ' . $pageId
3201  );
3202  return false;
3203  }
3204 
3205  // Load the row from cache if possible. If not possible, populate the cache.
3206  // As a minor optimization, remember if this was a cache hit or miss.
3207  // We can sometimes avoid a database query later if this is a cache miss.
3208  $fromCache = true;
3209  $row = $this->cache->getWithSetCallback(
3210  // Page/rev IDs passed in from DB to reflect history merges
3211  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
3213  function ( $curValue, &$ttl, array &$setOpts ) use (
3214  $db, $pageId, $revId, &$fromCache
3215  ) {
3216  $setOpts += Database::getCacheSetOptions( $db );
3217  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
3218  if ( $row ) {
3219  $fromCache = false;
3220  }
3221  return $row; // don't cache negatives
3222  }
3223  );
3224 
3225  // Reflect revision deletion and user renames.
3226  if ( $row ) {
3227  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
3228  } else {
3229  return false;
3230  }
3231  }
3232 
3244  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3245  return $this->cache->makeGlobalKey(
3246  self::ROW_CACHE_KEY,
3247  $db->getDomainID(),
3248  $pageId,
3249  $revId
3250  );
3251  }
3252 
3260  private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
3261  if ( $rev ) {
3262  if ( $rev->getId() === null ) {
3263  throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
3264  }
3265  if ( $rev->getPageId() !== $pageId ) {
3266  throw new InvalidArgumentException(
3267  "Revision {$rev->getId()} doesn't belong to page {$pageId}"
3268  );
3269  }
3270  }
3271  }
3272 
3285  private function getRevisionLimitConditions(
3286  IDatabase $dbr,
3287  RevisionRecord $old = null,
3288  RevisionRecord $new = null,
3289  $options = []
3290  ) {
3291  $options = (array)$options;
3292  $oldCmp = '>';
3293  $newCmp = '<';
3294  if ( in_array( 'include_old', $options ) ) {
3295  $oldCmp = '>=';
3296  }
3297  if ( in_array( 'include_new', $options ) ) {
3298  $newCmp = '<=';
3299  }
3300  if ( in_array( 'include_both', $options ) ) {
3301  $oldCmp = '>=';
3302  $newCmp = '<=';
3303  }
3304 
3305  $conds = [];
3306  if ( $old ) {
3307  $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3308  $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId()}) " .
3309  "OR rev_timestamp > {$oldTs}";
3310  }
3311  if ( $new ) {
3312  $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3313  $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId()}) " .
3314  "OR rev_timestamp < {$newTs}";
3315  }
3316  return $conds;
3317  }
3318 
3340  public function getAuthorsBetween(
3341  $pageId,
3342  RevisionRecord $old = null,
3343  RevisionRecord $new = null,
3344  User $user = null,
3345  $max = null,
3346  $options = []
3347  ) {
3348  $this->assertRevisionParameter( 'old', $pageId, $old );
3349  $this->assertRevisionParameter( 'new', $pageId, $new );
3350  $options = (array)$options;
3351 
3352  // No DB query needed if old and new are the same revision.
3353  // Can't check for consecutive revisions with 'getParentId' for a similar
3354  // optimization as edge cases exist when there are revisions between
3355  //a revision and it's parent. See T185167 for more details.
3356  if ( $old && $new && $new->getId() === $old->getId() ) {
3357  if ( empty( $options ) ) {
3358  return [];
3359  } else {
3360  return $user ? [ $new->getUser( RevisionRecord::FOR_PUBLIC, $user ) ] : [ $new->getUser() ];
3361  }
3362  }
3363 
3364  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3365  $conds = array_merge(
3366  [
3367  'rev_page' => $pageId,
3368  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3369  ],
3370  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3371  );
3372 
3373  $queryOpts = [ 'DISTINCT' ];
3374  if ( $max !== null ) {
3375  $queryOpts['LIMIT'] = $max + 1;
3376  }
3377 
3378  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3379  return array_map( function ( $row ) {
3380  return new UserIdentityValue( (int)$row->rev_user, $row->rev_user_text, (int)$row->rev_actor );
3381  }, iterator_to_array( $dbr->select(
3382  array_merge( [ 'revision' ], $actorQuery['tables'] ),
3383  $actorQuery['fields'],
3384  $conds, __METHOD__,
3385  $queryOpts,
3386  $actorQuery['joins']
3387  ) ) );
3388  }
3389 
3411  public function countAuthorsBetween(
3412  $pageId,
3413  RevisionRecord $old = null,
3414  RevisionRecord $new = null,
3415  User $user = null,
3416  $max = null,
3417  $options = []
3418  ) {
3419  // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3420  // and creation of UserIdentity stuff.
3421  return count( $this->getAuthorsBetween( $pageId, $old, $new, $user, $max, $options ) );
3422  }
3423 
3444  public function countRevisionsBetween(
3445  $pageId,
3446  RevisionRecord $old = null,
3447  RevisionRecord $new = null,
3448  $max = null,
3449  $options = []
3450  ) {
3451  $this->assertRevisionParameter( 'old', $pageId, $old );
3452  $this->assertRevisionParameter( 'new', $pageId, $new );
3453 
3454  // No DB query needed if old and new are the same revision.
3455  // Can't check for consecutive revisions with 'getParentId' for a similar
3456  // optimization as edge cases exist when there are revisions between
3457  //a revision and it's parent. See T185167 for more details.
3458  if ( $old && $new && $new->getId() === $old->getId() ) {
3459  return 0;
3460  }
3461 
3462  $dbr = $this->getDBConnectionRef( DB_REPLICA );
3463  $conds = array_merge(
3464  [
3465  'rev_page' => $pageId,
3466  $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3467  ],
3468  $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3469  );
3470  if ( $max !== null ) {
3471  return $dbr->selectRowCount( 'revision', '1',
3472  $conds,
3473  __METHOD__,
3474  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3475  );
3476  } else {
3477  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3478  }
3479  }
3480 
3481  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3482 }
3483 
3488 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
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:2545
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:2488
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:2522
User\getId
getId()
Get the user's ID.
Definition: User.php:2255
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:2209
Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:280
Revision\RevisionStore\getKnownCurrentRevision
getKnownCurrentRevision(Title $title, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: RevisionStore.php:3184
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:208
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:1203
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:71
Revision\RevisionStore\initializeMutableRevisionFromArray
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
Definition: RevisionStore.php:2294
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:1871
Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags, Title $title)
Definition: RevisionStore.php:1576
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:2049
Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2839
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:442
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:3070
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\assertRevisionParameter
assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev=null)
Asserts that if revision is provided, it's saved and belongs to the page with provided pageId.
Definition: RevisionStore.php:3260
Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2746
Message
$res
$res
Definition: testCompression.php:54
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:118
$revQuery
$revQuery
Definition: testCompression.php:53
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:1153
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:2897
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:3142
$dbr
$dbr
Definition: testCompression.php:52
MediaWiki\Revision
Created by PhpStorm.
Definition: FallbackSlotRoleHandler.php:23
Revision
Definition: Revision.php:40
Revision\RevisionStore\getRevisionLimitConditions
getRevisionLimitConditions(IDatabase $dbr, RevisionRecord $old=null, RevisionRecord $new=null, $options=[])
Converts revision limits to query conditions.
Definition: RevisionStore.php:3285
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:1910
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:1397
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:2645
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:4426
Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1454
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:516
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:1520
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:2401
$blob
$blob
Definition: testCompression.php:67
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:371
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:1684
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:3034
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:409
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:563
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:677
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:2881
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:2382
$title
$title
Definition: testCompression.php:36
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
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:1900
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:618
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:331
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:2990
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
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:1610
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:279
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:1371
Revision\RevisionRecord\getParentId
getParentId()
Get parent revision ID (the original previous page revision).
Definition: RevisionRecord.php:295
$content
$content
Definition: router.php:78
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:522
Revision\RevisionRecord\DELETED_USER
const DELETED_USER
Definition: RevisionRecord.php:51
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:3013
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:2162
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:3116
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:396
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:490
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:2927
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:1474
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:269
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
Revision\RevisionRecord\FOR_PUBLIC
const FOR_PUBLIC
Definition: RevisionRecord.php:58
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:431
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:351
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:1556
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:3093
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:2567
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:1728
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:80
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:49
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:39
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:1416
MediaWiki\Storage\BlobStore\PARENT_HINT
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition: BlobStore.php:66
Revision\RevisionStore\getAuthorsBetween
getAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, User $user=null, $max=null, $options=[])
Get the authors between the given revisions or revisions.
Definition: RevisionStore.php:3340
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
$t
$t
Definition: testCompression.php:71
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Revision\RevisionStore\newRevisionFromRowAndSlots
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, $fromCache=false)
Definition: RevisionStore.php:1828
Revision\RevisionStore\loadRevisionFromTimestamp
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:2461
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:513
Revision\RevisionStore\countAuthorsBetween
countAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, User $user=null, $max=null, $options=[])
Get the number of authors between the given revisions.
Definition: RevisionStore.php:3411
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:52
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:465
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:2284
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:3244
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:1802
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:2599
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:2427
Revision\RevisionStore\countRevisionsBetween
countRevisionsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, $max=null, $options=[])
Get the number of revisions between the given revisions.
Definition: RevisionStore.php:3444
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