Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 170 |
|
0.00% |
0 / 26 |
CRAP | |
0.00% |
0 / 1 |
OATHManage | |
0.00% |
0 / 170 |
|
0.00% |
0 / 26 |
4692 | |
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 | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 | |||
checkPermissions | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
56 | |||
setAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setModule | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
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 |
12 | |||
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 | |||
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 |
12 | |||
shouldShowDisableWarning | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
showDisableWarning | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
20 | |||
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 HTMLForm; |
23 | use MediaWiki\Config\ConfigException; |
24 | use MediaWiki\Extension\OATHAuth\HTMLForm\IManageForm; |
25 | use MediaWiki\Extension\OATHAuth\IModule; |
26 | use MediaWiki\Extension\OATHAuth\OATHAuthModuleRegistry; |
27 | use MediaWiki\Extension\OATHAuth\OATHUser; |
28 | use MediaWiki\Extension\OATHAuth\OATHUserRepository; |
29 | use MediaWiki\Html\Html; |
30 | use MediaWiki\SpecialPage\SpecialPage; |
31 | use Message; |
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 | /** |
57 | * @var IModule|null |
58 | */ |
59 | protected $requestedModule; |
60 | |
61 | /** |
62 | * Initializes a page to manage available 2FA modules |
63 | * |
64 | * @param OATHUserRepository $userRepo |
65 | * @param OATHAuthModuleRegistry $moduleRegistry |
66 | * |
67 | * @throws ConfigException |
68 | * @throws MWException |
69 | */ |
70 | public function __construct( OATHUserRepository $userRepo, OATHAuthModuleRegistry $moduleRegistry ) { |
71 | // messages used: oathmanage (display "name" on Special:SpecialPages), |
72 | // right-oathauth-enable, action-oathauth-enable |
73 | parent::__construct( 'OATHManage', 'oathauth-enable' ); |
74 | |
75 | $this->userRepo = $userRepo; |
76 | $this->moduleRegistry = $moduleRegistry; |
77 | $this->authUser = $this->userRepo->findByUser( $this->getUser() ); |
78 | } |
79 | |
80 | /** |
81 | * @inheritDoc |
82 | */ |
83 | protected function getGroupName() { |
84 | return 'login'; |
85 | } |
86 | |
87 | /** |
88 | * @param null|string $subPage |
89 | */ |
90 | public function execute( $subPage ) { |
91 | $this->getOutput()->enableOOUI(); |
92 | $this->getOutput()->disallowUserJs(); |
93 | $this->setAction(); |
94 | $this->setModule(); |
95 | |
96 | parent::execute( $subPage ); |
97 | |
98 | if ( $this->requestedModule instanceof IModule ) { |
99 | // Performing an action on a requested module |
100 | $this->clearPage(); |
101 | if ( $this->shouldShowDisableWarning() ) { |
102 | $this->showDisableWarning(); |
103 | return; |
104 | } |
105 | $this->addModuleHTML( $this->requestedModule ); |
106 | return; |
107 | } |
108 | |
109 | $this->addGeneralHelp(); |
110 | if ( $this->authUser->isTwoFactorAuthEnabled() ) { |
111 | $this->addEnabledHTML(); |
112 | if ( $this->hasAlternativeModules() ) { |
113 | $this->addAlternativesHTML(); |
114 | } |
115 | return; |
116 | } |
117 | $this->nothingEnabled(); |
118 | } |
119 | |
120 | /** |
121 | * @throws PermissionsError |
122 | * @throws UserNotLoggedIn |
123 | */ |
124 | public function checkPermissions() { |
125 | $this->requireLogin(); |
126 | |
127 | $canEnable = $this->getUser()->isAllowed( 'oathauth-enable' ); |
128 | |
129 | if ( $this->action === static::ACTION_ENABLE && !$canEnable ) { |
130 | $this->displayRestrictionError(); |
131 | } |
132 | |
133 | if ( !$this->authUser->isTwoFactorAuthEnabled() && !$canEnable ) { |
134 | // No enabled module and cannot enable - nothing to do |
135 | $this->displayRestrictionError(); |
136 | } |
137 | |
138 | if ( $this->action === static::ACTION_ENABLE && !$this->getRequest()->wasPosted() ) { |
139 | // Trying to change the 2FA method (one is already enabled) |
140 | $this->checkLoginSecurityLevel( 'oathauth-enable' ); |
141 | } |
142 | } |
143 | |
144 | private function setAction(): void { |
145 | $this->action = $this->getRequest()->getVal( 'action', '' ); |
146 | } |
147 | |
148 | private function setModule(): void { |
149 | $moduleKey = $this->getRequest()->getVal( 'module', '' ); |
150 | $this->requestedModule = $this->moduleRegistry->getModuleByKey( $moduleKey ); |
151 | } |
152 | |
153 | private function addEnabledHTML(): void { |
154 | $this->addHeading( $this->msg( 'oathauth-ui-enabled-module' ) ); |
155 | $this->addModuleHTML( $this->authUser->getModule() ); |
156 | } |
157 | |
158 | private function addAlternativesHTML(): void { |
159 | $this->addHeading( $this->msg( 'oathauth-ui-not-enabled-modules' ) ); |
160 | $this->addInactiveHTML(); |
161 | } |
162 | |
163 | private function nothingEnabled(): void { |
164 | $this->addHeading( $this->msg( 'oathauth-ui-available-modules' ) ); |
165 | $this->addInactiveHTML(); |
166 | } |
167 | |
168 | private function addInactiveHTML(): void { |
169 | foreach ( $this->moduleRegistry->getAllModules() as $module ) { |
170 | if ( $this->isModuleEnabled( $module ) ) { |
171 | continue; |
172 | } |
173 | $this->addModuleHTML( $module ); |
174 | } |
175 | } |
176 | |
177 | private function addGeneralHelp(): void { |
178 | $this->getOutput()->addHTML( $this->msg( |
179 | 'oathauth-ui-general-help' |
180 | )->parseAsBlock() ); |
181 | } |
182 | |
183 | private function addModuleHTML( ?IModule $module ): void { |
184 | if ( $module instanceof IModule && $this->isModuleRequested( $module ) ) { |
185 | $this->addCustomContent( $module ); |
186 | return; |
187 | } |
188 | |
189 | $panel = $this->getGenericContent( $module ); |
190 | if ( $module instanceof IModule && $this->isModuleEnabled( $module ) ) { |
191 | $this->addCustomContent( $module, $panel ); |
192 | } |
193 | |
194 | $this->getOutput()->addHTML( (string)$panel ); |
195 | } |
196 | |
197 | /** |
198 | * Get the panel with generic content for a module |
199 | */ |
200 | private function getGenericContent( ?IModule $module ): PanelLayout { |
201 | $modulePanel = new PanelLayout( [ |
202 | 'framed' => true, |
203 | 'expanded' => false, |
204 | 'padded' => true |
205 | ] ); |
206 | $headerLayout = new HorizontalLayout(); |
207 | |
208 | $label = new LabelWidget( [ |
209 | 'label' => $module->getDisplayName()->text() |
210 | ] ); |
211 | if ( $this->shouldShowGenericButtons() ) { |
212 | $enabled = $module && $this->isModuleEnabled( $module ); |
213 | $button = new ButtonWidget( [ |
214 | 'label' => $this |
215 | ->msg( $enabled ? 'oathauth-disable-generic' : 'oathauth-enable-generic' ) |
216 | ->text(), |
217 | 'href' => $this->getOutput()->getTitle()->getLocalURL( [ |
218 | 'action' => $enabled ? static::ACTION_DISABLE : static::ACTION_ENABLE, |
219 | 'module' => $module->getName(), |
220 | 'warn' => 1 |
221 | ] ) |
222 | ] ); |
223 | $headerLayout->addItems( [ $button ] ); |
224 | } |
225 | $headerLayout->addItems( [ $label ] ); |
226 | |
227 | $modulePanel->appendContent( $headerLayout ); |
228 | $modulePanel->appendContent( new HtmlSnippet( |
229 | $module->getDescriptionMessage()->parseAsBlock() |
230 | ) ); |
231 | return $modulePanel; |
232 | } |
233 | |
234 | private function addCustomContent( IModule $module, PanelLayout $panel = null ): void { |
235 | $form = $module->getManageForm( |
236 | $this->action, |
237 | $this->authUser, |
238 | $this->userRepo, |
239 | $this->getContext() |
240 | ); |
241 | if ( $form === null || !$this->isValidFormType( $form ) ) { |
242 | return; |
243 | } |
244 | $form->setTitle( $this->getOutput()->getTitle() ); |
245 | $this->ensureRequiredFormFields( $form, $module ); |
246 | $form->setSubmitCallback( [ $form, 'onSubmit' ] ); |
247 | if ( $form->show( $panel ) ) { |
248 | $form->onSuccess(); |
249 | } |
250 | } |
251 | |
252 | private function addHeading( Message $message ): void { |
253 | $this->getOutput()->addHTML( Html::element( 'h2', [], $message->text() ) ); |
254 | } |
255 | |
256 | private function shouldShowGenericButtons(): bool { |
257 | return !$this->requestedModule instanceof IModule || !$this->isGenericAction(); |
258 | } |
259 | |
260 | private function isModuleRequested( ?IModule $module ): bool { |
261 | return ( |
262 | $this->requestedModule instanceof IModule |
263 | && $module instanceof IModule |
264 | && $this->requestedModule->getName() === $module->getName() |
265 | ); |
266 | } |
267 | |
268 | private function isModuleEnabled( IModule $module ): bool { |
269 | $enabled = $this->authUser->getModule(); |
270 | if ( !$enabled ) { |
271 | return false; |
272 | } |
273 | return $enabled->getName() === $module->getName(); |
274 | } |
275 | |
276 | /** |
277 | * Verifies if the given form instance fulfills the required conditions |
278 | * |
279 | * @param mixed $form |
280 | * @return bool |
281 | */ |
282 | private function isValidFormType( $form ): bool { |
283 | if ( !( $form instanceof HTMLForm ) ) { |
284 | return false; |
285 | } |
286 | $implements = class_implements( $form ); |
287 | if ( !isset( $implements[IManageForm::class] ) ) { |
288 | return false; |
289 | } |
290 | |
291 | return true; |
292 | } |
293 | |
294 | private function ensureRequiredFormFields( IManageForm $form, IModule $module ): void { |
295 | if ( !$form->hasField( 'module' ) ) { |
296 | $form->addHiddenField( 'module', $module->getName() ); |
297 | } |
298 | if ( !$form->hasField( 'action' ) ) { |
299 | $form->addHiddenField( 'action', $this->action ); |
300 | } |
301 | } |
302 | |
303 | /** |
304 | * When performing an action on a module (like enable/disable), |
305 | * page should contain only the form for that action. |
306 | */ |
307 | private function clearPage(): void { |
308 | if ( $this->isGenericAction() ) { |
309 | $displayName = $this->requestedModule->getDisplayName(); |
310 | $pageTitleMessage = $this->isModuleEnabled( $this->requestedModule ) ? |
311 | $this->msg( 'oathauth-disable-page-title', $displayName ) : |
312 | $this->msg( 'oathauth-enable-page-title', $displayName ); |
313 | $this->getOutput()->setPageTitleMsg( $pageTitleMessage ); |
314 | } |
315 | |
316 | $this->getOutput()->clearHTML(); |
317 | $this->getOutput()->addBacklinkSubtitle( $this->getOutput()->getTitle() ); |
318 | } |
319 | |
320 | /** |
321 | * The enable and disable actions are generic, and all modules must |
322 | * implement them, while all other actions are module-specific. |
323 | */ |
324 | private function isGenericAction(): bool { |
325 | return in_array( $this->action, [ static::ACTION_ENABLE, static::ACTION_DISABLE ] ); |
326 | } |
327 | |
328 | private function hasAlternativeModules(): bool { |
329 | foreach ( $this->moduleRegistry->getAllModules() as $module ) { |
330 | if ( !$this->isModuleEnabled( $module ) ) { |
331 | return true; |
332 | } |
333 | } |
334 | return false; |
335 | } |
336 | |
337 | private function shouldShowDisableWarning(): bool { |
338 | return $this->getRequest()->getBool( 'warn' ) && |
339 | $this->requestedModule instanceof IModule && |
340 | $this->authUser->isTwoFactorAuthEnabled(); |
341 | } |
342 | |
343 | private function showDisableWarning(): void { |
344 | $panel = new PanelLayout( [ |
345 | 'padded' => true, |
346 | 'framed' => true, |
347 | 'expanded' => false |
348 | ] ); |
349 | $headerMessage = $this->isSwitch() ? |
350 | $this->msg( 'oathauth-switch-method-warning-header' ) : |
351 | $this->msg( 'oathauth-disable-method-warning-header' ); |
352 | $genericMessage = $this->isSwitch() ? |
353 | $this->msg( |
354 | 'oathauth-switch-method-warning', |
355 | $this->authUser->getModule()->getDisplayName(), |
356 | $this->requestedModule->getDisplayName() |
357 | ) : |
358 | $this->msg( 'oathauth-disable-method-warning', $this->authUser->getModule()->getDisplayName() ); |
359 | |
360 | $panel->appendContent( new HtmlSnippet( |
361 | $genericMessage->parseAsBlock() |
362 | ) ); |
363 | |
364 | $customMessage = $this->authUser->getModule()->getDisableWarningMessage(); |
365 | if ( $customMessage instanceof Message ) { |
366 | $panel->appendContent( new HtmlSnippet( |
367 | $customMessage->parseAsBlock() |
368 | ) ); |
369 | } |
370 | |
371 | $button = new ButtonWidget( [ |
372 | 'label' => $this->msg( 'oathauth-disable-method-warning-button-label' )->plain(), |
373 | 'href' => $this->getOutput()->getTitle()->getLocalURL( [ |
374 | 'action' => $this->action, |
375 | 'module' => $this->requestedModule->getName() |
376 | ] ), |
377 | 'flags' => [ 'primary', 'progressive' ] |
378 | ] ); |
379 | $panel->appendContent( $button ); |
380 | |
381 | $this->getOutput()->setPageTitleMsg( $headerMessage ); |
382 | $this->getOutput()->addHTML( $panel->toString() ); |
383 | } |
384 | |
385 | private function isSwitch(): bool { |
386 | return $this->requestedModule instanceof IModule && |
387 | $this->action === static::ACTION_ENABLE && |
388 | $this->authUser->isTwoFactorAuthEnabled(); |
389 | } |
390 | |
391 | } |