Completed
Branch feature/currentUserRefactoring (c13c1d)
by Schlaefer
04:13
created

UsersTable   F

Complexity

Total Complexity 59

Size/Duplication

Total Lines 879
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 21

Importance

Changes 0
Metric Value
dl 0
loc 879
rs 3.801
c 0
b 0
f 0
wmc 59
lcom 3
cbo 21

27 Methods

Rating   Name   Duplication   Size   Complexity  
A autoUpdatePassword() 0 10 2
B initialize() 0 72 1
B validationDefault() 0 236 1
A _initializeSchema() 0 7 1
A setLastRefresh() 0 17 2
A incrementLogins() 0 11 2
A userlist() 0 7 1
A deleteAllExceptEntries() 0 18 3
A updateUsername() 0 17 1
A afterSave() 0 6 2
A beforeSave() 0 10 2
A beforeValidate() 0 13 3
A validateCheckOldPassword() 0 8 1
A validateConfirmPassword() 0 8 2
A validateHasAllowedChars() 0 10 3
A validateUserRoleExists() 0 11 2
A validateUsernameEqual() 0 19 4
A register() 0 38 4
A registerGc() 0 9 1
A userBlockGc() 0 4 1
A activate() 0 26 5
A countSolved() 0 31 1
B setCategory() 0 42 10
A getPasswordHasher() 0 4 1
A findProfile() 0 16 1
A findPaginated() 0 8 1
A findLatest() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like UsersTable often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UsersTable, and based on these observations, apply Extract Interface, too.

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