Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 259 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
SpecialMobileOptions | |
0.00% |
0 / 259 |
|
0.00% |
0 / 9 |
1260 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setJsConfigVars | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
buildAMCToggle | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
2 | |||
buildMobileUserPreferences | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
2 | |||
addSettingsForm | |
0.00% |
0 / 108 |
|
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\Deferred\DeferredUpdates; |
4 | use MediaWiki\Html\Html; |
5 | use MediaWiki\MediaWikiServices; |
6 | use MediaWiki\Request\WebRequest; |
7 | use MediaWiki\Title\Title; |
8 | use MediaWiki\User\Options\UserOptionsManager; |
9 | use MobileFrontend\Amc\UserMode; |
10 | use MobileFrontend\Features\FeaturesManager; |
11 | use MobileFrontend\Features\IFeature; |
12 | use Wikimedia\Rdbms\ReadOnlyMode; |
13 | |
14 | /** |
15 | * Adds a special page with mobile specific preferences |
16 | */ |
17 | class 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 | } |