Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMobileOptions
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 10
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setJsConfigVars
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 buildAMCToggle
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 buildMobileUserPreferences
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
2
 contentElement
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 addSettingsForm
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
42
 getRedirectUrl
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 submitSettingsForm
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3use MediaWiki\Config\Config;
4use MediaWiki\Deferred\DeferredUpdates;
5use MediaWiki\Html\Html;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Request\WebRequest;
8use MediaWiki\SpecialPage\UnlistedSpecialPage;
9use MediaWiki\Title\Title;
10use MediaWiki\User\Options\UserOptionsManager;
11use MobileFrontend\Amc\Manager;
12use MobileFrontend\Amc\UserMode;
13use MobileFrontend\Features\FeaturesManager;
14use Wikimedia\Rdbms\ReadOnlyMode;
15
16/**
17 * Adds a special page with mobile specific preferences
18 */
19class SpecialMobileOptions extends UnlistedSpecialPage {
20    /** @var bool Whether this special page has a desktop version or not */
21    protected $hasDesktopVersion = true;
22
23    /**
24     * Advanced Mobile Contributions mode
25     */
26    private Manager $amc;
27    private FeaturesManager $featuresManager;
28    private UserMode $userMode;
29    private MobileContext $mobileContext;
30
31    public function __construct(
32        private readonly UserOptionsManager $userOptionsManager,
33        private readonly ReadOnlyMode $readOnlyMode,
34        private readonly Config $config,
35    ) {
36        parent::__construct( 'MobileOptions' );
37        $services = MediaWikiServices::getInstance();
38        $this->amc = $services->getService( 'MobileFrontend.AMC.Manager' );
39        $this->featuresManager = $services->getService( 'MobileFrontend.FeaturesManager' );
40        $this->userMode = $services->getService( 'MobileFrontend.AMC.UserMode' );
41        $this->mobileContext = $services->getService( 'MobileFrontend.Context' );
42    }
43
44    /**
45     * @return bool
46     */
47    public function doesWrites() {
48        return true;
49    }
50
51    /**
52     * Set the required config for the page.
53     */
54    public function setJsConfigVars() {
55        $this->getOutput()->addJsConfigVars( [
56            'wgMFEnableFontChanger' => $this->featuresManager->isFeatureAvailableForCurrentUser(
57                'MFEnableFontChanger'
58            ),
59        ] );
60    }
61
62    /**
63     * Render the special page
64     * @param string|null $par Parameter submitted as subpage
65     */
66    public function execute( $par = '' ) {
67        parent::execute( $par );
68        $out = $this->getOutput();
69
70        $this->setHeaders();
71        $out->addBodyClasses( 'mw-mf-special-page' );
72        $out->addModuleStyles( [
73            'mobile.special.styles',
74            'mobile.special.codex.styles',
75            'mobile.special.mobileoptions.styles',
76        ] );
77        $out->addModules( [
78            'mobile.special.mobileoptions.scripts',
79        ] );
80        $this->setJsConfigVars();
81
82        $this->mobileContext->setForceMobileView( true );
83
84        if ( $this->getRequest()->wasPosted() ) {
85            $this->submitSettingsForm();
86        } else {
87            $this->addSettingsForm();
88        }
89    }
90
91    private function buildAMCToggle(): OOUI\FieldLayout {
92        $amcToggle = new OOUI\CheckboxInputWidget( [
93            'name' => 'enableAMC',
94            'infusable' => true,
95            'selected' => $this->userMode->isEnabled(),
96            'id' => 'enable-amc-toggle',
97            'value' => '1',
98        ] );
99        $layout = new OOUI\FieldLayout(
100            $amcToggle,
101            [
102                'label' => new OOUI\LabelWidget( [
103                    'input' => $amcToggle,
104                    'label' => new OOUI\HtmlSnippet(
105                        Html::openElement( 'div' ) .
106                        Html::rawElement( 'strong', [],
107                            $this->msg( 'mw-mf-amc-name' )->parse() ) .
108                        Html::rawElement( 'div', [ 'class' => 'option-description' ],
109                            $this->msg( 'mw-mf-amc-description' )->parse()
110                        ) .
111                        Html::closeElement( 'div' )
112                    )
113                ] ),
114                'id' => 'amc-field',
115            ]
116        );
117        // placing links inside a label reduces usability and accessibility so
118        // append links to $layout and outside of label instead
119        // https://www.w3.org/TR/html52/sec-forms.html#example-42c5e0c5
120        $layout->appendContent( new OOUI\HtmlSnippet(
121            Html::openElement( 'ul', [ 'class' => 'hlist option-links' ] ) .
122            Html::openElement( 'li' ) .
123            Html::rawElement(
124                    'a',
125                    // phpcs:ignore Generic.Files.LineLength.TooLong
126                    [ 'href' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Reading/Web/Advanced_mobile_contributions' ],
127                    $this->msg( 'mobile-frontend-mobile-option-amc-learn-more' )->parse()
128            ) .
129            Html::closeElement( 'li' ) .
130            Html::openElement( 'li' ) .
131            Html::rawElement(
132                    'a',
133                    // phpcs:ignore Generic.Files.LineLength.TooLong
134                    [ 'href' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Talk:Reading/Web/Advanced_mobile_contributions' ],
135                    $this->msg( 'mobile-frontend-mobile-option-amc-send-feedback' )->parse()
136            ) .
137            Html::closeElement( 'li' ) .
138            Html::closeElement( 'ul' )
139        ) );
140        return $layout;
141    }
142
143    /**
144     * Builds mobile user preferences field.
145     * @return \OOUI\FieldLayout
146     */
147    private function buildMobileUserPreferences() {
148        $spacer = new OOUI\LabelWidget( [
149            'name' => 'mobile_preference_spacer',
150        ] );
151        $userPreferences = new OOUI\FieldLayout(
152            $spacer,
153            [
154                'label' => new OOUI\LabelWidget( [
155                    'input' => $spacer,
156                    'label' => new OOUI\HtmlSnippet(
157                        Html::openElement( 'div' ) .
158                        Html::rawElement( 'strong', [],
159                             $this->msg( 'mobile-frontend-user-pref-option' )->parse() ) .
160                        Html::rawElement( 'div', [ 'class' => 'option-description' ],
161                             $this->msg( 'mobile-frontend-user-pref-description' )->parse()
162                        ) .
163                        Html::closeElement( 'div' )
164                    )
165                ] ),
166                'id' => 'mobile-user-pref',
167            ]
168        );
169        $userPreferences->appendContent( new OOUI\HtmlSnippet(
170            Html::openElement( 'ul', [ 'class' => 'hlist option-links' ] ) .
171            Html::openElement( 'li' ) .
172            Html::rawElement(
173                'a',
174                [ 'href' => Title::newFromText( 'Special:Preferences' )->getLocalURL() ],
175                $this->msg( 'mobile-frontend-user-pref-link' )->parse()
176            ) .
177            Html::closeElement( 'li' ) .
178            Html::closeElement( 'ul' )
179        ) );
180        return $userPreferences;
181    }
182
183    /**
184     * Mark some html as being content
185     * @param string $html HTML content
186     * @return string of html
187     */
188    private static function contentElement( $html ) {
189        return Html::rawElement( 'div', [
190            'class' => 'content'
191        ], $html );
192    }
193
194    /**
195     * Render the settings form (with actual set settings) and add it to the
196     * output as well as any supporting modules.
197     */
198    private function addSettingsForm() {
199        $out = $this->getOutput();
200        $user = $this->getUser();
201        $isTemp = $user->isTemp();
202
203        $out->setPageTitleMsg( $this->msg( 'mobile-frontend-main-menu-settings-heading' ) );
204        $out->enableOOUI();
205
206        if ( $this->getRequest()->getCheck( 'success' ) ) {
207            $out->wrapWikiMsg(
208                self::contentElement(
209                    Html::successBox(
210                        $this->msg( 'savedprefs' )->parse(),
211                        'mw-mf-mobileoptions-message'
212                    )
213                )
214            );
215        }
216
217        $fields = [];
218        $form = new OOUI\FormLayout( [
219            'method' => 'POST',
220            'id' => 'mobile-options',
221            'action' => $this->getPageTitle()->getLocalURL(),
222        ] );
223        $form->addClasses( [ 'mw-mf-settings' ] );
224
225        if ( $this->amc->isAvailable() && !$isTemp ) {
226            $fields[] = $this->buildAMCToggle();
227        }
228
229        $fields[] = new OOUI\ButtonInputWidget( [
230            'id' => 'mw-mf-settings-save',
231            'infusable' => true,
232            'value' => $this->msg( 'mobile-frontend-save-settings' )->text(),
233            'label' => $this->msg( 'mobile-frontend-save-settings' )->text(),
234            'flags' => [ 'primary', 'progressive' ],
235            'type' => 'submit',
236        ] );
237
238        if ( $user->isRegistered() && !$isTemp ) {
239            $fields[] = new OOUI\HiddenInputWidget( [ 'name' => 'token',
240                'value' => $user->getEditToken() ] );
241            // Special:Preferences link (https://phabricator.wikimedia.org/T327506)
242            $fields[] = $this->buildMobileUserPreferences();
243        }
244
245        $form->appendContent(
246            ...$fields
247        );
248        $out->addHTML( $form );
249    }
250
251    /**
252     * @param WebRequest $request
253     * @return string url to redirect to
254     */
255    private function getRedirectUrl( WebRequest $request ) {
256        $returnTo = $request->getText( 'returnto' );
257        if ( $returnTo !== '' ) {
258            $title = Title::newFromText( $returnTo );
259
260            if ( $title !== null ) {
261                return $title->getFullURL( $request->getText( 'returntoquery' ) );
262            }
263        }
264
265        return $this->mobileContext->getMobileUrl(
266            $this->getPageTitle()->getFullURL( 'success' )
267        );
268    }
269
270    /**
271     * Saves the settings submitted by the settings form
272     */
273    private function submitSettingsForm() {
274        $request = $this->getRequest();
275        $user = $this->getUser();
276
277        if ( $user->isRegistered() && !$user->matchEditToken( $request->getVal( 'token' ) ) ) {
278            $errorText = __METHOD__ . '(): token mismatch';
279            wfDebugLog( 'mobile', $errorText );
280            $this->getOutput()->addHTML(
281                Html::errorBox(
282                    $this->msg( "mobile-frontend-save-error" )->parse()
283                )
284            );
285            $this->addSettingsForm();
286            return;
287        }
288
289        $enableAMC = $request->getBool( 'enableAMC' );
290
291        if ( $this->amc->isAvailable() ) {
292            $this->userMode->setEnabled( $enableAMC );
293        }
294
295        DeferredUpdates::addCallableUpdate( function () use ( $enableAMC ) {
296            if ( $this->readOnlyMode->isReadOnly() ) {
297                return;
298            }
299
300            $user = $this->getUser();
301            if ( !$user->isNamed() ) {
302                // The user is anon, temp user or could not be loaded from the database.
303                return;
304            }
305
306            if ( $this->amc->isAvailable() ) {
307                $this->userOptionsManager->setOption(
308                    $user,
309                    UserMode::USER_OPTION_MODE_AMC,
310                    $enableAMC ? UserMode::OPTION_ENABLED : UserMode::OPTION_DISABLED
311                );
312            }
313            $this->userOptionsManager->saveOptions( $user );
314        }, DeferredUpdates::PRESEND );
315
316        $this->getOutput()->redirect( $this->getRedirectUrl( $request ) );
317    }
318}