Completed
Branch feature/Authentication4 (554da3)
by Schlaefer
03:43
created

UsersTable::checkPassword()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 11
rs 9.9
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\Model\Table;
14
15
use App\Lib\Model\Table\AppTable;
16
use App\Lib\Model\Table\FieldFilter;
17
use App\Model\Table\EntriesTable;
18
use App\Model\Table\UserBlocksTable;
19
use App\Model\Table\UserIgnoresTable;
20
use Authentication\PasswordHasher\DefaultPasswordHasher;
21
use Authentication\PasswordHasher\PasswordHasherFactory;
22
use Authentication\PasswordHasher\PasswordHasherInterface;
23
use Cake\Core\Configure;
24
use Cake\Database\Schema\TableSchema;
25
use Cake\Datasource\EntityInterface;
26
use Cake\Datasource\Exception\RecordNotFoundException;
27
use Cake\Event\Event;
28
use Cake\ORM\Entity;
29
use Cake\ORM\Query;
30
use Cake\Validation\Validation;
31
use Cake\Validation\Validator;
32
use DateTimeInterface;
33
use Saito\User\Upload\AvatarFilenameListener;
34
use Stopwatch\Lib\Stopwatch;
35
36
/**
37
 * Users table
38
 *
39
 * @property EntriesTable $Entries
40
 * @property UserBlocksTable $UserBlocks
41
 * @property UserIgnoresTable $UserIgnores
42
 * @property UserOnlineTable $UserOnline
43
 */
