MediaWiki  master
RevisionStore.php
Go to the documentation of this file.
1 <?php
27 namespace MediaWiki\Revision;
28 
32 use Content;
35 use Hooks;
38 use IP;
48 use Message;
58 use Title;
59 use User;
67 
78  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
79 
80  const ROW_CACHE_KEY = 'revision-row-1.29';
81 
85  private $blobStore;
86 
90  private $dbDomain;
91 
96  private $contentHandlerUseDB = true;
97 
101  private $loadBalancer;
102 
106  private $cache;
107 
111  private $commentStore;
112 
117 
121  private $logger;
122 
127 
131  private $slotRoleStore;
132 
135 
138 
158  public function __construct(
159  ILoadBalancer $loadBalancer,
160  SqlBlobStore $blobStore,
163  NameTableStore $contentModelStore,
164  NameTableStore $slotRoleStore,
168  $dbDomain = false
169  ) {
170  Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
171  Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
172  Assert::parameter(
173  ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH,
174  '$mcrMigrationStage',
175  'Reading from the old and the new schema at the same time is not supported.'
176  );
177  Assert::parameter(
178  ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== 0,
179  '$mcrMigrationStage',
180  'Reading needs to be enabled for the old or the new schema.'
181  );
182  Assert::parameter(
184  '$mcrMigrationStage',
185  'Writing needs to be enabled for the old or the new schema.'
186  );
187  Assert::parameter(
190  '$mcrMigrationStage',
191  'Cannot read the old schema when not also writing it.'
192  );
193  Assert::parameter(
196  '$mcrMigrationStage',
197  'Cannot read the new schema when not also writing it.'
198  );
199 
200  $this->loadBalancer = $loadBalancer;
201  $this->blobStore = $blobStore;
202  $this->cache = $cache;
203  $this->commentStore = $commentStore;
204  $this->contentModelStore = $contentModelStore;
205  $this->slotRoleStore = $slotRoleStore;
206  $this->slotRoleRegistry = $slotRoleRegistry;
207  $this->mcrMigrationStage = $mcrMigrationStage;
208  $this->actorMigration = $actorMigration;
209  $this->dbDomain = $dbDomain;
210  $this->logger = new NullLogger();
211  }
212 
218  private function hasMcrSchemaFlags( $flags ) {
219  return ( $this->mcrMigrationStage & $flags ) === $flags;
220  }
221 
229  if ( $this->dbDomain !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
230  throw new RevisionAccessException(
231  "Cross-wiki content loading is not supported by the pre-MCR schema"
232  );
233  }
234  }
235 
236  public function setLogger( LoggerInterface $logger ) {
237  $this->logger = $logger;
238  }
239 
243  public function isReadOnly() {
244  return $this->blobStore->isReadOnly();
245  }
246 
250  public function getContentHandlerUseDB() {
252  }
253 
262  ) {
263  if ( !$contentHandlerUseDB ) {
264  throw new MWException(
265  'Content model must be stored in the database for multi content revision migration.'
266  );
267  }
268  }
269  $this->contentHandlerUseDB = $contentHandlerUseDB;
270  }
271 
275  private function getDBLoadBalancer() {
276  return $this->loadBalancer;
277  }
278 
284  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
285  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
286  return $this->getDBConnectionRef( $mode );
287  }
288 
295  private function getDBConnectionRef( $mode, $groups = [] ) {
296  $lb = $this->getDBLoadBalancer();
297  return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
298  }
299 
314  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
315  if ( !$pageId && !$revId ) {
316  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
317  }
318 
319  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
320  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
321  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
322  $queryFlags = self::READ_NORMAL;
323  }
324 
325  $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
326  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
327  $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
328 
329  // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
330  if ( $canUseTitleNewFromId ) {
331  // TODO: better foreign title handling (introduce TitleFactory)
332  $title = Title::newFromID( $pageId, $titleFlags );
333  if ( $title ) {
334  return $title;
335  }
336  }
337 
338  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
339  $canUseRevId = ( $revId !== null && $revId > 0 );
340 
341  if ( $canUseRevId ) {
342  $dbr = $this->getDBConnectionRef( $dbMode );
343  // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
344  $row = $dbr->selectRow(
345  [ 'revision', 'page' ],
346  [
347  'page_namespace',
348  'page_title',
349  'page_id',
350  'page_latest',
351  'page_is_redirect',
352  'page_len',
353  ],
354  [ 'rev_id' => $revId ],
355  __METHOD__,
356  $dbOptions,
357  [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
358  );
359  if ( $row ) {
360  // TODO: better foreign title handling (introduce TitleFactory)
361  return Title::newFromRow( $row );
362  }
363  }
364 
365  // If we still don't have a title, fallback to master if that wasn't already happening.
366  if ( $dbMode !== DB_MASTER ) {
367  $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
368  if ( $title ) {
369  $this->logger->info(
370  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
371  [ 'trace' => wfBacktrace() ]
372  );
373  return $title;
374  }
375  }
376 
377  throw new RevisionAccessException(
378  "Could not determine title for page ID $pageId and revision ID $revId"
379  );
380  }
381 
389  private function failOnNull( $value, $name ) {
390  if ( $value === null ) {
391  throw new IncompleteRevisionException(
392  "$name must not be " . var_export( $value, true ) . "!"
393  );
394  }
395 
396  return $value;
397  }
398 
406  private function failOnEmpty( $value, $name ) {
407  if ( $value === null || $value === 0 || $value === '' ) {
408  throw new IncompleteRevisionException(
409  "$name must not be " . var_export( $value, true ) . "!"
410  );
411  }
412 
413  return $value;
414  }
415 
428  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
429  // TODO: pass in a DBTransactionContext instead of a database connection.
430  $this->checkDatabaseDomain( $dbw );
431 
432  $slotRoles = $rev->getSlotRoles();
433 
434  // Make sure the main slot is always provided throughout migration
435  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
436  throw new InvalidArgumentException(
437  'main slot must be provided'
438  );
439  }
440 
441  // If we are not writing into the new schema, we can't support extra slots.
443  && $slotRoles !== [ SlotRecord::MAIN ]
444  ) {
445  throw new InvalidArgumentException(
446  'Only the main slot is supported when not writing to the MCR enabled schema!'
447  );
448  }
449 
450  // As long as we are not reading from the new schema, we don't want to write extra slots.
452  && $slotRoles !== [ SlotRecord::MAIN ]
453  ) {
454  throw new InvalidArgumentException(
455  'Only the main slot is supported when not reading from the MCR enabled schema!'
456  );
457  }
458 
459  // Checks
460  $this->failOnNull( $rev->getSize(), 'size field' );
461  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
462  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
463  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
464  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
465  $this->failOnNull( $user->getId(), 'user field' );
466  $this->failOnEmpty( $user->getName(), 'user_text field' );
467 
468  if ( !$rev->isReadyForInsertion() ) {
469  // This is here for future-proofing. At the time this check being added, it
470  // was redundant to the individual checks above.
471  throw new IncompleteRevisionException( 'Revision is incomplete' );
472  }
473 
474  // TODO: we shouldn't need an actual Title here.
476  $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
477 
478  $parentId = $rev->getParentId() === null
479  ? $this->getPreviousRevisionId( $dbw, $rev )
480  : $rev->getParentId();
481 
483  $rev = $dbw->doAtomicSection(
484  __METHOD__,
485  function ( IDatabase $dbw, $fname ) use (
486  $rev,
487  $user,
488  $comment,
489  $title,
490  $pageId,
491  $parentId
492  ) {
493  return $this->insertRevisionInternal(
494  $rev,
495  $dbw,
496  $user,
497  $comment,
498  $title,
499  $pageId,
500  $parentId
501  );
502  }
503  );
504 
505  // sanity checks
506  Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
507  Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
508  Assert::postcondition(
509  $rev->getComment( RevisionRecord::RAW ) !== null,
510  'revision must have a comment'
511  );
512  Assert::postcondition(
513  $rev->getUser( RevisionRecord::RAW ) !== null,
514  'revision must have a user'
515  );
516 
517  // Trigger exception if the main slot is missing.
518  // Technically, this could go away after MCR migration: while
519  // calling code may require a main slot to exist, RevisionStore
520  // really should not know or care about that requirement.
522 
523  foreach ( $slotRoles as $role ) {
524  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
525  Assert::postcondition(
526  $slot->getContent() !== null,
527  $role . ' slot must have content'
528  );
529  Assert::postcondition(
530  $slot->hasRevision(),
531  $role . ' slot must have a revision associated'
532  );
533  }
534 
535  Hooks::run( 'RevisionRecordInserted', [ $rev ] );
536 
537  // TODO: deprecate in 1.32!
538  $legacyRevision = new Revision( $rev );
539  Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
540 
541  return $rev;
542  }
543 
544  private function insertRevisionInternal(
546  IDatabase $dbw,
547  User $user,
548  CommentStoreComment $comment,
549  Title $title,
550  $pageId,
551  $parentId
552  ) {
553  $slotRoles = $rev->getSlotRoles();
554 
555  $revisionRow = $this->insertRevisionRowOn(
556  $dbw,
557  $rev,
558  $title,
559  $parentId
560  );
561 
562  $revisionId = $revisionRow['rev_id'];
563 
564  $blobHints = [
565  BlobStore::PAGE_HINT => $pageId,
566  BlobStore::REVISION_HINT => $revisionId,
567  BlobStore::PARENT_HINT => $parentId,
568  ];
569 
570  $newSlots = [];
571  foreach ( $slotRoles as $role ) {
572  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
573 
574  // If the SlotRecord already has a revision ID set, this means it already exists
575  // in the database, and should already belong to the current revision.
576  // However, a slot may already have a revision, but no content ID, if the slot
577  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
578  // mode, and the respective archive row was not yet migrated to the new schema.
579  // In that case, a new slot row (and content row) must be inserted even during
580  // undeletion.
581  if ( $slot->hasRevision() && $slot->hasContentId() ) {
582  // TODO: properly abort transaction if the assertion fails!
583  Assert::parameter(
584  $slot->getRevision() === $revisionId,
585  'slot role ' . $slot->getRole(),
586  'Existing slot should belong to revision '
587  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
588  );
589 
590  // Slot exists, nothing to do, move along.
591  // This happens when restoring archived revisions.
592 
593  $newSlots[$role] = $slot;
594 
595  // Write the main slot's text ID to the revision table for backwards compatibility
596  if ( $slot->getRole() === SlotRecord::MAIN
598  ) {
599  $blobAddress = $slot->getAddress();
600  $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
601  }
602  } else {
603  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
604  }
605  }
606 
607  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
608 
609  $rev = new RevisionStoreRecord(
610  $title,
611  $user,
612  $comment,
613  (object)$revisionRow,
614  new RevisionSlots( $newSlots ),
615  $this->dbDomain
616  );
617 
618  return $rev;
619  }
620 
628  private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
629  $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
630  if ( !$textId ) {
631  throw new LogicException(
632  'Blob address not supported in 1.29 database schema: ' . $blobAddress
633  );
634  }
635 
636  // getTextIdFromAddress() is free to insert something into the text table, so $textId
637  // may be a new value, not anything already contained in $blobAddress.
638  $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
639 
640  $dbw->update(
641  'revision',
642  [ 'rev_text_id' => $textId ],
643  [ 'rev_id' => $revisionId ],
644  __METHOD__
645  );
646 
647  return $textId;
648  }
649 
658  private function insertSlotOn(
659  IDatabase $dbw,
660  $revisionId,
661  SlotRecord $protoSlot,
662  Title $title,
663  array $blobHints = []
664  ) {
665  if ( $protoSlot->hasAddress() ) {
666  $blobAddress = $protoSlot->getAddress();
667  } else {
668  $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
669  }
670 
671  $contentId = null;
672 
673  // Write the main slot's text ID to the revision table for backwards compatibility
674  if ( $protoSlot->getRole() === SlotRecord::MAIN
676  ) {
677  // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
678  // with the real content ID below.
679  $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
680  $contentId = $this->emulateContentId( $textId );
681  }
682 
683  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
684  if ( $protoSlot->hasContentId() ) {
685  $contentId = $protoSlot->getContentId();
686  } else {
687  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
688  }
689 
690  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
691  }
692 
693  $savedSlot = SlotRecord::newSaved(
694  $revisionId,
695  $contentId,
696  $blobAddress,
697  $protoSlot
698  );
699 
700  return $savedSlot;
701  }
702 
710  private function insertIpChangesRow(
711  IDatabase $dbw,
712  User $user,
714  $revisionId
715  ) {
716  if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
717  $ipcRow = [
718  'ipc_rev_id' => $revisionId,
719  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
720  'ipc_hex' => IP::toHex( $user->getName() ),
721  ];
722  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
723  }
724  }
725 
737  private function insertRevisionRowOn(
738  IDatabase $dbw,
740  Title $title,
741  $parentId
742  ) {
743  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
744 
745  list( $commentFields, $commentCallback ) =
746  $this->commentStore->insertWithTempTable(
747  $dbw,
748  'rev_comment',
750  );
751  $revisionRow += $commentFields;
752 
753  list( $actorFields, $actorCallback ) =
754  $this->actorMigration->getInsertValuesWithTempTable(
755  $dbw,
756  'rev_user',
758  );
759  $revisionRow += $actorFields;
760 
761  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
762 
763  if ( !isset( $revisionRow['rev_id'] ) ) {
764  // only if auto-increment was used
765  $revisionRow['rev_id'] = intval( $dbw->insertId() );
766 
767  if ( $dbw->getType() === 'mysql' ) {
768  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
769  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
770  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
771 
772  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
773  $table = 'archive';
774  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
775  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
776  if ( $maxRevId2 >= $maxRevId ) {
777  $maxRevId = $maxRevId2;
778  $table = 'slots';
779  }
780  }
781 
782  if ( $maxRevId >= $revisionRow['rev_id'] ) {
783  $this->logger->debug(
784  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
785  . ' Trying to fix it.',
786  [
787  'revid' => $revisionRow['rev_id'],
788  'table' => $table,
789  'maxrevid' => $maxRevId,
790  ]
791  );
792 
793  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
794  throw new MWException( 'Failed to get database lock for T202032' );
795  }
796  $fname = __METHOD__;
797  $dbw->onTransactionResolution(
798  function ( $trigger, IDatabase $dbw ) use ( $fname ) {
799  $dbw->unlock( 'fix-for-T202032', $fname );
800  }
801  );
802 
803  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
804 
805  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
806  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
807  // inserts too, though, at least on MariaDB 10.1.29.
808  //
809  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
810  // transactions in this code path thanks to the row lock from the original ->insert() above.
811  //
812  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
813  // that's for non-MySQL DBs.
814  $row1 = $dbw->query(
815  $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
816  )->fetchObject();
817  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
818  $row2 = $dbw->query(
819  $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
820  . ' FOR UPDATE'
821  )->fetchObject();
822  } else {
823  $row2 = null;
824  }
825  $maxRevId = max(
826  $maxRevId,
827  $row1 ? intval( $row1->v ) : 0,
828  $row2 ? intval( $row2->v ) : 0
829  );
830 
831  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
832  // transactions will throw a duplicate key error here. It doesn't seem worth trying
833  // to avoid that.
834  $revisionRow['rev_id'] = $maxRevId + 1;
835  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
836  }
837  }
838  }
839 
840  $commentCallback( $revisionRow['rev_id'] );
841  $actorCallback( $revisionRow['rev_id'], $revisionRow );
842 
843  return $revisionRow;
844  }
845 
856  private function getBaseRevisionRow(
857  IDatabase $dbw,
859  Title $title,
860  $parentId
861  ) {
862  // Record the edit in revisions
863  $revisionRow = [
864  'rev_page' => $rev->getPageId(),
865  'rev_parent_id' => $parentId,
866  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
867  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
868  'rev_deleted' => $rev->getVisibility(),
869  'rev_len' => $rev->getSize(),
870  'rev_sha1' => $rev->getSha1(),
871  ];
872 
873  if ( $rev->getId() !== null ) {
874  // Needed to restore revisions with their original ID
875  $revisionRow['rev_id'] = $rev->getId();
876  }
877 
878  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
879  // In non MCR mode this IF section will relate to the main slot
880  $mainSlot = $rev->getSlot( SlotRecord::MAIN );
881  $model = $mainSlot->getModel();
882  $format = $mainSlot->getFormat();
883 
884  // MCR migration note: rev_content_model and rev_content_format will go away
885  if ( $this->contentHandlerUseDB ) {
887 
888  $defaultModel = ContentHandler::getDefaultModelFor( $title );
889  $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
890 
891  $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
892  $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
893  }
894  }
895 
896  return $revisionRow;
897  }
898 
907  private function storeContentBlob(
908  SlotRecord $slot,
909  Title $title,
910  array $blobHints = []
911  ) {
912  $content = $slot->getContent();
913  $format = $content->getDefaultFormat();
914  $model = $content->getModel();
915 
916  $this->checkContent( $content, $title, $slot->getRole() );
917 
918  return $this->blobStore->storeBlob(
919  $content->serialize( $format ),
920  // These hints "leak" some information from the higher abstraction layer to
921  // low level storage to allow for optimization.
922  array_merge(
923  $blobHints,
924  [
925  BlobStore::DESIGNATION_HINT => 'page-content',
926  BlobStore::ROLE_HINT => $slot->getRole(),
927  BlobStore::SHA1_HINT => $slot->getSha1(),
928  BlobStore::MODEL_HINT => $model,
929  BlobStore::FORMAT_HINT => $format,
930  ]
931  )
932  );
933  }
934 
941  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
942  $slotRow = [
943  'slot_revision_id' => $revisionId,
944  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
945  'slot_content_id' => $contentId,
946  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
947  // that we just inserted.
948  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
949  ];
950  $dbw->insert( 'slots', $slotRow, __METHOD__ );
951  }
952 
959  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
960  $contentRow = [
961  'content_size' => $slot->getSize(),
962  'content_sha1' => $slot->getSha1(),
963  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
964  'content_address' => $blobAddress,
965  ];
966  $dbw->insert( 'content', $contentRow, __METHOD__ );
967  return intval( $dbw->insertId() );
968  }
969 
980  private function checkContent( Content $content, Title $title, $role ) {
981  // Note: may return null for revisions that have not yet been inserted
982 
983  $model = $content->getModel();
984  $format = $content->getDefaultFormat();
985  $handler = $content->getContentHandler();
986 
987  $name = "$title";
988 
989  if ( !$handler->isSupportedFormat( $format ) ) {
990  throw new MWException( "Can't use format $format with content model $model on $name" );
991  }
992 
993  if ( !$this->contentHandlerUseDB ) {
994  // if $wgContentHandlerUseDB is not set,
995  // all revisions must use the default content model and format.
996 
998 
999  $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
1000  $defaultModel = $roleHandler->getDefaultModel( $title );
1001  $defaultHandler = ContentHandler::getForModelID( $defaultModel );
1002  $defaultFormat = $defaultHandler->getDefaultFormat();
1003 
1004  if ( $model != $defaultModel ) {
1005  throw new MWException( "Can't save non-default content model with "
1006  . "\$wgContentHandlerUseDB disabled: model is $model, "
1007  . "default for $name is $defaultModel"
1008  );
1009  }
1010 
1011  if ( $format != $defaultFormat ) {
1012  throw new MWException( "Can't use non-default content format with "
1013  . "\$wgContentHandlerUseDB disabled: format is $format, "
1014  . "default for $name is $defaultFormat"
1015  );
1016  }
1017  }
1018 
1019  if ( !$content->isValid() ) {
1020  throw new MWException(
1021  "New content for $name is not valid! Content model is $model"
1022  );
1023  }
1024  }
1025 
1051  public function newNullRevision(
1052  IDatabase $dbw,
1053  Title $title,
1054  CommentStoreComment $comment,
1055  $minor,
1056  User $user
1057  ) {
1058  $this->checkDatabaseDomain( $dbw );
1059 
1060  $pageId = $title->getArticleID();
1061 
1062  // T51581: Lock the page table row to ensure no other process
1063  // is adding a revision to the page at the same time.
1064  // Avoid locking extra tables, compare T191892.
1065  $pageLatest = $dbw->selectField(
1066  'page',
1067  'page_latest',
1068  [ 'page_id' => $pageId ],
1069  __METHOD__,
1070  [ 'FOR UPDATE' ]
1071  );
1072 
1073  if ( !$pageLatest ) {
1074  return null;
1075  }
1076 
1077  // Fetch the actual revision row from master, without locking all extra tables.
1078  $oldRevision = $this->loadRevisionFromConds(
1079  $dbw,
1080  [ 'rev_id' => intval( $pageLatest ) ],
1081  self::READ_LATEST,
1082  $title
1083  );
1084 
1085  if ( !$oldRevision ) {
1086  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1087  $this->logger->error(
1088  $msg,
1089  [ 'exception' => new RuntimeException( $msg ) ]
1090  );
1091  return null;
1092  }
1093 
1094  // Construct the new revision
1095  $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1096  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1097 
1098  $newRevision->setComment( $comment );
1099  $newRevision->setUser( $user );
1100  $newRevision->setTimestamp( $timestamp );
1101  $newRevision->setMinorEdit( $minor );
1102 
1103  return $newRevision;
1104  }
1105 
1116  $rc = $this->getRecentChange( $rev );
1117  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1118  return $rc->getAttribute( 'rc_id' );
1119  } else {
1120  return 0;
1121  }
1122  }
1123 
1137  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1138  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1139  $db = $this->getDBConnectionRef( $dbType );
1140 
1141  $userIdentity = $rev->getUser( RevisionRecord::RAW );
1142 
1143  if ( !$userIdentity ) {
1144  // If the revision has no user identity, chances are it never went
1145  // into the database, and doesn't have an RC entry.
1146  return null;
1147  }
1148 
1149  // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
1150  $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
1152  [
1153  $actorWhere['conds'],
1154  'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
1155  'rc_this_oldid' => $rev->getId()
1156  ],
1157  __METHOD__,
1158  $dbType
1159  );
1160 
1161  // XXX: cache this locally? Glue it to the RevisionRecord?
1162  return $rc;
1163  }
1164 
1172  private static function mapArchiveFields( $archiveRow ) {
1173  $fieldMap = [
1174  // keep with ar prefix:
1175  'ar_id' => 'ar_id',
1176 
1177  // not the same suffix:
1178  'ar_page_id' => 'rev_page',
1179  'ar_rev_id' => 'rev_id',
1180 
1181  // same suffix:
1182  'ar_text_id' => 'rev_text_id',
1183  'ar_timestamp' => 'rev_timestamp',
1184  'ar_user_text' => 'rev_user_text',
1185  'ar_user' => 'rev_user',
1186  'ar_actor' => 'rev_actor',
1187  'ar_minor_edit' => 'rev_minor_edit',
1188  'ar_deleted' => 'rev_deleted',
1189  'ar_len' => 'rev_len',
1190  'ar_parent_id' => 'rev_parent_id',
1191  'ar_sha1' => 'rev_sha1',
1192  'ar_comment' => 'rev_comment',
1193  'ar_comment_cid' => 'rev_comment_cid',
1194  'ar_comment_id' => 'rev_comment_id',
1195  'ar_comment_text' => 'rev_comment_text',
1196  'ar_comment_data' => 'rev_comment_data',
1197  'ar_comment_old' => 'rev_comment_old',
1198  'ar_content_format' => 'rev_content_format',
1199  'ar_content_model' => 'rev_content_model',
1200  ];
1201 
1202  $revRow = new stdClass();
1203  foreach ( $fieldMap as $arKey => $revKey ) {
1204  if ( property_exists( $archiveRow, $arKey ) ) {
1205  $revRow->$revKey = $archiveRow->$arKey;
1206  }
1207  }
1208 
1209  return $revRow;
1210  }
1211 
1222  private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
1223  $mainSlotRow = new stdClass();
1224  $mainSlotRow->role_name = SlotRecord::MAIN;
1225  $mainSlotRow->model_name = null;
1226  $mainSlotRow->slot_revision_id = null;
1227  $mainSlotRow->slot_content_id = null;
1228  $mainSlotRow->content_address = null;
1229 
1230  $content = null;
1231  $blobData = null;
1232  $blobFlags = null;
1233 
1234  if ( is_object( $row ) ) {
1235  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1236  // Don't emulate from a row when using the new schema.
1237  // Emulating from an array is still OK.
1238  throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
1239  }
1240 
1241  // archive row
1242  if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
1243  $row = $this->mapArchiveFields( $row );
1244  }
1245 
1246  if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
1247  $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
1248  $row->rev_text_id
1249  );
1250  }
1251 
1252  // This is used by null-revisions
1253  $mainSlotRow->slot_origin = isset( $row->slot_origin )
1254  ? intval( $row->slot_origin )
1255  : null;
1256 
1257  if ( isset( $row->old_text ) ) {
1258  // this happens when the text-table gets joined directly, in the pre-1.30 schema
1259  $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
1260  // Check against selects that might have not included old_flags
1261  if ( !property_exists( $row, 'old_flags' ) ) {
1262  throw new InvalidArgumentException( 'old_flags was not set in $row' );
1263  }
1264  $blobFlags = $row->old_flags ?? '';
1265  }
1266 
1267  $mainSlotRow->slot_revision_id = intval( $row->rev_id );
1268 
1269  $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
1270  $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
1271  $mainSlotRow->model_name = isset( $row->rev_content_model )
1272  ? strval( $row->rev_content_model )
1273  : null;
1274  // XXX: in the future, we'll probably always use the default format, and drop content_format
1275  $mainSlotRow->format_name = isset( $row->rev_content_format )
1276  ? strval( $row->rev_content_format )
1277  : null;
1278 
1279  if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
1280  // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1281  $mainSlotRow->slot_content_id
1282  = $this->emulateContentId( intval( $row->rev_text_id ) );
1283  }
1284  } elseif ( is_array( $row ) ) {
1285  $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
1286 
1287  $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
1288  ? intval( $row['slot_origin'] )
1289  : null;
1290  $mainSlotRow->content_address = isset( $row['text_id'] )
1291  ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
1292  : null;
1293  $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
1294  $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
1295 
1296  $mainSlotRow->model_name = isset( $row['content_model'] )
1297  ? strval( $row['content_model'] ) : null; // XXX: must be a string!
1298  // XXX: in the future, we'll probably always use the default format, and drop content_format
1299  $mainSlotRow->format_name = isset( $row['content_format'] )
1300  ? strval( $row['content_format'] ) : null;
1301  $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
1302  // XXX: If the flags field is not set then $blobFlags should be null so that no
1303  // decoding will happen. An empty string will result in default decodings.
1304  $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
1305 
1306  // if we have a Content object, override mText and mContentModel
1307  if ( !empty( $row['content'] ) ) {
1308  if ( !( $row['content'] instanceof Content ) ) {
1309  throw new MWException( 'content field must contain a Content object.' );
1310  }
1311 
1313  $content = $row['content'];
1314  $handler = $content->getContentHandler();
1315 
1316  $mainSlotRow->model_name = $content->getModel();
1317 
1318  // XXX: in the future, we'll probably always use the default format.
1319  if ( $mainSlotRow->format_name === null ) {
1320  $mainSlotRow->format_name = $handler->getDefaultFormat();
1321  }
1322  }
1323 
1324  if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
1325  // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1326  $mainSlotRow->slot_content_id
1327  = $this->emulateContentId( intval( $row['text_id'] ) );
1328  }
1329  } else {
1330  throw new MWException( 'Revision constructor passed invalid row format.' );
1331  }
1332 
1333  // With the old schema, the content changes with every revision,
1334  // except for null-revisions.
1335  if ( !isset( $mainSlotRow->slot_origin ) ) {
1336  $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
1337  }
1338 
1339  if ( $mainSlotRow->model_name === null ) {
1340  $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
1342 
1343  return $this->slotRoleRegistry->getRoleHandler( $slot->getRole() )
1344  ->getDefaultModel( $title );
1345  };
1346  }
1347 
1348  if ( !$content ) {
1349  // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
1350  // is missing, but "empty revisions" with no content are used in some edge cases.
1351 
1352  $content = function ( SlotRecord $slot )
1353  use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
1354  {
1355  return $this->loadSlotContent(
1356  $slot,
1357  $blobData,
1358  $blobFlags,
1359  $mainSlotRow->format_name,
1360  $queryFlags
1361  );
1362  };
1363  }
1364 
1365  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
1366  // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
1367  // the inherited slot to have the same content_id as the original slot. In that case,
1368  // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
1369  $mainSlotRow->slot_content_id =
1370  function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
1371  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1372  return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
1373  };
1374  }
1375 
1376  return new SlotRecord( $mainSlotRow, $content );
1377  }
1378 
1390  private function emulateContentId( $textId ) {
1391  // Return a negative number to ensure the ID is distinct from any real content IDs
1392  // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
1393  // mode.
1394  return -$textId;
1395  }
1396 
1416  private function loadSlotContent(
1417  SlotRecord $slot,
1418  $blobData = null,
1419  $blobFlags = null,
1420  $blobFormat = null,
1421  $queryFlags = 0
1422  ) {
1423  if ( $blobData !== null ) {
1424  Assert::parameterType( 'string', $blobData, '$blobData' );
1425  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1426 
1427  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1428 
1429  if ( $blobFlags === null ) {
1430  // No blob flags, so use the blob verbatim.
1431  $data = $blobData;
1432  } else {
1433  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1434  if ( $data === false ) {
1435  throw new RevisionAccessException(
1436  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1437  );
1438  }
1439  }
1440 
1441  } else {
1442  $address = $slot->getAddress();
1443  try {
1444  $data = $this->blobStore->getBlob( $address, $queryFlags );
1445  } catch ( BlobAccessException $e ) {
1446  throw new RevisionAccessException(
1447  "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1448  );
1449  }
1450  }
1451 
1452  // Unserialize content
1454 
1455  $content = $handler->unserializeContent( $data, $blobFormat );
1456  return $content;
1457  }
1458 
1473  public function getRevisionById( $id, $flags = 0 ) {
1474  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1475  }
1476 
1493  public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1494  // TODO should not require Title in future (T206498)
1495  $title = Title::newFromLinkTarget( $linkTarget );
1496  $conds = [
1497  'page_namespace' => $title->getNamespace(),
1498  'page_title' => $title->getDBkey()
1499  ];
1500  if ( $revId ) {
1501  // Use the specified revision ID.
1502  // Note that we use newRevisionFromConds here because we want to retry
1503  // and fall back to master if the page is not found on a replica.
1504  // Since the caller supplied a revision ID, we are pretty sure the revision is
1505  // supposed to exist, so we should try hard to find it.
1506  $conds['rev_id'] = $revId;
1507  return $this->newRevisionFromConds( $conds, $flags, $title );
1508  } else {
1509  // Use a join to get the latest revision.
1510  // Note that we don't use newRevisionFromConds here because we don't want to retry
1511  // and fall back to master. The assumption is that we only want to force the fallback
1512  // if we are quite sure the revision exists because the caller supplied a revision ID.
1513  // If the page isn't found at all on a replica, it probably simply does not exist.
1514  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1515 
1516  $conds[] = 'rev_id=page_latest';
1517  $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1518 
1519  return $rev;
1520  }
1521  }
1522 
1539  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1540  $conds = [ 'page_id' => $pageId ];
1541  if ( $revId ) {
1542  // Use the specified revision ID.
1543  // Note that we use newRevisionFromConds here because we want to retry
1544  // and fall back to master if the page is not found on a replica.
1545  // Since the caller supplied a revision ID, we are pretty sure the revision is
1546  // supposed to exist, so we should try hard to find it.
1547  $conds['rev_id'] = $revId;
1548  return $this->newRevisionFromConds( $conds, $flags );
1549  } else {
1550  // Use a join to get the latest revision.
1551  // Note that we don't use newRevisionFromConds here because we don't want to retry
1552  // and fall back to master. The assumption is that we only want to force the fallback
1553  // if we are quite sure the revision exists because the caller supplied a revision ID.
1554  // If the page isn't found at all on a replica, it probably simply does not exist.
1555  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1556 
1557  $conds[] = 'rev_id=page_latest';
1558  $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1559 
1560  return $rev;
1561  }
1562  }
1563 
1575  public function getRevisionByTimestamp( $title, $timestamp ) {
1576  $db = $this->getDBConnectionRef( DB_REPLICA );
1577  return $this->newRevisionFromConds(
1578  [
1579  'rev_timestamp' => $db->timestamp( $timestamp ),
1580  'page_namespace' => $title->getNamespace(),
1581  'page_title' => $title->getDBkey()
1582  ],
1583  0,
1584  $title
1585  );
1586  }
1587 
1595  private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
1596  $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1597 
1598  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1599  $db = $this->getDBConnectionRef( $dbMode );
1600 
1601  $res = $db->select(
1602  $revQuery['tables'],
1603  $revQuery['fields'],
1604  [
1605  'slot_revision_id' => $revId,
1606  ],
1607  __METHOD__,
1608  $dbOptions,
1609  $revQuery['joins']
1610  );
1611 
1612  $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
1613 
1614  return $slots;
1615  }
1616 
1627  private function constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title ) {
1628  $slots = [];
1629 
1630  foreach ( $slotRows as $row ) {
1631  // Resolve role names and model names from in-memory cache, if they were not joined in.
1632  if ( !isset( $row->role_name ) ) {
1633  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1634  }
1635 
1636  if ( !isset( $row->model_name ) ) {
1637  if ( isset( $row->content_model ) ) {
1638  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1639  } else {
1640  // We may get here if $row->model_name is set but null, perhaps because it
1641  // came from rev_content_model, which is NULL for the default model.
1642  $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1643  $row->model_name = $slotRoleHandler->getDefaultModel( $title );
1644  }
1645  }
1646 
1647  if ( !isset( $row->content_id ) && isset( $row->rev_text_id ) ) {
1648  $row->slot_content_id
1649  = $this->emulateContentId( intval( $row->rev_text_id ) );
1650  }
1651 
1652  $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags ) {
1653  return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
1654  };
1655 
1656  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1657  }
1658 
1659  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1660  throw new RevisionAccessException(
1661  'Main slot of revision ' . $revId . ' not found in database!'
1662  );
1663  }
1664 
1665  return $slots;
1666  }
1667 
1683  private function newRevisionSlots(
1684  $revId,
1685  $revisionRow,
1686  $slotRows,
1687  $queryFlags,
1688  Title $title
1689  ) {
1690  if ( $slotRows ) {
1691  $slots = new RevisionSlots(
1692  $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
1693  );
1694  } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1695  $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
1696  // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive
1697  $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
1698  } else {
1699  // XXX: do we need the same kind of caching here
1700  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1701 
1702  $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
1703  return $this->loadSlotRecords( $revId, $queryFlags, $title );
1704  } );
1705  }
1706 
1707  return $slots;
1708  }
1709 
1727  public function newRevisionFromArchiveRow(
1728  $row,
1729  $queryFlags = 0,
1730  Title $title = null,
1731  array $overrides = []
1732  ) {
1733  Assert::parameterType( 'object', $row, '$row' );
1734 
1735  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1736  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1737 
1738  if ( !$title && isset( $overrides['title'] ) ) {
1739  if ( !( $overrides['title'] instanceof Title ) ) {
1740  throw new MWException( 'title field override must contain a Title object.' );
1741  }
1742 
1743  $title = $overrides['title'];
1744  }
1745 
1746  if ( !isset( $title ) ) {
1747  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1748  $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1749  } else {
1750  throw new InvalidArgumentException(
1751  'A Title or ar_namespace and ar_title must be given'
1752  );
1753  }
1754  }
1755 
1756  foreach ( $overrides as $key => $value ) {
1757  $field = "ar_$key";
1758  $row->$field = $value;
1759  }
1760 
1761  try {
1763  $row->ar_user ?? null,
1764  $row->ar_user_text ?? null,
1765  $row->ar_actor ?? null,
1766  $this->dbDomain
1767  );
1768  } catch ( InvalidArgumentException $ex ) {
1769  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1770  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1771  }
1772 
1773  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1774  // Legacy because $row may have come from self::selectFields()
1775  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1776 
1777  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, null, $queryFlags, $title );
1778 
1779  return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
1780  }
1781 
1794  public function newRevisionFromRow(
1795  $row,
1796  $queryFlags = 0,
1797  Title $title = null,
1798  $fromCache = false
1799  ) {
1800  return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
1801  }
1802 
1820  $row,
1821  $slotRows,
1822  $queryFlags = 0,
1823  Title $title = null,
1824  $fromCache = false
1825  ) {
1826  Assert::parameterType( 'object', $row, '$row' );
1827 
1828  if ( !$title ) {
1829  $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
1830  $revId = $row->rev_id ?? 0;
1831 
1832  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1833  }
1834 
1835  if ( !isset( $row->page_latest ) ) {
1836  $row->page_latest = $title->getLatestRevID();
1837  if ( $row->page_latest === 0 && $title->exists() ) {
1838  wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1839  }
1840  }
1841 
1842  try {
1844  $row->rev_user ?? null,
1845  $row->rev_user_text ?? null,
1846  $row->rev_actor ?? null,
1847  $this->dbDomain
1848  );
1849  } catch ( InvalidArgumentException $ex ) {
1850  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1851  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1852  }
1853 
1854  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1855  // Legacy because $row may have come from self::selectFields()
1856  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1857 
1858  $slots = $this->newRevisionSlots( $row->rev_id, $row, $slotRows, $queryFlags, $title );
1859 
1860  // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1861  if ( $fromCache ) {
1863  function ( $revId ) use ( $queryFlags ) {
1864  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1865  return $this->fetchRevisionRowFromConds(
1866  $db,
1867  [ 'rev_id' => intval( $revId ) ]
1868  );
1869  },
1870  $title, $user, $comment, $row, $slots, $this->dbDomain
1871  );
1872  } else {
1873  $rev = new RevisionStoreRecord(
1874  $title, $user, $comment, $row, $slots, $this->dbDomain );
1875  }
1876  return $rev;
1877  }
1878 
1894  array $fields,
1895  $queryFlags = 0,
1896  Title $title = null
1897  ) {
1898  if ( !$title && isset( $fields['title'] ) ) {
1899  if ( !( $fields['title'] instanceof Title ) ) {
1900  throw new MWException( 'title field must contain a Title object.' );
1901  }
1902 
1903  $title = $fields['title'];
1904  }
1905 
1906  if ( !$title ) {
1907  $pageId = $fields['page'] ?? 0;
1908  $revId = $fields['id'] ?? 0;
1909 
1910  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1911  }
1912 
1913  if ( !isset( $fields['page'] ) ) {
1914  $fields['page'] = $title->getArticleID( $queryFlags );
1915  }
1916 
1917  // if we have a content object, use it to set the model and type
1918  if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
1919  && !is_array( $fields['content'] )
1920  ) {
1921  throw new MWException(
1922  'content field must contain a Content object or an array of Content objects.'
1923  );
1924  }
1925 
1926  if ( !empty( $fields['text_id'] ) ) {
1927  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
1928  throw new MWException( "The text_id field is only available in the pre-MCR schema" );
1929  }
1930 
1931  if ( !empty( $fields['content'] ) ) {
1932  throw new MWException(
1933  "Text already stored in external store (id {$fields['text_id']}), " .
1934  "can't specify content object"
1935  );
1936  }
1937  }
1938 
1939  if (
1940  isset( $fields['comment'] )
1941  && !( $fields['comment'] instanceof CommentStoreComment )
1942  ) {
1943  $commentData = $fields['comment_data'] ?? null;
1944 
1945  if ( $fields['comment'] instanceof Message ) {
1946  $fields['comment'] = CommentStoreComment::newUnsavedComment(
1947  $fields['comment'],
1948  $commentData
1949  );
1950  } else {
1951  $commentText = trim( strval( $fields['comment'] ) );
1952  $fields['comment'] = CommentStoreComment::newUnsavedComment(
1953  $commentText,
1954  $commentData
1955  );
1956  }
1957  }
1958 
1959  $revision = new MutableRevisionRecord( $title, $this->dbDomain );
1960  $this->initializeMutableRevisionFromArray( $revision, $fields );
1961 
1962  if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
1963  // @phan-suppress-next-line PhanTypeNoPropertiesForeach
1964  foreach ( $fields['content'] as $role => $content ) {
1965  $revision->setContent( $role, $content );
1966  }
1967  } else {
1968  $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
1969  $revision->setSlot( $mainSlot );
1970  }
1971 
1972  return $revision;
1973  }
1974 
1980  MutableRevisionRecord $record,
1981  array $fields
1982  ) {
1984  $user = null;
1985 
1986  // If a user is passed in, use it if possible. We cannot use a user from a
1987  // remote wiki with unsuppressed ids, due to issues described in T222212.
1988  if ( isset( $fields['user'] ) &&
1989  ( $fields['user'] instanceof UserIdentity ) &&
1990  ( $this->dbDomain === false ||
1991  ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
1992  ) {
1993  $user = $fields['user'];
1994  } else {
1995  try {
1997  $fields['user'] ?? null,
1998  $fields['user_text'] ?? null,
1999  $fields['actor'] ?? null,
2000  $this->dbDomain
2001  );
2002  } catch ( InvalidArgumentException $ex ) {
2003  $user = null;
2004  }
2005  }
2006 
2007  if ( $user ) {
2008  $record->setUser( $user );
2009  }
2010 
2011  $timestamp = isset( $fields['timestamp'] )
2012  ? strval( $fields['timestamp'] )
2013  : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
2014 
2015  $record->setTimestamp( $timestamp );
2016 
2017  if ( isset( $fields['page'] ) ) {
2018  $record->setPageId( intval( $fields['page'] ) );
2019  }
2020 
2021  if ( isset( $fields['id'] ) ) {
2022  $record->setId( intval( $fields['id'] ) );
2023  }
2024  if ( isset( $fields['parent_id'] ) ) {
2025  $record->setParentId( intval( $fields['parent_id'] ) );
2026  }
2027 
2028  if ( isset( $fields['sha1'] ) ) {
2029  $record->setSha1( $fields['sha1'] );
2030  }
2031  if ( isset( $fields['size'] ) ) {
2032  $record->setSize( intval( $fields['size'] ) );
2033  }
2034 
2035  if ( isset( $fields['minor_edit'] ) ) {
2036  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2037  }
2038  if ( isset( $fields['deleted'] ) ) {
2039  $record->setVisibility( intval( $fields['deleted'] ) );
2040  }
2041 
2042  if ( isset( $fields['comment'] ) ) {
2043  Assert::parameterType(
2045  $fields['comment'],
2046  '$row[\'comment\']'
2047  );
2048  $record->setComment( $fields['comment'] );
2049  }
2050  }
2051 
2066  public function loadRevisionFromId( IDatabase $db, $id ) {
2067  return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
2068  }
2069 
2085  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2086  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2087  if ( $id ) {
2088  $conds['rev_id'] = intval( $id );
2089  } else {
2090  $conds[] = 'rev_id=page_latest';
2091  }
2092  return $this->loadRevisionFromConds( $db, $conds );
2093  }
2094 
2111  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2112  if ( $id ) {
2113  $matchId = intval( $id );
2114  } else {
2115  $matchId = 'page_latest';
2116  }
2117 
2118  return $this->loadRevisionFromConds(
2119  $db,
2120  [
2121  "rev_id=$matchId",
2122  'page_namespace' => $title->getNamespace(),
2123  'page_title' => $title->getDBkey()
2124  ],
2125  0,
2126  $title
2127  );
2128  }
2129 
2145  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2146  return $this->loadRevisionFromConds( $db,
2147  [
2148  'rev_timestamp' => $db->timestamp( $timestamp ),
2149  'page_namespace' => $title->getNamespace(),
2150  'page_title' => $title->getDBkey()
2151  ],
2152  0,
2153  $title
2154  );
2155  }
2156 
2172  private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
2173  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2174  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
2175 
2176  $lb = $this->getDBLoadBalancer();
2177 
2178  // Make sure new pending/committed revision are visibile later on
2179  // within web requests to certain avoid bugs like T93866 and T94407.
2180  if ( !$rev
2181  && !( $flags & self::READ_LATEST )
2182  && $lb->hasStreamingReplicaServers()
2183  && $lb->hasOrMadeRecentMasterChanges()
2184  ) {
2185  $flags = self::READ_LATEST;
2186  $dbw = $this->getDBConnectionRef( DB_MASTER );
2187  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
2188  }
2189 
2190  return $rev;
2191  }
2192 
2206  private function loadRevisionFromConds(
2207  IDatabase $db,
2208  $conditions,
2209  $flags = 0,
2210  Title $title = null
2211  ) {
2212  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
2213  if ( $row ) {
2214  $rev = $this->newRevisionFromRow( $row, $flags, $title );
2215 
2216  return $rev;
2217  }
2218 
2219  return null;
2220  }
2221 
2229  private function checkDatabaseDomain( IDatabase $db ) {
2230  $dbDomain = $db->getDomainID();
2231  $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2232  if ( $dbDomain === $storeDomain ) {
2233  return;
2234  }
2235 
2236  throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2237  }
2238 
2251  private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
2252  $this->checkDatabaseDomain( $db );
2253 
2254  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2255  $options = [];
2256  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2257  $options[] = 'FOR UPDATE';
2258  }
2259  return $db->selectRow(
2260  $revQuery['tables'],
2261  $revQuery['fields'],
2262  $conditions,
2263  __METHOD__,
2264  $options,
2265  $revQuery['joins']
2266  );
2267  }
2268 
2283  private function findSlotContentId( IDatabase $db, $revId, $role ) {
2284  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
2285  return null;
2286  }
2287 
2288  try {
2289  $roleId = $this->slotRoleStore->getId( $role );
2290  $conditions = [
2291  'slot_revision_id' => $revId,
2292  'slot_role_id' => $roleId,
2293  ];
2294 
2295  $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
2296 
2297  return $contentId ?: null;
2298  } catch ( NameTableAccessException $ex ) {
2299  // If the role is missing from the slot_roles table,
2300  // the corresponding row in slots cannot exist.
2301  return null;
2302  }
2303  }
2304 
2328  public function getQueryInfo( $options = [] ) {
2329  $ret = [
2330  'tables' => [],
2331  'fields' => [],
2332  'joins' => [],
2333  ];
2334 
2335  $ret['tables'][] = 'revision';
2336  $ret['fields'] = array_merge( $ret['fields'], [
2337  'rev_id',
2338  'rev_page',
2339  'rev_timestamp',
2340  'rev_minor_edit',
2341  'rev_deleted',
2342  'rev_len',
2343  'rev_parent_id',
2344  'rev_sha1',
2345  ] );
2346 
2347  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2348  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2349  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2350  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2351 
2352  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2353  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2354  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2355  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2356 
2357  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2358  $ret['fields'][] = 'rev_text_id';
2359 
2360  if ( $this->contentHandlerUseDB ) {
2361  $ret['fields'][] = 'rev_content_format';
2362  $ret['fields'][] = 'rev_content_model';
2363  }
2364  }
2365 
2366  if ( in_array( 'page', $options, true ) ) {
2367  $ret['tables'][] = 'page';
2368  $ret['fields'] = array_merge( $ret['fields'], [
2369  'page_namespace',
2370  'page_title',
2371  'page_id',
2372  'page_latest',
2373  'page_is_redirect',
2374  'page_len',
2375  ] );
2376  $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2377  }
2378 
2379  if ( in_array( 'user', $options, true ) ) {
2380  $ret['tables'][] = 'user';
2381  $ret['fields'] = array_merge( $ret['fields'], [
2382  'user_name',
2383  ] );
2384  $u = $actorQuery['fields']['rev_user'];
2385  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2386  }
2387 
2388  if ( in_array( 'text', $options, true ) ) {
2389  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
2390  throw new InvalidArgumentException( 'text table can no longer be joined directly' );
2391  } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2392  // NOTE: even when this class is set to not read from the old schema, callers
2393  // should still be able to join against the text table, as long as we are still
2394  // writing the old schema for compatibility.
2395  // TODO: This should trigger a deprecation warning eventually (T200918), but not
2396  // before all known usages are removed (see T198341 and T201164).
2397  // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
2398  }
2399 
2400  $ret['tables'][] = 'text';
2401  $ret['fields'] = array_merge( $ret['fields'], [
2402  'old_text',
2403  'old_flags'
2404  ] );
2405  $ret['joins']['text'] = [ 'JOIN', [ 'rev_text_id=old_id' ] ];
2406  }
2407 
2408  return $ret;
2409  }
2410 
2428  public function getSlotsQueryInfo( $options = [] ) {
2429  $ret = [
2430  'tables' => [],
2431  'fields' => [],
2432  'joins' => [],
2433  ];
2434 
2435  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2436  $db = $this->getDBConnectionRef( DB_REPLICA );
2437  $ret['tables'][] = 'revision';
2438 
2439  $ret['fields']['slot_revision_id'] = 'rev_id';
2440  $ret['fields']['slot_content_id'] = 'NULL';
2441  $ret['fields']['slot_origin'] = 'rev_id';
2442  $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
2443 
2444  if ( in_array( 'content', $options, true ) ) {
2445  $ret['fields']['content_size'] = 'rev_len';
2446  $ret['fields']['content_sha1'] = 'rev_sha1';
2447  $ret['fields']['content_address']
2448  = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'rev_text_id' ] );
2449 
2450  // Allow the content_id field to be emulated later
2451  $ret['fields']['rev_text_id'] = 'rev_text_id';
2452 
2453  if ( $this->contentHandlerUseDB ) {
2454  $ret['fields']['model_name'] = 'rev_content_model';
2455  } else {
2456  $ret['fields']['model_name'] = 'NULL';
2457  }
2458  }
2459  } else {
2460  $ret['tables'][] = 'slots';
2461  $ret['fields'] = array_merge( $ret['fields'], [
2462  'slot_revision_id',
2463  'slot_content_id',
2464  'slot_origin',
2465  'slot_role_id',
2466  ] );
2467 
2468  if ( in_array( 'role', $options, true ) ) {
2469  // Use left join to attach role name, so we still find the revision row even
2470  // if the role name is missing. This triggers a more obvious failure mode.
2471  $ret['tables'][] = 'slot_roles';
2472  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2473  $ret['fields'][] = 'role_name';
2474  }
2475 
2476  if ( in_array( 'content', $options, true ) ) {
2477  $ret['tables'][] = 'content';
2478  $ret['fields'] = array_merge( $ret['fields'], [
2479  'content_size',
2480  'content_sha1',
2481  'content_address',
2482  'content_model',
2483  ] );
2484  $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2485 
2486  if ( in_array( 'model', $options, true ) ) {
2487  // Use left join to attach model name, so we still find the revision row even
2488  // if the model name is missing. This triggers a more obvious failure mode.
2489  $ret['tables'][] = 'content_models';
2490  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2491  $ret['fields'][] = 'model_name';
2492  }
2493 
2494  }
2495  }
2496 
2497  return $ret;
2498  }
2499 
2513  public function getArchiveQueryInfo() {
2514  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2515  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2516  $ret = [
2517  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2518  'fields' => [
2519  'ar_id',
2520  'ar_page_id',
2521  'ar_namespace',
2522  'ar_title',
2523  'ar_rev_id',
2524  'ar_timestamp',
2525  'ar_minor_edit',
2526  'ar_deleted',
2527  'ar_len',
2528  'ar_parent_id',
2529  'ar_sha1',
2530  ] + $commentQuery['fields'] + $actorQuery['fields'],
2531  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2532  ];
2533 
2534  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2535  $ret['fields'][] = 'ar_text_id';
2536 
2537  if ( $this->contentHandlerUseDB ) {
2538  $ret['fields'][] = 'ar_content_format';
2539  $ret['fields'][] = 'ar_content_model';
2540  }
2541  }
2542 
2543  return $ret;
2544  }
2545 
2555  public function getRevisionSizes( array $revIds ) {
2556  return $this->listRevisionSizes( $this->getDBConnectionRef( DB_REPLICA ), $revIds );
2557  }
2558 
2571  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2572  $this->checkDatabaseDomain( $db );
2573 
2574  $revLens = [];
2575  if ( !$revIds ) {
2576  return $revLens; // empty
2577  }
2578 
2579  $res = $db->select(
2580  'revision',
2581  [ 'rev_id', 'rev_len' ],
2582  [ 'rev_id' => $revIds ],
2583  __METHOD__
2584  );
2585 
2586  foreach ( $res as $row ) {
2587  $revLens[$row->rev_id] = intval( $row->rev_len );
2588  }
2589 
2590  return $revLens;
2591  }
2592 
2601  private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2602  $op = $dir === 'next' ? '>' : '<';
2603  $sort = $dir === 'next' ? 'ASC' : 'DESC';
2604 
2605  if ( !$rev->getId() || !$rev->getPageId() ) {
2606  // revision is unsaved or otherwise incomplete
2607  return null;
2608  }
2609 
2610  if ( $rev instanceof RevisionArchiveRecord ) {
2611  // revision is deleted, so it's not part of the page history
2612  return null;
2613  }
2614 
2615  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2616  $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2617 
2618  $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2619  if ( $ts === false ) {
2620  // XXX Should this be moved into getTimestampFromId?
2621  $ts = $db->selectField( 'archive', 'ar_timestamp',
2622  [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2623  if ( $ts === false ) {
2624  // XXX Is this reachable? How can we have a page id but no timestamp?
2625  return null;
2626  }
2627  }
2628  $ts = $db->addQuotes( $db->timestamp( $ts ) );
2629 
2630  $revId = $db->selectField( 'revision', 'rev_id',
2631  [
2632  'rev_page' => $rev->getPageId(),
2633  "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})"
2634  ],
2635  __METHOD__,
2636  [
2637  'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
2638  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2639  ]
2640  );
2641 
2642  if ( $revId === false ) {
2643  return null;
2644  }
2645 
2646  return $this->getRevisionById( intval( $revId ) );
2647  }
2648 
2664  public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
2665  if ( $flags instanceof Title ) {
2666  // Old calling convention, we don't use Title here anymore
2667  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2668  $flags = 0;
2669  }
2670 
2671  return $this->getRelativeRevision( $rev, $flags, 'prev' );
2672  }
2673 
2687  public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
2688  if ( $flags instanceof Title ) {
2689  // Old calling convention, we don't use Title here anymore
2690  wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2691  $flags = 0;
2692  }
2693 
2694  return $this->getRelativeRevision( $rev, $flags, 'next' );
2695  }
2696 
2708  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2709  $this->checkDatabaseDomain( $db );
2710 
2711  if ( $rev->getPageId() === null ) {
2712  return 0;
2713  }
2714  # Use page_latest if ID is not given
2715  if ( !$rev->getId() ) {
2716  $prevId = $db->selectField(
2717  'page', 'page_latest',
2718  [ 'page_id' => $rev->getPageId() ],
2719  __METHOD__
2720  );
2721  } else {
2722  $prevId = $db->selectField(
2723  'revision', 'rev_id',
2724  [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
2725  __METHOD__,
2726  [ 'ORDER BY' => 'rev_id DESC' ]
2727  );
2728  }
2729  return intval( $prevId );
2730  }
2731 
2744  public function getTimestampFromId( $id, $flags = 0 ) {
2745  if ( $id instanceof Title ) {
2746  // Old deprecated calling convention supported for backwards compatibility
2747  $id = $flags;
2748  $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2749  }
2750  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2751 
2752  $timestamp =
2753  $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2754 
2755  return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
2756  }
2757 
2767  public function countRevisionsByPageId( IDatabase $db, $id ) {
2768  $this->checkDatabaseDomain( $db );
2769 
2770  $row = $db->selectRow( 'revision',
2771  [ 'revCount' => 'COUNT(*)' ],
2772  [ 'rev_page' => $id ],
2773  __METHOD__
2774  );
2775  if ( $row ) {
2776  return intval( $row->revCount );
2777  }
2778  return 0;
2779  }
2780 
2790  public function countRevisionsByTitle( IDatabase $db, $title ) {
2791  $id = $title->getArticleID();
2792  if ( $id ) {
2793  return $this->countRevisionsByPageId( $db, $id );
2794  }
2795  return 0;
2796  }
2797 
2816  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2817  $this->checkDatabaseDomain( $db );
2818 
2819  if ( !$userId ) {
2820  return false;
2821  }
2822 
2823  $revQuery = $this->getQueryInfo();
2824  $res = $db->select(
2825  $revQuery['tables'],
2826  [
2827  'rev_user' => $revQuery['fields']['rev_user'],
2828  ],
2829  [
2830  'rev_page' => $pageId,
2831  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2832  ],
2833  __METHOD__,
2834  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2835  $revQuery['joins']
2836  );
2837  foreach ( $res as $row ) {
2838  if ( $row->rev_user != $userId ) {
2839  return false;
2840  }
2841  }
2842  return true;
2843  }
2844 
2858  public function getKnownCurrentRevision( Title $title, $revId ) {
2859  $db = $this->getDBConnectionRef( DB_REPLICA );
2860 
2861  $pageId = $title->getArticleID();
2862 
2863  if ( !$pageId ) {
2864  return false;
2865  }
2866 
2867  if ( !$revId ) {
2868  $revId = $title->getLatestRevID();
2869  }
2870 
2871  if ( !$revId ) {
2872  wfWarn(
2873  'No latest revision known for page ' . $title->getPrefixedDBkey()
2874  . ' even though it exists with page ID ' . $pageId
2875  );
2876  return false;
2877  }
2878 
2879  // Load the row from cache if possible. If not possible, populate the cache.
2880  // As a minor optimization, remember if this was a cache hit or miss.
2881  // We can sometimes avoid a database query later if this is a cache miss.
2882  $fromCache = true;
2883  $row = $this->cache->getWithSetCallback(
2884  // Page/rev IDs passed in from DB to reflect history merges
2885  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2887  function ( $curValue, &$ttl, array &$setOpts ) use (
2888  $db, $pageId, $revId, &$fromCache
2889  ) {
2890  $setOpts += Database::getCacheSetOptions( $db );
2891  $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2892  if ( $row ) {
2893  $fromCache = false;
2894  }
2895  return $row; // don't cache negatives
2896  }
2897  );
2898 
2899  // Reflect revision deletion and user renames.
2900  if ( $row ) {
2901  return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2902  } else {
2903  return false;
2904  }
2905  }
2906 
2918  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2919  return $this->cache->makeGlobalKey(
2920  self::ROW_CACHE_KEY,
2921  $db->getDomainID(),
2922  $pageId,
2923  $revId
2924  );
2925  }
2926 
2927  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
2928 
2929 }
2930 
2935 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:264
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
ActorMigration $actorMigration
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
failOnEmpty( $value, $name)
const SCHEMA_COMPAT_READ_BOTH
Definition: Defines.php:269
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot...
Definition: SlotRecord.php:164
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save...
isValid()
Returns whether the content is valid.
isMinor()
MCR migration note: this replaces Revision::isMinor.
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3027
static toHex( $ip)
Return a zero-padded upper case hexadecimal representation of an IP address.
Definition: IP.php:404
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob...
Definition: BlobStore.php:76
A RevisionRecord representing an existing revision persisted in the revision table.
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:473
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there&#39;s one.
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
NameTableStore $slotRoleStore
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
NameTableStore $contentModelStore
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1972
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ &#39;page&#39;, &#39;user&#39; ] ) Caching rows w...
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
userWasLastToEdit(IDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page...
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2147
getKnownCurrentRevision(Title $title, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
failOnNull( $value, $name)
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
static makeAddressFromTextId( $id)
Returns an address referring to content stored in the text table row with the given ID...
setLogger(LoggerInterface $logger)
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:627
static mapArchiveFields( $archiveRow)
Maps fields of the archive row to corresponding revision rows.
$sort
static getDefaultModelFor(Title $title)
Returns the name of the default content model to be used for the page with the given title...
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page...
$value
assertCrossWikiContentLoadingIsSafe()
Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading and still...
const SHA1_HINT
Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the method...
Definition: BlobStore.php:70
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:267
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with. ...
Definition: BlobStore.php:46
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
newRevisionFromRow( $row, $queryFlags=0, Title $title=null, $fromCache=false)
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
getDBConnectionRefForQueryFlags( $queryFlags)
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:522
CommentStore $commentStore
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
getDBConnectionRef( $mode, $groups=[])
const DB_MASTER
Definition: defines.php:26
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2251
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
getModel()
Returns the content model.
Definition: SlotRecord.php:566
Mutable RevisionRecord implementation, for building new revision entries programmatically.
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:767
Created by PhpStorm.
you have access to all of the normal MediaWiki so you can get a DB use the cache
Definition: maintenance.txt:52
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
newMutableRevisionFromArray(array $fields, $queryFlags=0, Title $title=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1...
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, Title $title)
Factory method for RevisionSlots based on a revision ID.
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
setSize( $size)
Set nominal revision size, for optimization.
emulateMainSlot_1_29( $row, $queryFlags, Title $title)
Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page&#39;s history.
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
getVisibility()
Get the deletion bitfield of the revision.
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition: BlobStore.php:64
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot&#39;s content.
Definition: SlotRecord.php:446
setComment(CommentStoreComment $comment)
loadRevisionFromId(IDatabase $db, $id)
Load a page revision from a given revision ID number.
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given page ID...
loadSlotRecords( $revId, $queryFlags, Title $title)
Service for looking up page revisions.
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:266
$res
Definition: database.txt:21
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
fetchRevisionRowFromConds(IDatabase $db, $conditions, $flags=0)
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects...
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with. ...
Definition: BlobStore.php:52
newRevisionFromArchiveRow( $row, $queryFlags=0, Title $title=null, array $overrides=[])
Make a fake revision object from an archive table row.
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
const GAID_FOR_UPDATE
Used to be GAID_FOR_UPDATE define.
Definition: Title.php:57
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
newRevisionFromRowAndSlots( $row, $slotRows, $queryFlags=0, Title $title=null, $fromCache=false)
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
SlotRoleRegistry $slotRoleRegistry
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1972
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:912
Service for looking up page revisions.
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
static hasFlags( $bitfield, $flags)
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1748
getNextRevision(RevisionRecord $rev, $flags=0)
Get the revision after $rev in the page&#39;s history, if any.
A RevisionRecord representing a revision of a deleted page persisted in the archive table...
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
newRevisionFromConds( $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision.
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:469
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision, and inherits all slots form it.
const SCHEMA_COMPAT_WRITE_BOTH
Definition: Defines.php:268
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings, LocalSettings).
Definition: Setup.php:131
loadRevisionFromPageId(IDatabase $db, $pageid, $id=0)
Load either the current, or a specified, revision that&#39;s attached to a given page.
setSha1( $sha1)
Set revision hash, for optimization.
getOrigin()
Returns the revision ID of the revision that originated the slot&#39;s content.
Definition: SlotRecord.php:405
updateRevisionTextId(IDatabase $dbw, $revisionId, &$blobAddress)
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
const PRC_UNPATROLLED
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:274
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, $mcrMigrationStage, ActorMigration $actorMigration, $dbDomain=false)
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:592
Service for constructing revision objects.
getRevisionByTimestamp( $title, $timestamp)
Load the revision for the given title with the given timestamp.
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
getId()
Get revision ID.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:489
getSha1()
Returns the content size.
Definition: SlotRecord.php:538
getId()
Get the user&#39;s ID.
Definition: User.php:2224
setId( $id)
Set the revision ID.
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
getSha1()
Returns the base36 sha1 of this revision.
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:513
setContentHandlerUseDB( $contentHandlerUseDB)
getSize()
Returns the content size.
Definition: SlotRecord.php:522
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
$revQuery
getAddress()
Returns the address of this slot&#39;s content.
Definition: SlotRecord.php:499
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
Exception representing a failure to look up a revision.
ILoadBalancer $loadBalancer
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
getLatestRevID( $flags=0)
What is the page_latest field for this page?
Definition: Title.php:3114
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that&#39;s attached to a given page.
findSlotContentId(IDatabase $db, $revId, $role)
Finds the ID of a content row for a given revision and slot role.
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
getPreviousRevision(RevisionRecord $rev, $flags=0)
Get the revision before $rev in the page&#39;s history, if any.
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4393
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application...
Definition: BlobStore.php:40
getSize()
Returns the nominal size of this revision, in bogo-bytes.
Page revision base class.
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:435
const DB_REPLICA
Definition: defines.php:25
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision&#39;s author&#39;s user identity, if it&#39;s available to the specified audience.
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
const SCHEMA_COMPAT_READ_OLD
Definition: Defines.php:265
Value object representing the set of slots belonging to a revision.
getModel()
Returns the ID of the content model used by this Content object.
loadRevisionFromConds(IDatabase $db, $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision from the given database connection.
loadSlotContent(SlotRecord $slot, $blobData=null, $blobFlags=null, $blobFormat=null, $queryFlags=0)
Loads a Content object based on a slot row.
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it&#39;s available to the specified audience.
$content
Definition: pageupdater.txt:72
emulateContentId( $textId)
Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode, based on the revisi...
int $mcrMigrationStage
An appropriate combination of SCHEMA_COMPAT_XXX flags.
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition: BlobStore.php:58
getParentId()
Get parent revision ID (the original previous page revision).
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
LoggerInterface $logger
constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title)
Factory method for SlotRecords based on known slot rows.
getDefaultFormat()
Convenience method that returns the default serialization format for the content model that this Cont...
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1454
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
getPageId()
Get the page ID.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1712
const FORMAT_HINT
Hint key for use with storeBlob, indicating the serialization format used to create the blob...
Definition: BlobStore.php:82