MediaWiki REL1_37
RevisionStore.php
Go to the documentation of this file.
1<?php
28namespace MediaWiki\Revision;
29
31use CommentStore;
33use Content;
37use InvalidArgumentException;
38use LogicException;
44use MediaWiki\Page\LegacyArticleIdAccess;
56use MWException;
57use MWTimestamp;
59use Psr\Log\LoggerAwareInterface;
60use Psr\Log\LoggerInterface;
61use Psr\Log\NullLogger;
62use RecentChange;
63use RuntimeException;
64use StatusValue;
65use stdClass;
66use Title;
67use TitleFactory;
68use Traversable;
70use Wikimedia\Assert\Assert;
71use Wikimedia\IPUtils;
77
88 implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
89
90 use LegacyArticleIdAccess;
91
92 public const ROW_CACHE_KEY = 'revision-row-1.29';
93
94 public const ORDER_OLDEST_TO_NEWEST = 'ASC';
95 public const ORDER_NEWEST_TO_OLDEST = 'DESC';
96
97 // Constants for get(...)Between methods
98 public const INCLUDE_OLD = 'include_old';
99 public const INCLUDE_NEW = 'include_new';
100 public const INCLUDE_BOTH = 'include_both';
101
105 private $blobStore;
106
110 private $wikiId;
111
116
120 private $cache;
121
126
131
133 private $actorStore;
134
138 private $logger;
139
144
149
152
155
157 private $hookRunner;
158
160 private $pageStore;
161
164
189 public function __construct(
202 HookContainer $hookContainer,
203 $wikiId = WikiAwareEntity::LOCAL
204 ) {
205 Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
206
207 $this->loadBalancer = $loadBalancer;
208 $this->blobStore = $blobStore;
209 $this->cache = $cache;
210 $this->commentStore = $commentStore;
211 $this->contentModelStore = $contentModelStore;
212 $this->slotRoleStore = $slotRoleStore;
213 $this->slotRoleRegistry = $slotRoleRegistry;
214 $this->actorMigration = $actorMigration;
215 $this->actorStore = $actorStore;
216 $this->wikiId = $wikiId;
217 $this->logger = new NullLogger();
218 $this->contentHandlerFactory = $contentHandlerFactory;
219 $this->pageStore = $pageStore;
220 $this->titleFactory = $titleFactory;
221 $this->hookRunner = new HookRunner( $hookContainer );
222 }
223
224 public function setLogger( LoggerInterface $logger ) {
225 $this->logger = $logger;
226 }
227
231 public function isReadOnly() {
232 return $this->blobStore->isReadOnly();
233 }
234
238 private function getDBLoadBalancer() {
239 return $this->loadBalancer;
240 }
241
247 public function getWikiId() {
248 return $this->wikiId;
249 }
250
256 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
257 list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
258 return $this->getDBConnectionRef( $mode );
259 }
260
267 private function getDBConnectionRef( $mode, $groups = [] ) {
268 $lb = $this->getDBLoadBalancer();
269 return $lb->getConnectionRef( $mode, $groups, $this->wikiId );
270 }
271
288 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
289 // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069
290 if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
291 wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' );
292 }
293
294 $page = $this->getPage( $pageId, $revId, $queryFlags );
295 return $this->titleFactory->castFromPageIdentity( $page );
296 }
297
308 private function getPage( ?int $pageId, ?int $revId, int $queryFlags = self::READ_NORMAL ) {
309 if ( !$pageId && !$revId ) {
310 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
311 }
312
313 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
314 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
315 if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
316 $queryFlags = self::READ_NORMAL;
317 }
318
319 $canUsePageId = ( $pageId !== null && $pageId > 0 );
320
321 // Loading by ID is best
322 if ( $canUsePageId ) {
323 $page = $this->pageStore->getPageById( $pageId, $queryFlags );
324 if ( $page ) {
325 return $this->wrapPage( $page );
326 }
327 }
328
329 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
330 $canUseRevId = ( $revId !== null && $revId > 0 );
331
332 if ( $canUseRevId ) {
333 $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags )
334 ->join( 'revision', null, 'page_id=rev_page' )
335 ->conds( [ 'rev_id' => $revId ] )
336 ->caller( __METHOD__ );
337
338 $page = $pageQuery->fetchPageRecord();
339 if ( $page ) {
340 return $this->wrapPage( $page );
341 }
342 }
343
344 // If we still don't have a title, fallback to primary DB if that wasn't already happening.
345 if ( $queryFlags === self::READ_NORMAL ) {
346 $title = $this->getPage( $pageId, $revId, self::READ_LATEST );
347 if ( $title ) {
348 $this->logger->info(
349 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
350 [ 'trace' => wfBacktrace() ]
351 );
352 return $title;
353 }
354 }
355
356 throw new RevisionAccessException(
357 'Could not determine title for page ID {page_id} and revision ID {rev_id}',
358 [
359 'page_id' => $pageId,
360 'rev_id' => $revId,
361 ]
362 );
363 }
364
370 private function wrapPage( PageIdentity $page ): PageIdentity {
371 if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
372 // NOTE: since there is still a lot of code that needs a full Title,
373 // and uses Title::castFromPageIdentity() to get one, it's beneficial
374 // to create a Title right away if we can, so we don't have to convert
375 // over and over later on.
376 // When there is less need to convert to Title, this special case can
377 // be removed.
378 return $this->titleFactory->castFromPageIdentity( $page );
379 } else {
380 return $page;
381 }
382 }
383
391 private function failOnNull( $value, $name ) {
392 if ( $value === null ) {
394 "$name must not be " . var_export( $value, true ) . "!"
395 );
396 }
397
398 return $value;
399 }
400
408 private function failOnEmpty( $value, $name ) {
409 if ( $value === null || $value === 0 || $value === '' ) {
411 "$name must not be " . var_export( $value, true ) . "!"
412 );
413 }
414
415 return $value;
416 }
417
429 public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
430 // TODO: pass in a DBTransactionContext instead of a database connection.
431 $this->checkDatabaseDomain( $dbw );
432
433 $slotRoles = $rev->getSlotRoles();
434
435 // Make sure the main slot is always provided throughout migration
436 if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
438 'main slot must be provided'
439 );
440 }
441
442 // Checks
443 $this->failOnNull( $rev->getSize(), 'size field' );
444 $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
445 $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
446 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
447 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
448 $this->failOnNull( $user->getId(), 'user field' );
449 $this->failOnEmpty( $user->getName(), 'user_text field' );
450
451 if ( !$rev->isReadyForInsertion() ) {
452 // This is here for future-proofing. At the time this check being added, it
453 // was redundant to the individual checks above.
454 throw new IncompleteRevisionException( 'Revision is incomplete' );
455 }
456
457 if ( $slotRoles == [ SlotRecord::MAIN ] ) {
458 // T239717: If the main slot is the only slot, make sure the revision's nominal size
459 // and hash match the main slot's nominal size and hash.
460 $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
461 Assert::precondition(
462 $mainSlot->getSize() === $rev->getSize(),
463 'The revisions\'s size must match the main slot\'s size (see T239717)'
464 );
465 Assert::precondition(
466 $mainSlot->getSha1() === $rev->getSha1(),
467 'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
468 );
469 }
470
471 $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early
472
473 $parentId = $rev->getParentId() ?? $this->getPreviousRevisionId( $dbw, $rev );
474
476 $rev = $dbw->doAtomicSection(
477 __METHOD__,
478 function ( IDatabase $dbw, $fname ) use (
479 $rev,
480 $user,
481 $comment,
482 $pageId,
483 $parentId
484 ) {
485 return $this->insertRevisionInternal(
486 $rev,
487 $dbw,
488 $user,
489 $comment,
490 $rev->getPage(),
491 $pageId,
492 $parentId
493 );
494 }
495 );
496
497 // sanity checks
498 Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' );
499 Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' );
500 Assert::postcondition(
501 $rev->getComment( RevisionRecord::RAW ) !== null,
502 'revision must have a comment'
503 );
504 Assert::postcondition(
505 $rev->getUser( RevisionRecord::RAW ) !== null,
506 'revision must have a user'
507 );
508
509 // Trigger exception if the main slot is missing.
510 // Technically, this could go away after MCR migration: while
511 // calling code may require a main slot to exist, RevisionStore
512 // really should not know or care about that requirement.
514
515 foreach ( $slotRoles as $role ) {
516 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
517 Assert::postcondition(
518 $slot->getContent() !== null,
519 $role . ' slot must have content'
520 );
521 Assert::postcondition(
522 $slot->hasRevision(),
523 $role . ' slot must have a revision associated'
524 );
525 }
526
527 $this->hookRunner->onRevisionRecordInserted( $rev );
528
529 return $rev;
530 }
531
544 public function updateSlotsOn(
545 RevisionRecord $revision,
546 RevisionSlotsUpdate $revisionSlotsUpdate,
547 IDatabase $dbw
548 ): array {
549 $this->checkDatabaseDomain( $dbw );
550
551 // Make sure all modified and removed slots are derived slots
552 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
553 Assert::precondition(
554 $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
555 'Trying to modify a slot that is not derived'
556 );
557 }
558 foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) {
559 $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
560 Assert::precondition(
561 $isDerived,
562 'Trying to remove a slot that is not derived'
563 );
564 throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' );
565 }
566
568 $slotRecords = $dbw->doAtomicSection(
569 __METHOD__,
570 function ( IDatabase $dbw, $fname ) use (
571 $revision,
572 $revisionSlotsUpdate
573 ) {
574 return $this->updateSlotsInternal(
575 $revision,
576 $revisionSlotsUpdate,
577 $dbw
578 );
579 }
580 );
581
582 foreach ( $slotRecords as $role => $slot ) {
583 Assert::postcondition(
584 $slot->getContent() !== null,
585 $role . ' slot must have content'
586 );
587 Assert::postcondition(
588 $slot->hasRevision(),
589 $role . ' slot must have a revision associated'
590 );
591 }
592
593 return $slotRecords;
594 }
595
602 private function updateSlotsInternal(
603 RevisionRecord $revision,
604 RevisionSlotsUpdate $revisionSlotsUpdate,
605 IDatabase $dbw
606 ): array {
607 $page = $revision->getPage();
608 $revId = $revision->getId( $this->wikiId );
609 $blobHints = [
610 BlobStore::PAGE_HINT => $page->getId( $this->wikiId ),
611 BlobStore::REVISION_HINT => $revId,
612 BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
613 ];
614
615 $newSlots = [];
616 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
617 $slot = $revisionSlotsUpdate->getModifiedSlot( $role );
618 $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
619 }
620
621 return $newSlots;
622 }
623
624 private function insertRevisionInternal(
625 RevisionRecord $rev,
626 IDatabase $dbw,
627 UserIdentity $user,
628 CommentStoreComment $comment,
629 PageIdentity $page,
630 $pageId,
631 $parentId
632 ) {
633 $slotRoles = $rev->getSlotRoles();
634
635 $revisionRow = $this->insertRevisionRowOn(
636 $dbw,
637 $rev,
638 $parentId
639 );
640
641 $revisionId = $revisionRow['rev_id'];
642
643 $blobHints = [
644 BlobStore::PAGE_HINT => $pageId,
645 BlobStore::REVISION_HINT => $revisionId,
646 BlobStore::PARENT_HINT => $parentId,
647 ];
648
649 $newSlots = [];
650 foreach ( $slotRoles as $role ) {
651 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
652
653 // If the SlotRecord already has a revision ID set, this means it already exists
654 // in the database, and should already belong to the current revision.
655 // However, a slot may already have a revision, but no content ID, if the slot
656 // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
657 // mode, and the respective archive row was not yet migrated to the new schema.
658 // In that case, a new slot row (and content row) must be inserted even during
659 // undeletion.
660 if ( $slot->hasRevision() && $slot->hasContentId() ) {
661 // TODO: properly abort transaction if the assertion fails!
662 Assert::parameter(
663 $slot->getRevision() === $revisionId,
664 'slot role ' . $slot->getRole(),
665 'Existing slot should belong to revision '
666 . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
667 );
668
669 // Slot exists, nothing to do, move along.
670 // This happens when restoring archived revisions.
671
672 $newSlots[$role] = $slot;
673 } else {
674 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
675 }
676 }
677
678 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
679
680 $rev = new RevisionStoreRecord(
681 $page,
682 $user,
683 $comment,
684 (object)$revisionRow,
685 new RevisionSlots( $newSlots ),
686 $this->wikiId
687 );
688
689 return $rev;
690 }
691
700 private function insertSlotOn(
701 IDatabase $dbw,
702 $revisionId,
703 SlotRecord $protoSlot,
704 PageIdentity $page,
705 array $blobHints = []
706 ) {
707 if ( $protoSlot->hasAddress() ) {
708 $blobAddress = $protoSlot->getAddress();
709 } else {
710 $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
711 }
712
713 $contentId = null;
714
715 if ( $protoSlot->hasContentId() ) {
716 $contentId = $protoSlot->getContentId();
717 } else {
718 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
719 }
720
721 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
722
723 return SlotRecord::newSaved(
724 $revisionId,
725 $contentId,
726 $blobAddress,
727 $protoSlot
728 );
729 }
730
738 private function insertIpChangesRow(
739 IDatabase $dbw,
740 UserIdentity $user,
741 RevisionRecord $rev,
742 $revisionId
743 ) {
744 if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
745 $ipcRow = [
746 'ipc_rev_id' => $revisionId,
747 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
748 'ipc_hex' => IPUtils::toHex( $user->getName() ),
749 ];
750 $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
751 }
752 }
753
764 private function insertRevisionRowOn(
765 IDatabase $dbw,
766 RevisionRecord $rev,
767 $parentId
768 ) {
769 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
770
771 list( $commentFields, $commentCallback ) =
772 $this->commentStore->insertWithTempTable(
773 $dbw,
774 'rev_comment',
775 $rev->getComment( RevisionRecord::RAW )
776 );
777 $revisionRow += $commentFields;
778
779 list( $actorFields, $actorCallback ) =
780 $this->actorMigration->getInsertValuesWithTempTable(
781 $dbw,
782 'rev_user',
783 $rev->getUser( RevisionRecord::RAW )
784 );
785 $revisionRow += $actorFields;
786
787 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
788
789 if ( !isset( $revisionRow['rev_id'] ) ) {
790 // only if auto-increment was used
791 $revisionRow['rev_id'] = intval( $dbw->insertId() );
792
793 if ( $dbw->getType() === 'mysql' ) {
794 // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
795 // auto-increment value to disk, so on server restart it might reuse IDs from deleted
796 // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
797
798 $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
799 $table = 'archive';
800 $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
801 if ( $maxRevId2 >= $maxRevId ) {
802 $maxRevId = $maxRevId2;
803 $table = 'slots';
804 }
805
806 if ( $maxRevId >= $revisionRow['rev_id'] ) {
807 $this->logger->debug(
808 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
809 . ' Trying to fix it.',
810 [
811 'revid' => $revisionRow['rev_id'],
812 'table' => $table,
813 'maxrevid' => $maxRevId,
814 ]
815 );
816
817 if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
818 throw new MWException( 'Failed to get database lock for T202032' );
819 }
820 $fname = __METHOD__;
822 static function ( $trigger, IDatabase $dbw ) use ( $fname ) {
823 $dbw->unlock( 'fix-for-T202032', $fname );
824 },
825 __METHOD__
826 );
827
828 $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
829
830 // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
831 // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
832 // inserts too, though, at least on MariaDB 10.1.29.
833 //
834 // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
835 // transactions in this code path thanks to the row lock from the original ->insert() above.
836 //
837 // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
838 // that's for non-MySQL DBs.
839 $row1 = $dbw->query(
840 $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
841 __METHOD__
842 )->fetchObject();
843
844 $row2 = $dbw->query(
845 $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
846 . ' FOR UPDATE',
847 __METHOD__
848 )->fetchObject();
849
850 $maxRevId = max(
851 $maxRevId,
852 $row1 ? intval( $row1->v ) : 0,
853 $row2 ? intval( $row2->v ) : 0
854 );
855
856 // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
857 // transactions will throw a duplicate key error here. It doesn't seem worth trying
858 // to avoid that.
859 $revisionRow['rev_id'] = $maxRevId + 1;
860 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
861 }
862 }
863 }
864
865 $commentCallback( $revisionRow['rev_id'] );
866 $actorCallback( $revisionRow['rev_id'], $revisionRow );
867
868 return $revisionRow;
869 }
870
878 private function getBaseRevisionRow(
879 IDatabase $dbw,
880 RevisionRecord $rev,
881 $parentId
882 ) {
883 // Record the edit in revisions
884 $revisionRow = [
885 'rev_page' => $rev->getPageId( $this->wikiId ),
886 'rev_parent_id' => $parentId,
887 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
888 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
889 'rev_deleted' => $rev->getVisibility(),
890 'rev_len' => $rev->getSize(),
891 'rev_sha1' => $rev->getSha1(),
892 ];
893
894 if ( $rev->getId( $this->wikiId ) !== null ) {
895 // Needed to restore revisions with their original ID
896 $revisionRow['rev_id'] = $rev->getId( $this->wikiId );
897 }
898
899 return $revisionRow;
900 }
901
910 private function storeContentBlob(
911 SlotRecord $slot,
912 PageIdentity $page,
913 array $blobHints = []
914 ) {
915 $content = $slot->getContent();
916 $format = $content->getDefaultFormat();
917 $model = $content->getModel();
918
919 $this->checkContent( $content, $page, $slot->getRole() );
920
921 return $this->blobStore->storeBlob(
922 $content->serialize( $format ),
923 // These hints "leak" some information from the higher abstraction layer to
924 // low level storage to allow for optimization.
925 array_merge(
926 $blobHints,
927 [
928 BlobStore::DESIGNATION_HINT => 'page-content',
929 BlobStore::ROLE_HINT => $slot->getRole(),
930 BlobStore::SHA1_HINT => $slot->getSha1(),
931 BlobStore::MODEL_HINT => $model,
932 BlobStore::FORMAT_HINT => $format,
933 ]
934 )
935 );
936 }
937
944 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
945 $slotRow = [
946 'slot_revision_id' => $revisionId,
947 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
948 'slot_content_id' => $contentId,
949 // If the slot has a specific origin use that ID, otherwise use the ID of the revision
950 // that we just inserted.
951 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
952 ];
953 $dbw->insert( 'slots', $slotRow, __METHOD__ );
954 }
955
962 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
963 $contentRow = [
964 'content_size' => $slot->getSize(),
965 'content_sha1' => $slot->getSha1(),
966 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
967 'content_address' => $blobAddress,
968 ];
969 $dbw->insert( 'content', $contentRow, __METHOD__ );
970 return intval( $dbw->insertId() );
971 }
972
983 private function checkContent( Content $content, PageIdentity $page, string $role ) {
984 // Note: may return null for revisions that have not yet been inserted
985
986 $model = $content->getModel();
987 $format = $content->getDefaultFormat();
988 $handler = $content->getContentHandler();
989
990 if ( !$handler->isSupportedFormat( $format ) ) {
991 throw new MWException(
992 "Can't use format $format with content model $model on $page role $role"
993 );
994 }
995
996 if ( !$content->isValid() ) {
997 throw new MWException(
998 "New content for $page role $role is not valid! Content model is $model"
999 );
1000 }
1001 }
1002
1028 public function newNullRevision(
1029 IDatabase $dbw,
1030 PageIdentity $page,
1031 CommentStoreComment $comment,
1032 $minor,
1033 UserIdentity $user
1034 ) {
1035 $this->checkDatabaseDomain( $dbw );
1036
1037 $pageId = $this->getArticleId( $page );
1038
1039 // T51581: Lock the page table row to ensure no other process
1040 // is adding a revision to the page at the same time.
1041 // Avoid locking extra tables, compare T191892.
1042 $pageLatest = $dbw->selectField(
1043 'page',
1044 'page_latest',
1045 [ 'page_id' => $pageId ],
1046 __METHOD__,
1047 [ 'FOR UPDATE' ]
1048 );
1049
1050 if ( !$pageLatest ) {
1051 $msg = 'T235589: Failed to select table row during null revision creation' .
1052 " Page id '$pageId' does not exist.";
1053 $this->logger->error(
1054 $msg,
1055 [ 'exception' => new RuntimeException( $msg ) ]
1056 );
1057
1058 return null;
1059 }
1060
1061 // Fetch the actual revision row from primary DB, without locking all extra tables.
1062 $oldRevision = $this->loadRevisionFromConds(
1063 $dbw,
1064 [ 'rev_id' => intval( $pageLatest ) ],
1065 self::READ_LATEST,
1066 $page
1067 );
1068
1069 if ( !$oldRevision ) {
1070 $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1071 $this->logger->error(
1072 $msg,
1073 [ 'exception' => new RuntimeException( $msg ) ]
1074 );
1075 return null;
1076 }
1077
1078 // Construct the new revision
1079 $timestamp = MWTimestamp::now( TS_MW );
1080 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1081
1082 $newRevision->setComment( $comment );
1083 $newRevision->setUser( $user );
1084 $newRevision->setTimestamp( $timestamp );
1085 $newRevision->setMinorEdit( $minor );
1086
1087 return $newRevision;
1088 }
1089
1099 public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1100 $rc = $this->getRecentChange( $rev );
1101 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1102 return $rc->getAttribute( 'rc_id' );
1103 } else {
1104 return 0;
1105 }
1106 }
1107
1121 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1122 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1123
1124 $rc = RecentChange::newFromConds(
1125 [ 'rc_this_oldid' => $rev->getId( $this->wikiId ) ],
1126 __METHOD__,
1127 $dbType
1128 );
1129
1130 // XXX: cache this locally? Glue it to the RevisionRecord?
1131 return $rc;
1132 }
1133
1153 private function loadSlotContent(
1154 SlotRecord $slot,
1155 ?string $blobData = null,
1156 ?string $blobFlags = null,
1157 ?string $blobFormat = null,
1158 int $queryFlags = 0
1159 ) {
1160 if ( $blobData !== null ) {
1161 $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1162
1163 if ( $blobFlags === null ) {
1164 // No blob flags, so use the blob verbatim.
1165 $data = $blobData;
1166 } else {
1167 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1168 if ( $data === false ) {
1169 throw new RevisionAccessException(
1170 'Failed to expand blob data using flags {flags} (key: {cache_key})',
1171 [
1172 'flags' => $blobFlags,
1173 'cache_key' => $cacheKey,
1174 ]
1175 );
1176 }
1177 }
1178
1179 } else {
1180 $address = $slot->getAddress();
1181 try {
1182 $data = $this->blobStore->getBlob( $address, $queryFlags );
1183 } catch ( BlobAccessException $e ) {
1184 throw new RevisionAccessException(
1185 'Failed to load data blob from {address}'
1186 . 'If this problem persist, use the findBadBlobs maintenance script '
1187 . 'to investigate the issue and mark bad blobs.',
1188 [ 'address' => $e->getMessage() ],
1189 0,
1190 $e
1191 );
1192 }
1193 }
1194
1195 $model = $slot->getModel();
1196
1197 // If the content model is not known, don't fail here (T220594, T220793, T228921)
1198 if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1199 $this->logger->warning(
1200 "Undefined content model '$model', falling back to UnknownContent",
1201 [
1202 'content_address' => $slot->getAddress(),
1203 'rev_id' => $slot->getRevision(),
1204 'role_name' => $slot->getRole(),
1205 'model_name' => $model,
1206 'trace' => wfBacktrace()
1207 ]
1208 );
1209
1210 return new FallbackContent( $data, $model );
1211 }
1212
1213 return $this->contentHandlerFactory
1214 ->getContentHandler( $model )
1215 ->unserializeContent( $data, $blobFormat );
1216 }
1217
1235 public function getRevisionById( $id, $flags = 0, PageIdentity $page = null ) {
1236 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags, $page );
1237 }
1238
1255 public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) {
1256 $conds = [
1257 'page_namespace' => $page->getNamespace(),
1258 'page_title' => $page->getDBkey()
1259 ];
1260
1261 if ( $page instanceof LinkTarget ) {
1262 // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1263 $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1264 }
1265
1266 if ( $revId ) {
1267 // Use the specified revision ID.
1268 // Note that we use newRevisionFromConds here because we want to retry
1269 // and fall back to primary DB if the page is not found on a replica.
1270 // Since the caller supplied a revision ID, we are pretty sure the revision is
1271 // supposed to exist, so we should try hard to find it.
1272 $conds['rev_id'] = $revId;
1273 return $this->newRevisionFromConds( $conds, $flags, $page );
1274 } else {
1275 // Use a join to get the latest revision.
1276 // Note that we don't use newRevisionFromConds here because we don't want to retry
1277 // and fall back to primary DB. The assumption is that we only want to force the fallback
1278 // if we are quite sure the revision exists because the caller supplied a revision ID.
1279 // If the page isn't found at all on a replica, it probably simply does not exist.
1280 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1281 $conds[] = 'rev_id=page_latest';
1282 return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1283 }
1284 }
1285
1302 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1303 $conds = [ 'page_id' => $pageId ];
1304 if ( $revId ) {
1305 // Use the specified revision ID.
1306 // Note that we use newRevisionFromConds here because we want to retry
1307 // and fall back to primary DB if the page is not found on a replica.
1308 // Since the caller supplied a revision ID, we are pretty sure the revision is
1309 // supposed to exist, so we should try hard to find it.
1310 $conds['rev_id'] = $revId;
1311 return $this->newRevisionFromConds( $conds, $flags );
1312 } else {
1313 // Use a join to get the latest revision.
1314 // Note that we don't use newRevisionFromConds here because we don't want to retry
1315 // and fall back to primary DB. The assumption is that we only want to force the fallback
1316 // if we are quite sure the revision exists because the caller supplied a revision ID.
1317 // If the page isn't found at all on a replica, it probably simply does not exist.
1318 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1319
1320 $conds[] = 'rev_id=page_latest';
1321
1322 return $this->loadRevisionFromConds( $db, $conds, $flags );
1323 }
1324 }
1325
1341 public function getRevisionByTimestamp(
1342 $page,
1343 string $timestamp,
1344 int $flags = IDBAccessObject::READ_NORMAL
1345 ): ?RevisionRecord {
1346 if ( $page instanceof LinkTarget ) {
1347 // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
1348 $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
1349 }
1350 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1351 return $this->newRevisionFromConds(
1352 [
1353 'rev_timestamp' => $db->timestamp( $timestamp ),
1354 'page_namespace' => $page->getNamespace(),
1355 'page_title' => $page->getDBkey()
1356 ],
1357 $flags,
1358 $page
1359 );
1360 }
1361
1369 private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) {
1370 $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1371
1372 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1373 $db = $this->getDBConnectionRef( $dbMode );
1374
1375 $res = $db->select(
1376 $revQuery['tables'],
1377 $revQuery['fields'],
1378 [
1379 'slot_revision_id' => $revId,
1380 ],
1381 __METHOD__,
1382 $dbOptions,
1383 $revQuery['joins']
1384 );
1385
1386 if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1387 // If we found no slots, try looking on the primary database (T212428, T252156)
1388 $this->logger->info(
1389 __METHOD__ . ' falling back to READ_LATEST.',
1390 [
1391 'revid' => $revId,
1392 'trace' => wfBacktrace( true )
1393 ]
1394 );
1395 return $this->loadSlotRecords(
1396 $revId,
1397 $queryFlags | self::READ_LATEST,
1398 $page
1399 );
1400 }
1401
1402 return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1403 }
1404
1417 private function constructSlotRecords(
1418 $revId,
1419 $slotRows,
1420 $queryFlags,
1421 PageIdentity $page,
1422 $slotContents = null
1423 ) {
1424 $slots = [];
1425
1426 foreach ( $slotRows as $row ) {
1427 // Resolve role names and model names from in-memory cache, if they were not joined in.
1428 if ( !isset( $row->role_name ) ) {
1429 $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1430 }
1431
1432 if ( !isset( $row->model_name ) ) {
1433 if ( isset( $row->content_model ) ) {
1434 $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1435 } else {
1436 // We may get here if $row->model_name is set but null, perhaps because it
1437 // came from rev_content_model, which is NULL for the default model.
1438 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1439 $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1440 }
1441 }
1442
1443 // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1444 if ( isset( $row->blob_data ) ) {
1445 $slotContents[$row->content_address] = $row->blob_data;
1446 }
1447
1448 $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1449 $blob = null;
1450 if ( isset( $slotContents[$slot->getAddress()] ) ) {
1451 $blob = $slotContents[$slot->getAddress()];
1452 if ( $blob instanceof Content ) {
1453 return $blob;
1454 }
1455 }
1456 return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1457 };
1458
1459 $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1460 }
1461
1462 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1463 $this->logger->error(
1464 __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1465 [
1466 'revid' => $revId,
1467 'queryFlags' => $queryFlags,
1468 'trace' => wfBacktrace( true )
1469 ]
1470 );
1471
1472 throw new RevisionAccessException(
1473 'Main slot of revision not found in database. See T212428.'
1474 );
1475 }
1476
1477 return $slots;
1478 }
1479
1495 private function newRevisionSlots(
1496 $revId,
1497 $revisionRow,
1498 $slotRows,
1499 $queryFlags,
1500 PageIdentity $page
1501 ) {
1502 if ( $slotRows ) {
1503 $slots = new RevisionSlots(
1504 $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1505 );
1506 } else {
1507 // XXX: do we need the same kind of caching here
1508 // that getKnownCurrentRevision uses (if $revId == page_latest?)
1509
1510 $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) {
1511 return $this->loadSlotRecords( $revId, $queryFlags, $page );
1512 } );
1513 }
1514
1515 return $slots;
1516 }
1517
1540 $row,
1541 $queryFlags = 0,
1542 PageIdentity $page = null,
1543 array $overrides = []
1544 ) {
1545 return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides );
1546 }
1547
1560 public function newRevisionFromRow(
1561 $row,
1562 $queryFlags = 0,
1563 PageIdentity $page = null,
1564 $fromCache = false
1565 ) {
1566 return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache );
1567 }
1568
1589 stdClass $row,
1590 $slots,
1591 int $queryFlags = 0,
1592 ?PageIdentity $page = null,
1593 array $overrides = []
1594 ) {
1595 if ( !$page && isset( $overrides['title'] ) ) {
1596 if ( !( $overrides['title'] instanceof PageIdentity ) ) {
1597 throw new MWException( 'title field override must contain a PageIdentity object.' );
1598 }
1599
1600 $page = $overrides['title'];
1601 }
1602
1603 if ( !isset( $page ) ) {
1604 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1605 $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1606 } else {
1607 throw new InvalidArgumentException(
1608 'A Title or ar_namespace and ar_title must be given'
1609 );
1610 }
1611 }
1612
1613 foreach ( $overrides as $key => $value ) {
1614 $field = "ar_$key";
1615 $row->$field = $value;
1616 }
1617
1618 try {
1619 $user = $this->actorStore->newActorFromRowFields(
1620 $row->ar_user ?? null,
1621 $row->ar_user_text ?? null,
1622 $row->ar_actor ?? null
1623 );
1624 } catch ( InvalidArgumentException $ex ) {
1625 $this->logger->warning( 'Could not load user for archive revision {rev_id}', [
1626 'ar_rev_id' => $row->ar_rev_id,
1627 'ar_actor' => $row->ar_actor ?? 'null',
1628 'ar_user_text' => $row->ar_user_text ?? 'null',
1629 'ar_user' => $row->ar_user ?? 'null',
1630 'exception' => $ex
1631 ] );
1632 $user = $this->actorStore->getUnknownActor();
1633 }
1634
1635 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1636 // Legacy because $row may have come from self::selectFields()
1637 $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1638
1639 if ( !( $slots instanceof RevisionSlots ) ) {
1640 $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $page );
1641 }
1642 return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId );
1643 }
1644
1664 stdClass $row,
1665 $slots,
1666 int $queryFlags = 0,
1667 ?PageIdentity $page = null,
1668 bool $fromCache = false
1669 ) {
1670 if ( !$page ) {
1671 if ( isset( $row->page_id )
1672 && isset( $row->page_namespace )
1673 && isset( $row->page_title )
1674 ) {
1675 $page = new PageIdentityValue(
1676 (int)$row->page_id,
1677 (int)$row->page_namespace,
1678 $row->page_title,
1679 $this->wikiId
1680 );
1681
1682 $page = $this->wrapPage( $page );
1683 } else {
1684 $pageId = (int)( $row->rev_page ?? 0 );
1685 $revId = (int)( $row->rev_id ?? 0 );
1686
1687 $page = $this->getPage( $pageId, $revId, $queryFlags );
1688 }
1689 } else {
1690 $page = $this->ensureRevisionRowMatchesPage( $row, $page );
1691 }
1692
1693 if ( !$page ) {
1694 // This should already have been caught about, but apparently
1695 // it not always is, see T286877.
1696 throw new RevisionAccessException(
1697 "Failed to determine page associated with revision {$row->rev_id}"
1698 );
1699 }
1700
1701 try {
1702 $user = $this->actorStore->newActorFromRowFields(
1703 $row->rev_user ?? null,
1704 $row->rev_user_text ?? null,
1705 $row->rev_actor ?? null
1706 );
1707 } catch ( InvalidArgumentException $ex ) {
1708 $this->logger->warning( 'Could not load user for revision {rev_id}', [
1709 'rev_id' => $row->rev_id,
1710 'rev_actor' => $row->rev_actor ?? 'null',
1711 'rev_user_text' => $row->rev_user_text ?? 'null',
1712 'rev_user' => $row->rev_user ?? 'null',
1713 'exception' => $ex
1714 ] );
1715 $user = $this->actorStore->getUnknownActor();
1716 }
1717
1718 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1719 // Legacy because $row may have come from self::selectFields()
1720 $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1721
1722 if ( !( $slots instanceof RevisionSlots ) ) {
1723 $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $page );
1724 }
1725
1726 // If this is a cached row, instantiate a cache-aware RevisionRecord to avoid stale data.
1727 if ( $fromCache ) {
1728 $rev = new RevisionStoreCacheRecord(
1729 function ( $revId ) use ( $queryFlags ) {
1730 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1731 $row = $this->fetchRevisionRowFromConds(
1732 $db,
1733 [ 'rev_id' => intval( $revId ) ]
1734 );
1735 if ( !$row && !( $queryFlags & self::READ_LATEST ) ) {
1736 // If we found no slots, try looking on the primary database (T259738)
1737 $this->logger->info(
1738 'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1739 [
1740 'revid' => $revId,
1741 'trace' => wfBacktrace( true )
1742 ]
1743 );
1744 $dbw = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
1745 $row = $this->fetchRevisionRowFromConds(
1746 $dbw,
1747 [ 'rev_id' => intval( $revId ) ]
1748 );
1749 }
1750 if ( !$row ) {
1751 return [ null, null ];
1752 }
1753 return [
1754 $row->rev_deleted,
1755 $this->actorStore->newActorFromRowFields(
1756 $row->rev_user ?? null,
1757 $row->rev_user_text ?? null,
1758 $row->rev_actor ?? null
1759 )
1760 ];
1761 },
1762 $page, $user, $comment, $row, $slots, $this->wikiId
1763 );
1764 } else {
1765 $rev = new RevisionStoreRecord(
1766 $page, $user, $comment, $row, $slots, $this->wikiId );
1767 }
1768 return $rev;
1769 }
1770
1782 private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) {
1783 $revId = (int)( $row->rev_id ?? 0 );
1784 $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1785 $expectedPageId = $page->getId( $this->wikiId );
1786 // Avoid fatal error when the Title's ID changed, T246720
1787 if ( $revPageId && $expectedPageId && $revPageId !== $expectedPageId ) {
1788 // NOTE: PageStore::getPageByReference may use the page ID, which we don't want here.
1789 $pageRec = $this->pageStore->getPageByName(
1790 $page->getNamespace(),
1791 $page->getDBkey(),
1792 PageStore::READ_LATEST
1793 );
1794 $masterPageId = $pageRec->getId( $this->wikiId );
1795 $masterLatest = $pageRec->getLatest( $this->wikiId );
1796 if ( $revPageId === $masterPageId ) {
1797 if ( $page instanceof Title ) {
1798 // If we were using a Title object, keep using it, but update the page ID.
1799 // This way, we don't unexpectedly mix Titles with immutable value objects.
1800 $page->resetArticleID( $masterPageId );
1801
1802 } else {
1803 $page = $pageRec;
1804 }
1805
1806 $this->logger->info(
1807 "Encountered stale Title object",
1808 [
1809 'page_id_stale' => $expectedPageId,
1810 'page_id_reloaded' => $masterPageId,
1811 'page_latest' => $masterLatest,
1812 'rev_id' => $revId,
1813 'trace' => wfBacktrace()
1814 ] + $context
1815 );
1816 } else {
1817 $expectedTitle = (string)$page;
1818 if ( $page instanceof Title ) {
1819 // If we started with a Title, keep using a Title.
1820 $page = $this->titleFactory->newFromID( $revPageId );
1821 } else {
1822 $page = $pageRec;
1823 }
1824
1825 // This could happen if a caller to e.g. getRevisionById supplied a Title that is
1826 // plain wrong. In this case, we should ideally throw an IllegalArgumentException.
1827 // However, it is more likely that we encountered a race condition during a page
1828 // move (T268910, T279832) or database corruption (T263340). That situation
1829 // should not be ignored, but we can allow the request to continue in a reasonable
1830 // manner without breaking things for the user.
1831 $this->logger->error(
1832 "Encountered mismatching Title object (see T259022, T268910, T279832, T263340)",
1833 [
1834 'expected_page_id' => $masterPageId,
1835 'expected_page_title' => $expectedTitle,
1836 'rev_page' => $revPageId,
1837 'rev_page_title' => (string)$page,
1838 'page_latest' => $masterLatest,
1839 'rev_id' => $revId,
1840 'trace' => wfBacktrace()
1841 ] + $context
1842 );
1843 }
1844 }
1845
1846 return $page;
1847 }
1848
1874 public function newRevisionsFromBatch(
1875 $rows,
1876 array $options = [],
1877 $queryFlags = 0,
1878 PageIdentity $page = null
1879 ) {
1880 $result = new StatusValue();
1881 $archiveMode = $options['archive'] ?? false;
1882
1883 if ( $archiveMode ) {
1884 $revIdField = 'ar_rev_id';
1885 } else {
1886 $revIdField = 'rev_id';
1887 }
1888
1889 $rowsByRevId = [];
1890 $pageIdsToFetchTitles = [];
1891 $titlesByPageKey = [];
1892 foreach ( $rows as $row ) {
1893 if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1894 $result->warning(
1895 'internalerror_info',
1896 "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1897 );
1898 }
1899
1900 // Attach a page key to the row, so we can find and reuse Title objects easily.
1901 $row->_page_key =
1902 $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1903
1904 if ( $page ) {
1905 if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1906 throw new InvalidArgumentException(
1907 "Revision {$row->$revIdField} doesn't belong to page "
1908 . $this->getArticleId( $page )
1909 );
1910 }
1911
1912 if ( $archiveMode
1913 && ( $row->ar_namespace != $page->getNamespace()
1914 || $row->ar_title !== $page->getDBkey() )
1915 ) {
1916 throw new InvalidArgumentException(
1917 "Revision {$row->$revIdField} doesn't belong to page "
1918 . $page
1919 );
1920 }
1921 } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1922 if ( isset( $row->page_namespace ) && isset( $row->page_title )
1923 // This should always be true, but just in case we don't have a page_id
1924 // set or it doesn't match rev_page, let's fetch the title again.
1925 && isset( $row->page_id ) && isset( $row->rev_page )
1926 && $row->rev_page === $row->page_id
1927 ) {
1928 $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1929 } elseif ( $archiveMode ) {
1930 // Can't look up deleted pages by ID, but we have namespace and title
1931 $titlesByPageKey[ $row->_page_key ] =
1932 Title::makeTitle( $row->ar_namespace, $row->ar_title );
1933 } else {
1934 $pageIdsToFetchTitles[] = $row->rev_page;
1935 }
1936 }
1937 $rowsByRevId[$row->$revIdField] = $row;
1938 }
1939
1940 if ( empty( $rowsByRevId ) ) {
1941 $result->setResult( true, [] );
1942 return $result;
1943 }
1944
1945 // If the page is not supplied, batch-fetch Title objects.
1946 if ( $page ) {
1947 // same logic as for $row->_page_key above
1948 $pageKey = $archiveMode
1949 ? $page->getNamespace() . ':' . $page->getDBkey()
1950 : $this->getArticleId( $page );
1951
1952 $titlesByPageKey[$pageKey] = $page;
1953 } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1954 // Note: when we fetch titles by ID, the page key is also the ID.
1955 // We should never get here if $archiveMode is true.
1956 Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1957
1958 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1959 foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1960 $titlesByPageKey[$t->getArticleID()] = $t;
1961 }
1962 }
1963
1964 // which method to use for creating RevisionRecords
1965 $newRevisionRecord = [
1966 $this,
1967 $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1968 ];
1969
1970 if ( !isset( $options['slots'] ) ) {
1971 $result->setResult(
1972 true,
1973 array_map(
1974 static function ( $row )
1975 use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1976 try {
1977 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1978 $result->warning(
1979 'internalerror_info',
1980 "Couldn't find title for rev {$row->$revIdField} "
1981 . "(page key {$row->_page_key})"
1982 );
1983 return null;
1984 }
1985 return $newRevisionRecord( $row, null, $queryFlags,
1986 $titlesByPageKey[ $row->_page_key ] );
1987 } catch ( MWException $e ) {
1988 $result->warning( 'internalerror_info', $e->getMessage() );
1989 return null;
1990 }
1991 },
1992 $rowsByRevId
1993 )
1994 );
1995 return $result;
1996 }
1997
1998 $slotRowOptions = [
1999 'slots' => $options['slots'] ?? true,
2000 'blobs' => $options['content'] ?? false,
2001 ];
2002
2003 if ( is_array( $slotRowOptions['slots'] )
2004 && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
2005 ) {
2006 // Make sure the main slot is always loaded, RevisionRecord requires this.
2007 $slotRowOptions['slots'][] = SlotRecord::MAIN;
2008 }
2009
2010 $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2011
2012 $result->merge( $slotRowsStatus );
2013 $slotRowsByRevId = $slotRowsStatus->getValue();
2014
2015 $result->setResult(
2016 true,
2017 array_map(
2018 function ( $row )
2019 use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
2020 $revIdField, $newRevisionRecord
2021 ) {
2022 if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
2023 $result->warning(
2024 'internalerror_info',
2025 "Couldn't find slots for rev {$row->$revIdField}"
2026 );
2027 return null;
2028 }
2029 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2030 $result->warning(
2031 'internalerror_info',
2032 "Couldn't find title for rev {$row->$revIdField} "
2033 . "(page key {$row->_page_key})"
2034 );
2035 return null;
2036 }
2037 try {
2038 return $newRevisionRecord(
2039 $row,
2040 new RevisionSlots(
2041 $this->constructSlotRecords(
2042 $row->$revIdField,
2043 $slotRowsByRevId[$row->$revIdField],
2044 $queryFlags,
2045 $titlesByPageKey[$row->_page_key]
2046 )
2047 ),
2048 $queryFlags,
2049 $titlesByPageKey[$row->_page_key]
2050 );
2051 } catch ( MWException $e ) {
2052 $result->warning( 'internalerror_info', $e->getMessage() );
2053 return null;
2054 }
2055 },
2056 $rowsByRevId
2057 )
2058 );
2059 return $result;
2060 }
2061
2085 private function getSlotRowsForBatch(
2086 $rowsOrIds,
2087 array $options = [],
2088 $queryFlags = 0
2089 ) {
2090 $result = new StatusValue();
2091
2092 $revIds = [];
2093 foreach ( $rowsOrIds as $row ) {
2094 if ( is_object( $row ) ) {
2095 $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
2096 } else {
2097 $revIds[] = (int)$row;
2098 }
2099 }
2100
2101 // Nothing to do.
2102 // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2103 if ( empty( $revIds ) ) {
2104 $result->setResult( true, [] );
2105 return $result;
2106 }
2107
2108 // We need to set the `content` flag to join in content meta-data
2109 $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
2110 $revIdField = $slotQueryInfo['keys']['rev_id'];
2111 $slotQueryConds = [ $revIdField => $revIds ];
2112
2113 if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2114 if ( empty( $options['slots'] ) ) {
2115 // Degenerate case: return no slots for each revision.
2116 $result->setResult( true, array_fill_keys( $revIds, [] ) );
2117 return $result;
2118 }
2119
2120 $roleIdField = $slotQueryInfo['keys']['role_id'];
2121 $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
2122 return $this->slotRoleStore->getId( $slot_name );
2123 }, $options['slots'] );
2124 }
2125
2126 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2127 $slotRows = $db->select(
2128 $slotQueryInfo['tables'],
2129 $slotQueryInfo['fields'],
2130 $slotQueryConds,
2131 __METHOD__,
2132 [],
2133 $slotQueryInfo['joins']
2134 );
2135
2136 $slotContents = null;
2137 if ( $options['blobs'] ?? false ) {
2138 $blobAddresses = [];
2139 foreach ( $slotRows as $slotRow ) {
2140 $blobAddresses[] = $slotRow->content_address;
2141 }
2142 $slotContentFetchStatus = $this->blobStore
2143 ->getBlobBatch( $blobAddresses, $queryFlags );
2144 foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2145 $result->warning( $error['message'], ...$error['params'] );
2146 }
2147 $slotContents = $slotContentFetchStatus->getValue();
2148 }
2149
2150 $slotRowsByRevId = [];
2151 foreach ( $slotRows as $slotRow ) {
2152 if ( $slotContents === null ) {
2153 // nothing to do
2154 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2155 $slotRow->blob_data = $slotContents[$slotRow->content_address];
2156 } else {
2157 $result->warning(
2158 'internalerror_info',
2159 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2160 );
2161 $slotRow->blob_data = null;
2162 }
2163
2164 // conditional needed for SCHEMA_COMPAT_READ_OLD
2165 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2166 $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2167 }
2168
2169 // conditional needed for SCHEMA_COMPAT_READ_OLD
2170 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2171 $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2172 }
2173
2174 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2175 }
2176
2177 $result->setResult( true, $slotRowsByRevId );
2178 return $result;
2179 }
2180
2202 $rowsOrIds,
2203 $slots = null,
2204 $queryFlags = 0
2205 ) {
2206 $result = $this->getSlotRowsForBatch(
2207 $rowsOrIds,
2208 [ 'slots' => $slots, 'blobs' => true ],
2209 $queryFlags
2210 );
2211
2212 if ( $result->isOK() ) {
2213 // strip out all internal meta data that we don't want to expose
2214 foreach ( $result->value as $revId => $rowsByRole ) {
2215 foreach ( $rowsByRole as $role => $slotRow ) {
2216 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2217 // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2218 // if we didn't ask for it.
2219 unset( $result->value[$revId][$role] );
2220 continue;
2221 }
2222
2223 $result->value[$revId][$role] = (object)[
2224 'blob_data' => $slotRow->blob_data,
2225 'model_name' => $slotRow->model_name,
2226 ];
2227 }
2228 }
2229 }
2230
2231 return $result;
2232 }
2233
2250 private function newRevisionFromConds(
2251 array $conditions,
2252 int $flags = IDBAccessObject::READ_NORMAL,
2253 PageIdentity $page = null,
2254 array $options = []
2255 ) {
2256 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2257 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2258
2259 $lb = $this->getDBLoadBalancer();
2260
2261 // Make sure new pending/committed revision are visibile later on
2262 // within web requests to certain avoid bugs like T93866 and T94407.
2263 if ( !$rev
2264 && !( $flags & self::READ_LATEST )
2265 && $lb->hasStreamingReplicaServers()
2266 && $lb->hasOrMadeRecentPrimaryChanges()
2267 ) {
2268 $flags = self::READ_LATEST;
2269 $dbw = $this->getDBConnectionRef( DB_PRIMARY );
2270 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2271 }
2272
2273 return $rev;
2274 }
2275
2290 private function loadRevisionFromConds(
2291 IDatabase $db,
2292 array $conditions,
2293 int $flags = IDBAccessObject::READ_NORMAL,
2294 PageIdentity $page = null,
2295 array $options = []
2296 ) {
2297 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2298 if ( $row ) {
2299 return $this->newRevisionFromRow( $row, $flags, $page );
2300 }
2301
2302 return null;
2303 }
2304
2312 private function checkDatabaseDomain( IDatabase $db ) {
2313 $dbDomain = $db->getDomainID();
2314 $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2315 if ( $dbDomain === $storeDomain ) {
2316 return;
2317 }
2318
2319 throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2320 }
2321
2336 IDatabase $db,
2337 array $conditions,
2338 int $flags = IDBAccessObject::READ_NORMAL,
2339 array $options = []
2340 ) {
2341 $this->checkDatabaseDomain( $db );
2342
2343 $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2344 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2345 $options[] = 'FOR UPDATE';
2346 }
2347 return $db->selectRow(
2348 $revQuery['tables'],
2349 $revQuery['fields'],
2350 $conditions,
2351 __METHOD__,
2352 $options,
2353 $revQuery['joins']
2354 );
2355 }
2356
2378 public function getQueryInfo( $options = [] ) {
2379 $ret = [
2380 'tables' => [],
2381 'fields' => [],
2382 'joins' => [],
2383 ];
2384
2385 $ret['tables'][] = 'revision';
2386 $ret['fields'] = array_merge( $ret['fields'], [
2387 'rev_id',
2388 'rev_page',
2389 'rev_timestamp',
2390 'rev_minor_edit',
2391 'rev_deleted',
2392 'rev_len',
2393 'rev_parent_id',
2394 'rev_sha1',
2395 ] );
2396
2397 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2398 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2399 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2400 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2401
2402 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2403 $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2404 $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2405 $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2406
2407 if ( in_array( 'page', $options, true ) ) {
2408 $ret['tables'][] = 'page';
2409 $ret['fields'] = array_merge( $ret['fields'], [
2410 'page_namespace',
2411 'page_title',
2412 'page_id',
2413 'page_latest',
2414 'page_is_redirect',
2415 'page_len',
2416 ] );
2417 $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2418 }
2419
2420 if ( in_array( 'user', $options, true ) ) {
2421 $ret['tables'][] = 'user';
2422 $ret['fields'] = array_merge( $ret['fields'], [
2423 'user_name',
2424 ] );
2425 $u = $actorQuery['fields']['rev_user'];
2426 $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2427 }
2428
2429 if ( in_array( 'text', $options, true ) ) {
2430 throw new InvalidArgumentException(
2431 'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2432 );
2433 }
2434
2435 return $ret;
2436 }
2437
2458 public function getSlotsQueryInfo( $options = [] ) {
2459 $ret = [
2460 'tables' => [],
2461 'fields' => [],
2462 'joins' => [],
2463 'keys' => [],
2464 ];
2465
2466 $ret['keys']['rev_id'] = 'slot_revision_id';
2467 $ret['keys']['role_id'] = 'slot_role_id';
2468
2469 $ret['tables'][] = 'slots';
2470 $ret['fields'] = array_merge( $ret['fields'], [
2471 'slot_revision_id',
2472 'slot_content_id',
2473 'slot_origin',
2474 'slot_role_id',
2475 ] );
2476
2477 if ( in_array( 'role', $options, true ) ) {
2478 // Use left join to attach role name, so we still find the revision row even
2479 // if the role name is missing. This triggers a more obvious failure mode.
2480 $ret['tables'][] = 'slot_roles';
2481 $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2482 $ret['fields'][] = 'role_name';
2483 }
2484
2485 if ( in_array( 'content', $options, true ) ) {
2486 $ret['keys']['model_id'] = 'content_model';
2487
2488 $ret['tables'][] = 'content';
2489 $ret['fields'] = array_merge( $ret['fields'], [
2490 'content_size',
2491 'content_sha1',
2492 'content_address',
2493 'content_model',
2494 ] );
2495 $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2496
2497 if ( in_array( 'model', $options, true ) ) {
2498 // Use left join to attach model name, so we still find the revision row even
2499 // if the model name is missing. This triggers a more obvious failure mode.
2500 $ret['tables'][] = 'content_models';
2501 $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2502 $ret['fields'][] = 'model_name';
2503 }
2504
2505 }
2506
2507 return $ret;
2508 }
2509
2518 public function isRevisionRow( $row, string $table = '' ) {
2519 if ( !( $row instanceof stdClass ) ) {
2520 return false;
2521 }
2522 $queryInfo = $table === 'archive' ? $this->getArchiveQueryInfo() : $this->getQueryInfo();
2523 foreach ( $queryInfo['fields'] as $alias => $field ) {
2524 $name = is_numeric( $alias ) ? $field : $alias;
2525 if ( !property_exists( $row, $name ) ) {
2526 return false;
2527 }
2528 }
2529 return true;
2530 }
2531
2549 public function getArchiveQueryInfo() {
2550 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2551 $ret = [
2552 'tables' => [
2553 'archive',
2554 'archive_actor' => 'actor'
2555 ] + $commentQuery['tables'],
2556 'fields' => [
2557 'ar_id',
2558 'ar_page_id',
2559 'ar_namespace',
2560 'ar_title',
2561 'ar_rev_id',
2562 'ar_timestamp',
2563 'ar_minor_edit',
2564 'ar_deleted',
2565 'ar_len',
2566 'ar_parent_id',
2567 'ar_sha1',
2568 'ar_actor',
2569 'ar_user' => 'archive_actor.actor_user',
2570 'ar_user_text' => 'archive_actor.actor_name',
2571 ] + $commentQuery['fields'],
2572 'joins' => [
2573 'archive_actor' => [ 'JOIN', 'actor_id=ar_actor' ]
2574 ] + $commentQuery['joins'],
2575 ];
2576
2577 return $ret;
2578 }
2579
2589 public function getRevisionSizes( array $revIds ) {
2590 $dbr = $this->getDBConnectionRef( DB_REPLICA );
2591 $revLens = [];
2592 if ( !$revIds ) {
2593 return $revLens; // empty
2594 }
2595
2596 $res = $dbr->select(
2597 'revision',
2598 [ 'rev_id', 'rev_len' ],
2599 [ 'rev_id' => $revIds ],
2600 __METHOD__
2601 );
2602
2603 foreach ( $res as $row ) {
2604 $revLens[$row->rev_id] = intval( $row->rev_len );
2605 }
2606
2607 return $revLens;
2608 }
2609
2618 private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2619 $op = $dir === 'next' ? '>' : '<';
2620 $sort = $dir === 'next' ? 'ASC' : 'DESC';
2621
2622 $revisionIdValue = $rev->getId( $this->wikiId );
2623
2624 if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) {
2625 // revision is unsaved or otherwise incomplete
2626 return null;
2627 }
2628
2629 if ( $rev instanceof RevisionArchiveRecord ) {
2630 // revision is deleted, so it's not part of the page history
2631 return null;
2632 }
2633
2634 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2635 $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2636
2637 $ts = $this->getTimestampFromId( $revisionIdValue, $flags );
2638 if ( $ts === false ) {
2639 // XXX Should this be moved into getTimestampFromId?
2640 $ts = $db->selectField( 'archive', 'ar_timestamp',
2641 [ 'ar_rev_id' => $revisionIdValue ], __METHOD__ );
2642 if ( $ts === false ) {
2643 // XXX Is this reachable? How can we have a page id but no timestamp?
2644 return null;
2645 }
2646 }
2647 $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2648
2649 $revId = $db->selectField( 'revision', 'rev_id',
2650 [
2651 'rev_page' => $rev->getPageId( $this->wikiId ),
2652 "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op $revisionIdValue )"
2653 ],
2654 __METHOD__,
2655 [
2656 'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2657 'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2658 ]
2659 );
2660
2661 if ( $revId === false ) {
2662 return null;
2663 }
2664
2665 return $this->getRevisionById( intval( $revId ) );
2666 }
2667
2682 public function getPreviousRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2683 return $this->getRelativeRevision( $rev, $flags, 'prev' );
2684 }
2685
2697 public function getNextRevision( RevisionRecord $rev, $flags = self::READ_NORMAL ) {
2698 return $this->getRelativeRevision( $rev, $flags, 'next' );
2699 }
2700
2712 private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2713 $this->checkDatabaseDomain( $db );
2714
2715 if ( $rev->getPageId( $this->wikiId ) === null ) {
2716 return 0;
2717 }
2718 # Use page_latest if ID is not given
2719 if ( !$rev->getId( $this->wikiId ) ) {
2720 $prevId = $db->selectField(
2721 'page', 'page_latest',
2722 [ 'page_id' => $rev->getPageId( $this->wikiId ) ],
2723 __METHOD__
2724 );
2725 } else {
2726 $prevId = $db->selectField(
2727 'revision', 'rev_id',
2728 [ 'rev_page' => $rev->getPageId( $this->wikiId ), 'rev_id < ' . $rev->getId( $this->wikiId ) ],
2729 __METHOD__,
2730 [ 'ORDER BY' => 'rev_id DESC' ]
2731 );
2732 }
2733 return intval( $prevId );
2734 }
2735
2748 public function getTimestampFromId( $id, $flags = 0 ) {
2749 if ( $id instanceof Title ) {
2750 // Old deprecated calling convention supported for backwards compatibility
2751 $id = $flags;
2752 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2753 }
2754
2755 // T270149: Bail out if we know the query will definitely return false. Some callers are
2756 // passing RevisionRecord::getId() call directly as $id which can possibly return null.
2757 // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL'
2758 // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null.
2759 // @todo typehint $id and remove the null check
2760 if ( $id === null || $id <= 0 ) {
2761 return false;
2762 }
2763
2764 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2765
2766 $timestamp =
2767 $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2768
2769 return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2770 }
2771
2781 public function countRevisionsByPageId( IDatabase $db, $id ) {
2782 $this->checkDatabaseDomain( $db );
2783
2784 $row = $db->selectRow( 'revision',
2785 [ 'revCount' => 'COUNT(*)' ],
2786 [ 'rev_page' => $id ],
2787 __METHOD__
2788 );
2789 if ( $row ) {
2790 return intval( $row->revCount );
2791 }
2792 return 0;
2793 }
2794
2804 public function countRevisionsByTitle( IDatabase $db, PageIdentity $page ) {
2805 $id = $this->getArticleId( $page );
2806 if ( $id ) {
2807 return $this->countRevisionsByPageId( $db, $id );
2808 }
2809 return 0;
2810 }
2811
2830 public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2831 $this->checkDatabaseDomain( $db );
2832
2833 if ( !$userId ) {
2834 return false;
2835 }
2836
2837 $revQuery = $this->getQueryInfo();
2838 $res = $db->select(
2839 $revQuery['tables'],
2840 [
2841 'rev_user' => $revQuery['fields']['rev_user'],
2842 ],
2843 [
2844 'rev_page' => $pageId,
2845 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2846 ],
2847 __METHOD__,
2848 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2849 $revQuery['joins']
2850 );
2851 foreach ( $res as $row ) {
2852 if ( $row->rev_user != $userId ) {
2853 return false;
2854 }
2855 }
2856 return true;
2857 }
2858
2872 public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) {
2873 $db = $this->getDBConnectionRef( DB_REPLICA );
2874 $revIdPassed = $revId;
2875 $pageId = $this->getArticleId( $page );
2876 if ( !$pageId ) {
2877 return false;
2878 }
2879
2880 if ( !$revId ) {
2881 if ( $page instanceof Title ) {
2882 $revId = $page->getLatestRevID();
2883 } else {
2884 $pageRecord = $this->pageStore->getPageByReference( $page );
2885 if ( $pageRecord ) {
2886 $revId = $pageRecord->getLatest( $this->getWikiId() );
2887 }
2888 }
2889 }
2890
2891 if ( !$revId ) {
2892 $this->logger->warning(
2893 'No latest revision known for page {page} even though it exists with page ID {page_id}', [
2894 'page' => $page->__toString(),
2895 'page_id' => $pageId,
2896 'wiki_id' => $this->getWikiId() ?: 'local',
2897 ] );
2898 return false;
2899 }
2900
2901 // Load the row from cache if possible. If not possible, populate the cache.
2902 // As a minor optimization, remember if this was a cache hit or miss.
2903 // We can sometimes avoid a database query later if this is a cache miss.
2904 $fromCache = true;
2905 $row = $this->cache->getWithSetCallback(
2906 // Page/rev IDs passed in from DB to reflect history merges
2907 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2908 WANObjectCache::TTL_WEEK,
2909 function ( $curValue, &$ttl, array &$setOpts ) use (
2910 $db, $revId, &$fromCache
2911 ) {
2912 $setOpts += Database::getCacheSetOptions( $db );
2913 $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2914 if ( $row ) {
2915 $fromCache = false;
2916 }
2917 return $row; // don't cache negatives
2918 }
2919 );
2920
2921 // Reflect revision deletion and user renames.
2922 if ( $row ) {
2923 $title = $this->ensureRevisionRowMatchesPage( $row, $page, [
2924 'from_cache_flag' => $fromCache,
2925 'page_id_initial' => $pageId,
2926 'rev_id_used' => $revId,
2927 'rev_id_requested' => $revIdPassed,
2928 ] );
2929
2930 return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2931 } else {
2932 return false;
2933 }
2934 }
2935
2944 public function getFirstRevision(
2945 $page,
2946 int $flags = IDBAccessObject::READ_NORMAL
2947 ): ?RevisionRecord {
2948 if ( $page instanceof LinkTarget ) {
2949 // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756)
2950 $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null;
2951 }
2952 return $this->newRevisionFromConds(
2953 [
2954 'page_namespace' => $page->getNamespace(),
2955 'page_title' => $page->getDBkey()
2956 ],
2957 $flags,
2958 $page,
2959 [
2960 'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
2961 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
2962 ]
2963 );
2964 }
2965
2977 private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2978 return $this->cache->makeGlobalKey(
2979 self::ROW_CACHE_KEY,
2980 $db->getDomainID(),
2981 $pageId,
2982 $revId
2983 );
2984 }
2985
2993 private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
2994 if ( $rev ) {
2995 if ( $rev->getId( $this->wikiId ) === null ) {
2996 throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
2997 }
2998 if ( $rev->getPageId( $this->wikiId ) !== $pageId ) {
2999 throw new InvalidArgumentException(
3000 "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
3001 );
3002 }
3003 }
3004 }
3005
3022 RevisionRecord $old = null,
3023 RevisionRecord $new = null,
3024 $options = []
3025 ) {
3026 $options = (array)$options;
3027 $oldCmp = '>';
3028 $newCmp = '<';
3029 if ( in_array( self::INCLUDE_OLD, $options ) ) {
3030 $oldCmp = '>=';
3031 }
3032 if ( in_array( self::INCLUDE_NEW, $options ) ) {
3033 $newCmp = '<=';
3034 }
3035 if ( in_array( self::INCLUDE_BOTH, $options ) ) {
3036 $oldCmp = '>=';
3037 $newCmp = '<=';
3038 }
3039
3040 $conds = [];
3041 if ( $old ) {
3042 $oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
3043 $conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId( $this->wikiId )}) " .
3044 "OR rev_timestamp > {$oldTs}";
3045 }
3046 if ( $new ) {
3047 $newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
3048 $conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId( $this->wikiId )}) " .
3049 "OR rev_timestamp < {$newTs}";
3050 }
3051 return $conds;
3052 }
3053
3080 public function getRevisionIdsBetween(
3081 int $pageId,
3082 RevisionRecord $old = null,
3083 RevisionRecord $new = null,
3084 ?int $max = null,
3085 $options = [],
3086 ?string $order = null,
3087 int $flags = IDBAccessObject::READ_NORMAL
3088 ): array {
3089 $this->assertRevisionParameter( 'old', $pageId, $old );
3090 $this->assertRevisionParameter( 'new', $pageId, $new );
3091
3092 $options = (array)$options;
3093 $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3094 in_array( self::INCLUDE_BOTH, $options );
3095 $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3096 in_array( self::INCLUDE_BOTH, $options );
3097
3098 // No DB query needed if old and new are the same revision.
3099 // Can't check for consecutive revisions with 'getParentId' for a similar
3100 // optimization as edge cases exist when there are revisions between
3101 // a revision and it's parent. See T185167 for more details.
3102 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3103 return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3104 }
3105
3106 $db = $this->getDBConnectionRefForQueryFlags( $flags );
3107 $conds = array_merge(
3108 [
3109 'rev_page' => $pageId,
3110 $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3111 ],
3112 $this->getRevisionLimitConditions( $db, $old, $new, $options )
3113 );
3114
3115 $queryOptions = [];
3116 if ( $order !== null ) {
3117 $queryOptions['ORDER BY'] = [ "rev_timestamp $order", "rev_id $order" ];
3118 }
3119 if ( $max !== null ) {
3120 $queryOptions['LIMIT'] = $max + 1; // extra to detect truncation
3121 }
3122
3123 $values = $db->selectFieldValues(
3124 'revision',
3125 'rev_id',
3126 $conds,
3127 __METHOD__,
3128 $queryOptions
3129 );
3130 return array_map( 'intval', $values );
3131 }
3132
3154 public function getAuthorsBetween(
3155 $pageId,
3156 RevisionRecord $old = null,
3157 RevisionRecord $new = null,
3158 Authority $performer = null,
3159 $max = null,
3160 $options = []
3161 ) {
3162 $this->assertRevisionParameter( 'old', $pageId, $old );
3163 $this->assertRevisionParameter( 'new', $pageId, $new );
3164 $options = (array)$options;
3165
3166 // No DB query needed if old and new are the same revision.
3167 // Can't check for consecutive revisions with 'getParentId' for a similar
3168 // optimization as edge cases exist when there are revisions between
3169 //a revision and it's parent. See T185167 for more details.
3170 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3171 if ( empty( $options ) ) {
3172 return [];
3173 } elseif ( $performer ) {
3174 return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3175 } else {
3176 return [ $new->getUser() ];
3177 }
3178 }
3179
3180 $dbr = $this->getDBConnectionRef( DB_REPLICA );
3181 $conds = array_merge(
3182 [
3183 'rev_page' => $pageId,
3184 $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3185 ],
3186 $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3187 );
3188
3189 $queryOpts = [ 'DISTINCT' ];
3190 if ( $max !== null ) {
3191 $queryOpts['LIMIT'] = $max + 1;
3192 }
3193
3194 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3195 return array_map( function ( $row ) {
3196 return $this->actorStore->newActorFromRowFields(
3197 $row->rev_user,
3198 $row->rev_user_text,
3199 $row->rev_actor
3200 );
3201 }, iterator_to_array( $dbr->select(
3202 array_merge( [ 'revision' ], $actorQuery['tables'] ),
3203 $actorQuery['fields'],
3204 $conds, __METHOD__,
3205 $queryOpts,
3206 $actorQuery['joins']
3207 ) ) );
3208 }
3209
3231 public function countAuthorsBetween(
3232 $pageId,
3233 RevisionRecord $old = null,
3234 RevisionRecord $new = null,
3235 Authority $performer = null,
3236 $max = null,
3237 $options = []
3238 ) {
3239 // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3240 // and creation of UserIdentity stuff.
3241 return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3242 }
3243
3264 public function countRevisionsBetween(
3265 $pageId,
3266 RevisionRecord $old = null,
3267 RevisionRecord $new = null,
3268 $max = null,
3269 $options = []
3270 ) {
3271 $this->assertRevisionParameter( 'old', $pageId, $old );
3272 $this->assertRevisionParameter( 'new', $pageId, $new );
3273
3274 // No DB query needed if old and new are the same revision.
3275 // Can't check for consecutive revisions with 'getParentId' for a similar
3276 // optimization as edge cases exist when there are revisions between
3277 //a revision and it's parent. See T185167 for more details.
3278 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3279 return 0;
3280 }
3281
3282 $dbr = $this->getDBConnectionRef( DB_REPLICA );
3283 $conds = array_merge(
3284 [
3285 'rev_page' => $pageId,
3286 $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3287 ],
3288 $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3289 );
3290 if ( $max !== null ) {
3291 return $dbr->selectRowCount( 'revision', '1',
3292 $conds,
3293 __METHOD__,
3294 [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3295 );
3296 } else {
3297 return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3298 }
3299 }
3300
3312 public function findIdenticalRevision(
3313 RevisionRecord $revision,
3314 int $searchLimit
3315 ): ?RevisionRecord {
3316 $revision->assertWiki( $this->wikiId );
3317 $db = $this->getDBConnectionRef( DB_REPLICA );
3318 $revQuery = $this->getQueryInfo();
3319 $subquery = $db->buildSelectSubquery(
3320 $revQuery['tables'],
3321 $revQuery['fields'],
3322 [ 'rev_page' => $revision->getPageId( $this->wikiId ) ],
3323 __METHOD__,
3324 [
3325 'ORDER BY' => [
3326 'rev_timestamp DESC',
3327 // for cases where there are multiple revs with same timestamp
3328 'rev_id DESC'
3329 ],
3330 'LIMIT' => $searchLimit,
3331 // skip the most recent edit, we can't revert to it anyway
3332 'OFFSET' => 1
3333 ],
3334 $revQuery['joins']
3335 );
3336
3337 // selectRow effectively uses LIMIT 1 clause, returning only the first result
3338 $revisionRow = $db->selectRow(
3339 [ 'recent_revs' => $subquery ],
3340 '*',
3341 [ 'rev_sha1' => $revision->getSha1() ],
3342 __METHOD__
3343 );
3344
3345 return $revisionRow ? $this->newRevisionFromRow( $revisionRow ) : null;
3346 }
3347
3348 // TODO: move relevant methods from Title here, e.g. isBigDeletion, etc.
3349}
3350
3355class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
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.
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).
getVisibility()
Get the deletion bitfield of the 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.
isMinor()
MCR migration note: this replaced Revision::isMinor.
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.
getDBConnectionRef( $mode, $groups=[])
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context=[])
Check that the given row matches the given Title object.
constructSlotRecords( $revId, $slotRows, $queryFlags, PageIdentity $page, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
IContentHandlerFactory $contentHandlerFactory
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.
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, PageIdentity $page)
Factory method for RevisionSlots based on a revision ID.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, PageIdentity $page, array $blobHints=[])
newRevisionFromArchiveRowAndSlots(stdClass $row, $slots, int $queryFlags=0, ?PageIdentity $page=null, array $overrides=[])
getWikiId()
Get the ID of the wiki this revision belongs to.
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
newRevisionFromConds(array $conditions, int $flags=IDBAccessObject::READ_NORMAL, PageIdentity $page=null, array $options=[])
Given a set of conditions, fetch a revision.
storeContentBlob(SlotRecord $slot, PageIdentity $page, array $blobHints=[])
loadRevisionFromConds(IDatabase $db, array $conditions, int $flags=IDBAccessObject::READ_NORMAL, PageIdentity $page=null, array $options=[])
Given a set of conditions, fetch a revision from the given database connection.
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorMigration $actorMigration, ActorStore $actorStore, IContentHandlerFactory $contentHandlerFactory, PageStore $pageStore, TitleFactory $titleFactory, HookContainer $hookContainer, $wikiId=WikiAwareEntity::LOCAL)
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...
insertContentRowOn(SlotRecord $slot, IDatabase $dbw, $blobAddress)
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
fetchRevisionRowFromConds(IDatabase $db, array $conditions, int $flags=IDBAccessObject::READ_NORMAL, array $options=[])
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects.
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.
loadSlotRecords( $revId, $queryFlags, PageIdentity $page)
assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev=null)
Asserts that if revision is provided, it's saved and belongs to the page with provided pageId.
checkContent(Content $content, PageIdentity $page, string $role)
MCR migration note: this corresponded to Revision::checkContentModel.
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.
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
insertIpChangesRow(IDatabase $dbw, UserIdentity $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, $parentId)
getNextRevision(RevisionRecord $rev, $flags=self::READ_NORMAL)
Get the revision after $rev in the page's history, if any.
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
newRevisionFromArchiveRow( $row, $queryFlags=0, PageIdentity $page=null, array $overrides=[])
Make a fake RevisionRecord object from an archive table row.
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
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.
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) Caching rows w...
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.
getRevisionLimitConditions(IDatabase $dbr, RevisionRecord $old=null, RevisionRecord $new=null, $options=[])
Converts revision limits to query conditions.
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.
updateSlotsInternal(RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw)
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, UserIdentity $user, CommentStoreComment $comment, PageIdentity $page, $pageId, $parentId)
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.
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save.
getRevisionByTitle( $page, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, $parentId)
loadSlotContent(SlotRecord $slot, ?string $blobData=null, ?string $blobFlags=null, ?string $blobFormat=null, int $queryFlags=0)
Loads a Content object based on a slot row.
getPage(?int $pageId, ?int $revId, int $queryFlags=self::READ_NORMAL)
Determines the page based on the available information.
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.
hasContentId()
Whether this slot has a content ID.
getContentId()
Returns the ID of the content meta data row associated with the slot.
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(),...
getModifiedSlot( $role)
Returns the SlotRecord associated with the given role, if the slot with that role was modified (and n...
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:48
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
Relational database abstraction object.
Definition Database.php:52
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.
__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.
getId( $wikiId=self::LOCAL)
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback when the current transaction commits or rolls back.
unlock( $lockName, $method)
Release a lock.
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 reversable SQL statements from a callback.
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.
getDomainID()
Return the currently selected domain ID.
lock( $lockName, $method, $timeout=5, $flags=0)
Acquire a named lock.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
delete( $table, $conds, $fname=__METHOD__)
Delete all rows in a table that match a condition.
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
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 RDBMS type of the server (e.g.
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert the given row(s) into a table.
insertId()
Get the inserted value of an auto-increment row.
Database cluster connection, tracking, load balancing, and transaction manager interface.
Result wrapper for grabbing data queried from an IDatabase object.
const DB_REPLICA
Definition defines.php:25
const DB_PRIMARY
Definition defines.php:27
$content
Definition router.php:76