Completed
Push — master ( 225222...7d085e )
by Schlaefer
05:18 queued 02:52
created

UsersTable::beforeSave()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 0
loc 10
rs 9.9332
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\Locator\TableLocator;
30
use Cake\ORM\Query;
31
use Cake\ORM\TableRegistry;
32
use Cake\Validation\Validation;
33
use Cake\Validation\Validator;
34
use DateTimeInterface;
35
use Saito\User\Upload\AvatarFilenameListener;
36
use Stopwatch\Lib\Stopwatch;
37
38
/**
39
 * Users table
40
 *
41
 * @property EntriesTable $Entries
42
 * @property UserBlocksTable $UserBlocks
43
 * @property UserIgnoresTable $UserIgnores
44
 * @property UserOnlineTable $UserOnline
45
 */
46
class UsersTable extends AppTable
47
{
48
    /**
49
     * Max lenght for username.
50
     *
51
     * Constrained to 191 due to InnoDB index max-length on MySQL 5.6.
52
     */
53
    public const USERNAME_MAXLENGTH = 191;
54
55
    /**
56
     * {@inheritDoc}
57
     */
58
    protected $_defaultConfig = [
59
        'user_name_disallowed_chars' => ['\'', ';', '&', '<', '>']
60
    ];
61
62
    /**
63
     * {@inheritDoc}
64
     */
65
    public function initialize(array $config)
66
    {
67
        $this->addBehavior(
68
            'Cron.Cron',
69
            [
70
                'registerGc' => [
71
                    'id' => 'User.registerGc',
72
                    'due' => '+1 day',
73
                ],
74
                'userBlockGc' => [
75
                    'id' => 'User.userBlockGc',
76
                    'due' => '+15 minutes',
77
                ]
78
            ]
79
        );
80
81
        $avatarRootDir = Configure::read('Saito.Settings.uploadDirectory');
82
        $this->addBehavior(
83
            'Proffer.Proffer',
84
            [
85
                'avatar' => [ // The name of your upload field (filename)
86
                    'root' => $avatarRootDir,
87
                    'dir' => 'avatar_dir', // field for upload directory
88
                    'thumbnailSizes' => [
89
                        'square' => ['w' => 100, 'h' => 100],
90
                    ],
91
                    // Options are Imagick, Gd or Gmagick
92
                    'thumbnailMethod' => 'Gd'
93
                ]
94
            ]
95
        );
96
        $this->getEventManager()->on(new AvatarFilenameListener($avatarRootDir));
97
98
        $this->hasOne(
99
            'UserOnline',
100
            ['dependent' => true, 'foreignKey' => 'user_id']
101
        );
102
103
        $this->hasMany(
104
            'Bookmarks',
105
            ['foreignKey' => 'user_id', 'dependent' => true]
106
        );
107
        $this->hasMany('Drafts', ['dependent' => true]);
108
        $this->hasMany('UserIgnores', ['foreignKey' => 'user_id']);
109
        $this->hasMany(
110
            'Entries',
111
            [
112
                'foreignKey' => 'user_id',
113
                'conditions' => ['Entries.user_id' => 'Users.id'],
114
            ]
115
        );
116
        $this->hasMany(
117
            'ImageUploader.Uploads',
118
            ['dependent' => true, 'foreignKey' => 'user_id']
119
        );
120
        $this->hasMany(
121
            'UserReads',
122
            ['foreignKey' => 'user_id', 'dependent' => true]
123
        );
124
        $this->hasMany(
125
            'UserBlocks',
126
            [
127
                'foreignKey' => 'user_id',
128
                'dependent' => true,
129
                'sort' => [
130
                    'UserBlocks.ended IS NULL DESC',
131
                    'UserBlocks.ended' => 'DESC',
132
                    'UserBlocks.id' => 'DESC'
133
                ]
134
            ]
135
        );
136
    }
137
138
    /**
139
     * {@inheritDoc}
140
     */
141
    public function validationDefault(Validator $validator)
142
    {
143
        $validator->setProvider(
144
            'saito',
145
            'Saito\Validation\SaitoValidationProvider'
146
        );
147
148
        $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...
149
            ->setProvider(
150
                'proffer',
151
                'Proffer\Model\Validation\ProfferRules'
152
            )
153
            ->allowEmpty('avatar_dir')
154
            ->allowEmpty('avatar')
155
            ->add(
156
                'avatar',
157
                'avatar-extension',
158
                [
159
                    'rule' => ['extension', ['jpg', 'jpeg', 'png']],
160
                    'message' => __('user.avatar.error.extension', ['jpg, jpeg, png'])
161
                ]
162
            )
163
            ->add(
164
                'avatar',
165
                'avatar-size',
166
                [
167
                    'rule' => ['fileSize', Validation::COMPARE_LESS, '3MB'],
168
                    'message' => __('user.avatar.error.size', ['3'])
169
                ]
170
            )
171
            ->add(
172
                'avatar',
173
                'avatar-mime',
174
                [
175
                    'rule' => ['mimetype', ['image/jpeg', 'image/png']],
176
                    'message' => __('user.avatar.error.mime')
177
                ]
178
            )
179
            ->add(
180
                'avatar',
181
                'avatar-dimension',
182
                [
183
                    'rule' => [
184
                        'dimensions',
185
                        [
186
                            'min' => ['w' => 100, 'h' => 100],
187
                            'max' => ['w' => 1500, 'h' => 1500]
188
                        ]
189
                    ],
190
                    'message' => __(
191
                        'user.avatar.error.dimension',
192
                        ['100x100', '1500x1500']
193
                    ),
194
                    'provider' => 'proffer'
195
                ]
196
            );
197
198
        $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...
199
            ->notEmpty('password')
200
            ->add(
201
                'password',
202
                [
203
                    'pwConfirm' => [
204
                        'rule' => [$this, 'validateConfirmPassword'],
205
                        'last' => true,
206
                        'message' => __('error_password_confirm')
207
                    ]
208
                ]
209
            );
210
211
        $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...
212
            ->notEmpty('password_old')
213
            ->add(
214
                'password_old',
215
                [
216
                    'pwCheckOld' => [
217
                        'rule' => [$this, 'validateCheckOldPassword'],
218
                        'last' => true,
219
                        'message' => 'validation_error_pwCheckOld'
220
                    ]
221
                ]
222
            );
223
224
        $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...
225
            ->notEmpty('username', __('error_no_name'))
226
            ->add(
227
                'username',
228
                [
229
                    'isUnique' => [
230
                        'rule' => 'validateIsUniqueCiString',
231
                        'provider' => 'saito',
232
                        'last' => true,
233
                        'message' => __('error_name_reserved')
234
                    ],
235
                    'isUsernameEqual' => [
236
                        'on' => 'create',
237
                        'last' => true,
238
                        'rule' => [$this, 'validateUsernameEqual']
239
                    ],
240
                    'hasAllowedChars' => [
241
                        'rule' => [$this, 'validateHasAllowedChars'],
242
                        'message' => __(
243
                            'model.user.validate.username.hasAllowedChars'
244
                        )
245
                    ],
246
                    'isNotEmoji' => [
247
                        'rule' => 'utf8',
248
                        'message' => __(
249
                            'model.user.validate.username.hasAllowedChars'
250
                        )
251
                    ],
252
                    'maxLength' => [
253
                        'last' => true,
254
                        'message' => __('vld.users.username.maxlength', self::USERNAME_MAXLENGTH),
255
                        'rule' => ['maxLength', self::USERNAME_MAXLENGTH],
256
                    ],
257
                ]
258
            );
259
260
        $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...
261
            ->notEmpty('user_email')
262
            ->add(
263
                'user_email',
264
                [
265
                    'isUnique' => [
266
                        'rule' => 'validateUnique',
267
                        'provider' => 'table',
268
                        'last' => true,
269
                        'message' => __('error_email_reserved')
270
                    ],
271
                    'isEmail' => [
272
                        'rule' => ['email', true],
273
                        'last' => true,
274
                        'message' => __('error_email_wrong')
275
                    ]
276
                ]
277
            );
278
279
        $validator->add(
280
            'user_forum_refresh_time',
281
            [
282
                'numeric' => ['rule' => 'numeric'],
283
                'greaterNull' => ['rule' => ['comparison', '>=', 0]],
284
                'maxLength' => ['rule' => ['maxLength', 3]],
285
            ]
286
        );
287
288
        $validator->add(
289
            'user_type',
290
            [
291
                'allowedType' => [
292
                    'rule' => ['inList', ['user', 'mod', 'admin']]
293
                ]
294
            ]
295
        );
296
297
        $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...
298
299
        $validator->add(
300
            'logins',
301
            ['numeric' => ['rule' => ['numeric']]]
302
        );
303
304
        $validator->add(
305
            'personal_messages',
306
            ['bool' => ['rule' => ['boolean']]]
307
        );
308
309
        $validator->add(
310
            'user_lock',
311
            ['bool' => ['rule' => ['boolean']]]
312
        );
313
314
        $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...
315
            ->notEmpty('activate_code')
316
            ->add(
317
                'activate_code',
318
                [
319
                    'numeric' => ['rule' => ['numeric']],
320
                    'between' => ['rule' => ['range', 0, 9999999]]
321
                ]
322
            );
323
324
        $validator->add(
325
            'user_signatures_hide',
326
            ['bool' => ['rule' => ['boolean']]]
327
        );
328
329
        $validator->add(
330
            'user_signature_images_hide',
331
            ['bool' => ['rule' => ['boolean']]]
332
        );
333
334
        $validator->add(
335
            'user_automaticaly_mark_as_read',
336
            ['bool' => ['rule' => ['boolean']]]
337
        );
338
339
        $validator->add(
340
            'user_sort_last_answer',
341
            ['bool' => ['rule' => ['boolean']]]
342
        );
343
344
        $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...
345
            ->allowEmpty('user_color_new_postings')
346
            ->add(
347
                'user_color_new_postings',
348
                [
349
                    'hexformat' => [
350
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i']
351
                    ]
352
                ]
353
            );
354
        $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...
355
            ->allowEmpty('user_color_old_postings')
356
            ->add(
357
                'user_color_old_postings',
358
                [
359
                    'hexformat' => [
360
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i']
361
                    ]
362
                ]
363
            );
364
        $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...
365
            ->allowEmpty('user_color_actual_posting')
366
            ->add(
367
                'user_color_actual_posting',
368
                [
369
                    'hexformat' => [
370
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i']
371
                    ]
372
                ]
373
            );
374
375
        return $validator;
376
    }
377
378
    /**
379
     * {@inheritDoc}
380
     */
381
    protected function _initializeSchema(TableSchema $table)
382
    {
383
        $table->setColumnType('avatar', 'proffer.file');
384
        $table->setColumnType('user_category_custom', 'serialize');
385
386
        return $table;
387
    }
388
389
    /**
390
     * set last refresh
391
     *
392
     * @param int $userId user-ID
393
     * @param DateTimeInterface|null $lastRefresh last refresh
394
     * @return void
395
     */
396
    public function setLastRefresh(int $userId, DateTimeInterface $lastRefresh = null)
397
    {
398
        Stopwatch::start('Users->setLastRefresh()');
399
        $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...
400
401
        if ($lastRefresh) {
402
            $data['last_refresh'] = $lastRefresh;
403
        }
404
405
        $this->query()
406
            ->update()
407
            ->set($data)
408
            ->where(['id' => $userId])
409
            ->execute();
410
411
        Stopwatch::end('Users->setLastRefresh()');
412
    }
413
414
    /**
415
     * Increment logins
416
     *
417
     * @param Entity $user user
418
     * @param int $amount amount
419
     * @return void
420
     * @throws \Exception
421
     */
422
    public function incrementLogins(Entity $user, $amount = 1)
423
    {
424
        $data = [
425
            'logins' => $user->get('logins') + $amount,
426
            'last_login' => bDate()
427
        ];
428
        $this->patchEntity($user, $data);
429
        if (!$this->save($user)) {
430
            throw new \Exception('Increment logins failed.');
431
        }
432
    }
433
434
    /**
435
     * get userlist
436
     *
437
     * @return array
438
     */
439
    public function userlist()
440
    {
441
        return $this->find(
442
            'list',
443
            ['keyField' => 'id', 'valueField' => 'username']
444
        )->toArray();
445
    }
446
447
    /**
448
     * Removes a user and all his data execpt for his entries
449
     *
450
     * @param int $userId user-ID
451
     * @return bool
452
     */
453
    public function deleteAllExceptEntries(int $userId)
454
    {
455
        if ($userId == 1) {
456
            return false;
457
        }
458
        $user = $this->get($userId);
459
        if (!$user) {
460
            return false;
461
        }
462
463
        try {
464
            $this->Entries->anonymizeEntriesFromUser($userId);
465
            $this->UserIgnores->deleteUser($userId);
466
            $this->delete($user);
467
        } catch (\Exception $e) {
468
            return false;
469
        }
470
        $this->_dispatchEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
471
472
        return true;
473
    }
474
475
    /**
476
     * Updates the hashed password if hash-algo is out-of-date
477
     *
478
     * @param int $userId user-ID
479
     * @param string $password password
480
     * @return void
481
     */
482
    public function autoUpdatePassword(int $userId, string $password): void
483
    {
484
        $user = $this->get($userId, ['fields' => ['id', 'password']]);
485
        $oldPassword = $user->get('password');
486
        $needsRehash = $this->getPasswordHasher()->needsRehash($oldPassword);
487
        if ($needsRehash) {
488
            $user->set('password', $password);
489
            $this->save($user);
490
        }
491
    }
492
493
    /**
494
     * Post processing when updating a username.
495
     *
496
     * @param Entity $entity The updated entity.
497
     * @return void
498
     */
499
    protected function updateUsername(Entity $entity)
500
    {
501
        // Using associating with $this->Entries->updateAll() not working in
502
        // Cake 3.8.
503
        $Entries = TableRegistry::getTableLocator()->get('Entries');
504
        $Entries->updateAll(
505
            ['name' => $entity->get('username')],
506
            ['user_id' => $entity->get('id')]
507
        );
508
509
        $Entries->updateAll(
510
            ['edited_by' => $entity->get('username')],
511
            ['edited_by' => $entity->getOriginal('username')]
512
        );
513
514
        $this->_dispatchEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
515
    }
516
517
    /**
518
     * {@inheritDoc}
519
     */
520
    public function afterSave(Event $event, Entity $entity, \ArrayObject $options)
521
    {
522
        if ($entity->isDirty('username')) {
523
            $this->updateUsername($entity);
524
        }
525
    }
526
527
    /**
528
     * {@inheritDoc}
529
     */
530
    public function beforeSave(
531
        Event $event,
532
        Entity $entity,
533
        \ArrayObject $options
534
    ) {
535
        if ($entity->isDirty('password')) {
536
            $hashedPassword = $this->getPasswordHasher()->hash($entity->get('password'));
537
            $entity->set('password', $hashedPassword);
538
        }
539
    }
540
541
    /**
542
     * {@inheritDoc}
543
     */
544
    public function beforeValidate(
545
        Event $event,
546
        Entity $entity,
547
        \ArrayObject $options,
548
        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...
549
    ) {
550
        if ($entity->isDirty('user_forum_refresh_time')) {
551
            $time = $entity->get('user_forum_refresh_time');
552
            if (empty($time)) {
553
                $entity->set('user_forum_refresh_time', 0);
554
            }
555
        }
556
    }
557
558
    /**
559
     * validate old password
560
     *
561
     * @param string $value value
562
     * @param array $context context
563
     * @return bool
564
     */
565
    public function validateCheckOldPassword($value, array $context)
566
    {
567
        $userId = $context['data']['id'];
568
        $oldPasswordHash = $this->get($userId, ['fields' => ['password']])
569
            ->get('password');
570
571
        return $this->getPasswordHasher()->check($value, $oldPasswordHash);
572
    }
573
574
    /**
575
     * validate confirm password
576
     *
577
     * @param string $value value
578
     * @param array $context context
579
     * @return bool
580
     */
581
    public function validateConfirmPassword($value, array $context)
582
    {
583
        if ($value === $context['data']['password_confirm']) {
584
            return true;
585
        }
586
587
        return false;
588
    }
589
590
    /**
591
     * Validate allowed chars
592
     *
593
     * @param string $value value
594
     * @param array $context context
595
     * @return bool
596
     */
597
    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...
598
    {
599
        foreach ($this->getConfig('user_name_disallowed_chars') as $char) {
600
            if (mb_strpos($value, $char) !== false) {
601
                return false;
602
            }
603
        }
604
605
        return true;
606
    }
607
608
    /**
609
     * checks if equal username exists
610
     *
611
     * @param string $value value
612
     * @param array $context context
613
     * @return bool|string
614
     */
615
    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...
616
    {
617
        Stopwatch::start('validateUsernameEqual');
618
        $users = $this->userlist();
619
        $lc = mb_strtolower($value);
620
        foreach ($users as $name) {
621
            if ($name === $value) {
622
                continue;
623
            }
624
            $name = mb_strtolower($name);
625
            $distance = levenshtein($lc, $name);
626
            if ($distance < 2) {
627
                return __('error.name.equalExists', $name);
628
            }
629
        }
630
        Stopwatch::stop('validateUsernameEqual');
631
632
        return true;
633
    }
634
635
    /**
636
     * Registers new user
637
     *
638
     * @param array $data data
639
     * @param bool $activate activate
640
     * @return EntityInterface
641
     */
642
    public function register($data, $activate = false): EntityInterface
643
    {
644
        $defaults = [
645
            'registered' => bDate(),
646
            'user_type' => 'user'
647
        ];
648
        $fields = [
649
            'password',
650
            'registered',
651
            'user_email',
652
            'user_type',
653
            'username'
654
        ];
655
656
        if ($activate !== true) {
657
            $defaults['activate_code'] = mt_rand(1000000, 9999999);
658
            $fields[] = 'activate_code';
659
        }
660
661
        $data = array_merge($data, $defaults);
662
663
        $fieldFilter = (new FieldFilter())->setConfig('register', $fields);
664
        if (!$fieldFilter->requireFields($data, 'register')) {
665
            throw new \RuntimeException(
666
                'Required fields for registration were not provided.',
667
                1563789683
668
            );
669
        }
670
671
        $user = $this->newEntity($data, ['fields' => $fields]);
672
        $errors = $user->getErrors();
673
        if (!empty($errors)) {
674
            return $user;
675
        }
676
        $this->save($user);
677
678
        return $user;
679
    }
680
681
    /**
682
     * Garbage collection for registration
683
     *
684
     * Deletes all timed out and unactivated registrations
685
     *
686
     * @return void
687
     */
688
    public function registerGc()
689
    {
690
        $this->deleteAll(
691
            [
692
                'activate_code >' => 0,
693
                'registered <' => bDate(time() - 86400)
694
            ]
695
        );
696
    }
697
698
    /**
699
     * calls garbage collection for UserBlock
700
     *
701
     * UserBlock is lazy-loaded rarely and gc may not trigger often enough (at
702
     * least with manual blocking and ignore blocking only)
703
     *
704
     * @return void
705
     */
706
    public function userBlockGc()
707
    {
708
        $this->UserBlocks->gc();
709
    }
710
711
    /**
712
     * activates user
713
     *
714
     * @param int $userId user-ID
715
     * @param string $code activation code
716
     * @return array|bool false if activation failed; array with status and
717
     *     user data on success
718
     * @throws \InvalidArgumentException
719
     */
720
    public function activate($userId, $code)
721
    {
722
        if (!is_int($userId) || !is_string($code)) {
723
            throw new \InvalidArgumentException();
724
        }
725
726
        try {
727
            $user = $this->get($userId);
728
        } catch (RecordNotFoundException $e) {
729
            throw new \InvalidArgumentException();
730
        }
731
732
        $activateCode = strval($user->get('activate_code'));
733
734
        if (empty($activateCode)) {
735
            return ['status' => 'already', 'User' => $user];
736
        } elseif ($activateCode !== $code) {
737
            return false;
738
        }
739
740
        $user->set('activate_code', 0);
741
        $success = $this->save($user);
742
        if (empty($success)) {
743
            return false;
744
        }
745
746
        $this->_dispatchEvent('Model.User.afterActivate', ['User' => $user]);
747
748
        return ['status' => 'activated', 'User' => $user];
749
    }
750
751
    /**
752
     * Count solved posting for a user.
753
     *
754
     *
755
     * @param int $userId user-ID
756
     * @return int count
757
     */
758
    public function countSolved($userId)
759
    {
760
        $count = $this->find()
761
            ->select(['Users.id'])
762
            ->where(['Users.id' => $userId])
763
            ->join(
764
                [
765
                    'Entries' => [
766
                        'table' => $this->Entries->getTable(),
767
                        'type' => 'INNER',
768
                        'conditions' => [
769
                            [
770
                                'Entries.solves >' => '0',
771
                                'Entries.user_id' => $userId
772
                            ]
773
                        ],
774
                    ],
775
                    'Root' => [
776
                        'table' => $this->Entries->getTable(),
777
                        'type' => 'INNER',
778
                        // Don't answers to own question.
779
                        'conditions' => [
780
                            'Root.id = Entries.solves',
781
                            'Root.user_id != Users.id',
782
                        ]
783
                    ]
784
                ]
785
            );
786
787
        return $count->count();
788
    }
789
790
    /**
791
     * Set view categories preferences
792
     *
793
     * ## $category
794
     *
795
     * - 'all': set to all categories
796
     * - array: (cat_id1 => true|1|'1', cat_id2 => true|1|'1')
797
     * - int: set to single category_id
798
     *
799
     * @param int $userId user-ID
800
     * @param string|int|array $category category
801
     * @return void
802
     * @throws \InvalidArgumentException
803
     */
804
    public function setCategory($userId, $category)
805
    {
806
        $User = $this->find()->select(['id' => $userId])->first();
807
        if (!$User) {
808
            throw new \InvalidArgumentException(
809
                "Can't find user with id $userId.",
810
                1420807691
811
            );
812
        }
813
814
        if ($category === 'all') {
815
            //=if show all cateogries
816
            $active = -1;
817
        } elseif (is_array($category)) {
818
            //=if set a custom set of categories
819
            $active = 0;
820
821
            $availableCats = $this->Entries->Categories->find('list')->toArray(
822
            );
823
            $categories = array_intersect_key($category, $availableCats);
824
            if (count($categories) === 0) {
825
                throw new \InvalidArgumentException();
826
            }
827
            $newCats = [];
828
            foreach ($categories as $cat => $v) {
829
                $newCats[$cat] = ($v === true || $v === 1 || $v === '1');
830
            }
831
            $User->set('user_category_custom', $newCats);
832
        } else {
833
            //=if set a single category
834
            $category = (int)$category;
835
            if ($category > 0 && $this->Entries->Categories->exists((int)$category)
836
            ) {
837
                $active = $category;
838
            } else {
839
                throw new \InvalidArgumentException();
840
            }
841
        }
842
843
        $User->set('user_category_active', $active);
844
        $this->save($User);
0 ignored issues
show
Bug introduced by
It seems like $User defined by $this->find()->select(ar...' => $userId))->first() on line 806 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...
845
    }
846
847
    /**
848
     * Get default password hasher for hashing user passwords.
849
     *
850
     * @return PasswordHasherInterface
851
     */
852
    public function getPasswordHasher(): PasswordHasherInterface
853
    {
854
        return PasswordHasherFactory::build(DefaultPasswordHasher::class);
855
    }
856
857
    /**
858
     * Finds a user with additional profil informations from associated tables
859
     *
860
     * @param Query $query query
861
     * @param array $options options
862
     * @return Query
863
     */
864
    public function findProfile(Query $query, array $options): Query
865
    {
866
        $query
867
            // ->enableHydration(false)
868
            ->contain(
869
                [
870
                    'UserIgnores' => function ($query) {
871
                        return $query->enableHydration(false)->select(
872
                            ['blocked_user_id', 'user_id']
873
                        );
874
                    }
875
                ]
876
            );
877
878
        return $query;
879
    }
880
881
    /**
882
     * Find all sorted by username
883
     *
884
     * @param Query $query query
885
     * @param array $options options
886
     * @return Query
887
     */
888
    public function findPaginated(Query $query, array $options)
889
    {
890
        $query
891
            ->contain(['UserOnline'])
892
            ->order(['Users.username' => 'ASC']);
893
894
        return $query;
895
    }
896
897
    /**
898
     * Find the latest, successfully registered user
899
     *
900
     * @param Query $query query
901
     * @param array $options options
902
     * @return Query
903
     */
904
    public function findLatest(Query $query, array $options)
905
    {
906
        $query->where(['activate_code' => 0])
907
            ->order(['id' => 'DESC'])
908
            ->limit(1);
909
910
        return $query;
911
    }
912
}
913