Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
OATHManage
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 26
4692
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 checkPermissions
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
 setAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setModule
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addEnabledHTML
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addAlternativesHTML
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 nothingEnabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addInactiveHTML
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 addGeneralHelp
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 addModuleHTML
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 getGenericContent
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 addCustomContent
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 addHeading
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 shouldShowGenericButtons
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isModuleRequested
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isModuleEnabled
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isValidFormType
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 ensureRequiredFormFields
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 clearPage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 isGenericAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasAlternativeModules
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 shouldShowDisableWarning
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 showDisableWarning
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
20
 isSwitch
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
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
20namespace MediaWiki\Extension\OATHAuth\Special;
21
22use HTMLForm;
23use MediaWiki\Config\ConfigException;
24use MediaWiki\Extension\OATHAuth\HTMLForm\IManageForm;
25use MediaWiki\Extension\OATHAuth\IModule;
26use MediaWiki\Extension\OATHAuth\OATHAuthModuleRegistry;
27use MediaWiki\Extension\OATHAuth\OATHUser;
28use MediaWiki\Extension\OATHAuth\OATHUserRepository;
29use MediaWiki\Html\Html;
30use MediaWiki\SpecialPage\SpecialPage;
31use Message;
32use MWException;
33use OOUI\ButtonWidget;
34use OOUI\HorizontalLayout;
35use OOUI\HtmlSnippet;
36use OOUI\LabelWidget;
37use OOUI\PanelLayout;
38use PermissionsError;
39use UserNotLoggedIn;
40
41class 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}