Passed
Push — master ( 29358d...2ff380 )
by Darko
06:09
created

User::getRange()   B

Complexity

Conditions 7
Paths 2

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 16
dl 0
loc 33
rs 8.8333
c 1
b 1
f 0
cc 7
nc 2
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace App\Models;
4
5
use App\Jobs\SendAccountExpiredEmail;
6
use App\Jobs\SendAccountWillExpireEmail;
7
use App\Jobs\SendInviteEmail;
8
use Carbon\CarbonImmutable;
9
use DariusIII\Token\Facades\Token;
10
use Illuminate\Database\Eloquent\Builder;
11
use Illuminate\Database\Eloquent\Collection;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, App\Models\Collection. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\ModelNotFoundException;
14
use Illuminate\Database\Eloquent\Relations\BelongsTo;
15
use Illuminate\Database\Eloquent\Relations\HasMany;
16
use Illuminate\Database\Eloquent\Relations\HasOne;
17
use Illuminate\Foundation\Auth\User as Authenticatable;
18
use Illuminate\Http\Request;
19
use Illuminate\Notifications\Notifiable;
20
use Illuminate\Support\Arr;
21
use Illuminate\Support\Carbon;
22
use Illuminate\Support\Facades\Hash;
23
use Illuminate\Support\Facades\Password;
24
use Illuminate\Support\Facades\Validator;
25
use Illuminate\Support\Str;
26
use Jrean\UserVerification\Traits\UserVerification;
27
use Junaidnasir\Larainvite\Facades\Invite;
28
use Junaidnasir\Larainvite\InviteTrait;
29
use Spatie\Permission\Models\Role;
30
use Spatie\Permission\Traits\HasRoles;
0 ignored issues
show
Bug introduced by
The type Spatie\Permission\Traits\HasRoles 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...
31
32
/**
33
 * App\Models\User.
34
 *
35
 * App\Models\User.
36
 *
37
 * @property int $id
38
 * @property string $username
39
 * @property string|null $firstname
40
 * @property string|null $lastname
41
 * @property string $email
42
 * @property string $password
43
 * @property int $user_roles_id FK to roles.id
44
 * @property string|null $host
45
 * @property int $grabs
46
 * @property string $rsstoken
47
 * @property \Carbon\Carbon|null $created_at
48
 * @property \Carbon\Carbon|null $updated_at
49
 * @property string|null $resetguid
50
 * @property string|null $lastlogin
51
 * @property string|null $apiaccess
52
 * @property int $invites
53
 * @property int|null $invitedby
54
 * @property int $movieview
55
 * @property int $xxxview
56
 * @property int $musicview
57
 * @property int $consoleview
58
 * @property int $bookview
59
 * @property int $gameview
60
 * @property string|null $saburl
61
 * @property string|null $sabapikey
62
 * @property bool|null $sabapikeytype
63
 * @property bool|null $sabpriority
64
 * @property string|null $nzbgeturl
65
 * @property string|null $nzbgetusername
66
 * @property string|null $nzbgetpassword
67
 * @property string|null $nzbvortex_api_key
68
 * @property string|null $nzbvortex_server_url
69
 * @property string $userseed
70
 * @property string $notes
71
 * @property string|null $cp_url
72
 * @property string|null $cp_api
73
 * @property string|null $style
74
 * @property string|null $rolechangedate When does the role expire
75
 * @property string|null $remember_token
76
 * @property-read Collection|\App\Models\ReleaseComment[] $comment
77
 * @property-read Collection|\App\Models\UserDownload[] $download
78
 * @property-read Collection|\App\Models\DnzbFailure[] $failedRelease
79
 * @property-read Collection|\App\Models\Invitation[] $invitation
80
 * @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
81
 * @property-read Collection|\App\Models\UsersRelease[] $release
82
 * @property-read Collection|\App\Models\UserRequest[] $request
83
 * @property-read Collection|\App\Models\UserSerie[] $series
84
 *
85
 * @method static Builder|\App\Models\User whereApiaccess($value)
86
 * @method static Builder|\App\Models\User whereBookview($value)
87
 * @method static Builder|\App\Models\User whereConsoleview($value)
88
 * @method static Builder|\App\Models\User whereCpApi($value)
89
 * @method static Builder|\App\Models\User whereCpUrl($value)
90
 * @method static Builder|\App\Models\User whereCreatedAt($value)
91
 * @method static Builder|\App\Models\User whereEmail($value)
92
 * @method static Builder|\App\Models\User whereFirstname($value)
93
 * @method static Builder|\App\Models\User whereGameview($value)
94
 * @method static Builder|\App\Models\User whereGrabs($value)
95
 * @method static Builder|\App\Models\User whereHost($value)
96
 * @method static Builder|\App\Models\User whereId($value)
97
 * @method static Builder|\App\Models\User whereInvitedby($value)
98
 * @method static Builder|\App\Models\User whereInvites($value)
99
 * @method static Builder|\App\Models\User whereLastlogin($value)
100
 * @method static Builder|\App\Models\User whereLastname($value)
101
 * @method static Builder|\App\Models\User whereMovieview($value)
102
 * @method static Builder|\App\Models\User whereMusicview($value)
103
 * @method static Builder|\App\Models\User whereNotes($value)
104
 * @method static Builder|\App\Models\User whereNzbgetpassword($value)
105
 * @method static Builder|\App\Models\User whereNzbgeturl($value)
106
 * @method static Builder|\App\Models\User whereNzbgetusername($value)
107
 * @method static Builder|\App\Models\User whereNzbvortexApiKey($value)
108
 * @method static Builder|\App\Models\User whereNzbvortexServerUrl($value)
109
 * @method static Builder|\App\Models\User wherePassword($value)
110
 * @method static Builder|\App\Models\User whereRememberToken($value)
111
 * @method static Builder|\App\Models\User whereResetguid($value)
112
 * @method static Builder|\App\Models\User whereRolechangedate($value)
113
 * @method static Builder|\App\Models\User whereRsstoken($value)
114
 * @method static Builder|\App\Models\User whereSabapikey($value)
115
 * @method static Builder|\App\Models\User whereSabapikeytype($value)
116
 * @method static Builder|\App\Models\User whereSabpriority($value)
117
 * @method static Builder|\App\Models\User whereSaburl($value)
118
 * @method static Builder|\App\Models\User whereStyle($value)
119
 * @method static Builder|\App\Models\User whereUpdatedAt($value)
120
 * @method static Builder|\App\Models\User whereUserRolesId($value)
121
 * @method static Builder|\App\Models\User whereUsername($value)
122
 * @method static Builder|\App\Models\User whereUserseed($value)
123
 * @method static Builder|\App\Models\User whereXxxview($value)
124
 * @method static Builder|\App\Models\User whereVerified($value)
125
 * @method static Builder|\App\Models\User whereApiToken($value)
126
 *
127
 * @mixin \Eloquent
128
 *
129
 * @property int $roles_id FK to roles.id
130
 * @property string $api_token
131
 * @property int $rate_limit
132
 * @property string|null $email_verified_at
133
 * @property int $verified
134
 * @property string|null $verification_token
135
 * @property-read Collection|\Junaidnasir\Larainvite\Models\LaraInviteModel[] $invitationPending
136
 * @property-read Collection|\Junaidnasir\Larainvite\Models\LaraInviteModel[] $invitationSuccess
137
 * @property-read Collection|\Junaidnasir\Larainvite\Models\LaraInviteModel[] $invitations
138
 * @property-read Collection|\Spatie\Permission\Models\Permission[] $permissions
139
 * @property-read Role $role
140
 * @property-read Collection|\Spatie\Permission\Models\Role[] $roles
141
 *
142
 * @method static Builder|\App\Models\User newModelQuery()
143
 * @method static Builder|\App\Models\User newQuery()
144
 * @method static Builder|\App\Models\User permission($permissions)
145
 * @method static Builder|\App\Models\User query()
146
 * @method static Builder|\App\Models\User whereEmailVerifiedAt($value)
147
 * @method static Builder|\App\Models\User whereRateLimit($value)
148
 * @method static Builder|\App\Models\User whereRolesId($value)
149
 * @method static Builder|\App\Models\User whereVerificationToken($value)
150
 */
