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

SettingsController::actionPrivacy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
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
                            'session-history',
119
                            'terminate-sessions',
120
                        ],
121
                        'roles' => ['@'],
122
                    ],
123
                    [
124
                        'allow' => true,
125
                        'actions' => ['confirm'],
126
                        'roles' => ['?', '@'],
127
                    ],
128
                ],
129
            ],
130
        ];
131
    }
132
133
    /**
134
     * @throws \yii\base\InvalidConfigException
135
     * @return string|Response
136
     */
137 1
    public function actionProfile()
138
    {
139 1
        $profile = $this->profileQuery->whereUserId(Yii::$app->user->identity->getId())->one();
140
141 1
        if ($profile === null) {
142
            $profile = $this->make(Profile::class);
143
            $profile->link('user', Yii::$app->user->identity);
144
        }
145
146
        /** @var ProfileEvent $event */
147 1
        $event = $this->make(ProfileEvent::class, [$profile]);
148
149 1
        $this->make(AjaxRequestModelValidator::class, [$profile])->validate();
150
151 1
        if ($profile->load(Yii::$app->request->post())) {
152
            $this->trigger(UserEvent::EVENT_BEFORE_PROFILE_UPDATE, $event);
153
            if ($profile->save()) {
154
                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...
155
                $this->trigger(UserEvent::EVENT_AFTER_PROFILE_UPDATE, $event);
156
157
                return $this->refresh();
158
            }
159
        }
160
161 1
        return $this->render(
162 1
            'profile',
163
            [
164 1
                'model' => $profile,
165
            ]
166
        );
167
    }
168
169
    /**
170
     * @throws NotFoundHttpException
171
     * @return string
172
     */
173 2
    public function actionPrivacy()
174
    {
175 2
        if (!$this->module->enableGdprCompliance) {
176 1
            throw new NotFoundHttpException();
177
        }
178 1
        return $this->render('privacy', [
179 1
            'module' => $this->module
180
        ]);
181
    }
182
183
    /**
184
     * @throws NotFoundHttpException
185
     * @throws \Throwable
186
     * @throws \yii\base\Exception
187
     * @throws \yii\base\InvalidConfigException
188
     * @throws \yii\db\StaleObjectException
189
     * @throws ForbiddenHttpException
190
     * @return string|Response
191
     */
192 1
    public function actionGdprDelete()
193
    {
194 1
        if (!$this->module->enableGdprCompliance) {
195
            throw new NotFoundHttpException();
196
        }
197
        /** @var GdprDeleteForm $form */
198 1
        $form = $this->make(GdprDeleteForm::class);
199
200 1
        $user = $form->getUser();
201
        /* @var $event GdprEvent */
202 1
        $event = $this->make(GdprEvent::class, [$user]);
203
204 1
        if ($form->load(Yii::$app->request->post()) && $form->validate()) {
205 1
            $this->trigger(GdprEvent::EVENT_BEFORE_DELETE, $event);
206
207 1
            if ($event->isValid) {
208 1
                Yii::$app->user->logout();
209
                //Disconnect social networks
210 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...
211 1
                foreach ($networks as $network) {
212
                    $this->disconnectSocialNetwork($network->id);
213
                }
214
215
                /* @var $security SecurityHelper */
216 1
                $security = $this->make(SecurityHelper::class);
217 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...
218
219 1
                $user->updateAttributes([
220 1
                    'email' => $anonymReplacement . "@example.com",
221 1
                    'username' => $anonymReplacement,
222 1
                    'gdpr_deleted' => 1,
223 1
                    'blocked_at' => time(),
224 1
                    'auth_key' => $security->generateRandomString()
225
                ]);
226 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...
227 1
                    'public_email' => $anonymReplacement . "@example.com",
228 1
                    'name' => $anonymReplacement,
229 1
                    'gravatar_email' => $anonymReplacement . "@example.com",
230 1
                    'location' => $anonymReplacement,
231 1
                    'website' => $anonymReplacement . ".tld",
232 1
                    'bio' => Yii::t('usuario', 'Deleted by GDPR request')
233
                ]);
234
            }
235 1
            $this->trigger(GdprEvent::EVENT_AFTER_DELETE, $event);
236
237 1
            Yii::$app->session->setFlash('info', Yii::t('usuario', 'Your personal information has been removed'));
238
239 1
            return $this->goHome();
240
        }
241
242 1
        return $this->render('gdpr-delete', [
243 1
            'model' => $form,
244
        ]);
245
    }
246
247 1
    public function actionGdprConsent()
