Completed
Branch feature/currentUserRefactoring (c13c1d)
by Schlaefer
09:08
created

UsersController::role()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 4
nop 1
dl 0
loc 28
rs 9.4555
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Saito - The Threaded Web Forum
7
 *
8
 * @copyright Copyright (c) the Saito Project Developers
9
 * @link https://github.com/Schlaefer/Saito
10
 * @license http://opensource.org/licenses/MIT
11
 */
12
13
namespace App\Controller;
14
15
use App\Form\BlockForm;
16
use App\Model\Entity\User;
17
use Cake\Core\Configure;
18
use Cake\Event\Event;
19
use Cake\Http\Exception\BadRequestException;
20
use Cake\Http\Exception\ForbiddenException;
21
use Cake\Http\Response;
22
use Cake\I18n\Time;
23
use Saito\App\Registry;
24
use Saito\Exception\Logger\ExceptionLogger;
25
use Saito\Exception\Logger\ForbiddenLogger;
26
use Saito\Exception\SaitoForbiddenException;
27
use Saito\User\Blocker\ManualBlocker;
28
use Saito\User\Permission\Identifier\Owner;
29
use Saito\User\Permission\Identifier\Role;
30
use Saito\User\Permission\Permissions;
31
use Siezi\SimpleCaptcha\Model\Validation\SimpleCaptchaValidator;
32
use Stopwatch\Lib\Stopwatch;
33
34
/**
35
 * User controller
36
 */
