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;
69use Wikimedia\Timestamp\TimestampFormat as TS;
70
80class RevisionStore implements RevisionFactory, RevisionLookup, LoggerAwareInterface {
81
82 use LegacyArticleIdAccess;
83
84 public const ROW_CACHE_KEY = 'revision-row-1.29';
85
86 public const ORDER_OLDEST_TO_NEWEST = 'ASC';
87 public const ORDER_NEWEST_TO_OLDEST = 'DESC';
88
89 // Constants for get(...)Between methods
90 public const INCLUDE_OLD = 'include_old';
91 public const INCLUDE_NEW = 'include_new';
92 public const INCLUDE_BOTH = 'include_both';
93 public const INCLUDE_DELETED_REVISIONS = 'include_deleted_revisions';
94
98 private $blobStore;
99
103 private $wikiId;
104
105 private ILoadBalancer $loadBalancer;
106 private WANObjectCache $cache;
107 private BagOStuff $localCache;
108 private CommentStore $commentStore;
109 private ActorStore $actorStore;
110 private LoggerInterface $logger;
111 private NameTableStore $contentModelStore;
112 private NameTableStore $slotRoleStore;
113 private SlotRoleRegistry $slotRoleRegistry;
114 private IContentHandlerFactory $contentHandlerFactory;
115 private HookRunner $hookRunner;
116 private PageStore $pageStore;
117 private TitleFactory $titleFactory;
118 private RecentChangeLookup $recentChangeLookup;
119
144 public function __construct(
145 ILoadBalancer $loadBalancer,
146 SqlBlobStore $blobStore,
147 WANObjectCache $cache,
148 BagOStuff $localCache,
149 CommentStore $commentStore,
150 NameTableStore $contentModelStore,
151 NameTableStore $slotRoleStore,
152 SlotRoleRegistry $slotRoleRegistry,
153 ActorStore $actorStore,
154 IContentHandlerFactory $contentHandlerFactory,
155 PageStore $pageStore,
156 TitleFactory $titleFactory,
157 HookContainer $hookContainer,
158 RecentChangeLookup $recentChangeLookup,
159 $wikiId = WikiAwareEntity::LOCAL
160 ) {
161 Assert::parameterType( [ 'string', 'false' ], $wikiId, '$wikiId' );
162
163 $this->loadBalancer = $loadBalancer;
164 $this->blobStore = $blobStore;
165 $this->cache = $cache;
166 $this->localCache = $localCache;
167 $this->commentStore = $commentStore;
168 $this->contentModelStore = $contentModelStore;
169 $this->slotRoleStore = $slotRoleStore;
170 $this->slotRoleRegistry = $slotRoleRegistry;
171 $this->actorStore = $actorStore;
172 $this->wikiId = $wikiId;
173 $this->logger = new NullLogger();
174 $this->contentHandlerFactory = $contentHandlerFactory;
175 $this->pageStore = $pageStore;
176 $this->titleFactory = $titleFactory;
177 $this->hookRunner = new HookRunner( $hookContainer );
178 $this->recentChangeLookup = $recentChangeLookup;
179 }
180
181 public function setLogger( LoggerInterface $logger ): void {
182 $this->logger = $logger;
183 }
184
188 public function isReadOnly() {
189 return $this->blobStore->isReadOnly();
190 }
191
197 public function getWikiId() {
198 return $this->wikiId;
199 }
200
206 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
207 if ( ( $queryFlags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) {
208 return $this->getPrimaryConnection();
209 } else {
210 return $this->getReplicaConnection();
211 }
212 }
213
218 private function getReplicaConnection( $groups = [] ) {
219 // TODO: Replace with ICP
220 return $this->loadBalancer->getConnection( DB_REPLICA, $groups, $this->wikiId );
221 }
222
223 private function getPrimaryConnection(): IDatabase {
224 // TODO: Replace with ICP
225 return $this->loadBalancer->getConnection( DB_PRIMARY, [], $this->wikiId );
226 }
227
244 public function getTitle( $pageId, $revId, $queryFlags = IDBAccessObject::READ_NORMAL ) {
245 // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069
246 if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
247 wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' );
248 }
249
250 $page = $this->getPage( $pageId, $revId, $queryFlags );
251 return $this->titleFactory->newFromPageIdentity( $page );
252 }
253
264 private function getPage( ?int $pageId, ?int $revId, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
265 if ( !$pageId && !$revId ) {
266 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
267 }
268
269 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
270 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
271 if ( DBAccessObjectUtils::hasFlags( $queryFlags, IDBAccessObject::READ_LATEST_IMMUTABLE ) ) {
272 $queryFlags = IDBAccessObject::READ_NORMAL;
273 }
274
275 // Loading by ID is best
276 if ( $pageId !== null && $pageId > 0 ) {
277 $page = $this->pageStore->getPageById( $pageId, $queryFlags );
278 if ( $page ) {
279 return $this->wrapPage( $page );
280 }
281 }
282
283 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
284 if ( $revId !== null && $revId > 0 ) {
285 $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags )
286 ->join( 'revision', null, 'page_id=rev_page' )
287 ->conds( [ 'rev_id' => $revId ] )
288 ->caller( __METHOD__ );
289
290 $page = $pageQuery->fetchPageRecord();
291 if ( $page ) {
292 return $this->wrapPage( $page );
293 }
294 }
295
296 // If we still don't have a title, fallback to primary DB if that wasn't already happening.
297 if ( $queryFlags === IDBAccessObject::READ_NORMAL ) {
298 $title = $this->getPage( $pageId, $revId, IDBAccessObject::READ_LATEST );
299 if ( $title ) {
300 $this->logger->info(
301 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
302 [ 'exception' => new RuntimeException() ]
303 );
304 return $title;
305 }
306 }
307
308 throw new RevisionAccessException(
309 'Could not determine title for page ID {page_id} and revision ID {rev_id}',
310 [
311 'page_id' => $pageId,
312 'rev_id' => $revId,
313 ]
314 );
315 }
316
317 private function wrapPage( PageIdentity $page ): PageIdentity {
318 if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
319 // NOTE: since there is still a lot of code that needs a full Title,
320 // and uses Title::castFromPageIdentity() to get one, it's beneficial
321 // to create a Title right away if we can, so we don't have to convert
322 // over and over later on.
323 // When there is less need to convert to Title, this special case can
324 // be removed.
325 return $this->titleFactory->newFromPageIdentity( $page );
326 } else {
327 return $page;
328 }
329 }
330
338 private function failOnNull( $value, $name ) {
339 if ( $value === null ) {
340 throw new IncompleteRevisionException(
341 "$name must not be " . var_export( $value, true ) . "!"
342 );
343 }
344
345 return $value;
346 }
347
355 private function failOnEmpty( $value, $name ) {
356 if ( $value === null || $value === 0 || $value === '' ) {
357 throw new IncompleteRevisionException(
358 "$name must not be " . var_export( $value, true ) . "!"
359 );
360 }
361
362 return $value;
363 }
364
379 public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
380 // TODO: pass in a DBTransactionContext instead of a database connection.
381 $this->checkDatabaseDomain( $dbw );
382
383 $slotRoles = $rev->getSlotRoles();
384
385 // Make sure the main slot is always provided throughout migration
386 if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
388 'main slot must be provided'
389 );
390 }
391
392 // Checks
393 $this->failOnNull( $rev->getSize(), 'size field' );
394 $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
395 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
396 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
397 $this->failOnNull( $user->getId(), 'user field' );
398 $this->failOnEmpty( $user->getName(), 'user_text field' );
399
400 if ( !$rev->isReadyForInsertion() ) {
401 // This is here for future-proofing. At the time this check being added, it
402 // was redundant to the individual checks above.
403 throw new IncompleteRevisionException( 'Revision is incomplete' );
404 }
405
406 if ( $slotRoles == [ SlotRecord::MAIN ] ) {
407 // T239717: If the main slot is the only slot, make sure the revision's nominal size
408 // and hash match the main slot's nominal size and hash.
409 $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
410 Assert::precondition(
411 $mainSlot->getSize() === $rev->getSize(),
412 'The revisions\'s size must match the main slot\'s size (see T239717)'
413 );
414 }
415
416 $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early
417
418 $parentId = $rev->getParentId() ?? $this->getPreviousRevisionId( $dbw, $rev );
419
420 Assert::precondition( $pageId > 0, 'revision must have an ID' );
421
423 $rev = $dbw->doAtomicSection(
424 __METHOD__,
425 function ( IDatabase $dbw, $fname ) use (
426 $rev,
427 $user,
428 $comment,
429 $pageId,
430 $parentId
431 ) {
432 return $this->insertRevisionInternal(
433 $rev,
434 $dbw,
435 $user,
436 $comment,
437 $rev->getPage(),
438 $pageId,
439 $parentId
440 );
441 }
442 );
443
444 Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' );
445 Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' );
446 Assert::postcondition(
447 $rev->getComment( RevisionRecord::RAW ) !== null,
448 'revision must have a comment'
449 );
450 Assert::postcondition(
451 $rev->getUser( RevisionRecord::RAW ) !== null,
452 'revision must have a user'
453 );
454
455 // Trigger exception if the main slot is missing.
456 // Technically, this could go away after MCR migration: while
457 // calling code may require a main slot to exist, RevisionStore
458 // really should not know or care about that requirement.
459 $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
460
461 foreach ( $slotRoles as $role ) {
462 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
463 Assert::postcondition(
464 $slot->getContent() !== null,
465 $role . ' slot must have content'
466 );
467 Assert::postcondition(
468 $slot->hasRevision(),
469 $role . ' slot must have a revision associated'
470 );
471 }
472
473 $this->hookRunner->onRevisionRecordInserted( $rev );
474
475 return $rev;
476 }
477
490 public function updateSlotsOn(
491 RevisionRecord $revision,
492 RevisionSlotsUpdate $revisionSlotsUpdate,
493 IDatabase $dbw
494 ): array {
495 $this->checkDatabaseDomain( $dbw );
496
497 // Make sure all modified and removed slots are derived slots
498 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
499 Assert::precondition(
500 $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
501 'Trying to modify a slot that is not derived'
502 );
503 }
504 foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) {
505 $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
506 Assert::precondition(
507 $isDerived,
508 'Trying to remove a slot that is not derived'
509 );
510 throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' );
511 }
512
514 $slotRecords = $dbw->doAtomicSection(
515 __METHOD__,
516 function ( IDatabase $dbw, $fname ) use (
517 $revision,
518 $revisionSlotsUpdate
519 ) {
520 return $this->updateSlotsInternal(
521 $revision,
522 $revisionSlotsUpdate,
523 $dbw
524 );
525 }
526 );
527
528 foreach ( $slotRecords as $role => $slot ) {
529 Assert::postcondition(
530 $slot->getContent() !== null,
531 $role . ' slot must have content'
532 );
533 Assert::postcondition(
534 $slot->hasRevision(),
535 $role . ' slot must have a revision associated'
536 );
537 }
538
539 return $slotRecords;
540 }
541
548 private function updateSlotsInternal(
549 RevisionRecord $revision,
550 RevisionSlotsUpdate $revisionSlotsUpdate,
551 IDatabase $dbw
552 ): array {
553 $page = $revision->getPage();
554 $revId = $revision->getId( $this->wikiId );
555 $blobHints = [
556 BlobStore::PAGE_HINT => $page->getId( $this->wikiId ),
557 BlobStore::REVISION_HINT => $revId,
558 BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
559 ];
560
561 $newSlots = [];
562 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
563 $slot = $revisionSlotsUpdate->getModifiedSlot( $role );
564 $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
565 }
566
567 return $newSlots;
568 }
569
570 private function insertRevisionInternal(
571 RevisionRecord $rev,
572 IDatabase $dbw,
573 UserIdentity $user,
574 CommentStoreComment $comment,
575 PageIdentity $page,
576 int $pageId,
577 int $parentId
578 ): RevisionRecord {
579 $slotRoles = $rev->getSlotRoles();
580
581 $revisionRow = $this->insertRevisionRowOn(
582 $dbw,
583 $rev,
584 $parentId
585 );
586
587 $revisionId = $revisionRow['rev_id'];
588
589 $blobHints = [
590 BlobStore::PAGE_HINT => $pageId,
591 BlobStore::REVISION_HINT => $revisionId,
592 BlobStore::PARENT_HINT => $parentId,
593 ];
594
595 $newSlots = [];
596 foreach ( $slotRoles as $role ) {
597 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
598
599 // If the SlotRecord already has a revision ID set, this means it already exists
600 // in the database, and should already belong to the current revision.
601 // However, a slot may already have a revision, but no content ID, if the slot
602 // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
603 // mode, and the respective archive row was not yet migrated to the new schema.
604 // In that case, a new slot row (and content row) must be inserted even during
605 // undeletion.
606 if ( $slot->hasRevision() && $slot->hasContentId() ) {
607 // TODO: properly abort transaction if the assertion fails!
608 Assert::parameter(
609 $slot->getRevision() === $revisionId,
610 'slot role ' . $slot->getRole(),
611 'Existing slot should belong to revision '
612 . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
613 );
614
615 // Slot exists, nothing to do, move along.
616 // This happens when restoring archived revisions.
617
618 $newSlots[$role] = $slot;
619 } else {
620 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
621 }
622 }
623
624 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
625
626 $rev = new RevisionStoreRecord(
627 $page,
628 $user,
629 $comment,
630 (object)$revisionRow,
631 new RevisionSlots( $newSlots ),
632 $this->wikiId
633 );
634
635 return $rev;
636 }
637
646 private function insertSlotOn(
647 IDatabase $dbw,
648 $revisionId,
649 SlotRecord $protoSlot,
650 PageIdentity $page,
651 array $blobHints = []
652 ) {
653 if ( $protoSlot->hasAddress() ) {
654 $blobAddress = $protoSlot->getAddress();
655 } else {
656 $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
657 }
658
659 if ( $protoSlot->hasContentId() ) {
660 $contentId = $protoSlot->getContentId();
661 } else {
662 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
663 }
664
665 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
666
667 return SlotRecord::newSaved(
668 $revisionId,
669 $contentId,
670 $blobAddress,
671 $protoSlot
672 );
673 }
674
682 private function insertIpChangesRow(
683 IDatabase $dbw,
684 UserIdentity $user,
685 RevisionRecord $rev,
686 $revisionId
687 ) {
688 if ( !$user->isRegistered() && IPUtils::isValid( $user->getName() ) ) {
689 $dbw->newInsertQueryBuilder()
690 ->insertInto( 'ip_changes' )
691 ->row( [
692 'ipc_rev_id' => $revisionId,
693 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
694 'ipc_hex' => IPUtils::toHex( $user->getName() ),
695 ] )
696 ->caller( __METHOD__ )->execute();
697
698 }
699 }
700
711 private function insertRevisionRowOn(
712 IDatabase $dbw,
713 RevisionRecord $rev,
714 $parentId
715 ) {
716 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
717
718 $revisionRow += $this->commentStore->insert(
719 $dbw,
720 'rev_comment',
721 $rev->getComment( RevisionRecord::RAW )
722 );
723
724 $dbw->newInsertQueryBuilder()
725 ->insertInto( 'revision' )
726 ->row( $revisionRow )
727 ->caller( __METHOD__ )->execute();
728
729 if ( !isset( $revisionRow['rev_id'] ) ) {
730 // only if auto-increment was used
731 $revisionRow['rev_id'] = intval( $dbw->insertId() );
732
733 if ( $dbw->getType() === 'mysql' ) {
734 // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
735 // auto-increment value to disk, so on server restart it might reuse IDs from deleted
736 // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
737
738 $maxRevId = intval( $dbw->newSelectQueryBuilder()
739 ->select( 'MAX(ar_rev_id)' )
740 ->from( 'archive' )
741 ->caller( __METHOD__ )
742 ->fetchField() );
743 $table = 'archive';
744 $maxRevId2 = intval( $dbw->newSelectQueryBuilder()
745 ->select( 'MAX(slot_revision_id)' )
746 ->from( 'slots' )
747 ->caller( __METHOD__ )
748 ->fetchField() );
749 if ( $maxRevId2 >= $maxRevId ) {
750 $maxRevId = $maxRevId2;
751 $table = 'slots';
752 }
753
754 if ( $maxRevId >= $revisionRow['rev_id'] ) {
755 $this->logger->debug(
756 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
757 . ' Trying to fix it.',
758 [
759 'revid' => $revisionRow['rev_id'],
760 'table' => $table,
761 'maxrevid' => $maxRevId,
762 ]
763 );
764
765 if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
766 throw new MWException( 'Failed to get database lock for T202032' );
767 }
768 $fname = __METHOD__;
769 $dbw->onTransactionResolution(
770 static fn () => $dbw->unlock( 'fix-for-T202032', $fname ),
771 __METHOD__
772 );
773
774 $dbw->newDeleteQueryBuilder()
775 ->deleteFrom( 'revision' )
776 ->where( [ 'rev_id' => $revisionRow['rev_id'] ] )
777 ->caller( __METHOD__ )->execute();
778
779 // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
780 // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
781 // inserts too, though, at least on MariaDB 10.1.29.
782 //
783 // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
784 // transactions in this code path thanks to the row lock from the original ->insert() above.
785 //
786 // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
787 // that's for non-MySQL DBs.
788 $row1 = $dbw->query(
789 $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE',
790 __METHOD__
791 )->fetchObject();
792
793 $row2 = $dbw->query(
794 $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
795 . ' FOR UPDATE',
796 __METHOD__
797 )->fetchObject();
798
799 $maxRevId = max(
800 $maxRevId,
801 $row1 ? intval( $row1->v ) : 0,
802 $row2 ? intval( $row2->v ) : 0
803 );
804
805 // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
806 // transactions will throw a duplicate key error here. It doesn't seem worth trying
807 // to avoid that.
808 $revisionRow['rev_id'] = $maxRevId + 1;
809 $dbw->newInsertQueryBuilder()
810 ->insertInto( 'revision' )
811 ->row( $revisionRow )
812 ->caller( __METHOD__ )->execute();
813 }
814 }
815 }
816
817 return $revisionRow;
818 }
819
827 private function getBaseRevisionRow(
828 IDatabase $dbw,
829 RevisionRecord $rev,
830 $parentId
831 ) {
832 // Record the edit in revisions
833 $revisionRow = [
834 'rev_page' => $rev->getPageId( $this->wikiId ),
835 'rev_parent_id' => $parentId,
836 'rev_actor' => $this->actorStore->acquireActorId(
837 $rev->getUser( RevisionRecord::RAW ),
838 $dbw
839 ),
840 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
841 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
842 'rev_deleted' => $rev->getVisibility(),
843 'rev_len' => $rev->getSize(),
844 ];
845
846 if ( $rev->getId( $this->wikiId ) !== null ) {
847 // Needed to restore revisions with their original ID
848 $revisionRow['rev_id'] = $rev->getId( $this->wikiId );
849 }
850
851 return $revisionRow;
852 }
853
862 private function storeContentBlob(
863 SlotRecord $slot,
864 PageIdentity $page,
865 array $blobHints = []
866 ) {
867 $content = $slot->getContent();
868 $format = $content->getDefaultFormat();
869 $model = $content->getModel();
870
871 $this->checkContent( $content, $page, $slot->getRole() );
872
873 return $this->blobStore->storeBlob(
874 $content->serialize( $format ),
875 // These hints "leak" some information from the higher abstraction layer to
876 // low level storage to allow for optimization.
877 array_merge(
878 $blobHints,
879 [
880 BlobStore::DESIGNATION_HINT => 'page-content',
881 BlobStore::ROLE_HINT => $slot->getRole(),
882 BlobStore::SHA1_HINT => $slot->getSha1(),
883 BlobStore::MODEL_HINT => $model,
884 BlobStore::FORMAT_HINT => $format,
885 ]
886 )
887 );
888 }
889
896 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
897 $dbw->newInsertQueryBuilder()
898 ->insertInto( 'slots' )
899 ->row( [
900 'slot_revision_id' => $revisionId,
901 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
902 'slot_content_id' => $contentId,
903 // If the slot has a specific origin use that ID, otherwise use the ID of the revision
904 // that we just inserted.
905 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
906 ] )
907 ->caller( __METHOD__ )->execute();
908 }
909
916 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
917 $dbw->newInsertQueryBuilder()
918 ->insertInto( 'content' )
919 ->row( [
920 'content_size' => $slot->getSize(),
921 'content_sha1' => $slot->getSha1(),
922 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
923 'content_address' => $blobAddress,
924 ] )
925 ->caller( __METHOD__ )->execute();
926 return intval( $dbw->insertId() );
927 }
928
939 private function checkContent( Content $content, PageIdentity $page, string $role ) {
940 // Note: may return null for revisions that have not yet been inserted
941
942 $model = $content->getModel();
943 $format = $content->getDefaultFormat();
944 $handler = $content->getContentHandler();
945
946 if ( !$handler->isSupportedFormat( $format ) ) {
947 throw new MWException(
948 "Can't use format $format with content model $model on $page role $role"
949 );
950 }
951
952 if ( !$content->isValid() ) {
953 throw new MWException(
954 "New content for $page role $role is not valid! Content model is $model"
955 );
956 }
957 }
958
983 public function newNullRevision(
984 IDatabase $dbw,
985 PageIdentity $page,
986 CommentStoreComment $comment,
987 $minor,
988 UserIdentity $user
989 ) {
990 $this->checkDatabaseDomain( $dbw );
991
992 $pageId = $this->getArticleId( $page );
993
994 // T51581: Lock the page table row to ensure no other process
995 // is adding a revision to the page at the same time.
996 // Avoid locking extra tables, compare T191892.
997 $pageLatest = $dbw->newSelectQueryBuilder()
998 ->select( 'page_latest' )
999 ->forUpdate()
1000 ->from( 'page' )
1001 ->where( [ 'page_id' => $pageId ] )
1002 ->caller( __METHOD__ )->fetchField();
1003
1004 if ( !$pageLatest ) {
1005 $msg = 'T235589: Failed to select table row during dummy revision creation' .
1006 " Page id '$pageId' does not exist.";
1007 $this->logger->error(
1008 $msg,
1009 [ 'exception' => new RuntimeException( $msg ) ]
1010 );
1011
1012 return null;
1013 }
1014
1015 // Fetch the actual revision row from primary DB, without locking all extra tables.
1016 $oldRevision = $this->loadRevisionFromConds(
1017 $dbw,
1018 [ 'rev_id' => intval( $pageLatest ) ],
1019 IDBAccessObject::READ_LATEST,
1020 $page
1021 );
1022
1023 if ( !$oldRevision ) {
1024 $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
1025 $this->logger->error(
1026 $msg,
1027 [ 'exception' => new RuntimeException( $msg ) ]
1028 );
1029 return null;
1030 }
1031
1032 // Construct the new revision
1033 $timestamp = MWTimestamp::now( TS::MW );
1034 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1035
1036 $newRevision->setComment( $comment );
1037 $newRevision->setUser( $user );
1038 $newRevision->setTimestamp( $timestamp );
1039 $newRevision->setMinorEdit( $minor );
1040
1041 return $newRevision;
1042 }
1043
1053 public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
1054 $rc = $this->getRecentChange( $rev );
1055 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
1056 return $rc->getAttribute( 'rc_id' );
1057 } else {
1058 return 0;
1059 }
1060 }
1061
1075 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
1076 if ( $this->wikiId !== RevisionRecord::LOCAL ) {
1077 throw new PreconditionException( 'RecentChangeLookup is only available for the local wiki' );
1078 }
1079
1080 $rc = $this->recentChangeLookup->getRecentChangeByConds(
1081 [
1082 'rc_this_oldid' => $rev->getId( $this->wikiId ),
1083 // rc_this_oldid does not have to be unique,
1084 // in particular, it is shared with categorization
1085 // changes. Prefer the original change because callers
1086 // often expect a change for patrolling.
1087 'rc_source' => [ RecentChange::SRC_EDIT, RecentChange::SRC_NEW, RecentChange::SRC_LOG ],
1088 ],
1089 __METHOD__,
1090 ( $flags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST
1091 );
1092
1093 // XXX: cache this locally? Glue it to the RevisionRecord?
1094 return $rc;
1095 }
1096
1116 private function loadSlotContent(
1117 SlotRecord $slot,
1118 ?string $blobData = null,
1119 ?string $blobFlags = null,
1120 ?string $blobFormat = null,
1121 int $queryFlags = 0
1122 ) {
1123 if ( $blobData !== null ) {
1124 $blobAddress = $slot->hasAddress() ? $slot->getAddress() : null;
1125
1126 if ( $blobFlags === null ) {
1127 // No blob flags, so use the blob verbatim.
1128 $data = $blobData;
1129 } else {
1130 try {
1131 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $blobAddress );
1132 } catch ( BadBlobException $e ) {
1133 throw new BadRevisionException( $e->getMessage(), [], 0, $e );
1134 }
1135
1136 if ( $data === false ) {
1137 throw new RevisionAccessException(
1138 'Failed to expand blob data using flags {flags} (key: {cache_key})',
1139 [
1140 'flags' => $blobFlags,
1141 'cache_key' => $blobAddress,
1142 ]
1143 );
1144 }
1145 }
1146
1147 } else {
1148 $address = $slot->getAddress();
1149 try {
1150 $data = $this->blobStore->getBlob( $address, $queryFlags );
1151 } catch ( BadBlobException $e ) {
1152 throw new BadRevisionException( $e->getMessage(), [], 0, $e );
1153 } catch ( BlobAccessException $e ) {
1154 throw new RevisionAccessException(
1155 'Failed to load data blob from {address} for revision {revision}. '
1156 . 'If this problem persists, use the findBadBlobs maintenance script '
1157 . 'to investigate the issue and mark the bad blobs.',
1158 [ 'address' => $e->getMessage(), 'revision' => $slot->getRevision() ],
1159 0,
1160 $e
1161 );
1162 }
1163 }
1164
1165 $model = $slot->getModel();
1166
1167 // If the content model is not known, don't fail here (T220594, T220793, T228921)
1168 if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1169 $this->logger->warning(
1170 "Undefined content model '$model', falling back to FallbackContent",
1171 [
1172 'content_address' => $slot->getAddress(),
1173 'rev_id' => $slot->getRevision(),
1174 'role_name' => $slot->getRole(),
1175 'model_name' => $model,
1176 'exception' => new RuntimeException()
1177 ]
1178 );
1179
1180 return new FallbackContent( $data, $model );
1181 }
1182
1183 return $this->contentHandlerFactory
1184 ->getContentHandler( $model )
1185 ->unserializeContent( $data, $blobFormat );
1186 }
1187
1205 public function getRevisionById( $id, $flags = 0, ?PageIdentity $page = null ) {
1206 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags, $page );
1207 }
1208
1225 public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) {
1226 $conds = $this->getPageConditions( $page );
1227 if ( !$conds ) {
1228 return null;
1229 }
1230 if ( !( $page instanceof PageIdentity ) ) {
1231 if ( !( $page instanceof PageReference ) ) {
1232 wfDeprecated( __METHOD__ . ' with a LinkTarget', '1.45' );
1233 }
1234 $page = null;
1235 }
1236
1237 if ( $revId ) {
1238 // Use the specified revision ID.
1239 // Note that we use newRevisionFromConds here because we want to retry
1240 // and fall back to primary DB if the page is not found on a replica.
1241 // Since the caller supplied a revision ID, we are pretty sure the revision is
1242 // supposed to exist, so we should try hard to find it.
1243 $conds['rev_id'] = $revId;
1244 return $this->newRevisionFromConds( $conds, $flags, $page );
1245 } else {
1246 // Use a join to get the latest revision.
1247 // Note that we don't use newRevisionFromConds here because we don't want to retry
1248 // and fall back to primary DB. The assumption is that we only want to force the fallback
1249 // if we are quite sure the revision exists because the caller supplied a revision ID.
1250 // If the page isn't found at all on a replica, it probably simply does not exist.
1251 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1252 $conds[] = 'rev_id=page_latest';
1253 return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1254 }
1255 }
1256
1273 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1274 $conds = [ 'page_id' => $pageId ];
1275 if ( $revId ) {
1276 // Use the specified revision ID.
1277 // Note that we use newRevisionFromConds here because we want to retry
1278 // and fall back to primary DB if the page is not found on a replica.
1279 // Since the caller supplied a revision ID, we are pretty sure the revision is
1280 // supposed to exist, so we should try hard to find it.
1281 $conds['rev_id'] = $revId;
1282 return $this->newRevisionFromConds( $conds, $flags );
1283 } else {
1284 // Use a join to get the latest revision.
1285 // Note that we don't use newRevisionFromConds here because we don't want to retry
1286 // and fall back to primary DB. The assumption is that we only want to force the fallback
1287 // if we are quite sure the revision exists because the caller supplied a revision ID.
1288 // If the page isn't found at all on a replica, it probably simply does not exist.
1289 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1290
1291 $conds[] = 'rev_id=page_latest';
1292
1293 return $this->loadRevisionFromConds( $db, $conds, $flags );
1294 }
1295 }
1296
1312 public function getRevisionByTimestamp(
1313 $page,
1314 string $timestamp,
1315 int $flags = IDBAccessObject::READ_NORMAL
1316 ): ?RevisionRecord {
1317 $conds = $this->getPageConditions( $page );
1318 if ( !$conds ) {
1319 return null;
1320 }
1321 if ( !( $page instanceof PageIdentity ) ) {
1322 if ( !( $page instanceof PageReference ) ) {
1323 wfDeprecated( __METHOD__ . ' with a LinkTarget', '1.45' );
1324 }
1325 $page = null;
1326 }
1327
1328 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1329 $conds['rev_timestamp'] = $db->timestamp( $timestamp );
1330 return $this->newRevisionFromConds( $conds, $flags, $page );
1331 }
1332
1340 private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) {
1341 // TODO: Find a way to add NS_MODULE from Scribunto here
1342 if ( $page->getNamespace() !== NS_TEMPLATE ) {
1343 $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page );
1344 return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1345 }
1346
1347 // TODO: These caches should not be needed. See T297147#7563670
1348 $res = $this->localCache->getWithSetCallback(
1349 $this->localCache->makeKey(
1350 'revision-slots',
1351 $page->getWikiId(),
1352 $page->getId( $page->getWikiId() ),
1353 $revId
1354 ),
1355 $this->localCache::TTL_HOUR,
1356 function () use ( $revId, $queryFlags, $page ) {
1357 return $this->cache->getWithSetCallback(
1358 $this->cache->makeKey(
1359 'revision-slots',
1360 $page->getWikiId(),
1361 $page->getId( $page->getWikiId() ),
1362 $revId
1363 ),
1364 WANObjectCache::TTL_DAY,
1365 function () use ( $revId, $queryFlags, $page ) {
1366 $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page );
1367 if ( !$res ) {
1368 // Avoid caching
1369 return false;
1370 }
1371 return $res;
1372 }
1373 );
1374 }
1375 );
1376 if ( !$res ) {
1377 $res = [];
1378 }
1379
1380 return $this->constructSlotRecords( $revId, $res, $queryFlags, $page );
1381 }
1382
1383 private function loadSlotRecordsFromDb( int $revId, int $queryFlags, PageIdentity $page ): array {
1384 $revQuery = $this->getSlotsQueryInfo( [ 'content' ] );
1385
1386 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1387 $res = $db->newSelectQueryBuilder()
1388 ->queryInfo( $revQuery )
1389 ->where( [ 'slot_revision_id' => $revId ] )
1390 ->recency( $queryFlags )
1391 ->caller( __METHOD__ )->fetchResultSet();
1392
1393 if ( !$res->numRows() && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
1394 // If we found no slots, try looking on the primary database (T212428, T252156)
1395 $this->logger->info(
1396 __METHOD__ . ' falling back to READ_LATEST.',
1397 [
1398 'revid' => $revId,
1399 'exception' => new RuntimeException(),
1400 ]
1401 );
1402 return $this->loadSlotRecordsFromDb(
1403 $revId,
1404 $queryFlags | IDBAccessObject::READ_LATEST,
1405 $page
1406 );
1407 }
1408 return iterator_to_array( $res );
1409 }
1410
1423 private function constructSlotRecords(
1424 $revId,
1425 $slotRows,
1426 $queryFlags,
1427 PageIdentity $page,
1428 $slotContents = null
1429 ) {
1430 $slots = [];
1431
1432 foreach ( $slotRows as $row ) {
1433 // Resolve role names and model names from in-memory cache, if they were not joined in.
1434 if ( !isset( $row->role_name ) ) {
1435 $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
1436 }
1437
1438 if ( !isset( $row->model_name ) ) {
1439 if ( isset( $row->content_model ) ) {
1440 $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
1441 } else {
1442 // We may get here if $row->model_name is set but null, perhaps because it
1443 // came from rev_content_model, which is NULL for the default model.
1444 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1445 $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1446 }
1447 }
1448
1449 // We may have a fake blob_data field from getSlotRowsForBatch(), use it!
1450 if ( isset( $row->blob_data ) ) {
1451 $slotContents[$row->content_address] = $row->blob_data;
1452 }
1453
1454 $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1455 $blob = null;
1456 if ( isset( $slotContents[$slot->getAddress()] ) ) {
1457 $blob = $slotContents[$slot->getAddress()];
1458 if ( $blob instanceof Content ) {
1459 return $blob;
1460 }
1461 }
1462 return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
1463 };
1464
1465 $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
1466 }
1467
1468 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1469 $this->logger->error(
1470 __METHOD__ . ': Main slot of revision not found in database. See T212428.',
1471 [
1472 'revid' => $revId,
1473 'queryFlags' => $queryFlags,
1474 'exception' => new RuntimeException(),
1475 ]
1476 );
1477
1478 throw new RevisionAccessException(
1479 'Main slot of revision not found in database. See T212428.'
1480 );
1481 }
1482
1483 return $slots;
1484 }
1485
1499 private function newRevisionSlots(
1500 $revId,
1501 $slotRows,
1502 $queryFlags,
1503 PageIdentity $page
1504 ) {
1505 if ( $slotRows ) {
1506 $slots = new RevisionSlots(
1507 $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1508 );
1509 } else {
1510 $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) {
1511 return $this->loadSlotRecords( $revId, $queryFlags, $page );
1512 } );
1513 }
1514
1515 return $slots;
1516 }
1517
1539 $row,
1540 $queryFlags = 0,
1541 ?PageIdentity $page = null,
1542 array $overrides = []
1543 ) {
1544 return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides );
1545 }
1546
1559 public function newRevisionFromRow(
1560 $row,
1561 $queryFlags = 0,
1562 ?PageIdentity $page = null,
1563 $fromCache = false
1564 ) {
1565 return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache );
1566 }
1567
1587 stdClass $row,
1588 $slots,
1589 int $queryFlags = 0,
1590 ?PageIdentity $page = null,
1591 array $overrides = []
1592 ) {
1593 if ( !$page && isset( $overrides['title'] ) ) {
1594 if ( !( $overrides['title'] instanceof PageIdentity ) ) {
1595 throw new InvalidArgumentException( 'title field override must contain a PageIdentity object.' );
1596 }
1597
1598 $page = $overrides['title'];
1599 }
1600
1601 if ( $page === null ) {
1602 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1603 // Represent a non-existing page.
1604 // NOTE: The page title may be invalid by current rules (T384628).
1605 $page = PageIdentityValue::localIdentity( 0, $row->ar_namespace, $row->ar_title );
1606 } else {
1607 throw new InvalidArgumentException(
1608 'A Title or ar_namespace and ar_title must be given'
1609 );
1610 }
1611 }
1612
1613 foreach ( $overrides as $key => $value ) {
1614 $field = "ar_$key";
1615 $row->$field = $value;
1616 }
1617
1618 try {
1619 $user = $this->actorStore->newActorFromRowFields(
1620 $row->ar_user ?? null,
1621 $row->ar_user_text ?? null,
1622 $row->ar_actor ?? null
1623 );
1624 } catch ( InvalidArgumentException $ex ) {
1625 $this->logger->warning( 'Could not load user for archive revision {rev_id}', [
1626 'ar_rev_id' => $row->ar_rev_id,
1627 'ar_actor' => $row->ar_actor ?? 'null',
1628 'ar_user_text' => $row->ar_user_text ?? 'null',
1629 'ar_user' => $row->ar_user ?? 'null',
1630 'exception' => $ex
1631 ] );
1632 $user = $this->actorStore->getUnknownActor();
1633 }
1634
1635 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1636 // Legacy because $row may have come from self::selectFields()
1637 $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
1638
1639 if ( !( $slots instanceof RevisionSlots ) ) {
1640 $slots = $this->newRevisionSlots( (int)$row->ar_rev_id, $slots, $queryFlags, $page );
1641 }
1642 return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId );
1643 }
1644
1663 stdClass $row,
1664 $slots,
1665 int $queryFlags = 0,
1666 ?PageIdentity $page = null,
1667 bool $fromCache = false
1668 ) {
1669 if ( !$page ) {
1670 if ( isset( $row->page_id )
1671 && isset( $row->page_namespace )
1672 && isset( $row->page_title )
1673 ) {
1674 $page = new PageIdentityValue(
1675 (int)$row->page_id,
1676 (int)$row->page_namespace,
1677 $row->page_title,
1678 $this->wikiId
1679 );
1680
1681 $page = $this->wrapPage( $page );
1682 } else {
1683 $pageId = (int)( $row->rev_page ?? 0 );
1684 $revId = (int)( $row->rev_id ?? 0 );
1685
1686 $page = $this->getPage( $pageId, $revId, $queryFlags );
1687 }
1688 } else {
1689 $page = $this->ensureRevisionRowMatchesPage( $row, $page );
1690 }
1691
1692 if ( !$page ) {
1693 // This should already have been caught about, but apparently
1694 // it not always is, see T286877.
1695 throw new RevisionAccessException(
1696 "Failed to determine page associated with revision {$row->rev_id}"
1697 );
1698 }
1699
1700 try {
1701 $user = $this->actorStore->newActorFromRowFields(
1702 $row->rev_user ?? null,
1703 $row->rev_user_text ?? null,
1704 $row->rev_actor ?? null
1705 );
1706 } catch ( InvalidArgumentException $ex ) {
1707 $this->logger->warning( 'Could not load user for revision {rev_id}', [
1708 'rev_id' => $row->rev_id,
1709 'rev_actor' => $row->rev_actor ?? 'null',
1710 'rev_user_text' => $row->rev_user_text ?? 'null',
1711 'rev_user' => $row->rev_user ?? 'null',
1712 'exception' => $ex
1713 ] );
1714 $user = $this->actorStore->getUnknownActor();
1715 }
1716
1717 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1718 // Legacy because $row may have come from self::selectFields()
1719 $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
1720
1721 if ( !( $slots instanceof RevisionSlots ) ) {
1722 $slots = $this->newRevisionSlots( (int)$row->rev_id, $slots, $queryFlags, $page );
1723 }
1724
1725 // If this is a cached row, instantiate a cache-aware RevisionRecord to avoid stale data.
1726 if ( $fromCache ) {
1727 $rev = new RevisionStoreCacheRecord(
1728 function ( $revId ) use ( $queryFlags ) {
1729 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1730 $row = $this->fetchRevisionRowFromConds(
1731 $db,
1732 [ 'rev_id' => intval( $revId ) ]
1733 );
1734 if ( !$row && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
1735 // If we found no slots, try looking on the primary database (T259738)
1736 $this->logger->info(
1737 'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1738 [
1739 'revid' => $revId,
1740 'exception' => new RuntimeException(),
1741 ]
1742 );
1743 $dbw = $this->getDBConnectionRefForQueryFlags( IDBAccessObject::READ_LATEST );
1744 $row = $this->fetchRevisionRowFromConds(
1745 $dbw,
1746 [ 'rev_id' => intval( $revId ) ]
1747 );
1748 }
1749 if ( !$row ) {
1750 return [ null, null ];
1751 }
1752 return [
1753 $row->rev_deleted,
1754 $this->actorStore->newActorFromRowFields(
1755 $row->rev_user ?? null,
1756 $row->rev_user_text ?? null,
1757 $row->rev_actor ?? null
1758 )
1759 ];
1760 },
1761 $page, $user, $comment, $row, $slots, $this->wikiId
1762 );
1763 } else {
1764 $rev = new RevisionStoreRecord(
1765 $page, $user, $comment, $row, $slots, $this->wikiId );
1766 }
1767 return $rev;
1768 }
1769
1781 private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) {
1782 $revId = (int)( $row->rev_id ?? 0 );
1783 $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
1784 $expectedPageId = $page->getId( $this->wikiId );
1785 // Avoid fatal error when the Title's ID changed, T246720
1786 if ( $revPageId && $expectedPageId && $revPageId !== $expectedPageId ) {
1787 // NOTE: PageStore::getPageByReference may use the page ID, which we don't want here.
1788 $pageRec = $this->pageStore->getPageByName(
1789 $page->getNamespace(),
1790 $page->getDBkey(),
1791 IDBAccessObject::READ_LATEST
1792 );
1793 $masterPageId = $pageRec->getId( $this->wikiId );
1794 $masterLatest = $pageRec->getLatest( $this->wikiId );
1795 if ( $revPageId === $masterPageId ) {
1796 if ( $page instanceof Title ) {
1797 // If we were using a Title object, keep using it, but update the page ID.
1798 // This way, we don't unexpectedly mix Titles with immutable value objects.
1799 $page->resetArticleID( $masterPageId );
1800
1801 } else {
1802 $page = $pageRec;
1803 }
1804
1805 $this->logger->info(
1806 "Encountered stale Title object",
1807 [
1808 'page_id_stale' => $expectedPageId,
1809 'page_id_reloaded' => $masterPageId,
1810 'page_latest' => $masterLatest,
1811 'rev_id' => $revId,
1812 'exception' => new RuntimeException(),
1813 ] + $context
1814 );
1815 } else {
1816 $expectedTitle = (string)$page;
1817 if ( $page instanceof Title ) {
1818 // If we started with a Title, keep using a Title.
1819 $page = $this->titleFactory->newFromID( $revPageId );
1820 } else {
1821 $page = $pageRec;
1822 }
1823
1824 // This could happen if a caller to e.g. getRevisionById supplied a Title that is
1825 // plain wrong. In this case, we should ideally throw an IllegalArgumentException.
1826 // However, it is more likely that we encountered a race condition during a page
1827 // move (T268910, T279832) or database corruption (T263340). That situation
1828 // should not be ignored, but we can allow the request to continue in a reasonable
1829 // manner without breaking things for the user.
1830 $this->logger->error(
1831 "Encountered mismatching Title object (see T259022, T268910, T279832, T263340)",
1832 [
1833 'expected_page_id' => $masterPageId,
1834 'expected_page_title' => $expectedTitle,
1835 'rev_page' => $revPageId,
1836 'rev_page_title' => (string)$page,
1837 'page_latest' => $masterLatest,
1838 'rev_id' => $revId,
1839 'exception' => new RuntimeException(),
1840 ] + $context
1841 );
1842 }
1843 }
1844
1845 // @phan-suppress-next-line PhanTypeMismatchReturnNullable getPageByName/newFromID should not return null
1846 return $page;
1847 }
1848
1874 public function newRevisionsFromBatch(
1875 $rows,
1876 array $options = [],
1877 $queryFlags = 0,
1878 ?PageIdentity $page = null
1879 ) {
1880 $result = new StatusValue();
1881 $archiveMode = $options['archive'] ?? false;
1882
1883 if ( $archiveMode ) {
1884 $revIdField = 'ar_rev_id';
1885 } else {
1886 $revIdField = 'rev_id';
1887 }
1888
1889 $rowsByRevId = [];
1890 $pageIdsToFetchTitles = [];
1891 $titlesByPageKey = [];
1892 foreach ( $rows as $row ) {
1893 if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1894 $result->warning(
1895 'internalerror_info',
1896 "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1897 );
1898 }
1899
1900 // Attach a page key to the row, so we can find and reuse Title objects easily.
1901 $row->_page_key =
1902 $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
1903
1904 if ( $page ) {
1905 if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1906 throw new InvalidArgumentException(
1907 "Revision {$row->$revIdField} doesn't belong to page "
1908 . $this->getArticleId( $page )
1909 );
1910 }
1911
1912 if ( $archiveMode
1913 && ( $row->ar_namespace != $page->getNamespace()
1914 || $row->ar_title !== $page->getDBkey() )
1915 ) {
1916 throw new InvalidArgumentException(
1917 "Revision {$row->$revIdField} doesn't belong to page "
1918 . $page
1919 );
1920 }
1921 } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1922 if ( isset( $row->page_namespace ) && isset( $row->page_title )
1923 // This should always be true, but just in case we don't have a page_id
1924 // set or it doesn't match rev_page, let's fetch the title again.
1925 && isset( $row->page_id ) && isset( $row->rev_page )
1926 && $row->rev_page === $row->page_id
1927 ) {
1928 $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1929 } elseif ( $archiveMode ) {
1930 // Can't look up deleted pages by ID, but we have namespace and title
1931 $titlesByPageKey[ $row->_page_key ] =
1932 Title::makeTitle( $row->ar_namespace, $row->ar_title );
1933 } else {
1934 $pageIdsToFetchTitles[] = $row->rev_page;
1935 }
1936 }
1937 $rowsByRevId[$row->$revIdField] = $row;
1938 }
1939
1940 if ( !$rowsByRevId ) {
1941 $result->setResult( true, [] );
1942 return $result;
1943 }
1944
1945 // If the page is not supplied, batch-fetch Title objects.
1946 if ( $page ) {
1947 // same logic as for $row->_page_key above
1948 $pageKey = $archiveMode
1949 ? $page->getNamespace() . ':' . $page->getDBkey()
1950 : $this->getArticleId( $page );
1951
1952 $titlesByPageKey[$pageKey] = $page;
1953 } elseif ( $pageIdsToFetchTitles ) {
1954 // Note: when we fetch titles by ID, the page key is also the ID.
1955 // We should never get here if $archiveMode is true.
1956 Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
1957
1958 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1959 $pageRecords = $this->pageStore
1960 ->newSelectQueryBuilder()
1961 ->wherePageIds( $pageIdsToFetchTitles )
1962 ->caller( __METHOD__ )
1963 ->fetchPageRecordArray();
1964 // Cannot array_merge because it re-indexes entries
1965 $titlesByPageKey = $pageRecords + $titlesByPageKey;
1966 }
1967
1968 // which method to use for creating RevisionRecords
1969 $newRevisionRecord = $archiveMode
1970 ? $this->newRevisionFromArchiveRowAndSlots( ... )
1971 : $this->newRevisionFromRowAndSlots( ... );
1972
1973 if ( !isset( $options['slots'] ) ) {
1974 $result->setResult(
1975 true,
1976 array_map(
1977 static function ( $row )
1978 use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1979 try {
1980 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1981 $result->warning(
1982 'internalerror_info',
1983 "Couldn't find title for rev {$row->$revIdField} "
1984 . "(page key {$row->_page_key})"
1985 );
1986 return null;
1987 }
1988 return $newRevisionRecord( $row, null, $queryFlags,
1989 $titlesByPageKey[ $row->_page_key ] );
1990 } catch ( MWException $e ) {
1991 $result->warning( 'internalerror_info', $e->getMessage() );
1992 return null;
1993 }
1994 },
1995 $rowsByRevId
1996 )
1997 );
1998 return $result;
1999 }
2000
2001 $slotRowOptions = [
2002 'slots' => $options['slots'] ?? true,
2003 'blobs' => $options['content'] ?? false,
2004 ];
2005
2006 if ( is_array( $slotRowOptions['slots'] )
2007 && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
2008 ) {
2009 // Make sure the main slot is always loaded, RevisionRecord requires this.
2010 $slotRowOptions['slots'][] = SlotRecord::MAIN;
2011 }
2012
2013 $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2014
2015 $result->merge( $slotRowsStatus );
2016 $slotRowsByRevId = $slotRowsStatus->getValue();
2017
2018 $result->setResult(
2019 true,
2020 array_map(
2021 function ( $row )
2022 use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
2023 $revIdField, $newRevisionRecord
2024 ) {
2025 if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
2026 $result->warning(
2027 'internalerror_info',
2028 "Couldn't find slots for rev {$row->$revIdField}"
2029 );
2030 return null;
2031 }
2032 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2033 $result->warning(
2034 'internalerror_info',
2035 "Couldn't find title for rev {$row->$revIdField} "
2036 . "(page key {$row->_page_key})"
2037 );
2038 return null;
2039 }
2040 try {
2041 return $newRevisionRecord(
2042 $row,
2043 new RevisionSlots(
2044 $this->constructSlotRecords(
2045 $row->$revIdField,
2046 $slotRowsByRevId[$row->$revIdField],
2047 $queryFlags,
2048 $titlesByPageKey[$row->_page_key]
2049 )
2050 ),
2051 $queryFlags,
2052 $titlesByPageKey[$row->_page_key]
2053 );
2054 } catch ( MWException | ParameterAssertionException $e ) {
2055 $result->warning( 'internalerror_info', $e->getMessage() );
2056 return null;
2057 }
2058 },
2059 $rowsByRevId
2060 )
2061 );
2062 return $result;
2063 }
2064
2089 private function getSlotRowsForBatch(
2090 $rowsOrIds,
2091 array $options = [],
2092 $queryFlags = 0
2093 ) {
2094 $result = new StatusValue();
2095
2096 $revIds = [];
2097 foreach ( $rowsOrIds as $id ) {
2098 if ( $id instanceof stdClass ) {
2099 $id = $id->ar_rev_id ?? $id->rev_id;
2100 }
2101 $revIds[] = (int)$id;
2102 }
2103
2104 // Nothing to do.
2105 // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
2106 if ( !$revIds ) {
2107 $result->setResult( true, [] );
2108 return $result;
2109 }
2110
2111 // We need to set the `content` flag to join in content meta-data
2112 $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] );
2113 $revIdField = $slotQueryInfo['keys']['rev_id'];
2114 $slotQueryConds = [ $revIdField => $revIds ];
2115
2116 if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
2117 $slotIds = [];
2118 foreach ( $options['slots'] as $slot ) {
2119 try {
2120 $slotIds[] = $this->slotRoleStore->getId( $slot );
2121 } catch ( NameTableAccessException ) {
2122 // Do not fail when slot has no id (unused slot)
2123 // This also means for this slot are never data in the database
2124 }
2125 }
2126 if ( $slotIds === [] ) {
2127 // Degenerate case: return no slots for each revision.
2128 $result->setResult( true, array_fill_keys( $revIds, [] ) );
2129 return $result;
2130 }
2131
2132 $roleIdField = $slotQueryInfo['keys']['role_id'];
2133 $slotQueryConds[$roleIdField] = $slotIds;
2134 }
2135
2136 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2137 $slotRows = $db->newSelectQueryBuilder()
2138 ->queryInfo( $slotQueryInfo )
2139 ->where( $slotQueryConds )
2140 ->caller( __METHOD__ )
2141 ->fetchResultSet();
2142
2143 $slotContents = null;
2144 if ( $options['blobs'] ?? false ) {
2145 $blobAddresses = [];
2146 foreach ( $slotRows as $slotRow ) {
2147 $blobAddresses[] = $slotRow->content_address;
2148 }
2149 $slotContentFetchStatus = $this->blobStore
2150 ->getBlobBatch( $blobAddresses, $queryFlags );
2151 foreach ( $slotContentFetchStatus->getMessages() as $msg ) {
2152 $result->warning( $msg );
2153 }
2154 $slotContents = $slotContentFetchStatus->getValue();
2155 }
2156
2157 $slotRowsByRevId = [];
2158 foreach ( $slotRows as $slotRow ) {
2159 if ( $slotContents === null ) {
2160 // nothing to do
2161 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2162 $slotRow->blob_data = $slotContents[$slotRow->content_address];
2163 } else {
2164 $result->warning(
2165 'internalerror_info',
2166 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2167 );
2168 $slotRow->blob_data = null;
2169 }
2170
2171 // conditional needed for SCHEMA_COMPAT_READ_OLD
2172 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2173 $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
2174 }
2175
2176 // conditional needed for SCHEMA_COMPAT_READ_OLD
2177 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2178 $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
2179 }
2180
2181 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2182 }
2183
2184 $result->setResult( true, $slotRowsByRevId );
2185 return $result;
2186 }
2187
2210 $rowsOrIds,
2211 $slots = null,
2212 $queryFlags = 0
2213 ) {
2214 $result = $this->getSlotRowsForBatch(
2215 $rowsOrIds,
2216 [ 'slots' => $slots, 'blobs' => true ],
2217 $queryFlags
2218 );
2219
2220 if ( $result->isOK() ) {
2221 // strip out all internal meta data that we don't want to expose
2222 foreach ( $result->value as $revId => $rowsByRole ) {
2223 foreach ( $rowsByRole as $role => $slotRow ) {
2224 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2225 // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
2226 // if we didn't ask for it.
2227 unset( $result->value[$revId][$role] );
2228 continue;
2229 }
2230
2231 $result->value[$revId][$role] = (object)[
2232 'blob_data' => $slotRow->blob_data,
2233 'model_name' => $slotRow->model_name,
2234 ];
2235 }
2236 }
2237 }
2238
2239 return $result;
2240 }
2241
2258 private function newRevisionFromConds(
2259 array $conditions,
2260 int $flags = IDBAccessObject::READ_NORMAL,
2261 ?PageIdentity $page = null,
2262 array $options = []
2263 ) {
2264 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2265 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2266
2267 // Make sure new pending/committed revision are visible later on
2268 // within web requests to certain avoid bugs like T93866 and T94407.
2269 if ( !$rev
2270 && !( $flags & IDBAccessObject::READ_LATEST )
2271 && $this->loadBalancer->hasStreamingReplicaServers()
2272 && $this->loadBalancer->hasOrMadeRecentPrimaryChanges()
2273 ) {
2274 $flags = IDBAccessObject::READ_LATEST;
2275 $dbw = $this->getPrimaryConnection();
2276 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2277 }
2278
2279 return $rev;
2280 }
2281
2296 private function loadRevisionFromConds(
2297 IReadableDatabase $db,
2298 array $conditions,
2299 int $flags = IDBAccessObject::READ_NORMAL,
2300 ?PageIdentity $page = null,
2301 array $options = []
2302 ) {
2303 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2304 if ( $row ) {
2305 return $this->newRevisionFromRow( $row, $flags, $page );
2306 }
2307
2308 return null;
2309 }
2310
2315 private function checkDatabaseDomain( IReadableDatabase $db ) {
2316 $dbDomain = $db->getDomainID();
2317 $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2318 if ( $dbDomain === $storeDomain ) {
2319 return;
2320 }
2321
2322 throw new RuntimeException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
2323 }
2324
2338 private function fetchRevisionRowFromConds(
2339 IReadableDatabase $db,
2340 array $conditions,
2341 int $flags = IDBAccessObject::READ_NORMAL,
2342 array $options = []
2343 ) {
2344 $this->checkDatabaseDomain( $db );
2345
2346 $queryBuilder = $this->newSelectQueryBuilder( $db )
2347 ->joinComment()
2348 ->joinPage()
2349 ->joinUser()
2350 ->where( $conditions )
2351 ->options( $options );
2352 if ( ( $flags & IDBAccessObject::READ_LOCKING ) == IDBAccessObject::READ_LOCKING ) {
2353 $queryBuilder->forUpdate();
2354 }
2355 return $queryBuilder->caller( __METHOD__ )->fetchRow();
2356 }
2357
2380 public function getQueryInfo( $options = [] ) {
2381 $ret = [
2382 'tables' => [
2383 'revision',
2384 'actor_rev_user' => 'actor',
2385 ],
2386 'fields' => [
2387 'rev_id',
2388 'rev_page',
2389 'rev_actor' => 'rev_actor',
2390 'rev_user' => 'actor_rev_user.actor_user',
2391 'rev_user_text' => 'actor_rev_user.actor_name',
2392 'rev_timestamp',
2393 'rev_minor_edit',
2394 'rev_deleted',
2395 'rev_len',
2396 'rev_parent_id',
2397 ],
2398 'joins' => [
2399 'actor_rev_user' => [ 'JOIN', "actor_rev_user.actor_id = rev_actor" ],
2400 ]
2401 ];
2402
2403 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
2404 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
2405 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
2406 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
2407
2408 if ( in_array( 'page', $options, true ) ) {
2409 $ret['tables'][] = 'page';
2410 $ret['fields'] = array_merge( $ret['fields'], [
2411 'page_namespace',
2412 'page_title',
2413 'page_id',
2414 'page_latest',
2415 'page_is_redirect',
2416 'page_len',
2417 ] );
2418 $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
2419 }
2420
2421 if ( in_array( 'user', $options, true ) ) {
2422 $ret['tables'][] = 'user';
2423 $ret['fields'][] = 'user_name';
2424 $ret['joins']['user'] = [
2425 'LEFT JOIN',
2426 [ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ]
2427 ];
2428 }
2429
2430 if ( in_array( 'text', $options, true ) ) {
2431 throw new InvalidArgumentException(
2432 'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2433 );
2434 }
2435
2436 return $ret;
2437 }
2438
2445
2452
2474 public function getSlotsQueryInfo( $options = [] ) {
2475 $ret = [
2476 'tables' => [ 'slots' ],
2477 'fields' => [
2478 'slot_revision_id',
2479 'slot_content_id',
2480 'slot_origin',
2481 'slot_role_id',
2482 ],
2483 'joins' => [],
2484 'keys' => [
2485 'rev_id' => 'slot_revision_id',
2486 'role_id' => 'slot_role_id',
2487 ],
2488 ];
2489
2490 if ( in_array( 'role', $options, true ) ) {
2491 // Use left join to attach role name, so we still find the revision row even
2492 // if the role name is missing. This triggers a more obvious failure mode.
2493 $ret['tables'][] = 'slot_roles';
2494 $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
2495 $ret['fields'][] = 'role_name';
2496 }
2497
2498 if ( in_array( 'content', $options, true ) ) {
2499 $ret['keys']['model_id'] = 'content_model';
2500
2501 $ret['tables'][] = 'content';
2502 $ret['fields'] = array_merge( $ret['fields'], [
2503 'content_size',
2504 'content_sha1',
2505 'content_address',
2506 'content_model',
2507 ] );
2508 $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
2509
2510 if ( in_array( 'model', $options, true ) ) {
2511 // Use left join to attach model name, so we still find the revision row even
2512 // if the model name is missing. This triggers a more obvious failure mode.
2513 $ret['tables'][] = 'content_models';
2514 $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
2515 $ret['fields'][] = 'model_name';
2516 }
2517
2518 }
2519
2520 return $ret;
2521 }
2522
2531 public function isRevisionRow( $row, string $table = '' ) {
2532 if ( !( $row instanceof stdClass ) ) {
2533 return false;
2534 }
2535 $queryInfo = $table === 'archive' ? $this->getArchiveQueryInfo() : $this->getQueryInfo();
2536 foreach ( $queryInfo['fields'] as $alias => $field ) {
2537 $name = is_numeric( $alias ) ? $field : $alias;
2538 if ( !property_exists( $row, $name ) ) {
2539 return false;
2540 }
2541 }
2542 return true;
2543 }
2544
2564 public function getArchiveQueryInfo() {
2565 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
2566 $ret = [
2567 'tables' => [
2568 'archive',
2569 'archive_actor' => 'actor'
2570 ] + $commentQuery['tables'],
2571 'fields' => [
2572 'ar_id',
2573 'ar_page_id',
2574 'ar_namespace',
2575 'ar_title',
2576 'ar_rev_id',
2577 'ar_timestamp',
2578 'ar_minor_edit',
2579 'ar_deleted',
2580 'ar_len',
2581 'ar_parent_id',
2582 'ar_actor',
2583 'ar_user' => 'archive_actor.actor_user',
2584 'ar_user_text' => 'archive_actor.actor_name',
2585 ] + $commentQuery['fields'],
2586 'joins' => [
2587 'archive_actor' => [ 'JOIN', 'actor_id=ar_actor' ]
2588 ] + $commentQuery['joins'],
2589 ];
2590
2591 return $ret;
2592 }
2593
2603 public function getRevisionSizes( array $revIds ) {
2604 $dbr = $this->getReplicaConnection();
2605 $revLens = [];
2606 if ( !$revIds ) {
2607 return $revLens; // empty
2608 }
2609
2610 $res = $dbr->newSelectQueryBuilder()
2611 ->select( [ 'rev_id', 'rev_len' ] )
2612 ->from( 'revision' )
2613 ->where( [ 'rev_id' => $revIds ] )
2614 ->caller( __METHOD__ )->fetchResultSet();
2615
2616 foreach ( $res as $row ) {
2617 $revLens[$row->rev_id] = intval( $row->rev_len );
2618 }
2619
2620 return $revLens;
2621 }
2622
2631 private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
2632 $op = $dir === 'next' ? '>' : '<';
2633 $sort = $dir === 'next' ? 'ASC' : 'DESC';
2634
2635 $revisionIdValue = $rev->getId( $this->wikiId );
2636
2637 if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) {
2638 // revision is unsaved or otherwise incomplete
2639 return null;
2640 }
2641
2642 if ( $rev instanceof RevisionArchiveRecord ) {
2643 // revision is deleted, so it's not part of the page history
2644 return null;
2645 }
2646
2647 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2648
2649 $ts = $rev->getTimestamp() ?? $this->getTimestampFromId( $revisionIdValue, $flags );
2650 if ( $ts === false ) {
2651 // XXX Should this be moved into getTimestampFromId?
2652 $ts = $db->newSelectQueryBuilder()
2653 ->select( 'ar_timestamp' )
2654 ->from( 'archive' )
2655 ->where( [ 'ar_rev_id' => $revisionIdValue ] )
2656 ->caller( __METHOD__ )->fetchField();
2657 if ( $ts === false ) {
2658 // XXX Is this reachable? How can we have a page id but no timestamp?
2659 return null;
2660 }
2661 }
2662
2663 $revId = $db->newSelectQueryBuilder()
2664 ->select( 'rev_id' )
2665 ->from( 'revision' )
2666 ->where( [
2667 'rev_page' => $rev->getPageId( $this->wikiId ),
2668 $db->buildComparison( $op, [
2669 'rev_timestamp' => $db->timestamp( $ts ),
2670 'rev_id' => $revisionIdValue,
2671 ] ),
2672 ] )
2673 ->orderBy( [ 'rev_timestamp', 'rev_id' ], $sort )
2674 ->ignoreIndex( 'rev_timestamp' ) // Probably needed for T159319
2675 ->caller( __METHOD__ )
2676 ->fetchField();
2677
2678 if ( $revId === false ) {
2679 return null;
2680 }
2681
2682 return $this->getRevisionById( intval( $revId ), $flags );
2683 }
2684
2699 public function getPreviousRevision( RevisionRecord $rev, $flags = IDBAccessObject::READ_NORMAL ) {
2700 return $this->getRelativeRevision( $rev, $flags, 'prev' );
2701 }
2702
2714 public function getNextRevision( RevisionRecord $rev, $flags = IDBAccessObject::READ_NORMAL ) {
2715 return $this->getRelativeRevision( $rev, $flags, 'next' );
2716 }
2717
2729 private function getPreviousRevisionId( IReadableDatabase $db, RevisionRecord $rev ) {
2730 $this->checkDatabaseDomain( $db );
2731
2732 if ( $rev->getPageId( $this->wikiId ) === null ) {
2733 return 0;
2734 }
2735 # Use page_latest if ID is not given
2736 if ( !$rev->getId( $this->wikiId ) ) {
2737 $prevId = $db->newSelectQueryBuilder()
2738 ->select( 'page_latest' )
2739 ->from( 'page' )
2740 ->where( [ 'page_id' => $rev->getPageId( $this->wikiId ) ] )
2741 ->caller( __METHOD__ )->fetchField();
2742 } else {
2743 $prevId = $db->newSelectQueryBuilder()
2744 ->select( 'rev_id' )
2745 ->from( 'revision' )
2746 ->where( [ 'rev_page' => $rev->getPageId( $this->wikiId ) ] )
2747 ->andWhere( $db->expr( 'rev_id', '<', $rev->getId( $this->wikiId ) ) )
2748 ->orderBy( 'rev_id DESC' )
2749 ->caller( __METHOD__ )->fetchField();
2750 }
2751 return intval( $prevId );
2752 }
2753
2766 public function getTimestampFromId( $id, $flags = 0 ) {
2767 if ( $id instanceof Title ) {
2768 // Old deprecated calling convention supported for backwards compatibility
2769 $id = $flags;
2770 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2771 }
2772
2773 // T270149: Bail out if we know the query will definitely return false. Some callers are
2774 // passing RevisionRecord::getId() call directly as $id which can possibly return null.
2775 // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL'
2776 // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null.
2777 // @todo typehint $id and remove the null check
2778 if ( $id === null || $id <= 0 ) {
2779 return false;
2780 }
2781
2782 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2783
2784 $timestamp =
2786 ->select( 'rev_timestamp' )
2787 ->from( 'revision' )
2788 ->where( [ 'rev_id' => $id ] )
2789 ->caller( __METHOD__ )->fetchField();
2790
2791 return ( $timestamp !== false ) ? MWTimestamp::convert( TS::MW, $timestamp ) : false;
2792 }
2793
2803 public function countRevisionsByPageId( IReadableDatabase $db, $id ) {
2804 $this->checkDatabaseDomain( $db );
2805
2806 $row = $db->newSelectQueryBuilder()
2807 ->select( [ 'revCount' => 'COUNT(*)' ] )
2808 ->from( 'revision' )
2809 ->where( [ 'rev_page' => $id ] )
2810 ->caller( __METHOD__ )->fetchRow();
2811 if ( $row ) {
2812 return intval( $row->revCount );
2813 }
2814 return 0;
2815 }
2816
2827 $id = $this->getArticleId( $page );
2828 if ( $id ) {
2829 return $this->countRevisionsByPageId( $db, $id );
2830 }
2831 return 0;
2832 }
2833
2852 public function userWasLastToEdit( IReadableDatabase $db, $pageId, $userId, $since ) {
2853 $this->checkDatabaseDomain( $db );
2854
2855 if ( !$userId ) {
2856 return false;
2857 }
2858
2859 $queryBuilder = $this->newSelectQueryBuilder( $db )
2860 ->where( [
2861 'rev_page' => $pageId,
2862 $db->expr( 'rev_timestamp', '>', $db->timestamp( $since ) )
2863 ] )
2864 ->orderBy( 'rev_timestamp', SelectQueryBuilder::SORT_ASC )
2865 ->limit( 50 );
2866 $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
2867 foreach ( $res as $row ) {
2868 if ( $row->rev_user != $userId ) {
2869 return false;
2870 }
2871 }
2872 return true;
2873 }
2874
2888 public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) {
2889 $db = $this->getReplicaConnection();
2890 $revIdPassed = $revId;
2891 $pageId = $this->getArticleId( $page );
2892 if ( !$pageId ) {
2893 return false;
2894 }
2895
2896 if ( !$revId ) {
2897 if ( $page instanceof Title ) {
2898 $revId = $page->getLatestRevID();
2899 } else {
2900 $pageRecord = $this->pageStore->getPageByReference( $page );
2901 if ( $pageRecord ) {
2902 $revId = $pageRecord->getLatest( $this->getWikiId() );
2903 }
2904 }
2905 }
2906
2907 if ( !$revId ) {
2908 $this->logger->warning(
2909 'No latest revision known for page {page} even though it exists with page ID {page_id}', [
2910 'page' => $page->__toString(),
2911 'page_id' => $pageId,
2912 'wiki_id' => $this->getWikiId() ?: 'local',
2913 ] );
2914 return false;
2915 }
2916
2917 // Load the row from cache if possible. If not possible, populate the cache.
2918 // As a minor optimization, remember if this was a cache hit or miss.
2919 // We can sometimes avoid a database query later if this is a cache miss.
2920 $fromCache = true;
2921 $row = $this->cache->getWithSetCallback(
2922 // Page/rev IDs passed in from DB to reflect history merges
2923 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2924 WANObjectCache::TTL_WEEK,
2925 function ( $curValue, &$ttl, array &$setOpts ) use (
2926 $db, $revId, &$fromCache
2927 ) {
2928 $setOpts += Database::getCacheSetOptions( $db );
2929 $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
2930 if ( $row ) {
2931 $fromCache = false;
2932 }
2933 return $row; // don't cache negatives
2934 }
2935 );
2936
2937 // Reflect revision deletion and user renames.
2938 if ( $row ) {
2939 $title = $this->ensureRevisionRowMatchesPage( $row, $page, [
2940 'from_cache_flag' => $fromCache,
2941 'page_id_initial' => $pageId,
2942 'rev_id_used' => $revId,
2943 'rev_id_requested' => $revIdPassed,
2944 ] );
2945
2946 return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
2947 } else {
2948 return false;
2949 }
2950 }
2951
2960 public function getFirstRevision(
2961 $page,
2962 int $flags = IDBAccessObject::READ_NORMAL
2963 ): ?RevisionRecord {
2964 $conds = $this->getPageConditions( $page );
2965 if ( !$conds ) {
2966 return null;
2967 }
2968 if ( !( $page instanceof PageIdentity ) ) {
2969 if ( !( $page instanceof PageReference ) ) {
2970 wfDeprecated( __METHOD__ . ' with a LinkTarget', '1.45' );
2971 }
2972 $page = null;
2973 }
2974
2975 return $this->newRevisionFromConds( $conds, $flags, $page,
2976 [
2977 'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
2978 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
2979 ]
2980 );
2981 }
2982
2994 private function getRevisionRowCacheKey( IReadableDatabase $db, $pageId, $revId ) {
2995 return $this->cache->makeGlobalKey(
2996 self::ROW_CACHE_KEY,
2997 $db->getDomainID(),
2998 $pageId,
2999 $revId
3000 );
3001 }
3002
3009 private function assertRevisionParameter( $paramName, $pageId, ?RevisionRecord $rev = null ) {
3010 if ( $rev ) {
3011 if ( $rev->getId( $this->wikiId ) === null ) {
3012 throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
3013 }
3014 if ( $rev->getPageId( $this->wikiId ) !== $pageId ) {
3015 throw new InvalidArgumentException(
3016 "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
3017 );
3018 }
3019 }
3020 }
3021
3026 private function getPageConditions( LinkTarget|PageReference $page ): ?array {
3027 if ( $page instanceof PageIdentity ) {
3028 return $page->exists() ? [ 'page_id' => $page->getId( $this->wikiId ) ] : null;
3029 } elseif ( $page instanceof PageReference ) {
3030 if ( $page->getWikiId() !== $this->wikiId ) {
3031 throw new InvalidArgumentException( 'Non-matching wiki ID for PageReference' );
3032 }
3033 return [
3034 'page_namespace' => $page->getNamespace(),
3035 'page_title' => $page->getDBkey(),
3036 ];
3037 } else {
3038 // Only resolve LinkTarget when operating in the context of the local wiki (T248756)
3039 if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
3040 throw new InvalidArgumentException( 'Cannot use non-local LinkTarget' );
3041 }
3042 return [
3043 'page_namespace' => $page->getNamespace(),
3044 'page_title' => $page->getDBkey(),
3045 ];
3046 }
3047 }
3048
3063 private function getRevisionLimitConditions(
3064 ISQLPlatform $dbr,
3065 ?RevisionRecord $old = null,
3066 ?RevisionRecord $new = null,
3067 $options = []
3068 ) {
3069 $options = (array)$options;
3070 if ( in_array( self::INCLUDE_OLD, $options ) || in_array( self::INCLUDE_BOTH, $options ) ) {
3071 $oldCmp = '>=';
3072 } else {
3073 $oldCmp = '>';
3074 }
3075 if ( in_array( self::INCLUDE_NEW, $options ) || in_array( self::INCLUDE_BOTH, $options ) ) {
3076 $newCmp = '<=';
3077 } else {
3078 $newCmp = '<';
3079 }
3080
3081 $conds = [];
3082 if ( $old ) {
3083 $conds[] = $dbr->buildComparison( $oldCmp, [
3084 'rev_timestamp' => $dbr->timestamp( $old->getTimestamp() ),
3085 'rev_id' => $old->getId( $this->wikiId ),
3086 ] );
3087 }
3088 if ( $new ) {
3089 $conds[] = $dbr->buildComparison( $newCmp, [
3090 'rev_timestamp' => $dbr->timestamp( $new->getTimestamp() ),
3091 'rev_id' => $new->getId( $this->wikiId ),
3092 ] );
3093 }
3094 return $conds;
3095 }
3096
3123 public function getRevisionIdsBetween(
3124 int $pageId,
3125 ?RevisionRecord $old = null,
3126 ?RevisionRecord $new = null,
3127 ?int $max = null,
3128 $options = [],
3129 ?string $order = null,
3130 int $flags = IDBAccessObject::READ_NORMAL
3131 ): array {
3132 $this->assertRevisionParameter( 'old', $pageId, $old );
3133 $this->assertRevisionParameter( 'new', $pageId, $new );
3134
3135 $options = (array)$options;
3136 $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3137 in_array( self::INCLUDE_BOTH, $options );
3138 $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3139 in_array( self::INCLUDE_BOTH, $options );
3140
3141 // No DB query needed if old and new are the same revision.
3142 // Can't check for consecutive revisions with 'getParentId' for a similar
3143 // optimization as edge cases exist when there are revisions between
3144 // a revision and it's parent. See T185167 for more details.
3145 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3146 return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3147 }
3148
3149 $db = $this->getDBConnectionRefForQueryFlags( $flags );
3150 $queryBuilder = $db->newSelectQueryBuilder()
3151 ->select( 'rev_id' )
3152 ->from( 'revision' )
3153 ->where( [
3154 'rev_page' => $pageId,
3155 $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
3156 ] )
3157 ->andWhere( $this->getRevisionLimitConditions( $db, $old, $new, $options ) );
3158
3159 if ( $order !== null ) {
3160 $queryBuilder->orderBy( [ 'rev_timestamp', 'rev_id' ], $order );
3161 }
3162 if ( $max !== null ) {
3163 // extra to detect truncation
3164 $queryBuilder->limit( $max + 1 );
3165 }
3166
3167 $values = $queryBuilder->caller( __METHOD__ )->fetchFieldValues();
3168 return array_map( 'intval', $values );
3169 }
3170
3192 public function getAuthorsBetween(
3193 $pageId,
3194 ?RevisionRecord $old = null,
3195 ?RevisionRecord $new = null,
3196 ?Authority $performer = null,
3197 $max = null,
3198 $options = []
3199 ) {
3200 $this->assertRevisionParameter( 'old', $pageId, $old );
3201 $this->assertRevisionParameter( 'new', $pageId, $new );
3202 $options = (array)$options;
3203
3204 // No DB query needed if old and new are the same revision.
3205 // Can't check for consecutive revisions with 'getParentId' for a similar
3206 // optimization as edge cases exist when there are revisions between
3207 //a revision and it's parent. See T185167 for more details.
3208 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3209 if ( !$options ) {
3210 return [];
3211 } elseif ( $performer ) {
3212 return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3213 } else {
3214 return [ $new->getUser() ];
3215 }
3216 }
3217
3218 $dbr = $this->getReplicaConnection();
3219 $queryBuilder = $dbr->newSelectQueryBuilder()
3220 ->select( [
3221 'rev_actor',
3222 'rev_user' => 'revision_actor.actor_user',
3223 'rev_user_text' => 'revision_actor.actor_name',
3224 ] )
3225 ->distinct()
3226 ->from( 'revision' )
3227 ->join( 'actor', 'revision_actor', 'revision_actor.actor_id = rev_actor' )
3228 ->where( [
3229 'rev_page' => $pageId,
3230 $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
3231 ] )
3232 ->andWhere( $this->getRevisionLimitConditions( $dbr, $old, $new, $options ) )
3233 ->caller( __METHOD__ );
3234 if ( $max !== null ) {
3235 $queryBuilder->limit( $max + 1 );
3236 }
3237
3238 return array_map(
3239 function ( $row ) {
3240 return $this->actorStore->newActorFromRowFields(
3241 $row->rev_user,
3242 $row->rev_user_text,
3243 $row->rev_actor
3244 );
3245 },
3246 iterator_to_array( $queryBuilder->fetchResultSet() )
3247 );
3248 }
3249
3271 public function countAuthorsBetween(
3272 $pageId,
3273 ?RevisionRecord $old = null,
3274 ?RevisionRecord $new = null,
3275 ?Authority $performer = null,
3276 $max = null,
3277 $options = []
3278 ) {
3279 // TODO: Implement with a separate query to avoid cost of selecting unneeded fields
3280 // and creation of UserIdentity stuff.
3281 return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3282 }
3283
3309 public function countRevisionsBetween(
3310 $pageId,
3311 ?RevisionRecord $old = null,
3312 ?RevisionRecord $new = null,
3313 $max = null,
3314 $options = []
3315 ) {
3316 $this->assertRevisionParameter( 'old', $pageId, $old );
3317 $this->assertRevisionParameter( 'new', $pageId, $new );
3318
3319 // No DB query needed if old and new are the same revision.
3320 // Can't check for consecutive revisions with 'getParentId' for a similar
3321 // optimization as edge cases exist when there are revisions between
3322 //a revision and it's parent. See T185167 for more details.
3323 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3324 return 0;
3325 }
3326
3327 $dbr = $this->getReplicaConnection();
3328 $where = [ 'rev_page' => $pageId ];
3329 // If $options is a string, convert it to an array
3330 $options = (array)$options;
3331 if ( !in_array( self::INCLUDE_DELETED_REVISIONS, $options ) ) {
3332 $where[] = $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0";
3333 }
3334 $conds = array_merge(
3335 $where,
3336 $this->getRevisionLimitConditions( $dbr, $old, $new, $options )
3337 );
3338 if ( $max !== null ) {
3339 return $dbr->newSelectQueryBuilder()
3340 ->select( '1' )
3341 ->from( 'revision' )
3342 ->where( $conds )
3343 ->caller( __METHOD__ )
3344 ->limit( $max + 1 ) // extra to detect truncation
3345 ->fetchRowCount();
3346 } else {
3347 return (int)$dbr->newSelectQueryBuilder()
3348 ->select( 'count(*)' )
3349 ->from( 'revision' )
3350 ->where( $conds )
3351 ->caller( __METHOD__ )->fetchField();
3352 }
3353 }
3354
3366 public function findIdenticalRevision(
3367 RevisionRecord $revision,
3368 int $searchLimit
3369 ): ?RevisionRecord {
3370 $revision->assertWiki( $this->wikiId );
3371 $db = $this->getReplicaConnection();
3372
3373 // Build the target slot role ID => sha1 array from the provided revision
3374 // (this is seemingly necessary *before* fetching the candidate revisions: T406027)
3375 $searchedSlotHashes = [];
3376 $slots = $revision->getSlots()->getPrimarySlots();
3377 foreach ( $slots as $slot ) {
3378 $roleId = $this->slotRoleStore->acquireId( $slot->getRole() );
3379 $searchedSlotHashes[$roleId] = $slot->getSha1();
3380 }
3381 ksort( $searchedSlotHashes );
3382
3383 // First fetch the IDs of the most recent revisions for this page, limited by the search limit.
3384 $candidateRevIds = $db->newSelectQueryBuilder()
3385 ->select( 'rev_id' )
3386 ->from( 'revision' )
3387 ->where( [ 'rev_page' => $revision->getPageId( $this->wikiId ) ] )
3388 // Include 'rev_id' in the ordering in case there are multiple revs with same timestamp
3389 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
3390 // T354015
3391 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
3392 ->limit( $searchLimit )
3393 // skip the most recent edit, we can't revert to it anyway
3394 ->offset( 1 )
3395 ->caller( __METHOD__ )
3396 ->fetchFieldValues();
3397
3398 if ( $candidateRevIds === [] ) {
3399 return null;
3400 }
3401
3402 // Then, for only those revisions, fetch their slot role ID => sha1 data.
3403 $candidateRevisions = $db->newSelectQueryBuilder()
3404 ->select( [ 'slot_revision_id', 'slot_role_id', 'content_sha1' ] )
3405 ->from( 'slots' )
3406 ->join( 'content', null, 'content_id = slot_content_id' )
3407 ->where( [ 'slot_revision_id' => $candidateRevIds ] )
3408 ->caller( __METHOD__ )
3409 ->fetchResultSet();
3410
3411 // Build a map of candidate revisions to their slot role ID => sha1 arrays
3412 $candidateSlotHashes = [];
3413 foreach ( $candidateRevisions as $candidate ) {
3414 $candidateSlotHashes[$candidate->slot_revision_id][$candidate->slot_role_id] = $candidate->content_sha1;
3415 }
3416
3417 // Find the first revision that has the same slot role ID => sha1 array as the provided revision.
3418 // We use $candidateRevIds, which are ordered by the revision timestamps, to ensure we return
3419 // the most recent revision that matches.
3420 $matchRevId = null;
3421 foreach ( $candidateRevIds as $revId ) {
3422 ksort( $candidateSlotHashes[$revId] );
3423 if ( $candidateSlotHashes[$revId] === $searchedSlotHashes ) {
3424 $matchRevId = $revId;
3425 break;
3426 }
3427 }
3428
3429 $revisionRow = null;
3430 if ( $matchRevId !== null ) {
3431 $revisionRow = $this->newSelectQueryBuilder( $db )
3432 ->joinComment()
3433 ->where( [ 'rev_id' => $matchRevId ] )
3434 ->caller( __METHOD__ )
3435 ->fetchRow();
3436 }
3437
3438 return $revisionRow ? $this->newRevisionFromRow( $revisionRow ) : null;
3439 }
3440
3441 // TODO: move relevant methods from Title here, e.g. isBigDeletion, etc.
3442}
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:68
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( $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.
getParentId( $wikiId=self::LOCAL)
Get parent revision ID (the original previous page revision).
getPageId( $wikiId=self::LOCAL)
Get the page ID.
getComment( $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Fetch revision comment, if it's available to the specified audience.
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
updateSlotsOn(RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw)
Update derived slots in an existing revision into the database, returning the modified slots on succe...
getSlotsQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new SlotRecord.
getContentBlobsForBatch( $rowsOrIds, $slots=null, $queryFlags=0)
Gets raw (serialized) content blobs for the given set of revisions.
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
newRevisionFromRow( $row, $queryFlags=0, ?PageIdentity $page=null, $fromCache=false)
getKnownCurrentRevision(PageIdentity $page, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaced Revision::isUnpatrolled.
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, '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'=> '', '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'=>[], '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'=> false, '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 -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> false, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, '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, 250, 300,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailStepsRatio'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'EnableSpecialMute'=> false, 'EnableUserEmailMuteList'=> false, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'UserEmailConfirmationUseHTML'=> false, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, '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'=>[], 'FileSchemaMigrationStage'=> 3, 'ImageLinksSchemaMigrationStage'=> 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',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory',],], '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'=> '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, 'WarmParsoidParserCache'=> false,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, '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',],],], '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, ], '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, 'UseLegacyMediaStyles' => 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, ], ], '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, ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'default' => true, ], '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, '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, ], '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, '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' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], '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, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => 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, '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, '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, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => 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', 'editviewmywatchlist' => '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', ], 'GrantRiskGroups' => [ 'basic' => 'low', 'editpage' => 'low', 'createeditmovepage' => 'low', 'editprotected' => 'vandalism', 'patrol' => 'low', 'uploadfile' => 'low', 'uploadeditmovefile' => 'low', 'sendemail' => 'security', 'viewmywatchlist' => 'low', 'editviewmywatchlist' => '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', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => 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, 'CachePrefix' => false, 'DebugToolbar' => false, '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: ], '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' => [ ], 'RCEngines' => [ 'redis' => 'MediaWiki\\RCFeed\\RedisPubSubFeedEngine', 'udp' => 'MediaWiki\\RCFeed\\UDPRCFeedEngine', ], '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, ], '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, '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\\JobQueue\\Jobs\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', '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', 'parsoidCachePrewarm' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\ParsoidCachePrewarmJob', 'services' => [ 'ParserOutputAccess', 'PageStore', 'RevisionLookup', 'ParsoidSiteConfig', ], 'needsPage' => false, ], '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', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], '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, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'VirtualRestConfig' => [ 'paths' => [ ], 'modules' => [ ], 'global' => [ 'timeout' => 360, 'forwardCookies' => false, 'HTTPProxy' => 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' => [ ], 'CloneArticleParserOutput' => true, 'UseLeximorph' => false, 'UsePostprocCache' => false, 'ParserOptionsLogUnsafeSampleRate' => 0, ], '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', 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', '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', ], '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', ], 'ThumbnailStepsRatio' => [ 'number', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'UserEmailConfirmationUseHTML' => 'boolean', 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ImageLinksSchemaMigrationStage' => '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', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'PHPSessionHandling' => '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', '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', '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', 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => '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', 'RestrictionTypes' => 'array', 'RestrictionLevels' => 'array', 'CascadingRestrictionLevels' => 'array', 'SemiprotectedRestrictionLevels' => 'array', 'NamespaceProtection' => 'object', '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', ], 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ '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', 'RCEngines' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => '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', 'RestSandboxSpecs' => 'object', 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'VirtualRestConfig' => 'object', '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', 'CloneArticleParserOutput' => 'boolean', 'UseLeximorph' => 'boolean', 'UsePostprocCache' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', ], 'mergeStrategy' => [ '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', 'VirtualRestConfig' => 'array_plus_2d', ], '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', ], ], ], ], ], 'PHPSessionHandling' => [ 'deprecated' => 'since 1.45 Integration with PHP session handling will be removed in the future', ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], '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', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], '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', ], ], 'RestSandboxSpecs' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'url' => [ 'type' => 'string', 'format' => 'url', ], 'name' => [ 'type' => 'string', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], 'required' => [ 'url', ], ], ], '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...