151
class User extends Authenticatable
152
{
153
    use HasRoles, InviteTrait, Notifiable, UserVerification;
154
155
    public const ERR_SIGNUP_BADUNAME = -1;
156
157
    public const ERR_SIGNUP_BADPASS = -2;
158
159
    public const ERR_SIGNUP_BADEMAIL = -3;
160
161
    public const ERR_SIGNUP_UNAMEINUSE = -4;
162
163
    public const ERR_SIGNUP_EMAILINUSE = -5;
164
165
    public const ERR_SIGNUP_BADINVITECODE = -6;
166
167
    public const SUCCESS = 1;
168
169
    public const ROLE_USER = 1;
170
171
    public const ROLE_ADMIN = 2;
172
173
    /**
174
     * Users SELECT queue type.
175
     */
176
    public const QUEUE_NONE = 0;
177
178
    public const QUEUE_SABNZBD = 1;
179
180
    public const QUEUE_NZBGET = 2;
181
182
    /**
183
     * @var string
184
     */
185
186
    /**
187
     * @var bool
188
     */
189
    protected $dateFormat = false;
190
191
    /**
192
     * @var array
193
     */
194
    protected $hidden = ['remember_token', 'password'];
195
196
    /**
197
     * @var array
198
     */
199
    protected $guarded = [];
200
201
    protected function getDefaultGuardName(): string
202
    {
203
        return 'web';
204
    }
205
206
    public function role(): BelongsTo
207
    {
208
        return $this->belongsTo(Role::class, 'roles_id');
209
    }
210
211
    public function request(): HasMany
212
    {
213
        return $this->hasMany(UserRequest::class, 'users_id');
214
    }
215
216
    public function download(): HasMany
217
    {
218
        return $this->hasMany(UserDownload::class, 'users_id');
219
    }
220
221
    public function release(): HasMany
222
    {
223
        return $this->hasMany(UsersRelease::class, 'users_id');
224
    }
225
226
    public function series(): HasMany
227
    {
228
        return $this->hasMany(UserSerie::class, 'users_id');
229
    }
230
231
    public function invitation(): HasMany
232
    {
233
        return $this->hasMany(Invitation::class, 'users_id');
234
    }
235
236
    public function failedRelease(): HasMany
237
    {
238
        return $this->hasMany(DnzbFailure::class, 'users_id');
239
    }
240
241
    public function comment(): HasMany
242
    {
243
        return $this->hasMany(ReleaseComment::class, 'users_id');
244
    }
245
246
    /**
247
     * @throws \Exception
248
     */
249
    public static function deleteUser($id): void
250
    {
251
        self::find($id)->delete();
252
    }
253
254
    public static function getCount(?string $role = null, ?string $username = '', ?string $host = '', ?string $email = ''): int
255
    {
256
        $res = self::query()->where('email', '<>', '[email protected]');
257
258
        if (! empty($role)) {
259
            $res->where('roles_id', $role);
260
        }
261
262
        if ($username !== '') {
263
            $res->where('username', 'like', '%'.$username.'%');
264
        }
265
266
        if ($host !== '') {
267
            $res->where('host', 'like', '%'.$host.'%');
268
        }
269
270
        if ($email !== '') {
271
            $res->where('email', 'like', '%'.$email.'%');
272
        }
273
274
        return $res->count(['id']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $res->count(array('id')) could return the type Illuminate\Database\Eloquent\Builder which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
275
    }
276
277
    public static function updateUser(int $id, string $userName, ?string $email, int $grabs, int $role, ?string $notes, int $invites, int $movieview, int $musicview, int $gameview, int $xxxview, int $consoleview, int $bookview, string $style = 'None'): int
278
    {
279
        $userName = trim($userName);
280
281
        $rateLimit = Role::query()->where('id', $role)->first();
282
283
        $sql = [
284
            'username' => $userName,
285
            'grabs' => $grabs,
286
            'roles_id' => $role,
287
            'notes' => substr($notes, 0, 255),
0 ignored issues
show
Bug introduced by
It seems like $notes can also be of type null; however, parameter $string of substr() does only seem to accept string, 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

287
            'notes' => substr(/** @scrutinizer ignore-type */ $notes, 0, 255),
Loading history...
288
            'invites' => $invites,
289
            'movieview' => $movieview,
290
            'musicview' => $musicview,
291
            'gameview' => $gameview,
292
            'xxxview' => $xxxview,
293
            'consoleview' => $consoleview,
294
            'bookview' => $bookview,
295
            'style' => $style,
296
            'rate_limit' => $rateLimit ? $rateLimit['rate_limit'] : 60,
297
        ];
298
299
        if (! empty($email)) {
300
            $email = trim($email);
301
            $sql += ['email' => $email];
302
        }
303
304
        $user = self::find($id);
305
        $user->update($sql);
306
        $user->syncRoles([$rateLimit['name']]);
307
308
        return self::SUCCESS;
309
    }
310
311
    /**
312
     * @return User|Builder|Model|object|null
313
     */
314
    public static function getByUsername(string $userName)
315
    {
316
        return self::whereUsername($userName)->first();
317
    }
318
319
    /**
320
     * @return Model|static
321
     *
322
     * @throws ModelNotFoundException
323
     */
324
    public static function getByEmail(string $email)
325
    {
326
        return self::whereEmail($email)->first();
327
    }
328
329
    public static function updateUserRole(int $uid, int|string $role): bool
330
    {
331
        if (is_int($role)) {
332
            $roleQuery = Role::query()->where('id', $role)->first();
333
        } else {
334
            $roleQuery = Role::query()->where('name', $role)->first();
335
        }
336
        $roleName = $roleQuery->name;
337
338
        $user = self::find($uid);
339
        $user->syncRoles([$roleName]);
340
341
        return self::find($uid)->update(['roles_id' => $roleQuery->id]);
342
    }
343
344
    public static function updateUserRoleChangeDate(int $uid, $date = '', int $addYear = 0): void
345
    {
346
        $user = self::find($uid);
347
        $currRoleExp = $user->rolechangedate ?? now()->toDateTimeString();
348
        if (! empty($date)) {
349
            $user->update(['rolechangedate' => $date]);
350
        }
351
        if (empty($date) && ! empty($addYear)) {
352
            $user->update(['rolechangedate' => Carbon::createFromDate($currRoleExp)->addYears($addYear)]);
0 ignored issues
show
Bug introduced by
It seems like $currRoleExp can also be of type string; however, parameter $year of Carbon\Carbon::createFromDate() does only seem to accept integer|null, 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

352
            $user->update(['rolechangedate' => Carbon::createFromDate(/** @scrutinizer ignore-type */ $currRoleExp)->addYears($addYear)]);
Loading history...
353
        }
354
    }
355
356
    public static function updateExpiredRoles(): void
357
    {
358
        $now = CarbonImmutable::now();
359
        $period = [
360
            'day' => $now->addDay(),
361
            'week' => $now->addWeek(),
362
            'month' => $now->addMonth(),
363
        ];
364
365
        foreach ($period as $value) {
366
            $users = self::query()->whereDate('rolechangedate', '=', $value)->get();
367
            $days = $now->diffInDays($value);
368
            foreach ($users as $user) {
369
                SendAccountWillExpireEmail::dispatch($user, $days)->onQueue('emails');
370
            }
371
        }
372
        foreach (self::query()->whereDate('rolechangedate', '<', $now)->get() as $expired) {
373
            $expired->update(['roles_id' => self::ROLE_USER, 'rolechangedate' => null]);
374
            $expired->syncRoles(['User']);
375
            SendAccountExpiredEmail::dispatch($expired)->onQueue('emails');
376
        }
377
    }
378
379
    /**
380
     * @throws \Throwable
381
     */
382
    public static function getRange($start, $offset, $orderBy, ?string $userName = '', ?string $email = '', ?string $host = '', ?string $role = '', bool $apiRequests = false): Collection
383
    {
384
        if ($apiRequests) {
385
            UserRequest::clearApiRequests(false);
386
            $query = "
387
				SELECT users.*, roles.name AS rolename, COUNT(user_requests.id) AS apirequests
388
				FROM users
389
				INNER JOIN roles ON roles.id = users.roles_id
390
				LEFT JOIN user_requests ON user_requests.users_id = users.id
391
				WHERE users.id != 0 %s %s %s %s
392
				AND email != '[email protected]'
393
				GROUP BY users.id
394
				ORDER BY %s %s %s ";
395
        } else {
396
            $query = '
397
				SELECT users.*, roles.name AS rolename
398
				FROM users
399
				INNER JOIN roles ON roles.id = users.roles_id
400
				WHERE 1=1 %s %s %s %s
401
				ORDER BY %s %s %s';
402
        }
403
        $order = self::getBrowseOrder($orderBy);
404
405
        return self::fromQuery(
406
            sprintf(
407
                $query,
408
                ! empty($userName) ? 'AND users.username '.'LIKE '.escapeString('%'.$userName.'%') : '',
409
                ! empty($email) ? 'AND users.email '.'LIKE '.escapeString('%'.$email.'%') : '',
410
                ! empty($host) ? 'AND users.host '.'LIKE '.escapeString('%'.$host.'%') : '',
411
                (! empty($role) ? ('AND users.roles_id = '.$role) : ''),
412
                $order[0],
413
                $order[1],
414
                ($start === false ? '' : ('LIMIT '.$offset.' OFFSET '.$start))
415
            )
416
        );
417
    }
418
419
    /**
420
     * Get sort types for sorting users on the web page user list.
421
     *
422
     * @return string[]
423
     */
424
    public static function getBrowseOrder($orderBy): array
425
    {
426
        $order = (empty($orderBy) ? 'username_desc' : $orderBy);
427
        $orderArr = explode('_', $order);
428
        $orderField = match ($orderArr[0]) {
429
            'email' => 'email',
430
            'host' => 'host',
431
            'createdat' => 'created_at',
432
            'lastlogin' => 'lastlogin',
433
            'apiaccess' => 'apiaccess',
434
            'grabs' => 'grabs',
435
            'role' => 'rolename',
436
            'rolechangedate' => 'rolechangedate',
437
            'verification' => 'verified',
438
            default => 'username',
439
        };
440
        $orderSort = (isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1])) ? $orderArr[1] : 'desc';
441
442
        return [$orderField, $orderSort];
443
    }
