Completed
Push — master ( 7d085e...e849bf )
by Schlaefer
15:17 queued 07:36
created

UsersController::role()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 38
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 22
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 38
rs 8.9457
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 Cake\Routing\Router;
24
use Saito\App\Registry;
25
use Saito\Exception\Logger\ExceptionLogger;
26
use Saito\Exception\Logger\ForbiddenLogger;
27
use Saito\Exception\SaitoForbiddenException;
28
use Saito\User\Blocker\ManualBlocker;
29
use Saito\User\Permission\Permissions;
30
use Saito\User\Permission\ResourceAI;
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
            // AuthenticationService puts the full local path into the redirect
84
            // parameter, so we have to strip the base-path off again.
85
            $target = Router::normalize($target);
86
            // Referer from Request
87
            $target = $target ?: $this->referer(null, true);
88
89
            if (empty($target)) {
90
                $target = '/';
91
            }
92
93
            return $this->redirect($target);
94
        }
95
96
        /// error on login
97
        $username = $this->request->getData('username');
98
        /** @var User */
99
        $User = $this->Users->find()
100
            ->where(['username' => $username])
101
            ->first();
102
103
        $message = __('user.authe.e.generic');
104
105
        if (!empty($User)) {
106
            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

106
            if (!$User->/** @scrutinizer ignore-call */ isActivated()) {
Loading history...
107
                $message = __('user.actv.ny');
108
            } 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

108
            } elseif ($User->/** @scrutinizer ignore-call */ isLocked()) {
Loading history...
109
                $ends = $this->Users->UserBlocks
110
                    ->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

110
                    ->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...
111
                if ($ends) {
112
                    $time = new Time($ends);
113
                    $data = [
114
                        'name' => $username,
115
                        'end' => $time->timeAgoInWords(['accuracy' => 'hour'])
116
                    ];
117
                    $message = __('user.block.pubExpEnds', $data);
118
                } else {
119
                    $message = __('user.block.pubExp', $username);
120
                }
121
            }
122
        }
123
124
        // don't autofill password
125
        $this->setRequest($this->getRequest()->withData('password', ''));
126
127
        $Logger = new ForbiddenLogger;
128
        $Logger->write(
129
            "Unsuccessful login for user: $username",
130
            ['msgs' => [$message]]
131
        );
132
133
        $this->Flash->set($message, [
134
            'element' => 'error', 'params' => ['title' => __('user.authe.e.t')]
135
        ]);
136
    }
137
138
    /**
139
     * Logout user.
140
     *
141
     * @return void|Response
142
     */
143
    public function logout()
144
    {
145
        $request = $this->getRequest();
146
        $cookies = $request->getCookieCollection();
147
        foreach ($cookies as $cookie) {
148
            $cookie = $cookie->withPath($request->getAttribute('webroot'));
149
            $this->setResponse($this->getResponse()->withExpiredCookie($cookie));
150
        }
151
152
        $this->AuthUser->logout();
153
        $this->redirect('/');
154
    }
155
156
    /**
157
     * Register new user.
158
     *
159
     * @return void|Response
160
     */
161
    public function register()
162
    {
163
        $this->set('status', 'view');
164
165
        $this->AuthUser->logout();
166
167
        $tosRequired = Configure::read('Saito.Settings.tos_enabled');
168
        $this->set(compact('tosRequired'));
169
170
        $user = $this->Users->newEntity();
171
        $this->set('user', $user);
172
173
        if (!$this->request->is('post')) {
174
            $logout = $this->_logoutAndComeHereAgain();
175
            if ($logout) {
176
                return $logout;
177
            }
178
179
            return;
180
        }
181
182
        $data = $this->request->getData();
183
184
        if (!$tosRequired) {
185
            $data['tos_confirm'] = true;
186
        }
187
        $tosConfirmed = $data['tos_confirm'];
188
        if (!$tosConfirmed) {
189
            return;
190
        }
191
192
        $validator = new SimpleCaptchaValidator();
193
        $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

193
        $errors = $validator->errors(/** @scrutinizer ignore-type */ $this->request->getData());
Loading history...
194
195
        $user = $this->Users->register($data);
196
        $user->setErrors($errors);
197
198
        $errors = $user->getErrors();
199
        if (!empty($errors)) {
200
            // registering failed, show form again
201
            if (isset($errors['password'])) {
202
                $user->setErrors($errors);
203
            }
204
            $user->set('tos_confirm', false);
205
            $this->set('user', $user);
206
207
            return;
208
        }
209
210
        // registered successfully
211
        try {
212
            $forumName = Configure::read('Saito.Settings.forum_name');
213
            $subject = __('register_email_subject', $forumName);
214
            $this->SaitoEmail->email(
215
                [
216
                    'recipient' => $user,
217
                    'subject' => $subject,
218
                    'sender' => 'register',
219
                    'template' => 'user_register',
220
                    'viewVars' => ['user' => $user]
221
                ]
222
            );
223
        } catch (\Exception $e) {
224
            $Logger = new ExceptionLogger();
225
            $Logger->write(
226
                'Registering email confirmation failed',
227
                ['e' => $e]
228
            );
229
            $this->set('status', 'fail: email');
230
231
            return;
232
        }
233
234
        $this->set('status', 'success');
235
    }
