Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.26% covered (danger)
44.26%
104 / 235
20.00% covered (danger)
20.00%
5 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
44.26% covered (danger)
44.26%
104 / 235
20.00% covered (danger)
20.00%
5 / 25
1336.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isVectorSkin
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getActiveABTest
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 getVectorSearchResourceLoaderConfig
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 onSkinPageReadyConfig
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 updateActionsMenu
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 updateViewsMenuIcons
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 updateAssociatedPagesMenuIcons
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 appendClassToItem
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 updateUserLinksDropdownItems
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
9
 fixEcho
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 updateUserLinksItems
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 makeMenuItemCollapsible
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 makeIcon
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 updateItemData
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 updateMenuItemData
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 updateMenuItems
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 createMoreOverflowMenu
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 onSkinTemplateNavigation
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
7.23
 onResourceLoaderSiteStylesModulePages
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 onResourceLoaderSiteModulePages
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 onLocalUserCreated
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 isSkinVersionLegacy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onGetBetaFeaturePreferences
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace MediaWiki\Skins\Vector;
4
5use MediaWiki\Auth\Hook\LocalUserCreatedHook;
6use MediaWiki\Config\Config;
7use MediaWiki\MediaWikiServices;
8use MediaWiki\Preferences\Hook\GetPreferencesHook;
9use MediaWiki\ResourceLoader as RL;
10use MediaWiki\ResourceLoader\Hook\ResourceLoaderSiteModulePagesHook;
11use MediaWiki\ResourceLoader\Hook\ResourceLoaderSiteStylesModulePagesHook;
12use MediaWiki\Skins\Hook\SkinPageReadyConfigHook;
13use MediaWiki\Skins\Vector\Hooks\HookRunner;
14use MediaWiki\User\Options\UserOptionsManager;
15use MediaWiki\User\User;
16use RequestContext;
17use RuntimeException;
18use SkinTemplate;
19
20/**
21 * Presentation hook handlers for Vector skin.
22 *
23 * Hook handler method names should be in the form of:
24 *    on<HookName>()
25 * @package Vector
26 * @internal
27 */
28class Hooks implements
29    GetPreferencesHook,
30    LocalUserCreatedHook,
31    ResourceLoaderSiteModulePagesHook,
32    ResourceLoaderSiteStylesModulePagesHook,
33    SkinPageReadyConfigHook
34{
35    private Config $config;
36    private UserOptionsManager $userOptionsManager;
37
38    public function __construct(
39        Config $config,
40        UserOptionsManager $userOptionsManager
41    ) {
42        $this->config = $config;
43        $this->userOptionsManager = $userOptionsManager;
44    }
45
46    /**
47     * Checks if the current skin is a variant of Vector
48     *
49     * @param string $skinName
50     * @return bool
51     */
52    private static function isVectorSkin( string $skinName ): bool {
53        return (
54            $skinName === Constants::SKIN_NAME_LEGACY ||
55            $skinName === Constants::SKIN_NAME_MODERN
56        );
57    }
58
59    /**
60     * @param RL\Context $context
61     * @param Config $config
62     * @return array
63     */
64    public static function getActiveABTest(
65        RL\Context $context,
66        Config $config
67    ) {
68        $ab = $config->get(
69            Constants::CONFIG_WEB_AB_TEST_ENROLLMENT
70        );
71        if ( count( $ab ) === 0 ) {
72            // If array is empty then no experiment and need to validate.
73            return $ab;
74        }
75        if ( !array_key_exists( 'buckets', $ab ) ) {
76            throw new RuntimeException( 'Invalid VectorWebABTestEnrollment value: Must contain buckets key.' );
77        }
78        if ( !array_key_exists( 'unsampled', $ab['buckets'] ) ) {
79            throw new RuntimeException( 'Invalid VectorWebABTestEnrollment value: Must define an `unsampled` bucket.' );
80        } else {
81            // check bucket values.
82            foreach ( $ab['buckets'] as $bucketName => $bucketDefinition ) {
83                if ( !is_array( $bucketDefinition ) ) {
84                    throw new RuntimeException( 'Invalid VectorWebABTestEnrollment value: Buckets should be arrays' );
85                }
86                $samplingRate = $bucketDefinition['samplingRate'];
87                if ( is_string( $samplingRate ) ) {
88                    throw new RuntimeException(
89                        'Invalid VectorWebABTestEnrollment value: Sampling rate should be number between 0 and 1.'
90                    );
91                }
92            }
93        }
94
95        return $ab;
96    }
97
98    /**
99     * Generates config variables for skins.vector.search Resource Loader module (defined in
100     * skin.json).
101     *
102     * @param RL\Context $context
103     * @param Config $config
104     * @return array<string,mixed>
105     */
106    public static function getVectorSearchResourceLoaderConfig(
107        RL\Context $context,
108        Config $config
109    ): array {
110        $vectorSearchConfig = [
111            'highlightQuery' =>
112                VectorServices::getLanguageService()->canWordsBeSplitSafely( $context->getLanguage() )
113        ];
114
115        $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
116        $hookRunner->onVectorSearchResourceLoaderConfig( $vectorSearchConfig );
117
118        return array_merge( $config->get( 'VectorWvuiSearchOptions' ), $vectorSearchConfig );
119    }
120
121    /**
122     * SkinPageReadyConfig hook handler
123     *
124     * Replace searchModule provided by skin.
125     *
126     * @since 1.35
127     * @param RL\Context $context
128     * @param mixed[] &$config Associative array of configurable options
129     * @return void This hook must not abort, it must return no value
130     */
131    public function onSkinPageReadyConfig(
132        RL\Context $context,
133        array &$config
134    ): void {
135        // It's better to exit before any additional check
136        if ( !self::isVectorSkin( $context->getSkin() ) ) {
137            return;
138        }
139
140        // Tell the `mediawiki.page.ready` module not to wire up search.
141        // This allows us to use the new Vue implementation.
142        // Context has no knowledge of legacy / modern Vector
143        // and from its point of view they are the same thing.
144        // Please see the modules `skins.vector.js` and `skins.vector.legacy.js`
145        // for the wire up of search.
146        $config['search'] = false;
147    }
148
149    /**
150     * Moves watch item from actions to views menu.
151     *
152     * @internal used inside Hooks::onSkinTemplateNavigation
153     * @param array &$content_navigation
154     */
155    private static function updateActionsMenu( &$content_navigation ) {
156        $key = null;
157        if ( isset( $content_navigation['actions']['watch'] ) ) {
158            $key = 'watch';
159        }
160        if ( isset( $content_navigation['actions']['unwatch'] ) ) {
161            $key = 'unwatch';
162        }
163
164        // Promote watch link from actions to views and add an icon
165        // The second check to isset is pointless but shuts up phan.
166        if ( $key !== null && isset( $content_navigation['actions'][ $key ] ) ) {
167            $content_navigation['views'][$key] = $content_navigation['actions'][$key];
168            unset( $content_navigation['actions'][$key] );
169        }
170    }
171
172    /**
173     * Adds icons to items in the "views" menu.
174     *
175     * @internal used inside Hooks::onSkinTemplateNavigation
176     * @param array &$content_navigation
177     * @param bool $isLegacy is this the legacy Vector skin?
178     */
179    private static function updateViewsMenuIcons( &$content_navigation, $isLegacy ) {
180        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
181        foreach ( $content_navigation['views'] as &$item ) {
182            $icon = $item['icon'] ?? null;
183            if ( $icon ) {
184                if ( $isLegacy ) {
185                    self::appendClassToItem(
186                        $item['class'],
187                        [ 'icon' ]
188                    );
189                } else {
190                    // Force the item as a button with hidden text.
191                    $item['button'] = true;
192                    $item['text-hidden'] = true;
193                    $item = self::updateMenuItemData( $item, false );
194                }
195            } elseif ( !$isLegacy ) {
196                // The vector-tab-noicon class is only used in Vector-22.
197                self::appendClassToItem(
198                    $item['class'],
199                    [ 'vector-tab-noicon' ]
200                );
201            }
202        }
203    }
204
205    /**
206     * All associated pages menu items do not have icons so are given the vector-tab-noicon class.
207     *
208     * @internal used inside Hooks::onSkinTemplateNavigation
209     * @param array &$content_navigation
210     */
211    private static function updateAssociatedPagesMenuIcons( &$content_navigation ) {
212        foreach ( $content_navigation['associated-pages'] as &$item ) {
213            self::appendClassToItem(
214                $item['class'],
215                [ 'vector-tab-noicon' ]
216            );
217        }
218    }
219
220    /**
221     * Adds class to a property
222     *
223     * @param array|string &$item to update
224     * @param array|string $classes to add to the item
225     */
226    private static function appendClassToItem( &$item, $classes ) {
227        $existingClasses = $item;
228
229        if ( is_array( $existingClasses ) ) {
230            // Treat as array
231            $newArrayClasses = is_array( $classes ) ? $classes : [ trim( $classes ) ];
232            $item = array_merge( $existingClasses, $newArrayClasses );
233        } elseif ( is_string( $existingClasses ) ) {
234            // Treat as string
235            $newStrClasses = is_string( $classes ) ? trim( $classes ) : implode( ' ', $classes );
236            $item .= ' ' . $newStrClasses;
237        } else {
238            // Treat as whatever $classes is
239            $item = $classes;
240        }
241
242        if ( is_string( $item ) ) {
243            $item = trim( $item );
244        }
245    }
246
247    /**
248     * Updates personal navigation menu (user links) dropdown for modern Vector:
249     *  - Adds icons
250     *  - Makes user page and watchlist collapsible
251     *
252     * @internal used inside ::updateUserLinksItems
253     * @param SkinTemplate $sk
254     * @param array &$content_navigation
255     * @suppress PhanTypeInvalidDimOffset
256     */
257    private static function updateUserLinksDropdownItems( $sk, &$content_navigation ) {
258        // For logged-in users in modern Vector, rearrange some links in the personal toolbar.
259        $user = $sk->getUser();
260        if ( $user->isRegistered() ) {
261            // Remove user page from personal menu dropdown for logged in use
262            $content_navigation['user-menu']['userpage']['collapsible'] = true;
263            // watchlist may be disabled if $wgGroupPermissions['*']['viewmywatchlist'] = false;
264            // See [[phab:T299671]]
265            if ( isset( $content_navigation['user-menu']['watchlist'] ) ) {
266                $content_navigation['user-menu']['watchlist']['collapsible'] = true;
267            }
268
269            // Anon editor links handled manually in new anon editor menu
270            $logoutMenu = [];
271            if ( isset( $content_navigation['user-menu']['logout'] ) ) {
272                $logoutMenu['logout'] = $content_navigation['user-menu']['logout'];
273                $logoutMenu['logout']['id'] = 'pt-logout';
274                unset( $content_navigation['user-menu']['logout'] );
275            }
276            $content_navigation['user-menu-logout'] = $logoutMenu;
277
278            self::updateMenuItems( $content_navigation, 'user-menu' );
279            self::updateMenuItems( $content_navigation, 'user-menu-logout' );
280        } else {
281            // Remove "Not logged in" from personal menu dropdown for anon users.
282            unset( $content_navigation['user-menu']['anonuserpage'] );
283
284            // Make login and create account collapsible
285            if ( isset( $content_navigation['user-menu']['login'] ) ) {
286                $content_navigation['user-menu']['login']['collapsible'] = true;
287            }
288            if ( isset( $content_navigation['user-menu']['login-private'] ) ) {
289                $content_navigation['user-menu']['login-private']['collapsible'] = true;
290            }
291            if ( isset( $content_navigation['user-menu']['createaccount'] ) ) {
292                $content_navigation['user-menu']['createaccount']['collapsible'] = true;
293            }
294
295            // Anon editor links handled manually in new anon editor menu
296            $anonEditorMenu = [];
297            if ( isset( $content_navigation['user-menu']['anoncontribs'] ) ) {
298                $anonEditorMenu['anoncontribs'] = $content_navigation['user-menu']['anoncontribs'];
299                $anonEditorMenu['anoncontribs']['id'] = 'pt-anoncontribs';
300                unset( $content_navigation['user-menu']['anoncontribs'] );
301            }
302            if ( isset( $content_navigation['user-menu']['anontalk'] ) ) {
303                $anonEditorMenu['anontalk'] = $content_navigation['user-menu']['anontalk'];
304                $anonEditorMenu['anontalk']['id'] = 'pt-anontalk';
305                unset( $content_navigation['user-menu']['anontalk'] );
306            }
307            $content_navigation['user-menu-anon-editor'] = $anonEditorMenu;
308
309            // Only show icons for anon menu items (login and create account).
310            self::updateMenuItems( $content_navigation, 'user-menu' );
311        }
312    }
313
314    /**
315     * Echo has styles that control icons rendering in places we don't want them.
316     * This code works around T343838.
317     *
318     * @param SkinTemplate $sk
319     * @param array &$content_navigation
320     */
321    private static function fixEcho( $sk, &$content_navigation ) {
322        if ( isset( $content_navigation['notifications'] ) ) {
323            foreach ( $content_navigation['notifications'] as &$item ) {
324                $icon = $item['icon'] ?? null;
325                if ( $icon ) {
326                    $linkClass = $item['link-class'] ?? [];
327                    $newLinkClass = [
328                        // Allows Echo to react to clicks
329                        'mw-echo-notification-badge-nojs'
330                    ];
331                    if ( in_array( 'mw-echo-unseen-notifications', $linkClass ) ) {
332                        $newLinkClass[] = 'mw-echo-unseen-notifications';
333                    }
334                    $item['link-class'] = $newLinkClass;
335                }
336            }
337        }
338    }
339
340    /**
341     * Updates personal navigation menu (user links) for modern Vector wherein user page, create account and login links
342     * are removed from the dropdown to be handled separately. In legacy Vector, the custom "user-page" bucket is
343     * removed to preserve existing behavior.
344     *
345     * @internal used inside Hooks::onSkinTemplateNavigation
346     * @param SkinTemplate $sk
347     * @param array &$content_navigation
348     */
349    private static function updateUserLinksItems( $sk, &$content_navigation ) {
350        $skinName = $sk->getSkinName();
351        if ( self::isSkinVersionLegacy( $skinName ) ) {
352            // Remove user page from personal toolbar since it will be inside the personal menu for logged-in
353            // users in legacy Vector.
354            unset( $content_navigation['user-page'] );
355        } else {
356            self::fixEcho( $sk, $content_navigation );
357            self::updateUserLinksDropdownItems( $sk, $content_navigation );
358        }
359    }
360
361    /**
362     * Modifies list item to make it collapsible.
363     *
364     * @internal used in ::updateItemData and ::createMoreOverflowMenu
365     * @param array &$item
366     * @param string $prefix defaults to user-links-
367     */
368    private static function makeMenuItemCollapsible( array &$item, string $prefix = 'user-links-' ) {
369        $collapseMenuItemClass = $prefix . 'collapsible-item';
370        self::appendClassToItem( $item[ 'class' ], $collapseMenuItemClass );
371    }
372
373    /**
374     * Make an icon
375     *
376     * @internal for use inside Vector skin.
377     * @param string $name
378     * @return string of HTML
379     */
380    private static function makeIcon( $name ) {
381        // Html::makeLink will pass this through rawElement
382        return '<span class="vector-icon mw-ui-icon-' . $name . ' mw-ui-icon-wikimedia-' . $name . '"></span>';
383    }
384
385    /**
386     * Update template data to include classes and html that handle buttons, icons, and collapsible items.
387     *
388     * @internal used in ::updateMenuItemData
389     * @param array $item data to update
390     * @param string $buttonClassProp property to append button classes
391     * @param string $iconHtmlProp property to set icon HTML
392     * @param bool $unsetIcon should the icon field be unset?
393     * @return array $item Updated data
394     */
395    private static function updateItemData(
396        $item, $buttonClassProp, $iconHtmlProp, $unsetIcon = true
397    ) {
398        $hasButton = $item['button'] ?? false;
399        $hideText = $item['text-hidden'] ?? false;
400        $isCollapsible = $item['collapsible'] ?? false;
401        $icon = $item['icon'] ?? '';
402        if ( $unsetIcon ) {
403            unset( $item['icon'] );
404        }
405        unset( $item['button'] );
406        unset( $item['text-hidden'] );
407        unset( $item['collapsible'] );
408
409        if ( $isCollapsible ) {
410            self::makeMenuItemCollapsible( $item );
411        }
412        if ( $hasButton ) {
413            // Hardcoded button classes, this should be fixed by replacing Hooks.php with VectorComponentButton.php
414            self::appendClassToItem( $item[ $buttonClassProp ], [
415                'cdx-button',
416                'cdx-button--fake-button',
417                'cdx-button--fake-button--enabled',
418                'cdx-button--weight-quiet'
419            ] );
420        }
421        if ( $icon ) {
422            if ( $hideText && $hasButton ) {
423                self::appendClassToItem( $item[ $buttonClassProp ], [ 'cdx-button--icon-only' ] );
424            }
425
426            $item[ $iconHtmlProp ] = self::makeIcon( $icon );
427        }
428        return $item;
429    }
430
431    /**
432     * Updates template data for Vector menu items.
433     *
434     * @internal used inside Hooks::updateMenuItems ::updateViewsMenuIcons and ::updateUserLinksDropdownItems
435     * @param array $item menu item data to update
436     * @param bool $unsetIcon should the icon field be unset?
437     * @return array $item Updated menu item data
438     */
439    private static function updateMenuItemData( $item, $unsetIcon = true ) {
440        $buttonClassProp = 'link-class';
441        $iconHtmlProp = 'link-html';
442        return self::updateItemData( $item, $buttonClassProp, $iconHtmlProp, $unsetIcon );
443    }
444
445    /**
446     * Updates user interface preferences for modern Vector to upgrade icon/button menu items.
447     *
448     * @param array &$content_navigation
449     * @param string $menu identifier
450     */
451    private static function updateMenuItems( &$content_navigation, $menu ) {
452        foreach ( $content_navigation[$menu] as &$item ) {
453            $item = self::updateMenuItemData( $item );
454        }
455    }
456
457    /**
458     * Vector 2022 only:
459     * Creates an additional menu that will be injected inside the more (cactions)
460     * dropdown menu. This menu is a clone of `views` and this menu will only be
461     * shown at low resolutions (when the `views` menu is hidden).
462     *
463     * An additional menu is used instead of adding to the existing cactions menu
464     * so that the emptyPortlet logic for that menu is preserved and the cactions menu
465     * is not shown at large resolutions when empty (e.g. all items including collapsed
466     * items are hidden).
467     *
468     * @param array &$content_navigation
469     */
470    private static function createMoreOverflowMenu( &$content_navigation ) {
471        $clonedViews = [];
472        foreach ( $content_navigation['views'] ?? [] as $key => $item ) {
473            $newItem = $item;
474            self::makeMenuItemCollapsible(
475                $newItem,
476                'vector-more-'
477            );
478            $clonedViews['more-' . $key] = $newItem;
479        }
480        // Inject collapsible menu items ahead of existing actions.
481        $content_navigation['views-overflow'] = $clonedViews;
482    }
483
484    /**
485     * Upgrades Vector's watch action to a watchstar.
486     * This is invoked inside SkinVector, not via skin registration, as skin hooks
487     * are not guaranteed to run last.
488     * This can possibly be revised based on the outcome of T287622.
489     *
490     * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation
491     * @param SkinTemplate $sk
492     * @param array &$content_navigation
493     */
494    public static function onSkinTemplateNavigation( $sk, &$content_navigation ) {
495        $skinName = $sk->getSkinName();
496        // These changes should only happen in Vector.
497        if ( !$skinName || !self::isVectorSkin( $skinName ) ) {
498            return;
499        }
500
501        $title = $sk->getRelevantTitle();
502        if (
503            $sk->getConfig()->get( 'VectorUseIconWatch' ) &&
504            $title && $title->canExist()
505        ) {
506            self::updateActionsMenu( $content_navigation );
507        }
508
509        self::updateUserLinksItems( $sk, $content_navigation );
510        if ( $skinName === Constants::SKIN_NAME_MODERN ) {
511            self::createMoreOverflowMenu( $content_navigation );
512        }
513
514        // The updating of the views menu happens /after/ the overflow menu has been created
515        // this avoids icons showing in the more overflow menu.
516        self::updateViewsMenuIcons( $content_navigation, self::isSkinVersionLegacy( $skinName ) );
517        self::updateAssociatedPagesMenuIcons( $content_navigation );
518    }
519
520    /**
521     * Adds MediaWiki:Vector.css as the skin style that controls classic Vector.
522     *
523     * @param string $skin
524     * @param array &$pages
525     */
526    public function onResourceLoaderSiteStylesModulePages( $skin, &$pages ): void {
527        if ( $skin === Constants::SKIN_NAME_MODERN && $this->config->get( 'VectorShareUserScripts' ) ) {
528            $pages['MediaWiki:Vector.css'] = [ 'type' => 'style' ];
529        }
530    }
531
532    /**
533     * Adds MediaWiki:Vector.css as the skin style that controls classic Vector.
534     *
535     * @param string $skin
536     * @param array &$pages
537     */
538    public function onResourceLoaderSiteModulePages( $skin, &$pages ): void {
539        if ( $skin === Constants::SKIN_NAME_MODERN && $this->config->get( 'VectorShareUserScripts' ) ) {
540            $pages['MediaWiki:Vector.js'] = [ 'type' => 'script' ];
541        }
542    }
543
544    /**
545     * Adds Vector specific user preferences that can only be accessed via API.
546     *
547     * @param User $user User whose preferences are being modified.
548     * @param array[] &$prefs Preferences description array, to be fed to a HTMLForm object.
549     */
550    public function onGetPreferences( $user, &$prefs ): void {
551        $vectorPrefs = [
552            Constants::PREF_KEY_FONT_SIZE => [
553                'type' => 'api'
554            ],
555            Constants::PREF_KEY_PAGE_TOOLS_PINNED => [
556                'type' => 'api'
557            ],
558            Constants::PREF_KEY_MAIN_MENU_PINNED => [
559                'type' => 'api'
560            ],
561            Constants::PREF_KEY_TOC_PINNED => [
562                'type' => 'api'
563            ],
564            Constants::PREF_KEY_APPEARANCE_PINNED => [
565                'type' => 'api'
566            ],
567            Constants::PREF_KEY_LIMITED_WIDTH => [
568                'type' => 'toggle',
569                'label-message' => 'vector-prefs-limited-width',
570                'section' => 'rendering/skin/skin-prefs',
571                'help-message' => 'vector-prefs-limited-width-help',
572                'hide-if' => [ '!==', 'skin', Constants::SKIN_NAME_MODERN ],
573            ],
574            Constants::PREF_KEY_NIGHT_MODE => [
575                'type' => 'api'
576            ],
577        ];
578        $prefs += $vectorPrefs;
579    }
580
581    /**
582     * Called one time when initializing a users preferences for a newly created account.
583     *
584     * @param User $user Newly created user object.
585     * @param bool $isAutoCreated
586     */
587    public function onLocalUserCreated( $user, $isAutoCreated ) {
588        $default = $this->config->get( Constants::CONFIG_KEY_DEFAULT_SKIN_VERSION_FOR_NEW_ACCOUNTS );
589        if ( $default ) {
590            $this->userOptionsManager->setOption(
591                $user,
592                Constants::PREF_KEY_SKIN,
593                $default === Constants::SKIN_VERSION_LEGACY ?
594                    Constants::SKIN_NAME_LEGACY : Constants::SKIN_NAME_MODERN
595            );
596        }
597    }
598
599    /**
600     * Gets whether the current skin version is the legacy version.
601     *
602     * @param string $skinName hint that can be used to detect modern vector.
603     * @return bool
604     */
605    private static function isSkinVersionLegacy( $skinName ): bool {
606        return $skinName === Constants::SKIN_NAME_LEGACY;
607    }
608
609    /**
610     * Register Vector 2022 beta feature to the beta features list
611     *
612     * @param User $user User the preferences are for
613     * @param array &$betaFeatures
614     */
615    public function onGetBetaFeaturePreferences( User $user, array &$betaFeatures ) {
616        $skinName = RequestContext::getMain()->getSkinName();
617        // Only Vector 2022 is supported for beta features
618        if ( $skinName !== Constants::SKIN_NAME_MODERN ) {
619            return;
620        }
621        // Only add Vector 2022 beta feature if there is at least one beta feature present in config
622        $configHasBeta = false;
623        foreach ( Constants::VECTOR_BETA_FEATURES as $featureName ) {
624            if ( $this->config->has( $featureName ) && $this->config->get( $featureName )[ 'beta' ] === true ) {
625                $configHasBeta = true;
626                break;
627            }
628        }
629        if ( !$configHasBeta ) {
630            return;
631        }
632        $skinsAssetsPath = $this->config->get( 'StylePath' );
633        $imagesDir = "$skinsAssetsPath/Vector/resources/images";
634        $betaFeatures[ Constants::VECTOR_2022_BETA_KEY ] = [
635            'label-message' => 'vector-2022-beta-preview-label',
636            'desc-message' => 'vector-2022-beta-preview-description',
637            'screenshot' => [
638                // follow up work to add images is required in T349321
639                'ltr' => "$imagesDir/vector-2022-beta-preview-ltr.svg",
640                'rtl' => "$imagesDir/vector-2022-beta-preview-rtl.svg",
641            ],
642            'info-link' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Reading/Web/Accessibility_for_reading',
643            'discussion-link' => 'https://www.mediawiki.org/wiki/Talk:Reading/Web/Accessibility_for_reading',
644        ];
645    }
646}