Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
202 / 202 |
|
100.00% |
14 / 14 |
CRAP | |
100.00% |
1 / 1 |
SpecialGlobalWatchlistSettings | |
100.00% |
202 / 202 |
|
100.00% |
14 / 14 |
27 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
newFromGlobalState | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getFormFields | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
3 | |||
maybeLoadTour | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
maybeGetValidSites | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
getDisplayFormat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMessagePrefix | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
alterForm | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getActualFormFields | |
100.00% |
96 / 96 |
|
100.00% |
1 / 1 |
5 | |||
onSubmit | |
100.00% |
35 / 35 |
|
100.00% |
1 / 1 |
1 | |||
onSuccess | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGroupName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isListed | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * Implements Special:GlobalWatchlistSettings |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | * http://www.gnu.org/copyleft/gpl.html |
20 | * |
21 | * @file |
22 | * @ingroup SpecialPage |
23 | */ |
24 | |
25 | namespace MediaWiki\Extension\GlobalWatchlist; |
26 | |
27 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
28 | use MediaWiki\HTMLForm\HTMLForm; |
29 | use MediaWiki\Json\FormatJson; |
30 | use MediaWiki\Logger\LoggerFactory; |
31 | use MediaWiki\Registration\ExtensionRegistry; |
32 | use MediaWiki\SpecialPage\FormSpecialPage; |
33 | use MediaWiki\SpecialPage\SpecialPageFactory; |
34 | use MediaWiki\Status\Status; |
35 | use MediaWiki\User\Options\UserOptionsLookup; |
36 | use MediaWiki\WikiMap\WikiMap; |
37 | use Psr\Log\LoggerInterface; |
38 | |
39 | /** |
40 | * @ingroup SpecialPage |
41 | * @author DannyS712 |
42 | */ |
43 | class SpecialGlobalWatchlistSettings extends FormSpecialPage { |
44 | |
45 | /** @var LoggerInterface */ |
46 | private $logger; |
47 | |
48 | /** @var ExtensionRegistry */ |
49 | private $extensionRegistry; |
50 | |
51 | /** @var SettingsManager */ |
52 | private $settingsManager; |
53 | |
54 | /** @var SpecialPageFactory */ |
55 | private $specialPageFactory; |
56 | |
57 | /** @var UserOptionsLookup */ |
58 | private $userOptionsLookup; |
59 | |
60 | /** |
61 | * @param LoggerInterface $logger |
62 | * @param ExtensionRegistry $extensionRegistry |
63 | * @param SettingsManager $settingsManager |
64 | * @param SpecialPageFactory $specialPageFactory |
65 | * @param UserOptionsLookup $userOptionsLookup |
66 | */ |
67 | public function __construct( |
68 | LoggerInterface $logger, |
69 | ExtensionRegistry $extensionRegistry, |
70 | SettingsManager $settingsManager, |
71 | SpecialPageFactory $specialPageFactory, |
72 | UserOptionsLookup $userOptionsLookup |
73 | ) { |
74 | parent::__construct( 'GlobalWatchlistSettings', 'editmyoptions' ); |
75 | |
76 | $this->logger = $logger; |
77 | $this->extensionRegistry = $extensionRegistry; |
78 | $this->settingsManager = $settingsManager; |
79 | $this->specialPageFactory = $specialPageFactory; |
80 | $this->userOptionsLookup = $userOptionsLookup; |
81 | } |
82 | |
83 | /** |
84 | * Need a factory method to inject LoggerInstance and ExtensionRegistry, |
85 | * which are not available from the service container |
86 | * |
87 | * @param SettingsManager $settingsManager |
88 | * @param SpecialPageFactory $specialPageFactory |
89 | * @param UserOptionsLookup $userOptionsLookup |
90 | * @return SpecialGlobalWatchlistSettings |
91 | */ |
92 | public static function newFromGlobalState( |
93 | SettingsManager $settingsManager, |
94 | SpecialPageFactory $specialPageFactory, |
95 | UserOptionsLookup $userOptionsLookup |
96 | ) { |
97 | return new SpecialGlobalWatchlistSettings( |
98 | LoggerFactory::getInstance( 'GlobalWatchlist' ), |
99 | ExtensionRegistry::getInstance(), |
100 | $settingsManager, |
101 | $specialPageFactory, |
102 | $userOptionsLookup |
103 | ); |
104 | } |
105 | |
106 | /** |
107 | * @param string|null $par |
108 | */ |
109 | public function execute( $par ) { |
110 | $this->addHelpLink( 'Extension:GlobalWatchlist' ); |
111 | |
112 | $this->requireNamedUser( 'globalwatchlist-must-login' ); |
113 | |
114 | $this->getOutput()->addModules( 'mediawiki.htmlform.ooui' ); |
115 | $this->getOutput()->addModuleStyles( |
116 | 'ext.globalwatchlist.specialglobalwatchlistsettings' |
117 | ); |
118 | |
119 | parent::execute( $par ); |
120 | } |
121 | |
122 | /** |
123 | * Get an HTMLForm descriptor array |
124 | * @return array |
125 | */ |
126 | protected function getFormFields() { |
127 | $currentOptions = $this->userOptionsLookup->getOption( |
128 | $this->getUser(), |
129 | SettingsManager::PREFERENCE_NAME, |
130 | false |
131 | ); |
132 | |
133 | // This should be kept in sync with getSettings.js in terms of defaults |
134 | $server = $this->getConfig()->get( 'Server' ); |
135 | $defaultOptions = [ |
136 | 'sites' => [ |
137 | preg_replace( '/.*?\/\//', '', $server ) |
138 | ], |
139 | 'anonfilter' => SettingsManager::FILTER_EITHER, |
140 | 'botfilter' => SettingsManager::FILTER_EITHER, |
141 | 'minorfilter' => SettingsManager::FILTER_EITHER, |
142 | 'confirmallsites' => true, |
143 | 'fastmode' => false, |
144 | 'grouppage' => true, |
145 | 'showtypes' => [ 'edit', 'log', 'new' ], |
146 | ]; |
147 | |
148 | if ( $currentOptions === false ) { |
149 | $userOptions = $defaultOptions; |
150 | $this->maybeLoadTour(); |
151 | } else { |
152 | // User has options, try to handle them |
153 | $parsedOptions = FormatJson::parse( $currentOptions ); |
154 | if ( $parsedOptions->isGood() ) { |
155 | $userOptions = (array)$parsedOptions->getValue(); |
156 | } else { |
157 | // Alert the user that their settings couldn't be used |
158 | $this->getOutput()->addModules( |
159 | 'ext.globalwatchlist.getsettingserror' |
160 | ); |
161 | |
162 | $userOptions = $defaultOptions; |
163 | } |
164 | } |
165 | |
166 | $formValidator = new SettingsFormValidator( |
167 | $this->getContext(), |
168 | $this->getConfig()->get( 'GlobalWatchlistSiteLimit' ), |
169 | $this->maybeGetValidSites() |
170 | ); |
171 | |
172 | return $this->getActualFormFields( $formValidator, $userOptions ); |
173 | } |
174 | |
175 | /** |
176 | * If $wgGlobalWatchlistEnableGuidedTour is true, and the GuidedTour extension is available, |
177 | * load the tour for the settings page |
178 | * |
179 | * Only called if the user does not currently have any settings saved (i.e. is a new user) |
180 | */ |
181 | private function maybeLoadTour() { |
182 | if ( |
183 | $this->getConfig()->get( 'GlobalWatchlistEnableGuidedTour' ) && |
184 | $this->extensionRegistry->isLoaded( 'GuidedTour' ) |
185 | ) { |
186 | $this->getOutput()->addModules( |
187 | 'ext.guidedTour.globalWatchlistSettings' |
188 | ); |
189 | } |
190 | } |
191 | |
192 | /** |
193 | * Used to validate the site list provided against the wikis a user has an attached |
194 | * account on, if CentralAuth is available. If not, there is no validation. |
195 | * |
196 | * @codeCoverageIgnore |
197 | * @return ?array either an array of the sites that are okay, or null for no validation |
198 | */ |
199 | private function maybeGetValidSites(): ?array { |
200 | if ( !$this->extensionRegistry->isLoaded( 'CentralAuth' ) ) { |
201 | $this->logger->debug( 'CentralAuth is not installed, no site validation' ); |
202 | return null; |
203 | } |
204 | $this->logger->debug( 'CentralAuth is installed, validating against attached wikis' ); |
205 | $attachedWikis = CentralAuthUser::getInstance( $this->getUser() )->listAttached(); |
206 | |
207 | return array_map( |
208 | static function ( $dbName ) { |
209 | $wiki = WikiMap::getWiki( $dbName ); |
210 | if ( !$wiki ) { |
211 | // This should never happen, but just in case |
212 | return ''; |
213 | } |
214 | // WikiReference::getDisplayName() only returns the 'host' for |
215 | // the server url, but we need to also handle sites that include |
216 | // a port at the end, eg Vagrant wikis. See T289384 |
217 | $bits = wfParseUrl( $wiki->getCanonicalServer() ); |
218 | if ( !$bits ) { |
219 | // Match behavior of WikiReference::getDisplayName() |
220 | // Invalid server spec. |
221 | // There's no sane thing to do here, so just return the canonical server name in full. |
222 | return $wiki->getCanonicalServer(); |
223 | } |
224 | $url = $bits['host']; |
225 | if ( isset( $bits['port'] ) ) { |
226 | $url .= ':' . $bits['port']; |
227 | } |
228 | return $url; |
229 | }, |
230 | $attachedWikis |
231 | ); |
232 | } |
233 | |
234 | /** |
235 | * Display form as OOUI |
236 | * |
237 | * @return string |
238 | */ |
239 | protected function getDisplayFormat() { |
240 | return 'ooui'; |
241 | } |
242 | |
243 | /** |
244 | * Get message prefix for HTMLForm |
245 | * |
246 | * @return string |
247 | */ |
248 | protected function getMessagePrefix() { |
249 | return 'globalwatchlist'; |
250 | } |
251 | |
252 | /** |
253 | * Set correct label for submit button |
254 | * |
255 | * @param HTMLForm $form |
256 | */ |
257 | protected function alterForm( HTMLForm $form ) { |
258 | $form->setSubmitText( |
259 | $this->msg( 'globalwatchlist-save' )->escaped() |
260 | ); |
261 | |
262 | // Enable cancel button, target is Special:GlobalWatchlist |
263 | // See T268259 |
264 | $globalWatchlistSpecial = $this->specialPageFactory->getPage( 'GlobalWatchlist' ); |
265 | $form->showCancel(); |
266 | $form->setCancelTarget( $globalWatchlistSpecial->getPageTitle() ); |
267 | } |
268 | |
269 | /** |
270 | * Get form fields with defaults filled in based on $userOptions |
271 | * |
272 | * @param SettingsFormValidator $formValidator |
273 | * @param array $userOptions |
274 | * @return array |
275 | */ |
276 | private function getActualFormFields( |
277 | SettingsFormValidator $formValidator, |
278 | array $userOptions |
279 | ): array { |
280 | $fields = []; |
281 | |
282 | // Due to the "implicit submission" feature of html forms, hitting enter |
283 | // triggers the first submit button in the form - add an extra button at |
284 | // the start, so that the button that is triggered is not the button |
285 | // to remove the first site row from HTMLFormFieldCloner. Put that button |
286 | // in the "sitelist" section so that it goes on top, and disable it so that |
287 | // the "implicit submission" attempt doesn't actually submit the form. |
288 | // We hide the button from the viewer via the css class, see styles in |
289 | // the SpecialGlobalWatchlistSettings.css file. See T275588 for more |
290 | // Known issue: only fixes the "implicit submission" from hitting enter |
291 | // if the user is focused on one of the site rows, elsewhere in the form |
292 | // enter still triggers the removal of the top site row (or the addition |
293 | // of a new row if there are no existing rows). |
294 | $fields['fake-submit'] = [ |
295 | 'type' => 'submit', |
296 | 'disabled' => true, |
297 | 'section' => 'sitelist', |
298 | 'cssclass' => 'ext-globalwatchlist-settings-fakesubmit', |
299 | ]; |
300 | |
301 | // ******** Site rows ******* |
302 | $siteFields = [ |
303 | 'site' => [ |
304 | 'type' => 'text', |
305 | 'size' => 200, |
306 | ], |
307 | 'delete' => [ |
308 | 'type' => 'submit', |
309 | 'default' => $this->msg( 'globalwatchlist-remove' )->escaped(), |
310 | 'flags' => [ 'destructive' ], |
311 | ], |
312 | ]; |
313 | $sitesDefault = []; |
314 | foreach ( $userOptions['sites'] as $defaultSite ) { |
315 | $sitesDefault[] = [ 'site' => $defaultSite ]; |
316 | } |
317 | $sitesDefault[] = [ 'site' => null ]; |
318 | $fields['sites'] = [ |
319 | 'type' => 'cloner', |
320 | 'fields' => $siteFields, |
321 | 'section' => 'sitelist', |
322 | 'validation-callback' => [ $formValidator, 'validateSitesChosen' ], |
323 | 'default' => $sitesDefault, |
324 | 'create-button-message' => 'globalwatchlist-add', |
325 | ]; |
326 | |
327 | // ******** Filters ******** |
328 | $fields['anon'] = [ |
329 | 'type' => 'radio', |
330 | 'options' => [ |
331 | $this->msg( 'globalwatchlist-filter-either' )->escaped() => SettingsManager::FILTER_EITHER, |
332 | $this->msg( 'globalwatchlist-filter-only-anon' )->escaped() => SettingsManager::FILTER_REQUIRE, |
333 | $this->msg( 'globalwatchlist-filter-not-anon' )->escaped() => SettingsManager::FILTER_EXCLUDE, |
334 | ], |
335 | 'label-message' => 'globalwatchlist-filter-anon', |
336 | 'section' => 'filters', |
337 | 'default' => $userOptions['anonfilter'], |
338 | ]; |
339 | $fields['bot'] = [ |
340 | 'type' => 'radio', |
341 | 'options' => [ |
342 | $this->msg( 'globalwatchlist-filter-either' )->escaped() => SettingsManager::FILTER_EITHER, |
343 | $this->msg( 'globalwatchlist-filter-only-bot' )->escaped() => SettingsManager::FILTER_REQUIRE, |
344 | $this->msg( 'globalwatchlist-filter-not-bot' )->escaped() => SettingsManager::FILTER_EXCLUDE, |
345 | ], |
346 | 'label-message' => 'globalwatchlist-filter-bot', |
347 | 'validation-callback' => [ $formValidator, 'validateAnonBot' ], |
348 | 'section' => 'filters', |
349 | 'default' => $userOptions['botfilter'], |
350 | ]; |
351 | $fields['minor'] = [ |
352 | 'type' => 'radio', |
353 | 'options' => [ |
354 | $this->msg( 'globalwatchlist-filter-either' )->escaped() => SettingsManager::FILTER_EITHER, |
355 | $this->msg( 'globalwatchlist-filter-only-minor' )->escaped() => SettingsManager::FILTER_REQUIRE, |
356 | $this->msg( 'globalwatchlist-filter-not-minor' )->escaped() => SettingsManager::FILTER_EXCLUDE, |
357 | ], |
358 | 'label-message' => 'globalwatchlist-filter-minor', |
359 | 'validation-callback' => [ $formValidator, 'validateAnonMinor' ], |
360 | 'section' => 'filters', |
361 | 'default' => $userOptions['minorfilter'], |
362 | ]; |
363 | |
364 | // ********** Other options ******** |
365 | $fields['types'] = [ |
366 | 'type' => 'multiselect', |
367 | 'label-message' => 'globalwatchlist-changetypes', |
368 | 'options' => [ |
369 | $this->msg( 'globalwatchlist-show-edits' )->escaped() => 'edit', |
370 | $this->msg( 'globalwatchlist-show-logentries' )->escaped() => 'log', |
371 | $this->msg( 'globalwatchlist-show-newpages' )->escaped() => 'new', |
372 | ], |
373 | 'validation-callback' => [ $formValidator, 'requireShowingOneType' ], |
374 | 'section' => 'otheroptions', |
375 | 'default' => $userOptions['showtypes'], |
376 | ]; |
377 | |
378 | $otherOptionsDefaults = []; |
379 | if ( $userOptions['grouppage'] ) { |
380 | $otherOptionsDefaults[] = 'grouppage'; |
381 | } |
382 | if ( $userOptions['confirmallsites'] ) { |
383 | $otherOptionsDefaults[] = 'confirmallsites'; |
384 | } |
385 | if ( $userOptions['fastmode'] ) { |
386 | $otherOptionsDefaults[] = 'fastmode'; |
387 | } |
388 | $fields['otheroptions'] = [ |
389 | 'type' => 'multiselect', |
390 | 'label-message' => 'globalwatchlist-otheroptions', |
391 | 'options' => [ |
392 | $this->msg( 'globalwatchlist-option-grouppage' )->escaped() => 'grouppage', |
393 | $this->msg( 'globalwatchlist-option-confirmallsites' )->escaped() => 'confirmallsites', |
394 | $this->msg( 'globalwatchlist-option-fastmode' )->escaped() => 'fastmode', |
395 | ], |
396 | 'section' => 'otheroptions', |
397 | 'default' => $otherOptionsDefaults, |
398 | ]; |
399 | |
400 | return $fields; |
401 | } |
402 | |
403 | /** |
404 | * @param array $data |
405 | * @param HTMLForm|null $form |
406 | * @return bool|string|array|Status |
407 | */ |
408 | public function onSubmit( array $data, ?HTMLForm $form = null ) { |
409 | $this->logger->info( |
410 | "Settings form submitted with {options}", |
411 | [ |
412 | 'options' => FormatJson::encode( $data ) |
413 | ] |
414 | ); |
415 | |
416 | $sites = array_map( |
417 | static function ( $row ) { |
418 | // Accept and handle sites with a protocol, see T262762 |
419 | return preg_replace( '/^(?:https?:)?\/\//', '', trim( $row['site'] ) ); |
420 | }, |
421 | $data['sites'] |
422 | ); |
423 | |
424 | // Use array_values to ensure we don't save the keys if there are empty sites, |
425 | // keys don't matter and take up space in the database |
426 | $sites = array_values( |
427 | array_filter( |
428 | $sites, |
429 | static function ( $site ) { |
430 | return ( $site !== '' ); |
431 | } |
432 | ) |
433 | ); |
434 | |
435 | $userOptions = [ |
436 | 'sites' => $sites, |
437 | 'anonfilter' => (int)$data['anon'], |
438 | 'botfilter' => (int)$data['bot'], |
439 | 'minorfilter' => (int)$data['minor'], |
440 | 'confirmallsites' => in_array( 'confirmallsites', $data['otheroptions'] ), |
441 | 'fastmode' => in_array( 'fastmode', $data['otheroptions'] ), |
442 | 'grouppage' => in_array( 'grouppage', $data['otheroptions'] ), |
443 | 'showtypes' => $data['types'], |
444 | ]; |
445 | |
446 | $this->settingsManager->saveUserOptions( |
447 | $this->getUser(), |
448 | $userOptions |
449 | ); |
450 | return true; |
451 | } |
452 | |
453 | /** |
454 | * Settings were saved successfully |
455 | */ |
456 | public function onSuccess() { |
457 | $this->getOutput()->addWikiMsg( 'globalwatchlist-notify-settingssaved' ); |
458 | } |
459 | |
460 | /** |
461 | * @return bool |
462 | */ |
463 | public function doesWrites() { |
464 | return true; |
465 | } |
466 | |
467 | /** |
468 | * @return string |
469 | */ |
470 | protected function getGroupName() { |
471 | return 'changes'; |
472 | } |
473 | |
474 | /** |
475 | * Only shown for logged in users |
476 | * |
477 | * @return bool |
478 | */ |
479 | public function isListed() { |
480 | return $this->getUser()->isNamed(); |
481 | } |
482 | |
483 | } |