236
237
    /**
238
     * register success (user clicked link in confirm mail)
239
     *
240
     * @param string $id user-ID
241
     * @return void
242
     * @throws BadRequestException
243
     */
244
    public function rs($id = null)
245
    {
246
        if (!$id) {
247
            throw new BadRequestException();
248
        }
249
        $code = $this->request->getQuery('c');
250
        try {
251
            $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

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

425
            ($user->/** @scrutinizer ignore-call */ numberOfPostings() - $entriesShownOnPage) > 0
Loading history...
426
        );
427
428
        if ($this->CurrentUser->getId() === $id) {
429
            $ignores = $this->Users->UserIgnores->getAllIgnoredBy($id);
430
            $user->set('ignores', $ignores);
431
        }
432
433
        $blockForm = new BlockForm();
434
        $solved = $this->Users->countSolved($id);
435
        $this->set(compact('blockForm', 'isEditingAllowed', 'solved', 'user'));
436
        $this->set('titleForLayout', $user->get('username'));
437
    }
438
439
    /**
440
     * Set user avatar.
441
     *
442
     * @param string $userId user-ID
443
     * @return void|\Cake\Network\Response
444
     */
445
    public function avatar($userId)
446
    {
447
        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

447
        if (!$this->Users->exists(/** @scrutinizer ignore-type */ $userId)) {
Loading history...
448
            throw new BadRequestException;
449
        }
450
451
        /** @var User */
452
        $user = $this->Users->get($userId);
453
454
        $permissionEditing = $this->CurrentUser->permission(
455
            'saito.core.user.edit',
456
            (new ResourceAI())->onRole($user->getRole())->onOwner($user->getId())
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

456
            (new ResourceAI())->onRole($user->/** @scrutinizer ignore-call */ getRole())->onOwner($user->getId())
Loading history...
457
        );
458
        if (!$permissionEditing) {
459
            throw new \Saito\Exception\SaitoForbiddenException(
460
                "Attempt to edit user $userId.",
461
                ['CurrentUser' => $this->CurrentUser]
462
            );
463
        }
464
465
        if ($this->request->is('post') || $this->request->is('put')) {
466
            $data = [
467
                'avatar' => $this->request->getData('avatar'),
468
                'avatarDelete' => $this->request->getData('avatarDelete')
469
            ];
470
            if (!empty($data['avatarDelete'])) {
471
                $data = [
472
                    'avatar' => null,
473
                    'avatar_dir' => null
474
                ];
475
            }
476
            $patched = $this->Users->patchEntity($user, $data);
477
            $errors = $patched->getErrors();
478
            if (empty($errors) && $this->Users->save($patched)) {
479
                return $this->redirect(['action' => 'edit', $userId]);
480
            } else {
481
                $this->Flash->set(
482
                    __('The user could not be saved. Please, try again.'),
483
                    ['element' => 'error']
484
                );
485
            }
486
        }
487
488
        $this->set('user', $user);
489
490
        $this->set(
491
            'titleForPage',
492
            __('user.avatar.edit.t', [$user->get('username')])
493
        );
494
    }
495
496
    /**
497
     * Edit user.
498
     *
499
     * @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...
500
     *
501
     * @return \Cake\Network\Response|void
502
     */
503
    public function edit($id = null)
504
    {
505
        /** @var User */
506
        $user = $this->Users->get($id);
507
508
        $permissionEditing = $this->CurrentUser->permission(
509
            'saito.core.user.edit',
510
            (new ResourceAI())->onRole($user->getRole())->onOwner($user->getId())
511
        );
512
        if (!$permissionEditing) {
513
            throw new \Saito\Exception\SaitoForbiddenException(
514
                sprintf('Attempt to edit user "%s".', $user->get('id')),
515
                ['CurrentUser' => $this->CurrentUser]
516
            );
517
        }
518
519
        if ($this->request->is('post') || $this->request->is('put')) {
520
            $data = $this->request->getData();
521
            $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

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

579
        } elseif ($this->CurrentUser->isUser(/** @scrutinizer ignore-type */ $readUser)) {
Loading history...
580
            $failure = true;
581
            $this->Flash->set(__('user.del.fail.1'), ['element' => 'error']);
582
        }
583
584
        if (!$failure) {
585
            $result = $this->Users->deleteAllExceptEntries($id);
586
            if (empty($result)) {
587
                $failure = true;
588
                $this->Flash->set(__('user.del.fail.2'), ['element' => 'error']);
589
            }
590
        }
591
592
        if ($failure) {
593
            return $this->redirect(
594
                [
595
                    'prefix' => false,
596
                    'controller' => 'users',
597
                    'action' => 'view',
598
                    $id
599
                ]
600
            );
601
        }
602
603
        $this->Flash->set(
604
            __('user.del.ok.m', $readUser->get('username')),
605
            ['element' => 'success']
606
        );
607
608
        return $this->redirect('/');
609
    }
