Completed
Pull Request — master (#361)
by
unknown
03:02
created

SettingsController::actionTwoFactorEnable()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 0
cts 14
cp 0
rs 9.472
c 0
b 0
f 0
cc 4
nc 5
nop 1
crap 20
1
<?php
2
3
/*
4
 * This file is part of the 2amigos/yii2-usuario project.
5
 *
6
 * (c) 2amigOS! <http://2amigos.us/>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace Da\User\Controller;
13
14
use Da\User\Contracts\MailChangeStrategyInterface;
15
use Da\User\Event\GdprEvent;
16
use Da\User\Event\ProfileEvent;
17
use Da\User\Event\SocialNetworkConnectEvent;
18
use Da\User\Event\UserEvent;
19
use Da\User\Form\GdprDeleteForm;
20
use Da\User\Form\SettingsForm;
21
use Da\User\Helper\SecurityHelper;
22
use Da\User\Model\Profile;
23
use Da\User\Model\SocialNetworkAccount;
24
use Da\User\Model\User;
25
use Da\User\Module;
26
use Da\User\Query\ProfileQuery;
27
use Da\User\Query\SocialNetworkAccountQuery;
28
use Da\User\Query\UserQuery;
29
use Da\User\Search\SessionHistorySearch;
30
use Da\User\Service\EmailChangeService;
31
use Da\User\Service\SessionHistory\TerminateUserSessionsService;
32
use Da\User\Service\TwoFactorQrCodeUriGeneratorService;
33
use Da\User\Traits\ContainerAwareTrait;
34
use Da\User\Traits\ModuleAwareTrait;
35
use Da\User\Validator\AjaxRequestModelValidator;
36
use Da\User\Validator\TwoFactorCodeValidator;
37
use Yii;
38
use yii\base\DynamicModel;
39
use yii\filters\AccessControl;
40
use yii\filters\VerbFilter;
41
use yii\helpers\ArrayHelper;
42
use yii\web\Controller;
43
use yii\web\ForbiddenHttpException;
44
use yii\web\NotFoundHttpException;
45
use yii\web\Response;
46
47
class SettingsController extends Controller
48
{
49
    use ContainerAwareTrait;
50
    use ModuleAwareTrait;
51
52
    /**
53
     * {@inheritdoc}
54
     */
55
    public $defaultAction = 'profile';
56
57
    protected $profileQuery;
58
    protected $userQuery;
59
    protected $socialNetworkAccountQuery;
60
61
    /**
62
     * SettingsController constructor.
63
     *
64
     * @param string                    $id
65
     * @param Module                    $module
66
     * @param ProfileQuery              $profileQuery
67
     * @param UserQuery                 $userQuery
68
     * @param SocialNetworkAccountQuery $socialNetworkAccountQuery
69
     * @param array                     $config
70
     */
71 4
    public function __construct(
72
        $id,
73
        Module $module,
74
        ProfileQuery $profileQuery,
75
        UserQuery $userQuery,
76
        SocialNetworkAccountQuery $socialNetworkAccountQuery,
77
        array $config = []
78
    ) {
79 4
        $this->profileQuery = $profileQuery;
80 4
        $this->userQuery = $userQuery;
81 4
        $this->socialNetworkAccountQuery = $socialNetworkAccountQuery;
82 4
        parent::__construct($id, $module, $config);
83 4
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 4
    public function behaviors()
89
    {
90
        return [
91 4
            'verbs' => [
92
                'class' => VerbFilter::class,
93
                'actions' => [
94
                    'disconnect' => ['post'],
95
                    'delete' => ['post'],
96
                    'two-factor-disable' => ['post'],
97
                    'terminate-sessions' => ['post'],
98
                ],
99
            ],
100
            'access' => [
101
                'class' => AccessControl::class,
102
                'rules' => [
103
                    [
104
                        'allow' => true,
105
                        'actions' => [
106
                            'profile',
107
                            'account',
108
                            'export',
109
                            'networks',
110
                            'privacy',
111
                            'gdpr-consent',
112
                            'gdpr-delete',
113
                            'disconnect',
114
                            'delete',
115
                            'two-factor',
116
                            'two-factor-enable',
117
                            'two-factor-disable',
118
                        ],
119
                        'roles' => ['@'],
120
                    ],
121
                    [
122
                        'allow' => true,
123
                        'actions' => ['confirm'],
124
                        'roles' => ['?', '@'],
125
                    ],
126
                    [
127 4
                        'allow' => $this->getModule()->enableSessionHistory,
128
                        'actions' => ['session-history', 'terminate-sessions'],
129
                        'roles' => ['@'],
130
                    ],
131
                ],
132
            ],
133
        ];
134
    }
135
136
    /**
137
     * @throws \yii\base\InvalidConfigException
138
     * @return string|Response
139
     */
140 1
    public function actionProfile()
141
    {
142 1
        $profile = $this->profileQuery->whereUserId(Yii::$app->user->identity->getId())->one();
143
144 1
        if ($profile === null) {
145
            $profile = $this->make(Profile::class);
146
            $profile->link('user', Yii::$app->user->identity);
147
        }
148
149
        /** @var ProfileEvent $event */
150 1
        $event = $this->make(ProfileEvent::class, [$profile]);
151
152 1
        $this->make(AjaxRequestModelValidator::class, [$profile])->validate();
153
154 1
        if ($profile->load(Yii::$app->request->post())) {
155
            $this->trigger(UserEvent::EVENT_BEFORE_PROFILE_UPDATE, $event);
156
            if ($profile->save()) {
157
                Yii::$app->getSession()->setFlash('success', Yii::t('usuario', 'Your profile has been updated'));
0 ignored issues
show
Bug introduced by
The method getSession does only exist in yii\web\Application, but not in yii\console\Application.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
158
                $this->trigger(UserEvent::EVENT_AFTER_PROFILE_UPDATE, $event);
159
160
                return $this->refresh();
161
            }
162
        }
163
164 1
        return $this->render(
165 1
            'profile',
166
            [
167 1
                'model' => $profile,
168
            ]
169
        );
170
    }
171
172
    /**
173
     * @throws NotFoundHttpException
174
     * @return string
175
     */
176 2
    public function actionPrivacy()
177
    {
178 2
        if (!$this->module->enableGdprCompliance) {
179 1
            throw new NotFoundHttpException();
180
        }
181 1
        return $this->render('privacy', [
182 1
            'module' => $this->module
183
        ]);
184
    }
185
186
    /**
187
     * @throws NotFoundHttpException
188
     * @throws \Throwable
189
     * @throws \yii\base\Exception
190
     * @throws \yii\base\InvalidConfigException
191
     * @throws \yii\db\StaleObjectException
192
     * @throws ForbiddenHttpException
193
     * @return string|Response
194
     */
195 1
    public function actionGdprDelete()
196
    {
197 1
        if (!$this->module->enableGdprCompliance) {
198
            throw new NotFoundHttpException();
199
        }
200
        /** @var GdprDeleteForm $form */
201 1
        $form = $this->make(GdprDeleteForm::class);
202
203 1
        $user = $form->getUser();
204
        /* @var $event GdprEvent */
205 1
        $event = $this->make(GdprEvent::class, [$user]);
206
207 1
        if ($form->load(Yii::$app->request->post()) && $form->validate()) {
208 1
            $this->trigger(GdprEvent::EVENT_BEFORE_DELETE, $event);
209
210 1
            if ($event->isValid) {
211 1
                Yii::$app->user->logout();
212
                //Disconnect social networks
213 1
                $networks = $this->socialNetworkAccountQuery->where(['user_id' => $user->id])->all();
0 ignored issues
show
Bug introduced by
Accessing id on the interface yii\web\IdentityInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
214 1
                foreach ($networks as $network) {
215
                    $this->disconnectSocialNetwork($network->id);
216
                }
217
218
                /* @var $security SecurityHelper */
219 1
                $security = $this->make(SecurityHelper::class);
220 1
                $anonymReplacement = $this->module->gdprAnonymizePrefix . $user->id;
0 ignored issues
show
Bug introduced by
Accessing id on the interface yii\web\IdentityInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
221
222 1
                $user->updateAttributes([
223 1
                    'email' => $anonymReplacement . "@example.com",
224 1
                    'username' => $anonymReplacement,
225 1
                    'gdpr_deleted' => 1,
226 1
                    'blocked_at' => time(),
227 1
                    'auth_key' => $security->generateRandomString()
228
                ]);
229 1
                $user->profile->updateAttributes([
0 ignored issues
show
Bug introduced by
Accessing profile on the interface yii\web\IdentityInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
230 1
                    'public_email' => $anonymReplacement . "@example.com",
231 1
                    'name' => $anonymReplacement,
232 1
                    'gravatar_email' => $anonymReplacement . "@example.com",
233 1
                    'location' => $anonymReplacement,
234 1
                    'website' => $anonymReplacement . ".tld",
235 1
                    'bio' => Yii::t('usuario', 'Deleted by GDPR request')
236
                ]);
237
            }
238 1
            $this->trigger(GdprEvent::EVENT_AFTER_DELETE, $event);
239
240 1
            Yii::$app->session->setFlash('info', Yii::t('usuario', 'Your personal information has been removed'));
241
242 1
            return $this->goHome();
243
        }
244
245 1
        return $this->render('gdpr-delete', [
246 1
            'model' => $form,
247
        ]);
248
    }
249
250 1
    public function actionGdprConsent()
251
    {
252
        /** @var User $user */
253 1
        $user = Yii::$app->user->identity;
254 1
        if ($user->gdpr_consent) {
255
            return $this->redirect(['profile']);
256
        }
257 1
        $model = new DynamicModel(['gdpr_consent']);
258 1
        $model->addRule('gdpr_consent', 'boolean');
259 1
        $model->addRule('gdpr_consent', 'default', ['value' => 0, 'skipOnEmpty' => false]);
260 1
        $model->addRule('gdpr_consent', 'compare', [
261 1
            'compareValue' => true,
262 1
            'message' => Yii::t('usuario', 'Your consent is required to work with this site'),
263
            'when' => function () {
264 1
                return $this->module->enableGdprCompliance;
265 1
            },
266
        ]);
267 1
        if ($model->load(Yii::$app->request->post()) && $model->validate()) {
268 1
            $user->updateAttributes([
269 1
                'gdpr_consent' => 1,
270 1
                'gdpr_consent_date' => time(),
271
            ]);
272 1
            return $this->redirect(['profile']);
273
        }
274
275 1
        return $this->render('gdpr-consent', [
276 1
            'model' => $model,
277 1
            'gdpr_consent_hint' => $this->module->getConsentMessage(),
278
        ]);
279
    }
280
281
    /**
282
     * Exports the data from the current user in a mechanical readable format (csv). Properties exported can be defined
283
     * in the module configuration.
284
     * @throws NotFoundHttpException if gdpr compliance is not enabled
285
     * @throws \Exception
286
     * @throws \Throwable
287
     */
288
    public function actionExport()
289
    {
290
        if (!$this->module->enableGdprCompliance) {
291
            throw new NotFoundHttpException();
292
        }
293
        try {
294
            $properties = $this->module->gdprExportProperties;
295
            $user = Yii::$app->user->identity;
296
            $data = [$properties, []];
297
298
            $formatter = Yii::$app->formatter;
299
            // override the default html-specific format for nulls
300
            $formatter->nullDisplay = "";
301
302
            foreach ($properties as $property) {
303
                $data[1][] = $formatter->asText(ArrayHelper::getValue($user, $property));
304
            }
305
306
            array_walk($data[0], function (&$value, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
307
                $splitted = explode('.', $value);
308
                $value = array_pop($splitted);
309
            });
310
311
            Yii::$app->response->headers->removeAll();
312
            Yii::$app->response->headers->add('Content-type', 'text/csv');
313
            Yii::$app->response->headers->add('Content-Disposition', 'attachment;filename=gdpr-data.csv');
314
            Yii::$app->response->send();
315
            $f = fopen('php://output', 'w');
316
            foreach ($data as $line) {
317
                fputcsv($f, $line);
318
            }
319
        } catch (\Exception $e) {
320
            throw $e;
321
        } catch (\Throwable $e) {
322
            throw $e;
323
        }
324
    }
325
326 1
    public function actionAccount()
327
    {
328
        /** @var SettingsForm $form */
329 1
        $form = $this->make(SettingsForm::class);
330 1
        $event = $this->make(UserEvent::class, [$form->getUser()]);
331
332 1
        $this->make(AjaxRequestModelValidator::class, [$form])->validate();
333
334 1
        if ($form->load(Yii::$app->request->post())) {
335 1
            $this->trigger(UserEvent::EVENT_BEFORE_ACCOUNT_UPDATE, $event);
336
337 1
            if ($form->save()) {
338 1
                Yii::$app->getSession()->setFlash(
0 ignored issues
show
Bug introduced by
The method getSession does only exist in yii\web\Application, but not in yii\console\Application.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
339 1
                    'success',
340 1
                    Yii::t('usuario', 'Your account details have been updated')
341
                );
342 1
                $this->trigger(UserEvent::EVENT_AFTER_ACCOUNT_UPDATE, $event);
343
344 1
                return $this->refresh();
345
            }
346
        }
347
348 1
        return $this->render(
349 1
            'account',
350
            [
351 1
                'model' => $form,
352
            ]
353
        );
354
    }
355
356
    public function actionConfirm($id, $code)
357
    {
358
        $user = $this->userQuery->whereId($id)->one();
359
360
        if ($user === null || MailChangeStrategyInterface::TYPE_INSECURE === $this->module->emailChangeStrategy) {
361
            throw new NotFoundHttpException();
362
        }
363
        $event = $this->make(UserEvent::class, [$user]);
364
365
        $this->trigger(UserEvent::EVENT_BEFORE_CONFIRMATION, $event);
366
        if ($this->make(EmailChangeService::class, [$code, $user])->run()) {
367
            $this->trigger(UserEvent::EVENT_AFTER_CONFIRMATION, $event);
368
        }
369
370
        return $this->redirect(['account']);
371
    }
372
373
    public function actionNetworks()
374
    {
375
        return $this->render(
376
            'networks',
377
            [
378
                'user' => Yii::$app->user->identity,
379
            ]
380
        );
381
    }
382
383
    public function actionDisconnect($id)
384
    {
385
        $this->disconnectSocialNetwork($id);
386
        return $this->redirect(['networks']);
387
    }
388
389
    public function actionDelete()
390
    {
391
        if (!$this->module->allowAccountDelete) {
392
            throw new NotFoundHttpException(Yii::t('usuario', 'Not found'));
393
        }
394
395
        /** @var User $user */
396
        $user = Yii::$app->user->identity;
397
        $event = $this->make(UserEvent::class, [$user]);
398
        Yii::$app->user->logout();
399
400
        $this->trigger(UserEvent::EVENT_BEFORE_DELETE, $event);
401
        $user->delete();
402
        $this->trigger(UserEvent::EVENT_AFTER_DELETE, $event);
403
404
        Yii::$app->session->setFlash('info', Yii::t('usuario', 'Your account has been completely deleted'));
405
406
        return $this->goHome();
407
    }
408
409
    public function actionTwoFactor($id)
410
    {
411
        /** @var User $user */
412
        $user = $this->userQuery->whereId($id)->one();
413
414
        if (null === $user) {
415
            throw new NotFoundHttpException();
416
        }
417
418
        $uri = $this->make(TwoFactorQrCodeUriGeneratorService::class, [$user])->run();
419
420
        return $this->renderAjax('two-factor', ['id' => $id, 'uri' => $uri]);
421
    }
422
423
    public function actionTwoFactorEnable($id)
424
    {
425
        Yii::$app->response->format = Response::FORMAT_JSON;
426
427
        /** @var User $user */
428
        $user = $this->userQuery->whereId($id)->one();
429
430
        if (null === $user) {
431
            return [
432
                'success' => false,
433
                'message' => Yii::t('usuario', 'User not found.')
434
            ];
435
        }
436
        $code = Yii::$app->request->get('code');
437
438
        $success = $this
439
            ->make(TwoFactorCodeValidator::class, [$user, $code, $this->module->twoFactorAuthenticationCycles])
440
            ->validate();
441
442
        $success = $success && $user->updateAttributes(['auth_tf_enabled' => '1']);
443
444
        return [
445
            'success' => $success,
446
            'message' => $success
447
                ? Yii::t('usuario', 'Two factor authentication successfully enabled.')
448
                : Yii::t('usuario', 'Verification failed. Please, enter new code.')
449
        ];
450
    }
451
452
    public function actionTwoFactorDisable($id)
453
    {
454
        /** @var User $user */
455
        $user = $this->userQuery->whereId($id)->one();
456
457
        if (null === $user) {
458
            throw new NotFoundHttpException();
459
        }
460
461
        if ($user->updateAttributes(['auth_tf_enabled' => '0'])) {
462
            Yii::$app
0 ignored issues
show
Bug introduced by
The method getSession does only exist in yii\web\Application, but not in yii\console\Application.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
463
                ->getSession()
464
                ->setFlash('success', Yii::t('usuario', 'Two factor authentication has been disabled.'));
465
        } else {
466
            Yii::$app
467
                ->getSession()
468
                ->setFlash('danger', Yii::t('usuario', 'Unable to disable Two factor authentication.'));
469
        }
470
471
        $this->redirect(['account']);
472
    }
473
474
    /**
475
     * Display list session history.
476
     */
477
    public function actionSessionHistory()
478
    {
479
        $searchModel = new SessionHistorySearch([
480
            'user_id' => Yii::$app->user->id,
481
        ]);
482
        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
483
484
        return $this->render('session-history', [
485
            'searchModel' => $searchModel,
486
            'dataProvider' => $dataProvider,
487
        ]);
488
    }
489
490
    /**
491
     * Terminate all session user
492
     */
493
    public function actionTerminateSessions()
494
    {
495
        $this->make(TerminateUserSessionsService::class, [Yii::$app->user->id])->run();
496
497
        return $this->redirect(['session-history']);
498
    }
499
500
    /**
501
     * @param $id
502
     * @throws ForbiddenHttpException
503
     * @throws NotFoundHttpException
504
     * @throws \Exception
505
     * @throws \Throwable
506
     * @throws \yii\db\StaleObjectException
507
     */
508
    protected function disconnectSocialNetwork($id)
509
    {
510
        /** @var SocialNetworkAccount $account */
511
        $account = $this->socialNetworkAccountQuery->whereId($id)->one();
512
513
        if ($account === null) {
514
            throw new NotFoundHttpException();
515
        }
516
        if ($account->user_id !== Yii::$app->user->id) {
517
            throw new ForbiddenHttpException();
518
        }
519
        $event = $this->make(SocialNetworkConnectEvent::class, [Yii::$app->user->identity, $account]);
520
521
        $this->trigger(SocialNetworkConnectEvent::EVENT_BEFORE_DISCONNECT, $event);
522
        $account->delete();
523
        $this->trigger(SocialNetworkConnectEvent::EVENT_AFTER_DISCONNECT, $event);
524
    }
525
}
526