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

User::getRange()   B

Complexity

Conditions 9
Paths 2

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 35
rs 8.0555
c 0
b 0
f 0
cc 9
nc 2
nop 10

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\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