248
    {
249
        /** @var User $user */
250 1
        $user = Yii::$app->user->identity;
251 1
        if ($user->gdpr_consent) {
252
            return $this->redirect(['profile']);
253
        }
254 1
        $model = new DynamicModel(['gdpr_consent']);
255 1
        $model->addRule('gdpr_consent', 'boolean');
256 1
        $model->addRule('gdpr_consent', 'default', ['value' => 0, 'skipOnEmpty' => false]);
257 1
        $model->addRule('gdpr_consent', 'compare', [
258 1
            'compareValue' => true,
259 1
            'message' => Yii::t('usuario', 'Your consent is required to work with this site'),
260
            'when' => function () {
261 1
                return $this->module->enableGdprCompliance;
262 1
            },
263
        ]);
264 1
        if ($model->load(Yii::$app->request->post()) && $model->validate()) {
265 1
            $user->updateAttributes([
266 1
                'gdpr_consent' => 1,
267 1
                'gdpr_consent_date' => time(),
268
            ]);
269 1
            return $this->redirect(['profile']);
270
        }
271
272 1
        return $this->render('gdpr-consent', [
273 1
            'model' => $model,
274 1
            'gdpr_consent_hint' => $this->module->getConsentMessage(),
275
        ]);
276
    }
277
278
    /**
279
     * Exports the data from the current user in a mechanical readable format (csv). Properties exported can be defined
280
     * in the module configuration.
281
     * @throws NotFoundHttpException if gdpr compliance is not enabled
282
     * @throws \Exception
283
     * @throws \Throwable
284
     */
285
    public function actionExport()
286
    {
287
        if (!$this->module->enableGdprCompliance) {
288
            throw new NotFoundHttpException();
289
        }
290
        try {
291
            $properties = $this->module->gdprExportProperties;
292
            $user = Yii::$app->user->identity;
293
            $data = [$properties, []];
294
295
            $formatter = Yii::$app->formatter;
296
            // override the default html-specific format for nulls
297
            $formatter->nullDisplay = "";
298
299
            foreach ($properties as $property) {
300
                $data[1][] = $formatter->asText(ArrayHelper::getValue($user, $property));
301
            }
302
303
            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...
304
                $splitted = explode('.', $value);
305
                $value = array_pop($splitted);
306
            });
307
308
            Yii::$app->response->headers->removeAll();
309
            Yii::$app->response->headers->add('Content-type', 'text/csv');
310
            Yii::$app->response->headers->add('Content-Disposition', 'attachment;filename=gdpr-data.csv');
311
            Yii::$app->response->send();
312
            $f = fopen('php://output', 'w');
313
            foreach ($data as $line) {
314
                fputcsv($f, $line);
315
            }
316
        } catch (\Exception $e) {
317
            throw $e;
318
        } catch (\Throwable $e) {
319
            throw $e;
320
        }
321
    }
322
323 1
    public function actionAccount()
324
    {
325
        /** @var SettingsForm $form */
326 1
        $form = $this->make(SettingsForm::class);
327 1
        $event = $this->make(UserEvent::class, [$form->getUser()]);
328
329 1
        $this->make(AjaxRequestModelValidator::class, [$form])->validate();
330
331 1
        if ($form->load(Yii::$app->request->post())) {
332 1
            $this->trigger(UserEvent::EVENT_BEFORE_ACCOUNT_UPDATE, $event);
333
334 1
            if ($form->save()) {
335 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...
336 1
                    'success',
337 1
                    Yii::t('usuario', 'Your account details have been updated')
338
                );
339 1
                $this->trigger(UserEvent::EVENT_AFTER_ACCOUNT_UPDATE, $event);
340
341 1
                return $this->refresh();
342
            }
343
        }
344
345 1
        return $this->render(
346 1
            'account',
347
            [
348 1
                'model' => $form,
349
            ]
350
        );
351
    }
352
353
    public function actionConfirm($id, $code)
354
    {
355
        $user = $this->userQuery->whereId($id)->one();
356
357
        if ($user === null || MailChangeStrategyInterface::TYPE_INSECURE === $this->module->emailChangeStrategy) {
358
            throw new NotFoundHttpException();
359
        }
360
        $event = $this->make(UserEvent::class, [$user]);
361
362
        $this->trigger(UserEvent::EVENT_BEFORE_CONFIRMATION, $event);
363
        if ($this->make(EmailChangeService::class, [$code, $user])->run()) {
364
            $this->trigger(UserEvent::EVENT_AFTER_CONFIRMATION, $event);
365
        }
366
367
        return $this->redirect(['account']);
368
    }
369
370
    public function actionNetworks()
371
    {
372
        return $this->render(
373
            'networks',
374
            [
375
                'user' => Yii::$app->user->identity,
376
            ]
377
        );
378
    }
379
380
    public function actionDisconnect($id)
381
    {
382
        $this->disconnectSocialNetwork($id);
383
        return $this->redirect(['networks']);
384
    }