444
445
    /**
446
     * Verify a password against a hash.
447
     *
448
     * Automatically update the hash if it needs to be.
449
     *
450
     * @param  string  $password  Password to check against hash.
451
     * @param  bool|string  $hash  Hash to check against password.
452
     * @param  int  $userID  ID of the user.
453
     */
454
    public static function checkPassword(string $password, bool|string $hash, int $userID = -1): bool
455
    {
456
        if (Hash::check($password, $hash) === false) {
457
            return false;
458
        }
459
460
        // Update the hash if it needs to be.
461
        if (is_numeric($userID) && $userID > 0 && Hash::needsRehash($hash)) {
462
            $hash = self::hashPassword($password);
463
464
            if ($hash !== false) {
0 ignored issues
show
introduced by
The condition $hash !== false is always true.
Loading history...
465
                self::find($userID)->update(['password' => $hash]);
466
            }
467
        }
468
469
        return true;
470
    }
471
472
    public static function updateRssKey($uid): int
473
    {
474
        self::find($uid)->update(['api_token' => md5(Password::getRepository()->createNewToken())]);
0 ignored issues
show
Bug introduced by
The method createNewToken() does not exist on Illuminate\Auth\Passwords\TokenRepositoryInterface. Did you maybe mean create()? ( Ignorable by Annotation )

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

474
        self::find($uid)->update(['api_token' => md5(Password::getRepository()->/** @scrutinizer ignore-call */ createNewToken())]);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
475
476
        return self::SUCCESS;
477
    }
