MediaWiki REL1_34
RevisionStore.php
Go to the documentation of this file.
1<?php
27namespace MediaWiki\Revision;
28
30use CommentStore;
32use Content;
35use Hooks;
37use InvalidArgumentException;
38use IP;
39use LogicException;
48use Message;
49use MWException;
51use Psr\Log\LoggerAwareInterface;
52use Psr\Log\LoggerInterface;
53use Psr\Log\NullLogger;
54use RecentChange;
55use Revision;
56use RuntimeException;
57use StatusValue;
58use stdClass;
59use Title;
60use Traversable;
61use User;
63use Wikimedia\Assert\Assert;
69
80 implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
81
82 const ROW_CACHE_KEY = 'revision-row-1.29';
83
87 private $blobStore;
88
92 private $dbDomain;
93
98 private $contentHandlerUseDB = true;
99
104
108 private $cache;
109
114
119
123 private $logger;
124
129
134
137
140
160 public function __construct(
170 $dbDomain = false
171 ) {
172 Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
173 Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
174 Assert::parameter(
176 '$mcrMigrationStage',
177 'Reading from the old and the new schema at the same time is not supported.'
178 );
179 Assert::parameter(
181 '$mcrMigrationStage',
182 'Reading needs to be enabled for the old or the new schema.'
183 );
184 Assert::parameter(
186 '$mcrMigrationStage',
187 'Writing needs to be enabled for the new schema.'
188 );
189 Assert::parameter(
192 '$mcrMigrationStage',
193 'Cannot read the old schema when not also writing it.'
194 );
195
196 $this->loadBalancer = $loadBalancer;
197 $this->blobStore = $blobStore;
198 $this->cache = $cache;
199 $this->commentStore = $commentStore;
200 $this->contentModelStore = $contentModelStore;
201 $this->slotRoleStore = $slotRoleStore;
202 $this->slotRoleRegistry = $slotRoleRegistry;
203 $this->mcrMigrationStage = $mcrMigrationStage;
204 $this->actorMigration = $actorMigration;
205 $this->dbDomain = $dbDomain;
206 $this->logger = new NullLogger();
207 }
208
214 private function hasMcrSchemaFlags( $flags ) {
215 return ( $this->mcrMigrationStage & $flags ) === $flags;
216 }
217
225 if ( $this->dbDomain !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
226 throw new RevisionAccessException(
227 "Cross-wiki content loading is not supported by the pre-MCR schema"
228 );
229 }
230 }
231
232 public function setLogger( LoggerInterface $logger ) {
233 $this->logger = $logger;
234 }
235
239 public function isReadOnly() {
240 return $this->blobStore->isReadOnly();
241 }
242
246 public function getContentHandlerUseDB() {
248 }
249
258 ) {
259 if ( !$contentHandlerUseDB ) {
260 throw new MWException(
261 'Content model must be stored in the database for multi content revision migration.'
262 );
263 }
264 }
265 $this->contentHandlerUseDB = $contentHandlerUseDB;
266 }
267
271 private function getDBLoadBalancer() {
272 return $this->loadBalancer;
273 }
274
280 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
281 list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
282 return $this->getDBConnectionRef( $mode );
283 }
284
291 private function getDBConnectionRef( $mode, $groups = [] ) {
292 $lb = $this->getDBLoadBalancer();
293 return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
294 }
295
310 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
311 if ( !$pageId && !$revId ) {
312 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
313 }
314
315 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
316 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
317 if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
318 $queryFlags = self::READ_NORMAL;
319 }
320
321 $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
322 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
323
324 // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
325 if ( $canUseTitleNewFromId ) {
326 $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
327 // TODO: better foreign title handling (introduce TitleFactory)
328 $title = Title::newFromID( $pageId, $titleFlags );
329 if ( $title ) {
330 return $title;
331 }
332 }
333
334 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
335 $canUseRevId = ( $revId !== null && $revId > 0 );
336
337 if ( $canUseRevId ) {
338 $dbr = $this->getDBConnectionRef( $dbMode );
339 // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
340 $row = $dbr->selectRow(
341 [ 'revision', 'page' ],
342 [
343 'page_namespace',
344 'page_title',
345 'page_id',
346 'page_latest',
347 'page_is_redirect',
348 'page_len',
349 ],
350 [ 'rev_id' => $revId ],
351 __METHOD__,
352 $dbOptions,
353 [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
354 );
355 if ( $row ) {
356 // TODO: better foreign title handling (introduce TitleFactory)
357 return Title::newFromRow( $row );
358 }
359 }
360
361 // If we still don't have a title, fallback to master if that wasn't already happening.
362 if ( $dbMode !== DB_MASTER ) {
363 $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
364 if ( $title ) {
365 $this->logger->info(
366 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
367 [ 'trace' => wfBacktrace() ]
368 );
369 return $title;
370 }
371 }
372
373 throw new RevisionAccessException(
374 "Could not determine title for page ID $pageId and revision ID $revId"
375 );
376 }
377
385 private function failOnNull( $value, $name ) {
386 if ( $value === null ) {
388 "$name must not be " . var_export( $value, true ) . "!"
389 );
390 }
391
392 return $value;
393 }
394
402 private function failOnEmpty( $value, $name ) {
403 if ( $value === null || $value === 0 || $value === '' ) {
405 "$name must not be " . var_export( $value, true ) . "!"
406 );
407 }
408
409 return $value;
410 }
411
424 public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
425 // TODO: pass in a DBTransactionContext instead of a database connection.
426 $this->checkDatabaseDomain( $dbw );
427
428 $slotRoles = $rev->getSlotRoles();
429
430 // Make sure the main slot is always provided throughout migration
431 if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
432 throw new InvalidArgumentException(
433 'main slot must be provided'
434 );
435 }
436
437 // If we are not writing into the new schema, we can't support extra slots.
439 && $slotRoles !== [ SlotRecord::MAIN ]
440 ) {
441 throw new InvalidArgumentException(
442 'Only the main slot is supported when not writing to the MCR enabled schema!'
443 );
444 }
445
446 // As long as we are not reading from the new schema, we don't want to write extra slots.
448 && $slotRoles !== [ SlotRecord::MAIN ]
449 ) {
450 throw new InvalidArgumentException(
451 'Only the main slot is supported when not reading from the MCR enabled schema!'
452 );
453 }
454
455 // Checks
456 $this->failOnNull( $rev->getSize(), 'size field' );
457 $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
458 $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
459 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
460 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
461 $this->failOnNull( $user->getId(), 'user field' );
462 $this->failOnEmpty( $user->getName(), 'user_text field' );
463
464 if ( !$rev->isReadyForInsertion() ) {
465 // This is here for future-proofing. At the time this check being added, it
466 // was redundant to the individual checks above.
467 throw new IncompleteRevisionException( 'Revision is incomplete' );
468 }
469
470 // TODO: we shouldn't need an actual Title here.
471 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
472 $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
473
474 $parentId = $rev->getParentId() === null
475 ? $this->getPreviousRevisionId( $dbw, $rev )
476 : $rev->getParentId();
477
479 $rev = $dbw->doAtomicSection(
480 __METHOD__,
481 function ( IDatabase $dbw, $fname ) use (
482 $rev,
483 $user,
484 $comment,
485 $title,
486 $pageId,
487 $parentId
488 ) {
489 return $this->insertRevisionInternal(
490 $rev,
491 $dbw,
492 $user,
493 $comment,
494 $title,
495 $pageId,
496 $parentId
497 );
498 }
499 );
500
501 // sanity checks
502 Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
503 Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
504 Assert::postcondition(
505 $rev->getComment( RevisionRecord::RAW ) !== null,
506 'revision must have a comment'
507 );
508 Assert::postcondition(
509 $rev->getUser( RevisionRecord::RAW ) !== null,
510 'revision must have a user'
511 );
512
513 // Trigger exception if the main slot is missing.
514 // Technically, this could go away after MCR migration: while
515 // calling code may require a main slot to exist, RevisionStore
516 // really should not know or care about that requirement.
518
519 foreach ( $slotRoles as $role ) {
520 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
521 Assert::postcondition(
522 $slot->getContent() !== null,
523 $role . ' slot must have content'
524 );
525 Assert::postcondition(
526 $slot->hasRevision(),
527 $role . ' slot must have a revision associated'
528 );
529 }
530
531 Hooks::run( 'RevisionRecordInserted', [ $rev ] );
532
533 // TODO: deprecate in 1.32!
534 $legacyRevision = new Revision( $rev );
535 Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
536
537 return $rev;
538 }
539
540 private function insertRevisionInternal(
541 RevisionRecord $rev,
542 IDatabase $dbw,
543 User $user,
544 CommentStoreComment $comment,
546 $pageId,
547 $parentId
548 ) {
549 $slotRoles = $rev->getSlotRoles();
550
551 $revisionRow = $this->insertRevisionRowOn(
552 $dbw,
553 $rev,
554 $title,
555 $parentId
556 );
557
558 $revisionId = $revisionRow['rev_id'];
559
560 $blobHints = [
561 BlobStore::PAGE_HINT => $pageId,
562 BlobStore::REVISION_HINT => $revisionId,
563 BlobStore::PARENT_HINT => $parentId,
564 ];
565
566 $newSlots = [];
567 foreach ( $slotRoles as $role ) {
568 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
569
570 // If the SlotRecord already has a revision ID set, this means it already exists
571 // in the database, and should already belong to the current revision.
572 // However, a slot may already have a revision, but no content ID, if the slot
573 // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
574 // mode, and the respective archive row was not yet migrated to the new schema.
575 // In that case, a new slot row (and content row) must be inserted even during
576 // undeletion.
577 if ( $slot->hasRevision() && $slot->hasContentId() ) {
578 // TODO: properly abort transaction if the assertion fails!
579 Assert::parameter(
580 $slot->getRevision() === $revisionId,
581 'slot role ' . $slot->getRole(),
582 'Existing slot should belong to revision '
583 . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
584 );
585
586 // Slot exists, nothing to do, move along.
587 // This happens when restoring archived revisions.
588
589 $newSlots[$role] = $slot;
590
591 // Write the main slot's text ID to the revision table for backwards compatibility
592 if ( $slot->getRole() === SlotRecord::MAIN
593 && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
594 ) {
595 $blobAddress = $slot->getAddress();
596 $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
597 }
598 } else {
599 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
600 }
601 }
602
603 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
604
605 $rev = new RevisionStoreRecord(
606 $title,
607 $user,
608 $comment,
609 (object)$revisionRow,
610 new RevisionSlots( $newSlots ),
611 $this->dbDomain
612 );
613
614 return $rev;
615 }
616
624 private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
625 $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
626 if ( !$textId ) {
627 throw new LogicException(
628 'Blob address not supported in 1.29 database schema: ' . $blobAddress
629 );
630 }
631
632 // getTextIdFromAddress() is free to insert something into the text table, so $textId
633 // may be a new value, not anything already contained in $blobAddress.
634 $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
635
636 $dbw->update(
637 'revision',
638 [ 'rev_text_id' => $textId ],
639 [ 'rev_id' => $revisionId ],
640 __METHOD__
641 );
642
643 return $textId;
644 }
645
654 private function insertSlotOn(
655 IDatabase $dbw,
656 $revisionId,
657 SlotRecord $protoSlot,
659 array $blobHints = []
660 ) {
661 if ( $protoSlot->hasAddress() ) {
662 $blobAddress = $protoSlot->getAddress();
663 } else {
664 $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
665 }
666
667 $contentId = null;
668
669 // Write the main slot's text ID to the revision table for backwards compatibility
670 if ( $protoSlot->getRole() === SlotRecord::MAIN
671 && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
672 ) {
673 // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
674 // with the real content ID below.
675 $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
676 $contentId = $this->emulateContentId( $textId );
677 }
678
680 if ( $protoSlot->hasContentId() ) {
681 $contentId = $protoSlot->getContentId();
682 } else {
683 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
684 }
685
686 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
687 }
688
689 $savedSlot = SlotRecord::newSaved(
690 $revisionId,
691 $contentId,
692 $blobAddress,
693 $protoSlot
694 );
695
696 return $savedSlot;
697 }
698
706 private function insertIpChangesRow(
707 IDatabase $dbw,
708 User $user,
709 RevisionRecord $rev,
710 $revisionId
711 ) {
712 if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
713 $ipcRow = [
714 'ipc_rev_id' => $revisionId,
715 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
716 'ipc_hex' => IP::toHex( $user->getName() ),
717 ];
718 $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
719 }
720 }
721
733 private function insertRevisionRowOn(
734 IDatabase $dbw,
735 RevisionRecord $rev,
737 $parentId
738 ) {
739 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
740
741 list( $commentFields, $commentCallback ) =
742 $this->commentStore->insertWithTempTable(
743 $dbw,
744 'rev_comment',
746 );
747 $revisionRow += $commentFields;
748
749 list( $actorFields, $actorCallback ) =
750 $this->actorMigration->getInsertValuesWithTempTable(
751 $dbw,
752 'rev_user',
754 );
755 $revisionRow += $actorFields;
756
757 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
758
759 if ( !isset( $revisionRow['rev_id'] ) ) {
760 // only if auto-increment was used
761 $revisionRow['rev_id'] = intval( $dbw->insertId() );
762
763 if ( $dbw->getType() === 'mysql' ) {
764 // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
765 // auto-increment value to disk, so on server restart it might reuse IDs from deleted
766 // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
767
768 $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
769 $table = 'archive';
771 $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
772 if ( $maxRevId2 >= $maxRevId ) {
773 $maxRevId = $maxRevId2;
774 $table = 'slots';
775 }
776 }
777
778 if ( $maxRevId >= $revisionRow['rev_id'] ) {
779 $this->logger->debug(
780 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
781 . ' Trying to fix it.',
782 [
783 'revid' => $revisionRow['rev_id'],
784 'table' => $table,
785 'maxrevid' => $maxRevId,
786 ]
787 );
788
789 if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
790 throw new MWException( 'Failed to get database lock for T202032' );
791 }
792 $fname = __METHOD__;
794 function ( $trigger, IDatabase $dbw ) use ( $fname ) {
795 $dbw->unlock( 'fix-for-T202032', $fname );
796 }
797 );
798
799 $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
800
801 // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
802 // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
803 // inserts too, though, at least on MariaDB 10.1.29.
804 //
805 // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
806 // transactions in this code path thanks to the row lock from the original ->insert() above.
807 //
808 // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
809 // that's for non-MySQL DBs.
810 $row1 = $dbw->query(
811 $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
812 )->fetchObject();
814 $row2 = $dbw->query(
815 $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
816 . ' FOR UPDATE'
817 )->fetchObject();
818 } else {
819 $row2 = null;
820 }
821 $maxRevId = max(
822 $maxRevId,
823 $row1 ? intval( $row1->v ) : 0,
824 $row2 ? intval( $row2->v ) : 0
825 );
826
827 // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
828 // transactions will throw a duplicate key error here. It doesn't seem worth trying
829 // to avoid that.
830 $revisionRow['rev_id'] = $maxRevId + 1;
831 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
832 }
833 }
834 }
835
836 $commentCallback( $revisionRow['rev_id'] );
837 $actorCallback( $revisionRow['rev_id'], $revisionRow );
838
839 return $revisionRow;
840 }
841
852 private function getBaseRevisionRow(
853 IDatabase $dbw,
854 RevisionRecord $rev,
856 $parentId
857 ) {
858 // Record the edit in revisions
859 $revisionRow = [
860 'rev_page' => $rev->getPageId(),
861 'rev_parent_id' => $parentId,
862 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
863 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
864 'rev_deleted' => $rev->getVisibility(),
865 'rev_len' => $rev->getSize(),
866 'rev_sha1' => $rev->getSha1(),
867 ];
868
869 if ( $rev->getId() !== null ) {
870 // Needed to restore revisions with their original ID
871 $revisionRow['rev_id'] = $rev->getId();
872 }
873
875 // In non MCR mode this IF section will relate to the main slot
876 $mainSlot = $rev->getSlot( SlotRecord::MAIN );
877 $model = $mainSlot->getModel();
878 $format = $mainSlot->getFormat();
879
880 // MCR migration note: rev_content_model and rev_content_format will go away
881 if ( $this->contentHandlerUseDB ) {
883
884 $defaultModel = ContentHandler::getDefaultModelFor( $title );
885 $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
886
887 $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
888 $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
889 }
890 }
891
892 return $revisionRow;
893 }
894
903 private function storeContentBlob(
904 SlotRecord $slot,
906 array $blobHints = []
907 ) {
908 $content = $slot->getContent();
909 $format = $content->getDefaultFormat();
910 $model = $content->getModel();
911
912 $this->checkContent( $content, $title, $slot->getRole() );
913
914 return $this->blobStore->storeBlob(
915 $content->serialize( $format ),
916 // These hints "leak" some information from the higher abstraction layer to
917 // low level storage to allow for optimization.
918 array_merge(
919 $blobHints,
920 [
921 BlobStore::DESIGNATION_HINT => 'page-content',
922 BlobStore::ROLE_HINT => $slot->getRole(),
923 BlobStore::SHA1_HINT => $slot->getSha1(),
924 BlobStore::MODEL_HINT => $model,
925 BlobStore::FORMAT_HINT => $format,
926 ]
927 )
928 );
929 }
930
937 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
938 $slotRow = [
939 'slot_revision_id' => $revisionId,
940 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
941 'slot_content_id' => $contentId,
942 // If the slot has a specific origin use that ID, otherwise use the ID of the revision
943 // that we just inserted.
944 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
945 ];
946 $dbw->insert( 'slots', $slotRow, __METHOD__ );
947 }
948
955 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
956 $contentRow = [
957 'content_size' => $slot->getSize(),
958 'content_sha1' => $slot->getSha1(),
959 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
960 'content_address' => $blobAddress,
961 ];
962 $dbw->insert( 'content', $contentRow, __METHOD__ );
963 return intval( $dbw->insertId() );
964 }
965
976 private function checkContent( Content $content, Title $title, $role ) {
977 // Note: may return null for revisions that have not yet been inserted
978
979 $model = $content->getModel();
980 $format = $content->getDefaultFormat();
981 $handler = $content->getContentHandler();
982
983 $name = "$title";
984
985 if ( !$handler->isSupportedFormat( $format ) ) {
986 throw new MWException( "Can't use format $format with content model $model on $name" );
987 }
988
989 if ( !$this->contentHandlerUseDB ) {
990 // if $wgContentHandlerUseDB is not set,
991 // all revisions must use the default content model and format.
992
994
995 $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
996 $defaultModel = $roleHandler->getDefaultModel( $title );
997 $defaultHandler = ContentHandler::getForModelID( $defaultModel );
998 $defaultFormat = $defaultHandler->getDefaultFormat();
999
1000 if ( $model != $defaultModel ) {
1001 throw new MWException( "Can't save non-default content model with "
1002 . "\$wgContentHandlerUseDB disabled: model is $model, "
1003 . "default for $name is $defaultModel"
1004 );
1005 }
1006
1007 if ( $format != $defaultFormat ) {
1008 throw new MWException( "Can't use non-default content format with "
1009 . "\$wgContentHandlerUseDB disabled: format is $format, "
1010 . "default for $name is $defaultFormat"
1011 );
1012 }
1013 }
1014
1015 if ( !$content->isValid() ) {
1016 throw new MWException(
1017 "New content for $name is not valid! Content model is $model"
1018 );
1019 }
1020 }
1021
1047 public function newNullRevision(
1048 IDatabase $dbw,
1049 Title $title,
1050 CommentStoreComment $comment,
1051 $minor,
1052 User $user
1053 ) {
1054 $this->checkDatabaseDomain( $dbw );
1055
1056 $pageId = $title->getArticleID();
1057
1058 // T51581: Lock the page table row to ensure no other process
1059 // is adding a revision to the page at the same time.
1060 // Avoid locking extra tables, compare T191892.
1061 $pageLatest = $dbw->selectField(
1062 'page',
1063 'page_latest',
1064 [ 'page_id' => $pageId ],
1065 __METHOD__,
1066 [ 'FOR UPDATE' ]
1067 );
1068
1069 if ( !$pageLatest ) {
1070 return null;
1071 }
1072
1073 // Fetch the actual revision row from master, without locking all extra tables.
1074 $oldRevision = $this->loadRevisionFromConds(
1075 $dbw,
1076 [ 'rev_id' => intval( $pageLatest ) ],
1077 self::READ_LATEST,
1078 $title
1079 );
1080
1081 if ( !$oldRevision ) {
1082 $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1083 $this->logger->error(
1084 $msg,
1085 [ 'exception' => new RuntimeException( $msg ) ]
1086 );
1087 return null;
1088 }
1089
1090 // Construct the new revision
1091 $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1092 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1093
1094 $newRevision->setComment( $comment );
1095 $newRevision->setUser( $user );
1096 $newRevision->setTimestamp( $timestamp );
1097 $newRevision->setMinorEdit( $minor );
1098
1099 return $newRevision;
1100 }
1101
1111 public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1112 $rc = $this->getRecentChange( $rev );
1113 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1114 return $rc->getAttribute( 'rc_id' );
1115 } else {
1116 return 0;
1117 }
1118 }
1119
1133 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1134 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1135 $db = $this->getDBConnectionRef( $dbType );
1136
1137 $userIdentity = $rev->getUser( RevisionRecord::RAW );
1138
1139 if ( !$userIdentity ) {
1140 // If the revision has no user identity, chances are it never went
1141 // into the database, and doesn't have an RC entry.
1142 return null;
1143 }
1144
1145 // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
1146 $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
1148 [
1149 $actorWhere['conds'],
1150 'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
1151 'rc_this_oldid' => $rev->getId()
1152 ],
1153 __METHOD__,
1154 $dbType
1155 );
1156
1157 // XXX: cache this locally? Glue it to the RevisionRecord?
1158 return $rc;
1159 }
1160
1168 private static function mapArchiveFields( $archiveRow ) {
1169 $fieldMap = [
1170 // keep with ar prefix:
1171 'ar_id' => 'ar_id',
1172
1173 // not the same suffix:
1174 'ar_page_id' => 'rev_page',
1175 'ar_rev_id' => 'rev_id',
1176
1177 // same suffix:
1178 'ar_text_id' => 'rev_text_id',
1179 'ar_timestamp' => 'rev_timestamp',
1180 'ar_user_text' => 'rev_user_text',
1181 'ar_user' => 'rev_user',
1182 'ar_actor' => 'rev_actor',
1183 'ar_minor_edit' => 'rev_minor_edit',
1184 'ar_deleted' => 'rev_deleted',
1185 'ar_len' => 'rev_len',
1186 'ar_parent_id' => 'rev_parent_id',
1187 'ar_sha1' => 'rev_sha1',
1188 'ar_comment' => 'rev_comment',
1189 'ar_comment_cid' => 'rev_comment_cid',
1190 'ar_comment_id' => 'rev_comment_id',
1191 'ar_comment_text' => 'rev_comment_text',
1192 'ar_comment_data' => 'rev_comment_data',
1193 'ar_comment_old' => 'rev_comment_old',
1194 'ar_content_format' => 'rev_content_format',
1195 'ar_content_model' => 'rev_content_model',
1196 ];
1197
1198 $revRow = new stdClass();
1199 foreach ( $fieldMap as $arKey => $revKey ) {
1200 if ( property_exists( $archiveRow, $arKey ) ) {
1201 $revRow->$revKey = $archiveRow->$arKey;
1202 }
1203 }
1204
1205 return $revRow;
1206 }
1207
1218 private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
1219 $mainSlotRow = new stdClass();
1220 $mainSlotRow->role_name = SlotRecord::MAIN;
1221 $mainSlotRow->model_name = null;
1222 $mainSlotRow->slot_revision_id = null;
1223 $mainSlotRow->slot_content_id = null;
1224 $mainSlotRow->content_address = null;
1225
1226 $content = null;
1227 $blobData = null;
1228 $blobFlags = null;
1229
1230 if ( is_object( $row ) ) {
1231 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1232 // Don't emulate from a row when using the new schema.
1233 // Emulating from an array is still OK.
1234 throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
1235 }
1236
1237 // archive row
1238 if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
1239 $row = $this->mapArchiveFields( $row );
1240 }
1241
1242 if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
1243 $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
1244 $row->rev_text_id
1245 );
1246 }
1247
1248 // This is used by null-revisions
1249 $mainSlotRow->slot_origin = isset( $row->slot_origin )
1250 ? intval( $row->slot_origin )
1251 : null;
1252
1253 if ( isset( $row->old_text ) ) {
1254 // this happens when the text-table gets joined directly, in the pre-1.30 schema
1255 $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
1256 // Check against selects that might have not included old_flags
1257 if ( !property_exists( $row, 'old_flags' ) ) {
1258 throw new InvalidArgumentException( 'old_flags was not set in $row' );
1259 }
1260 $blobFlags = $row->old_flags ?? '';
1261 }
1262
1263 $mainSlotRow->slot_revision_id = intval( $row->rev_id );
1264
1265 $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
1266 $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
1267 $mainSlotRow->model_name = isset( $row->rev_content_model )
1268 ? strval( $row->rev_content_model )
1269 : null;
1270 // XXX: in the future, we'll probably always use the default format, and drop content_format
1271 $mainSlotRow->format_name = isset( $row->rev_content_format )
1272 ? strval( $row->rev_content_format )
1273 : null;
1274
1275 if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
1276 // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1277 $mainSlotRow->slot_content_id
1278 = $this->emulateContentId( intval( $row->rev_text_id ) );
1279 }
1280 } elseif ( is_array( $row ) ) {
1281 $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
1282
1283 $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
1284 ? intval( $row['slot_origin'] )
1285 : null;
1286 $mainSlotRow->content_address = isset( $row['text_id'] )
1287 ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
1288 : null;
1289 $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
1290 $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
1291
1292 $mainSlotRow->model_name = isset( $row['content_model'] )
1293 ? strval( $row['content_model'] ) : null; // XXX: must be a string!
1294 // XXX: in the future, we'll probably always use the default format, and drop content_format
1295 $mainSlotRow->format_name = isset( $row['content_format'] )
1296 ? strval( $row['content_format'] ) : null;
1297 $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
1298 // XXX: If the flags field is not set then $blobFlags should be null so that no
1299 // decoding will happen. An empty string will result in default decodings.
1300 $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
1301
1302 // if we have a Content object, override mText and mContentModel
1303 if ( !empty( $row['content'] ) ) {
1304 if ( !( $row['content'] instanceof Content ) ) {
1305 throw new MWException( 'content field must contain a Content object.' );
1306 }
1307
1309 $content = $row['content'];
1310 $handler = $content->getContentHandler();
1311
1312 $mainSlotRow->model_name = $content->getModel();
1313
1314 // XXX: in the future, we'll probably always use the default format.
1315 if ( $mainSlotRow->format_name === null ) {
1316 $mainSlotRow->format_name = $handler->getDefaultFormat();
1317 }
1318 }
1319
1320 if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
1321 // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1322 $mainSlotRow->slot_content_id
1323 = $this->emulateContentId( intval( $row['text_id'] ) );
1324 }
1325 } else {
1326 throw new MWException( 'Revision constructor passed invalid row format.' );
1327 }
1328
1329 // With the old schema, the content changes with every revision,
1330 // except for null-revisions.
1331 if ( !isset( $mainSlotRow->slot_origin ) ) {
1332 $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
1333 }
1334
1335 if ( $mainSlotRow->model_name === null ) {
1336 $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
1338
1339 return $this->slotRoleRegistry->getRoleHandler( $slot->getRole() )
1340 ->getDefaultModel( $title );
1341 };
1342 }
1343
1344 if ( !$content ) {
1345 // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
1346 // is missing, but "empty revisions" with no content are used in some edge cases.
1347
1348 $content = function ( SlotRecord $slot )
1349 use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
1350 {
1351 return $this->loadSlotContent(
1352 $slot,
1353 $blobData,
1354 $blobFlags,
1355 $mainSlotRow->format_name,
1356 $queryFlags
1357 );
1358 };
1359 }
1360
1362 // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
1363 // the inherited slot to have the same content_id as the original slot. In that case,
1364 // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
1365 $mainSlotRow->slot_content_id =
1366 function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
1367 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1368 return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
1369 };
1370 }
1371
1372 return new SlotRecord( $mainSlotRow, $content );
1373 }
1374
1386 private function emulateContentId( $textId ) {
1387 // Return a negative number to ensure the ID is distinct from any real content IDs
1388 // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
1389 // mode.
1390 return -$textId;
1391 }
1392
1412 private function loadSlotContent(
1413 SlotRecord $slot,
1414 $blobData = null,
1415 $blobFlags = null,
1416 $blobFormat = null,
1417 $queryFlags = 0
1418 ) {
1419 if ( $blobData !== null ) {
1420 Assert::parameterType( 'string', $blobData, '$blobData' );
1421 Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1422
1423 $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1424
1425 if ( $blobFlags === null ) {
1426 // No blob flags, so use the blob verbatim.
1427 $data = $blobData;
1428 } else {
1429 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1430 if ( $data === false ) {
1431 throw new RevisionAccessException(
1432 "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1433 );
1434 }
1435 }
1436
1437 } else {
1438 $address = $slot->getAddress();
1439 try {
1440 $data = $this->blobStore->getBlob( $address, $queryFlags );
1441 } catch ( BlobAccessException $e ) {
1442 throw new RevisionAccessException(
1443 "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1444 );
1445 }
1446 }
1447
1448 // Unserialize content
1449 $handler = ContentHandler::getForModelID( $slot->getModel() );
1450
1451 $content = $handler->unserializeContent( $data, $blobFormat );
1452 return $content;
1453 }
1454
1469 public function getRevisionById( $id, $flags = 0 ) {
1470 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1471 }
1472
1489 public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1490 // TODO should not require Title in future (T206498)
1491 $title = Title::newFromLinkTarget( $linkTarget );
1492 $conds = [
1493 'page_namespace' => $title->getNamespace(),
1494 'page_title' => $title->getDBkey()
1495 ];
1496 if ( $revId ) {
1497 // Use the specified revision ID.
1498 // Note that we use newRevisionFromConds here because we want to retry
1499 // and fall back to master if the page is not found on a replica.
1500 // Since the caller supplied a revision ID, we are pretty sure the revision is
1501 // supposed to exist, so we should try hard to find it.
1502 $conds['rev_id'] = $revId;
1503 return $this->newRevisionFromConds( $conds, $flags, $title );
1504 } else {
1505 // Use a join to get the latest revision.
1506 // Note that we don't use newRevisionFromConds here because we don't want to retry
1507 // and fall back to master. The assumption is that we only want to force the fallback
1508 // if we are quite sure the revision exists because the caller supplied a revision ID.
1509 // If the page isn't found at all on a replica, it probably simply does not exist.
1510 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1511
1512 $conds[] = 'rev_id=page_latest';
1513 $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
1514
1515 return $rev;
1516 }
1517 }
1518
1535 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1536 $conds = [ 'page_id' => $pageId ];
1537 if ( $revId ) {
1538 // Use the specified revision ID.
1539 // Note that we use newRevisionFromConds here because we want to retry
1540 // and fall back to master if the page is not found on a replica.
1541 // Since the caller supplied a revision ID, we are pretty sure the revision is
1542 // supposed to exist, so we should try hard to find it.
1543 $conds['rev_id'] = $revId;
1544 return $this->newRevisionFromConds( $conds, $flags );
1545 } else {
1546 // Use a join to get the latest revision.
1547 // Note that we don't use newRevisionFromConds here because we don't want to retry
1548 // and fall back to master. The assumption is that we only want to force the fallback
1549 // if we are quite sure the revision exists because the caller supplied a revision ID.
1550 // If the page isn't found at all on a replica, it probably simply does not exist.
1551 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1552
1553 $conds[] = 'rev_id=page_latest';
1554 $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1555
1556 return $rev;
1557 }
1558 }
1559
1571 public function getRevisionByTimestamp( $title, $timestamp ) {
1572 $db = $this->getDBConnectionRef( DB_REPLICA );
1573 return $this->newRevisionFromConds(
1574 [
1575 'rev_timestamp' => $db->timestamp( $timestamp ),
1576 'page_namespace' => $title->getNamespace(),
1577 'page_title' => $title->getDBkey()
1578 ],
1579 0,
1580 $title
1581 );
1582 }
1583
1591 private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
1592 $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1593
1594 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1595 $db = $this->getDBConnectionRef( $dbMode );
1596
1597 $res = $db->select(
1598 $revQuery['tables'],
1599 $revQuery['fields'],
1600 [
1601 'slot_revision_id' => $revId,
1602 ],
1603 __METHOD__,
1604 $dbOptions,
1605 $revQuery['joins']
1606 );
1607
1608 $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
1609
1610 return $slots;
1611 }
1612
1625 private function constructSlotRecords(
1626 $revId,
1627 $slotRows,
1628 $queryFlags,
1629 Title $title,
1630 $slotContents = null
1631 ) {
1632 $slots = [];
1633
1634 foreach ( $slotRows as $row ) {
1635 // Resolve role names and model names from in-memory cache, if they were not joined in.
1636 if ( !isset( $row->role_name ) ) {
1637 $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1638 }
1639
1640 if ( !isset( $row->model_name ) ) {
1641 if ( isset( $row->content_model ) ) {
1642 $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1643 } else {
1644 // We may get here if $row->model_name is set but null, perhaps because it
1645 // came from rev_content_model, which is NULL for the default model.
1646 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1647 $row->model_name = $slotRoleHandler->getDefaultModel( $title );
1648 }
1649 }
1650
1651 if ( !isset( $row->content_id ) && isset( $row->rev_text_id ) ) {
1652 $row->slot_content_id
1653 = $this->emulateContentId( intval( $row->rev_text_id ) );
1654 }
1655
1656 // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1657 if ( isset( $row->blob_data ) ) {
1658 $slotContents[$row->content_address] = $row->blob_data;
1659 }
1660
1661 $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1662 $blob = null;
1663 if ( isset( $slotContents[$slot->getAddress()] ) ) {
1664 $blob = $slotContents[$slot->getAddress()];
1665 if ( $blob instanceof Content ) {
1666 return $blob;
1667 }
1668 }
1669 return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1670 };
1671
1672 $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1673 }
1674
1675 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1676 throw new RevisionAccessException(
1677 'Main slot of revision ' . $revId . ' not found in database!'
1678 );
1679 }
1680
1681 return $slots;
1682 }
1683
1699 private function newRevisionSlots(
1700 $revId,
1701 $revisionRow,
1702 $slotRows,
1703 $queryFlags,
1705 ) {
1706 if ( $slotRows ) {
1707 $slots = new RevisionSlots(
1708 $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
1709 );
1710 } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1711 $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
1712 // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive
1713 $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
1714 } else {
1715 // XXX: do we need the same kind of caching here
1716 // that getKnownCurrentRevision uses (if $revId == page_latest?)
1717
1718 $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
1719 return $this->loadSlotRecords( $revId, $queryFlags, $title );
1720 } );
1721 }
1722
1723 return $slots;
1724 }
1725
1744 $row,
1745 $queryFlags = 0,
1746 Title $title = null,
1747 array $overrides = []
1748 ) {
1749 Assert::parameterType( 'object', $row, '$row' );
1750
1751 // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1752 Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1753
1754 if ( !$title && isset( $overrides['title'] ) ) {
1755 if ( !( $overrides['title'] instanceof Title ) ) {
1756 throw new MWException( 'title field override must contain a Title object.' );
1757 }
1758
1759 $title = $overrides['title'];
1760 }
1761
1762 if ( !isset( $title ) ) {
1763 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1764 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1765 } else {
1766 throw new InvalidArgumentException(
1767 'A Title or ar_namespace and ar_title must be given'
1768 );
1769 }
1770 }
1771
1772 foreach ( $overrides as $key => $value ) {
1773 $field = "ar_$key";
1774 $row->$field = $value;
1775 }
1776
1777 try {
1778 $user = User::newFromAnyId(
1779 $row->ar_user ?? null,
1780 $row->ar_user_text ?? null,
1781 $row->ar_actor ?? null,
1782 $this->dbDomain
1783 );
1784 } catch ( InvalidArgumentException $ex ) {
1785 wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1786 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1787 }
1788
1789 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1790 // Legacy because $row may have come from self::selectFields()
1791 $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1792
1793 $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, null, $queryFlags, $title );
1794
1795 return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
1796 }
1797
1810 public function newRevisionFromRow(
1811 $row,
1812 $queryFlags = 0,
1813 Title $title = null,
1814 $fromCache = false
1815 ) {
1816 return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
1817 }
1818
1837 $row,
1838 $slots,
1839 $queryFlags = 0,
1840 Title $title = null,
1841 $fromCache = false
1842 ) {
1843 Assert::parameterType( 'object', $row, '$row' );
1844
1845 if ( !$title ) {
1846 $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
1847 $revId = $row->rev_id ?? 0;
1848
1849 $title = $this->getTitle( $pageId, $revId, $queryFlags );
1850 }
1851
1852 if ( !isset( $row->page_latest ) ) {
1853 $row->page_latest = $title->getLatestRevID();
1854 if ( $row->page_latest === 0 && $title->exists() ) {
1855 wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1856 }
1857 }
1858
1859 try {
1860 $user = User::newFromAnyId(
1861 $row->rev_user ?? null,
1862 $row->rev_user_text ?? null,
1863 $row->rev_actor ?? null,
1864 $this->dbDomain
1865 );
1866 } catch ( InvalidArgumentException $ex ) {
1867 wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1868 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1869 }
1870
1871 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1872 // Legacy because $row may have come from self::selectFields()
1873 $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1874
1875 if ( !( $slots instanceof RevisionSlots ) ) {
1876 $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
1877 }
1878
1879 // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
1880 if ( $fromCache ) {
1881 $rev = new RevisionStoreCacheRecord(
1882 function ( $revId ) use ( $queryFlags ) {
1883 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1884 return $this->fetchRevisionRowFromConds(
1885 $db,
1886 [ 'rev_id' => intval( $revId ) ]
1887 );
1888 },
1889 $title, $user, $comment, $row, $slots, $this->dbDomain
1890 );
1891 } else {
1892 $rev = new RevisionStoreRecord(
1893 $title, $user, $comment, $row, $slots, $this->dbDomain );
1894 }
1895 return $rev;
1896 }
1897
1918 public function newRevisionsFromBatch(
1919 $rows,
1920 array $options = [],
1921 $queryFlags = 0,
1922 Title $title = null
1923 ) {
1924 $result = new StatusValue();
1925
1926 $rowsByRevId = [];
1927 $pageIdsToFetchTitles = [];
1928 $titlesByPageId = [];
1929 foreach ( $rows as $row ) {
1930 if ( isset( $rowsByRevId[$row->rev_id] ) ) {
1931 $result->warning(
1932 'internalerror',
1933 "Duplicate rows in newRevisionsFromBatch, rev_id {$row->rev_id}"
1934 );
1935 }
1936 if ( $title && $row->rev_page != $title->getArticleID() ) {
1937 throw new InvalidArgumentException(
1938 "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}"
1939 );
1940 } elseif ( !$title && !isset( $titlesByPageId[ $row->rev_page ] ) ) {
1941 if ( isset( $row->page_namespace ) && isset( $row->page_title ) &&
1942 // This should not happen, but just in case we don't have a page_id
1943 // set or it doesn't match rev_page, let's fetch the title again.
1944 isset( $row->page_id ) && $row->rev_page === $row->page_id
1945 ) {
1946 $titlesByPageId[ $row->rev_page ] = Title::newFromRow( $row );
1947 } else {
1948 $pageIdsToFetchTitles[] = $row->rev_page;
1949 }
1950 }
1951 $rowsByRevId[$row->rev_id] = $row;
1952 }
1953
1954 if ( empty( $rowsByRevId ) ) {
1955 $result->setResult( true, [] );
1956 return $result;
1957 }
1958
1959 // If the title is not supplied, batch-fetch Title objects.
1960 if ( $title ) {
1961 $titlesByPageId[$title->getArticleID()] = $title;
1962 } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1963 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1964 foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
1965 $titlesByPageId[$t->getArticleID()] = $t;
1966 }
1967 }
1968
1969 if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
1970 $result->setResult( true,
1971 array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) {
1972 try {
1973 return $this->newRevisionFromRow(
1974 $row,
1975 $queryFlags,
1976 $titlesByPageId[$row->rev_page]
1977 );
1978 } catch ( MWException $e ) {
1979 $result->warning( 'internalerror', $e->getMessage() );
1980 return null;
1981 }
1982 }, $rowsByRevId )
1983 );
1984 return $result;
1985 }
1986
1987 $slotRowOptions = [
1988 'slots' => $options['slots'] ?? true,
1989 'blobs' => $options['content'] ?? false,
1990 ];
1991
1992 if ( is_array( $slotRowOptions['slots'] )
1993 && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
1994 ) {
1995 // Make sure the main slot is always loaded, RevisionRecord requires this.
1996 $slotRowOptions['slots'][] = SlotRecord::MAIN;
1997 }
1998
1999 $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2000
2001 $result->merge( $slotRowsStatus );
2002 $slotRowsByRevId = $slotRowsStatus->getValue();
2003
2004 $result->setResult( true, array_map( function ( $row ) use
2005 ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $result ) {
2006 if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) {
2007 $result->warning(
2008 'internalerror',
2009 "Couldn't find slots for rev {$row->rev_id}"
2010 );
2011 return null;
2012 }
2013 try {
2014 return $this->newRevisionFromRowAndSlots(
2015 $row,
2016 new RevisionSlots(
2017 $this->constructSlotRecords(
2018 $row->rev_id,
2019 $slotRowsByRevId[$row->rev_id],
2020 $queryFlags,
2021 $titlesByPageId[$row->rev_page]
2022 )
2023 ),
2024 $queryFlags,
2025 $titlesByPageId[$row->rev_page]
2026 );
2027 } catch ( MWException $e ) {
2028 $result->warning( 'internalerror', $e->getMessage() );
2029 return null;
2030 }
2031 }, $rowsByRevId ) );
2032 return $result;
2033 }
2034
2057 private function getSlotRowsForBatch(
2058 $rowsOrIds,
2059 array $options = [],
2060 $queryFlags = 0
2061 ) {
2062 $readNew = $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW );
2063 $result = new StatusValue();
2064
2065 $revIds = [];
2066 foreach ( $rowsOrIds as $row ) {
2067 $revIds[] = is_object( $row ) ? (int)$row->rev_id : (int)$row;
2068 }
2069
2070 // Nothing to do.
2071 // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2072 if ( empty( $revIds ) ) {
2073 $result->setResult( true, [] );
2074 return $result;
2075 }
2076
2077 // We need to set the `content` flag to join in content meta-data
2078 $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
2079 $revIdField = $slotQueryInfo['keys']['rev_id'];
2080 $slotQueryConds = [ $revIdField => $revIds ];
2081
2082 if ( $readNew && isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2083 if ( empty( $options['slots'] ) ) {
2084 // Degenerate case: return no slots for each revision.
2085 $result->setResult( true, array_fill_keys( $revIds, [] ) );
2086 return $result;
2087 }
2088
2089 $roleIdField = $slotQueryInfo['keys']['role_id'];
2090 $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
2091 return $this->slotRoleStore->getId( $slot_name );
2092 }, $options['slots'] );
2093 }
2094
2095 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2096 $slotRows = $db->select(
2097 $slotQueryInfo['tables'],
2098 $slotQueryInfo['fields'],
2099 $slotQueryConds,
2100 __METHOD__,
2101 [],
2102 $slotQueryInfo['joins']
2103 );
2104
2105 $slotContents = null;
2106 if ( $options['blobs'] ?? false ) {
2107 $blobAddresses = [];
2108 foreach ( $slotRows as $slotRow ) {
2109 $blobAddresses[] = $slotRow->content_address;
2110 }
2111 $slotContentFetchStatus = $this->blobStore
2112 ->getBlobBatch( $blobAddresses, $queryFlags );
2113 foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2114 $result->warning( $error['message'], ...$error['params'] );
2115 }
2116 $slotContents = $slotContentFetchStatus->getValue();
2117 }
2118
2119 $slotRowsByRevId = [];
2120 foreach ( $slotRows as $slotRow ) {
2121 if ( $slotContents === null ) {
2122 // nothing to do
2123 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2124 $slotRow->blob_data = $slotContents[$slotRow->content_address];
2125 } else {
2126 $result->warning(
2127 'internalerror',
2128 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2129 );
2130 $slotRow->blob_data = null;
2131 }
2132
2133 // conditional needed for SCHEMA_COMPAT_READ_OLD
2134 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2135 $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2136 }
2137
2138 // conditional needed for SCHEMA_COMPAT_READ_OLD
2139 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2140 $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2141 }
2142
2143 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2144 }
2145
2146 $result->setResult( true, $slotRowsByRevId );
2147 return $result;
2148 }
2149
2171 $rowsOrIds,
2172 $slots = null,
2173 $queryFlags = 0
2174 ) {
2175 $result = $this->getSlotRowsForBatch(
2176 $rowsOrIds,
2177 [ 'slots' => $slots, 'blobs' => true ],
2178 $queryFlags
2179 );
2180
2181 if ( $result->isOK() ) {
2182 // strip out all internal meta data that we don't want to expose
2183 foreach ( $result->value as $revId => $rowsByRole ) {
2184 foreach ( $rowsByRole as $role => $slotRow ) {
2185 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2186 // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2187 // if we didn't ask for it.
2188 unset( $result->value[$revId][$role] );
2189 continue;
2190 }
2191
2192 $result->value[$revId][$role] = (object)[
2193 'blob_data' => $slotRow->blob_data,
2194 'model_name' => $slotRow->model_name,
2195 ];
2196 }
2197 }
2198 }
2199
2200 return $result;
2201 }
2202
2218 array $fields,
2219 $queryFlags = 0,
2220 Title $title = null
2221 ) {
2222 if ( !$title && isset( $fields['title'] ) ) {
2223 if ( !( $fields['title'] instanceof Title ) ) {
2224 throw new MWException( 'title field must contain a Title object.' );
2225 }
2226
2227 $title = $fields['title'];
2228 }
2229
2230 if ( !$title ) {
2231 $pageId = $fields['page'] ?? 0;
2232 $revId = $fields['id'] ?? 0;
2233
2234 $title = $this->getTitle( $pageId, $revId, $queryFlags );
2235 }
2236
2237 if ( !isset( $fields['page'] ) ) {
2238 $fields['page'] = $title->getArticleID( $queryFlags );
2239 }
2240
2241 // if we have a content object, use it to set the model and type
2242 if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
2243 && !is_array( $fields['content'] )
2244 ) {
2245 throw new MWException(
2246 'content field must contain a Content object or an array of Content objects.'
2247 );
2248 }
2249
2250 if ( !empty( $fields['text_id'] ) ) {
2251 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2252 throw new MWException( "The text_id field is only available in the pre-MCR schema" );
2253 }
2254
2255 if ( !empty( $fields['content'] ) ) {
2256 throw new MWException(
2257 "Text already stored in external store (id {$fields['text_id']}), " .
2258 "can't specify content object"
2259 );
2260 }
2261 }
2262
2263 if (
2264 isset( $fields['comment'] )
2265 && !( $fields['comment'] instanceof CommentStoreComment )
2266 ) {
2267 $commentData = $fields['comment_data'] ?? null;
2268
2269 if ( $fields['comment'] instanceof Message ) {
2270 $fields['comment'] = CommentStoreComment::newUnsavedComment(
2271 $fields['comment'],
2272 $commentData
2273 );
2274 } else {
2275 $commentText = trim( strval( $fields['comment'] ) );
2276 $fields['comment'] = CommentStoreComment::newUnsavedComment(
2277 $commentText,
2278 $commentData
2279 );
2280 }
2281 }
2282
2283 $revision = new MutableRevisionRecord( $title, $this->dbDomain );
2284 $this->initializeMutableRevisionFromArray( $revision, $fields );
2285
2286 if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
2287 // @phan-suppress-next-line PhanTypeNoPropertiesForeach
2288 foreach ( $fields['content'] as $role => $content ) {
2289 $revision->setContent( $role, $content );
2290 }
2291 } else {
2292 $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
2293 $revision->setSlot( $mainSlot );
2294 }
2295
2296 return $revision;
2297 }
2298
2304 MutableRevisionRecord $record,
2305 array $fields
2306 ) {
2308 $user = null;
2309
2310 // If a user is passed in, use it if possible. We cannot use a user from a
2311 // remote wiki with unsuppressed ids, due to issues described in T222212.
2312 if ( isset( $fields['user'] ) &&
2313 ( $fields['user'] instanceof UserIdentity ) &&
2314 ( $this->dbDomain === false ||
2315 ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
2316 ) {
2317 $user = $fields['user'];
2318 } else {
2319 try {
2320 $user = User::newFromAnyId(
2321 $fields['user'] ?? null,
2322 $fields['user_text'] ?? null,
2323 $fields['actor'] ?? null,
2324 $this->dbDomain
2325 );
2326 } catch ( InvalidArgumentException $ex ) {
2327 $user = null;
2328 }
2329 }
2330
2331 if ( $user ) {
2332 $record->setUser( $user );
2333 }
2334
2335 $timestamp = isset( $fields['timestamp'] )
2336 ? strval( $fields['timestamp'] )
2337 : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
2338
2339 $record->setTimestamp( $timestamp );
2340
2341 if ( isset( $fields['page'] ) ) {
2342 $record->setPageId( intval( $fields['page'] ) );
2343 }
2344
2345 if ( isset( $fields['id'] ) ) {
2346 $record->setId( intval( $fields['id'] ) );
2347 }
2348 if ( isset( $fields['parent_id'] ) ) {
2349 $record->setParentId( intval( $fields['parent_id'] ) );
2350 }
2351
2352 if ( isset( $fields['sha1'] ) ) {
2353 $record->setSha1( $fields['sha1'] );
2354 }
2355 if ( isset( $fields['size'] ) ) {
2356 $record->setSize( intval( $fields['size'] ) );
2357 }
2358
2359 if ( isset( $fields['minor_edit'] ) ) {
2360 $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
2361 }
2362 if ( isset( $fields['deleted'] ) ) {
2363 $record->setVisibility( intval( $fields['deleted'] ) );
2364 }
2365
2366 if ( isset( $fields['comment'] ) ) {
2367 Assert::parameterType(
2368 CommentStoreComment::class,
2369 $fields['comment'],
2370 '$row[\'comment\']'
2371 );
2372 $record->setComment( $fields['comment'] );
2373 }
2374 }
2375
2390 public function loadRevisionFromId( IDatabase $db, $id ) {
2391 return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
2392 }
2393
2409 public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
2410 $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
2411 if ( $id ) {
2412 $conds['rev_id'] = intval( $id );
2413 } else {
2414 $conds[] = 'rev_id=page_latest';
2415 }
2416 return $this->loadRevisionFromConds( $db, $conds );
2417 }
2418
2435 public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2436 if ( $id ) {
2437 $matchId = intval( $id );
2438 } else {
2439 $matchId = 'page_latest';
2440 }
2441
2442 return $this->loadRevisionFromConds(
2443 $db,
2444 [
2445 "rev_id=$matchId",
2446 'page_namespace' => $title->getNamespace(),
2447 'page_title' => $title->getDBkey()
2448 ],
2449 0,
2450 $title
2451 );
2452 }
2453
2469 public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2470 return $this->loadRevisionFromConds( $db,
2471 [
2472 'rev_timestamp' => $db->timestamp( $timestamp ),
2473 'page_namespace' => $title->getNamespace(),
2474 'page_title' => $title->getDBkey()
2475 ],
2476 0,
2477 $title
2478 );
2479 }
2480
2496 private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
2497 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2498 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
2499
2500 $lb = $this->getDBLoadBalancer();
2501
2502 // Make sure new pending/committed revision are visibile later on
2503 // within web requests to certain avoid bugs like T93866 and T94407.
2504 if ( !$rev
2505 && !( $flags & self::READ_LATEST )
2506 && $lb->hasStreamingReplicaServers()
2507 && $lb->hasOrMadeRecentMasterChanges()
2508 ) {
2509 $flags = self::READ_LATEST;
2510 $dbw = $this->getDBConnectionRef( DB_MASTER );
2511 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
2512 }
2513
2514 return $rev;
2515 }
2516
2530 private function loadRevisionFromConds(
2531 IDatabase $db,
2532 $conditions,
2533 $flags = 0,
2534 Title $title = null
2535 ) {
2536 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
2537 if ( $row ) {
2538 $rev = $this->newRevisionFromRow( $row, $flags, $title );
2539
2540 return $rev;
2541 }
2542
2543 return null;
2544 }
2545
2553 private function checkDatabaseDomain( IDatabase $db ) {
2554 $dbDomain = $db->getDomainID();
2555 $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2556 if ( $dbDomain === $storeDomain ) {
2557 return;
2558 }
2559
2560 throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2561 }
2562
2575 private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
2576 $this->checkDatabaseDomain( $db );
2577
2578 $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2579 $options = [];
2580 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2581 $options[] = 'FOR UPDATE';
2582 }
2583 return $db->selectRow(
2584 $revQuery['tables'],
2585 $revQuery['fields'],
2586 $conditions,
2587 __METHOD__,
2588 $options,
2589 $revQuery['joins']
2590 );
2591 }
2592
2607 private function findSlotContentId( IDatabase $db, $revId, $role ) {
2608 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
2609 return null;
2610 }
2611
2612 try {
2613 $roleId = $this->slotRoleStore->getId( $role );
2614 $conditions = [
2615 'slot_revision_id' => $revId,
2616 'slot_role_id' => $roleId,
2617 ];
2618
2619 $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
2620
2621 return $contentId ?: null;
2622 } catch ( NameTableAccessException $ex ) {
2623 // If the role is missing from the slot_roles table,
2624 // the corresponding row in slots cannot exist.
2625 return null;
2626 }
2627 }
2628
2653 public function getQueryInfo( $options = [] ) {
2654 $ret = [
2655 'tables' => [],
2656 'fields' => [],
2657 'joins' => [],
2658 ];
2659
2660 $ret['tables'][] = 'revision';
2661 $ret['fields'] = array_merge( $ret['fields'], [
2662 'rev_id',
2663 'rev_page',
2664 'rev_timestamp',
2665 'rev_minor_edit',
2666 'rev_deleted',
2667 'rev_len',
2668 'rev_parent_id',
2669 'rev_sha1',
2670 ] );
2671
2672 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2673 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2674 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2675 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2676
2677 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2678 $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2679 $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2680 $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2681
2682 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2683 $ret['fields'][] = 'rev_text_id';
2684
2685 if ( $this->contentHandlerUseDB ) {
2686 $ret['fields'][] = 'rev_content_format';
2687 $ret['fields'][] = 'rev_content_model';
2688 }
2689 }
2690
2691 if ( in_array( 'page', $options, true ) ) {
2692 $ret['tables'][] = 'page';
2693 $ret['fields'] = array_merge( $ret['fields'], [
2694 'page_namespace',
2695 'page_title',
2696 'page_id',
2697 'page_latest',
2698 'page_is_redirect',
2699 'page_len',
2700 ] );
2701 $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2702 }
2703
2704 if ( in_array( 'user', $options, true ) ) {
2705 $ret['tables'][] = 'user';
2706 $ret['fields'] = array_merge( $ret['fields'], [
2707 'user_name',
2708 ] );
2709 $u = $actorQuery['fields']['rev_user'];
2710 $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2711 }
2712
2713 if ( in_array( 'text', $options, true ) ) {
2714 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
2715 throw new InvalidArgumentException( 'text table can no longer be joined directly' );
2716 } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2717 // NOTE: even when this class is set to not read from the old schema, callers
2718 // should still be able to join against the text table, as long as we are still
2719 // writing the old schema for compatibility.
2720 // TODO: This should trigger a deprecation warning eventually (T200918), but not
2721 // before all known usages are removed (see T198341 and T201164).
2722 // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
2723 }
2724
2725 $ret['tables'][] = 'text';
2726 $ret['fields'] = array_merge( $ret['fields'], [
2727 'old_text',
2728 'old_flags'
2729 ] );
2730 $ret['joins']['text'] = [ 'JOIN', [ 'rev_text_id=old_id' ] ];
2731 }
2732
2733 return $ret;
2734 }
2735
2756 public function getSlotsQueryInfo( $options = [] ) {
2757 $ret = [
2758 'tables' => [],
2759 'fields' => [],
2760 'joins' => [],
2761 'keys' => [],
2762 ];
2763
2764 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2765 $db = $this->getDBConnectionRef( DB_REPLICA );
2766 $ret['keys']['rev_id'] = 'rev_id';
2767
2768 $ret['tables'][] = 'revision';
2769
2770 $ret['fields']['slot_revision_id'] = 'rev_id';
2771 $ret['fields']['slot_content_id'] = 'NULL';
2772 $ret['fields']['slot_origin'] = 'rev_id';
2773 $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
2774
2775 if ( in_array( 'content', $options, true ) ) {
2776 $ret['fields']['content_size'] = 'rev_len';
2777 $ret['fields']['content_sha1'] = 'rev_sha1';
2778 $ret['fields']['content_address']
2779 = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'rev_text_id' ] );
2780
2781 // Allow the content_id field to be emulated later
2782 $ret['fields']['rev_text_id'] = 'rev_text_id';
2783
2784 if ( $this->contentHandlerUseDB ) {
2785 $ret['fields']['model_name'] = 'rev_content_model';
2786 } else {
2787 $ret['fields']['model_name'] = 'NULL';
2788 }
2789 }
2790 } else {
2791 $ret['keys']['rev_id'] = 'slot_revision_id';
2792 $ret['keys']['role_id'] = 'slot_role_id';
2793
2794 $ret['tables'][] = 'slots';
2795 $ret['fields'] = array_merge( $ret['fields'], [
2796 'slot_revision_id',
2797 'slot_content_id',
2798 'slot_origin',
2799 'slot_role_id',
2800 ] );
2801
2802 if ( in_array( 'role', $options, true ) ) {
2803 // Use left join to attach role name, so we still find the revision row even
2804 // if the role name is missing. This triggers a more obvious failure mode.
2805 $ret['tables'][] = 'slot_roles';
2806 $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2807 $ret['fields'][] = 'role_name';
2808 }
2809
2810 if ( in_array( 'content', $options, true ) ) {
2811 $ret['keys']['model_id'] = 'content_model';
2812
2813 $ret['tables'][] = 'content';
2814 $ret['fields'] = array_merge( $ret['fields'], [
2815 'content_size',
2816 'content_sha1',
2817 'content_address',
2818 'content_model',
2819 ] );
2820 $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2821
2822 if ( in_array( 'model', $options, true ) ) {
2823 // Use left join to attach model name, so we still find the revision row even
2824 // if the model name is missing. This triggers a more obvious failure mode.
2825 $ret['tables'][] = 'content_models';
2826 $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2827 $ret['fields'][] = 'model_name';
2828 }
2829
2830 }
2831 }
2832
2833 return $ret;
2834 }
2835
2849 public function getArchiveQueryInfo() {
2850 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2851 $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2852 $ret = [
2853 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2854 'fields' => [
2855 'ar_id',
2856 'ar_page_id',
2857 'ar_namespace',
2858 'ar_title',
2859 'ar_rev_id',
2860 'ar_timestamp',
2861 'ar_minor_edit',
2862 'ar_deleted',
2863 'ar_len',
2864 'ar_parent_id',
2865 'ar_sha1',
2866 ] + $commentQuery['fields'] + $actorQuery['fields'],
2867 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2868 ];
2869
2870 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2871 $ret['fields'][] = 'ar_text_id';
2872
2873 if ( $this->contentHandlerUseDB ) {
2874 $ret['fields'][] = 'ar_content_format';
2875 $ret['fields'][] = 'ar_content_model';
2876 }
2877 }
2878
2879 return $ret;
2880 }
2881
2891 public function getRevisionSizes( array $revIds ) {
2892 return $this->listRevisionSizes( $this->getDBConnectionRef( DB_REPLICA ), $revIds );
2893 }
2894
2907 public function listRevisionSizes( IDatabase $db, array $revIds ) {
2908 $this->checkDatabaseDomain( $db );
2909
2910 $revLens = [];
2911 if ( !$revIds ) {
2912 return $revLens; // empty
2913 }
2914
2915 $res = $db->select(
2916 'revision',
2917 [ 'rev_id', 'rev_len' ],
2918 [ 'rev_id' => $revIds ],
2919 __METHOD__
2920 );
2921
2922 foreach ( $res as $row ) {
2923 $revLens[$row->rev_id] = intval( $row->rev_len );
2924 }
2925
2926 return $revLens;
2927 }
2928
2937 private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2938 $op = $dir === 'next' ? '>' : '<';
2939 $sort = $dir === 'next' ? 'ASC' : 'DESC';
2940
2941 if ( !$rev->getId() || !$rev->getPageId() ) {
2942 // revision is unsaved or otherwise incomplete
2943 return null;
2944 }
2945
2946 if ( $rev instanceof RevisionArchiveRecord ) {
2947 // revision is deleted, so it's not part of the page history
2948 return null;
2949 }
2950
2951 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2952 $db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
2953
2954 $ts = $this->getTimestampFromId( $rev->getId(), $flags );
2955 if ( $ts === false ) {
2956 // XXX Should this be moved into getTimestampFromId?
2957 $ts = $db->selectField( 'archive', 'ar_timestamp',
2958 [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
2959 if ( $ts === false ) {
2960 // XXX Is this reachable? How can we have a page id but no timestamp?
2961 return null;
2962 }
2963 }
2964 $ts = $db->addQuotes( $db->timestamp( $ts ) );
2965
2966 $revId = $db->selectField( 'revision', 'rev_id',
2967 [
2968 'rev_page' => $rev->getPageId(),
2969 "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})"
2970 ],
2971 __METHOD__,
2972 [
2973 'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
2974 'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
2975 ]
2976 );
2977
2978 if ( $revId === false ) {
2979 return null;
2980 }
2981
2982 return $this->getRevisionById( intval( $revId ) );
2983 }
2984
3000 public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
3001 if ( $flags instanceof Title ) {
3002 // Old calling convention, we don't use Title here anymore
3003 wfDeprecated( __METHOD__ . ' with Title', '1.34' );
3004 $flags = 0;
3005 }
3006
3007 return $this->getRelativeRevision( $rev, $flags, 'prev' );
3008 }
3009
3023 public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
3024 if ( $flags instanceof Title ) {
3025 // Old calling convention, we don't use Title here anymore
3026 wfDeprecated( __METHOD__ . ' with Title', '1.34' );
3027 $flags = 0;
3028 }
3029
3030 return $this->getRelativeRevision( $rev, $flags, 'next' );
3031 }
3032
3044 private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
3045 $this->checkDatabaseDomain( $db );
3046
3047 if ( $rev->getPageId() === null ) {
3048 return 0;
3049 }
3050 # Use page_latest if ID is not given
3051 if ( !$rev->getId() ) {
3052 $prevId = $db->selectField(
3053 'page', 'page_latest',
3054 [ 'page_id' => $rev->getPageId() ],
3055 __METHOD__
3056 );
3057 } else {
3058 $prevId = $db->selectField(
3059 'revision', 'rev_id',
3060 [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
3061 __METHOD__,
3062 [ 'ORDER BY' => 'rev_id DESC' ]
3063 );
3064 }
3065 return intval( $prevId );
3066 }
3067
3080 public function getTimestampFromId( $id, $flags = 0 ) {
3081 if ( $id instanceof Title ) {
3082 // Old deprecated calling convention supported for backwards compatibility
3083 $id = $flags;
3084 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
3085 }
3086 $db = $this->getDBConnectionRefForQueryFlags( $flags );
3087
3088 $timestamp =
3089 $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
3090
3091 return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
3092 }
3093
3103 public function countRevisionsByPageId( IDatabase $db, $id ) {
3104 $this->checkDatabaseDomain( $db );
3105
3106 $row = $db->selectRow( 'revision',
3107 [ 'revCount' => 'COUNT(*)' ],
3108 [ 'rev_page' => $id ],
3109 __METHOD__
3110 );
3111 if ( $row ) {
3112 return intval( $row->revCount );
3113 }
3114 return 0;
3115 }
3116
3126 public function countRevisionsByTitle( IDatabase $db, $title ) {
3127 $id = $title->getArticleID();
3128 if ( $id ) {
3129 return $this->countRevisionsByPageId( $db, $id );
3130 }
3131 return 0;
3132 }
3133
3152 public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
3153 $this->checkDatabaseDomain( $db );
3154
3155 if ( !$userId ) {
3156 return false;
3157 }
3158
3159 $revQuery = $this->getQueryInfo();
3160 $res = $db->select(
3161 $revQuery['tables'],
3162 [
3163 'rev_user' => $revQuery['fields']['rev_user'],
3164 ],
3165 [
3166 'rev_page' => $pageId,
3167 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
3168 ],
3169 __METHOD__,
3170 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
3171 $revQuery['joins']
3172 );
3173 foreach ( $res as $row ) {
3174 if ( $row->rev_user != $userId ) {
3175 return false;
3176 }
3177 }
3178 return true;
3179 }
3180
3194 public function getKnownCurrentRevision( Title $title, $revId ) {
3195 $db = $this->getDBConnectionRef( DB_REPLICA );
3196
3197 $pageId = $title->getArticleID();
3198
3199 if ( !$pageId ) {
3200 return false;
3201 }
3202
3203 if ( !$revId ) {
3204 $revId = $title->getLatestRevID();
3205 }
3206
3207 if ( !$revId ) {
3208 wfWarn(
3209 'No latest revision known for page ' . $title->getPrefixedDBkey()
3210 . ' even though it exists with page ID ' . $pageId
3211 );
3212 return false;
3213 }
3214
3215 // Load the row from cache if possible. If not possible, populate the cache.
3216 // As a minor optimization, remember if this was a cache hit or miss.
3217 // We can sometimes avoid a database query later if this is a cache miss.
3218 $fromCache = true;
3219 $row = $this->cache->getWithSetCallback(
3220 // Page/rev IDs passed in from DB to reflect history merges
3221 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
3222 WANObjectCache::TTL_WEEK,
3223 function ( $curValue, &$ttl, array &$setOpts ) use (
3224 $db, $pageId, $revId, &$fromCache
3225 ) {
3226 $setOpts += Database::getCacheSetOptions( $db );
3227 $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
3228 if ( $row ) {
3229 $fromCache = false;
3230 }
3231 return $row; // don't cache negatives
3232 }
3233 );
3234
3235 // Reflect revision deletion and user renames.
3236 if ( $row ) {
3237 return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
3238 } else {
3239 return false;
3240 }
3241 }
3242
3254 private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3255 return $this->cache->makeGlobalKey(
3256 self::ROW_CACHE_KEY,
3257 $db->getDomainID(),
3258 $pageId,
3259 $revId
3260 );
3261 }
3262
3263 // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
3264
3265}
3266
3271class_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.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
This class handles the logic for the actor table migration.
CommentStoreComment represents a comment stored by CommentStore.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
A content handler knows how do deal with a specific type of content on a wiki page.
Helper class for DAO classes.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static hasFlags( $bitfield, $flags)
Hooks class.
Definition Hooks.php:34
A collection of public static functions to play with IP address and IP ranges.
Definition IP.php:67
MediaWiki exception.
Exception thrown when an unregistered content model is requested.
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
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.
setContentHandlerUseDB( $contentHandlerUseDB)
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)
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.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
loadRevisionFromId(IDatabase $db, $id)
Load a page revision from a given revision ID number.
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
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...
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.
getKnownCurrentRevision(Title $title, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
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.
emulateContentId( $textId)
Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode, based on the revisi...
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
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...
updateRevisionTextId(IDatabase $dbw, $revisionId, &$blobAddress)
findSlotContentId(IDatabase $db, $revId, $role)
Finds the ID of a content row for a given revision and slot role.
loadRevisionFromConds(IDatabase $db, $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision from the given database connection.
newRevisionFromRowAndSlots( $row, $slots, $queryFlags=0, Title $title=null, $fromCache=false)
newRevisionFromConds( $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision.
checkContent(Content $content, Title $title, $role)
MCR migration note: this corresponds to Revision::checkContentModel.
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)
getRevisionRowCacheKey(IDatabase $db, $pageId, $revId)
Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) Caching rows w...
userWasLastToEdit(IDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
newMutableRevisionFromArray(array $fields, $queryFlags=0, Title $title=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1....
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, $mcrMigrationStage, ActorMigration $actorMigration, $dbDomain=false)
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
fetchRevisionRowFromConds(IDatabase $db, $conditions, $flags=0)
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects.
static mapArchiveFields( $archiveRow)
Maps fields of the archive row to corresponding revision rows.
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save.
getRevisionByTimestamp( $title, $timestamp)
Load the revision for the given title with the given timestamp.
getPreviousRevision(RevisionRecord $rev, $flags=0)
Get the revision before $rev in the page's history, if any.
assertCrossWikiContentLoadingIsSafe()
Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading and still...
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
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...
int $mcrMigrationStage
An appropriate combination of SCHEMA_COMPAT_XXX flags.
emulateMainSlot_1_29( $row, $queryFlags, Title $title)
Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
getRole()
Returns the role of the slot.
hasAddress()
Whether this slot has an address.
getSha1()
Returns the content size.
getSize()
Returns the content size.
getAddress()
Returns the address of this slot's content.
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
getModel()
Returns the content model.
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
hasContentId()
Whether this slot has a content ID.
getContentId()
Returns the ID of the content meta data row associated with the slot.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Exception representing a failure to access a data blob.
Exception representing a failure to look up a row from a name table.
Service for storing and loading Content objects.
Value object representing a user's identity.
The Message class provides methods which fulfil two basic services:
Definition Message.php:162
Utility class for creating new RC entries.
const PRC_UNPATROLLED
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
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:51
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2364
getId()
Get the user's ID.
Definition User.php:2335
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:599
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:49
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:276
const SCHEMA_COMPAT_READ_BOTH
Definition Defines.php:278
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:273
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:274
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:275
Base interface for content objects.
Definition Content.php:34
Interface for database access objects.
Service for constructing revision objects.
Service for looking up page revisions.
Service for loading and storing data blobs.
Definition BlobStore.php: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 query wrapper.
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.
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
insertId()
Get the inserted value of an auto-increment row.
Database cluster connection, tracking, load balancing, and transaction manager interface.
Result wrapper for grabbing data queried from an IDatabase object.
$sort
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
$content
Definition router.php:78