MediaWiki master
WatchedItemStore.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Watchlist;
4
5use DateInterval;
6use InvalidArgumentException;
7use LogicException;
8use MediaWiki\Cache\LinkBatchFactory;
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( WatchedItem $item ) {
207 $user = $item->getUserIdentity();
208 $target = $item->getTarget();
209 $key = $this->getCacheKey( $user, $target );
210 $this->cache->set( $key, $item );
211 $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
212 }
213
214 private function uncache( UserIdentity $user, PageReference $target ) {
215 $this->cache->delete( $this->getCacheKey( $user, $target ) );
216 unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
217 }
218
219 private function uncacheTitle( PageReference $target ) {
220 if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
221 return;
222 }
223
224 foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
225 $this->cache->delete( $key );
226 }
227 }
228
229 private function uncacheUser( UserIdentity $user ) {
230 foreach ( $this->cacheIndex as $dbKeyArray ) {
231 foreach ( $dbKeyArray as $userArray ) {
232 if ( isset( $userArray[$user->getId()] ) ) {
233 $this->cache->delete( $userArray[$user->getId()] );
234 }
235 }
236 }
237
238 $pageSeenKey = $this->getPageSeenTimestampsKey( $user );
239 $this->latestUpdateCache->delete( $pageSeenKey );
240 $this->stash->delete( $pageSeenKey );
241 }
242
249 private function getCached( UserIdentity $user, PageReference $target ) {
250 return $this->cache->get( $this->getCacheKey( $user, $target ) );
251 }
252
260 private function modifyQueryBuilderForExpiry(
261 SelectQueryBuilder $queryBuilder,
262 IReadableDatabase $db
263 ) {
264 if ( $this->expiryEnabled ) {
265 $queryBuilder->where( $db->expr( 'we_expiry', '=', null )->or( 'we_expiry', '>', $db->timestamp() ) );
266 $queryBuilder->leftJoin( 'watchlist_expiry', null, 'wl_id = we_item' );
267 }
268 }
269
276 private function addLabelSummaryField(
277 SelectQueryBuilder $queryBuilder,
278 IReadableDatabase $db
279 ) {
280 $subquery = $db->newSelectQueryBuilder()
281 ->select( 'wlm_label' )
282 ->distinct()
283 ->from( 'watchlist_label_member' )
284 ->where( [ 'wlm_item=wl_id' ] )
285 ->buildGroupConcatField( ',' );
286 $queryBuilder->fields( [ 'wlm_label_summary' => $subquery ] );
287 }
288
299 public function clearUserWatchedItems( UserIdentity $user ): bool {
300 if ( $this->mustClearWatchedItemsUsingJobQueue( $user ) ) {
301 return false;
302 }
303
304 $dbw = $this->lbFactory->getPrimaryDatabase();
305
306 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
307 // First fetch the wl_ids.
308 $wlIds = $dbw->newSelectQueryBuilder()
309 ->select( 'wl_id' )
310 ->from( 'watchlist' )
311 ->where( [ 'wl_user' => $user->getId() ] )
312 ->caller( __METHOD__ )
313 ->fetchFieldValues();
314 if ( $wlIds ) {
315 // Delete rows from both the watchlist and watchlist_expiry tables.
316 $dbw->newDeleteQueryBuilder()
317 ->deleteFrom( 'watchlist' )
318 ->where( [ 'wl_id' => $wlIds ] )
319 ->caller( __METHOD__ )->execute();
320
321 if ( $this->expiryEnabled ) {
322 $dbw->newDeleteQueryBuilder()
323 ->deleteFrom( 'watchlist_expiry' )
324 ->where( [ 'we_item' => $wlIds ] )
325 ->caller( __METHOD__ )->execute();
326 }
327 if ( $this->labelsEnabled ) {
328 $dbw->newDeleteQueryBuilder()
329 ->deleteFrom( 'watchlist_label_member' )
330 ->where( [ 'wlm_item' => $wlIds ] )
331 ->caller( __METHOD__ )->execute();
332 }
333 }
334 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
335 $this->uncacheAllItemsForUser( $user );
336
337 return true;
338 }
339
340 public function mustClearWatchedItemsUsingJobQueue( UserIdentity $user ): bool {
341 return $this->countWatchedItems( $user ) > $this->updateRowsPerQuery;
342 }
343
344 private function uncacheAllItemsForUser( UserIdentity $user ) {
345 $userId = $user->getId();
346 foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
347 foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
348 if ( array_key_exists( $userId, $userIndex ) ) {
349 $this->cache->delete( $userIndex[$userId] );
350 unset( $this->cacheIndex[$ns][$dbKey][$userId] );
351 }
352 }
353 }
354
355 // Cleanup empty cache keys
356 foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
357 foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
358 if ( empty( $this->cacheIndex[$ns][$dbKey] ) ) {
359 unset( $this->cacheIndex[$ns][$dbKey] );
360 }
361 }
362 if ( empty( $this->cacheIndex[$ns] ) ) {
363 unset( $this->cacheIndex[$ns] );
364 }
365 }
366 }
367
376 $job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() );
377 $this->queueGroup->push( $job );
378 }
379
383 public function maybeEnqueueWatchlistExpiryJob(): void {
384 if ( !$this->expiryEnabled ) {
385 // No need to purge expired entries if there are none
386 return;
387 }
388
389 $max = mt_getrandmax();
390 if ( mt_rand( 0, $max ) < $max * $this->watchlistPurgeRate ) {
391 // The higher the watchlist purge rate, the more likely we are to enqueue a job.
392 $this->queueGroup->lazyPush( new WatchlistExpiryJob() );
393 }
394 }
395
400 public function getMaxId(): int {
401 return (int)$this->lbFactory->getReplicaDatabase()->newSelectQueryBuilder()
402 ->select( 'MAX(wl_id)' )
403 ->from( 'watchlist' )
404 ->caller( __METHOD__ )
405 ->fetchField();
406 }
407
413 public function countWatchedItems( UserIdentity $user ): int {
414 $dbr = $this->lbFactory->getReplicaDatabase();
415 $queryBuilder = $dbr->newSelectQueryBuilder()
416 ->select( 'COUNT(*)' )
417 ->from( 'watchlist' )
418 ->where( [ 'wl_user' => $user->getId() ] )
419 ->caller( __METHOD__ );
420
421 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
422
423 return (int)$queryBuilder->fetchField();
424 }
425
431 public function countWatchers( PageReference $target ): int {
432 $dbr = $this->lbFactory->getReplicaDatabase();
433 $queryBuilder = $dbr->newSelectQueryBuilder()
434 ->select( 'COUNT(*)' )
435 ->from( 'watchlist' )
436 ->where( [
437 'wl_namespace' => $target->getNamespace(),
438 'wl_title' => $target->getDBkey()
439 ] )
440 ->caller( __METHOD__ );
441
442 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
443
444 return (int)$queryBuilder->fetchField();
445 }
446
453 public function countVisitingWatchers( PageReference $target, $threshold ): int {
454 $dbr = $this->lbFactory->getReplicaDatabase();
455 $queryBuilder = $dbr->newSelectQueryBuilder()
456 ->select( 'COUNT(*)' )
457 ->from( 'watchlist' )
458 ->where( [
459 'wl_namespace' => $target->getNamespace(),
460 'wl_title' => $target->getDBkey(),
461 $dbr->expr( 'wl_notificationtimestamp', '>=', $dbr->timestamp( $threshold ) )
462 ->or( 'wl_notificationtimestamp', '=', null )
463 ] )
464 ->caller( __METHOD__ );
465
466 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
467
468 return (int)$queryBuilder->fetchField();
469 }
470
476 public function removeWatchBatchForUser( UserIdentity $user, array $titles ): bool {
477 if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
478 return false;
479 }
480 if ( !$titles ) {
481 return true;
482 }
483
484 $this->uncacheTitlesForUser( $user, $titles );
485
486 $dbw = $this->lbFactory->getPrimaryDatabase();
487 $ticket = count( $titles ) > $this->updateRowsPerQuery ?
488 $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) : null;
489 $affectedRows = 0;
490
491 $wlIds = $this->loadIdsForTargets( $dbw, $user, $titles );
492 foreach ( $this->batch( $wlIds ) as $ids ) {
493 // Delete rows from the watchlist and associated tables.
494 $dbw->newDeleteQueryBuilder()
495 ->deleteFrom( 'watchlist' )
496 ->where( [ 'wl_id' => $ids ] )
497 ->caller( __METHOD__ )->execute();
498 $affectedRows += $dbw->affectedRows();
499
500 if ( $this->expiryEnabled ) {
501 $dbw->newDeleteQueryBuilder()
502 ->deleteFrom( 'watchlist_expiry' )
503 ->where( [ 'we_item' => $ids ] )
504 ->caller( __METHOD__ )->execute();
505 }
506 if ( $this->labelsEnabled ) {
507 $dbw->newDeleteQueryBuilder()
508 ->deleteFrom( 'watchlist_label_member' )
509 ->where( [ 'wlm_item' => $ids ] )
510 ->caller( __METHOD__ )->execute();
511 }
512
513 if ( $ticket ) {
514 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
515 }
516 }
517
518 return (bool)$affectedRows;
519 }
520
528 public function countWatchersMultiple( array $targets, array $options = [] ): array {
529 $lb = $this->linkBatchFactory->newLinkBatch( $targets );
530 $dbr = $this->lbFactory->getReplicaDatabase();
531 $queryBuilder = $dbr->newSelectQueryBuilder();
532 $queryBuilder
533 ->select( [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ] )
534 ->from( 'watchlist' )
535 ->where( [ $lb->constructSet( 'wl', $dbr ) ] )
536 ->groupBy( [ 'wl_namespace', 'wl_title' ] )
537 ->caller( __METHOD__ );
538
539 if ( array_key_exists( 'minimumWatchers', $options ) ) {
540 $queryBuilder->having( 'COUNT(*) >= ' . (int)$options['minimumWatchers'] );
541 }
542
543 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
544
545 $res = $queryBuilder->fetchResultSet();
546
547 $watchCounts = [];
548 foreach ( $targets as $linkTarget ) {
549 $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
550 }
551
552 foreach ( $res as $row ) {
553 $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
554 }
555
556 return $watchCounts;
557 }
558
568 array $targetsWithVisitThresholds,
569 $minimumWatchers = null
570 ): array {
571 if ( $targetsWithVisitThresholds === [] ) {
572 // No titles requested => no results returned
573 return [];
574 }
575
576 $dbr = $this->lbFactory->getReplicaDatabase();
577 $queryBuilder = $dbr->newSelectQueryBuilder()
578 ->select( [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ] )
579 ->from( 'watchlist' )
580 ->where( [ $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds ) ] )
581 ->groupBy( [ 'wl_namespace', 'wl_title' ] )
582 ->caller( __METHOD__ );
583 if ( $minimumWatchers !== null ) {
584 $queryBuilder->having( 'COUNT(*) >= ' . (int)$minimumWatchers );
585 }
586 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbr );
587
588 $res = $queryBuilder->fetchResultSet();
589
590 $watcherCounts = [];
591 foreach ( $targetsWithVisitThresholds as [ $target ] ) {
592 $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0;
593 }
594
595 foreach ( $res as $row ) {
596 $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
597 }
598
599 return $watcherCounts;
600 }
601
610 private function getVisitingWatchersCondition(
611 IReadableDatabase $db,
612 array $targetsWithVisitThresholds
613 ): string {
614 $missingTargets = [];
615 $namespaceConds = [];
616 foreach ( $targetsWithVisitThresholds as [ $target, $threshold ] ) {
617 if ( $threshold === null ) {
618 $missingTargets[] = $target;
619 continue;
620 }
622 $namespaceConds[$target->getNamespace()][] = $db->expr( 'wl_title', '=', $target->getDBkey() )
623 ->andExpr(
624 $db->expr( 'wl_notificationtimestamp', '>=', $db->timestamp( $threshold ) )
625 ->or( 'wl_notificationtimestamp', '=', null )
626 );
627 }
628
629 $conds = [];
630 foreach ( $namespaceConds as $namespace => $pageConds ) {
631 $conds[] = $db->makeList( [
632 'wl_namespace = ' . $namespace,
633 '(' . $db->makeList( $pageConds, LIST_OR ) . ')'
634 ], LIST_AND );
635 }
636
637 if ( $missingTargets ) {
638 $lb = $this->linkBatchFactory->newLinkBatch( $missingTargets );
639 $conds[] = $lb->constructSet( 'wl', $db );
640 }
641
642 return $db->makeList( $conds, LIST_OR );
643 }
644
651 public function getWatchedItem( UserIdentity $user, PageReference $target ) {
652 if ( !$user->isRegistered() ) {
653 return false;
654 }
655
656 $cached = $this->getCached( $user, $target );
657 if ( $cached && !$cached->isExpired() ) {
658 return $cached;
659 }
660 return $this->loadWatchedItem( $user, $target );
661 }
662
669 public function loadWatchedItem( UserIdentity $user, PageReference $target ) {
670 $item = $this->loadWatchedItemsBatch( $user, [ $target ] );
671 return $item ? $item[0] : false;
672 }
673
680 public function loadWatchedItemsBatch( UserIdentity $user, array $targets ) {
681 // Only registered user can have a watchlist
682 if ( !$user->isRegistered() ) {
683 return [];
684 }
685
686 $dbr = $this->lbFactory->getReplicaDatabase();
687
688 $rows = $this->fetchWatchedItemRows(
689 $dbr,
690 $user,
691 $targets,
692 [],
693 );
694
695 if ( $this->labelsEnabled && $rows->numRows() ) {
696 $labels = $this->labelStore->loadAllForUser( $user );
697 } else {
698 $labels = [];
699 }
700
701 $items = [];
702 foreach ( $rows as $row ) {
703 $item = $this->getWatchedItemFromRow( $user, $row, $labels );
704 $this->cache( $item );
705 $items[] = $item;
706 }
707
708 return $items;
709 }
710
722 public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ): array {
723 $options += [ 'forWrite' => false, 'sort' => self::SORT_ASC ];
724 if ( $options['forWrite'] ) {
725 $db = $this->lbFactory->getPrimaryDatabase();
726 } else {
727 $db = $this->lbFactory->getReplicaDatabase();
728 }
729
730 if ( $options['sort'] == self::SORT_ASC ) {
731 $orderBy = [ 'wl_namespace', 'wl_title' ];
732 } else {
733 $orderBy = [ 'wl_namespace DESC', 'wl_title DESC' ];
734 }
735 return $this->fetchWatchedItems( $db, $user, $options, $orderBy );
736 }
737
749 private function fetchWatchedItems(
750 IDatabase $db, UserIdentity $user, array $options, array $orderBy, array $extraConditions = []
751 ): array {
752 $fetchOptions = [];
753 $fetchOptions['orderBy'] = $orderBy;
754 if ( isset( $options['limit'] ) ) {
755 $fetchOptions['limit'] = $options['limit'];
756 }
757 if ( isset( $options['offsetConds'] ) ) {
758 $offsetConds = is_array( $options['offsetConds'] )
759 ? $options['offsetConds'] :
760 [ $options['offsetConds'] ];
761 $extraConditions = array_merge( $extraConditions, $offsetConds );
762 }
763 if ( isset( $options['namespaces'] ) ) {
764 $extraConditions['wl_namespace'] = $options['namespaces'];
765 }
766 $fetchOptions['extraConds'] = $extraConditions;
767 $res = $this->fetchWatchedItemRows( $db, $user, null, $fetchOptions );
768
769 // Load label names
770 if ( $this->labelsEnabled && $res->numRows() ) {
771 $labels = $this->labelStore->loadAllForUser( $user );
772 } else {
773 $labels = [];
774 }
775
776 $watchedItems = [];
777 foreach ( $res as $row ) {
778 $watchedItems[] = $this->getWatchedItemFromRow( $user, $row, $labels );
779 }
780 return $watchedItems;
781 }
782
790 private function getWatchedItemFromRow(
791 UserIdentity $user,
792 stdClass $row,
793 array $labelsForUser
794 ): WatchedItem {
795 $target = PageReferenceValue::localReference( (int)$row->wl_namespace, $row->wl_title );
796 if ( ( $row->wlm_label_summary ?? '' ) !== '' ) {
797 $labelIds = explode( ',', $row->wlm_label_summary );
798 $labels = array_intersect_key( $labelsForUser, array_fill_keys( $labelIds, true ) );
799 } else {
800 $labels = [];
801 }
802 return new WatchedItem(
803 $user,
804 $target,
805 $this->getLatestNotificationTimestamp(
806 $row->wl_notificationtimestamp, $user, $target ),
807 wfTimestampOrNull( TS::ISO_8601, $row->we_expiry ?? null ),
808 array_values( $labels )
809 );
810 }
811
826 private function fetchWatchedItemRows(
827 IReadableDatabase $db,
828 UserIdentity $user,
829 $target = null,
830 array $options = [],
831 ) {
832 $fieldNames = [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ];
833 if ( $this->expiryEnabled ) {
834 $fieldNames[] = 'we_expiry';
835 }
836 $queryBuilder = $db->newSelectQueryBuilder()
837 ->select( $fieldNames )
838 ->from( 'watchlist' )
839 ->where( [ 'wl_user' => $user->getId() ] )
840 ->caller( __METHOD__ );
841 if ( $target ) {
842 $queryBuilder->where( $this->getTargetsCond( $target ) );
843 }
844 $this->modifyQueryBuilderForExpiry( $queryBuilder, $db );
845 if ( $this->labelsEnabled ) {
846 $this->addLabelSummaryField( $queryBuilder, $db );
847 }
848
849 if ( array_key_exists( 'orderBy', $options ) && is_array( $options['orderBy'] ) ) {
850 $queryBuilder->orderBy( $options['orderBy'] );
851 }
852 if ( array_key_exists( 'extraConds', $options ) && is_array( $options['extraConds'] ) ) {
853 $queryBuilder->where( $options['extraConds'] );
854 }
855 if ( array_key_exists( 'limit', $options ) && ( intval( $options['limit'] ) > 0 ) ) {
856 $queryBuilder->limit( $options['limit'] );
857 }
858
859 return $queryBuilder->fetchResultSet();
860 }
861
868 public function isWatched( UserIdentity $user, PageReference $target ): bool {
869 return (bool)$this->getWatchedItem( $user, $target );
870 }
871
879 public function isTempWatched( UserIdentity $user, PageReference $target ): bool {
880 $item = $this->getWatchedItem( $user, $target );
881 return $item && $item->getExpiry();
882 }
883
891 public function getNotificationTimestampsBatch( UserIdentity $user, array $targets ): array {
892 $timestamps = [];
893 foreach ( $targets as $target ) {
894 $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
895 }
896
897 if ( !$user->isRegistered() ) {
898 return $timestamps;
899 }
900
901 $targetsToLoad = [];
902 foreach ( $targets as $target ) {
903 $cachedItem = $this->getCached( $user, $target );
904 if ( $cachedItem ) {
905 $timestamps[$target->getNamespace()][$target->getDBkey()] =
906 $cachedItem->getNotificationTimestamp();
907 } else {
908 $targetsToLoad[] = $target;
909 }
910 }
911
912 if ( !$targetsToLoad ) {
913 return $timestamps;
914 }
915
916 $dbr = $this->lbFactory->getReplicaDatabase();
917
918 $res = $dbr->newSelectQueryBuilder()
919 ->select( [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ] )
920 ->from( 'watchlist' )
921 ->where( [
922 $this->getTargetsCond( $targetsToLoad ),
923 'wl_user' => $user->getId(),
924 ] )
925 ->caller( __METHOD__ )
926 ->fetchResultSet();
927
928 foreach ( $res as $row ) {
929 $target = PageReferenceValue::localReference( (int)$row->wl_namespace, $row->wl_title );
930 $timestamps[$row->wl_namespace][$row->wl_title] =
931 $this->getLatestNotificationTimestamp(
932 $row->wl_notificationtimestamp, $user, $target );
933 }
934
935 return $timestamps;
936 }
937
946 public function addWatch( UserIdentity $user, PageReference $target, ?string $expiry = null ) {
947 $this->addWatchBatchForUser( $user, [ $target ], $expiry );
948
949 if ( $this->expiryEnabled ) {
950 // When re-watching a page with a null $expiry, any existing expiry is left unchanged.
951 // However we must re-fetch the preexisting expiry or else the cached WatchedItem will
952 // incorrectly have a null expiry. Note that loadWatchedItem() does the caching.
953 // See T259379
954 if ( !$expiry ) {
955 $this->loadWatchedItem( $user, $target );
956 return;
957 }
958
959 $expiry = ExpiryDef::normalizeUsingMaxExpiry( $expiry, $this->maxExpiryDuration, TS::ISO_8601 );
960 } else {
961 $expiry = null;
962 }
963
964 // Create a new WatchedItem and add it to the process cache.
965 $item = new WatchedItem(
966 $user,
967 $target,
968 null,
969 $expiry
970 );
971 $this->cache( $item );
972 }
973
987 public function addWatchBatchForUser(
988 UserIdentity $user,
989 array $targets,
990 ?string $expiry = null
991 ): bool {
992 // Only registered user can have a watchlist
993 if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
994 return false;
995 }
996
997 if ( !$targets ) {
998 return true;
999 }
1000 $expiry = ExpiryDef::normalizeUsingMaxExpiry( $expiry, $this->maxExpiryDuration, TS::ISO_8601 );
1001 $rows = [];
1002 foreach ( $targets as $target ) {
1003 $rows[] = [
1004 'wl_user' => $user->getId(),
1005 'wl_namespace' => $target->getNamespace(),
1006 'wl_title' => $target->getDBkey(),
1007 'wl_notificationtimestamp' => null,
1008 ];
1009 $this->uncache( $user, $target );
1010 }
1011
1012 $dbw = $this->lbFactory->getPrimaryDatabase();
1013 $ticket = count( $targets ) > $this->updateRowsPerQuery ?
1014 $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) : null;
1015 $affectedRows = 0;
1016 $rowBatches = array_chunk( $rows, $this->updateRowsPerQuery );
1017 foreach ( $rowBatches as $toInsert ) {
1018 // Use INSERT IGNORE to avoid overwriting the notification timestamp
1019 // if there's already an entry for this page
1020 $dbw->newInsertQueryBuilder()
1021 ->insertInto( 'watchlist' )
1022 ->ignore()
1023 ->rows( $toInsert )
1024 ->caller( __METHOD__ )->execute();
1025 $affectedRows += $dbw->affectedRows();
1026
1027 if ( $this->expiryEnabled ) {
1028 $affectedRows += $this->updateOrDeleteExpiries( $dbw, $user->getId(), $toInsert, $expiry );
1029 }
1030
1031 if ( $ticket ) {
1032 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1033 }
1034 }
1035
1036 return (bool)$affectedRows;
1037 }
1038
1048 private function updateOrDeleteExpiries(
1049 IDatabase $dbw,
1050 int $userId,
1051 array $rows,
1052 ?string $expiry = null
1053 ): int {
1054 if ( !$expiry ) {
1055 // if expiry is null (shouldn't change), 0 rows affected.
1056 return 0;
1057 }
1058
1059 // Build the giant `(...) OR (...)` part to be used with WHERE.
1060 $conds = [];
1061 foreach ( $rows as $row ) {
1062 $conds[] = $dbw->makeList(
1063 [
1064 'wl_user' => $userId,
1065 'wl_namespace' => $row['wl_namespace'],
1066 'wl_title' => $row['wl_title']
1067 ],
1068 $dbw::LIST_AND
1069 );
1070 }
1071 $cond = $dbw->makeList( $conds, $dbw::LIST_OR );
1072
1073 if ( wfIsInfinity( $expiry ) ) {
1074 // Rows should be deleted rather than updated.
1075 $dbw->deleteJoin(
1076 'watchlist_expiry',
1077 'watchlist',
1078 'we_item',
1079 'wl_id',
1080 [ $cond ],
1081 __METHOD__
1082 );
1083
1084 return $dbw->affectedRows();
1085 }
1086
1087 return $this->updateExpiries( $dbw, $expiry, $cond );
1088 }
1089
1097 private function updateExpiries( IDatabase $dbw, string $expiry, string $cond ): int {
1098 // First fetch the wl_ids from the watchlist table.
1099 // We'd prefer to do a INSERT/SELECT in the same query with IDatabase::insertSelect(),
1100 // but it doesn't allow us to use the "ON DUPLICATE KEY UPDATE" clause.
1101 $wlIds = $dbw->newSelectQueryBuilder()
1102 ->select( 'wl_id' )
1103 ->from( 'watchlist' )
1104 ->where( $cond )
1105 ->caller( __METHOD__ )
1106 ->fetchFieldValues();
1107
1108 if ( !$wlIds ) {
1109 return 0;
1110 }
1111
1112 $expiry = $dbw->timestamp( $expiry );
1113 $weRows = [];
1114 foreach ( $wlIds as $wlId ) {
1115 $weRows[] = [
1116 'we_item' => $wlId,
1117 'we_expiry' => $expiry
1118 ];
1119 }
1120
1121 // Insert into watchlist_expiry, updating the expiry for duplicate rows.
1122 $dbw->newInsertQueryBuilder()
1123 ->insertInto( 'watchlist_expiry' )
1124 ->rows( $weRows )
1125 ->onDuplicateKeyUpdate()
1126 ->uniqueIndexFields( [ 'we_item' ] )
1127 ->set( [ 'we_expiry' => $expiry ] )
1128 ->caller( __METHOD__ )->execute();
1129
1130 return $dbw->affectedRows();
1131 }
1132
1139 public function removeWatch( UserIdentity $user, PageReference $target ): bool {
1140 return $this->removeWatchBatchForUser( $user, [ $target ] );
1141 }
1142
1161 UserIdentity $user,
1162 $timestamp,
1163 array $targets = []
1164 ): bool {
1165 // Only registered user can have a watchlist
1166 if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
1167 return false;
1168 }
1169
1170 if ( !$targets ) {
1171 // Backwards compatibility
1172 $this->resetAllNotificationTimestampsForUser( $user, $timestamp );
1173 return true;
1174 }
1175
1176 $dbw = $this->lbFactory->getPrimaryDatabase();
1177 if ( $timestamp !== null ) {
1178 $timestamp = $dbw->timestamp( $timestamp );
1179 }
1180 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
1181 $affectedSinceWait = 0;
1182
1183 $wlIds = $this->loadIdsForTargets( $dbw, $user, $targets );
1184 foreach ( $this->batch( $wlIds ) as $ids ) {
1185 $dbw->newUpdateQueryBuilder()
1186 ->update( 'watchlist' )
1187 ->set( [ 'wl_notificationtimestamp' => $timestamp ] )
1188 ->where( [ 'wl_id' => $ids ] )
1189 ->caller( __METHOD__ )->execute();
1190
1191 $affectedSinceWait += $dbw->affectedRows();
1192 // Wait for replication every time we've touched updateRowsPerQuery rows
1193 if ( $affectedSinceWait >= $this->updateRowsPerQuery ) {
1194 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1195 $affectedSinceWait = 0;
1196 }
1197 }
1198
1199 $this->uncacheUser( $user );
1200
1201 return true;
1202 }
1203
1211 $timestamp,
1212 UserIdentity $user,
1213 PageReference $target
1214 ) {
1215 $timestamp = wfTimestampOrNull( TS::MW, $timestamp );
1216 if ( $timestamp === null ) {
1217 return null; // no notification
1218 }
1219
1220 $seenTimestamps = $this->getPageSeenTimestamps( $user );
1221 if ( $seenTimestamps ) {
1222 $seenKey = $this->getPageSeenKey( $target );
1223 if ( isset( $seenTimestamps[$seenKey] ) && $seenTimestamps[$seenKey] >= $timestamp ) {
1224 // If a reset job did not yet run, then the "seen" timestamp will be higher
1225 return null;
1226 }
1227 }
1228
1229 return $timestamp;
1230 }
1231
1238 public function resetAllNotificationTimestampsForUser( UserIdentity $user, $timestamp = null ) {
1239 // Only registered user can have a watchlist
1240 if ( !$user->isRegistered() ) {
1241 return;
1242 }
1243
1244 // If the page is watched by the user (or may be watched), update the timestamp
1246 'userId' => $user->getId(), 'timestamp' => $timestamp, 'casTime' => time()
1247 ] );
1248
1249 // Try to run this post-send
1250 // Calls DeferredUpdates::addCallableUpdate in normal operation
1251 ( $this->deferredUpdatesAddCallableUpdateCallback )(
1252 static function () use ( $job ) {
1253 $job->run();
1254 }
1255 );
1256 }
1257
1267 UserIdentity $editor,
1268 PageReference $target,
1269 $timestamp
1270 ): array {
1271 $dbw = $this->lbFactory->getPrimaryDatabase();
1272 $queryBuilder = $dbw->newSelectQueryBuilder()
1273 ->select( [ 'wl_id', 'wl_user' ] )
1274 ->from( 'watchlist' )
1275 ->where(
1276 [
1277 'wl_user != ' . $editor->getId(),
1278 'wl_namespace' => $target->getNamespace(),
1279 'wl_title' => $target->getDBkey(),
1280 'wl_notificationtimestamp' => null,
1281 ]
1282 )
1283 ->caller( __METHOD__ );
1284
1285 $this->modifyQueryBuilderForExpiry( $queryBuilder, $dbw );
1286
1287 $res = $queryBuilder->fetchResultSet();
1288 $watchers = [];
1289 $wlIds = [];
1290 foreach ( $res as $row ) {
1291 $watchers[] = (int)$row->wl_user;
1292 $wlIds[] = (int)$row->wl_id;
1293 }
1294
1295 if ( $wlIds ) {
1296 $fname = __METHOD__;
1297 // Try to run this post-send
1298 // Calls DeferredUpdates::addCallableUpdate in normal operation
1299 ( $this->deferredUpdatesAddCallableUpdateCallback )(
1300 function () use ( $timestamp, $wlIds, $target, $fname ) {
1301 $dbw = $this->lbFactory->getPrimaryDatabase();
1302 $ticket = $this->lbFactory->getEmptyTransactionTicket( $fname );
1303
1304 $wlIdsChunks = array_chunk( $wlIds, $this->updateRowsPerQuery );
1305 foreach ( $wlIdsChunks as $wlIdsChunk ) {
1306 $dbw->newUpdateQueryBuilder()
1307 ->update( 'watchlist' )
1308 ->set( [ 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) ] )
1309 ->where( [ 'wl_id' => $wlIdsChunk ] )
1310 ->caller( $fname )->execute();
1311
1312 if ( count( $wlIdsChunks ) > 1 ) {
1313 $this->lbFactory->commitAndWaitForReplication( $fname, $ticket );
1314 }
1315 }
1316 $this->uncacheTitle( $target );
1317 },
1318 DeferredUpdates::POSTSEND,
1319 $dbw
1320 );
1321 }
1322
1323 return $watchers;
1324 }
1325
1335 UserIdentity $user,
1336 PageReference $title,
1337 $force = '',
1338 $oldid = 0
1339 ): bool {
1340 $time = time();
1341
1342 // Only registered user can have a watchlist
1343 if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
1344 return false;
1345 }
1346
1347 $item = null;
1348 if ( $force != 'force' ) {
1349 $item = $this->getWatchedItem( $user, $title );
1350 if ( !$item || $item->getNotificationTimestamp() === null ) {
1351 return false;
1352 }
1353 }
1354
1355 // Get the timestamp (TS::MW) of this revision to track the latest one seen
1356 $id = $oldid;
1357 $seenTime = null;
1358 if ( !$id ) {
1359 $latestRev = $this->revisionLookup->getRevisionByTitle( $title );
1360 if ( $latestRev ) {
1361 $id = $latestRev->getId();
1362 // Save a DB query
1363 $seenTime = $latestRev->getTimestamp();
1364 }
1365 }
1366 if ( $seenTime === null ) {
1367 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable getId does not return null here
1368 $seenTime = $this->revisionLookup->getTimestampFromId( $id );
1369 }
1370
1371 // Mark the item as read immediately in lightweight storage
1372 $this->stash->merge(
1373 $this->getPageSeenTimestampsKey( $user ),
1374 function ( $cache, $key, $current ) use ( $title, $seenTime ) {
1375 if ( !$current ) {
1376 $value = new MapCacheLRU( 300 );
1377 } elseif ( is_array( $current ) ) {
1378 $value = MapCacheLRU::newFromArray( $current, 300 );
1379 } else {
1380 // Backwards compatibility for T282105
1381 $value = $current;
1382 }
1383 $subKey = $this->getPageSeenKey( $title );
1384
1385 if ( $seenTime > $value->get( $subKey ) ) {
1386 // Revision is newer than the last one seen
1387 $value->set( $subKey, $seenTime );
1388
1389 $this->latestUpdateCache->set( $key, $value->toArray(), BagOStuff::TTL_PROC_LONG );
1390 } elseif ( $seenTime === false ) {
1391 // Revision does not exist
1392 $value->set( $subKey, ConvertibleTimestamp::now( TS::MW ) );
1393 $this->latestUpdateCache->set( $key,
1394 $value->toArray(),
1395 BagOStuff::TTL_PROC_LONG );
1396 } else {
1397 return false; // nothing to update
1398 }
1399
1400 return $value->toArray();
1401 },
1402 BagOStuff::TTL_HOUR
1403 );
1404
1405 // If the page is watched by the user (or may be watched), update the timestamp
1406 // ActivityUpdateJob accepts both LinkTarget and PageReference
1407 $job = new ActivityUpdateJob(
1408 $title,
1409 [
1410 'type' => 'updateWatchlistNotification',
1411 'userid' => $user->getId(),
1412 'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ),
1413 'curTime' => $time
1414 ]
1415 );
1416 // Try to enqueue this post-send
1417 $this->queueGroup->lazyPush( $job );
1418
1419 $this->uncache( $user, $title );
1420
1421 return true;
1422 }
1423
1428 private function getPageSeenTimestamps( UserIdentity $user ) {
1429 $key = $this->getPageSeenTimestampsKey( $user );
1430
1431 $cache = $this->latestUpdateCache->getWithSetCallback(
1432 $key,
1433 BagOStuff::TTL_PROC_LONG,
1434 function () use ( $key ) {
1435 return $this->stash->get( $key ) ?: null;
1436 }
1437 );
1438 // Backwards compatibility for T282105
1439 if ( $cache instanceof MapCacheLRU ) {
1440 $cache = $cache->toArray();
1441 }
1442 return $cache;
1443 }
1444
1445 private function getPageSeenTimestampsKey( UserIdentity $user ): string {
1446 return $this->stash->makeGlobalKey(
1447 'watchlist-recent-updates',
1448 $this->lbFactory->getLocalDomainID(),
1449 $user->getId()
1450 );
1451 }
1452
1457 private function getPageSeenKey( $target ): string {
1458 return "{$target->getNamespace()}:{$target->getDBkey()}";
1459 }
1460
1469 private function getNotificationTimestamp(
1470 UserIdentity $user,
1471 PageReference $title,
1472 $item,
1473 $force,
1474 $oldid
1475 ) {
1476 if ( !$oldid ) {
1477 // No oldid given, assuming latest revision; clear the timestamp.
1478 return null;
1479 }
1480
1481 $oldRev = $this->revisionLookup->getRevisionById( $oldid );
1482 if ( !$oldRev ) {
1483 // Oldid given but does not exist (probably deleted)
1484 return false;
1485 }
1486
1487 $nextRev = $this->revisionLookup->getNextRevision( $oldRev );
1488 if ( !$nextRev ) {
1489 // Oldid given and is the latest revision for this title; clear the timestamp.
1490 return null;
1491 }
1492
1493 $item ??= $this->loadWatchedItem( $user, $title );
1494 if ( !$item ) {
1495 // This can only happen if $force is enabled.
1496 return null;
1497 }
1498
1499 // Oldid given and isn't the latest; update the timestamp.
1500 // This will result in no further notification emails being sent!
1501 $notificationTimestamp = $this->revisionLookup->getTimestampFromId( $oldid );
1502 // @FIXME: this should use getTimestamp() for consistency with updates on new edits
1503 // $notificationTimestamp = $nextRev->getTimestamp(); // first unseen revision timestamp
1504
1505 // We need to go one second to the future because of various strict comparisons
1506 // throughout the codebase
1507 $ts = new MWTimestamp( $notificationTimestamp );
1508 $ts->timestamp->add( new DateInterval( 'PT1S' ) );
1509 $notificationTimestamp = $ts->getTimestamp( TS::MW );
1510
1511 if ( $notificationTimestamp < $item->getNotificationTimestamp() ) {
1512 if ( $force != 'force' ) {
1513 return false;
1514 } else {
1515 // This is a little silly…
1516 return $item->getNotificationTimestamp();
1517 }
1518 }
1519
1520 return $notificationTimestamp;
1521 }
1522
1529 public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null ) {
1530 $queryBuilder = $this->lbFactory->getReplicaDatabase()->newSelectQueryBuilder()
1531 ->select( '1' )
1532 ->from( 'watchlist' )
1533 ->where( [
1534 'wl_user' => $user->getId(),
1535 'wl_notificationtimestamp IS NOT NULL'
1536 ] )
1537 ->caller( __METHOD__ );
1538 if ( $unreadLimit !== null ) {
1539 $unreadLimit = (int)$unreadLimit;
1540 $queryBuilder->limit( $unreadLimit );
1541 }
1542
1543 $rowCount = $queryBuilder->fetchRowCount();
1544
1545 if ( $unreadLimit === null ) {
1546 return $rowCount;
1547 }
1548
1549 if ( $rowCount >= $unreadLimit ) {
1550 return true;
1551 }
1552
1553 return $rowCount;
1554 }
1555
1562 PageReference $oldTarget,
1563 PageReference $newTarget
1564 ) {
1565 // Duplicate first the subject page, then the talk page
1566 $this->duplicateEntry(
1567 PageReferenceValue::localReference(
1568 $this->nsInfo->getSubject( $oldTarget->getNamespace() ),
1569 $oldTarget->getDBkey(),
1570 ),
1571 PageReferenceValue::localReference(
1572 $this->nsInfo->getSubject( $newTarget->getNamespace() ),
1573 $newTarget->getDBkey()
1574 )
1575 );
1576 $this->duplicateEntry(
1577 PageReferenceValue::localReference(
1578 $this->nsInfo->getTalk( $oldTarget->getNamespace() ),
1579 $oldTarget->getDBkey()
1580 ),
1581 PageReferenceValue::localReference(
1582 $this->nsInfo->getTalk( $newTarget->getNamespace() ),
1583 $newTarget->getDBkey()
1584 )
1585 );
1586 }
1587
1593 public function duplicateEntry( PageReference $oldTarget, PageReference $newTarget ) {
1594 $dbw = $this->lbFactory->getPrimaryDatabase();
1595 $result = $this->fetchWatchedItemsForPage( $dbw, $oldTarget );
1596 $newNamespace = $newTarget->getNamespace();
1597 $newDBkey = $newTarget->getDBkey();
1598
1599 # Construct array to replace into the watchlist
1600 $values = [];
1601 $expiries = [];
1602 $labels = [];
1603 foreach ( $result as $row ) {
1604 $values[] = [
1605 'wl_user' => $row->wl_user,
1606 'wl_namespace' => $newNamespace,
1607 'wl_title' => $newDBkey,
1608 'wl_notificationtimestamp' => $row->wl_notificationtimestamp,
1609 ];
1610
1611 if ( $this->expiryEnabled && $row->we_expiry ) {
1612 $expiries[$row->wl_user] = $row->we_expiry;
1613 }
1614 if ( $this->labelsEnabled && $row->wlm_label_summary !== '' ) {
1615 $labels[$row->wl_user] = $row->wlm_label_summary;
1616 }
1617 }
1618
1619 if ( !$values ) {
1620 return;
1621 }
1622
1623 // Perform a replace on the watchlist table rows.
1624 // Note that multi-row replace is very efficient for MySQL but may be inefficient for
1625 // some other DBMSes, mostly due to poor simulation by us.
1626 $dbw->newReplaceQueryBuilder()
1627 ->replaceInto( 'watchlist' )
1628 ->uniqueIndexFields( [ 'wl_user', 'wl_namespace', 'wl_title' ] )
1629 ->rows( $values )
1630 ->caller( __METHOD__ )->execute();
1631
1632 if ( $expiries || $labels ) {
1633 $this->updateAssociationsAfterMove( $dbw, $expiries, $labels, $newNamespace, $newDBkey );
1634 }
1635 }
1636
1642 private function fetchWatchedItemsForPage(
1643 IReadableDatabase $dbr,
1644 PageReference $target
1645 ): IResultWrapper {
1646 $queryBuilder = $dbr->newSelectQueryBuilder()
1647 ->select( [ 'wl_user', 'wl_notificationtimestamp' ] )
1648 ->from( 'watchlist' )
1649 ->where( [
1650 'wl_namespace' => $target->getNamespace(),
1651 'wl_title' => $target->getDBkey(),
1652 ] )
1653 ->caller( __METHOD__ )
1654 ->forUpdate();
1655
1656 if ( $this->expiryEnabled ) {
1657 $queryBuilder->leftJoin( 'watchlist_expiry', null, [ 'wl_id = we_item' ] )
1658 ->field( 'we_expiry' );
1659 }
1660 if ( $this->labelsEnabled ) {
1661 $this->addLabelSummaryField( $queryBuilder, $dbr );
1662 }
1663
1664 return $queryBuilder->fetchResultSet();
1665 }
1666
1674 private function updateAssociationsAfterMove(
1675 IDatabase $dbw,
1676 array $expiries,
1677 array $labels,
1678 int $namespace,
1679 string $dbKey
1680 ): void {
1681 DeferredUpdates::addCallableUpdate(
1682 function ( $fname ) use ( $dbw, $expiries, $labels, $namespace, $dbKey ) {
1683 // First fetch new wl_ids.
1684 $res = $dbw->newSelectQueryBuilder()
1685 ->select( [ 'wl_user', 'wl_id' ] )
1686 ->from( 'watchlist' )
1687 ->where( [
1688 'wl_namespace' => $namespace,
1689 'wl_title' => $dbKey,
1690 ] )
1691 ->caller( $fname )
1692 ->fetchResultSet();
1693
1694 // Build new array to INSERT into multiple rows at once.
1695 $expiryData = [];
1696 $labelData = [];
1697 foreach ( $res as $row ) {
1698 if ( !empty( $expiries[$row->wl_user] ) ) {
1699 $expiryData[] = [
1700 'we_item' => $row->wl_id,
1701 'we_expiry' => $expiries[$row->wl_user],
1702 ];
1703 }
1704 if ( isset( $labels[$row->wl_user] ) ) {
1705 foreach ( explode( ',', $labels[$row->wl_user] ) as $labelId ) {
1706 $labelData[] = [
1707 'wlm_item' => $row->wl_id,
1708 'wlm_label' => (int)$labelId,
1709 ];
1710 }
1711 }
1712 }
1713
1714 foreach ( $this->batch( $expiryData ) as $toInsert ) {
1715 $dbw->newReplaceQueryBuilder()
1716 ->replaceInto( 'watchlist_expiry' )
1717 ->uniqueIndexFields( [ 'we_item' ] )
1718 ->rows( $toInsert )
1719 ->caller( $fname )
1720 ->execute();
1721 }
1722 foreach ( $this->batch( $labelData ) as $toInsert ) {
1723 $dbw->newReplaceQueryBuilder()
1724 ->replaceInto( 'watchlist_label_member' )
1725 ->uniqueIndexFields( [ 'wlm_label', 'wlm_item' ] )
1726 ->rows( $toInsert )
1727 ->caller( $fname )
1728 ->execute();
1729 }
1730 },
1731 DeferredUpdates::POSTSEND,
1732 $dbw
1733 );
1734 }
1735
1742 private function batch( array $data ) {
1743 return array_chunk( $data, $this->updateRowsPerQuery );
1744 }
1745
1750 private function uncacheTitlesForUser( UserIdentity $user, array $titles ) {
1751 foreach ( $titles as $title ) {
1752 $this->uncache( $user, $title );
1753 }
1754 }
1755
1759 public function countExpired(): int {
1760 $dbr = $this->lbFactory->getReplicaDatabase();
1761 return $dbr->newSelectQueryBuilder()
1762 ->select( '*' )
1763 ->from( 'watchlist_expiry' )
1764 ->where( $dbr->expr( 'we_expiry', '<=', $dbr->timestamp() ) )
1765 ->caller( __METHOD__ )
1766 ->fetchRowCount();
1767 }
1768
1772 public function removeExpired( int $limit, bool $deleteOrphans = false ): void {
1773 $dbr = $this->lbFactory->getReplicaDatabase();
1774 $dbw = $this->lbFactory->getPrimaryDatabase();
1775 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
1776
1777 // Get a batch of watchlist IDs to delete.
1778 $toDelete = $dbr->newSelectQueryBuilder()
1779 ->select( 'we_item' )
1780 ->from( 'watchlist_expiry' )
1781 ->where( $dbr->expr( 'we_expiry', '<=', $dbr->timestamp() ) )
1782 ->limit( $limit )
1783 ->caller( __METHOD__ )
1784 ->fetchFieldValues();
1785
1786 if ( count( $toDelete ) > 0 ) {
1787 // Delete them from the watchlist and associated tables
1788 $dbw->newDeleteQueryBuilder()
1789 ->deleteFrom( 'watchlist' )
1790 ->where( [ 'wl_id' => $toDelete ] )
1791 ->caller( __METHOD__ )->execute();
1792 $dbw->newDeleteQueryBuilder()
1793 ->deleteFrom( 'watchlist_expiry' )
1794 ->where( [ 'we_item' => $toDelete ] )
1795 ->caller( __METHOD__ )->execute();
1796 if ( $this->labelsEnabled ) {
1797 $dbw->newDeleteQueryBuilder()
1798 ->deleteFrom( 'watchlist_label_member' )
1799 ->where( [ 'wlm_item' => $toDelete ] )
1800 ->caller( __METHOD__ )->execute();
1801 }
1802 }
1803
1804 // Also delete any orphaned or null-expiry watchlist_expiry rows
1805 // (they should not exist, but might because not everywhere knows about the expiry table yet).
1806 if ( $deleteOrphans ) {
1807 $expiryToDelete = $dbr->newSelectQueryBuilder()
1808 ->select( 'we_item' )
1809 ->from( 'watchlist_expiry' )
1810 ->leftJoin( 'watchlist', null, 'wl_id = we_item' )
1811 ->where( $dbr->makeList(
1812 [ 'wl_id' => null, 'we_expiry' => null ],
1813 $dbr::LIST_OR
1814 ) )
1815 ->caller( __METHOD__ )
1816 ->fetchFieldValues();
1817 if ( count( $expiryToDelete ) > 0 ) {
1818 $dbw->newDeleteQueryBuilder()
1819 ->deleteFrom( 'watchlist_expiry' )
1820 ->where( [ 'we_item' => $expiryToDelete ] )
1821 ->caller( __METHOD__ )->execute();
1822 }
1823 }
1824
1825 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1826 }
1827
1829 public function addLabels( UserIdentity $user, array $targets, array $labels ): void {
1830 if ( !$labels ) {
1831 return;
1832 }
1833 if ( !$this->labelsEnabled ) {
1834 throw new LogicException( 'addLabels was called when ' .
1835 '$wgEnableWatchlistLabels was false -- caller should check' );
1836 }
1837 $labelIds = $this->getLabelIds( $labels );
1838 $dbw = $this->lbFactory->getPrimaryDatabase();
1839 $wlIds = $this->loadIdsForTargets( $dbw, $user, $targets );
1840
1841 foreach ( $this->batch( $wlIds ) as $wlIdsBatch ) {
1842 $rows = [];
1843 foreach ( $wlIdsBatch as $wlId ) {
1844 foreach ( $labelIds as $labelId ) {
1845 $rows[] = [
1846 'wlm_label' => $labelId,
1847 'wlm_item' => $wlId,
1848 ];
1849 }
1850 }
1851
1852 $dbw->newInsertQueryBuilder()
1853 ->insertInto( 'watchlist_label_member' )
1854 ->ignore()
1855 ->rows( $rows )
1856 ->caller( __METHOD__ )
1857 ->execute();
1858 }
1859 }
1860
1862 public function removeLabels( UserIdentity $user, array $targets, array $labels ): void {
1863 if ( !$labels ) {
1864 return;
1865 }
1866 if ( !$this->labelsEnabled ) {
1867 throw new LogicException( 'removeLabels was called when ' .
1868 '$wgEnableWatchlistLabels was false -- caller should check' );
1869 }
1870 $labelIds = $this->getLabelIds( $labels );
1871 $dbw = $this->lbFactory->getPrimaryDatabase();
1872 $wlIds = $this->loadIdsForTargets( $dbw, $user, $targets );
1873 foreach ( $this->batch( $wlIds ) as $wlIdsBatch ) {
1874 $dbw->newDeleteQueryBuilder()
1875 ->deleteFrom( 'watchlist_label_member' )
1876 ->where( [
1877 'wlm_item' => $wlIdsBatch,
1878 'wlm_label' => $labelIds
1879 ] )
1880 ->execute();
1881 }
1882 }
1883
1888 private function getLabelIds( array $labels ) {
1889 $labelIds = [];
1890 foreach ( $labels as $label ) {
1891 if ( $label instanceof WatchlistLabel ) {
1892 $labelId = $label->getId();
1893 if ( !$labelId ) {
1894 throw new InvalidArgumentException( 'WatchlistLabel has no label ID -- ' .
1895 'it must be loaded before adding it to an item' );
1896 }
1897 $labelIds[] = $labelId;
1898 } else {
1899 $labelIds[] = (int)$label;
1900 }
1901 }
1902 return $labelIds;
1903 }
1904
1913 private function loadIdsForTargets( IReadableDatabase $db, UserIdentity $user, array $targets ) {
1914 $idStrings = $db->newSelectQueryBuilder()
1915 ->select( 'wl_id' )
1916 ->from( 'watchlist' )
1917 ->where( [
1918 $this->getTargetsCond( $targets ),
1919 'wl_user' => $user->getId(),
1920 ] )
1921 ->caller( __METHOD__ )
1922 ->fetchFieldValues();
1923 return array_map( 'intval', $idStrings );
1924 }
1925
1932 private function getTargetsCond( array $targets ) {
1933 return $this->linkBatchFactory->newLinkBatch( $targets )
1934 ->constructSet( 'wl', $this->lbFactory->getReplicaDatabase() );
1935 }
1936
1937}
1939class_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:68
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()
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.
Store key-value entries in a size-limited in-memory LRU cache.
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.
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'=> false, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, 'TiffThumbnailType'=>[], 'ThumbnailEpoch'=> '20030516000000', 'AttemptFailureEpoch'=> 1, 'IgnoreImageErrors'=> false, 'GenerateThumbnailOnParse'=> true, 'ShowArchiveThumbnails'=> true, 'EnableAutoRotation'=> null, 'Antivirus'=> null, 'AntivirusSetup'=>['clamav'=>['command'=> 'clamscan --no-summary ', 'codemap'=>[0=> 0, 1=> 1, 52=> -1, ' *'=> false,], 'messagepattern'=> '/.*?:(.*)/sim',],], 'AntivirusRequired'=> true, 'VerifyMimeType'=> true, 'MimeTypeFile'=> 'internal', 'MimeInfoFile'=> 'internal', 'MimeDetectorCommand'=> null, 'TrivialMimeDetection'=> false, 'XMLMimeTypes'=>['http:'svg'=> 'image/svg+xml', 'http:'http:'html'=> 'text/html',], 'ImageLimits'=>[[320, 240,], [640, 480,], [800, 600,], [1024, 768,], [1280, 1024,], [2560, 2048,],], 'ThumbLimits'=>[120, 150, 180, 200, 250, 300,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailStepsRatio'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'EnableSpecialMute'=> false, 'EnableUserEmailMuteList'=> false, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'UserEmailConfirmationUseHTML'=> false, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EnotifWatchlist'=> false, 'EnotifUserTalk'=> false, 'EnotifRevealEditorAddress'=> false, 'EnotifMinorEdits'=> true, 'EnotifUseRealName'=> false, 'UsersNotifiedOnAllChanges'=>[], 'DBname'=> 'my_wiki', 'DBmwschema'=> null, 'DBprefix'=> '', 'DBserver'=> 'localhost', 'DBport'=> 5432, 'DBuser'=> 'wikiuser', 'DBpassword'=> '', 'DBtype'=> 'mysql', 'DBssl'=> false, 'DBcompress'=> false, 'DBStrictWarnings'=> false, 'DBadminuser'=> null, 'DBadminpassword'=> null, 'SearchType'=> null, 'SearchTypeAlternatives'=> null, 'DBTableOptions'=> 'ENGINE=InnoDB, DEFAULT CHARSET=binary', 'SQLMode'=> '', 'SQLiteDataDir'=> '', 'SharedDB'=> null, 'SharedPrefix'=> false, 'SharedTables'=>['user', 'user_properties', 'user_autocreate_serial',], 'SharedSchema'=> false, 'DBservers'=> false, 'LBFactoryConf'=>['class'=> 'Wikimedia\\Rdbms\\LBFactorySimple',], 'DataCenterUpdateStickTTL'=> 10, 'DBerrorLog'=> false, 'DBerrorLogTZ'=> false, 'LocalDatabases'=>[], 'DatabaseReplicaLagWarning'=> 10, 'DatabaseReplicaLagCritical'=> 30, 'MaxExecutionTimeForExpensiveQueries'=> 0, 'VirtualDomainsMapping'=>[], 'FileSchemaMigrationStage'=> 3, 'ImageLinksSchemaMigrationStage'=> 3, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory',],], 'text'=> 'MediaWiki\\Content\\TextContentHandler', 'unknown'=> 'MediaWiki\\Content\\FallbackContentHandler',], 'NamespaceContentModels'=>[], 'TextModelsToParse'=>['wikitext', 'javascript', 'css',], 'CompressRevisions'=> false, 'ExternalStores'=>[], 'ExternalServers'=>[], 'DefaultExternalStore'=> false, 'RevisionCacheExpiry'=> 604800, 'PageLanguageUseDB'=> false, 'DiffEngine'=> null, 'ExternalDiffEngine'=> false, 'Wikidiff2Options'=>[], 'RequestTimeLimit'=> null, 'TransactionalTimeLimit'=> 120, 'CriticalSectionTimeLimit'=> 180.0, 'MiserMode'=> false, 'DisableQueryPages'=> false, 'QueryCacheLimit'=> 1000, 'WantedPagesThreshold'=> 1, 'AllowSlowParserFunctions'=> false, 'AllowSchemaUpdates'=> true, 'MaxArticleSize'=> 2048, 'MemoryLimit'=> '50M', 'PoolCounterConf'=> null, 'PoolCountClientConf'=>['servers'=>['127.0.0.1',], 'timeout'=> 0.1,], 'MaxUserDBWriteDuration'=> false, 'MaxJobDBWriteDuration'=> false, 'LinkHolderBatchSize'=> 1000, 'MaximumMovedPages'=> 100, 'ForceDeferredUpdatesPreSend'=> false, 'MultiShardSiteStats'=> false, 'CacheDirectory'=> false, 'MainCacheType'=> 0, 'MessageCacheType'=> -1, 'ParserCacheType'=> -1, 'SessionCacheType'=> -1, 'AnonSessionCacheType'=> false, 'LanguageConverterCacheType'=> -1, 'ObjectCaches'=>[0=>['class'=> 'Wikimedia\\ObjectCache\\EmptyBagOStuff', 'reportDupes'=> false,], 1=>['class'=> 'SqlBagOStuff', 'loggroup'=> 'SQLBagOStuff',], 'memcached-php'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPhpBagOStuff', 'loggroup'=> 'memcached',], 'memcached-pecl'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPeclBagOStuff', 'loggroup'=> 'memcached',], 'hash'=>['class'=> 'Wikimedia\\ObjectCache\\HashBagOStuff', 'reportDupes'=> false,], 'apc'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,], 'apcu'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,],], 'WANObjectCache'=>[], 'MicroStashType'=> -1, 'MainStash'=> 1, 'ParsoidCacheConfig'=>['StashType'=> null, 'StashDuration'=> 86400, 'WarmParsoidParserCache'=> false,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> 'LocalisationCache', 'store'=> 'detect', 'storeClass'=> false, 'storeDirectory'=> false, 'storeServer'=>[], 'forceRecache'=> false, 'manualRecache'=> false,], 'CachePages'=> true, 'CacheEpoch'=> '20030516000000', 'GitInfoCacheDirectory'=> false, 'UseFileCache'=> false, 'FileCacheDepth'=> 2, 'RenderHashAppend'=> '', 'EnableSidebarCache'=> false, 'SidebarCacheExpiry'=> 86400, 'UseGzip'=> false, 'InvalidateCacheOnLocalSettingsChange'=> true, 'ExtensionInfoMTime'=> false, 'EnableRemoteBagOStuffTests'=> false, 'UseCdn'=> false, 'VaryOnXFP'=> false, 'InternalServer'=> false, 'CdnMaxAge'=> 18000, 'CdnMaxageLagged'=> 30, 'CdnMaxageStale'=> 10, 'CdnReboundPurgeDelay'=> 0, 'CdnMaxageSubstitute'=> 60, 'ForcedRawSMaxage'=> 300, 'CdnServers'=>[], 'CdnServersNoPurge'=>[], 'HTCPRouting'=>[], 'HTCPMulticastTTL'=> 1, 'UsePrivateIPs'=> false, 'CdnMatchParameterOrder'=> true, 'LanguageCode'=> 'en', 'GrammarForms'=>[], 'InterwikiMagic'=> true, 'HideInterlanguageLinks'=> false, 'ExtraInterlanguageLinkPrefixes'=>[], 'InterlanguageLinkCodeMap'=>[], 'ExtraLanguageNames'=>[], 'ExtraLanguageCodes'=>['bh'=> 'bho', 'no'=> 'nb', 'simple'=> 'en',], 'DummyLanguageCodes'=>[], 'AllUnicodeFixes'=> false, 'LegacyEncoding'=> false, 'AmericanDates'=> false, 'TranslateNumerals'=> true, 'UseDatabaseMessages'=> true, 'MaxMsgCacheEntrySize'=> 10000, 'DisableLangConversion'=> false, 'DisableTitleConversion'=> false, 'DefaultLanguageVariant'=> false, 'UsePigLatinVariant'=> false, 'DisabledVariants'=>[], 'VariantArticlePath'=> false, 'UseXssLanguage'=> false, 'LoginLanguageSelector'=> false, 'ForceUIMsgAsContentMsg'=>[], 'RawHtmlMessages'=>[], 'Localtimezone'=> null, 'LocalTZoffset'=> null, 'OverrideUcfirstCharacters'=>[], 'MimeType'=> 'text/html', 'Html5Version'=> null, 'EditSubmitButtonLabelPublish'=> false, 'XhtmlNamespaces'=>[], 'SiteNotice'=> '', 'BrowserFormatDetection'=> 'telephone=no', 'SkinMetaTags'=>[], 'DefaultSkin'=> 'vector-2022', 'FallbackSkin'=> 'fallback', 'SkipSkins'=>[], 'DisableOutputCompression'=> false, 'FragmentMode'=>['html5', 'legacy',], 'ExternalInterwikiFragmentMode'=> 'legacy', 'FooterIcons'=>['copyright'=>['copyright'=>[],], 'poweredby'=>['mediawiki'=>['src'=> null, 'url'=> 'https:'alt'=> 'Powered by MediaWiki', 'lang'=> 'en',],],], 'UseCombinedLoginLink'=> false, 'Edititis'=> false, 'Send404Code'=> true, 'ShowRollbackEditCount'=> 10, 'EnableCanonicalServerLink'=> false, 'InterwikiLogoOverride'=>[], 'ResourceModules'=>[], 'ResourceModuleSkinStyles'=>[], 'ResourceLoaderSources'=>[], 'ResourceBasePath'=> null, 'ResourceLoaderMaxage'=>[], 'ResourceLoaderDebug'=> false, 'ResourceLoaderMaxQueryLength'=> false, 'ResourceLoaderValidateJS'=> true, 'ResourceLoaderEnableJSProfiler'=> false, 'ResourceLoaderStorageEnabled'=> true, 'ResourceLoaderStorageVersion'=> 1, 'ResourceLoaderEnableSourceMapLinks'=> true, 'AllowSiteCSSOnRestrictedPages'=> false, 'VueDevelopmentMode'=> false, 'CodexDevelopmentDir'=> null, 'MetaNamespace'=> false, 'MetaNamespaceTalk'=> false, 'CanonicalNamespaceNames'=>[-2=> 'Media', -1=> 'Special', 0=> '', 1=> 'Talk', 2=> 'User', 3=> 'User_talk', 4=> 'Project', 5=> 'Project_talk', 6=> 'File', 7=> 'File_talk', 8=> 'MediaWiki', 9=> 'MediaWiki_talk', 10=> 'Template', 11=> 'Template_talk', 12=> 'Help', 13=> 'Help_talk', 14=> 'Category', 15=> 'Category_talk',], 'ExtraNamespaces'=>[], 'ExtraGenderNamespaces'=>[], 'NamespaceAliases'=>[], 'LegalTitleChars'=> ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 'CapitalLinks' => true, 'CapitalLinkOverrides' => [ ], 'NamespacesWithSubpages' => [ 1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 7 => true, 8 => true, 9 => true, 10 => true, 11 => true, 12 => true, 13 => true, 15 => true, ], 'ContentNamespaces' => [ 0, ], 'ShortPagesNamespaceExclusions' => [ ], 'ExtraSignatureNamespaces' => [ ], 'InvalidRedirectTargets' => [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect', 'Mylog', ], 'DisableHardRedirects' => false, 'FixDoubleRedirects' => false, 'LocalInterwikis' => [ ], 'InterwikiExpiry' => 10800, 'InterwikiCache' => false, 'InterwikiScopes' => 3, 'InterwikiFallbackSite' => 'wiki', 'RedirectSources' => false, 'SiteTypes' => [ 'mediawiki' => 'MediaWiki\\Site\\MediaWikiSite', ], 'MaxTocLevel' => 999, 'MaxPPNodeCount' => 1000000, 'MaxTemplateDepth' => 100, 'MaxPPExpandDepth' => 100, 'UrlProtocols' => [ 'bitcoin:', 'ftp: 'ftps: 'geo:', 'git: 'gopher: 'http: 'https: 'irc: 'ircs: 'magnet:', 'mailto:', 'matrix:', 'mms: 'news:', 'nntp: 'redis: 'sftp: 'sip:', 'sips:', 'sms:', 'ssh: 'svn: 'tel:', 'telnet: 'urn:', 'wikipedia: 'worldwind: 'xmpp:', ' ], 'CleanSignatures' => true, 'AllowExternalImages' => false, 'AllowExternalImagesFrom' => '', 'EnableImageWhitelist' => false, 'TidyConfig' => [ ], 'ParsoidSettings' => [ 'useSelser' => true, ], 'ParsoidExperimentalParserFunctionOutput' => false, 'UseLegacyMediaStyles' => false, 'RawHtml' => false, 'ExternalLinkTarget' => false, 'NoFollowLinks' => true, 'NoFollowNsExceptions' => [ ], 'NoFollowDomainExceptions' => [ 'mediawiki.org', ], 'RegisterInternalExternals' => false, 'ExternalLinksIgnoreDomains' => [ ], 'AllowDisplayTitle' => true, 'RestrictDisplayTitle' => true, 'ExpensiveParserFunctionLimit' => 100, 'PreprocessorCacheThreshold' => 1000, 'EnableScaryTranscluding' => false, 'TranscludeCacheExpiry' => 3600, 'EnableMagicLinks' => [ 'ISBN' => false, 'PMID' => false, 'RFC' => false, ], 'ParserEnableUserLanguage' => false, 'ArticleCountMethod' => 'link', 'ActiveUserDays' => 30, 'LearnerEdits' => 10, 'LearnerMemberSince' => 4, 'ExperiencedUserEdits' => 500, 'ExperiencedUserMemberSince' => 30, 'ManualRevertSearchRadius' => 15, 'RevertedTagMaxDepth' => 15, 'CentralIdLookupProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\CentralId\\LocalIdLookup', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', 'HideUserUtils', ], ], ], 'CentralIdLookupProvider' => 'local', 'UserRegistrationProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'ConnectionProvider', ], ], ], 'PasswordPolicy' => [ 'policies' => [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true, ], 'PasswordCannotBeSubstringInUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'PasswordCannotMatchDefaults' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true, ], 'PasswordNotInCommonList' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], ], ], 'checks' => [ 'MinimalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimalPasswordLength', ], 'MinimumPasswordLengthToLogin' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimumPasswordLengthToLogin', ], 'PasswordCannotBeSubstringInUsername' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotBeSubstringInUsername', ], 'PasswordCannotMatchDefaults' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotMatchDefaults', ], 'MaximalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMaximalPasswordLength', ], 'PasswordNotInCommonList' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordNotInCommonList', ], ], ], 'AuthManagerConfig' => null, 'AuthManagerAutoConfig' => [ 'preauth' => [ 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider', 'sort' => 0, ], ], 'primaryauth' => [ 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', 'UserOptionsLookup', ], 'args' => [ [ 'authoritative' => false, ], ], 'sort' => 0, ], 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'args' => [ [ 'authoritative' => true, ], ], 'sort' => 100, ], ], 'secondaryauth' => [ 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider', 'sort' => 100, ], 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'sort' => 200, ], ], ], 'RememberMe' => 'choose', 'ReauthenticateTime' => [ 'default' => 3600, ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'default' => true, ], 'ChangeCredentialsBlacklist' => [ 'MediaWiki\\Auth\\TemporaryPasswordAuthenticationRequest', ], 'RemoveCredentialsBlacklist' => [ 'MediaWiki\\Auth\\PasswordAuthenticationRequest', ], 'InvalidPasswordReset' => true, 'PasswordDefault' => 'pbkdf2', 'PasswordConfig' => [ 'A' => [ 'class' => 'MediaWiki\\Password\\MWOldPassword', ], 'B' => [ 'class' => 'MediaWiki\\Password\\MWSaltedPassword', ], 'pbkdf2-legacyA' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'A', 'pbkdf2', ], ], 'pbkdf2-legacyB' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'B', 'pbkdf2', ], ], 'bcrypt' => [ 'class' => 'MediaWiki\\Password\\BcryptPassword', 'cost' => 9, ], 'pbkdf2' => [ 'class' => 'MediaWiki\\Password\\Pbkdf2PasswordUsingOpenSSL', 'algo' => 'sha512', 'cost' => '30000', 'length' => '64', ], 'argon2' => [ 'class' => 'MediaWiki\\Password\\Argon2Password', 'algo' => 'auto', ], ], 'PasswordResetRoutes' => [ 'username' => true, 'email' => true, ], 'MaxSigChars' => 255, 'SignatureValidation' => 'warning', 'SignatureAllowedLintErrors' => [ 'obsolete-tag', ], 'MaxNameChars' => 255, 'ReservedUsernames' => [ 'MediaWiki default', 'Conversion script', 'Maintenance script', 'Template namespace initialisation script', 'ScriptImporter', 'Delete page script', 'Move page script', 'Command line script', 'Unknown user', 'msg:double-redirect-fixer', 'msg:usermessage-editor', 'msg:proxyblocker', 'msg:sorbs', 'msg:spambot_username', 'msg:autochange-username', ], 'DefaultUserOptions' => [ 'ccmeonemails' => 0, 'date' => 'default', 'diffonly' => 0, 'diff-type' => 'table', 'disablemail' => 0, 'editfont' => 'monospace', 'editondblclick' => 0, 'editrecovery' => 0, 'editsectiononrightclick' => 0, 'email-allow-new-users' => 1, 'enotifminoredits' => 0, 'enotifrevealaddr' => 0, 'enotifusertalkpages' => 1, 'enotifwatchlistpages' => 1, 'extendwatchlist' => 1, 'fancysig' => 0, 'forceeditsummary' => 0, 'forcesafemode' => 0, 'gender' => 'unknown', 'hidecategorization' => 1, 'hideminor' => 0, 'hidepatrolled' => 0, 'imagesize' => 2, 'minordefault' => 0, 'newpageshidepatrolled' => 0, 'nickname' => '', 'norollbackdiff' => 0, 'prefershttps' => 1, 'previewonfirst' => 0, 'previewontop' => 1, 'pst-cssjs' => 1, 'rcdays' => 7, 'rcenhancedfilters-disable' => 0, 'rclimit' => 50, 'requireemail' => 0, 'search-match-redirect' => true, 'search-special-page' => 'Search', 'search-thumbnail-extra-namespaces' => true, 'searchlimit' => 20, 'showhiddencats' => 0, 'shownumberswatching' => 1, 'showrollbackconfirmation' => 0, 'skin' => false, 'skin-responsive' => 1, 'thumbsize' => 5, 'underline' => 2, 'useeditwarning' => 1, 'uselivepreview' => 0, 'usenewrc' => 1, 'watchcreations' => 1, 'watchcreations-expiry' => 'infinite', 'watchdefault' => 1, 'watchdefault-expiry' => 'infinite', 'watchdeletion' => 0, 'watchlistdays' => 7, 'watchlisthideanons' => 0, 'watchlisthidebots' => 0, 'watchlisthidecategorization' => 1, 'watchlisthideliu' => 0, 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, 'watchlistreloadautomatically' => 0, 'watchlistunwatchlinks' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'watchuploads' => 1, 'watchrollback-expiry' => 'infinite', 'watchstar-expiry' => 'infinite', 'wlenhancedfilters-disable' => 0, 'wllimit' => 250, ], 'ConditionalUserOptions' => [ ], 'HiddenPrefs' => [ ], 'UserJsPrefLimit' => 100, 'InvalidUsernameCharacters' => '@:>=', 'UserrightsInterwikiDelimiter' => '@', 'SecureLogin' => false, 'AuthenticationTokenVersion' => null, 'SessionProviders' => [ 'MediaWiki\\Session\\CookieSessionProvider' => [ 'class' => 'MediaWiki\\Session\\CookieSessionProvider', 'args' => [ [ 'priority' => 30, ], ], 'services' => [ 'JwtCodec', 'UrlUtils', ], ], 'MediaWiki\\Session\\BotPasswordSessionProvider' => [ 'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider', 'args' => [ [ 'priority' => 75, ], ], 'services' => [ 'GrantsInfo', ], ], ], 'AutoCreateTempUser' => [ 'known' => false, 'enabled' => false, 'actions' => [ 'edit', ], 'genPattern' => '~$1', 'matchPattern' => null, 'reservedPattern' => '~$1', 'serialProvider' => [ 'type' => 'local', 'useYear' => true, ], 'serialMapping' => [ 'type' => 'readable-numeric', ], 'expireAfterDays' => 90, 'notifyBeforeExpirationDays' => 10, ], 'AutoblockExemptions' => [ ], 'AutoblockExpiry' => 86400, 'BlockAllowsUTEdit' => true, 'BlockCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 19, ], 'BlockDisablesLogin' => false, 'EnableMultiBlocks' => false, 'WhitelistRead' => false, 'WhitelistReadRegexp' => false, 'EmailConfirmToEdit' => false, 'HideIdentifiableRedirects' => true, 'GroupPermissions' => [ '*' => [ 'createaccount' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'viewmyprivateinfo' => true, 'editmyprivateinfo' => true, 'editmyoptions' => true, ], 'user' => [ 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'movefile' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'minoredit' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, 'editmyuserjsredirect' => true, 'sendemail' => true, 'applychangetags' => true, 'changetags' => true, 'viewmywatchlist' => true, 'editmywatchlist' => true, ], 'autoconfirmed' => [ 'autoconfirmed' => true, 'editsemiprotected' => true, ], 'bot' => [ 'bot' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'nominornewtalk' => true, 'autopatrol' => true, 'suppressredirect' => true, 'apihighlimits' => true, ], 'sysop' => [ 'block' => true, 'createaccount' => true, 'delete' => true, 'bigdelete' => true, 'deletedhistory' => true, 'deletedtext' => true, 'undelete' => true, 'editcontentmodel' => true, 'editinterface' => true, 'editsitejson' => true, 'edituserjson' => true, 'import' => true, 'importupload' => true, 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'patrol' => true, 'autopatrol' => true, 'protect' => true, 'editprotected' => true, 'rollback' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'unwatchedpages' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'blockemail' => true, 'markbotedits' => true, 'apihighlimits' => true, 'browsearchive' => true, 'noratelimit' => true, 'movefile' => true, 'unblockself' => true, 'suppressredirect' => true, 'mergehistory' => true, 'managechangetags' => true, 'deletechangetags' => true, ], 'interface-admin' => [ 'editinterface' => true, 'editsitecss' => true, 'editsitejson' => true, 'editsitejs' => true, 'editusercss' => true, 'edituserjson' => true, 'edituserjs' => true, ], 'bureaucrat' => [ 'userrights' => true, 'noratelimit' => true, 'renameuser' => true, ], 'suppress' => [ 'hideuser' => true, 'suppressrevision' => true, 'viewsuppressed' => true, 'suppressionlog' => true, 'deleterevision' => true, 'deletelogentry' => true, ], ], 'PrivilegedGroups' => [ 'bureaucrat', 'interface-admin', 'suppress', 'sysop', ], 'RevokePermissions' => [ ], 'GroupInheritsPermissions' => [ ], 'ImplicitGroups' => [ '*', 'user', 'autoconfirmed', ], 'GroupsAddToSelf' => [ ], 'GroupsRemoveFromSelf' => [ ], 'RestrictedGroups' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], 'NonincludableNamespaces' => [ ], 'AutoConfirmAge' => 0, 'AutoConfirmCount' => 0, 'Autopromote' => [ 'autoconfirmed' => [ '&', [ 1, null, ], [ 2, null, ], ], ], 'AutopromoteOnce' => [ 'onEdit' => [ ], ], 'AutopromoteOnceLogInRC' => true, 'AutopromoteOnceRCExcludedGroups' => [ ], 'AddGroups' => [ ], 'RemoveGroups' => [ ], 'AvailableRights' => [ ], 'ImplicitRights' => [ ], 'DeleteRevisionsLimit' => 0, 'DeleteRevisionsBatchSize' => 1000, 'HideUserContribLimit' => 1000, 'AccountCreationThrottle' => [ [ 'count' => 0, 'seconds' => 86400, ], ], 'TempAccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 600, ], [ 'count' => 6, 'seconds' => 86400, ], ], 'TempAccountNameAcquisitionThrottle' => [ [ 'count' => 60, 'seconds' => 86400, ], ], 'SpamRegex' => [ ], 'SummarySpamRegex' => [ ], 'EnableDnsBlacklist' => false, 'DnsBlacklistUrls' => [ ], 'ProxyList' => [ ], 'ProxyWhitelist' => [ ], 'SoftBlockRanges' => [ ], 'ApplyIpBlocksToXff' => false, 'RateLimits' => [ 'edit' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], 'user' => [ 90, 60, ], ], 'move' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], 'upload' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'rollback' => [ 'user' => [ 10, 60, ], 'newbie' => [ 5, 120, ], ], 'mailpassword' => [ 'ip' => [ 5, 3600, ], ], 'sendemail' => [ 'ip' => [ 5, 86400, ], 'newbie' => [ 5, 86400, ], 'user' => [ 20, 86400, ], ], 'changeemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'confirmemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'purge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'linkpurge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'renderfile' => [ 'ip' => [ 700, 30, ], 'user' => [ 700, 30, ], ], 'renderfile-nonstandard' => [ 'ip' => [ 70, 30, ], 'user' => [ 70, 30, ], ], 'stashedit' => [ 'ip' => [ 30, 60, ], 'newbie' => [ 30, 60, ], ], 'stashbasehtml' => [ 'ip' => [ 5, 60, ], 'newbie' => [ 5, 60, ], ], 'changetags' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'editcontentmodel' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], ], 'RateLimitsExcludedIPs' => [ ], 'PutIPinRC' => true, 'QueryPageDefaultLimit' => 50, 'ExternalQuerySources' => [ ], 'PasswordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300, ], [ 'count' => 150, 'seconds' => 172800, ], ], 'GrantPermissions' => [ 'basic' => [ 'autocreateaccount' => true, 'autoconfirmed' => true, 'autopatrol' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'nominornewtalk' => true, 'patrolmarks' => true, 'read' => true, 'unwatchedpages' => true, ], 'highvolume' => [ 'bot' => true, 'apihighlimits' => true, 'noratelimit' => true, 'markbotedits' => true, ], 'import' => [ 'import' => true, 'importupload' => true, ], 'editpage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, 'editusercss' => true, 'edituserjs' => true, 'editsitecss' => true, 'editsitejs' => true, ], 'createeditmovepage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createpage' => true, 'createtalk' => true, 'delete-redirect' => true, 'move' => true, 'move-rootuserpages' => true, 'move-subpages' => true, 'move-categorypages' => true, 'suppressredirect' => true, ], 'uploadfile' => [ 'upload' => true, 'reupload-own' => true, ], 'uploadeditmovefile' => [ 'upload' => true, 'reupload-own' => true, 'reupload' => true, 'reupload-shared' => true, 'upload_by_url' => true, 'movefile' => true, 'suppressredirect' => true, ], 'patrol' => [ 'patrol' => true, ], 'rollback' => [ 'rollback' => true, ], 'blockusers' => [ 'block' => true, 'blockemail' => true, ], 'viewdeleted' => [ 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, ], 'viewrestrictedlogs' => [ 'suppressionlog' => true, ], 'delete' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, 'delete' => true, 'bigdelete' => true, 'deletelogentry' => true, 'deleterevision' => true, 'undelete' => true, ], 'oversight' => [ 'suppressrevision' => true, 'viewsuppressed' => true, ], 'protect' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], ], 'GrantPermissionGroups' => [ 'basic' => 'hidden', 'editpage' => 'page-interaction', 'createeditmovepage' => 'page-interaction', 'editprotected' => 'page-interaction', 'patrol' => 'page-interaction', 'uploadfile' => 'file-interaction', 'uploadeditmovefile' => 'file-interaction', 'sendemail' => 'email', 'viewmywatchlist' => 'watchlist-interaction', 'editviewmywatchlist' => 'watchlist-interaction', 'editmycssjs' => 'customization', 'editmyoptions' => 'customization', 'editinterface' => 'administration', 'editsiteconfig' => 'administration', 'rollback' => 'administration', 'blockusers' => 'administration', 'delete' => 'administration', 'viewdeleted' => 'administration', 'viewrestrictedlogs' => 'administration', 'protect' => 'administration', 'oversight' => 'administration', 'createaccount' => 'administration', 'mergehistory' => 'administration', 'import' => 'administration', 'highvolume' => 'high-volume', 'privateinfo' => 'private-information', ], 'GrantRiskGroups' => [ 'basic' => 'low', 'editpage' => 'low', 'createeditmovepage' => 'low', 'editprotected' => 'vandalism', 'patrol' => 'low', 'uploadfile' => 'low', 'uploadeditmovefile' => 'low', 'sendemail' => 'security', 'viewmywatchlist' => 'low', 'editviewmywatchlist' => 'low', 'editmycssjs' => 'security', 'editmyoptions' => 'security', 'editinterface' => 'vandalism', 'editsiteconfig' => 'security', 'rollback' => 'low', 'blockusers' => 'vandalism', 'delete' => 'vandalism', 'viewdeleted' => 'vandalism', 'viewrestrictedlogs' => 'security', 'protect' => 'vandalism', 'oversight' => 'security', 'createaccount' => 'low', 'mergehistory' => 'vandalism', 'import' => 'security', 'highvolume' => 'low', 'privateinfo' => 'low', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => false, 'CSPFalsePositiveUrls' => [ 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'chrome-extension' => true, ], 'AllowCrossOrigin' => false, 'RestAllowCrossOriginCookieAuth' => false, 'SessionSecret' => false, 'CookieExpiration' => 2592000, 'ExtendedLoginCookieExpiration' => 15552000, 'SessionCookieJwtExpiration' => 14400, 'CookieDomain' => '', 'CookiePath' => '/', 'CookieSecure' => 'detect', 'CookiePrefix' => false, 'CookieHttpOnly' => true, 'CookieSameSite' => null, 'CacheVaryCookies' => [ ], 'SessionName' => false, 'CookieSetOnAutoblock' => true, 'CookieSetOnIpBlock' => true, 'DebugLogFile' => '', 'DebugLogPrefix' => '', 'DebugRedirects' => false, 'DebugRawPage' => false, 'DebugComments' => false, 'DebugDumpSql' => false, 'TrxProfilerLimits' => [ 'GET' => [ 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'POST-nonwrite' => [ 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 10000, 'maxAffected' => 1000, 'masterConns' => 0, 'writes' => 0, ], 'PostSend-POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'JobRunner' => [ 'readQueryTime' => 30, 'writeQueryTime' => 5, 'readQueryRows' => 100000, 'maxAffected' => 500, ], 'Maintenance' => [ 'writeQueryTime' => 5, 'maxAffected' => 1000, ], ], 'DebugLogGroups' => [ ], 'MWLoggerDefaultSpi' => [ 'class' => 'MediaWiki\\Logger\\LegacySpi', ], 'ShowDebug' => false, 'SpecialVersionShowHooks' => false, 'ShowExceptionDetails' => false, 'LogExceptionBacktrace' => true, 'PropagateErrors' => true, 'ShowHostnames' => false, 'OverrideHostname' => false, 'DevelopmentWarnings' => false, 'DeprecationReleaseLimit' => false, 'Profiler' => [ ], 'StatsdServer' => false, 'StatsdMetricPrefix' => 'MediaWiki', 'StatsTarget' => null, 'StatsFormat' => null, 'StatsPrefix' => 'mediawiki', 'OpenTelemetryConfig' => null, 'PageInfoTransclusionLimit' => 50, 'EnableJavaScriptTest' => false, 'CachePrefix' => false, 'DebugToolbar' => false, 'DisableTextSearch' => false, 'AdvancedSearchHighlighting' => false, 'SearchHighlightBoundaries' => '[\\p{Z}\\p{P}\\p{C}]', 'OpenSearchTemplates' => [ 'application/x-suggestions+json' => false, 'application/x-suggestions+xml' => false, ], 'OpenSearchDefaultLimit' => 10, 'OpenSearchDescriptionLength' => 100, 'SearchSuggestCacheExpiry' => 1200, 'DisableSearchUpdate' => false, 'NamespacesToBeSearchedDefault' => [ true, ], 'DisableInternalSearch' => false, 'SearchForwardUrl' => null, 'SitemapNamespaces' => false, 'SitemapNamespacesPriorities' => false, 'SitemapApiConfig' => [ ], 'SpecialSearchFormOptions' => [ ], 'SearchMatchRedirectPreference' => false, 'SearchRunSuggestedQuery' => true, 'Diff3' => '/usr/bin/diff3', 'Diff' => '/usr/bin/diff', 'PreviewOnOpenNamespaces' => [ 14 => true, ], 'UniversalEditButton' => true, 'UseAutomaticEditSummaries' => true, 'CommandLineDarkBg' => false, 'ReadOnly' => null, 'ReadOnlyWatchedItemStore' => false, 'ReadOnlyFile' => false, 'UpgradeKey' => false, 'GitBin' => '/usr/bin/git', 'GitRepositoryViewers' => [ 'https: 'ssh: ], 'InstallerInitialPages' => [ [ 'titlemsg' => 'mainpage', 'text' => '{{subst:int:mainpagetext}}{{subst:int:mainpagedocfooter}}', ], ], 'RCMaxAge' => 7776000, 'WatchersMaxAge' => 15552000, 'UnwatchedPageSecret' => 1, 'RCFilterByAge' => false, 'RCLinkLimits' => [ 50, 100, 250, 500, ], 'RCLinkDays' => [ 1, 3, 7, 14, 30, ], 'RCFeeds' => [ ], 'RCEngines' => [ 'redis' => 'MediaWiki\\RCFeed\\RedisPubSubFeedEngine', 'udp' => 'MediaWiki\\RCFeed\\UDPRCFeedEngine', ], 'RCWatchCategoryMembership' => false, 'UseRCPatrol' => true, 'StructuredChangeFiltersLiveUpdatePollingRate' => 3, 'UseNPPatrol' => true, 'UseFilePatrol' => true, 'Feed' => true, 'FeedLimit' => 50, 'FeedCacheTimeout' => 60, 'FeedDiffCutoff' => 32768, 'OverrideSiteFeed' => [ ], 'FeedClasses' => [ 'rss' => 'MediaWiki\\Feed\\RSSFeed', 'atom' => 'MediaWiki\\Feed\\AtomFeed', ], 'AdvertisedFeedTypes' => [ 'atom', ], 'RCShowWatchingUsers' => false, 'RCShowChangedSize' => true, 'RCChangedSizeThreshold' => 500, 'ShowUpdatedMarker' => true, 'DisableAnonTalk' => false, 'UseTagFilter' => true, 'SoftwareTags' => [ 'mw-contentmodelchange' => true, 'mw-new-redirect' => true, 'mw-removed-redirect' => true, 'mw-changed-redirect-target' => true, 'mw-blank' => true, 'mw-replace' => true, 'mw-recreated' => true, 'mw-rollback' => true, 'mw-undo' => true, 'mw-manual-revert' => true, 'mw-reverted' => true, 'mw-server-side-upload' => true, 'mw-ipblock-appeal' => true, ], 'UnwatchedPageThreshold' => false, 'RecentChangesFlags' => [ 'newpage' => [ 'letter' => 'newpageletter', 'title' => 'recentchanges-label-newpage', 'legend' => 'recentchanges-legend-newpage', 'grouping' => 'any', ], 'minor' => [ 'letter' => 'minoreditletter', 'title' => 'recentchanges-label-minor', 'legend' => 'recentchanges-legend-minor', 'class' => 'minoredit', 'grouping' => 'all', ], 'bot' => [ 'letter' => 'boteditletter', 'title' => 'recentchanges-label-bot', 'legend' => 'recentchanges-legend-bot', 'class' => 'botedit', 'grouping' => 'all', ], 'unpatrolled' => [ 'letter' => 'unpatrolledletter', 'title' => 'recentchanges-label-unpatrolled', 'legend' => 'recentchanges-legend-unpatrolled', 'grouping' => 'any', ], ], 'WatchlistExpiry' => false, 'EnableWatchlistLabels' => false, 'WatchlistLabelsMaxPerUser' => 100, 'WatchlistPurgeRate' => 0.1, 'WatchlistExpiryMaxDuration' => '1 year', 'EnableChangesListQueryPartitioning' => false, 'RightsPage' => null, 'RightsUrl' => null, 'RightsText' => null, 'RightsIcon' => null, 'UseCopyrightUpload' => false, 'MaxCredits' => 0, 'ShowCreditsIfMax' => true, 'ImportSources' => [ ], 'ImportTargetNamespace' => null, 'ExportAllowHistory' => true, 'ExportMaxHistory' => 0, 'ExportAllowListContributors' => false, 'ExportMaxLinkDepth' => 0, 'ExportFromNamespaces' => false, 'ExportAllowAll' => false, 'ExportPagelistLimit' => 5000, 'XmlDumpSchemaVersion' => '0.11', 'WikiFarmSettingsDirectory' => null, 'WikiFarmSettingsExtension' => 'yaml', 'ExtensionFunctions' => [ ], 'ExtensionMessagesFiles' => [ ], 'MessagesDirs' => [ ], 'TranslationAliasesDirs' => [ ], 'ExtensionEntryPointListFiles' => [ ], 'EnableParserLimitReporting' => true, 'ValidSkinNames' => [ ], 'SpecialPages' => [ ], 'ExtensionCredits' => [ ], 'Hooks' => [ ], 'ServiceWiringFiles' => [ ], 'JobClasses' => [ 'deletePage' => 'MediaWiki\\Page\\DeletePageJob', 'refreshLinks' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'deleteLinks' => 'MediaWiki\\Page\\DeleteLinksJob', 'htmlCacheUpdate' => 'MediaWiki\\JobQueue\\Jobs\\HTMLCacheUpdateJob', 'sendMail' => [ 'class' => 'MediaWiki\\Mail\\EmaillingJob', 'services' => [ 'Emailer', ], ], 'enotifNotify' => [ 'class' => 'MediaWiki\\RecentChanges\\RecentChangeNotifyJob', 'services' => [ 'RecentChangeLookup', ], ], 'fixDoubleRedirect' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\DoubleRedirectJob', 'services' => [ 'RevisionLookup', 'MagicWordFactory', 'WikiPageFactory', ], 'needsPage' => true, ], 'AssembleUploadChunks' => 'MediaWiki\\JobQueue\\Jobs\\AssembleUploadChunksJob', 'PublishStashedFile' => 'MediaWiki\\JobQueue\\Jobs\\PublishStashedFileJob', 'ThumbnailRender' => 'MediaWiki\\JobQueue\\Jobs\\ThumbnailRenderJob', 'UploadFromUrl' => 'MediaWiki\\JobQueue\\Jobs\\UploadFromUrlJob', 'recentChangesUpdate' => 'MediaWiki\\RecentChanges\\RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'refreshLinksDynamic' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'activityUpdateJob' => 'MediaWiki\\Watchlist\\ActivityUpdateJob', 'categoryMembershipChange' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', 'cdnPurge' => 'MediaWiki\\JobQueue\\Jobs\\CdnPurgeJob', 'userGroupExpiry' => 'MediaWiki\\User\\UserGroupExpiryJob', 'clearWatchlistNotifications' => 'MediaWiki\\Watchlist\\ClearWatchlistNotificationsJob', 'userOptionsUpdate' => 'MediaWiki\\User\\Options\\UserOptionsUpdateJob', 'revertedTagUpdate' => 'MediaWiki\\JobQueue\\Jobs\\RevertedTagUpdateJob', 'null' => 'MediaWiki\\JobQueue\\Jobs\\NullJob', 'userEditCountInit' => 'MediaWiki\\User\\UserEditCountInitJob', 'parsoidCachePrewarm' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\ParsoidCachePrewarmJob', 'services' => [ 'ParserOutputAccess', 'PageStore', 'RevisionLookup', 'ParsoidSiteConfig', ], 'needsPage' => false, ], 'renameUserTable' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], 'renameUserDerived' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserDerivedJob', 'services' => [ 'RenameUserFactory', 'UserFactory', ], ], 'renameUser' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], ], 'JobTypesExcludedFromDefaultQueue' => [ 'AssembleUploadChunks', 'PublishStashedFile', 'UploadFromUrl', ], 'JobBackoffThrottling' => [ ], 'JobTypeConf' => [ 'default' => [ 'class' => 'MediaWiki\\JobQueue\\JobQueueDB', 'order' => 'random', 'claimTTL' => 3600, ], ], 'JobQueueIncludeInMaxLagFactor' => false, 'SpecialPageCacheUpdates' => [ 'Statistics' => [ 'MediaWiki\\Deferred\\SiteStatsUpdate', 'cacheUpdate', ], ], 'PagePropLinkInvalidations' => [ 'hiddencat' => 'categorylinks', ], 'CategoryMagicGallery' => true, 'CategoryPagingLimit' => 200, 'CategoryCollation' => 'uppercase', 'TempCategoryCollations' => [ ], 'SortedCategories' => false, 'TrackingCategories' => [ ], 'LogTypes' => [ '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'import', 'interwiki', 'patrol', 'merge', 'suppress', 'tag', 'managetags', 'contentmodel', 'renameuser', ], 'LogRestrictions' => [ 'suppress' => 'suppressionlog', ], 'FilterLogTypes' => [ 'patrol' => true, 'tag' => true, 'newusers' => false, ], 'LogNames' => [ '' => 'all-logs-page', 'block' => 'blocklogpage', 'protect' => 'protectlogpage', 'rights' => 'rightslog', 'delete' => 'dellogpage', 'upload' => 'uploadlogpage', 'move' => 'movelogpage', 'import' => 'importlogpage', 'patrol' => 'patrol-log-page', 'merge' => 'mergelog', 'suppress' => 'suppressionlog', ], 'LogHeaders' => [ '' => 'alllogstext', 'block' => 'blocklogtext', 'delete' => 'dellogpagetext', 'import' => 'importlogpagetext', 'merge' => 'mergelogpagetext', 'move' => 'movelogpagetext', 'patrol' => 'patrol-log-header', 'protect' => 'protectlogtext', 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', 'upload' => 'uploadlogpagetext', ], 'LogActions' => [ ], 'LogActionsHandlers' => [ 'block/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/unblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'contentmodel/change' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'contentmodel/new' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'delete/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir2' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/restore' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'import/interwiki' => 'MediaWiki\\Logging\\ImportLogFormatter', 'import/upload' => 'MediaWiki\\Logging\\ImportLogFormatter', 'interwiki/iw_add' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_delete' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_edit' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'managetags/activate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/create' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/deactivate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/delete' => 'MediaWiki\\Logging\\LogFormatter', 'merge/merge' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'merge/merge-into' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move_redir' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'patrol/patrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'patrol/autopatrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'protect/modify' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/move_prot' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/protect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/unprotect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'renameuser/renameuser' => [ 'class' => 'MediaWiki\\Logging\\RenameuserLogFormatter', 'services' => [ 'TitleParser', ], ], 'rights/autopromote' => 'MediaWiki\\Logging\\RightsLogFormatter', 'rights/rights' => 'MediaWiki\\Logging\\RightsLogFormatter', 'suppress/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'tag/update' => 'MediaWiki\\Logging\\TagLogFormatter', 'upload/overwrite' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/revert' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/upload' => 'MediaWiki\\Logging\\UploadLogFormatter', ], 'ActionFilteredLogs' => [ 'block' => [ 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], 'unblock' => [ 'unblock', ], ], 'contentmodel' => [ 'change' => [ 'change', ], 'new' => [ 'new', ], ], 'delete' => [ 'delete' => [ 'delete', ], 'delete_redir' => [ 'delete_redir', 'delete_redir2', ], 'restore' => [ 'restore', ], 'event' => [ 'event', ], 'revision' => [ 'revision', ], ], 'import' => [ 'interwiki' => [ 'interwiki', ], 'upload' => [ 'upload', ], ], 'managetags' => [ 'create' => [ 'create', ], 'delete' => [ 'delete', ], 'activate' => [ 'activate', ], 'deactivate' => [ 'deactivate', ], ], 'move' => [ 'move' => [ 'move', ], 'move_redir' => [ 'move_redir', ], ], 'newusers' => [ 'create' => [ 'create', 'newusers', ], 'create2' => [ 'create2', ], 'autocreate' => [ 'autocreate', ], 'byemail' => [ 'byemail', ], ], 'protect' => [ 'protect' => [ 'protect', ], 'modify' => [ 'modify', ], 'unprotect' => [ 'unprotect', ], 'move_prot' => [ 'move_prot', ], ], 'rights' => [ 'rights' => [ 'rights', ], 'autopromote' => [ 'autopromote', ], ], 'suppress' => [ 'event' => [ 'event', ], 'revision' => [ 'revision', ], 'delete' => [ 'delete', ], 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], ], 'upload' => [ 'upload' => [ 'upload', ], 'overwrite' => [ 'overwrite', ], 'revert' => [ 'revert', ], ], ], 'NewUserLog' => true, 'PageCreationLog' => true, 'AllowSpecialInclusion' => true, 'DisableQueryPageUpdate' => false, 'CountCategorizedImagesAsUsed' => false, 'MaxRedirectLinksRetrieved' => 500, 'RangeContributionsCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 32, ], 'Actions' => [ ], 'DefaultRobotPolicy' => 'index,follow', 'NamespaceRobotPolicies' => [ ], 'ArticleRobotPolicies' => [ ], 'ExemptFromUserRobotsControl' => null, 'DebugAPI' => false, 'APIModules' => [ ], 'APIFormatModules' => [ ], 'APIMetaModules' => [ ], 'APIPropModules' => [ ], 'APIListModules' => [ ], 'APIMaxDBRows' => 5000, 'APIMaxResultSize' => 8388608, 'APIMaxUncachedDiffs' => 1, 'APIMaxLagThreshold' => 7, 'APICacheHelpTimeout' => 3600, 'APIUselessQueryPages' => [ 'MIMEsearch', 'LinkSearch', ], 'AjaxLicensePreview' => true, 'CrossSiteAJAXdomains' => [ ], 'CrossSiteAJAXdomainExceptions' => [ ], 'AllowedCorsHeaders' => [ 'Accept', 'Accept-Language', 'Content-Language', 'Content-Type', 'Accept-Encoding', 'DNT', 'Origin', 'User-Agent', 'Api-User-Agent', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], 'MaxShellMemory' => 307200, 'MaxShellFileSize' => 102400, 'MaxShellTime' => 180, 'MaxShellWallClockTime' => 180, 'ShellCgroup' => false, 'PhpCli' => '/usr/bin/php', 'ShellRestrictionMethod' => 'autodetect', 'ShellboxUrls' => [ 'default' => null, ], 'ShellboxSecretKey' => null, 'ShellboxShell' => '/bin/sh', 'HTTPTimeout' => 25, 'HTTPConnectTimeout' => 5.0, 'HTTPMaxTimeout' => 0, 'HTTPMaxConnectTimeout' => 0, 'HTTPImportTimeout' => 25, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'VirtualRestConfig' => [ 'paths' => [ ], 'modules' => [ ], 'global' => [ 'timeout' => 360, 'forwardCookies' => false, 'HTTPProxy' => null, ], ], 'EventRelayerConfig' => [ 'default' => [ 'class' => 'Wikimedia\\EventRelayer\\EventRelayerNull', ], ], 'Pingback' => false, 'OriginTrials' => [ ], 'ReportToExpiry' => 86400, 'ReportToEndpoints' => [ ], 'FeaturePolicyReportOnly' => [ ], 'SkinsPreferred' => [ 'vector-2022', 'vector', ], 'SpecialContributeSkinsEnabled' => [ ], 'SpecialContributeNewPageTarget' => null, 'EnableEditRecovery' => false, 'EditRecoveryExpiry' => 2592000, 'UseCodexSpecialBlock' => false, 'ShowLogoutConfirmation' => false, 'EnableProtectionIndicators' => true, 'OutputPipelineStages' => [ ], 'FeatureShutdown' => [ ], 'CloneArticleParserOutput' => true, 'UseLeximorph' => false, 'UsePostprocCache' => false, 'ParserOptionsLogUnsafeSampleRate' => 0, ], 'type' => [ 'ConfigRegistry' => 'object', 'AssumeProxiesUseDefaultProtocolPorts' => 'boolean', 'ForceHTTPS' => 'boolean', 'ExtensionDirectory' => [ 'string', 'null', ], 'StyleDirectory' => [ 'string', 'null', ], 'UploadDirectory' => [ 'string', 'boolean', 'null', ], 'Logos' => [ 'object', 'boolean', ], 'ReferrerPolicy' => [ 'array', 'string', 'boolean', ], 'ActionPaths' => 'object', 'MainPageIsDomainRoot' => 'boolean', 'ImgAuthUrlPathMap' => 'object', 'LocalFileRepo' => 'object', 'ForeignFileRepos' => 'array', 'UseSharedUploads' => 'boolean', 'SharedUploadDirectory' => [ 'string', 'null', ], 'SharedUploadPath' => [ 'string', 'null', ], 'HashedSharedUploadDirectory' => 'boolean', 'FetchCommonsDescriptions' => 'boolean', 'SharedUploadDBname' => [ 'boolean', 'string', ], 'SharedUploadDBprefix' => 'string', 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', 'CopyUploadsDomains' => 'array', 'CopyUploadTimeout' => [ 'boolean', 'integer', ], 'SharedThumbnailScriptPath' => [ 'string', 'boolean', ], 'HashedUploadDirectory' => 'boolean', 'CSPUploadEntryPoint' => 'boolean', 'FileExtensions' => 'array', 'ProhibitedFileExtensions' => 'array', 'MimeTypeExclusions' => 'array', 'TrustedMediaFormats' => 'array', 'MediaHandlers' => 'object', 'NativeImageLazyLoading' => 'boolean', 'ParserTestMediaHandlers' => 'object', 'MaxInterlacingAreas' => 'object', 'SVGConverters' => 'object', 'SVGNativeRendering' => [ 'string', 'boolean', ], 'MaxImageArea' => [ 'string', 'integer', 'boolean', ], 'TiffThumbnailType' => 'array', 'GenerateThumbnailOnParse' => 'boolean', 'EnableAutoRotation' => [ 'boolean', 'null', ], 'Antivirus' => [ 'string', 'null', ], 'AntivirusSetup' => 'object', 'MimeDetectorCommand' => [ 'string', 'null', ], 'XMLMimeTypes' => 'object', 'ImageLimits' => 'array', 'ThumbLimits' => 'array', 'ThumbnailNamespaces' => 'array', 'ThumbnailSteps' => [ 'array', 'null', ], 'ThumbnailStepsRatio' => [ 'number', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'UserEmailConfirmationUseHTML' => 'boolean', 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ImageLinksSchemaMigrationStage' => 'integer', 'ExternalLinksDomainGaps' => 'object', 'ContentHandlers' => 'object', 'NamespaceContentModels' => 'object', 'TextModelsToParse' => 'array', 'ExternalStores' => 'array', 'ExternalServers' => 'object', 'DefaultExternalStore' => [ 'array', 'boolean', ], 'RevisionCacheExpiry' => 'integer', 'PageLanguageUseDB' => 'boolean', 'DiffEngine' => [ 'string', 'null', ], 'ExternalDiffEngine' => [ 'string', 'boolean', ], 'Wikidiff2Options' => 'object', 'RequestTimeLimit' => [ 'integer', 'null', ], 'CriticalSectionTimeLimit' => 'number', 'PoolCounterConf' => [ 'object', 'null', ], 'PoolCountClientConf' => 'object', 'MaxUserDBWriteDuration' => [ 'integer', 'boolean', ], 'MaxJobDBWriteDuration' => [ 'integer', 'boolean', ], 'MultiShardSiteStats' => 'boolean', 'ObjectCaches' => 'object', 'WANObjectCache' => 'object', 'MicroStashType' => [ 'string', 'integer', ], 'ParsoidCacheConfig' => 'object', 'ParsoidSelectiveUpdateSampleRate' => 'integer', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'PHPSessionHandling' => 'string', 'SuspiciousIpExpiry' => [ 'integer', 'boolean', ], 'MemCachedServers' => 'array', 'LocalisationCacheConf' => 'object', 'ExtensionInfoMTime' => [ 'integer', 'boolean', ], 'CdnServers' => 'object', 'CdnServersNoPurge' => 'object', 'HTCPRouting' => 'object', 'GrammarForms' => 'object', 'ExtraInterlanguageLinkPrefixes' => 'array', 'InterlanguageLinkCodeMap' => 'object', 'ExtraLanguageNames' => 'object', 'ExtraLanguageCodes' => 'object', 'DummyLanguageCodes' => 'object', 'DisabledVariants' => 'object', 'ForceUIMsgAsContentMsg' => 'object', 'RawHtmlMessages' => 'array', 'OverrideUcfirstCharacters' => 'object', 'XhtmlNamespaces' => 'object', 'BrowserFormatDetection' => 'string', 'SkinMetaTags' => 'object', 'SkipSkins' => 'object', 'FragmentMode' => 'array', 'FooterIcons' => 'object', 'InterwikiLogoOverride' => 'array', 'ResourceModules' => 'object', 'ResourceModuleSkinStyles' => 'object', 'ResourceLoaderSources' => 'object', 'ResourceLoaderMaxage' => 'object', 'ResourceLoaderMaxQueryLength' => [ 'integer', 'boolean', ], 'CanonicalNamespaceNames' => 'object', 'ExtraNamespaces' => 'object', 'ExtraGenderNamespaces' => 'object', 'NamespaceAliases' => 'object', 'CapitalLinkOverrides' => 'object', 'NamespacesWithSubpages' => 'object', 'ContentNamespaces' => 'array', 'ShortPagesNamespaceExclusions' => 'array', 'ExtraSignatureNamespaces' => 'array', 'InvalidRedirectTargets' => 'array', 'LocalInterwikis' => 'array', 'InterwikiCache' => [ 'boolean', 'object', ], 'SiteTypes' => 'object', 'UrlProtocols' => 'array', 'TidyConfig' => 'object', 'ParsoidSettings' => 'object', 'ParsoidExperimentalParserFunctionOutput' => 'boolean', 'NoFollowNsExceptions' => 'array', 'NoFollowDomainExceptions' => 'array', 'ExternalLinksIgnoreDomains' => 'array', 'EnableMagicLinks' => 'object', 'ManualRevertSearchRadius' => 'integer', 'RevertedTagMaxDepth' => 'integer', 'CentralIdLookupProviders' => 'object', 'CentralIdLookupProvider' => 'string', 'UserRegistrationProviders' => 'object', 'PasswordPolicy' => 'object', 'AuthManagerConfig' => [ 'object', 'null', ], 'AuthManagerAutoConfig' => 'object', 'RememberMe' => 'string', 'ReauthenticateTime' => 'object', 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => 'object', 'ChangeCredentialsBlacklist' => 'array', 'RemoveCredentialsBlacklist' => 'array', 'PasswordConfig' => 'object', 'PasswordResetRoutes' => 'object', 'SignatureAllowedLintErrors' => 'array', 'ReservedUsernames' => 'array', 'DefaultUserOptions' => 'object', 'ConditionalUserOptions' => 'object', 'HiddenPrefs' => 'array', 'UserJsPrefLimit' => 'integer', 'AuthenticationTokenVersion' => [ 'string', 'null', ], 'SessionProviders' => 'object', 'AutoCreateTempUser' => 'object', 'AutoblockExemptions' => 'array', 'BlockCIDRLimit' => 'object', 'EnableMultiBlocks' => 'boolean', 'GroupPermissions' => 'object', 'PrivilegedGroups' => 'array', 'RevokePermissions' => 'object', 'GroupInheritsPermissions' => 'object', 'ImplicitGroups' => 'array', 'GroupsAddToSelf' => 'object', 'GroupsRemoveFromSelf' => 'object', 'RestrictedGroups' => 'object', 'RestrictionTypes' => 'array', 'RestrictionLevels' => 'array', 'CascadingRestrictionLevels' => 'array', 'SemiprotectedRestrictionLevels' => 'array', 'NamespaceProtection' => 'object', 'NonincludableNamespaces' => 'object', 'Autopromote' => 'object', 'AutopromoteOnce' => 'object', 'AutopromoteOnceRCExcludedGroups' => 'array', 'AddGroups' => 'object', 'RemoveGroups' => 'object', 'AvailableRights' => 'array', 'ImplicitRights' => 'array', 'AccountCreationThrottle' => [ 'integer', 'array', ], 'TempAccountCreationThrottle' => 'array', 'TempAccountNameAcquisitionThrottle' => 'array', 'SpamRegex' => 'array', 'SummarySpamRegex' => 'array', 'DnsBlacklistUrls' => 'array', 'ProxyList' => [ 'string', 'array', ], 'ProxyWhitelist' => 'array', 'SoftBlockRanges' => 'array', 'RateLimits' => 'object', 'RateLimitsExcludedIPs' => 'array', 'ExternalQuerySources' => 'object', 'PasswordAttemptThrottle' => 'array', 'GrantPermissions' => 'object', 'GrantPermissionGroups' => 'object', 'GrantRiskGroups' => 'object', 'EnableBotPasswords' => 'boolean', 'BotPasswordsCluster' => [ 'string', 'boolean', ], 'BotPasswordsDatabase' => [ 'string', 'boolean', ], 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPFalsePositiveUrls' => 'object', 'AllowCrossOrigin' => 'boolean', 'RestAllowCrossOriginCookieAuth' => 'boolean', 'CookieSameSite' => [ 'string', 'null', ], 'CacheVaryCookies' => 'array', 'TrxProfilerLimits' => 'object', 'DebugLogGroups' => 'object', 'MWLoggerDefaultSpi' => 'object', 'Profiler' => 'object', 'StatsTarget' => [ 'string', 'null', ], 'StatsFormat' => [ 'string', 'null', ], 'StatsPrefix' => 'string', 'OpenTelemetryConfig' => [ 'object', 'null', ], 'OpenSearchTemplates' => 'object', 'NamespacesToBeSearchedDefault' => 'object', 'SitemapNamespaces' => [ 'boolean', 'array', ], 'SitemapNamespacesPriorities' => [ 'boolean', 'object', ], 'SitemapApiConfig' => 'object', 'SpecialSearchFormOptions' => 'object', 'SearchMatchRedirectPreference' => 'boolean', 'SearchRunSuggestedQuery' => 'boolean', 'PreviewOnOpenNamespaces' => 'object', 'ReadOnlyWatchedItemStore' => 'boolean', 'GitRepositoryViewers' => 'object', 'InstallerInitialPages' => 'array', 'RCLinkLimits' => 'array', 'RCLinkDays' => 'array', 'RCFeeds' => 'object', 'RCEngines' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchlistLabels' => 'boolean', 'WatchlistLabelsMaxPerUser' => 'integer', 'WatchlistPurgeRate' => 'number', 'WatchlistExpiryMaxDuration' => [ 'string', 'null', ], 'EnableChangesListQueryPartitioning' => 'boolean', 'ImportSources' => 'object', 'ExtensionFunctions' => 'array', 'ExtensionMessagesFiles' => 'object', 'MessagesDirs' => 'object', 'TranslationAliasesDirs' => 'object', 'ExtensionEntryPointListFiles' => 'object', 'ValidSkinNames' => 'object', 'SpecialPages' => 'object', 'ExtensionCredits' => 'object', 'Hooks' => 'object', 'ServiceWiringFiles' => 'array', 'JobClasses' => 'object', 'JobTypesExcludedFromDefaultQueue' => 'array', 'JobBackoffThrottling' => 'object', 'JobTypeConf' => 'object', 'SpecialPageCacheUpdates' => 'object', 'PagePropLinkInvalidations' => 'object', 'TempCategoryCollations' => 'array', 'SortedCategories' => 'boolean', 'TrackingCategories' => 'array', 'LogTypes' => 'array', 'LogRestrictions' => 'object', 'FilterLogTypes' => 'object', 'LogNames' => 'object', 'LogHeaders' => 'object', 'LogActions' => 'object', 'LogActionsHandlers' => 'object', 'ActionFilteredLogs' => 'object', 'RangeContributionsCIDRLimit' => 'object', 'Actions' => 'object', 'NamespaceRobotPolicies' => 'object', 'ArticleRobotPolicies' => 'object', 'ExemptFromUserRobotsControl' => [ 'array', 'null', ], 'APIModules' => 'object', 'APIFormatModules' => 'object', 'APIMetaModules' => 'object', 'APIPropModules' => 'object', 'APIListModules' => 'object', 'APIUselessQueryPages' => 'array', 'CrossSiteAJAXdomains' => 'object', 'CrossSiteAJAXdomainExceptions' => 'object', 'AllowedCorsHeaders' => 'array', 'RestAPIAdditionalRouteFiles' => 'array', 'RestSandboxSpecs' => 'object', 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'VirtualRestConfig' => 'object', 'EventRelayerConfig' => 'object', 'Pingback' => 'boolean', 'OriginTrials' => 'array', 'ReportToExpiry' => 'integer', 'ReportToEndpoints' => 'array', 'FeaturePolicyReportOnly' => 'array', 'SkinsPreferred' => 'array', 'SpecialContributeSkinsEnabled' => 'array', 'SpecialContributeNewPageTarget' => [ 'string', 'null', ], 'EnableEditRecovery' => 'boolean', 'EditRecoveryExpiry' => 'integer', 'UseCodexSpecialBlock' => 'boolean', 'ShowLogoutConfirmation' => 'boolean', 'EnableProtectionIndicators' => 'boolean', 'OutputPipelineStages' => 'object', 'FeatureShutdown' => 'array', 'CloneArticleParserOutput' => 'boolean', 'UseLeximorph' => 'boolean', 'UsePostprocCache' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', ], 'mergeStrategy' => [ 'TiffThumbnailType' => 'replace', 'LBFactoryConf' => 'replace', 'InterwikiCache' => 'replace', 'PasswordPolicy' => 'array_replace_recursive', 'AuthManagerAutoConfig' => 'array_plus_2d', 'GroupPermissions' => 'array_plus_2d', 'RevokePermissions' => 'array_plus_2d', 'AddGroups' => 'array_merge_recursive', 'RemoveGroups' => 'array_merge_recursive', 'RateLimits' => 'array_plus_2d', 'GrantPermissions' => 'array_plus_2d', 'MWLoggerDefaultSpi' => 'replace', 'Profiler' => 'replace', 'Hooks' => 'array_merge_recursive', 'VirtualRestConfig' => 'array_plus_2d', ], 'dynamicDefault' => [ 'UsePathInfo' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUsePathInfo', ], ], 'Script' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultScript', ], ], 'LoadScript' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLoadScript', ], ], 'RestPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultRestPath', ], ], 'StylePath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultStylePath', ], ], 'LocalStylePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalStylePath', ], ], 'ExtensionAssetsPath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultExtensionAssetsPath', ], ], 'ArticlePath' => [ 'use' => [ 'Script', 'UsePathInfo', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultArticlePath', ], ], 'UploadPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUploadPath', ], ], 'FileCacheDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultFileCacheDirectory', ], ], 'Logo' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLogo', ], ], 'DeletedDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDeletedDirectory', ], ], 'ShowEXIF' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultShowEXIF', ], ], 'SharedPrefix' => [ 'use' => [ 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedPrefix', ], ], 'SharedSchema' => [ 'use' => [ 'DBmwschema', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedSchema', ], ], 'DBerrorLogTZ' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDBerrorLogTZ', ], ], 'Localtimezone' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocaltimezone', ], ], 'LocalTZoffset' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalTZoffset', ], ], 'ResourceBasePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultResourceBasePath', ], ], 'MetaNamespace' => [ 'use' => [ 'Sitename', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultMetaNamespace', ], ], 'CookieSecure' => [ 'use' => [ 'ForceHTTPS', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookieSecure', ], ], 'CookiePrefix' => [ 'use' => [ 'SharedDB', 'SharedPrefix', 'SharedTables', 'DBname', 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookiePrefix', ], ], 'ReadOnlyFile' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultReadOnlyFile', ], ], ], ], 'config-schema' => [ 'UploadStashScalerBaseUrl' => [ 'deprecated' => 'since 1.36 Use thumbProxyUrl in $wgLocalFileRepo', ], 'IllegalFileChars' => [ 'deprecated' => 'since 1.41; no longer customizable', ], 'ThumbnailNamespaces' => [ 'items' => [ 'type' => 'integer', ], ], 'LocalDatabases' => [ 'items' => [ 'type' => 'string', ], ], 'ParserCacheFilterConfig' => [ 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of namespace IDs to filter definitions.', 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of filter names to values.', 'properties' => [ 'minCpuTime' => [ 'type' => 'number', ], ], ], ], ], 'PHPSessionHandling' => [ 'deprecated' => 'since 1.45 Integration with PHP session handling will be removed in the future', ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'ChangeCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'RemoveCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'GroupPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GroupInheritsPermissions' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'AvailableRights' => [ 'items' => [ 'type' => 'string', ], ], 'ImplicitRights' => [ 'items' => [ 'type' => 'string', ], ], 'SoftBlockRanges' => [ 'items' => [ 'type' => 'string', ], ], 'ExternalQuerySources' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'enabled' => [ 'type' => 'boolean', 'default' => false, ], 'url' => [ 'type' => 'string', 'format' => 'uri', ], 'timeout' => [ 'type' => 'integer', 'default' => 10, ], ], 'required' => [ 'enabled', 'url', ], 'additionalProperties' => false, ], ], 'GrantPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GrantPermissionGroups' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'SitemapNamespacesPriorities' => [ 'deprecated' => 'since 1.45 and ignored', ], 'SitemapApiConfig' => [ 'additionalProperties' => [ 'enabled' => [ 'type' => 'bool', ], 'sitemapsPerIndex' => [ 'type' => 'int', ], 'pagesPerSitemap' => [ 'type' => 'int', ], 'expiry' => [ 'type' => 'int', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'JobBackoffThrottling' => [ 'additionalProperties' => [ 'type' => 'number', ], ], 'JobTypeConf' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'class' => [ 'type' => 'string', ], 'order' => [ 'type' => 'string', ], 'claimTTL' => [ 'type' => 'integer', ], ], ], ], 'TrackingCategories' => [ 'deprecated' => 'since 1.25 Extensions should now register tracking categories using the new extension registration system.', ], 'RangeContributionsCIDRLimit' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'RestSandboxSpecs' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'url' => [ 'type' => 'string', 'format' => 'url', ], 'name' => [ 'type' => 'string', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], 'required' => [ 'url', ], ], ], 'ShellboxUrls' => [ 'additionalProperties' => [ 'type' => [ 'string', 'boolean', 'null', ], ], ], ], 'obsolete-config' => [ 'MangleFlashPolicy' => 'Since 1.39; no longer has any effect.', 'EnableOpenSearchSuggest' => 'Since 1.35, no longer used', 'AutoloadAttemptLowercase' => 'Since 1.40; no longer has any effect.', ],]
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