MediaWiki REL1_35
RevisionStore.php
Go to the documentation of this file.
1<?php
27namespace MediaWiki\Revision;
28
30use CommentStore;
32use Content;
36use InvalidArgumentException;
47use Message;
48use MWException;
49use MWTimestamp;
51use Psr\Log\LoggerAwareInterface;
52use Psr\Log\LoggerInterface;
53use Psr\Log\NullLogger;
54use RecentChange;
55use Revision;
56use RuntimeException;
57use StatusValue;
58use Title;
59use Traversable;
60use User;
62use Wikimedia\Assert\Assert;
63use Wikimedia\IPUtils;
69
80 implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
81
82 public const ROW_CACHE_KEY = 'revision-row-1.29';
83
84 public const ORDER_OLDEST_TO_NEWEST = 'ASC';
85 public const ORDER_NEWEST_TO_OLDEST = 'DESC';
86
90 private $blobStore;
91
95 private $dbDomain;
96
101
105 private $cache;
106
111
116
120 private $logger;
121
126
131
134
137
140
142 private $hookRunner;
143
164 public function __construct(
175 $dbDomain = false
176 ) {
177 Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
178
179 $this->loadBalancer = $loadBalancer;
180 $this->blobStore = $blobStore;
181 $this->cache = $cache;
182 $this->commentStore = $commentStore;
183 $this->contentModelStore = $contentModelStore;
184 $this->slotRoleStore = $slotRoleStore;
185 $this->slotRoleRegistry = $slotRoleRegistry;
186 $this->actorMigration = $actorMigration;
187 $this->dbDomain = $dbDomain;
188 $this->logger = new NullLogger();
189 $this->contentHandlerFactory = $contentHandlerFactory;
190 $this->hookContainer = $hookContainer;
191 $this->hookRunner = new HookRunner( $hookContainer );
192 }
193
194 public function setLogger( LoggerInterface $logger ) {
195 $this->logger = $logger;
196 }
197
201 public function isReadOnly() {
202 return $this->blobStore->isReadOnly();
203 }
204
208 private function getDBLoadBalancer() {
209 return $this->loadBalancer;
210 }
211
217 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
218 list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
219 return $this->getDBConnectionRef( $mode );
220 }
221
228 private function getDBConnectionRef( $mode, $groups = [] ) {
229 $lb = $this->getDBLoadBalancer();
230 return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
231 }
232
247 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
248 if ( !$pageId && !$revId ) {
249 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
250 }
251
252 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
253 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
254 if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
255 $queryFlags = self::READ_NORMAL;
256 }
257
258 $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
259 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
260
261 // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
262 if ( $canUseTitleNewFromId ) {
263 $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
264 // TODO: better foreign title handling (introduce TitleFactory)
265 $title = Title::newFromID( $pageId, $titleFlags );
266 if ( $title ) {
267 return $title;
268 }
269 }
270
271 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
272 $canUseRevId = ( $revId !== null && $revId > 0 );
273
274 if ( $canUseRevId ) {
275 $dbr = $this->getDBConnectionRef( $dbMode );
276 // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
277 $row = $dbr->selectRow(
278 [ 'revision', 'page' ],
279 [
280 'page_namespace',
281 'page_title',
282 'page_id',
283 'page_latest',
284 'page_is_redirect',
285 'page_len',
286 ],
287 [ 'rev_id' => $revId ],
288 __METHOD__,
289 $dbOptions,
290 [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
291 );
292 if ( $row ) {
293 // TODO: better foreign title handling (introduce TitleFactory)
294 return Title::newFromRow( $row );
295 }
296 }
297
298 // If we still don't have a title, fallback to master if that wasn't already happening.
299 if ( $dbMode !== DB_MASTER ) {
300 $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
301 if ( $title ) {
302 $this->logger->info(
303 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
304 [ 'trace' => wfBacktrace() ]
305 );
306 return $title;
307 }
308 }
309
310 throw new RevisionAccessException(
311 "Could not determine title for page ID $pageId and revision ID $revId"
312 );
313 }
314
322 private function failOnNull( $value, $name ) {
323 if ( $value === null ) {
325 "$name must not be " . var_export( $value, true ) . "!"
326 );
327 }
328
329 return $value;
330 }
331
339 private function failOnEmpty( $value, $name ) {
340 if ( $value === null || $value === 0 || $value === '' ) {
342 "$name must not be " . var_export( $value, true ) . "!"
343 );
344 }
345
346 return $value;
347 }
348
360 public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
361 // TODO: pass in a DBTransactionContext instead of a database connection.
362 $this->checkDatabaseDomain( $dbw );
363
364 $slotRoles = $rev->getSlotRoles();
365
366 // Make sure the main slot is always provided throughout migration
367 if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
369 'main slot must be provided'
370 );
371 }
372
373 // Checks
374 $this->failOnNull( $rev->getSize(), 'size field' );
375 $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
376 $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
377 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
378 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
379 $this->failOnNull( $user->getId(), 'user field' );
380 $this->failOnEmpty( $user->getName(), 'user_text field' );
381
382 if ( !$rev->isReadyForInsertion() ) {
383 // This is here for future-proofing. At the time this check being added, it
384 // was redundant to the individual checks above.
385 throw new IncompleteRevisionException( 'Revision is incomplete' );
386 }
387
388 if ( $slotRoles == [ SlotRecord::MAIN ] ) {
389 // T239717: If the main slot is the only slot, make sure the revision's nominal size
390 // and hash match the main slot's nominal size and hash.
391 $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
392 Assert::precondition(
393 $mainSlot->getSize() === $rev->getSize(),
394 'The revisions\'s size must match the main slot\'s size (see T239717)'
395 );
396 Assert::precondition(
397 $mainSlot->getSha1() === $rev->getSha1(),
398 'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
399 );
400 }
401
402 // TODO: we shouldn't need an actual Title here.
403 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
404 $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
405
406 $parentId = $rev->getParentId() === null
407 ? $this->getPreviousRevisionId( $dbw, $rev )
408 : $rev->getParentId();
409
411 $rev = $dbw->doAtomicSection(
412 __METHOD__,
413 function ( IDatabase $dbw, $fname ) use (
414 $rev,
415 $user,
416 $comment,
417 $title,
418 $pageId,
419 $parentId
420 ) {
421 return $this->insertRevisionInternal(
422 $rev,
423 $dbw,
424 $user,
425 $comment,
426 $title,
427 $pageId,
428 $parentId
429 );
430 }
431 );
432
433 // sanity checks
434 Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
435 Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
436 Assert::postcondition(
437 $rev->getComment( RevisionRecord::RAW ) !== null,
438 'revision must have a comment'
439 );
440 Assert::postcondition(
441 $rev->getUser( RevisionRecord::RAW ) !== null,
442 'revision must have a user'
443 );
444
445 // Trigger exception if the main slot is missing.
446 // Technically, this could go away after MCR migration: while
447 // calling code may require a main slot to exist, RevisionStore
448 // really should not know or care about that requirement.
450
451 foreach ( $slotRoles as $role ) {
452 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
453 Assert::postcondition(
454 $slot->getContent() !== null,
455 $role . ' slot must have content'
456 );
457 Assert::postcondition(
458 $slot->hasRevision(),
459 $role . ' slot must have a revision associated'
460 );
461 }
462
463 $this->hookRunner->onRevisionRecordInserted( $rev );
464
465 // Soft deprecated in 1.31, hard deprecated in 1.35
466 if ( $this->hookContainer->isRegistered( 'RevisionInsertComplete' ) ) {
467 // Only create the Revision object if its needed
468 $legacyRevision = new Revision( $rev );
469 $this->hookRunner->onRevisionInsertComplete( $legacyRevision, null, null );
470 }
471
472 return $rev;
473 }
474
475 private function insertRevisionInternal(
476 RevisionRecord $rev,
477 IDatabase $dbw,
478 User $user,
479 CommentStoreComment $comment,
481 $pageId,
482 $parentId
483 ) {
484 $slotRoles = $rev->getSlotRoles();
485
486 $revisionRow = $this->insertRevisionRowOn(
487 $dbw,
488 $rev,
489 $title,
490 $parentId
491 );
492
493 $revisionId = $revisionRow['rev_id'];
494
495 $blobHints = [
496 BlobStore::PAGE_HINT => $pageId,
497 BlobStore::REVISION_HINT => $revisionId,
498 BlobStore::PARENT_HINT => $parentId,
499 ];
500
501 $newSlots = [];
502 foreach ( $slotRoles as $role ) {
503 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
504
505 // If the SlotRecord already has a revision ID set, this means it already exists
506 // in the database, and should already belong to the current revision.
507 // However, a slot may already have a revision, but no content ID, if the slot
508 // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
509 // mode, and the respective archive row was not yet migrated to the new schema.
510 // In that case, a new slot row (and content row) must be inserted even during
511 // undeletion.
512 if ( $slot->hasRevision() && $slot->hasContentId() ) {
513 // TODO: properly abort transaction if the assertion fails!
514 Assert::parameter(
515 $slot->getRevision() === $revisionId,
516 'slot role ' . $slot->getRole(),
517 'Existing slot should belong to revision '
518 . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
519 );
520
521 // Slot exists, nothing to do, move along.
522 // This happens when restoring archived revisions.
523
524 $newSlots[$role] = $slot;
525 } else {
526 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
527 }
528 }
529
530 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
531
532 $rev = new RevisionStoreRecord(
533 $title,
534 $user,
535 $comment,
536 (object)$revisionRow,
537 new RevisionSlots( $newSlots ),
538 $this->dbDomain
539 );
540
541 return $rev;
542 }
543
552 private function insertSlotOn(
553 IDatabase $dbw,
554 $revisionId,
555 SlotRecord $protoSlot,
557 array $blobHints = []
558 ) {
559 if ( $protoSlot->hasAddress() ) {
560 $blobAddress = $protoSlot->getAddress();
561 } else {
562 $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
563 }
564
565 $contentId = null;
566
567 if ( $protoSlot->hasContentId() ) {
568 $contentId = $protoSlot->getContentId();
569 } else {
570 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
571 }
572
573 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
574
575 $savedSlot = SlotRecord::newSaved(
576 $revisionId,
577 $contentId,
578 $blobAddress,
579 $protoSlot
580 );
581
582 return $savedSlot;
583 }
584
592 private function insertIpChangesRow(
593 IDatabase $dbw,
594 User $user,
595 RevisionRecord $rev,
596 $revisionId
597 ) {
598 if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
599 $ipcRow = [
600 'ipc_rev_id' => $revisionId,
601 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
602 'ipc_hex' => IPUtils::toHex( $user->getName() ),
603 ];
604 $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
605 }
606 }
607
619 private function insertRevisionRowOn(
620 IDatabase $dbw,
621 RevisionRecord $rev,
623 $parentId
624 ) {
625 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
626
627 list( $commentFields, $commentCallback ) =
628 $this->commentStore->insertWithTempTable(
629 $dbw,
630 'rev_comment',
632 );
633 $revisionRow += $commentFields;
634
635 list( $actorFields, $actorCallback ) =
636 $this->actorMigration->getInsertValuesWithTempTable(
637 $dbw,
638 'rev_user',
640 );
641 $revisionRow += $actorFields;
642
643 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
644
645 if ( !isset( $revisionRow['rev_id'] ) ) {
646 // only if auto-increment was used
647 $revisionRow['rev_id'] = intval( $dbw->insertId() );
648
649 if ( $dbw->getType() === 'mysql' ) {
650 // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
651 // auto-increment value to disk, so on server restart it might reuse IDs from deleted
652 // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
653
654 $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
655 $table = 'archive';
656 $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
657 if ( $maxRevId2 >= $maxRevId ) {
658 $maxRevId = $maxRevId2;
659 $table = 'slots';
660 }
661
662 if ( $maxRevId >= $revisionRow['rev_id'] ) {
663 $this->logger->debug(
664 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
665 . ' Trying to fix it.',
666 [
667 'revid' => $revisionRow['rev_id'],
668 'table' => $table,
669 'maxrevid' => $maxRevId,
670 ]
671 );
672
673 if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
674 throw new MWException( 'Failed to get database lock for T202032' );
675 }
676 $fname = __METHOD__;
678 function ( $trigger, IDatabase $dbw ) use ( $fname ) {
679 $dbw->unlock( 'fix-for-T202032', $fname );
680 },
681 __METHOD__
682 );
683
684 $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
685
686 // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
687 // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
688 // inserts too, though, at least on MariaDB 10.1.29.
689 //
690 // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
691 // transactions in this code path thanks to the row lock from the original ->insert() above.
692 //
693 // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
694 // that's for non-MySQL DBs.
695 $row1 = $dbw->query(
696 $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
697 __METHOD__
698 )->fetchObject();
699
700 $row2 = $dbw->query(
701 $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
702 . ' FOR UPDATE',
703 __METHOD__
704 )->fetchObject();
705
706 $maxRevId = max(
707 $maxRevId,
708 $row1 ? intval( $row1->v ) : 0,
709 $row2 ? intval( $row2->v ) : 0
710 );
711
712 // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
713 // transactions will throw a duplicate key error here. It doesn't seem worth trying
714 // to avoid that.
715 $revisionRow['rev_id'] = $maxRevId + 1;
716 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
717 }
718 }
719 }
720
721 $commentCallback( $revisionRow['rev_id'] );
722 $actorCallback( $revisionRow['rev_id'], $revisionRow );
723
724 return $revisionRow;
725 }
726
737 private function getBaseRevisionRow(
738 IDatabase $dbw,
739 RevisionRecord $rev,
741 $parentId
742 ) {
743 // Record the edit in revisions
744 $revisionRow = [
745 'rev_page' => $rev->getPageId(),
746 'rev_parent_id' => $parentId,
747 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
748 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
749 'rev_deleted' => $rev->getVisibility(),
750 'rev_len' => $rev->getSize(),
751 'rev_sha1' => $rev->getSha1(),
752 ];
753
754 if ( $rev->getId() !== null ) {
755 // Needed to restore revisions with their original ID
756 $revisionRow['rev_id'] = $rev->getId();
757 }
758
759 return $revisionRow;
760 }
761
770 private function storeContentBlob(
771 SlotRecord $slot,
773 array $blobHints = []
774 ) {
775 $content = $slot->getContent();
776 $format = $content->getDefaultFormat();
777 $model = $content->getModel();
778
779 $this->checkContent( $content, $title, $slot->getRole() );
780
781 return $this->blobStore->storeBlob(
782 $content->serialize( $format ),
783 // These hints "leak" some information from the higher abstraction layer to
784 // low level storage to allow for optimization.
785 array_merge(
786 $blobHints,
787 [
788 BlobStore::DESIGNATION_HINT => 'page-content',
789 BlobStore::ROLE_HINT => $slot->getRole(),
790 BlobStore::SHA1_HINT => $slot->getSha1(),
791 BlobStore::MODEL_HINT => $model,
792 BlobStore::FORMAT_HINT => $format,
793 ]
794 )
795 );
796 }
797
804 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
805 $slotRow = [
806 'slot_revision_id' => $revisionId,
807 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
808 'slot_content_id' => $contentId,
809 // If the slot has a specific origin use that ID, otherwise use the ID of the revision
810 // that we just inserted.
811 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
812 ];
813 $dbw->insert( 'slots', $slotRow, __METHOD__ );
814 }
815
822 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
823 $contentRow = [
824 'content_size' => $slot->getSize(),
825 'content_sha1' => $slot->getSha1(),
826 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
827 'content_address' => $blobAddress,
828 ];
829 $dbw->insert( 'content', $contentRow, __METHOD__ );
830 return intval( $dbw->insertId() );
831 }
832
843 private function checkContent( Content $content, Title $title, $role ) {
844 // Note: may return null for revisions that have not yet been inserted
845
846 $model = $content->getModel();
847 $format = $content->getDefaultFormat();
848 $handler = $content->getContentHandler();
849
850 $name = "$title";
851
852 if ( !$handler->isSupportedFormat( $format ) ) {
853 throw new MWException( "Can't use format $format with content model $model on $name" );
854 }
855
856 if ( !$content->isValid() ) {
857 throw new MWException(
858 "New content for $name is not valid! Content model is $model"
859 );
860 }
861 }
862
888 public function newNullRevision(
889 IDatabase $dbw,
891 CommentStoreComment $comment,
892 $minor,
893 User $user
894 ) {
895 $this->checkDatabaseDomain( $dbw );
896
897 $pageId = $title->getArticleID();
898
899 // T51581: Lock the page table row to ensure no other process
900 // is adding a revision to the page at the same time.
901 // Avoid locking extra tables, compare T191892.
902 $pageLatest = $dbw->selectField(
903 'page',
904 'page_latest',
905 [ 'page_id' => $pageId ],
906 __METHOD__,
907 [ 'FOR UPDATE' ]
908 );
909
910 if ( !$pageLatest ) {
911 return null;
912 }
913
914 // Fetch the actual revision row from master, without locking all extra tables.
915 $oldRevision = $this->loadRevisionFromConds(
916 $dbw,
917 [ 'rev_id' => intval( $pageLatest ) ],
918 self::READ_LATEST,
919 $title
920 );
921
922 if ( !$oldRevision ) {
923 $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
924 $this->logger->error(
925 $msg,
926 [ 'exception' => new RuntimeException( $msg ) ]
927 );
928 return null;
929 }
930
931 // Construct the new revision
932 $timestamp = MWTimestamp::now( TS_MW );
933 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
934
935 $newRevision->setComment( $comment );
936 $newRevision->setUser( $user );
937 $newRevision->setTimestamp( $timestamp );
938 $newRevision->setMinorEdit( $minor );
939
940 return $newRevision;
941 }
942
952 public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
953 $rc = $this->getRecentChange( $rev );
954 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
955 return $rc->getAttribute( 'rc_id' );
956 } else {
957 return 0;
958 }
959 }
960
974 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
975 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
976
977 $rc = RecentChange::newFromConds(
978 [ 'rc_this_oldid' => $rev->getId() ],
979 __METHOD__,
980 $dbType
981 );
982
983 // XXX: cache this locally? Glue it to the RevisionRecord?
984 return $rc;
985 }
986
1006 private function loadSlotContent(
1007 SlotRecord $slot,
1008 $blobData = null,
1009 $blobFlags = null,
1010 $blobFormat = null,
1011 $queryFlags = 0
1012 ) {
1013 if ( $blobData !== null ) {
1014 Assert::parameterType( 'string', $blobData, '$blobData' );
1015 Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1016
1017 $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1018
1019 if ( $blobFlags === null ) {
1020 // No blob flags, so use the blob verbatim.
1021 $data = $blobData;
1022 } else {
1023 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1024 if ( $data === false ) {
1025 throw new RevisionAccessException(
1026 "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1027 );
1028 }
1029 }
1030
1031 } else {
1032 $address = $slot->getAddress();
1033 try {
1034 $data = $this->blobStore->getBlob( $address, $queryFlags );
1035 } catch ( BlobAccessException $e ) {
1036 throw new RevisionAccessException(
1037 "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1038 );
1039 }
1040 }
1041
1042 return $this->contentHandlerFactory
1043 ->getContentHandler( $slot->getModel() )
1044 ->unserializeContent( $data, $blobFormat );
1045 }
1046
1061 public function getRevisionById( $id, $flags = 0 ) {
1062 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1063 }
1064
1081 public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1082 $conds = [
1083 'page_namespace' => $linkTarget->getNamespace(),
1084 'page_title' => $linkTarget->getDBkey()
1085 ];
1086
1087 // Only resolve to a Title when operating in the context of the local wiki (T248756)
1088 // TODO should not require Title in future (T206498)
1089 $title = $this->dbDomain === false ? Title::newFromLinkTarget( $linkTarget ) : null;
1090
1091 if ( $revId ) {
1092 // Use the specified revision ID.
1093 // Note that we use newRevisionFromConds here because we want to retry
1094 // and fall back to master if the page is not found on a replica.
1095 // Since the caller supplied a revision ID, we are pretty sure the revision is
1096 // supposed to exist, so we should try hard to find it.
1097 $conds['rev_id'] = $revId;
1098 return $this->newRevisionFromConds( $conds, $flags, $title );
1099 } else {
1100 // Use a join to get the latest revision.
1101 // Note that we don't use newRevisionFromConds here because we don't want to retry
1102 // and fall back to master. The assumption is that we only want to force the fallback
1103 // if we are quite sure the revision exists because the caller supplied a revision ID.
1104 // If the page isn't found at all on a replica, it probably simply does not exist.
1105 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1106
1107 $conds[] = 'rev_id=page_latest';
1108 $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1109
1110 return $rev;
1111 }
1112 }
1113
1130 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1131 $conds = [ 'page_id' => $pageId ];
1132 if ( $revId ) {
1133 // Use the specified revision ID.
1134 // Note that we use newRevisionFromConds here because we want to retry
1135 // and fall back to master if the page is not found on a replica.
1136 // Since the caller supplied a revision ID, we are pretty sure the revision is
1137 // supposed to exist, so we should try hard to find it.
1138 $conds['rev_id'] = $revId;
1139 return $this->newRevisionFromConds( $conds, $flags );
1140 } else {
1141 // Use a join to get the latest revision.
1142 // Note that we don't use newRevisionFromConds here because we don't want to retry
1143 // and fall back to master. The assumption is that we only want to force the fallback
1144 // if we are quite sure the revision exists because the caller supplied a revision ID.
1145 // If the page isn't found at all on a replica, it probably simply does not exist.
1146 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1147
1148 $conds[] = 'rev_id=page_latest';
1149 $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1150
1151 return $rev;
1152 }
1153 }
1154
1170 public function getRevisionByTimestamp(
1172 string $timestamp,
1173 int $flags = IDBAccessObject::READ_NORMAL
1174 ): ?RevisionRecord {
1175 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1176 return $this->newRevisionFromConds(
1177 [
1178 'rev_timestamp' => $db->timestamp( $timestamp ),
1179 'page_namespace' => $title->getNamespace(),
1180 'page_title' => $title->getDBkey()
1181 ],
1182 $flags,
1183 Title::newFromLinkTarget( $title )
1184 );
1185 }
1186
1194 private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
1195 $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1196
1197 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1198 $db = $this->getDBConnectionRef( $dbMode );
1199
1200 $res = $db->select(
1201 $revQuery['tables'],
1202 $revQuery['fields'],
1203 [
1204 'slot_revision_id' => $revId,
1205 ],
1206 __METHOD__,
1207 $dbOptions,
1208 $revQuery['joins']
1209 );
1210
1211 if ( !$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1212 // If we found no slots, try looking on the master database (T212428, T252156)
1213 $this->logger->info(
1214 __METHOD__ . ' falling back to READ_LATEST.',
1215 [
1216 'revid' => $revId,
1217 'trace' => wfBacktrace( true )
1218 ]
1219 );
1220 return $this->loadSlotRecords(
1221 $revId,
1222 $queryFlags | self::READ_LATEST,
1223 $title
1224 );
1225 }
1226
1227 $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
1228
1229 return $slots;
1230 }
1231
1244 private function constructSlotRecords(
1245 $revId,
1246 $slotRows,
1247 $queryFlags,
1248 Title $title,
1249 $slotContents = null
1250 ) {
1251 $slots = [];
1252
1253 foreach ( $slotRows as $row ) {
1254 // Resolve role names and model names from in-memory cache, if they were not joined in.
1255 if ( !isset( $row->role_name ) ) {
1256 $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1257 }
1258
1259 if ( !isset( $row->model_name ) ) {
1260 if ( isset( $row->content_model ) ) {
1261 $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1262 } else {
1263 // We may get here if $row->model_name is set but null, perhaps because it
1264 // came from rev_content_model, which is NULL for the default model.
1265 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1266 $row->model_name = $slotRoleHandler->getDefaultModel( $title );
1267 }
1268 }
1269
1270 // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1271 if ( isset( $row->blob_data ) ) {
1272 $slotContents[$row->content_address] = $row->blob_data;
1273 }
1274
1275 $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1276 $blob = null;
1277 if ( isset( $slotContents[$slot->getAddress()] ) ) {
1278 $blob = $slotContents[$slot->getAddress()];
1279 if ( $blob instanceof Content ) {
1280 return $blob;
1281 }
1282 }
1283 return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1284 };
1285
1286 $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1287 }
1288
1289 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1290 $this->logger->error(
1291 __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1292 [
1293 'revid' => $revId,
1294 'queryFlags' => $queryFlags,
1295 'trace' => wfBacktrace( true )
1296 ]
1297 );
1298
1299 throw new RevisionAccessException(
1300 'Main slot of revision not found in database. See T212428.'
1301 );
1302 }
1303
1304 return $slots;
1305 }
1306
1322 private function newRevisionSlots(
1323 $revId,
1324 $revisionRow,
1325 $slotRows,
1326 $queryFlags,
1328 ) {
1329 if ( $slotRows ) {
1330 $slots = new RevisionSlots(
1331 $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
1332 );
1333 } else {
1334 // XXX: do we need the same kind of caching here
1335 // that getKnownCurrentRevision uses (if $revId == page_latest?)
1336
1337 $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
1338 return $this->loadSlotRecords( $revId, $queryFlags, $title );
1339 } );
1340 }
1341
1342 return $slots;
1343 }
1344
1363 $row,
1364 $queryFlags = 0,
1365 Title $title = null,
1366 array $overrides = []
1367 ) {
1368 return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $title, $overrides );
1369 }
1370
1384 public function newRevisionFromRow(
1385 $row,
1386 $queryFlags = 0,
1387 Title $title = null,
1388 $fromCache = false
1389 ) {
1390 return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
1391 }
1392
1413 $row,
1414 $slots,
1415 $queryFlags = 0,
1416 Title $title = null,
1417 array $overrides = []
1418 ) {
1419 Assert::parameterType( \stdClass::class, $row, '$row' );
1420
1421 // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1422 Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1423
1424 if ( !$title && isset( $overrides['title'] ) ) {
1425 if ( !( $overrides['title'] instanceof Title ) ) {
1426 throw new MWException( 'title field override must contain a Title object.' );
1427 }
1428
1429 $title = $overrides['title'];
1430 }
1431
1432 if ( !isset( $title ) ) {
1433 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1434 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1435 } else {
1436 throw new InvalidArgumentException(
1437 'A Title or ar_namespace and ar_title must be given'
1438 );
1439 }
1440 }
1441
1442 foreach ( $overrides as $key => $value ) {
1443 $field = "ar_$key";
1444 $row->$field = $value;
1445 }
1446
1447 try {
1448 $user = User::newFromAnyId(
1449 $row->ar_user ?? null,
1450 $row->ar_user_text ?? null,
1451 $row->ar_actor ?? null,
1452 $this->dbDomain
1453 );
1454 } catch ( InvalidArgumentException $ex ) {
1455 wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1456 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1457 }
1458
1459 if ( $user->getName() === '' ) {
1460 // T236624: If the user name is empty, force 'Unknown user',
1461 // even if the actor table has an entry for the empty user name.
1462 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1463 }
1464
1465 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1466 // Legacy because $row may have come from self::selectFields()
1467 $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1468
1469 if ( !( $slots instanceof RevisionSlots ) ) {
1470 $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $title );
1471 }
1472
1473 return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
1474 }
1475
1494 $row,
1495 $slots,
1496 $queryFlags = 0,
1497 Title $title = null,
1498 $fromCache = false
1499 ) {
1500 Assert::parameterType( \stdClass::class, $row, '$row' );
1501
1502 if ( !$title ) {
1503 $pageId = (int)( $row->rev_page ?? 0 ); // XXX: fall back to page_id?
1504 $revId = (int)( $row->rev_id ?? 0 );
1505
1506 $title = $this->getTitle( $pageId, $revId, $queryFlags );
1507 } else {
1508 $this->ensureRevisionRowMatchesTitle( $row, $title );
1509 }
1510
1511 if ( !isset( $row->page_latest ) ) {
1512 $row->page_latest = $title->getLatestRevID();
1513 if ( $row->page_latest === 0 && $title->exists() ) {
1514 wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1515 }
1516 }
1517
1518 try {
1519 $user = User::newFromAnyId(
1520 $row->rev_user ?? null,
1521 $row->rev_user_text ?? null,
1522 $row->rev_actor ?? null,
1523 $this->dbDomain
1524 );
1525 } catch ( InvalidArgumentException $ex ) {
1526 wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1527 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1528 }
1529
1530 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1531 // Legacy because $row may have come from self::selectFields()
1532 $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1533
1534 if ( !( $slots instanceof RevisionSlots ) ) {
1535 $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
1536 }
1537
1538 // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1539 if ( $fromCache ) {
1540 $rev = new RevisionStoreCacheRecord(
1541 function ( $revId ) use ( $queryFlags ) {
1542 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1543 return $this->fetchRevisionRowFromConds(
1544 $db,
1545 [ 'rev_id' => intval( $revId ) ]
1546 );
1547 },
1548 $title, $user, $comment, $row, $slots, $this->dbDomain
1549 );
1550 } else {
1551 $rev = new RevisionStoreRecord(
1552 $title, $user, $comment, $row, $slots, $this->dbDomain );
1553 }
1554 return $rev;
1555 }
1556
1566 private function ensureRevisionRowMatchesTitle( $row, Title $title, $context = [] ) {
1567 $revId = (int)( $row->rev_id ?? 0 );
1568 $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1569 $titlePageId = $title->getArticleID();
1570
1571 // Avoid fatal error when the Title's ID changed, T246720
1572 if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1573 $masterPageId = $title->getArticleID( Title::READ_LATEST );
1574 $masterLatest = $title->getLatestRevID( Title::READ_LATEST );
1575
1576 if ( $revPageId === $masterPageId ) {
1577 $this->logger->warning(
1578 "Encountered stale Title object",
1579 [
1580 'page_id_stale' => $titlePageId,
1581 'page_id_reloaded' => $masterPageId,
1582 'page_latest' => $masterLatest,
1583 'rev_id' => $revId,
1584 'trace' => wfBacktrace()
1585 ] + $context
1586 );
1587 } else {
1588 throw new InvalidArgumentException(
1589 "Revision $revId belongs to page ID $revPageId, "
1590 . "the provided Title object belongs to page ID $masterPageId"
1591 );
1592 }
1593 }
1594 }
1595
1621 public function newRevisionsFromBatch(
1622 $rows,
1623 array $options = [],
1624 $queryFlags = 0,
1625 Title $title = null
1626 ) {
1627 $result = new StatusValue();
1628 $archiveMode = $options['archive'] ?? false;
1629
1630 if ( $archiveMode ) {
1631 $revIdField = 'ar_rev_id';
1632 } else {
1633 $revIdField = 'rev_id';
1634 }
1635
1636 $rowsByRevId = [];
1637 $pageIdsToFetchTitles = [];
1638 $titlesByPageKey = [];
1639 foreach ( $rows as $row ) {
1640 if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1641 $result->warning(
1642 'internalerror_info',
1643 "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1644 );
1645 }
1646
1647 // Attach a page key to the row, so we can find and reuse Title objects easily.
1648 $row->_page_key =
1649 $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1650
1651 if ( $title ) {
1652 if ( !$archiveMode && $row->rev_page != $title->getArticleID() ) {
1653 throw new InvalidArgumentException(
1654 "Revision {$row->$revIdField} doesn't belong to page "
1655 . $title->getArticleID()
1656 );
1657 }
1658
1659 if ( $archiveMode
1660 && ( $row->ar_namespace != $title->getNamespace()
1661 || $row->ar_title !== $title->getDBkey() )
1662 ) {
1663 throw new InvalidArgumentException(
1664 "Revision {$row->$revIdField} doesn't belong to page "
1665 . $title->getPrefixedDBkey()
1666 );
1667 }
1668 } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1669 if ( isset( $row->page_namespace ) && isset( $row->page_title )
1670 // This should always be true, but just in case we don't have a page_id
1671 // set or it doesn't match rev_page, let's fetch the title again.
1672 && isset( $row->page_id ) && isset( $row->rev_page )
1673 && $row->rev_page === $row->page_id
1674 ) {
1675 $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1676 } elseif ( $archiveMode ) {
1677 // Can't look up deleted pages by ID, but we have namespace and title
1678 $titlesByPageKey[ $row->_page_key ] =
1679 Title::makeTitle( $row->ar_namespace, $row->ar_title );
1680 } else {
1681 $pageIdsToFetchTitles[] = $row->rev_page;
1682 }
1683 }
1684 $rowsByRevId[$row->$revIdField] = $row;
1685 }
1686
1687 if ( empty( $rowsByRevId ) ) {
1688 $result->setResult( true, [] );
1689 return $result;
1690 }
1691
1692 // If the title is not supplied, batch-fetch Title objects.
1693 if ( $title ) {
1694 // same logic as for $row->_page_key above
1695 $pageKey = $archiveMode
1696 ? $title->getNamespace() . ':' . $title->getDBkey()
1697 : $title->getArticleID();
1698
1699 $titlesByPageKey[$pageKey] = $title;
1700 } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1701 // Note: when we fetch titles by ID, the page key is also the ID.
1702 // We should never get here if $archiveMode is true.
1703 Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1704
1705 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1706 foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1707 $titlesByPageKey[$t->getArticleID()] = $t;
1708 }
1709 }
1710
1711 // which method to use for creating RevisionRecords
1712 $newRevisionRecord = [
1713 $this,
1714 $archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
1715 ];
1716
1717 if ( !isset( $options['slots'] ) ) {
1718 $result->setResult(
1719 true,
1720 array_map(
1721 function ( $row )
1722 use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1723 try {
1724 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1725 $result->warning(
1726 'internalerror_info',
1727 "Couldn't find title for rev {$row->$revIdField} "
1728 . "(page key {$row->_page_key})"
1729 );
1730 return null;
1731 }
1732 return $newRevisionRecord( $row, null, $queryFlags,
1733 $titlesByPageKey[ $row->_page_key ] );
1734 } catch ( MWException $e ) {
1735 $result->warning( 'internalerror_info', $e->getMessage() );
1736 return null;
1737 }
1738 },
1739 $rowsByRevId
1740 )
1741 );
1742 return $result;
1743 }
1744
1745 $slotRowOptions = [
1746 'slots' => $options['slots'] ?? true,
1747 'blobs' => $options['content'] ?? false,
1748 ];
1749
1750 if ( is_array( $slotRowOptions['slots'] )
1751 && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
1752 ) {
1753 // Make sure the main slot is always loaded, RevisionRecord requires this.
1754 $slotRowOptions['slots'][] = SlotRecord::MAIN;
1755 }
1756
1757 $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
1758
1759 $result->merge( $slotRowsStatus );
1760 $slotRowsByRevId = $slotRowsStatus->getValue();
1761
1762 $result->setResult(
1763 true,
1764 array_map(
1765 function ( $row )
1766 use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
1767 $revIdField, $newRevisionRecord
1768 ) {
1769 if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
1770 $result->warning(
1771 'internalerror_info',
1772 "Couldn't find slots for rev {$row->$revIdField}"
1773 );
1774 return null;
1775 }
1776 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1777 $result->warning(
1778 'internalerror_info',
1779 "Couldn't find title for rev {$row->$revIdField} "
1780 . "(page key {$row->_page_key})"
1781 );
1782 return null;
1783 }
1784 try {
1785 return $newRevisionRecord(
1786 $row,
1787 new RevisionSlots(
1788 $this->constructSlotRecords(
1789 $row->$revIdField,
1790 $slotRowsByRevId[$row->$revIdField],
1791 $queryFlags,
1792 $titlesByPageKey[$row->_page_key]
1793 )
1794 ),
1795 $queryFlags,
1796 $titlesByPageKey[$row->_page_key]
1797 );
1798 } catch ( MWException $e ) {
1799 $result->warning( 'internalerror_info', $e->getMessage() );
1800 return null;
1801 }
1802 },
1803 $rowsByRevId
1804 )
1805 );
1806 return $result;
1807 }
1808
1832 private function getSlotRowsForBatch(
1833 $rowsOrIds,
1834 array $options = [],
1835 $queryFlags = 0
1836 ) {
1837 $result = new StatusValue();
1838
1839 $revIds = [];
1840 foreach ( $rowsOrIds as $row ) {
1841 if ( is_object( $row ) ) {
1842 $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
1843 } else {
1844 $revIds[] = (int)$row;
1845 }
1846 }
1847
1848 // Nothing to do.
1849 // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
1850 if ( empty( $revIds ) ) {
1851 $result->setResult( true, [] );
1852 return $result;
1853 }
1854
1855 // We need to set the `content` flag to join in content meta-data
1856 $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
1857 $revIdField = $slotQueryInfo['keys']['rev_id'];
1858 $slotQueryConds = [ $revIdField => $revIds ];
1859
1860 if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
1861 if ( empty( $options['slots'] ) ) {
1862 // Degenerate case: return no slots for each revision.
1863 $result->setResult( true, array_fill_keys( $revIds, [] ) );
1864 return $result;
1865 }
1866
1867 $roleIdField = $slotQueryInfo['keys']['role_id'];
1868 $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
1869 return $this->slotRoleStore->getId( $slot_name );
1870 }, $options['slots'] );
1871 }
1872
1873 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1874 $slotRows = $db->select(
1875 $slotQueryInfo['tables'],
1876 $slotQueryInfo['fields'],
1877 $slotQueryConds,
1878 __METHOD__,
1879 [],
1880 $slotQueryInfo['joins']
1881 );
1882
1883 $slotContents = null;
1884 if ( $options['blobs'] ?? false ) {
1885 $blobAddresses = [];
1886 foreach ( $slotRows as $slotRow ) {
1887 $blobAddresses[] = $slotRow->content_address;
1888 }
1889 $slotContentFetchStatus = $this->blobStore
1890 ->getBlobBatch( $blobAddresses, $queryFlags );
1891 foreach ( $slotContentFetchStatus->getErrors() as $error ) {
1892 $result->warning( $error['message'], ...$error['params'] );
1893 }
1894 $slotContents = $slotContentFetchStatus->getValue();
1895 }
1896
1897 $slotRowsByRevId = [];
1898 foreach ( $slotRows as $slotRow ) {
1899 if ( $slotContents === null ) {
1900 // nothing to do
1901 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
1902 $slotRow->blob_data = $slotContents[$slotRow->content_address];
1903 } else {
1904 $result->warning(
1905 'internalerror_info',
1906 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
1907 );
1908 $slotRow->blob_data = null;
1909 }
1910
1911 // conditional needed for SCHEMA_COMPAT_READ_OLD
1912 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
1913 $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
1914 }
1915
1916 // conditional needed for SCHEMA_COMPAT_READ_OLD
1917 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
1918 $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
1919 }
1920
1921 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
1922 }
1923
1924 $result->setResult( true, $slotRowsByRevId );
1925 return $result;
1926 }
1927
1949 $rowsOrIds,
1950 $slots = null,
1951 $queryFlags = 0
1952 ) {
1953 $result = $this->getSlotRowsForBatch(
1954 $rowsOrIds,
1955 [ 'slots' => $slots, 'blobs' => true ],
1956 $queryFlags
1957 );
1958
1959 if ( $result->isOK() ) {
1960 // strip out all internal meta data that we don't want to expose
1961 foreach ( $result->value as $revId => $rowsByRole ) {
1962 foreach ( $rowsByRole as $role => $slotRow ) {
1963 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
1964 // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
1965 // if we didn't ask for it.
1966 unset( $result->value[$revId][$role] );
1967 continue;
1968 }
1969
1970 $result->value[$revId][$role] = (object)[
1971 'blob_data' => $slotRow->blob_data,
1972 'model_name' => $slotRow->model_name,
1973 ];
1974 }
1975 }
1976 }
1977
1978 return $result;
1979 }
1980
1996 array $fields,
1997 $queryFlags = 0,
1998 Title $title = null
1999 ) {
2000 if ( !$title && isset( $fields['title'] ) ) {
2001 if ( !( $fields['title'] instanceof Title ) ) {
2002 throw new MWException( 'title field must contain a Title object.' );
2003 }
2004
2005 $title = $fields['title'];
2006 }
2007
2008 if ( !$title ) {
2009 $pageId = $fields['page'] ?? 0;
2010 $revId = $fields['id'] ?? 0;
2011
2012 $title = $this->getTitle( $pageId, $revId, $queryFlags );
2013 }
2014
2015 if ( !isset( $fields['page'] ) ) {
2016 $fields['page'] = $title->getArticleID( $queryFlags );
2017 }
2018
2019 // if we have a content object, use it to set the model and type
2020 if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
2021 && !is_array( $fields['content'] )
2022 ) {
2023 throw new MWException(
2024 'content field must contain a Content object or an array of Content objects.'
2025 );
2026 }
2027
2028 if ( !empty( $fields['text_id'] ) ) {
2029 throw new MWException( 'The text_id field can not be used in MediaWiki 1.35 and later' );
2030 }
2031
2032 if (
2033 isset( $fields['comment'] )
2034 && !( $fields['comment'] instanceof CommentStoreComment )
2035 ) {
2036 $commentData = $fields['comment_data'] ?? null;
2037
2038 if ( $fields['comment'] instanceof Message ) {
2039 $fields['comment'] = CommentStoreComment::newUnsavedComment(
2040 $fields['comment'],
2041 $commentData
2042 );
2043 } else {
2044 $commentText = trim( strval( $fields['comment'] ) );
2045 $fields['comment'] = CommentStoreComment::newUnsavedComment(
2046 $commentText,
2047 $commentData
2048 );
2049 }
2050 }
2051
2052 $revision = new MutableRevisionRecord( $title, $this->dbDomain );
2053
2055 if ( isset( $fields['content'] ) ) {
2056 if ( is_array( $fields['content'] ) ) {
2057 $slotContent = $fields['content'];
2058 } else {
2059 $slotContent = [ SlotRecord::MAIN => $fields['content'] ];
2060 }
2061 } elseif ( isset( $fields['text'] ) ) {
2062 if ( isset( $fields['content_model'] ) ) {
2063 $model = $fields['content_model'];
2064 } else {
2065 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( SlotRecord::MAIN );
2066 $model = $slotRoleHandler->getDefaultModel( $title );
2067 }
2068
2069 $contentHandler = ContentHandler::getForModelID( $model );
2070 $content = $contentHandler->unserializeContent( $fields['text'] );
2071 $slotContent = [ SlotRecord::MAIN => $content ];
2072 } else {
2073 $slotContent = [];
2074 }
2075
2076 foreach ( $slotContent as $role => $content ) {
2077 $revision->setContent( $role, $content );
2078 }
2079
2080 $this->initializeMutableRevisionFromArray( $revision, $fields );
2081
2082 return $revision;
2083 }
2084
2090 MutableRevisionRecord $record,
2091 array $fields
2092 ) {
2094 $user = null;
2095
2096 // If a user is passed in, use it if possible. We cannot use a user from a
2097 // remote wiki with unsuppressed ids, due to issues described in T222212.
2098 if ( isset( $fields['user'] ) &&
2099 ( $fields['user'] instanceof UserIdentity ) &&
2100 ( $this->dbDomain === false ||
2101 ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
2102 ) {
2103 $user = $fields['user'];
2104 } else {
2105 $userID = isset( $fields['user'] ) && is_numeric( $fields['user'] ) ? $fields['user'] : null;
2106 try {
2107 $user = User::newFromAnyId(
2108 $userID,
2109 $fields['user_text'] ?? null,
2110 $fields['actor'] ?? null,
2111 $this->dbDomain
2112 );
2113 } catch ( InvalidArgumentException $ex ) {
2114 $user = null;
2115 }
2116 }
2117
2118 if ( $user ) {
2119 $record->setUser( $user );
2120 }
2121
2122 $timestamp = isset( $fields['timestamp'] )
2123 ? strval( $fields['timestamp'] )
2124 : MWTimestamp::now( TS_MW );
2125
2126 $record->setTimestamp( $timestamp );
2127
2128 if ( isset( $fields['page'] ) ) {
2129 $record->setPageId( intval( $fields['page'] ) );
2130 }
2131
2132 if ( isset( $fields['id'] ) ) {
2133 $record->setId( intval( $fields['id'] ) );
2134 }
2135 if ( isset( $fields['parent_id'] ) ) {
2136 $record->setParentId( intval( $fields['parent_id'] ) );
2137 }
2138
2139 if ( isset( $fields['sha1'] ) ) {
2140 $record->setSha1( $fields['sha1'] );
2141 }
2142
2143 if ( isset( $fields['size'] ) ) {
2144 $record->setSize( intval( $fields['size'] ) );
2145 } elseif ( isset( $fields['len'] ) ) {
2146 $record->setSize( intval( $fields['len'] ) );
2147 }
2148
2149 if ( isset( $fields['minor_edit'] ) ) {
2150 $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2151 }
2152 if ( isset( $fields['deleted'] ) ) {
2153 $record->setVisibility( intval( $fields['deleted'] ) );
2154 }
2155
2156 if ( isset( $fields['comment'] ) ) {
2157 Assert::parameterType(
2158 CommentStoreComment::class,
2159 $fields['comment'],
2160 '$row[\'comment\']'
2161 );
2162 $record->setComment( $fields['comment'] );
2163 }
2164 }
2165
2180 public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2181 wfDeprecated( __METHOD__, '1.35' );
2182 $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2183 if ( $id ) {
2184 $conds['rev_id'] = intval( $id );
2185 } else {
2186 $conds[] = 'rev_id=page_latest';
2187 }
2188 return $this->loadRevisionFromConds( $db, $conds );
2189 }
2190
2208 public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2209 wfDeprecated( __METHOD__, '1.35' );
2210 if ( $id ) {
2211 $matchId = intval( $id );
2212 } else {
2213 $matchId = 'page_latest';
2214 }
2215
2216 return $this->loadRevisionFromConds(
2217 $db,
2218 [
2219 "rev_id=$matchId",
2220 'page_namespace' => $title->getNamespace(),
2221 'page_title' => $title->getDBkey()
2222 ],
2223 0,
2224 $title
2225 );
2226 }
2227
2242 public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2243 wfDeprecated( __METHOD__, '1.35' );
2244 return $this->loadRevisionFromConds( $db,
2245 [
2246 'rev_timestamp' => $db->timestamp( $timestamp ),
2247 'page_namespace' => $title->getNamespace(),
2248 'page_title' => $title->getDBkey()
2249 ],
2250 0,
2251 $title
2252 );
2253 }
2254
2271 private function newRevisionFromConds(
2272 array $conditions,
2273 int $flags = IDBAccessObject::READ_NORMAL,
2274 Title $title = null,
2275 array $options = []
2276 ) {
2277 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2278 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title, $options );
2279
2280 $lb = $this->getDBLoadBalancer();
2281
2282 // Make sure new pending/committed revision are visibile later on
2283 // within web requests to certain avoid bugs like T93866 and T94407.
2284 if ( !$rev
2285 && !( $flags & self::READ_LATEST )
2286 && $lb->hasStreamingReplicaServers()
2287 && $lb->hasOrMadeRecentMasterChanges()
2288 ) {
2289 $flags = self::READ_LATEST;
2290 $dbw = $this->getDBConnectionRef( DB_MASTER );
2291 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title, $options );
2292 }
2293
2294 return $rev;
2295 }
2296
2311 private function loadRevisionFromConds(
2312 IDatabase $db,
2313 array $conditions,
2314 int $flags = IDBAccessObject::READ_NORMAL,
2315 Title $title = null,
2316 array $options = []
2317 ) {
2318 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2319 if ( $row ) {
2320 $rev = $this->newRevisionFromRow( $row, $flags, $title );
2321
2322 return $rev;
2323 }
2324
2325 return null;
2326 }
2327
2335 private function checkDatabaseDomain( IDatabase $db ) {
2336 $dbDomain = $db->getDomainID();
2337 $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2338 if ( $dbDomain === $storeDomain ) {
2339 return;
2340 }
2341
2342 throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2343 }
2344
2359 IDatabase $db,
2360 array $conditions,
2361 int $flags = IDBAccessObject::READ_NORMAL,
2362 array $options = []
2363 ) {
2364 $this->checkDatabaseDomain( $db );
2365
2366 $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2367 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2368 $options[] = 'FOR UPDATE';
2369 }
2370 return $db->selectRow(
2371 $revQuery['tables'],
2372 $revQuery['fields'],
2373 $conditions,
2374 __METHOD__,
2375 $options,
2376 $revQuery['joins']
2377 );
2378 }
2379
2401 public function getQueryInfo( $options = [] ) {
2402 $ret = [
2403 'tables' => [],
2404 'fields' => [],
2405 'joins' => [],
2406 ];
2407
2408 $ret['tables'][] = 'revision';
2409 $ret['fields'] = array_merge( $ret['fields'], [
2410 'rev_id',
2411 'rev_page',
2412 'rev_timestamp',
2413 'rev_minor_edit',
2414 'rev_deleted',
2415 'rev_len',
2416 'rev_parent_id',
2417 'rev_sha1',
2418 ] );
2419
2420 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2421 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2422 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2423 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2424
2425 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2426 $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2427 $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2428 $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2429
2430 if ( in_array( 'page', $options, true ) ) {
2431 $ret['tables'][] = 'page';
2432 $ret['fields'] = array_merge( $ret['fields'], [
2433 'page_namespace',
2434 'page_title',
2435 'page_id',
2436 'page_latest',
2437 'page_is_redirect',
2438 'page_len',
2439 ] );
2440 $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2441 }
2442
2443 if ( in_array( 'user', $options, true ) ) {
2444 $ret['tables'][] = 'user';
2445 $ret['fields'] = array_merge( $ret['fields'], [
2446 'user_name',
2447 ] );
2448 $u = $actorQuery['fields']['rev_user'];
2449 $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2450 }
2451
2452 if ( in_array( 'text', $options, true ) ) {
2453 throw new InvalidArgumentException(
2454 'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2455 );
2456 }
2457
2458 return $ret;
2459 }
2460
2481 public function getSlotsQueryInfo( $options = [] ) {
2482 $ret = [
2483 'tables' => [],
2484 'fields' => [],
2485 'joins' => [],
2486 'keys' => [],
2487 ];
2488
2489 $ret['keys']['rev_id'] = 'slot_revision_id';
2490 $ret['keys']['role_id'] = 'slot_role_id';
2491
2492 $ret['tables'][] = 'slots';
2493 $ret['fields'] = array_merge( $ret['fields'], [
2494 'slot_revision_id',
2495 'slot_content_id',
2496 'slot_origin',
2497 'slot_role_id',
2498 ] );
2499
2500 if ( in_array( 'role', $options, true ) ) {
2501 // Use left join to attach role name, so we still find the revision row even
2502 // if the role name is missing. This triggers a more obvious failure mode.
2503 $ret['tables'][] = 'slot_roles';
2504 $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2505 $ret['fields'][] = 'role_name';
2506 }
2507
2508 if ( in_array( 'content', $options, true ) ) {
2509 $ret['keys']['model_id'] = 'content_model';
2510
2511 $ret['tables'][] = 'content';
2512 $ret['fields'] = array_merge( $ret['fields'], [
2513 'content_size',
2514 'content_sha1',
2515 'content_address',
2516 'content_model',
2517 ] );
2518 $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2519
2520 if ( in_array( 'model', $options, true ) ) {
2521 // Use left join to attach model name, so we still find the revision row even
2522 // if the model name is missing. This triggers a more obvious failure mode.
2523 $ret['tables'][] = 'content_models';
2524 $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2525 $ret['fields'][] = 'model_name';
2526 }
2527
2528 }
2529
2530 return $ret;
2531 }
2532
2546 public function getArchiveQueryInfo() {
2547 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2548 $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2549 $ret = [
2550 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2551 'fields' => [
2552 'ar_id',
2553 'ar_page_id',
2554 'ar_namespace',
2555 'ar_title',
2556 'ar_rev_id',
2557 'ar_timestamp',
2558 'ar_minor_edit',
2559 'ar_deleted',
2560 'ar_len',
2561 'ar_parent_id',
2562 'ar_sha1',
2563 ] + $commentQuery['fields'] + $actorQuery['fields'],
2564 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2565 ];
2566
2567 return $ret;
2568 }
2569
2579 public function getRevisionSizes( array $revIds ) {
2580 $dbr = $this->getDBConnectionRef( DB_REPLICA );
2581 $revLens = [];
2582 if ( !$revIds ) {
2583 return $revLens; // empty
2584 }
2585
2586 $res = $dbr->select(
2587 'revision',
2588 [ 'rev_id', 'rev_len' ],
2589 [ 'rev_id' => $revIds ],
2590 __METHOD__
2591 );
2592
2593 foreach ( $res as $row ) {
2594 $revLens[$row->rev_id] = intval( $row->rev_len );
2595 }
2596
2597 return $revLens;
2598 }
2599
2612 public function listRevisionSizes( IDatabase $db, array $revIds ) {
2613 wfDeprecated( __METHOD__, '1.35' );
2614 return $this->getRevisionSizes( $revIds );
2615 }
2616
2625 private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2626 $op = $dir === 'next' ? '>' : '<';
2627 $sort = $dir === 'next' ? 'ASC' : 'DESC';
2628
2629 if ( !$rev->getId() || !$rev->getPageId() ) {
2630 // revision is unsaved or otherwise incomplete
2631 return null;
2632 }
2633
2634 if ( $rev instanceof RevisionArchiveRecord ) {
2635 // revision is deleted, so it's not part of the page history
2636 return null;
2637 }
2638
2639 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2640 $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2641
2642 $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2643 if ( $ts === false ) {
2644 // XXX Should this be moved into getTimestampFromId?
2645 $ts = $db->selectField( 'archive', 'ar_timestamp',
2646 [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2647 if ( $ts === false ) {
2648 // XXX Is this reachable? How can we have a page id but no timestamp?
2649 return null;
2650 }
2651 }
2652 $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2653
2654 $revId = $db->selectField( 'revision', 'rev_id',
2655 [
2656 'rev_page' => $rev->getPageId(),
2657 "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op {$rev->getId()})"
2658 ],
2659 __METHOD__,
2660 [
2661 'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
2662 'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2663 ]
2664 );
2665
2666 if ( $revId === false ) {
2667 return null;
2668 }
2669
2670 return $this->getRevisionById( intval( $revId ) );
2671 }
2672
2688 public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
2689 if ( $flags instanceof Title ) {
2690 // Old calling convention, we don't use Title here anymore
2691 wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2692 $flags = 0;
2693 }
2694
2695 return $this->getRelativeRevision( $rev, $flags, 'prev' );
2696 }
2697
2711 public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
2712 if ( $flags instanceof Title ) {
2713 // Old calling convention, we don't use Title here anymore
2714 wfDeprecated( __METHOD__ . ' with Title', '1.34' );
2715 $flags = 0;
2716 }
2717
2718 return $this->getRelativeRevision( $rev, $flags, 'next' );
2719 }
2720
2732 private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
2733 $this->checkDatabaseDomain( $db );
2734
2735 if ( $rev->getPageId() === null ) {
2736 return 0;
2737 }
2738 # Use page_latest if ID is not given
2739 if ( !$rev->getId() ) {
2740 $prevId = $db->selectField(
2741 'page', 'page_latest',
2742 [ 'page_id' => $rev->getPageId() ],
2743 __METHOD__
2744 );
2745 } else {
2746 $prevId = $db->selectField(
2747 'revision', 'rev_id',
2748 [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
2749 __METHOD__,
2750 [ 'ORDER BY' => 'rev_id DESC' ]
2751 );
2752 }
2753 return intval( $prevId );
2754 }
2755
2768 public function getTimestampFromId( $id, $flags = 0 ) {
2769 if ( $id instanceof Title ) {
2770 // Old deprecated calling convention supported for backwards compatibility
2771 $id = $flags;
2772 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2773 }
2774 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2775
2776 $timestamp =
2777 $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
2778
2779 return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
2780 }
2781
2791 public function countRevisionsByPageId( IDatabase $db, $id ) {
2792 $this->checkDatabaseDomain( $db );
2793
2794 $row = $db->selectRow( 'revision',
2795 [ 'revCount' => 'COUNT(*)' ],
2796 [ 'rev_page' => $id ],
2797 __METHOD__
2798 );
2799 if ( $row ) {
2800 return intval( $row->revCount );
2801 }
2802 return 0;
2803 }
2804
2814 public function countRevisionsByTitle( IDatabase $db, $title ) {
2815 $id = $title->getArticleID();
2816 if ( $id ) {
2817 return $this->countRevisionsByPageId( $db, $id );
2818 }
2819 return 0;
2820 }
2821
2840 public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2841 $this->checkDatabaseDomain( $db );
2842
2843 if ( !$userId ) {
2844 return false;
2845 }
2846
2847 $revQuery = $this->getQueryInfo();
2848 $res = $db->select(
2849 $revQuery['tables'],
2850 [
2851 'rev_user' => $revQuery['fields']['rev_user'],
2852 ],
2853 [
2854 'rev_page' => $pageId,
2855 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2856 ],
2857 __METHOD__,
2858 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2859 $revQuery['joins']
2860 );
2861 foreach ( $res as $row ) {
2862 if ( $row->rev_user != $userId ) {
2863 return false;
2864 }
2865 }
2866 return true;
2867 }
2868
2882 public function getKnownCurrentRevision( Title $title, $revId = 0 ) {
2883 $db = $this->getDBConnectionRef( DB_REPLICA );
2884
2885 $revIdPassed = $revId;
2886 $pageId = $title->getArticleID();
2887
2888 if ( !$pageId ) {
2889 return false;
2890 }
2891
2892 if ( !$revId ) {
2893 $revId = $title->getLatestRevID();
2894 }
2895
2896 if ( !$revId ) {
2897 wfWarn(
2898 'No latest revision known for page ' . $title->getPrefixedDBkey()
2899 . ' even though it exists with page ID ' . $pageId
2900 );
2901 return false;
2902 }
2903
2904 // Load the row from cache if possible. If not possible, populate the cache.
2905 // As a minor optimization, remember if this was a cache hit or miss.
2906 // We can sometimes avoid a database query later if this is a cache miss.
2907 $fromCache = true;
2908 $row = $this->cache->getWithSetCallback(
2909 // Page/rev IDs passed in from DB to reflect history merges
2910 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2911 WANObjectCache::TTL_WEEK,
2912 function ( $curValue, &$ttl, array &$setOpts ) use (
2913 $db, $revId, &$fromCache
2914 ) {
2915 $setOpts += Database::getCacheSetOptions( $db );
2916 $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2917 if ( $row ) {
2918 $fromCache = false;
2919 }
2920 return $row; // don't cache negatives
2921 }
2922 );
2923
2924 // Reflect revision deletion and user renames.
2925 if ( $row ) {
2926 $this->ensureRevisionRowMatchesTitle( $row, $title, [
2927 'from_cache_flag' => $fromCache,
2928 'page_id_initial' => $pageId,
2929 'rev_id_used' => $revId,
2930 'rev_id_requested' => $revIdPassed,
2931 ] );
2932
2933 return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2934 } else {
2935 return false;
2936 }
2937 }
2938
2947 public function getFirstRevision(
2949 int $flags = IDBAccessObject::READ_NORMAL
2950 ): ?RevisionRecord {
2951 $titleObj = Title::newFromLinkTarget( $title ); // TODO: eventually we shouldn't need a title
2952 return $this->newRevisionFromConds(
2953 [
2954 'page_namespace' => $title->getNamespace(),
2955 'page_title' => $title->getDBkey()
2956 ],
2957 $flags,
2958 $titleObj,
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() === null ) {
2996 throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
2997 }
2998 if ( $rev->getPageId() !== $pageId ) {
2999 throw new InvalidArgumentException(
3000 "Revision {$rev->getId()} 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( 'include_old', $options ) ) {
3030 $oldCmp = '>=';
3031 }
3032 if ( in_array( 'include_new', $options ) ) {
3033 $newCmp = '<=';
3034 }
3035 if ( in_array( '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()}) " .
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()}) " .
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( 'include_old', $options ) ||
3094 in_array( 'include_both', $options );
3095 $includeNew = in_array( 'include_new', $options ) ||
3096 in_array( '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() === $old->getId() ) {
3103 return $includeOld || $includeNew ? [ $new->getId() ] : [];
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 User $user = 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() === $old->getId() ) {
3171 if ( empty( $options ) ) {
3172 return [];
3173 } else {
3174 return $user ? [ $new->getUser( RevisionRecord::FOR_PUBLIC, $user ) ] : [ $new->getUser() ];
3175 }
3176 }
3177
3178 $dbr = $this->getDBConnectionRef( DB_REPLICA );
3179 $conds = array_merge(
3180 [
3181 'rev_page' => $pageId,
3182 $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3183 ],
3184 $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3185 );
3186
3187 $queryOpts = [ 'DISTINCT' ];
3188 if ( $max !== null ) {
3189 $queryOpts['LIMIT'] = $max + 1;
3190 }
3191
3192 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
3193 return array_map( function ( $row ) {
3194 return new UserIdentityValue( (int)$row->rev_user, $row->rev_user_text, (int)$row->rev_actor );
3195 }, iterator_to_array( $dbr->select(
3196 array_merge( [ 'revision' ], $actorQuery['tables'] ),
3197 $actorQuery['fields'],
3198 $conds, __METHOD__,
3199 $queryOpts,
3200 $actorQuery['joins']
3201 ) ) );
3202 }
3203
3225 public function countAuthorsBetween(
3226 $pageId,
3227 RevisionRecord $old = null,
3228 RevisionRecord $new = null,
3229 User $user = null,
3230 $max = null,
3231 $options = []
3232 ) {
3233 // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3234 // and creation of UserIdentity stuff.
3235 return count( $this->getAuthorsBetween( $pageId, $old, $new, $user, $max, $options ) );
3236 }
3237
3258 public function countRevisionsBetween(
3259 $pageId,
3260 RevisionRecord $old = null,
3261 RevisionRecord $new = null,
3262 $max = null,
3263 $options = []
3264 ) {
3265 $this->assertRevisionParameter( 'old', $pageId, $old );
3266 $this->assertRevisionParameter( 'new', $pageId, $new );
3267
3268 // No DB query needed if old and new are the same revision.
3269 // Can't check for consecutive revisions with 'getParentId' for a similar
3270 // optimization as edge cases exist when there are revisions between
3271 //a revision and it's parent. See T185167 for more details.
3272 if ( $old && $new && $new->getId() === $old->getId() ) {
3273 return 0;
3274 }
3275
3276 $dbr = $this->getDBConnectionRef( DB_REPLICA );
3277 $conds = array_merge(
3278 [
3279 'rev_page' => $pageId,
3280 $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
3281 ],
3282 $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3283 );
3284 if ( $max !== null ) {
3285 return $dbr->selectRowCount( 'revision', '1',
3286 $conds,
3287 __METHOD__,
3288 [ 'LIMIT' => $max + 1 ] // extra to detect truncation
3289 );
3290 } else {
3291 return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
3292 }
3293 }
3294
3295 // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3296}
3297
3302class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:85
This class handles the logic for the actor table migration and should always be used in lieu of direc...
CommentStoreComment represents a comment stored by CommentStore.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
A content handler knows how do deal with a specific type of content on a wiki page.
Helper class for DAO classes.
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...
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
setSize( $size)
Set nominal revision size, for optimization.
setSha1( $sha1)
Set revision hash, for optimization.
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Exception representing a failure to look up a revision.
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
getParentId()
Get parent revision ID (the original previous page revision).
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...
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
getVisibility()
Get the deletion bitfield of the revision.
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
getSha1()
Returns the base36 sha1 of this revision.
isMinor()
MCR migration note: this replaces Revision::isMinor.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Value object representing the set of slots belonging to a revision.
A RevisionRecord representing an existing revision persisted in the revision table.
Service for looking up page revisions.
constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title, $slotContents=null)
Factory method for SlotRecords based on known slot rows.
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.
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
IContentHandlerFactory $contentHandlerFactory
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
newRevisionFromArchiveRow( $row, $queryFlags=0, Title $title=null, array $overrides=[])
Make a fake revision object from an archive table row.
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
getFirstRevision(LinkTarget $title, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
getRevisionByTimestamp(LinkTarget $title, string $timestamp, int $flags=IDBAccessObject::READ_NORMAL)
Load the revision for the given title with the given timestamp.
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
loadSlotRecords( $revId, $queryFlags, Title $title)
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page's history.
loadSlotContent(SlotRecord $slot, $blobData=null, $blobFlags=null, $blobFormat=null, $queryFlags=0)
Loads a Content object based on a slot row.
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...
getAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, User $user=null, $max=null, $options=[])
Get the authors between the given revisions or revisions.
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.
newRevisionFromRow( $row, $queryFlags=0, Title $title=null, $fromCache=false)
newRevisionSlots( $revId, $revisionRow, $slotRows, $queryFlags, Title $title)
Factory method for RevisionSlots based on a revision ID.
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
countAuthorsBetween( $pageId, RevisionRecord $old=null, RevisionRecord $new=null, User $user=null, $max=null, $options=[])
Get the number of authors between the given revisions.
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.
assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev=null)
Asserts that if revision is provided, it's saved and belongs to the page with provided pageId.
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
getKnownCurrentRevision(Title $title, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, $fromCache=false)
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
getRelativeRevision(RevisionRecord $rev, $flags, $dir)
Implementation of getPreviousRevision and getNextRevision.
getSlotRowsForBatch( $rowsOrIds, array $options=[], $queryFlags=0)
Gets the slot rows associated with a batch of revisions.
loadRevisionFromPageId(IDatabase $db, $pageid, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
setLogger(LoggerInterface $logger)
loadRevisionFromConds(IDatabase $db, array $conditions, int $flags=IDBAccessObject::READ_NORMAL, Title $title=null, array $options=[])
Given a set of conditions, fetch a revision from the given database connection.
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) Caching rows w...
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.
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorMigration $actorMigration, IContentHandlerFactory $contentHandlerFactory, HookContainer $hookContainer, $dbDomain=false)
newRevisionFromConds(array $conditions, int $flags=IDBAccessObject::READ_NORMAL, Title $title=null, array $options=[])
Given a set of conditions, fetch a revision.
newMutableRevisionFromArray(array $fields, $queryFlags=0, Title $title=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1....
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
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.
getPreviousRevision(RevisionRecord $rev, $flags=0)
Get the revision before $rev in the page's history, if any.
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
getNextRevision(RevisionRecord $rev, $flags=0)
Get the revision after $rev in the page's history, if any.
newRevisionsFromBatch( $rows, array $options=[], $queryFlags=0, Title $title=null)
Construct a RevisionRecord instance for each row in $rows, and return them as an associative array in...
newRevisionFromArchiveRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, array $overrides=[])
ensureRevisionRowMatchesTitle( $row, Title $title, $context=[])
Check that the given row matches the given Title object.
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
getRole()
Returns the role of the slot.
hasAddress()
Whether this slot has an address.
getSha1()
Returns the content size.
getSize()
Returns the content size.
getAddress()
Returns the address of this slot's content.
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
getModel()
Returns the content model.
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
hasContentId()
Whether this slot has a content ID.
getContentId()
Returns the ID of the content meta data row associated with the slot.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Exception representing a failure to access a data blob.
Service for storing and loading Content objects.
Value object representing a user's identity.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:161
Utility class for creating new RC entries.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Represents a title within MediaWiki.
Definition Title.php:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2150
getId()
Get the user's ID.
Definition User.php:2121
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:616
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:50
Base interface for content objects.
Definition Content.php:35
Interface for database access objects.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
Service for constructing revision objects.
Service for looking up page revisions.
Service for loading and storing data blobs.
Definition BlobStore.php:35
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition BlobStore.php:66
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition BlobStore.php:42
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with.
Definition BlobStore.php:48
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with.
Definition BlobStore.php:54
const FORMAT_HINT
Hint key for use with storeBlob, indicating the serialization format used to create the blob,...
Definition BlobStore.php:84
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition BlobStore.php:60
const SHA1_HINT
Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the method.
Definition BlobStore.php:72
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob.
Definition BlobStore.php:78
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
unlock( $lockName, $method)
Release a lock.
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
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.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
delete( $table, $conds, $fname=__METHOD__)
Delete 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 type of the DBMS (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_MASTER
Definition defines.php:29
$content
Definition router.php:76