Issues (326)

src/Model/Table/UsersTable.php (26 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Saito - The Threaded Web Forum
7
 *
8
 * @copyright Copyright (c) the Saito Project Developers
9
 * @link https://github.com/Schlaefer/Saito
10
 * @license http://opensource.org/licenses/MIT
11
 */
12
13
namespace App\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 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

149
        /** @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...
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 function Cake\Validation\Validator::notEmpty() has been deprecated: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead. ( Ignorable by Annotation )

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

199
        /** @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...
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 function Cake\Validation\Validator::notEmpty() has been deprecated: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead. ( Ignorable by Annotation )

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

212
        /** @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...
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 function Cake\Validation\Validator::notEmpty() has been deprecated: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead. ( Ignorable by Annotation )

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

225
        /** @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...
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 function Cake\Validation\Validator::notEmpty() has been deprecated: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead. ( Ignorable by Annotation )

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

261
        /** @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...
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 function Cake\Validation\Validator::notEmpty() has been deprecated: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead. ( Ignorable by Annotation )

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

298
        /** @scrutinizer ignore-deprecated */ $validator->notEmpty('registered');

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...
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 function Cake\Validation\Validator::notEmpty() has been deprecated: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead. ( Ignorable by Annotation )

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

315
        /** @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...
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 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

345
        /** @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...
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 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

355
        /** @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...
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 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

365
        /** @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...
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
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...
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->dispatchDbEvent('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->dispatchDbEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
513
    }
514
515
    /**
516
     * {@inheritDoc}
517
     */
518
    public function afterSave(Event $event, Entity $entity, \ArrayObject $options)
0 ignored issues
show
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

518
    public function afterSave(Event $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...
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

518
    public function afterSave(/** @scrutinizer ignore-unused */ Event $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...
519
    {
520
        if ($entity->isDirty('username')) {
521
            $this->updateUsername($entity);
522
        }
523
    }
524
525
    /**
526
     * {@inheritDoc}
527
     */
528
    public function beforeSave(
529
        Event $event,
0 ignored issues
show
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

529
        /** @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...
530
        Entity $entity,
531
        \ArrayObject $options
0 ignored issues
show
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

531
        /** @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...
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,
0 ignored issues
show
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

543
        /** @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...
544
        Entity $entity,
545
        \ArrayObject $options,
0 ignored issues
show
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

545
        /** @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...
546
        Validator $validator
0 ignored issues
show
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

546
        /** @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...
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
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

595
    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...
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
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

613
    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...
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
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

632
    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...
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
        $user = $this->save($user);
694
        if ($user !== false) {
695
            $this->dispatchDbEvent('saito.core.user.register.after', [
696
                'subject' => $user,
697
                'table' => $this,
698
            ]);
699
        }
700
701
        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...
702
    }
703
704
    /**
705
     * Garbage collection for registration
706
     *
707
     * Deletes all timed out and unactivated registrations
708
     *
709
     * @return void
710
     */
711
    public function registerGc()
712
    {
713
        $this->deleteAll(
714
            [
715
                'activate_code >' => 0,
716
                'registered <' => bDate(time() - 86400),
717
            ]
718
        );
719
    }
720
721
    /**
722
     * calls garbage collection for UserBlock
723
     *
724
     * UserBlock is lazy-loaded rarely and gc may not trigger often enough (at
725
     * least with manual blocking and ignore blocking only)
726
     *
727
     * @return void
728
     */
729
    public function userBlockGc()
730
    {
731
        $this->UserBlocks->gc();
732
    }
733
734
    /**
735
     * activates user
736
     *
737
     * @param int $userId user-ID
738
     * @param string $code activation code
739
     * @return array|bool false if activation failed; array with status and
740
     *     user data on success
741
     * @throws \InvalidArgumentException
742
     */
743
    public function activate(int $userId, string $code)
744
    {
745
        try {
746
            $user = $this->get($userId);
747
        } catch (RecordNotFoundException $e) {
748
            throw new \InvalidArgumentException();
749
        }
750
751
        $activateCode = strval($user->get('activate_code'));
752
753
        if (empty($activateCode)) {
754
            return ['status' => 'already', 'User' => $user];
755
        } elseif ($activateCode !== $code) {
756
            return false;
757
        }
758
759
        $user->set('activate_code', 0);
760
        $user = $this->save($user);
761
        if ($user === false) {
762
            return false;
763
        }
764
765
        $this->dispatchDbEvent('saito.core.user.activate.after', [
766
            'subject' => $user,
767
            'table' => $this,
768
        ]);
769
770
        return ['status' => 'activated', 'User' => $user];
771
    }
772
773
    /**
774
     * Count solved posting for a user.
775
     *
776
     *
777
     * @param int $userId user-ID
778
     * @return int count
779
     */
780
    public function countSolved($userId)
781
    {
782
        $count = $this->find()
783
            ->select(['Users.id'])
784
            ->where(['Users.id' => $userId])
785
            ->join(
786
                [
787
                    'Entries' => [
788
                        'table' => $this->Entries->getTable(),
789
                        'type' => 'INNER',
790
                        'conditions' => [
791
                            [
792
                                'Entries.solves >' => '0',
793
                                'Entries.user_id' => $userId,
794
                            ],
795
                        ],
796
                    ],
797
                    'Root' => [
798
                        'table' => $this->Entries->getTable(),
799
                        'type' => 'INNER',
800
                        // Don't answers to own question.
801
                        'conditions' => [
802
                            'Root.id = Entries.solves',
803
                            'Root.user_id != Users.id',
804
                        ],
805
                    ],
806
                ]
807
            );
808
809
        return $count->count();
810
    }
811
812
    /**
813
     * Set view categories preferences
814
     *
815
     * ## $category
816
     *
817
     * - 'all': set to all categories
818
     * - array: (cat_id1 => true|1|'1', cat_id2 => true|1|'1')
819
     * - int: set to single category_id
820
     *
821
     * @param int $userId user-ID
822
     * @param string|int|array $category category
823
     * @return void
824
     * @throws \InvalidArgumentException
825
     */
826
    public function setCategory($userId, $category)
827
    {
828
        $User = $this->find()->select(['id' => $userId])->first();
829
        if (!$User) {
830
            throw new \InvalidArgumentException(
831
                "Can't find user with id $userId.",
832
                1420807691
833
            );
834
        }
835
836
        if ($category === 'all') {
837
            //=if show all cateogries
838
            $active = -1;
839
        } elseif (is_array($category)) {
840
            //=if set a custom set of categories
841
            $active = 0;
842
843
            $availableCats = $this->Entries->Categories->find('list')->toArray(
844
            );
845
            $categories = array_intersect_key($category, $availableCats);
846
            if (count($categories) === 0) {
847
                throw new \InvalidArgumentException();
848
            }
849
            $newCats = [];
850
            foreach ($categories as $cat => $v) {
851
                $newCats[$cat] = ($v === true || $v === 1 || $v === '1');
852
            }
853
            $User->set('user_category_custom', $newCats);
854
        } else {
855
            //=if set a single category
856
            $category = (int)$category;
857
            if (
858
                $category > 0 && $this->Entries->Categories->exists((int)$category)
859
            ) {
860
                $active = $category;
861
            } else {
862
                throw new \InvalidArgumentException();
863
            }
864
        }
865
866
        $User->set('user_category_active', $active);
867
        $this->save($User);
0 ignored issues
show
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

867
        $this->save(/** @scrutinizer ignore-type */ $User);
Loading history...
868
    }
869
870
    /**
871
     * Get default password hasher for hashing user passwords.
872
     *
873
     * @return PasswordHasherInterface
874
     */
875
    public function getPasswordHasher(): PasswordHasherInterface
876
    {
877
        return PasswordHasherFactory::build(DefaultPasswordHasher::class);
878
    }
879
880
    /**
881
     * Finds a user with additional profil informations from associated tables
882
     *
883
     * @param Query $query query
884
     * @param array $options options
885
     * @return Query
886
     */
887
    public function findProfile(Query $query, array $options): Query
0 ignored issues
show
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

887
    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...
888
    {
889
        $query
890
            // ->enableHydration(false)
891
            ->contain(
892
                [
893
                    'UserIgnores' => function ($query) {
894
                        return $query->enableHydration(false)->select(
895
                            ['blocked_user_id', 'user_id']
896
                        );
897
                    },
898
                ]
899
            );
900
901
        return $query;
902
    }
903
904
    /**
905
     * Find all sorted by username
906
     *
907
     * @param Query $query query
908
     * @param array $options options
909
     * @return Query
910
     */
911
    public function findPaginated(Query $query, array $options)
0 ignored issues
show
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

911
    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...
912
    {
913
        $query
914
            ->contain(['UserOnline'])
915
            ->order(['Users.username' => 'ASC']);
916
917
        return $query;
918
    }
919
920
    /**
921
     * Find the latest, successfully registered user
922
     *
923
     * @param Query $query query
924
     * @param array $options options
925
     * @return Query
926
     */
927
    public function findLatest(Query $query, array $options)
0 ignored issues
show
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

927
    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...
928
    {
929
        $query->where(['activate_code' => 0])
930
            ->order(['id' => 'DESC'])
931
            ->limit(1);
932
933
        return $query;
934
    }
935
}
936