Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
4.74% |
9 / 190 |
|
0.00% |
0 / 27 |
CRAP | |
0.00% |
0 / 1 |
OATHManage | |
4.74% |
9 / 190 |
|
0.00% |
0 / 27 |
4937.92 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
47.37% |
9 / 19 |
|
0.00% |
0 / 1 |
8.64 | |||
checkPermissions | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
56 | |||
setAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setModule | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
addEnabledHTML | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addAlternativesHTML | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
nothingEnabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addInactiveHTML | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
addGeneralHelp | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
addModuleHTML | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
getGenericContent | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
30 | |||
addCustomContent | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
addHeading | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
shouldShowGenericButtons | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
isModuleRequested | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
isModuleEnabled | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isModuleAvailable | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
isValidFormType | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
ensureRequiredFormFields | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
clearPage | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
isGenericAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hasAlternativeModules | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
shouldShowDisableWarning | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
showDisableWarning | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
30 | |||
isSwitch | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License along |
15 | * with this program; if not, write to the Free Software Foundation, Inc., |
16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * http://www.gnu.org/copyleft/gpl.html |
18 | */ |
19 | |
20 | namespace MediaWiki\Extension\OATHAuth\Special; |
21 | |
22 | use MediaWiki\Config\ConfigException; |
23 | use MediaWiki\Extension\OATHAuth\HTMLForm\IManageForm; |
24 | use MediaWiki\Extension\OATHAuth\IModule; |
25 | use MediaWiki\Extension\OATHAuth\OATHAuthModuleRegistry; |
26 | use MediaWiki\Extension\OATHAuth\OATHUser; |
27 | use MediaWiki\Extension\OATHAuth\OATHUserRepository; |
28 | use MediaWiki\Html\Html; |
29 | use MediaWiki\HTMLForm\HTMLForm; |
30 | use MediaWiki\Message\Message; |
31 | use MediaWiki\SpecialPage\SpecialPage; |
32 | use MWException; |
33 | use OOUI\ButtonWidget; |
34 | use OOUI\HorizontalLayout; |
35 | use OOUI\HtmlSnippet; |
36 | use OOUI\LabelWidget; |
37 | use OOUI\PanelLayout; |
38 | use PermissionsError; |
39 | use UserNotLoggedIn; |
40 | |
41 | class OATHManage extends SpecialPage { |
42 | public const ACTION_ENABLE = 'enable'; |
43 | public const ACTION_DISABLE = 'disable'; |
44 | |
45 | protected OATHAuthModuleRegistry $moduleRegistry; |
46 | |
47 | protected OATHUserRepository $userRepo; |
48 | |
49 | protected OATHUser $authUser; |
50 | |
51 | /** |
52 | * @var string |
53 | */ |
54 | protected $action; |
55 | |
56 | protected ?IModule $requestedModule; |
57 | |
58 | /** |
59 | * Initializes a page to manage available 2FA modules |
60 | * |
61 | * @param OATHUserRepository $userRepo |
62 | * @param OATHAuthModuleRegistry $moduleRegistry |
63 | * |
64 | * @throws ConfigException |
65 | * @throws MWException |
66 | */ |
67 | public function __construct( OATHUserRepository $userRepo, OATHAuthModuleRegistry $moduleRegistry ) { |
68 | // messages used: oathmanage (display "name" on Special:SpecialPages), |
69 | // right-oathauth-enable, action-oathauth-enable |
70 | parent::__construct( 'OATHManage', 'oathauth-enable' ); |
71 | |
72 | $this->userRepo = $userRepo; |
73 | $this->moduleRegistry = $moduleRegistry; |
74 | $this->authUser = $this->userRepo->findByUser( $this->getUser() ); |
75 | } |
76 | |
77 | /** |
78 | * @inheritDoc |
79 | */ |
80 | protected function getGroupName() { |
81 | return 'login'; |
82 | } |
83 | |
84 | /** |
85 | * @param null|string $subPage |
86 | */ |
87 | public function execute( $subPage ) { |
88 | $this->getOutput()->enableOOUI(); |
89 | $this->getOutput()->disallowUserJs(); |
90 | $this->setAction(); |
91 | $this->setModule(); |
92 | |
93 | parent::execute( $subPage ); |
94 | |
95 | if ( $this->requestedModule instanceof IModule ) { |
96 | // Performing an action on a requested module |
97 | $this->clearPage(); |
98 | if ( $this->shouldShowDisableWarning() ) { |
99 | $this->showDisableWarning(); |
100 | return; |
101 | } |
102 | $this->addModuleHTML( $this->requestedModule ); |
103 | return; |
104 | } |
105 | |
106 | $this->addGeneralHelp(); |
107 | if ( $this->authUser->isTwoFactorAuthEnabled() ) { |
108 | $this->addEnabledHTML(); |
109 | if ( $this->hasAlternativeModules() ) { |
110 | $this->addAlternativesHTML(); |
111 | } |
112 | return; |
113 | } |
114 | $this->nothingEnabled(); |
115 | } |
116 | |
117 | /** |
118 | * @throws PermissionsError |
119 | * @throws UserNotLoggedIn |
120 | */ |
121 | public function checkPermissions() { |
122 | $this->requireNamedUser(); |
123 | |
124 | $canEnable = $this->getUser()->isAllowed( 'oathauth-enable' ); |
125 | |
126 | if ( $this->action === static::ACTION_ENABLE && !$canEnable ) { |
127 | $this->displayRestrictionError(); |
128 | } |
129 | |
130 | if ( !$this->authUser->isTwoFactorAuthEnabled() && !$canEnable ) { |
131 | // No enabled module and cannot enable - nothing to do |
132 | $this->displayRestrictionError(); |
133 | } |
134 | |
135 | if ( $this->action === static::ACTION_ENABLE && !$this->getRequest()->wasPosted() ) { |
136 | // Trying to change the 2FA method (one is already enabled) |
137 | $this->checkLoginSecurityLevel( 'oathauth-enable' ); |
138 | } |
139 | } |
140 | |
141 | private function setAction(): void { |
142 | $this->action = $this->getRequest()->getVal( 'action', '' ); |
143 | } |
144 | |
145 | private function setModule(): void { |
146 | $moduleKey = $this->getRequest()->getVal( 'module', '' ); |
147 | $this->requestedModule = ( $moduleKey && $this->moduleRegistry->moduleExists( $moduleKey ) ) |
148 | ? $this->moduleRegistry->getModuleByKey( $moduleKey ) |
149 | : null; |
150 | } |
151 | |
152 | private function addEnabledHTML(): void { |
153 | $this->addHeading( $this->msg( 'oathauth-ui-enabled-module' ) ); |
154 | $this->addModuleHTML( $this->authUser->getModule() ); |
155 | } |
156 | |
157 | private function addAlternativesHTML(): void { |
158 | $this->addHeading( $this->msg( 'oathauth-ui-not-enabled-modules' ) ); |
159 | $this->addInactiveHTML(); |
160 | } |
161 | |
162 | private function nothingEnabled(): void { |
163 | $this->addHeading( $this->msg( 'oathauth-ui-available-modules' ) ); |
164 | $this->addInactiveHTML(); |
165 | } |
166 | |
167 | private function addInactiveHTML(): void { |
168 | foreach ( $this->moduleRegistry->getAllModules() as $module ) { |
169 | if ( $this->isModuleEnabled( $module ) || !$this->isModuleAvailable( $module ) ) { |
170 | continue; |
171 | } |
172 | $this->addModuleHTML( $module ); |
173 | } |
174 | } |
175 | |
176 | private function addGeneralHelp(): void { |
177 | $this->getOutput()->addHTML( $this->msg( |
178 | 'oathauth-ui-general-help' |
179 | )->parseAsBlock() ); |
180 | } |
181 | |
182 | private function addModuleHTML( ?IModule $module ): void { |
183 | if ( $module instanceof IModule && $this->isModuleRequested( $module ) ) { |
184 | $this->addCustomContent( $module ); |
185 | return; |
186 | } |
187 | |
188 | $panel = $this->getGenericContent( $module ); |
189 | if ( $module instanceof IModule && $this->isModuleEnabled( $module ) ) { |
190 | $this->addCustomContent( $module, $panel ); |
191 | } |
192 | |
193 | $this->getOutput()->addHTML( (string)$panel ); |
194 | } |
195 | |
196 | /** |
197 | * Get the panel with generic content for a module |
198 | */ |
199 | private function getGenericContent( ?IModule $module ): PanelLayout { |
200 | $modulePanel = new PanelLayout( [ |
201 | 'framed' => true, |
202 | 'expanded' => false, |
203 | 'padded' => true |
204 | ] ); |
205 | $headerLayout = new HorizontalLayout(); |
206 | |
207 | $label = new LabelWidget( [ |
208 | 'label' => $module->getDisplayName()->text() |
209 | ] ); |
210 | if ( $this->shouldShowGenericButtons() ) { |
211 | $enabled = $module && $this->isModuleEnabled( $module ); |
212 | $button = new ButtonWidget( [ |
213 | 'label' => $this |
214 | ->msg( $enabled ? 'oathauth-disable-generic' : 'oathauth-enable-generic' ) |
215 | ->text(), |
216 | 'href' => $this->getOutput()->getTitle()->getLocalURL( [ |
217 | 'action' => $enabled ? static::ACTION_DISABLE : static::ACTION_ENABLE, |
218 | 'module' => $module->getName(), |
219 | 'warn' => 1 |
220 | ] ) |
221 | ] ); |
222 | $headerLayout->addItems( [ $button ] ); |
223 | } |
224 | $headerLayout->addItems( [ $label ] ); |
225 | |
226 | $modulePanel->appendContent( $headerLayout ); |
227 | $modulePanel->appendContent( new HtmlSnippet( |
228 | $module->getDescriptionMessage()->parseAsBlock() |
229 | ) ); |
230 | return $modulePanel; |
231 | } |
232 | |
233 | private function addCustomContent( IModule $module, ?PanelLayout $panel = null ): void { |
234 | $form = $module->getManageForm( |
235 | $this->action, |
236 | $this->authUser, |
237 | $this->userRepo, |
238 | $this->getContext() |
239 | ); |
240 | if ( $form === null || !$this->isValidFormType( $form ) ) { |
241 | return; |
242 | } |
243 | $form->setTitle( $this->getOutput()->getTitle() ); |
244 | $this->ensureRequiredFormFields( $form, $module ); |
245 | $form->setSubmitCallback( [ $form, 'onSubmit' ] ); |
246 | if ( $form->show( $panel ) ) { |
247 | $form->onSuccess(); |
248 | } |
249 | } |
250 | |
251 | private function addHeading( Message $message ): void { |
252 | $this->getOutput()->addHTML( Html::element( 'h2', [], $message->text() ) ); |
253 | } |
254 | |
255 | private function shouldShowGenericButtons(): bool { |
256 | return !$this->requestedModule instanceof IModule || !$this->isGenericAction(); |
257 | } |
258 | |
259 | private function isModuleRequested( ?IModule $module ): bool { |
260 | return ( |
261 | $this->requestedModule instanceof IModule |
262 | && $module instanceof IModule |
263 | && $this->requestedModule->getName() === $module->getName() |
264 | ); |
265 | } |
266 | |
267 | private function isModuleEnabled( IModule $module ): bool { |
268 | $enabled = $this->authUser->getModule(); |
269 | if ( !$enabled ) { |
270 | return false; |
271 | } |
272 | return $enabled->getName() === $module->getName(); |
273 | } |
274 | |
275 | /** |
276 | * Verifies if the module is available to be enabled |
277 | * |
278 | * @param IModule $module |
279 | * @return bool |
280 | */ |
281 | private function isModuleAvailable( IModule $module ): bool { |
282 | $form = $module->getManageForm( |
283 | static::ACTION_ENABLE, |
284 | $this->authUser, |
285 | $this->userRepo, |
286 | $this->getContext() |
287 | ); |
288 | if ( $form === '' ) { |
289 | return false; |
290 | } |
291 | return true; |
292 | } |
293 | |
294 | /** |
295 | * Verifies if the given form instance fulfills the required conditions |
296 | * |
297 | * @param mixed $form |
298 | * @return bool |
299 | */ |
300 | private function isValidFormType( $form ): bool { |
301 | if ( !( $form instanceof HTMLForm ) ) { |
302 | return false; |
303 | } |
304 | $implements = class_implements( $form ); |
305 | if ( !isset( $implements[IManageForm::class] ) ) { |
306 | return false; |
307 | } |
308 | |
309 | return true; |
310 | } |
311 | |
312 | private function ensureRequiredFormFields( IManageForm $form, IModule $module ): void { |
313 | if ( !$form->hasField( 'module' ) ) { |
314 | $form->addHiddenField( 'module', $module->getName() ); |
315 | } |
316 | if ( !$form->hasField( 'action' ) ) { |
317 | $form->addHiddenField( 'action', $this->action ); |
318 | } |
319 | } |
320 | |
321 | /** |
322 | * When performing an action on a module (like enable/disable), |
323 | * page should contain only the form for that action. |
324 | */ |
325 | private function clearPage(): void { |
326 | if ( $this->isGenericAction() ) { |
327 | $displayName = $this->requestedModule->getDisplayName(); |
328 | $pageTitleMessage = $this->isModuleEnabled( $this->requestedModule ) ? |
329 | $this->msg( 'oathauth-disable-page-title', $displayName ) : |
330 | $this->msg( 'oathauth-enable-page-title', $displayName ); |
331 | $this->getOutput()->setPageTitleMsg( $pageTitleMessage ); |
332 | } |
333 | |
334 | $this->getOutput()->clearHTML(); |
335 | $this->getOutput()->addBacklinkSubtitle( $this->getOutput()->getTitle() ); |
336 | } |
337 | |
338 | /** |
339 | * The enable and disable actions are generic, and all modules must |
340 | * implement them, while all other actions are module-specific. |
341 | */ |
342 | private function isGenericAction(): bool { |
343 | return in_array( $this->action, [ static::ACTION_ENABLE, static::ACTION_DISABLE ] ); |
344 | } |
345 | |
346 | private function hasAlternativeModules(): bool { |
347 | foreach ( $this->moduleRegistry->getAllModules() as $module ) { |
348 | if ( !$this->isModuleEnabled( $module ) && $this->isModuleAvailable( $module ) ) { |
349 | return true; |
350 | } |
351 | } |
352 | return false; |
353 | } |
354 | |
355 | private function shouldShowDisableWarning(): bool { |
356 | return $this->getRequest()->getBool( 'warn' ) && |
357 | $this->requestedModule instanceof IModule && |
358 | $this->authUser->isTwoFactorAuthEnabled(); |
359 | } |
360 | |
361 | private function showDisableWarning(): void { |
362 | $panel = new PanelLayout( [ |
363 | 'padded' => true, |
364 | 'framed' => true, |
365 | 'expanded' => false |
366 | ] ); |
367 | |
368 | $isSwitch = $this->isSwitch(); |
369 | $currentDisplayName = $this->authUser->getModule()->getDisplayName(); |
370 | $newDisplayName = $this->requestedModule->getDisplayName(); |
371 | |
372 | $genericMessage = $isSwitch ? |
373 | $this->msg( |
374 | 'oathauth-switch-method-warning', |
375 | $currentDisplayName, |
376 | $newDisplayName |
377 | ) : |
378 | $this->msg( 'oathauth-disable-method-warning', $currentDisplayName ); |
379 | |
380 | $panel->appendContent( new HtmlSnippet( |
381 | $genericMessage->parseAsBlock() |
382 | ) ); |
383 | |
384 | $customMessage = $this->authUser->getModule()->getDisableWarningMessage(); |
385 | if ( $customMessage instanceof Message ) { |
386 | $panel->appendContent( new HtmlSnippet( |
387 | $customMessage->parseAsBlock() |
388 | ) ); |
389 | } |
390 | |
391 | $nextStepMessage = $isSwitch ? |
392 | $this->msg( 'oathauth-switch-method-next-step', $currentDisplayName ) : |
393 | $this->msg( 'oathauth-disable-method-next-step', $currentDisplayName, $newDisplayName ); |
394 | |
395 | $panel->appendContent( new HtmlSnippet( |
396 | $nextStepMessage->parseAsBlock() |
397 | ) ); |
398 | |
399 | $button = new ButtonWidget( [ |
400 | 'label' => $this->msg( 'oathauth-disable-method-warning-button-label' )->plain(), |
401 | 'href' => $this->getOutput()->getTitle()->getLocalURL( [ |
402 | 'action' => $this->action, |
403 | 'module' => $this->requestedModule->getName() |
404 | ] ), |
405 | 'flags' => [ 'primary', 'progressive' ] |
406 | ] ); |
407 | $panel->appendContent( $button ); |
408 | |
409 | $headerMessage = $isSwitch ? |
410 | $this->msg( 'oathauth-switch-method-warning-header' ) : |
411 | $this->msg( 'oathauth-disable-method-warning-header' ); |
412 | |
413 | $this->getOutput()->setPageTitleMsg( $headerMessage ); |
414 | $this->getOutput()->addHTML( $panel->toString() ); |
415 | } |
416 | |
417 | private function isSwitch(): bool { |
418 | return $this->requestedModule instanceof IModule && |
419 | $this->action === static::ACTION_ENABLE && |
420 | $this->authUser->isTwoFactorAuthEnabled(); |
421 | } |
422 | |
423 | } |