MediaWiki REL1_39
RevisionStore.php
Go to the documentation of this file.
1<?php
28namespace MediaWiki\Revision;
29
31use BagOStuff;
32use CommentStore;
34use Content;
38use InvalidArgumentException;
39use LogicException;
45use MediaWiki\Page\LegacyArticleIdAccess;
57use MWException;
58use MWTimestamp;
60use Psr\Log\LoggerAwareInterface;
61use Psr\Log\LoggerInterface;
62use Psr\Log\NullLogger;
63use RecentChange;
64use RuntimeException;
65use StatusValue;
66use stdClass;
67use Title;
68use TitleFactory;
69use Traversable;
71use Wikimedia\Assert\Assert;
72use Wikimedia\IPUtils;
78
89 implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
90
91 use LegacyArticleIdAccess;
92
93 public const ROW_CACHE_KEY = 'revision-row-1.29';
94
95 public const ORDER_OLDEST_TO_NEWEST = 'ASC';
96 public const ORDER_NEWEST_TO_OLDEST = 'DESC';
97
98 // Constants for get(...)Between methods
99 public const INCLUDE_OLD = 'include_old';
100 public const INCLUDE_NEW = 'include_new';
101 public const INCLUDE_BOTH = 'include_both';
102
106 private $blobStore;
107
111 private $wikiId;
112
116 private $loadBalancer;
117
121 private $cache;
122
126 private $localCache;
127
131 private $commentStore;
132
136 private $actorMigration;
137
139 private $actorStore;
140
144 private $logger;
145
149 private $contentModelStore;
150
154 private $slotRoleStore;
155
157 private $slotRoleRegistry;
158
160 private $contentHandlerFactory;
161
163 private $hookRunner;
164
166 private $pageStore;
167
169 private $titleFactory;
170
196 public function __construct(
197 ILoadBalancer $loadBalancer,
198 SqlBlobStore $blobStore,
199 WANObjectCache $cache,
200 BagOStuff $localCache,
201 CommentStore $commentStore,
202 NameTableStore $contentModelStore,
203 NameTableStore $slotRoleStore,
204 SlotRoleRegistry $slotRoleRegistry,
205 ActorMigration $actorMigration,
206 ActorStore $actorStore,
207 IContentHandlerFactory $contentHandlerFactory,
208 PageStore $pageStore,
209 TitleFactory $titleFactory,
210 HookContainer $hookContainer,
211 $wikiId = WikiAwareEntity::LOCAL
212 ) {
213 Assert::parameterType( [ 'string', 'false' ], $wikiId, '$wikiId' );
214
215 $this->loadBalancer = $loadBalancer;
216 $this->blobStore = $blobStore;
217 $this->cache = $cache;
218 $this->localCache = $localCache;
219 $this->commentStore = $commentStore;
220 $this->contentModelStore = $contentModelStore;
221 $this->slotRoleStore = $slotRoleStore;
222 $this->slotRoleRegistry = $slotRoleRegistry;
223 $this->actorMigration = $actorMigration;
224 $this->actorStore = $actorStore;
225 $this->wikiId = $wikiId;
226 $this->logger = new NullLogger();
227 $this->contentHandlerFactory = $contentHandlerFactory;
228 $this->pageStore = $pageStore;
229 $this->titleFactory = $titleFactory;
230 $this->hookRunner = new HookRunner( $hookContainer );
231 }
232
233 public function setLogger( LoggerInterface $logger ) {
234 $this->logger = $logger;
235 }
236
240 public function isReadOnly() {
241 return $this->blobStore->isReadOnly();
242 }
243
247 private function getDBLoadBalancer() {
248 return $this->loadBalancer;
249 }
250
256 public function getWikiId() {
257 return $this->wikiId;
258 }
259
265 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
266 list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
267 return $this->getDBConnectionRef( $mode );
268 }
269
276 private function getDBConnectionRef( $mode, $groups = [] ) {
277 $lb = $this->getDBLoadBalancer();
278 return $lb->getConnectionRef( $mode, $groups, $this->wikiId );
279 }
280
297 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
298 // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069
299 if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
300 wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' );
301 }
302
303 $page = $this->getPage( $pageId, $revId, $queryFlags );
304 // @phan-suppress-next-line PhanTypeMismatchReturnNullable castFrom does not return null here
305 return $this->titleFactory->castFromPageIdentity( $page );
306 }
307
318 private function getPage( ?int $pageId, ?int $revId, int $queryFlags = self::READ_NORMAL ) {
319 if ( !$pageId && !$revId ) {
320 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
321 }
322
323 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
324 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
325 if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
326 $queryFlags = self::READ_NORMAL;
327 }
328
329 // Loading by ID is best
330 if ( $pageId !== null && $pageId > 0 ) {
331 $page = $this->pageStore->getPageById( $pageId, $queryFlags );
332 if ( $page ) {
333 return $this->wrapPage( $page );
334 }
335 }
336
337 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
338 if ( $revId !== null && $revId > 0 ) {
339 $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags )
340 ->join( 'revision', null, 'page_id=rev_page' )
341 ->conds( [ 'rev_id' => $revId ] )
342 ->caller( __METHOD__ );
343
344 $page = $pageQuery->fetchPageRecord();
345 if ( $page ) {
346 return $this->wrapPage( $page );
347 }
348 }
349
350 // If we still don't have a title, fallback to primary DB if that wasn't already happening.
351 if ( $queryFlags === self::READ_NORMAL ) {
352 $title = $this->getPage( $pageId, $revId, self::READ_LATEST );
353 if ( $title ) {
354 $this->logger->info(
355 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
356 [ 'exception' => new RuntimeException() ]
357 );
358 return $title;
359 }
360 }
361
362 throw new RevisionAccessException(
363 'Could not determine title for page ID {page_id} and revision ID {rev_id}',
364 [
365 'page_id' => $pageId,
366 'rev_id' => $revId,
367 ]
368 );
369 }
370
376 private function wrapPage( PageIdentity $page ): PageIdentity {
377 if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
378 // NOTE: since there is still a lot of code that needs a full Title,
379 // and uses Title::castFromPageIdentity() to get one, it's beneficial
380 // to create a Title right away if we can, so we don't have to convert
381 // over and over later on.
382 // When there is less need to convert to Title, this special case can
383 // be removed.
384 // @phan-suppress-next-line PhanTypeMismatchReturnNullable castFrom does not return null here
385 return $this->titleFactory->castFromPageIdentity( $page );
386 } else {
387 return $page;
388 }
389 }
390
398 private function failOnNull( $value, $name ) {
399 if ( $value === null ) {
400 throw new IncompleteRevisionException(
401 "$name must not be " . var_export( $value, true ) . "!"
402 );
403 }
404
405 return $value;
406 }
407
415 private function failOnEmpty( $value, $name ) {
416 if ( $value === null || $value === 0 || $value === '' ) {
417 throw new IncompleteRevisionException(
418 "$name must not be " . var_export( $value, true ) . "!"
419 );
420 }
421
422 return $value;
423 }
424
436 public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
437 // TODO: pass in a DBTransactionContext instead of a database connection.
438 $this->checkDatabaseDomain( $dbw );
439
440 $slotRoles = $rev->getSlotRoles();
441
442 // Make sure the main slot is always provided throughout migration
443 if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
445 'main slot must be provided'
446 );
447 }
448
449 // Checks
450 $this->failOnNull( $rev->getSize(), 'size field' );
451 $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
452 $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
453 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
454 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
455 $this->failOnNull( $user->getId(), 'user field' );
456 $this->failOnEmpty( $user->getName(), 'user_text field' );
457
458 if ( !$rev->isReadyForInsertion() ) {
459 // This is here for future-proofing. At the time this check being added, it
460 // was redundant to the individual checks above.
461 throw new IncompleteRevisionException( 'Revision is incomplete' );
462 }
463
464 if ( $slotRoles == [ SlotRecord::MAIN ] ) {
465 // T239717: If the main slot is the only slot, make sure the revision's nominal size
466 // and hash match the main slot's nominal size and hash.
467 $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
468 Assert::precondition(
469 $mainSlot->getSize() === $rev->getSize(),
470 'The revisions\'s size must match the main slot\'s size (see T239717)'
471 );
472 Assert::precondition(
473 $mainSlot->getSha1() === $rev->getSha1(),
474 'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
475 );
476 }
477
478 $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early
479
480 $parentId = $rev->getParentId() ?? $this->getPreviousRevisionId( $dbw, $rev );
481
483 $rev = $dbw->doAtomicSection(
484 __METHOD__,
485 function ( IDatabase $dbw, $fname ) use (
486 $rev,
487 $user,
488 $comment,
489 $pageId,
490 $parentId
491 ) {
492 return $this->insertRevisionInternal(
493 $rev,
494 $dbw,
495 $user,
496 $comment,
497 $rev->getPage(),
498 $pageId,
499 $parentId
500 );
501 }
502 );
503
504 Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' );
505 Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' );
506 Assert::postcondition(
507 $rev->getComment( RevisionRecord::RAW ) !== null,
508 'revision must have a comment'
509 );
510 Assert::postcondition(
511 $rev->getUser( RevisionRecord::RAW ) !== null,
512 'revision must have a user'
513 );
514
515 // Trigger exception if the main slot is missing.
516 // Technically, this could go away after MCR migration: while
517 // calling code may require a main slot to exist, RevisionStore
518 // really should not know or care about that requirement.
520
521 foreach ( $slotRoles as $role ) {
522 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
523 Assert::postcondition(
524 $slot->getContent() !== null,
525 $role . ' slot must have content'
526 );
527 Assert::postcondition(
528 $slot->hasRevision(),
529 $role . ' slot must have a revision associated'
530 );
531 }
532
533 $this->hookRunner->onRevisionRecordInserted( $rev );
534
535 return $rev;
536 }
537
550 public function updateSlotsOn(
551 RevisionRecord $revision,
552 RevisionSlotsUpdate $revisionSlotsUpdate,
553 IDatabase $dbw
554 ): array {
555 $this->checkDatabaseDomain( $dbw );
556
557 // Make sure all modified and removed slots are derived slots
558 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
559 Assert::precondition(
560 $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
561 'Trying to modify a slot that is not derived'
562 );
563 }
564 foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) {
565 $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
566 Assert::precondition(
567 $isDerived,
568 'Trying to remove a slot that is not derived'
569 );
570 throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' );
571 }
572
574 $slotRecords = $dbw->doAtomicSection(
575 __METHOD__,
576 function ( IDatabase $dbw, $fname ) use (
577 $revision,
578 $revisionSlotsUpdate
579 ) {
580 return $this->updateSlotsInternal(
581 $revision,
582 $revisionSlotsUpdate,
583 $dbw
584 );
585 }
586 );
587
588 foreach ( $slotRecords as $role => $slot ) {
589 Assert::postcondition(
590 $slot->getContent() !== null,
591 $role . ' slot must have content'
592 );
593 Assert::postcondition(
594 $slot->hasRevision(),
595 $role . ' slot must have a revision associated'
596 );
597 }
598
599 return $slotRecords;
600 }
601
608 private function updateSlotsInternal(
609 RevisionRecord $revision,
610 RevisionSlotsUpdate $revisionSlotsUpdate,
611 IDatabase $dbw
612 ): array {
613 $page = $revision->getPage();
614 $revId = $revision->getId( $this->wikiId );
615 $blobHints = [
616 BlobStore::PAGE_HINT => $page->getId( $this->wikiId ),
617 BlobStore::REVISION_HINT => $revId,
618 BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
619 ];
620
621 $newSlots = [];
622 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
623 $slot = $revisionSlotsUpdate->getModifiedSlot( $role );
624 $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
625 }
626
627 return $newSlots;
628 }
629
630 private function insertRevisionInternal(
631 RevisionRecord $rev,
632 IDatabase $dbw,
633 UserIdentity $user,
634 CommentStoreComment $comment,
635 PageIdentity $page,
636 $pageId,
637 $parentId
638 ) {
639 $slotRoles = $rev->getSlotRoles();
640
641 $revisionRow = $this->insertRevisionRowOn(
642 $dbw,
643 $rev,
644 $parentId
645 );
646
647 $revisionId = $revisionRow['rev_id'];
648
649 $blobHints = [
650 BlobStore::PAGE_HINT => $pageId,
651 BlobStore::REVISION_HINT => $revisionId,
652 BlobStore::PARENT_HINT => $parentId,
653 ];
654
655 $newSlots = [];
656 foreach ( $slotRoles as $role ) {
657 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
658
659 // If the SlotRecord already has a revision ID set, this means it already exists
660 // in the database, and should already belong to the current revision.
661 // However, a slot may already have a revision, but no content ID, if the slot
662 // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
663 // mode, and the respective archive row was not yet migrated to the new schema.
664 // In that case, a new slot row (and content row) must be inserted even during
665 // undeletion.
666 if ( $slot->hasRevision() && $slot->hasContentId() ) {
667 // TODO: properly abort transaction if the assertion fails!
668 Assert::parameter(
669 $slot->getRevision() === $revisionId,
670 'slot role ' . $slot->getRole(),
671 'Existing slot should belong to revision '
672 . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
673 );
674
675 // Slot exists, nothing to do, move along.
676 // This happens when restoring archived revisions.
677
678 $newSlots[$role] = $slot;
679 } else {
680 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
681 }
682 }
683
684 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
685
686 $rev = new RevisionStoreRecord(
687 $page,
688 $user,
689 $comment,
690 (object)$revisionRow,
691 new RevisionSlots( $newSlots ),
692 $this->wikiId
693 );
694
695 return $rev;
696 }
697
706 private function insertSlotOn(
707 IDatabase $dbw,
708 $revisionId,
709 SlotRecord $protoSlot,
710 PageIdentity $page,
711 array $blobHints = []
712 ) {
713 if ( $protoSlot->hasAddress() ) {
714 $blobAddress = $protoSlot->getAddress();
715 } else {
716 $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
717 }
718
719 if ( $protoSlot->hasContentId() ) {
720 $contentId = $protoSlot->getContentId();
721 } else {
722 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
723 }
724
725 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
726
727 return SlotRecord::newSaved(
728 $revisionId,
729 $contentId,
730 $blobAddress,
731 $protoSlot
732 );
733 }
734
742 private function insertIpChangesRow(
743 IDatabase $dbw,
744 UserIdentity $user,
745 RevisionRecord $rev,
746 $revisionId
747 ) {
748 if ( !$user->isRegistered() && IPUtils::isValid( $user->getName() ) ) {
749 $ipcRow = [
750 'ipc_rev_id' => $revisionId,
751 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
752 'ipc_hex' => IPUtils::toHex( $user->getName() ),
753 ];
754 $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
755 }
756 }
757
768 private function insertRevisionRowOn(
769 IDatabase $dbw,
770 RevisionRecord $rev,
771 $parentId
772 ) {
773 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
774
775 list( $commentFields, $commentCallback ) =
776 $this->commentStore->insertWithTempTable(
777 $dbw,
778 'rev_comment',
779 $rev->getComment( RevisionRecord::RAW )
780 );
781 $revisionRow += $commentFields;
782
783 list( $actorFields, $actorCallback ) =
784 $this->actorMigration->getInsertValuesWithTempTable(
785 $dbw,
786 'rev_user',
787 $rev->getUser( RevisionRecord::RAW )
788 );
789 $revisionRow += $actorFields;
790
791 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
792
793 if ( !isset( $revisionRow['rev_id'] ) ) {
794 // only if auto-increment was used
795 $revisionRow['rev_id'] = intval( $dbw->insertId() );
796
797 if ( $dbw->getType() === 'mysql' ) {
798 // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
799 // auto-increment value to disk, so on server restart it might reuse IDs from deleted
800 // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
801
802 $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
803 $table = 'archive';
804 $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
805 if ( $maxRevId2 >= $maxRevId ) {
806 $maxRevId = $maxRevId2;
807 $table = 'slots';
808 }
809
810 if ( $maxRevId >= $revisionRow['rev_id'] ) {
811 $this->logger->debug(
812 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
813 . ' Trying to fix it.',
814 [
815 'revid' => $revisionRow['rev_id'],
816 'table' => $table,
817 'maxrevid' => $maxRevId,
818 ]
819 );
820
821 if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
822 throw new MWException( 'Failed to get database lock for T202032' );
823 }
824 $fname = __METHOD__;
825 $dbw->onTransactionResolution(
826 static function ( $trigger, IDatabase $dbw ) use ( $fname ) {
827 $dbw->unlock( 'fix-for-T202032', $fname );
828 },
829 __METHOD__
830 );
831
832 $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
833
834 // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
835 // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
836 // inserts too, though, at least on MariaDB 10.1.29.
837 //
838 // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
839 // transactions in this code path thanks to the row lock from the original ->insert() above.
840 //
841 // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
842 // that's for non-MySQL DBs.
843 $row1 = $dbw->query(
844 $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
845 __METHOD__
846 )->fetchObject();
847
848 $row2 = $dbw->query(
849 $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
850 . ' FOR UPDATE',
851 __METHOD__
852 )->fetchObject();
853
854 $maxRevId = max(
855 $maxRevId,
856 $row1 ? intval( $row1->v ) : 0,
857 $row2 ? intval( $row2->v ) : 0
858 );
859
860 // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
861 // transactions will throw a duplicate key error here. It doesn't seem worth trying
862 // to avoid that.
863 $revisionRow['rev_id'] = $maxRevId + 1;
864 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
865 }
866 }
867 }
868
869 $commentCallback( $revisionRow['rev_id'] );
870 $actorCallback( $revisionRow['rev_id'], $revisionRow );
871
872 return $revisionRow;
873 }
874
882 private function getBaseRevisionRow(
883 IDatabase $dbw,
884 RevisionRecord $rev,
885 $parentId
886 ) {
887 // Record the edit in revisions
888 $revisionRow = [
889 'rev_page' => $rev->getPageId( $this->wikiId ),
890 'rev_parent_id' => $parentId,
891 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
892 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
893 'rev_deleted' => $rev->getVisibility(),
894 'rev_len' => $rev->getSize(),
895 'rev_sha1' => $rev->getSha1(),
896 ];
897
898 if ( $rev->getId( $this->wikiId ) !== null ) {
899 // Needed to restore revisions with their original ID
900 $revisionRow['rev_id'] = $rev->getId( $this->wikiId );
901 }
902
903 return $revisionRow;
904 }
905
914 private function storeContentBlob(
915 SlotRecord $slot,
916 PageIdentity $page,
917 array $blobHints = []
918 ) {
919 $content = $slot->getContent();
920 $format = $content->getDefaultFormat();
921 $model = $content->getModel();
922
923 $this->checkContent( $content, $page, $slot->getRole() );
924
925 return $this->blobStore->storeBlob(
926 $content->serialize( $format ),
927 // These hints "leak" some information from the higher abstraction layer to
928 // low level storage to allow for optimization.
929 array_merge(
930 $blobHints,
931 [
932 BlobStore::DESIGNATION_HINT => 'page-content',
933 BlobStore::ROLE_HINT => $slot->getRole(),
934 BlobStore::SHA1_HINT => $slot->getSha1(),
935 BlobStore::MODEL_HINT => $model,
936 BlobStore::FORMAT_HINT => $format,
937 ]
938 )
939 );
940 }
941
948 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
949 $slotRow = [
950 'slot_revision_id' => $revisionId,
951 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
952 'slot_content_id' => $contentId,
953 // If the slot has a specific origin use that ID, otherwise use the ID of the revision
954 // that we just inserted.
955 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
956 ];
957 $dbw->insert( 'slots', $slotRow, __METHOD__ );
958 }
959
966 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
967 $contentRow = [
968 'content_size' => $slot->getSize(),
969 'content_sha1' => $slot->getSha1(),
970 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
971 'content_address' => $blobAddress,
972 ];
973 $dbw->insert( 'content', $contentRow, __METHOD__ );
974 return intval( $dbw->insertId() );
975 }
976
987 private function checkContent( Content $content, PageIdentity $page, string $role ) {
988 // Note: may return null for revisions that have not yet been inserted
989
990 $model = $content->getModel();
991 $format = $content->getDefaultFormat();
992 $handler = $content->getContentHandler();
993
994 if ( !$handler->isSupportedFormat( $format ) ) {
995 throw new MWException(
996 "Can't use format $format with content model $model on $page role $role"
997 );
998 }
999
1000 if ( !$content->isValid() ) {
1001 throw new MWException(
1002 "New content for $page role $role is not valid! Content model is $model"
1003 );
1004 }
1005 }
1006
1032 public function newNullRevision(
1033 IDatabase $dbw,
1034 PageIdentity $page,
1035 CommentStoreComment $comment,
1036 $minor,
1037 UserIdentity $user
1038 ) {
1039 $this->checkDatabaseDomain( $dbw );
1040
1041 $pageId = $this->getArticleId( $page );
1042
1043 // T51581: Lock the page table row to ensure no other process
1044 // is adding a revision to the page at the same time.
1045 // Avoid locking extra tables, compare T191892.
1046 $pageLatest = $dbw->selectField(
1047 'page',
1048 'page_latest',
1049 [ 'page_id' => $pageId ],
1050 __METHOD__,
1051 [ 'FOR UPDATE' ]
1052 );
1053
1054 if ( !$pageLatest ) {
1055 $msg = 'T235589: Failed to select table row during null revision creation' .
1056 " Page id '$pageId' does not exist.";
1057 $this->logger->error(
1058 $msg,
1059 [ 'exception' => new RuntimeException( $msg ) ]
1060 );
1061
1062 return null;
1063 }
1064
1065 // Fetch the actual revision row from primary DB, without locking all extra tables.
1066 $oldRevision = $this->loadRevisionFromConds(
1067 $dbw,
1068 [ 'rev_id' => intval( $pageLatest ) ],
1069 self::READ_LATEST,
1070 $page
1071 );
1072
1073 if ( !$oldRevision ) {
1074 $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1075 $this->logger->error(
1076 $msg,
1077 [ 'exception' => new RuntimeException( $msg ) ]
1078 );
1079 return null;
1080 }
1081
1082 // Construct the new revision
1083 $timestamp = MWTimestamp::now( TS_MW );
1084 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1085
1086 $newRevision->setComment( $comment );
1087 $newRevision->setUser( $user );
1088 $newRevision->setTimestamp( $timestamp );
1089 $newRevision->setMinorEdit( $minor );
1090
1091 return $newRevision;
1092 }
1093
1103 public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1104 $rc = $this->getRecentChange( $rev );
1105 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1106 return $rc->getAttribute( 'rc_id' );
1107 } else {
1108 return 0;
1109 }
1110 }
1111
1125 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1126 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1127
1128 $rc = RecentChange::newFromConds(
1129 [ 'rc_this_oldid' => $rev->getId( $this->wikiId ) ],
1130 __METHOD__,
1131 $dbType
1132 );
1133
1134 // XXX: cache this locally? Glue it to the RevisionRecord?
1135 return $rc;
1136 }
1137
1157 private function loadSlotContent(
1158 SlotRecord $slot,
1159 ?string $blobData = null,
1160 ?string $blobFlags = null,
1161 ?string $blobFormat = null,
1162 int $queryFlags = 0
1163 ) {
1164 if ( $blobData !== null ) {
1165 $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1166
1167 if ( $blobFlags === null ) {
1168 // No blob flags, so use the blob verbatim.
1169 $data = $blobData;
1170 } else {
1171 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1172 if ( $data === false ) {
1173 throw new RevisionAccessException(
1174 'Failed to expand blob data using flags {flags} (key: {cache_key})',
1175 [
1176 'flags' => $blobFlags,
1177 'cache_key' => $cacheKey,
1178 ]
1179 );
1180 }
1181 }
1182
1183 } else {
1184 $address = $slot->getAddress();
1185 try {
1186 $data = $this->blobStore->getBlob( $address, $queryFlags );
1187 } catch ( BlobAccessException $e ) {
1188 throw new RevisionAccessException(
1189 'Failed to load data blob from {address}'
1190 . 'If this problem persist, use the findBadBlobs maintenance script '
1191 . 'to investigate the issue and mark bad blobs.',
1192 [ 'address' => $e->getMessage() ],
1193 0,
1194 $e
1195 );
1196 }
1197 }
1198
1199 $model = $slot->getModel();
1200
1201 // If the content model is not known, don't fail here (T220594, T220793, T228921)
1202 if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1203 $this->logger->warning(
1204 "Undefined content model '$model', falling back to FallbackContent",
1205 [
1206 'content_address' => $slot->getAddress(),
1207 'rev_id' => $slot->getRevision(),
1208 'role_name' => $slot->getRole(),
1209 'model_name' => $model,
1210 'exception' => new RuntimeException()
1211 ]
1212 );
1213
1214 return new FallbackContent( $data, $model );
1215 }
1216
1217 return $this->contentHandlerFactory
1218 ->getContentHandler( $model )
1219 ->unserializeContent( $data, $blobFormat );
1220 }
1221
1239 public function getRevisionById( $id, $flags = 0, PageIdentity $page = null ) {
1240 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags, $page );
1241 }
1242
1259 public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) {
1260 $conds = [
1261 'page_namespace' => $page->getNamespace(),
1262 'page_title' => $page->getDBkey()
1263 ];
1264
1265 if ( $page instanceof LinkTarget ) {
1266 // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1267 $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1268 }
1269
1270 if ( $revId ) {
1271 // Use the specified revision ID.
1272 // Note that we use newRevisionFromConds here because we want to retry
1273 // and fall back to primary DB if the page is not found on a replica.
1274 // Since the caller supplied a revision ID, we are pretty sure the revision is
1275 // supposed to exist, so we should try hard to find it.
1276 $conds['rev_id'] = $revId;
1277 return $this->newRevisionFromConds( $conds, $flags, $page );
1278 } else {
1279 // Use a join to get the latest revision.
1280 // Note that we don't use newRevisionFromConds here because we don't want to retry
1281 // and fall back to primary DB. The assumption is that we only want to force the fallback
1282 // if we are quite sure the revision exists because the caller supplied a revision ID.
1283 // If the page isn't found at all on a replica, it probably simply does not exist.
1284 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1285 $conds[] = 'rev_id=page_latest';
1286 return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1287 }
1288 }
1289
1306 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1307 $conds = [ 'page_id' => $pageId ];
1308 if ( $revId ) {
1309 // Use the specified revision ID.
1310 // Note that we use newRevisionFromConds here because we want to retry
1311 // and fall back to primary DB if the page is not found on a replica.
1312 // Since the caller supplied a revision ID, we are pretty sure the revision is
1313 // supposed to exist, so we should try hard to find it.
1314 $conds['rev_id'] = $revId;
1315 return $this->newRevisionFromConds( $conds, $flags );
1316 } else {
1317 // Use a join to get the latest revision.
1318 // Note that we don't use newRevisionFromConds here because we don't want to retry
1319 // and fall back to primary DB. The assumption is that we only want to force the fallback
1320 // if we are quite sure the revision exists because the caller supplied a revision ID.
1321 // If the page isn't found at all on a replica, it probably simply does not exist.
1322 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1323
1324 $conds[] = 'rev_id=page_latest';
1325
1326 return $this->loadRevisionFromConds( $db, $conds, $flags );
1327 }
1328 }
1329
1345 public function getRevisionByTimestamp(
1346 $page,
1347 string $timestamp,
1348 int $flags = IDBAccessObject::READ_NORMAL
1349 ): ?RevisionRecord {
1350 if ( $page instanceof LinkTarget ) {
1351 // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1352 $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1353 }
1354 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1355 return $this->newRevisionFromConds(
1356 [
1357 'rev_timestamp' => $db->timestamp( $timestamp ),
1358 'page_namespace' => $page->getNamespace(),
1359 'page_title' => $page->getDBkey()
1360 ],
1361 $flags,
1362 $page
1363 );
1364 }
1365
1373 private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) {
1374 // TODO: Find a way to add NS_MODULE from Scribunto here
1375 if ( $page->getNamespace() !== NS_TEMPLATE ) {
1376 $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page );
1377 return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1378 }
1379
1380 // TODO: These caches should not be needed. See T297147#7563670
1381 $res = $this->localCache->getWithSetCallback(
1382 $this->localCache->makeKey(
1383 'revision-slots',
1384 $page->getWikiId(),
1385 $page->getId( $page->getWikiId() ),
1386 $revId
1387 ),
1388 $this->localCache::TTL_HOUR,
1389 function () use ( $revId, $queryFlags, $page ) {
1390 return $this->cache->getWithSetCallback(
1391 $this->cache->makeKey(
1392 'revision-slots',
1393 $page->getWikiId(),
1394 $page->getId( $page->getWikiId() ),
1395 $revId
1396 ),
1397 WANObjectCache::TTL_DAY,
1398 function () use ( $revId, $queryFlags, $page ) {
1399 $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page );
1400 if ( !$res ) {
1401 // Avoid caching
1402 return false;
1403 }
1404 return $res;
1405 }
1406 );
1407 }
1408 );
1409 if ( !$res ) {
1410 $res = [];
1411 }
1412
1413 return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1414 }
1415
1416 private function loadSlotRecordsFromDb( $revId, $queryFlags, PageIdentity $page ): array {
1417 $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1418
1419 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1420 $db = $this->getDBConnectionRef( $dbMode );
1421
1422 $res = $db->select(
1423 $revQuery['tables'],
1424 $revQuery['fields'],
1425 [
1426 'slot_revision_id' => $revId,
1427 ],
1428 __METHOD__,
1429 $dbOptions,
1430 $revQuery['joins']
1431 );
1432
1433 if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1434 // If we found no slots, try looking on the primary database (T212428, T252156)
1435 $this->logger->info(
1436 __METHOD__ . ' falling back to READ_LATEST.',
1437 [
1438 'revid' => $revId,
1439 'exception' => new RuntimeException(),
1440 ]
1441 );
1442 return $this->loadSlotRecordsFromDb(
1443 $revId,
1444 $queryFlags | self::READ_LATEST,
1445 $page
1446 );
1447 }
1448 return iterator_to_array( $res );
1449 }
1450
1463 private function constructSlotRecords(
1464 $revId,
1465 $slotRows,
1466 $queryFlags,
1467 PageIdentity $page,
1468 $slotContents = null
1469 ) {
1470 $slots = [];
1471
1472 foreach ( $slotRows as $row ) {
1473 // Resolve role names and model names from in-memory cache, if they were not joined in.
1474 if ( !isset( $row->role_name ) ) {
1475 $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1476 }
1477
1478 if ( !isset( $row->model_name ) ) {
1479 if ( isset( $row->content_model ) ) {
1480 $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1481 } else {
1482 // We may get here if $row->model_name is set but null, perhaps because it
1483 // came from rev_content_model, which is NULL for the default model.
1484 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1485 $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1486 }
1487 }
1488
1489 // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1490 if ( isset( $row->blob_data ) ) {
1491 $slotContents[$row->content_address] = $row->blob_data;
1492 }
1493
1494 $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1495 $blob = null;
1496 if ( isset( $slotContents[$slot->getAddress()] ) ) {
1497 $blob = $slotContents[$slot->getAddress()];
1498 if ( $blob instanceof Content ) {
1499 return $blob;
1500 }
1501 }
1502 return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1503 };
1504
1505 $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1506 }
1507
1508 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1509 $this->logger->error(
1510 __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1511 [
1512 'revid' => $revId,
1513 'queryFlags' => $queryFlags,
1514 'exception' => new RuntimeException(),
1515 ]
1516 );
1517
1518 throw new RevisionAccessException(
1519 'Main slot of revision not found in database. See T212428.'
1520 );
1521 }
1522
1523 return $slots;
1524 }
1525
1541 private function newRevisionSlots(
1542 $revId,
1543 $revisionRow,
1544 $slotRows,
1545 $queryFlags,
1546 PageIdentity $page
1547 ) {
1548 if ( $slotRows ) {
1549 $slots = new RevisionSlots(
1550 $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1551 );
1552 } else {
1553 $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) {
1554 return $this->loadSlotRecords( $revId, $queryFlags, $page );
1555 } );
1556 }
1557
1558 return $slots;
1559 }
1560
1583 $row,
1584 $queryFlags = 0,
1585 PageIdentity $page = null,
1586 array $overrides = []
1587 ) {
1588 return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides );
1589 }
1590
1603 public function newRevisionFromRow(
1604 $row,
1605 $queryFlags = 0,
1606 PageIdentity $page = null,
1607 $fromCache = false
1608 ) {
1609 return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache );
1610 }
1611
1632 stdClass $row,
1633 $slots,
1634 int $queryFlags = 0,
1635 ?PageIdentity $page = null,
1636 array $overrides = []
1637 ) {
1638 if ( !$page && isset( $overrides['title'] ) ) {
1639 if ( !( $overrides['title'] instanceof PageIdentity ) ) {
1640 throw new MWException( 'title field override must contain a PageIdentity object.' );
1641 }
1642
1643 $page = $overrides['title'];
1644 }
1645
1646 if ( !isset( $page ) ) {
1647 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1648 $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1649 } else {
1650 throw new InvalidArgumentException(
1651 'A Title or ar_namespace and ar_title must be given'
1652 );
1653 }
1654 }
1655
1656 foreach ( $overrides as $key => $value ) {
1657 $field = "ar_$key";
1658 $row->$field = $value;
1659 }
1660
1661 try {
1662 $user = $this->actorStore->newActorFromRowFields(
1663 $row->ar_user ?? null,
1664 $row->ar_user_text ?? null,
1665 $row->ar_actor ?? null
1666 );
1667 } catch ( InvalidArgumentException $ex ) {
1668 $this->logger->warning( 'Could not load user for archive revision {rev_id}', [
1669 'ar_rev_id' => $row->ar_rev_id,
1670 'ar_actor' => $row->ar_actor ?? 'null',
1671 'ar_user_text' => $row->ar_user_text ?? 'null',
1672 'ar_user' => $row->ar_user ?? 'null',
1673 'exception' => $ex
1674 ] );
1675 $user = $this->actorStore->getUnknownActor();
1676 }
1677
1678 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1679 // Legacy because $row may have come from self::selectFields()
1680 $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1681
1682 if ( !( $slots instanceof RevisionSlots ) ) {
1683 $slots = $this->newRevisionSlots( (int)$row->ar_rev_id, $row, $slots, $queryFlags, $page );
1684 }
1685 return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId );
1686 }
1687
1707 stdClass $row,
1708 $slots,
1709 int $queryFlags = 0,
1710 ?PageIdentity $page = null,
1711 bool $fromCache = false
1712 ) {
1713 if ( !$page ) {
1714 if ( isset( $row->page_id )
1715 && isset( $row->page_namespace )
1716 && isset( $row->page_title )
1717 ) {
1718 $page = new PageIdentityValue(
1719 (int)$row->page_id,
1720 (int)$row->page_namespace,
1721 $row->page_title,
1722 $this->wikiId
1723 );
1724
1725 $page = $this->wrapPage( $page );
1726 } else {
1727 $pageId = (int)( $row->rev_page ?? 0 );
1728 $revId = (int)( $row->rev_id ?? 0 );
1729
1730 $page = $this->getPage( $pageId, $revId, $queryFlags );
1731 }
1732 } else {
1733 $page = $this->ensureRevisionRowMatchesPage( $row, $page );
1734 }
1735
1736 if ( !$page ) {
1737 // This should already have been caught about, but apparently
1738 // it not always is, see T286877.
1739 throw new RevisionAccessException(
1740 "Failed to determine page associated with revision {$row->rev_id}"
1741 );
1742 }
1743
1744 try {
1745 $user = $this->actorStore->newActorFromRowFields(
1746 $row->rev_user ?? null,
1747 $row->rev_user_text ?? null,
1748 $row->rev_actor ?? null
1749 );
1750 } catch ( InvalidArgumentException $ex ) {
1751 $this->logger->warning( 'Could not load user for revision {rev_id}', [
1752 'rev_id' => $row->rev_id,
1753 'rev_actor' => $row->rev_actor ?? 'null',
1754 'rev_user_text' => $row->rev_user_text ?? 'null',
1755 'rev_user' => $row->rev_user ?? 'null',
1756 'exception' => $ex
1757 ] );
1758 $user = $this->actorStore->getUnknownActor();
1759 }
1760
1761 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1762 // Legacy because $row may have come from self::selectFields()
1763 $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1764
1765 if ( !( $slots instanceof RevisionSlots ) ) {
1766 $slots = $this->newRevisionSlots( (int)$row->rev_id, $row, $slots, $queryFlags, $page );
1767 }
1768
1769 // If this is a cached row, instantiate a cache-aware RevisionRecord to avoid stale data.
1770 if ( $fromCache ) {
1771 $rev = new RevisionStoreCacheRecord(
1772 function ( $revId ) use ( $queryFlags ) {
1773 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1774 $row = $this->fetchRevisionRowFromConds(
1775 $db,
1776 [ 'rev_id' => intval( $revId ) ]
1777 );
1778 if ( !$row && !( $queryFlags & self::READ_LATEST ) ) {
1779 // If we found no slots, try looking on the primary database (T259738)
1780 $this->logger->info(
1781 'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1782 [
1783 'revid' => $revId,
1784 'exception' => new RuntimeException(),
1785 ]
1786 );
1787 $dbw = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
1788 $row = $this->fetchRevisionRowFromConds(
1789 $dbw,
1790 [ 'rev_id' => intval( $revId ) ]
1791 );
1792 }
1793 if ( !$row ) {
1794 return [ null, null ];
1795 }
1796 return [
1797 $row->rev_deleted,
1798 $this->actorStore->newActorFromRowFields(
1799 $row->rev_user ?? null,
1800 $row->rev_user_text ?? null,
1801 $row->rev_actor ?? null
1802 )
1803 ];
1804 },
1805 $page, $user, $comment, $row, $slots, $this->wikiId
1806 );
1807 } else {
1808 $rev = new RevisionStoreRecord(
1809 $page, $user, $comment, $row, $slots, $this->wikiId );
1810 }
1811 return $rev;
1812 }
1813
1825 private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) {
1826 $revId = (int)( $row->rev_id ?? 0 );
1827 $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1828 $expectedPageId = $page->getId( $this->wikiId );
1829 // Avoid fatal error when the Title's ID changed, T246720
1830 if ( $revPageId && $expectedPageId && $revPageId !== $expectedPageId ) {
1831 // NOTE: PageStore::getPageByReference may use the page ID, which we don't want here.
1832 $pageRec = $this->pageStore->getPageByName(
1833 $page->getNamespace(),
1834 $page->getDBkey(),
1835 PageStore::READ_LATEST
1836 );
1837 $masterPageId = $pageRec->getId( $this->wikiId );
1838 $masterLatest = $pageRec->getLatest( $this->wikiId );
1839 if ( $revPageId === $masterPageId ) {
1840 if ( $page instanceof Title ) {
1841 // If we were using a Title object, keep using it, but update the page ID.
1842 // This way, we don't unexpectedly mix Titles with immutable value objects.
1843 $page->resetArticleID( $masterPageId );
1844
1845 } else {
1846 $page = $pageRec;
1847 }
1848
1849 $this->logger->info(
1850 "Encountered stale Title object",
1851 [
1852 'page_id_stale' => $expectedPageId,
1853 'page_id_reloaded' => $masterPageId,
1854 'page_latest' => $masterLatest,
1855 'rev_id' => $revId,
1856 'exception' => new RuntimeException(),
1857 ] + $context
1858 );
1859 } else {
1860 $expectedTitle = (string)$page;
1861 if ( $page instanceof Title ) {
1862 // If we started with a Title, keep using a Title.
1863 $page = $this->titleFactory->newFromID( $revPageId );
1864 } else {
1865 $page = $pageRec;
1866 }
1867
1868 // This could happen if a caller to e.g. getRevisionById supplied a Title that is
1869 // plain wrong. In this case, we should ideally throw an IllegalArgumentException.
1870 // However, it is more likely that we encountered a race condition during a page
1871 // move (T268910, T279832) or database corruption (T263340). That situation
1872 // should not be ignored, but we can allow the request to continue in a reasonable
1873 // manner without breaking things for the user.
1874 $this->logger->error(
1875 "Encountered mismatching Title object (see T259022, T268910, T279832, T263340)",
1876 [
1877 'expected_page_id' => $masterPageId,
1878 'expected_page_title' => $expectedTitle,
1879 'rev_page' => $revPageId,
1880 'rev_page_title' => (string)$page,
1881 'page_latest' => $masterLatest,
1882 'rev_id' => $revId,
1883 'exception' => new RuntimeException(),
1884 ] + $context
1885 );
1886 }
1887 }
1888
1889 // @phan-suppress-next-line PhanTypeMismatchReturnNullable getPageByName/newFromID should not return null
1890 return $page;
1891 }
1892
1918 public function newRevisionsFromBatch(
1919 $rows,
1920 array $options = [],
1921 $queryFlags = 0,
1922 PageIdentity $page = null
1923 ) {
1924 $result = new StatusValue();
1925 $archiveMode = $options['archive'] ?? false;
1926
1927 if ( $archiveMode ) {
1928 $revIdField = 'ar_rev_id';
1929 } else {
1930 $revIdField = 'rev_id';
1931 }
1932
1933 $rowsByRevId = [];
1934 $pageIdsToFetchTitles = [];
1935 $titlesByPageKey = [];
1936 foreach ( $rows as $row ) {
1937 if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1938 $result->warning(
1939 'internalerror_info',
1940 "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1941 );
1942 }
1943
1944 // Attach a page key to the row, so we can find and reuse Title objects easily.
1945 $row->_page_key =
1946 $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1947
1948 if ( $page ) {
1949 if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1950 throw new InvalidArgumentException(
1951 "Revision {$row->$revIdField} doesn't belong to page "
1952 . $this->getArticleId( $page )
1953 );
1954 }
1955
1956 if ( $archiveMode
1957 && ( $row->ar_namespace != $page->getNamespace()
1958 || $row->ar_title !== $page->getDBkey() )
1959 ) {
1960 throw new InvalidArgumentException(
1961 "Revision {$row->$revIdField} doesn't belong to page "
1962 . $page
1963 );
1964 }
1965 } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1966 if ( isset( $row->page_namespace ) && isset( $row->page_title )
1967 // This should always be true, but just in case we don't have a page_id
1968 // set or it doesn't match rev_page, let's fetch the title again.
1969 && isset( $row->page_id ) && isset( $row->rev_page )
1970 && $row->rev_page === $row->page_id
1971 ) {
1972 $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1973 } elseif ( $archiveMode ) {
1974 // Can't look up deleted pages by ID, but we have namespace and title
1975 $titlesByPageKey[ $row->_page_key ] =
1976 Title::makeTitle( $row->ar_namespace, $row->ar_title );
1977 } else {
1978 $pageIdsToFetchTitles[] = $row->rev_page;
1979 }
1980 }
1981 $rowsByRevId[$row->$revIdField] = $row;
1982 }
1983
1984 if ( empty( $rowsByRevId ) ) {
1985 $result->setResult( true, [] );
1986 return $result;
1987 }
1988
1989 // If the page is not supplied, batch-fetch Title objects.
1990 if ( $page ) {
1991 // same logic as for $row->_page_key above
1992 $pageKey = $archiveMode
1993 ? $page->getNamespace() . ':' . $page->getDBkey()
1994 : $this->getArticleId( $page );
1995
1996 $titlesByPageKey[$pageKey] = $page;
1997 } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1998 // Note: when we fetch titles by ID, the page key is also the ID.
1999 // We should never get here if $archiveMode is true.
2000 Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
2001
2002 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
2003 $pageRecords = $this->pageStore
2004 ->newSelectQueryBuilder()
2005 ->wherePageIds( $pageIdsToFetchTitles )
2006 ->caller( __METHOD__ )
2007 ->fetchPageRecordArray();
2008 // Cannot array_merge because it re-indexes entries
2009 $titlesByPageKey = $pageRecords + $titlesByPageKey;
2010 }
2011
2012 // which method to use for creating RevisionRecords
2013 $newRevisionRecord = [
2014 $this,
2015 $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
2016 ];
2017
2018 if ( !isset( $options['slots'] ) ) {
2019 $result->setResult(
2020 true,
2021 array_map(
2022 static function ( $row )
2023 use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
2024 try {
2025 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2026 $result->warning(
2027 'internalerror_info',
2028 "Couldn't find title for rev {$row->$revIdField} "
2029 . "(page key {$row->_page_key})"
2030 );
2031 return null;
2032 }
2033 return $newRevisionRecord( $row, null, $queryFlags,
2034 $titlesByPageKey[ $row->_page_key ] );
2035 } catch ( MWException $e ) {
2036 $result->warning( 'internalerror_info', $e->getMessage() );
2037 return null;
2038 }
2039 },
2040 $rowsByRevId
2041 )
2042 );
2043 return $result;
2044 }
2045
2046 $slotRowOptions = [
2047 'slots' => $options['slots'] ?? true,
2048 'blobs' => $options['content'] ?? false,
2049 ];
2050
2051 if ( is_array( $slotRowOptions['slots'] )
2052 && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
2053 ) {
2054 // Make sure the main slot is always loaded, RevisionRecord requires this.
2055 $slotRowOptions['slots'][] = SlotRecord::MAIN;
2056 }
2057
2058 $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2059
2060 $result->merge( $slotRowsStatus );
2061 $slotRowsByRevId = $slotRowsStatus->getValue();
2062
2063 $result->setResult(
2064 true,
2065 array_map(
2066 function ( $row )
2067 use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
2068 $revIdField, $newRevisionRecord
2069 ) {
2070 if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
2071 $result->warning(
2072 'internalerror_info',
2073 "Couldn't find slots for rev {$row->$revIdField}"
2074 );
2075 return null;
2076 }
2077 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2078 $result->warning(
2079 'internalerror_info',
2080 "Couldn't find title for rev {$row->$revIdField} "
2081 . "(page key {$row->_page_key})"
2082 );
2083 return null;
2084 }
2085 try {
2086 return $newRevisionRecord(
2087 $row,
2088 new RevisionSlots(
2089 $this->constructSlotRecords(
2090 $row->$revIdField,
2091 $slotRowsByRevId[$row->$revIdField],
2092 $queryFlags,
2093 $titlesByPageKey[$row->_page_key]
2094 )
2095 ),
2096 $queryFlags,
2097 $titlesByPageKey[$row->_page_key]
2098 );
2099 } catch ( MWException $e ) {
2100 $result->warning( 'internalerror_info', $e->getMessage() );
2101 return null;
2102 }
2103 },
2104 $rowsByRevId
2105 )
2106 );
2107 return $result;
2108 }
2109
2133 private function getSlotRowsForBatch(
2134 $rowsOrIds,
2135 array $options = [],
2136 $queryFlags = 0
2137 ) {
2138 $result = new StatusValue();
2139
2140 $revIds = [];
2141 foreach ( $rowsOrIds as $row ) {
2142 if ( is_object( $row ) ) {
2143 $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
2144 } else {
2145 $revIds[] = (int)$row;
2146 }
2147 }
2148
2149 // Nothing to do.
2150 // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2151 if ( empty( $revIds ) ) {
2152 $result->setResult( true, [] );
2153 return $result;
2154 }
2155
2156 // We need to set the `content` flag to join in content meta-data
2157 $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
2158 $revIdField = $slotQueryInfo['keys']['rev_id'];
2159 $slotQueryConds = [ $revIdField => $revIds ];
2160
2161 if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2162 if ( empty( $options['slots'] ) ) {
2163 // Degenerate case: return no slots for each revision.
2164 $result->setResult( true, array_fill_keys( $revIds, [] ) );
2165 return $result;
2166 }
2167
2168 $roleIdField = $slotQueryInfo['keys']['role_id'];
2169 $slotQueryConds[$roleIdField] = array_map(
2170 [ $this->slotRoleStore, 'getId' ],
2171 $options['slots']
2172 );
2173 }
2174
2175 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2176 $slotRows = $db->select(
2177 $slotQueryInfo['tables'],
2178 $slotQueryInfo['fields'],
2179 $slotQueryConds,
2180 __METHOD__,
2181 [],
2182 $slotQueryInfo['joins']
2183 );
2184
2185 $slotContents = null;
2186 if ( $options['blobs'] ?? false ) {
2187 $blobAddresses = [];
2188 foreach ( $slotRows as $slotRow ) {
2189 $blobAddresses[] = $slotRow->content_address;
2190 }
2191 $slotContentFetchStatus = $this->blobStore
2192 ->getBlobBatch( $blobAddresses, $queryFlags );
2193 foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2194 $result->warning( $error['message'], ...$error['params'] );
2195 }
2196 $slotContents = $slotContentFetchStatus->getValue();
2197 }
2198
2199 $slotRowsByRevId = [];
2200 foreach ( $slotRows as $slotRow ) {
2201 if ( $slotContents === null ) {
2202 // nothing to do
2203 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2204 $slotRow->blob_data = $slotContents[$slotRow->content_address];
2205 } else {
2206 $result->warning(
2207 'internalerror_info',
2208 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2209 );
2210 $slotRow->blob_data = null;
2211 }
2212
2213 // conditional needed for SCHEMA_COMPAT_READ_OLD
2214 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2215 $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2216 }
2217
2218 // conditional needed for SCHEMA_COMPAT_READ_OLD
2219 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2220 $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2221 }
2222
2223 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2224 }
2225
2226 $result->setResult( true, $slotRowsByRevId );
2227 return $result;
2228 }
2229
2251 $rowsOrIds,
2252 $slots = null,
2253 $queryFlags = 0
2254 ) {
2255 $result = $this->getSlotRowsForBatch(
2256 $rowsOrIds,
2257 [ 'slots' => $slots, 'blobs' => true ],
2258 $queryFlags
2259 );
2260
2261 if ( $result->isOK() ) {
2262 // strip out all internal meta data that we don't want to expose
2263 foreach ( $result->value as $revId => $rowsByRole ) {
2264 foreach ( $rowsByRole as $role => $slotRow ) {
2265 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2266 // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2267 // if we didn't ask for it.
2268 unset( $result->value[$revId][$role] );
2269 continue;
2270 }
2271
2272 $result->value[$revId][$role] = (object)[
2273 'blob_data' => $slotRow->blob_data,
2274 'model_name' => $slotRow->model_name,
2275 ];
2276 }
2277 }
2278 }
2279
2280 return $result;
2281 }
2282
2299 private function newRevisionFromConds(
2300 array $conditions,
2301 int $flags = IDBAccessObject::READ_NORMAL,
2302 PageIdentity $page = null,
2303 array $options = []
2304 ) {
2305 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2306 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2307
2308 $lb = $this->getDBLoadBalancer();
2309
2310 // Make sure new pending/committed revision are visible later on
2311 // within web requests to certain avoid bugs like T93866 and T94407.
2312 if ( !$rev
2313 && !( $flags & self::READ_LATEST )
2314 && $lb->hasStreamingReplicaServers()
2315 && $lb->hasOrMadeRecentPrimaryChanges()
2316 ) {
2317 $flags = self::READ_LATEST;
2318 $dbw = $this->getDBConnectionRef( DB_PRIMARY );
2319 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2320 }
2321
2322 return $rev;
2323 }
2324
2339 private function loadRevisionFromConds(
2340 IDatabase $db,
2341 array $conditions,
2342 int $flags = IDBAccessObject::READ_NORMAL,
2343 PageIdentity $page = null,
2344 array $options = []
2345 ) {
2346 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2347 if ( $row ) {
2348 return $this->newRevisionFromRow( $row, $flags, $page );
2349 }
2350
2351 return null;
2352 }
2353
2361 private function checkDatabaseDomain( IDatabase $db ) {
2362 $dbDomain = $db->getDomainID();
2363 $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2364 if ( $dbDomain === $storeDomain ) {
2365 return;
2366 }
2367
2368 throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2369 }
2370
2384 private function fetchRevisionRowFromConds(
2385 IDatabase $db,
2386 array $conditions,
2387 int $flags = IDBAccessObject::READ_NORMAL,
2388 array $options = []
2389 ) {
2390 $this->checkDatabaseDomain( $db );
2391
2392 $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2393 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2394 $options[] = 'FOR UPDATE';
2395 }
2396 return $db->selectRow(
2397 $revQuery['tables'],
2398 $revQuery['fields'],
2399 $conditions,
2400 __METHOD__,
2401 $options,
2402 $revQuery['joins']
2403 );
2404 }
2405
2427 public function getQueryInfo( $options = [] ) {
2428 $ret = [
2429 'tables' => [],
2430 'fields' => [],
2431 'joins' => [],
2432 ];
2433
2434 $ret['tables'][] = 'revision';
2435 $ret['fields'] = array_merge( $ret['fields'], [
2436 'rev_id',
2437 'rev_page',
2438 'rev_timestamp',
2439 'rev_minor_edit',
2440 'rev_deleted',
2441 'rev_len',
2442 'rev_parent_id',
2443 'rev_sha1',
2444 ] );
2445
2446 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2447 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2448 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2449 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2450
2451 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2452 $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2453 $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2454 $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2455
2456 if ( in_array( 'page', $options, true ) ) {
2457 $ret['tables'][] = 'page';
2458 $ret['fields'] = array_merge( $ret['fields'], [
2459 'page_namespace',
2460 'page_title',
2461 'page_id',
2462 'page_latest',
2463 'page_is_redirect',
2464 'page_len',
2465 ] );
2466 $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2467 }
2468
2469 if ( in_array( 'user', $options, true ) ) {
2470 $ret['tables'][] = 'user';
2471 $ret['fields'] = array_merge( $ret['fields'], [
2472 'user_name',
2473 ] );
2474 $u = $actorQuery['fields']['rev_user'];
2475 $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2476 }
2477
2478 if ( in_array( 'text', $options, true ) ) {
2479 throw new InvalidArgumentException(
2480 'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2481 );
2482 }
2483
2484 return $ret;
2485 }
2486
2508 public function getSlotsQueryInfo( $options = [] ) {
2509 $ret = [
2510 'tables' => [],
2511 'fields' => [],
2512 'joins' => [],
2513 'keys' => [],
2514 ];
2515
2516 $ret['keys']['rev_id'] = 'slot_revision_id';
2517 $ret['keys']['role_id'] = 'slot_role_id';
2518
2519 $ret['tables'][] = 'slots';
2520 $ret['fields'] = array_merge( $ret['fields'], [
2521 'slot_revision_id',
2522 'slot_content_id',
2523 'slot_origin',
2524 'slot_role_id',
2525 ] );
2526
2527 if ( in_array( 'role', $options, true ) ) {
2528 // Use left join to attach role name, so we still find the revision row even
2529 // if the role name is missing. This triggers a more obvious failure mode.
2530 $ret['tables'][] = 'slot_roles';
2531 $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2532 $ret['fields'][] = 'role_name';
2533 }
2534
2535 if ( in_array( 'content', $options, true ) ) {
2536 $ret['keys']['model_id'] = 'content_model';
2537
2538 $ret['tables'][] = 'content';
2539 $ret['fields'] = array_merge( $ret['fields'], [
2540 'content_size',
2541 'content_sha1',
2542 'content_address',
2543 'content_model',
2544 ] );
2545 $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2546
2547 if ( in_array( 'model', $options, true ) ) {
2548 // Use left join to attach model name, so we still find the revision row even
2549 // if the model name is missing. This triggers a more obvious failure mode.
2550 $ret['tables'][] = 'content_models';
2551 $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2552 $ret['fields'][] = 'model_name';
2553 }
2554
2555 }
2556
2557 return $ret;
2558 }
2559
2568 public function isRevisionRow( $row, string $table = '' ) {
2569 if ( !( $row instanceof stdClass ) ) {
2570 return false;
2571 }
2572 $queryInfo = $table === 'archive' ? $this->getArchiveQueryInfo() : $this->getQueryInfo();
2573 foreach ( $queryInfo['fields'] as $alias => $field ) {
2574 $name = is_numeric( $alias ) ? $field : $alias;
2575 if ( !property_exists( $row, $name ) ) {
2576 return false;
2577 }
2578 }
2579 return true;
2580 }
2581
2600 public function getArchiveQueryInfo() {
2601 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2602 $ret = [
2603 'tables' => [
2604 'archive',
2605 'archive_actor' => 'actor'
2606 ] + $commentQuery['tables'],
2607 'fields' => [
2608 'ar_id',
2609 'ar_page_id',
2610 'ar_namespace',
2611 'ar_title',
2612 'ar_rev_id',
2613 'ar_timestamp',
2614 'ar_minor_edit',
2615 'ar_deleted',
2616 'ar_len',
2617 'ar_parent_id',
2618 'ar_sha1',
2619 'ar_actor',
2620 'ar_user' => 'archive_actor.actor_user',
2621 'ar_user_text' => 'archive_actor.actor_name',
2622 ] + $commentQuery['fields'],
2623 'joins' => [
2624 'archive_actor' => [ 'JOIN', 'actor_id=ar_actor' ]
2625 ] + $commentQuery['joins'],
2626 ];
2627
2628 return $ret;
2629 }
2630
2640 public function getRevisionSizes( array $revIds ) {
2641 $dbr = $this->getDBConnectionRef( DB_REPLICA );
2642 $revLens = [];
2643 if ( !$revIds ) {
2644 return $revLens; // empty
2645 }
2646
2647 $res = $dbr->select(
2648 'revision',
2649 [ 'rev_id', 'rev_len' ],
2650 [ 'rev_id' => $revIds ],
2651 __METHOD__
2652 );
2653
2654 foreach ( $res as $row ) {
2655 $revLens[$row->rev_id] = intval( $row->rev_len );
2656 }
2657
2658 return $revLens;
2659 }
2660
2669 private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2670 $op = $dir === 'next' ? '>' : '<';
2671 $sort = $dir === 'next' ? 'ASC' : 'DESC';
2672
2673 $revisionIdValue = $rev->getId( $this->wikiId );
2674
2675 if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) {
2676 // revision is unsaved or otherwise incomplete
2677 return null;
2678 }
2679
2680 if ( $rev instanceof RevisionArchiveRecord ) {
2681 // revision is deleted, so it's not part of the page history
2682 return null;
2683 }
2684
2685 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2686 $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2687
2688 $ts = $rev->getTimestamp();
2689 if ( $ts === null ) {
2690 $ts = $this->getTimestampFromId( $revisionIdValue, $flags );
2691 }
2692 if ( $ts === false ) {
2693 // XXX Should this be moved into getTimestampFromId?
2694 $ts = $db->selectField( 'archive', 'ar_timestamp',
2695 [ 'ar_rev_id' => $revisionIdValue ], __METHOD__ );
2696 if ( $ts === false ) {
2697 // XXX Is this reachable? How can we have a page id but no timestamp?
2698 return null;
2699 }
2700 }
2701 $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2702
2703 $revId = $db->selectField( 'revision', 'rev_id',
2704 [
2705 'rev_page' => $rev->getPageId( $this->wikiId ),
2706 "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op $revisionIdValue )"
2707 ],
2708 __METHOD__,
2709 [
2710 'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2711 'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2712 ]
2713 );
2714
2715 if ( $revId === false ) {
2716 return null;
2717 }
2718
2719 return $this->getRevisionById( intval( $revId ), $flags );
2720 }
2721
2736 public function getPreviousRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2737 return $this->getRelativeRevision( $rev, $flags, 'prev' );
2738 }
2739
2751 public function getNextRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2752 return $this->getRelativeRevision( $rev, $flags, 'next' );
2753 }
2754
2766 private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2767 $this->checkDatabaseDomain( $db );
2768
2769 if ( $rev->getPageId( $this->wikiId ) === null ) {
2770 return 0;
2771 }
2772 # Use page_latest if ID is not given
2773 if ( !$rev->getId( $this->wikiId ) ) {
2774 $prevId = $db->selectField(
2775 'page', 'page_latest',
2776 [ 'page_id' => $rev->getPageId( $this->wikiId ) ],
2777 __METHOD__
2778 );
2779 } else {
2780 $prevId = $db->selectField(
2781 'revision', 'rev_id',
2782 [ 'rev_page' => $rev->getPageId( $this->wikiId ), 'rev_id < ' . $rev->getId( $this->wikiId ) ],
2783 __METHOD__,
2784 [ 'ORDER BY' => 'rev_id DESC' ]
2785 );
2786 }
2787 return intval( $prevId );
2788 }
2789
2802 public function getTimestampFromId( $id, $flags = 0 ) {
2803 if ( $id instanceof Title ) {
2804 // Old deprecated calling convention supported for backwards compatibility
2805 $id = $flags;
2806 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2807 }
2808
2809 // T270149: Bail out if we know the query will definitely return false. Some callers are
2810 // passing RevisionRecord::getId() call directly as $id which can possibly return null.
2811 // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL'
2812 // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null.
2813 // @todo typehint $id and remove the null check
2814 if ( $id === null || $id <= 0 ) {
2815 return false;
2816 }
2817
2818 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2819
2820 $timestamp =
2821 $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2822
2823 return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2824 }
2825
2835 public function countRevisionsByPageId( IDatabase $db, $id ) {
2836 $this->checkDatabaseDomain( $db );
2837
2838 $row = $db->selectRow( 'revision',
2839 [ 'revCount' => 'COUNT(*)' ],
2840 [ 'rev_page' => $id ],
2841 __METHOD__
2842 );
2843 if ( $row ) {
2844 return intval( $row->revCount );
2845 }
2846 return 0;
2847 }
2848
2858 public function countRevisionsByTitle( IDatabase $db, PageIdentity $page ) {
2859 $id = $this->getArticleId( $page );
2860 if ( $id ) {
2861 return $this->countRevisionsByPageId( $db, $id );
2862 }
2863 return 0;
2864 }
2865
2884 public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2885 $this->checkDatabaseDomain( $db );
2886
2887 if ( !$userId ) {
2888 return false;
2889 }
2890
2891 $revQuery = $this->getQueryInfo();
2892 $res = $db->select(
2893 $revQuery['tables'],
2894 [
2895 'rev_user' => $revQuery['fields']['rev_user'],
2896 ],
2897 [
2898 'rev_page' => $pageId,
2899 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2900 ],
2901 __METHOD__,
2902 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2903 $revQuery['joins']
2904 );
2905 foreach ( $res as $row ) {
2906 if ( $row->rev_user != $userId ) {
2907 return false;
2908 }
2909 }
2910 return true;
2911 }
2912
2926 public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) {
2927 $db = $this->getDBConnectionRef( DB_REPLICA );
2928 $revIdPassed = $revId;
2929 $pageId = $this->getArticleId( $page );
2930 if ( !$pageId ) {
2931 return false;
2932 }
2933
2934 if ( !$revId ) {
2935 if ( $page instanceof Title ) {
2936 $revId = $page->getLatestRevID();
2937 } else {
2938 $pageRecord = $this->pageStore->getPageByReference( $page );
2939 if ( $pageRecord ) {
2940 $revId = $pageRecord->getLatest( $this->getWikiId() );
2941 }
2942 }
2943 }
2944
2945 if ( !$revId ) {
2946 $this->logger->warning(
2947 'No latest revision known for page {page} even though it exists with page ID {page_id}', [
2948 'page' => $page->__toString(),
2949 'page_id' => $pageId,
2950 'wiki_id' => $this->getWikiId() ?: 'local',
2951 ] );
2952 return false;
2953 }
2954
2955 // Load the row from cache if possible. If not possible, populate the cache.
2956 // As a minor optimization, remember if this was a cache hit or miss.
2957 // We can sometimes avoid a database query later if this is a cache miss.
2958 $fromCache = true;
2959 $row = $this->cache->getWithSetCallback(
2960 // Page/rev IDs passed in from DB to reflect history merges
2961 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2962 WANObjectCache::TTL_WEEK,
2963 function ( $curValue, &$ttl, array &$setOpts ) use (
2964 $db, $revId, &$fromCache
2965 ) {
2966 $setOpts += Database::getCacheSetOptions( $db );
2967 $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2968 if ( $row ) {
2969 $fromCache = false;
2970 }
2971 return $row; // don't cache negatives
2972 }
2973 );
2974
2975 // Reflect revision deletion and user renames.
2976 if ( $row ) {
2977 $title = $this->ensureRevisionRowMatchesPage( $row, $page, [
2978 'from_cache_flag' => $fromCache,
2979 'page_id_initial' => $pageId,
2980 'rev_id_used' => $revId,
2981 'rev_id_requested' => $revIdPassed,
2982 ] );
2983
2984 return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2985 } else {
2986 return false;
2987 }
2988 }
2989
2998 public function getFirstRevision(
2999 $page,
3000 int $flags = IDBAccessObject::READ_NORMAL
3001 ): ?RevisionRecord {
3002 if ( $page instanceof LinkTarget ) {
3003 // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
3004 $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
3005 }
3006 return $this->newRevisionFromConds(
3007 [
3008 'page_namespace' => $page->getNamespace(),
3009 'page_title' => $page->getDBkey()
3010 ],
3011 $flags,
3012 $page,
3013 [
3014 'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
3015 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
3016 ]
3017 );
3018 }
3019
3031 private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3032 return $this->cache->makeGlobalKey(
3033 self::ROW_CACHE_KEY,
3034 $db->getDomainID(),
3035 $pageId,
3036 $revId
3037 );
3038 }
3039
3047 private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
3048 if ( $rev ) {
3049 if ( $rev->getId( $this->wikiId ) === null ) {
3050 throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
3051 }
3052 if ( $rev->getPageId( $this->wikiId ) !== $pageId ) {
3053 throw new InvalidArgumentException(
3054 "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
3055 );
3056 }
3057 }
3058 }
3059
3074 private function getRevisionLimitConditions(
3075 IDatabase $dbr,
3076 RevisionRecord $old = null,
3077 RevisionRecord $new = null,
3078 $options = []
3079 ) {
3080 $options = (array)$options;
3081 $oldCmp = '>';
3082 $newCmp = '<';
3083 if ( in_array( self::INCLUDE_OLD, $options ) ) {
3084 $oldCmp = '>=';
3085 }
3086 if ( in_array( self::INCLUDE_NEW, $options ) ) {
3087 $newCmp = '<=';
3088 }
3089 if ( in_array( self::INCLUDE_BOTH, $options ) ) {
3090 $oldCmp = '>=';
3091 $newCmp = '<=';
3092 }
3093
3094 $conds = [];
3095 if ( $old ) {
3096 $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3097 $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId( $this->wikiId )}) " .
3098 "OR rev_timestamp > {$oldTs}";
3099 }
3100 if ( $new ) {
3101 $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3102 $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId( $this->wikiId )}) " .
3103 "OR rev_timestamp < {$newTs}";
3104 }
3105 return $conds;
3106 }
3107
3134 public function getRevisionIdsBetween(
3135 int $pageId,
3136 RevisionRecord $old = null,
3137 RevisionRecord $new = null,
3138 ?int $max = null,
3139 $options = [],
3140 ?string $order = null,
3141 int $flags = IDBAccessObject::READ_NORMAL
3142 ): array {
3143 $this->assertRevisionParameter( 'old', $pageId, $old );
3144 $this->assertRevisionParameter( 'new', $pageId, $new );
3145
3146 $options = (array)$options;
3147 $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3148 in_array( self::INCLUDE_BOTH, $options );
3149 $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3150 in_array( self::INCLUDE_BOTH, $options );
3151
3152 // No DB query needed if old and new are the same revision.
3153 // Can't check for consecutive revisions with 'getParentId' for a similar
3154 // optimization as edge cases exist when there are revisions between
3155 // a revision and it's parent. See T185167 for more details.
3156 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3157 return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3158 }
3159
3160 $db = $this->getDBConnectionRefForQueryFlags( $flags );
3161 $conds = array_merge(
3162 [
3163 'rev_page' => $pageId,
3164 $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3165 ],
3166 $this->getRevisionLimitConditions( $db, $old, $new, $options )
3167 );
3168
3169 $queryOptions = [];
3170 if ( $order !== null ) {
3171 $queryOptions['ORDER BY'] = [ "rev_timestamp $order", "rev_id $order" ];
3172 }
3173 if ( $max !== null ) {
3174 $queryOptions['LIMIT'] = $max + 1; // extra to detect truncation
3175 }
3176
3177 $values = $db->selectFieldValues(
3178 'revision',
3179 'rev_id',
3180 $conds,
3181 __METHOD__,
3182 $queryOptions
3183 );
3184 return array_map( 'intval', $values );
3185 }
3186
3208 public function getAuthorsBetween(
3209 $pageId,
3210 RevisionRecord $old = null,
3211 RevisionRecord $new = null,
3212 Authority $performer = null,
3213 $max = null,
3214 $options = []
3215 ) {
3216 $this->assertRevisionParameter( 'old', $pageId, $old );
3217 $this->assertRevisionParameter( 'new', $pageId, $new );
3218 $options = (array)$options;
3219
3220 // No DB query needed if old and new are the same revision.
3221 // Can't check for consecutive revisions with 'getParentId' for a similar
3222 // optimization as edge cases exist when there are revisions between
3223 //a revision and it's parent. See T185167 for more details.
3224 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3225 if ( empty( $options ) ) {
3226 return [];
3227 } elseif ( $performer ) {
3228 return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3229 } else {
3230 return [ $new->getUser() ];
3231 }
3232 }
3233
3234 $dbr = $this->getDBConnectionRef( DB_REPLICA );
3235 $conds = array_merge(
3236 [
3237 'rev_page' => $pageId,
3238 $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3239 ],
3240 $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3241 );
3242
3243 $queryOpts = [ 'DISTINCT' ];
3244 if ( $max !== null ) {
3245 $queryOpts['LIMIT'] = $max + 1;
3246 }
3247
3248 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3249 return array_map( function ( $row ) {
3250 return $this->actorStore->newActorFromRowFields(
3251 $row->rev_user,
3252 $row->rev_user_text,
3253 $row->rev_actor
3254 );
3255 }, iterator_to_array( $dbr->select(
3256 array_merge( [ 'revision' ], $actorQuery['tables'] ),
3257 $actorQuery['fields'],
3258 $conds, __METHOD__,
3259 $queryOpts,
3260 $actorQuery['joins']
3261 ) ) );
3262 }
3263
3285 public function countAuthorsBetween(
3286 $pageId,
3287 RevisionRecord $old = null,
3288 RevisionRecord $new = null,
3289 Authority $performer = null,
3290 $max = null,
3291 $options = []
3292 ) {
3293 // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3294 // and creation of UserIdentity stuff.
3295 return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3296 }
3297
3318 public function countRevisionsBetween(
3319 $pageId,
3320 RevisionRecord $old = null,
3321 RevisionRecord $new = null,
3322 $max = null,
3323 $options = []
3324 ) {
3325 $this->assertRevisionParameter( 'old', $pageId, $old );
3326 $this->assertRevisionParameter( 'new', $pageId, $new );
3327
3328 // No DB query needed if old and new are the same revision.
3329 // Can't check for consecutive revisions with 'getParentId' for a similar
3330 // optimization as edge cases exist when there are revisions between
3331 //a revision and it's parent. See T185167 for more details.
3332 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3333 return 0;
3334 }
3335
3336 $dbr = $this->getDBConnectionRef( DB_REPLICA );
3337 $conds = array_merge(
3338 [
3339 'rev_page' => $pageId,
3340 $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3341 ],
3342 $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3343 );
3344 if ( $max !== null ) {
3345 return $dbr->selectRowCount( 'revision', '1',
3346 $conds,
3347 __METHOD__,
3348 [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3349 );
3350 } else {
3351 return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3352 }
3353 }
3354
3366 public function findIdenticalRevision(
3367 RevisionRecord $revision,
3368 int $searchLimit
3369 ): ?RevisionRecord {
3370 $revision->assertWiki( $this->wikiId );
3371 $db = $this->getDBConnectionRef( DB_REPLICA );
3372 $revQuery = $this->getQueryInfo();
3373 $subquery = $db->buildSelectSubquery(
3374 $revQuery['tables'],
3375 $revQuery['fields'],
3376 [ 'rev_page' => $revision->getPageId( $this->wikiId ) ],
3377 __METHOD__,
3378 [
3379 'ORDER BY' => [
3380 'rev_timestamp DESC',
3381 // for cases where there are multiple revs with same timestamp
3382 'rev_id DESC'
3383 ],
3384 'LIMIT' => $searchLimit,
3385 // skip the most recent edit, we can't revert to it anyway
3386 'OFFSET' => 1
3387 ],
3388 $revQuery['joins']
3389 );
3390
3391 // selectRow effectively uses LIMIT 1 clause, returning only the first result
3392 $revisionRow = $db->selectRow(
3393 [ 'recent_revs' => $subquery ],
3394 '*',
3395 [ 'rev_sha1' => $revision->getSha1() ],
3396 __METHOD__
3397 );
3398
3399 return $revisionRow ? $this->newRevisionFromRow( $revisionRow ) : null;
3400 }
3401
3402 // TODO: move relevant methods from Title here, e.g. isBigDeletion, etc.
3403}
const NS_TEMPLATE
Definition Defines.php:74
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
Value object for a comment stored by CommentStore.
Handle database storage of comments such as edit summaries and log reasons.
Helper class for DAO classes.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Content object implementation representing unknown content.
MediaWiki exception.
Library for creating and parsing MW-style timestamps.
Exception thrown when an unregistered content model is requested.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Immutable value object representing a page identity.
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
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.
getUser( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
getSize()
Returns the nominal size of this revision, in bogo-bytes.
isReadyForInsertion()
Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all informat...
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
getPage()
Returns the page this revision belongs to.
getParentId( $wikiId=self::LOCAL)
Get parent revision ID (the original previous page revision).
getPageId( $wikiId=self::LOCAL)
Get the page ID.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getComment( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision comment, if it's available to the specified audience.
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
getSha1()
Returns the base36 sha1 of this revision.
getId( $wikiId=self::LOCAL)
Get revision ID.
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.
countRevisionsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, $max=null, $options=[])
Get the number of revisions between the given revisions.
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
getRevisionByTimestamp( $page, string $timestamp, int $flags=IDBAccessObject::READ_NORMAL)
Load the revision for the given title with the given timestamp.
countAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, Authority $performer=null, $max=null, $options=[])
Get the number of authors between the given revisions.
isRevisionRow( $row, string $table='')
Determine whether the parameter is a row containing all the fields that RevisionStore needs to create...
newNullRevision(IDatabase $dbw, PageIdentity $page, CommentStoreComment $comment, $minor, UserIdentity $user)
Create a new null-revision for insertion into a page's history.
newRevisionsFromBatch( $rows, array $options=[], $queryFlags=0, PageIdentity $page=null)
Construct a RevisionRecord instance for each row in $rows, and return them as an associative array in...
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
newRevisionFromArchiveRowAndSlots(stdClass $row, $slots, int $queryFlags=0, ?PageIdentity $page=null, array $overrides=[])
getWikiId()
Get the ID of the wiki this revision belongs to.
findIdenticalRevision(RevisionRecord $revision, int $searchLimit)
Tries to find a revision identical to $revision in $searchLimit most recent revisions of this page.
newRevisionFromRow( $row, $queryFlags=0, PageIdentity $page=null, $fromCache=false)
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
updateSlotsOn(RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw)
Update derived slots in an existing revision into the database, returning the modified slots on succe...
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
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.
getAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, Authority $performer=null, $max=null, $options=[])
Get the authors between the given revisions or revisions.
getKnownCurrentRevision(PageIdentity $page, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaced Revision::isUnpatrolled.
getNextRevision(RevisionRecord $rev, $flags=self::READ_NORMAL)
Get the revision after $rev in the page's history, if any.
newRevisionFromArchiveRow( $row, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Make a fake RevisionRecord object from an archive table row.
getRevisionById( $id, $flags=0, PageIdentity $page=null)
Load a page revision from a given revision ID number.
setLogger(LoggerInterface $logger)
countRevisionsByTitle(IDatabase $db, PageIdentity $page)
Get count of revisions per page...not very efficient.
getPreviousRevision(RevisionRecord $rev, $flags=self::READ_NORMAL)
Get the revision before $rev in the page's history, if any.
newRevisionFromRowAndSlots(stdClass $row, $slots, int $queryFlags=0, ?PageIdentity $page=null, bool $fromCache=false)
getFirstRevision( $page, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
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.
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, BagOStuff $localCache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorMigration $actorMigration, ActorStore $actorStore, IContentHandlerFactory $contentHandlerFactory, PageStore $pageStore, TitleFactory $titleFactory, HookContainer $hookContainer, $wikiId=WikiAwareEntity::LOCAL)
getRevisionIdsBetween(int $pageId, RevisionRecord $old=null, RevisionRecord $new=null, ?int $max=null, $options=[], ?string $order=null, int $flags=IDBAccessObject::READ_NORMAL)
Get IDs of revisions between the given revisions.
getRevisionByTitle( $page, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
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.
getAddress()
Returns the address of this slot's content.
hasRevision()
Whether this slot has revision ID associated.
getModel()
Returns the content model.
hasContentId()
Whether this slot has a content ID.
getRevision()
Returns the ID of the revision this slot is associated with.
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.
Value object representing a modification of revision slots.
getRemovedRoles()
Returns a list of removed slot roles, that is, roles removed by calling removeSlot(),...
getModifiedRoles()
Returns a list of modified slot roles, that is, roles modified by calling modifySlot(),...
Service for storing and loading Content objects.
Utility class for creating new RC entries.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Creates Title objects.
Represents a title within MediaWiki.
Definition Title.php:49
Multi-datacenter aware caching interface.
Helper class used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition DBConnRef.php:29
Base interface for content objects.
Definition Content.php:35
Interface for database access objects.
Marker interface for entities aware of the wiki they belong to.
Interface for objects (potentially) representing an editable wiki page.
getId( $wikiId=self::LOCAL)
Returns the page ID.
getNamespace()
Returns the page's namespace number.
getDBkey()
Get the page title in DB key form.
getWikiId()
Get the ID of the wiki this page belongs to.
__toString()
Returns an informative human readable unique representation of the page identity, for use as a cache ...
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
Service for constructing RevisionRecord objects.
Service for looking up page revisions.
Service for loading and storing data blobs.
Definition BlobStore.php:35
Interface for objects representing user identity.
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversible SQL statements from a callback.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
Create and track the database connections and transactions for a given database cluster.
Result wrapper for grabbing data queried from an IDatabase object.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28
$content
Definition router.php:76