44
class UsersTable extends AppTable
45
{
46
    /**
47
     * Max lenght for username.
48
     *
49
     * Constrained to 191 due to InnoDB index max-length on MySQL 5.6.
50
     */
51
    public const USERNAME_MAXLENGTH = 191;
52
53
    /**
54
     * {@inheritDoc}
55
     */
56
    protected $_defaultConfig = [
57
        'user_name_disallowed_chars' => ['\'', ';', '&', '<', '>']
58
    ];
59
60
    /**
61
     * {@inheritDoc}
62
     */
63
    public function initialize(array $config)
64
    {
65
        $this->addBehavior(
66
            'Cron.Cron',
67
            [
68
                'registerGc' => [
69
                    'id' => 'User.registerGc',
70
                    'due' => 'daily',
71
                ],
72
                'userBlockGc' => [
73
                    'id' => 'User.userBlockGc',
74
                    'due' => '+15 minutes',
75
                ]
76
            ]
77
        );
78
79
        $avatarRootDir = Configure::read('Saito.Settings.uploadDirectory');
80
        $this->addBehavior(
81
            'Proffer.Proffer',
82
            [
83
                'avatar' => [ // The name of your upload field (filename)
84
                    'root' => $avatarRootDir,
85
                    'dir' => 'avatar_dir', // field for upload directory
86
                    'thumbnailSizes' => [
87
                        'square' => ['w' => 100, 'h' => 100],
88
                    ],
89
                    // Options are Imagick, Gd or Gmagick
90
                    'thumbnailMethod' => 'Gd'
91
                ]
92
            ]
93
        );
94
        $this->getEventManager()->on(new AvatarFilenameListener($avatarRootDir));
95
96
        $this->hasOne(
97
            'UserOnline',
98
            ['dependent' => true, 'foreignKey' => 'user_id']
99
        );
100
101
        $this->hasMany(
102
            'Bookmarks',
103
            ['foreignKey' => 'user_id', 'dependent' => true]
104
        );
105
        $this->hasMany('Drafts', ['dependent' => true]);
106
        $this->hasMany('UserIgnores', ['foreignKey' => 'user_id']);
107
        $this->hasMany(
108
            'Entries',
109
            [
110
                'foreignKey' => 'user_id',
111
                'conditions' => ['Entries.user_id' => 'Users.id'],
112
            ]
113
        );
114
        $this->hasMany(
115
            'ImageUploader.Uploads',
116
            ['dependent' => true, 'foreignKey' => 'user_id']
117
        );
118
        $this->hasMany(
119
            'UserReads',
120
            ['foreignKey' => 'user_id', 'dependent' => true]
121
        );
122
        $this->hasMany(
123
            'UserBlocks',
124
            [
125
                'foreignKey' => 'user_id',
126
                'dependent' => true,
127
                'sort' => [
128
                    'UserBlocks.ended IS NULL DESC',
129
                    'UserBlocks.ended' => 'DESC',
130
                    'UserBlocks.id' => 'DESC'
131
                ]
132
            ]
133
        );
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     */
139
    public function validationDefault(Validator $validator)
140
    {
141
        $validator->setProvider(
142
            'saito',
143
            'Saito\Validation\SaitoValidationProvider'
144
        );
145
146
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::allowEmpty() has been deprecated with message: 3.7.0 Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(), allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
147
            ->setProvider(
148
                'proffer',
149
                'Proffer\Model\Validation\ProfferRules'
150
            )
151
            ->allowEmpty('avatar_dir')
152
            ->allowEmpty('avatar')
153
            ->add(
154
                'avatar',
155
                'avatar-extension',
156
                [
157
                    'rule' => ['extension', ['jpg', 'jpeg', 'png']],
158
                    'message' => __('user.avatar.error.extension', ['jpg, jpeg, png'])
159
                ]
160
            )
161
            ->add(
162
                'avatar',
163
                'avatar-size',
164
                [
165
                    'rule' => ['fileSize', Validation::COMPARE_LESS, '3MB'],
166
                    'message' => __('user.avatar.error.size', ['3'])
167
                ]
168
            )
169
            ->add(
170
                'avatar',
171
                'avatar-mime',
172
                [
173
                    'rule' => ['mimetype', ['image/jpeg', 'image/png']],
174
                    'message' => __('user.avatar.error.mime')
175
                ]
176
            )
177
            ->add(
178
                'avatar',
179
                'avatar-dimension',
180
                [
181
                    'rule' => [
182
                        'dimensions',
183
                        [
184
                            'min' => ['w' => 100, 'h' => 100],
185
                            'max' => ['w' => 1500, 'h' => 1500]
186
                        ]
187
                    ],
188
                    'message' => __(
189
                        'user.avatar.error.dimension',
190
                        ['100x100', '1500x1500']
191
                    ),
192
                    'provider' => 'proffer'
193
                ]
194
            );
195
196
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
197
            ->notEmpty('password')
198
            ->add(
199
                'password',
200
                [
201
                    'pwConfirm' => [
202
                        'rule' => [$this, 'validateConfirmPassword'],
203
                        'last' => true,
204
                        'message' => __('error_password_confirm')
205
                    ]
206
                ]
207
            );
208
209
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
210
            ->notEmpty('password_old')
211
            ->add(
212
                'password_old',
213
                [
214
                    'pwCheckOld' => [
215
                        'rule' => [$this, 'validateCheckOldPassword'],
216
                        'last' => true,
217
                        'message' => 'validation_error_pwCheckOld'
218
                    ]
219
                ]
220
            );
221
222
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
223
            ->notEmpty('username', __('error_no_name'))
224
            ->add(
225
                'username',
226
                [
227
                    'isUnique' => [
228
                        'rule' => 'validateIsUniqueCiString',
229
                        'provider' => 'saito',
230
                        'last' => true,
231
                        'message' => __('error_name_reserved')
232
                    ],
233
                    'isUsernameEqual' => [
234
                        'on' => 'create',
235
                        'last' => true,
236
                        'rule' => [$this, 'validateUsernameEqual']
237
                    ],
238
                    'hasAllowedChars' => [
239
                        'rule' => [$this, 'validateHasAllowedChars'],
240
                        'message' => __(
241
                            'model.user.validate.username.hasAllowedChars'
242
                        )
243
                    ],
244
                    'isNotEmoji' => [
245
                        'rule' => 'utf8',
246
                        'message' => __(
247
                            'model.user.validate.username.hasAllowedChars'
248
                        )
249
                    ],
250
                    'maxLength' => [
251
                        'last' => true,
252
                        'message' => __('vld.users.username.maxlength', self::USERNAME_MAXLENGTH),
253
                        'rule' => ['maxLength', self::USERNAME_MAXLENGTH],
254
                    ],
255
                ]
256
            );
257
258
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
259
            ->notEmpty('user_email')
260
            ->add(
261
                'user_email',
262
                [
263
                    'isUnique' => [
264
                        'rule' => 'validateUnique',
265
                        'provider' => 'table',
266
                        'last' => true,
267
                        'message' => __('error_email_reserved')
268
                    ],
269
                    'isEmail' => [
270
                        'rule' => ['email', true],
271
                        'last' => true,
272
                        'message' => __('error_email_wrong')
273
                    ]
274
                ]
275
            );
276
277
        $validator->add(
278
            'user_forum_refresh_time',
279
            [
280
                'numeric' => ['rule' => 'numeric'],
281
                'greaterNull' => ['rule' => ['comparison', '>=', 0]],
282
                'maxLength' => ['rule' => ['maxLength', 3]],
283
            ]
284
        );
285
286
        $validator->add(
287
            'user_type',
288
            [
289
                'allowedType' => [
290
                    'rule' => ['inList', ['user', 'mod', 'admin']]
291
                ]
292
            ]
293
        );
294
295
        $validator->notEmpty('registered');
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
296
297
        $validator->add(
298
            'logins',
299
            ['numeric' => ['rule' => ['numeric']]]
300
        );
301
302
        $validator->add(
303
            'personal_messages',
304
            ['bool' => ['rule' => ['boolean']]]
305
        );
306
307
        $validator->add(
308
            'user_lock',
309
            ['bool' => ['rule' => ['boolean']]]
310
        );
311
312
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
313
            ->notEmpty('activate_code')
314
            ->add(
315
                'activate_code',
316
                [
317
                    'numeric' => ['rule' => ['numeric']],
318
                    'between' => ['rule' => ['range', 0, 9999999]]
319
                ]
320
            );
321
322
        $validator->add(
323
            'user_signatures_hide',
324
            ['bool' => ['rule' => ['boolean']]]
325
        );
326
327
        $validator->add(
328
            'user_signature_images_hide',
329
            ['bool' => ['rule' => ['boolean']]]
330
        );
331
332
        $validator->add(
333
            'user_automaticaly_mark_as_read',
334
            ['bool' => ['rule' => ['boolean']]]
335
        );
336
337
        $validator->add(
338
            'user_sort_last_answer',
339
            ['bool' => ['rule' => ['boolean']]]
340
        );
341
342
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::allowEmpty() has been deprecated with message: 3.7.0 Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(), allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
343
            ->allowEmpty('user_color_new_postings')
344
            ->add(
345
                'user_color_new_postings',
346
                [
347
                    'hexformat' => [
348
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i']
349
                    ]
350
                ]
351
            );
352
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::allowEmpty() has been deprecated with message: 3.7.0 Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(), allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
353
            ->allowEmpty('user_color_old_postings')
354
            ->add(
355
                'user_color_old_postings',
356
                [
357
                    'hexformat' => [
358
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i']
359
                    ]
360
                ]
361
            );
362
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::allowEmpty() has been deprecated with message: 3.7.0 Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(), allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
363
            ->allowEmpty('user_color_actual_posting')
364
            ->add(
365
                'user_color_actual_posting',
366
                [
367
                    'hexformat' => [
368
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i']
369
                    ]
370
                ]
371
            );
372
373
        return $validator;
374
    }
375
376
    /**
377
     * {@inheritDoc}
378
     */
379
    protected function _initializeSchema(TableSchema $table)
380
    {
381
        $table->setColumnType('avatar', 'proffer.file');
382
        $table->setColumnType('user_category_custom', 'serialize');
383
384
        return $table;
385
    }
386
387
    /**
388
     * set last refresh
389
     *
390
     * @param int $userId user-ID
391
     * @param DateTimeInterface|null $lastRefresh last refresh
392
     * @return void
393
     */
394
    public function setLastRefresh(int $userId, DateTimeInterface $lastRefresh = null)
395
    {
396
        Stopwatch::start('Users->setLastRefresh()');
397
        $data['last_refresh_tmp'] = bDate();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
398
399
        if ($lastRefresh) {
400
            $data['last_refresh'] = $lastRefresh;
401
        }
402
403
        $this->query()
404
            ->update()
405
            ->set($data)
406
            ->where(['id' => $userId])
407
            ->execute();
408
409
        Stopwatch::end('Users->setLastRefresh()');
410
    }
411
412
    /**
413
     * Increment logins
414
     *
415
     * @param Entity $user user
416
     * @param int $amount amount
417
     * @return void
418
     * @throws \Exception
419
     */
420
    public function incrementLogins(Entity $user, $amount = 1)
421
    {
422
        $data = [
423
            'logins' => $user->get('logins') + $amount,
424
            'last_login' => bDate()
425
        ];
426
        $this->patchEntity($user, $data);
427
        if (!$this->save($user)) {
428
            throw new \Exception('Increment logins failed.');
429
        }
430
    }
431
432
    /**
433
     * get userlist
434
     *
435
     * @return array
436
     */
437
    public function userlist()
438
    {
439
        return $this->find(
440
            'list',
441
            ['keyField' => 'id', 'valueField' => 'username']
442
        )->toArray();
443
    }
444
445
    /**
446
     * Removes a user and all his data execpt for his entries
447
     *
448
     * @param int $userId user-ID
449
     * @return bool
450
     */
451
    public function deleteAllExceptEntries(int $userId)
452
    {
453
        if ($userId == 1) {
454
            return false;
455
        }
456
        $user = $this->get($userId);
457
        if (!$user) {
458
            return false;
459
        }
460
461
        try {
462
            $this->Entries->anonymizeEntriesFromUser($userId);
463
            $this->UserIgnores->deleteUser($userId);
464
            $this->delete($user);
465
        } catch (\Exception $e) {
466
            return false;
467
        }
468
        $this->_dispatchEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
469
470
        return true;
471
    }
472
473
    /**
474
     * Updates the hashed password if hash-algo is out-of-date
475
     *
476
     * @param int $userId user-ID
477
     * @param string $password password
478
     * @return void
479
     */
480
    public function autoUpdatePassword(int $userId, string $password): void
481
    {
482
        $user = $this->get($userId, ['fields' => ['id', 'password']]);
483
        $oldPassword = $user->get('password');
484
        $needsRehash = $this->getPasswordHasher()->needsRehash($oldPassword);
485
        if ($needsRehash) {
486
            $user->set('password', $password);
487
            $this->save($user);
488
        }
489
    }
490
491
    /**
492
     * {@inheritDoc}
493
     */
494
    public function afterSave(
495
        Event $event,
496
        Entity $entity,
497
        \ArrayObject $options
498
    ) {
499
        if ($entity->getOriginal('username')) {
500
            $this->_dispatchEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
501
        }
502
    }
503
504
    /**
505
     * {@inheritDoc}
506
     */
507
    public function beforeSave(
508
        Event $event,
509
        Entity $entity,
510
        \ArrayObject $options
511
    ) {
512
        if ($entity->isDirty('password')) {
513
            $hashedPassword = $this->getPasswordHasher()->hash($entity->get('password'));
514
            $entity->set('password', $hashedPassword);
515
        }
516
    }
517
518
    /**
519
     * {@inheritDoc}
520
     */
521
    public function beforeValidate(
522
        Event $event,
523
        Entity $entity,
524
        \ArrayObject $options,
525
        Validator $validator
0 ignored issues
show
Unused Code introduced by
The parameter $validator is not used and could be removed.

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

Loading history...
526
    ) {
527
        if ($entity->isDirty('user_forum_refresh_time')) {
528
            $time = $entity->get('user_forum_refresh_time');
529
            if (empty($time)) {
530
                $entity->set('user_forum_refresh_time', 0);
531
            }
532
        }
533
    }
534
535
    /**
536
     * validate old password
537
     *
538
     * @param string $value value
539
     * @param array $context context
540
     * @return bool
541
     */
542
    public function validateCheckOldPassword($value, array $context)
543
    {
544
        $userId = $context['data']['id'];
545
        $oldPasswordHash = $this->get($userId, ['fields' => ['password']])
546
            ->get('password');
547
548
        return $this->getPasswordHasher()->check($value, $oldPasswordHash);
549
    }
550
551
    /**
552
     * validate confirm password
553
     *
554
     * @param string $value value
555
     * @param array $context context
556
     * @return bool
557
     */
558
    public function validateConfirmPassword($value, array $context)
559
    {
560
        if ($value === $context['data']['password_confirm']) {
561
            return true;
562
        }
563
564
        return false;
565
    }
566
567
    /**
568
     * Validate allowed chars
569
     *
570
     * @param string $value value
571
     * @param array $context context
572
     * @return bool
573
     */
574
    public function validateHasAllowedChars($value, array $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

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

Loading history...
575
    {
576
        foreach ($this->getConfig('user_name_disallowed_chars') as $char) {
577
            if (mb_strpos($value, $char) !== false) {
578
                return false;
579
            }
580
        }
581
582
        return true;
583
    }
584
585
    /**
586
     * checks if equal username exists
587
     *
588
     * @param string $value value
589
     * @param array $context context
590
     * @return bool|string
591
     */
592
    public function validateUsernameEqual($value, array $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

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

Loading history...
593
    {
594
        Stopwatch::start('validateUsernameEqual');
595
        $users = $this->userlist();
596
        $lc = mb_strtolower($value);
597
        foreach ($users as $name) {
598
            if ($name === $value) {
599
                continue;
600
            }
601
            $name = mb_strtolower($name);
602
            $distance = levenshtein($lc, $name);
603
            if ($distance < 2) {
604
                return __('error.name.equalExists', $name);
605
            }
606
        }
607
        Stopwatch::stop('validateUsernameEqual');
608
609
        return true;
610
    }
611
612
    /**
613
     * Registers new user
614
     *
615
     * @param array $data data
616
     * @param bool $activate activate
617
     * @return EntityInterface
618
     */
619
    public function register($data, $activate = false): EntityInterface
620
    {
621
        $defaults = [
622
            'registered' => bDate(),
623
            'user_type' => 'user'
624
        ];
625
        $fields = [
626
            'password',
627
            'registered',
628
            'user_email',
629
            'user_type',
630
            'username'
631
        ];
632
633
        if ($activate !== true) {
634
            $defaults['activate_code'] = mt_rand(1000000, 9999999);
635
            $fields[] = 'activate_code';
636
        }
637
638
        $data = array_merge($data, $defaults);
639
640
        $fieldFilter = (new FieldFilter())->setConfig('register', $fields);
641
        if (!$fieldFilter->requireFields($data, 'register')) {
642
            throw new \RuntimeException(
643
                'Required fields for registration were not provided.',
644
                1563789683
645
            );
646
        }
647
648
        $user = $this->newEntity($data, ['fields' => $fields]);
649
        $errors = $user->getErrors();
650
        if (!empty($errors)) {
651
            return $user;
652
        }
653
        $this->save($user);
654
655
        return $user;
656
    }
657
658
    /**
659
     * Garbage collection for registration
660
     *
661
     * Deletes all timed out and unactivated registrations
662
     *
663
     * @return void
664
     */
665
    public function registerGc()
666
    {
667
        $this->deleteAll(
668
            [
669
                'activate_code >' => 0,
670
                'registered <' => bDate(time() - 86400)
671
            ]
672
        );
673
    }
674
675
    /**
676
     * calls garbage collection for UserBlock
677
     *
678
     * UserBlock is lazy-loaded rarely and gc may not trigger often enough (at
679
     * least with manual blocking and ignore blocking only)
680
     *
681
     * @return void
682
     */
683
    public function userBlockGc()
684
    {
685
        $this->UserBlocks->gc();
686
    }
687
688
    /**
689
     * activates user
690
     *
691
     * @param int $userId user-ID
692
     * @param string $code activation code
693
     * @return array|bool false if activation failed; array with status and
694
     *     user data on success
695
     * @throws \InvalidArgumentException
696
     */
697
    public function activate($userId, $code)
698
    {
699
        if (!is_int($userId) || !is_string($code)) {
700
            throw new \InvalidArgumentException();
701
        }
702
703
        try {
704
            $user = $this->get($userId);
705
        } catch (RecordNotFoundException $e) {
706
            throw new \InvalidArgumentException();
707
        }
708
709
        $activateCode = strval($user->get('activate_code'));
710
711
        if (empty($activateCode)) {
712
            return ['status' => 'already', 'User' => $user];
713
        } elseif ($activateCode !== $code) {
714
            return false;
715
        }
716
717
        $user->set('activate_code', 0);
718
        $success = $this->save($user);
719
        if (empty($success)) {
720
            return false;
721
        }
722
723
        $this->_dispatchEvent('Model.User.afterActivate', ['User' => $user]);
724
725
        return ['status' => 'activated', 'User' => $user];
726
    }
727
728
    /**
729
     * Count solved posting for a user.
730
     *
731
     *
732
     * @param int $userId user-ID
733
     * @return int count
734
     */
735
    public function countSolved($userId)
736
    {
737
        $count = $this->find()
738
            ->select(['Users.id'])
739
            ->where(['Users.id' => $userId])
740
            ->join(
741
                [
742
                    'Entries' => [
743
                        'table' => $this->Entries->getTable(),
744
                        'type' => 'INNER',
745
                        'conditions' => [
746
                            [
747
                                'Entries.solves >' => '0',
748
                                'Entries.user_id' => $userId
749
                            ]
750
                        ],
751
                    ],
752
                    'Root' => [
753
                        'table' => $this->Entries->getTable(),
754
                        'type' => 'INNER',
755
                        // Don't answers to own question.
756
                        'conditions' => [
757
                            'Root.id = Entries.solves',
758
                            'Root.user_id != Users.id',
759
                        ]
760
                    ]
761
                ]
762
            );
763
764
        return $count->count();
765
    }
766
767
    /**
768
     * Set view categories preferences
769
     *
770
     * ## $category
771
     *
772
     * - 'all': set to all categories
773
     * - array: (cat_id1 => true|1|'1', cat_id2 => true|1|'1')
774
     * - int: set to single category_id
775
     *
776
     * @param int $userId user-ID
777
     * @param string|int|array $category category
778
     * @return void
779
     * @throws \InvalidArgumentException
780
     */
781
    public function setCategory($userId, $category)
782
    {
783
        $User = $this->find()->select(['id' => $userId])->first();
784
        if (!$User) {
785
            throw new \InvalidArgumentException(
786
                "Can't find user with id $userId.",
787
                1420807691
788
            );
789
        }
790
791
        if ($category === 'all') {
792
            //=if show all cateogries
793
            $active = -1;
794
        } elseif (is_array($category)) {
795
            //=if set a custom set of categories
796
            $active = 0;
797
798
            $availableCats = $this->Entries->Categories->find('list')->toArray(
799
            );
800
            $categories = array_intersect_key($category, $availableCats);
801
            if (count($categories) === 0) {
802
                throw new \InvalidArgumentException();
803
            }
804
            $newCats = [];
805
            foreach ($categories as $cat => $v) {
806
                $newCats[$cat] = ($v === true || $v === 1 || $v === '1');
807
            }
808
            $User->set('user_category_custom', $newCats);
809
        } else {
810
            //=if set a single category
811
            $category = (int)$category;
812
            if ($category > 0 && $this->Entries->Categories->exists((int)$category)
813
            ) {
814
                $active = $category;
815
            } else {
816
                throw new \InvalidArgumentException();
817
            }
818
        }
819
820
        $User->set('user_category_active', $active);
821
        $this->save($User);
0 ignored issues
show
Bug introduced by
It seems like $User defined by $this->find()->select(ar...' => $userId))->first() on line 783 can also be of type array; however, Cake\ORM\Table::save() does only seem to accept object<Cake\Datasource\EntityInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
822
    }
823
824
    /**
825
     * Get default password hasher for hashing user passwords.
826
     *
827
     * @return PasswordHasherInterface
828
     */
829
    public function getPasswordHasher(): PasswordHasherInterface
830
    {
831
        return PasswordHasherFactory::build(DefaultPasswordHasher::class);
832
    }
833
834
    /**
835
     * Finds a user with additional profil informations from associated tables
836
     *
837
     * @param Query $query query
838
     * @param array $options options
839
     * @return Query
840
     */
841
    public function findProfile(Query $query, array $options): Query
842
    {
843
        $query
844
            // ->enableHydration(false)
845
            ->contain(
846
                [
847
                    'UserIgnores' => function ($query) {
848
                        return $query->enableHydration(false)->select(
849
                            ['blocked_user_id', 'user_id']
850
                        );
851
                    }
852
                ]
853
            );
854
855
        return $query;
856
    }
857
858
    /**
859
     * Find all sorted by username
860
     *
861
     * @param Query $query query
862
     * @param array $options options
863
     * @return Query
864
     */
865
    public function findPaginated(Query $query, array $options)
866
    {
867
        $query
868
            ->contain(['UserOnline'])
869
            ->order(['Users.username' => 'ASC']);
870
871
        return $query;
872
    }
873
874
    /**
875
     * Find the latest, successfully registered user
876
     *
877
     * @param Query $query query
878
     * @param array $options options
879
     * @return Query
880
     */
881
    public function findLatest(Query $query, array $options)
882
    {
883
        $query->where(['activate_code' => 0])
884
            ->order(['id' => 'DESC'])
885
            ->limit(1);
886
887
        return $query;
888
    }
889
}
890