Completed
Push — feature/6.x ( 4633a7...5f23eb )
by Schlaefer
04:09 queued 10s
created

UsersTable   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 887
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 385
c 4
b 0
f 0
dl 0
loc 887
rs 3.6
wmc 60

27 Methods

Rating   Name   Duplication   Size   Complexity  
A validateUserRoleExists() 0 10 2
A updateUsername() 0 16 1
A findLatest() 0 7 1
B validationDefault() 0 235 1
A validateConfirmPassword() 0 7 2
A beforeSave() 0 8 2
A beforeValidate() 0 10 3
A setLastRefresh() 0 16 2
A validateHasAllowedChars() 0 9 3
A countSolved() 0 30 1
A registerGc() 0 6 1
A autoUpdatePassword() 0 8 2
A findPaginated() 0 7 1
A validateCheckOldPassword() 0 7 1
A initialize() 0 68 1
A userlist() 0 6 1
A register() 0 43 5
A deleteAllExceptEntries() 0 17 3
A getPasswordHasher() 0 3 1
A incrementLogins() 0 9 2
A validateUsernameEqual() 0 18 4
B setCategory() 0 42 10
A afterSave() 0 4 2
A findProfile() 0 15 1
A activate() 0 28 5
A userBlockGc() 0 3 1
A _initializeSchema() 0 6 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.

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
declare(strict_types=1);
3
4
/**
5
 * Saito - The Threaded Web Forum
6
 *
7
 * @copyright Copyright (c) the Saito Project Developers
8
 * @link https://github.com/Schlaefer/Saito
9
 * @license http://opensource.org/licenses/MIT
10
 */
11
12
namespace App\Model\Table;
13
14
use App\Lib\Model\Table\AppTable;
15
use App\Lib\Model\Table\FieldFilter;
16
use Authentication\PasswordHasher\DefaultPasswordHasher;
17
use Authentication\PasswordHasher\PasswordHasherFactory;
18
use Authentication\PasswordHasher\PasswordHasherInterface;
19
use Cake\Core\Configure;
20
use Cake\Database\Schema\TableSchemaInterface;
21
use Cake\Datasource\EntityInterface;
22
use Cake\Datasource\Exception\RecordNotFoundException;
23
use Cake\Event\Event;
24
use Cake\ORM\Entity;
25
use Cake\ORM\Query;
26
use Cake\ORM\TableRegistry;
27
use Cake\Validation\Validator;
28
use DateTimeInterface;
29
use Saito\App\Registry;
30
use Stopwatch\Lib\Stopwatch;
31
32
/**
33
 * Users table
34
 *
35
 * @property \App\Model\Table\EntriesTable $Entries
36
 * @property \App\Model\Table\UserBlocksTable $UserBlocks
37
 * @property \App\Model\Table\UserIgnoresTable $UserIgnores
38
 * @property \App\Model\Table\UserOnlineTable $UserOnline
39
 */