478
479
    public static function updatePassResetGuid($id, $guid): int
480
    {
481
        self::find($id)->update(['resetguid' => $guid]);
482
483
        return self::SUCCESS;
484
    }
485
486
    public static function updatePassword(int $id, string $password): int
487
    {
488
        self::find($id)->update(['password' => self::hashPassword($password), 'userseed' => md5(Str::uuid()->toString())]);
489
490
        return self::SUCCESS;
491
    }
492
493
    public static function hashPassword($password): string
494
    {
495
        return Hash::make($password);
496
    }
497
498
    /**
499
     * @return Model|static
500
     *
501
     * @throws ModelNotFoundException
502
     */
503
    public static function getByPassResetGuid(string $guid)
504
    {
505
        return self::whereResetguid($guid)->first();
506
    }
507
508
    public static function incrementGrabs(int $id, int $num = 1): void
509
    {
510
        self::find($id)->increment('grabs', $num);
511
    }
512
513
    /**
514
     * @return Model|null|static
515
     */
516
    public static function getByRssToken(string $rssToken)
517
    {
518
        return self::whereApiToken($rssToken)->first();
519
    }
520
521
    public static function isValidUrl($url): bool
522
    {
523
        return (! preg_match('/^(http|https|ftp):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?/i', $url)) ? false : true;
524
    }
