MediaWiki  1.32.0
RevisionStore.php
Go to the documentation of this file.
1 <?php
27 namespace MediaWiki\Revision;
28 
32 use Content;
35 use Hooks;
37 use InvalidArgumentException;
38 use IP;
39 use LogicException;
48 use Message;
51 use Psr\Log\LoggerAwareInterface;
52 use Psr\Log\LoggerInterface;
53 use Psr\Log\NullLogger;
56 use RuntimeException;
57 use stdClass;
58 use Title;
59 use User;
61 use Wikimedia\Assert\Assert;
66 
77  implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
78 
79  const ROW_CACHE_KEY = 'revision-row-1.29';
80 
84  private $blobStore;
85 
89  private $wikiId;
90 
95  private $contentHandlerUseDB = true;
96 
100  private $loadBalancer;
101 
105  private $cache;
106 
110  private $commentStore;
111 
116 
120  private $logger;
121 
126 
130  private $slotRoleStore;
131 
134 
155  public function __construct(
156  ILoadBalancer $loadBalancer,
157  SqlBlobStore $blobStore,
160  NameTableStore $contentModelStore,
161  NameTableStore $slotRoleStore,
164  $wikiId = false
165  ) {
166  Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
167  Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
168  Assert::parameter(
170  '$mcrMigrationStage',
171  'Reading from the old and the new schema at the same time is not supported.'
172  );
173  Assert::parameter(
175  '$mcrMigrationStage',
176  'Reading needs to be enabled for the old or the new schema.'
177  );
178  Assert::parameter(
180  '$mcrMigrationStage',
181  'Writing needs to be enabled for the old or the new schema.'
182  );
183  Assert::parameter(
186  '$mcrMigrationStage',
187  'Cannot read the old schema when not also writing it.'
188  );
189  Assert::parameter(
192  '$mcrMigrationStage',
193  'Cannot read the new 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->mcrMigrationStage = $mcrMigrationStage;
203  $this->actorMigration = $actorMigration;
204  $this->wikiId = $wikiId;
205  $this->logger = new NullLogger();
206  }
207 
213  private function hasMcrSchemaFlags( $flags ) {
214  return ( $this->mcrMigrationStage & $flags ) === $flags;
215  }
216 
224  if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
225  throw new RevisionAccessException(
226  "Cross-wiki content loading is not supported by the pre-MCR schema"
227  );
228  }
229  }
230 
231  public function setLogger( LoggerInterface $logger ) {
232  $this->logger = $logger;
233  }
234 
238  public function isReadOnly() {
239  return $this->blobStore->isReadOnly();
240  }
241 
245  public function getContentHandlerUseDB() {
247  }
248 
257  ) {
258  if ( !$contentHandlerUseDB ) {
259  throw new MWException(
260  'Content model must be stored in the database for multi content revision migration.'
261  );
262  }
263  }
264  $this->contentHandlerUseDB = $contentHandlerUseDB;
265  }
266 
270  private function getDBLoadBalancer() {
271  return $this->loadBalancer;
272  }
273 
279  private function getDBConnection( $mode ) {
280  $lb = $this->getDBLoadBalancer();
281  return $lb->getConnection( $mode, [], $this->wikiId );
282  }
283 
289  private function getDBConnectionRefForQueryFlags( $queryFlags ) {
290  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
291  return $this->getDBConnectionRef( $mode );
292  }
293 
297  private function releaseDBConnection( IDatabase $connection ) {
298  $lb = $this->getDBLoadBalancer();
299  $lb->reuseConnection( $connection );
300  }
301 
307  private function getDBConnectionRef( $mode ) {
308  $lb = $this->getDBLoadBalancer();
309  return $lb->getConnectionRef( $mode, [], $this->wikiId );
310  }
311 
326  public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
327  if ( !$pageId && !$revId ) {
328  throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
329  }
330 
331  // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
332  // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
333  if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
334  $queryFlags = self::READ_NORMAL;
335  }
336 
337  $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
338  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
339  $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
340 
341  // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
342  if ( $canUseTitleNewFromId ) {
343  // TODO: better foreign title handling (introduce TitleFactory)
344  $title = Title::newFromID( $pageId, $titleFlags );
345  if ( $title ) {
346  return $title;
347  }
348  }
349 
350  // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
351  $canUseRevId = ( $revId !== null && $revId > 0 );
352 
353  if ( $canUseRevId ) {
354  $dbr = $this->getDBConnectionRef( $dbMode );
355  // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
356  $row = $dbr->selectRow(
357  [ 'revision', 'page' ],
358  [
359  'page_namespace',
360  'page_title',
361  'page_id',
362  'page_latest',
363  'page_is_redirect',
364  'page_len',
365  ],
366  [ 'rev_id' => $revId ],
367  __METHOD__,
368  $dbOptions,
369  [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
370  );
371  if ( $row ) {
372  // TODO: better foreign title handling (introduce TitleFactory)
373  return Title::newFromRow( $row );
374  }
375  }
376 
377  // If we still don't have a title, fallback to master if that wasn't already happening.
378  if ( $dbMode !== DB_MASTER ) {
379  $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
380  if ( $title ) {
381  $this->logger->info(
382  __METHOD__ . ' fell back to READ_LATEST and got a Title.',
383  [ 'trace' => wfBacktrace() ]
384  );
385  return $title;
386  }
387  }
388 
389  throw new RevisionAccessException(
390  "Could not determine title for page ID $pageId and revision ID $revId"
391  );
392  }
393 
401  private function failOnNull( $value, $name ) {
402  if ( $value === null ) {
403  throw new IncompleteRevisionException(
404  "$name must not be " . var_export( $value, true ) . "!"
405  );
406  }
407 
408  return $value;
409  }
410 
418  private function failOnEmpty( $value, $name ) {
419  if ( $value === null || $value === 0 || $value === '' ) {
420  throw new IncompleteRevisionException(
421  "$name must not be " . var_export( $value, true ) . "!"
422  );
423  }
424 
425  return $value;
426  }
427 
440  public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
441  // TODO: pass in a DBTransactionContext instead of a database connection.
442  $this->checkDatabaseWikiId( $dbw );
443 
444  $slotRoles = $rev->getSlotRoles();
445 
446  // Make sure the main slot is always provided throughout migration
447  if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
448  throw new InvalidArgumentException(
449  'main slot must be provided'
450  );
451  }
452 
453  // If we are not writing into the new schema, we can't support extra slots.
455  && $slotRoles !== [ SlotRecord::MAIN ]
456  ) {
457  throw new InvalidArgumentException(
458  'Only the main slot is supported when not writing to the MCR enabled schema!'
459  );
460  }
461 
462  // As long as we are not reading from the new schema, we don't want to write extra slots.
464  && $slotRoles !== [ SlotRecord::MAIN ]
465  ) {
466  throw new InvalidArgumentException(
467  'Only the main slot is supported when not reading from the MCR enabled schema!'
468  );
469  }
470 
471  // Checks
472  $this->failOnNull( $rev->getSize(), 'size field' );
473  $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
474  $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
475  $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
476  $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
477  $this->failOnNull( $user->getId(), 'user field' );
478  $this->failOnEmpty( $user->getName(), 'user_text field' );
479 
480  if ( !$rev->isReadyForInsertion() ) {
481  // This is here for future-proofing. At the time this check being added, it
482  // was redundant to the individual checks above.
483  throw new IncompleteRevisionException( 'Revision is incomplete' );
484  }
485 
486  // TODO: we shouldn't need an actual Title here.
487  $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
488  $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
489 
490  $parentId = $rev->getParentId() === null
491  ? $this->getPreviousRevisionId( $dbw, $rev )
492  : $rev->getParentId();
493 
495  $rev = $dbw->doAtomicSection(
496  __METHOD__,
497  function ( IDatabase $dbw, $fname ) use (
498  $rev,
499  $user,
500  $comment,
501  $title,
502  $pageId,
503  $parentId
504  ) {
505  return $this->insertRevisionInternal(
506  $rev,
507  $dbw,
508  $user,
509  $comment,
510  $title,
511  $pageId,
512  $parentId
513  );
514  }
515  );
516 
517  // sanity checks
518  Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
519  Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
520  Assert::postcondition(
521  $rev->getComment( RevisionRecord::RAW ) !== null,
522  'revision must have a comment'
523  );
524  Assert::postcondition(
525  $rev->getUser( RevisionRecord::RAW ) !== null,
526  'revision must have a user'
527  );
528 
529  // Trigger exception if the main slot is missing.
530  // Technically, this could go away after MCR migration: while
531  // calling code may require a main slot to exist, RevisionStore
532  // really should not know or care about that requirement.
534 
535  foreach ( $slotRoles as $role ) {
536  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
537  Assert::postcondition(
538  $slot->getContent() !== null,
539  $role . ' slot must have content'
540  );
541  Assert::postcondition(
542  $slot->hasRevision(),
543  $role . ' slot must have a revision associated'
544  );
545  }
546 
547  Hooks::run( 'RevisionRecordInserted', [ $rev ] );
548 
549  // TODO: deprecate in 1.32!
550  $legacyRevision = new Revision( $rev );
551  Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
552 
553  return $rev;
554  }
555 
556  private function insertRevisionInternal(
558  IDatabase $dbw,
559  User $user,
560  CommentStoreComment $comment,
561  Title $title,
562  $pageId,
563  $parentId
564  ) {
565  $slotRoles = $rev->getSlotRoles();
566 
567  $revisionRow = $this->insertRevisionRowOn(
568  $dbw,
569  $rev,
570  $title,
571  $parentId
572  );
573 
574  $revisionId = $revisionRow['rev_id'];
575 
576  $blobHints = [
577  BlobStore::PAGE_HINT => $pageId,
578  BlobStore::REVISION_HINT => $revisionId,
579  BlobStore::PARENT_HINT => $parentId,
580  ];
581 
582  $newSlots = [];
583  foreach ( $slotRoles as $role ) {
584  $slot = $rev->getSlot( $role, RevisionRecord::RAW );
585 
586  // If the SlotRecord already has a revision ID set, this means it already exists
587  // in the database, and should already belong to the current revision.
588  // However, a slot may already have a revision, but no content ID, if the slot
589  // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
590  // mode, and the respective archive row was not yet migrated to the new schema.
591  // In that case, a new slot row (and content row) must be inserted even during
592  // undeletion.
593  if ( $slot->hasRevision() && $slot->hasContentId() ) {
594  // TODO: properly abort transaction if the assertion fails!
595  Assert::parameter(
596  $slot->getRevision() === $revisionId,
597  'slot role ' . $slot->getRole(),
598  'Existing slot should belong to revision '
599  . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
600  );
601 
602  // Slot exists, nothing to do, move along.
603  // This happens when restoring archived revisions.
604 
605  $newSlots[$role] = $slot;
606 
607  // Write the main slot's text ID to the revision table for backwards compatibility
608  if ( $slot->getRole() === SlotRecord::MAIN
610  ) {
611  $blobAddress = $slot->getAddress();
612  $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
613  }
614  } else {
615  $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
616  }
617  }
618 
619  $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
620 
622  $title,
623  $user,
624  $comment,
625  (object)$revisionRow,
626  new RevisionSlots( $newSlots ),
627  $this->wikiId
628  );
629 
630  return $rev;
631  }
632 
640  private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
641  $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
642  if ( !$textId ) {
643  throw new LogicException(
644  'Blob address not supported in 1.29 database schema: ' . $blobAddress
645  );
646  }
647 
648  // getTextIdFromAddress() is free to insert something into the text table, so $textId
649  // may be a new value, not anything already contained in $blobAddress.
650  $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
651 
652  $dbw->update(
653  'revision',
654  [ 'rev_text_id' => $textId ],
655  [ 'rev_id' => $revisionId ],
656  __METHOD__
657  );
658 
659  return $textId;
660  }
661 
670  private function insertSlotOn(
671  IDatabase $dbw,
672  $revisionId,
673  SlotRecord $protoSlot,
674  Title $title,
675  array $blobHints = []
676  ) {
677  if ( $protoSlot->hasAddress() ) {
678  $blobAddress = $protoSlot->getAddress();
679  } else {
680  $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
681  }
682 
683  $contentId = null;
684 
685  // Write the main slot's text ID to the revision table for backwards compatibility
686  if ( $protoSlot->getRole() === SlotRecord::MAIN
688  ) {
689  // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
690  // with the real content ID below.
691  $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
692  $contentId = $this->emulateContentId( $textId );
693  }
694 
695  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
696  if ( $protoSlot->hasContentId() ) {
697  $contentId = $protoSlot->getContentId();
698  } else {
699  $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
700  }
701 
702  $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
703  }
704 
705  $savedSlot = SlotRecord::newSaved(
706  $revisionId,
707  $contentId,
708  $blobAddress,
709  $protoSlot
710  );
711 
712  return $savedSlot;
713  }
714 
722  private function insertIpChangesRow(
723  IDatabase $dbw,
724  User $user,
726  $revisionId
727  ) {
728  if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
729  $ipcRow = [
730  'ipc_rev_id' => $revisionId,
731  'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
732  'ipc_hex' => IP::toHex( $user->getName() ),
733  ];
734  $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
735  }
736  }
737 
749  private function insertRevisionRowOn(
750  IDatabase $dbw,
752  Title $title,
753  $parentId
754  ) {
755  $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
756 
757  list( $commentFields, $commentCallback ) =
758  $this->commentStore->insertWithTempTable(
759  $dbw,
760  'rev_comment',
761  $rev->getComment( RevisionRecord::RAW )
762  );
763  $revisionRow += $commentFields;
764 
765  list( $actorFields, $actorCallback ) =
766  $this->actorMigration->getInsertValuesWithTempTable(
767  $dbw,
768  'rev_user',
769  $rev->getUser( RevisionRecord::RAW )
770  );
771  $revisionRow += $actorFields;
772 
773  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
774 
775  if ( !isset( $revisionRow['rev_id'] ) ) {
776  // only if auto-increment was used
777  $revisionRow['rev_id'] = intval( $dbw->insertId() );
778 
779  if ( $dbw->getType() === 'mysql' ) {
780  // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
781  // auto-increment value to disk, so on server restart it might reuse IDs from deleted
782  // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
783 
784  $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
785  $table = 'archive';
786  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
787  $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
788  if ( $maxRevId2 >= $maxRevId ) {
789  $maxRevId = $maxRevId2;
790  $table = 'slots';
791  }
792  }
793 
794  if ( $maxRevId >= $revisionRow['rev_id'] ) {
795  $this->logger->debug(
796  '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
797  . ' Trying to fix it.',
798  [
799  'revid' => $revisionRow['rev_id'],
800  'table' => $table,
801  'maxrevid' => $maxRevId,
802  ]
803  );
804 
805  if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
806  throw new MWException( 'Failed to get database lock for T202032' );
807  }
808  $fname = __METHOD__;
809  $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) {
810  $dbw->unlock( 'fix-for-T202032', $fname );
811  } );
812 
813  $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
814 
815  // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
816  // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
817  // inserts too, though, at least on MariaDB 10.1.29.
818  //
819  // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
820  // transactions in this code path thanks to the row lock from the original ->insert() above.
821  //
822  // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
823  // that's for non-MySQL DBs.
824  $row1 = $dbw->query(
825  $dbw->selectSqlText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
826  )->fetchObject();
827  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
828  $row2 = $dbw->query(
829  $dbw->selectSqlText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
830  . ' FOR UPDATE'
831  )->fetchObject();
832  } else {
833  $row2 = null;
834  }
835  $maxRevId = max(
836  $maxRevId,
837  $row1 ? intval( $row1->v ) : 0,
838  $row2 ? intval( $row2->v ) : 0
839  );
840 
841  // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
842  // transactions will throw a duplicate key error here. It doesn't seem worth trying
843  // to avoid that.
844  $revisionRow['rev_id'] = $maxRevId + 1;
845  $dbw->insert( 'revision', $revisionRow, __METHOD__ );
846  }
847  }
848  }
849 
850  $commentCallback( $revisionRow['rev_id'] );
851  $actorCallback( $revisionRow['rev_id'], $revisionRow );
852 
853  return $revisionRow;
854  }
855 
866  private function getBaseRevisionRow(
867  IDatabase $dbw,
869  Title $title,
870  $parentId
871  ) {
872  // Record the edit in revisions
873  $revisionRow = [
874  'rev_page' => $rev->getPageId(),
875  'rev_parent_id' => $parentId,
876  'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
877  'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
878  'rev_deleted' => $rev->getVisibility(),
879  'rev_len' => $rev->getSize(),
880  'rev_sha1' => $rev->getSha1(),
881  ];
882 
883  if ( $rev->getId() !== null ) {
884  // Needed to restore revisions with their original ID
885  $revisionRow['rev_id'] = $rev->getId();
886  }
887 
888  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
889  // In non MCR mode this IF section will relate to the main slot
890  $mainSlot = $rev->getSlot( SlotRecord::MAIN );
891  $model = $mainSlot->getModel();
892  $format = $mainSlot->getFormat();
893 
894  // MCR migration note: rev_content_model and rev_content_format will go away
895  if ( $this->contentHandlerUseDB ) {
897 
898  $defaultModel = ContentHandler::getDefaultModelFor( $title );
899  $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
900 
901  $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
902  $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
903  }
904  }
905 
906  return $revisionRow;
907  }
908 
917  private function storeContentBlob(
918  SlotRecord $slot,
919  Title $title,
920  array $blobHints = []
921  ) {
922  $content = $slot->getContent();
923  $format = $content->getDefaultFormat();
924  $model = $content->getModel();
925 
926  $this->checkContent( $content, $title );
927 
928  return $this->blobStore->storeBlob(
929  $content->serialize( $format ),
930  // These hints "leak" some information from the higher abstraction layer to
931  // low level storage to allow for optimization.
932  array_merge(
933  $blobHints,
934  [
935  BlobStore::DESIGNATION_HINT => 'page-content',
936  BlobStore::ROLE_HINT => $slot->getRole(),
937  BlobStore::SHA1_HINT => $slot->getSha1(),
938  BlobStore::MODEL_HINT => $model,
939  BlobStore::FORMAT_HINT => $format,
940  ]
941  )
942  );
943  }
944 
951  private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
952  $slotRow = [
953  'slot_revision_id' => $revisionId,
954  'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
955  'slot_content_id' => $contentId,
956  // If the slot has a specific origin use that ID, otherwise use the ID of the revision
957  // that we just inserted.
958  'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
959  ];
960  $dbw->insert( 'slots', $slotRow, __METHOD__ );
961  }
962 
969  private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
970  $contentRow = [
971  'content_size' => $slot->getSize(),
972  'content_sha1' => $slot->getSha1(),
973  'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
974  'content_address' => $blobAddress,
975  ];
976  $dbw->insert( 'content', $contentRow, __METHOD__ );
977  return intval( $dbw->insertId() );
978  }
979 
989  private function checkContent( Content $content, Title $title ) {
990  // Note: may return null for revisions that have not yet been inserted
991 
992  $model = $content->getModel();
993  $format = $content->getDefaultFormat();
994  $handler = $content->getContentHandler();
995 
996  $name = "$title";
997 
998  if ( !$handler->isSupportedFormat( $format ) ) {
999  throw new MWException( "Can't use format $format with content model $model on $name" );
1000  }
1001 
1002  if ( !$this->contentHandlerUseDB ) {
1003  // if $wgContentHandlerUseDB is not set,
1004  // all revisions must use the default content model and format.
1005 
1007 
1008  $defaultModel = ContentHandler::getDefaultModelFor( $title );
1009  $defaultHandler = ContentHandler::getForModelID( $defaultModel );
1010  $defaultFormat = $defaultHandler->getDefaultFormat();
1011 
1012  if ( $model != $defaultModel ) {
1013  throw new MWException( "Can't save non-default content model with "
1014  . "\$wgContentHandlerUseDB disabled: model is $model, "
1015  . "default for $name is $defaultModel"
1016  );
1017  }
1018 
1019  if ( $format != $defaultFormat ) {
1020  throw new MWException( "Can't use non-default content format with "
1021  . "\$wgContentHandlerUseDB disabled: format is $format, "
1022  . "default for $name is $defaultFormat"
1023  );
1024  }
1025  }
1026 
1027  if ( !$content->isValid() ) {
1028  throw new MWException(
1029  "New content for $name is not valid! Content model is $model"
1030  );
1031  }
1032  }
1033 
1059  public function newNullRevision(
1060  IDatabase $dbw,
1061  Title $title,
1062  CommentStoreComment $comment,
1063  $minor,
1064  User $user
1065  ) {
1066  $this->checkDatabaseWikiId( $dbw );
1067 
1068  $pageId = $title->getArticleID();
1069 
1070  // T51581: Lock the page table row to ensure no other process
1071  // is adding a revision to the page at the same time.
1072  // Avoid locking extra tables, compare T191892.
1073  $pageLatest = $dbw->selectField(
1074  'page',
1075  'page_latest',
1076  [ 'page_id' => $pageId ],
1077  __METHOD__,
1078  [ 'FOR UPDATE' ]
1079  );
1080 
1081  if ( !$pageLatest ) {
1082  return null;
1083  }
1084 
1085  // Fetch the actual revision row from master, without locking all extra tables.
1086  $oldRevision = $this->loadRevisionFromConds(
1087  $dbw,
1088  [ 'rev_id' => intval( $pageLatest ) ],
1089  self::READ_LATEST,
1090  $title
1091  );
1092 
1093  if ( !$oldRevision ) {
1094  $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1095  $this->logger->error(
1096  $msg,
1097  [ 'exception' => new RuntimeException( $msg ) ]
1098  );
1099  return null;
1100  }
1101 
1102  // Construct the new revision
1103  $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1104  $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1105 
1106  $newRevision->setComment( $comment );
1107  $newRevision->setUser( $user );
1108  $newRevision->setTimestamp( $timestamp );
1109  $newRevision->setMinorEdit( $minor );
1110 
1111  return $newRevision;
1112  }
1113 
1124  $rc = $this->getRecentChange( $rev );
1125  if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1126  return $rc->getAttribute( 'rc_id' );
1127  } else {
1128  return 0;
1129  }
1130  }
1131 
1145  public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1146  list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1147  $db = $this->getDBConnection( $dbType );
1148 
1149  $userIdentity = $rev->getUser( RevisionRecord::RAW );
1150 
1151  if ( !$userIdentity ) {
1152  // If the revision has no user identity, chances are it never went
1153  // into the database, and doesn't have an RC entry.
1154  return null;
1155  }
1156 
1157  // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
1158  $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
1160  [
1161  $actorWhere['conds'],
1162  'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
1163  'rc_this_oldid' => $rev->getId()
1164  ],
1165  __METHOD__,
1166  $dbType
1167  );
1168 
1169  $this->releaseDBConnection( $db );
1170 
1171  // XXX: cache this locally? Glue it to the RevisionRecord?
1172  return $rc;
1173  }
1174 
1182  private static function mapArchiveFields( $archiveRow ) {
1183  $fieldMap = [
1184  // keep with ar prefix:
1185  'ar_id' => 'ar_id',
1186 
1187  // not the same suffix:
1188  'ar_page_id' => 'rev_page',
1189  'ar_rev_id' => 'rev_id',
1190 
1191  // same suffix:
1192  'ar_text_id' => 'rev_text_id',
1193  'ar_timestamp' => 'rev_timestamp',
1194  'ar_user_text' => 'rev_user_text',
1195  'ar_user' => 'rev_user',
1196  'ar_actor' => 'rev_actor',
1197  'ar_minor_edit' => 'rev_minor_edit',
1198  'ar_deleted' => 'rev_deleted',
1199  'ar_len' => 'rev_len',
1200  'ar_parent_id' => 'rev_parent_id',
1201  'ar_sha1' => 'rev_sha1',
1202  'ar_comment' => 'rev_comment',
1203  'ar_comment_cid' => 'rev_comment_cid',
1204  'ar_comment_id' => 'rev_comment_id',
1205  'ar_comment_text' => 'rev_comment_text',
1206  'ar_comment_data' => 'rev_comment_data',
1207  'ar_comment_old' => 'rev_comment_old',
1208  'ar_content_format' => 'rev_content_format',
1209  'ar_content_model' => 'rev_content_model',
1210  ];
1211 
1212  $revRow = new stdClass();
1213  foreach ( $fieldMap as $arKey => $revKey ) {
1214  if ( property_exists( $archiveRow, $arKey ) ) {
1215  $revRow->$revKey = $archiveRow->$arKey;
1216  }
1217  }
1218 
1219  return $revRow;
1220  }
1221 
1232  private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
1233  $mainSlotRow = new stdClass();
1234  $mainSlotRow->role_name = SlotRecord::MAIN;
1235  $mainSlotRow->model_name = null;
1236  $mainSlotRow->slot_revision_id = null;
1237  $mainSlotRow->slot_content_id = null;
1238  $mainSlotRow->content_address = null;
1239 
1240  $content = null;
1241  $blobData = null;
1242  $blobFlags = null;
1243 
1244  if ( is_object( $row ) ) {
1245  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1246  // Don't emulate from a row when using the new schema.
1247  // Emulating from an array is still OK.
1248  throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
1249  }
1250 
1251  // archive row
1252  if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
1253  $row = $this->mapArchiveFields( $row );
1254  }
1255 
1256  if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
1257  $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
1258  $row->rev_text_id
1259  );
1260  }
1261 
1262  // This is used by null-revisions
1263  $mainSlotRow->slot_origin = isset( $row->slot_origin )
1264  ? intval( $row->slot_origin )
1265  : null;
1266 
1267  if ( isset( $row->old_text ) ) {
1268  // this happens when the text-table gets joined directly, in the pre-1.30 schema
1269  $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
1270  // Check against selects that might have not included old_flags
1271  if ( !property_exists( $row, 'old_flags' ) ) {
1272  throw new InvalidArgumentException( 'old_flags was not set in $row' );
1273  }
1274  $blobFlags = $row->old_flags ?? '';
1275  }
1276 
1277  $mainSlotRow->slot_revision_id = intval( $row->rev_id );
1278 
1279  $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
1280  $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
1281  $mainSlotRow->model_name = isset( $row->rev_content_model )
1282  ? strval( $row->rev_content_model )
1283  : null;
1284  // XXX: in the future, we'll probably always use the default format, and drop content_format
1285  $mainSlotRow->format_name = isset( $row->rev_content_format )
1286  ? strval( $row->rev_content_format )
1287  : null;
1288 
1289  if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
1290  // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1291  $mainSlotRow->slot_content_id
1292  = $this->emulateContentId( intval( $row->rev_text_id ) );
1293  }
1294  } elseif ( is_array( $row ) ) {
1295  $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
1296 
1297  $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
1298  ? intval( $row['slot_origin'] )
1299  : null;
1300  $mainSlotRow->content_address = isset( $row['text_id'] )
1301  ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
1302  : null;
1303  $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
1304  $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
1305 
1306  $mainSlotRow->model_name = isset( $row['content_model'] )
1307  ? strval( $row['content_model'] ) : null; // XXX: must be a string!
1308  // XXX: in the future, we'll probably always use the default format, and drop content_format
1309  $mainSlotRow->format_name = isset( $row['content_format'] )
1310  ? strval( $row['content_format'] ) : null;
1311  $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
1312  // XXX: If the flags field is not set then $blobFlags should be null so that no
1313  // decoding will happen. An empty string will result in default decodings.
1314  $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
1315 
1316  // if we have a Content object, override mText and mContentModel
1317  if ( !empty( $row['content'] ) ) {
1318  if ( !( $row['content'] instanceof Content ) ) {
1319  throw new MWException( 'content field must contain a Content object.' );
1320  }
1321 
1323  $content = $row['content'];
1324  $handler = $content->getContentHandler();
1325 
1326  $mainSlotRow->model_name = $content->getModel();
1327 
1328  // XXX: in the future, we'll probably always use the default format.
1329  if ( $mainSlotRow->format_name === null ) {
1330  $mainSlotRow->format_name = $handler->getDefaultFormat();
1331  }
1332  }
1333 
1334  if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
1335  // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1336  $mainSlotRow->slot_content_id
1337  = $this->emulateContentId( intval( $row['text_id'] ) );
1338  }
1339  } else {
1340  throw new MWException( 'Revision constructor passed invalid row format.' );
1341  }
1342 
1343  // With the old schema, the content changes with every revision,
1344  // except for null-revisions.
1345  if ( !isset( $mainSlotRow->slot_origin ) ) {
1346  $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
1347  }
1348 
1349  if ( $mainSlotRow->model_name === null ) {
1350  $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
1352 
1353  // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
1354  // TODO: MCR: deprecate $title->getModel().
1355  return ContentHandler::getDefaultModelFor( $title );
1356  };
1357  }
1358 
1359  if ( !$content ) {
1360  // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
1361  // is missing, but "empty revisions" with no content are used in some edge cases.
1362 
1363  $content = function ( SlotRecord $slot )
1364  use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
1365  {
1366  return $this->loadSlotContent(
1367  $slot,
1368  $blobData,
1369  $blobFlags,
1370  $mainSlotRow->format_name,
1371  $queryFlags
1372  );
1373  };
1374  }
1375 
1376  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
1377  // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
1378  // the inherited slot to have the same content_id as the original slot. In that case,
1379  // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
1380  $mainSlotRow->slot_content_id =
1381  function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
1382  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1383  return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
1384  };
1385  }
1386 
1387  return new SlotRecord( $mainSlotRow, $content );
1388  }
1389 
1401  private function emulateContentId( $textId ) {
1402  // Return a negative number to ensure the ID is distinct from any real content IDs
1403  // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
1404  // mode.
1405  return -$textId;
1406  }
1407 
1427  private function loadSlotContent(
1428  SlotRecord $slot,
1429  $blobData = null,
1430  $blobFlags = null,
1431  $blobFormat = null,
1432  $queryFlags = 0
1433  ) {
1434  if ( $blobData !== null ) {
1435  Assert::parameterType( 'string', $blobData, '$blobData' );
1436  Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1437 
1438  $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1439 
1440  if ( $blobFlags === null ) {
1441  // No blob flags, so use the blob verbatim.
1442  $data = $blobData;
1443  } else {
1444  $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1445  if ( $data === false ) {
1446  throw new RevisionAccessException(
1447  "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1448  );
1449  }
1450  }
1451 
1452  } else {
1453  $address = $slot->getAddress();
1454  try {
1455  $data = $this->blobStore->getBlob( $address, $queryFlags );
1456  } catch ( BlobAccessException $e ) {
1457  throw new RevisionAccessException(
1458  "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1459  );
1460  }
1461  }
1462 
1463  // Unserialize content
1465 
1466  $content = $handler->unserializeContent( $data, $blobFormat );
1467  return $content;
1468  }
1469 
1484  public function getRevisionById( $id, $flags = 0 ) {
1485  return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1486  }
1487 
1504  public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1505  $conds = [
1506  'page_namespace' => $linkTarget->getNamespace(),
1507  'page_title' => $linkTarget->getDBkey()
1508  ];
1509  if ( $revId ) {
1510  // Use the specified revision ID.
1511  // Note that we use newRevisionFromConds here because we want to retry
1512  // and fall back to master if the page is not found on a replica.
1513  // Since the caller supplied a revision ID, we are pretty sure the revision is
1514  // supposed to exist, so we should try hard to find it.
1515  $conds['rev_id'] = $revId;
1516  return $this->newRevisionFromConds( $conds, $flags );
1517  } else {
1518  // Use a join to get the latest revision.
1519  // Note that we don't use newRevisionFromConds here because we don't want to retry
1520  // and fall back to master. The assumption is that we only want to force the fallback
1521  // if we are quite sure the revision exists because the caller supplied a revision ID.
1522  // If the page isn't found at all on a replica, it probably simply does not exist.
1523  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1524 
1525  $conds[] = 'rev_id=page_latest';
1526  $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1527 
1528  return $rev;
1529  }
1530  }
1531 
1548  public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1549  $conds = [ 'page_id' => $pageId ];
1550  if ( $revId ) {
1551  // Use the specified revision ID.
1552  // Note that we use newRevisionFromConds here because we want to retry
1553  // and fall back to master if the page is not found on a replica.
1554  // Since the caller supplied a revision ID, we are pretty sure the revision is
1555  // supposed to exist, so we should try hard to find it.
1556  $conds['rev_id'] = $revId;
1557  return $this->newRevisionFromConds( $conds, $flags );
1558  } else {
1559  // Use a join to get the latest revision.
1560  // Note that we don't use newRevisionFromConds here because we don't want to retry
1561  // and fall back to master. The assumption is that we only want to force the fallback
1562  // if we are quite sure the revision exists because the caller supplied a revision ID.
1563  // If the page isn't found at all on a replica, it probably simply does not exist.
1564  $db = $this->getDBConnectionRefForQueryFlags( $flags );
1565 
1566  $conds[] = 'rev_id=page_latest';
1567  $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1568 
1569  return $rev;
1570  }
1571  }
1572 
1584  public function getRevisionByTimestamp( $title, $timestamp ) {
1585  $db = $this->getDBConnection( DB_REPLICA );
1586  return $this->newRevisionFromConds(
1587  [
1588  'rev_timestamp' => $db->timestamp( $timestamp ),
1589  'page_namespace' => $title->getNamespace(),
1590  'page_title' => $title->getDBkey()
1591  ],
1592  0,
1593  $title
1594  );
1595  }
1596 
1603  private function loadSlotRecords( $revId, $queryFlags ) {
1604  $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1605 
1606  list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1607  $db = $this->getDBConnectionRef( $dbMode );
1608 
1609  $res = $db->select(
1610  $revQuery['tables'],
1611  $revQuery['fields'],
1612  [
1613  'slot_revision_id' => $revId,
1614  ],
1615  __METHOD__,
1616  $dbOptions,
1617  $revQuery['joins']
1618  );
1619 
1620  $slots = [];
1621 
1622  foreach ( $res as $row ) {
1623  // resolve role names and model names from in-memory cache, instead of joining.
1624  $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1625  $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1626 
1627  $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
1628  return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
1629  };
1630 
1631  $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1632  }
1633 
1634  if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1635  throw new RevisionAccessException(
1636  'Main slot of revision ' . $revId . ' not found in database!'
1637  );
1638  };
1639 
1640  return $slots;
1641  }
1642 
1657  private function newRevisionSlots(
1658  $revId,
1659  $revisionRow,
1660  $queryFlags,
1661  Title $title
1662  ) {
1663  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1664  $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
1665  $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
1666  } else {
1667  // XXX: do we need the same kind of caching here
1668  // that getKnownCurrentRevision uses (if $revId == page_latest?)
1669 
1670  $slots = new RevisionSlots( function () use( $revId, $queryFlags ) {
1671  return $this->loadSlotRecords( $revId, $queryFlags );
1672  } );
1673  }
1674 
1675  return $slots;
1676  }
1677 
1695  public function newRevisionFromArchiveRow(
1696  $row,
1697  $queryFlags = 0,
1698  Title $title = null,
1699  array $overrides = []
1700  ) {
1701  Assert::parameterType( 'object', $row, '$row' );
1702 
1703  // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1704  Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1705 
1706  if ( !$title && isset( $overrides['title'] ) ) {
1707  if ( !( $overrides['title'] instanceof Title ) ) {
1708  throw new MWException( 'title field override must contain a Title object.' );
1709  }
1710 
1711  $title = $overrides['title'];
1712  }
1713 
1714  if ( !isset( $title ) ) {
1715  if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1716  $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1717  } else {
1718  throw new InvalidArgumentException(
1719  'A Title or ar_namespace and ar_title must be given'
1720  );
1721  }
1722  }
1723 
1724  foreach ( $overrides as $key => $value ) {
1725  $field = "ar_$key";
1726  $row->$field = $value;
1727  }
1728 
1729  try {
1731  $row->ar_user ?? null,
1732  $row->ar_user_text ?? null,
1733  $row->ar_actor ?? null
1734  );
1735  } catch ( InvalidArgumentException $ex ) {
1736  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1737  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1738  }
1739 
1740  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1741  // Legacy because $row may have come from self::selectFields()
1742  $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1743 
1744  $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
1745 
1746  return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
1747  }
1748 
1760  public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
1761  Assert::parameterType( 'object', $row, '$row' );
1762 
1763  if ( !$title ) {
1764  $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
1765  $revId = $row->rev_id ?? 0;
1766 
1767  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1768  }
1769 
1770  if ( !isset( $row->page_latest ) ) {
1771  $row->page_latest = $title->getLatestRevID();
1772  if ( $row->page_latest === 0 && $title->exists() ) {
1773  wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1774  }
1775  }
1776 
1777  try {
1779  $row->rev_user ?? null,
1780  $row->rev_user_text ?? null,
1781  $row->rev_actor ?? null
1782  );
1783  } catch ( InvalidArgumentException $ex ) {
1784  wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1785  $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1786  }
1787 
1788  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1789  // Legacy because $row may have come from self::selectFields()
1790  $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1791 
1792  $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
1793 
1794  return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
1795  }
1796 
1812  array $fields,
1813  $queryFlags = 0,
1814  Title $title = null
1815  ) {
1816  if ( !$title && isset( $fields['title'] ) ) {
1817  if ( !( $fields['title'] instanceof Title ) ) {
1818  throw new MWException( 'title field must contain a Title object.' );
1819  }
1820 
1821  $title = $fields['title'];
1822  }
1823 
1824  if ( !$title ) {
1825  $pageId = $fields['page'] ?? 0;
1826  $revId = $fields['id'] ?? 0;
1827 
1828  $title = $this->getTitle( $pageId, $revId, $queryFlags );
1829  }
1830 
1831  if ( !isset( $fields['page'] ) ) {
1832  $fields['page'] = $title->getArticleID( $queryFlags );
1833  }
1834 
1835  // if we have a content object, use it to set the model and type
1836  if ( !empty( $fields['content'] ) ) {
1837  if ( !( $fields['content'] instanceof Content ) && !is_array( $fields['content'] ) ) {
1838  throw new MWException(
1839  'content field must contain a Content object or an array of Content objects.'
1840  );
1841  }
1842  }
1843 
1844  if ( !empty( $fields['text_id'] ) ) {
1845  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
1846  throw new MWException( "The text_id field is only available in the pre-MCR schema" );
1847  }
1848 
1849  if ( !empty( $fields['content'] ) ) {
1850  throw new MWException(
1851  "Text already stored in external store (id {$fields['text_id']}), " .
1852  "can't specify content object"
1853  );
1854  }
1855  }
1856 
1857  if (
1858  isset( $fields['comment'] )
1859  && !( $fields['comment'] instanceof CommentStoreComment )
1860  ) {
1861  $commentData = $fields['comment_data'] ?? null;
1862 
1863  if ( $fields['comment'] instanceof Message ) {
1864  $fields['comment'] = CommentStoreComment::newUnsavedComment(
1865  $fields['comment'],
1866  $commentData
1867  );
1868  } else {
1869  $commentText = trim( strval( $fields['comment'] ) );
1870  $fields['comment'] = CommentStoreComment::newUnsavedComment(
1871  $commentText,
1872  $commentData
1873  );
1874  }
1875  }
1876 
1877  $revision = new MutableRevisionRecord( $title, $this->wikiId );
1878  $this->initializeMutableRevisionFromArray( $revision, $fields );
1879 
1880  if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
1881  foreach ( $fields['content'] as $role => $content ) {
1882  $revision->setContent( $role, $content );
1883  }
1884  } else {
1885  $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
1886  $revision->setSlot( $mainSlot );
1887  }
1888 
1889  return $revision;
1890  }
1891 
1897  MutableRevisionRecord $record,
1898  array $fields
1899  ) {
1901  $user = null;
1902 
1903  if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
1904  $user = $fields['user'];
1905  } else {
1906  try {
1908  $fields['user'] ?? null,
1909  $fields['user_text'] ?? null,
1910  $fields['actor'] ?? null
1911  );
1912  } catch ( InvalidArgumentException $ex ) {
1913  $user = null;
1914  }
1915  }
1916 
1917  if ( $user ) {
1918  $record->setUser( $user );
1919  }
1920 
1921  $timestamp = isset( $fields['timestamp'] )
1922  ? strval( $fields['timestamp'] )
1923  : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1924 
1925  $record->setTimestamp( $timestamp );
1926 
1927  if ( isset( $fields['page'] ) ) {
1928  $record->setPageId( intval( $fields['page'] ) );
1929  }
1930 
1931  if ( isset( $fields['id'] ) ) {
1932  $record->setId( intval( $fields['id'] ) );
1933  }
1934  if ( isset( $fields['parent_id'] ) ) {
1935  $record->setParentId( intval( $fields['parent_id'] ) );
1936  }
1937 
1938  if ( isset( $fields['sha1'] ) ) {
1939  $record->setSha1( $fields['sha1'] );
1940  }
1941  if ( isset( $fields['size'] ) ) {
1942  $record->setSize( intval( $fields['size'] ) );
1943  }
1944 
1945  if ( isset( $fields['minor_edit'] ) ) {
1946  $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
1947  }
1948  if ( isset( $fields['deleted'] ) ) {
1949  $record->setVisibility( intval( $fields['deleted'] ) );
1950  }
1951 
1952  if ( isset( $fields['comment'] ) ) {
1953  Assert::parameterType(
1955  $fields['comment'],
1956  '$row[\'comment\']'
1957  );
1958  $record->setComment( $fields['comment'] );
1959  }
1960  }
1961 
1976  public function loadRevisionFromId( IDatabase $db, $id ) {
1977  return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
1978  }
1979 
1995  public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
1996  $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
1997  if ( $id ) {
1998  $conds['rev_id'] = intval( $id );
1999  } else {
2000  $conds[] = 'rev_id=page_latest';
2001  }
2002  return $this->loadRevisionFromConds( $db, $conds );
2003  }
2004 
2021  public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2022  if ( $id ) {
2023  $matchId = intval( $id );
2024  } else {
2025  $matchId = 'page_latest';
2026  }
2027 
2028  return $this->loadRevisionFromConds(
2029  $db,
2030  [
2031  "rev_id=$matchId",
2032  'page_namespace' => $title->getNamespace(),
2033  'page_title' => $title->getDBkey()
2034  ],
2035  0,
2036  $title
2037  );
2038  }
2039 
2055  public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2056  return $this->loadRevisionFromConds( $db,
2057  [
2058  'rev_timestamp' => $db->timestamp( $timestamp ),
2059  'page_namespace' => $title->getNamespace(),
2060  'page_title' => $title->getDBkey()
2061  ],
2062  0,
2063  $title
2064  );
2065  }
2066 
2082  private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
2083  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2084  $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
2085 
2086  $lb = $this->getDBLoadBalancer();
2087 
2088  // Make sure new pending/committed revision are visibile later on
2089  // within web requests to certain avoid bugs like T93866 and T94407.
2090  if ( !$rev
2091  && !( $flags & self::READ_LATEST )
2092  && $lb->getServerCount() > 1
2093  && $lb->hasOrMadeRecentMasterChanges()
2094  ) {
2095  $flags = self::READ_LATEST;
2096  $dbw = $this->getDBConnection( DB_MASTER );
2097  $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
2098  $this->releaseDBConnection( $dbw );
2099  }
2100 
2101  return $rev;
2102  }
2103 
2117  private function loadRevisionFromConds(
2118  IDatabase $db,
2119  $conditions,
2120  $flags = 0,
2121  Title $title = null
2122  ) {
2123  $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
2124  if ( $row ) {
2125  $rev = $this->newRevisionFromRow( $row, $flags, $title );
2126 
2127  return $rev;
2128  }
2129 
2130  return null;
2131  }
2132 
2140  private function checkDatabaseWikiId( IDatabase $db ) {
2141  $storeWiki = $this->wikiId;
2142  $dbWiki = $db->getDomainID();
2143 
2144  if ( $dbWiki === $storeWiki ) {
2145  return;
2146  }
2147 
2148  $storeWiki = $storeWiki ?: $this->loadBalancer->getLocalDomainID();
2149  // @FIXME: when would getDomainID() be false here?
2150  $dbWiki = $dbWiki ?: wfWikiID();
2151 
2152  if ( $dbWiki === $storeWiki ) {
2153  return;
2154  }
2155 
2156  // HACK: counteract encoding imposed by DatabaseDomain
2157  $storeWiki = str_replace( '?h', '-', $storeWiki );
2158  $dbWiki = str_replace( '?h', '-', $dbWiki );
2159 
2160  if ( $dbWiki === $storeWiki ) {
2161  return;
2162  }
2163 
2164  throw new MWException( "RevisionStore for $storeWiki "
2165  . "cannot be used with a DB connection for $dbWiki" );
2166  }
2167 
2180  private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
2181  $this->checkDatabaseWikiId( $db );
2182 
2183  $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2184  $options = [];
2185  if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2186  $options[] = 'FOR UPDATE';
2187  }
2188  return $db->selectRow(
2189  $revQuery['tables'],
2190  $revQuery['fields'],
2191  $conditions,
2192  __METHOD__,
2193  $options,
2194  $revQuery['joins']
2195  );
2196  }
2197 
2212  private function findSlotContentId( IDatabase $db, $revId, $role ) {
2213  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
2214  return null;
2215  }
2216 
2217  try {
2218  $roleId = $this->slotRoleStore->getId( $role );
2219  $conditions = [
2220  'slot_revision_id' => $revId,
2221  'slot_role_id' => $roleId,
2222  ];
2223 
2224  $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
2225 
2226  return $contentId ?: null;
2227  } catch ( NameTableAccessException $ex ) {
2228  // If the role is missing from the slot_roles table,
2229  // the corresponding row in slots cannot exist.
2230  return null;
2231  }
2232  }
2233 
2257  public function getQueryInfo( $options = [] ) {
2258  $ret = [
2259  'tables' => [],
2260  'fields' => [],
2261  'joins' => [],
2262  ];
2263 
2264  $ret['tables'][] = 'revision';
2265  $ret['fields'] = array_merge( $ret['fields'], [
2266  'rev_id',
2267  'rev_page',
2268  'rev_timestamp',
2269  'rev_minor_edit',
2270  'rev_deleted',
2271  'rev_len',
2272  'rev_parent_id',
2273  'rev_sha1',
2274  ] );
2275 
2276  $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2277  $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2278  $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2279  $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2280 
2281  $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2282  $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2283  $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2284  $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2285 
2286  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2287  $ret['fields'][] = 'rev_text_id';
2288 
2289  if ( $this->contentHandlerUseDB ) {
2290  $ret['fields'][] = 'rev_content_format';
2291  $ret['fields'][] = 'rev_content_model';
2292  }
2293  }
2294 
2295  if ( in_array( 'page', $options, true ) ) {
2296  $ret['tables'][] = 'page';
2297  $ret['fields'] = array_merge( $ret['fields'], [
2298  'page_namespace',
2299  'page_title',
2300  'page_id',
2301  'page_latest',
2302  'page_is_redirect',
2303  'page_len',
2304  ] );
2305  $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
2306  }
2307 
2308  if ( in_array( 'user', $options, true ) ) {
2309  $ret['tables'][] = 'user';
2310  $ret['fields'] = array_merge( $ret['fields'], [
2311  'user_name',
2312  ] );
2313  $u = $actorQuery['fields']['rev_user'];
2314  $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2315  }
2316 
2317  if ( in_array( 'text', $options, true ) ) {
2318  if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
2319  throw new InvalidArgumentException( 'text table can no longer be joined directly' );
2320  } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2321  // NOTE: even when this class is set to not read from the old schema, callers
2322  // should still be able to join against the text table, as long as we are still
2323  // writing the old schema for compatibility.
2324  // TODO: This should trigger a deprecation warning eventually (T200918), but not
2325  // before all known usages are removed (see T198341 and T201164).
2326  // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
2327  }
2328 
2329  $ret['tables'][] = 'text';
2330  $ret['fields'] = array_merge( $ret['fields'], [
2331  'old_text',
2332  'old_flags'
2333  ] );
2334  $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
2335  }
2336 
2337  return $ret;
2338  }
2339 
2357  public function getSlotsQueryInfo( $options = [] ) {
2358  $ret = [
2359  'tables' => [],
2360  'fields' => [],
2361  'joins' => [],
2362  ];
2363 
2364  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2365  $db = $this->getDBConnectionRef( DB_REPLICA );
2366  $ret['tables']['slots'] = 'revision';
2367 
2368  $ret['fields']['slot_revision_id'] = 'slots.rev_id';
2369  $ret['fields']['slot_content_id'] = 'NULL';
2370  $ret['fields']['slot_origin'] = 'slots.rev_id';
2371  $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
2372 
2373  if ( in_array( 'content', $options, true ) ) {
2374  $ret['fields']['content_size'] = 'slots.rev_len';
2375  $ret['fields']['content_sha1'] = 'slots.rev_sha1';
2376  $ret['fields']['content_address']
2377  = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
2378 
2379  if ( $this->contentHandlerUseDB ) {
2380  $ret['fields']['model_name'] = 'slots.rev_content_model';
2381  } else {
2382  $ret['fields']['model_name'] = 'NULL';
2383  }
2384  }
2385  } else {
2386  $ret['tables'][] = 'slots';
2387  $ret['fields'] = array_merge( $ret['fields'], [
2388  'slot_revision_id',
2389  'slot_content_id',
2390  'slot_origin',
2391  'slot_role_id',
2392  ] );
2393 
2394  if ( in_array( 'role', $options, true ) ) {
2395  // Use left join to attach role name, so we still find the revision row even
2396  // if the role name is missing. This triggers a more obvious failure mode.
2397  $ret['tables'][] = 'slot_roles';
2398  $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2399  $ret['fields'][] = 'role_name';
2400  }
2401 
2402  if ( in_array( 'content', $options, true ) ) {
2403  $ret['tables'][] = 'content';
2404  $ret['fields'] = array_merge( $ret['fields'], [
2405  'content_size',
2406  'content_sha1',
2407  'content_address',
2408  'content_model',
2409  ] );
2410  $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ];
2411 
2412  if ( in_array( 'model', $options, true ) ) {
2413  // Use left join to attach model name, so we still find the revision row even
2414  // if the model name is missing. This triggers a more obvious failure mode.
2415  $ret['tables'][] = 'content_models';
2416  $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2417  $ret['fields'][] = 'model_name';
2418  }
2419 
2420  }
2421  }
2422 
2423  return $ret;
2424  }
2425 
2439  public function getArchiveQueryInfo() {
2440  $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2441  $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2442  $ret = [
2443  'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2444  'fields' => [
2445  'ar_id',
2446  'ar_page_id',
2447  'ar_namespace',
2448  'ar_title',
2449  'ar_rev_id',
2450  'ar_timestamp',
2451  'ar_minor_edit',
2452  'ar_deleted',
2453  'ar_len',
2454  'ar_parent_id',
2455  'ar_sha1',
2456  ] + $commentQuery['fields'] + $actorQuery['fields'],
2457  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2458  ];
2459 
2460  if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2461  $ret['fields'][] = 'ar_text_id';
2462 
2463  if ( $this->contentHandlerUseDB ) {
2464  $ret['fields'][] = 'ar_content_format';
2465  $ret['fields'][] = 'ar_content_model';
2466  }
2467  }
2468 
2469  return $ret;
2470  }
2471 
2481  public function getRevisionSizes( array $revIds ) {
2482  return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
2483  }
2484 
2497  public function listRevisionSizes( IDatabase $db, array $revIds ) {
2498  $this->checkDatabaseWikiId( $db );
2499 
2500  $revLens = [];
2501  if ( !$revIds ) {
2502  return $revLens; // empty
2503  }
2504 
2505  $res = $db->select(
2506  'revision',
2507  [ 'rev_id', 'rev_len' ],
2508  [ 'rev_id' => $revIds ],
2509  __METHOD__
2510  );
2511 
2512  foreach ( $res as $row ) {
2513  $revLens[$row->rev_id] = intval( $row->rev_len );
2514  }
2515 
2516  return $revLens;
2517  }
2518 
2529  public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
2530  if ( $title === null ) {
2531  $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
2532  }
2533  $prev = $title->getPreviousRevisionID( $rev->getId() );
2534  if ( $prev ) {
2535  return $this->getRevisionByTitle( $title, $prev );
2536  }
2537  return null;
2538  }
2539 
2550  public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
2551  if ( $title === null ) {
2552  $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
2553  }
2554  $next = $title->getNextRevisionID( $rev->getId() );
2555  if ( $next ) {
2556  return $this->getRevisionByTitle( $title, $next );
2557  }
2558  return null;
2559  }
2560 
2572  private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2573  $this->checkDatabaseWikiId( $db );
2574 
2575  if ( $rev->getPageId() === null ) {
2576  return 0;
2577  }
2578  # Use page_latest if ID is not given
2579  if ( !$rev->getId() ) {
2580  $prevId = $db->selectField(
2581  'page', 'page_latest',
2582  [ 'page_id' => $rev->getPageId() ],
2583  __METHOD__
2584  );
2585  } else {
2586  $prevId = $db->selectField(
2587  'revision', 'rev_id',
2588  [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
2589  __METHOD__,
2590  [ 'ORDER BY' => 'rev_id DESC' ]
2591  );
2592  }
2593  return intval( $prevId );
2594  }
2595 
2606  public function getTimestampFromId( $title, $id, $flags = 0 ) {
2607  $db = $this->getDBConnectionRefForQueryFlags( $flags );
2608 
2609  $conds = [ 'rev_id' => $id ];
2610  $conds['rev_page'] = $title->getArticleID();
2611  $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
2612 
2613  return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
2614  }
2615 
2625  public function countRevisionsByPageId( IDatabase $db, $id ) {
2626  $this->checkDatabaseWikiId( $db );
2627 
2628  $row = $db->selectRow( 'revision',
2629  [ 'revCount' => 'COUNT(*)' ],
2630  [ 'rev_page' => $id ],
2631  __METHOD__
2632  );
2633  if ( $row ) {
2634  return intval( $row->revCount );
2635  }
2636  return 0;
2637  }
2638 
2648  public function countRevisionsByTitle( IDatabase $db, $title ) {
2649  $id = $title->getArticleID();
2650  if ( $id ) {
2651  return $this->countRevisionsByPageId( $db, $id );
2652  }
2653  return 0;
2654  }
2655 
2674  public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2675  $this->checkDatabaseWikiId( $db );
2676 
2677  if ( !$userId ) {
2678  return false;
2679  }
2680 
2681  $revQuery = $this->getQueryInfo();
2682  $res = $db->select(
2683  $revQuery['tables'],
2684  [
2685  'rev_user' => $revQuery['fields']['rev_user'],
2686  ],
2687  [
2688  'rev_page' => $pageId,
2689  'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2690  ],
2691  __METHOD__,
2692  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2693  $revQuery['joins']
2694  );
2695  foreach ( $res as $row ) {
2696  if ( $row->rev_user != $userId ) {
2697  return false;
2698  }
2699  }
2700  return true;
2701  }
2702 
2716  public function getKnownCurrentRevision( Title $title, $revId ) {
2717  $db = $this->getDBConnectionRef( DB_REPLICA );
2718 
2719  $pageId = $title->getArticleID();
2720 
2721  if ( !$pageId ) {
2722  return false;
2723  }
2724 
2725  if ( !$revId ) {
2726  $revId = $title->getLatestRevID();
2727  }
2728 
2729  if ( !$revId ) {
2730  wfWarn(
2731  'No latest revision known for page ' . $title->getPrefixedDBkey()
2732  . ' even though it exists with page ID ' . $pageId
2733  );
2734  return false;
2735  }
2736 
2737  $row = $this->cache->getWithSetCallback(
2738  // Page/rev IDs passed in from DB to reflect history merges
2739  $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2741  function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
2742  $setOpts += Database::getCacheSetOptions( $db );
2743 
2744  $conds = [
2745  'rev_page' => intval( $pageId ),
2746  'page_id' => intval( $pageId ),
2747  'rev_id' => intval( $revId ),
2748  ];
2749 
2750  $row = $this->fetchRevisionRowFromConds( $db, $conds );
2751  return $row ?: false; // don't cache negatives
2752  }
2753  );
2754 
2755  // Reflect revision deletion and user renames
2756  if ( $row ) {
2757  return $this->newRevisionFromRow( $row, 0, $title );
2758  } else {
2759  return false;
2760  }
2761  }
2762 
2774  private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2775  return $this->cache->makeGlobalKey(
2776  self::ROW_CACHE_KEY,
2777  $db->getDomainID(),
2778  $pageId,
2779  $revId
2780  );
2781  }
2782 
2783  // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
2784 
2785 }
2786 
2791 class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Revision\MutableRevisionRecord\setMinorEdit
setMinorEdit( $minorEdit)
Definition: MutableRevisionRecord.php:240
Revision\RevisionStore\$commentStore
CommentStore $commentStore
Definition: RevisionStore.php:110
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:32
Revision\RevisionStore\$logger
LoggerInterface $logger
Definition: RevisionStore.php:120
IP\toHex
static toHex( $ip)
Return a zero-padded upper case hexadecimal representation of an IP address.
Definition: IP.php:417
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:46
ContentHandler
A content handler knows how do deal with a specific type of content on a wiki page.
Definition: ContentHandler.php:53
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:297
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:48
Revision\RevisionStore\insertSlotOn
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:670
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
$user
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:244
Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:33
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:40
MediaWiki\Storage\BlobAccessException
Exception representing a failure to access a data blob.
Definition: BlobAccessException.php:32
Revision\RevisionStore\getRecentChange
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
Definition: RevisionStore.php:1145
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:45
SCHEMA_COMPAT_READ_NEW
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:287
Revision\RevisionStore\newRevisionFromConds
newRevisionFromConds( $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision.
Definition: RevisionStore.php:2082
Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:31
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:304
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:2117
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:437
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:1811
Revision\RevisionStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags( $queryFlags)
Definition: RevisionStore.php:289
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:204
Revision\RevisionStore\failOnEmpty
failOnEmpty( $value, $name)
Definition: RevisionStore.php:418
Revision\MutableRevisionRecord\setSha1
setSha1( $sha1)
Set revision hash, for optimization.
Definition: MutableRevisionRecord.php:198
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:1232
Revision\MutableRevisionRecord\setParentId
setParentId( $parentId)
Definition: MutableRevisionRecord.php:89
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:76
MediaWiki\Storage\SqlBlobStore
Service for storing and loading Content objects.
Definition: SqlBlobStore.php:50
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:68
Revision\RevisionStore\initializeMutableRevisionFromArray
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
Definition: RevisionStore.php:1896
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1954
Revision\RevisionStore\loadSlotRecords
loadSlotRecords( $revId, $queryFlags)
Definition: RevisionStore.php:1603
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:448
Revision\RevisionStore\getArchiveQueryInfo
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Definition: RevisionStore.php:2439
Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:52
Revision\MutableRevisionRecord\setPageId
setPageId( $pageId)
Definition: MutableRevisionRecord.php:275
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:436
IP
A collection of public static functions to play with IP address and IP ranges.
Definition: IP.php:67
CommentStore
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
Definition: CommentStore.php:31
Revision\RevisionStore\$cache
WANObjectCache $cache
Definition: RevisionStore.php:105
Revision\RevisionStore\getRcIdIfUnpatrolled
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
Definition: RevisionStore.php:1123
User\newFromAnyId
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:682
Revision\RevisionFactory
Service for constructing revision objects.
Definition: RevisionFactory.php:37
Revision\MutableRevisionRecord\setId
setId( $id)
Set the revision ID.
Definition: MutableRevisionRecord.php:257
$res
$res
Definition: database.txt:21
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:35
User
User
Definition: All_system_messages.txt:425
Revision\RevisionStore\getSlotsQueryInfo
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
Definition: RevisionStore.php:2357
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
cache
you have access to all of the normal MediaWiki so you can get a DB use the cache
Definition: maintenance.txt:52
Revision\RevisionStore\$actorMigration
ActorMigration $actorMigration
Definition: RevisionStore.php:115
$revQuery
$revQuery
Definition: testCompression.php:51
Revision\RevisionStore\insertIpChangesRow
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
Definition: RevisionStore.php:722
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:1182
Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
php
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
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:2497
Revision\RevisionStore\$mcrMigrationStage
int $mcrMigrationStage
An appropriate combination of SCHEMA_COMPAT_XXX flags.
Definition: RevisionStore.php:133
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:2674
$dbr
$dbr
Definition: testCompression.php:50
MediaWiki\Revision
Created by PhpStorm.
Definition: IncompleteRevisionException.php:23
Revision\RevisionStore\checkContent
checkContent(Content $content, Title $title)
MCR migration note: this corresponds to Revision::checkContentModel.
Definition: RevisionStore.php:989
Revision
Definition: Revision.php:41
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:1427
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:182
Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:407
Revision\RevisionStore\getQueryInfo
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
Definition: RevisionStore.php:2257
MWException
MediaWiki exception.
Definition: MWException.php:26
Revision\RevisionStore\newRevisionSlots
newRevisionSlots( $revId, $revisionRow, $queryFlags, Title $title)
Factory method for RevisionSlots.
Definition: RevisionStore.php:1657
Revision\RevisionRecord\getSize
getSize()
Returns the nominal size of this revision, in bogo-bytes.
$title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:964
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:70
Revision\RevisionStore\getContentHandlerUseDB
getContentHandlerUseDB()
Definition: RevisionStore.php:245
Revision\MutableRevisionRecord\setUser
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
Definition: MutableRevisionRecord.php:268
Revision\RevisionRecord\getSha1
getSha1()
Returns the base36 sha1 of this revision.
Revision\RevisionStore\storeContentBlob
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
Definition: RevisionStore.php:917
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:440
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:4227
Revision\RevisionStore\getRevisionById
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1484
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:475
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:1548
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:1995
Revision\RevisionRecord\getUser
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Definition: RevisionRecord.php:365
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget)
Create a new Title from a LinkTarget.
Definition: Title.php:251
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:491
Revision\MutableRevisionRecord\setVisibility
setVisibility( $visibility)
Definition: MutableRevisionRecord.php:222
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:471
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:76
Revision\RevisionStore\getTimestampFromId
getTimestampFromId( $title, $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
Definition: RevisionStore.php:2606
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:2572
Revision\RevisionStore\getDBConnection
getDBConnection( $mode)
Definition: RevisionStore.php:279
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:605
Revision\RevisionStore\insertSlotRowOn
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
Definition: RevisionStore.php:951
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
Revision\RevisionStore\getRevisionSizes
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
Definition: RevisionStore.php:2481
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:59
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:563
Revision\RevisionStore\loadRevisionFromId
loadRevisionFromId(IDatabase $db, $id)
Load a page revision from a given revision ID number.
Definition: RevisionStore.php:1976
Revision\RevisionStore\getKnownCurrentRevision
getKnownCurrentRevision(Title $title, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: RevisionStore.php:2716
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:545
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:501
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1983
DB_MASTER
const DB_MASTER
Definition: defines.php:26
array
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
Revision\RevisionStore\checkDatabaseWikiId
checkDatabaseWikiId(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
Definition: RevisionStore.php:2140
Revision\RevisionRecord\getPageId
getPageId()
Get the page ID.
Definition: RevisionRecord.php:325
Revision\RevisionStore\$blobStore
SqlBlobStore $blobStore
Definition: RevisionStore.php:84
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
list
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
Revision\RevisionStore\setLogger
setLogger(LoggerInterface $logger)
Definition: RevisionStore.php:231
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:540
Revision\RevisionStore\ROW_CACHE_KEY
const ROW_CACHE_KEY
Definition: RevisionStore.php:79
$fname
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition: Setup.php:121
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:302
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:1401
wfWikiID
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
Definition: GlobalFunctions.php:2644
$e
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2213
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:524
$value
$value
Definition: styleTest.css.php:49
DBAccessObjectUtils\hasFlags
static hasFlags( $bitfield, $flags)
Definition: DBAccessObjectUtils.php:35
Revision\RevisionStore\newRevisionFromRow
newRevisionFromRow( $row, $queryFlags=0, Title $title=null)
Definition: RevisionStore.php:1760
Revision\MutableRevisionRecord
Mutable RevisionRecord implementation, for building new revision entries programmatically.
Definition: MutableRevisionRecord.php:41
SCHEMA_COMPAT_WRITE_OLD
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:284
Title\GAID_FOR_UPDATE
const GAID_FOR_UPDATE
Used to be GAID_FOR_UPDATE define.
Definition: Title.php:54
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:118
Revision\RevisionStore\getNextRevision
getNextRevision(RevisionRecord $rev, Title $title=null)
Get next revision for this title.
Definition: RevisionStore.php:2550
Revision\RevisionStore\$slotRoleStore
NameTableStore $slotRoleStore
Definition: RevisionStore.php:130
SCHEMA_COMPAT_WRITE_NEW
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:286
Revision\RevisionStore\setContentHandlerUseDB
setContentHandlerUseDB( $contentHandlerUseDB)
Definition: RevisionStore.php:254
$ret
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:2036
Revision\RevisionStore\countRevisionsByTitle
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2648
Revision\RevisionStore\hasMcrSchemaFlags
hasMcrSchemaFlags( $flags)
Definition: RevisionStore.php:213
Revision\RevisionRecord\getComment
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
Definition: RevisionRecord.php:390
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:35
Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:39
Revision\RevisionStore\releaseDBConnection
releaseDBConnection(IDatabase $connection)
Definition: RevisionStore.php:297
$handler
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:813
IP\isValid
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
Revision\RevisionStore\getRevisionByTitle
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: RevisionStore.php:1504
Revision\RevisionStore\assertCrossWikiContentLoadingIsSafe
assertCrossWikiContentLoadingIsSafe()
Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading and still...
Definition: RevisionStore.php:223
MediaWiki\Storage\BlobStore
Service for loading and storing data blobs.
Definition: BlobStore.php:33
Content
Base interface for content objects.
Definition: Content.php:34
IExpiringStore\TTL_WEEK
const TTL_WEEK
Definition: IExpiringStore.php:37
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:1059
Revision\RevisionStore\getPreviousRevision
getPreviousRevision(RevisionRecord $rev, Title $title=null)
Get previous revision for this title.
Definition: RevisionStore.php:2529
Wikimedia\Rdbms\DBConnRef
Helper class to handle automatically marking connections as reusable (via RAII pattern) as well handl...
Definition: DBConnRef.php:15
Title
Represents a title within MediaWiki.
Definition: Title.php:39
Revision\RevisionStore\insertContentRowOn
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
Definition: RevisionStore.php:969
Revision\RevisionStore\getRevisionByTimestamp
getRevisionByTimestamp( $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:1584
Revision\RevisionStore\$contentHandlerUseDB
boolean $contentHandlerUseDB
Definition: RevisionStore.php:95
Revision\RevisionStore\countRevisionsByPageId
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
Definition: RevisionStore.php:2625
Revision\RevisionStore\insertRevisionInternal
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
Definition: RevisionStore.php:556
$options
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:2036
Revision\RevisionStore\__construct
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, $mcrMigrationStage, ActorMigration $actorMigration, $wikiId=false)
Definition: RevisionStore.php:155
Revision\RevisionStore\$contentModelStore
NameTableStore $contentModelStore
Definition: RevisionStore.php:125
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:2180
Revision\RevisionStore\getDBLoadBalancer
getDBLoadBalancer()
Definition: RevisionStore.php:270
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:1695
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:77
$rev
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:1808
as
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
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:32
SCHEMA_COMPAT_WRITE_BOTH
const SCHEMA_COMPAT_WRITE_BOTH
Definition: Defines.php:288
Revision\RevisionSlots
Value object representing the set of slots belonging to a revision.
Definition: RevisionSlots.php:35
MediaWiki\Storage\BlobStore\REVISION_HINT
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition: BlobStore.php:58
wfBacktrace
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
Definition: GlobalFunctions.php:1483
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:64
Revision\RevisionStore\$wikiId
bool string $wikiId
Definition: RevisionStore.php:89
$content
$content
Definition: pageupdater.txt:72
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:52
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:1132
Revision\MutableRevisionRecord\setTimestamp
setTimestamp( $timestamp)
Definition: MutableRevisionRecord.php:231
Revision\RevisionStore\failOnNull
failOnNull( $value, $name)
Definition: RevisionStore.php:401
class
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
Revision\RevisionStore\insertRevisionRowOn
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:749
Revision\MutableRevisionRecord\setSize
setSize( $size)
Set nominal revision size, for optimization.
Definition: MutableRevisionRecord.php:213
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Revision\RevisionStore\loadRevisionFromTimestamp
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: RevisionStore.php:2055
Revision\RevisionStore\getDBConnectionRef
getDBConnectionRef( $mode)
Definition: RevisionStore.php:307
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:515
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:47
Revision\RevisionStore\updateRevisionTextId
updateRevisionTextId(IDatabase $dbw, $revisionId, &$blobAddress)
Definition: RevisionStore.php:640
Title\newFromID
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:427
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
SCHEMA_COMPAT_READ_BOTH
const SCHEMA_COMPAT_READ_BOTH
Definition: Defines.php:289
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:2774
CommentStoreComment
CommentStoreComment represents a comment stored by CommentStore.
Definition: CommentStoreComment.php:29
Revision\MutableRevisionRecord\setComment
setComment(CommentStoreComment $comment)
Definition: MutableRevisionRecord.php:185
SCHEMA_COMPAT_READ_OLD
const SCHEMA_COMPAT_READ_OLD
Definition: Defines.php:285
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:78
Revision\RevisionStore\getBaseRevisionRow
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
Definition: RevisionStore.php:866
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:2212
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:2021
Revision\RevisionStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevisionStore.php:100
Revision\RevisionStore\isReadOnly
isReadOnly()
Definition: RevisionStore.php:238
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:82
Revision\RevisionStore\getTitle
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
Definition: RevisionStore.php:326