40
class UsersTable extends AppTable
41
{
42
    /**
43
     * Max lenght for username.
44
     *
45
     * Constrained to 191 due to InnoDB index max-length on MySQL 5.6.
46
     */
47
    public const USERNAME_MAXLENGTH = 191;
48
49
    /**
50
     * {@inheritDoc}
51
     */
52
    protected $_defaultConfig = [
53
        'user_name_disallowed_chars' => ['\'', ';', '&', '<', '>'],
54
    ];
55
56
    /**
57
     * {@inheritDoc}
58
     */
59
    public function initialize(array $config): void
60
    {
61
        $this->addBehavior(
62
            'Cron.Cron',
63
            [
64
                'registerGc' => [
65
                    'id' => 'User.registerGc',
66
                    'due' => '+1 day',
67
                ],
68
                'userBlockGc' => [
69
                    'id' => 'User.userBlockGc',
70
                    'due' => '+15 minutes',
71
                ],
72
            ]
73
        );
74
75
        $avatarRootDir = Configure::read('Saito.Settings.uploadDirectory');
76
        $this->addBehavior(
77
            'Proffer.Proffer',
78
            [
79
                'avatar' => [ // The name of your upload field (filename)
80
                    'root' => $avatarRootDir,
81
                    'dir' => 'avatar_dir', // field for upload directory
82
                    'thumbnailSizes' => [
83
                        'square' => ['w' => 100, 'h' => 100],
84
                    ],
85
                    // Options are Imagick, Gd or Gmagick
86
                    'thumbnailMethod' => 'Gd',
87
                ],
88
            ]
89
        );
90
        $this->getEventManager()->on(new AvatarFilenameListener($avatarRootDir));
0 ignored issues
show
Bug introduced by
The type App\Model\Table\AvatarFilenameListener was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
91
92
        $this->hasOne(
93
            'UserOnline',
94
            ['dependent' => true, 'foreignKey' => 'user_id']
95
        );
96
97
        $this->hasMany(
98
            'Bookmarks',
99
            ['foreignKey' => 'user_id', 'dependent' => true]
100
        );
101
        $this->hasMany('Drafts', ['dependent' => true]);
102
        $this->hasMany('UserIgnores', ['foreignKey' => 'user_id']);
103
        $this->hasMany(
104
            'Entries',
105
            [
106
                'foreignKey' => 'user_id',
107
                'conditions' => ['Entries.user_id' => 'Users.id'],
108
            ]
109
        );
110
        $this->hasMany(
111
            'ImageUploader.Uploads',
112
            ['dependent' => true, 'foreignKey' => 'user_id']
113
        );
114
        $this->hasMany(
115
            'UserReads',
116
            ['foreignKey' => 'user_id', 'dependent' => true]
117
        );
118
        $this->hasMany(
119
            'UserBlocks',
120
            [
121
                'foreignKey' => 'user_id',
122
                'dependent' => true,
123
                'sort' => [
124
                    'UserBlocks.ended IS NULL DESC',
125
                    'UserBlocks.ended' => 'DESC',
126
                    'UserBlocks.id' => 'DESC',
127
                ],
128
            ]
129
        );
130
    }
131
132
    /**
133
     * {@inheritDoc}
134
     */
135
    public function validationDefault(Validator $validator): \Cake\Validation\Validator
136
    {
137
        $validator->setProvider(
138
            'saito',
139
            'Saito\Validation\SaitoValidationProvider'
140
        );
141
142
        $validator
0 ignored issues
show
Deprecated Code introduced by
The function Cake\Validation\Validator::allowEmpty() has been deprecated: 3.7.0 Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(), allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

142
        /** @scrutinizer ignore-deprecated */ $validator

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

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

Loading history...
143
            ->setProvider(
144
                'proffer',
145
                'Proffer\Model\Validation\ProfferRules'
146
            )
147
            ->allowEmpty('avatar_dir')
148
            ->allowEmpty('avatar')
149
            ->add(
150
                'avatar',
151
                'avatar-extension',
152
                [
153
                    'rule' => ['extension', ['jpg', 'jpeg', 'png']],
154
                    'message' => __('user.avatar.error.extension', ['jpg, jpeg, png']),
155
                ]
156
            )
157
            ->add(
158
                'avatar',
159
                'avatar-size',
160
                [
161
                    'rule' => ['fileSize', Validation::COMPARE_LESS, '3MB'],
0 ignored issues
show
Bug introduced by
The type App\Model\Table\Validation was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
162
                    'message' => __('user.avatar.error.size', ['3']),
163
                ]
164
            )
165
            ->add(
166
                'avatar',
167
                'avatar-mime',
168
                [
169
                    'rule' => ['mimetype', ['image/jpeg', 'image/png']],
170
                    'message' => __('user.avatar.error.mime'),
171
                ]
172
            )
173
            ->add(
174
                'avatar',
175
                'avatar-dimension',
176
                [
177
                    'rule' => [
178
                        'dimensions',
179
                        [
180
                            'min' => ['w' => 100, 'h' => 100],
181
                            'max' => ['w' => 1500, 'h' => 1500],
182
                        ],
183
                    ],
184
                    'message' => __(
185
                        'user.avatar.error.dimension',
186
                        ['100x100', '1500x1500']
187
                    ),
188
                    'provider' => 'proffer',
189
                ]
190
            );
191
192
        $validator
193
            ->notEmptyString('password')
194
            ->add(
195
                'password',
196
                [
197
                    'pwConfirm' => [
198
                        'rule' => [$this, 'validateConfirmPassword'],
199
                        'last' => true,
200
                        'message' => __('error_password_confirm'),
201
                    ],
202
                ]
203
            );
204
205
        $validator
206
            ->notEmptyString('password_old')
207
            ->add(
208
                'password_old',
209
                [
210
                    'pwCheckOld' => [
211
                        'rule' => [$this, 'validateCheckOldPassword'],
212
                        'last' => true,
213
                        'message' => 'validation_error_pwCheckOld',
214
                    ],
215
                ]
216
            );
217
218
        $validator
219
            ->notEmptyString('username', __('error_no_name'))
220
            ->add(
221
                'username',
222
                [
223
                    'isUnique' => [
224
                        'rule' => 'validateIsUniqueCiString',
225
                        'provider' => 'saito',
226
                        'last' => true,
227
                        'message' => __('error_name_reserved'),
228
                    ],
229
                    'isUsernameEqual' => [
230
                        'on' => 'create',
231
                        'last' => true,
232
                        'rule' => [$this, 'validateUsernameEqual'],
233
                    ],
234
                    'hasAllowedChars' => [
235
                        'rule' => [$this, 'validateHasAllowedChars'],
236
                        'message' => __(
237
                            'model.user.validate.username.hasAllowedChars'
238
                        ),
239
                    ],
240
                    'isNotEmoji' => [
241
                        'rule' => 'utf8',
242
                        'message' => __(
243
                            'model.user.validate.username.hasAllowedChars'
244
                        ),
245
                    ],
246
                    'maxLength' => [
247
                        'last' => true,
248
                        'message' => __('vld.users.username.maxlength', self::USERNAME_MAXLENGTH),
249
                        'rule' => ['maxLength', self::USERNAME_MAXLENGTH],
250
                    ],
251
                ]
252
            );
253
254
        $validator
255
            ->notEmptyString('user_email')
256
            ->add(
257
                'user_email',
258
                [
259
                    'isUnique' => [
260
                        'rule' => 'validateUnique',
261
                        'provider' => 'table',
262
                        'last' => true,
263
                        'message' => __('error_email_reserved'),
264
                    ],
265
                    'isEmail' => [
266
                        'rule' => ['email', true],
267
                        'last' => true,
268
                        'message' => __('error_email_wrong'),
269
                    ],
270
                ]
271
            );
272
273
        $validator->add(
274
            'user_forum_refresh_time',
275
            [
276
                'numeric' => ['rule' => 'numeric'],
277
                'greaterNull' => ['rule' => ['comparison', '>=', 0]],
278
                'maxLength' => ['rule' => ['maxLength', 3]],
279
            ]
280
        );
281
282
        $validator->add(
283
            'user_type',
284
            [
285
                'allowedType' => [
286
                    'rule' => [$this, 'validateUserRoleExists'],
287
                ],
288
            ]
289
        );
290
291
        $validator->notEmptyDateTime('registered');
292
293
        $validator->add(
294
            'logins',
295
            ['numeric' => ['rule' => ['numeric']]]
296
        );
297
298
        $validator->add(
299
            'personal_messages',
300
            ['bool' => ['rule' => ['boolean']]]
301
        );
302
303
        $validator->add(
304
            'user_lock',
305
            ['bool' => ['rule' => ['boolean']]]
306
        );
307
308
        $validator
309
            ->notEmptyString('activate_code')
310
            ->add(
311
                'activate_code',
312
                [
313
                    'numeric' => ['rule' => ['numeric']],
314
                    'between' => ['rule' => ['range', 0, 9999999]],
315
                ]
316
            );
317
318
        $validator->add(
319
            'user_signatures_hide',
320
            ['bool' => ['rule' => ['boolean']]]
321
        );
322
323
        $validator->add(
324
            'user_signature_images_hide',
325
            ['bool' => ['rule' => ['boolean']]]
326
        );
327
328
        $validator->add(
329
            'user_automaticaly_mark_as_read',
330
            ['bool' => ['rule' => ['boolean']]]
331
        );
332
333
        $validator->add(
334
            'user_sort_last_answer',
335
            ['bool' => ['rule' => ['boolean']]]
336
        );
337
338
        $validator
339
            ->allowEmptyString('user_color_new_postings')
340
            ->add(
341
                'user_color_new_postings',
342
                [
343
                    'hexformat' => [
344
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i'],
345
                    ],
346
                ]
347
            );
348
        $validator
349
            ->allowEmptyString('user_color_old_postings')
350
            ->add(
351
                'user_color_old_postings',
352
                [
353
                    'hexformat' => [
354
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i'],
355
                    ],
356
                ]
357
            );
358
        $validator
359
            ->allowEmptyString('user_color_actual_posting')
360
            ->add(
361
                'user_color_actual_posting',
362
                [
363
                    'hexformat' => [
364
                        'rule' => ['custom', '/^#?[a-f0-9]{0,6}$/i'],
365
                    ],
366
                ]
367
            );
368
369
        return $validator;
370
    }
371
372
    /**
373
     * {@inheritDoc}
374
     */
375
    protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface
376
    {
377
        $table->setColumnType('avatar', 'proffer.file');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $table seems to be never defined.
Loading history...
378
        $table->setColumnType('user_category_custom', 'serialize');
379
380
        return $table;
381
    }
382
383
    /**
384
     * set last refresh
385
     *
386
     * @param int $userId user-ID
387
     * @param \DateTimeInterface|null $lastRefresh last refresh
388
     * @return void
389
     */
390
    public function setLastRefresh(int $userId, ?DateTimeInterface $lastRefresh = null)
391
    {
392
        Stopwatch::start('Users->setLastRefresh()');
393
        $data['last_refresh_tmp'] = bDate();
0 ignored issues
show
Comprehensibility Best Practice introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.
Loading history...
394
395
        if ($lastRefresh) {
396
            $data['last_refresh'] = $lastRefresh;
397
        }
398
399
        $this->query()
400
            ->update()
401
            ->set($data)
402
            ->where(['id' => $userId])
403
            ->execute();
404
405
        Stopwatch::end('Users->setLastRefresh()');
406
    }
407
408
    /**
409
     * Increment logins
410
     *
411
     * @param \Cake\ORM\Entity $user user
412
     * @param int $amount amount
413
     * @return void
414
     * @throws \Exception
415
     */
416
    public function incrementLogins(Entity $user, $amount = 1)
417
    {
418
        $data = [
419
            'logins' => $user->get('logins') + $amount,
420
            'last_login' => bDate(),
421
        ];
422
        $this->patchEntity($user, $data);
423
        if (!$this->save($user)) {
424
            throw new \Exception('Increment logins failed.');
425
        }
426
    }
427
428
    /**
429
     * get userlist
430
     *
431
     * @return array
432
     */
433
    public function userlist()
434
    {
435
        return $this->find(
436
            'list',
437
            ['keyField' => 'id', 'valueField' => 'username']
438
        )->toArray();
439
    }
440
441
    /**
442
     * Removes a user and all his data execpt for his entries
443
     *
444
     * @param int $userId user-ID
445
     * @return bool
446
     */
447
    public function deleteAllExceptEntries(int $userId)
448
    {
449
        $user = $this->get($userId);
450
        if (empty($user)) {
451
            return false;
452
        }
453
454
        try {
455
            $this->Entries->anonymizeEntriesFromUser($userId);
456
            $this->UserIgnores->deleteUser($userId);
457
            $this->delete($user);
458
        } catch (\Exception $e) {
459
            return false;
460
        }
461
        $this->dispatchDbEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
462
463
        return true;
464
    }
465
466
    /**
467
     * Updates the hashed password if hash-algo is out-of-date
468
     *
469
     * @param int $userId user-ID
470
     * @param string $password password
471
     * @return void
472
     */
473
    public function autoUpdatePassword(int $userId, string $password): void
474
    {
475
        $user = $this->get($userId, ['fields' => ['id', 'password']]);
476
        $oldPassword = $user->get('password');
477
        $needsRehash = $this->getPasswordHasher()->needsRehash($oldPassword);
478
        if ($needsRehash) {
479
            $user->set('password', $password);
480
            $this->save($user);
481
        }
482
    }
483
484
    /**
485
     * Post processing when updating a username.
486
     *
487
     * @param \Cake\ORM\Entity $entity The updated entity.
488
     * @return void
489
     */
490
    protected function updateUsername(Entity $entity)
491
    {
492
        // Using associating with $this->Entries->updateAll() not working in
493
        // Cake 3.8.
494
        $Entries = TableRegistry::getTableLocator()->get('Entries');
495
        $Entries->updateAll(
496
            ['name' => $entity->get('username')],
497
            ['user_id' => $entity->get('id')]
498
        );
499
500
        $Entries->updateAll(
501
            ['edited_by' => $entity->get('username')],
502
            ['edited_by' => $entity->getOriginal('username')]
503
        );
504
505
        $this->dispatchDbEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
506
    }
507
508
    /**
509
     * {@inheritDoc}
510
     */
511
    public function afterSave(\Cake\Event\EventInterface $event, Entity $entity, \ArrayObject $options)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

511
    public function afterSave(/** @scrutinizer ignore-unused */ \Cake\Event\EventInterface $event, Entity $entity, \ArrayObject $options)

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

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

511
    public function afterSave(\Cake\Event\EventInterface $event, Entity $entity, /** @scrutinizer ignore-unused */ \ArrayObject $options)

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

Loading history...
512
    {
513
        if ($entity->isDirty('username')) {
514
            $this->updateUsername($entity);
515
        }
516
    }
517
518
    /**
519
     * {@inheritDoc}
520
     */
521
    public function beforeSave(
522
        \Cake\Event\EventInterface $event,
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

522
        /** @scrutinizer ignore-unused */ \Cake\Event\EventInterface $event,

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

Loading history...
523
        Entity $entity,
524
        \ArrayObject $options
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

524
        /** @scrutinizer ignore-unused */ \ArrayObject $options

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

Loading history...
525
    ) {
526
        if ($entity->isDirty('password')) {
527
            $hashedPassword = $this->getPasswordHasher()->hash((string)$entity->get('password'));
528
            $entity->set('password', $hashedPassword);
529
        }
530
    }
531
532
    /**
533
     * {@inheritDoc}
534
     */
535
    public function beforeValidate(
536
        Event $event,
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

536
        /** @scrutinizer ignore-unused */ Event $event,

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

Loading history...
537
        Entity $entity,
538
        \ArrayObject $options,
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

538
        /** @scrutinizer ignore-unused */ \ArrayObject $options,

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

Loading history...
539
        Validator $validator
0 ignored issues
show
Unused Code introduced by
The parameter $validator is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

539
        /** @scrutinizer ignore-unused */ Validator $validator

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

Loading history...
540
    ) {
541
        if ($entity->isDirty('user_forum_refresh_time')) {
542
            $time = $entity->get('user_forum_refresh_time');
543
            if (empty($time)) {
544
                $entity->set('user_forum_refresh_time', 0);
545
            }
546
        }
547
    }
548
549
    /**
550
     * validate old password
551
     *
552
     * @param string $value value
553
     * @param array $context context
554
     * @return bool
555
     */
556
    public function validateCheckOldPassword($value, array $context)
557
    {
558
        $userId = $context['data']['id'];
559
        $oldPasswordHash = $this->get($userId, ['fields' => ['password']])
560
            ->get('password');
561
562
        return $this->getPasswordHasher()->check((string)$value, $oldPasswordHash);
563
    }
564
565
    /**
566
     * validate confirm password
567
     *
568
     * @param string $value value
569
     * @param array $context context
570
     * @return bool
571
     */
572
    public function validateConfirmPassword($value, array $context)
573
    {
574
        if ($value === $context['data']['password_confirm']) {
575
            return true;
576
        }
577
578
        return false;
579
    }
580
581
    /**
582
     * Validate allowed chars
583
     *
584
     * @param string $value value
585
     * @param array $context context
586
     * @return bool
587
     */
588
    public function validateHasAllowedChars($value, array $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

588
    public function validateHasAllowedChars($value, /** @scrutinizer ignore-unused */ array $context)

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

Loading history...
589
    {
590
        foreach ($this->getConfig('user_name_disallowed_chars') as $char) {
591
            if (mb_strpos($value, $char) !== false) {
592
                return false;
593
            }
594
        }
595
596
        return true;
597
    }
598
599
    /**
600
     * Check if the role exists
601
     *
602
     * @param string $value value
603
     * @param array $context context
604
     * @return bool|string
605
     */
606
    public function validateUserRoleExists($value, array $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

606
    public function validateUserRoleExists($value, /** @scrutinizer ignore-unused */ array $context)

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

Loading history...
607
    {
608
        /** @var \Saito\User\Permission\Permissions $Permissions */
609
        $Permissions = Registry::get('Permissions');
610
        $roles = array_column($Permissions->getRoles()->getAvailable(), 'type');
611
        if (in_array($value, $roles)) {
612
            return true;
613
        }
614
615
        return __('vld.user.user_type.allowedType', h($value));
616
    }
617
618
    /**
619
     * checks if equal username exists
620
     *
621
     * @param string $value value
622
     * @param array $context context
623
     * @return bool|string
624
     */
625
    public function validateUsernameEqual($value, array $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

625
    public function validateUsernameEqual($value, /** @scrutinizer ignore-unused */ array $context)

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

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

860
        $this->save(/** @scrutinizer ignore-type */ $User);
Loading history...
861
    }
862
863
    /**
864
     * Get default password hasher for hashing user passwords.
865
     *
866
     * @return \Authentication\PasswordHasher\PasswordHasherInterface
867
     */
868
    public function getPasswordHasher(): PasswordHasherInterface
869
    {
870
        return PasswordHasherFactory::build(DefaultPasswordHasher::class);
871
    }
872
873
    /**
874
     * Finds a user with additional profil informations from associated tables
875
     *
876
     * @param \Cake\ORM\Query $query query
877
     * @param array $options options
878
     * @return \Cake\ORM\Query
879
     */
880
    public function findProfile(Query $query, array $options): Query
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

880
    public function findProfile(Query $query, /** @scrutinizer ignore-unused */ array $options): Query

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

Loading history...
881
    {
882
        $query
883
            // ->enableHydration(false)
884
            ->contain(
885
                [
886
                    'UserIgnores' => function ($query) {
887
                        return $query->enableHydration(false)->select(
888
                            ['blocked_user_id', 'user_id']
889
                        );
890
                    },
891
                ]
892
            );
893
894
        return $query;
895
    }
896
897
    /**
898
     * Find all sorted by username
899
     *
900
     * @param \Cake\ORM\Query $query query
901
     * @param array $options options
902
     * @return \Cake\ORM\Query
903
     */
904
    public function findPaginated(Query $query, array $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

904
    public function findPaginated(Query $query, /** @scrutinizer ignore-unused */ array $options)

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

Loading history...
905
    {
906
        $query
907
            ->contain(['UserOnline'])
908
            ->order(['Users.username' => 'ASC']);
909
910
        return $query;
911
    }
912
913
    /**
914
     * Find the latest, successfully registered user
915
     *
916
     * @param \Cake\ORM\Query $query query
917
     * @param array $options options
918
     * @return \Cake\ORM\Query
919
     */
920
    public function findLatest(Query $query, array $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

920
    public function findLatest(Query $query, /** @scrutinizer ignore-unused */ array $options)

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

Loading history...
921
    {
922
        $query->where(['activate_code' => 0])
923
            ->order(['id' => 'DESC'])
924
            ->limit(1);
925
926
        return $query;
927
    }
928
}
929