525
526
    /**
527
     * @throws \Exception
528
     */
529
    public static function generatePassword(int $length = 15): string
530
    {
531
        return Token::random($length, true);
532
    }
533
534
    /**
535
     * @throws \Exception
536
     */
537
    public static function signUp($userName, $password, $email, $host, $notes, int $invites = Invitation::DEFAULT_INVITES, string $inviteCode = '', bool $forceInviteMode = false, int $role = self::ROLE_USER, bool $validate = true): bool|int|string
538
    {
539
        $user = [
540
            'username' => trim($userName),
541
            'password' => trim($password),
542
            'email' => trim($email),
543
        ];
544
545
        if ($validate) {
546
            $validator = Validator::make($user, [
547
                'username' => ['required', 'string', 'min:5', 'max:255', 'unique:users'],
548
                'email' => ['required', 'string', 'email', 'max:255', 'unique:users', 'indisposable'],
549
                'password' => ['required', 'string', 'min:8', 'confirmed', 'regex:/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/'],
550
            ]);
551
552
            if ($validator->fails()) {
553
                return implode('', Arr::collapse($validator->errors()->toArray()));
554
            }
555
        }
556
557
        // Make sure this is the last check, as if a further validation check failed, the invite would still have been used up.
558
        $invitedBy = 0;
559
        if (! $forceInviteMode && (int) Settings::settingValue('..registerstatus') === Settings::REGISTER_STATUS_INVITE) {
560
            if ($inviteCode === '') {
561
                return self::ERR_SIGNUP_BADINVITECODE;
562
            }
563
564
            $invitedBy = self::checkAndUseInvite($inviteCode);
565
            if ($invitedBy < 0) {
566
                return self::ERR_SIGNUP_BADINVITECODE;
567
            }
568
        }
569
570
        return self::add($user['username'], $user['password'], $user['email'], $role, $notes, $host, $invites, $invitedBy);
571
    }
