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