Passed
Push — master ( 5d0f2f...49b4e9 )
by Darko
10:54
created

User::getCount()   B

Complexity

Conditions 7
Paths 64

Size

Total Lines 29
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

309
            'notes' => substr(/** @scrutinizer ignore-type */ $notes, 0, 255),
Loading history...
310
            'invites' => $invites,
311
            'movieview' => $movieview,
312
            'musicview' => $musicview,
313
            'gameview' => $gameview,
314
            'xxxview' => $xxxview,
315
            'consoleview' => $consoleview,
316
            'bookview' => $bookview,
317
            'style' => $style,
318
            'rate_limit' => $rateLimit ? $rateLimit['rate_limit'] : 60,
319
        ];
320
321
        if (! empty($email)) {
322
            $email = trim($email);
323
            $sql += ['email' => $email];
324
        }
325
326
        $user = self::find($id);
327
        $user->update($sql);
328
        $user->syncRoles([$rateLimit['name']]);
329
330
        return self::SUCCESS;
331
    }
332
333
    /**
334
     * @return User|Builder|Model|object|null
335
     */
336
    public static function getByUsername(string $userName)
337
    {
338
        return self::whereUsername($userName)->first();
339
    }
340
341
    /**
342
     * @return Model|static
343
     *
344
     * @throws ModelNotFoundException
345
     */
346
    public static function getByEmail(string $email)
347
    {
348
        return self::whereEmail($email)->first();
349
    }
350
351
    public static function updateUserRole(int $uid, int|string $role, bool $applyPromotions = true): bool
352
    {
353
        if (is_int($role)) {
354
            $roleQuery = Role::query()->where('id', $role)->first();
355
        } else {
356
            $roleQuery = Role::query()->where('name', $role)->first();
357
        }
358
        $roleName = $roleQuery->name;
0 ignored issues
show
Unused Code introduced by
The assignment to $roleName is dead and can be removed.
Loading history...
359
360
        $user = self::find($uid);
361
        $currentRoleId = $user->roles_id;
362
363
        $updated = self::find($uid)->update(['roles_id' => $roleQuery->id]);
364
365
        // Apply promotions if enabled and role is being upgraded
366
        if ($applyPromotions && $updated && $currentRoleId !== $roleQuery->id) {
367
            $additionalDays = RolePromotion::calculateAdditionalDays($currentRoleId, $roleQuery->id);
0 ignored issues
show
Unused Code introduced by
The call to App\Models\RolePromotion...lculateAdditionalDays() has too many arguments starting with $roleQuery->id. ( Ignorable by Annotation )

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

367
            /** @scrutinizer ignore-call */ 
368
            $additionalDays = RolePromotion::calculateAdditionalDays($currentRoleId, $roleQuery->id);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
368
369
            if ($additionalDays > 0) {
370
                $currentExpiry = $user->rolechangedate ? Carbon::parse($user->rolechangedate) : Carbon::now();
371
                $newExpiry = $currentExpiry->addDays($additionalDays);
372
                $user->update(['rolechangedate' => $newExpiry]);
373
            }
374
        }
375
376
        return $updated;
377
    }
378
    public static function updateExpiredRoles(): void
379
    {
380
        $now = CarbonImmutable::now();
381
        $period = [
382
            'day' => $now->addDay(),
383
            'week' => $now->addWeek(),
384
            'month' => $now->addMonth(),
385
        ];
386
387
        foreach ($period as $value) {
388
            $users = self::query()->whereDate('rolechangedate', '=', $value)->get();
389
            $days = $now->diffInDays($value, true);
390
            foreach ($users as $user) {
391
                SendAccountWillExpireEmail::dispatch($user, $days)->onQueue('emails');
392
            }
393
        }
394
        foreach (self::query()->whereDate('rolechangedate', '<', $now)->get() as $expired) {
395
            $expired->update(['roles_id' => self::ROLE_USER, 'rolechangedate' => null]);
396
            $expired->syncRoles(['User']);
397
            SendAccountExpiredEmail::dispatch($expired)->onQueue('emails');
398
        }
399
    }
400
401
    /**
402
     * @throws \Throwable
403
     */
404
    public static function getRange($start, $offset, $orderBy, ?string $userName = '', ?string $email = '', ?string $host = '', ?string $role = '', bool $apiRequests = false, ?string $createdFrom = '', ?string $createdTo = ''): Collection
