MediaWiki master
WatchedItemStore.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Watchlist;
4
5use DateInterval;
6use InvalidArgumentException;
7use LogicException;
19use stdClass;
30use Wikimedia\ScopedCallback;
31use Wikimedia\Timestamp\ConvertibleTimestamp;
32use Wikimedia\Timestamp\TimestampFormat as TS;
33
43
47 public const CONSTRUCTOR_OPTIONS = [
53 ];
54
58 private $lbFactory;
59
63 private $queueGroup;
64
68 private $stash;
69
73 private $readOnlyMode;
74
78 private $cache;
79
83 private $latestUpdateCache;
84
92 private $cacheIndex = [];
93
97 private $deferredUpdatesAddCallableUpdateCallback;
98
102 private $updateRowsPerQuery;
103
107 private $nsInfo;
108
112 private $revisionLookup;
113
117 private $expiryEnabled;
118
119 private bool $labelsEnabled;
120
124 private $linkBatchFactory;
125
127 private $labelStore;
128
132 private $maxExpiryDuration;
133
135 private $watchlistPurgeRate;
136
137 public function __construct(
138 ServiceOptions $options,
139 ILBFactory $lbFactory,
140 JobQueueGroup $queueGroup,
141 BagOStuff $stash,
142 HashBagOStuff $cache,
143 ReadOnlyMode $readOnlyMode,
144 NamespaceInfo $nsInfo,
145 RevisionLookup $revisionLookup,
146 LinkBatchFactory $linkBatchFactory,
147 WatchlistLabelStore $labelStore,
148 ) {
149 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
150 $this->updateRowsPerQuery = $options->get( MainConfigNames::UpdateRowsPerQuery );
151 $this->expiryEnabled = $options->get( MainConfigNames::WatchlistExpiry );
152 $this->maxExpiryDuration = $options->get( MainConfigNames::WatchlistExpiryMaxDuration );
153 $this->watchlistPurgeRate = $options->get( MainConfigNames::WatchlistPurgeRate );
154 $this->labelsEnabled = $options->get( MainConfigNames::EnableWatchlistLabels );
155 $this->labelStore = $labelStore;
156
157 $this->lbFactory = $lbFactory;
158 $this->queueGroup = $queueGroup;
159 $this->stash = $stash;
160 $this->cache = $cache;
161 $this->readOnlyMode = $readOnlyMode;
162 $this->deferredUpdatesAddCallableUpdateCallback =
163 DeferredUpdates::addCallableUpdate( ... );
164 $this->nsInfo = $nsInfo;
165 $this->revisionLookup = $revisionLookup;
166 $this->linkBatchFactory = $linkBatchFactory;
167
168 $this->latestUpdateCache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
169 }
170
179 #[\NoDiscard]
180 public function overrideDeferredUpdatesAddCallableUpdateCallback( callable $callback ): ScopedCallback {
181 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
182 throw new LogicException(
183 'Cannot override DeferredUpdates::addCallableUpdate callback in operation.'
184 );
185 }
186 $previousValue = $this->deferredUpdatesAddCallableUpdateCallback;
187 $this->deferredUpdatesAddCallableUpdateCallback = $callback;
188 return new ScopedCallback( function () use ( $previousValue ) {
189 $this->deferredUpdatesAddCallableUpdateCallback = $previousValue;
190 } );
191 }
192
198 private function getCacheKey( UserIdentity $user, PageReference $target ): string {
199 return $this->cache->makeKey(
200 (string)$target->getNamespace(),
201 $target->getDBkey(),
202 (string)$user->getId()
203 );
204 }
205
206 private function cache( UserIdentity $user, PageReference $target, ?WatchedItem $item ) {
207 $key = $this->getCacheKey( $user, $target );
208 $this->cache->set( $key, $item );
209 $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
210 }
211
212 private function uncache( UserIdentity $user, PageReference $target ) {
213 $this->cache->delete( $this->getCacheKey( $user, $target ) );
214 unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
215 }
216
217 private function uncacheTitle( PageReference $target ) {
218 if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
219 return;
220 }
221
222 foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
223 $this->cache->delete( $key );
224 }
225 }
226
227 private function uncacheUser( UserIdentity $user ) {
228 foreach ( $this->cacheIndex as $dbKeyArray ) {
229 foreach ( $dbKeyArray as $userArray ) {
230 if ( isset( $userArray[$user->getId()] ) ) {
231 $this->cache->delete( $userArray[$user->getId()] );
232 }
233 }
234 }
235
236 $pageSeenKey = $this->getPageSeenTimestampsKey( $user );
237 $this->latestUpdateCache->delete( $pageSeenKey );
238 $this->stash->delete( $pageSeenKey );
239 }
240
247 private function getCached( UserIdentity $user, PageReference $target ) {
248 return $this->cache->get( $this->getCacheKey( $user, $target ) );
249 }
250
258 private function modifyQueryBuilderForExpiry(
259 SelectQueryBuilder $queryBuilder,
260 IReadableDatabase $db
261 ) {
262 if ( $this->expiryEnabled ) {
263 $queryBuilder->where( $db->expr( 'we_expiry', '=', null )->or( 'we_expiry', '>', $db->timestamp() ) );
264 $queryBuilder->leftJoin( 'watchlist_expiry', null, 'wl_id = we_item' );
265 }
266 }
267
274 private function addLabelSummaryField(
275 SelectQueryBuilder $queryBuilder,
276 IReadableDatabase $db
277 ) {
278 $subquery = $db->newSelectQueryBuilder()
279 ->select( 'wlm_label' )
280 ->distinct()
281 ->from( 'watchlist_label_member' )
282 ->where( [ 'wlm_item=wl_id' ] )
283 ->buildGroupConcatField( ',' );
284 $queryBuilder->fields( [ 'wlm_label_summary' => $subquery ] );
285 }
286
297 public function clearUserWatchedItems( UserIdentity $user ): bool {
298 if ( $this->mustClearWatchedItemsUsingJobQueue( $user ) ) {
299 return false;
300 }
301
302 $dbw = $this->lbFactory->getPrimaryDatabase();
303
304 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
305 // First fetch the wl_ids.
306 $wlIds = $dbw->newSelectQueryBuilder()
307 ->select( 'wl_id' )
308 ->from( 'watchlist' )
309 ->where( [ 'wl_user' => $user->getId() ] )
310 ->caller( __METHOD__ )
311 ->fetchFieldValues();
312 if ( $wlIds ) {
313 // Delete rows from both the watchlist and watchlist_expiry tables.
314 $dbw->newDeleteQueryBuilder()
315 ->deleteFrom( 'watchlist' )
316 ->where( [ 'wl_id' => $wlIds ] )
317 ->caller( __METHOD__ )->execute();
318
319 if ( $this->expiryEnabled ) {
320 $dbw->newDeleteQueryBuilder()
321 ->deleteFrom( 'watchlist_expiry' )
322 ->where( [ 'we_item' => $wlIds ] )
323 ->caller( __METHOD__ )->execute();
324 }
325 if ( $this->labelsEnabled ) {
326 $dbw->newDeleteQueryBuilder()
327 ->deleteFrom( 'watchlist_label_member' )
328 ->where( [ 'wlm_item' => $wlIds ] )
329 ->caller( __METHOD__ )->execute();
330 }
331 }
332 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
333 $this->uncacheAllItemsForUser( $user );
334
335 return true;
336 }
337
338 public function mustClearWatchedItemsUsingJobQueue( UserIdentity $user ): bool {
339 return $this->countWatchedItems( $user ) > $this->updateRowsPerQuery;
340 }
341
342 private function uncacheAllItemsForUser( UserIdentity $user ) {
343 $userId = $user->getId();
344 foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
345 foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
346 if ( array_key_exists( $userId, $userIndex ) ) {
347 $this->cache->delete( $userIndex[$userId] );
348 unset( $this->cacheIndex[$ns][$dbKey][$userId] );
349 }
350 }
351 }
352
353 // Cleanup empty cache keys
354 foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
355 foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
356 if ( empty( $this->cacheIndex[$ns][$dbKey] ) ) {
357 unset( $this->cacheIndex[$ns][$dbKey] );
358 }
359 }
360 if ( empty( $this->cacheIndex[$ns] ) ) {
361 unset( $this->cacheIndex[$ns] );
362 }
363 }
364 }
365
374 $job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() );
375 $this->queueGroup->push( $job );
376 }
377
381 public function maybeEnqueueWatchlistExpiryJob(): void {
382 if ( !$this->expiryEnabled ) {
383 // No need to purge expired entries if there are none
384 return;
385 }
386
387 $max = mt_getrandmax();
388 if ( mt_rand( 0, $max ) < $max * $this->watchlistPurgeRate ) {
389 // The higher the watchlist purge rate, the more likely we are to enqueue a job.
390 $this->queueGroup->lazyPush( new WatchlistExpiryJob() );
391 }
392 }
393
398 public function getMaxId(): int {
399 return (int)$this->lbFactory->getReplicaDatabase()->newSelectQueryBuilder()
400 ->select( 'MAX(wl_id)' )
401 ->from( 'watchlist' )
402 ->caller( __METHOD__ )
403 ->fetchField();
404 }
405
411 public function countWatchedItems( UserIdentity $user ): int {
412 $dbr = $this->lbFactory->getReplicaDatabase();
413 $queryBuilder = $dbr->newSelectQueryBuilder()
414 ->select( 'COUNT(*)' )
415 ->from( 'watchlist' )
416 ->where( [ 'wl_user' => $user->getId() ] )
417 ->caller( __METHOD__ );
418
419 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
420
421 return (int)$queryBuilder->fetchField();
422 }
423
429 public function countWatchers( PageReference $target ): int {
430 $dbr = $this->lbFactory->getReplicaDatabase();
431 $queryBuilder = $dbr->newSelectQueryBuilder()
432 ->select( 'COUNT(*)' )
433 ->from( 'watchlist' )
434 ->where( [
435 'wl_namespace' => $target->getNamespace(),
436 'wl_title' => $target->getDBkey()
437 ] )
438 ->caller( __METHOD__ );
439
440 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
441
442 return (int)$queryBuilder->fetchField();
443 }
444
451 public function countVisitingWatchers( PageReference $target, $threshold ): int {
452 $dbr = $this->lbFactory->getReplicaDatabase();
453 $queryBuilder = $dbr->newSelectQueryBuilder()
454 ->select( 'COUNT(*)' )
455 ->from( 'watchlist' )
456 ->where( [
457 'wl_namespace' => $target->getNamespace(),
458 'wl_title' => $target->getDBkey(),
459 $dbr->expr( 'wl_notificationtimestamp', '>=', $dbr->timestamp( $threshold ) )
460 ->or( 'wl_notificationtimestamp', '=', null )
461 ] )
462 ->caller( __METHOD__ );
463
464 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
465
466 return (int)$queryBuilder->fetchField();
467 }
468
474 public function removeWatchBatchForUser( UserIdentity $user, array $titles ): bool {
475 if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
476 return false;
477 }
478 if ( !$titles ) {
479 return true;
480 }
481
482 $this->uncacheTitlesForUser( $user, $titles );
483
484 $dbw = $this->lbFactory->getPrimaryDatabase();
485 $ticket = count( $titles ) > $this->updateRowsPerQuery ?
486 $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) : null;
487 $affectedRows = 0;
488
489 $wlIds = $this->loadIdsForTargets( $dbw, $user, $titles );
490 foreach ( $this->batch( $wlIds ) as $ids ) {
491 // Delete rows from the watchlist and associated tables.
492 $dbw->newDeleteQueryBuilder()
493 ->deleteFrom( 'watchlist' )
494 ->where( [ 'wl_id' => $ids ] )
495 ->caller( __METHOD__ )->execute();
496 $affectedRows += $dbw->affectedRows();
497
498 if ( $this->expiryEnabled ) {
499 $dbw->newDeleteQueryBuilder()
500 ->deleteFrom( 'watchlist_expiry' )
501 ->where( [ 'we_item' => $ids ] )
502 ->caller( __METHOD__ )->execute();
503 }
504 if ( $this->labelsEnabled ) {
505 $dbw->newDeleteQueryBuilder()
506 ->deleteFrom( 'watchlist_label_member' )
507 ->where( [ 'wlm_item' => $ids ] )
508 ->caller( __METHOD__ )->execute();
509 }
510
511 if ( $ticket ) {
512 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
513 }
514 }
515
516 return (bool)$affectedRows;
517 }
518
526 public function countWatchersMultiple( array $targets, array $options = [] ): array {
527 $lb = $this->linkBatchFactory->newLinkBatch( $targets );
528 $dbr = $this->lbFactory->getReplicaDatabase();
529 $queryBuilder = $dbr->newSelectQueryBuilder();
530 $queryBuilder
531 ->select( [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ] )
532 ->from( 'watchlist' )
533 ->where( [ $lb->constructSet( 'wl', $dbr ) ] )
534 ->groupBy( [ 'wl_namespace', 'wl_title' ] )
535 ->caller( __METHOD__ );
536
537 if ( array_key_exists( 'minimumWatchers', $options ) ) {
538 $queryBuilder->having( 'COUNT(*) >= ' . (int)$options['minimumWatchers'] );
539 }
540
541 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
542
543 $res = $queryBuilder->fetchResultSet();
544
545 $watchCounts = [];
546 foreach ( $targets as $linkTarget ) {
547 $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
548 }
549
550 foreach ( $res as $row ) {
551 $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
552 }
553
554 return $watchCounts;
555 }
556
566 array $targetsWithVisitThresholds,
567 $minimumWatchers = null
568 ): array {
569 if ( $targetsWithVisitThresholds === [] ) {
570 // No titles requested => no results returned
571 return [];
572 }
573
574 $dbr = $this->lbFactory->getReplicaDatabase();
575 $queryBuilder = $dbr->newSelectQueryBuilder()
576 ->select( [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ] )
577 ->from( 'watchlist' )
578 ->where( [ $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds ) ] )
579 ->groupBy( [ 'wl_namespace', 'wl_title' ] )
580 ->caller( __METHOD__ );
581 if ( $minimumWatchers !== null ) {
582 $queryBuilder->having( 'COUNT(*) >= ' . (int)$minimumWatchers );
583 }
584 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
585
586 $res = $queryBuilder->fetchResultSet();
587
588 $watcherCounts = [];
589 foreach ( $targetsWithVisitThresholds as [ $target ] ) {
590 $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0;
591 }
592
593 foreach ( $res as $row ) {
594 $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
595 }
596
597 return $watcherCounts;
598 }
599
608 private function getVisitingWatchersCondition(
609 IReadableDatabase $db,
610 array $targetsWithVisitThresholds
611 ): string {
612 $missingTargets = [];
613 $namespaceConds = [];
614 foreach ( $targetsWithVisitThresholds as [ $target, $threshold ] ) {
615 if ( $threshold === null ) {
616 $missingTargets[] = $target;
617 continue;
618 }
620 $namespaceConds[$target->getNamespace()][] = $db->expr( 'wl_title', '=', $target->getDBkey() )
621 ->andExpr(
622 $db->expr( 'wl_notificationtimestamp', '>=', $db->timestamp( $threshold ) )
623 ->or( 'wl_notificationtimestamp', '=', null )
624 );
625 }
626
627 $conds = [];
628 foreach ( $namespaceConds as $namespace => $pageConds ) {
629 $conds[] = $db->makeList( [
630 'wl_namespace = ' . $namespace,
631 '(' . $db->makeList( $pageConds, LIST_OR ) . ')'
632 ], LIST_AND );
633 }
634
635 if ( $missingTargets ) {
636 $lb = $this->linkBatchFactory->newLinkBatch( $missingTargets );
637 $conds[] = $lb->constructSet( 'wl', $db );
638 }
639
640 return $db->makeList( $conds, LIST_OR );
641 }
642
649 public function getWatchedItem( UserIdentity $user, PageReference $target ) {
650 if ( !$user->isRegistered() ) {
651 return false;
652 }
653
654 $cached = $this->getCached( $user, $target );
655 if ( $cached !== false ) {
656 return $cached && !$cached->isExpired() ? $cached : false;
657 }
658 return $this->loadWatchedItem( $user, $target );
659 }
660
667 public function loadWatchedItem( UserIdentity $user, PageReference $target ) {
668 $item = $this->loadWatchedItemsBatch( $user, [ $target ] );
669 return $item ? $item[0] : false;
670 }
671
678 public function loadWatchedItemsBatch( UserIdentity $user, array $targets ) {
679 // Only registered user can have a watchlist
680 if ( !$user->isRegistered() ) {
681 return [];
682 }
683
684 $dbr = $this->lbFactory->getReplicaDatabase();
685
686 $rows = $this->fetchWatchedItemRows(
687 $dbr,
688 $user,
689 $targets,
690 [],
691 );
692
693 if ( $this->labelsEnabled && $rows->numRows() ) {
694 $labels = $this->labelStore->loadAllForUser( $user );
695 } else {
696 $labels = [];
697 }
698
699 $items = [];
700 $targetMap = [];
701 foreach ( $rows as $row ) {
702 $item = $this->getWatchedItemFromRow( $user, $row, $labels );
703 $items[] = $item;
704 $target = $item->getTarget();
705 $targetMap[$target->getNamespace()][$target->getDBkey()] = $item;
706 }
707
708 foreach ( $targets as $target ) {
709 $this->cache( $user, $target, $targetMap[$target->getNamespace()][$target->getDBkey()] ?? null );
710 }
711
712 return $items;
713 }
714
726 public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ): array {
727 $options += [ 'forWrite' => false, 'sort' => self::SORT_ASC ];
728 if ( $options['forWrite'] ) {
729 $db = $this->lbFactory->getPrimaryDatabase();
730 } else {
731 $db = $this->lbFactory->getReplicaDatabase();
732 }
733
734 if ( $options['sort'] == self::SORT_ASC ) {
735 $orderBy = [ 'wl_namespace', 'wl_title' ];
736 } else {
737 $orderBy = [ 'wl_namespace DESC', 'wl_title DESC' ];
738 }
739 return $this->fetchWatchedItems( $db, $user, $options, $orderBy );
740 }
741
753 private function fetchWatchedItems(
754 IDatabase $db, UserIdentity $user, array $options, array $orderBy, array $extraConditions = []
755 ): array {
756 $fetchOptions = [];
757 $fetchOptions['orderBy'] = $orderBy;
758 if ( isset( $options['limit'] ) ) {
759 $fetchOptions['limit'] = $options['limit'];
760 }
761 if ( isset( $options['offsetConds'] ) ) {
762 $offsetConds = is_array( $options['offsetConds'] )
763 ? $options['offsetConds'] :
764 [ $options['offsetConds'] ];
765 $extraConditions = array_merge( $extraConditions, $offsetConds );
766 }
767 if ( isset( $options['namespaces'] ) ) {
768 $extraConditions['wl_namespace'] = $options['namespaces'];
769 }
770 $fetchOptions['extraConds'] = $extraConditions;
771 $res = $this->fetchWatchedItemRows( $db, $user, null, $fetchOptions );
772
773 // Load label names
774 if ( $this->labelsEnabled && $res->numRows() ) {
775 $labels = $this->labelStore->loadAllForUser( $user );
776 } else {
777 $labels = [];
778 }
779
780 $watchedItems = [];
781 foreach ( $res as $row ) {
782 $watchedItems[] = $this->getWatchedItemFromRow( $user, $row, $labels );
783 }
784 return $watchedItems;
785 }
786
794 private function getWatchedItemFromRow(
795 UserIdentity $user,
796 stdClass $row,
797 array $labelsForUser
798 ): WatchedItem {
799 $target = PageReferenceValue::localReference( (int)$row->wl_namespace, $row->wl_title );
800 if ( ( $row->wlm_label_summary ?? '' ) !== '' ) {
801 $labelIds = explode( ',', $row->wlm_label_summary );
802 $labels = array_intersect_key( $labelsForUser, array_fill_keys( $labelIds, true ) );
803 } else {
804 $labels = [];
805 }
806 return new WatchedItem(
807 $user,
808 $target,
809 $this->getLatestNotificationTimestamp(
810 $row->wl_notificationtimestamp, $user, $target ),
811 wfTimestampOrNull( TS::ISO_8601, $row->we_expiry ?? null ),
812 array_values( $labels )
813 );
814 }
815
830 private function fetchWatchedItemRows(
831 IReadableDatabase $db,
832 UserIdentity $user,
833 $target = null,
834 array $options = [],
835 ) {
836 $fieldNames = [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ];
837 if ( $this->expiryEnabled ) {
838 $fieldNames[] = 'we_expiry';
839 }
840 $queryBuilder = $db->newSelectQueryBuilder()
841 ->select( $fieldNames )
842 ->from( 'watchlist' )
843 ->where( [ 'wl_user' => $user->getId() ] )
844 ->caller( __METHOD__ );
845 if ( $target ) {
846 $queryBuilder->where( $this->getTargetsCond( $target ) );
847 }
848 $this->modifyQueryBuilderForExpiry( $queryBuilder, $db );
849 if ( $this->labelsEnabled ) {
850 $this->addLabelSummaryField( $queryBuilder, $db );
851 }
852
853 if ( array_key_exists( 'orderBy', $options ) && is_array( $options['orderBy'] ) ) {
854 $queryBuilder->orderBy( $options['orderBy'] );
855 }
856 if ( array_key_exists( 'extraConds', $options ) && is_array( $options['extraConds'] ) ) {
857 $queryBuilder->where( $options['extraConds'] );
858 }
859 if ( array_key_exists( 'limit', $options ) && ( intval( $options['limit'] ) > 0 ) ) {
860 $queryBuilder->limit( $options['limit'] );
861 }
862
863 return $queryBuilder->fetchResultSet();
864 }
865
872 public function isWatched( UserIdentity $user, PageReference $target ): bool {
873 return (bool)$this->getWatchedItem( $user, $target );
874 }
875
883 public function isTempWatched( UserIdentity $user, PageReference $target ): bool {
884 $item = $this->getWatchedItem( $user, $target );
885 return $item && $item->getExpiry();
886 }
887
895 public function getNotificationTimestampsBatch( UserIdentity $user, array $targets ): array {
896 $timestamps = [];
897 foreach ( $targets as $target ) {
898 $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
899 }
900
901 if ( !$user->isRegistered() ) {
902 return $timestamps;
903 }
904
905 $targetsToLoad = [];
906 foreach ( $targets as $target ) {
907 $cachedItem = $this->getCached( $user, $target );
908 if ( $cachedItem ) {
909 $timestamps[$target->getNamespace()][$target->getDBkey()] =
910 $cachedItem->getNotificationTimestamp();
911 } else {
912 $targetsToLoad[] = $target;
913 }
914 }
915
916 if ( !$targetsToLoad ) {
917 return $timestamps;
918 }
919
920 $dbr = $this->lbFactory->getReplicaDatabase();
921
922 $res = $dbr->newSelectQueryBuilder()
923 ->select( [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ] )
924 ->from( 'watchlist' )
925 ->where( [
926 $this->getTargetsCond( $targetsToLoad ),
927 'wl_user' => $user->getId(),
928 ] )
929 ->caller( __METHOD__ )
930 ->fetchResultSet();
931
932 foreach ( $res as $row ) {
933 $target = PageReferenceValue::localReference( (int)$row->wl_namespace, $row->wl_title );
934 $timestamps[$row->wl_namespace][$row->wl_title] =
935 $this->getLatestNotificationTimestamp(
936 $row->wl_notificationtimestamp, $user, $target );
937 }
938
939 return $timestamps;
940 }
941
950 public function addWatch( UserIdentity $user, PageReference $target, ?string $expiry = null ) {
951 $this->addWatchBatchForUser( $user, [ $target ], $expiry );
952
953 if ( $this->expiryEnabled ) {
954 // When re-watching a page with a null $expiry, any existing expiry is left unchanged.
955 // However we must re-fetch the preexisting expiry or else the cached WatchedItem will
956 // incorrectly have a null expiry. Note that loadWatchedItem() does the caching.
957 // See T259379
958 if ( !$expiry ) {
959 $this->loadWatchedItem( $user, $target );
960 return;
961 }
962
963 $expiry = ExpiryDef::normalizeUsingMaxExpiry( $expiry, $this->maxExpiryDuration, TS::ISO_8601 );
964 } else {
965 $expiry = null;
966 }
967
968 // Create a new WatchedItem and add it to the process cache.
969 $item = new WatchedItem(
970 $user,
971 $target,
972 null,
973 $expiry
974 );
975 $this->cache( $user, $target, $item );
976 }
977
991 public function addWatchBatchForUser(
992 UserIdentity $user,
993 array $targets,
994 ?string $expiry = null
995 ): bool {
996 // Only registered user can have a watchlist
997 if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
998 return false;
999 }
1000
1001 if ( !$targets ) {
1002 return true;
1003 }
1004 $expiry = ExpiryDef::normalizeUsingMaxExpiry( $expiry, $this->maxExpiryDuration, TS::ISO_8601 );
1005 $rows = [];
1006 foreach ( $targets as $target ) {
1007 $rows[] = [
1008 'wl_user' => $user->getId(),
1009 'wl_namespace' => $target->getNamespace(),
1010 'wl_title' => $target->getDBkey(),
1011 'wl_notificationtimestamp' => null,
1012 ];
1013 $this->uncache( $user, $target );
1014 }
1015
1016 $dbw = $this->lbFactory->getPrimaryDatabase();
1017 $ticket = count( $targets ) > $this->updateRowsPerQuery ?
1018 $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) : null;
1019 $affectedRows = 0;
1020 $rowBatches = array_chunk( $rows, $this->updateRowsPerQuery );
1021 foreach ( $rowBatches as $toInsert ) {
1022 // Use INSERT IGNORE to avoid overwriting the notification timestamp
1023 // if there's already an entry for this page
1024 $dbw->newInsertQueryBuilder()
1025 ->insertInto( 'watchlist' )
1026 ->ignore()
1027 ->rows( $toInsert )
1028 ->caller( __METHOD__ )->execute();
1029 $affectedRows += $dbw->affectedRows();
1030
1031 if ( $this->expiryEnabled ) {
1032 $affectedRows += $this->updateOrDeleteExpiries( $dbw, $user->getId(), $toInsert, $expiry );
1033 }
1034
1035 if ( $ticket ) {
1036 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1037 }
1038 }
1039
1040 return (bool)$affectedRows;
1041 }
1042
1052 private function updateOrDeleteExpiries(
1053 IDatabase $dbw,
1054 int $userId,
1055 array $rows,
1056 ?string $expiry = null
1057 ): int {
1058 if ( !$expiry ) {
1059 // if expiry is null (shouldn't change), 0 rows affected.
1060 return 0;
1061 }
1062
1063 // Build the giant `(...) OR (...)` part to be used with WHERE.
1064 $conds = [];
1065 foreach ( $rows as $row ) {
1066 $conds[] = $dbw->makeList(
1067 [
1068 'wl_user' => $userId,
1069 'wl_namespace' => $row['wl_namespace'],
1070 'wl_title' => $row['wl_title']
1071 ],
1072 $dbw::LIST_AND
1073 );
1074 }
1075 $cond = $dbw->makeList( $conds, $dbw::LIST_OR );
1076
1077 if ( wfIsInfinity( $expiry ) ) {
1078 // Rows should be deleted rather than updated.
1079 $dbw->deleteJoin(
1080 'watchlist_expiry',
1081 'watchlist',
1082 'we_item',
1083 'wl_id',
1084 [ $cond ],
1085 __METHOD__
1086 );
1087
1088 return $dbw->affectedRows();
1089 }
1090
1091 return $this->updateExpiries( $dbw, $expiry, $cond );
1092 }
1093
1101 private function updateExpiries( IDatabase $dbw, string $expiry, string $cond ): int {
1102 // First fetch the wl_ids from the watchlist table.
1103 // We'd prefer to do a INSERT/SELECT in the same query with IDatabase::insertSelect(),
1104 // but it doesn't allow us to use the "ON DUPLICATE KEY UPDATE" clause.
1105 $wlIds = $dbw->newSelectQueryBuilder()
1106 ->select( 'wl_id' )
1107 ->from( 'watchlist' )
1108 ->where( $cond )
1109 ->caller( __METHOD__ )
1110 ->fetchFieldValues();
1111
1112 if ( !$wlIds ) {
1113 return 0;
1114 }
1115
1116 $expiry = $dbw->timestamp( $expiry );
1117 $weRows = [];
1118 foreach ( $wlIds as $wlId ) {
1119 $weRows[] = [
1120 'we_item' => $wlId,
1121 'we_expiry' => $expiry
1122 ];
1123 }
1124
1125 // Insert into watchlist_expiry, updating the expiry for duplicate rows.
1126 $dbw->newInsertQueryBuilder()
1127 ->insertInto( 'watchlist_expiry' )
1128 ->rows( $weRows )
1129 ->onDuplicateKeyUpdate()
1130 ->uniqueIndexFields( [ 'we_item' ] )
1131 ->set( [ 'we_expiry' => $expiry ] )
1132 ->caller( __METHOD__ )->execute();
1133
1134 return $dbw->affectedRows();
1135 }
1136
1143 public function removeWatch( UserIdentity $user, PageReference $target ): bool {
1144 return $this->removeWatchBatchForUser( $user, [ $target ] );
1145 }
1146
1165 UserIdentity $user,
1166 $timestamp,
1167 array $targets = []
1168 ): bool {
1169 // Only registered user can have a watchlist
1170 if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
1171 return false;
1172 }
1173
1174 if ( !$targets ) {
1175 // Backwards compatibility
1176 $this->resetAllNotificationTimestampsForUser( $user, $timestamp );
1177 return true;
1178 }
1179
1180 $dbw = $this->lbFactory->getPrimaryDatabase();
1181 if ( $timestamp !== null ) {
1182 $timestamp = $dbw->timestamp( $timestamp );
1183 }
1184 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
1185 $affectedSinceWait = 0;
1186
1187 $wlIds = $this->loadIdsForTargets( $dbw, $user, $targets );
1188 foreach ( $this->batch( $wlIds ) as $ids ) {
1189 $dbw->newUpdateQueryBuilder()
1190 ->update( 'watchlist' )
1191 ->set( [ 'wl_notificationtimestamp' => $timestamp ] )
1192 ->where( [ 'wl_id' => $ids ] )
1193 ->caller( __METHOD__ )->execute();
1194
1195 $affectedSinceWait += $dbw->affectedRows();
1196 // Wait for replication every time we've touched updateRowsPerQuery rows
1197 if ( $affectedSinceWait >= $this->updateRowsPerQuery ) {
1198 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1199 $affectedSinceWait = 0;
1200 }
1201 }
1202
1203 $this->uncacheUser( $user );
1204
1205 return true;
1206 }
1207
1215 $timestamp,
1216 UserIdentity $user,
1217 PageReference $target
1218 ) {
1219 $timestamp = wfTimestampOrNull( TS::MW, $timestamp );
1220 if ( $timestamp === null ) {
1221 return null; // no notification
1222 }
1223
1224 $seenTimestamps = $this->getPageSeenTimestamps( $user );
1225 if ( $seenTimestamps ) {
1226 $seenKey = $this->getPageSeenKey( $target );
1227 if ( isset( $seenTimestamps[$seenKey] ) && $seenTimestamps[$seenKey] >= $timestamp ) {
1228 // If a reset job did not yet run, then the "seen" timestamp will be higher
1229 return null;
1230 }
1231 }
1232
1233 return $timestamp;
1234 }
1235
1242 public function resetAllNotificationTimestampsForUser( UserIdentity $user, $timestamp = null ) {
1243 // Only registered user can have a watchlist
1244 if ( !$user->isRegistered() ) {
1245 return;
1246 }
1247
1248 // If the page is watched by the user (or may be watched), update the timestamp
1250 'userId' => $user->getId(), 'timestamp' => $timestamp, 'casTime' => time()
1251 ] );
1252
1253 // Try to run this post-send
1254 // Calls DeferredUpdates::addCallableUpdate in normal operation
1255 ( $this->deferredUpdatesAddCallableUpdateCallback )(
1256 static function () use ( $job ) {
1257 $job->run();
1258 }
1259 );
1260 }
1261
1271 UserIdentity $editor,
1272 PageReference $target,
1273 $timestamp
1274 ): array {
1275 $dbw = $this->lbFactory->getPrimaryDatabase();
1276 $queryBuilder = $dbw->newSelectQueryBuilder()
1277 ->select( [ 'wl_id', 'wl_user' ] )
1278 ->from( 'watchlist' )
1279 ->where(
1280 [
1281 'wl_user != ' . $editor->getId(),
1282 'wl_namespace' => $target->getNamespace(),
1283 'wl_title' => $target->getDBkey(),
1284 'wl_notificationtimestamp' => null,
1285 ]
1286 )
1287 ->caller( __METHOD__ );
1288
1289 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbw );
1290
1291 $res = $queryBuilder->fetchResultSet();
1292 $watchers = [];
1293 $wlIds = [];
1294 foreach ( $res as $row ) {
1295 $watchers[] = (int)$row->wl_user;
1296 $wlIds[] = (int)$row->wl_id;
1297 }
1298
1299 if ( $wlIds ) {
1300 $fname = __METHOD__;
1301 // Try to run this post-send
1302 // Calls DeferredUpdates::addCallableUpdate in normal operation
1303 ( $this->deferredUpdatesAddCallableUpdateCallback )(
1304 function () use ( $timestamp, $wlIds, $target, $fname ) {
1305 $dbw = $this->lbFactory->getPrimaryDatabase();
1306 $ticket = $this->lbFactory->getEmptyTransactionTicket( $fname );
1307
1308 $wlIdsChunks = array_chunk( $wlIds, $this->updateRowsPerQuery );
1309 foreach ( $wlIdsChunks as $wlIdsChunk ) {
1310 $dbw->newUpdateQueryBuilder()
1311 ->update( 'watchlist' )
1312 ->set( [ 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) ] )
1313 ->where( [ 'wl_id' => $wlIdsChunk ] )
1314 ->caller( $fname )->execute();
1315
1316 if ( count( $wlIdsChunks ) > 1 ) {
1317 $this->lbFactory->commitAndWaitForReplication( $fname, $ticket );
1318 }
1319 }
1320 $this->uncacheTitle( $target );
1321 },
1322 DeferredUpdates::POSTSEND,
1323 $dbw
1324 );
1325 }
1326
1327 return $watchers;
1328 }
1329
1339 UserIdentity $user,
1340 PageReference $title,
1341 $force = '',
1342 $oldid = 0
1343 ): bool {
1344 $time = time();
1345
1346 // Only registered user can have a watchlist
1347 if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
1348 return false;
1349 }
1350
1351 $item = null;
1352 if ( $force != 'force' ) {
1353 $item = $this->getWatchedItem( $user, $title );
1354 if ( !$item || $item->getNotificationTimestamp() === null ) {
1355 return false;
1356 }
1357 }
1358
1359 // Get the timestamp (TS::MW) of this revision to track the latest one seen
1360 $id = $oldid;
1361 $seenTime = null;
1362 if ( !$id ) {
1363 $latestRev = $this->revisionLookup->getRevisionByTitle( $title );
1364 if ( $latestRev ) {
1365 $id = $latestRev->getId();
1366 // Save a DB query
1367 $seenTime = $latestRev->getTimestamp();
1368 }
1369 }
1370 if ( $seenTime === null ) {
1371 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable getId does not return null here
1372 $seenTime = $this->revisionLookup->getTimestampFromId( $id );
1373 }
1374
1375 // Mark the item as read immediately in lightweight storage
1376 $this->stash->merge(
1377 $this->getPageSeenTimestampsKey( $user ),
1378 function ( $cache, $key, $current ) use ( $title, $seenTime ) {
1379 if ( !$current ) {
1380 $value = new MapCacheLRU( 300 );
1381 } elseif ( is_array( $current ) ) {
1382 $value = MapCacheLRU::newFromArray( $current, 300 );
1383 } else {
1384 // Backwards compatibility for T282105
1385 $value = $current;
1386 }
1387 $subKey = $this->getPageSeenKey( $title );
1388
1389 if ( $seenTime > $value->get( $subKey ) ) {
1390 // Revision is newer than the last one seen
1391 $value->set( $subKey, $seenTime );
1392
1393 $this->latestUpdateCache->set( $key, $value->toArray(), BagOStuff::TTL_PROC_LONG );
1394 } elseif ( $seenTime === false ) {
1395 // Revision does not exist
1396 $value->set( $subKey, ConvertibleTimestamp::now( TS::MW ) );
1397 $this->latestUpdateCache->set( $key,
1398 $value->toArray(),
1399 BagOStuff::TTL_PROC_LONG );
1400 } else {
1401 return false; // nothing to update
1402 }
1403
1404 return $value->toArray();
1405 },
1406 BagOStuff::TTL_HOUR
1407 );
1408
1409 // If the page is watched by the user (or may be watched), update the timestamp
1410 // ActivityUpdateJob accepts both LinkTarget and PageReference
1411 $job = new ActivityUpdateJob(
1412 $title,
1413 [
1414 'type' => 'updateWatchlistNotification',
1415 'userid' => $user->getId(),
1416 'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ),
1417 'curTime' => $time
1418 ]
1419 );
1420 // Try to enqueue this post-send
1421 $this->queueGroup->lazyPush( $job );
1422
1423 $this->uncache( $user, $title );
1424
1425 return true;
1426 }
1427
1432 private function getPageSeenTimestamps( UserIdentity $user ) {
1433 $key = $this->getPageSeenTimestampsKey( $user );
1434
1435 $cache = $this->latestUpdateCache->getWithSetCallback(
1436 $key,
1437 BagOStuff::TTL_PROC_LONG,
1438 function () use ( $key ) {
1439 return $this->stash->get( $key ) ?: null;
1440 }
1441 );
1442 // Backwards compatibility for T282105
1443 if ( $cache instanceof MapCacheLRU ) {
1444 $cache = $cache->toArray();
1445 }
1446 return $cache;
1447 }
1448
1449 private function getPageSeenTimestampsKey( UserIdentity $user ): string {
1450 return $this->stash->makeGlobalKey(
1451 'watchlist-recent-updates',
1452 $this->lbFactory->getLocalDomainID(),
1453 $user->getId()
1454 );
1455 }
1456
1461 private function getPageSeenKey( $target ): string {
1462 return "{$target->getNamespace()}:{$target->getDBkey()}";
1463 }
1464
1473 private function getNotificationTimestamp(
1474 UserIdentity $user,
1475 PageReference $title,
1476 $item,
1477 $force,
1478 $oldid
1479 ) {
1480 if ( !$oldid ) {
1481 // No oldid given, assuming latest revision; clear the timestamp.
1482 return null;
1483 }
1484
1485 $oldRev = $this->revisionLookup->getRevisionById( $oldid );
1486 if ( !$oldRev ) {
1487 // Oldid given but does not exist (probably deleted)
1488 return false;
1489 }
1490
1491 $nextRev = $this->revisionLookup->getNextRevision( $oldRev );
1492 if ( !$nextRev ) {
1493 // Oldid given and is the latest revision for this title; clear the timestamp.
1494 return null;
1495 }
1496
1497 $item ??= $this->loadWatchedItem( $user, $title );
1498 if ( !$item ) {
1499 // This can only happen if $force is enabled.
1500 return null;
1501 }
1502
1503 // Oldid given and isn't the latest; update the timestamp.
1504 // This will result in no further notification emails being sent!
1505 $notificationTimestamp = $this->revisionLookup->getTimestampFromId( $oldid );
1506 // @FIXME: this should use getTimestamp() for consistency with updates on new edits
1507 // $notificationTimestamp = $nextRev->getTimestamp(); // first unseen revision timestamp
1508
1509 // We need to go one second to the future because of various strict comparisons
1510 // throughout the codebase
1511 $ts = new MWTimestamp( $notificationTimestamp );
1512 $ts->timestamp->add( new DateInterval( 'PT1S' ) );
1513 $notificationTimestamp = $ts->getTimestamp( TS::MW );
1514
1515 if ( $notificationTimestamp < $item->getNotificationTimestamp() ) {
1516 if ( $force != 'force' ) {
1517 return false;
1518 } else {
1519 // This is a little silly…
1520 return $item->getNotificationTimestamp();
1521 }
1522 }
1523
1524 return $notificationTimestamp;
1525 }
1526
1533 public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null ) {
1534 $queryBuilder = $this->lbFactory->getReplicaDatabase()->newSelectQueryBuilder()
1535 ->select( '1' )
1536 ->from( 'watchlist' )
1537 ->where( [
1538 'wl_user' => $user->getId(),
1539 'wl_notificationtimestamp IS NOT NULL'
1540 ] )
1541 ->caller( __METHOD__ );
1542 if ( $unreadLimit !== null ) {
1543 $unreadLimit = (int)$unreadLimit;
1544 $queryBuilder->limit( $unreadLimit );
1545 }
1546
1547 $rowCount = $queryBuilder->fetchRowCount();
1548
1549 if ( $unreadLimit === null ) {
1550 return $rowCount;
1551 }
1552
1553 if ( $rowCount >= $unreadLimit ) {
1554 return true;
1555 }
1556
1557 return $rowCount;
1558 }
1559
1566 PageReference $oldTarget,
1567 PageReference $newTarget
1568 ) {
1569 // Duplicate first the subject page, then the talk page
1570 $this->duplicateEntry(
1571 PageReferenceValue::localReference(
1572 $this->nsInfo->getSubject( $oldTarget->getNamespace() ),
1573 $oldTarget->getDBkey(),
1574 ),
1575 PageReferenceValue::localReference(
1576 $this->nsInfo->getSubject( $newTarget->getNamespace() ),
1577 $newTarget->getDBkey()
1578 )
1579 );
1580 $this->duplicateEntry(
1581 PageReferenceValue::localReference(
1582 $this->nsInfo->getTalk( $oldTarget->getNamespace() ),
1583 $oldTarget->getDBkey()
1584 ),
1585 PageReferenceValue::localReference(
1586 $this->nsInfo->getTalk( $newTarget->getNamespace() ),
1587 $newTarget->getDBkey()
1588 )
1589 );
1590 }
1591
1597 public function duplicateEntry( PageReference $oldTarget, PageReference $newTarget ) {
1598 $dbw = $this->lbFactory->getPrimaryDatabase();
1599 $result = $this->fetchWatchedItemsForPage( $dbw, $oldTarget );
1600 $newNamespace = $newTarget->getNamespace();
1601 $newDBkey = $newTarget->getDBkey();
1602
1603 # Construct array to replace into the watchlist
1604 $values = [];
1605 $expiries = [];
1606 $labels = [];
1607 foreach ( $result as $row ) {
1608 $values[] = [
1609 'wl_user' => $row->wl_user,
1610 'wl_namespace' => $newNamespace,
1611 'wl_title' => $newDBkey,
1612 'wl_notificationtimestamp' => $row->wl_notificationtimestamp,
1613 ];
1614
1615 if ( $this->expiryEnabled && $row->we_expiry ) {
1616 $expiries[$row->wl_user] = $row->we_expiry;
1617 }
1618 if ( $this->labelsEnabled && $row->wlm_label_summary !== '' ) {
1619 $labels[$row->wl_user] = $row->wlm_label_summary;
1620 }
1621 }
1622
1623 if ( !$values ) {
1624 return;
1625 }
1626
1627 // Perform a replace on the watchlist table rows.
1628 // Note that multi-row replace is very efficient for MySQL but may be inefficient for
1629 // some other DBMSes, mostly due to poor simulation by us.
1630 $dbw->newReplaceQueryBuilder()
1631 ->replaceInto( 'watchlist' )
1632 ->uniqueIndexFields( [ 'wl_user', 'wl_namespace', 'wl_title' ] )
1633 ->rows( $values )
1634 ->caller( __METHOD__ )->execute();
1635
1636 if ( $expiries || $labels ) {
1637 $this->updateAssociationsAfterMove( $dbw, $expiries, $labels, $newNamespace, $newDBkey );
1638 }
1639 }
1640
1646 private function fetchWatchedItemsForPage(
1647 IReadableDatabase $dbr,
1648 PageReference $target
1649 ): IResultWrapper {
1650 $queryBuilder = $dbr->newSelectQueryBuilder()
1651 ->select( [ 'wl_user', 'wl_notificationtimestamp' ] )
1652 ->from( 'watchlist' )
1653 ->where( [
1654 'wl_namespace' => $target->getNamespace(),
1655 'wl_title' => $target->getDBkey(),
1656 ] )
1657 ->caller( __METHOD__ )
1658 ->forUpdate();
1659
1660 if ( $this->expiryEnabled ) {
1661 $queryBuilder->leftJoin( 'watchlist_expiry', null, [ 'wl_id = we_item' ] )
1662 ->field( 'we_expiry' );
1663 }
1664 if ( $this->labelsEnabled ) {
1665 $this->addLabelSummaryField( $queryBuilder, $dbr );
1666 }
1667
1668 return $queryBuilder->fetchResultSet();
1669 }
1670
1678 private function updateAssociationsAfterMove(
1679 IDatabase $dbw,
1680 array $expiries,
1681 array $labels,
1682 int $namespace,
1683 string $dbKey
1684 ): void {
1685 DeferredUpdates::addCallableUpdate(
1686 function ( $fname ) use ( $dbw, $expiries, $labels, $namespace, $dbKey ) {
1687 // First fetch new wl_ids.
1688 $res = $dbw->newSelectQueryBuilder()
1689 ->select( [ 'wl_user', 'wl_id' ] )
1690 ->from( 'watchlist' )
1691 ->where( [
1692 'wl_namespace' => $namespace,
1693 'wl_title' => $dbKey,
1694 ] )
1695 ->caller( $fname )
1696 ->fetchResultSet();
1697
1698 // Build new array to INSERT into multiple rows at once.
1699 $expiryData = [];
1700 $labelData = [];
1701 foreach ( $res as $row ) {
1702 if ( !empty( $expiries[$row->wl_user] ) ) {
1703 $expiryData[] = [
1704 'we_item' => $row->wl_id,
1705 'we_expiry' => $expiries[$row->wl_user],
1706 ];
1707 }
1708 if ( isset( $labels[$row->wl_user] ) ) {
1709 foreach ( explode( ',', $labels[$row->wl_user] ) as $labelId ) {
1710 $labelData[] = [
1711 'wlm_item' => $row->wl_id,
1712 'wlm_label' => (int)$labelId,
1713 ];
1714 }
1715 }
1716 }
1717
1718 foreach ( $this->batch( $expiryData ) as $toInsert ) {
1719 $dbw->newReplaceQueryBuilder()
1720 ->replaceInto( 'watchlist_expiry' )
1721 ->uniqueIndexFields( [ 'we_item' ] )
1722 ->rows( $toInsert )
1723 ->caller( $fname )
1724 ->execute();
1725 }
1726 foreach ( $this->batch( $labelData ) as $toInsert ) {
1727 $dbw->newReplaceQueryBuilder()
1728 ->replaceInto( 'watchlist_label_member' )
1729 ->uniqueIndexFields( [ 'wlm_label', 'wlm_item' ] )
1730 ->rows( $toInsert )
1731 ->caller( $fname )
1732 ->execute();
1733 }
1734 },
1735 DeferredUpdates::POSTSEND,
1736 $dbw
1737 );
1738 }
1739
1746 private function batch( array $data ) {
1747 return array_chunk( $data, $this->updateRowsPerQuery );
1748 }
1749
1754 private function uncacheTitlesForUser( UserIdentity $user, array $titles ) {
1755 foreach ( $titles as $title ) {
1756 $this->uncache( $user, $title );
1757 }
1758 }
1759
1763 public function countExpired(): int {
1764 $dbr = $this->lbFactory->getReplicaDatabase();
1765 return $dbr->newSelectQueryBuilder()
1766 ->select( '*' )
1767 ->from( 'watchlist_expiry' )
1768 ->where( $dbr->expr( 'we_expiry', '<=', $dbr->timestamp() ) )
1769 ->caller( __METHOD__ )
1770 ->fetchRowCount();
1771 }
1772
1776 public function removeExpired( int $limit, bool $deleteOrphans = false ): void {
1777 $dbr = $this->lbFactory->getReplicaDatabase();
1778 $dbw = $this->lbFactory->getPrimaryDatabase();
1779 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
1780
1781 // Get a batch of watchlist IDs to delete.
1782 $toDelete = $dbr->newSelectQueryBuilder()
1783 ->select( 'we_item' )
1784 ->from( 'watchlist_expiry' )
1785 ->where( $dbr->expr( 'we_expiry', '<=', $dbr->timestamp() ) )
1786 ->limit( $limit )
1787 ->caller( __METHOD__ )
1788 ->fetchFieldValues();
1789
1790 if ( count( $toDelete ) > 0 ) {
1791 // Delete them from the watchlist and associated tables
1792 $dbw->newDeleteQueryBuilder()
1793 ->deleteFrom( 'watchlist' )
1794 ->where( [ 'wl_id' => $toDelete ] )
1795 ->caller( __METHOD__ )->execute();
1796 $dbw->newDeleteQueryBuilder()
1797 ->deleteFrom( 'watchlist_expiry' )
1798 ->where( [ 'we_item' => $toDelete ] )
1799 ->caller( __METHOD__ )->execute();
1800 if ( $this->labelsEnabled ) {
1801 $dbw->newDeleteQueryBuilder()
1802 ->deleteFrom( 'watchlist_label_member' )
1803 ->where( [ 'wlm_item' => $toDelete ] )
1804 ->caller( __METHOD__ )->execute();
1805 }
1806 }
1807
1808 // Also delete any orphaned or null-expiry watchlist_expiry rows
1809 // (they should not exist, but might because not everywhere knows about the expiry table yet).
1810 if ( $deleteOrphans ) {
1811 $expiryToDelete = $dbr->newSelectQueryBuilder()
1812 ->select( 'we_item' )
1813 ->from( 'watchlist_expiry' )
1814 ->leftJoin( 'watchlist', null, 'wl_id = we_item' )
1815 ->where( $dbr->makeList(
1816 [ 'wl_id' => null, 'we_expiry' => null ],
1817 $dbr::LIST_OR
1818 ) )
1819 ->caller( __METHOD__ )
1820 ->fetchFieldValues();
1821 if ( count( $expiryToDelete ) > 0 ) {
1822 $dbw->newDeleteQueryBuilder()
1823 ->deleteFrom( 'watchlist_expiry' )
1824 ->where( [ 'we_item' => $expiryToDelete ] )
1825 ->caller( __METHOD__ )->execute();
1826 }
1827 }
1828
1829 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1830 }
1831
1833 public function addLabels( UserIdentity $user, array $targets, array $labels ): void {
1834 if ( !$labels ) {
1835 return;
1836 }
1837 if ( !$this->labelsEnabled ) {
1838 throw new LogicException( 'addLabels was called when ' .
1839 '$wgEnableWatchlistLabels was false -- caller should check' );
1840 }
1841 $labelIds = $this->getLabelIds( $labels );
1842 $dbw = $this->lbFactory->getPrimaryDatabase();
1843 $wlIds = $this->loadIdsForTargets( $dbw, $user, $targets );
1844
1845 foreach ( $this->batch( $wlIds ) as $wlIdsBatch ) {
1846 $rows = [];
1847 foreach ( $wlIdsBatch as $wlId ) {
1848 foreach ( $labelIds as $labelId ) {
1849 $rows[] = [
1850 'wlm_label' => $labelId,
1851 'wlm_item' => $wlId,
1852 ];
1853 }
1854 }
1855
1856 $dbw->newInsertQueryBuilder()
1857 ->insertInto( 'watchlist_label_member' )
1858 ->ignore()
1859 ->rows( $rows )
1860 ->caller( __METHOD__ )
1861 ->execute();
1862 }
1863 }
1864
1866 public function removeLabels( UserIdentity $user, array $targets, array $labels ): void {
1867 if ( !$labels ) {
1868 return;
1869 }
1870 if ( !$this->labelsEnabled ) {
1871 throw new LogicException( 'removeLabels was called when ' .
1872 '$wgEnableWatchlistLabels was false -- caller should check' );
1873 }
1874 $labelIds = $this->getLabelIds( $labels );
1875 $dbw = $this->lbFactory->getPrimaryDatabase();
1876 $wlIds = $this->loadIdsForTargets( $dbw, $user, $targets );
1877 foreach ( $this->batch( $wlIds ) as $wlIdsBatch ) {
1878 $dbw->newDeleteQueryBuilder()
1879 ->deleteFrom( 'watchlist_label_member' )
1880 ->where( [
1881 'wlm_item' => $wlIdsBatch,
1882 'wlm_label' => $labelIds
1883 ] )
1884 ->execute();
1885 }
1886 }
1887
1892 private function getLabelIds( array $labels ) {
1893 $labelIds = [];
1894 foreach ( $labels as $label ) {
1895 if ( $label instanceof WatchlistLabel ) {
1896 $labelId = $label->getId();
1897 if ( !$labelId ) {
1898 throw new InvalidArgumentException( 'WatchlistLabel has no label ID -- ' .
1899 'it must be loaded before adding it to an item' );
1900 }
1901 $labelIds[] = $labelId;
1902 } else {
1903 $labelIds[] = (int)$label;
1904 }
1905 }
1906 return $labelIds;
1907 }
1908
1917 private function loadIdsForTargets( IReadableDatabase $db, UserIdentity $user, array $targets ) {
1918 $idStrings = $db->newSelectQueryBuilder()
1919 ->select( 'wl_id' )
1920 ->from( 'watchlist' )
1921 ->where( [
1922 $this->getTargetsCond( $targets ),
1923 'wl_user' => $user->getId(),
1924 ] )
1925 ->caller( __METHOD__ )
1926 ->fetchFieldValues();
1927 return array_map( 'intval', $idStrings );
1928 }
1929
1936 private function getTargetsCond( array $targets ) {
1937 return $this->linkBatchFactory->newLinkBatch( $targets )
1938 ->constructSet( 'wl', $this->lbFactory->getReplicaDatabase() );
1939 }
1940
1941}
1943class_alias( WatchedItemStore::class, 'WatchedItemStore' );
const LIST_OR
Definition Defines.php:33
const LIST_AND
Definition Defines.php:30
wfTimestampOrNull( $outputtype=TS::UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
wfIsInfinity( $str)
Determine input string is represents as infinity.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Defer callable updates to run later in the PHP process.
Handle enqueueing of background jobs.
A class containing constants representing the names of configuration variables.
const UpdateRowsPerQuery
Name constant for the UpdateRowsPerQuery setting, for use with Config::get()
const WatchlistPurgeRate
Name constant for the WatchlistPurgeRate setting, for use with Config::get()
const EnableWatchlistLabels
Name constant for the EnableWatchlistLabels setting, for use with Config::get()
const WatchlistExpiry
Name constant for the WatchlistExpiry setting, for use with Config::get()
const WatchlistExpiryMaxDuration
Name constant for the WatchlistExpiryMaxDuration setting, for use with Config::get()
Factory for LinkBatch objects to batch query page metadata.
Immutable value object representing a page reference.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Library for creating and parsing MW-style timestamps.
Job for clearing all of the "last viewed" timestamps for a user's watchlist, or setting them all to t...
Storage layer class for WatchedItems.
addWatch(UserIdentity $user, PageReference $target, ?string $expiry=null)
getWatchedItemsForUser(UserIdentity $user, array $options=[])
getLatestNotificationTimestamp( $timestamp, UserIdentity $user, PageReference $target)
removeWatch(UserIdentity $user, PageReference $target)
countExpired()
Get the number of watchlist items that expire before the current time.1.35int
addLabels(UserIdentity $user, array $targets, array $labels)
Add a labels to a set of watchlist items.The same labels are applied to each item....
setNotificationTimestampsForUser(UserIdentity $user, $timestamp, array $targets=[])
Set the "last viewed" timestamps for certain titles on a user's watchlist.
maybeEnqueueWatchlistExpiryJob()
Probabilistically add a job to purge the expired watchlist items, if watchlist expiration is enabled,...
isWatched(UserIdentity $user, PageReference $target)
clearUserWatchedItemsUsingJobQueue(UserIdentity $user)
Queues a job that will clear the users watchlist using the Job Queue.
countVisitingWatchersMultiple(array $targetsWithVisitThresholds, $minimumWatchers=null)
mustClearWatchedItemsUsingJobQueue(UserIdentity $user)
Does the size of the users watchlist require clearUserWatchedItemsUsingJobQueue() to be used instead ...
resetNotificationTimestamp(UserIdentity $user, PageReference $title, $force='', $oldid=0)
loadWatchedItemsBatch(UserIdentity $user, array $targets)
getWatchedItem(UserIdentity $user, PageReference $target)
getNotificationTimestampsBatch(UserIdentity $user, array $targets)
addWatchBatchForUser(UserIdentity $user, array $targets, ?string $expiry=null)
Add multiple items to the user's watchlist.
countUnreadNotifications(UserIdentity $user, $unreadLimit=null)
updateNotificationTimestamp(UserIdentity $editor, PageReference $target, $timestamp)
Update wl_notificationtimestamp for all watching users except the editor.
removeLabels(UserIdentity $user, array $targets, array $labels)
Remove labels from a set of watchlist items.The same labels are removed from each item....
countVisitingWatchers(PageReference $target, $threshold)
duplicateEntry(PageReference $oldTarget, PageReference $newTarget)
loadWatchedItem(UserIdentity $user, PageReference $target)
duplicateAllAssociatedEntries(PageReference $oldTarget, PageReference $newTarget)
clearUserWatchedItems(UserIdentity $user)
Deletes ALL watched items for the given user when under $updateRowsPerQuery entries exist.
countWatchersMultiple(array $targets, array $options=[])
overrideDeferredUpdatesAddCallableUpdateCallback(callable $callback)
Overrides the DeferredUpdates::addCallableUpdate callback This is intended for use while testing and ...
__construct(ServiceOptions $options, ILBFactory $lbFactory, JobQueueGroup $queueGroup, BagOStuff $stash, HashBagOStuff $cache, ReadOnlyMode $readOnlyMode, NamespaceInfo $nsInfo, RevisionLookup $revisionLookup, LinkBatchFactory $linkBatchFactory, WatchlistLabelStore $labelStore,)
resetAllNotificationTimestampsForUser(UserIdentity $user, $timestamp=null)
Schedule a DeferredUpdate that sets all of the "last viewed" timestamps for a given user to the same ...
removeWatchBatchForUser(UserIdentity $user, array $titles)
removeExpired(int $limit, bool $deleteOrphans=false)
Remove some number of expired watchlist items.1.35
isTempWatched(UserIdentity $user, PageReference $target)
Check if the user is temporarily watching the page.
Representation of a pair of user and title for watchlist entries.
Service class for storage of watchlist labels.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:73
getWithSetCallback( $key, $exptime, $callback, $flags=0)
Get an item, regenerating and setting it if not found.
Store data in a memory for the current request/process only.
Store key-value entries in a size-limited in-memory LRU cache.
Type definition for expiry timestamps.
Definition ExpiryDef.php:18
Determine whether a site is currently in read-only mode.
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'=> true, '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, 220, 250, 300, 400,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'TrackMediaRequestProvenance'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EmailConfirmationBanner'=> false, 'EnotifWatchlist'=> false, 'EnotifUserTalk'=> false, 'EnotifRevealEditorAddress'=> false, 'EnotifMinorEdits'=> true, 'EnotifUseRealName'=> false, 'UsersNotifiedOnAllChanges'=>[], 'DBname'=> 'my_wiki', 'DBmwschema'=> null, 'DBprefix'=> '', 'DBserver'=> 'localhost', 'DBport'=> 5432, 'DBuser'=> 'wikiuser', 'DBpassword'=> '', 'DBtype'=> 'mysql', 'DBssl'=> false, 'DBcompress'=> false, 'DBStrictWarnings'=> false, 'DBadminuser'=> null, 'DBadminpassword'=> null, 'SearchType'=> null, 'SearchTypeAlternatives'=> null, 'DBTableOptions'=> 'ENGINE=InnoDB, DEFAULT CHARSET=binary', 'SQLMode'=> '', 'SQLiteDataDir'=> '', 'SharedDB'=> null, 'SharedPrefix'=> false, 'SharedTables'=>['user', 'user_properties', 'user_autocreate_serial',], 'SharedSchema'=> false, 'DBservers'=> false, 'LBFactoryConf'=>['class'=> 'Wikimedia\\Rdbms\\LBFactorySimple',], 'DataCenterUpdateStickTTL'=> 10, 'DBerrorLog'=> false, 'DBerrorLogTZ'=> false, 'LocalDatabases'=>[], 'DatabaseReplicaLagWarning'=> 10, 'DatabaseReplicaLagCritical'=> 30, 'MaxExecutionTimeForExpensiveQueries'=> 0, 'VirtualDomainsMapping'=>[], 'FileSchemaMigrationStage'=> 3, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'CodeHighlighter',],], 'text'=> 'MediaWiki\\Content\\TextContentHandler', 'unknown'=> 'MediaWiki\\Content\\FallbackContentHandler',], 'NamespaceContentModels'=>[], 'TextModelsToParse'=>['wikitext', 'javascript', 'css',], 'CompressRevisions'=> false, 'ExternalStores'=>[], 'ExternalServers'=>[], 'DefaultExternalStore'=> false, 'RevisionCacheExpiry'=> 604800, 'PageLanguageUseDB'=> false, 'DiffEngine'=> null, 'ExternalDiffEngine'=> false, 'Wikidiff2Options'=>[], 'RequestTimeLimit'=> null, 'TransactionalTimeLimit'=> 120, 'CriticalSectionTimeLimit'=> 180.0, 'MiserMode'=> false, 'DisableQueryPages'=> false, 'QueryCacheLimit'=> 1000, 'WantedPagesThreshold'=> 1, 'AllowSlowParserFunctions'=> false, 'AllowSchemaUpdates'=> true, 'MaxArticleSize'=> 2048, 'MemoryLimit'=> '50M', 'PoolCounterConf'=> null, 'PoolCountClientConf'=>['servers'=>['127.0.0.1',], 'timeout'=> 0.1,], 'MaxUserDBWriteDuration'=> false, 'MaxJobDBWriteDuration'=> false, 'LinkHolderBatchSize'=> 1000, 'MaximumMovedPages'=> 100, 'ForceDeferredUpdatesPreSend'=> false, 'MultiShardSiteStats'=> false, 'CacheDirectory'=> false, 'MainCacheType'=> 0, 'MessageCacheType'=> -1, 'ParserCacheType'=> -1, 'SessionCacheType'=> -1, 'AnonSessionCacheType'=> false, 'LanguageConverterCacheType'=> -1, 'ObjectCaches'=>[0=>['class'=> 'Wikimedia\\ObjectCache\\EmptyBagOStuff', 'reportDupes'=> false,], 1=>['class'=> 'MediaWiki\\ObjectCache\\SqlBagOStuff', 'loggroup'=> 'SQLBagOStuff',], 'memcached-php'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPhpBagOStuff', 'loggroup'=> 'memcached',], 'memcached-pecl'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPeclBagOStuff', 'loggroup'=> 'memcached',], 'hash'=>['class'=> 'Wikimedia\\ObjectCache\\HashBagOStuff', 'reportDupes'=> false,], 'apc'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,], 'apcu'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,],], 'WANObjectCache'=>[], 'MicroStashType'=> -1, 'MainStash'=> 1, 'ParsoidCacheConfig'=>['StashType'=> null, 'StashDuration'=> 86400, 'WarmParsoidParserCache'=> false,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'JwtSessionCookieIssuer'=> null, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> 'MediaWiki\\Language\\LocalisationCache', 'store'=> 'detect', 'storeClass'=> false, 'storeDirectory'=> false, 'storeServer'=>[], 'forceRecache'=> false, 'manualRecache'=> false,], 'CachePages'=> true, 'CacheEpoch'=> '20030516000000', 'GitInfoCacheDirectory'=> false, 'UseFileCache'=> false, 'FileCacheDepth'=> 2, 'RenderHashAppend'=> '', 'EnableSidebarCache'=> false, 'SidebarCacheExpiry'=> 86400, 'UseGzip'=> false, 'InvalidateCacheOnLocalSettingsChange'=> true, 'ExtensionInfoMTime'=> false, 'EnableRemoteBagOStuffTests'=> false, 'UseCdn'=> false, 'VaryOnXFP'=> false, 'InternalServer'=> false, 'CdnMaxAge'=> 18000, 'CdnMaxageLagged'=> 30, 'CdnMaxageStale'=> 10, 'CdnReboundPurgeDelay'=> 0, 'CdnMaxageSubstitute'=> 60, 'ForcedRawSMaxage'=> 300, 'CdnServers'=>[], 'CdnServersNoPurge'=>[], 'HTCPRouting'=>[], 'HTCPMulticastTTL'=> 1, 'UsePrivateIPs'=> false, 'CdnMatchParameterOrder'=> true, 'LanguageCode'=> 'en', 'GrammarForms'=>[], 'InterwikiMagic'=> true, 'HideInterlanguageLinks'=> false, 'ExtraInterlanguageLinkPrefixes'=>[], 'InterlanguageLinkCodeMap'=>[], 'ExtraLanguageNames'=>[], 'ExtraLanguageCodes'=>['bh'=> 'bho', 'no'=> 'nb', 'simple'=> 'en',], 'DummyLanguageCodes'=>[], 'AllUnicodeFixes'=> false, 'LegacyEncoding'=> false, 'AmericanDates'=> false, 'TranslateNumerals'=> true, 'UseDatabaseMessages'=> true, 'MaxMsgCacheEntrySize'=> 10000, 'DisableLangConversion'=> false, 'DisableTitleConversion'=> false, 'DefaultLanguageVariant'=> false, 'UsePigLatinVariant'=> false, 'DisabledVariants'=>[], 'VariantArticlePath'=> false, 'UseXssLanguage'=> false, 'LoginLanguageSelector'=> false, 'ForceUIMsgAsContentMsg'=>[], 'RawHtmlMessages'=>[], 'Localtimezone'=> null, 'LocalTZoffset'=> null, 'OverrideUcfirstCharacters'=>[], 'MimeType'=> 'text/html', 'Html5Version'=> null, 'EditSubmitButtonLabelPublish'=> false, 'XhtmlNamespaces'=>[], 'SiteNotice'=> '', 'BrowserFormatDetection'=> 'telephone=no', 'SkinMetaTags'=>[], 'DefaultSkin'=> 'vector-2022', 'FallbackSkin'=> 'fallback', 'SkipSkins'=>[], 'DisableOutputCompression'=> false, 'FragmentMode'=>['html5', 'legacy',], 'ExternalInterwikiFragmentMode'=> 'legacy', 'FooterIcons'=>['copyright'=>['copyright'=>[],], 'poweredby'=>['mediawiki'=>['src'=> null, 'url'=> 'https:'alt'=> 'Powered by MediaWiki', 'lang'=> 'en',],],], 'UseCombinedLoginLink'=> false, 'Edititis'=> false, 'Send404Code'=> true, 'ShowRollbackEditCount'=> 10, 'EnableCanonicalServerLink'=> false, 'InterwikiLogoOverride'=>[], 'ResourceModules'=>[], 'ResourceModuleSkinStyles'=>[], 'ResourceLoaderSources'=>[], 'ResourceBasePath'=> null, 'ResourceLoaderMaxage'=>[], 'ResourceLoaderDebug'=> false, 'ResourceLoaderMaxQueryLength'=> false, 'ResourceLoaderValidateJS'=> true, 'ResourceLoaderEnableJSProfiler'=> false, 'ResourceLoaderStorageEnabled'=> true, 'ResourceLoaderStorageVersion'=> 1, 'ResourceLoaderEnableSourceMapLinks'=> true, 'AllowSiteCSSOnRestrictedPages'=> false, 'VueDevelopmentMode'=> false, 'CodexDevelopmentDir'=> null, 'MetaNamespace'=> false, 'MetaNamespaceTalk'=> false, 'CanonicalNamespaceNames'=>[-2=> 'Media', -1=> 'Special', 0=> '', 1=> 'Talk', 2=> 'User', 3=> 'User_talk', 4=> 'Project', 5=> 'Project_talk', 6=> 'File', 7=> 'File_talk', 8=> 'MediaWiki', 9=> 'MediaWiki_talk', 10=> 'Template', 11=> 'Template_talk', 12=> 'Help', 13=> 'Help_talk', 14=> 'Category', 15=> 'Category_talk',], 'ExtraNamespaces'=>[], 'ExtraGenderNamespaces'=>[], 'NamespaceAliases'=>[], 'LegalTitleChars'=> ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 'CapitalLinks' => true, 'CapitalLinkOverrides' => [ ], 'NamespacesWithSubpages' => [ 1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 7 => true, 8 => true, 9 => true, 10 => true, 11 => true, 12 => true, 13 => true, 15 => true, ], 'NamespacesWithoutAutoSummaries' => [ ], 'ContentNamespaces' => [ 0, ], 'ShortPagesNamespaceExclusions' => [ ], 'ExtraSignatureNamespaces' => [ ], 'InvalidRedirectTargets' => [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect', 'Mylog', ], 'DisableHardRedirects' => false, 'FixDoubleRedirects' => false, 'LocalInterwikis' => [ ], 'InterwikiExpiry' => 10800, 'InterwikiCache' => false, 'InterwikiScopes' => 3, 'InterwikiFallbackSite' => 'wiki', 'RedirectSources' => false, 'SiteTypes' => [ 'mediawiki' => 'MediaWiki\\Site\\MediaWikiSite', ], 'MaxTocLevel' => 999, 'MaxPPNodeCount' => 1000000, 'MaxTemplateDepth' => 100, 'MaxPPExpandDepth' => 100, 'UrlProtocols' => [ 'bitcoin:', 'ftp: 'ftps: 'geo:', 'git: 'gopher: 'http: 'https: 'irc: 'ircs: 'magnet:', 'mailto:', 'matrix:', 'mms: 'news:', 'nntp: 'redis: 'sftp: 'sip:', 'sips:', 'sms:', 'ssh: 'svn: 'tel:', 'telnet: 'urn:', 'wikipedia: 'worldwind: 'xmpp:', ' ], 'CleanSignatures' => true, 'AllowExternalImages' => false, 'AllowExternalImagesFrom' => '', 'EnableImageWhitelist' => false, 'TidyConfig' => [ ], 'ParsoidSettings' => [ 'useSelser' => true, ], 'ParsoidExperimentalParserFunctionOutput' => false, 'RawHtml' => false, 'ExternalLinkTarget' => false, 'NoFollowLinks' => true, 'NoFollowNsExceptions' => [ ], 'NoFollowDomainExceptions' => [ 'mediawiki.org', ], 'RegisterInternalExternals' => false, 'ExternalLinksIgnoreDomains' => [ ], 'AllowDisplayTitle' => true, 'RestrictDisplayTitle' => true, 'ExpensiveParserFunctionLimit' => 100, 'PreprocessorCacheThreshold' => 1000, 'EnableScaryTranscluding' => false, 'TranscludeCacheExpiry' => 3600, 'EnableMagicLinks' => [ 'ISBN' => false, 'PMID' => false, 'RFC' => false, ], 'ParserEnableUserLanguage' => false, 'ArticleCountMethod' => 'link', 'ActiveUserDays' => 30, 'LearnerEdits' => 10, 'LearnerMemberSince' => 4, 'ExperiencedUserEdits' => 500, 'ExperiencedUserMemberSince' => 30, 'ManualRevertSearchRadius' => 15, 'RevertedTagMaxDepth' => 15, 'CentralIdLookupProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\CentralId\\LocalIdLookup', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', 'HideUserUtils', ], ], ], 'CentralIdLookupProvider' => 'local', 'UserRegistrationProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'ConnectionProvider', ], ], ], 'PasswordPolicy' => [ 'policies' => [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true, ], 'PasswordCannotBeSubstringInUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'PasswordCannotMatchDefaults' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true, ], 'PasswordNotInCommonList' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], ], ], 'checks' => [ 'MinimalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimalPasswordLength', ], 'MinimumPasswordLengthToLogin' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimumPasswordLengthToLogin', ], 'PasswordCannotBeSubstringInUsername' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotBeSubstringInUsername', ], 'PasswordCannotMatchDefaults' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotMatchDefaults', ], 'MaximalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMaximalPasswordLength', ], 'PasswordNotInCommonList' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordNotInCommonList', ], ], ], 'AuthManagerConfig' => null, 'AuthManagerAutoConfig' => [ 'preauth' => [ 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider', 'services' => [ 'ConnectionProvider', 'UserFactory', ], 'sort' => 0, ], ], 'primaryauth' => [ 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', 'UserOptionsLookup', ], 'args' => [ [ 'authoritative' => false, ], ], 'sort' => 0, ], 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'args' => [ [ 'authoritative' => true, ], ], 'sort' => 100, ], ], 'secondaryauth' => [ 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider', 'sort' => 100, ], 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'sort' => 200, ], ], ], 'RememberMe' => 'choose', 'ReauthenticateTime' => [ 'default' => 3600, ], '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, 'autocreateaccount' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'viewmyprivateinfo' => true, 'editmyprivateinfo' => true, 'editmyoptions' => true, ], 'user' => [ 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'movefile' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'minoredit' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, 'editmyuserjsredirect' => true, 'sendemail' => true, 'applychangetags' => true, 'changetags' => true, 'viewmywatchlist' => true, 'editmywatchlist' => true, 'createwithcontentmodel' => true, 'logout' => true, ], 'autoconfirmed' => [ 'autoconfirmed' => true, 'editsemiprotected' => true, ], 'bot' => [ 'bot' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'nominornewtalk' => true, 'autopatrol' => true, 'suppressredirect' => true, 'apihighlimits' => true, ], 'sysop' => [ 'block' => true, 'createaccount' => true, 'createpreviouslyrenamedaccount' => true, 'delete' => true, 'bigdelete' => true, 'deletedhistory' => true, 'deletedtext' => true, 'undelete' => true, 'editcontentmodel' => true, 'editinterface' => true, 'editsitejson' => true, 'edituserjson' => true, 'import' => true, 'importupload' => true, 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'patrol' => true, 'autopatrol' => true, 'protect' => true, 'editprotected' => true, 'rollback' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'unwatchedpages' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'blockemail' => true, 'markbotedits' => true, 'apihighlimits' => true, 'browsearchive' => true, 'noratelimit' => true, 'movefile' => true, 'unblockself' => true, 'suppressredirect' => true, 'mergehistory' => true, 'managechangetags' => true, 'deletechangetags' => true, ], 'interface-admin' => [ 'editinterface' => true, 'editsitecss' => true, 'editsitejson' => true, 'editsitejs' => true, 'editusercss' => true, 'edituserjson' => true, 'edituserjs' => true, ], 'bureaucrat' => [ 'userrights' => true, 'noratelimit' => true, 'renameuser' => true, ], 'suppress' => [ 'hideuser' => true, 'suppressrevision' => true, 'viewsuppressed' => true, 'suppressionlog' => true, 'deleterevision' => true, 'deletelogentry' => true, ], ], 'PrivilegedGroups' => [ 'bureaucrat', 'interface-admin', 'suppress', 'sysop', ], 'RevokePermissions' => [ ], 'GroupInheritsPermissions' => [ ], 'ImplicitGroups' => [ '*', 'user', 'autoconfirmed', ], 'GroupsAddToSelf' => [ ], 'GroupsRemoveFromSelf' => [ ], 'RestrictedGroups' => [ ], 'UserRequirementsPrivateConditions' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], 'NonincludableNamespaces' => [ ], 'AutoConfirmAge' => 0, 'AutoConfirmCount' => 0, 'Autopromote' => [ 'autoconfirmed' => [ '&', [ 1, null, ], [ 2, null, ], ], ], 'AutopromoteOnce' => [ 'onEdit' => [ ], ], 'AutopromoteOnceLogInRC' => true, 'AutopromoteOnceRCExcludedGroups' => [ ], 'AddGroups' => [ ], 'RemoveGroups' => [ ], 'AvailableRights' => [ ], 'ImplicitRights' => [ ], 'DeleteRevisionsLimit' => 0, 'DeleteRevisionsBatchSize' => 1000, 'HideUserContribLimit' => 1000, 'AccountCreationThrottle' => [ [ 'count' => 0, 'seconds' => 86400, ], ], 'TempAccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 600, ], [ 'count' => 6, 'seconds' => 86400, ], ], 'TempAccountNameAcquisitionThrottle' => [ [ 'count' => 60, 'seconds' => 86400, ], ], 'SpamRegex' => [ ], 'SummarySpamRegex' => [ ], 'EnableDnsBlacklist' => false, 'DnsBlacklistUrls' => [ ], 'ProxyList' => [ ], 'ProxyWhitelist' => [ ], 'SoftBlockRanges' => [ ], 'ApplyIpBlocksToXff' => false, 'RateLimits' => [ 'edit' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], 'user' => [ 90, 60, ], ], 'move' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], 'upload' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'rollback' => [ 'user' => [ 10, 60, ], 'newbie' => [ 5, 120, ], ], 'mailpassword' => [ 'ip' => [ 5, 3600, ], ], 'sendemail' => [ 'ip' => [ 5, 86400, ], 'newbie' => [ 5, 86400, ], 'user' => [ 20, 86400, ], ], 'changeemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'confirmemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'purge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'linkpurge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'renderfile' => [ 'ip' => [ 700, 30, ], 'user' => [ 700, 30, ], ], 'renderfile-nonstandard' => [ 'ip' => [ 70, 30, ], 'user' => [ 70, 30, ], ], 'stashedit' => [ 'ip' => [ 30, 60, ], 'newbie' => [ 30, 60, ], ], 'stashbasehtml' => [ 'ip' => [ 5, 60, ], 'newbie' => [ 5, 60, ], ], 'changetags' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'editcontentmodel' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], ], 'RateLimitsExcludedIPs' => [ ], 'PutIPinRC' => true, 'QueryPageDefaultLimit' => 50, 'ExternalQuerySources' => [ ], 'PasswordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300, ], [ 'count' => 150, 'seconds' => 172800, ], ], 'GrantPermissions' => [ 'basic' => [ 'autocreateaccount' => true, 'autoconfirmed' => true, 'autopatrol' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'nominornewtalk' => true, 'patrolmarks' => true, 'read' => true, 'unwatchedpages' => true, ], 'highvolume' => [ 'bot' => true, 'apihighlimits' => true, 'noratelimit' => true, 'markbotedits' => true, ], 'import' => [ 'import' => true, 'importupload' => true, ], 'editpage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, 'editusercss' => true, 'edituserjs' => true, 'editsitecss' => true, 'editsitejs' => true, ], 'createeditmovepage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'createpage' => true, 'createtalk' => true, 'delete-redirect' => true, 'move' => true, 'move-rootuserpages' => true, 'move-subpages' => true, 'move-categorypages' => true, 'suppressredirect' => true, ], 'uploadfile' => [ 'upload' => true, 'reupload-own' => true, ], 'uploadeditmovefile' => [ 'upload' => true, 'reupload-own' => true, 'reupload' => true, 'reupload-shared' => true, 'upload_by_url' => true, 'movefile' => true, 'suppressredirect' => true, ], 'patrol' => [ 'patrol' => true, ], 'rollback' => [ 'rollback' => true, ], 'blockusers' => [ 'block' => true, 'blockemail' => true, ], 'viewdeleted' => [ 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, ], 'viewrestrictedlogs' => [ 'suppressionlog' => true, ], 'delete' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, 'delete' => true, 'bigdelete' => true, 'deletelogentry' => true, 'deleterevision' => true, 'undelete' => true, ], 'oversight' => [ 'suppressrevision' => true, 'viewsuppressed' => true, ], 'protect' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], 'managesessions' => [ 'logout' => true, ], ], 'GrantPermissionGroups' => [ 'basic' => 'hidden', 'editpage' => 'page-interaction', 'createeditmovepage' => 'page-interaction', 'editprotected' => 'page-interaction', 'patrol' => 'page-interaction', 'uploadfile' => 'file-interaction', 'uploadeditmovefile' => 'file-interaction', 'sendemail' => 'email', 'viewmywatchlist' => 'watchlist-interaction', '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', 'managesessions' => '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, 'BotPasswordsLimit' => 100, '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, 'CSPUseReportURIDirective' => false, 'CSPFalsePositiveUrls' => [ 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'chrome-extension' => true, ], 'AllowCrossOrigin' => false, 'RestAllowCrossOriginCookieAuth' => false, 'SessionSecret' => false, 'CookieExpiration' => 2592000, 'ExtendedLoginCookieExpiration' => 15552000, 'SessionCookieJwtExpiration' => 14400, 'CookieDomain' => '', 'CookiePath' => '/', 'CookieSecure' => 'detect', 'CookiePrefix' => false, 'CookieHttpOnly' => true, 'CookieSameSite' => null, 'CacheVaryCookies' => [ ], 'SessionName' => false, 'CookieSetOnAutoblock' => true, 'CookieSetOnIpBlock' => true, 'DebugLogFile' => '', 'DebugLogPrefix' => '', 'DebugRedirects' => false, 'DebugRawPage' => false, 'DebugComments' => false, 'DebugDumpSql' => false, 'TrxProfilerLimits' => [ 'GET' => [ 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'POST-nonwrite' => [ 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 10000, 'maxAffected' => 1000, 'masterConns' => 0, 'writes' => 0, ], 'PostSend-POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'JobRunner' => [ 'readQueryTime' => 30, 'writeQueryTime' => 5, 'readQueryRows' => 100000, 'maxAffected' => 500, ], 'Maintenance' => [ 'writeQueryTime' => 5, 'maxAffected' => 1000, ], ], 'DebugLogGroups' => [ ], 'MWLoggerDefaultSpi' => [ 'class' => 'MediaWiki\\Logger\\LegacySpi', ], 'ShowDebug' => false, 'SpecialVersionShowHooks' => false, 'ShowExceptionDetails' => false, 'LogExceptionBacktrace' => true, 'PropagateErrors' => true, 'ShowHostnames' => false, 'OverrideHostname' => false, 'DevelopmentWarnings' => false, 'DeprecationReleaseLimit' => false, 'Profiler' => [ ], 'StatsdServer' => false, 'StatsdMetricPrefix' => 'MediaWiki', 'StatsTarget' => null, 'StatsFormat' => null, 'StatsPrefix' => 'mediawiki', 'OpenTelemetryConfig' => null, 'PageInfoTransclusionLimit' => 50, 'EnableJavaScriptTest' => false, 'CachePrefix' => false, 'DebugToolbar' => false, 'ApiClientErrorSampleRate' => 1.0, 'DisableTextSearch' => false, 'AdvancedSearchHighlighting' => false, 'SearchHighlightBoundaries' => '[\\p{Z}\\p{P}\\p{C}]', 'OpenSearchTemplates' => [ 'application/x-suggestions+json' => false, 'application/x-suggestions+xml' => false, ], 'OpenSearchDefaultLimit' => 10, 'OpenSearchDescriptionLength' => 100, 'SearchSuggestCacheExpiry' => 1200, 'DisableSearchUpdate' => false, 'NamespacesToBeSearchedDefault' => [ true, ], 'DisableInternalSearch' => false, 'SearchForwardUrl' => null, 'SitemapNamespaces' => false, 'SitemapNamespacesPriorities' => false, 'SitemapApiConfig' => [ ], 'SpecialSearchFormOptions' => [ ], 'SearchMatchRedirectPreference' => false, 'SearchRunSuggestedQuery' => true, 'Diff3' => '/usr/bin/diff3', 'Diff' => '/usr/bin/diff', 'PreviewOnOpenNamespaces' => [ 14 => true, ], 'UniversalEditButton' => true, 'UseAutomaticEditSummaries' => true, 'CommandLineDarkBg' => false, 'ReadOnly' => null, 'ReadOnlyWatchedItemStore' => false, 'ReadOnlyFile' => false, 'UpgradeKey' => false, 'GitBin' => '/usr/bin/git', 'GitRepositoryViewers' => [ 'https: 'ssh: 'https: 'git@github\\.com:(.*?)(\\.git)?' => 'https: ], 'InstallerInitialPages' => [ [ 'titlemsg' => 'mainpage', 'text' => '{{subst:int:mainpagetext}}{{subst:int:mainpagedocfooter}}', ], ], 'RCMaxAge' => 7776000, 'WatchersMaxAge' => 15552000, 'UnwatchedPageSecret' => 1, 'RCFilterByAge' => false, 'RCLinkLimits' => [ 50, 100, 250, 500, ], 'RCLinkDays' => [ 1, 3, 7, 14, 30, ], 'RCFeeds' => [ ], 'RCWatchCategoryMembership' => false, 'UseRCPatrol' => true, 'StructuredChangeFiltersLiveUpdatePollingRate' => 3, 'UseNPPatrol' => true, 'UseFilePatrol' => true, 'Feed' => true, 'FeedLimit' => 50, 'FeedCacheTimeout' => 60, 'FeedDiffCutoff' => 32768, 'OverrideSiteFeed' => [ ], 'FeedClasses' => [ 'rss' => 'MediaWiki\\Feed\\RSSFeed', 'atom' => 'MediaWiki\\Feed\\AtomFeed', ], 'AdvertisedFeedTypes' => [ 'atom', ], 'RCShowWatchingUsers' => false, 'RCShowChangedSize' => true, 'RCChangedSizeThreshold' => 500, 'ShowUpdatedMarker' => true, 'DisableAnonTalk' => false, 'UseTagFilter' => true, 'SoftwareTags' => [ 'mw-contentmodelchange' => true, 'mw-new-redirect' => true, 'mw-removed-redirect' => true, 'mw-changed-redirect-target' => true, 'mw-blank' => true, 'mw-replace' => true, 'mw-recreated' => true, 'mw-rollback' => true, 'mw-undo' => true, 'mw-manual-revert' => true, 'mw-reverted' => true, 'mw-server-side-upload' => true, 'mw-ipblock-appeal' => true, 'mw-edited-other-users-js' => true, 'mw-edited-other-users-css' => true, ], 'UnwatchedPageThreshold' => false, 'RecentChangesFlags' => [ 'newpage' => [ 'letter' => 'newpageletter', 'title' => 'recentchanges-label-newpage', 'legend' => 'recentchanges-legend-newpage', 'grouping' => 'any', ], 'minor' => [ 'letter' => 'minoreditletter', 'title' => 'recentchanges-label-minor', 'legend' => 'recentchanges-legend-minor', 'class' => 'minoredit', 'grouping' => 'all', ], 'bot' => [ 'letter' => 'boteditletter', 'title' => 'recentchanges-label-bot', 'legend' => 'recentchanges-legend-bot', 'class' => 'botedit', 'grouping' => 'all', ], 'unpatrolled' => [ 'letter' => 'unpatrolledletter', 'title' => 'recentchanges-label-unpatrolled', 'legend' => 'recentchanges-legend-unpatrolled', 'grouping' => 'any', ], ], 'WatchlistExpiry' => false, 'EnableWatchstarPopover' => false, 'EnableWatchlistLabels' => false, 'WatchlistLabelsMaxPerUser' => 100, 'WatchlistPurgeRate' => 0.1, 'WatchlistExpiryMaxDuration' => '1 year', 'EnableChangesListQueryPartitioning' => false, 'RightsPage' => null, 'RightsUrl' => null, 'RightsText' => null, 'RightsIcon' => null, 'UseCopyrightUpload' => false, 'MaxCredits' => 0, 'ShowCreditsIfMax' => true, 'ImportSources' => [ ], 'ImportTargetNamespace' => null, 'ExportAllowHistory' => true, 'ExportMaxHistory' => 0, 'ExportAllowListContributors' => false, 'ExportMaxLinkDepth' => 0, 'ExportFromNamespaces' => false, 'ExportAllowAll' => false, 'ExportPagelistLimit' => 5000, 'XmlDumpSchemaVersion' => '0.11', 'WikiFarmSettingsDirectory' => null, 'WikiFarmSettingsExtension' => 'yaml', 'ExtensionFunctions' => [ ], 'ExtensionMessagesFiles' => [ ], 'MessagesDirs' => [ ], 'TranslationAliasesDirs' => [ ], 'ExtensionEntryPointListFiles' => [ ], 'EnableParserLimitReporting' => true, 'ValidSkinNames' => [ ], 'SpecialPages' => [ ], 'ExtensionCredits' => [ ], 'Hooks' => [ ], 'ServiceWiringFiles' => [ ], 'JobClasses' => [ 'deletePage' => 'MediaWiki\\Page\\DeletePageJob', 'refreshLinks' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'deleteLinks' => 'MediaWiki\\Page\\DeleteLinksJob', 'htmlCacheUpdate' => 'MediaWiki\\JobQueue\\Jobs\\HTMLCacheUpdateJob', 'sendMail' => [ 'class' => 'MediaWiki\\Mail\\EmaillingJob', 'services' => [ 'Emailer', ], ], 'enotifNotify' => [ 'class' => 'MediaWiki\\RecentChanges\\RecentChangeNotifyJob', 'services' => [ 'RecentChangeLookup', ], ], 'fixDoubleRedirect' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\DoubleRedirectJob', 'services' => [ 'RevisionLookup', 'MagicWordFactory', 'WikiPageFactory', ], 'needsPage' => true, ], 'AssembleUploadChunks' => 'MediaWiki\\JobQueue\\Jobs\\AssembleUploadChunksJob', 'PublishStashedFile' => 'MediaWiki\\JobQueue\\Jobs\\PublishStashedFileJob', 'ThumbnailRender' => 'MediaWiki\\JobQueue\\Jobs\\ThumbnailRenderJob', 'UploadFromUrl' => 'MediaWiki\\JobQueue\\Jobs\\UploadFromUrlJob', 'recentChangesUpdate' => 'MediaWiki\\RecentChanges\\RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'refreshLinksDynamic' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'activityUpdateJob' => 'MediaWiki\\Watchlist\\ActivityUpdateJob', 'categoryMembershipChange' => [ 'class' => 'MediaWiki\\RecentChanges\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => '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', 'Promise-Non-Write-API-Action', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], 'RestModuleOverrides' => [ ], '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, 'GenerateReqIDFormat' => 'rand24', '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, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, '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', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EmailConfirmationBanner' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ExternalLinksDomainGaps' => 'object', 'ContentHandlers' => 'object', 'NamespaceContentModels' => 'object', 'TextModelsToParse' => 'array', 'ExternalStores' => 'array', 'ExternalServers' => 'object', 'DefaultExternalStore' => [ 'array', 'boolean', ], 'RevisionCacheExpiry' => 'integer', 'PageLanguageUseDB' => 'boolean', 'DiffEngine' => [ 'string', 'null', ], 'ExternalDiffEngine' => [ 'string', 'boolean', ], 'Wikidiff2Options' => 'object', 'RequestTimeLimit' => [ 'integer', 'null', ], 'CriticalSectionTimeLimit' => 'number', 'PoolCounterConf' => [ 'object', 'null', ], 'PoolCountClientConf' => 'object', 'MaxUserDBWriteDuration' => [ 'integer', 'boolean', ], 'MaxJobDBWriteDuration' => [ 'integer', 'boolean', ], 'MultiShardSiteStats' => 'boolean', 'ObjectCaches' => 'object', 'WANObjectCache' => 'object', 'MicroStashType' => [ 'string', 'integer', ], 'ParsoidCacheConfig' => 'object', 'ParsoidSelectiveUpdateSampleRate' => 'integer', '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', 'NamespacesWithoutAutoSummaries' => 'array', 'ContentNamespaces' => 'array', 'ShortPagesNamespaceExclusions' => 'array', 'ExtraSignatureNamespaces' => 'array', 'InvalidRedirectTargets' => 'array', 'LocalInterwikis' => 'array', 'InterwikiCache' => [ 'boolean', 'object', ], 'SiteTypes' => 'object', 'UrlProtocols' => 'array', 'TidyConfig' => 'object', 'ParsoidSettings' => 'object', 'ParsoidExperimentalParserFunctionOutput' => 'boolean', 'NoFollowNsExceptions' => 'array', 'NoFollowDomainExceptions' => 'array', 'ExternalLinksIgnoreDomains' => 'array', 'EnableMagicLinks' => 'object', 'ManualRevertSearchRadius' => 'integer', 'RevertedTagMaxDepth' => 'integer', 'CentralIdLookupProviders' => 'object', 'CentralIdLookupProvider' => 'string', 'UserRegistrationProviders' => 'object', 'PasswordPolicy' => 'object', 'AuthManagerConfig' => [ 'object', 'null', ], 'AuthManagerAutoConfig' => 'object', 'RememberMe' => 'string', 'ReauthenticateTime' => 'object', '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', 'UserRequirementsPrivateConditions' => 'array', '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', ], 'BotPasswordsLimit' => 'integer', 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPUseReportURIDirective' => [ 'boolean', 'object', ], 'CSPFalsePositiveUrls' => 'object', 'AllowCrossOrigin' => 'boolean', 'RestAllowCrossOriginCookieAuth' => 'boolean', 'CookieSameSite' => [ 'string', 'null', ], 'CacheVaryCookies' => 'array', 'TrxProfilerLimits' => 'object', 'DebugLogGroups' => 'object', 'MWLoggerDefaultSpi' => 'object', 'Profiler' => 'object', 'StatsTarget' => [ 'string', 'null', ], 'StatsFormat' => [ 'string', 'null', ], 'StatsPrefix' => 'string', 'OpenTelemetryConfig' => [ 'object', 'null', ], 'OpenSearchTemplates' => 'object', 'NamespacesToBeSearchedDefault' => 'object', 'SitemapNamespaces' => [ 'boolean', 'array', ], 'SitemapNamespacesPriorities' => [ 'boolean', 'object', ], 'SitemapApiConfig' => 'object', 'SpecialSearchFormOptions' => 'object', 'SearchMatchRedirectPreference' => 'boolean', 'SearchRunSuggestedQuery' => 'boolean', 'PreviewOnOpenNamespaces' => 'object', 'ReadOnlyWatchedItemStore' => 'boolean', 'GitRepositoryViewers' => 'object', 'InstallerInitialPages' => 'array', 'RCLinkLimits' => 'array', 'RCLinkDays' => 'array', 'RCFeeds' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchstarPopover' => 'boolean', 'EnableWatchlistLabels' => 'boolean', 'WatchlistLabelsMaxPerUser' => 'integer', 'WatchlistPurgeRate' => 'number', 'WatchlistExpiryMaxDuration' => [ 'string', 'null', ], 'EnableChangesListQueryPartitioning' => 'boolean', 'ImportSources' => 'object', 'ExtensionFunctions' => 'array', 'ExtensionMessagesFiles' => 'object', 'MessagesDirs' => 'object', 'TranslationAliasesDirs' => 'object', 'ExtensionEntryPointListFiles' => 'object', 'ValidSkinNames' => 'object', 'SpecialPages' => 'object', 'ExtensionCredits' => 'object', 'Hooks' => 'object', 'ServiceWiringFiles' => 'array', 'JobClasses' => 'object', 'JobTypesExcludedFromDefaultQueue' => 'array', 'JobBackoffThrottling' => 'object', 'JobTypeConf' => 'object', 'SpecialPageCacheUpdates' => 'object', 'PagePropLinkInvalidations' => 'object', 'TempCategoryCollations' => 'array', 'SortedCategories' => 'boolean', 'TrackingCategories' => 'array', 'LogTypes' => 'array', 'LogRestrictions' => 'object', 'FilterLogTypes' => 'object', 'LogNames' => 'object', 'LogHeaders' => 'object', 'LogActions' => 'object', 'LogActionsHandlers' => 'object', 'ActionFilteredLogs' => 'object', 'RangeContributionsCIDRLimit' => 'object', 'Actions' => 'object', 'NamespaceRobotPolicies' => 'object', 'ArticleRobotPolicies' => 'object', 'ExemptFromUserRobotsControl' => [ 'array', 'null', ], 'APIModules' => 'object', 'APIFormatModules' => 'object', 'APIMetaModules' => 'object', 'APIPropModules' => 'object', 'APIListModules' => 'object', 'APIUselessQueryPages' => 'array', 'CrossSiteAJAXdomains' => 'object', 'CrossSiteAJAXdomainExceptions' => 'object', 'AllowedCorsHeaders' => 'array', 'RestAPIAdditionalRouteFiles' => 'array', 'RestSandboxSpecs' => 'object', 'RestModuleOverrides' => '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', ], 'GenerateReqIDFormat' => 'string', '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', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => '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', 'RestModuleOverrides' => 'array_replace_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', ], 'file' => [ 'type' => 'string', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'mode' => [ 'type' => 'string', ], ], 'required' => [ 'mode', ], ], ], '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.', ],]
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.
Service for looking up page revisions.
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
getId( $wikiId=self::LOCAL)
Interface to a relational database.
Definition IDatabase.php:31
Manager of ILoadBalancer objects and, indirectly, IDatabase connections.
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.
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
getCacheKey()
Get the cache key used to store status.
if(count( $args)< 1) $job