MediaWiki REL1_32
RevisionStore.php
Go to the documentation of this file.
1<?php
27namespace MediaWiki\Revision;
28
37use InvalidArgumentException;
38use IP;
39use LogicException;
51use Psr\Log\LoggerAwareInterface;
52use Psr\Log\LoggerInterface;
53use Psr\Log\NullLogger;
56use RuntimeException;
57use stdClass;
59use User;
61use Wikimedia\Assert\Assert;
66
77 implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
78
79 const ROW_CACHE_KEY = 'revision-row-1.29';
80
84 private $blobStore;
85
89 private $wikiId;
90
95 private $contentHandlerUseDB = true;
96
101
105 private $cache;
106
111
116
120 private $logger;
121
126
131
134
155 public function __construct(
164 $wikiId = false
165 ) {
166 Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
167 Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
168 Assert::parameter(
170 '$mcrMigrationStage',
171 'Reading from the old and the new schema at the same time is not supported.'
172 );
173 Assert::parameter(
175 '$mcrMigrationStage',
176 'Reading needs to be enabled for the old or the new schema.'
177 );
178 Assert::parameter(
180 '$mcrMigrationStage',
181 'Writing needs to be enabled for the old or the new schema.'
182 );
183 Assert::parameter(
186 '$mcrMigrationStage',
187 'Cannot read the old schema when not also writing it.'
188 );
189 Assert::parameter(
192 '$mcrMigrationStage',
193 'Cannot read the new 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->mcrMigrationStage = $mcrMigrationStage;
203 $this->actorMigration = $actorMigration;
204 $this->wikiId = $wikiId;
205 $this->logger = new NullLogger();
206 }
207
213 private function hasMcrSchemaFlags( $flags ) {
214 return ( $this->mcrMigrationStage & $flags ) === $flags;
215 }
216
224 if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
225 throw new RevisionAccessException(
226 "Cross-wiki content loading is not supported by the pre-MCR schema"
227 );
228 }
229 }
230
231 public function setLogger( LoggerInterface $logger ) {
232 $this->logger = $logger;
233 }
234
238 public function isReadOnly() {
239 return $this->blobStore->isReadOnly();
240 }
241
245 public function getContentHandlerUseDB() {
247 }
248
257 ) {
258 if ( !$contentHandlerUseDB ) {
259 throw new MWException(
260 'Content model must be stored in the database for multi content revision migration.'
261 );
262 }
263 }
264 $this->contentHandlerUseDB = $contentHandlerUseDB;
265 }
266
270 private function getDBLoadBalancer() {
271 return $this->loadBalancer;
272 }
273
279 private function getDBConnection( $mode ) {
280 $lb = $this->getDBLoadBalancer();
281 return $lb->getConnection( $mode, [], $this->wikiId );
282 }
283
289 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
290 list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
291 return $this->getDBConnectionRef( $mode );
292 }
293
297 private function releaseDBConnection( IDatabase $connection ) {
298 $lb = $this->getDBLoadBalancer();
299 $lb->reuseConnection( $connection );
300 }
301
307 private function getDBConnectionRef( $mode ) {
308 $lb = $this->getDBLoadBalancer();
309 return $lb->getConnectionRef( $mode, [], $this->wikiId );
310 }
311
326 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
327 if ( !$pageId && !$revId ) {
328 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
329 }
330
331 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
332 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
333 if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
334 $queryFlags = self::READ_NORMAL;
335 }
336
337 $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
338 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
339 $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
340
341 // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
342 if ( $canUseTitleNewFromId ) {
343 // TODO: better foreign title handling (introduce TitleFactory)
344 $title = Title::newFromID( $pageId, $titleFlags );
345 if ( $title ) {
346 return $title;
347 }
348 }
349
350 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
351 $canUseRevId = ( $revId !== null && $revId > 0 );
352
353 if ( $canUseRevId ) {
354 $dbr = $this->getDBConnectionRef( $dbMode );
355 // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
356 $row = $dbr->selectRow(
357 [ 'revision', 'page' ],
358 [
359 'page_namespace',
360 'page_title',
361 'page_id',
362 'page_latest',
363 'page_is_redirect',
364 'page_len',
365 ],
366 [ 'rev_id' => $revId ],
367 __METHOD__,
368 $dbOptions,
369 [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
370 );
371 if ( $row ) {
372 // TODO: better foreign title handling (introduce TitleFactory)
373 return Title::newFromRow( $row );
374 }
375 }
376
377 // If we still don't have a title, fallback to master if that wasn't already happening.
378 if ( $dbMode !== DB_MASTER ) {
379 $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
380 if ( $title ) {
381 $this->logger->info(
382 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
383 [ 'trace' => wfBacktrace() ]
384 );
385 return $title;
386 }
387 }
388
389 throw new RevisionAccessException(
390 "Could not determine title for page ID $pageId and revision ID $revId"
391 );
392 }
393
401 private function failOnNull( $value, $name ) {
402 if ( $value === null ) {
404 "$name must not be " . var_export( $value, true ) . "!"
405 );
406 }
407
408 return $value;
409 }
410
418 private function failOnEmpty( $value, $name ) {
419 if ( $value === null || $value === 0 || $value === '' ) {
421 "$name must not be " . var_export( $value, true ) . "!"
422 );
423 }
424
425 return $value;
426 }
427
441 // TODO: pass in a DBTransactionContext instead of a database connection.
442 $this->checkDatabaseWikiId( $dbw );
443
444 $slotRoles = $rev->getSlotRoles();
445
446 // Make sure the main slot is always provided throughout migration
447 if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
448 throw new InvalidArgumentException(
449 'main slot must be provided'
450 );
451 }
452
453 // If we are not writing into the new schema, we can't support extra slots.
455 && $slotRoles !== [ SlotRecord::MAIN ]
456 ) {
457 throw new InvalidArgumentException(
458 'Only the main slot is supported when not writing to the MCR enabled schema!'
459 );
460 }
461
462 // As long as we are not reading from the new schema, we don't want to write extra slots.
464 && $slotRoles !== [ SlotRecord::MAIN ]
465 ) {
466 throw new InvalidArgumentException(
467 'Only the main slot is supported when not reading from the MCR enabled schema!'
468 );
469 }
470
471 // Checks
472 $this->failOnNull( $rev->getSize(), 'size field' );
473 $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
474 $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
475 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
476 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
477 $this->failOnNull( $user->getId(), 'user field' );
478 $this->failOnEmpty( $user->getName(), 'user_text field' );
479
480 if ( !$rev->isReadyForInsertion() ) {
481 // This is here for future-proofing. At the time this check being added, it
482 // was redundant to the individual checks above.
483 throw new IncompleteRevisionException( 'Revision is incomplete' );
484 }
485
486 // TODO: we shouldn't need an actual Title here.
487 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
488 $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
489
490 $parentId = $rev->getParentId() === null
491 ? $this->getPreviousRevisionId( $dbw, $rev )
492 : $rev->getParentId();
493
495 $rev = $dbw->doAtomicSection(
496 __METHOD__,
497 function ( IDatabase $dbw, $fname ) use (
498 $rev,
499 $user,
500 $comment,
501 $title,
502 $pageId,
503 $parentId
504 ) {
505 return $this->insertRevisionInternal(
506 $rev,
507 $dbw,
508 $user,
509 $comment,
510 $title,
511 $pageId,
512 $parentId
513 );
514 }
515 );
516
517 // sanity checks
518 Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
519 Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
520 Assert::postcondition(
521 $rev->getComment( RevisionRecord::RAW ) !== null,
522 'revision must have a comment'
523 );
524 Assert::postcondition(
525 $rev->getUser( RevisionRecord::RAW ) !== null,
526 'revision must have a user'
527 );
528
529 // Trigger exception if the main slot is missing.
530 // Technically, this could go away after MCR migration: while
531 // calling code may require a main slot to exist, RevisionStore
532 // really should not know or care about that requirement.
534
535 foreach ( $slotRoles as $role ) {
536 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
537 Assert::postcondition(
538 $slot->getContent() !== null,
539 $role . ' slot must have content'
540 );
541 Assert::postcondition(
542 $slot->hasRevision(),
543 $role . ' slot must have a revision associated'
544 );
545 }
546
547 Hooks::run( 'RevisionRecordInserted', [ $rev ] );
548
549 // TODO: deprecate in 1.32!
550 $legacyRevision = new Revision( $rev );
551 Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
552
553 return $rev;
554 }
555
556 private function insertRevisionInternal(
558 IDatabase $dbw,
559 User $user,
560 CommentStoreComment $comment,
561 Title $title,
562 $pageId,
563 $parentId
564 ) {
565 $slotRoles = $rev->getSlotRoles();
566
567 $revisionRow = $this->insertRevisionRowOn(
568 $dbw,
569 $rev,
570 $title,
571 $parentId
572 );
573
574 $revisionId = $revisionRow['rev_id'];
575
576 $blobHints = [
577 BlobStore::PAGE_HINT => $pageId,
578 BlobStore::REVISION_HINT => $revisionId,
579 BlobStore::PARENT_HINT => $parentId,
580 ];
581
582 $newSlots = [];
583 foreach ( $slotRoles as $role ) {
584 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
585
586 // If the SlotRecord already has a revision ID set, this means it already exists
587 // in the database, and should already belong to the current revision.
588 // However, a slot may already have a revision, but no content ID, if the slot
589 // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
590 // mode, and the respective archive row was not yet migrated to the new schema.
591 // In that case, a new slot row (and content row) must be inserted even during
592 // undeletion.
593 if ( $slot->hasRevision() && $slot->hasContentId() ) {
594 // TODO: properly abort transaction if the assertion fails!
595 Assert::parameter(
596 $slot->getRevision() === $revisionId,
597 'slot role ' . $slot->getRole(),
598 'Existing slot should belong to revision '
599 . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
600 );
601
602 // Slot exists, nothing to do, move along.
603 // This happens when restoring archived revisions.
604
605 $newSlots[$role] = $slot;
606
607 // Write the main slot's text ID to the revision table for backwards compatibility
608 if ( $slot->getRole() === SlotRecord::MAIN
609 && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
610 ) {
611 $blobAddress = $slot->getAddress();
612 $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
613 }
614 } else {
615 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
616 }
617 }
618
619 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
620
622 $title,
623 $user,
624 $comment,
625 (object)$revisionRow,
626 new RevisionSlots( $newSlots ),
627 $this->wikiId
628 );
629
630 return $rev;
631 }
632
640 private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
641 $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
642 if ( !$textId ) {
643 throw new LogicException(
644 'Blob address not supported in 1.29 database schema: ' . $blobAddress
645 );
646 }
647
648 // getTextIdFromAddress() is free to insert something into the text table, so $textId
649 // may be a new value, not anything already contained in $blobAddress.
650 $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
651
652 $dbw->update(
653 'revision',
654 [ 'rev_text_id' => $textId ],
655 [ 'rev_id' => $revisionId ],
656 __METHOD__
657 );
658
659 return $textId;
660 }
661
670 private function insertSlotOn(
671 IDatabase $dbw,
672 $revisionId,
673 SlotRecord $protoSlot,
674 Title $title,
675 array $blobHints = []
676 ) {
677 if ( $protoSlot->hasAddress() ) {
678 $blobAddress = $protoSlot->getAddress();
679 } else {
680 $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
681 }
682
683 $contentId = null;
684
685 // Write the main slot's text ID to the revision table for backwards compatibility
686 if ( $protoSlot->getRole() === SlotRecord::MAIN
687 && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
688 ) {
689 // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
690 // with the real content ID below.
691 $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
692 $contentId = $this->emulateContentId( $textId );
693 }
694
696 if ( $protoSlot->hasContentId() ) {
697 $contentId = $protoSlot->getContentId();
698 } else {
699 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
700 }
701
702 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
703 }
704
705 $savedSlot = SlotRecord::newSaved(
706 $revisionId,
707 $contentId,
708 $blobAddress,
709 $protoSlot
710 );
711
712 return $savedSlot;
713 }
714
722 private function insertIpChangesRow(
723 IDatabase $dbw,
724 User $user,
726 $revisionId
727 ) {
728 if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
729 $ipcRow = [
730 'ipc_rev_id' => $revisionId,
731 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
732 'ipc_hex' => IP::toHex( $user->getName() ),
733 ];
734 $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
735 }
736 }
737
749 private function insertRevisionRowOn(
750 IDatabase $dbw,
752 Title $title,
753 $parentId
754 ) {
755 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
756
757 list( $commentFields, $commentCallback ) =
758 $this->commentStore->insertWithTempTable(
759 $dbw,
760 'rev_comment',
761 $rev->getComment( RevisionRecord::RAW )
762 );
763 $revisionRow += $commentFields;
764
765 list( $actorFields, $actorCallback ) =
766 $this->actorMigration->getInsertValuesWithTempTable(
767 $dbw,
768 'rev_user',
769 $rev->getUser( RevisionRecord::RAW )
770 );
771 $revisionRow += $actorFields;
772
773 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
774
775 if ( !isset( $revisionRow['rev_id'] ) ) {
776 // only if auto-increment was used
777 $revisionRow['rev_id'] = intval( $dbw->insertId() );
778
779 if ( $dbw->getType() === 'mysql' ) {
780 // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
781 // auto-increment value to disk, so on server restart it might reuse IDs from deleted
782 // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
783
784 $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
785 $table = 'archive';
787 $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
788 if ( $maxRevId2 >= $maxRevId ) {
789 $maxRevId = $maxRevId2;
790 $table = 'slots';
791 }
792 }
793
794 if ( $maxRevId >= $revisionRow['rev_id'] ) {
795 $this->logger->debug(
796 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
797 . ' Trying to fix it.',
798 [
799 'revid' => $revisionRow['rev_id'],
800 'table' => $table,
801 'maxrevid' => $maxRevId,
802 ]
803 );
804
805 if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
806 throw new MWException( 'Failed to get database lock for T202032' );
807 }
808 $fname = __METHOD__;
809 $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) {
810 $dbw->unlock( 'fix-for-T202032', $fname );
811 } );
812
813 $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
814
815 // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
816 // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
817 // inserts too, though, at least on MariaDB 10.1.29.
818 //
819 // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
820 // transactions in this code path thanks to the row lock from the original ->insert() above.
821 //
822 // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
823 // that's for non-MySQL DBs.
824 $row1 = $dbw->query(
825 $dbw->selectSqlText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
826 )->fetchObject();
828 $row2 = $dbw->query(
829 $dbw->selectSqlText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
830 . ' FOR UPDATE'
831 )->fetchObject();
832 } else {
833 $row2 = null;
834 }
835 $maxRevId = max(
836 $maxRevId,
837 $row1 ? intval( $row1->v ) : 0,
838 $row2 ? intval( $row2->v ) : 0
839 );
840
841 // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
842 // transactions will throw a duplicate key error here. It doesn't seem worth trying
843 // to avoid that.
844 $revisionRow['rev_id'] = $maxRevId + 1;
845 $dbw->insert( 'revision', $revisionRow, __METHOD__ );
846 }
847 }
848 }
849
850 $commentCallback( $revisionRow['rev_id'] );
851 $actorCallback( $revisionRow['rev_id'], $revisionRow );
852
853 return $revisionRow;
854 }
855
866 private function getBaseRevisionRow(
867 IDatabase $dbw,
869 Title $title,
870 $parentId
871 ) {
872 // Record the edit in revisions
873 $revisionRow = [
874 'rev_page' => $rev->getPageId(),
875 'rev_parent_id' => $parentId,
876 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
877 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
878 'rev_deleted' => $rev->getVisibility(),
879 'rev_len' => $rev->getSize(),
880 'rev_sha1' => $rev->getSha1(),
881 ];
882
883 if ( $rev->getId() !== null ) {
884 // Needed to restore revisions with their original ID
885 $revisionRow['rev_id'] = $rev->getId();
886 }
887
889 // In non MCR mode this IF section will relate to the main slot
890 $mainSlot = $rev->getSlot( SlotRecord::MAIN );
891 $model = $mainSlot->getModel();
892 $format = $mainSlot->getFormat();
893
894 // MCR migration note: rev_content_model and rev_content_format will go away
895 if ( $this->contentHandlerUseDB ) {
897
898 $defaultModel = ContentHandler::getDefaultModelFor( $title );
899 $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
900
901 $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
902 $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
903 }
904 }
905
906 return $revisionRow;
907 }
908
917 private function storeContentBlob(
918 SlotRecord $slot,
919 Title $title,
920 array $blobHints = []
921 ) {
922 $content = $slot->getContent();
923 $format = $content->getDefaultFormat();
924 $model = $content->getModel();
925
926 $this->checkContent( $content, $title );
927
928 return $this->blobStore->storeBlob(
929 $content->serialize( $format ),
930 // These hints "leak" some information from the higher abstraction layer to
931 // low level storage to allow for optimization.
932 array_merge(
933 $blobHints,
934 [
935 BlobStore::DESIGNATION_HINT => 'page-content',
936 BlobStore::ROLE_HINT => $slot->getRole(),
937 BlobStore::SHA1_HINT => $slot->getSha1(),
938 BlobStore::MODEL_HINT => $model,
939 BlobStore::FORMAT_HINT => $format,
940 ]
941 )
942 );
943 }
944
951 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
952 $slotRow = [
953 'slot_revision_id' => $revisionId,
954 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
955 'slot_content_id' => $contentId,
956 // If the slot has a specific origin use that ID, otherwise use the ID of the revision
957 // that we just inserted.
958 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
959 ];
960 $dbw->insert( 'slots', $slotRow, __METHOD__ );
961 }
962
969 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
970 $contentRow = [
971 'content_size' => $slot->getSize(),
972 'content_sha1' => $slot->getSha1(),
973 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
974 'content_address' => $blobAddress,
975 ];
976 $dbw->insert( 'content', $contentRow, __METHOD__ );
977 return intval( $dbw->insertId() );
978 }
979
989 private function checkContent( Content $content, Title $title ) {
990 // Note: may return null for revisions that have not yet been inserted
991
992 $model = $content->getModel();
993 $format = $content->getDefaultFormat();
994 $handler = $content->getContentHandler();
995
996 $name = "$title";
997
998 if ( !$handler->isSupportedFormat( $format ) ) {
999 throw new MWException( "Can't use format $format with content model $model on $name" );
1000 }
1001
1002 if ( !$this->contentHandlerUseDB ) {
1003 // if $wgContentHandlerUseDB is not set,
1004 // all revisions must use the default content model and format.
1005
1007
1008 $defaultModel = ContentHandler::getDefaultModelFor( $title );
1009 $defaultHandler = ContentHandler::getForModelID( $defaultModel );
1010 $defaultFormat = $defaultHandler->getDefaultFormat();
1011
1012 if ( $model != $defaultModel ) {
1013 throw new MWException( "Can't save non-default content model with "
1014 . "\$wgContentHandlerUseDB disabled: model is $model, "
1015 . "default for $name is $defaultModel"
1016 );
1017 }
1018
1019 if ( $format != $defaultFormat ) {
1020 throw new MWException( "Can't use non-default content format with "
1021 . "\$wgContentHandlerUseDB disabled: format is $format, "
1022 . "default for $name is $defaultFormat"
1023 );
1024 }
1025 }
1026
1027 if ( !$content->isValid() ) {
1028 throw new MWException(
1029 "New content for $name is not valid! Content model is $model"
1030 );
1031 }
1032 }
1033
1059 public function newNullRevision(
1060 IDatabase $dbw,
1061 Title $title,
1062 CommentStoreComment $comment,
1063 $minor,
1064 User $user
1065 ) {
1066 $this->checkDatabaseWikiId( $dbw );
1067
1068 $pageId = $title->getArticleID();
1069
1070 // T51581: Lock the page table row to ensure no other process
1071 // is adding a revision to the page at the same time.
1072 // Avoid locking extra tables, compare T191892.
1073 $pageLatest = $dbw->selectField(
1074 'page',
1075 'page_latest',
1076 [ 'page_id' => $pageId ],
1077 __METHOD__,
1078 [ 'FOR UPDATE' ]
1079 );
1080
1081 if ( !$pageLatest ) {
1082 return null;
1083 }
1084
1085 // Fetch the actual revision row from master, without locking all extra tables.
1086 $oldRevision = $this->loadRevisionFromConds(
1087 $dbw,
1088 [ 'rev_id' => intval( $pageLatest ) ],
1089 self::READ_LATEST,
1090 $title
1091 );
1092
1093 if ( !$oldRevision ) {
1094 $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1095 $this->logger->error(
1096 $msg,
1097 [ 'exception' => new RuntimeException( $msg ) ]
1098 );
1099 return null;
1100 }
1101
1102 // Construct the new revision
1103 $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1104 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1105
1106 $newRevision->setComment( $comment );
1107 $newRevision->setUser( $user );
1108 $newRevision->setTimestamp( $timestamp );
1109 $newRevision->setMinorEdit( $minor );
1110
1111 return $newRevision;
1112 }
1113
1124 $rc = $this->getRecentChange( $rev );
1125 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1126 return $rc->getAttribute( 'rc_id' );
1127 } else {
1128 return 0;
1129 }
1130 }
1131
1145 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1146 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
1147 $db = $this->getDBConnection( $dbType );
1148
1149 $userIdentity = $rev->getUser( RevisionRecord::RAW );
1150
1151 if ( !$userIdentity ) {
1152 // If the revision has no user identity, chances are it never went
1153 // into the database, and doesn't have an RC entry.
1154 return null;
1155 }
1156
1157 // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
1158 $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
1160 [
1161 $actorWhere['conds'],
1162 'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
1163 'rc_this_oldid' => $rev->getId()
1164 ],
1165 __METHOD__,
1166 $dbType
1167 );
1168
1169 $this->releaseDBConnection( $db );
1170
1171 // XXX: cache this locally? Glue it to the RevisionRecord?
1172 return $rc;
1173 }
1174
1182 private static function mapArchiveFields( $archiveRow ) {
1183 $fieldMap = [
1184 // keep with ar prefix:
1185 'ar_id' => 'ar_id',
1186
1187 // not the same suffix:
1188 'ar_page_id' => 'rev_page',
1189 'ar_rev_id' => 'rev_id',
1190
1191 // same suffix:
1192 'ar_text_id' => 'rev_text_id',
1193 'ar_timestamp' => 'rev_timestamp',
1194 'ar_user_text' => 'rev_user_text',
1195 'ar_user' => 'rev_user',
1196 'ar_actor' => 'rev_actor',
1197 'ar_minor_edit' => 'rev_minor_edit',
1198 'ar_deleted' => 'rev_deleted',
1199 'ar_len' => 'rev_len',
1200 'ar_parent_id' => 'rev_parent_id',
1201 'ar_sha1' => 'rev_sha1',
1202 'ar_comment' => 'rev_comment',
1203 'ar_comment_cid' => 'rev_comment_cid',
1204 'ar_comment_id' => 'rev_comment_id',
1205 'ar_comment_text' => 'rev_comment_text',
1206 'ar_comment_data' => 'rev_comment_data',
1207 'ar_comment_old' => 'rev_comment_old',
1208 'ar_content_format' => 'rev_content_format',
1209 'ar_content_model' => 'rev_content_model',
1210 ];
1211
1212 $revRow = new stdClass();
1213 foreach ( $fieldMap as $arKey => $revKey ) {
1214 if ( property_exists( $archiveRow, $arKey ) ) {
1215 $revRow->$revKey = $archiveRow->$arKey;
1216 }
1217 }
1218
1219 return $revRow;
1220 }
1221
1232 private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
1233 $mainSlotRow = new stdClass();
1234 $mainSlotRow->role_name = SlotRecord::MAIN;
1235 $mainSlotRow->model_name = null;
1236 $mainSlotRow->slot_revision_id = null;
1237 $mainSlotRow->slot_content_id = null;
1238 $mainSlotRow->content_address = null;
1239
1240 $content = null;
1241 $blobData = null;
1242 $blobFlags = null;
1243
1244 if ( is_object( $row ) ) {
1245 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1246 // Don't emulate from a row when using the new schema.
1247 // Emulating from an array is still OK.
1248 throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
1249 }
1250
1251 // archive row
1252 if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
1253 $row = $this->mapArchiveFields( $row );
1254 }
1255
1256 if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
1257 $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
1258 $row->rev_text_id
1259 );
1260 }
1261
1262 // This is used by null-revisions
1263 $mainSlotRow->slot_origin = isset( $row->slot_origin )
1264 ? intval( $row->slot_origin )
1265 : null;
1266
1267 if ( isset( $row->old_text ) ) {
1268 // this happens when the text-table gets joined directly, in the pre-1.30 schema
1269 $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
1270 // Check against selects that might have not included old_flags
1271 if ( !property_exists( $row, 'old_flags' ) ) {
1272 throw new InvalidArgumentException( 'old_flags was not set in $row' );
1273 }
1274 $blobFlags = $row->old_flags ?? '';
1275 }
1276
1277 $mainSlotRow->slot_revision_id = intval( $row->rev_id );
1278
1279 $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
1280 $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
1281 $mainSlotRow->model_name = isset( $row->rev_content_model )
1282 ? strval( $row->rev_content_model )
1283 : null;
1284 // XXX: in the future, we'll probably always use the default format, and drop content_format
1285 $mainSlotRow->format_name = isset( $row->rev_content_format )
1286 ? strval( $row->rev_content_format )
1287 : null;
1288
1289 if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
1290 // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1291 $mainSlotRow->slot_content_id
1292 = $this->emulateContentId( intval( $row->rev_text_id ) );
1293 }
1294 } elseif ( is_array( $row ) ) {
1295 $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
1296
1297 $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
1298 ? intval( $row['slot_origin'] )
1299 : null;
1300 $mainSlotRow->content_address = isset( $row['text_id'] )
1301 ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
1302 : null;
1303 $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
1304 $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
1305
1306 $mainSlotRow->model_name = isset( $row['content_model'] )
1307 ? strval( $row['content_model'] ) : null; // XXX: must be a string!
1308 // XXX: in the future, we'll probably always use the default format, and drop content_format
1309 $mainSlotRow->format_name = isset( $row['content_format'] )
1310 ? strval( $row['content_format'] ) : null;
1311 $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
1312 // XXX: If the flags field is not set then $blobFlags should be null so that no
1313 // decoding will happen. An empty string will result in default decodings.
1314 $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
1315
1316 // if we have a Content object, override mText and mContentModel
1317 if ( !empty( $row['content'] ) ) {
1318 if ( !( $row['content'] instanceof Content ) ) {
1319 throw new MWException( 'content field must contain a Content object.' );
1320 }
1321
1323 $content = $row['content'];
1324 $handler = $content->getContentHandler();
1325
1326 $mainSlotRow->model_name = $content->getModel();
1327
1328 // XXX: in the future, we'll probably always use the default format.
1329 if ( $mainSlotRow->format_name === null ) {
1330 $mainSlotRow->format_name = $handler->getDefaultFormat();
1331 }
1332 }
1333
1334 if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
1335 // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
1336 $mainSlotRow->slot_content_id
1337 = $this->emulateContentId( intval( $row['text_id'] ) );
1338 }
1339 } else {
1340 throw new MWException( 'Revision constructor passed invalid row format.' );
1341 }
1342
1343 // With the old schema, the content changes with every revision,
1344 // except for null-revisions.
1345 if ( !isset( $mainSlotRow->slot_origin ) ) {
1346 $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
1347 }
1348
1349 if ( $mainSlotRow->model_name === null ) {
1350 $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
1352
1353 // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
1354 // TODO: MCR: deprecate $title->getModel().
1355 return ContentHandler::getDefaultModelFor( $title );
1356 };
1357 }
1358
1359 if ( !$content ) {
1360 // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
1361 // is missing, but "empty revisions" with no content are used in some edge cases.
1362
1363 $content = function ( SlotRecord $slot )
1364 use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
1365 {
1366 return $this->loadSlotContent(
1367 $slot,
1368 $blobData,
1369 $blobFlags,
1370 $mainSlotRow->format_name,
1371 $queryFlags
1372 );
1373 };
1374 }
1375
1377 // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
1378 // the inherited slot to have the same content_id as the original slot. In that case,
1379 // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
1380 $mainSlotRow->slot_content_id =
1381 function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
1382 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1383 return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
1384 };
1385 }
1386
1387 return new SlotRecord( $mainSlotRow, $content );
1388 }
1389
1401 private function emulateContentId( $textId ) {
1402 // Return a negative number to ensure the ID is distinct from any real content IDs
1403 // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
1404 // mode.
1405 return -$textId;
1406 }
1407
1427 private function loadSlotContent(
1428 SlotRecord $slot,
1429 $blobData = null,
1430 $blobFlags = null,
1431 $blobFormat = null,
1432 $queryFlags = 0
1433 ) {
1434 if ( $blobData !== null ) {
1435 Assert::parameterType( 'string', $blobData, '$blobData' );
1436 Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
1437
1438 $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
1439
1440 if ( $blobFlags === null ) {
1441 // No blob flags, so use the blob verbatim.
1442 $data = $blobData;
1443 } else {
1444 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1445 if ( $data === false ) {
1446 throw new RevisionAccessException(
1447 "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1448 );
1449 }
1450 }
1451
1452 } else {
1453 $address = $slot->getAddress();
1454 try {
1455 $data = $this->blobStore->getBlob( $address, $queryFlags );
1456 } catch ( BlobAccessException $e ) {
1457 throw new RevisionAccessException(
1458 "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1459 );
1460 }
1461 }
1462
1463 // Unserialize content
1464 $handler = ContentHandler::getForModelID( $slot->getModel() );
1465
1466 $content = $handler->unserializeContent( $data, $blobFormat );
1467 return $content;
1468 }
1469
1484 public function getRevisionById( $id, $flags = 0 ) {
1485 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
1486 }
1487
1504 public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
1505 $conds = [
1506 'page_namespace' => $linkTarget->getNamespace(),
1507 'page_title' => $linkTarget->getDBkey()
1508 ];
1509 if ( $revId ) {
1510 // Use the specified revision ID.
1511 // Note that we use newRevisionFromConds here because we want to retry
1512 // and fall back to master if the page is not found on a replica.
1513 // Since the caller supplied a revision ID, we are pretty sure the revision is
1514 // supposed to exist, so we should try hard to find it.
1515 $conds['rev_id'] = $revId;
1516 return $this->newRevisionFromConds( $conds, $flags );
1517 } else {
1518 // Use a join to get the latest revision.
1519 // Note that we don't use newRevisionFromConds here because we don't want to retry
1520 // and fall back to master. The assumption is that we only want to force the fallback
1521 // if we are quite sure the revision exists because the caller supplied a revision ID.
1522 // If the page isn't found at all on a replica, it probably simply does not exist.
1523 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1524
1525 $conds[] = 'rev_id=page_latest';
1526 $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1527
1528 return $rev;
1529 }
1530 }
1531
1548 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1549 $conds = [ 'page_id' => $pageId ];
1550 if ( $revId ) {
1551 // Use the specified revision ID.
1552 // Note that we use newRevisionFromConds here because we want to retry
1553 // and fall back to master if the page is not found on a replica.
1554 // Since the caller supplied a revision ID, we are pretty sure the revision is
1555 // supposed to exist, so we should try hard to find it.
1556 $conds['rev_id'] = $revId;
1557 return $this->newRevisionFromConds( $conds, $flags );
1558 } else {
1559 // Use a join to get the latest revision.
1560 // Note that we don't use newRevisionFromConds here because we don't want to retry
1561 // and fall back to master. The assumption is that we only want to force the fallback
1562 // if we are quite sure the revision exists because the caller supplied a revision ID.
1563 // If the page isn't found at all on a replica, it probably simply does not exist.
1564 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1565
1566 $conds[] = 'rev_id=page_latest';
1567 $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1568
1569 return $rev;
1570 }
1571 }
1572
1584 public function getRevisionByTimestamp( $title, $timestamp ) {
1585 $db = $this->getDBConnection( DB_REPLICA );
1586 return $this->newRevisionFromConds(
1587 [
1588 'rev_timestamp' => $db->timestamp( $timestamp ),
1589 'page_namespace' => $title->getNamespace(),
1590 'page_title' => $title->getDBkey()
1591 ],
1592 0,
1593 $title
1594 );
1595 }
1596
1603 private function loadSlotRecords( $revId, $queryFlags ) {
1604 $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
1605
1606 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1607 $db = $this->getDBConnectionRef( $dbMode );
1608
1609 $res = $db->select(
1610 $revQuery['tables'],
1611 $revQuery['fields'],
1612 [
1613 'slot_revision_id' => $revId,
1614 ],
1615 __METHOD__,
1616 $dbOptions,
1617 $revQuery['joins']
1618 );
1619
1620 $slots = [];
1621
1622 foreach ( $res as $row ) {
1623 // resolve role names and model names from in-memory cache, instead of joining.
1624 $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1625 $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1626
1627 $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
1628 return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
1629 };
1630
1631 $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1632 }
1633
1634 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1635 throw new RevisionAccessException(
1636 'Main slot of revision ' . $revId . ' not found in database!'
1637 );
1638 };
1639
1640 return $slots;
1641 }
1642
1657 private function newRevisionSlots(
1658 $revId,
1659 $revisionRow,
1660 $queryFlags,
1661 Title $title
1662 ) {
1663 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
1664 $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
1665 $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
1666 } else {
1667 // XXX: do we need the same kind of caching here
1668 // that getKnownCurrentRevision uses (if $revId == page_latest?)
1669
1670 $slots = new RevisionSlots( function () use( $revId, $queryFlags ) {
1671 return $this->loadSlotRecords( $revId, $queryFlags );
1672 } );
1673 }
1674
1675 return $slots;
1676 }
1677
1696 $row,
1697 $queryFlags = 0,
1698 Title $title = null,
1699 array $overrides = []
1700 ) {
1701 Assert::parameterType( 'object', $row, '$row' );
1702
1703 // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1704 Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1705
1706 if ( !$title && isset( $overrides['title'] ) ) {
1707 if ( !( $overrides['title'] instanceof Title ) ) {
1708 throw new MWException( 'title field override must contain a Title object.' );
1709 }
1710
1711 $title = $overrides['title'];
1712 }
1713
1714 if ( !isset( $title ) ) {
1715 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1716 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1717 } else {
1718 throw new InvalidArgumentException(
1719 'A Title or ar_namespace and ar_title must be given'
1720 );
1721 }
1722 }
1723
1724 foreach ( $overrides as $key => $value ) {
1725 $field = "ar_$key";
1726 $row->$field = $value;
1727 }
1728
1729 try {
1731 $row->ar_user ?? null,
1732 $row->ar_user_text ?? null,
1733 $row->ar_actor ?? null
1734 );
1735 } catch ( InvalidArgumentException $ex ) {
1736 wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1737 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1738 }
1739
1740 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1741 // Legacy because $row may have come from self::selectFields()
1742 $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1743
1744 $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
1745
1746 return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
1747 }
1748
1760 public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
1761 Assert::parameterType( 'object', $row, '$row' );
1762
1763 if ( !$title ) {
1764 $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
1765 $revId = $row->rev_id ?? 0;
1766
1767 $title = $this->getTitle( $pageId, $revId, $queryFlags );
1768 }
1769
1770 if ( !isset( $row->page_latest ) ) {
1771 $row->page_latest = $title->getLatestRevID();
1772 if ( $row->page_latest === 0 && $title->exists() ) {
1773 wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1774 }
1775 }
1776
1777 try {
1779 $row->rev_user ?? null,
1780 $row->rev_user_text ?? null,
1781 $row->rev_actor ?? null
1782 );
1783 } catch ( InvalidArgumentException $ex ) {
1784 wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
1785 $user = new UserIdentityValue( 0, 'Unknown user', 0 );
1786 }
1787
1788 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1789 // Legacy because $row may have come from self::selectFields()
1790 $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1791
1792 $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
1793
1794 return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
1795 }
1796
1812 array $fields,
1813 $queryFlags = 0,
1814 Title $title = null
1815 ) {
1816 if ( !$title && isset( $fields['title'] ) ) {
1817 if ( !( $fields['title'] instanceof Title ) ) {
1818 throw new MWException( 'title field must contain a Title object.' );
1819 }
1820
1821 $title = $fields['title'];
1822 }
1823
1824 if ( !$title ) {
1825 $pageId = $fields['page'] ?? 0;
1826 $revId = $fields['id'] ?? 0;
1827
1828 $title = $this->getTitle( $pageId, $revId, $queryFlags );
1829 }
1830
1831 if ( !isset( $fields['page'] ) ) {
1832 $fields['page'] = $title->getArticleID( $queryFlags );
1833 }
1834
1835 // if we have a content object, use it to set the model and type
1836 if ( !empty( $fields['content'] ) ) {
1837 if ( !( $fields['content'] instanceof Content ) && !is_array( $fields['content'] ) ) {
1838 throw new MWException(
1839 'content field must contain a Content object or an array of Content objects.'
1840 );
1841 }
1842 }
1843
1844 if ( !empty( $fields['text_id'] ) ) {
1845 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
1846 throw new MWException( "The text_id field is only available in the pre-MCR schema" );
1847 }
1848
1849 if ( !empty( $fields['content'] ) ) {
1850 throw new MWException(
1851 "Text already stored in external store (id {$fields['text_id']}), " .
1852 "can't specify content object"
1853 );
1854 }
1855 }
1856
1857 if (
1858 isset( $fields['comment'] )
1859 && !( $fields['comment'] instanceof CommentStoreComment )
1860 ) {
1861 $commentData = $fields['comment_data'] ?? null;
1862
1863 if ( $fields['comment'] instanceof Message ) {
1864 $fields['comment'] = CommentStoreComment::newUnsavedComment(
1865 $fields['comment'],
1866 $commentData
1867 );
1868 } else {
1869 $commentText = trim( strval( $fields['comment'] ) );
1870 $fields['comment'] = CommentStoreComment::newUnsavedComment(
1871 $commentText,
1872 $commentData
1873 );
1874 }
1875 }
1876
1877 $revision = new MutableRevisionRecord( $title, $this->wikiId );
1878 $this->initializeMutableRevisionFromArray( $revision, $fields );
1879
1880 if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
1881 foreach ( $fields['content'] as $role => $content ) {
1882 $revision->setContent( $role, $content );
1883 }
1884 } else {
1885 $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
1886 $revision->setSlot( $mainSlot );
1887 }
1888
1889 return $revision;
1890 }
1891
1897 MutableRevisionRecord $record,
1898 array $fields
1899 ) {
1901 $user = null;
1902
1903 if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
1904 $user = $fields['user'];
1905 } else {
1906 try {
1908 $fields['user'] ?? null,
1909 $fields['user_text'] ?? null,
1910 $fields['actor'] ?? null
1911 );
1912 } catch ( InvalidArgumentException $ex ) {
1913 $user = null;
1914 }
1915 }
1916
1917 if ( $user ) {
1918 $record->setUser( $user );
1919 }
1920
1921 $timestamp = isset( $fields['timestamp'] )
1922 ? strval( $fields['timestamp'] )
1923 : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1924
1925 $record->setTimestamp( $timestamp );
1926
1927 if ( isset( $fields['page'] ) ) {
1928 $record->setPageId( intval( $fields['page'] ) );
1929 }
1930
1931 if ( isset( $fields['id'] ) ) {
1932 $record->setId( intval( $fields['id'] ) );
1933 }
1934 if ( isset( $fields['parent_id'] ) ) {
1935 $record->setParentId( intval( $fields['parent_id'] ) );
1936 }
1937
1938 if ( isset( $fields['sha1'] ) ) {
1939 $record->setSha1( $fields['sha1'] );
1940 }
1941 if ( isset( $fields['size'] ) ) {
1942 $record->setSize( intval( $fields['size'] ) );
1943 }
1944
1945 if ( isset( $fields['minor_edit'] ) ) {
1946 $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
1947 }
1948 if ( isset( $fields['deleted'] ) ) {
1949 $record->setVisibility( intval( $fields['deleted'] ) );
1950 }
1951
1952 if ( isset( $fields['comment'] ) ) {
1953 Assert::parameterType(
1954 CommentStoreComment::class,
1955 $fields['comment'],
1956 '$row[\'comment\']'
1957 );
1958 $record->setComment( $fields['comment'] );
1959 }
1960 }
1961
1976 public function loadRevisionFromId( IDatabase $db, $id ) {
1977 return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
1978 }
1979
1995 public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
1996 $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
1997 if ( $id ) {
1998 $conds['rev_id'] = intval( $id );
1999 } else {
2000 $conds[] = 'rev_id=page_latest';
2001 }
2002 return $this->loadRevisionFromConds( $db, $conds );
2003 }
2004
2021 public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
2022 if ( $id ) {
2023 $matchId = intval( $id );
2024 } else {
2025 $matchId = 'page_latest';
2026 }
2027
2028 return $this->loadRevisionFromConds(
2029 $db,
2030 [
2031 "rev_id=$matchId",
2032 'page_namespace' => $title->getNamespace(),
2033 'page_title' => $title->getDBkey()
2034 ],
2035 0,
2036 $title
2037 );
2038 }
2039
2055 public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
2056 return $this->loadRevisionFromConds( $db,
2057 [
2058 'rev_timestamp' => $db->timestamp( $timestamp ),
2059 'page_namespace' => $title->getNamespace(),
2060 'page_title' => $title->getDBkey()
2061 ],
2062 0,
2063 $title
2064 );
2065 }
2066
2082 private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
2083 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2084 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
2085
2086 $lb = $this->getDBLoadBalancer();
2087
2088 // Make sure new pending/committed revision are visibile later on
2089 // within web requests to certain avoid bugs like T93866 and T94407.
2090 if ( !$rev
2091 && !( $flags & self::READ_LATEST )
2092 && $lb->getServerCount() > 1
2093 && $lb->hasOrMadeRecentMasterChanges()
2094 ) {
2095 $flags = self::READ_LATEST;
2096 $dbw = $this->getDBConnection( DB_MASTER );
2097 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
2098 $this->releaseDBConnection( $dbw );
2099 }
2100
2101 return $rev;
2102 }
2103
2117 private function loadRevisionFromConds(
2118 IDatabase $db,
2119 $conditions,
2120 $flags = 0,
2121 Title $title = null
2122 ) {
2123 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
2124 if ( $row ) {
2125 $rev = $this->newRevisionFromRow( $row, $flags, $title );
2126
2127 return $rev;
2128 }
2129
2130 return null;
2131 }
2132
2140 private function checkDatabaseWikiId( IDatabase $db ) {
2141 $storeWiki = $this->wikiId;
2142 $dbWiki = $db->getDomainID();
2143
2144 if ( $dbWiki === $storeWiki ) {
2145 return;
2146 }
2147
2148 $storeWiki = $storeWiki ?: $this->loadBalancer->getLocalDomainID();
2149 // @FIXME: when would getDomainID() be false here?
2150 $dbWiki = $dbWiki ?: wfWikiID();
2151
2152 if ( $dbWiki === $storeWiki ) {
2153 return;
2154 }
2155
2156 // HACK: counteract encoding imposed by DatabaseDomain
2157 $storeWiki = str_replace( '?h', '-', $storeWiki );
2158 $dbWiki = str_replace( '?h', '-', $dbWiki );
2159
2160 if ( $dbWiki === $storeWiki ) {
2161 return;
2162 }
2163
2164 throw new MWException( "RevisionStore for $storeWiki "
2165 . "cannot be used with a DB connection for $dbWiki" );
2166 }
2167
2180 private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
2181 $this->checkDatabaseWikiId( $db );
2182
2183 $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
2184 $options = [];
2185 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2186 $options[] = 'FOR UPDATE';
2187 }
2188 return $db->selectRow(
2189 $revQuery['tables'],
2190 $revQuery['fields'],
2191 $conditions,
2192 __METHOD__,
2193 $options,
2194 $revQuery['joins']
2195 );
2196 }
2197
2212 private function findSlotContentId( IDatabase $db, $revId, $role ) {
2213 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
2214 return null;
2215 }
2216
2217 try {
2218 $roleId = $this->slotRoleStore->getId( $role );
2219 $conditions = [
2220 'slot_revision_id' => $revId,
2221 'slot_role_id' => $roleId,
2222 ];
2223
2224 $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
2225
2226 return $contentId ?: null;
2227 } catch ( NameTableAccessException $ex ) {
2228 // If the role is missing from the slot_roles table,
2229 // the corresponding row in slots cannot exist.
2230 return null;
2231 }
2232 }
2233
2257 public function getQueryInfo( $options = [] ) {
2258 $ret = [
2259 'tables' => [],
2260 'fields' => [],
2261 'joins' => [],
2262 ];
2263
2264 $ret['tables'][] = 'revision';
2265 $ret['fields'] = array_merge( $ret['fields'], [
2266 'rev_id',
2267 'rev_page',
2268 'rev_timestamp',
2269 'rev_minor_edit',
2270 'rev_deleted',
2271 'rev_len',
2272 'rev_parent_id',
2273 'rev_sha1',
2274 ] );
2275
2276 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2277 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2278 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2279 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2280
2281 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
2282 $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
2283 $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
2284 $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
2285
2286 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2287 $ret['fields'][] = 'rev_text_id';
2288
2289 if ( $this->contentHandlerUseDB ) {
2290 $ret['fields'][] = 'rev_content_format';
2291 $ret['fields'][] = 'rev_content_model';
2292 }
2293 }
2294
2295 if ( in_array( 'page', $options, true ) ) {
2296 $ret['tables'][] = 'page';
2297 $ret['fields'] = array_merge( $ret['fields'], [
2298 'page_namespace',
2299 'page_title',
2300 'page_id',
2301 'page_latest',
2302 'page_is_redirect',
2303 'page_len',
2304 ] );
2305 $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
2306 }
2307
2308 if ( in_array( 'user', $options, true ) ) {
2309 $ret['tables'][] = 'user';
2310 $ret['fields'] = array_merge( $ret['fields'], [
2311 'user_name',
2312 ] );
2313 $u = $actorQuery['fields']['rev_user'];
2314 $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
2315 }
2316
2317 if ( in_array( 'text', $options, true ) ) {
2318 if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
2319 throw new InvalidArgumentException( 'text table can no longer be joined directly' );
2320 } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2321 // NOTE: even when this class is set to not read from the old schema, callers
2322 // should still be able to join against the text table, as long as we are still
2323 // writing the old schema for compatibility.
2324 // TODO: This should trigger a deprecation warning eventually (T200918), but not
2325 // before all known usages are removed (see T198341 and T201164).
2326 // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
2327 }
2328
2329 $ret['tables'][] = 'text';
2330 $ret['fields'] = array_merge( $ret['fields'], [
2331 'old_text',
2332 'old_flags'
2333 ] );
2334 $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
2335 }
2336
2337 return $ret;
2338 }
2339
2357 public function getSlotsQueryInfo( $options = [] ) {
2358 $ret = [
2359 'tables' => [],
2360 'fields' => [],
2361 'joins' => [],
2362 ];
2363
2364 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2365 $db = $this->getDBConnectionRef( DB_REPLICA );
2366 $ret['tables']['slots'] = 'revision';
2367
2368 $ret['fields']['slot_revision_id'] = 'slots.rev_id';
2369 $ret['fields']['slot_content_id'] = 'NULL';
2370 $ret['fields']['slot_origin'] = 'slots.rev_id';
2371 $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
2372
2373 if ( in_array( 'content', $options, true ) ) {
2374 $ret['fields']['content_size'] = 'slots.rev_len';
2375 $ret['fields']['content_sha1'] = 'slots.rev_sha1';
2376 $ret['fields']['content_address']
2377 = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
2378
2379 if ( $this->contentHandlerUseDB ) {
2380 $ret['fields']['model_name'] = 'slots.rev_content_model';
2381 } else {
2382 $ret['fields']['model_name'] = 'NULL';
2383 }
2384 }
2385 } else {
2386 $ret['tables'][] = 'slots';
2387 $ret['fields'] = array_merge( $ret['fields'], [
2388 'slot_revision_id',
2389 'slot_content_id',
2390 'slot_origin',
2391 'slot_role_id',
2392 ] );
2393
2394 if ( in_array( 'role', $options, true ) ) {
2395 // Use left join to attach role name, so we still find the revision row even
2396 // if the role name is missing. This triggers a more obvious failure mode.
2397 $ret['tables'][] = 'slot_roles';
2398 $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2399 $ret['fields'][] = 'role_name';
2400 }
2401
2402 if ( in_array( 'content', $options, true ) ) {
2403 $ret['tables'][] = 'content';
2404 $ret['fields'] = array_merge( $ret['fields'], [
2405 'content_size',
2406 'content_sha1',
2407 'content_address',
2408 'content_model',
2409 ] );
2410 $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ];
2411
2412 if ( in_array( 'model', $options, true ) ) {
2413 // Use left join to attach model name, so we still find the revision row even
2414 // if the model name is missing. This triggers a more obvious failure mode.
2415 $ret['tables'][] = 'content_models';
2416 $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2417 $ret['fields'][] = 'model_name';
2418 }
2419
2420 }
2421 }
2422
2423 return $ret;
2424 }
2425
2439 public function getArchiveQueryInfo() {
2440 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2441 $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
2442 $ret = [
2443 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
2444 'fields' => [
2445 'ar_id',
2446 'ar_page_id',
2447 'ar_namespace',
2448 'ar_title',
2449 'ar_rev_id',
2450 'ar_timestamp',
2451 'ar_minor_edit',
2452 'ar_deleted',
2453 'ar_len',
2454 'ar_parent_id',
2455 'ar_sha1',
2456 ] + $commentQuery['fields'] + $actorQuery['fields'],
2457 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
2458 ];
2459
2460 if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
2461 $ret['fields'][] = 'ar_text_id';
2462
2463 if ( $this->contentHandlerUseDB ) {
2464 $ret['fields'][] = 'ar_content_format';
2465 $ret['fields'][] = 'ar_content_model';
2466 }
2467 }
2468
2469 return $ret;
2470 }
2471
2481 public function getRevisionSizes( array $revIds ) {
2482 return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
2483 }
2484
2497 public function listRevisionSizes( IDatabase $db, array $revIds ) {
2498 $this->checkDatabaseWikiId( $db );
2499
2500 $revLens = [];
2501 if ( !$revIds ) {
2502 return $revLens; // empty
2503 }
2504
2505 $res = $db->select(
2506 'revision',
2507 [ 'rev_id', 'rev_len' ],
2508 [ 'rev_id' => $revIds ],
2509 __METHOD__
2510 );
2511
2512 foreach ( $res as $row ) {
2513 $revLens[$row->rev_id] = intval( $row->rev_len );
2514 }
2515
2516 return $revLens;
2517 }
2518
2529 public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
2530 if ( $title === null ) {
2531 $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
2532 }
2533 $prev = $title->getPreviousRevisionID( $rev->getId() );
2534 if ( $prev ) {
2535 return $this->getRevisionByTitle( $title, $prev );
2536 }
2537 return null;
2538 }
2539
2550 public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
2551 if ( $title === null ) {
2552 $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
2553 }
2554 $next = $title->getNextRevisionID( $rev->getId() );
2555 if ( $next ) {
2556 return $this->getRevisionByTitle( $title, $next );
2557 }
2558 return null;
2559 }
2560
2573 $this->checkDatabaseWikiId( $db );
2574
2575 if ( $rev->getPageId() === null ) {
2576 return 0;
2577 }
2578 # Use page_latest if ID is not given
2579 if ( !$rev->getId() ) {
2580 $prevId = $db->selectField(
2581 'page', 'page_latest',
2582 [ 'page_id' => $rev->getPageId() ],
2583 __METHOD__
2584 );
2585 } else {
2586 $prevId = $db->selectField(
2587 'revision', 'rev_id',
2588 [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
2589 __METHOD__,
2590 [ 'ORDER BY' => 'rev_id DESC' ]
2591 );
2592 }
2593 return intval( $prevId );
2594 }
2595
2606 public function getTimestampFromId( $title, $id, $flags = 0 ) {
2607 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2608
2609 $conds = [ 'rev_id' => $id ];
2610 $conds['rev_page'] = $title->getArticleID();
2611 $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
2612
2613 return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
2614 }
2615
2625 public function countRevisionsByPageId( IDatabase $db, $id ) {
2626 $this->checkDatabaseWikiId( $db );
2627
2628 $row = $db->selectRow( 'revision',
2629 [ 'revCount' => 'COUNT(*)' ],
2630 [ 'rev_page' => $id ],
2631 __METHOD__
2632 );
2633 if ( $row ) {
2634 return intval( $row->revCount );
2635 }
2636 return 0;
2637 }
2638
2648 public function countRevisionsByTitle( IDatabase $db, $title ) {
2649 $id = $title->getArticleID();
2650 if ( $id ) {
2651 return $this->countRevisionsByPageId( $db, $id );
2652 }
2653 return 0;
2654 }
2655
2674 public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
2675 $this->checkDatabaseWikiId( $db );
2676
2677 if ( !$userId ) {
2678 return false;
2679 }
2680
2681 $revQuery = $this->getQueryInfo();
2682 $res = $db->select(
2683 $revQuery['tables'],
2684 [
2685 'rev_user' => $revQuery['fields']['rev_user'],
2686 ],
2687 [
2688 'rev_page' => $pageId,
2689 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
2690 ],
2691 __METHOD__,
2692 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
2693 $revQuery['joins']
2694 );
2695 foreach ( $res as $row ) {
2696 if ( $row->rev_user != $userId ) {
2697 return false;
2698 }
2699 }
2700 return true;
2701 }
2702
2716 public function getKnownCurrentRevision( Title $title, $revId ) {
2717 $db = $this->getDBConnectionRef( DB_REPLICA );
2718
2719 $pageId = $title->getArticleID();
2720
2721 if ( !$pageId ) {
2722 return false;
2723 }
2724
2725 if ( !$revId ) {
2726 $revId = $title->getLatestRevID();
2727 }
2728
2729 if ( !$revId ) {
2730 wfWarn(
2731 'No latest revision known for page ' . $title->getPrefixedDBkey()
2732 . ' even though it exists with page ID ' . $pageId
2733 );
2734 return false;
2735 }
2736
2737 $row = $this->cache->getWithSetCallback(
2738 // Page/rev IDs passed in from DB to reflect history merges
2739 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2740 WANObjectCache::TTL_WEEK,
2741 function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
2742 $setOpts += Database::getCacheSetOptions( $db );
2743
2744 $conds = [
2745 'rev_page' => intval( $pageId ),
2746 'page_id' => intval( $pageId ),
2747 'rev_id' => intval( $revId ),
2748 ];
2749
2750 $row = $this->fetchRevisionRowFromConds( $db, $conds );
2751 return $row ?: false; // don't cache negatives
2752 }
2753 );
2754
2755 // Reflect revision deletion and user renames
2756 if ( $row ) {
2757 return $this->newRevisionFromRow( $row, 0, $title );
2758 } else {
2759 return false;
2760 }
2761 }
2762
2774 private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
2775 return $this->cache->makeGlobalKey(
2776 self::ROW_CACHE_KEY,
2777 $db->getDomainID(),
2778 $pageId,
2779 $revId
2780 );
2781 }
2782
2783 // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
2784
2785}
2786
2791class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:121
This class handles the logic for the actor table migration.
CommentStoreComment represents a comment stored by CommentStore.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
A content handler knows how do deal with a specific type of content on a wiki page.
Helper class for DAO classes.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static hasFlags( $bitfield, $flags)
Hooks class.
Definition Hooks.php:34
A collection of public static functions to play with IP address and IP ranges.
Definition IP.php:67
MediaWiki exception.
Exception thrown when an unregistered content model is requested.
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Mutable RevisionRecord implementation, for building new revision entries programmatically.
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
setSize( $size)
Set nominal revision size, for optimization.
setSha1( $sha1)
Set revision hash, for optimization.
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Exception representing a failure to look up a revision.
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
getSize()
Returns the nominal size of this revision, in bogo-bytes.
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
getSha1()
Returns the base36 sha1 of this revision.
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Value object representing the set of slots belonging to a revision.
A RevisionRecord representing an existing revision persisted in the revision table.
Service for looking up page revisions.
setContentHandlerUseDB( $contentHandlerUseDB)
getTimestampFromId( $title, $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
getBaseRevisionRow(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
newRevisionSlots( $revId, $revisionRow, $queryFlags, Title $title)
Factory method for RevisionSlots.
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
storeContentBlob(SlotRecord $slot, Title $title, array $blobHints=[])
newRevisionFromArchiveRow( $row, $queryFlags=0, Title $title=null, array $overrides=[])
Make a fake revision object from an archive table row.
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
loadRevisionFromId(IDatabase $db, $id)
Load a page revision from a given revision ID number.
insertSlotRowOn(SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId)
insertIpChangesRow(IDatabase $dbw, User $user, RevisionRecord $rev, $revisionId)
Insert IP revision into ip_changes for use when querying for a range.
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page's history.
loadSlotContent(SlotRecord $slot, $blobData=null, $blobFlags=null, $blobFormat=null, $queryFlags=0)
Loads a Content object based on a slot row.
getNextRevision(RevisionRecord $rev, Title $title=null)
Get next revision for this title.
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)
newRevisionFromRow( $row, $queryFlags=0, Title $title=null)
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
getKnownCurrentRevision(Title $title, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
checkContent(Content $content, Title $title)
MCR migration note: this corresponds to Revision::checkContentModel.
emulateContentId( $textId)
Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode, based on the revisi...
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
insertSlotOn(IDatabase $dbw, $revisionId, SlotRecord $protoSlot, Title $title, array $blobHints=[])
checkDatabaseWikiId(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
insertRevisionRowOn(IDatabase $dbw, RevisionRecord $rev, Title $title, $parentId)
updateRevisionTextId(IDatabase $dbw, $revisionId, &$blobAddress)
findSlotContentId(IDatabase $db, $revId, $role)
Finds the ID of a content row for a given revision and slot role.
loadRevisionFromConds(IDatabase $db, $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision from the given database connection.
newRevisionFromConds( $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision.
getPreviousRevision(RevisionRecord $rev, Title $title=null)
Get previous revision for this title.
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....
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, $mcrMigrationStage, ActorMigration $actorMigration, $wikiId=false)
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
fetchRevisionRowFromConds(IDatabase $db, $conditions, $flags=0)
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects.
static mapArchiveFields( $archiveRow)
Maps fields of the archive row to corresponding revision rows.
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save.
getRevisionByTimestamp( $title, $timestamp)
Load the revision for the given title with the given timestamp.
assertCrossWikiContentLoadingIsSafe()
Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading and still...
insertRevisionInternal(RevisionRecord $rev, IDatabase $dbw, User $user, CommentStoreComment $comment, Title $title, $pageId, $parentId)
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
int $mcrMigrationStage
An appropriate combination of SCHEMA_COMPAT_XXX flags.
emulateMainSlot_1_29( $row, $queryFlags, Title $title)
Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
loadSlotRecords( $revId, $queryFlags)
releaseDBConnection(IDatabase $connection)
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
getRole()
Returns the role of the slot.
hasAddress()
Whether this slot has an address.
getSha1()
Returns the content size.
getSize()
Returns the content size.
getAddress()
Returns the address of this slot's content.
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
getModel()
Returns the content model.
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
hasContentId()
Whether this slot has a content ID.
getContentId()
Returns the ID of the content meta data row associated with the slot.
Exception representing a failure to access a data blob.
Exception representing a failure to look up a row from a name table.
Service for storing and loading Content objects.
Value object representing a user's identity.
The Message class provides methods which fulfil two basic services:
Definition Message.php:160
Utility class for creating new RC entries.
const PRC_UNPATROLLED
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Represents a title within MediaWiki.
Definition Title.php:39
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:47
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:682
Multi-datacenter aware caching interface.
Helper class to handle automatically marking connections as reusable (via RAII pattern) as well handl...
Definition DBConnRef.php:15
Relational database abstraction object.
Definition Database.php:48
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
const SCHEMA_COMPAT_WRITE_BOTH
Definition Defines.php:288
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:287
const SCHEMA_COMPAT_READ_BOTH
Definition Defines.php:289
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:284
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:285
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:286
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2050
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:994
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2054
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:933
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1818
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
returning false will NOT prevent logging $e
Definition hooks.txt:2226
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Base interface for content objects.
Definition Content.php:34
Interface for database access objects.
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:33
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition BlobStore.php:64
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition BlobStore.php:40
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with.
Definition BlobStore.php:46
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with.
Definition BlobStore.php:52
const FORMAT_HINT
Hint key for use with storeBlob, indicating the serialization format used to create the blob,...
Definition BlobStore.php:82
const REVISION_HINT
Hint key for use with storeBlob, indicating the revision the blob is associated with.
Definition BlobStore.php:58
const SHA1_HINT
Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the method.
Definition BlobStore.php:70
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob.
Definition BlobStore.php:76
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
unlock( $lockName, $method)
Release a lock.
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Single row SELECT wrapper.
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
getDomainID()
Return the currently selected domain ID.
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
addQuotes( $s)
Adds quotes and backslashes.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
insertId()
Get the inserted value of an auto-increment row.
Database cluster connection, tracking, load balancing, and transaction manager interface.
you have access to all of the normal MediaWiki so you can get a DB use the cache
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$content
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26