Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 323
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralNoticeHooks
0.00% covered (danger)
0.00%
0 / 323
0.00% covered (danger)
0.00%
0 / 16
2756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onRegistration
0.00% covered (danger)
0.00%
0 / 207
0.00% covered (danger)
0.00%
0 / 1
56
 addCascadingRestrictionRight
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 initCentralNotice
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 onCanonicalNamespaces
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
110
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onSiteNoticeAfter
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onResourceLoaderGetConfigVars
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 onResourceLoaderRegisterModules
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addDefinedTags
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 onPreferencesGetIcon
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3/**
4 * This file is part of the CentralNotice Extension to MediaWiki
5 * https://www.mediawiki.org/wiki/Extension:CentralNotice
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * http://www.gnu.org/copyleft/gpl.html
21 *
22 * @file
23 */
24
25use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
26use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
27use MediaWiki\Hook\PreferencesGetIconHook;
28use MediaWiki\Message\Message;
29use MediaWiki\Output\OutputPage;
30use MediaWiki\Preferences\Hook\GetPreferencesHook;
31use MediaWiki\ResourceLoader as RL;
32use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
33use MediaWiki\ResourceLoader\ResourceLoader;
34use MediaWiki\Skin\Hook\SkinTemplateNavigation__UniversalHook;
35use MediaWiki\Skin\Skin;
36use MediaWiki\SpecialPage\SpecialPage;
37use MediaWiki\SpecialPage\SpecialPageFactory;
38use MediaWiki\Title\Hook\CanonicalNamespacesHook;
39use MediaWiki\User\User;
40
41/**
42 * General hook definitions
43 *
44 * @ingroup Extensions
45 */
46class CentralNoticeHooks implements
47    CanonicalNamespacesHook,
48    ChangeTagsListActiveHook,
49    ListDefinedTagsHook,
50    SkinTemplateNavigation__UniversalHook,
51    ResourceLoaderRegisterModulesHook,
52    GetPreferencesHook,
53    PreferencesGetIconHook
54{
55
56    public function __construct(
57        private readonly SpecialPageFactory $specialPageFactory,
58    ) {
59    }
60
61    /**
62     * Conditional configuration
63     */
64    public static function onRegistration() {
65        // @phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgHooks
66        global $wgHooks, $wgNoticeInfrastructure, $wgSpecialPages,
67            $wgCentralNoticeLoader, $wgNoticeUseTranslateExtension,
68            $wgAvailableRights, $wgGroupPermissions, $wgCentralNoticeAdminGroup,
69            $wgCentralNoticeMessageProtectRight, $wgResourceModules,
70            $wgDefaultUserOptions;
71
72        // If CentralNotice banners should be shown on this wiki, load the components we need for
73        // showing banners. For discussion of banner loading strategies, see
74        // http://wikitech.wikimedia.org/view/CentralNotice/Optimizing_banner_loading
75        if ( $wgCentralNoticeLoader ) {
76            $wgHooks['MakeGlobalVariablesScript'][] =
77                'CentralNoticeHooks::onMakeGlobalVariablesScript';
78            $wgHooks['BeforePageDisplay'][] = 'CentralNoticeHooks::onBeforePageDisplay';
79            $wgHooks['SiteNoticeAfter'][] = 'CentralNoticeHooks::onSiteNoticeAfter';
80            $wgHooks['ResourceLoaderGetConfigVars'][] =
81                'CentralNoticeHooks::onResourceLoaderGetConfigVars';
82        }
83
84        // Set default user preferences for campaign type filtering.
85        // All types are on by default.
86        foreach ( CampaignType::getTypes() as $type ) {
87            $wgDefaultUserOptions[ $type->getPreferenceKey() ] = 1;
88        }
89
90        // If this is the wiki that hosts the management interface, load further components
91        if ( $wgNoticeInfrastructure ) {
92            if ( $wgNoticeUseTranslateExtension ) {
93                $wgHooks['TranslatePostInitGroups'][] = 'BannerMessageGroup::registerGroupHook';
94                $wgHooks['TranslateEventMessageGroupStateChange'][] =
95                    'BannerMessageGroup::updateBannerGroupStateHook';
96            }
97
98            $wgSpecialPages['CentralNotice'] = CentralNotice::class;
99            $wgSpecialPages['NoticeTemplate'] = SpecialNoticeTemplate::class;
100            $wgSpecialPages['BannerAllocation'] = [
101                'class' => SpecialBannerAllocation::class,
102                'services' => [
103                    'LanguageNameUtils'
104                ],
105            ];
106            $wgSpecialPages['CentralNoticeLogs'] = [
107                'class' => SpecialCentralNoticeLogs::class,
108                'services' => [
109                    'UrlUtils',
110                ],
111            ];
112            $wgSpecialPages['CentralNoticeBanners'] = [
113                'class' => SpecialCentralNoticeBanners::class,
114                'services' => [
115                    'LanguageNameUtils'
116                ],
117            ];
118
119            $moduleTemplate = [
120                'localBasePath' => dirname( __DIR__ ) . '/resources',
121                'remoteExtPath' => 'CentralNotice/resources',
122            ];
123            $wgResourceModules += [
124                'ext.centralNotice.adminUi' => $moduleTemplate + [
125                    'dependencies' => [
126                        'jquery.ui',
127                        'mediawiki.jqueryMsg',
128                        'mediawiki.util',
129                    ],
130                    'scripts' => [
131                        'vendor/jquery.ui.multiselect/ui.multiselect.js',
132                        'vendor/jquery.jstree/jstree.js',
133                        'infrastructure/centralnotice.js',
134                    ],
135                    'styles' => [
136                        'vendor/jquery.ui.multiselect/ui.multiselect.css',
137                        'vendor/jquery.jstree/themes/default/style.css',
138                        'infrastructure/ext.centralNotice.adminUi.less'
139                    ],
140                    'messages' => [
141                        'centralnotice-documentwrite-error',
142                        'centralnotice-close-title',
143                        'centralnotice-select-all',
144                        'centralnotice-remove-all',
145                        'centralnotice-items-selected',
146                        'centralnotice-geo-status'
147                    ]
148                ],
149                'ext.centralNotice.adminUi.campaignPager' => $moduleTemplate + [
150                    'scripts' => 'infrastructure/ext.centralNotice.adminUi.campaignPager.js',
151                    'styles' => 'infrastructure/ext.centralNotice.adminUi.campaignPager.less'
152                ],
153                'ext.centralNotice.adminUi.bannerManager' => $moduleTemplate + [
154                    'dependencies' => [
155                        'ext.centralNotice.adminUi',
156                        'jquery.ui',
157                    ],
158                    'scripts' => 'infrastructure/bannermanager.js',
159                    'styles' => 'infrastructure/bannermanager.less',
160                    'messages' => [
161                        'centralnotice-add-notice-button',
162                        'centralnotice-add-notice-cancel-button',
163                        'centralnotice-archive-banner',
164                        'centralnotice-archive-banner-title',
165                        'centralnotice-archive-banner-confirm',
166                        'centralnotice-archive-banner-cancel',
167                        'centralnotice-add-new-banner-title',
168                        'centralnotice-delete-banner',
169                        'centralnotice-delete-banner-title',
170                        'centralnotice-delete-banner-confirm',
171                        'centralnotice-delete-banner-cancel'
172                    ]
173                ],
174                'ext.centralNotice.adminUi.bannerEditor' => $moduleTemplate + [
175                    'dependencies' => [
176                        'ext.centralNotice.adminUi',
177                        'jquery.ui',
178                        'ext.centralNotice.kvStore',
179                        'mediawiki.api',
180                        'mediawiki.Title',
181                        'mediawiki.user',
182                    ],
183                    'scripts' => 'infrastructure/bannereditor.js',
184                    'styles' => 'infrastructure/bannereditor.css',
185                    'messages' => [
186                        'centralnotice-clone',
187                        'centralnotice-clone-notice',
188                        'centralnotice-clone-cancel',
189                        'centralnotice-archive-banner',
190                        'centralnotice-archive-banner-title',
191                        'centralnotice-archive-banner-confirm',
192                        'centralnotice-archive-banner-cancel',
193                        'centralnotice-delete-banner',
194                        'centralnotice-delete-banner-title',
195                        'centralnotice-delete-banner-confirm',
196                        'centralnotice-delete-banner-cancel',
197                        'centralnotice-banner-cdn-dialog-waiting-text',
198                        'centralnotice-banner-cdn-dialog-error',
199                        'centralnotice-banner-cdn-dialog-success',
200                        'centralnotice-fieldset-preview',
201                        'centralnotice-preview-page',
202                        'centralnotice-update-preview',
203                        'centralnotice-preview-loader-error-dialog-title',
204                    ]
205                ],
206                'ext.centralNotice.adminUi.campaignManager' => [
207                    'localBasePath' => dirname( __DIR__ ),
208                    'remoteExtPath' => 'CentralNotice',
209                    'dependencies' => [
210                        'ext.centralNotice.adminUi',
211                        'oojs-ui',
212                        'mediawiki.template',
213                        'mediawiki.template.mustache'
214                    ],
215                    'scripts' => 'resources/infrastructure/campaignManager.js',
216                    'styles' => 'resources/infrastructure/campaignManager.less',
217                    'templates' => [
218                        'campaignMixinParamControls.mustache' => 'templates/campaignMixinParamControls.mustache'
219                    ],
220                    'messages' => [
221                        'centralnotice-notice-mixins-int-required',
222                        'centralnotice-notice-mixins-float-required',
223                        'centralnotice-notice-mixins-out-of-bound',
224                        'centralnotice-banner-history-logger-rate',
225                        'centralnotice-banner-history-logger-rate-help',
226                        'centralnotice-banner-history-logger-max-entry-age',
227                        'centralnotice-banner-history-logger-max-entry-age-help',
228                        'centralnotice-banner-history-logger-max-entries',
229                        'centralnotice-banner-history-logger-max-entries-help',
230                        'centralnotice-set-record-impression-sample-rate',
231                        'centralnotice-custom-record-impression-sample-rate',
232                        'centralnotice-banners-not-guaranteed-to-display',
233                        'centralnotice-impression-diet-identifier',
234                        'centralnotice-impression-diet-identifier-help',
235                        'centralnotice-impression-diet-maximum-seen',
236                        'centralnotice-impression-diet-maximum-seen-help',
237                        'centralnotice-impression-diet-daily-limit',
238                        'centralnotice-impression-diet-daily-limit-help',
239                        'centralnotice-impression-diet-restart-cycle-delay',
240                        'centralnotice-impression-diet-restart-cycle-delay-help',
241                        'centralnotice-impression-diet-skip-initial',
242                        'centralnotice-impression-diet-skip-initial-help',
243                        'centralnotice-large-banner-limit-days',
244                        'centralnotice-large-banner-limit-days-help',
245                        'centralnotice-large-banner-limit-randomize',
246                        'centralnotice-large-banner-limit-randomize-help',
247                        'centralnotice-large-banner-limit-identifier',
248                        'centralnotice-large-banner-limit-identifier-help',
249                        'centralnotice-impression-events-sample-rate',
250                        'centralnotice-impression-events-sample-rate-help',
251                        'centralnotice-impression-events-sample-rate-field',
252                        'centralnotice-blackout-start-time',
253                        'centralnotice-blackout-end-time',
254                        'centralnotice-blackout-time-help',
255                    ]
256                ],
257                'ext.centralNotice.adminUi.bannerSequence' => $moduleTemplate + [
258                    'scripts' => 'infrastructure/ext.centralNotice.adminUi.bannerSequence.js',
259                    'styles' => 'infrastructure/ext.centralNotice.adminUi.bannerSequence.less',
260                    'dependencies' => [
261                        'ext.centralNotice.adminUi.campaignManager',
262                        'oojs-ui',
263                        'oojs-ui.styles.icons-moderation',
264                        'mediawiki.jqueryMsg'
265                    ],
266                    'messages' => [
267                        'centralnotice-banner-sequence',
268                        'centralnotice-banner-sequence-days',
269                        'centralnotice-banner-sequence-days-error',
270                        'centralnotice-banner-sequence-days-help',
271                        'centralnotice-banner-sequence-help',
272                        'centralnotice-banner-sequence-bucket-seq',
273                        'centralnotice-banner-sequence-bucket-add-step',
274                        'centralnotice-banner-sequence-banner',
275                        'centralnotice-banner-sequence-page-views',
276                        'centralnotice-banner-sequence-skip-with-id',
277                        'centralnotice-banner-sequence-page-views-error',
278                        'centralnotice-banner-sequence-skip-with-id-error',
279                        'centralnotice-banner-sequence-banner-removed-error',
280                        'centralnotice-banner-sequence-no-banner',
281                        'centralnotice-banner-sequence-detailed-help'
282                    ]
283                ],
284            ];
285
286            // Register user rights for editing
287            $wgAvailableRights[] = 'centralnotice-admin';
288
289            if ( $wgCentralNoticeAdminGroup ) {
290                // Grant admin permissions to this group
291                $wgGroupPermissions[$wgCentralNoticeAdminGroup]['centralnotice-admin'] = true;
292            }
293
294            if ( !in_array( $wgCentralNoticeMessageProtectRight, $wgAvailableRights ) ) {
295                $wgAvailableRights[] = $wgCentralNoticeMessageProtectRight;
296            }
297            self::addCascadingRestrictionRight( $wgCentralNoticeMessageProtectRight );
298            self::addCascadingRestrictionRight( 'centralnotice-admin' );
299        }
300    }
301
302    /**
303     * @param string $right
304     */
305    private static function addCascadingRestrictionRight( $right ) {
306        global $wgCascadingRestrictionLevels, $wgRestrictionLevels;
307        if ( !in_array( $right, $wgRestrictionLevels ) ) {
308            $wgRestrictionLevels[] = $right;
309        }
310        if ( !in_array( $right, $wgCascadingRestrictionLevels ) ) {
311            $wgCascadingRestrictionLevels[] = $right;
312        }
313    }
314
315    /**
316     * Initialization: set default values for some config globals. Invoked via
317     * $wgExtensionFunctions.
318     */
319    public static function initCentralNotice() {
320        global $wgCentralBannerRecorder, $wgCentralSelectedBannerDispatcher,
321            $wgNoticeProject, $wgNoticeProjects;
322
323        // Defaults for infrastructure wiki URLs
324        if ( !$wgCentralBannerRecorder ) {
325            $wgCentralBannerRecorder =
326                SpecialPage::getTitleFor( 'RecordImpression' )->getLocalUrl();
327        }
328
329        if ( !$wgCentralSelectedBannerDispatcher ) {
330            $wgCentralSelectedBannerDispatcher =
331                SpecialPage::getTitleFor( 'BannerLoader' )->getLocalUrl();
332        }
333
334        if ( !$wgNoticeProjects ) {
335            $wgNoticeProjects = [ $wgNoticeProject ];
336        }
337    }
338
339    /**
340     * CanonicalNamespaces hook; adds the CentralNotice namespaces if this is an infrastructure
341     * wiki, and if CentralNotice is configured to use the Translate extension.
342     *
343     * We do this here because there are initialization problems wrt Translate and MW core if
344     * the language object is initialized before all namespaces are registered -- which would
345     * be the case if we just used the wgExtensionFunctions hook system.
346     *
347     * @param array &$namespaces Modifiable list of namespaces -- similar to $wgExtraNamespaces
348     */
349    public function onCanonicalNamespaces( &$namespaces ) {
350        global $wgExtraNamespaces, $wgNamespacesWithSubpages, $wgTranslateMessageNamespaces;
351        global $wgNoticeUseTranslateExtension, $wgNoticeInfrastructure;
352
353        // TODO XXX Old doc copied from legacy follows, verify accuracy!
354        // When using the group review feature of translate; this
355        // will be the namespace ID for the banner staging area -- ie: banners
356        // here are world editable and will not be moved to the MW namespace
357        // until they are in $wgNoticeTranslateDeployStates
358
359        // TODO This may be unnecessary. Must coordinate with extension.json
360        if ( !defined( 'NS_CN_BANNER' ) ) {
361            define( 'NS_CN_BANNER', 866 );
362            define( 'NS_CN_BANNER_TALK', 867 );
363        }
364
365        if ( $wgNoticeInfrastructure && $wgNoticeUseTranslateExtension ) {
366            $wgExtraNamespaces[NS_CN_BANNER] = 'CNBanner';
367            $wgTranslateMessageNamespaces[] = NS_CN_BANNER;
368
369            $wgExtraNamespaces[NS_CN_BANNER_TALK] = 'CNBanner_talk';
370            $wgNamespacesWithSubpages[NS_CN_BANNER_TALK] = true;
371
372            $namespaces[NS_CN_BANNER] = 'CNBanner';
373            $namespaces[NS_CN_BANNER_TALK] = 'CNBanner_talk';
374        }
375    }
376
377    /**
378     * BeforePageDisplay hook handler
379     * This function adds the startUp and geoIP modules to the page as needed,
380     * and if there is a forced banner preview, add CSP headers and violation
381     * reporting javascript.
382     *
383     * @param OutputPage $out
384     * @param Skin $skin
385     * @return bool
386     */
387    public static function onBeforePageDisplay( $out, $skin ) {
388        global $wgCentralSelectedBannerDispatcher, $wgServerName, $wgCentralNoticeContentSecurityPolicy;
389
390        // Always add geoIP
391        // TODO Separate geoIP from CentralNotice
392        $out->addModules( 'ext.centralNotice.geoIP' );
393
394        // Banners can contain user-contributed JavaScript.
395        // Do not show them when executing such scripts is generally disallowed.
396        $isSiteJsAllowed = $out->getAllowedModules( RL\Module::TYPE_SCRIPTS )
397            >= RL\Module::ORIGIN_USER_SITEWIDE;
398
399        $request = $skin->getRequest();
400        $actionsToSkipBanners = [ 'edit', 'history', 'submit' ];
401        // If we're on a special page (or not a normal page view at all),
402        // editing, viewing history or a diff, bow out now
403        // This is to reduce the chance of bad misclicks from delayed banner loading
404        if (
405            !$isSiteJsAllowed ||
406            !$out->getTitle() ||
407            $out->getTitle()->inNamespace( NS_SPECIAL ) ||
408            in_array( $request->getText( 'action' ), $actionsToSkipBanners, true ) ||
409            $request->getCheck( 'diff' )
410        ) {
411            return true;
412        }
413
414        $centralHost = parse_url( $wgCentralSelectedBannerDispatcher, PHP_URL_HOST );
415        // Insert DNS prefetch for banner loading
416        if ( $centralHost && $centralHost !== $wgServerName ) {
417            $out->addHeadItem(
418                'cn-dns-prefetch',
419                '<link rel="dns-prefetch" href="//' . htmlspecialchars( $centralHost ) . '" />'
420            );
421        }
422
423        // Insert the startup module
424        $out->addModules( 'ext.centralNotice.startUp' );
425
426        // FIXME: I80f6f469ba4c0b60 has been in core since REL1_32.
427        // Get rid of $wgCentralNoticeContentSecurityPolicy and use their stuff.
428        if (
429            $wgCentralNoticeContentSecurityPolicy &&
430            $request->getVal( 'banner' )
431        ) {
432            $request->response()->header(
433                "content-security-policy: $wgCentralNoticeContentSecurityPolicy"
434            );
435            $out->addModules( 'ext.centralNotice.cspViolationAlert' );
436        }
437        return true;
438    }
439
440    /**
441     * MakeGlobalVariablesScript hook handler
442     * This function sets the pseudo-global JavaScript variables that are used by CentralNotice
443     *
444     * @param array &$vars
445     * @param OutputPage $out
446     * @return bool
447     */
448    public static function onMakeGlobalVariablesScript( &$vars, $out ) {
449        global $wgNoticeProject, $wgCentralNoticeGeoIPBackgroundLookupModule;
450
451        // FIXME: Is this no longer used anywhere in JS following the switch to
452        // client-side banner selection? If so, remove it.
453        $vars[ 'wgNoticeProject' ] = $wgNoticeProject;
454
455        // No need to provide this variable if it's null, because mw.config.get()
456        // will return null if it's not there.
457        if ( $wgCentralNoticeGeoIPBackgroundLookupModule ) {
458            $vars[ 'wgCentralNoticeGeoIPBackgroundLookupModule' ] =
459                $wgCentralNoticeGeoIPBackgroundLookupModule;
460        }
461
462        return true;
463    }
464
465    /**
466     * SiteNoticeAfter hook handler
467     * This function outputs the siteNotice div that the banners are loaded into.
468     *
469     * @param string &$notice
470     * @return bool
471     */
472    public static function onSiteNoticeAfter( &$notice ) {
473        // TODO Legacy comment below, likely inaccurate; check and fix
474        // Ensure that the div including #siteNotice is actually included
475        $notice = "<!-- CentralNotice -->$notice";
476
477        return true;
478    }
479
480    /**
481     * ResourceLoaderGetConfigVars hook handler
482     * Send php config vars to js via ResourceLoader
483     *
484     * @param array &$vars variables to be added to the output of the startup module
485     * @return bool
486     */
487    public static function onResourceLoaderGetConfigVars( &$vars ) {
488        global $wgNoticeInfrastructure, $wgCentralBannerRecorder,
489            $wgNoticeNumberOfBuckets, $wgNoticeBucketExpiry,
490            $wgNoticeNumberOfControllerBuckets, $wgNoticeCookieDurations,
491            $wgNoticeHideUrls, $wgCentralNoticeSampleRate,
492            $wgCentralNoticeImpressionEventSampleRate,
493            $wgCentralSelectedBannerDispatcher,
494            $wgCentralNoticePerCampaignBucketExtension, $wgCentralNoticeCampaignMixins,
495            $wgCentralNoticeMaxCampaignFallback;
496
497        // TODO Check if the following comment still applies
498        // Making these calls too soon will causes issues with the namespace localisation cache.
499        // This seems to be just right. We require them at all because MW will 302 page requests
500        // made to non localised namespaces which results in wasteful extra calls.
501
502        $vars[ 'wgCentralNoticeActiveBannerDispatcher' ] = $wgCentralSelectedBannerDispatcher;
503        $vars[ 'wgCentralBannerRecorder' ] = $wgCentralBannerRecorder;
504        $vars[ 'wgCentralNoticeSampleRate' ] = $wgCentralNoticeSampleRate;
505
506        $vars[ 'wgCentralNoticeImpressionEventSampleRate' ] =
507            $wgCentralNoticeImpressionEventSampleRate;
508
509        $vars[ 'wgNoticeNumberOfBuckets' ] = $wgNoticeNumberOfBuckets;
510        $vars[ 'wgNoticeBucketExpiry' ] = $wgNoticeBucketExpiry;
511        $vars[ 'wgNoticeNumberOfControllerBuckets' ] = $wgNoticeNumberOfControllerBuckets;
512        $vars[ 'wgNoticeCookieDurations' ] = $wgNoticeCookieDurations;
513        $vars[ 'wgNoticeHideUrls' ] = $wgNoticeHideUrls;
514        $vars[ 'wgCentralNoticeMaxCampaignFallback' ] = $wgCentralNoticeMaxCampaignFallback;
515
516        $vars[ 'wgCentralNoticePerCampaignBucketExtension' ] =
517            $wgCentralNoticePerCampaignBucketExtension;
518
519        if ( $wgNoticeInfrastructure ) {
520            // Add campaign mixin defs for use in admin interface
521            $vars[ 'wgCentralNoticeCampaignMixins' ] = $wgCentralNoticeCampaignMixins;
522        }
523        return true;
524    }
525
526    /**
527     * Conditionally register resource loader modules.
528     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderRegisterModules
529     *
530     * @param ResourceLoader $resourceLoader
531     */
532    public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
533        global $wgEnableJavaScriptTest, $wgAutoloadClasses;
534
535        if ( $wgEnableJavaScriptTest ) {
536            // These classes are only used here or in phpunit tests
537            $wgAutoloadClasses['CNTestFixturesResourceLoaderModule'] =
538                dirname( __DIR__ ) . '/tests/phpunit/CNTestFixturesResourceLoaderModule.php';
539            $wgAutoloadClasses['CentralNoticeTestFixtures'] =
540                dirname( __DIR__ ) . '/tests/phpunit/CentralNoticeTestFixtures.php';
541
542            // Set up test fixtures module, which is added as a dependency for all QUnit
543            // tests.
544            $resourceLoader->register( 'ext.centralNotice.testFixtures', [
545                'class' => 'CNTestFixturesResourceLoaderModule'
546            ] );
547        }
548    }
549
550    /**
551     * Add tags defined by this extension to the list of active tags.
552     *
553     * @param array &$tags List of defined or active tags
554     */
555    public function onChangeTagsListActive( &$tags ) {
556        $this->addDefinedTags( $tags );
557    }
558
559    /**
560     * Add tags defined by this extension to list of defined tags.
561     *
562     * @param array &$tags List of defined or active tags
563     */
564    public function onListDefinedTags( &$tags ) {
565        $this->addDefinedTags( $tags );
566    }
567
568    /**
569     * @param string[] &$tags
570     */
571    private function addDefinedTags( &$tags ): void {
572        $tags[] = 'centralnotice';
573        $tags[] = 'centralnotice translation';
574    }
575
576    /**
577     * @param User $user
578     * @param array &$preferences
579     */
580    public function onGetPreferences( $user, &$preferences ) {
581        // Explanatory text
582        $preferences['centralnotice-intro'] = [
583            'type' => 'info',
584            'default' => wfMessage( 'centralnotice-user-prefs-intro' )->parseAsBlock(),
585            'section' => 'centralnotice-banners',
586            'raw' => true,
587        ];
588
589        foreach ( CampaignType::getTypes() as $type ) {
590            // This allows fallback languages while also showing something not-too-
591            // horrible if the config variable has types that don't have i18n
592            // messages.
593            // Note also that the value of 'label' will be escaped prior to output.
594            $message = Message::newFromKey( $type->getMessageKey() );
595            $label = $message->exists() ? $message->text() : $type->getId();
596
597            $preferences[ $type->getPreferenceKey() ] = [
598                'type' => 'toggle',
599                'section' => 'centralnotice-banners/centralnotice-display-banner-types',
600                'label' => $label,
601                'disabled' => $type->getOnForAll()
602            ];
603        }
604    }
605
606    /**
607     * Add icon for Special:Preferences mobile layout
608     *
609     * @param array &$iconNames Array of icon names for their respective sections.
610     */
611    public function onPreferencesGetIcon( &$iconNames ) {
612        $iconNames[ 'centralnotice-banners' ] = 'feedback';
613    }
614
615    /**
616     * Adds CentralNotice specific navigation tabs to the UI.
617     * Implementation of SkinTemplateNavigation::Universal hook.
618     *
619     * @param Skin $skin Reference to the Skin object
620     * @param array &$tabs Any current skin tabs
621     */
622    public function onSkinTemplateNavigation__Universal( $skin, &$tabs ): void {
623        global $wgNoticeTabifyPages, $wgNoticeInfrastructure;
624
625        // Only show tabs if this wiki is in infrastructure mode
626        if ( !$wgNoticeInfrastructure ) {
627            return;
628        }
629
630        // Return if skin allows special pages to register navigation links (in which
631        // case this is handled by getShortDescription() and
632        // getAssociatedNavigationLinks()).
633        // See T315562, T313349.
634        if ( $skin->supportsMenu( 'associated-pages' ) ) {
635            return;
636        }
637
638        $title = $skin->getTitle();
639
640        // Only add tabs to special pages
641        if ( !$title || !$title->isSpecialPage() ) {
642            return;
643        }
644
645        [ $alias, ] = $this->specialPageFactory->resolveAlias( $title->getText() );
646
647        if ( $alias === null || !array_key_exists( $alias, $wgNoticeTabifyPages ) ) {
648            return;
649        }
650
651        // Clear the special page tab that's there already
652        $tabs['namespaces'] = [];
653
654        // Now add our own
655        foreach ( $wgNoticeTabifyPages as $page => $keys ) {
656            $tabs[ $keys[ 'type' ] ][ $page ] = [
657                'text' => wfMessage( $keys[ 'message' ] )->parse(),
658                'href' => SpecialPage::getTitleFor( $page )->getFullURL(),
659                'class' => ( $alias === $page ) ? 'selected' : '',
660            ];
661        }
662    }
663}