Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 318
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 / 318
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 / 202
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-restart-cycle-delay',
238                        'centralnotice-impression-diet-restart-cycle-delay-help',
239                        'centralnotice-impression-diet-skip-initial',
240                        'centralnotice-impression-diet-skip-initial-help',
241                        'centralnotice-large-banner-limit-days',
242                        'centralnotice-large-banner-limit-days-help',
243                        'centralnotice-large-banner-limit-randomize',
244                        'centralnotice-large-banner-limit-randomize-help',
245                        'centralnotice-large-banner-limit-identifier',
246                        'centralnotice-large-banner-limit-identifier-help',
247                        'centralnotice-impression-events-sample-rate',
248                        'centralnotice-impression-events-sample-rate-help',
249                        'centralnotice-impression-events-sample-rate-field'
250                    ]
251                ],
252                'ext.centralNotice.adminUi.bannerSequence' => $moduleTemplate + [
253                    'scripts' => 'infrastructure/ext.centralNotice.adminUi.bannerSequence.js',
254                    'styles' => 'infrastructure/ext.centralNotice.adminUi.bannerSequence.less',
255                    'dependencies' => [
256                        'ext.centralNotice.adminUi.campaignManager',
257                        'oojs-ui',
258                        'oojs-ui.styles.icons-moderation',
259                        'mediawiki.jqueryMsg'
260                    ],
261                    'messages' => [
262                        'centralnotice-banner-sequence',
263                        'centralnotice-banner-sequence-days',
264                        'centralnotice-banner-sequence-days-error',
265                        'centralnotice-banner-sequence-days-help',
266                        'centralnotice-banner-sequence-help',
267                        'centralnotice-banner-sequence-bucket-seq',
268                        'centralnotice-banner-sequence-bucket-add-step',
269                        'centralnotice-banner-sequence-banner',
270                        'centralnotice-banner-sequence-page-views',
271                        'centralnotice-banner-sequence-skip-with-id',
272                        'centralnotice-banner-sequence-page-views-error',
273                        'centralnotice-banner-sequence-skip-with-id-error',
274                        'centralnotice-banner-sequence-banner-removed-error',
275                        'centralnotice-banner-sequence-no-banner',
276                        'centralnotice-banner-sequence-detailed-help'
277                    ]
278                ],
279            ];
280
281            // Register user rights for editing
282            $wgAvailableRights[] = 'centralnotice-admin';
283
284            if ( $wgCentralNoticeAdminGroup ) {
285                // Grant admin permissions to this group
286                $wgGroupPermissions[$wgCentralNoticeAdminGroup]['centralnotice-admin'] = true;
287            }
288
289            if ( !in_array( $wgCentralNoticeMessageProtectRight, $wgAvailableRights ) ) {
290                $wgAvailableRights[] = $wgCentralNoticeMessageProtectRight;
291            }
292            self::addCascadingRestrictionRight( $wgCentralNoticeMessageProtectRight );
293            self::addCascadingRestrictionRight( 'centralnotice-admin' );
294        }
295    }
296
297    /**
298     * @param string $right
299     */
300    private static function addCascadingRestrictionRight( $right ) {
301        global $wgCascadingRestrictionLevels, $wgRestrictionLevels;
302        if ( !in_array( $right, $wgRestrictionLevels ) ) {
303            $wgRestrictionLevels[] = $right;
304        }
305        if ( !in_array( $right, $wgCascadingRestrictionLevels ) ) {
306            $wgCascadingRestrictionLevels[] = $right;
307        }
308    }
309
310    /**
311     * Initialization: set default values for some config globals. Invoked via
312     * $wgExtensionFunctions.
313     */
314    public static function initCentralNotice() {
315        global $wgCentralBannerRecorder, $wgCentralSelectedBannerDispatcher,
316            $wgNoticeProject, $wgNoticeProjects;
317
318        // Defaults for infrastructure wiki URLs
319        if ( !$wgCentralBannerRecorder ) {
320            $wgCentralBannerRecorder =
321                SpecialPage::getTitleFor( 'RecordImpression' )->getLocalUrl();
322        }
323
324        if ( !$wgCentralSelectedBannerDispatcher ) {
325            $wgCentralSelectedBannerDispatcher =
326                SpecialPage::getTitleFor( 'BannerLoader' )->getLocalUrl();
327        }
328
329        if ( !$wgNoticeProjects ) {
330            $wgNoticeProjects = [ $wgNoticeProject ];
331        }
332    }
333
334    /**
335     * CanonicalNamespaces hook; adds the CentralNotice namespaces if this is an infrastructure
336     * wiki, and if CentralNotice is configured to use the Translate extension.
337     *
338     * We do this here because there are initialization problems wrt Translate and MW core if
339     * the language object is initialized before all namespaces are registered -- which would
340     * be the case if we just used the wgExtensionFunctions hook system.
341     *
342     * @param array &$namespaces Modifiable list of namespaces -- similar to $wgExtraNamespaces
343     */
344    public function onCanonicalNamespaces( &$namespaces ) {
345        global $wgExtraNamespaces, $wgNamespacesWithSubpages, $wgTranslateMessageNamespaces;
346        global $wgNoticeUseTranslateExtension, $wgNoticeInfrastructure;
347
348        // TODO XXX Old doc copied from legacy follows, verify accuracy!
349        // When using the group review feature of translate; this
350        // will be the namespace ID for the banner staging area -- ie: banners
351        // here are world editable and will not be moved to the MW namespace
352        // until they are in $wgNoticeTranslateDeployStates
353
354        // TODO This may be unnecessary. Must coordinate with extension.json
355        if ( !defined( 'NS_CN_BANNER' ) ) {
356            define( 'NS_CN_BANNER', 866 );
357            define( 'NS_CN_BANNER_TALK', 867 );
358        }
359
360        if ( $wgNoticeInfrastructure && $wgNoticeUseTranslateExtension ) {
361            $wgExtraNamespaces[NS_CN_BANNER] = 'CNBanner';
362            $wgTranslateMessageNamespaces[] = NS_CN_BANNER;
363
364            $wgExtraNamespaces[NS_CN_BANNER_TALK] = 'CNBanner_talk';
365            $wgNamespacesWithSubpages[NS_CN_BANNER_TALK] = true;
366
367            $namespaces[NS_CN_BANNER] = 'CNBanner';
368            $namespaces[NS_CN_BANNER_TALK] = 'CNBanner_talk';
369        }
370    }
371
372    /**
373     * BeforePageDisplay hook handler
374     * This function adds the startUp and geoIP modules to the page as needed,
375     * and if there is a forced banner preview, add CSP headers and violation
376     * reporting javascript.
377     *
378     * @param OutputPage $out
379     * @param Skin $skin
380     * @return bool
381     */
382    public static function onBeforePageDisplay( $out, $skin ) {
383        global $wgCentralSelectedBannerDispatcher, $wgServerName, $wgCentralNoticeContentSecurityPolicy;
384
385        // Always add geoIP
386        // TODO Separate geoIP from CentralNotice
387        $out->addModules( 'ext.centralNotice.geoIP' );
388
389        // Banners can contain user-contributed JavaScript.
390        // Do not show them when executing such scripts is generally disallowed.
391        $isSiteJsAllowed = $out->getAllowedModules( RL\Module::TYPE_SCRIPTS )
392            >= RL\Module::ORIGIN_USER_SITEWIDE;
393
394        $request = $skin->getRequest();
395        $actionsToSkipBanners = [ 'edit', 'history', 'submit' ];
396        // If we're on a special page (or not a normal page view at all),
397        // editing, viewing history or a diff, bow out now
398        // This is to reduce the chance of bad misclicks from delayed banner loading
399        if (
400            !$isSiteJsAllowed ||
401            !$out->getTitle() ||
402            $out->getTitle()->inNamespace( NS_SPECIAL ) ||
403            in_array( $request->getText( 'action' ), $actionsToSkipBanners, true ) ||
404            $request->getCheck( 'diff' )
405        ) {
406            return true;
407        }
408
409        $centralHost = parse_url( $wgCentralSelectedBannerDispatcher, PHP_URL_HOST );
410        // Insert DNS prefetch for banner loading
411        if ( $centralHost && $centralHost !== $wgServerName ) {
412            $out->addHeadItem(
413                'cn-dns-prefetch',
414                '<link rel="dns-prefetch" href="//' . htmlspecialchars( $centralHost ) . '" />'
415            );
416        }
417
418        // Insert the startup module
419        $out->addModules( 'ext.centralNotice.startUp' );
420
421        // FIXME: I80f6f469ba4c0b60 has been in core since REL1_32.
422        // Get rid of $wgCentralNoticeContentSecurityPolicy and use their stuff.
423        if (
424            $wgCentralNoticeContentSecurityPolicy &&
425            $request->getVal( 'banner' )
426        ) {
427            $request->response()->header(
428                "content-security-policy: $wgCentralNoticeContentSecurityPolicy"
429            );
430            $out->addModules( 'ext.centralNotice.cspViolationAlert' );
431        }
432        return true;
433    }
434
435    /**
436     * MakeGlobalVariablesScript hook handler
437     * This function sets the pseudo-global JavaScript variables that are used by CentralNotice
438     *
439     * @param array &$vars
440     * @param OutputPage $out
441     * @return bool
442     */
443    public static function onMakeGlobalVariablesScript( &$vars, $out ) {
444        global $wgNoticeProject, $wgCentralNoticeGeoIPBackgroundLookupModule;
445
446        // FIXME: Is this no longer used anywhere in JS following the switch to
447        // client-side banner selection? If so, remove it.
448        $vars[ 'wgNoticeProject' ] = $wgNoticeProject;
449
450        // No need to provide this variable if it's null, because mw.config.get()
451        // will return null if it's not there.
452        if ( $wgCentralNoticeGeoIPBackgroundLookupModule ) {
453            $vars[ 'wgCentralNoticeGeoIPBackgroundLookupModule' ] =
454                $wgCentralNoticeGeoIPBackgroundLookupModule;
455        }
456
457        return true;
458    }
459
460    /**
461     * SiteNoticeAfter hook handler
462     * This function outputs the siteNotice div that the banners are loaded into.
463     *
464     * @param string &$notice
465     * @return bool
466     */
467    public static function onSiteNoticeAfter( &$notice ) {
468        // TODO Legacy comment below, likely inaccurate; check and fix
469        // Ensure that the div including #siteNotice is actually included
470        $notice = "<!-- CentralNotice -->$notice";
471
472        return true;
473    }
474
475    /**
476     * ResourceLoaderGetConfigVars hook handler
477     * Send php config vars to js via ResourceLoader
478     *
479     * @param array &$vars variables to be added to the output of the startup module
480     * @return bool
481     */
482    public static function onResourceLoaderGetConfigVars( &$vars ) {
483        global $wgNoticeInfrastructure, $wgCentralBannerRecorder,
484            $wgNoticeNumberOfBuckets, $wgNoticeBucketExpiry,
485            $wgNoticeNumberOfControllerBuckets, $wgNoticeCookieDurations,
486            $wgNoticeHideUrls, $wgCentralNoticeSampleRate,
487            $wgCentralNoticeImpressionEventSampleRate,
488            $wgCentralSelectedBannerDispatcher,
489            $wgCentralNoticePerCampaignBucketExtension, $wgCentralNoticeCampaignMixins,
490            $wgCentralNoticeMaxCampaignFallback;
491
492        // TODO Check if the following comment still applies
493        // Making these calls too soon will causes issues with the namespace localisation cache.
494        // This seems to be just right. We require them at all because MW will 302 page requests
495        // made to non localised namespaces which results in wasteful extra calls.
496
497        $vars[ 'wgCentralNoticeActiveBannerDispatcher' ] = $wgCentralSelectedBannerDispatcher;
498        $vars[ 'wgCentralBannerRecorder' ] = $wgCentralBannerRecorder;
499        $vars[ 'wgCentralNoticeSampleRate' ] = $wgCentralNoticeSampleRate;
500
501        $vars[ 'wgCentralNoticeImpressionEventSampleRate' ] =
502            $wgCentralNoticeImpressionEventSampleRate;
503
504        $vars[ 'wgNoticeNumberOfBuckets' ] = $wgNoticeNumberOfBuckets;
505        $vars[ 'wgNoticeBucketExpiry' ] = $wgNoticeBucketExpiry;
506        $vars[ 'wgNoticeNumberOfControllerBuckets' ] = $wgNoticeNumberOfControllerBuckets;
507        $vars[ 'wgNoticeCookieDurations' ] = $wgNoticeCookieDurations;
508        $vars[ 'wgNoticeHideUrls' ] = $wgNoticeHideUrls;
509        $vars[ 'wgCentralNoticeMaxCampaignFallback' ] = $wgCentralNoticeMaxCampaignFallback;
510
511        $vars[ 'wgCentralNoticePerCampaignBucketExtension' ] =
512            $wgCentralNoticePerCampaignBucketExtension;
513
514        if ( $wgNoticeInfrastructure ) {
515            // Add campaign mixin defs for use in admin interface
516            $vars[ 'wgCentralNoticeCampaignMixins' ] = $wgCentralNoticeCampaignMixins;
517        }
518        return true;
519    }
520
521    /**
522     * Conditionally register resource loader modules.
523     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderRegisterModules
524     *
525     * @param ResourceLoader $resourceLoader
526     */
527    public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
528        global $wgEnableJavaScriptTest, $wgAutoloadClasses;
529
530        if ( $wgEnableJavaScriptTest ) {
531            // These classes are only used here or in phpunit tests
532            $wgAutoloadClasses['CNTestFixturesResourceLoaderModule'] =
533                dirname( __DIR__ ) . '/tests/phpunit/CNTestFixturesResourceLoaderModule.php';
534            $wgAutoloadClasses['CentralNoticeTestFixtures'] =
535                dirname( __DIR__ ) . '/tests/phpunit/CentralNoticeTestFixtures.php';
536
537            // Set up test fixtures module, which is added as a dependency for all QUnit
538            // tests.
539            $resourceLoader->register( 'ext.centralNotice.testFixtures', [
540                'class' => 'CNTestFixturesResourceLoaderModule'
541            ] );
542        }
543    }
544
545    /**
546     * Add tags defined by this extension to the list of active tags.
547     *
548     * @param array &$tags List of defined or active tags
549     */
550    public function onChangeTagsListActive( &$tags ) {
551        $this->addDefinedTags( $tags );
552    }
553
554    /**
555     * Add tags defined by this extension to list of defined tags.
556     *
557     * @param array &$tags List of defined or active tags
558     */
559    public function onListDefinedTags( &$tags ) {
560        $this->addDefinedTags( $tags );
561    }
562
563    /**
564     * @param string[] &$tags
565     */
566    private function addDefinedTags( &$tags ): void {
567        $tags[] = 'centralnotice';
568        $tags[] = 'centralnotice translation';
569    }
570
571    /**
572     * @param User $user
573     * @param array &$preferences
574     */
575    public function onGetPreferences( $user, &$preferences ) {
576        // Explanatory text
577        $preferences['centralnotice-intro'] = [
578            'type' => 'info',
579            'default' => wfMessage( 'centralnotice-user-prefs-intro' )->parseAsBlock(),
580            'section' => 'centralnotice-banners',
581            'raw' => true,
582        ];
583
584        foreach ( CampaignType::getTypes() as $type ) {
585            // This allows fallback languages while also showing something not-too-
586            // horrible if the config variable has types that don't have i18n
587            // messages.
588            // Note also that the value of 'label' will be escaped prior to output.
589            $message = Message::newFromKey( $type->getMessageKey() );
590            $label = $message->exists() ? $message->text() : $type->getId();
591
592            $preferences[ $type->getPreferenceKey() ] = [
593                'type' => 'toggle',
594                'section' => 'centralnotice-banners/centralnotice-display-banner-types',
595                'label' => $label,
596                'disabled' => $type->getOnForAll()
597            ];
598        }
599    }
600
601    /**
602     * Add icon for Special:Preferences mobile layout
603     *
604     * @param array &$iconNames Array of icon names for their respective sections.
605     */
606    public function onPreferencesGetIcon( &$iconNames ) {
607        $iconNames[ 'centralnotice-banners' ] = 'feedback';
608    }
609
610    /**
611     * Adds CentralNotice specific navigation tabs to the UI.
612     * Implementation of SkinTemplateNavigation::Universal hook.
613     *
614     * @param Skin $skin Reference to the Skin object
615     * @param array &$tabs Any current skin tabs
616     */
617    public function onSkinTemplateNavigation__Universal( $skin, &$tabs ): void {
618        global $wgNoticeTabifyPages, $wgNoticeInfrastructure;
619
620        // Only show tabs if this wiki is in infrastructure mode
621        if ( !$wgNoticeInfrastructure ) {
622            return;
623        }
624
625        // Return if skin allows special pages to register navigation links (in which
626        // case this is handled by getShortDescription() and
627        // getAssociatedNavigationLinks()).
628        // See T315562, T313349.
629        if ( $skin->supportsMenu( 'associated-pages' ) ) {
630            return;
631        }
632
633        $title = $skin->getTitle();
634
635        // Only add tabs to special pages
636        if ( !$title || !$title->isSpecialPage() ) {
637            return;
638        }
639
640        [ $alias, ] = $this->specialPageFactory->resolveAlias( $title->getText() );
641
642        if ( $alias === null || !array_key_exists( $alias, $wgNoticeTabifyPages ) ) {
643            return;
644        }
645
646        // Clear the special page tab that's there already
647        $tabs['namespaces'] = [];
648
649        // Now add our own
650        foreach ( $wgNoticeTabifyPages as $page => $keys ) {
651            $tabs[ $keys[ 'type' ] ][ $page ] = [
652                'text' => wfMessage( $keys[ 'message' ] )->parse(),
653                'href' => SpecialPage::getTitleFor( $page )->getFullURL(),
654                'class' => ( $alias === $page ) ? 'selected' : '',
655            ];
656        }
657    }
658}