405
    {
406
        if ($apiRequests) {
407
            UserRequest::clearApiRequests(false);
408
            $query = "
409
				SELECT users.*, roles.name AS rolename, COUNT(user_requests.id) AS apirequests
410
				FROM users
411
				INNER JOIN roles ON roles.id = users.roles_id
412
				LEFT JOIN user_requests ON user_requests.users_id = users.id
413
				WHERE users.id != 0 %s %s %s %s %s %s
414
				AND email != '[email protected]'
415
				GROUP BY users.id
416
				ORDER BY %s %s %s ";
417
        } else {
418
            $query = '
419
				SELECT users.*, roles.name AS rolename
420
				FROM users
421
				INNER JOIN roles ON roles.id = users.roles_id
422
				WHERE 1=1 %s %s %s %s %s %s
423
				ORDER BY %s %s %s';
424
        }
425
        $order = self::getBrowseOrder($orderBy);
426
427
        return self::fromQuery(
428
            sprintf(
429
                $query,
430
                ! empty($userName) ? 'AND users.username '.'LIKE '.escapeString('%'.$userName.'%') : '',
431
                ! empty($email) ? 'AND users.email '.'LIKE '.escapeString('%'.$email.'%') : '',
432
                ! empty($host) ? 'AND users.host '.'LIKE '.escapeString('%'.$host.'%') : '',
433
                (! empty($role) ? ('AND users.roles_id = '.$role) : ''),
434
                ! empty($createdFrom) ? 'AND users.created_at >= '.escapeString($createdFrom.' 00:00:00') : '',
435
                ! empty($createdTo) ? 'AND users.created_at <= '.escapeString($createdTo.' 23:59:59') : '',
436
                $order[0],
437
                $order[1],
438
                ($start === false ? '' : ('LIMIT '.$offset.' OFFSET '.$start))
439
            )
440
        );
441
    }
442
443
    /**
444
     * Get sort types for sorting users on the web page user list.
445
     *
446
     * @return string[]
447
     */
448
    public static function getBrowseOrder($orderBy): array
449
    {
450
        $order = (empty($orderBy) ? 'username_desc' : $orderBy);
451
        $orderArr = explode('_', $order);
452
        $orderField = match ($orderArr[0]) {
453
            'email' => 'email',
454
            'host' => 'host',
455
            'createdat' => 'created_at',
456
            'lastlogin' => 'lastlogin',
457
            'apiaccess' => 'apiaccess',
458
            'grabs' => 'grabs',
459
            'role' => 'rolename',
460
            'rolechangedate' => 'rolechangedate',
461
            'verification' => 'verified',
462
            default => 'username',
463
        };
464
        $orderSort = (isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1])) ? $orderArr[1] : 'desc';
465
466
        return [$orderField, $orderSort];
467
    }
468
469
    /**
470
     * Verify a password against a hash.
471
     *
472
     * Automatically update the hash if it needs to be.
473
     *
474
     * @param  string  $password  Password to check against hash.
475
     * @param  bool|string  $hash  Hash to check against password.
476
     * @param  int  $userID  ID of the user.
477
     */
478
    public static function checkPassword(string $password, bool|string $hash, int $userID = -1): bool
479
    {
480
        if (Hash::check($password, $hash) === false) {
481
            return false;
482
        }
483
484
        // Update the hash if it needs to be.
485
        if (is_numeric($userID) && $userID > 0 && Hash::needsRehash($hash)) {
486
            $hash = self::hashPassword($password);
487
488
            if ($hash !== false) {
0 ignored issues
show
introduced by
The condition $hash !== false is always true.
Loading history...
489
                self::find($userID)->update(['password' => $hash]);
490
            }
491
        }
492
493
        return true;
494
    }
495
496
    public static function updateRssKey($uid): int
497
    {
498
        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

498
        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...
499
500
        return self::SUCCESS;
501
    }
502
503
    public static function updatePassResetGuid($id, $guid): int
504
    {
505
        self::find($id)->update(['resetguid' => $guid]);
506
507
        return self::SUCCESS;
508
    }
509
510
    public static function updatePassword(int $id, string $password): int
511
    {
512
        self::find($id)->update(['password' => self::hashPassword($password)]);
513
514
        return self::SUCCESS;
515
    }
516
517
    public static function hashPassword($password): string
518
    {
519
        return Hash::make($password);
520
    }
521
522
    /**
523
     * @return Model|static
524
     *
525
     * @throws ModelNotFoundException
526
     */
527
    public static function getByPassResetGuid(string $guid)
528
    {
529
        return self::whereResetguid($guid)->first();
530
    }
531
532
    public static function incrementGrabs(int $id, int $num = 1): void
533
    {
534
        self::find($id)->increment('grabs', $num);
535
    }
536
537
    /**
538
     * @return Model|null|static
539
     */
540
    public static function getByRssToken(string $rssToken)
541
    {
542
        return self::whereApiToken($rssToken)->first();
543
    }
544
545
    public static function isValidUrl($url): bool
546
    {
547
        return (! preg_match('/^(http|https|ftp):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?/i', $url)) ? false : true;
548
    }
549
550
    /**
551
     * @throws \Exception
552
     */
553
    public static function generatePassword(int $length = 15): string
554
    {
555
        return Str::password($length);
556
    }
557
558
    /**
559
     * @throws \Exception
560
     */
561
    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
