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