37
class UsersController extends AppController
38
{
39
    public $helpers = [
40
        'SpectrumColorpicker.SpectrumColorpicker',
41
        'Posting',
42
        'Siezi/SimpleCaptcha.SimpleCaptcha',
43
        'Text'
44
    ];
45
46
    /**
47
     * {@inheritDoc}
48
     */
49
    public function initialize()
50
    {
51
        parent::initialize();
52
        $this->loadComponent('Referer');
53
    }
54
55
    /**
56
     * Login user.
57
     *
58
     * @return void|Response
59
     */
60
    public function login()
61
    {
62
        $data = $this->request->getData();
63
        if (empty($data['username'])) {
64
            $logout = $this->_logoutAndComeHereAgain();
65
            if ($logout) {
66
                return $logout;
67
            }
68
69
            /// Show form to user.
70
            if ($this->getRequest()->getQuery('redirect', null)) {
71
                $this->Flash->set(
72
                    __('user.authe.required.exp'),
73
                    ['element' => 'warning', 'params' => ['title' => __('user.authe.required.t')]]
74
                );
75
            };
76
77
            return;
78
        }
79
80
        if ($this->AuthUser->login()) {
81
            // Redirect query-param in URL.
82
            $target = $this->getRequest()->getQuery('redirect');
83
            // Referer from Request
84
            $target = $target ?: $this->referer(null, true);
85
86
            if (!$target || $this->Referer->wasAction('login')) {
87
                $target = '/';
88
            }
89
90
            return $this->redirect($target);
91
        }
92
93
        /// error on login
94
        $username = $this->request->getData('username');
95
        /** @var User */
96
        $User = $this->Users->find()
97
            ->where(['username' => $username])
98
            ->first();
99
100
        $message = __('user.authe.e.generic');
101
102
        if (!empty($User)) {
103
            if (!$User->isActivated()) {
0 ignored issues
show
Bug introduced by
The method isActivated() does not exist on Cake\Datasource\EntityInterface. It seems like you code against a sub-type of Cake\Datasource\EntityInterface such as App\Model\Entity\User. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

103
            if (!$User->/** @scrutinizer ignore-call */ isActivated()) {
Loading history...
104
                $message = __('user.actv.ny');
105
            } elseif ($User->isLocked()) {
0 ignored issues
show
Bug introduced by
The method isLocked() does not exist on Cake\Datasource\EntityInterface. It seems like you code against a sub-type of Cake\Datasource\EntityInterface such as App\Model\Entity\User or App\Model\Entity\Entry. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
            } elseif ($User->/** @scrutinizer ignore-call */ isLocked()) {
Loading history...
106
                $ends = $this->Users->UserBlocks
107
                    ->getBlockEndsForUser($User->getId());
0 ignored issues
show
Bug introduced by
The method getId() does not exist on Cake\Datasource\EntityInterface. Did you maybe mean get()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

107
                    ->getBlockEndsForUser($User->/** @scrutinizer ignore-call */ getId());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
108
                if ($ends) {
109
                    $time = new Time($ends);
110
                    $data = [
111
                        'name' => $username,
112
                        'end' => $time->timeAgoInWords(['accuracy' => 'hour'])
113
                    ];
114
                    $message = __('user.block.pubExpEnds', $data);
115
                } else {
116
                    $message = __('user.block.pubExp', $username);
117
                }
118
            }
119
        }
120
121
        // don't autofill password
122
        $this->setRequest($this->getRequest()->withData('password', ''));
123
124
        $Logger = new ForbiddenLogger;
125
        $Logger->write(
126
            "Unsuccessful login for user: $username",
127
            ['msgs' => [$message]]
128
        );
129
130
        $this->Flash->set($message, [
131
            'element' => 'error', 'params' => ['title' => __('user.authe.e.t')]
132
        ]);
133
    }
134
135
    /**
136
     * Logout user.
137
     *
138
     * @return void|Response
139
     */
140
    public function logout()
141
    {
142
        $request = $this->getRequest();
143
        $cookies = $request->getCookieCollection();
144
        foreach ($cookies as $cookie) {
145
            $cookie = $cookie->withPath($request->getAttribute('webroot'));
146
            $this->setResponse($this->getResponse()->withExpiredCookie($cookie));
147
        }
148
149
        $this->AuthUser->logout();
150
        $this->redirect('/');
151
    }
152
153
    /**
154
     * Register new user.
155
     *
156
     * @return void|Response
157
     */
158
    public function register()
159
    {
160
        $this->set('status', 'view');
161
162
        $this->AuthUser->logout();
163
164
        $tosRequired = Configure::read('Saito.Settings.tos_enabled');
165
        $this->set(compact('tosRequired'));
166
167
        $user = $this->Users->newEntity();
168
        $this->set('user', $user);
169
170
        if (!$this->request->is('post')) {
171
            $logout = $this->_logoutAndComeHereAgain();
172
            if ($logout) {
173
                return $logout;
174
            }
175
176
            return;
177
        }
178
179
        $data = $this->request->getData();
180
181
        if (!$tosRequired) {
182
            $data['tos_confirm'] = true;
183
        }
184
        $tosConfirmed = $data['tos_confirm'];
185
        if (!$tosConfirmed) {
186
            return;
187
        }
188
189
        $validator = new SimpleCaptchaValidator();
190
        $errors = $validator->errors($this->request->getData());
0 ignored issues
show
Bug introduced by
It seems like $this->request->getData() can also be of type null; however, parameter $data of Cake\Validation\Validator::errors() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

190
        $errors = $validator->errors(/** @scrutinizer ignore-type */ $this->request->getData());
Loading history...
191
192
        $user = $this->Users->register($data);
193
        $user->setErrors($errors);
194
195
        $errors = $user->getErrors();
196
        if (!empty($errors)) {
197
            // registering failed, show form again
198
            if (isset($errors['password'])) {
199
                $user->setErrors($errors);
200
            }
201
            $user->set('tos_confirm', false);
202
            $this->set('user', $user);
203
204
            return;
205
        }
206
207
        // registered successfully
208
        try {
209
            $forumName = Configure::read('Saito.Settings.forum_name');
210
            $subject = __('register_email_subject', $forumName);
211
            $this->SaitoEmail->email(
212
                [
213
                    'recipient' => $user,
214
                    'subject' => $subject,
215
                    'sender' => 'register',
216
                    'template' => 'user_register',
217
                    'viewVars' => ['user' => $user]
218
                ]
219
            );
220
        } catch (\Exception $e) {
221
            $Logger = new ExceptionLogger();
222
            $Logger->write(
223
                'Registering email confirmation failed',
224
                ['e' => $e]
225
            );
226
            $this->set('status', 'fail: email');
227
228
            return;
229
        }
230
231
        $this->set('status', 'success');
232
    }
233
234
    /**
235
     * register success (user clicked link in confirm mail)
236
     *
237
     * @param string $id user-ID
238
     * @return void
239
     * @throws BadRequestException
240
     */
241
    public function rs($id = null)
242
    {
243
        if (!$id) {
244
            throw new BadRequestException();
245
        }
246
        $code = $this->request->getQuery('c');
247
        try {
248
            $activated = $this->Users->activate((int)$id, $code);
0 ignored issues
show
Bug introduced by
It seems like $code can also be of type array and null; however, parameter $code of App\Model\Table\UsersTable::activate() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

248
            $activated = $this->Users->activate((int)$id, /** @scrutinizer ignore-type */ $code);
Loading history...
249
        } catch (\Exception $e) {
250
            $activated = false;
251
        }
252
        if (!$activated) {
253
            $activated = ['status' => 'fail'];
254
        }
255
        $this->set('status', $activated['status']);
256
    }
257
258
    /**
259
     * Show list of all users.
260
     *
261
     * @return void
262
     */
263
    public function index()
264
    {
265
        $menuItems = [
266
            'username' => [__('username_marking'), []],
267
            'user_type' => [__('user_type'), []],
268
            'UserOnline.logged_in' => [
269
                __('userlist_online'),
270
                ['direction' => 'desc']
271
            ],
272
            'registered' => [__('registered'), ['direction' => 'desc']]
273
        ];
274
        $showBlocked = $this->CurrentUser->permission('saito.core.user.lock.view');
275
        if ($showBlocked) {
276
            $menuItems['user_lock'] = [
277
                __('user.set.lock.t'),
278
                ['direction' => 'desc']
279
            ];
280
        }
281
282
        $this->paginate = $options = [
0 ignored issues
show
Unused Code introduced by
The assignment to $options is dead and can be removed.
Loading history...
283
            'contain' => ['UserOnline'],
284
            'sortWhitelist' => array_keys($menuItems),
285
            'finder' => 'paginated',
286
            'limit' => 400,
287
            'order' => [
288
                'UserOnline.logged_in' => 'desc',
289
            ]
290
        ];
291
        $users = $this->paginate($this->Users);
292
293
        $showBottomNavigation = true;
294
295
        $this->set(compact('menuItems', 'showBottomNavigation', 'users'));
296
    }
297
298
    /**
299
     * Ignore user.
300
     *
301
     * @return void
302
     */
303
    public function ignore()
304
    {
305
        $this->request->allowMethod('POST');
306
        $blockedId = (int)$this->request->getData('id');
307
        $this->_ignore($blockedId, true);
308
    }
309
310
    /**
311
     * Unignore user.
312
     *
313
     * @return void
314
     */
315
    public function unignore()
316
    {
317
        $this->request->allowMethod('POST');
318
        $blockedId = (int)$this->request->getData('id');
319
        $this->_ignore($blockedId, false);
320
    }
321
322
    /**
323
     * Mark user as un-/ignored
324
     *
325
     * @param int $blockedId user to ignore
326
     * @param bool $set block or unblock
327
     * @return \Cake\Network\Response
328
     */
329
    protected function _ignore($blockedId, $set)
330
    {
331
        $userId = $this->CurrentUser->getId();
332
        if ((int)$userId === (int)$blockedId) {
333
            throw new BadRequestException();
334
        }
335
        if ($set) {
336
            $this->Users->UserIgnores->ignore($userId, $blockedId);
337
        } else {
338
            $this->Users->UserIgnores->unignore($userId, $blockedId);
339
        }
340
341
        return $this->redirect($this->referer());
342
    }
343
344
    /**
345
     * Show user with profile $name
346
     *
347
     * @param string $name username
348
     * @return void
349
     */
350
    public function name($name = null)
351
    {
352
        if (!empty($name)) {
353
            $viewedUser = $this->Users->find()
354
                ->select(['id'])
355
                ->where(['username' => $name])
356
                ->first();
357
            if (!empty($viewedUser)) {
358
                $this->redirect(
359
                    [
360
                        'controller' => 'users',
361
                        'action' => 'view',
362
                        $viewedUser->get('id')
363
                    ]
364
                );
365
366
                return;
367
            }
368
        }
369
        $this->Flash->set(__('Invalid user'), ['element' => 'error']);
370
        $this->redirect('/');
371
    }
372
373
    /**
374
     * View user profile.
375
     *
376
     * @param null $id user-ID
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $id is correct as it would always require null to be passed?
Loading history...
377
     * @return \Cake\Network\Response|void
378
     */
379
    public function view($id = null)
380
    {
381
        // redirect view/<username> to name/<username>
382
        if (!empty($id) && !is_numeric($id)) {
383
            $this->redirect(
384
                ['controller' => 'users', 'action' => 'name', $id]
385
            );
386
387
            return;
388
        }
389
390
        $id = (int)$id;
391
392
        /** @var User */
393
        $user = $this->Users->find()
394
            ->contain(
395
                [
396
                    'UserBlocks' => function ($q) {
397
                        return $q->find('assocUsers');
398
                    },
399
                    'UserOnline'
400
                ]
401
            )
402
            ->where(['Users.id' => (int)$id])
403
            ->first();
404
405
        if (empty($user)) {
406
            $this->Flash->set(__('Invalid user'), ['element' => 'error']);
407
408
            return $this->redirect('/');
409
        }
410
411
        $entriesShownOnPage = 20;
412
        $this->set(
413
            'lastEntries',
414
            $this->Users->Entries->getRecentPostings(
415
                $this->CurrentUser,
416
                ['user_id' => $id, 'limit' => $entriesShownOnPage]
417
            )
418
        );
419
420
        $this->set(
421
            'hasMoreEntriesThanShownOnPage',
422
            ($user->numberOfPostings() - $entriesShownOnPage) > 0
0 ignored issues
show
Bug introduced by
The method numberOfPostings() does not exist on Cake\Datasource\EntityInterface. It seems like you code against a sub-type of Cake\Datasource\EntityInterface such as App\Model\Entity\User. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

422
            ($user->/** @scrutinizer ignore-call */ numberOfPostings() - $entriesShownOnPage) > 0
Loading history...
423
        );
424
425
        if ($this->CurrentUser->getId() === $id) {
426
            $ignores = $this->Users->UserIgnores->getAllIgnoredBy($id);
427
            $user->set('ignores', $ignores);
428
        }
429
430
        $blockForm = new BlockForm();
431
        $solved = $this->Users->countSolved($id);
432
        $this->set(compact('blockForm', 'isEditingAllowed', 'solved', 'user'));
433
        $this->set('titleForLayout', $user->get('username'));
434
    }
435
436
    /**
437
     * Set user avatar.
438
     *
439
     * @param string $userId user-ID
440
     * @return void|\Cake\Network\Response
441
     */
442
    public function avatar($userId)
443
    {
444
        if (!$this->Users->exists($userId)) {
0 ignored issues
show
Bug introduced by
$userId of type string is incompatible with the type ArrayAccess|array|integer expected by parameter $conditions of App\Lib\Model\Table\AppTable::exists(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

444
        if (!$this->Users->exists(/** @scrutinizer ignore-type */ $userId)) {
Loading history...
445
            throw new BadRequestException;
446
        }
447
448
        /** @var User */
449
        $user = $this->Users->get($userId);
450
451
        $permissionEditing = $this->CurrentUser->permission(
452
            'saito.core.user.edit',
453
            new Role($user->getRole()),
0 ignored issues
show
Bug introduced by
The method getRole() does not exist on Cake\Datasource\EntityInterface. It seems like you code against a sub-type of Cake\Datasource\EntityInterface such as App\Model\Entity\User. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

453
            new Role($user->/** @scrutinizer ignore-call */ getRole()),
Loading history...
454
            new Owner($user)
0 ignored issues
show
Bug introduced by
$user of type Cake\Datasource\EntityInterface is incompatible with the type Saito\User\ForumsUserInterface|integer expected by parameter $token of Saito\User\Permission\Id...er\Owner::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

454
            new Owner(/** @scrutinizer ignore-type */ $user)
Loading history...
455
        );
456
        if (!$permissionEditing) {
457
            throw new \Saito\Exception\SaitoForbiddenException(
458
                "Attempt to edit user $userId.",
459
                ['CurrentUser' => $this->CurrentUser]
460
            );
461
        }
462
463
        if ($this->request->is('post') || $this->request->is('put')) {
464
            $data = [
465
                'avatar' => $this->request->getData('avatar'),
466
                'avatarDelete' => $this->request->getData('avatarDelete')
467
            ];
468
            if (!empty($data['avatarDelete'])) {
469
                $data = [
470
                    'avatar' => null,
471
                    'avatar_dir' => null
472
                ];
473
            }
474
            $patched = $this->Users->patchEntity($user, $data);
475
            $errors = $patched->getErrors();
476
            if (empty($errors) && $this->Users->save($patched)) {
477
                return $this->redirect(['action' => 'edit', $userId]);
478
            } else {
479
                $this->Flash->set(
480
                    __('The user could not be saved. Please, try again.'),
481
                    ['element' => 'error']
482
                );
483
            }
484
        }
485
486
        $this->set('user', $user);
487
488
        $this->set(
489
            'titleForPage',
490
            __('user.avatar.edit.t', [$user->get('username')])
491
        );
492
    }
493
494
    /**
495
     * Edit user.
496
     *
497
     * @param null $id user-ID
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $id is correct as it would always require null to be passed?
Loading history...
498
     *
499
     * @return \Cake\Network\Response|void
500
     */
501
    public function edit($id = null)
502
    {
503
        /** @var User */
504
        $user = $this->Users->get($id);
505
506
        $permissionEditing = $this->CurrentUser->permission(
507
            'saito.core.user.edit',
508
            new Role($user->getRole()),
509
            new Owner($user)
0 ignored issues
show
Bug introduced by
$user of type Cake\Datasource\EntityInterface is incompatible with the type Saito\User\ForumsUserInterface|integer expected by parameter $token of Saito\User\Permission\Id...er\Owner::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

509
            new Owner(/** @scrutinizer ignore-type */ $user)
Loading history...
510
        );
511
        if (!$permissionEditing) {
512
            throw new \Saito\Exception\SaitoForbiddenException(
513
                sprintf('Attempt to edit user "%s".', $user->get('id')),
514
                ['CurrentUser' => $this->CurrentUser]
515
            );
516
        }
517
518
        if ($this->request->is('post') || $this->request->is('put')) {
519
            $data = $this->request->getData();
520
            $patched = $this->Users->patchEntity($user, $data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type null; however, parameter $data of Cake\ORM\Table::patchEntity() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

520
            $patched = $this->Users->patchEntity($user, /** @scrutinizer ignore-type */ $data);
Loading history...
521
            $errors = $patched->getErrors();
522
            if (empty($errors) && $this->Users->save($patched)) {
523
                return $this->redirect(['action' => 'view', $id]);
524
            }
525
526
            $this->Flash->set(
527
                __('The user could not be saved. Please, try again.'),
528
                ['element' => 'error']
529
            );
530
        }
531
        $this->set('user', $user);
532
533
        $this->set(
534
            'titleForPage',
535
            __('user.edit.t', [$user->get('username')])
536
        );
537
538
        $availableThemes = $this->Themes->getAvailable($this->CurrentUser);
539
        $availableThemes = array_combine($availableThemes, $availableThemes);
540
        $currentTheme = $this->Themes->getThemeForUser($this->CurrentUser);
541
        $this->set(compact('availableThemes', 'currentTheme'));
542
    }
543
544
    /**
545
     * delete user
546
     *
547
     * @param string $id user-ID
548
     * @return \Cake\Network\Response|void
549
     */
550
    public function delete($id)
551
    {
552
        $id = (int)$id;
553
        /** @var User */
554
        $readUser = $this->Users->get($id);
555
556
        /// Check permission
557
        $permission = $this->CurrentUser->permission(
558
            'saito.core.user.delete',
559
            new Role($readUser->getRole())
560
        );
561
        if (!$permission) {
562
            throw new ForbiddenException(
563
                sprintf(
564
                    'User "%s" is not allowed to delete user "%s".',
565
                    $this->CurrentUser->get('username'),
566
                    $readUser->get('username')
567
                ),
568
                1571811593
569
            );
570
        }
571
572
        $this->set('user', $readUser);
573
574
        $failure = false;
575
        if (!$this->request->getData('userdeleteconfirm')) {
576
            $failure = true;
577
            $this->Flash->set(__('user.del.fail.3'), ['element' => 'error']);
578
        } elseif ($this->CurrentUser->isUser($readUser)) {
0 ignored issues
show
Bug introduced by
$readUser of type Cake\Datasource\EntityInterface is incompatible with the type Saito\User\ForumsUserInterface expected by parameter $user of Saito\User\ForumsUserInterface::isUser(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

578
        } elseif ($this->CurrentUser->isUser(/** @scrutinizer ignore-type */ $readUser)) {
Loading history...
579
            $failure = true;
580
            $this->Flash->set(__('user.del.fail.1'), ['element' => 'error']);
581
        }
582
583
        if (!$failure) {
584
            $result = $this->Users->deleteAllExceptEntries($id);
585
            if (empty($result)) {
586
                $failure = true;
587
                $this->Flash->set(__('user.del.fail.2'), ['element' => 'error']);
588
            }
589
        }
590
591
        if ($failure) {
592
            return $this->redirect(
593
                [
594
                    'prefix' => false,
595
                    'controller' => 'users',
596
                    'action' => 'view',
597
                    $id
598
                ]
599
            );
600
        }
601
602
        $this->Flash->set(
603
            __('user.del.ok.m', $readUser->get('username')),
604
            ['element' => 'success']
605
        );
606
607
        return $this->redirect('/');
608
    }
609
610
    /**
611
     * Lock user.
612
     *
613
     * @return \Cake\Network\Response|void
614
     * @throws BadRequestException
615
     */
616
    public function lock()
617
    {
618
        $form = new BlockForm();
619
        if (!$form->validate($this->request->getData())) {
0 ignored issues
show
Bug introduced by
It seems like $this->request->getData() can also be of type null; however, parameter $data of Cake\Form\Form::validate() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

619
        if (!$form->validate(/** @scrutinizer ignore-type */ $this->request->getData())) {
Loading history...
620
            throw new BadRequestException;
621
        }
622
623
        $id = (int)$this->request->getData('lockUserId');
624
625
        /** @var User */
626
        $readUser = $this->Users->get($id);
627
628
        $permission = $this->CurrentUser->permission(
629
            'saito.core.user.lock.set',
630
            new Role($readUser->getRole())
631
        );
632
        if (!$permission) {
633
            throw new ForbiddenException(null, 1571316877);
634
        }
635
636
        if ($this->CurrentUser->isUser($readUser)) {
0 ignored issues
show
Bug introduced by
$readUser of type Cake\Datasource\EntityInterface is incompatible with the type Saito\User\ForumsUserInterface expected by parameter $user of Saito\User\ForumsUserInterface::isUser(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

636
        if ($this->CurrentUser->isUser(/** @scrutinizer ignore-type */ $readUser)) {
Loading history...
637
            $message = __('You can\'t lock yourself.');
638
            $this->Flash->set($message, ['element' => 'error']);
639
        } else {
640
            try {
641
                $duration = (int)$this->request->getData('lockPeriod');
642
                $blocker = new ManualBlocker($this->CurrentUser->getId(), $duration);
643
                $status = $this->Users->UserBlocks->block($blocker, $id);
644
                if (!$status) {
645
                    throw new \Exception();
646
                }
647
                $message = __('User {0} is locked.', $readUser->get('username'));
648
                $this->Flash->set($message, ['element' => 'success']);
649
            } catch (\Exception $e) {
650
                $message = __('Error while locking.');
651
                $this->Flash->set($message, ['element' => 'error']);
652
            }
653
        }
654
655
        return $this->redirect($this->referer());
656
    }
657
658
    /**
659
     * Unblock user.
660
     *
661
     * @param string $id user-ID
662
     * @return void
663
     */
664
    public function unlock(string $id)
665
    {
666
        $id = (int)$id;
667
668
        /** @var User */
669
        $user = $this->Users
670
            ->find()
671
            ->matching('UserBlocks', function ($q) use ($id) {
672
                return $q->where(['UserBlocks.id' => $id]);
673
            })
674
            ->first();
675
676
        $permission = $this->CurrentUser->permission(
677
            'saito.core.user.lock.set',
678
            new Role($user->getRole())
679
        );
680
        if (!$permission) {
681
            throw new ForbiddenException(null, 1571316877);
682
        }
683
684
        if (!$this->Users->UserBlocks->unblock($id)) {
685
            $this->Flash->set(
686
                __('Error while unlocking.'),
687
                ['element' => 'error']
688
            );
689
        }
690
691
        $message = __('User {0} is unlocked.', $user->get('username'));
692
        $this->Flash->set($message, ['element' => 'success']);
693
        $this->redirect($this->referer());
694
    }
695
696
    /**
697
     * changes user password
698
     *
699
     * @param null $id user-ID
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $id is correct as it would always require null to be passed?
Loading history...
700
     * @return void
701
     * @throws \Saito\Exception\SaitoForbiddenException
702
     * @throws BadRequestException
703
     */
704
    public function changepassword($id = null)
705
    {
706
        if (empty($id)) {
707
            throw new BadRequestException();
708
        }
709
710
        /** @var User */
711
        $user = $this->Users->get($id);
712
        $allowed = $this->CurrentUser->isUser($user);
0 ignored issues
show
Bug introduced by
$user of type Cake\Datasource\EntityInterface is incompatible with the type Saito\User\ForumsUserInterface expected by parameter $user of Saito\User\ForumsUserInterface::isUser(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

712
        $allowed = $this->CurrentUser->isUser(/** @scrutinizer ignore-type */ $user);
Loading history...
713
        if (empty($user) || !$allowed) {
714
            throw new SaitoForbiddenException(
715
                "Attempt to change password for user $id.",
716
                ['CurrentUser' => $this->CurrentUser]
717
            );
718
        }
719
        $this->set('userId', $id);
720
        $this->set('username', $user->get('username'));
721
722
        //= just show empty form
723
        if (empty($this->request->getData())) {
724
            return;
725
        }
726
727
        $formFields = ['password', 'password_old', 'password_confirm'];
728
729
        //= process submitted form
730
        $data = [];
731
        foreach ($formFields as $field) {
732
            $data[$field] = $this->request->getData($field);
733
        }
734
        $this->Users->patchEntity($user, $data);
735
        $success = $this->Users->save($user);
736
737
        if ($success) {
738
            $this->Flash->set(
739
                __('change_password_success'),
740
                ['element' => 'success']
741
            );
742
            $this->redirect(['controller' => 'users', 'action' => 'edit', $id]);
743
744
            return;
745
        }
746
747
        $errors = $user->getErrors();
748
        if (!empty($errors)) {
749
            $this->Flash->set(
750
                __d('nondynamic', current(array_pop($errors))),
751
                ['element' => 'error']
752
            );
753
        }
754
755
        //= unset all autofill form data
756
        foreach ($formFields as $field) {
757
            $this->request = $this->request->withoutData($field);
758
        }
759
    }
760
761
    /**
762
     * Directly set password for user
763
     *
764
     * @param string $id user-ID
765
     * @return Response|null
766
     */
767
    public function setpassword($id)
768
    {
769
        /** @var User */
770
        $user = $this->Users->get($id);
771
772
        if (!$this->CurrentUser->permission('saito.core.user.password.set', new Role($user->getRole()))) {
773
            throw new SaitoForbiddenException(
774
                "Attempt to set password for user $id.",
775
                ['CurrentUser' => $this->CurrentUser]
776
            );
777
        }
778
779
        if ($this->getRequest()->is('post')) {
780
            $this->Users->patchEntity($user, $this->getRequest()->getData(), ['fields' => 'password']);
0 ignored issues
show
Bug introduced by
It seems like $this->getRequest()->getData() can also be of type null; however, parameter $data of Cake\ORM\Table::patchEntity() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

780
            $this->Users->patchEntity($user, /** @scrutinizer ignore-type */ $this->getRequest()->getData(), ['fields' => 'password']);
Loading history...
781
782
            if ($this->Users->save($user)) {
783
                $this->Flash->set(
784
                    __('user.pw.set.s'),
785
                    ['element' => 'success']
786
                );
787
788
                return $this->redirect(['controller' => 'users', 'action' => 'edit', $id]);
789
            }
790
            $errors = $user->getErrors();
791
            if (!empty($errors)) {
792
                $this->Flash->set(
793
                    __d('nondynamic', current(array_pop($errors))),
794
                    ['element' => 'error']
795
                );
796
            }
797
        }
798
799
        $this->set(compact('user'));
800
    }
801
802
    /**
803
     * View and set user role
804
     *
805
     * @param string $id User-ID
806
     * @return void|Response
807
     */
808
    public function role($id)
809
    {
810
        /** @var User */
811
        $user = $this->Users->get($id);
812
        if (!$this->CurrentUser->permission('saito.core.user.role.set', new Role($user->getRole()))) {
813
            throw new ForbiddenException();
814
        }
815
816
        /** @var Permissions */
817
        $Permissions = Registry::get('Permissions');
818
        $roles = $Permissions->getRoles()->get($this->CurrentUser->getRole(), false);
819
820
        if ($this->getRequest()->is('post') || $this->getRequest()->is('put')) {
821
            $type = $this->getRequest()->getData('user_type');
822
            $patched = $this->Users->patchEntity($user, ['user_type' => $type]);
823
824
            $errors = $patched->getErrors();
825
            if (empty($errors)) {
826
                $this->Users->save($patched);
827
828
                return $this->redirect(['action' => 'edit', $user->get('id')]);
829
            }
830
831
            $msg = current(current($errors));
832
            $this->Flash->set($msg, ['element' => 'error']);
833
        }
834
835
        $this->set(compact('roles', 'user'));
836
    }
837
838
    /**
839
     * Set slidetab-order.
840
     *
841
     * @return \Cake\Network\Response
842
     * @throws BadRequestException
843
     */
844
    public function slidetabOrder()
845
    {
846
        if (!$this->request->is('ajax')) {
847
            throw new BadRequestException;
848
        }
849
850
        $order = $this->request->getData('slidetabOrder');
851
        if (!$order) {
852
            throw new BadRequestException;
853
        }
854
855
        $allowed = $this->Slidetabs->getAvailable();
856
        $order = array_filter(
857
            $order,
0 ignored issues
show
Bug introduced by
It seems like $order can also be of type string; however, parameter $input of array_filter() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

857
            /** @scrutinizer ignore-type */ $order,
Loading history...
858
            function ($item) use ($allowed) {
859
                return in_array($item, $allowed);
860
            }
861
        );
862
        $order = serialize($order);
863
864
        $userId = $this->CurrentUser->getId();
865
        $user = $this->Users->get($userId);
866
        $this->Users->patchEntity($user, ['slidetab_order' => $order]);
867
        $this->Users->save($user);
868
869
        $this->CurrentUser->set('slidetab_order', $order);
870
871
        $this->response = $this->response->withStringBody(true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $string of Cake\Http\Response::withStringBody(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

871
        $this->response = $this->response->withStringBody(/** @scrutinizer ignore-type */ true);
Loading history...
872
873
        return $this->response;
874
    }
875
876
    /**
877
     * Shows user's uploads
878
     *
879
     * @return void
880
     */
881
    public function uploads()
882
    {
883
    }
884
885
    /**
886
     * Set category for user.
887
     *
888
     * @param string|null $id category-ID
889
     * @return \Cake\Network\Response
890
     */
891
    public function setcategory(?string $id = null)
892
    {
893
        $userId = $this->CurrentUser->getId();
894
        if ($id === 'all') {
895
            $this->Users->setCategory($userId, 'all');
896
        } elseif (!$id && $this->request->getData()) {
897
            $data = $this->request->getData('CatChooser');
898
            $this->Users->setCategory($userId, $data);
899
        } else {
900
            $this->Users->setCategory($userId, $id);
901
        }
902
903
        return $this->redirect($this->referer());
904
    }
905
906
    /**
907
     * {@inheritdoc}
908
     */
909
    public function beforeFilter(Event $event)
910
    {
911
        parent::beforeFilter($event);
912
        Stopwatch::start('Users->beforeFilter()');
913
914
        $unlocked = ['slidetabToggle', 'slidetabOrder'];
915
        $this->Security->setConfig('unlockedActions', $unlocked);
916
917
        $this->Authentication->allowUnauthenticated(['login', 'logout', 'register', 'rs']);
918
        $this->AuthUser->authorizeAction('register', 'saito.core.user.register');
919
        $this->AuthUser->authorizeAction('rs', 'saito.core.user.register');
920
921
        // Login form times-out and degrades user experience.
922
        // See https://github.com/Schlaefer/Saito/issues/339
923
        if (($this->getRequest()->getParam('action') === 'login')
924
            && $this->components()->has('Security')) {
925
            $this->components()->unload('Security');
926
        }
927
928
        Stopwatch::stop('Users->beforeFilter()');
929
    }
930
931
    /**
932
     * Logout user if logged in and create response to revisit logged out
933
     *
934
     * @return Response|null
935
     */
936
    protected function _logoutAndComeHereAgain(): ?Response
937
    {
938
        if (!$this->CurrentUser->isLoggedIn()) {
939
            return null;
940
        }
941
        $this->AuthUser->logout();
942
943
        return $this->redirect($this->getRequest()->getRequestTarget());
944
    }
945
}
946