562
    {
563
        $user = [
564
            'username' => trim($userName),
565
            'password' => trim($password),
566
            'email' => trim($email),
567
        ];
568
569
        if ($validate) {
570
            $validator = Validator::make($user, [
571
                'username' => ['required', 'string', 'min:5', 'max:255', 'unique:users'],
572
                'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new ValidEmailDomain()],
573
                'password' => ['required', 'string', 'min:8', 'confirmed', 'regex:/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/'],
574
            ]);
575
576
            if ($validator->fails()) {
577
                return implode('', Arr::collapse($validator->errors()->toArray()));
578
            }
579
        }
580
581
        // Make sure this is the last check, as if a further validation check failed, the invite would still have been used up.
582
        $invitedBy = 0;
583
        if (! $forceInviteMode && (int) Settings::settingValue('registerstatus') === Settings::REGISTER_STATUS_INVITE) {
584
            if ($inviteCode === '') {
585
                return self::ERR_SIGNUP_BADINVITECODE;
586
            }
587
588
            $invitedBy = self::checkAndUseInvite($inviteCode);
589
            if ($invitedBy < 0) {
590
                return self::ERR_SIGNUP_BADINVITECODE;
591
            }
592
        }
593
594
        return self::add($user['username'], $user['password'], $user['email'], $role, $notes, $host, $invites, $invitedBy);
595
    }
596
597
    /**
598
     * If a invite is used, decrement the person who invited's invite count.
599
     */
600
    public static function checkAndUseInvite(string $inviteCode): int
601
    {
602
        $invite = Invitation::findValidByToken($inviteCode);
603
        if (! $invite) {
604
            return -1;
605
        }
606
607
        self::query()->where('id', $invite->invited_by)->decrement('invites');
608
        $invite->markAsUsed(0); // Will be updated with actual user ID later
609
610
        return $invite->invited_by;
611
    }
612
613
    /**
614
     * @return false|int|mixed
615
     */
616
    public static function add(string $userName, string $password, string $email, int $role, ?string $notes = '', string $host = '', int $invites = Invitation::DEFAULT_INVITES, int $invitedBy = 0)
617
    {
618
        $password = self::hashPassword($password);
619
        if (! $password) {
620
            return false;
621
        }
622
623
        $storeips = config('nntmux:settings.store_user_ip') === true ? $host : '';
624
625
        $user = self::create(
626
            [
627
                'username' => $userName,
628
                'password' => $password,
629
                'email' => $email,
630
                'host' => $storeips,
631
                'roles_id' => $role,
632
                'invites' => $invites,
633
                'invitedby' => (int) $invitedBy === 0 ? null : $invitedBy,
634
                'notes' => $notes,
635
            ]
636
        );
637
638
        return $user->id;
639
    }
640
641
    /**
642
     * Get the list of categories the user has excluded.
643
     *
644
     * @param  int  $userID  ID of the user.
645
     *
646
     * @throws \Exception
647
     */
648
    public static function getCategoryExclusionById(int $userID): array
649
    {
650
        $ret = [];
651
652
        $user = self::find($userID);
653
654
        $userAllowed = $user->getDirectPermissions()->pluck('name')->toArray();
655
        $roleAllowed = $user->getAllPermissions()->pluck('name')->toArray();
656
657
        $allowed = array_intersect($roleAllowed, $userAllowed);
658
659
        $cats = ['view console', 'view movies', 'view audio', 'view tv', 'view pc', 'view adult', 'view books', 'view other'];
660
661
        if (! empty($allowed)) {
662
            foreach ($cats as $cat) {
663
                if (! \in_array($cat, $allowed, false)) {
664
                    $ret[] = match ($cat) {
665
                        'view console' => 1000,
666
                        'view movies' => 2000,
667
                        'view audio' => 3000,
668
                        'view pc' => 4000,
669
                        'view tv' => 5000,
670
                        'view adult' => 6000,
671
                        'view books' => 7000,
672
                        'view other' => 1,
673
                    };
674
                }
675
            }
676
        }
677
678
        return Category::query()->whereIn('root_categories_id', $ret)->pluck('id')->toArray();
679
    }
680
681
    /**
682
     * @throws \Exception
683
     */
684
    public static function getCategoryExclusionForApi(Request $request): array
685
    {
686
        $apiToken = $request->has('api_token') ? $request->input('api_token') : $request->input('apikey');
687
        $user = self::getByRssToken($apiToken);
688
689
        return self::getCategoryExclusionById($user->id);
690
    }
691
692
    /**
693
     * @throws \Exception
694
     */
695
    public static function sendInvite($serverUrl, $uid, $emailTo): string
696
    {
697
        $user = self::find($uid);
698
699
        // Create invitation using our custom system
700
        $invitation = Invitation::createInvitation($emailTo, $user->id);
701
        $url = $serverUrl.'/register?token='.$invitation->token;
702
703
        // Send invitation email
704
        $invitationService = app(InvitationService::class);
705
        $invitationService->sendInvitationEmail($invitation);
706
707
        return $url;
708
    }
709
710
    /**
711
     * Deletes users that have not verified their accounts for 3 or more days.
712
     */
713
    public static function deleteUnVerified(): void
714
    {
715
        static::whereVerified(0)->where('created_at', '<', now()->subDays(3))->delete();
716
    }
717
718
    public function passwordSecurity(): HasOne
719
    {
720
        return $this->hasOne(PasswordSecurity::class);
721
    }
722
723
    public static function canPost($user_id): bool
724
    {
725
        // return true if can_post column is true and false if can_post column is false
726
        return self::where('id', $user_id)->value('can_post');
727
    }
728
}
729