385
386
    public function actionDelete()
387
    {
388
        if (!$this->module->allowAccountDelete) {
389
            throw new NotFoundHttpException(Yii::t('usuario', 'Not found'));
390
        }
391
392
        /** @var User $user */
393
        $user = Yii::$app->user->identity;
394
        $event = $this->make(UserEvent::class, [$user]);
395
        Yii::$app->user->logout();
396
397
        $this->trigger(UserEvent::EVENT_BEFORE_DELETE, $event);
398
        $user->delete();
399
        $this->trigger(UserEvent::EVENT_AFTER_DELETE, $event);
400
401
        Yii::$app->session->setFlash('info', Yii::t('usuario', 'Your account has been completely deleted'));
402
403
        return $this->goHome();
404
    }
405
406
    public function actionTwoFactor($id)
407
    {
408
        /** @var User $user */
409
        $user = $this->userQuery->whereId($id)->one();
410
411
        if (null === $user) {
412
            throw new NotFoundHttpException();
413
        }
414
415
        $uri = $this->make(TwoFactorQrCodeUriGeneratorService::class, [$user])->run();
416
417
        return $this->renderAjax('two-factor', ['id' => $id, 'uri' => $uri]);
418
    }
419
420
    public function actionTwoFactorEnable($id)
421
    {
422
        Yii::$app->response->format = Response::FORMAT_JSON;
423
424
        /** @var User $user */
425
        $user = $this->userQuery->whereId($id)->one();
426
427
        if (null === $user) {
428
            return [
429
                'success' => false,
430
                'message' => Yii::t('usuario', 'User not found.')
431
            ];
432
        }
433
        $code = Yii::$app->request->get('code');
434
435
        $success = $this
436
            ->make(TwoFactorCodeValidator::class, [$user, $code, $this->module->twoFactorAuthenticationCycles])
437
            ->validate();
438
439
        $success = $success && $user->updateAttributes(['auth_tf_enabled' => '1']);
440
441
        return [
442
            'success' => $success,
443
            'message' => $success
444
                ? Yii::t('usuario', 'Two factor authentication successfully enabled.')
445
                : Yii::t('usuario', 'Verification failed. Please, enter new code.')
446
        ];
447
    }
448
449
    public function actionTwoFactorDisable($id)
450
    {
451
        /** @var User $user */
452
        $user = $this->userQuery->whereId($id)->one();
453
454
        if (null === $user) {
455
            throw new NotFoundHttpException();
456
        }
457
458
        if ($user->updateAttributes(['auth_tf_enabled' => '0'])) {
459
            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...
460
                ->getSession()
461
                ->setFlash('success', Yii::t('usuario', 'Two factor authentication has been disabled.'));
462
        } else {
463
            Yii::$app
464
                ->getSession()
465
                ->setFlash('danger', Yii::t('usuario', 'Unable to disable Two factor authentication.'));
466
        }
467
468
        $this->redirect(['account']);
469
    }
470
471
    /**
472
     * Display list session history.
473
     */
474
    public function actionSessionHistory()
475
    {
476
        $searchModel = new SessionHistorySearch([
477
            'user_id' => Yii::$app->user->id,
478
        ]);
479
        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
480
481
        return $this->render('session-history', [
482
            'searchModel' => $searchModel,
483
            'dataProvider' => $dataProvider,
484
        ]);
485
    }
486
487
    /**
488
     * Terminate all session user
489
     */
490
    public function actionTerminateSessions()
491
    {
492
        $this->make(TerminateUserSessionsService::class, [Yii::$app->user->id])->run();
493
494
        return $this->redirect(['session-history']);
495
    }
496
497
    /**
498
     * @param $id
499
     * @throws ForbiddenHttpException
500
     * @throws NotFoundHttpException
501
     * @throws \Exception
502
     * @throws \Throwable
503
     * @throws \yii\db\StaleObjectException
504
     */
505
    protected function disconnectSocialNetwork($id)
506
    {
507
        /** @var SocialNetworkAccount $account */
508
        $account = $this->socialNetworkAccountQuery->whereId($id)->one();
509
510
        if ($account === null) {
511
            throw new NotFoundHttpException();
512
        }
513
        if ($account->user_id !== Yii::$app->user->id) {
514
            throw new ForbiddenHttpException();
515
        }
516
        $event = $this->make(SocialNetworkConnectEvent::class, [Yii::$app->user->identity, $account]);
517
518
        $this->trigger(SocialNetworkConnectEvent::EVENT_BEFORE_DISCONNECT, $event);
519
        $account->delete();
520
        $this->trigger(SocialNetworkConnectEvent::EVENT_AFTER_DISCONNECT, $event);
521
    }
522
}
523