Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 259
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMobileOptions
0.00% covered (danger)
0.00%
0 / 259
0.00% covered (danger)
0.00%
0 / 9
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
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 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 buildAMCToggle
0.00% covered (danger)
0.00%
0 / 45
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
 addSettingsForm
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 1
132
 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 / 44
0.00% covered (danger)
0.00%
0 / 1
210
1<?php
2
3use MediaWiki\Deferred\DeferredUpdates;
4use MediaWiki\Html\Html;
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Request\WebRequest;
7use MediaWiki\Title\Title;
8use MediaWiki\User\Options\UserOptionsManager;
9use MobileFrontend\Amc\UserMode;
10use MobileFrontend\Features\FeaturesManager;
11use MobileFrontend\Features\IFeature;
12use Wikimedia\Rdbms\ReadOnlyMode;
13
14/**
15 * Adds a special page with mobile specific preferences
16 */
17class SpecialMobileOptions extends MobileSpecialPage {
18    /** @var bool Whether this special page has a desktop version or not */
19    protected $hasDesktopVersion = true;
20
21    /**
22     * @var MediaWikiServices
23     */
24    private $services;
25
26    /**
27     * Advanced Mobile Contributions mode
28     * @var \MobileFrontend\Amc\Manager
29     */
30    private $amc;
31
32    /**
33     * @var \MobileFrontend\Features\FeaturesManager
34     */
35    private $featureManager;
36
37    /** @var UserMode */
38    private $userMode;
39
40    /** @var UserOptionsManager */
41    private $userOptionsManager;
42
43    /** @var ReadOnlyMode */
44    private $readOnlyMode;
45
46    public function __construct() {
47        parent::__construct( 'MobileOptions' );
48        $this->services = MediaWikiServices::getInstance();
49        $this->amc = $this->services->getService( 'MobileFrontend.AMC.Manager' );
50        $this->featureManager = $this->services->getService( 'MobileFrontend.FeaturesManager' );
51        $this->userMode = $this->services->getService( 'MobileFrontend.AMC.UserMode' );
52        $this->userOptionsManager = $this->services->getUserOptionsManager();
53        $this->readOnlyMode = $this->services->getReadOnlyMode();
54    }
55
56    /**
57     * @return bool
58     */
59    public function doesWrites() {
60        return true;
61    }
62
63    /**
64     * Set the required config for the page.
65     */
66    public function setJsConfigVars() {
67        $this->getOutput()->addJsConfigVars( [
68            'wgMFCollapseSectionsByDefault' => $this->getConfig()->get( 'MFCollapseSectionsByDefault' ),
69            'wgMFEnableFontChanger' => $this->featureManager->isFeatureAvailableForCurrentUser(
70                'MFEnableFontChanger'
71            ),
72        ] );
73    }
74
75    /**
76     * Render the special page
77     * @param string|null $par Parameter submitted as subpage
78     */
79    public function execute( $par = '' ) {
80        parent::execute( $par );
81
82        $this->setHeaders();
83        $this->setJsConfigVars();
84
85        $this->mobileContext->setForceMobileView( true );
86
87        if ( $this->getRequest()->wasPosted() ) {
88            $this->submitSettingsForm();
89        } else {
90            $this->addSettingsForm();
91        }
92    }
93
94    private function buildAMCToggle() {
95        /** @var \MobileFrontend\Amc\UserMode $userMode */
96        $userMode = $this->services->getService( 'MobileFrontend.AMC.UserMode' );
97        $amcToggle = new OOUI\CheckboxInputWidget( [
98            'name' => 'enableAMC',
99            'infusable' => true,
100            'selected' => $userMode->isEnabled(),
101            'id' => 'enable-amc-toggle',
102            'value' => '1',
103        ] );
104        $layout = new OOUI\FieldLayout(
105            $amcToggle,
106            [
107                'label' => new OOUI\LabelWidget( [
108                    'input' => $amcToggle,
109                    'label' => new OOUI\HtmlSnippet(
110                        Html::openElement( 'div' ) .
111                        Html::rawElement( 'strong', [],
112                            $this->msg( 'mw-mf-amc-name' )->parse() ) .
113                        Html::rawElement( 'div', [ 'class' => 'option-description' ],
114                            $this->msg( 'mw-mf-amc-description' )->parse()
115                        ) .
116                        Html::closeElement( 'div' )
117                    )
118                ] ),
119                'id' => 'amc-field',
120            ]
121        );
122        // placing links inside a label reduces usability and accessibility so
123        // append links to $layout and outside of label instead
124        // https://www.w3.org/TR/html52/sec-forms.html#example-42c5e0c5
125        $layout->appendContent( new OOUI\HtmlSnippet(
126            Html::openElement( 'ul', [ 'class' => 'hlist option-links' ] ) .
127            Html::openElement( 'li' ) .
128            Html::rawElement(
129                    'a',
130                    // phpcs:ignore Generic.Files.LineLength.TooLong
131                    [ 'href' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Reading/Web/Advanced_mobile_contributions' ],
132                    $this->msg( 'mobile-frontend-mobile-option-amc-learn-more' )->parse()
133            ) .
134            Html::closeElement( 'li' ) .
135            Html::openElement( 'li' ) .
136            Html::rawElement(
137                    'a',
138                    // phpcs:ignore Generic.Files.LineLength.TooLong
139                    [ 'href' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Talk:Reading/Web/Advanced_mobile_contributions' ],
140                    $this->msg( 'mobile-frontend-mobile-option-amc-send-feedback' )->parse()
141            ) .
142            Html::closeElement( 'li' ) .
143            Html::closeElement( 'ul' )
144        ) );
145        return $layout;
146    }
147
148    /**
149     * Builds mobile user preferences field.
150     * @return \OOUI\FieldLayout
151     * @throws \OOUI\Exception
152     */
153    private function buildMobileUserPreferences() {
154        $spacer = new OOUI\LabelWidget( [
155            'name' => 'mobile_preference_spacer',
156        ] );
157        $userPreferences = new OOUI\FieldLayout(
158            $spacer,
159            [
160                'label' => new OOUI\LabelWidget( [
161                    'input' => $spacer,
162                    'label' => new OOUI\HtmlSnippet(
163                        Html::openElement( 'div' ) .
164                        Html::rawElement( 'strong', [],
165                             $this->msg( 'mobile-frontend-user-pref-option' )->parse() ) .
166                        Html::rawElement( 'div', [ 'class' => 'option-description' ],
167                             $this->msg( 'mobile-frontend-user-pref-description' )->parse()
168                        ) .
169                        Html::closeElement( 'div' )
170                    )
171                ] ),
172                'id' => 'mobile-user-pref',
173            ]
174        );
175
176        $userPreferences->appendContent( new OOUI\HtmlSnippet(
177            Html::openElement( 'ul', [ 'class' => 'hlist option-links' ] ) .
178            Html::openElement( 'li' ) .
179            Html::rawElement(
180                'a',
181                [ 'href' => Title::newFromText( 'Special:Preferences' )->getLocalURL() ],
182                $this->msg( 'mobile-frontend-user-pref-link' )->parse()
183            ) .
184            Html::closeElement( 'li' ) .
185            Html::closeElement( 'ul' )
186        ) );
187        return $userPreferences;
188    }
189
190    /**
191     * Render the settings form (with actual set settings) and add it to the
192     * output as well as any supporting modules.
193     */
194    private function addSettingsForm() {
195        $out = $this->getOutput();
196        $user = $this->getUser();
197        $isTemp = $user->isTemp();
198
199        $out->setPageTitleMsg( $this->msg( 'mobile-frontend-main-menu-settings-heading' ) );
200        $out->enableOOUI();
201
202        if ( $this->getRequest()->getCheck( 'success' ) ) {
203            $out->wrapWikiMsg(
204                MobileUI::contentElement(
205                    Html::successBox(
206                        $this->msg( 'savedprefs' )->parse(),
207                        'mw-mf-mobileoptions-message'
208                    )
209                )
210            );
211        }
212
213        $fields = [];
214        $form = new OOUI\FormLayout( [
215            'method' => 'POST',
216            'id' => 'mobile-options',
217            'action' => $this->getPageTitle()->getLocalURL(),
218        ] );
219        $form->addClasses( [ 'mw-mf-settings' ] );
220
221        if ( $this->amc->isAvailable() && !$isTemp ) {
222            $fields[] = $this->buildAMCToggle();
223        }
224
225        // beta settings
226        $isInBeta = $this->mobileContext->isBetaGroupMember();
227        if ( $this->config->get( 'MFEnableBeta' ) ) {
228            $input = new OOUI\CheckboxInputWidget( [
229                'name' => 'enableBeta',
230                'infusable' => true,
231                'selected' => $isInBeta,
232                'id' => 'enable-beta-toggle',
233                'value' => '1',
234            ] );
235            $fields[] = new OOUI\FieldLayout(
236                $input,
237                [
238                    'label' => new OOUI\LabelWidget( [
239                        'input' => $input,
240                        'label' => new OOUI\HtmlSnippet(
241                            Html::openElement( 'div' ) .
242                            Html::rawElement( 'strong', [],
243                                $this->msg( 'mobile-frontend-settings-beta' )->parse() ) .
244                            Html::rawElement( 'div', [ 'class' => 'option-description' ],
245                                $this->msg( 'mobile-frontend-opt-in-explain' )->parse()
246                            ) .
247                            Html::closeElement( 'div' )
248                        )
249                    ] ),
250                    'id' => 'beta-field',
251                ]
252            );
253
254            /** @var FeaturesManager $manager */
255            $manager = $this->services->getService( 'MobileFrontend.FeaturesManager' );
256            // TODO The userMode should know how to retrieve features assigned to that mode,
257            // we shouldn't do any special logic like this in anywhere else in the code
258            $features = array_diff(
259                $manager->getAvailableForMode( $manager->getMode( IFeature::CONFIG_BETA ) ),
260                $manager->getAvailableForMode( $manager->getMode( IFeature::CONFIG_STABLE ) )
261            );
262
263            $classNames = [ 'mobile-options-beta-feature' ];
264            if ( $isInBeta ) {
265                $classNames[] = 'is-enabled';
266                $icon = 'check';
267            } else {
268                $icon = 'lock';
269            }
270            /** @var IFeature $feature */
271            foreach ( $features as $feature ) {
272                $fields[] = new OOUI\FieldLayout(
273                    new OOUI\IconWidget( [
274                        'icon' => $icon,
275                        'title' => wfMessage( 'mobile-frontend-beta-only' )->text(),
276                    ] ),
277                    [
278                        'classes' => $classNames,
279                        'label' => new OOUI\LabelWidget( [
280                            'label' => new OOUI\HtmlSnippet(
281                                Html::rawElement( 'div', [],
282                                    Html::element( 'strong', [],
283                                        wfMessage( $feature->getNameKey() )->text() ) .
284                                    Html::element( 'div', [ 'class' => 'option-description' ],
285                                        wfMessage( $feature->getDescriptionKey() )->text() )
286                                )
287                            ),
288                        ] )
289                    ]
290                );
291            }
292        }
293
294        $fields[] = new OOUI\ButtonInputWidget( [
295            'id' => 'mw-mf-settings-save',
296            'infusable' => true,
297            'value' => $this->msg( 'mobile-frontend-save-settings' )->text(),
298            'label' => $this->msg( 'mobile-frontend-save-settings' )->text(),
299            'flags' => [ 'primary', 'progressive' ],
300            'type' => 'submit',
301        ] );
302
303        if ( $user->isRegistered() && !$isTemp ) {
304            $fields[] = new OOUI\HiddenInputWidget( [ 'name' => 'token',
305                'value' => $user->getEditToken() ] );
306            // Special:Preferences link (https://phabricator.wikimedia.org/T327506)
307            $fields[] = $this->buildMobileUserPreferences();
308        }
309
310        $feedbackLink = $this->getConfig()->get( 'MFBetaFeedbackLink' );
311        if ( $feedbackLink && $isInBeta ) {
312            $fields[] = new OOUI\ButtonWidget( [
313                'framed' => false,
314                'href' => $feedbackLink,
315                'icon' => 'feedback',
316                'flags' => [
317                    'progressive',
318                ],
319                'classes' => [ 'mobile-options-feedback' ],
320                'label' => $this->msg( 'mobile-frontend-send-feedback' )->text(),
321            ] );
322        }
323
324        $form->appendContent(
325            ...$fields
326        );
327        $out->addHTML( $form );
328    }
329
330    /**
331     * @param WebRequest $request
332     * @return string url to redirect to
333     */
334    private function getRedirectUrl( WebRequest $request ) {
335        $returnTo = $request->getText( 'returnto' );
336        if ( $returnTo !== '' ) {
337            $title = Title::newFromText( $returnTo );
338
339            if ( $title !== null ) {
340                return $title->getFullURL( $request->getText( 'returntoquery' ) );
341            }
342        }
343
344        return $this->mobileContext->getMobileUrl(
345            $this->getPageTitle()->getFullURL( 'success' )
346        );
347    }
348
349    /**
350     * Saves the settings submitted by the settings form
351     */
352    private function submitSettingsForm() {
353        $request = $this->getRequest();
354        $user = $this->getUser();
355
356        if ( $user->isRegistered() && !$user->matchEditToken( $request->getVal( 'token' ) ) ) {
357            $errorText = __METHOD__ . '(): token mismatch';
358            wfDebugLog( 'mobile', $errorText );
359            $this->getOutput()->addHTML(
360                Html::errorBox(
361                    $this->msg( "mobile-frontend-save-error" )->parse()
362                )
363            );
364            $this->addSettingsForm();
365            return;
366        }
367
368        // We must treat forms that only update a single field specially because if we
369        // don't, all the other options will be clobbered with default values
370        $updateSingleOption = $request->getRawVal( 'updateSingleOption' );
371        $enableAMC = $request->getBool( 'enableAMC' );
372        $enableBetaMode = $request->getBool( 'enableBeta' );
373        $mobileMode = $enableBetaMode ? MobileContext::MODE_BETA : '';
374
375        if ( $updateSingleOption !== 'enableAMC' ) {
376            $this->mobileContext->setMobileMode( $mobileMode );
377        }
378
379        if ( $this->amc->isAvailable() && $updateSingleOption !== 'enableBeta' ) {
380            $this->userMode->setEnabled( $enableAMC );
381        }
382
383        DeferredUpdates::addCallableUpdate( function () use (
384            $updateSingleOption,
385            $mobileMode,
386            $enableAMC ) {
387            if ( $this->readOnlyMode->isReadOnly() ) {
388                return;
389            }
390
391            $latestUser = $this->getUser()->getInstanceForUpdate();
392            if ( $latestUser === null || !$latestUser->isNamed() ) {
393                // The user is anon, temp user or could not be loaded from the database.
394                return;
395            }
396
397            if ( $updateSingleOption !== 'enableAMC' ) {
398                $this->userOptionsManager->setOption(
399                    $latestUser,
400                    MobileContext::USER_MODE_PREFERENCE_NAME,
401                    $mobileMode
402                );
403            }
404
405            if ( $this->amc->isAvailable() && $updateSingleOption !== 'enableBeta' ) {
406                $this->userOptionsManager->setOption(
407                    $latestUser,
408                    UserMode::USER_OPTION_MODE_AMC,
409                    $enableAMC ? UserMode::OPTION_ENABLED : UserMode::OPTION_DISABLED
410                );
411            }
412            $latestUser->saveSettings();
413        }, DeferredUpdates::PRESEND );
414
415        $this->getOutput()->redirect( $this->getRedirectUrl( $request ) );
416    }
417}