Issues (326)

src/Controller/UsersController.php (21 issues)

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
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
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
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
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
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
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\Http\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\Http\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
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', '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\Http\Response
444
     */
445
    public function avatar($userId)
446
    {
447
        if (!$this->Users->exists($userId)) {
0 ignored issues
show
$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
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\Http\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
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\Http\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 SaitoForbiddenException(
564
                'Not allowed to delete a user.',
565
                ['CurrentUser' => $this->CurrentUser, 'user_id' => $readUser->get('username')]
566
            );
567
        }
568
569
        $this->set('user', $readUser);
570
571
        $failure = false;
572
        if (!$this->request->getData('userdeleteconfirm')) {
573
            $failure = true;
574
            $this->Flash->set(__('user.del.fail.3'), ['element' => 'error']);
575
        } elseif ($this->CurrentUser->isUser($readUser)) {
0 ignored issues
show
$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

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

616
        if (!$form->validate(/** @scrutinizer ignore-type */ $this->request->getData())) {
Loading history...
617
            throw new BadRequestException();
618
        }
619
620
        $id = (int)$this->request->getData('lockUserId');
621
622
        /** @var User */
623
        $readUser = $this->Users->get($id);
624
625
        $permission = $this->CurrentUser->permission(
626
            'saito.core.user.lock.set',
627
            (new ResourceAI())->onRole($readUser->getRole())
628
        );
629
        if (!$permission) {
630
            throw new SaitoForbiddenException(
631
                null,
632
                ['CurrentUser' => $this->CurrentUser]
633
            );
634
        }
635
636
        if ($this->CurrentUser->isUser($readUser)) {
0 ignored issues
show
$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 ResourceAI())->onRole($user->getRole())
679
        );
680
        if (!$permission) {
681
            throw new SaitoForbiddenException(
682
                null,
683
                ['CurrentUser' => $this->CurrentUser]
684
            );
685
        }
686
687
        if (!$this->Users->UserBlocks->unblock($id)) {
688
            $this->Flash->set(
689
                __('Error while unlocking.'),
690
                ['element' => 'error']
691
            );
692
        }
693
694
        $message = __('User {0} is unlocked.', $user->get('username'));
695
        $this->Flash->set($message, ['element' => 'success']);
696
        $this->redirect($this->referer());
697
    }
698
699
    /**
700
     * changes user password
701
     *
702
     * @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...
703
     * @return void
704
     * @throws \Saito\Exception\SaitoForbiddenException
705
     * @throws BadRequestException
706
     */
707
    public function changepassword($id = null)
708
    {
709
        if (empty($id)) {
710
            throw new BadRequestException();
711
        }
712
713
        /** @var User */
714
        $user = $this->Users->get($id);
715
        $allowed = $this->CurrentUser->isUser($user);
0 ignored issues
show
$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

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

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

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

873
            /** @scrutinizer ignore-type */ $order,
Loading history...
874
            function ($item) use ($allowed) {
875
                return in_array($item, $allowed);
876
            }
877
        );
878
        $order = serialize($order);
879
880
        $userId = $this->CurrentUser->getId();
881
        $user = $this->Users->get($userId);
882
        $this->Users->patchEntity($user, ['slidetab_order' => $order]);
883
        $this->Users->save($user);
884
885
        $this->CurrentUser->set('slidetab_order', $order);
886
887
        $this->response = $this->response->withStringBody(true);
0 ignored issues
show
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

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