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