572
573
    /**
574
     * If a invite is used, decrement the person who invited's invite count.
575
     */
576
    public static function checkAndUseInvite(string $inviteCode): int
577
    {
578
        $invite = Invitation::getInvite($inviteCode);
579
        if (! $invite) {
580
            return -1;
581
        }
582
583
        self::query()->where('id', $invite['users_id'])->decrement('invites');
584
        Invitation::deleteInvite($inviteCode);
585
586
        return $invite['users_id'];
587
    }
588
589
    /**
590
     * @return false|int|mixed
591
     */
592
    public static function add(string $userName, string $password, string $email, int $role, ?string $notes = '', string $host = '', int $invites = Invitation::DEFAULT_INVITES, int $invitedBy = 0)
593
    {
594
        $password = self::hashPassword($password);
595
        if (! $password) {
596
            return false;
597
        }
598
599
        $storeips = (int) Settings::settingValue('..storeuserips') === 1 ? $host : '';
600
601
        $user = self::create(
602
            [
603
                'username' => $userName,
604
                'password' => $password,
605
                'email' => $email,
606
                'host' => $storeips,
607
                'roles_id' => $role,
608
                'invites' => $invites,
609
                'invitedby' => (int) $invitedBy === 0 ? null : $invitedBy,
610
                'notes' => $notes,
611
            ]
612
        );
613
614
        return $user->id;
615
    }
