Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.03% covered (danger)
18.03%
11 / 61
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SkinOptions
18.03% covered (danger)
18.03%
11 / 61
50.00% covered (danger)
50.00%
3 / 6
217.81
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
 setMultiple
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getAll
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSkinOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setMinervaSkinOptions
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20namespace MediaWiki\Minerva;
21
22use MediaWiki\HookContainer\HookContainer;
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Minerva\Hooks\HookRunner;
25use MediaWiki\Minerva\Skins\SkinMinerva;
26use MediaWiki\Minerva\Skins\SkinUserPageHelper;
27use MobileContext;
28use OutOfBoundsException;
29use Skin;
30
31/**
32 * A wrapper for all available Skin options.
33 */
34final class SkinOptions {
35
36    public const MOBILE_OPTIONS = 'mobileOptionsLink';
37    public const CATEGORIES = 'categories';
38    public const PAGE_ISSUES = 'pageIssues';
39    public const BETA_MODE = 'beta';
40    public const TALK_AT_TOP = 'talkAtTop';
41    public const SHOW_DONATE = 'donate';
42    public const HISTORY_IN_PAGE_ACTIONS = 'historyInPageActions';
43    public const TOOLBAR_SUBMENU = 'overflowSubmenu';
44    public const TABS_ON_SPECIALS = 'tabsOnSpecials';
45    public const MAIN_MENU_EXPANDED = 'mainMenuExpanded';
46    public const PERSONAL_MENU = 'personalMenu';
47    public const SINGLE_ECHO_BUTTON = 'echo';
48    public const NIGHT_MODE = 'nightMode';
49
50    /**
51     * Note stable skin options default to true for desktop-Minerva and are expected to be
52     * overridden on mobile.
53     * @var array skin specific options, initialized with default values
54     */
55    private array $skinOptions = [
56        self::BETA_MODE => false,
57        self::SHOW_DONATE => true,
58        /**
59         * Whether the main menu should include a link to
60         * Special:Preferences of Special:MobileOptions
61         */
62        self::MOBILE_OPTIONS => false,
63        /** Whether a categories button should appear at the bottom of the skin. */
64        self::CATEGORIES => false,
65        /** requires a wiki using Template:Ambox */
66        self::PAGE_ISSUES => false,
67        /** no extension requirements */
68        self::TALK_AT_TOP => true,
69        /** no extension requirements */
70        self::HISTORY_IN_PAGE_ACTIONS => true,
71        /** no extension requirements */
72        self::TOOLBAR_SUBMENU => true,
73        /** Whether to show tabs on special pages */
74        self::TABS_ON_SPECIALS => true,
75        /** whether to show a personal menu */
76        self::PERSONAL_MENU => true,
77        /** whether to show a main menu with additional items */
78        self::MAIN_MENU_EXPANDED => true,
79        /** whether Echo should be replaced with a single button */
80        self::SINGLE_ECHO_BUTTON => false,
81        /** whether night mode is available to the user */
82        self::NIGHT_MODE => false,
83    ];
84
85    private HookContainer $hookContainer;
86    private SkinUserPageHelper $skinUserPageHelper;
87
88    public function __construct(
89        HookContainer $hookContainer,
90        SkinUserPageHelper $skinUserPageHelper
91    ) {
92        $this->hookContainer = $hookContainer;
93        $this->skinUserPageHelper = $skinUserPageHelper;
94    }
95
96    /**
97     * override an existing option or options with new values
98     * @param array $options
99     */
100    public function setMultiple( array $options ): void {
101        foreach ( $options as $option => $value ) {
102            if ( !array_key_exists( $option, $this->skinOptions ) ) {
103                throw new OutOfBoundsException( "SkinOption $option is not defined" );
104            }
105        }
106        $this->skinOptions = array_merge( $this->skinOptions, $options );
107    }
108
109    /**
110     * Return whether a skin option is truthy. Should be one of self:* constants
111     * @param string $key
112     * @return bool
113     */
114    public function get( string $key ): bool {
115        if ( !array_key_exists( $key, $this->skinOptions ) ) {
116            throw new OutOfBoundsException( "SkinOption $key doesn't exist" );
117        }
118        return $this->skinOptions[$key];
119    }
120
121    /**
122     * Get all skin options
123     * @return array
124     */
125    public function getAll(): array {
126        return $this->skinOptions;
127    }
128
129    /**
130     * Return whether any of the skin options have been set
131     * @return bool
132     */
133    public function hasSkinOptions(): bool {
134        foreach ( $this->skinOptions as $key => $val ) {
135            if ( $val ) {
136                return true;
137            }
138        }
139        return false;
140    }
141
142    /**
143     * Set the skin options for Minerva
144     *
145     * @param MobileContext $mobileContext
146     * @param Skin $skin
147     */
148    public function setMinervaSkinOptions(
149        MobileContext $mobileContext, Skin $skin
150    ): void {
151        // setSkinOptions is not available
152        if ( $skin instanceof SkinMinerva ) {
153            $services = MediaWikiServices::getInstance();
154            $featuresManager = $services
155                ->getService( 'MobileFrontend.FeaturesManager' );
156            $title = $skin->getTitle();
157
158            // T245162 - this should only apply if the context relates to a page view.
159            // Examples:
160            // - parsing wikitext during an REST response
161            // - a ResourceLoader response
162            if ( $title !== null ) {
163                // T232653: TALK_AT_TOP, HISTORY_IN_PAGE_ACTIONS, TOOLBAR_SUBMENU should
164                // be true on user pages and user talk pages for all users
165                $this->skinUserPageHelper
166                    ->setContext( $mobileContext )
167                    ->setTitle(
168                        $title->inNamespace( NS_USER_TALK ) ? $title->getSubjectPage() : $title
169                    );
170
171                $isDiffLink = $mobileContext->getRequest()->getCheck( 'diff' );
172                $isUserPage = $this->skinUserPageHelper->isUserPage();
173                $isUserPageAccessible = $this->skinUserPageHelper->isUserPageAccessibleToCurrentUser();
174                $isUserPageOrUserTalkPage = $isUserPage && $isUserPageAccessible;
175                $requiresHistoryLink = $isUserPageOrUserTalkPage || $isDiffLink;
176            } else {
177                // If no title this must be false
178                $isUserPageOrUserTalkPage = false;
179                $requiresHistoryLink = false;
180            }
181
182            $isBeta = $mobileContext->isBetaGroupMember();
183            $this->setMultiple( [
184                self::SHOW_DONATE => $featuresManager->isFeatureAvailableForCurrentUser( 'MinervaDonateLink' ),
185                self::TALK_AT_TOP => $isUserPageOrUserTalkPage ?
186                    true : $featuresManager->isFeatureAvailableForCurrentUser( 'MinervaTalkAtTop' ),
187                self::BETA_MODE
188                    => $isBeta,
189                self::CATEGORIES
190                    => $featuresManager->isFeatureAvailableForCurrentUser( 'MinervaShowCategories' ),
191                self::PAGE_ISSUES
192                    => $featuresManager->isFeatureAvailableForCurrentUser( 'MinervaPageIssuesNewTreatment' ),
193                self::MOBILE_OPTIONS => true,
194                self::PERSONAL_MENU => $featuresManager->isFeatureAvailableForCurrentUser(
195                    'MinervaPersonalMenu'
196                ),
197                self::MAIN_MENU_EXPANDED => $featuresManager->isFeatureAvailableForCurrentUser(
198                    'MinervaAdvancedMainMenu'
199                ),
200                // In mobile, always resort to single icon.
201                self::SINGLE_ECHO_BUTTON => true,
202                self::HISTORY_IN_PAGE_ACTIONS => $requiresHistoryLink ?
203                    true : $featuresManager->isFeatureAvailableForCurrentUser( 'MinervaHistoryInPageActions' ),
204                self::TOOLBAR_SUBMENU => $isUserPageOrUserTalkPage ?
205                    true : $featuresManager->isFeatureAvailableForCurrentUser(
206                        Hooks::FEATURE_OVERFLOW_PAGE_ACTIONS
207                    ),
208                self::TABS_ON_SPECIALS => true,
209                self::NIGHT_MODE => $featuresManager->isFeatureAvailableForCurrentUser( 'MinervaNightMode' ),
210            ] );
211            ( new HookRunner( $this->hookContainer ) )->onSkinMinervaOptionsInit( $skin, $this );
212        }
213    }
214}