Passed
Push — master ( cc786a...5052e6 )
by Darko
07:18 queued 12s
created

User::release()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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;
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 \Illuminate\Database\Eloquent\Collection|\App\Models\ReleaseComment[] $comment
77
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UserDownload[] $download
78
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\DnzbFailure[] $failedRelease
79
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Invitation[] $invitation
80
 * @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
81
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UsersRelease[] $release
82
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UserRequest[] $request
83
 * @property-read \Illuminate\Database\Eloquent\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 \Illuminate\Database\Eloquent\Collection|\Junaidnasir\Larainvite\Models\LaraInviteModel[] $invitationPending
136
 * @property-read \Illuminate\Database\Eloquent\Collection|\Junaidnasir\Larainvite\Models\LaraInviteModel[] $invitationSuccess
137
 * @property-read \Illuminate\Database\Eloquent\Collection|\Junaidnasir\Larainvite\Models\LaraInviteModel[] $invitations
138
 * @property-read \Illuminate\Database\Eloquent\Collection|\Spatie\Permission\Models\Permission[] $permissions
139
 * @property-read \Spatie\Permission\Models\Role $role
140
 * @property-read \Illuminate\Database\Eloquent\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;
0 ignored issues
show
introduced by
The trait Spatie\Permission\Traits\HasRoles requires some properties which are not provided by App\Models\User: $name, $guard_name
Loading history...
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
    public function role(): BelongsTo
202
    {
203
        return $this->belongsTo(Role::class, 'roles_id');
204
    }
205
206
    public function request(): HasMany
207
    {
208
        return $this->hasMany(UserRequest::class, 'users_id');
209
    }
210
211
    public function download(): HasMany
212
    {
213
        return $this->hasMany(UserDownload::class, 'users_id');
214
    }
215
216
    public function release(): HasMany
217
    {
218
        return $this->hasMany(UsersRelease::class, 'users_id');
219
    }
220
221
    public function series(): HasMany
222
    {
223
        return $this->hasMany(UserSerie::class, 'users_id');
224
    }
225
226
    public function invitation(): HasMany
227
    {
228
        return $this->hasMany(Invitation::class, 'users_id');
229
    }
230
231
    public function failedRelease(): HasMany
232
    {
233
        return $this->hasMany(DnzbFailure::class, 'users_id');
234
    }
235
236
    public function comment(): HasMany
237
    {
238
        return $this->hasMany(ReleaseComment::class, 'users_id');
239
    }
240
241
    /**
242
     * @throws \Exception
243
     */
244
    public static function deleteUser($id): void
245
    {
246
        self::find($id)->delete();
247
    }
248
249
    public static function getCount(string $role = null, ?string $username = '', ?string $host = '', ?string $email = ''): int
250
    {
251
        $res = self::query()->where('email', '<>', '[email protected]');
252
253
        if (! empty($role)) {
254
            $res->where('roles_id', $role);
255
        }
256
257
        if ($username !== '') {
258
            $res->where('username', 'like', '%'.$username.'%');
259
        }
260
261
        if ($host !== '') {
262
            $res->where('host', 'like', '%'.$host.'%');
263
        }
264
265
        if ($email !== '') {
266
            $res->where('email', 'like', '%'.$email.'%');
267
        }
268
269
        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...
270
    }
271
272
    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
273
    {
274
        $userName = trim($userName);
275
276
        $rateLimit = Role::query()->where('id', $role)->first();
277
278
        $sql = [
279
            'username' => $userName,
280
            'grabs' => $grabs,
281
            'roles_id' => $role,
282
            '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

282
            'notes' => substr(/** @scrutinizer ignore-type */ $notes, 0, 255),
Loading history...
283
            'invites' => $invites,
284
            'movieview' => $movieview,
285
            'musicview' => $musicview,
286
            'gameview' => $gameview,
287
            'xxxview' => $xxxview,
288
            'consoleview' => $consoleview,
289
            'bookview' => $bookview,
290
            'style' => $style,
291
            'rate_limit' => $rateLimit ? $rateLimit['rate_limit'] : 60,
292
        ];
293
294
        if (! empty($email)) {
295
            $email = trim($email);
296
            $sql += ['email' => $email];
297
        }
298
299
        $user = self::find($id);
300
        $user->update($sql);
301
        $user->syncRoles([$rateLimit['name']]);
302
303
        return self::SUCCESS;
304
    }
305
306
    /**
307
     * @return User|Builder|Model|object|null
308
     */
309
    public static function getByUsername(string $userName)
310
    {
311
        return self::whereUsername($userName)->first();
312
    }
313
314
    /**
315
     * @return Model|static
316
     *
317
     * @throws ModelNotFoundException
318
     */
319
    public static function getByEmail(string $email)
320
    {
321
        return self::whereEmail($email)->first();
322
    }
323
324
    public static function updateUserRole(int $uid, int $role): bool
325
    {
326
        $roleQuery = Role::query()->where('id', $role)->first();
327
        $roleName = $roleQuery->name;
328
329
        $user = self::find($uid);
330
        $user->syncRoles([$roleName]);
331
332
        return self::find($uid)->update(['roles_id' => $role]);
333
    }
334
335
    public static function updateUserRoleChangeDate(int $uid, $date = '', int $addYear = 0): void
336
    {
337
        $user = self::find($uid);
338
        $currRoleExp = $user->rolechangedate ?? now()->toDateTimeString();
339
        if (! empty($date)) {
340
            $user->update(['rolechangedate' => $date]);
341
        }
342
        if (empty($date) && ! empty($addYear)) {
343
            $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

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

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