MediaWiki REL1_33
RevisionStore.php
Go to the documentation of this file.
1<?php
27namespace MediaWiki\Revision;
28
37use InvalidArgumentException;
38use IP;
39use LogicException;
51use Psr\Log\LoggerAwareInterface;
52use Psr\Log\LoggerInterface;
53use Psr\Log\NullLogger;
56use RuntimeException;
57use stdClass;
59use User;
61use 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
101
105 private $cache;
106
111
116
120 private $logger;
121
126
131
134
137
158 public function __construct(
168 $wikiId = false
169 ) {
170 Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
171 Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
172 Assert::parameter(
174 '$mcrMigrationStage',
175 'Reading from the old and the new schema at the same time is not supported.'
176 );
177 Assert::parameter(
179 '$mcrMigrationStage',
180 'Reading needs to be enabled for the old or the new schema.'
181 );
182 Assert::parameter(
184 '$mcrMigrationStage',
185 'Writing needs to be enabled for the old or the new schema.'
186 );
187 Assert::parameter(
190 '$mcrMigrationStage',
191 'Cannot read the old schema when not also writing it.'
192 );
193 Assert::parameter(
196 '$mcrMigrationStage',
197 'Cannot read the new schema when not also writing it.'
198 );
199
200 $this->loadBalancer = $loadBalancer;
201 $this->blobStore = $blobStore;
202 $this->cache = $cache;
203 $this->commentStore = $commentStore;
204 $this->contentModelStore = $contentModelStore;
205 $this->slotRoleStore = $slotRoleStore;
206 $this->slotRoleRegistry = $slotRoleRegistry;
207 $this->mcrMigrationStage = $mcrMigrationStage;
208 $this->actorMigration = $actorMigration;
209 $this->wikiId = $wikiId;
210 $this->logger = new NullLogger();
211 }
212
218 private function hasMcrSchemaFlags( $flags ) {
219 return ( $this->mcrMigrationStage & $flags ) === $flags;
220 }
221
229 if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
230 throw new RevisionAccessException(
231 "Cross-wiki content loading is not supported by the pre-MCR schema"
232 );
233 }
234 }
235
236 public function setLogger( LoggerInterface $logger ) {
237 $this->logger = $logger;
238 }
239
243 public function isReadOnly() {
244 return $this->blobStore->isReadOnly();
245 }
246
250 public function getContentHandlerUseDB() {
252 }
253
262 ) {
263 if ( !$contentHandlerUseDB ) {
264 throw new MWException(
265 'Content model must be stored in the database for multi content revision migration.'
266 );
267 }
268 }
269 $this->contentHandlerUseDB = $contentHandlerUseDB;
270 }
271
275 private function getDBLoadBalancer() {
276 return $this->loadBalancer;
277 }
278
284 private function getDBConnection( $mode ) {
285 $lb = $this->getDBLoadBalancer();
286 return $lb->getConnection( $mode, [], $this->wikiId );
287 }
288
294 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
295 list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
296 return $this->getDBConnectionRef( $mode );
297 }
298
302 private function releaseDBConnection( IDatabase $connection ) {
303 $lb = $this->getDBLoadBalancer();
304 $lb->reuseConnection( $connection );
305 }
306
312 private function getDBConnectionRef( $mode ) {
313 $lb = $this->getDBLoadBalancer();
314 return $lb->getConnectionRef( $mode, [], $this->wikiId );
315 }
316
331 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
332 if ( !$pageId && !$revId ) {
333 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
334 }
335
336 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
337 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
338 if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
339 $queryFlags = self::READ_NORMAL;
340 }
341
342 $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
343 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
344 $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
345
346 // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
347 if ( $canUseTitleNewFromId ) {
348 // TODO: better foreign title handling (introduce TitleFactory)
349 $title = Title::newFromID( $pageId, $titleFlags );
350 if ( $title ) {
351 return $title;
352 }
353 }
354
355 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
356 $canUseRevId = ( $revId !== null && $revId > 0 );
357
358 if ( $canUseRevId ) {
359 $dbr = $this->getDBConnectionRef( $dbMode );
360 // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
361 $row = $dbr->selectRow(
362 [ 'revision', 'page' ],
363 [
364 'page_namespace',
365 'page_title',
366 'page_id',
367 'page_latest',
368 'page_is_redirect',
369 'page_len',
370 ],
371 [ 'rev_id' => $revId ],
372 __METHOD__,
373 $dbOptions,
374 [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
375 );
376 if ( $row ) {
377 // TODO: better foreign title handling (introduce TitleFactory)
378 return Title::newFromRow( $row );
379 }
380 }
381
382 // If we still don't have a title, fallback to master if that wasn't already happening.
383 if ( $dbMode !== DB_MASTER ) {
384 $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
385 if ( $title ) {
386 $this->logger->info(
387 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
388 [ 'trace' => wfBacktrace() ]
389 );
390 return $title;
391 }
392 }
393
394 throw new RevisionAccessException(
395 "Could not determine title for page ID $pageId and revision ID $revId"
396 );
397 }
398
406 private function failOnNull( $value, $name ) {
407 if ( $value === null ) {
409 "$name must not be " . var_export( $value, true ) . "!"
410 );
411 }
412
413 return $value;
414 }
415
423 private function failOnEmpty( $value, $name ) {
424 if ( $value === null || $value === 0 || $value === '' ) {
426 "$name must not be " . var_export( $value, true ) . "!"
427 );
428 }
429
430 return $value;
431 }
432
446 // TODO: pass in a DBTransactionContext instead of a database connection.
447 $this->checkDatabaseWikiId( $dbw );
448
449 $slotRoles = $rev->getSlotRoles();
450
451 // Make sure the main slot is always provided throughout migration
452 if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
453 throw new InvalidArgumentException(
454 'main slot must be provided'
455 );
456 }
457
458 // If we are not writing into the new schema, we can't support extra slots.
460 && $slotRoles !== [ SlotRecord::MAIN ]
461 ) {
462 throw new InvalidArgumentException(
463 'Only the main slot is supported when not writing to the MCR enabled schema!'
464 );
465 }
466
467 // As long as we are not reading from the new schema, we don't want to write extra slots.
469 && $slotRoles !== [ SlotRecord::MAIN ]
470 ) {
471 throw new InvalidArgumentException(
472 'Only the main slot is supported when not reading from the MCR enabled schema!'
473 );
474 }
475
476 // Checks
477 $this->failOnNull( $rev->getSize(), 'size field' );
478 $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
479 $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
480 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
481 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
482 $this->failOnNull( $user->getId(), 'user field' );
483 $this->failOnEmpty( $user->getName(), 'user_text field' );
484
485 if ( !$rev->isReadyForInsertion() ) {
486 // This is here for future-proofing. At the time this check being added, it
487 // was redundant to the individual checks above.
488 throw new IncompleteRevisionException( 'Revision is incomplete' );
489 }
490
491 // TODO: we shouldn't need an actual Title here.
492 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
493 $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
494
495 $parentId = $rev->getParentId() === null
496 ? $this->getPreviousRevisionId( $dbw, $rev )
497 : $rev->getParentId();
498
500 $rev = $dbw->doAtomicSection(
501 __METHOD__,
502 function ( IDatabase $dbw, $fname ) use (
503 $rev,
504 $user,
505 $comment,
506 $title,
507 $pageId,
508 $parentId
509 ) {
510 return $this->insertRevisionInternal(
511 $rev,
512 $dbw,
513 $user,
514 $comment,
515 $title,
516 $pageId,
517 $parentId
518 );
519 }
520 );
521
522 // sanity checks
523 Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
524 Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
525 Assert::postcondition(
526 $rev->getComment( RevisionRecord::RAW ) !== null,
527 'revision must have a comment'
528 );
529 Assert::postcondition(
530 $rev->getUser( RevisionRecord::RAW ) !== null,
531 'revision must have a user'
532 );
533
534 // Trigger exception if the main slot is missing.
535 // Technically, this could go away after MCR migration: while
536 // calling code may require a main slot to exist, RevisionStore
537 // really should not know or care about that requirement.
539
540 foreach ( $slotRoles as $role ) {
541 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
542 Assert::postcondition(
543 $slot->getContent() !== null,
544 $role . ' slot must have content'
545 );
546 Assert::postcondition(
547 $slot->hasRevision(),
548 $role . ' slot must have a revision associated'
549 );
550 }
551
552 Hooks::run( 'RevisionRecordInserted', [ $rev ] );
553
554 // TODO: deprecate in 1.32!
555 $legacyRevision = new Revision( $rev );
556 Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
557
558 return $rev;
559 }
560
561 private function insertRevisionInternal(
563 IDatabase $dbw,
564 User $user,
565 CommentStoreComment $comment,
566 Title $title,
567 $pageId,
568 $parentId
569 ) {
570 $slotRoles = $rev->getSlotRoles();
571
572 $revisionRow = $this->insertRevisionRowOn(
573 $dbw,
574 $rev,
575 $title,
576 $parentId
577 );
578
579 $revisionId = $revisionRow['rev_id'];
580
581 $blobHints = [
582 BlobStore::PAGE_HINT => $pageId,
583 BlobStore::REVISION_HINT => $revisionId,
584 BlobStore::PARENT_HINT => $parentId,
585 ];
586
587 $newSlots = [];
588 foreach ( $slotRoles as $role ) {
589 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
590
591 // If the SlotRecord already has a revision ID set, this means it already exists
592 // in the database, and should already belong to the current revision.
593 // However, a slot may already have a revision, but no content ID, if the slot
594 // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
595 // mode, and the respective archive row was not yet migrated to the new schema.
596 // In that case, a new slot row (and content row) must be inserted even during
597 // undeletion.
598 if ( $slot->hasRevision() && $slot->hasContentId() ) {
599 // TODO: properly abort transaction if the assertion fails!
600 Assert::parameter(
601 $slot->getRevision() === $revisionId,
602 'slot role ' . $slot->getRole(),
603 'Existing slot should belong to revision '
604 . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
605 );
606
607 // Slot exists, nothing to do, move along.
608 // This happens when restoring archived revisions.
609
610 $newSlots[$role] = $slot;
611
612 // Write the main slot's text ID to the revision table for backwards compatibility
613 if ( $slot->getRole() === SlotRecord::MAIN
614 && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
615 ) {
616 $blobAddress = $slot->getAddress();
617 $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
618 }
619 } else {
620 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
621 }
622 }
623
624 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
625
627 $title,
628 $user,
629 $comment,
630 (object)$revisionRow,
631 new RevisionSlots( $newSlots ),
632 $this->wikiId
633 );
634
635 return $rev;
636 }
637
645 private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
646 $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
647 if ( !$textId ) {
648 throw new LogicException(
649 'Blob address not supported in 1.29 database schema: ' . $blobAddress
650 );
651 }
652
653 // getTextIdFromAddress() is free to insert something into the text table, so $textId
654 // may be a new value, not anything already contained in $blobAddress.
655 $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
656
657 $dbw->update(
658 'revision',
659 [ 'rev_text_id' => $textId ],
660 [ 'rev_id' => $revisionId ],
661 __METHOD__
662 );
663
664 return $textId;
665 }
666
675 private function insertSlotOn(
676 IDatabase $dbw,
677 $revisionId,
678 SlotRecord $protoSlot,
679 Title $title,
680 array $blobHints = []
681 ) {
682 if ( $protoSlot->hasAddress() ) {
683 $blobAddress = $protoSlot->getAddress();
684 } else {
685 $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
686 }
687
688 $contentId = null;
689
690 // Write the main slot's text ID to the revision table for backwards compatibility
691 if ( $protoSlot->getRole() === SlotRecord::MAIN
692 && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
693 ) {
694 // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
695 // with the real content ID below.
696 $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
697 $contentId = $this->emulateContentId( $textId );
698 }
699
701 if ( $protoSlot->hasContentId() ) {
702 $contentId = $protoSlot->getContentId();
703 } else {
704 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
705 }
706
707 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
708 }
709
710 $savedSlot = SlotRecord::newSaved(
711 $revisionId,
712 $contentId,
713 $blobAddress,
714 $protoSlot
715 );
716
717 return $savedSlot;
718 }
719
727 private function insertIpChangesRow(
728 IDatabase $dbw,
729 User $user,
731 $revisionId
732 ) {
733 if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
734 $ipcRow = [
735 'ipc_rev_id' => $revisionId,
736 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
737 'ipc_hex' => IP::toHex( $user->getName() ),
738 ];
739 $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
740 }
741 }
742
754 private function insertRevisionRowOn(
755 IDatabase $dbw,
757 Title $title,
758 $parentId
759 ) {
760 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
761
762 list( $commentFields, $commentCallback ) =
763 $this->commentStore->insertWithTempTable(
764 $dbw,
765 'rev_comment',
766 $rev->getComment( RevisionRecord::RAW )
767 );
768 $revisionRow += $commentFields;
769
770 list( $actorFields, $actorCallback ) =
771 $this->actorMigration->getInsertValuesWithTempTable(
772 $dbw,
773 'rev_user',
774 $rev->getUser( RevisionRecord::RAW )
775 );
776 $revisionRow += $actorFields;
777
778 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
779
780 if ( !isset( $revisionRow['rev_id'] ) ) {
781 // only if auto-increment was used
782 $revisionRow['rev_id'] = intval( $dbw->insertId() );
783
784 if ( $dbw->getType() === 'mysql' ) {
785 // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
786 // auto-increment value to disk, so on server restart it might reuse IDs from deleted
787 // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
788
789 $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
790 $table = 'archive';
792 $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
793 if ( $maxRevId2 >= $maxRevId ) {
794 $maxRevId = $maxRevId2;
795 $table = 'slots';
796 }
797 }
798
799 if ( $maxRevId >= $revisionRow['rev_id'] ) {
800 $this->logger->debug(
801 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
802 . ' Trying to fix it.',
803 [
804 'revid' => $revisionRow['rev_id'],
805 'table' => $table,
806 'maxrevid' => $maxRevId,
807 ]
808 );
809
810 if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
811 throw new MWException( 'Failed to get database lock for T202032' );
812 }
813 $fname = __METHOD__;
814 $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) {
815 $dbw->unlock( 'fix-for-T202032', $fname );
816 } );
817
818 $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
819
820 // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
821 // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
822 // inserts too, though, at least on MariaDB 10.1.29.
823 //
824 // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
825 // transactions in this code path thanks to the row lock from the original ->insert() above.
826 //
827 // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
828 // that's for non-MySQL DBs.
829 $row1 = $dbw->query(
830 $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
831 )->fetchObject();
833 $row2 = $dbw->query(
834 $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
835 . ' FOR UPDATE'
836 )->fetchObject();
837 } else {
838 $row2 = null;
839 }
840 $maxRevId = max(
841 $maxRevId,
842 $row1 ? intval( $row1->v ) : 0,
843 $row2 ? intval( $row2->v ) : 0
844 );
845
846 // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
847 // transactions will throw a duplicate key error here. It doesn't seem worth trying
848 // to avoid that.
849 $revisionRow['rev_id'] = $maxRevId + 1;
850 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
851 }
852 }
853 }
854
855 $commentCallback( $revisionRow['rev_id'] );
856 $actorCallback( $revisionRow['rev_id'], $revisionRow );
857
858 return $revisionRow;
859 }
860
871 private function getBaseRevisionRow(
872 IDatabase $dbw,
874 Title $title,
875 $parentId
876 ) {
877 // Record the edit in revisions
878 $revisionRow = [
879 'rev_page' => $rev->getPageId(),
880 'rev_parent_id' => $parentId,
881 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
882 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
883 'rev_deleted' => $rev->getVisibility(),
884 'rev_len' => $rev->getSize(),
885 'rev_sha1' => $rev->getSha1(),
886 ];
887
888 if ( $rev->getId() !== null ) {
889 // Needed to restore revisions with their original ID
890 $revisionRow['rev_id'] = $rev->getId();
891 }
892
894 // In non MCR mode this IF section will relate to the main slot
895 $mainSlot = $rev->getSlot( SlotRecord::MAIN );
896 $model = $mainSlot->getModel();
897 $format = $mainSlot->getFormat();
898
899 // MCR migration note: rev_content_model and rev_content_format will go away
900 if ( $this->contentHandlerUseDB ) {
902
903 $defaultModel = ContentHandler::getDefaultModelFor( $title );
904 $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
905
906 $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
907 $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
908 }
909 }
910
911 return $revisionRow;
912 }
913
922 private function storeContentBlob(
923 SlotRecord $slot,
924 Title $title,
925 array $blobHints = []
926 ) {
927 $content = $slot->getContent();
928 $format = $content->getDefaultFormat();
929 $model = $content->getModel();
930
931 $this->checkContent( $content, $title, $slot->getRole() );
932
933 return $this->blobStore->storeBlob(
934 $content->serialize( $format ),
935 // These hints "leak" some information from the higher abstraction layer to
936 // low level storage to allow for optimization.
937 array_merge(
938 $blobHints,
939 [
940 BlobStore::DESIGNATION_HINT => 'page-content',
941 BlobStore::ROLE_HINT => $slot->getRole(),
942 BlobStore::SHA1_HINT => $slot->getSha1(),
943 BlobStore::MODEL_HINT => $model,
944 BlobStore::FORMAT_HINT => $format,
945 ]
946 )
947 );
948 }
949
956 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
957 $slotRow = [
958 'slot_revision_id' => $revisionId,
959 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
960 'slot_content_id' => $contentId,
961 // If the slot has a specific origin use that ID, otherwise use the ID of the revision
962 // that we just inserted.
963 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
964 ];
965 $dbw->insert( 'slots', $slotRow, __METHOD__ );
966 }
967
974 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
975 $contentRow = [
976 'content_size' => $slot->getSize(),
977 'content_sha1' => $slot->getSha1(),
978 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
979 'content_address' => $blobAddress,
980 ];
981 $dbw->insert( 'content', $contentRow, __METHOD__ );
982 return intval( $dbw->insertId() );
983 }
984
995 private function checkContent( Content $content, Title $title, $role ) {
996 // Note: may return null for revisions that have not yet been inserted
997
998 $model = $content->getModel();
999 $format = $content->getDefaultFormat();
1000 $handler = $content->getContentHandler();
1001
1002 $name = "$title";
1003
1004 if ( !$handler->isSupportedFormat( $format ) ) {
1005 throw new MWException( "Can't use format $format with content model $model on $name" );
1006 }
1007
1008 if ( !$this->contentHandlerUseDB ) {
1009 // if $wgContentHandlerUseDB is not set,
1010 // all revisions must use the default content model and format.
1011
1013
1014 $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
1015 $defaultModel = $roleHandler->getDefaultModel( $title );
1016 $defaultHandler = ContentHandler::getForModelID( $defaultModel );
1017 $defaultFormat = $defaultHandler->getDefaultFormat();
1018
1019 if ( $model != $defaultModel ) {
1020 throw new MWException( "Can't save non-default content model with "
1021 . "\$wgContentHandlerUseDB disabled: model is $model, "
1022 . "default for $name is $defaultModel"
1023 );
1024 }
1025
1026 if ( $format != $defaultFormat ) {
1027 throw new MWException( "Can't use non-default content format with "
1028 . "\$wgContentHandlerUseDB disabled: format is $format, "
1029 . "default for $name is $defaultFormat"
1030 );
1031 }
1032 }
1033
1034 if ( !$content->isValid() ) {
1035 throw new MWException(
1036 "New content for $name is not valid! Content model is $model"
1037 );
1038 }
1039 }
1040
1066 public function newNullRevision(
1067 IDatabase $dbw,
1068 Title $title,
1069 CommentStoreComment $comment,
1070 $minor,
1071 User $user
1072 ) {
1073 $this->checkDatabaseWikiId( $dbw );
1074
1075 $pageId = $title->getArticleID();
1076
1077 // T51581: Lock the page table row to ensure no other process
1078 // is adding a revision to the page at the same time.
1079 // Avoid locking extra tables, compare T191892.
1080 $pageLatest = $dbw->selectField(
1081 'page',
1082 'page_latest',
1083 [ 'page_id' => $pageId ],
1084 __METHOD__,
1085 [ 'FOR UPDATE' ]
1086 );
1087
1088 if ( !$pageLatest ) {
1089 return null;
1090 }
1091
1092 // Fetch the actual revision row from master, without locking all extra tables.
1093 $oldRevision = $this->loadRevisionFromConds(
1094 $dbw,
1095 [ 'rev_id' => intval( $pageLatest ) ],
1096 self::READ_LATEST,
1097 $title
1098 );
1099
1100 if ( !$oldRevision ) {
1101 $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1102 $this->logger->error(
1103 $msg,
1104 [ 'exception' => new RuntimeException( $msg ) ]
1105 );
1106 return null;
1107 }
1108
1109 // Construct the new revision
1110 $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1111 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1112
1113 $newRevision->setComment( $comment );
1114 $newRevision->setUser( $user );
1115 $newRevision->setTimestamp( $timestamp );
1116 $newRevision->setMinorEdit( $minor );
1117
1118 return $newRevision;
1119 }
1120
1131 $rc = $this->getRecentChange( $rev );
1132 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1133 return $rc->getAttribute( 'rc_id' );
1134 } else {
1135 return 0;
1136 }
1137 }
1138
1152 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1153 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1154 $db = $this->getDBConnection( $dbType );
1155
1156 $userIdentity = $rev->getUser( RevisionRecord::RAW );
1157
1158 if ( !$userIdentity ) {
1159 // If the revision has no user identity, chances are it never went
1160 // into the database, and doesn't have an RC entry.
1161 return null;
1162 }
1163
1164 // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
1165 $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
1167 [
1168 $actorWhere['conds'],
1169 'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
1170 'rc_this_oldid' => $rev->getId()
1171 ],
1172 __METHOD__,
1173 $dbType
1174 );
1175
1176 $this->releaseDBConnection( $db );
1177
1178 // XXX: cache this locally? Glue it to the RevisionRecord?
1179 return $rc;
1180 }
1181
1189 private static function mapArchiveFields( $archiveRow ) {
1190 $fieldMap = [
1191 // keep with ar prefix:
1192 'ar_id' => 'ar_id',
1193
1194 // not the same suffix:
1195 'ar_page_id' => 'rev_page',
1196 'ar_rev_id' => 'rev_id',
1197
1198 // same suffix:
1199 'ar_text_id' => 'rev_text_id',
1200 'ar_timestamp' => 'rev_timestamp',
1201 'ar_user_text' => 'rev_user_text',
1202 'ar_user' => 'rev_user',
1203 'ar_actor' => 'rev_actor',
1204 'ar_minor_edit' => 'rev_minor_edit',
1205 'ar_deleted' => 'rev_deleted',
1206 'ar_len' => 'rev_len',
1207 'ar_parent_id' => 'rev_parent_id',
1208 'ar_sha1' => 'rev_sha1',
1209 'ar_comment' => 'rev_comment',
1210 'ar_comment_cid' => 'rev_comment_cid',
1211 'ar_comment_id' => 'rev_comment_id',
1212 'ar_comment_text' => 'rev_comment_text',
1213 'ar_comment_data' => 'rev_comment_data',
1214 'ar_comment_old' => 'rev_comment_old',
1215 'ar_content_format' => 'rev_content_format',
1216 'ar_content_model' => 'rev_content_model',
1217 ];
1218
1219 $revRow = new stdClass();
1220 foreach ( $fieldMap as $arKey => $revKey ) {
1221 if ( property_exists( $archiveRow, $arKey ) ) {
1222 $revRow->$revKey = $archiveRow->$arKey;
1223 }
1224 }
1225
1226 return $revRow;
1227 }
1228
1239 private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
1240 $mainSlotRow = new stdClass();
1241 $mainSlotRow->role_name = SlotRecord::MAIN;
1242 $mainSlotRow->model_name = null;
1243 $mainSlotRow->slot_revision_id = null;
1244 $mainSlotRow->slot_content_id = null;
1245 $mainSlotRow->content_address = null;
1246
1247 $content = null;
1248 $blobData = null;
1249 $blobFlags = null;
1250
1251 if ( is_object( $row ) ) {
1252 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1253 // Don't emulate from a row when using the new schema.
1254 // Emulating from an array is still OK.
1255 throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
1256 }
1257
1258 // archive row
1259 if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
1260 $row = $this->mapArchiveFields( $row );
1261 }
1262
1263 if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
1264 $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
1265 $row->rev_text_id
1266 );
1267 }
1268
1269 // This is used by null-revisions
1270 $mainSlotRow->slot_origin = isset( $row->slot_origin )
1271 ? intval( $row->slot_origin )
1272 : null;
1273
1274 if ( isset( $row->old_text ) ) {
1275 // this happens when the text-table gets joined directly, in the pre-1.30 schema
1276 $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
1277 // Check against selects that might have not included old_flags
1278 if ( !property_exists( $row, 'old_flags' ) ) {
1279 throw new InvalidArgumentException( 'old_flags was not set in $row' );
1280 }
1281 $blobFlags = $row->old_flags ?? '';
1282 }
1283
1284 $mainSlotRow->slot_revision_id = intval( $row->rev_id );
1285
1286 $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
1287 $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
1288 $mainSlotRow->model_name = isset( $row->rev_content_model )
1289 ? strval( $row->rev_content_model )
1290 : null;
1291 // XXX: in the future, we'll probably always use the default format, and drop content_format
1292 $mainSlotRow->format_name = isset( $row->rev_content_format )
1293 ? strval( $row->rev_content_format )
1294 : null;
1295
1296 if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
1297 // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1298 $mainSlotRow->slot_content_id
1299 = $this->emulateContentId( intval( $row->rev_text_id ) );
1300 }
1301 } elseif ( is_array( $row ) ) {
1302 $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
1303
1304 $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
1305 ? intval( $row['slot_origin'] )
1306 : null;
1307 $mainSlotRow->content_address = isset( $row['text_id'] )
1308 ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
1309 : null;
1310 $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
1311 $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
1312
1313 $mainSlotRow->model_name = isset( $row['content_model'] )
1314 ? strval( $row['content_model'] ) : null; // XXX: must be a string!
1315 // XXX: in the future, we'll probably always use the default format, and drop content_format
1316 $mainSlotRow->format_name = isset( $row['content_format'] )
1317 ? strval( $row['content_format'] ) : null;
1318 $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
1319 // XXX: If the flags field is not set then $blobFlags should be null so that no
1320 // decoding will happen. An empty string will result in default decodings.
1321 $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
1322
1323 // if we have a Content object, override mText and mContentModel
1324 if ( !empty( $row['content'] ) ) {
1325 if ( !( $row['content'] instanceof Content ) ) {
1326 throw new MWException( 'content field must contain a Content object.' );
1327 }
1328
1330 $content = $row['content'];
1331 $handler = $content->getContentHandler();
1332
1333 $mainSlotRow->model_name = $content->getModel();
1334
1335 // XXX: in the future, we'll probably always use the default format.
1336 if ( $mainSlotRow->format_name === null ) {
1337 $mainSlotRow->format_name = $handler->getDefaultFormat();
1338 }
1339 }
1340
1341 if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
1342 // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1343 $mainSlotRow->slot_content_id
1344 = $this->emulateContentId( intval( $row['text_id'] ) );
1345 }
1346 } else {
1347 throw new MWException( 'Revision constructor passed invalid row format.' );
1348 }
1349
1350 // With the old schema, the content changes with every revision,
1351 // except for null-revisions.
1352 if ( !isset( $mainSlotRow->slot_origin ) ) {
1353 $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
1354 }
1355
1356 if ( $mainSlotRow->model_name === null ) {
1357 $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
1359
1360 return $this->slotRoleRegistry->getRoleHandler( $slot->getRole() )
1361 ->getDefaultModel( $title );
1362 };
1363 }
1364
1365 if ( !$content ) {
1366 // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
1367 // is missing, but "empty revisions" with no content are used in some edge cases.
1368
1369 $content = function ( SlotRecord $slot )
1370 use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
1371 {
1372 return $this->loadSlotContent(
1373 $slot,
1374 $blobData,
1375 $blobFlags,
1376 $mainSlotRow->format_name,
1377 $queryFlags
1378 );
1379 };
1380 }
1381
1383 // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
1384 // the inherited slot to have the same content_id as the original slot. In that case,
1385 // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
1386 $mainSlotRow->slot_content_id =
1387 function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
1388 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1389 return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
1390 };
1391 }
1392
1393 return new SlotRecord( $mainSlotRow, $content );
1394 }
1395
1407 private function emulateContentId( $textId ) {
1408 // Return a negative number to ensure the ID is distinct from any real content IDs
1409 // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
1410 // mode.
1411 return -$textId;
1412 }
1413
1433 private function loadSlotContent(
1434 SlotRecord $slot,
1435 $blobData = null,
1436 $blobFlags = null,
1437 $blobFormat = null,
1438 $queryFlags = 0
1439 ) {
1440 if ( $blobData !== null ) {
1441 Assert::parameterType( 'string', $blobData, '$blobData' );
1442 Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1443
1444 $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1445
1446 if ( $blobFlags === null ) {
1447 // No blob flags, so use the blob verbatim.
1448 $data = $blobData;
1449 } else {
1450 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1451 if ( $data === false ) {
1452 throw new RevisionAccessException(
1453 "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1454 );
1455 }
1456 }
1457
1458 } else {
1459 $address = $slot->getAddress();
1460 try {
1461 $data = $this->blobStore->getBlob( $address, $queryFlags );
1462 } catch ( BlobAccessException $e ) {
1463 throw new RevisionAccessException(
1464 "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1465 );
1466 }
1467 }
1468
1469 // Unserialize content
1470 $handler = ContentHandler::getForModelID( $slot->getModel() );
1471
1472 $content = $handler->unserializeContent( $data, $blobFormat );
1473 return $content;
1474 }
1475
1490 public function getRevisionById( $id, $flags = 0 ) {
1491 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1492 }
1493
1510 public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1511 // TODO should not require Title in future (T206498)
1512 $title = Title::newFromLinkTarget( $linkTarget );
1513 $conds = [
1514 'page_namespace' => $title->getNamespace(),
1515 'page_title' => $title->getDBkey()
1516 ];
1517 if ( $revId ) {
1518 // Use the specified revision ID.
1519 // Note that we use newRevisionFromConds here because we want to retry
1520 // and fall back to master if the page is not found on a replica.
1521 // Since the caller supplied a revision ID, we are pretty sure the revision is
1522 // supposed to exist, so we should try hard to find it.
1523 $conds['rev_id'] = $revId;
1524 return $this->newRevisionFromConds( $conds, $flags, $title );
1525 } else {
1526 // Use a join to get the latest revision.
1527 // Note that we don't use newRevisionFromConds here because we don't want to retry
1528 // and fall back to master. The assumption is that we only want to force the fallback
1529 // if we are quite sure the revision exists because the caller supplied a revision ID.
1530 // If the page isn't found at all on a replica, it probably simply does not exist.
1531 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1532
1533 $conds[] = 'rev_id=page_latest';
1534 $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1535
1536 return $rev;
1537 }
1538 }
1539
1556 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1557 $conds = [ 'page_id' => $pageId ];
1558 if ( $revId ) {
1559 // Use the specified revision ID.
1560 // Note that we use newRevisionFromConds here because we want to retry
1561 // and fall back to master if the page is not found on a replica.
1562 // Since the caller supplied a revision ID, we are pretty sure the revision is
1563 // supposed to exist, so we should try hard to find it.
1564 $conds['rev_id'] = $revId;
1565 return $this->newRevisionFromConds( $conds, $flags );
1566 } else {
1567 // Use a join to get the latest revision.
1568 // Note that we don't use newRevisionFromConds here because we don't want to retry
1569 // and fall back to master. The assumption is that we only want to force the fallback
1570 // if we are quite sure the revision exists because the caller supplied a revision ID.
1571 // If the page isn't found at all on a replica, it probably simply does not exist.
1572 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1573
1574 $conds[] = 'rev_id=page_latest';
1575 $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1576
1577 return $rev;
1578 }
1579 }
1580
1592 public function getRevisionByTimestamp( $title, $timestamp ) {
1593 $db = $this->getDBConnection( DB_REPLICA );
1594 return $this->newRevisionFromConds(
1595 [
1596 'rev_timestamp' => $db->timestamp( $timestamp ),
1597 'page_namespace' => $title->getNamespace(),
1598 'page_title' => $title->getDBkey()
1599 ],
1600 0,
1601 $title
1602 );
1603 }
1604
1611 private function loadSlotRecords( $revId, $queryFlags ) {
1612 $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1613
1614 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1615 $db = $this->getDBConnectionRef( $dbMode );
1616
1617 $res = $db->select(
1618 $revQuery['tables'],
1619 $revQuery['fields'],
1620 [
1621 'slot_revision_id' => $revId,
1622 ],
1623 __METHOD__,
1624 $dbOptions,
1625 $revQuery['joins']
1626 );
1627
1628 $slots = [];
1629
1630 foreach ( $res as $row ) {
1631 // resolve role names and model names from in-memory cache, instead of joining.
1632 $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1633 $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1634
1635 $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags ) {
1636 return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
1637 };
1638
1639 $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1640 }
1641
1642 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1643 throw new RevisionAccessException(
1644 'Main slot of revision ' . $revId . ' not found in database!'
1645 );
1646 };
1647
1648 return $slots;
1649 }
1650
1665 private function newRevisionSlots(
1666 $revId,
1667 $revisionRow,
1668 $queryFlags,
1669 Title $title
1670 ) {
1671 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1672 $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
1673 // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive
1674 $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
1675 } else {
1676 // XXX: do we need the same kind of caching here
1677 // that getKnownCurrentRevision uses (if $revId == page_latest?)
1678
1679 $slots = new RevisionSlots( function () use( $revId, $queryFlags ) {
1680 return $this->loadSlotRecords( $revId, $queryFlags );
1681 } );
1682 }
1683
1684 return $slots;
1685 }
1686
1705 $row,
1706 $queryFlags = 0,
1707 Title $title = null,
1708 array $overrides = []
1709 ) {
1710 Assert::parameterType( 'object', $row, '$row' );
1711
1712 // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1713 Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1714
1715 if ( !$title && isset( $overrides['title'] ) ) {
1716 if ( !( $overrides['title'] instanceof Title ) ) {
1717 throw new MWException( 'title field override must contain a Title object.' );
1718 }
1719
1720 $title = $overrides['title'];
1721 }
1722
1723 if ( !isset( $title ) ) {
1724 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1725 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1726 } else {
1727 throw new InvalidArgumentException(
1728 'A Title or ar_namespace and ar_title must be given'
1729 );
1730 }
1731 }
1732
1733 foreach ( $overrides as $key => $value ) {
1734 $field = "ar_$key";
1735 $row->$field = $value;
1736 }
1737
1738 try {
1740 $row->ar_user ?? null,
1741 $row->ar_user_text ?? null,
1742 $row->ar_actor ?? null
1743 );
1744 } catch ( InvalidArgumentException $ex ) {
1745 wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1746 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1747 }
1748
1749 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1750 // Legacy because $row may have come from self::selectFields()
1751 $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1752
1753 $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
1754
1755 return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
1756 }
1757
1770 public function newRevisionFromRow(
1771 $row,
1772 $queryFlags = 0,
1773 Title $title = null,
1774 $fromCache = false
1775 ) {
1776 Assert::parameterType( 'object', $row, '$row' );
1777
1778 if ( !$title ) {
1779 $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
1780 $revId = $row->rev_id ?? 0;
1781
1782 $title = $this->getTitle( $pageId, $revId, $queryFlags );
1783 }
1784
1785 if ( !isset( $row->page_latest ) ) {
1786 $row->page_latest = $title->getLatestRevID();
1787 if ( $row->page_latest === 0 && $title->exists() ) {
1788 wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1789 }
1790 }
1791
1792 try {
1794 $row->rev_user ?? null,
1795 $row->rev_user_text ?? null,
1796 $row->rev_actor ?? null
1797 );
1798 } catch ( InvalidArgumentException $ex ) {
1799 wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1800 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1801 }
1802
1803 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1804 // Legacy because $row may have come from self::selectFields()
1805 $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1806
1807 $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
1808
1809 // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1810 if ( $fromCache ) {
1812 function ( $revId ) use ( $queryFlags ) {
1813 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1814 return $this->fetchRevisionRowFromConds(
1815 $db,
1816 [ 'rev_id' => intval( $revId ) ]
1817 );
1818 },
1819 $title, $user, $comment, $row, $slots, $this->wikiId
1820 );
1821 } else {
1823 $title, $user, $comment, $row, $slots, $this->wikiId );
1824 }
1825 return $rev;
1826 }
1827
1843 array $fields,
1844 $queryFlags = 0,
1845 Title $title = null
1846 ) {
1847 if ( !$title && isset( $fields['title'] ) ) {
1848 if ( !( $fields['title'] instanceof Title ) ) {
1849 throw new MWException( 'title field must contain a Title object.' );
1850 }
1851
1852 $title = $fields['title'];
1853 }
1854
1855 if ( !$title ) {
1856 $pageId = $fields['page'] ?? 0;
1857 $revId = $fields['id'] ?? 0;
1858
1859 $title = $this->getTitle( $pageId, $revId, $queryFlags );
1860 }
1861
1862 if ( !isset( $fields['page'] ) ) {
1863 $fields['page'] = $title->getArticleID( $queryFlags );
1864 }
1865
1866 // if we have a content object, use it to set the model and type
1867 if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
1868 && !is_array( $fields['content'] )
1869 ) {
1870 throw new MWException(
1871 'content field must contain a Content object or an array of Content objects.'
1872 );
1873 }
1874
1875 if ( !empty( $fields['text_id'] ) ) {
1876 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
1877 throw new MWException( "The text_id field is only available in the pre-MCR schema" );
1878 }
1879
1880 if ( !empty( $fields['content'] ) ) {
1881 throw new MWException(
1882 "Text already stored in external store (id {$fields['text_id']}), " .
1883 "can't specify content object"
1884 );
1885 }
1886 }
1887
1888 if (
1889 isset( $fields['comment'] )
1890 && !( $fields['comment'] instanceof CommentStoreComment )
1891 ) {
1892 $commentData = $fields['comment_data'] ?? null;
1893
1894 if ( $fields['comment'] instanceof Message ) {
1895 $fields['comment'] = CommentStoreComment::newUnsavedComment(
1896 $fields['comment'],
1897 $commentData
1898 );
1899 } else {
1900 $commentText = trim( strval( $fields['comment'] ) );
1901 $fields['comment'] = CommentStoreComment::newUnsavedComment(
1902 $commentText,
1903 $commentData
1904 );
1905 }
1906 }
1907
1908 $revision = new MutableRevisionRecord( $title, $this->wikiId );
1909 $this->initializeMutableRevisionFromArray( $revision, $fields );
1910
1911 if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
1912 foreach ( $fields['content'] as $role => $content ) {
1913 $revision->setContent( $role, $content );
1914 }
1915 } else {
1916 $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
1917 $revision->setSlot( $mainSlot );
1918 }
1919
1920 return $revision;
1921 }
1922
1928 MutableRevisionRecord $record,
1929 array $fields
1930 ) {
1932 $user = null;
1933
1934 if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
1935 $user = $fields['user'];
1936 } else {
1937 try {
1939 $fields['user'] ?? null,
1940 $fields['user_text'] ?? null,
1941 $fields['actor'] ?? null
1942 );
1943 } catch ( InvalidArgumentException $ex ) {
1944 $user = null;
1945 }
1946 }
1947
1948 if ( $user ) {
1949 $record->setUser( $user );
1950 }
1951
1952 $timestamp = isset( $fields['timestamp'] )
1953 ? strval( $fields['timestamp'] )
1954 : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1955
1956 $record->setTimestamp( $timestamp );
1957
1958 if ( isset( $fields['page'] ) ) {
1959 $record->setPageId( intval( $fields['page'] ) );
1960 }
1961
1962 if ( isset( $fields['id'] ) ) {
1963 $record->setId( intval( $fields['id'] ) );
1964 }
1965 if ( isset( $fields['parent_id'] ) ) {
1966 $record->setParentId( intval( $fields['parent_id'] ) );
1967 }
1968
1969 if ( isset( $fields['sha1'] ) ) {
1970 $record->setSha1( $fields['sha1'] );
1971 }
1972 if ( isset( $fields['size'] ) ) {
1973 $record->setSize( intval( $fields['size'] ) );
1974 }
1975
1976 if ( isset( $fields['minor_edit'] ) ) {
1977 $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
1978 }
1979 if ( isset( $fields['deleted'] ) ) {
1980 $record->setVisibility( intval( $fields['deleted'] ) );
1981 }
1982
1983 if ( isset( $fields['comment'] ) ) {
1984 Assert::parameterType(
1985 CommentStoreComment::class,
1986 $fields['comment'],
1987 '$row[\'comment\']'
1988 );
1989 $record->setComment( $fields['comment'] );
1990 }
1991 }
1992
2007 public function loadRevisionFromId( IDatabase $db, $id ) {
2008 return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
2009 }
2010
2026 public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2027 $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2028 if ( $id ) {
2029 $conds['rev_id'] = intval( $id );
2030 } else {
2031 $conds[] = 'rev_id=page_latest';
2032 }
2033 return $this->loadRevisionFromConds( $db, $conds );
2034 }
2035
2052 public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2053 if ( $id ) {
2054 $matchId = intval( $id );
2055 } else {
2056 $matchId = 'page_latest';
2057 }
2058
2059 return $this->loadRevisionFromConds(
2060 $db,
2061 [
2062 "rev_id=$matchId",
2063 'page_namespace' => $title->getNamespace(),
2064 'page_title' => $title->getDBkey()
2065 ],
2066 0,
2067 $title
2068 );
2069 }
2070
2086 public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2087 return $this->loadRevisionFromConds( $db,
2088 [
2089 'rev_timestamp' => $db->timestamp( $timestamp ),
2090 'page_namespace' => $title->getNamespace(),
2091 'page_title' => $title->getDBkey()
2092 ],
2093 0,
2094 $title
2095 );
2096 }
2097
2113 private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
2114 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2115 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
2116
2117 $lb = $this->getDBLoadBalancer();
2118
2119 // Make sure new pending/committed revision are visibile later on
2120 // within web requests to certain avoid bugs like T93866 and T94407.
2121 if ( !$rev
2122 && !( $flags & self::READ_LATEST )
2123 && $lb->getServerCount() > 1
2124 && $lb->hasOrMadeRecentMasterChanges()
2125 ) {
2126 $flags = self::READ_LATEST;
2127 $dbw = $this->getDBConnection( DB_MASTER );
2128 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
2129 $this->releaseDBConnection( $dbw );
2130 }
2131
2132 return $rev;
2133 }
2134
2148 private function loadRevisionFromConds(
2149 IDatabase $db,
2150 $conditions,
2151 $flags = 0,
2152 Title $title = null
2153 ) {
2154 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
2155 if ( $row ) {
2156 $rev = $this->newRevisionFromRow( $row, $flags, $title );
2157
2158 return $rev;
2159 }
2160
2161 return null;
2162 }
2163
2171 private function checkDatabaseWikiId( IDatabase $db ) {
2172 $storeWiki = $this->wikiId;
2173 $dbWiki = $db->getDomainID();
2174
2175 if ( $dbWiki === $storeWiki ) {
2176 return;
2177 }
2178
2179 $storeWiki = $storeWiki ?: $this->loadBalancer->getLocalDomainID();
2180 // @FIXME: when would getDomainID() be false here?
2181 $dbWiki = $dbWiki ?: wfWikiID();
2182
2183 if ( $dbWiki === $storeWiki ) {
2184 return;
2185 }
2186
2187 // HACK: counteract encoding imposed by DatabaseDomain
2188 $storeWiki = str_replace( '?h', '-', $storeWiki );
2189 $dbWiki = str_replace( '?h', '-', $dbWiki );
2190
2191 if ( $dbWiki === $storeWiki ) {
2192 return;
2193 }
2194
2195 throw new MWException( "RevisionStore for $storeWiki "
2196 . "cannot be used with a DB connection for $dbWiki" );
2197 }
2198
2211 private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
2212 $this->checkDatabaseWikiId( $db );
2213
2214 $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2215 $options = [];
2216 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2217 $options[] = 'FOR UPDATE';
2218 }
2219 return $db->selectRow(
2220 $revQuery['tables'],
2221 $revQuery['fields'],
2222 $conditions,
2223 __METHOD__,
2224 $options,
2225 $revQuery['joins']
2226 );
2227 }
2228
2243 private function findSlotContentId( IDatabase $db, $revId, $role ) {
2244 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
2245 return null;
2246 }
2247
2248 try {
2249 $roleId = $this->slotRoleStore->getId( $role );
2250 $conditions = [
2251 'slot_revision_id' => $revId,
2252 'slot_role_id' => $roleId,
2253 ];
2254
2255 $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
2256
2257 return $contentId ?: null;
2258 } catch ( NameTableAccessException $ex ) {
2259 // If the role is missing from the slot_roles table,
2260 // the corresponding row in slots cannot exist.
2261 return null;
2262 }
2263 }
2264
2288 public function getQueryInfo( $options = [] ) {
2289 $ret = [
2290 'tables' => [],
2291 'fields' => [],
2292 'joins' => [],
2293 ];
2294
2295 $ret['tables'][] = 'revision';
2296 $ret['fields'] = array_merge( $ret['fields'], [
2297 'rev_id',
2298 'rev_page',
2299 'rev_timestamp',
2300 'rev_minor_edit',
2301 'rev_deleted',
2302 'rev_len',
2303 'rev_parent_id',
2304 'rev_sha1',
2305 ] );
2306
2307 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2308 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2309 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2310 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2311
2312 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2313 $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2314 $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2315 $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2316
2317 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2318 $ret['fields'][] = 'rev_text_id';
2319
2320 if ( $this->contentHandlerUseDB ) {
2321 $ret['fields'][] = 'rev_content_format';
2322 $ret['fields'][] = 'rev_content_model';
2323 }
2324 }
2325
2326 if ( in_array( 'page', $options, true ) ) {
2327 $ret['tables'][] = 'page';
2328 $ret['fields'] = array_merge( $ret['fields'], [
2329 'page_namespace',
2330 'page_title',
2331 'page_id',
2332 'page_latest',
2333 'page_is_redirect',
2334 'page_len',
2335 ] );
2336 $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2337 }
2338
2339 if ( in_array( 'user', $options, true ) ) {
2340 $ret['tables'][] = 'user';
2341 $ret['fields'] = array_merge( $ret['fields'], [
2342 'user_name',
2343 ] );
2344 $u = $actorQuery['fields']['rev_user'];
2345 $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2346 }
2347
2348 if ( in_array( 'text', $options, true ) ) {
2349 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
2350 throw new InvalidArgumentException( 'text table can no longer be joined directly' );
2351 } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2352 // NOTE: even when this class is set to not read from the old schema, callers
2353 // should still be able to join against the text table, as long as we are still
2354 // writing the old schema for compatibility.
2355 // TODO: This should trigger a deprecation warning eventually (T200918), but not
2356 // before all known usages are removed (see T198341 and T201164).
2357 // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
2358 }
2359
2360 $ret['tables'][] = 'text';
2361 $ret['fields'] = array_merge( $ret['fields'], [
2362 'old_text',
2363 'old_flags'
2364 ] );
2365 $ret['joins']['text'] = [ 'JOIN', [ 'rev_text_id=old_id' ] ];
2366 }
2367
2368 return $ret;
2369 }
2370
2388 public function getSlotsQueryInfo( $options = [] ) {
2389 $ret = [
2390 'tables' => [],
2391 'fields' => [],
2392 'joins' => [],
2393 ];
2394
2395 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2396 $db = $this->getDBConnectionRef( DB_REPLICA );
2397 $ret['tables']['slots'] = 'revision';
2398
2399 $ret['fields']['slot_revision_id'] = 'slots.rev_id';
2400 $ret['fields']['slot_content_id'] = 'NULL';
2401 $ret['fields']['slot_origin'] = 'slots.rev_id';
2402 $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
2403
2404 if ( in_array( 'content', $options, true ) ) {
2405 $ret['fields']['content_size'] = 'slots.rev_len';
2406 $ret['fields']['content_sha1'] = 'slots.rev_sha1';
2407 $ret['fields']['content_address']
2408 = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
2409
2410 if ( $this->contentHandlerUseDB ) {
2411 $ret['fields']['model_name'] = 'slots.rev_content_model';
2412 } else {
2413 $ret['fields']['model_name'] = 'NULL';
2414 }
2415 }
2416 } else {
2417 $ret['tables'][] = 'slots';
2418 $ret['fields'] = array_merge( $ret['fields'], [
2419 'slot_revision_id',
2420 'slot_content_id',
2421 'slot_origin',
2422 'slot_role_id',
2423 ] );
2424
2425 if ( in_array( 'role', $options, true ) ) {
2426 // Use left join to attach role name, so we still find the revision row even
2427 // if the role name is missing. This triggers a more obvious failure mode.
2428 $ret['tables'][] = 'slot_roles';
2429 $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2430 $ret['fields'][] = 'role_name';
2431 }
2432
2433 if ( in_array( 'content', $options, true ) ) {
2434 $ret['tables'][] = 'content';
2435 $ret['fields'] = array_merge( $ret['fields'], [
2436 'content_size',
2437 'content_sha1',
2438 'content_address',
2439 'content_model',
2440 ] );
2441 $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2442
2443 if ( in_array( 'model', $options, true ) ) {
2444 // Use left join to attach model name, so we still find the revision row even
2445 // if the model name is missing. This triggers a more obvious failure mode.
2446 $ret['tables'][] = 'content_models';
2447 $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2448 $ret['fields'][] = 'model_name';
2449 }
2450
2451 }
2452 }
2453
2454 return $ret;
2455 }
2456
2470 public function getArchiveQueryInfo() {
2471 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2472 $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2473 $ret = [
2474 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2475 'fields' => [
2476 'ar_id',
2477 'ar_page_id',
2478 'ar_namespace',
2479 'ar_title',
2480 'ar_rev_id',
2481 'ar_timestamp',
2482 'ar_minor_edit',
2483 'ar_deleted',
2484 'ar_len',
2485 'ar_parent_id',
2486 'ar_sha1',
2487 ] + $commentQuery['fields'] + $actorQuery['fields'],
2488 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2489 ];
2490
2491 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2492 $ret['fields'][] = 'ar_text_id';
2493
2494 if ( $this->contentHandlerUseDB ) {
2495 $ret['fields'][] = 'ar_content_format';
2496 $ret['fields'][] = 'ar_content_model';
2497 }
2498 }
2499
2500 return $ret;
2501 }
2502
2512 public function getRevisionSizes( array $revIds ) {
2513 return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
2514 }
2515
2528 public function listRevisionSizes( IDatabase $db, array $revIds ) {
2529 $this->checkDatabaseWikiId( $db );
2530
2531 $revLens = [];
2532 if ( !$revIds ) {
2533 return $revLens; // empty
2534 }
2535
2536 $res = $db->select(
2537 'revision',
2538 [ 'rev_id', 'rev_len' ],
2539 [ 'rev_id' => $revIds ],
2540 __METHOD__
2541 );
2542
2543 foreach ( $res as $row ) {
2544 $revLens[$row->rev_id] = intval( $row->rev_len );
2545 }
2546
2547 return $revLens;
2548 }
2549
2564 public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
2565 if ( !$rev->getId() || !$rev->getPageId() ) {
2566 // revision is unsaved or otherwise incomplete
2567 return null;
2568 }
2569
2570 if ( $rev instanceof RevisionArchiveRecord ) {
2571 // revision is deleted, so it's not part of the page history
2572 return null;
2573 }
2574
2575 if ( $title === null ) {
2576 // this would fail for deleted revisions
2577 $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
2578 }
2579
2580 $prev = $title->getPreviousRevisionID( $rev->getId() );
2581 if ( !$prev ) {
2582 return null;
2583 }
2584
2585 return $this->getRevisionByTitle( $title, $prev );
2586 }
2587
2601 public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
2602 if ( !$rev->getId() || !$rev->getPageId() ) {
2603 // revision is unsaved or otherwise incomplete
2604 return null;
2605 }
2606
2607 if ( $rev instanceof RevisionArchiveRecord ) {
2608 // revision is deleted, so it's not part of the page history
2609 return null;
2610 }
2611
2612 if ( $title === null ) {
2613 // this would fail for deleted revisions
2614 $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
2615 }
2616
2617 $next = $title->getNextRevisionID( $rev->getId() );
2618 if ( !$next ) {
2619 return null;
2620 }
2621
2622 return $this->getRevisionByTitle( $title, $next );
2623 }
2624
2637 $this->checkDatabaseWikiId( $db );
2638
2639 if ( $rev->getPageId() === null ) {
2640 return 0;
2641 }
2642 # Use page_latest if ID is not given
2643 if ( !$rev->getId() ) {
2644 $prevId = $db->selectField(
2645 'page', 'page_latest',
2646 [ 'page_id' => $rev->getPageId() ],
2647 __METHOD__
2648 );
2649 } else {
2650 $prevId = $db->selectField(
2651 'revision', 'rev_id',
2652 [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
2653 __METHOD__,
2654 [ 'ORDER BY' => 'rev_id DESC' ]
2655 );
2656 }
2657 return intval( $prevId );
2658 }
2659
2670 public function getTimestampFromId( $title, $id, $flags = 0 ) {
2671 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2672
2673 $conds = [ 'rev_id' => $id ];
2674 $conds['rev_page'] = $title->getArticleID();
2675 $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
2676
2677 return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
2678 }
2679
2689 public function countRevisionsByPageId( IDatabase $db, $id ) {
2690 $this->checkDatabaseWikiId( $db );
2691
2692 $row = $db->selectRow( 'revision',
2693 [ 'revCount' => 'COUNT(*)' ],
2694 [ 'rev_page' => $id ],
2695 __METHOD__
2696 );
2697 if ( $row ) {
2698 return intval( $row->revCount );
2699 }
2700 return 0;
2701 }
2702
2712 public function countRevisionsByTitle( IDatabase $db, $title ) {
2713 $id = $title->getArticleID();
2714 if ( $id ) {
2715 return $this->countRevisionsByPageId( $db, $id );
2716 }
2717 return 0;
2718 }
2719
2738 public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2739 $this->checkDatabaseWikiId( $db );
2740
2741 if ( !$userId ) {
2742 return false;
2743 }
2744
2745 $revQuery = $this->getQueryInfo();
2746 $res = $db->select(
2747 $revQuery['tables'],
2748 [
2749 'rev_user' => $revQuery['fields']['rev_user'],
2750 ],
2751 [
2752 'rev_page' => $pageId,
2753 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2754 ],
2755 __METHOD__,
2756 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2757 $revQuery['joins']
2758 );
2759 foreach ( $res as $row ) {
2760 if ( $row->rev_user != $userId ) {
2761 return false;
2762 }
2763 }
2764 return true;
2765 }
2766
2780 public function getKnownCurrentRevision( Title $title, $revId ) {
2781 $db = $this->getDBConnectionRef( DB_REPLICA );
2782
2783 $pageId = $title->getArticleID();
2784
2785 if ( !$pageId ) {
2786 return false;
2787 }
2788
2789 if ( !$revId ) {
2790 $revId = $title->getLatestRevID();
2791 }
2792
2793 if ( !$revId ) {
2794 wfWarn(
2795 'No latest revision known for page ' . $title->getPrefixedDBkey()
2796 . ' even though it exists with page ID ' . $pageId
2797 );
2798 return false;
2799 }
2800
2801 // Load the row from cache if possible. If not possible, populate the cache.
2802 // As a minor optimization, remember if this was a cache hit or miss.
2803 // We can sometimes avoid a database query later if this is a cache miss.
2804 $fromCache = true;
2805 $row = $this->cache->getWithSetCallback(
2806 // Page/rev IDs passed in from DB to reflect history merges
2807 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2808 WANObjectCache::TTL_WEEK,
2809 function ( $curValue, &$ttl, array &$setOpts ) use (
2810 $db, $pageId, $revId, &$fromCache
2811 ) {
2812 $setOpts += Database::getCacheSetOptions( $db );
2813 $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2814 if ( $row ) {
2815 $fromCache = false;
2816 }
2817 return $row; // don't cache negatives
2818 }
2819 );
2820
2821 // Reflect revision deletion and user renames.
2822 if ( $row ) {
2823 return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2824 } else {
2825 return false;
2826 }
2827 }
2828
2840 private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2841 return $this->cache->makeGlobalKey(
2842 self::ROW_CACHE_KEY,
2843 $db->getDomainID(),
2844 $pageId,
2845 $revId
2846 );
2847 }
2848
2849 // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
2850
2851}
2852
2857class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:123
This class handles the logic for the actor table migration.
CommentStoreComment represents a comment stored by CommentStore.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
A content handler knows how do deal with a specific type of content on a wiki page.
Helper class for DAO classes.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static hasFlags( $bitfield, $flags)
Hooks class.
Definition Hooks.php:34
A collection of public static functions to play with IP address and IP ranges.
Definition IP.php:67
MediaWiki exception.
Exception thrown when an unregistered content model is requested.
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Mutable RevisionRecord implementation, for building new revision entries programmatically.
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
setSize( $size)
Set nominal revision size, for optimization.
setSha1( $sha1)
Set revision hash, for optimization.
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Exception representing a failure to look up a revision.
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
getSize()
Returns the nominal size of this revision, in bogo-bytes.
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
getSha1()
Returns the base36 sha1 of this revision.
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Value object representing the set of slots belonging to a revision.
A RevisionRecord representing an existing revision persisted in the revision table.
Service for looking up page revisions.
setContentHandlerUseDB( $contentHandlerUseDB)
getTimestampFromId( $title, $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
newRevisionSlots( $revId, $revisionRow, $queryFlags, Title $title)
Factory method for RevisionSlots.
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
newRevisionFromArchiveRow( $row, $queryFlags=0, Title $title=null, array $overrides=[])
Make a fake revision object from an archive table row.
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
loadRevisionFromId(IDatabase $db, $id)
Load a page revision from a given revision ID number.
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page's history.
loadSlotContent(SlotRecord $slot, $blobData=null, $blobFlags=null, $blobFormat=null, $queryFlags=0)
Loads a Content object based on a slot row.
getNextRevision(RevisionRecord $rev, Title $title=null)
Get the revision after $rev in the page's history, if any.
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
newRevisionFromRow( $row, $queryFlags=0, Title $title=null, $fromCache=false)
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
getKnownCurrentRevision(Title $title, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
emulateContentId( $textId)
Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode, based on the revisi...
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
checkDatabaseWikiId(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
updateRevisionTextId(IDatabase $dbw, $revisionId, &$blobAddress)
findSlotContentId(IDatabase $db, $revId, $role)
Finds the ID of a content row for a given revision and slot role.
loadRevisionFromConds(IDatabase $db, $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision from the given database connection.
newRevisionFromConds( $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision.
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
getPreviousRevision(RevisionRecord $rev, Title $title=null)
Get the revision before $rev in the page's history, if any.
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, $mcrMigrationStage, ActorMigration $actorMigration, $wikiId=false)
loadRevisionFromPageId(IDatabase $db, $pageid, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
setLogger(LoggerInterface $logger)
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) Caching rows w...
userWasLastToEdit(IDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
newMutableRevisionFromArray(array $fields, $queryFlags=0, Title $title=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1....
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
fetchRevisionRowFromConds(IDatabase $db, $conditions, $flags=0)
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects.
static mapArchiveFields( $archiveRow)
Maps fields of the archive row to corresponding revision rows.
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save.
getRevisionByTimestamp( $title, $timestamp)
Load the revision for the given title with the given timestamp.
assertCrossWikiContentLoadingIsSafe()
Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading and still...
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
int $mcrMigrationStage
An appropriate combination of SCHEMA_COMPAT_XXX flags.
emulateMainSlot_1_29( $row, $queryFlags, Title $title)
Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
loadSlotRecords( $revId, $queryFlags)
releaseDBConnection(IDatabase $connection)
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
getRole()
Returns the role of the slot.
hasAddress()
Whether this slot has an address.
getSha1()
Returns the content size.
getSize()
Returns the content size.
getAddress()
Returns the address of this slot's content.
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
getModel()
Returns the content model.
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
hasContentId()
Whether this slot has a content ID.
getContentId()
Returns the ID of the content meta data row associated with the slot.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Exception representing a failure to access a data blob.
Exception representing a failure to look up a row from a name table.
Service for storing and loading Content objects.
Value object representing a user's identity.
The Message class provides methods which fulfil two basic services:
Definition Message.php:160
Utility class for creating new RC entries.
const PRC_UNPATROLLED
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Represents a title within MediaWiki.
Definition Title.php:40
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:676
Multi-datacenter aware caching interface.
Helper class to handle automatically marking connections as reusable (via RAII pattern) as well handl...
Definition DBConnRef.php:14
Relational database abstraction object.
Definition Database.php:49
$res
Definition database.txt:21
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
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
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const SCHEMA_COMPAT_WRITE_BOTH
Definition Defines.php:297
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:296
const SCHEMA_COMPAT_READ_BOTH
Definition Defines.php:298
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:293
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:294
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:295
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:894
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:1999
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
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:2003
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
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:1779
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2175
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:37
Base interface for content objects.
Definition Content.php:34
Interface for database access objects.
Service for constructing revision objects.
Service for looking up page revisions.
Service for loading and storing data blobs.
Definition BlobStore.php:33
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition BlobStore.php:64
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition BlobStore.php:40
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with.
Definition BlobStore.php:46
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with.
Definition BlobStore.php:52
const FORMAT_HINT
Hint key for use with storeBlob, indicating the serialization format used to create the blob,...
Definition BlobStore.php:82
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition BlobStore.php:58
const SHA1_HINT
Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the method.
Definition BlobStore.php:70
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob.
Definition BlobStore.php:76
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
unlock( $lockName, $method)
Release a lock.
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Single row SELECT wrapper.
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
The equivalent of IDatabase::select() except that the constructed SQL is returned,...
getDomainID()
Return the currently selected domain ID.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
addQuotes( $s)
Adds quotes and backslashes.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
insertId()
Get the inserted value of an auto-increment row.
Database cluster connection, tracking, load balancing, and transaction manager interface.
you have access to all of the normal MediaWiki so you can get a DB use the cache
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))
$content
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26