610
611
    /**
612
     * Lock user.
613
     *
614
     * @return \Cake\Network\Response|void
615
     * @throws BadRequestException
616
     */
617
    public function lock()
618
    {
619
        $form = new BlockForm();
620
        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

620
        if (!$form->validate(/** @scrutinizer ignore-type */ $this->request->getData())) {
Loading history...
621
            throw new BadRequestException;
622
        }
623
624
        $id = (int)$this->request->getData('lockUserId');
625
626
        /** @var User */
627
        $readUser = $this->Users->get($id);
628
629
        $permission = $this->CurrentUser->permission(
630
            'saito.core.user.lock.set',
631
            (new ResourceAI())->onRole($readUser->getRole())
632
        );
633
        if (!$permission) {
634
            throw new ForbiddenException(null, 1571316877);
635
        }
636
637
        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

637
        if ($this->CurrentUser->isUser(/** @scrutinizer ignore-type */ $readUser)) {
Loading history...
638
            $message = __('You can\'t lock yourself.');
639
            $this->Flash->set($message, ['element' => 'error']);
640
        } else {
641
            try {
642
                $duration = (int)$this->request->getData('lockPeriod');
643
                $blocker = new ManualBlocker($this->CurrentUser->getId(), $duration);
644
                $status = $this->Users->UserBlocks->block($blocker, $id);
645
                if (!$status) {
646
                    throw new \Exception();
647
                }
648
                $message = __('User {0} is locked.', $readUser->get('username'));
649
                $this->Flash->set($message, ['element' => 'success']);
650
            } catch (\Exception $e) {
651
                $message = __('Error while locking.');
652
                $this->Flash->set($message, ['element' => 'error']);
653
            }
654
        }
655
656
        return $this->redirect($this->referer());
657
    }
658
659
    /**
660
     * Unblock user.
661
     *
662
     * @param string $id user-ID
663
     * @return void
664
     */
665
    public function unlock(string $id)
666
    {
667
        $id = (int)$id;
668
669
        /** @var User */
670
        $user = $this->Users
671
            ->find()
672
            ->matching('UserBlocks', function ($q) use ($id) {
673
                return $q->where(['UserBlocks.id' => $id]);
674
            })
675
            ->first();
676
677
        $permission = $this->CurrentUser->permission(
678
            'saito.core.user.lock.set',
679
            (new ResourceAI())->onRole($user->getRole())
680
        );
681
        if (!$permission) {
682
            throw new ForbiddenException(null, 1571316877);
683
        }
684
685
        if (!$this->Users->UserBlocks->unblock($id)) {
686
            $this->Flash->set(
687
                __('Error while unlocking.'),
688
                ['element' => 'error']
689
            );
690
        }
691
692
        $message = __('User {0} is unlocked.', $user->get('username'));
693
        $this->Flash->set($message, ['element' => 'success']);
694
        $this->redirect($this->referer());
695
    }
696
697
    /**
698
     * changes user password
699
     *
700
     * @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...
701
     * @return void
702
     * @throws \Saito\Exception\SaitoForbiddenException
703
     * @throws BadRequestException
704
     */
705
    public function changepassword($id = null)
706
    {
707
        if (empty($id)) {
708
            throw new BadRequestException();
709
        }
710
711
        /** @var User */
712
        $user = $this->Users->get($id);
713
        $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

713
        $allowed = $this->CurrentUser->isUser(/** @scrutinizer ignore-type */ $user);
Loading history...
714
        if (empty($user) || !$allowed) {
715
            throw new SaitoForbiddenException(
716
                "Attempt to change password for user $id.",
717
                ['CurrentUser' => $this->CurrentUser]
718
            );
719
        }
720
        $this->set('userId', $id);
721
        $this->set('username', $user->get('username'));
722
723
        //= just show empty form
724
        if (empty($this->request->getData())) {
725
            return;
726
        }
727
728
        $formFields = ['password', 'password_old', 'password_confirm'];
729
730
        //= process submitted form
731
        $data = [];
732
        foreach ($formFields as $field) {
733
            $data[$field] = $this->request->getData($field);
734
        }
735
        $this->Users->patchEntity($user, $data);
736
        $success = $this->Users->save($user);
737
738
        if ($success) {
739
            $this->Flash->set(
740
                __('change_password_success'),
741
                ['element' => 'success']
742
            );
743
            $this->redirect(['controller' => 'users', 'action' => 'edit', $id]);
744
745
            return;
746
        }
747
748
        $errors = $user->getErrors();
749
        if (!empty($errors)) {
750
            $this->Flash->set(
751
                __d('nondynamic', current(array_pop($errors))),
752
                ['element' => 'error']
753
            );
754
        }
755
756
        //= unset all autofill form data
757
        foreach ($formFields as $field) {
758
            $this->request = $this->request->withoutData($field);
759
        }
760
    }
