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