MediaWiki master
RevisionStore.php
Go to the documentation of this file.
1<?php
12namespace MediaWiki\Revision;
13
14use InvalidArgumentException;
15use LogicException;
27use MediaWiki\Page\LegacyArticleIdAccess;
47use Psr\Log\LoggerAwareInterface;
48use Psr\Log\LoggerInterface;
49use Psr\Log\NullLogger;
50use RuntimeException;
51use StatusValue;
52use stdClass;
53use Traversable;
54use Wikimedia\Assert\Assert;
55use Wikimedia\Assert\ParameterAssertionException;
56use Wikimedia\Assert\PreconditionException;
57use Wikimedia\IPUtils;
68use Wikimedia\Timestamp\TimestampFormat as TS;
69
79class RevisionStore implements RevisionFactory, RevisionLookup, LoggerAwareInterface {
80
81 use LegacyArticleIdAccess;
82
83 public const ROW_CACHE_KEY = 'revision-row-1.29';
84
85 public const ORDER_OLDEST_TO_NEWEST = 'ASC';
86 public const ORDER_NEWEST_TO_OLDEST = 'DESC';
87
88 // Constants for get(...)Between methods
89 public const INCLUDE_OLD = 'include_old';
90 public const INCLUDE_NEW = 'include_new';
91 public const INCLUDE_BOTH = 'include_both';
92 public const INCLUDE_DELETED_REVISIONS = 'include_deleted_revisions';
93
97 private $blobStore;
98
102 private $wikiId;
103
104 private ILoadBalancer $loadBalancer;
105 private WANObjectCache $cache;
106 private BagOStuff $localCache;
107 private CommentStore $commentStore;
108 private ActorStore $actorStore;
109 private LoggerInterface $logger;
110 private NameTableStore $contentModelStore;
111 private NameTableStore $slotRoleStore;
112 private SlotRoleRegistry $slotRoleRegistry;
113 private IContentHandlerFactory $contentHandlerFactory;
114 private HookRunner $hookRunner;
115 private PageStore $pageStore;
116 private TitleFactory $titleFactory;
117 private RecentChangeLookup $recentChangeLookup;
118
143 public function __construct(
144 ILoadBalancer $loadBalancer,
145 SqlBlobStore $blobStore,
146 WANObjectCache $cache,
147 BagOStuff $localCache,
148 CommentStore $commentStore,
149 NameTableStore $contentModelStore,
150 NameTableStore $slotRoleStore,
151 SlotRoleRegistry $slotRoleRegistry,
152 ActorStore $actorStore,
153 IContentHandlerFactory $contentHandlerFactory,
154 PageStore $pageStore,
155 TitleFactory $titleFactory,
156 HookContainer $hookContainer,
157 RecentChangeLookup $recentChangeLookup,
158 $wikiId = WikiAwareEntity::LOCAL
159 ) {
160 Assert::parameterType( [ 'string', 'false' ], $wikiId, '$wikiId' );
161
162 $this->loadBalancer = $loadBalancer;
163 $this->blobStore = $blobStore;
164 $this->cache = $cache;
165 $this->localCache = $localCache;
166 $this->commentStore = $commentStore;
167 $this->contentModelStore = $contentModelStore;
168 $this->slotRoleStore = $slotRoleStore;
169 $this->slotRoleRegistry = $slotRoleRegistry;
170 $this->actorStore = $actorStore;
171 $this->wikiId = $wikiId;
172 $this->logger = new NullLogger();
173 $this->contentHandlerFactory = $contentHandlerFactory;
174 $this->pageStore = $pageStore;
175 $this->titleFactory = $titleFactory;
176 $this->hookRunner = new HookRunner( $hookContainer );
177 $this->recentChangeLookup = $recentChangeLookup;
178 }
179
180 public function setLogger( LoggerInterface $logger ): void {
181 $this->logger = $logger;
182 }
183
187 public function isReadOnly() {
188 return $this->blobStore->isReadOnly();
189 }
190
196 public function getWikiId() {
197 return $this->wikiId;
198 }
199
205 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
206 if ( ( $queryFlags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) {
207 return $this->getPrimaryConnection();
208 } else {
209 return $this->getReplicaConnection();
210 }
211 }
212
217 private function getReplicaConnection( $groups = [] ) {
218 // TODO: Replace with ICP
219 return $this->loadBalancer->getConnection( DB_REPLICA, $groups, $this->wikiId );
220 }
221
222 private function getPrimaryConnection(): IDatabase {
223 // TODO: Replace with ICP
224 return $this->loadBalancer->getConnection( DB_PRIMARY, [], $this->wikiId );
225 }
226
243 public function getTitle( $pageId, $revId, $queryFlags = IDBAccessObject::READ_NORMAL ) {
244 // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069
245 if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
246 wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' );
247 }
248
249 $page = $this->getPage( $pageId, $revId, $queryFlags );
250 return $this->titleFactory->newFromPageIdentity( $page );
251 }
252
263 private function getPage( ?int $pageId, ?int $revId, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
264 if ( !$pageId && !$revId ) {
265 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
266 }
267
268 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
269 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
270 if ( DBAccessObjectUtils::hasFlags( $queryFlags, IDBAccessObject::READ_LATEST_IMMUTABLE ) ) {
271 $queryFlags = IDBAccessObject::READ_NORMAL;
272 }
273
274 // Loading by ID is best
275 if ( $pageId !== null && $pageId > 0 ) {
276 $page = $this->pageStore->getPageById( $pageId, $queryFlags );
277 if ( $page ) {
278 return $this->wrapPage( $page );
279 }
280 }
281
282 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
283 if ( $revId !== null && $revId > 0 ) {
284 $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags )
285 ->join( 'revision', null, 'page_id=rev_page' )
286 ->conds( [ 'rev_id' => $revId ] )
287 ->caller( __METHOD__ );
288
289 $page = $pageQuery->fetchPageRecord();
290 if ( $page ) {
291 return $this->wrapPage( $page );
292 }
293 }
294
295 // If we still don't have a title, fallback to primary DB if that wasn't already happening.
296 if ( $queryFlags === IDBAccessObject::READ_NORMAL ) {
297 $title = $this->getPage( $pageId, $revId, IDBAccessObject::READ_LATEST );
298 if ( $title ) {
299 $this->logger->info(
300 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
301 [ 'exception' => new RuntimeException() ]
302 );
303 return $title;
304 }
305 }
306
307 throw new RevisionAccessException(
308 'Could not determine title for page ID {page_id} and revision ID {rev_id}',
309 [
310 'page_id' => $pageId,
311 'rev_id' => $revId,
312 ]
313 );
314 }
315
316 private function wrapPage( PageIdentity $page ): PageIdentity {
317 if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
318 // NOTE: since there is still a lot of code that needs a full Title,
319 // and uses Title::castFromPageIdentity() to get one, it's beneficial
320 // to create a Title right away if we can, so we don't have to convert
321 // over and over later on.
322 // When there is less need to convert to Title, this special case can
323 // be removed.
324 return $this->titleFactory->newFromPageIdentity( $page );
325 } else {
326 return $page;
327 }
328 }
329
337 private function failOnNull( $value, $name ) {
338 if ( $value === null ) {
339 throw new IncompleteRevisionException(
340 "$name must not be " . var_export( $value, true ) . "!"
341 );
342 }
343
344 return $value;
345 }
346
354 private function failOnEmpty( $value, $name ) {
355 if ( $value === null || $value === 0 || $value === '' ) {
356 throw new IncompleteRevisionException(
357 "$name must not be " . var_export( $value, true ) . "!"
358 );
359 }
360
361 return $value;
362 }
363
378 public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
379 // TODO: pass in a DBTransactionContext instead of a database connection.
380 $this->checkDatabaseDomain( $dbw );
381
382 $slotRoles = $rev->getSlotRoles();
383
384 // Make sure the main slot is always provided throughout migration
385 if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
387 'main slot must be provided'
388 );
389 }
390
391 // Checks
392 $this->failOnNull( $rev->getSize(), 'size field' );
393 $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
394 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
395 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
396 $this->failOnNull( $user->getId(), 'user field' );
397 $this->failOnEmpty( $user->getName(), 'user_text field' );
398
399 if ( !$rev->isReadyForInsertion() ) {
400 // This is here for future-proofing. At the time this check being added, it
401 // was redundant to the individual checks above.
402 throw new IncompleteRevisionException( 'Revision is incomplete' );
403 }
404
405 if ( $slotRoles == [ SlotRecord::MAIN ] ) {
406 // T239717: If the main slot is the only slot, make sure the revision's nominal size
407 // and hash match the main slot's nominal size and hash.
408 $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
409 Assert::precondition(
410 $mainSlot->getSize() === $rev->getSize(),
411 'The revisions\'s size must match the main slot\'s size (see T239717)'
412 );
413 }
414
415 $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early
416
417 $parentId = $rev->getParentId() ?? $this->getPreviousRevisionId( $dbw, $rev );
418
419 Assert::precondition( $pageId > 0, 'revision must have an ID' );
420
422 $rev = $dbw->doAtomicSection(
423 __METHOD__,
424 function ( IDatabase $dbw, $fname ) use (
425 $rev,
426 $user,
427 $comment,
428 $pageId,
429 $parentId
430 ) {
431 return $this->insertRevisionInternal(
432 $rev,
433 $dbw,
434 $user,
435 $comment,
436 $rev->getPage(),
437 $pageId,
438 $parentId
439 );
440 }
441 );
442
443 Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' );
444 Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' );
445 Assert::postcondition(
446 $rev->getComment( RevisionRecord::RAW ) !== null,
447 'revision must have a comment'
448 );
449 Assert::postcondition(
450 $rev->getUser( RevisionRecord::RAW ) !== null,
451 'revision must have a user'
452 );
453
454 // Trigger exception if the main slot is missing.
455 // Technically, this could go away after MCR migration: while
456 // calling code may require a main slot to exist, RevisionStore
457 // really should not know or care about that requirement.
458 $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
459
460 foreach ( $slotRoles as $role ) {
461 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
462 Assert::postcondition(
463 $slot->getContent() !== null,
464 $role . ' slot must have content'
465 );
466 Assert::postcondition(
467 $slot->hasRevision(),
468 $role . ' slot must have a revision associated'
469 );
470 }
471
472 $this->hookRunner->onRevisionRecordInserted( $rev );
473
474 return $rev;
475 }
476
489 public function updateSlotsOn(
490 RevisionRecord $revision,
491 RevisionSlotsUpdate $revisionSlotsUpdate,
492 IDatabase $dbw
493 ): array {
494 $this->checkDatabaseDomain( $dbw );
495
496 // Make sure all modified and removed slots are derived slots
497 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
498 Assert::precondition(
499 $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
500 'Trying to modify a slot that is not derived'
501 );
502 }
503 foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) {
504 $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
505 Assert::precondition(
506 $isDerived,
507 'Trying to remove a slot that is not derived'
508 );
509 throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' );
510 }
511
513 $slotRecords = $dbw->doAtomicSection(
514 __METHOD__,
515 function ( IDatabase $dbw, $fname ) use (
516 $revision,
517 $revisionSlotsUpdate
518 ) {
519 return $this->updateSlotsInternal(
520 $revision,
521 $revisionSlotsUpdate,
522 $dbw
523 );
524 }
525 );
526
527 foreach ( $slotRecords as $role => $slot ) {
528 Assert::postcondition(
529 $slot->getContent() !== null,
530 $role . ' slot must have content'
531 );
532 Assert::postcondition(
533 $slot->hasRevision(),
534 $role . ' slot must have a revision associated'
535 );
536 }
537
538 return $slotRecords;
539 }
540
547 private function updateSlotsInternal(
548 RevisionRecord $revision,
549 RevisionSlotsUpdate $revisionSlotsUpdate,
550 IDatabase $dbw
551 ): array {
552 $page = $revision->getPage();
553 $revId = $revision->getId( $this->wikiId );
554 $blobHints = [
555 BlobStore::PAGE_HINT => $page->getId( $this->wikiId ),
556 BlobStore::REVISION_HINT => $revId,
557 BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
558 ];
559
560 $newSlots = [];
561 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
562 $slot = $revisionSlotsUpdate->getModifiedSlot( $role );
563 $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
564 }
565
566 return $newSlots;
567 }
568
569 private function insertRevisionInternal(
570 RevisionRecord $rev,
571 IDatabase $dbw,
572 UserIdentity $user,
573 CommentStoreComment $comment,
574 PageIdentity $page,
575 int $pageId,
576 int $parentId
577 ): RevisionRecord {
578 $slotRoles = $rev->getSlotRoles();
579
580 $revisionRow = $this->insertRevisionRowOn(
581 $dbw,
582 $rev,
583 $parentId
584 );
585
586 $revisionId = $revisionRow['rev_id'];
587
588 $blobHints = [
589 BlobStore::PAGE_HINT => $pageId,
590 BlobStore::REVISION_HINT => $revisionId,
591 BlobStore::PARENT_HINT => $parentId,
592 ];
593
594 $newSlots = [];
595 foreach ( $slotRoles as $role ) {
596 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
597
598 // If the SlotRecord already has a revision ID set, this means it already exists
599 // in the database, and should already belong to the latest revision.
600 // However, a slot may already have a revision, but no content ID, if the slot
601 // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
602 // mode, and the respective archive row was not yet migrated to the new schema.
603 // In that case, a new slot row (and content row) must be inserted even during
604 // undeletion.
605 if ( $slot->hasRevision() && $slot->hasContentId() ) {
606 // TODO: properly abort transaction if the assertion fails!
607 Assert::parameter(
608 $slot->getRevision() === $revisionId,
609 'slot role ' . $slot->getRole(),
610 'Existing slot should belong to revision '
611 . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
612 );
613
614 // Slot exists, nothing to do, move along.
615 // This happens when restoring archived revisions.
616
617 $newSlots[$role] = $slot;
618 } else {
619 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
620 }
621 }
622
623 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
624
625 $rev = new RevisionStoreRecord(
626 $page,
627 $user,
628 $comment,
629 (object)$revisionRow,
630 new RevisionSlots( $newSlots ),
631 $this->wikiId
632 );
633
634 return $rev;
635 }
636
645 private function insertSlotOn(
646 IDatabase $dbw,
647 $revisionId,
648 SlotRecord $protoSlot,
649 PageIdentity $page,
650 array $blobHints = []
651 ) {
652 if ( $protoSlot->hasAddress() ) {
653 $blobAddress = $protoSlot->getAddress();
654 } else {
655 $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
656 }
657
658 if ( $protoSlot->hasContentId() ) {
659 $contentId = $protoSlot->getContentId();
660 } else {
661 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
662 }
663
664 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
665
666 return SlotRecord::newSaved(
667 $revisionId,
668 $contentId,
669 $blobAddress,
670 $protoSlot
671 );
672 }
673
681 private function insertIpChangesRow(
682 IDatabase $dbw,
683 UserIdentity $user,
684 RevisionRecord $rev,
685 $revisionId
686 ) {
687 if ( !$user->isRegistered() && IPUtils::isValid( $user->getName() ) ) {
688 $dbw->newInsertQueryBuilder()
689 ->insertInto( 'ip_changes' )
690 ->row( [
691 'ipc_rev_id' => $revisionId,
692 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
693 'ipc_hex' => IPUtils::toHex( $user->getName() ),
694 ] )
695 ->caller( __METHOD__ )->execute();
696
697 }
698 }
699
710 private function insertRevisionRowOn(
711 IDatabase $dbw,
712 RevisionRecord $rev,
713 $parentId
714 ) {
715 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
716
717 $revisionRow += $this->commentStore->insert(
718 $dbw,
719 'rev_comment',
720 $rev->getComment( RevisionRecord::RAW )
721 );
722
723 $dbw->newInsertQueryBuilder()
724 ->insertInto( 'revision' )
725 ->row( $revisionRow )
726 ->caller( __METHOD__ )->execute();
727
728 if ( !isset( $revisionRow['rev_id'] ) ) {
729 // only if auto-increment was used
730 $revisionRow['rev_id'] = intval( $dbw->insertId() );
731
732 if ( $dbw->getType() === 'mysql' ) {
733 // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
734 // auto-increment value to disk, so on server restart it might reuse IDs from deleted
735 // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
736
737 $maxRevId = intval( $dbw->newSelectQueryBuilder()
738 ->select( 'MAX(ar_rev_id)' )
739 ->from( 'archive' )
740 ->caller( __METHOD__ )
741 ->fetchField() );
742 $table = 'archive';
743 $maxRevId2 = intval( $dbw->newSelectQueryBuilder()
744 ->select( 'MAX(slot_revision_id)' )
745 ->from( 'slots' )
746 ->caller( __METHOD__ )
747 ->fetchField() );
748 if ( $maxRevId2 >= $maxRevId ) {
749 $maxRevId = $maxRevId2;
750 $table = 'slots';
751 }
752
753 if ( $maxRevId >= $revisionRow['rev_id'] ) {
754 $this->logger->debug(
755 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
756 . ' Trying to fix it.',
757 [
758 'revid' => $revisionRow['rev_id'],
759 'table' => $table,
760 'maxrevid' => $maxRevId,
761 ]
762 );
763
764 if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
765 throw new MWException( 'Failed to get database lock for T202032' );
766 }
767 $fname = __METHOD__;
768 $dbw->onTransactionResolution(
769 static function () use ( $dbw, $fname ): void {
770 $dbw->unlock( 'fix-for-T202032', $fname );
771 },
772 __METHOD__
773 );
774
775 $dbw->newDeleteQueryBuilder()
776 ->deleteFrom( 'revision' )
777 ->where( [ 'rev_id' => $revisionRow['rev_id'] ] )
778 ->caller( __METHOD__ )->execute();
779
780 // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
781 // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
782 // inserts too, though, at least on MariaDB 10.1.29.
783 //
784 // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
785 // transactions in this code path thanks to the row lock from the original ->insert() above.
786 //
787 // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
788 // that's for non-MySQL DBs.
789 $row1 = $dbw->query(
790 $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
791 __METHOD__
792 )->fetchObject();
793
794 $row2 = $dbw->query(
795 $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
796 . ' FOR UPDATE',
797 __METHOD__
798 )->fetchObject();
799
800 $maxRevId = max(
801 $maxRevId,
802 $row1 ? intval( $row1->v ) : 0,
803 $row2 ? intval( $row2->v ) : 0
804 );
805
806 // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
807 // transactions will throw a duplicate key error here. It doesn't seem worth trying
808 // to avoid that.
809 $revisionRow['rev_id'] = $maxRevId + 1;
810 $dbw->newInsertQueryBuilder()
811 ->insertInto( 'revision' )
812 ->row( $revisionRow )
813 ->caller( __METHOD__ )->execute();
814 }
815 }
816 }
817
818 return $revisionRow;
819 }
820
828 private function getBaseRevisionRow(
829 IDatabase $dbw,
830 RevisionRecord $rev,
831 $parentId
832 ) {
833 // Record the edit in revisions
834 $revisionRow = [
835 'rev_page' => $rev->getPageId( $this->wikiId ),
836 'rev_parent_id' => $parentId,
837 'rev_actor' => $this->actorStore->acquireActorId(
838 $rev->getUser( RevisionRecord::RAW ),
839 $dbw
840 ),
841 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
842 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
843 'rev_deleted' => $rev->getVisibility(),
844 'rev_len' => $rev->getSize(),
845 ];
846
847 if ( $rev->getId( $this->wikiId ) !== null ) {
848 // Needed to restore revisions with their original ID
849 $revisionRow['rev_id'] = $rev->getId( $this->wikiId );
850 }
851
852 return $revisionRow;
853 }
854
863 private function storeContentBlob(
864 SlotRecord $slot,
865 PageIdentity $page,
866 array $blobHints = []
867 ) {
868 $content = $slot->getContent();
869 $format = $content->getDefaultFormat();
870 $model = $content->getModel();
871
872 $this->checkContent( $content, $page, $slot->getRole() );
873
874 return $this->blobStore->storeBlob(
875 $content->serialize( $format ),
876 // These hints "leak" some information from the higher abstraction layer to
877 // low level storage to allow for optimization.
878 array_merge(
879 $blobHints,
880 [
881 BlobStore::DESIGNATION_HINT => 'page-content',
882 BlobStore::ROLE_HINT => $slot->getRole(),
883 BlobStore::SHA1_HINT => $slot->getSha1(),
884 BlobStore::MODEL_HINT => $model,
885 BlobStore::FORMAT_HINT => $format,
886 ]
887 )
888 );
889 }
890
897 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
898 $dbw->newInsertQueryBuilder()
899 ->insertInto( 'slots' )
900 ->row( [
901 'slot_revision_id' => $revisionId,
902 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
903 'slot_content_id' => $contentId,
904 // If the slot has a specific origin use that ID, otherwise use the ID of the revision
905 // that we just inserted.
906 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
907 ] )
908 ->caller( __METHOD__ )->execute();
909 }
910
917 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
918 $dbw->newInsertQueryBuilder()
919 ->insertInto( 'content' )
920 ->row( [
921 'content_size' => $slot->getSize(),
922 'content_sha1' => $slot->getSha1(),
923 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
924 'content_address' => $blobAddress,
925 ] )
926 ->caller( __METHOD__ )->execute();
927 return intval( $dbw->insertId() );
928 }
929
940 private function checkContent( Content $content, PageIdentity $page, string $role ) {
941 // Note: may return null for revisions that have not yet been inserted
942
943 $model = $content->getModel();
944 $format = $content->getDefaultFormat();
945 $handler = $content->getContentHandler();
946
947 if ( !$handler->isSupportedFormat( $format ) ) {
948 throw new MWException(
949 "Can't use format $format with content model $model on $page role $role"
950 );
951 }
952
953 if ( !$content->isValid() ) {
954 throw new MWException(
955 "New content for $page role $role is not valid! Content model is $model"
956 );
957 }
958 }
959
984 public function newNullRevision(
985 IDatabase $dbw,
986 PageIdentity $page,
987 CommentStoreComment $comment,
988 $minor,
989 UserIdentity $user
990 ) {
991 $this->checkDatabaseDomain( $dbw );
992
993 $pageId = $this->getArticleId( $page );
994
995 // T51581: Lock the page table row to ensure no other process
996 // is adding a revision to the page at the same time.
997 // Avoid locking extra tables, compare T191892.
998 $pageLatest = $dbw->newSelectQueryBuilder()
999 ->select( 'page_latest' )
1000 ->forUpdate()
1001 ->from( 'page' )
1002 ->where( [ 'page_id' => $pageId ] )
1003 ->caller( __METHOD__ )->fetchField();
1004
1005 if ( !$pageLatest ) {
1006 $msg = 'T235589: Failed to select table row during dummy revision creation' .
1007 " Page id '$pageId' does not exist.";
1008 $this->logger->error(
1009 $msg,
1010 [ 'exception' => new RuntimeException( $msg ) ]
1011 );
1012
1013 return null;
1014 }
1015
1016 // Fetch the actual revision row from primary DB, without locking all extra tables.
1017 $oldRevision = $this->loadRevisionFromConds(
1018 $dbw,
1019 [ 'rev_id' => intval( $pageLatest ) ],
1020 IDBAccessObject::READ_LATEST,
1021 $page
1022 );
1023
1024 if ( !$oldRevision ) {
1025 $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1026 $this->logger->error(
1027 $msg,
1028 [ 'exception' => new RuntimeException( $msg ) ]
1029 );
1030 return null;
1031 }
1032
1033 // Construct the new revision
1034 $timestamp = MWTimestamp::now( TS::MW );
1035 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1036
1037 $newRevision->setComment( $comment );
1038 $newRevision->setUser( $user );
1039 $newRevision->setTimestamp( $timestamp );
1040 $newRevision->setMinorEdit( $minor );
1041
1042 return $newRevision;
1043 }
1044
1054 public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1055 $rc = $this->getRecentChange( $rev );
1056 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1057 return $rc->getAttribute( 'rc_id' );
1058 } else {
1059 return 0;
1060 }
1061 }
1062
1076 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1077 if ( $this->wikiId !== RevisionRecord::LOCAL ) {
1078 throw new PreconditionException( 'RecentChangeLookup is only available for the local wiki' );
1079 }
1080
1081 $rc = $this->recentChangeLookup->getRecentChangeByConds(
1082 [
1083 'rc_this_oldid' => $rev->getId( $this->wikiId ),
1084 // rc_this_oldid does not have to be unique,
1085 // in particular, it is shared with categorization
1086 // changes. Prefer the original change because callers
1087 // often expect a change for patrolling.
1088 'rc_source' => [ RecentChange::SRC_EDIT, RecentChange::SRC_NEW, RecentChange::SRC_LOG ],
1089 ],
1090 __METHOD__,
1091 ( $flags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST
1092 );
1093
1094 // XXX: cache this locally? Glue it to the RevisionRecord?
1095 return $rc;
1096 }
1097
1117 private function loadSlotContent(
1118 SlotRecord $slot,
1119 ?string $blobData = null,
1120 ?string $blobFlags = null,
1121 ?string $blobFormat = null,
1122 int $queryFlags = 0
1123 ) {
1124 if ( $blobData !== null ) {
1125 $blobAddress = $slot->hasAddress() ? $slot->getAddress() : null;
1126
1127 if ( $blobFlags === null ) {
1128 // No blob flags, so use the blob verbatim.
1129 $data = $blobData;
1130 } else {
1131 try {
1132 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $blobAddress );
1133 } catch ( BadBlobException $e ) {
1134 throw new BadRevisionException( $e->getMessage(), [], 0, $e );
1135 }
1136
1137 if ( $data === false ) {
1138 throw new RevisionAccessException(
1139 'Failed to expand blob data using flags {flags} (key: {cache_key})',
1140 [
1141 'flags' => $blobFlags,
1142 'cache_key' => $blobAddress,
1143 ]
1144 );
1145 }
1146 }
1147
1148 } else {
1149 $address = $slot->getAddress();
1150 try {
1151 $data = $this->blobStore->getBlob( $address, $queryFlags );
1152 } catch ( BadBlobException $e ) {
1153 throw new BadRevisionException( $e->getMessage(), [], 0, $e );
1154 } catch ( BlobAccessException $e ) {
1155 throw new RevisionAccessException(
1156 'Failed to load data blob from {address} for revision {revision}. '
1157 . 'If this problem persists, use the findBadBlobs maintenance script '
1158 . 'to investigate the issue and mark the bad blobs.',
1159 [ 'address' => $e->getMessage(), 'revision' => $slot->getRevision() ],
1160 0,
1161 $e
1162 );
1163 }
1164 }
1165
1166 $model = $slot->getModel();
1167
1168 // If the content model is not known, don't fail here (T220594, T220793, T228921)
1169 if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1170 $this->logger->warning(
1171 "Undefined content model '$model', falling back to FallbackContent",
1172 [
1173 'content_address' => $slot->getAddress(),
1174 'rev_id' => $slot->getRevision(),
1175 'role_name' => $slot->getRole(),
1176 'model_name' => $model,
1177 'exception' => new RuntimeException()
1178 ]
1179 );
1180
1181 return new FallbackContent( $data, $model );
1182 }
1183
1184 return $this->contentHandlerFactory
1185 ->getContentHandler( $model )
1186 ->unserializeContent( $data, $blobFormat );
1187 }
1188
1206 public function getRevisionById( $id, $flags = 0, ?PageIdentity $page = null ) {
1207 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags, $page );
1208 }
1209
1226 public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) {
1227 $conds = $this->getPageConditions( $page );
1228 if ( !$conds ) {
1229 return null;
1230 }
1231 if ( !( $page instanceof PageIdentity ) ) {
1232 if ( !( $page instanceof PageReference ) ) {
1233 wfDeprecated( __METHOD__ . ' with a LinkTarget', '1.45' );
1234 }
1235 $page = null;
1236 }
1237
1238 if ( $revId ) {
1239 // Use the specified revision ID.
1240 // Note that we use newRevisionFromConds here because we want to retry
1241 // and fall back to primary DB if the page is not found on a replica.
1242 // Since the caller supplied a revision ID, we are pretty sure the revision is
1243 // supposed to exist, so we should try hard to find it.
1244 $conds['rev_id'] = $revId;
1245 return $this->newRevisionFromConds( $conds, $flags, $page );
1246 } else {
1247 // Use a join to get the latest revision.
1248 // Note that we don't use newRevisionFromConds here because we don't want to retry
1249 // and fall back to primary DB. The assumption is that we only want to force the fallback
1250 // if we are quite sure the revision exists because the caller supplied a revision ID.
1251 // If the page isn't found at all on a replica, it probably simply does not exist.
1252 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1253 $conds[] = 'rev_id=page_latest';
1254 return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1255 }
1256 }
1257
1274 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1275 $conds = [ 'page_id' => $pageId ];
1276 if ( $revId ) {
1277 // Use the specified revision ID.
1278 // Note that we use newRevisionFromConds here because we want to retry
1279 // and fall back to primary DB if the page is not found on a replica.
1280 // Since the caller supplied a revision ID, we are pretty sure the revision is
1281 // supposed to exist, so we should try hard to find it.
1282 $conds['rev_id'] = $revId;
1283 return $this->newRevisionFromConds( $conds, $flags );
1284 } else {
1285 // Use a join to get the latest revision.
1286 // Note that we don't use newRevisionFromConds here because we don't want to retry
1287 // and fall back to primary DB. The assumption is that we only want to force the fallback
1288 // if we are quite sure the revision exists because the caller supplied a revision ID.
1289 // If the page isn't found at all on a replica, it probably simply does not exist.
1290 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1291
1292 $conds[] = 'rev_id=page_latest';
1293
1294 return $this->loadRevisionFromConds( $db, $conds, $flags );
1295 }
1296 }
1297
1313 public function getRevisionByTimestamp(
1314 $page,
1315 string $timestamp,
1316 int $flags = IDBAccessObject::READ_NORMAL
1317 ): ?RevisionRecord {
1318 $conds = $this->getPageConditions( $page );
1319 if ( !$conds ) {
1320 return null;
1321 }
1322 if ( !( $page instanceof PageIdentity ) ) {
1323 if ( !( $page instanceof PageReference ) ) {
1324 wfDeprecated( __METHOD__ . ' with a LinkTarget', '1.45' );
1325 }
1326 $page = null;
1327 }
1328
1329 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1330 $conds['rev_timestamp'] = $db->timestamp( $timestamp );
1331 return $this->newRevisionFromConds( $conds, $flags, $page );
1332 }
1333
1341 private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) {
1342 // TODO: Find a way to add NS_MODULE from Scribunto here
1343 if ( $page->getNamespace() !== NS_TEMPLATE ) {
1344 $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page );
1345 return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1346 }
1347
1348 // TODO: These caches should not be needed. See T297147#7563670
1349 $res = $this->localCache->getWithSetCallback(
1350 $this->localCache->makeKey(
1351 'revision-slots',
1352 $page->getWikiId(),
1353 $page->getId( $page->getWikiId() ),
1354 $revId
1355 ),
1356 $this->localCache::TTL_HOUR,
1357 function () use ( $revId, $queryFlags, $page ) {
1358 return $this->cache->getWithSetCallback(
1359 $this->cache->makeKey(
1360 'revision-slots',
1361 $page->getWikiId(),
1362 $page->getId( $page->getWikiId() ),
1363 $revId
1364 ),
1365 WANObjectCache::TTL_DAY,
1366 function () use ( $revId, $queryFlags, $page ) {
1367 $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page );
1368 if ( !$res ) {
1369 // Avoid caching
1370 return false;
1371 }
1372 return $res;
1373 }
1374 );
1375 }
1376 );
1377 if ( !$res ) {
1378 $res = [];
1379 }
1380
1381 return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1382 }
1383
1384 private function loadSlotRecordsFromDb( int $revId, int $queryFlags, PageIdentity $page ): array {
1385 $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1386
1387 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1388 $res = $db->newSelectQueryBuilder()
1389 ->queryInfo( $revQuery )
1390 ->where( [ 'slot_revision_id' => $revId ] )
1391 ->recency( $queryFlags )
1392 ->caller( __METHOD__ )->fetchResultSet();
1393
1394 if ( !$res->numRows() && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
1395 // If we found no slots, try looking on the primary database (T212428, T252156)
1396 $this->logger->info(
1397 __METHOD__ . ' falling back to READ_LATEST.',
1398 [
1399 'revid' => $revId,
1400 'exception' => new RuntimeException(),
1401 ]
1402 );
1403 return $this->loadSlotRecordsFromDb(
1404 $revId,
1405 $queryFlags | IDBAccessObject::READ_LATEST,
1406 $page
1407 );
1408 }
1409 return iterator_to_array( $res );
1410 }
1411
1424 private function constructSlotRecords(
1425 $revId,
1426 $slotRows,
1427 $queryFlags,
1428 PageIdentity $page,
1429 $slotContents = null
1430 ) {
1431 $slots = [];
1432
1433 foreach ( $slotRows as $row ) {
1434 // Resolve role names and model names from in-memory cache, if they were not joined in.
1435 if ( !isset( $row->role_name ) ) {
1436 $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1437 }
1438
1439 if ( !isset( $row->model_name ) ) {
1440 if ( isset( $row->content_model ) ) {
1441 $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1442 } else {
1443 // We may get here if $row->model_name is set but null, perhaps because it
1444 // came from rev_content_model, which is NULL for the default model.
1445 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1446 $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1447 }
1448 }
1449
1450 // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1451 if ( isset( $row->blob_data ) ) {
1452 $slotContents[$row->content_address] = $row->blob_data;
1453 }
1454
1455 $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1456 $blob = null;
1457 if ( isset( $slotContents[$slot->getAddress()] ) ) {
1458 $blob = $slotContents[$slot->getAddress()];
1459 if ( $blob instanceof Content ) {
1460 return $blob;
1461 }
1462 }
1463 return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1464 };
1465
1466 $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1467 }
1468
1469 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1470 $this->logger->error(
1471 __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1472 [
1473 'revid' => $revId,
1474 'queryFlags' => $queryFlags,
1475 'exception' => new RuntimeException(),
1476 ]
1477 );
1478
1479 throw new RevisionAccessException(
1480 'Main slot of revision not found in database. See T212428.'
1481 );
1482 }
1483
1484 return $slots;
1485 }
1486
1500 private function newRevisionSlots(
1501 $revId,
1502 $slotRows,
1503 $queryFlags,
1504 PageIdentity $page
1505 ) {
1506 if ( $slotRows ) {
1507 $slots = new RevisionSlots(
1508 $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1509 );
1510 } else {
1511 $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) {
1512 return $this->loadSlotRecords( $revId, $queryFlags, $page );
1513 } );
1514 }
1515
1516 return $slots;
1517 }
1518
1540 $row,
1541 $queryFlags = 0,
1542 ?PageIdentity $page = null,
1543 array $overrides = []
1544 ) {
1545 return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides );
1546 }
1547
1560 public function newRevisionFromRow(
1561 $row,
1562 $queryFlags = 0,
1563 ?PageIdentity $page = null,
1564 $fromCache = false
1565 ) {
1566 return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache );
1567 }
1568
1588 stdClass $row,
1589 $slots,
1590 int $queryFlags = 0,
1591 ?PageIdentity $page = null,
1592 array $overrides = []
1593 ) {
1594 if ( !$page && isset( $overrides['title'] ) ) {
1595 if ( !( $overrides['title'] instanceof PageIdentity ) ) {
1596 throw new InvalidArgumentException( 'title field override must contain a PageIdentity object.' );
1597 }
1598
1599 $page = $overrides['title'];
1600 }
1601
1602 if ( $page === null ) {
1603 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1604 // Represent a non-existing page.
1605 // NOTE: The page title may be invalid by latest rules (T384628).
1606 $page = PageIdentityValue::localIdentity( 0, $row->ar_namespace, $row->ar_title );
1607 } else {
1608 throw new InvalidArgumentException(
1609 'A Title or ar_namespace and ar_title must be given'
1610 );
1611 }
1612 }
1613
1614 foreach ( $overrides as $key => $value ) {
1615 $field = "ar_$key";
1616 $row->$field = $value;
1617 }
1618
1619 try {
1620 $user = $this->actorStore->newActorFromRowFields(
1621 $row->ar_user ?? null,
1622 $row->ar_user_text ?? null,
1623 $row->ar_actor ?? null
1624 );
1625 } catch ( InvalidArgumentException $ex ) {
1626 $this->logger->warning( 'Could not load user for archive revision {rev_id}', [
1627 'ar_rev_id' => $row->ar_rev_id,
1628 'ar_actor' => $row->ar_actor ?? 'null',
1629 'ar_user_text' => $row->ar_user_text ?? 'null',
1630 'ar_user' => $row->ar_user ?? 'null',
1631 'exception' => $ex
1632 ] );
1633 $user = $this->actorStore->getUnknownActor();
1634 }
1635
1636 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1637 // Legacy because $row may have come from self::selectFields()
1638 $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1639
1640 if ( !( $slots instanceof RevisionSlots ) ) {
1641 $slots = $this->newRevisionSlots( (int)$row->ar_rev_id, $slots, $queryFlags, $page );
1642 }
1643 return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId );
1644 }
1645
1664 stdClass $row,
1665 $slots,
1666 int $queryFlags = 0,
1667 ?PageIdentity $page = null,
1668 bool $fromCache = false
1669 ) {
1670 if ( !$page ) {
1671 if ( isset( $row->page_id )
1672 && isset( $row->page_namespace )
1673 && isset( $row->page_title )
1674 ) {
1675 $page = new PageIdentityValue(
1676 (int)$row->page_id,
1677 (int)$row->page_namespace,
1678 $row->page_title,
1679 $this->wikiId
1680 );
1681
1682 $page = $this->wrapPage( $page );
1683 } else {
1684 $pageId = (int)( $row->rev_page ?? 0 );
1685 $revId = (int)( $row->rev_id ?? 0 );
1686
1687 $page = $this->getPage( $pageId, $revId, $queryFlags );
1688 }
1689 } else {
1690 $page = $this->ensureRevisionRowMatchesPage( $row, $page );
1691 }
1692
1693 if ( !$page ) {
1694 // This should already have been caught about, but apparently
1695 // it not always is, see T286877.
1696 throw new RevisionAccessException(
1697 "Failed to determine page associated with revision {$row->rev_id}"
1698 );
1699 }
1700
1701 try {
1702 $user = $this->actorStore->newActorFromRowFields(
1703 $row->rev_user ?? null,
1704 $row->rev_user_text ?? null,
1705 $row->rev_actor ?? null
1706 );
1707 } catch ( InvalidArgumentException $ex ) {
1708 $this->logger->warning( 'Could not load user for revision {rev_id}', [
1709 'rev_id' => $row->rev_id,
1710 'rev_actor' => $row->rev_actor ?? 'null',
1711 'rev_user_text' => $row->rev_user_text ?? 'null',
1712 'rev_user' => $row->rev_user ?? 'null',
1713 'exception' => $ex
1714 ] );
1715 $user = $this->actorStore->getUnknownActor();
1716 }
1717
1718 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1719 // Legacy because $row may have come from self::selectFields()
1720 $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1721
1722 if ( !( $slots instanceof RevisionSlots ) ) {
1723 $slots = $this->newRevisionSlots( (int)$row->rev_id, $slots, $queryFlags, $page );
1724 }
1725
1726 // If this is a cached row, instantiate a cache-aware RevisionRecord to avoid stale data.
1727 if ( $fromCache ) {
1728 $rev = new RevisionStoreCacheRecord(
1729 function ( $revId ) use ( $queryFlags ) {
1730 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1731 $row = $this->fetchRevisionRowFromConds(
1732 $db,
1733 [ 'rev_id' => intval( $revId ) ]
1734 );
1735 if ( !$row && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
1736 // If we found no slots, try looking on the primary database (T259738)
1737 $this->logger->info(
1738 'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1739 [
1740 'revid' => $revId,
1741 'exception' => new RuntimeException(),
1742 ]
1743 );
1744 $dbw = $this->getDBConnectionRefForQueryFlags( IDBAccessObject::READ_LATEST );
1745 $row = $this->fetchRevisionRowFromConds(
1746 $dbw,
1747 [ 'rev_id' => intval( $revId ) ]
1748 );
1749 }
1750 if ( !$row ) {
1751 return [ null, null ];
1752 }
1753 return [
1754 $row->rev_deleted,
1755 $this->actorStore->newActorFromRowFields(
1756 $row->rev_user ?? null,
1757 $row->rev_user_text ?? null,
1758 $row->rev_actor ?? null
1759 )
1760 ];
1761 },
1762 $page, $user, $comment, $row, $slots, $this->wikiId
1763 );
1764 } else {
1765 $rev = new RevisionStoreRecord(
1766 $page, $user, $comment, $row, $slots, $this->wikiId );
1767 }
1768 return $rev;
1769 }
1770
1782 private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) {
1783 $revId = (int)( $row->rev_id ?? 0 );
1784 $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1785 $expectedPageId = $page->getId( $this->wikiId );
1786 // Avoid fatal error when the Title's ID changed, T246720
1787 if ( $revPageId && $expectedPageId && $revPageId !== $expectedPageId ) {
1788 // NOTE: PageStore::getPageByReference may use the page ID, which we don't want here.
1789 $pageRec = $this->pageStore->getPageByName(
1790 $page->getNamespace(),
1791 $page->getDBkey(),
1792 IDBAccessObject::READ_LATEST
1793 );
1794 $masterPageId = $pageRec->getId( $this->wikiId );
1795 $masterLatest = $pageRec->getLatest( $this->wikiId );
1796 if ( $revPageId === $masterPageId ) {
1797 if ( $page instanceof Title ) {
1798 // If we were using a Title object, keep using it, but update the page ID.
1799 // This way, we don't unexpectedly mix Titles with immutable value objects.
1800 $page->resetArticleID( $masterPageId );
1801
1802 } else {
1803 $page = $pageRec;
1804 }
1805
1806 $this->logger->info(
1807 "Encountered stale Title object",
1808 [
1809 'page_id_stale' => $expectedPageId,
1810 'page_id_reloaded' => $masterPageId,
1811 'page_latest' => $masterLatest,
1812 'rev_id' => $revId,
1813 'exception' => new RuntimeException(),
1814 ] + $context
1815 );
1816 } else {
1817 $expectedTitle = (string)$page;
1818 if ( $page instanceof Title ) {
1819 // If we started with a Title, keep using a Title.
1820 $page = $this->titleFactory->newFromID( $revPageId );
1821 } else {
1822 $page = $pageRec;
1823 }
1824
1825 // This could happen if a caller to e.g. getRevisionById supplied a Title that is
1826 // plain wrong. In this case, we should ideally throw an IllegalArgumentException.
1827 // However, it is more likely that we encountered a race condition during a page
1828 // move (T268910, T279832) or database corruption (T263340). That situation
1829 // should not be ignored, but we can allow the request to continue in a reasonable
1830 // manner without breaking things for the user.
1831 $this->logger->error(
1832 "Encountered mismatching Title object (see T259022, T268910, T279832, T263340)",
1833 [
1834 'expected_page_id' => $masterPageId,
1835 'expected_page_title' => $expectedTitle,
1836 'rev_page' => $revPageId,
1837 'rev_page_title' => (string)$page,
1838 'page_latest' => $masterLatest,
1839 'rev_id' => $revId,
1840 'exception' => new RuntimeException(),
1841 ] + $context
1842 );
1843 }
1844 }
1845
1846 // @phan-suppress-next-line PhanTypeMismatchReturnNullable getPageByName/newFromID should not return null
1847 return $page;
1848 }
1849
1875 public function newRevisionsFromBatch(
1876 $rows,
1877 array $options = [],
1878 $queryFlags = 0,
1879 ?PageIdentity $page = null
1880 ) {
1881 $result = new StatusValue();
1882 $archiveMode = $options['archive'] ?? false;
1883
1884 if ( $archiveMode ) {
1885 $revIdField = 'ar_rev_id';
1886 } else {
1887 $revIdField = 'rev_id';
1888 }
1889
1890 $rowsByRevId = [];
1891 $pageIdsToFetchTitles = [];
1892 $titlesByPageKey = [];
1893 foreach ( $rows as $row ) {
1894 if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1895 $result->warning(
1896 'internalerror_info',
1897 "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1898 );
1899 }
1900
1901 // Attach a page key to the row, so we can find and reuse Title objects easily.
1902 $row->_page_key =
1903 $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1904
1905 if ( $page ) {
1906 if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1907 throw new InvalidArgumentException(
1908 "Revision {$row->$revIdField} doesn't belong to page "
1909 . $this->getArticleId( $page )
1910 );
1911 }
1912
1913 if ( $archiveMode
1914 && ( $row->ar_namespace != $page->getNamespace()
1915 || $row->ar_title !== $page->getDBkey() )
1916 ) {
1917 throw new InvalidArgumentException(
1918 "Revision {$row->$revIdField} doesn't belong to page "
1919 . $page
1920 );
1921 }
1922 } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1923 if ( isset( $row->page_namespace ) && isset( $row->page_title )
1924 // This should always be true, but just in case we don't have a page_id
1925 // set or it doesn't match rev_page, let's fetch the title again.
1926 && isset( $row->page_id ) && isset( $row->rev_page )
1927 && $row->rev_page === $row->page_id
1928 ) {
1929 $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1930 } elseif ( $archiveMode ) {
1931 // Can't look up deleted pages by ID, but we have namespace and title
1932 $titlesByPageKey[ $row->_page_key ] =
1933 Title::makeTitle( $row->ar_namespace, $row->ar_title );
1934 } else {
1935 $pageIdsToFetchTitles[] = $row->rev_page;
1936 }
1937 }
1938 $rowsByRevId[$row->$revIdField] = $row;
1939 }
1940
1941 if ( !$rowsByRevId ) {
1942 $result->setResult( true, [] );
1943 return $result;
1944 }
1945
1946 // If the page is not supplied, batch-fetch Title objects.
1947 if ( $page ) {
1948 // same logic as for $row->_page_key above
1949 $pageKey = $archiveMode
1950 ? $page->getNamespace() . ':' . $page->getDBkey()
1951 : $this->getArticleId( $page );
1952
1953 $titlesByPageKey[$pageKey] = $page;
1954 } elseif ( $pageIdsToFetchTitles ) {
1955 // Note: when we fetch titles by ID, the page key is also the ID.
1956 // We should never get here if $archiveMode is true.
1957 Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1958
1959 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1960 $pageRecords = $this->pageStore
1961 ->newSelectQueryBuilder()
1962 ->wherePageIds( $pageIdsToFetchTitles )
1963 ->caller( __METHOD__ )
1964 ->fetchPageRecordArray();
1965 // Cannot array_merge because it re-indexes entries
1966 $titlesByPageKey = $pageRecords + $titlesByPageKey;
1967 }
1968
1969 // which method to use for creating RevisionRecords
1970 $newRevisionRecord = $archiveMode
1971 ? $this->newRevisionFromArchiveRowAndSlots( ... )
1972 : $this->newRevisionFromRowAndSlots( ... );
1973
1974 if ( !isset( $options['slots'] ) ) {
1975 $result->setResult(
1976 true,
1977 array_map(
1978 static function ( $row )
1979 use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1980 try {
1981 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1982 $result->warning(
1983 'internalerror_info',
1984 "Couldn't find title for rev {$row->$revIdField} "
1985 . "(page key {$row->_page_key})"
1986 );
1987 return null;
1988 }
1989 return $newRevisionRecord( $row, null, $queryFlags,
1990 $titlesByPageKey[ $row->_page_key ] );
1991 } catch ( MWException $e ) {
1992 $result->warning( 'internalerror_info', $e->getMessage() );
1993 return null;
1994 }
1995 },
1996 $rowsByRevId
1997 )
1998 );
1999 return $result;
2000 }
2001
2002 $slotRowOptions = [
2003 'slots' => $options['slots'] ?? true,
2004 'blobs' => $options['content'] ?? false,
2005 ];
2006
2007 if ( is_array( $slotRowOptions['slots'] )
2008 && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
2009 ) {
2010 // Make sure the main slot is always loaded, RevisionRecord requires this.
2011 $slotRowOptions['slots'][] = SlotRecord::MAIN;
2012 }
2013
2014 $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2015
2016 $result->merge( $slotRowsStatus );
2017 $slotRowsByRevId = $slotRowsStatus->getValue();
2018
2019 $result->setResult(
2020 true,
2021 array_map(
2022 function ( $row )
2023 use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
2024 $revIdField, $newRevisionRecord
2025 ) {
2026 if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
2027 $result->warning(
2028 'internalerror_info',
2029 "Couldn't find slots for rev {$row->$revIdField}"
2030 );
2031 return null;
2032 }
2033 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2034 $result->warning(
2035 'internalerror_info',
2036 "Couldn't find title for rev {$row->$revIdField} "
2037 . "(page key {$row->_page_key})"
2038 );
2039 return null;
2040 }
2041 try {
2042 return $newRevisionRecord(
2043 $row,
2044 new RevisionSlots(
2045 $this->constructSlotRecords(
2046 $row->$revIdField,
2047 $slotRowsByRevId[$row->$revIdField],
2048 $queryFlags,
2049 $titlesByPageKey[$row->_page_key]
2050 )
2051 ),
2052 $queryFlags,
2053 $titlesByPageKey[$row->_page_key]
2054 );
2055 } catch ( MWException | ParameterAssertionException $e ) {
2056 $result->warning( 'internalerror_info', $e->getMessage() );
2057 return null;
2058 }
2059 },
2060 $rowsByRevId
2061 )
2062 );
2063 return $result;
2064 }
2065
2090 private function getSlotRowsForBatch(
2091 $rowsOrIds,
2092 array $options = [],
2093 $queryFlags = 0
2094 ) {
2095 $result = new StatusValue();
2096
2097 $revIds = [];
2098 foreach ( $rowsOrIds as $id ) {
2099 if ( $id instanceof stdClass ) {
2100 $id = $id->ar_rev_id ?? $id->rev_id;
2101 }
2102 $revIds[] = (int)$id;
2103 }
2104
2105 // Nothing to do.
2106 // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2107 if ( !$revIds ) {
2108 $result->setResult( true, [] );
2109 return $result;
2110 }
2111
2112 // We need to set the `content` flag to join in content meta-data
2113 $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
2114 $revIdField = $slotQueryInfo['keys']['rev_id'];
2115 $slotQueryConds = [ $revIdField => $revIds ];
2116
2117 if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2118 $slotIds = [];
2119 foreach ( $options['slots'] as $slot ) {
2120 try {
2121 $slotIds[] = $this->slotRoleStore->getId( $slot );
2122 } catch ( NameTableAccessException ) {
2123 // Do not fail when slot has no id (unused slot)
2124 // This also means for this slot are never data in the database
2125 }
2126 }
2127 if ( $slotIds === [] ) {
2128 // Degenerate case: return no slots for each revision.
2129 $result->setResult( true, array_fill_keys( $revIds, [] ) );
2130 return $result;
2131 }
2132
2133 $roleIdField = $slotQueryInfo['keys']['role_id'];
2134 $slotQueryConds[$roleIdField] = $slotIds;
2135 }
2136
2137 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2138 $slotRows = $db->newSelectQueryBuilder()
2139 ->queryInfo( $slotQueryInfo )
2140 ->where( $slotQueryConds )
2141 ->caller( __METHOD__ )
2142 ->fetchResultSet();
2143
2144 $slotContents = null;
2145 if ( $options['blobs'] ?? false ) {
2146 $blobAddresses = [];
2147 foreach ( $slotRows as $slotRow ) {
2148 $blobAddresses[] = $slotRow->content_address;
2149 }
2150 $slotContentFetchStatus = $this->blobStore
2151 ->getBlobBatch( $blobAddresses, $queryFlags );
2152 foreach ( $slotContentFetchStatus->getMessages() as $msg ) {
2153 $result->warning( $msg );
2154 }
2155 $slotContents = $slotContentFetchStatus->getValue();
2156 }
2157
2158 $slotRowsByRevId = [];
2159 foreach ( $slotRows as $slotRow ) {
2160 if ( $slotContents === null ) {
2161 // nothing to do
2162 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2163 $slotRow->blob_data = $slotContents[$slotRow->content_address];
2164 } else {
2165 $result->warning(
2166 'internalerror_info',
2167 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2168 );
2169 $slotRow->blob_data = null;
2170 }
2171
2172 // conditional needed for SCHEMA_COMPAT_READ_OLD
2173 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2174 $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2175 }
2176
2177 // conditional needed for SCHEMA_COMPAT_READ_OLD
2178 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2179 $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2180 }
2181
2182 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2183 }
2184
2185 $result->setResult( true, $slotRowsByRevId );
2186 return $result;
2187 }
2188
2211 $rowsOrIds,
2212 $slots = null,
2213 $queryFlags = 0
2214 ) {
2215 $result = $this->getSlotRowsForBatch(
2216 $rowsOrIds,
2217 [ 'slots' => $slots, 'blobs' => true ],
2218 $queryFlags
2219 );
2220
2221 if ( $result->isOK() ) {
2222 // strip out all internal meta data that we don't want to expose
2223 foreach ( $result->value as $revId => $rowsByRole ) {
2224 foreach ( $rowsByRole as $role => $slotRow ) {
2225 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2226 // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2227 // if we didn't ask for it.
2228 unset( $result->value[$revId][$role] );
2229 continue;
2230 }
2231
2232 $result->value[$revId][$role] = (object)[
2233 'blob_data' => $slotRow->blob_data,
2234 'model_name' => $slotRow->model_name,
2235 ];
2236 }
2237 }
2238 }
2239
2240 return $result;
2241 }
2242
2259 private function newRevisionFromConds(
2260 array $conditions,
2261 int $flags = IDBAccessObject::READ_NORMAL,
2262 ?PageIdentity $page = null,
2263 array $options = []
2264 ) {
2265 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2266 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2267
2268 // Make sure new pending/committed revision are visible later on
2269 // within web requests to certain avoid bugs like T93866 and T94407.
2270 if ( !$rev
2271 && !( $flags & IDBAccessObject::READ_LATEST )
2272 && $this->loadBalancer->hasStreamingReplicaServers()
2273 && $this->loadBalancer->hasOrMadeRecentPrimaryChanges()
2274 ) {
2275 $flags = IDBAccessObject::READ_LATEST;
2276 $dbw = $this->getPrimaryConnection();
2277 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2278 }
2279
2280 return $rev;
2281 }
2282
2297 private function loadRevisionFromConds(
2298 IReadableDatabase $db,
2299 array $conditions,
2300 int $flags = IDBAccessObject::READ_NORMAL,
2301 ?PageIdentity $page = null,
2302 array $options = []
2303 ) {
2304 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2305 if ( $row ) {
2306 return $this->newRevisionFromRow( $row, $flags, $page );
2307 }
2308
2309 return null;
2310 }
2311
2316 private function checkDatabaseDomain( IReadableDatabase $db ) {
2317 $dbDomain = $db->getDomainID();
2318 $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2319 if ( $dbDomain === $storeDomain ) {
2320 return;
2321 }
2322
2323 throw new RuntimeException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2324 }
2325
2339 private function fetchRevisionRowFromConds(
2340 IReadableDatabase $db,
2341 array $conditions,
2342 int $flags = IDBAccessObject::READ_NORMAL,
2343 array $options = []
2344 ) {
2345 $this->checkDatabaseDomain( $db );
2346
2347 $queryBuilder = $this->newSelectQueryBuilder( $db )
2348 ->joinComment()
2349 ->joinPage()
2350 ->joinUser()
2351 ->where( $conditions )
2352 ->options( $options );
2353 if ( ( $flags & IDBAccessObject::READ_LOCKING ) == IDBAccessObject::READ_LOCKING ) {
2354 $queryBuilder->forUpdate();
2355 }
2356 return $queryBuilder->caller( __METHOD__ )->fetchRow();
2357 }
2358
2381 public function getQueryInfo( $options = [] ) {
2382 $ret = [
2383 'tables' => [
2384 'revision',
2385 'actor_rev_user' => 'actor',
2386 ],
2387 'fields' => [
2388 'rev_id',
2389 'rev_page',
2390 'rev_actor' => 'rev_actor',
2391 'rev_user' => 'actor_rev_user.actor_user',
2392 'rev_user_text' => 'actor_rev_user.actor_name',
2393 'rev_timestamp',
2394 'rev_minor_edit',
2395 'rev_deleted',
2396 'rev_len',
2397 'rev_parent_id',
2398 ],
2399 'joins' => [
2400 'actor_rev_user' => [ 'JOIN', "actor_rev_user.actor_id = rev_actor" ],
2401 ]
2402 ];
2403
2404 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2405 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2406 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2407 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2408
2409 if ( in_array( 'page', $options, true ) ) {
2410 $ret['tables'][] = 'page';
2411 $ret['fields'] = array_merge( $ret['fields'], [
2412 'page_namespace',
2413 'page_title',
2414 'page_id',
2415 'page_latest',
2416 'page_is_redirect',
2417 'page_len',
2418 ] );
2419 $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2420 }
2421
2422 if ( in_array( 'user', $options, true ) ) {
2423 $ret['tables'][] = 'user';
2424 $ret['fields'][] = 'user_name';
2425 $ret['joins']['user'] = [
2426 'LEFT JOIN',
2427 [ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ]
2428 ];
2429 }
2430
2431 if ( in_array( 'text', $options, true ) ) {
2432 throw new InvalidArgumentException(
2433 'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2434 );
2435 }
2436
2437 return $ret;
2438 }
2439
2446
2453
2475 public function getSlotsQueryInfo( $options = [] ) {
2476 $ret = [
2477 'tables' => [ 'slots' ],
2478 'fields' => [
2479 'slot_revision_id',
2480 'slot_content_id',
2481 'slot_origin',
2482 'slot_role_id',
2483 ],
2484 'joins' => [],
2485 'keys' => [
2486 'rev_id' => 'slot_revision_id',
2487 'role_id' => 'slot_role_id',
2488 ],
2489 ];
2490
2491 if ( in_array( 'role', $options, true ) ) {
2492 // Use left join to attach role name, so we still find the revision row even
2493 // if the role name is missing. This triggers a more obvious failure mode.
2494 $ret['tables'][] = 'slot_roles';
2495 $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2496 $ret['fields'][] = 'role_name';
2497 }
2498
2499 if ( in_array( 'content', $options, true ) ) {
2500 $ret['keys']['model_id'] = 'content_model';
2501
2502 $ret['tables'][] = 'content';
2503 $ret['fields'] = array_merge( $ret['fields'], [
2504 'content_size',
2505 'content_sha1',
2506 'content_address',
2507 'content_model',
2508 ] );
2509 $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2510
2511 if ( in_array( 'model', $options, true ) ) {
2512 // Use left join to attach model name, so we still find the revision row even
2513 // if the model name is missing. This triggers a more obvious failure mode.
2514 $ret['tables'][] = 'content_models';
2515 $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2516 $ret['fields'][] = 'model_name';
2517 }
2518
2519 }
2520
2521 return $ret;
2522 }
2523
2532 public function isRevisionRow( $row, string $table = '' ) {
2533 if ( !( $row instanceof stdClass ) ) {
2534 return false;
2535 }
2536 $queryInfo = $table === 'archive' ? $this->getArchiveQueryInfo() : $this->getQueryInfo();
2537 foreach ( $queryInfo['fields'] as $alias => $field ) {
2538 $name = is_numeric( $alias ) ? $field : $alias;
2539 if ( !property_exists( $row, $name ) ) {
2540 return false;
2541 }
2542 }
2543 return true;
2544 }
2545
2565 public function getArchiveQueryInfo() {
2566 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2567 $ret = [
2568 'tables' => [
2569 'archive',
2570 'archive_actor' => 'actor'
2571 ] + $commentQuery['tables'],
2572 'fields' => [
2573 'ar_id',
2574 'ar_page_id',
2575 'ar_namespace',
2576 'ar_title',
2577 'ar_rev_id',
2578 'ar_timestamp',
2579 'ar_minor_edit',
2580 'ar_deleted',
2581 'ar_len',
2582 'ar_parent_id',
2583 'ar_actor',
2584 'ar_user' => 'archive_actor.actor_user',
2585 'ar_user_text' => 'archive_actor.actor_name',
2586 ] + $commentQuery['fields'],
2587 'joins' => [
2588 'archive_actor' => [ 'JOIN', 'actor_id=ar_actor' ]
2589 ] + $commentQuery['joins'],
2590 ];
2591
2592 return $ret;
2593 }
2594
2604 public function getRevisionSizes( array $revIds ) {
2605 $dbr = $this->getReplicaConnection();
2606 $revLens = [];
2607 if ( !$revIds ) {
2608 return $revLens; // empty
2609 }
2610
2611 $res = $dbr->newSelectQueryBuilder()
2612 ->select( [ 'rev_id', 'rev_len' ] )
2613 ->from( 'revision' )
2614 ->where( [ 'rev_id' => $revIds ] )
2615 ->caller( __METHOD__ )->fetchResultSet();
2616
2617 foreach ( $res as $row ) {
2618 $revLens[$row->rev_id] = intval( $row->rev_len );
2619 }
2620
2621 return $revLens;
2622 }
2623
2632 private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2633 $op = $dir === 'next' ? '>' : '<';
2634 $sort = $dir === 'next' ? 'ASC' : 'DESC';
2635
2636 $revisionIdValue = $rev->getId( $this->wikiId );
2637
2638 if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) {
2639 // revision is unsaved or otherwise incomplete
2640 return null;
2641 }
2642
2643 if ( $rev instanceof RevisionArchiveRecord ) {
2644 // revision is deleted, so it's not part of the page history
2645 return null;
2646 }
2647
2648 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2649
2650 $ts = $rev->getTimestamp() ?? $this->getTimestampFromId( $revisionIdValue, $flags );
2651 if ( $ts === false ) {
2652 // XXX Should this be moved into getTimestampFromId?
2653 $ts = $db->newSelectQueryBuilder()
2654 ->select( 'ar_timestamp' )
2655 ->from( 'archive' )
2656 ->where( [ 'ar_rev_id' => $revisionIdValue ] )
2657 ->caller( __METHOD__ )->fetchField();
2658 if ( $ts === false ) {
2659 // XXX Is this reachable? How can we have a page id but no timestamp?
2660 return null;
2661 }
2662 }
2663
2664 $revId = $db->newSelectQueryBuilder()
2665 ->select( 'rev_id' )
2666 ->from( 'revision' )
2667 ->where( [
2668 'rev_page' => $rev->getPageId( $this->wikiId ),
2669 $db->buildComparison( $op, [
2670 'rev_timestamp' => $db->timestamp( $ts ),
2671 'rev_id' => $revisionIdValue,
2672 ] ),
2673 ] )
2674 ->orderBy( [ 'rev_timestamp', 'rev_id' ], $sort )
2675 ->ignoreIndex( 'rev_timestamp' ) // Probably needed for T159319
2676 ->caller( __METHOD__ )
2677 ->fetchField();
2678
2679 if ( $revId === false ) {
2680 return null;
2681 }
2682
2683 return $this->getRevisionById( intval( $revId ), $flags );
2684 }
2685
2700 public function getPreviousRevision( RevisionRecord $rev, $flags = IDBAccessObject::READ_NORMAL ) {
2701 return $this->getRelativeRevision( $rev, $flags, 'prev' );
2702 }
2703
2715 public function getNextRevision( RevisionRecord $rev, $flags = IDBAccessObject::READ_NORMAL ) {
2716 return $this->getRelativeRevision( $rev, $flags, 'next' );
2717 }
2718
2730 private function getPreviousRevisionId( IReadableDatabase $db, RevisionRecord $rev ) {
2731 $this->checkDatabaseDomain( $db );
2732
2733 if ( $rev->getPageId( $this->wikiId ) === null ) {
2734 return 0;
2735 }
2736 # Use page_latest if ID is not given
2737 if ( !$rev->getId( $this->wikiId ) ) {
2738 $prevId = $db->newSelectQueryBuilder()
2739 ->select( 'page_latest' )
2740 ->from( 'page' )
2741 ->where( [ 'page_id' => $rev->getPageId( $this->wikiId ) ] )
2742 ->caller( __METHOD__ )->fetchField();
2743 } else {
2744 $prevId = $db->newSelectQueryBuilder()
2745 ->select( 'rev_id' )
2746 ->from( 'revision' )
2747 ->where( [ 'rev_page' => $rev->getPageId( $this->wikiId ) ] )
2748 ->andWhere( $db->expr( 'rev_id', '<', $rev->getId( $this->wikiId ) ) )
2749 ->orderBy( 'rev_id DESC' )
2750 ->caller( __METHOD__ )->fetchField();
2751 }
2752 return intval( $prevId );
2753 }
2754
2767 public function getTimestampFromId( $id, $flags = 0 ) {
2768 if ( $id instanceof Title ) {
2769 // Old deprecated calling convention supported for backwards compatibility
2770 $id = $flags;
2771 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2772 }
2773
2774 // T270149: Bail out if we know the query will definitely return false. Some callers are
2775 // passing RevisionRecord::getId() call directly as $id which can possibly return null.
2776 // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL'
2777 // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null.
2778 // @todo typehint $id and remove the null check
2779 if ( $id === null || $id <= 0 ) {
2780 return false;
2781 }
2782
2783 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2784
2785 $timestamp =
2787 ->select( 'rev_timestamp' )
2788 ->from( 'revision' )
2789 ->where( [ 'rev_id' => $id ] )
2790 ->caller( __METHOD__ )->fetchField();
2791
2792 return ( $timestamp !== false ) ? MWTimestamp::convert( TS::MW, $timestamp ) : false;
2793 }
2794
2804 public function countRevisionsByPageId( IReadableDatabase $db, $id ) {
2805 $this->checkDatabaseDomain( $db );
2806
2807 $row = $db->newSelectQueryBuilder()
2808 ->select( [ 'revCount' => 'COUNT(*)' ] )
2809 ->from( 'revision' )
2810 ->where( [ 'rev_page' => $id ] )
2811 ->caller( __METHOD__ )->fetchRow();
2812 if ( $row ) {
2813 return intval( $row->revCount );
2814 }
2815 return 0;
2816 }
2817
2828 $id = $this->getArticleId( $page );
2829 if ( $id ) {
2830 return $this->countRevisionsByPageId( $db, $id );
2831 }
2832 return 0;
2833 }
2834
2853 public function userWasLastToEdit( IReadableDatabase $db, $pageId, $userId, $since ) {
2854 $this->checkDatabaseDomain( $db );
2855
2856 if ( !$userId ) {
2857 return false;
2858 }
2859
2860 $queryBuilder = $this->newSelectQueryBuilder( $db )
2861 ->where( [
2862 'rev_page' => $pageId,
2863 $db->expr( 'rev_timestamp', '>', $db->timestamp( $since ) )
2864 ] )
2865 ->orderBy( 'rev_timestamp', SelectQueryBuilder::SORT_ASC )
2866 ->limit( 50 );
2867 $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
2868 foreach ( $res as $row ) {
2869 if ( $row->rev_user != $userId ) {
2870 return false;
2871 }
2872 }
2873 return true;
2874 }
2875
2890 public function getKnownLatestRevision( PageIdentity $page, $revId = 0 ) {
2891 $db = $this->getReplicaConnection();
2892 $revIdPassed = $revId;
2893 $pageId = $this->getArticleId( $page );
2894 if ( !$pageId ) {
2895 return false;
2896 }
2897
2898 if ( !$revId ) {
2899 if ( $page instanceof Title ) {
2900 $revId = $page->getLatestRevID();
2901 } else {
2902 $pageRecord = $this->pageStore->getPageByReference( $page );
2903 if ( $pageRecord ) {
2904 $revId = $pageRecord->getLatest( $this->getWikiId() );
2905 }
2906 }
2907 }
2908
2909 if ( !$revId ) {
2910 $this->logger->warning(
2911 'No latest revision known for page {page} even though it exists with page ID {page_id}', [
2912 'page' => $page->__toString(),
2913 'page_id' => $pageId,
2914 'wiki_id' => $this->getWikiId() ?: 'local',
2915 ] );
2916 return false;
2917 }
2918
2919 // Load the row from cache if possible. If not possible, populate the cache.
2920 // As a minor optimization, remember if this was a cache hit or miss.
2921 // We can sometimes avoid a database query later if this is a cache miss.
2922 $fromCache = true;
2923 $row = $this->cache->getWithSetCallback(
2924 // Page/rev IDs passed in from DB to reflect history merges
2925 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2926 WANObjectCache::TTL_WEEK,
2927 function () use ( $db, $revId, &$fromCache ) {
2928 $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2929 if ( $row ) {
2930 $fromCache = false;
2931 }
2932 return $row; // don't cache negatives
2933 }
2934 );
2935
2936 // Reflect revision deletion and user renames.
2937 if ( $row ) {
2938 $title = $this->ensureRevisionRowMatchesPage( $row, $page, [
2939 'from_cache_flag' => $fromCache,
2940 'page_id_initial' => $pageId,
2941 'rev_id_used' => $revId,
2942 'rev_id_requested' => $revIdPassed,
2943 ] );
2944
2945 return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2946 } else {
2947 return false;
2948 }
2949 }
2950
2957 public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) {
2958 return $this->getKnownLatestRevision( $page, $revId );
2959 }
2960
2969 public function getFirstRevision(
2970 $page,
2971 int $flags = IDBAccessObject::READ_NORMAL
2972 ): ?RevisionRecord {
2973 $conds = $this->getPageConditions( $page );
2974 if ( !$conds ) {
2975 return null;
2976 }
2977 if ( !( $page instanceof PageIdentity ) ) {
2978 if ( !( $page instanceof PageReference ) ) {
2979 wfDeprecated( __METHOD__ . ' with a LinkTarget', '1.45' );
2980 }
2981 $page = null;
2982 }
2983
2984 return $this->newRevisionFromConds( $conds, $flags, $page,
2985 [
2986 'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
2987 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
2988 ]
2989 );
2990 }
2991
3003 private function getRevisionRowCacheKey( IReadableDatabase $db, $pageId, $revId ) {
3004 return $this->cache->makeGlobalKey(
3005 self::ROW_CACHE_KEY,
3006 $db->getDomainID(),
3007 $pageId,
3008 $revId
3009 );
3010 }
3011
3018 private function assertRevisionParameter( $paramName, $pageId, ?RevisionRecord $rev = null ) {
3019 if ( $rev ) {
3020 if ( $rev->getId( $this->wikiId ) === null ) {
3021 throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
3022 }
3023 if ( $rev->getPageId( $this->wikiId ) !== $pageId ) {
3024 throw new InvalidArgumentException(
3025 "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
3026 );
3027 }
3028 }
3029 }
3030
3035 private function getPageConditions( LinkTarget|PageReference $page ): ?array {
3036 if ( $page instanceof PageIdentity ) {
3037 return $page->exists() ? [ 'page_id' => $page->getId( $this->wikiId ) ] : null;
3038 } elseif ( $page instanceof PageReference ) {
3039 if ( $page->getWikiId() !== $this->wikiId ) {
3040 throw new InvalidArgumentException( 'Non-matching wiki ID for PageReference' );
3041 }
3042 return [
3043 'page_namespace' => $page->getNamespace(),
3044 'page_title' => $page->getDBkey(),
3045 ];
3046 } else {
3047 // Only resolve LinkTarget when operating in the context of the local wiki (T248756)
3048 if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
3049 throw new InvalidArgumentException( 'Cannot use non-local LinkTarget' );
3050 }
3051 return [
3052 'page_namespace' => $page->getNamespace(),
3053 'page_title' => $page->getDBkey(),
3054 ];
3055 }
3056 }
3057
3072 private function getRevisionLimitConditions(
3073 ISQLPlatform $dbr,
3074 ?RevisionRecord $old = null,
3075 ?RevisionRecord $new = null,
3076 $options = []
3077 ) {
3078 $options = (array)$options;
3079 if ( in_array( self::INCLUDE_OLD, $options ) || in_array( self::INCLUDE_BOTH, $options ) ) {
3080 $oldCmp = '>=';
3081 } else {
3082 $oldCmp = '>';
3083 }
3084 if ( in_array( self::INCLUDE_NEW, $options ) || in_array( self::INCLUDE_BOTH, $options ) ) {
3085 $newCmp = '<=';
3086 } else {
3087 $newCmp = '<';
3088 }
3089
3090 $conds = [];
3091 if ( $old ) {
3092 $conds[] = $dbr->buildComparison( $oldCmp, [
3093 'rev_timestamp' => $dbr->timestamp( $old->getTimestamp() ),
3094 'rev_id' => $old->getId( $this->wikiId ),
3095 ] );
3096 }
3097 if ( $new ) {
3098 $conds[] = $dbr->buildComparison( $newCmp, [
3099 'rev_timestamp' => $dbr->timestamp( $new->getTimestamp() ),
3100 'rev_id' => $new->getId( $this->wikiId ),
3101 ] );
3102 }
3103 return $conds;
3104 }
3105
3132 public function getRevisionIdsBetween(
3133 int $pageId,
3134 ?RevisionRecord $old = null,
3135 ?RevisionRecord $new = null,
3136 ?int $max = null,
3137 $options = [],
3138 ?string $order = null,
3139 int $flags = IDBAccessObject::READ_NORMAL
3140 ): array {
3141 $this->assertRevisionParameter( 'old', $pageId, $old );
3142 $this->assertRevisionParameter( 'new', $pageId, $new );
3143
3144 $options = (array)$options;
3145 $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3146 in_array( self::INCLUDE_BOTH, $options );
3147 $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3148 in_array( self::INCLUDE_BOTH, $options );
3149
3150 // No DB query needed if old and new are the same revision.
3151 // Can't check for consecutive revisions with 'getParentId' for a similar
3152 // optimization as edge cases exist when there are revisions between
3153 // a revision and it's parent. See T185167 for more details.
3154 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3155 return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3156 }
3157
3158 $db = $this->getDBConnectionRefForQueryFlags( $flags );
3159 $queryBuilder = $db->newSelectQueryBuilder()
3160 ->select( 'rev_id' )
3161 ->from( 'revision' )
3162 ->where( [
3163 'rev_page' => $pageId,
3164 $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3165 ] )
3166 ->andWhere( $this->getRevisionLimitConditions( $db, $old, $new, $options ) );
3167
3168 if ( $order !== null ) {
3169 $queryBuilder->orderBy( [ 'rev_timestamp', 'rev_id' ], $order );
3170 }
3171 if ( $max !== null ) {
3172 // extra to detect truncation
3173 $queryBuilder->limit( $max + 1 );
3174 }
3175
3176 $values = $queryBuilder->caller( __METHOD__ )->fetchFieldValues();
3177 return array_map( 'intval', $values );
3178 }
3179
3201 public function getAuthorsBetween(
3202 $pageId,
3203 ?RevisionRecord $old = null,
3204 ?RevisionRecord $new = null,
3205 ?Authority $performer = null,
3206 $max = null,
3207 $options = []
3208 ) {
3209 $this->assertRevisionParameter( 'old', $pageId, $old );
3210 $this->assertRevisionParameter( 'new', $pageId, $new );
3211 $options = (array)$options;
3212
3213 // No DB query needed if old and new are the same revision.
3214 // Can't check for consecutive revisions with 'getParentId' for a similar
3215 // optimization as edge cases exist when there are revisions between
3216 //a revision and it's parent. See T185167 for more details.
3217 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3218 if ( !$options ) {
3219 return [];
3220 } elseif ( $performer ) {
3221 return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3222 } else {
3223 return [ $new->getUser() ];
3224 }
3225 }
3226
3227 $dbr = $this->getReplicaConnection();
3228 $queryBuilder = $dbr->newSelectQueryBuilder()
3229 ->select( [
3230 'rev_actor',
3231 'rev_user' => 'revision_actor.actor_user',
3232 'rev_user_text' => 'revision_actor.actor_name',
3233 ] )
3234 ->distinct()
3235 ->from( 'revision' )
3236 ->join( 'actor', 'revision_actor', 'revision_actor.actor_id = rev_actor' )
3237 ->where( [
3238 'rev_page' => $pageId,
3239 $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3240 ] )
3241 ->andWhere( $this->getRevisionLimitConditions( $dbr, $old, $new, $options ) )
3242 ->caller( __METHOD__ );
3243 if ( $max !== null ) {
3244 $queryBuilder->limit( $max + 1 );
3245 }
3246
3247 return array_map(
3248 function ( $row ) {
3249 return $this->actorStore->newActorFromRowFields(
3250 $row->rev_user,
3251 $row->rev_user_text,
3252 $row->rev_actor
3253 );
3254 },
3255 iterator_to_array( $queryBuilder->fetchResultSet() )
3256 );
3257 }
3258
3280 public function countAuthorsBetween(
3281 $pageId,
3282 ?RevisionRecord $old = null,
3283 ?RevisionRecord $new = null,
3284 ?Authority $performer = null,
3285 $max = null,
3286 $options = []
3287 ) {
3288 // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3289 // and creation of UserIdentity stuff.
3290 return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3291 }
3292
3318 public function countRevisionsBetween(
3319 $pageId,
3320 ?RevisionRecord $old = null,
3321 ?RevisionRecord $new = null,
3322 $max = null,
3323 $options = []
3324 ) {
3325 $this->assertRevisionParameter( 'old', $pageId, $old );
3326 $this->assertRevisionParameter( 'new', $pageId, $new );
3327
3328 // No DB query needed if old and new are the same revision.
3329 // Can't check for consecutive revisions with 'getParentId' for a similar
3330 // optimization as edge cases exist when there are revisions between
3331 //a revision and it's parent. See T185167 for more details.
3332 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3333 return 0;
3334 }
3335
3336 $dbr = $this->getReplicaConnection();
3337 $where = [ 'rev_page' => $pageId ];
3338 // If $options is a string, convert it to an array
3339 $options = (array)$options;
3340 if ( !in_array( self::INCLUDE_DELETED_REVISIONS, $options ) ) {
3341 $where[] = $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0";
3342 }
3343 $conds = array_merge(
3344 $where,
3345 $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3346 );
3347 if ( $max !== null ) {
3348 return $dbr->newSelectQueryBuilder()
3349 ->select( '1' )
3350 ->from( 'revision' )
3351 ->where( $conds )
3352 ->caller( __METHOD__ )
3353 ->limit( $max + 1 ) // extra to detect truncation
3354 ->fetchRowCount();
3355 } else {
3356 return (int)$dbr->newSelectQueryBuilder()
3357 ->select( 'count(*)' )
3358 ->from( 'revision' )
3359 ->where( $conds )
3360 ->caller( __METHOD__ )->fetchField();
3361 }
3362 }
3363
3375 public function findIdenticalRevision(
3376 RevisionRecord $revision,
3377 int $searchLimit
3378 ): ?RevisionRecord {
3379 $revision->assertWiki( $this->wikiId );
3380 $db = $this->getReplicaConnection();
3381
3382 // Build the target slot role ID => sha1 array from the provided revision
3383 // (this is seemingly necessary *before* fetching the candidate revisions: T406027)
3384 $searchedSlotHashes = [];
3385 $slots = $revision->getSlots()->getPrimarySlots();
3386 foreach ( $slots as $slot ) {
3387 $roleId = $this->slotRoleStore->acquireId( $slot->getRole() );
3388 $searchedSlotHashes[$roleId] = $slot->getSha1();
3389 }
3390 ksort( $searchedSlotHashes );
3391
3392 // First fetch the IDs of the most recent revisions for this page, limited by the search limit.
3393 $candidateRevIds = $db->newSelectQueryBuilder()
3394 ->select( 'rev_id' )
3395 ->from( 'revision' )
3396 ->where( [ 'rev_page' => $revision->getPageId( $this->wikiId ) ] )
3397 // Include 'rev_id' in the ordering in case there are multiple revs with same timestamp
3398 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
3399 // T354015
3400 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
3401 ->limit( $searchLimit )
3402 // skip the most recent edit, we can't revert to it anyway
3403 ->offset( 1 )
3404 ->caller( __METHOD__ )
3405 ->fetchFieldValues();
3406
3407 if ( $candidateRevIds === [] ) {
3408 return null;
3409 }
3410
3411 // Then, for only those revisions, fetch their slot role ID => sha1 data.
3412 $candidateRevisions = $db->newSelectQueryBuilder()
3413 ->select( [ 'slot_revision_id', 'slot_role_id', 'content_sha1' ] )
3414 ->from( 'slots' )
3415 ->join( 'content', null, 'content_id = slot_content_id' )
3416 ->where( [ 'slot_revision_id' => $candidateRevIds ] )
3417 ->caller( __METHOD__ )
3418 ->fetchResultSet();
3419
3420 // Build a map of candidate revisions to their slot role ID => sha1 arrays
3421 $candidateSlotHashes = [];
3422 foreach ( $candidateRevisions as $candidate ) {
3423 $candidateSlotHashes[$candidate->slot_revision_id][$candidate->slot_role_id] = $candidate->content_sha1;
3424 }
3425
3426 // Find the first revision that has the same slot role ID => sha1 array as the provided revision.
3427 // We use $candidateRevIds, which are ordered by the revision timestamps, to ensure we return
3428 // the most recent revision that matches.
3429 $matchRevId = null;
3430 foreach ( $candidateRevIds as $revId ) {
3431 ksort( $candidateSlotHashes[$revId] );
3432 if ( $candidateSlotHashes[$revId] === $searchedSlotHashes ) {
3433 $matchRevId = $revId;
3434 break;
3435 }
3436 }
3437
3438 $revisionRow = null;
3439 if ( $matchRevId !== null ) {
3440 $revisionRow = $this->newSelectQueryBuilder( $db )
3441 ->joinComment()
3442 ->where( [ 'rev_id' => $matchRevId ] )
3443 ->caller( __METHOD__ )
3444 ->fetchRow();
3445 }
3446
3447 return $revisionRow ? $this->newRevisionFromRow( $revisionRow ) : null;
3448 }
3449
3450 // TODO: move relevant methods from Title here, e.g. isBigDeletion, etc.
3451}
const NS_TEMPLATE
Definition Defines.php:61
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
Value object for a comment stored by CommentStore.
Handle database storage of comments such as edit summaries and log reasons.
Content object implementation representing unknown content.
Exception thrown when an unregistered content model is requested.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Immutable value object representing a page identity.
Utility class for creating and reading rows in the recentchanges table.
Help and centralize querying archive table.
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Exception representing a failure to look up a revision.
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
getSlot( $role, $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Returns meta-data for the given slot.
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...
getUser(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
getSlotRoles()
Returns the slot names (roles) of all slots present in this revision.
getPage()
Returns the page this revision belongs to.
getComment(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Fetch revision comment, if it's available to the specified audience.
getParentId( $wikiId=self::LOCAL)
Get parent revision ID (the original previous page revision).
getPageId( $wikiId=self::LOCAL)
Get the page ID.
getSlots()
Returns the slots defined for this revision.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getId( $wikiId=self::LOCAL)
Get revision ID.
Help and centralize querying revision table.
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.
getTimestampFromId( $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
getRevisionByTimestamp( $page, string $timestamp, int $flags=IDBAccessObject::READ_NORMAL)
Load the revision for the given title with the given timestamp.
isRevisionRow( $row, string $table='')
Determine whether the parameter is a row containing all the fields that RevisionStore needs to create...
newNullRevision(IDatabase $dbw, PageIdentity $page, CommentStoreComment $comment, $minor, UserIdentity $user)
Create a new dummy revision for insertion into a page's history.
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
countRevisionsByPageId(IReadableDatabase $db, $id)
Get count of revisions per page...not very efficient.
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
newRevisionFromArchiveRowAndSlots(stdClass $row, $slots, int $queryFlags=0, ?PageIdentity $page=null, array $overrides=[])
newRevisionsFromBatch( $rows, array $options=[], $queryFlags=0, ?PageIdentity $page=null)
Construct a RevisionRecord instance for each row in $rows, and return them as an associative array in...
getWikiId()
Get the ID of the wiki this revision belongs to.
getNextRevision(RevisionRecord $rev, $flags=IDBAccessObject::READ_NORMAL)
Get the revision after $rev in the page's history, if any.
getPreviousRevision(RevisionRecord $rev, $flags=IDBAccessObject::READ_NORMAL)
Get the revision before $rev in the page's history, if any.
findIdenticalRevision(RevisionRecord $revision, int $searchLimit)
Tries to find a revision identical to $revision in $searchLimit most recent revisions of this page.
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new RevisionStoreRecord obj...
newSelectQueryBuilder(IReadableDatabase $dbr)
Return a SelectQueryBuilder to allow querying revision store.1.41RevisionSelectQueryBuilder
getKnownLatestRevision(PageIdentity $page, $revId=0)
Load a revision based on a known page ID and latest revision ID from the DB.
updateSlotsOn(RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw)
Update derived slots in an existing revision into the database, returning the modified slots on succe...
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the latest revision, if there's one.
newRevisionFromRow( $row, $queryFlags=0, ?PageIdentity $page=null, $fromCache=false)
getKnownCurrentRevision(PageIdentity $page, $revId=0)
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaced Revision::isUnpatrolled.
getRevisionById( $id, $flags=0, ?PageIdentity $page=null)
Load a page revision from a given revision ID number.
getAuthorsBetween( $pageId, ?RevisionRecord $old=null, ?RevisionRecord $new=null, ?Authority $performer=null, $max=null, $options=[])
Get the authors between the given revisions or revisions.
setLogger(LoggerInterface $logger)
newArchiveSelectQueryBuilder(IReadableDatabase $dbr)
Return a SelectQueryBuilder to allow querying archive table.1.41ArchiveSelectQueryBuilder
newRevisionFromRowAndSlots(stdClass $row, $slots, int $queryFlags=0, ?PageIdentity $page=null, bool $fromCache=false)
getFirstRevision( $page, int $flags=IDBAccessObject::READ_NORMAL)
Get the first revision of a given page.
__construct(ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, BagOStuff $localCache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorStore $actorStore, IContentHandlerFactory $contentHandlerFactory, PageStore $pageStore, TitleFactory $titleFactory, HookContainer $hookContainer, RecentChangeLookup $recentChangeLookup, $wikiId=WikiAwareEntity::LOCAL)
countRevisionsBetween( $pageId, ?RevisionRecord $old=null, ?RevisionRecord $new=null, $max=null, $options=[])
Get the number of revisions between the given revisions.
getRevisionByTitle( $page, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
getTitle( $pageId, $revId, $queryFlags=IDBAccessObject::READ_NORMAL)
Determines the page Title based on the available information.
getRevisionIdsBetween(int $pageId, ?RevisionRecord $old=null, ?RevisionRecord $new=null, ?int $max=null, $options=[], ?string $order=null, int $flags=IDBAccessObject::READ_NORMAL)
Get IDs of revisions between the given revisions.
countRevisionsByTitle(IReadableDatabase $db, PageIdentity $page)
Get count of revisions per page...not very efficient.
countAuthorsBetween( $pageId, ?RevisionRecord $old=null, ?RevisionRecord $new=null, ?Authority $performer=null, $max=null, $options=[])
Get the number of authors between the given revisions.
userWasLastToEdit(IReadableDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new RevisionArchiveRecord o...
newRevisionFromArchiveRow( $row, $queryFlags=0, ?PageIdentity $page=null, array $overrides=[])
Make a fake RevisionRecord object from an archive table row.
Value object representing a content slot associated with a page revision.
getRole()
Returns the role of the slot.
hasAddress()
Whether this slot has an address.
getAddress()
Returns the address of this slot's content.
getModel()
Returns the content model.
getRevision()
Returns the ID of the revision this slot is associated with.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Exception thrown when a blob has the "bad" content address schema, or has "error" in its old_flags,...
Exception representing a failure to access a data blob.
Exception representing a failure to look up a row from a name table.
Value object representing a modification of revision slots.
getRemovedRoles()
Returns a list of removed slot roles, that is, roles removed by calling removeSlot(),...
getModifiedRoles()
Returns a list of modified slot roles, that is, roles modified by calling modifySlot(),...
Service for storing and loading Content objects representing revision data blobs.
Creates Title objects.
Represents a title within MediaWiki.
Definition Title.php:69
Service to read or write data in the actor table.
Library for creating and parsing MW-style timestamps.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:73
Multi-datacenter aware caching interface.
Build SELECT queries with a fluent interface.
return[ 'config-schema-inverse'=>['default'=>['ConfigRegistry'=>['main'=> 'MediaWiki\\Config\\GlobalVarConfig::newInstance',], 'Sitename'=> 'MediaWiki', 'Server'=> false, 'CanonicalServer'=> false, 'ServerName'=> false, 'AssumeProxiesUseDefaultProtocolPorts'=> true, 'HttpsPort'=> 443, 'ForceHTTPS'=> false, 'ScriptPath'=> '/wiki', 'UsePathInfo'=> null, 'Script'=> false, 'LoadScript'=> false, 'RestPath'=> false, 'StylePath'=> false, 'LocalStylePath'=> false, 'ExtensionAssetsPath'=> false, 'ExtensionDirectory'=> null, 'StyleDirectory'=> null, 'ArticlePath'=> false, 'UploadPath'=> false, 'ImgAuthPath'=> false, 'ThumbPath'=> false, 'UploadDirectory'=> false, 'FileCacheDirectory'=> false, 'Logo'=> false, 'Logos'=> false, 'Favicon'=> '/favicon.ico', 'AppleTouchIcon'=> false, 'ReferrerPolicy'=> false, 'TmpDirectory'=> false, 'UploadBaseUrl'=> '', 'UploadStashScalerBaseUrl'=> false, 'ActionPaths'=>[], 'MainPageIsDomainRoot'=> false, 'EnableUploads'=> false, 'UploadStashMaxAge'=> 21600, 'EnableAsyncUploads'=> false, 'EnableAsyncUploadsByURL'=> false, 'EnableChunkedUploads'=> false, 'UploadMaintenance'=> false, 'IllegalFileChars'=> ':\\/\\\\', 'DeletedDirectory'=> false, 'ImgAuthDetails'=> false, 'ImgAuthUrlPathMap'=>[], 'LocalFileRepo'=>['class'=> 'MediaWiki\\FileRepo\\LocalRepo', 'name'=> 'local', 'directory'=> null, 'scriptDirUrl'=> null, 'favicon'=> null, 'url'=> null, 'hashLevels'=> null, 'thumbScriptUrl'=> null, 'transformVia404'=> null, 'deletedDir'=> null, 'deletedHashLevels'=> null, 'updateCompatibleMetadata'=> null, 'reserializeMetadata'=> null,], 'ForeignFileRepos'=>[], 'UseInstantCommons'=> false, 'UseSharedUploads'=> false, 'SharedUploadDirectory'=> null, 'SharedUploadPath'=> null, 'HashedSharedUploadDirectory'=> true, 'RepositoryBaseUrl'=> 'https:'FetchCommonsDescriptions'=> false, 'SharedUploadDBname'=> false, 'SharedUploadDBprefix'=> '', 'SharedUploadDBschema'=> null, 'CacheSharedUploads'=> true, 'ForeignUploadTargets'=>['local',], 'UploadDialog'=>['fields'=>['description'=> true, 'date'=> false, 'categories'=> false,], 'licensemessages'=>['local'=> 'generic-local', 'foreign'=> 'generic-foreign',], 'comment'=>['local'=> '', 'foreign'=> '',], 'format'=>['filepage'=> ' $DESCRIPTION', 'description'=> ' $TEXT', 'ownwork'=> '', 'license'=> '', 'uncategorized'=> '',],], 'FileBackends'=>[], 'LockManagers'=>[], 'DefaultLockManager'=> null, 'ShowEXIF'=> null, 'UpdateCompatibleMetadata'=> false, 'AllowCopyUploads'=> false, 'CopyUploadsDomains'=>[], 'CopyUploadsFromSpecialUpload'=> false, 'CopyUploadProxy'=> false, 'CopyUploadTimeout'=> false, 'CopyUploadAllowOnWikiDomainConfig'=> false, 'MaxUploadSize'=> 104857600, 'MinUploadChunkSize'=> 1024, 'UploadNavigationUrl'=> false, 'UploadMissingFileUrl'=> false, 'ThumbnailScriptPath'=> false, 'SharedThumbnailScriptPath'=> false, 'HashedUploadDirectory'=> true, 'CSPUploadEntryPoint'=> true, 'FileExtensions'=>['png', 'gif', 'jpg', 'jpeg', 'webp',], 'ProhibitedFileExtensions'=>['html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht', 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', 'phar', 'shtml', 'jhtml', 'pl', 'py', 'cgi', 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl', 'xml',], 'MimeTypeExclusions'=>['text/html', 'application/javascript', 'text/javascript', 'text/x-javascript', 'application/x-shellscript', 'application/x-php', 'text/x-php', 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh', 'text/x-csh', 'text/scriptlet', 'application/x-msdownload', 'application/x-msmetafile', 'application/java', 'application/xml', 'text/xml',], 'CheckFileExtensions'=> true, 'StrictFileExtensions'=> true, 'DisableUploadScriptChecks'=> false, 'UploadSizeWarning'=> false, 'TrustedMediaFormats'=>['BITMAP', 'AUDIO', 'VIDEO', 'image/svg+xml', 'application/pdf',], 'MediaHandlers'=>[], 'NativeImageLazyLoading'=> true, 'ParserTestMediaHandlers'=>['image/jpeg'=> 'MockBitmapHandler', 'image/png'=> 'MockBitmapHandler', 'image/gif'=> 'MockBitmapHandler', 'image/tiff'=> 'MockBitmapHandler', 'image/webp'=> 'MockBitmapHandler', 'image/x-ms-bmp'=> 'MockBitmapHandler', 'image/x-bmp'=> 'MockBitmapHandler', 'image/x-xcf'=> 'MockBitmapHandler', 'image/svg+xml'=> 'MockSvgHandler', 'image/vnd.djvu'=> 'MockDjVuHandler',], 'UseImageResize'=> true, 'UseImageMagick'=> false, 'ImageMagickConvertCommand'=> '/usr/bin/convert', 'MaxInterlacingAreas'=>[], 'SharpenParameter'=> '0x0.4', 'SharpenReductionThreshold'=> 0.85, 'ImageMagickTempDir'=> false, 'CustomConvertCommand'=> false, 'JpegTran'=> '/usr/bin/jpegtran', 'JpegPixelFormat'=> 'yuv420', 'JpegQuality'=> 80, 'Exiv2Command'=> '/usr/bin/exiv2', 'Exiftool'=> '/usr/bin/exiftool', 'SVGConverters'=>['ImageMagick'=> ' $path/convert -background "#ffffff00" -thumbnail $widthx$height\\! $input PNG:$output', 'inkscape'=> ' $path/inkscape -w $width -o $output $input', 'batik'=> 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', 'rsvg'=> ' $path/rsvg-convert -w $width -h $height -l $lang -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, 'MaxAnimatedWebPArea'=> 12500000, 'WebPThumbnailType'=>['webp', 'image/webp',], 'TiffThumbnailType'=>[], 'ThumbnailEpoch'=> '20030516000000', 'AttemptFailureEpoch'=> 1, 'IgnoreImageErrors'=> false, 'GenerateThumbnailOnParse'=> true, 'ShowArchiveThumbnails'=> true, 'EnableAutoRotation'=> null, 'Antivirus'=> null, 'AntivirusSetup'=>['clamav'=>['command'=> 'clamscan --no-summary ', 'codemap'=>[0=> 0, 1=> 1, 52=> -1, ' *'=> false,], 'messagepattern'=> '/.*?:(.*)/sim',],], 'AntivirusRequired'=> true, 'VerifyMimeType'=> true, 'MimeTypeFile'=> 'internal', 'MimeInfoFile'=> 'internal', 'MimeDetectorCommand'=> null, 'TrivialMimeDetection'=> false, 'XMLMimeTypes'=>['http:'svg'=> 'image/svg+xml', 'http:'http:'html'=> 'text/html',], 'ImageLimits'=>[[320, 240,], [640, 480,], [800, 600,], [1024, 768,], [1280, 1024,], [2560, 2048,],], 'ThumbLimits'=>[120, 150, 180, 200, 220, 250, 300, 400,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'TrackMediaRequestProvenance'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EmailConfirmationBanner'=> false, 'EnotifWatchlist'=> false, 'EnotifUserTalk'=> false, 'EnotifRevealEditorAddress'=> false, 'EnotifMinorEdits'=> true, 'EnotifUseRealName'=> false, 'UsersNotifiedOnAllChanges'=>[], 'DBname'=> 'my_wiki', 'DBmwschema'=> null, 'DBprefix'=> '', 'DBserver'=> 'localhost', 'DBport'=> 5432, 'DBuser'=> 'wikiuser', 'DBpassword'=> '', 'DBtype'=> 'mysql', 'DBssl'=> false, 'DBcompress'=> false, 'DBStrictWarnings'=> false, 'DBadminuser'=> null, 'DBadminpassword'=> null, 'SearchType'=> null, 'SearchTypeAlternatives'=> null, 'DBTableOptions'=> 'ENGINE=InnoDB, DEFAULT CHARSET=binary', 'SQLMode'=> '', 'SQLiteDataDir'=> '', 'SharedDB'=> null, 'SharedPrefix'=> false, 'SharedTables'=>['user', 'user_properties', 'user_autocreate_serial',], 'SharedSchema'=> false, 'DBservers'=> false, 'LBFactoryConf'=>['class'=> 'Wikimedia\\Rdbms\\LBFactorySimple',], 'DataCenterUpdateStickTTL'=> 10, 'DBerrorLog'=> false, 'DBerrorLogTZ'=> false, 'LocalDatabases'=>[], 'DatabaseReplicaLagWarning'=> 10, 'DatabaseReplicaLagCritical'=> 30, 'MaxExecutionTimeForExpensiveQueries'=> 0, 'VirtualDomainsMapping'=>[], 'RemoteVirtualDomainsMapping'=>[], 'FileSchemaMigrationStage'=> 3, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'CodeHighlighter',],], 'text'=> 'MediaWiki\\Content\\TextContentHandler', 'unknown'=> 'MediaWiki\\Content\\FallbackContentHandler',], 'NamespaceContentModels'=>[], 'TextModelsToParse'=>['wikitext', 'javascript', 'css',], 'CompressRevisions'=> false, 'ExternalStores'=>[], 'ExternalServers'=>[], 'DefaultExternalStore'=> false, 'RevisionCacheExpiry'=> 604800, 'PageLanguageUseDB'=> false, 'DiffEngine'=> null, 'ExternalDiffEngine'=> false, 'Wikidiff2Options'=>[], 'RequestTimeLimit'=> null, 'TransactionalTimeLimit'=> 120, 'CriticalSectionTimeLimit'=> 180.0, 'MiserMode'=> false, 'DisableQueryPages'=> false, 'QueryCacheLimit'=> 1000, 'WantedPagesThreshold'=> 1, 'AllowSlowParserFunctions'=> false, 'AllowSchemaUpdates'=> true, 'MaxArticleSize'=> 2048, 'MemoryLimit'=> '50M', 'PoolCounterConf'=> null, 'PoolCountClientConf'=>['servers'=>['127.0.0.1',], 'timeout'=> 0.1,], 'MaxUserDBWriteDuration'=> false, 'MaxJobDBWriteDuration'=> false, 'LinkHolderBatchSize'=> 1000, 'MaximumMovedPages'=> 100, 'ForceDeferredUpdatesPreSend'=> false, 'MultiShardSiteStats'=> false, 'CacheDirectory'=> false, 'MainCacheType'=> 0, 'MessageCacheType'=> -1, 'ParserCacheType'=> -1, 'SessionCacheType'=> -1, 'AnonSessionCacheType'=> false, 'LanguageConverterCacheType'=> -1, 'ObjectCaches'=>[0=>['class'=> 'Wikimedia\\ObjectCache\\EmptyBagOStuff', 'reportDupes'=> false,], 1=>['class'=> 'MediaWiki\\ObjectCache\\SqlBagOStuff', 'loggroup'=> 'SQLBagOStuff',], 'memcached-php'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPhpBagOStuff', 'loggroup'=> 'memcached',], 'memcached-pecl'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPeclBagOStuff', 'loggroup'=> 'memcached',], 'hash'=>['class'=> 'Wikimedia\\ObjectCache\\HashBagOStuff', 'reportDupes'=> false,], 'apc'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,], 'apcu'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,],], 'WANObjectCache'=>[], 'MicroStashType'=> -1, 'MainStash'=> 1, 'ParsoidCacheConfig'=>['StashType'=> null, 'StashDuration'=> 86400,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'SplitParsoidParserCache'=> false, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'JwtSessionCookieIssuer'=> null, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> 'MediaWiki\\Language\\LocalisationCache', 'store'=> 'detect', 'storeClass'=> false, 'storeDirectory'=> false, 'storeServer'=>[], 'forceRecache'=> false, 'manualRecache'=> false,], 'CachePages'=> true, 'CacheEpoch'=> '20030516000000', 'GitInfoCacheDirectory'=> false, 'UseFileCache'=> false, 'FileCacheDepth'=> 2, 'RenderHashAppend'=> '', 'EnableSidebarCache'=> false, 'SidebarCacheExpiry'=> 86400, 'UseGzip'=> false, 'InvalidateCacheOnLocalSettingsChange'=> true, 'ExtensionInfoMTime'=> false, 'EnableRemoteBagOStuffTests'=> false, 'UseCdn'=> false, 'VaryOnXFP'=> false, 'InternalServer'=> false, 'CdnMaxAge'=> 18000, 'CdnMaxageLagged'=> 30, 'CdnMaxageStale'=> 10, 'CdnReboundPurgeDelay'=> 0, 'CdnMaxageSubstitute'=> 60, 'ForcedRawSMaxage'=> 300, 'CdnServers'=>[], 'CdnServersNoPurge'=>[], 'HTCPRouting'=>[], 'HTCPMulticastTTL'=> 1, 'UsePrivateIPs'=> false, 'CdnMatchParameterOrder'=> true, 'LanguageCode'=> 'en', 'GrammarForms'=>[], 'InterwikiMagic'=> true, 'HideInterlanguageLinks'=> false, 'ExtraInterlanguageLinkPrefixes'=>[], 'InterlanguageLinkCodeMap'=>[], 'ExtraLanguageNames'=>[], 'ExtraLanguageCodes'=>['bh'=> 'bho', 'no'=> 'nb', 'simple'=> 'en',], 'DummyLanguageCodes'=>[], 'AllUnicodeFixes'=> false, 'LegacyEncoding'=> false, 'AmericanDates'=> false, 'TranslateNumerals'=> true, 'UseDatabaseMessages'=> true, 'MaxMsgCacheEntrySize'=> 10000, 'DisableLangConversion'=> false, 'DisableTitleConversion'=> false, 'DefaultLanguageVariant'=> false, 'UsePigLatinVariant'=> false, 'DisabledVariants'=>[], 'VariantArticlePath'=> false, 'UseXssLanguage'=> false, 'LoginLanguageSelector'=> false, 'ForceUIMsgAsContentMsg'=>[], 'RawHtmlMessages'=>[], 'Localtimezone'=> null, 'LocalTZoffset'=> null, 'OverrideUcfirstCharacters'=>[], 'MimeType'=> 'text/html', 'Html5Version'=> null, 'EditSubmitButtonLabelPublish'=> false, 'XhtmlNamespaces'=>[], 'SiteNotice'=> '', 'BrowserFormatDetection'=> 'telephone=no', 'SkinMetaTags'=>[], 'DefaultSkin'=> 'vector-2022', 'FallbackSkin'=> 'fallback', 'SkipSkins'=>[], 'DisableOutputCompression'=> false, 'FragmentMode'=>['html5', 'legacy',], 'ExternalInterwikiFragmentMode'=> 'legacy', 'FooterIcons'=>['copyright'=>['copyright'=>[],], 'poweredby'=>['mediawiki'=>['src'=> null, 'url'=> 'https:'alt'=> 'Powered by MediaWiki', 'lang'=> 'en',],],], 'EnableSectionShare'=> false, 'PageShareSkinsEnabled'=>[], 'UseCombinedLoginLink'=> false, 'Edititis'=> false, 'Send404Code'=> true, 'ShowRollbackEditCount'=> 10, 'EnableCanonicalServerLink'=> false, 'InterwikiLogoOverride'=>[], 'ResourceModules'=>[], 'ResourceModuleSkinStyles'=>[], 'ResourceLoaderSources'=>[], 'ResourceBasePath'=> null, 'ResourceLoaderMaxage'=>[], 'ResourceLoaderDebug'=> false, 'ResourceLoaderMaxQueryLength'=> false, 'ResourceLoaderValidateJS'=> true, 'ResourceLoaderEnableJSProfiler'=> false, 'ResourceLoaderStorageEnabled'=> true, 'ResourceLoaderStorageVersion'=> 1, 'ResourceLoaderEnableSourceMapLinks'=> true, 'AllowSiteCSSOnRestrictedPages'=> false, 'VueDevelopmentMode'=> false, 'CodexDevelopmentDir'=> null, 'MetaNamespace'=> false, 'MetaNamespaceTalk'=> false, 'CanonicalNamespaceNames'=>[-2=> 'Media', -1=> 'Special', 0=> '', 1=> 'Talk', 2=> 'User', 3=> 'User_talk', 4=> 'Project', 5=> 'Project_talk', 6=> 'File', 7=> 'File_talk', 8=> 'MediaWiki', 9=> 'MediaWiki_talk', 10=> 'Template', 11=> 'Template_talk', 12=> 'Help', 13=> 'Help_talk', 14=> 'Category', 15=> 'Category_talk',], 'ExtraNamespaces'=>[], 'ExtraGenderNamespaces'=>[], 'NamespaceAliases'=>[], 'LegalTitleChars'=> ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 'CapitalLinks' => true, 'CapitalLinkOverrides' => [ ], 'NamespacesWithSubpages' => [ 1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 7 => true, 8 => true, 9 => true, 10 => true, 11 => true, 12 => true, 13 => true, 15 => true, ], 'NamespacesWithoutAutoSummaries' => [ ], 'ContentNamespaces' => [ 0, ], 'ShortPagesNamespaceExclusions' => [ ], 'ExtraSignatureNamespaces' => [ ], 'InvalidRedirectTargets' => [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect', 'Mylog', ], 'DisableHardRedirects' => false, 'FixDoubleRedirects' => false, 'LocalInterwikis' => [ ], 'InterwikiExpiry' => 10800, 'InterwikiCache' => false, 'InterwikiScopes' => 3, 'InterwikiFallbackSite' => 'wiki', 'RedirectSources' => false, 'SiteTypes' => [ 'mediawiki' => 'MediaWiki\\Site\\MediaWikiSite', ], 'MaxTocLevel' => 999, 'MaxPPNodeCount' => 1000000, 'MaxTemplateDepth' => 100, 'MaxPPExpandDepth' => 100, 'UrlProtocols' => [ 'bitcoin:', 'ftp: 'ftps: 'geo:', 'git: 'gopher: 'http: 'https: 'irc: 'ircs: 'magnet:', 'mailto:', 'matrix:', 'mms: 'news:', 'nntp: 'redis: 'sftp: 'sip:', 'sips:', 'sms:', 'ssh: 'svn: 'tel:', 'telnet: 'urn:', 'wikipedia: 'worldwind: 'xmpp:', ' ], 'CleanSignatures' => true, 'AllowExternalImages' => false, 'AllowExternalImagesFrom' => '', 'EnableImageWhitelist' => false, 'TidyConfig' => [ ], 'ParsoidSettings' => [ 'useSelser' => true, ], 'ParsoidExperimentalParserFunctionOutput' => false, 'RawHtml' => false, 'ExternalLinkTarget' => false, 'NoFollowLinks' => true, 'NoFollowNsExceptions' => [ ], 'NoFollowDomainExceptions' => [ 'mediawiki.org', ], 'RegisterInternalExternals' => false, 'ExternalLinksIgnoreDomains' => [ ], 'AllowDisplayTitle' => true, 'RestrictDisplayTitle' => true, 'ExpensiveParserFunctionLimit' => 100, 'PreprocessorCacheThreshold' => 1000, 'EnableScaryTranscluding' => false, 'TranscludeCacheExpiry' => 3600, 'EnableMagicLinks' => [ 'ISBN' => false, 'PMID' => false, 'RFC' => false, ], 'ParserEnableUserLanguage' => false, 'ArticleCountMethod' => 'link', 'ActiveUserDays' => 30, 'LearnerEdits' => 10, 'LearnerMemberSince' => 4, 'ExperiencedUserEdits' => 500, 'ExperiencedUserMemberSince' => 30, 'ManualRevertSearchRadius' => 15, 'RevertedTagMaxDepth' => 15, 'CentralIdLookupProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\CentralId\\LocalIdLookup', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', 'HideUserUtils', ], ], ], 'CentralIdLookupProvider' => 'local', 'UserRegistrationProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'ConnectionProvider', ], ], ], 'PasswordPolicy' => [ 'policies' => [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true, ], 'PasswordCannotBeSubstringInUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'PasswordCannotMatchDefaults' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true, ], 'PasswordNotInCommonList' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], ], ], 'checks' => [ 'MinimalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimalPasswordLength', ], 'MinimumPasswordLengthToLogin' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimumPasswordLengthToLogin', ], 'PasswordCannotBeSubstringInUsername' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotBeSubstringInUsername', ], 'PasswordCannotMatchDefaults' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotMatchDefaults', ], 'MaximalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMaximalPasswordLength', ], 'PasswordNotInCommonList' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordNotInCommonList', ], ], ], 'AuthManagerConfig' => null, 'AuthManagerAutoConfig' => [ 'preauth' => [ 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider', 'services' => [ 'ConnectionProvider', 'UserFactory', ], 'sort' => 0, ], ], 'primaryauth' => [ 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', 'UserOptionsLookup', ], 'args' => [ [ 'authoritative' => false, ], ], 'sort' => 0, ], 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'args' => [ [ 'authoritative' => true, ], ], 'sort' => 100, ], ], 'secondaryauth' => [ 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider', 'sort' => 100, ], 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'sort' => 200, ], ], ], 'RememberMe' => 'choose', 'ReauthenticateTime' => [ 'default' => 3600, ], 'ChangeCredentialsBlacklist' => [ 'MediaWiki\\Auth\\TemporaryPasswordAuthenticationRequest', ], 'RemoveCredentialsBlacklist' => [ 'MediaWiki\\Auth\\PasswordAuthenticationRequest', ], 'InvalidPasswordReset' => true, 'PasswordDefault' => 'pbkdf2', 'PasswordConfig' => [ 'A' => [ 'class' => 'MediaWiki\\Password\\MWOldPassword', ], 'B' => [ 'class' => 'MediaWiki\\Password\\MWSaltedPassword', ], 'pbkdf2-legacyA' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'A', 'pbkdf2', ], ], 'pbkdf2-legacyB' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'B', 'pbkdf2', ], ], 'bcrypt' => [ 'class' => 'MediaWiki\\Password\\BcryptPassword', 'cost' => 9, ], 'pbkdf2' => [ 'class' => 'MediaWiki\\Password\\Pbkdf2PasswordUsingOpenSSL', 'algo' => 'sha512', 'cost' => '30000', 'length' => '64', ], 'argon2' => [ 'class' => 'MediaWiki\\Password\\Argon2Password', 'algo' => 'auto', ], ], 'PasswordResetRoutes' => [ 'username' => true, 'email' => true, ], 'MaxSigChars' => 255, 'SignatureValidation' => 'warning', 'SignatureAllowedLintErrors' => [ 'obsolete-tag', ], 'MaxNameChars' => 255, 'ReservedUsernames' => [ 'MediaWiki default', 'Conversion script', 'Maintenance script', 'Template namespace initialisation script', 'ScriptImporter', 'Delete page script', 'Move page script', 'Command line script', 'Unknown user', 'msg:double-redirect-fixer', 'msg:usermessage-editor', 'msg:proxyblocker', 'msg:sorbs', 'msg:spambot_username', 'msg:autochange-username', ], 'DefaultUserOptions' => [ 'ccmeonemails' => 0, 'date' => 'default', 'diffonly' => 0, 'diff-type' => 'table', 'disablemail' => 0, 'editfont' => 'monospace', 'editondblclick' => 0, 'editrecovery' => 0, 'editsectiononrightclick' => 0, 'email-allow-new-users' => 1, 'enotifminoredits' => 0, 'enotifrevealaddr' => 0, 'enotifusertalkpages' => 1, 'enotifwatchlistpages' => 1, 'extendwatchlist' => 1, 'fancysig' => 0, 'forceeditsummary' => 0, 'forcesafemode' => 0, 'gender' => 'unknown', 'hidecategorization' => 1, 'hideminor' => 0, 'hidepatrolled' => 0, 'imagesize' => 2, 'minordefault' => 0, 'newpageshidepatrolled' => 0, 'nickname' => '', 'norollbackdiff' => 0, 'prefershttps' => 1, 'previewonfirst' => 0, 'previewontop' => 1, 'pst-cssjs' => 1, 'rcdays' => 7, 'rcenhancedfilters-disable' => 0, 'rclimit' => 50, 'requireemail' => 0, 'search-match-redirect' => true, 'search-special-page' => 'Search', 'search-thumbnail-extra-namespaces' => true, 'searchlimit' => 20, 'showhiddencats' => 0, 'shownumberswatching' => 1, 'showrollbackconfirmation' => 0, 'skin' => false, 'skin-responsive' => 1, 'thumbsize' => 5, 'underline' => 2, 'useeditwarning' => 1, 'uselivepreview' => 0, 'usenewrc' => 1, 'watchcreations' => 1, 'watchcreations-expiry' => 'infinite', 'watchdefault' => 1, 'watchdefault-expiry' => 'infinite', 'watchdeletion' => 0, 'watchlistdays' => 7, 'watchlisthideanons' => 0, 'watchlisthidebots' => 0, 'watchlisthidecategorization' => 1, 'watchlisthideliu' => 0, 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, 'watchlistreloadautomatically' => 0, 'watchlistunwatchlinks' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'watchuploads' => 1, 'watchrollback-expiry' => 'infinite', 'watchstar-expiry' => 'infinite', 'wlenhancedfilters-disable' => 0, 'wllimit' => 250, ], 'ConditionalUserOptions' => [ ], 'HiddenPrefs' => [ ], 'UserJsPrefLimit' => 100, 'InvalidUsernameCharacters' => '@:>=', 'UserrightsInterwikiDelimiter' => '@', 'SecureLogin' => false, 'AuthenticationTokenVersion' => null, 'SessionProviders' => [ 'MediaWiki\\Session\\CookieSessionProvider' => [ 'class' => 'MediaWiki\\Session\\CookieSessionProvider', 'args' => [ [ 'priority' => 30, ], ], 'services' => [ 'JwtCodec', 'UrlUtils', ], ], 'MediaWiki\\Session\\BotPasswordSessionProvider' => [ 'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider', 'args' => [ [ 'priority' => 75, ], ], 'services' => [ 'GrantsInfo', ], ], ], 'AutoCreateTempUser' => [ 'known' => false, 'enabled' => false, 'actions' => [ 'edit', ], 'genPattern' => '~$1', 'matchPattern' => null, 'reservedPattern' => '~$1', 'serialProvider' => [ 'type' => 'local', 'useYear' => true, ], 'serialMapping' => [ 'type' => 'readable-numeric', ], 'expireAfterDays' => 90, 'notifyBeforeExpirationDays' => 10, ], 'AutoblockExemptions' => [ ], 'AutoblockExpiry' => 86400, 'BlockAllowsUTEdit' => true, 'BlockCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 19, ], 'BlockDisablesLogin' => false, 'EnableMultiBlocks' => false, 'WhitelistRead' => false, 'WhitelistReadRegexp' => false, 'EmailConfirmToEdit' => false, 'HideIdentifiableRedirects' => true, 'GroupPermissions' => [ '*' => [ 'createaccount' => true, 'autocreateaccount' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'viewmyprivateinfo' => true, 'editmyprivateinfo' => true, 'editmyoptions' => true, ], 'user' => [ 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'movefile' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'minoredit' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, 'editmyuserjsredirect' => true, 'sendemail' => true, 'applychangetags' => true, 'changetags' => true, 'viewmywatchlist' => true, 'editmywatchlist' => true, 'createwithcontentmodel' => true, 'logout' => true, ], 'autoconfirmed' => [ 'autoconfirmed' => true, 'editsemiprotected' => true, ], 'bot' => [ 'bot' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'nominornewtalk' => true, 'autopatrol' => true, 'suppressredirect' => true, 'apihighlimits' => true, ], 'sysop' => [ 'block' => true, 'createaccount' => true, 'createpreviouslyrenamedaccount' => true, 'delete' => true, 'bigdelete' => true, 'deletedhistory' => true, 'deletedtext' => true, 'undelete' => true, 'editcontentmodel' => true, 'editinterface' => true, 'editsitejson' => true, 'edituserjson' => true, 'import' => true, 'importupload' => true, 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'patrol' => true, 'autopatrol' => true, 'protect' => true, 'editprotected' => true, 'rollback' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'unwatchedpages' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'blockemail' => true, 'markbotedits' => true, 'apihighlimits' => true, 'browsearchive' => true, 'noratelimit' => true, 'movefile' => true, 'unblockself' => true, 'suppressredirect' => true, 'mergehistory' => true, 'managechangetags' => true, 'deletechangetags' => true, ], 'interface-admin' => [ 'editinterface' => true, 'editsitecss' => true, 'editsitejson' => true, 'editsitejs' => true, 'editusercss' => true, 'edituserjson' => true, 'edituserjs' => true, ], 'bureaucrat' => [ 'userrights' => true, 'noratelimit' => true, 'renameuser' => true, ], 'suppress' => [ 'hideuser' => true, 'suppressrevision' => true, 'viewsuppressed' => true, 'suppressionlog' => true, 'deleterevision' => true, 'deletelogentry' => true, ], ], 'PrivilegedGroups' => [ 'bureaucrat', 'interface-admin', 'suppress', 'sysop', ], 'RevokePermissions' => [ ], 'GroupInheritsPermissions' => [ ], 'ImplicitGroups' => [ '*', 'user', 'autoconfirmed', ], 'GroupsAddToSelf' => [ ], 'GroupsRemoveFromSelf' => [ ], 'RestrictedGroups' => [ ], 'UserRequirementsPrivateConditions' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], 'RestrictUserPageEditing' => false, 'NonincludableNamespaces' => [ ], 'AutoConfirmAge' => 0, 'AutoConfirmCount' => 0, 'Autopromote' => [ 'autoconfirmed' => [ '&', [ 1, null, ], [ 2, null, ], ], ], 'AutopromoteOnce' => [ 'onEdit' => [ ], ], 'AutopromoteOnceLogInRC' => true, 'AutopromoteOnceRCExcludedGroups' => [ ], 'AddGroups' => [ ], 'RemoveGroups' => [ ], 'AvailableRights' => [ ], 'ImplicitRights' => [ ], 'DeleteRevisionsLimit' => 0, 'DeleteRevisionsBatchSize' => 1000, 'HideUserContribLimit' => 1000, 'AccountCreationThrottle' => [ [ 'count' => 0, 'seconds' => 86400, ], ], 'TempAccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 600, ], [ 'count' => 6, 'seconds' => 86400, ], ], 'TempAccountNameAcquisitionThrottle' => [ [ 'count' => 60, 'seconds' => 86400, ], ], 'SpamRegex' => [ ], 'SummarySpamRegex' => [ ], 'EnableDnsBlacklist' => false, 'DnsBlacklistUrls' => [ ], 'ProxyList' => [ ], 'ProxyWhitelist' => [ ], 'SoftBlockRanges' => [ ], 'ApplyIpBlocksToXff' => false, 'RateLimits' => [ 'edit' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], 'user' => [ 90, 60, ], ], 'move' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], 'upload' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'rollback' => [ 'user' => [ 10, 60, ], 'newbie' => [ 5, 120, ], ], 'mailpassword' => [ 'ip' => [ 5, 3600, ], ], 'sendemail' => [ 'ip' => [ 5, 86400, ], 'newbie' => [ 5, 86400, ], 'user' => [ 20, 86400, ], ], 'changeemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'confirmemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'purge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'linkpurge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'renderfile' => [ 'ip' => [ 700, 30, ], 'user' => [ 700, 30, ], ], 'renderfile-nonstandard' => [ 'ip' => [ 70, 30, ], 'user' => [ 70, 30, ], ], 'stashedit' => [ 'ip' => [ 30, 60, ], 'newbie' => [ 30, 60, ], ], 'stashbasehtml' => [ 'ip' => [ 5, 60, ], 'newbie' => [ 5, 60, ], ], 'changetags' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'editcontentmodel' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], ], 'RateLimitsExcludedIPs' => [ ], 'PutIPinRC' => true, 'QueryPageDefaultLimit' => 50, 'ExternalQuerySources' => [ ], 'PasswordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300, ], [ 'count' => 150, 'seconds' => 172800, ], ], 'GrantPermissions' => [ 'basic' => [ 'autocreateaccount' => true, 'autoconfirmed' => true, 'autopatrol' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'nominornewtalk' => true, 'patrolmarks' => true, 'read' => true, 'unwatchedpages' => true, ], 'highvolume' => [ 'bot' => true, 'apihighlimits' => true, 'noratelimit' => true, 'markbotedits' => true, ], 'import' => [ 'import' => true, 'importupload' => true, ], 'editpage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, 'editusercss' => true, 'edituserjs' => true, 'editsitecss' => true, 'editsitejs' => true, ], 'createeditmovepage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'createpage' => true, 'createtalk' => true, 'delete-redirect' => true, 'move' => true, 'move-rootuserpages' => true, 'move-subpages' => true, 'move-categorypages' => true, 'suppressredirect' => true, ], 'uploadfile' => [ 'upload' => true, 'reupload-own' => true, ], 'uploadeditmovefile' => [ 'upload' => true, 'reupload-own' => true, 'reupload' => true, 'reupload-shared' => true, 'upload_by_url' => true, 'movefile' => true, 'suppressredirect' => true, ], 'patrol' => [ 'patrol' => true, ], 'rollback' => [ 'rollback' => true, ], 'blockusers' => [ 'block' => true, 'blockemail' => true, ], 'viewdeleted' => [ 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, ], 'viewrestrictedlogs' => [ 'suppressionlog' => true, ], 'delete' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, 'delete' => true, 'bigdelete' => true, 'deletelogentry' => true, 'deleterevision' => true, 'undelete' => true, ], 'oversight' => [ 'suppressrevision' => true, 'viewsuppressed' => true, ], 'protect' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], 'managesessions' => [ 'logout' => true, ], ], 'GrantPermissionGroups' => [ 'basic' => 'hidden', 'editpage' => 'page-interaction', 'createeditmovepage' => 'page-interaction', 'editprotected' => 'page-interaction', 'patrol' => 'page-interaction', 'uploadfile' => 'file-interaction', 'uploadeditmovefile' => 'file-interaction', 'sendemail' => 'email', 'viewmywatchlist' => 'watchlist-interaction', 'editmywatchlist' => 'watchlist-interaction', 'editmycssjs' => 'customization', 'editmyoptions' => 'customization', 'editinterface' => 'administration', 'editsiteconfig' => 'administration', 'rollback' => 'administration', 'blockusers' => 'administration', 'delete' => 'administration', 'viewdeleted' => 'administration', 'viewrestrictedlogs' => 'administration', 'protect' => 'administration', 'oversight' => 'administration', 'createaccount' => 'administration', 'mergehistory' => 'administration', 'import' => 'administration', 'highvolume' => 'high-volume', 'privateinfo' => 'private-information', 'managesessions' => 'private-information', ], 'GrantRiskGroups' => [ 'basic' => 'low', 'editpage' => 'low', 'createeditmovepage' => 'low', 'editprotected' => 'vandalism', 'patrol' => 'low', 'uploadfile' => 'low', 'uploadeditmovefile' => 'low', 'sendemail' => 'security', 'viewmywatchlist' => 'low', 'editmywatchlist' => 'low', 'editmycssjs' => 'security', 'editmyoptions' => 'security', 'editinterface' => 'vandalism', 'editsiteconfig' => 'security', 'rollback' => 'low', 'blockusers' => 'vandalism', 'delete' => 'vandalism', 'viewdeleted' => 'vandalism', 'viewrestrictedlogs' => 'security', 'protect' => 'vandalism', 'oversight' => 'security', 'createaccount' => 'low', 'mergehistory' => 'vandalism', 'import' => 'security', 'highvolume' => 'low', 'privateinfo' => 'low', 'managesessions' => 'low', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'BotPasswordsLimit' => 100, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'ReauthenticateForActions' => [ 'edituserjs' => 'edituserjscss', 'editusercss' => 'edituserjscss', 'editsitejs' => 'editsitejscss', 'editsitecss' => 'editsitejscss', ], 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => false, 'CSPUseReportURIDirective' => false, 'CSPFalsePositiveUrls' => [ 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'chrome-extension' => true, ], 'AllowCrossOrigin' => false, 'RestAllowCrossOriginCookieAuth' => false, 'SessionSecret' => false, 'CookieExpiration' => 2592000, 'ExtendedLoginCookieExpiration' => 15552000, 'SessionCookieJwtExpiration' => 14400, 'CookieDomain' => '', 'CookiePath' => '/', 'CookieSecure' => 'detect', 'CookiePrefix' => false, 'CookieHttpOnly' => true, 'CookieSameSite' => null, 'CacheVaryCookies' => [ ], 'SessionName' => false, 'CookieSetOnAutoblock' => true, 'CookieSetOnIpBlock' => true, 'DebugLogFile' => '', 'DebugLogPrefix' => '', 'DebugRedirects' => false, 'DebugRawPage' => false, 'DebugComments' => false, 'DebugDumpSql' => false, 'TrxProfilerLimits' => [ 'GET' => [ 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'POST-nonwrite' => [ 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 10000, 'maxAffected' => 1000, 'masterConns' => 0, 'writes' => 0, ], 'PostSend-POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'JobRunner' => [ 'readQueryTime' => 30, 'writeQueryTime' => 5, 'readQueryRows' => 100000, 'maxAffected' => 500, ], 'Maintenance' => [ 'writeQueryTime' => 5, 'maxAffected' => 1000, ], ], 'DebugLogGroups' => [ ], 'MWLoggerDefaultSpi' => [ 'class' => 'MediaWiki\\Logger\\LegacySpi', ], 'ShowDebug' => false, 'SpecialVersionShowHooks' => false, 'ShowExceptionDetails' => false, 'LogExceptionBacktrace' => true, 'PropagateErrors' => true, 'ShowHostnames' => false, 'OverrideHostname' => false, 'DevelopmentWarnings' => false, 'DeprecationReleaseLimit' => false, 'Profiler' => [ ], 'StatsdServer' => false, 'StatsdMetricPrefix' => 'MediaWiki', 'StatsTarget' => null, 'StatsFormat' => null, 'StatsPrefix' => 'mediawiki', 'OpenTelemetryConfig' => null, 'PageInfoTransclusionLimit' => 50, 'EnableJavaScriptTest' => false, 'DebugToolbar' => false, 'ApiClientErrorSampleRate' => 1.0, 'DisableTextSearch' => false, 'AdvancedSearchHighlighting' => false, 'SearchHighlightBoundaries' => '[\\p{Z}\\p{P}\\p{C}]', 'OpenSearchTemplates' => [ 'application/x-suggestions+json' => false, 'application/x-suggestions+xml' => false, ], 'OpenSearchDefaultLimit' => 10, 'OpenSearchDescriptionLength' => 100, 'SearchSuggestCacheExpiry' => 1200, 'DisableSearchUpdate' => false, 'NamespacesToBeSearchedDefault' => [ true, ], 'DisableInternalSearch' => false, 'SearchForwardUrl' => null, 'SitemapNamespaces' => false, 'SitemapNamespacesPriorities' => false, 'SitemapApiConfig' => [ ], 'SpecialSearchFormOptions' => [ ], 'SearchMatchRedirectPreference' => false, 'SearchRunSuggestedQuery' => true, 'Diff3' => '/usr/bin/diff3', 'Diff' => '/usr/bin/diff', 'PreviewOnOpenNamespaces' => [ 14 => true, ], 'UniversalEditButton' => true, 'UseAutomaticEditSummaries' => true, 'CommandLineDarkBg' => false, 'ReadOnly' => null, 'ReadOnlyWatchedItemStore' => false, 'ReadOnlyFile' => false, 'UpgradeKey' => false, 'GitBin' => '/usr/bin/git', 'GitRepositoryViewers' => [ 'https: 'ssh: 'https: 'git@github\\.com:(.*?)(\\.git)?' => 'https: ], 'InstallerInitialPages' => [ [ 'titlemsg' => 'mainpage', 'text' => '{{subst:int:mainpagetext}}{{subst:int:mainpagedocfooter}}', ], ], 'RCMaxAge' => 7776000, 'WatchersMaxAge' => 15552000, 'UnwatchedPageSecret' => 1, 'RCFilterByAge' => false, 'RCLinkLimits' => [ 50, 100, 250, 500, ], 'RCLinkDays' => [ 1, 3, 7, 14, 30, ], 'RCFeeds' => [ ], 'RCWatchCategoryMembership' => false, 'UseRCPatrol' => true, 'StructuredChangeFiltersLiveUpdatePollingRate' => 3, 'UseNPPatrol' => true, 'UseFilePatrol' => true, 'Feed' => true, 'FeedLimit' => 50, 'FeedCacheTimeout' => 60, 'FeedDiffCutoff' => 32768, 'OverrideSiteFeed' => [ ], 'FeedClasses' => [ 'rss' => 'MediaWiki\\Feed\\RSSFeed', 'atom' => 'MediaWiki\\Feed\\AtomFeed', ], 'AdvertisedFeedTypes' => [ 'atom', ], 'RCShowWatchingUsers' => false, 'RCShowChangedSize' => true, 'RCChangedSizeThreshold' => 500, 'ShowUpdatedMarker' => true, 'DisableAnonTalk' => false, 'UseTagFilter' => true, 'SoftwareTags' => [ 'mw-contentmodelchange' => true, 'mw-new-redirect' => true, 'mw-removed-redirect' => true, 'mw-changed-redirect-target' => true, 'mw-blank' => true, 'mw-replace' => true, 'mw-recreated' => true, 'mw-rollback' => true, 'mw-undo' => true, 'mw-manual-revert' => true, 'mw-reverted' => true, 'mw-server-side-upload' => true, 'mw-ipblock-appeal' => true, 'mw-edited-other-users-js' => true, 'mw-edited-other-users-css' => true, ], 'RestrictedTagViewRights' => [ ], 'UnwatchedPageThreshold' => false, 'RecentChangesFlags' => [ 'newpage' => [ 'letter' => 'newpageletter', 'title' => 'recentchanges-label-newpage', 'legend' => 'recentchanges-legend-newpage', 'grouping' => 'any', ], 'minor' => [ 'letter' => 'minoreditletter', 'title' => 'recentchanges-label-minor', 'legend' => 'recentchanges-legend-minor', 'class' => 'minoredit', 'grouping' => 'all', ], 'bot' => [ 'letter' => 'boteditletter', 'title' => 'recentchanges-label-bot', 'legend' => 'recentchanges-legend-bot', 'class' => 'botedit', 'grouping' => 'all', ], 'unpatrolled' => [ 'letter' => 'unpatrolledletter', 'title' => 'recentchanges-label-unpatrolled', 'legend' => 'recentchanges-legend-unpatrolled', 'grouping' => 'any', ], ], 'WatchlistExpiry' => false, 'EnableWatchstarPopover' => false, 'EnableWatchlistLabels' => false, 'WatchlistLabelsMaxPerUser' => 100, 'WatchlistPurgeRate' => 0.1, 'WatchlistExpiryMaxDuration' => '1 year', 'EnableChangesListQueryPartitioning' => false, 'RightsPage' => null, 'RightsUrl' => null, 'RightsText' => null, 'RightsIcon' => null, 'UseCopyrightUpload' => false, 'MaxCredits' => 0, 'ShowCreditsIfMax' => true, 'ImportSources' => [ ], 'ImportTargetNamespace' => null, 'ExportAllowHistory' => true, 'ExportMaxHistory' => 0, 'ExportAllowListContributors' => false, 'ExportMaxLinkDepth' => 0, 'ExportFromNamespaces' => false, 'ExportAllowAll' => false, 'ExportPagelistLimit' => 5000, 'XmlDumpSchemaVersion' => '0.11', 'WikiFarmSettingsDirectory' => null, 'WikiFarmSettingsExtension' => 'yaml', 'ExtensionFunctions' => [ ], 'ExtensionMessagesFiles' => [ ], 'MessagesDirs' => [ ], 'TranslationAliasesDirs' => [ ], 'ExtensionEntryPointListFiles' => [ ], 'EnableParserLimitReporting' => true, 'ValidSkinNames' => [ ], 'SpecialPages' => [ ], 'ExtensionCredits' => [ ], 'Hooks' => [ ], 'ServiceWiringFiles' => [ ], 'JobClasses' => [ 'deletePage' => 'MediaWiki\\Page\\DeletePageJob', 'refreshLinks' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'deleteLinks' => 'MediaWiki\\Page\\DeleteLinksJob', 'htmlCacheUpdate' => 'MediaWiki\\JobQueue\\Jobs\\HTMLCacheUpdateJob', 'sendMail' => [ 'class' => 'MediaWiki\\Mail\\EmaillingJob', 'services' => [ 'Emailer', ], ], 'enotifNotify' => [ 'class' => 'MediaWiki\\RecentChanges\\RecentChangeNotifyJob', 'services' => [ 'RecentChangeLookup', ], ], 'fixDoubleRedirect' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\DoubleRedirectJob', 'services' => [ 'RevisionLookup', 'MagicWordFactory', 'WikiPageFactory', ], 'needsPage' => true, ], 'AssembleUploadChunks' => 'MediaWiki\\JobQueue\\Jobs\\AssembleUploadChunksJob', 'PublishStashedFile' => 'MediaWiki\\JobQueue\\Jobs\\PublishStashedFileJob', 'ThumbnailRender' => 'MediaWiki\\JobQueue\\Jobs\\ThumbnailRenderJob', 'UploadFromUrl' => 'MediaWiki\\JobQueue\\Jobs\\UploadFromUrlJob', 'recentChangesUpdate' => 'MediaWiki\\RecentChanges\\RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'refreshLinksDynamic' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'activityUpdateJob' => 'MediaWiki\\Watchlist\\ActivityUpdateJob', 'categoryMembershipChange' => [ 'class' => 'MediaWiki\\RecentChanges\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => [ 'class' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', 'needsPage' => false, ], 'cdnPurge' => 'MediaWiki\\JobQueue\\Jobs\\CdnPurgeJob', 'userGroupExpiry' => 'MediaWiki\\User\\UserGroupExpiryJob', 'clearWatchlistNotifications' => 'MediaWiki\\Watchlist\\ClearWatchlistNotificationsJob', 'userOptionsUpdate' => 'MediaWiki\\User\\Options\\UserOptionsUpdateJob', 'revertedTagUpdate' => 'MediaWiki\\JobQueue\\Jobs\\RevertedTagUpdateJob', 'null' => 'MediaWiki\\JobQueue\\Jobs\\NullJob', 'userEditCountInit' => 'MediaWiki\\User\\UserEditCountInitJob', 'renameUserTable' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], 'renameUserDerived' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserDerivedJob', 'services' => [ 'RenameUserFactory', 'UserFactory', ], ], 'renameUser' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], ], 'JobTypesExcludedFromDefaultQueue' => [ 'AssembleUploadChunks', 'PublishStashedFile', 'UploadFromUrl', ], 'JobBackoffThrottling' => [ ], 'JobTypeConf' => [ 'default' => [ 'class' => 'MediaWiki\\JobQueue\\JobQueueDB', 'order' => 'random', 'claimTTL' => 3600, ], ], 'JobQueueIncludeInMaxLagFactor' => false, 'SpecialPageCacheUpdates' => [ 'Statistics' => [ 'MediaWiki\\Deferred\\SiteStatsUpdate', 'cacheUpdate', ], ], 'PagePropLinkInvalidations' => [ 'hiddencat' => 'categorylinks', ], 'CategoryMagicGallery' => true, 'CategoryPagingLimit' => 200, 'CategoryCollation' => 'uppercase', 'TempCategoryCollations' => [ ], 'SortedCategories' => false, 'TrackingCategories' => [ ], 'LogTypes' => [ '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'import', 'interwiki', 'patrol', 'merge', 'suppress', 'tag', 'managetags', 'contentmodel', 'renameuser', ], 'LogRestrictions' => [ 'suppress' => 'suppressionlog', ], 'FilterLogTypes' => [ 'patrol' => true, 'tag' => true, 'newusers' => false, ], 'LogNames' => [ '' => 'all-logs-page', 'block' => 'blocklogpage', 'protect' => 'protectlogpage', 'rights' => 'rightslog', 'delete' => 'dellogpage', 'upload' => 'uploadlogpage', 'move' => 'movelogpage', 'import' => 'importlogpage', 'patrol' => 'patrol-log-page', 'merge' => 'mergelog', 'suppress' => 'suppressionlog', ], 'LogHeaders' => [ '' => 'alllogstext', 'block' => 'blocklogtext', 'delete' => 'dellogpagetext', 'import' => 'importlogpagetext', 'merge' => 'mergelogpagetext', 'move' => 'movelogpagetext', 'patrol' => 'patrol-log-header', 'protect' => 'protectlogtext', 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', 'upload' => 'uploadlogpagetext', ], 'LogActions' => [ ], 'LogActionsHandlers' => [ 'block/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/unblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'contentmodel/change' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'contentmodel/new' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'delete/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir2' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/restore' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'import/interwiki' => 'MediaWiki\\Logging\\ImportLogFormatter', 'import/upload' => 'MediaWiki\\Logging\\ImportLogFormatter', 'interwiki/iw_add' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_delete' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_edit' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'managetags/activate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/create' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/deactivate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/delete' => 'MediaWiki\\Logging\\LogFormatter', 'merge/merge' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'merge/merge-into' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move_redir' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'patrol/patrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'patrol/autopatrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'protect/modify' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/move_prot' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/protect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/unprotect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'renameuser/renameuser' => [ 'class' => 'MediaWiki\\Logging\\RenameuserLogFormatter', 'services' => [ 'TitleParser', ], ], 'rights/autopromote' => 'MediaWiki\\Logging\\RightsLogFormatter', 'rights/rights' => 'MediaWiki\\Logging\\RightsLogFormatter', 'suppress/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'tag/update' => 'MediaWiki\\Logging\\TagLogFormatter', 'upload/overwrite' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/revert' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/upload' => 'MediaWiki\\Logging\\UploadLogFormatter', ], 'ActionFilteredLogs' => [ 'block' => [ 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], 'unblock' => [ 'unblock', ], ], 'contentmodel' => [ 'change' => [ 'change', ], 'new' => [ 'new', ], ], 'delete' => [ 'delete' => [ 'delete', ], 'delete_redir' => [ 'delete_redir', 'delete_redir2', ], 'restore' => [ 'restore', ], 'event' => [ 'event', ], 'revision' => [ 'revision', ], ], 'import' => [ 'interwiki' => [ 'interwiki', ], 'upload' => [ 'upload', ], ], 'managetags' => [ 'create' => [ 'create', ], 'delete' => [ 'delete', ], 'activate' => [ 'activate', ], 'deactivate' => [ 'deactivate', ], ], 'move' => [ 'move' => [ 'move', ], 'move_redir' => [ 'move_redir', ], ], 'newusers' => [ 'create' => [ 'create', 'newusers', ], 'create2' => [ 'create2', ], 'autocreate' => [ 'autocreate', ], 'byemail' => [ 'byemail', ], ], 'protect' => [ 'protect' => [ 'protect', ], 'modify' => [ 'modify', ], 'unprotect' => [ 'unprotect', ], 'move_prot' => [ 'move_prot', ], ], 'rights' => [ 'rights' => [ 'rights', ], 'autopromote' => [ 'autopromote', ], ], 'suppress' => [ 'event' => [ 'event', ], 'revision' => [ 'revision', ], 'delete' => [ 'delete', ], 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], ], 'upload' => [ 'upload' => [ 'upload', ], 'overwrite' => [ 'overwrite', ], 'revert' => [ 'revert', ], ], ], 'NewUserLog' => true, 'PageCreationLog' => true, 'AllowSpecialInclusion' => true, 'DisableQueryPageUpdate' => false, 'CountCategorizedImagesAsUsed' => false, 'MaxRedirectLinksRetrieved' => 500, 'RangeContributionsCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 32, ], 'Actions' => [ ], 'DefaultRobotPolicy' => 'index,follow', 'NamespaceRobotPolicies' => [ ], 'ArticleRobotPolicies' => [ ], 'ExemptFromUserRobotsControl' => null, 'DebugAPI' => false, 'APIModules' => [ ], 'APIFormatModules' => [ ], 'APIMetaModules' => [ ], 'APIPropModules' => [ ], 'APIListModules' => [ ], 'APIMaxDBRows' => 5000, 'APIMaxResultSize' => 8388608, 'APIMaxUncachedDiffs' => 1, 'APIMaxLagThreshold' => 7, 'APICacheHelpTimeout' => 3600, 'APIUselessQueryPages' => [ 'MIMEsearch', 'LinkSearch', ], 'AjaxLicensePreview' => true, 'CrossSiteAJAXdomains' => [ ], 'CrossSiteAJAXdomainExceptions' => [ ], 'AllowedCorsHeaders' => [ 'Accept', 'Accept-Language', 'Content-Language', 'Content-Type', 'Accept-Encoding', 'DNT', 'Origin', 'User-Agent', 'Api-User-Agent', 'Promise-Non-Write-API-Action', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestLocalModuleTestBaseUrl' => null, 'RestModuleOverrides' => [ ], 'RestExternalModules' => [ ], 'RestTermsOfServiceUrl' => null, 'MaxShellMemory' => 307200, 'MaxShellFileSize' => 102400, 'MaxShellTime' => 180, 'MaxShellWallClockTime' => 180, 'ShellCgroup' => false, 'PhpCli' => '/usr/bin/php', 'ShellRestrictionMethod' => 'autodetect', 'ShellboxUrls' => [ 'default' => null, ], 'ShellboxSecretKey' => null, 'ShellboxShell' => '/bin/sh', 'HTTPTimeout' => 25, 'HTTPConnectTimeout' => 5.0, 'HTTPMaxTimeout' => 0, 'HTTPMaxConnectTimeout' => 0, 'HTTPImportTimeout' => 25, 'HTTPUserAgentContact' => false, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'GenerateReqIDFormat' => 'rand24', 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'EventRelayerConfig' => [ 'default' => [ 'class' => 'Wikimedia\\EventRelayer\\EventRelayerNull', ], ], 'Pingback' => false, 'OriginTrials' => [ ], 'ReportToExpiry' => 86400, 'ReportToEndpoints' => [ ], 'FeaturePolicyReportOnly' => [ ], 'SkinsPreferred' => [ 'vector-2022', 'vector', ], 'SpecialContributeSkinsEnabled' => [ ], 'SpecialContributeNewPageTarget' => null, 'EnableEditRecovery' => false, 'EditRecoveryExpiry' => 2592000, 'UseCodexSpecialBlock' => false, 'ShowLogoutConfirmation' => false, 'EnableProtectionIndicators' => true, 'OutputPipelineStages' => [ ], 'FeatureShutdown' => [ ], 'UseLeximorph' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, 'ParserOptionsLogUnsafeSampleRate' => 0, 'ReturnExperimentalPFragmentTypes' => [ ], 'UseParsoidLinksUpdate' => null, 'UseParsoidMessages' => false, 'UseParsoidParser' => true, 'SiteLookup' => [ ], ], 'type' => [ 'ConfigRegistry' => 'object', 'AssumeProxiesUseDefaultProtocolPorts' => 'boolean', 'ForceHTTPS' => 'boolean', 'ExtensionDirectory' => [ 'string', 'null', ], 'StyleDirectory' => [ 'string', 'null', ], 'UploadDirectory' => [ 'string', 'boolean', 'null', ], 'Logos' => [ 'object', 'boolean', ], 'ReferrerPolicy' => [ 'array', 'string', 'boolean', ], 'ActionPaths' => 'object', 'MainPageIsDomainRoot' => 'boolean', 'ImgAuthUrlPathMap' => 'object', 'LocalFileRepo' => 'object', 'ForeignFileRepos' => 'array', 'UseSharedUploads' => 'boolean', 'SharedUploadDirectory' => [ 'string', 'null', ], 'SharedUploadPath' => [ 'string', 'null', ], 'HashedSharedUploadDirectory' => 'boolean', 'FetchCommonsDescriptions' => 'boolean', 'SharedUploadDBname' => [ 'boolean', 'string', ], 'SharedUploadDBprefix' => 'string', 'SharedUploadDBschema' => [ 'string', 'null', ], 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', 'DefaultLockManager' => [ 'string', 'null', ], 'CopyUploadsDomains' => 'array', 'CopyUploadTimeout' => [ 'boolean', 'integer', ], 'SharedThumbnailScriptPath' => [ 'string', 'boolean', ], 'HashedUploadDirectory' => 'boolean', 'CSPUploadEntryPoint' => 'boolean', 'FileExtensions' => 'array', 'ProhibitedFileExtensions' => 'array', 'MimeTypeExclusions' => 'array', 'TrustedMediaFormats' => 'array', 'MediaHandlers' => 'object', 'NativeImageLazyLoading' => 'boolean', 'ParserTestMediaHandlers' => 'object', 'MaxInterlacingAreas' => 'object', 'SVGConverters' => 'object', 'SVGNativeRendering' => [ 'string', 'boolean', ], 'MaxImageArea' => [ 'string', 'integer', 'boolean', ], 'WebPThumbnailType' => 'array', 'TiffThumbnailType' => 'array', 'GenerateThumbnailOnParse' => 'boolean', 'EnableAutoRotation' => [ 'boolean', 'null', ], 'Antivirus' => [ 'string', 'null', ], 'AntivirusSetup' => 'object', 'MimeDetectorCommand' => [ 'string', 'null', ], 'XMLMimeTypes' => 'object', 'ImageLimits' => 'array', 'ThumbLimits' => 'array', 'ThumbnailNamespaces' => 'array', 'ThumbnailSteps' => [ 'array', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EmailConfirmationBanner' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'RemoteVirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ExternalLinksDomainGaps' => 'object', 'ContentHandlers' => 'object', 'NamespaceContentModels' => 'object', 'TextModelsToParse' => 'array', 'ExternalStores' => 'array', 'ExternalServers' => 'object', 'DefaultExternalStore' => [ 'array', 'boolean', ], 'RevisionCacheExpiry' => 'integer', 'PageLanguageUseDB' => 'boolean', 'DiffEngine' => [ 'string', 'null', ], 'ExternalDiffEngine' => [ 'string', 'boolean', ], 'Wikidiff2Options' => 'object', 'RequestTimeLimit' => [ 'integer', 'null', ], 'CriticalSectionTimeLimit' => 'number', 'PoolCounterConf' => [ 'object', 'null', ], 'PoolCountClientConf' => 'object', 'MaxUserDBWriteDuration' => [ 'integer', 'boolean', ], 'MaxJobDBWriteDuration' => [ 'integer', 'boolean', ], 'MultiShardSiteStats' => 'boolean', 'ObjectCaches' => 'object', 'WANObjectCache' => 'object', 'MicroStashType' => [ 'string', 'integer', ], 'ParsoidCacheConfig' => 'object', 'ParsoidSelectiveUpdateSampleRate' => 'integer', 'SplitParsoidParserCache' => 'boolean', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'SuspiciousIpExpiry' => [ 'integer', 'boolean', ], 'MemCachedServers' => 'array', 'LocalisationCacheConf' => 'object', 'ExtensionInfoMTime' => [ 'integer', 'boolean', ], 'CdnServers' => 'object', 'CdnServersNoPurge' => 'object', 'HTCPRouting' => 'object', 'GrammarForms' => 'object', 'ExtraInterlanguageLinkPrefixes' => 'array', 'InterlanguageLinkCodeMap' => 'object', 'ExtraLanguageNames' => 'object', 'ExtraLanguageCodes' => 'object', 'DummyLanguageCodes' => 'object', 'DisabledVariants' => 'object', 'ForceUIMsgAsContentMsg' => 'object', 'RawHtmlMessages' => 'array', 'OverrideUcfirstCharacters' => 'object', 'XhtmlNamespaces' => 'object', 'BrowserFormatDetection' => 'string', 'SkinMetaTags' => 'object', 'SkipSkins' => 'object', 'FragmentMode' => 'array', 'FooterIcons' => 'object', 'PageShareSkinsEnabled' => 'array', 'InterwikiLogoOverride' => 'array', 'ResourceModules' => 'object', 'ResourceModuleSkinStyles' => 'object', 'ResourceLoaderSources' => 'object', 'ResourceLoaderMaxage' => 'object', 'ResourceLoaderMaxQueryLength' => [ 'integer', 'boolean', ], 'CanonicalNamespaceNames' => 'object', 'ExtraNamespaces' => 'object', 'ExtraGenderNamespaces' => 'object', 'NamespaceAliases' => 'object', 'CapitalLinkOverrides' => 'object', 'NamespacesWithSubpages' => 'object', 'NamespacesWithoutAutoSummaries' => 'array', 'ContentNamespaces' => 'array', 'ShortPagesNamespaceExclusions' => 'array', 'ExtraSignatureNamespaces' => 'array', 'InvalidRedirectTargets' => 'array', 'LocalInterwikis' => 'array', 'InterwikiCache' => [ 'boolean', 'object', ], 'SiteTypes' => 'object', 'UrlProtocols' => 'array', 'TidyConfig' => 'object', 'ParsoidSettings' => 'object', 'ParsoidExperimentalParserFunctionOutput' => 'boolean', 'NoFollowNsExceptions' => 'array', 'NoFollowDomainExceptions' => 'array', 'ExternalLinksIgnoreDomains' => 'array', 'EnableMagicLinks' => 'object', 'ManualRevertSearchRadius' => 'integer', 'RevertedTagMaxDepth' => 'integer', 'CentralIdLookupProviders' => 'object', 'CentralIdLookupProvider' => 'string', 'UserRegistrationProviders' => 'object', 'PasswordPolicy' => 'object', 'AuthManagerConfig' => [ 'object', 'null', ], 'AuthManagerAutoConfig' => 'object', 'RememberMe' => 'string', 'ReauthenticateTime' => 'object', 'ChangeCredentialsBlacklist' => 'array', 'RemoveCredentialsBlacklist' => 'array', 'PasswordConfig' => 'object', 'PasswordResetRoutes' => 'object', 'SignatureAllowedLintErrors' => 'array', 'ReservedUsernames' => 'array', 'DefaultUserOptions' => 'object', 'ConditionalUserOptions' => 'object', 'HiddenPrefs' => 'array', 'UserJsPrefLimit' => 'integer', 'AuthenticationTokenVersion' => [ 'string', 'null', ], 'SessionProviders' => 'object', 'AutoCreateTempUser' => 'object', 'AutoblockExemptions' => 'array', 'BlockCIDRLimit' => 'object', 'EnableMultiBlocks' => 'boolean', 'GroupPermissions' => 'object', 'PrivilegedGroups' => 'array', 'RevokePermissions' => 'object', 'GroupInheritsPermissions' => 'object', 'ImplicitGroups' => 'array', 'GroupsAddToSelf' => 'object', 'GroupsRemoveFromSelf' => 'object', 'RestrictedGroups' => 'object', 'UserRequirementsPrivateConditions' => 'array', 'RestrictionTypes' => 'array', 'RestrictionLevels' => 'array', 'CascadingRestrictionLevels' => 'array', 'SemiprotectedRestrictionLevels' => 'array', 'NamespaceProtection' => 'object', 'RestrictUserPageEditing' => 'boolean', 'NonincludableNamespaces' => 'object', 'Autopromote' => 'object', 'AutopromoteOnce' => 'object', 'AutopromoteOnceRCExcludedGroups' => 'array', 'AddGroups' => 'object', 'RemoveGroups' => 'object', 'AvailableRights' => 'array', 'ImplicitRights' => 'array', 'AccountCreationThrottle' => [ 'integer', 'array', ], 'TempAccountCreationThrottle' => 'array', 'TempAccountNameAcquisitionThrottle' => 'array', 'SpamRegex' => 'array', 'SummarySpamRegex' => 'array', 'DnsBlacklistUrls' => 'array', 'ProxyList' => [ 'string', 'array', ], 'ProxyWhitelist' => 'array', 'SoftBlockRanges' => 'array', 'RateLimits' => 'object', 'RateLimitsExcludedIPs' => 'array', 'ExternalQuerySources' => 'object', 'PasswordAttemptThrottle' => 'array', 'GrantPermissions' => 'object', 'GrantPermissionGroups' => 'object', 'GrantRiskGroups' => 'object', 'EnableBotPasswords' => 'boolean', 'BotPasswordsCluster' => [ 'string', 'boolean', ], 'BotPasswordsDatabase' => [ 'string', 'boolean', ], 'BotPasswordsLimit' => 'integer', 'ReauthenticateForActions' => 'object', 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPUseReportURIDirective' => [ 'boolean', 'object', ], 'CSPFalsePositiveUrls' => 'object', 'AllowCrossOrigin' => 'boolean', 'RestAllowCrossOriginCookieAuth' => 'boolean', 'CookieSameSite' => [ 'string', 'null', ], 'CacheVaryCookies' => 'array', 'TrxProfilerLimits' => 'object', 'DebugLogGroups' => 'object', 'MWLoggerDefaultSpi' => 'object', 'Profiler' => 'object', 'StatsTarget' => [ 'string', 'null', ], 'StatsFormat' => [ 'string', 'null', ], 'StatsPrefix' => 'string', 'OpenTelemetryConfig' => [ 'object', 'null', ], 'OpenSearchTemplates' => 'object', 'NamespacesToBeSearchedDefault' => 'object', 'SitemapNamespaces' => [ 'boolean', 'array', ], 'SitemapNamespacesPriorities' => [ 'boolean', 'object', ], 'SitemapApiConfig' => 'object', 'SpecialSearchFormOptions' => 'object', 'SearchMatchRedirectPreference' => 'boolean', 'SearchRunSuggestedQuery' => 'boolean', 'PreviewOnOpenNamespaces' => 'object', 'ReadOnlyWatchedItemStore' => 'boolean', 'GitRepositoryViewers' => 'object', 'InstallerInitialPages' => 'array', 'RCLinkLimits' => 'array', 'RCLinkDays' => 'array', 'RCFeeds' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RestrictedTagViewRights' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchstarPopover' => 'boolean', 'EnableWatchlistLabels' => 'boolean', 'WatchlistLabelsMaxPerUser' => 'integer', 'WatchlistPurgeRate' => 'number', 'WatchlistExpiryMaxDuration' => [ 'string', 'null', ], 'EnableChangesListQueryPartitioning' => 'boolean', 'ImportSources' => 'object', 'ExtensionFunctions' => 'array', 'ExtensionMessagesFiles' => 'object', 'MessagesDirs' => 'object', 'TranslationAliasesDirs' => 'object', 'ExtensionEntryPointListFiles' => 'object', 'ValidSkinNames' => 'object', 'SpecialPages' => 'object', 'ExtensionCredits' => 'object', 'Hooks' => 'object', 'ServiceWiringFiles' => 'array', 'JobClasses' => 'object', 'JobTypesExcludedFromDefaultQueue' => 'array', 'JobBackoffThrottling' => 'object', 'JobTypeConf' => 'object', 'SpecialPageCacheUpdates' => 'object', 'PagePropLinkInvalidations' => 'object', 'TempCategoryCollations' => 'array', 'SortedCategories' => 'boolean', 'TrackingCategories' => 'array', 'LogTypes' => 'array', 'LogRestrictions' => 'object', 'FilterLogTypes' => 'object', 'LogNames' => 'object', 'LogHeaders' => 'object', 'LogActions' => 'object', 'LogActionsHandlers' => 'object', 'ActionFilteredLogs' => 'object', 'RangeContributionsCIDRLimit' => 'object', 'Actions' => 'object', 'NamespaceRobotPolicies' => 'object', 'ArticleRobotPolicies' => 'object', 'ExemptFromUserRobotsControl' => [ 'array', 'null', ], 'APIModules' => 'object', 'APIFormatModules' => 'object', 'APIMetaModules' => 'object', 'APIPropModules' => 'object', 'APIListModules' => 'object', 'APIUselessQueryPages' => 'array', 'CrossSiteAJAXdomains' => 'object', 'CrossSiteAJAXdomainExceptions' => 'object', 'AllowedCorsHeaders' => 'array', 'RestAPIAdditionalRouteFiles' => 'array', 'RestLocalModuleTestBaseUrl' => [ 'string', 'null', ], 'RestModuleOverrides' => 'object', 'RestExternalModules' => 'object', 'RestTermsOfServiceUrl' => [ 'string', 'null', ], 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'HTTPUserAgentContact' => [ 'string', 'boolean', ], 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'GenerateReqIDFormat' => 'string', 'EventRelayerConfig' => 'object', 'Pingback' => 'boolean', 'OriginTrials' => 'array', 'ReportToExpiry' => 'integer', 'ReportToEndpoints' => 'array', 'FeaturePolicyReportOnly' => 'array', 'SkinsPreferred' => 'array', 'SpecialContributeSkinsEnabled' => 'array', 'SpecialContributeNewPageTarget' => [ 'string', 'null', ], 'EnableEditRecovery' => 'boolean', 'EditRecoveryExpiry' => 'integer', 'UseCodexSpecialBlock' => 'boolean', 'ShowLogoutConfirmation' => 'boolean', 'EnableProtectionIndicators' => 'boolean', 'OutputPipelineStages' => 'object', 'FeatureShutdown' => 'array', 'UseLeximorph' => 'boolean', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', 'ReturnExperimentalPFragmentTypes' => 'array', 'UseParsoidLinksUpdate' => [ 'boolean', 'null', ], 'UseParsoidMessages' => [ 'boolean', 'null', ], 'UseParsoidParser' => 'boolean', 'SiteLookup' => 'object', ], 'mergeStrategy' => [ 'WebPThumbnailType' => 'replace', 'TiffThumbnailType' => 'replace', 'LBFactoryConf' => 'replace', 'InterwikiCache' => 'replace', 'PasswordPolicy' => 'array_replace_recursive', 'AuthManagerAutoConfig' => 'array_plus_2d', 'GroupPermissions' => 'array_plus_2d', 'RevokePermissions' => 'array_plus_2d', 'AddGroups' => 'array_merge_recursive', 'RemoveGroups' => 'array_merge_recursive', 'RateLimits' => 'array_plus_2d', 'GrantPermissions' => 'array_plus_2d', 'MWLoggerDefaultSpi' => 'replace', 'Profiler' => 'replace', 'Hooks' => 'array_merge_recursive', 'RestModuleOverrides' => 'array_replace_recursive', 'RestExternalModules' => 'array_replace_recursive', ], 'dynamicDefault' => [ 'UsePathInfo' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUsePathInfo', ], ], 'Script' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultScript', ], ], 'LoadScript' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLoadScript', ], ], 'RestPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultRestPath', ], ], 'StylePath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultStylePath', ], ], 'LocalStylePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalStylePath', ], ], 'ExtensionAssetsPath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultExtensionAssetsPath', ], ], 'ArticlePath' => [ 'use' => [ 'Script', 'UsePathInfo', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultArticlePath', ], ], 'UploadPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUploadPath', ], ], 'FileCacheDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultFileCacheDirectory', ], ], 'Logo' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLogo', ], ], 'DeletedDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDeletedDirectory', ], ], 'ShowEXIF' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultShowEXIF', ], ], 'SharedPrefix' => [ 'use' => [ 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedPrefix', ], ], 'SharedSchema' => [ 'use' => [ 'DBmwschema', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedSchema', ], ], 'DBerrorLogTZ' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDBerrorLogTZ', ], ], 'Localtimezone' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocaltimezone', ], ], 'LocalTZoffset' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalTZoffset', ], ], 'ResourceBasePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultResourceBasePath', ], ], 'MetaNamespace' => [ 'use' => [ 'Sitename', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultMetaNamespace', ], ], 'CookieSecure' => [ 'use' => [ 'ForceHTTPS', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookieSecure', ], ], 'CookiePrefix' => [ 'use' => [ 'SharedDB', 'SharedPrefix', 'SharedTables', 'DBname', 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookiePrefix', ], ], 'ReadOnlyFile' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultReadOnlyFile', ], ], ], ], 'config-schema' => [ 'UploadStashScalerBaseUrl' => [ 'deprecated' => 'since 1.36 Use thumbProxyUrl in $wgLocalFileRepo', ], 'IllegalFileChars' => [ 'deprecated' => 'since 1.41; no longer customizable', ], 'ThumbnailNamespaces' => [ 'items' => [ 'type' => 'integer', ], ], 'LocalDatabases' => [ 'items' => [ 'type' => 'string', ], ], 'ParserCacheFilterConfig' => [ 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of namespace IDs to filter definitions.', 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of filter names to values.', 'properties' => [ 'minCpuTime' => [ 'type' => 'number', ], ], ], ], ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => [ 'integer', 'array', ], ], ], 'ChangeCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'RemoveCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'GroupPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GroupInheritsPermissions' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'AvailableRights' => [ 'items' => [ 'type' => 'string', ], ], 'ImplicitRights' => [ 'items' => [ 'type' => 'string', ], ], 'SoftBlockRanges' => [ 'items' => [ 'type' => 'string', ], ], 'ExternalQuerySources' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'enabled' => [ 'type' => 'boolean', 'default' => false, ], 'url' => [ 'type' => 'string', 'format' => 'uri', ], 'timeout' => [ 'type' => 'integer', 'default' => 10, ], ], 'required' => [ 'enabled', 'url', ], 'additionalProperties' => false, ], ], 'GrantPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GrantPermissionGroups' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'SitemapNamespacesPriorities' => [ 'deprecated' => 'since 1.45 and ignored', ], 'SitemapApiConfig' => [ 'additionalProperties' => [ 'enabled' => [ 'type' => 'bool', ], 'sitemapsPerIndex' => [ 'type' => 'int', ], 'pagesPerSitemap' => [ 'type' => 'int', ], 'expiry' => [ 'type' => 'int', ], 'skipRedirects' => [ 'type' => 'bool', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'UseCopyrightUpload' => [ 'deprecated' => 'since 1.47 This feature is being removed.', ], 'JobBackoffThrottling' => [ 'additionalProperties' => [ 'type' => 'number', ], ], 'JobTypeConf' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'class' => [ 'type' => 'string', ], 'order' => [ 'type' => 'string', ], 'claimTTL' => [ 'type' => 'integer', ], ], ], ], 'TrackingCategories' => [ 'deprecated' => 'since 1.25 Extensions should now register tracking categories using the new extension registration system.', ], 'RangeContributionsCIDRLimit' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'availability' => [ 'type' => 'string', ], 'groups' => [ 'type' => 'array', ], ], ], ], 'RestExternalModules' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'info' => [ 'type' => 'object', 'properties' => [ 'version' => [ 'type' => 'string', ], 'title' => [ 'type' => 'string', ], 'x-i18n-title' => [ 'type' => 'string', ], 'description' => [ 'type' => 'string', ], 'x-i18n-description' => [ 'type' => 'string', ], ], 'required' => [ 'version', ], ], 'base' => [ 'type' => 'string', 'format' => 'uri', ], 'spec' => [ 'type' => 'string', 'format' => 'uri', ], ], 'required' => [ 'info', 'base', 'spec', ], ], ], 'ShellboxUrls' => [ 'additionalProperties' => [ 'type' => [ 'string', 'boolean', 'null', ], ], ], ], 'obsolete-config' => [ 'MangleFlashPolicy' => 'Since 1.39; no longer has any effect.', 'EnableOpenSearchSuggest' => 'Since 1.35, no longer used', 'AutoloadAttemptLowercase' => 'Since 1.40; no longer has any effect.', ],]
Content objects represent page content, e.g.
Definition Content.php:28
Marker interface for entities aware of the wiki they belong to.
Represents the target of a wiki link.
Interface for objects (potentially) representing an editable wiki page.
getId( $wikiId=self::LOCAL)
Returns the page ID.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
getNamespace()
Returns the page's namespace number.
getDBkey()
Get the page title in DB key form.
__toString()
Returns an informative human readable unique representation of the page identity, for use as a cache ...
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
Service for constructing RevisionRecord objects.
Service for looking up page revisions.
Service for loading and storing data blobs.
Definition BlobStore.php:19
Interface for objects representing user identity.
Interface for database access objects.
Interface to a relational database.
Definition IDatabase.php:31
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversible SQL statements from a callback.
This class is a delegate to ILBFactory for a given database cluster.
A database connection without write operations.
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
expr(string $field, string $op, $value)
See Expression::__construct()
Result wrapper for grabbing data queried from an IDatabase object.
Interface for query language.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...