616
617
    /**
618
     * Get the list of categories the user has excluded.
619
     *
620
     * @param  int  $userID  ID of the user.
621
     *
622
     * @throws \Exception
623
     */
624
    public static function getCategoryExclusionById(int $userID): array
625
    {
626
        $ret = [];
627
628
        $user = self::find($userID);
629
630
        $userAllowed = $user->getDirectPermissions()->pluck('name')->toArray();
631
        $roleAllowed = $user->getAllPermissions()->pluck('name')->toArray();
632
633
        $allowed = array_intersect($roleAllowed, $userAllowed);
634
635
        $cats = ['view console', 'view movies', 'view audio', 'view tv', 'view pc', 'view adult', 'view books', 'view other'];
636
637
        if (! empty($allowed)) {
638
            foreach ($cats as $cat) {
639
                if (! \in_array($cat, $allowed, false)) {
640
                    $ret[] = match ($cat) {
641
                        'view console' => 1000,
642
                        'view movies' => 2000,
643
                        'view audio' => 3000,
644
                        'view pc' => 4000,
645
                        'view tv' => 5000,
646
                        'view adult' => 6000,
647
                        'view books' => 7000,
648
                        'view other' => 1,
649
                    };
650
                }
651
            }
652
        }
653
654
        return Category::query()->whereIn('root_categories_id', $ret)->pluck('id')->toArray();
655
    }
656
657
    /**
658
     * @throws \Exception
659
     */
660
    public static function getCategoryExclusionForApi(Request $request): array
661
    {
662
        $apiToken = $request->has('api_token') ? $request->input('api_token') : $request->input('apikey');
663
        $user = self::getByRssToken($apiToken);
664
665
        return self::getCategoryExclusionById($user->id);
666
    }
667
668
    /**
669
     * @return Collection|\Illuminate\Support\Collection|static[]
670
     */
671
    public static function getTopGrabbers()
672
    {
673
        return self::query()->selectRaw('id, username, SUM(grabs) as grabs')->groupBy('id', 'username')->having('grabs', '>', 0)->orderByDesc('grabs')->limit(10)->get();
674
    }
675
676
    /**
677
     * @return Collection|\Illuminate\Support\Collection|static[]
678
     */
679
    public static function getUsersByMonth()
680
    {
681
        return self::query()->whereNotNull('created_at')->where('created_at', '<>', '0000-00-00 00:00:00')->selectRaw("DATE_FORMAT(created_at, '%M %Y') as mth, COUNT(id) as num")->groupBy(['mth'])->orderByDesc('created_at')->get();
682
    }
683
684
    /**
685
     * @throws \Exception
686
     */
687
    public static function sendInvite($serverUrl, $uid, $emailTo): string
688
    {
689
        $user = self::find($uid);
690
        $token = Invite::invite($emailTo, $user->id);
691
        $url = $serverUrl.'/register?invitecode='.$token;
692
693
        Invitation::addInvite($uid, $token);
694
        SendInviteEmail::dispatch($emailTo, $user, $url)->onQueue('emails');
695
696
        return $url;
697
    }
698
699
    /**
700
     * Deletes users that have not verified their accounts for 3 or more days.
701
     */
702
    public static function deleteUnVerified(): void
703
    {
704
        static::whereVerified(0)->where('created_at', '<', now()->subDays(3))->delete();
705
    }
706
707
    public function passwordSecurity(): HasOne
708
    {
709
        return $this->hasOne(PasswordSecurity::class);
710
    }
711
}
712