761
762
    /**
763
     * Directly set password for user
764
     *
765
     * @param string $id user-ID
766
     * @return Response|null
767
     */
768
    public function setpassword($id)
769
    {
770
        /** @var User */
771
        $user = $this->Users->get($id);
772
773
        if (!$this->CurrentUser->permission('saito.core.user.password.set', (new ResourceAI())->onRole($user->getRole()))) {
774
            throw new SaitoForbiddenException(
775
                "Attempt to set password for user $id.",
776
                ['CurrentUser' => $this->CurrentUser]
777
            );
778
        }
779
780
        if ($this->getRequest()->is('post')) {
781
            $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

781
            $this->Users->patchEntity($user, /** @scrutinizer ignore-type */ $this->getRequest()->getData(), ['fields' => 'password']);
Loading history...
782
783
            if ($this->Users->save($user)) {
784
                $this->Flash->set(
785
                    __('user.pw.set.s'),
786
                    ['element' => 'success']
787
                );
788
789
                return $this->redirect(['controller' => 'users', 'action' => 'edit', $id]);
790
            }
791
            $errors = $user->getErrors();
792
            if (!empty($errors)) {
793
                $this->Flash->set(
794
                    __d('nondynamic', current(array_pop($errors))),
795
                    ['element' => 'error']
796
                );
797
            }
798
        }
799
800
        $this->set(compact('user'));
801
    }
802
803
    /**
804
     * View and set user role
805
     *
806
     * @param string $id User-ID
807
     * @return void|Response
808
     */
809
    public function role($id)
810
    {
811
        /** @var User */
812
        $user = $this->Users->get($id);
813
        $identifier = (new ResourceAI())->onRole($user->getRole());
814
        $unrestricted = $this->CurrentUser->permission('saito.core.user.role.set.unrestricted', $identifier);
815
        $restricted = $this->CurrentUser->permission('saito.core.user.role.set.restricted', $identifier);
816
        if (!$restricted && !$unrestricted) {
817
            throw new ForbiddenException();
818
        }
819
820
        /** @var Permissions */
821
        $Permissions = Registry::get('Permissions');
822
823
        $roles = $Permissions->getRoles()->get($this->CurrentUser->getRole(), false, $unrestricted);
824
825
        if ($this->getRequest()->is('put')) {
826
            $type = $this->getRequest()->getData('user_type');
827
            if (!in_array($type, $roles)) {
828
                throw new \InvalidArgumentException(
829
                    sprintf('User type "%s" is not available.', $type),
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type array; however, parameter $args of sprintf() 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

829
                    sprintf('User type "%s" is not available.', /** @scrutinizer ignore-type */ $type),
Loading history...
830
                    1573376871
831
                );
832
            }
833
            $patched = $this->Users->patchEntity($user, ['user_type' => $type]);
834
835
            $errors = $patched->getErrors();
836
            if (empty($errors)) {
837
                $this->Users->save($patched);
838
839
                return $this->redirect(['action' => 'edit', $user->get('id')]);
840
            }
841
842
            $msg = current(current($errors));
843
            $this->Flash->set($msg, ['element' => 'error']);
844
        }
845
846
        $this->set(compact('roles', 'user'));
847
    }
848
849
    /**
850
     * Set slidetab-order.
851
     *
852
     * @return \Cake\Network\Response
853
     * @throws BadRequestException
854
     */
855
    public function slidetabOrder()
856
    {
857
        if (!$this->request->is('ajax')) {
858
            throw new BadRequestException;
859
        }
860
861
        $order = $this->request->getData('slidetabOrder');
862
        if (!$order) {
863
            throw new BadRequestException;
864
        }
865
866
        $allowed = $this->Slidetabs->getAvailable();
867
        $order = array_filter(
868
            $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

868
            /** @scrutinizer ignore-type */ $order,
Loading history...
869
            function ($item) use ($allowed) {
870
                return in_array($item, $allowed);
871
            }
872
        );
873
        $order = serialize($order);
874
875
        $userId = $this->CurrentUser->getId();
876
        $user = $this->Users->get($userId);
877
        $this->Users->patchEntity($user, ['slidetab_order' => $order]);
878
        $this->Users->save($user);
879
880
        $this->CurrentUser->set('slidetab_order', $order);
881
882
        $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

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