Passed
Push — master ( 5bce24...021a41 )
by Darko
11:55 queued 45s
created

makeFieldLinks()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 1
b 0
f 0
nc 12
nop 3
dl 0
loc 23
rs 8.8333
1
<?php
2
3
use App\Models\Country as CountryModel;
4
use App\Models\Release;
5
use Blacklight\NZB;
6
use Blacklight\XXX;
7
use GuzzleHttp\Client;
8
use GuzzleHttp\Cookie\CookieJar;
9
use GuzzleHttp\Cookie\SetCookie;
10
use GuzzleHttp\Exception\RequestException;
11
use Illuminate\Support\Facades\DB;
12
use Illuminate\Support\Facades\Log;
13
use Illuminate\Support\Str;
14
use sspat\ESQuerySanitizer\Sanitizer;
15
use Symfony\Component\Process\Process;
16
use Zip as ZipStream;
17
18
if (! function_exists('getRawHtml')) {
19
    /**
20
     * @param  bool  $cookie
21
     * @return bool|mixed|string
22
     */
23
    function getRawHtml($url, $cookie = false, $postData = null)
24
    {
25
        // Check if this is an adult site that needs age verification
26
        $adultSites = [
27
            'adultdvdempire.com',
28
            'adultdvdmarketplace.com',
29
            'aebn.net',
30
            'hotmovies.com',
31
            'popporn.com',
32
        ];
33
34
        $isAdultSite = false;
35
        foreach ($adultSites as $site) {
36
            if (stripos($url, $site) !== false) {
37
                $isAdultSite = true;
38
                break;
39
            }
40
        }
41
42
        // For adult sites, use age verification manager if available
43
        if ($isAdultSite && class_exists('\App\Services\AdultProcessing\AgeVerificationManager')) {
44
            try {
45
                static $ageVerificationManager = null;
46
                if ($ageVerificationManager === null) {
47
                    $ageVerificationManager = new \App\Services\AdultProcessing\AgeVerificationManager;
48
                }
49
50
                if ($postData !== null) {
51
                    // Handle POST requests
52
                    $cookieJar = $ageVerificationManager->getCookieJar($url);
53
                    $client = new Client([
54
                        'cookies' => $cookieJar,
55
                        'headers' => ['User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'],
56
                    ]);
57
58
                    $response = $client->post($url, ['form_params' => $postData]);
59
                    $response = $response->getBody()->getContents();
60
                } else {
61
                    $response = $ageVerificationManager->makeRequest($url);
62
                }
63
64
                if ($response !== false) {
65
                    $jsonResponse = json_decode($response, true);
66
                    if (json_last_error() === JSON_ERROR_NONE) {
67
                        return $jsonResponse;
68
                    }
69
70
                    return $response;
71
                }
72
            } catch (\Exception $e) {
73
                // Fall through to standard method
74
                if (function_exists('config') && config('app.debug') === true) {
75
                    Log::error('Age verification failed, falling back to standard method: '.$e->getMessage());
76
                }
77
            }
78
        }
79
80
        // Standard method for non-adult sites or if age verification fails
81
        $cookieJar = new CookieJar;
82
        $client = new Client(['headers' => ['User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246']]);
83
        if ($cookie !== false && $cookie !== null && $cookie !== '') {
84
            $cookie = $cookieJar->setCookie(SetCookie::fromString($cookie));
0 ignored issues
show
Bug introduced by
$cookie of type true is incompatible with the type string expected by parameter $cookie of GuzzleHttp\Cookie\SetCookie::fromString(). ( Ignorable by Annotation )

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

84
            $cookie = $cookieJar->setCookie(SetCookie::fromString(/** @scrutinizer ignore-type */ $cookie));
Loading history...
85
            $client = new Client(['cookies' => $cookie, 'headers' => ['User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246']]);
86
        }
87
        try {
88
            $response = $client->get($url)->getBody()->getContents();
89
            $jsonResponse = json_decode($response, true);
90
            if (json_last_error() === JSON_ERROR_NONE) {
91
                $response = $jsonResponse;
92
            }
93
        } catch (RequestException $e) {
94
            if (function_exists('config') && config('app.debug') === true) {
95
                Log::error($e->getMessage());
96
            }
97
            $response = false;
98
        } catch (RuntimeException $e) {
99
            if (function_exists('config') && config('app.debug') === true) {
100
                Log::error($e->getMessage());
101
            }
102
            $response = false;
103
        }
104
105
        return $response;
106
    }
107
}
108
109
if (! function_exists('makeFieldLinks')) {
110
    /**
111
     * @return string
112
     *
113
     * @throws Exception
114
     */
115
    function makeFieldLinks($data, $field, $type)
116
    {
117
        // Support both array and object access
118
        $fieldValue = is_array($data) ? ($data[$field] ?? '') : ($data->$field ?? '');
119
        $tmpArr = explode(', ', $fieldValue);
120
        $newArr = [];
121
        $i = 0;
122
        foreach ($tmpArr as $ta) {
123
            if (trim($ta) === '') {
124
                continue;
125
            }
126
            if ($type === 'xxx' && $field === 'genre') {
127
                $ta = (new XXX)->getGenres(true, $ta);
128
                $ta = $ta['title'] ?? '';
129
            }
130
            if ($i > 7) {
131
                break;
132
            }
133
            $newArr[] = '<a href="'.url('/'.ucfirst($type).'?'.$field.'='.urlencode($ta)).'" title="'.$ta.'">'.$ta.'</a>';
134
            $i++;
135
        }
136
137
        return implode(', ', $newArr);
138
    }
139
}
140
141
if (! function_exists('getUserBrowseOrder')) {
142
    /**
143
     * @param  string  $orderBy
144
     */
145
    function getUserBrowseOrder($orderBy): array
146
    {
147
        $order = ($orderBy === '' ? 'username_desc' : $orderBy);
148
        $orderArr = explode('_', $order);
149
        $orderField = match ($orderArr[0]) {
150
            'email' => 'email',
151
            'host' => 'host',
152
            'createdat' => 'created_at',
153
            'lastlogin' => 'lastlogin',
154
            'apiaccess' => 'apiaccess',
155
            'apirequests' => 'apirequests',
156
            'grabs' => 'grabs',
157
            'roles_id' => 'users_role_id',
158
            'rolechangedate' => 'rolechangedate',
159
            default => 'username',
160
        };
161
        $orderSort = (isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1])) ? $orderArr[1] : 'desc';
162
163
        return [$orderField, $orderSort];
164
    }
165
}
166
167
if (! function_exists('getUserBrowseOrdering')) {
168
    function getUserBrowseOrdering(): array
169
    {
170
        return [
171
            'username_asc',
172
            'username_desc',
173
            'email_asc',
174
            'email_desc',
175
            'host_asc',
176
            'host_desc',
177
            'createdat_asc',
178
            'createdat_desc',
179
            'lastlogin_asc',
180
            'lastlogin_desc',
181
            'apiaccess_asc',
182
            'apiaccess_desc',
183
            'apirequests_asc',
184
            'apirequests_desc',
185
            'grabs_asc',
186
            'grabs_desc',
187
            'role_asc',
188
            'role_desc',
189
            'rolechangedate_asc',
190
            'rolechangedate_desc',
191
            'verification_asc',
192
            'verification_desc',
193
        ];
194
    }
195
}
196
197
if (! function_exists('getSimilarName')) {
198
    /**
199
     * @param  string  $name
200
     */
201
    function getSimilarName($name): string
202
    {
203
        return implode(' ', \array_slice(str_word_count(str_replace(['.', '_', '-'], ' ', $name), 2), 0, 2));
0 ignored issues
show
Bug introduced by
It seems like str_word_count(str_repla..., '-'), ' ', $name), 2) can also be of type integer; however, parameter $array of array_slice() does only seem to accept array, 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

203
        return implode(' ', \array_slice(/** @scrutinizer ignore-type */ str_word_count(str_replace(['.', '_', '-'], ' ', $name), 2), 0, 2));
Loading history...
204
    }
205
}
206
207
if (! function_exists('human_filesize')) {
208
    /**
209
     * @param  int  $decimals
210
     */
211
    function human_filesize($bytes, $decimals = 0): string
212
    {
213
        $size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
214
        $factor = floor((\strlen($bytes) - 1) / 3);
215
216
        return round(sprintf("%.{$decimals}f", $bytes / (1024 ** $factor)), $decimals).@$size[$factor];
0 ignored issues
show
Bug introduced by
sprintf('%.'.$decimals.'...ytes / 1024 ** $factor) of type string is incompatible with the type double|integer expected by parameter $num of round(). ( Ignorable by Annotation )

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

216
        return round(/** @scrutinizer ignore-type */ sprintf("%.{$decimals}f", $bytes / (1024 ** $factor)), $decimals).@$size[$factor];
Loading history...
217
    }
218
}
219
220
if (! function_exists('bcdechex')) {
221
    /**
222
     * @return string
223
     */
224
    function bcdechex($dec)
225
    {
226
        $hex = '';
227
        do {
228
            $last = bcmod($dec, 16);
229
            $hex = dechex($last).$hex;
0 ignored issues
show
Bug introduced by
$last of type null|string is incompatible with the type integer expected by parameter $num of dechex(). ( Ignorable by Annotation )

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

229
            $hex = dechex(/** @scrutinizer ignore-type */ $last).$hex;
Loading history...
230
            $dec = bcdiv(bcsub($dec, $last), 16);
231
        } while ($dec > 0);
232
233
        return $hex;
234
    }
235
}
236
237
if (! function_exists('runCmd')) {
238
    /**
239
     * Run CLI command.
240
     *
241
     *
242
     * @param  string  $command
243
     * @param  bool  $debug
244
     * @return string
245
     */
246
    function runCmd($command, $debug = false)
247
    {
248
        if ($debug) {
249
            echo '-Running Command: '.PHP_EOL.'   '.$command.PHP_EOL;
250
        }
251
252
        $process = Process::fromShellCommandline('exec '.$command);
253
        $process->setTimeout(1800);
254
        $process->run();
255
        $output = $process->getOutput();
256
257
        if ($debug) {
258
            echo '-Command Output: '.PHP_EOL.'   '.$output.PHP_EOL;
259
        }
260
261
        return $output;
262
    }
263
}
264
265
if (! function_exists('escapeString')) {
266
267
    function escapeString($string): string
268
    {
269
        return DB::connection()->getPdo()->quote($string);
270
    }
271
}
272
273
if (! function_exists('realDuration')) {
274
275
    function realDuration($milliseconds): string
276
    {
277
        $time = round($milliseconds / 1000);
278
279
        return sprintf('%02dh:%02dm:%02ds', floor($time / 3600), floor($time / 60 % 60), $time % 60);
280
    }
281
}
282
283
if (! function_exists('is_it_json')) {
284
    /**
285
     * @throws JsonException
286
     */
287
    function is_it_json($isIt): bool
288
    {
289
        if (is_array($isIt)) {
290
            return false;
291
        }
292
        json_decode($isIt, true, 512, JSON_THROW_ON_ERROR);
293
294
        return json_last_error() === JSON_ERROR_NONE;
295
    }
296
}
297
298
if (! function_exists('getStreamingZip')) {
299
    /**
300
     * @throws Exception
301
     */
302
    function getStreamingZip(array $guids = []): STS\ZipStream\Builder
303
    {
304
        $nzb = new NZB;
305
        $zipped = ZipStream::create(now()->format('Ymdhis').'.zip');
306
        foreach ($guids as $guid) {
307
            $nzbPath = $nzb->NZBPath($guid);
308
            if ($nzbPath) {
309
                $nzbContents = unzipGzipFile($nzbPath);
310
                if ($nzbContents) {
311
                    $filename = $guid;
312
                    $r = Release::query()->where('guid', $guid)->first();
313
                    if ($r) {
314
                        $filename = $r['searchname'];
315
                    }
316
                    $zipped->addRaw($nzbContents, $filename.'.nzb');
317
                }
318
            }
319
        }
320
321
        return $zipped;
322
    }
323
}
324
325
if (! function_exists('release_flag')) {
326
    // Function inspired by c0r3@newznabforums adds country flags on the browse page.
327
    /**
328
     * @param  string  $text  Text to match against.
329
     * @param  string  $page  Type of page. browse or search.
330
     */
331
    function release_flag(string $text, string $page): bool|string
332
    {
333
        $code = $language = '';
334
335
        switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Italian|\bita\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/\bCzech\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/\bGreek\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Hungarian|\bhun\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/\bThai\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Chinese|Man...in|\bc[hn]\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Japanese|\bjp\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Flemish|\b(...|nl)\b|NlSub/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Hebrew|Yiddish/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Tagalog|Filipino/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/\bHindi\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Korean|\bkr\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/French|Vostfr|Multi/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/German(bed)?|\bger\b/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/Swe(dish|sub)/i', $text) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
336
            case stripos($text, 'Arabic') !== false:
337
                $code = 'PK';
338
                $language = 'Arabic';
339
                break;
340
            case stripos($text, 'Cantonese') !== false:
341
                $code = 'TW';
342
                $language = 'Cantonese';
343
                break;
344
            case preg_match('/Chinese|Mandarin|\bc[hn]\b/i', $text):
345
                $code = 'CN';
346
                $language = 'Chinese';
347
                break;
348
            case preg_match('/\bCzech\b/i', $text):
349
                $code = 'CZ';
350
                $language = 'Czech';
351
                break;
352
            case stripos($text, 'Danish') !== false:
353
                $code = 'DK';
354
                $language = 'Danish';
355
                break;
356
            case stripos($text, 'Finnish') !== false:
357
                $code = 'FI';
358
                $language = 'Finnish';
359
                break;
360
            case preg_match('/Flemish|\b(Dutch|nl)\b|NlSub/i', $text):
361
                $code = 'NL';
362
                $language = 'Dutch';
363
                break;
364
            case preg_match('/French|Vostfr|Multi/i', $text):
365
                $code = 'FR';
366
                $language = 'French';
367
                break;
368
            case preg_match('/German(bed)?|\bger\b/i', $text):
369
                $code = 'DE';
370
                $language = 'German';
371
                break;
372
            case preg_match('/\bGreek\b/i', $text):
373
                $code = 'GR';
374
                $language = 'Greek';
375
                break;
376
            case preg_match('/Hebrew|Yiddish/i', $text):
377
                $code = 'IL';
378
                $language = 'Hebrew';
379
                break;
380
            case preg_match('/\bHindi\b/i', $text):
381
                $code = 'IN';
382
                $language = 'Hindi';
383
                break;
384
            case preg_match('/Hungarian|\bhun\b/i', $text):
385
                $code = 'HU';
386
                $language = 'Hungarian';
387
                break;
388
            case preg_match('/Italian|\bita\b/i', $text):
389
                $code = 'IT';
390
                $language = 'Italian';
391
                break;
392
            case preg_match('/Japanese|\bjp\b/i', $text):
393
                $code = 'JP';
394
                $language = 'Japanese';
395
                break;
396
            case preg_match('/Korean|\bkr\b/i', $text):
397
                $code = 'KR';
398
                $language = 'Korean';
399
                break;
400
            case stripos($text, 'Norwegian') !== false:
401
                $code = 'NO';
402
                $language = 'Norwegian';
403
                break;
404
            case stripos($text, 'Polish') !== false:
405
                $code = 'PL';
406
                $language = 'Polish';
407
                break;
408
            case stripos($text, 'Portuguese') !== false:
409
                $code = 'PT';
410
                $language = 'Portugese';
411
                break;
412
            case stripos($text, 'Romanian') !== false:
413
                $code = 'RO';
414
                $language = 'Romanian';
415
                break;
416
            case stripos($text, 'Spanish') !== false:
417
                $code = 'ES';
418
                $language = 'Spanish';
419
                break;
420
            case preg_match('/Swe(dish|sub)/i', $text):
421
                $code = 'SE';
422
                $language = 'Swedish';
423
                break;
424
            case preg_match('/Tagalog|Filipino/i', $text):
425
                $code = 'PH';
426
                $language = 'Tagalog|Filipino';
427
                break;
428
            case preg_match('/\bThai\b/i', $text):
429
                $code = 'TH';
430
                $language = 'Thai';
431
                break;
432
            case stripos($text, 'Turkish') !== false:
433
                $code = 'TR';
434
                $language = 'Turkish';
435
                break;
436
            case stripos($text, 'Russian') !== false:
437
                $code = 'RU';
438
                $language = 'Russian';
439
                break;
440
            case stripos($text, 'Vietnamese') !== false:
441
                $code = 'VN';
442
                $language = 'Vietnamese';
443
                break;
444
        }
445
446
        if ($code !== '' && $page === 'browse') {
447
            return '<img title="'.$language.'" alt="'.$language.'" src="'.asset('/assets/images/flags/'.$code.'.png').'"/>';
448
        }
449
450
        if ($page === 'search') {
451
            if ($code === '') {
452
                return false;
453
            }
454
455
            return $code;
456
        }
457
458
        return '';
459
    }
460
}
461
462
if (! function_exists('getReleaseCover')) {
463
    /**
464
     * Get the cover image URL for a release based on its type and ID
465
     *
466
     * @param  object|array  $release  The release object or array
467
     * @return string The cover image URL or placeholder if no cover exists
468
     */
469
    function getReleaseCover($release): string
470
    {
471
        $coverType = null;
472
        $coverId = null;
473
474
        // Helper function to get value from object or array
475
        $getValue = function ($data, $key) {
476
            if (is_array($data)) {
477
                return $data[$key] ?? null;
478
            } elseif (is_object($data)) {
479
                return $data->$key ?? null;
480
            }
481
482
            return null;
483
        };
484
485
        // Determine cover type and ID based on category
486
        $imdbid = $getValue($release, 'imdbid');
487
        $musicinfo_id = $getValue($release, 'musicinfo_id');
488
        $consoleinfo_id = $getValue($release, 'consoleinfo_id');
489
        $bookinfo_id = $getValue($release, 'bookinfo_id');
490
        $gamesinfo_id = $getValue($release, 'gamesinfo_id');
491
        $xxxinfo_id = $getValue($release, 'xxxinfo_id');
492
        $anidbid = $getValue($release, 'anidbid');
493
494
        if (! empty($imdbid) && $imdbid > 0) {
495
            $coverType = 'movies';
496
            $coverId = str_pad($imdbid, 7, '0', STR_PAD_LEFT);
497
        } elseif (! empty($musicinfo_id)) {
498
            $coverType = 'music';
499
            $coverId = $musicinfo_id;
500
        } elseif (! empty($consoleinfo_id)) {
501
            $coverType = 'console';
502
            $coverId = $consoleinfo_id;
503
        } elseif (! empty($bookinfo_id)) {
504
            $coverType = 'book';
505
            $coverId = $bookinfo_id;
506
        } elseif (! empty($gamesinfo_id)) {
507
            $coverType = 'games';
508
            $coverId = $gamesinfo_id;
509
        } elseif (! empty($xxxinfo_id)) {
510
            $coverType = 'xxx';
511
            $coverId = $xxxinfo_id;
512
        } elseif (! empty($anidbid) && $anidbid > 0) {
513
            $coverType = 'anime';
514
            $coverId = $anidbid;
515
        }
516
517
        // Return the cover URL if we have a type and ID
518
        // The CoverController will handle serving the file or returning a placeholder
519
        if ($coverType && $coverId) {
520
            return url("/covers/{$coverType}/{$coverId}-cover.jpg");
521
        }
522
523
        // Return placeholder image if no cover type/ID found
524
        return asset('assets/images/no-cover.png');
525
    }
526
}
527
528
if (! function_exists('sanitize')) {
529
    function sanitize(array|string $phrases, array $doNotSanitize = []): string
530
    {
531
        if (! is_array($phrases)) {
0 ignored issues
show
introduced by
The condition is_array($phrases) is always true.
Loading history...
532
            $wordArray = explode(' ', str_replace('.', ' ', $phrases));
533
        } else {
534
            $wordArray = $phrases;
535
        }
536
537
        $keywords = [];
538
        $tempWords = [];
539
        foreach ($wordArray as $words) {
540
            $words = preg_split('/\s+/', $words);
541
            foreach ($words as $st) {
542
                if (Str::startsWith($st, ['!', '+', '-', '?', '*']) && Str::length($st) > 1 && ! preg_match('/([!+?\-*]){2,}/', $st)) {
543
                    $str = $st;
544
                } elseif (Str::endsWith($st, ['+', '-', '?', '*']) && Str::length($st) > 1 && ! preg_match('/([!+?\-*]){2,}/', $st)) {
545
                    $str = $st;
546
                } else {
547
                    $str = Sanitizer::escape($st, $doNotSanitize);
548
                }
549
                $tempWords[] = $str;
550
            }
551
552
            $keywords = $tempWords;
553
        }
554
555
        return implode(' ', $keywords);
556
    }
557
}
558
559
if (! function_exists('formatBytes')) {
560
    /**
561
     * Format bytes into human-readable file size.
562
     *
563
     * @param  int|float|null  $bytes
564
     */
565
    function formatBytes($bytes = 0): string
566
    {
567
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
568
        $bytes = max((int) $bytes, 0);
569
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
570
        $pow = min($pow, count($units) - 1);
571
572
        $bytes /= pow(1024, $pow);
573
574
        return round($bytes, 2).' '.$units[$pow];
575
    }
576
}
577
578
if (! function_exists('csp_nonce')) {
579
    /**
580
     * Generate a CSP nonce for inline scripts
581
     * This should be stored in the request and reused across the request lifecycle
582
     */
583
    function csp_nonce(): string
584
    {
585
        static $nonce = null;
586
587
        if ($nonce === null) {
588
            $nonce = base64_encode(random_bytes(16));
589
        }
590
591
        return $nonce;
592
    }
593
}
594
595
if (! function_exists('userDate')) {
596
    /**
597
     * Format a date/time string according to the authenticated user's timezone
598
     *
599
     * @param  string|null  $date  The date to format
600
     * @param  string  $format  The format string (default: 'M d, Y H:i')
601
     * @return string The formatted date in user's timezone
602
     */
603
    function userDate(?string $date, string $format = 'M d, Y H:i'): string
604
    {
605
        if (empty($date)) {
606
            return '';
607
        }
608
609
        try {
610
            // Parse the date in the app's timezone (which should be UTC)
611
            // If dates in DB are stored in server timezone, they'll be parsed correctly
612
            $appTimezone = config('app.timezone', 'UTC');
613
            $carbon = \Illuminate\Support\Carbon::parse($date, $appTimezone);
614
615
            // If user is authenticated and has a timezone set, convert to it
616
            if (\Illuminate\Support\Facades\Auth::check() && \Illuminate\Support\Facades\Auth::user()->timezone) {
0 ignored issues
show
Bug introduced by
Accessing timezone on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
617
                $carbon->setTimezone(\Illuminate\Support\Facades\Auth::user()->timezone);
618
            }
619
620
            return $carbon->format($format);
621
        } catch (\Exception $e) {
622
            return $date;
623
        }
624
    }
625
}
626
627
if (! function_exists('userDateDiffForHumans')) {
628
    /**
629
     * Format a date/time string as a human-readable diff according to the authenticated user's timezone
630
     *
631
     * @param  string|null  $date  The date to format
632
     * @return string The formatted date diff in user's timezone
633
     */
634
    function userDateDiffForHumans(?string $date): string
635
    {
636
        if (empty($date)) {
637
            return '';
638
        }
639
640
        try {
641
            // Parse the date in the app's timezone (which should be UTC)
642
            // If dates in DB are stored in server timezone, they'll be parsed correctly
643
            $appTimezone = config('app.timezone', 'UTC');
644
            $carbon = \Illuminate\Support\Carbon::parse($date, $appTimezone);
645
646
            // If user is authenticated and has a timezone set, convert to it
647
            if (\Illuminate\Support\Facades\Auth::check() && \Illuminate\Support\Facades\Auth::user()->timezone) {
0 ignored issues
show
Bug introduced by
Accessing timezone on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
648
                $carbon->setTimezone(\Illuminate\Support\Facades\Auth::user()->timezone);
649
            }
650
651
            return $carbon->diffForHumans();
652
        } catch (\Exception $e) {
653
            return $date;
654
        }
655
    }
656
}
657
658
if (! function_exists('getAvailableTimezones')) {
659
    /**
660
     * Get a list of available timezones grouped by region
661
     *
662
     * @return array Array of timezones grouped by region
663
     */
664
    function getAvailableTimezones(): array
665
    {
666
        $timezones = [];
667
        $regions = [
668
            'Africa' => \DateTimeZone::AFRICA,
669
            'America' => \DateTimeZone::AMERICA,
670
            'Antarctica' => \DateTimeZone::ANTARCTICA,
671
            'Arctic' => \DateTimeZone::ARCTIC,
672
            'Asia' => \DateTimeZone::ASIA,
673
            'Atlantic' => \DateTimeZone::ATLANTIC,
674
            'Australia' => \DateTimeZone::AUSTRALIA,
675
            'Europe' => \DateTimeZone::EUROPE,
676
            'Indian' => \DateTimeZone::INDIAN,
677
            'Pacific' => \DateTimeZone::PACIFIC,
678
        ];
679
680
        foreach ($regions as $name => $region) {
681
            $timezones[$name] = \DateTimeZone::listIdentifiers($region);
682
        }
683
684
        return $timezones;
685
    }
686
}
687
688
if (! function_exists('countryCode')) {
689
    /**
690
     * Get a country code for a country name.
691
     *
692
     * @return mixed
693
     */
694
    function countryCode(string $country)
695
    {
696
        if (\strlen($country) > 2) {
697
            $code = CountryModel::whereFullName($country)->orWhere('name', $country)->first(['iso_3166_2']);
698
            if ($code !== null && isset($code['iso_3166_2'])) {
699
                return $code['iso_3166_2'];
700
            }
701
        }
702
703
        return '';
704
    }
705
}
706
707
if (! function_exists('unzipGzipFile')) {
708
    /**
709
     * Unzip a gzip file, return the output. Return false on error / empty.
710
     *
711
     * @return bool|string
712
     */
713
    function unzipGzipFile(string $filePath)
714
    {
715
        $string = '';
716
        $gzFile = @gzopen($filePath, 'rb', 0);
717
        if ($gzFile) {
0 ignored issues
show
introduced by
$gzFile is of type false|resource, thus it always evaluated to false.
Loading history...
718
            while (! gzeof($gzFile)) {
719
                $temp = gzread($gzFile, 1024);
720
                // Check for empty string.
721
                // Without this the loop would be endless and consume 100% CPU.
722
                // Do not set $string empty here, as the data might still be good.
723
                if (! $temp) {
724
                    break;
725
                }
726
                $string .= $temp;
727
            }
728
            gzclose($gzFile);
729
        }
730
731
        return $string === '' ? false : $string;
0 ignored issues
show
introduced by
The condition $string === '' is always true.
Loading history...
732
    }
733
}
734
735
if (! function_exists('streamSslContextOptions')) {
736
    /**
737
     * Creates an array to be used with stream_context_create() to verify openssl certificates
738
     * when connecting to a tls or ssl connection when using stream functions (fopen/file_get_contents/etc).
739
     *
740
     * @param  bool  $forceIgnore  Force ignoring of verification (useful for self-signed certs in development).
741
     * @return array Stream context options for SSL/TLS connections
742
     */
743
    function streamSslContextOptions(bool $forceIgnore = false): array
744
    {
745
        $cafile = config('nntmux_ssl.ssl_cafile', '');
746
        $capath = config('nntmux_ssl.ssl_capath', '');
747
        $hasCustomCerts = $cafile !== '' || $capath !== '';
748
749
        // Base options - either insecure (no certs configured) or configured
750
        $options = [
751
            'verify_peer' => ! $forceIgnore && $hasCustomCerts && config('nntmux_ssl.ssl_verify_peer', false),
752
            'verify_peer_name' => ! $forceIgnore && $hasCustomCerts && config('nntmux_ssl.ssl_verify_host', false),
753
            'allow_self_signed' => $forceIgnore ? true : config('nntmux_ssl.ssl_allow_self_signed', true),
754
        ];
755
756
        // Add certificate paths if configured
757
        if ($hasCustomCerts && ! $forceIgnore) {
758
            if ($cafile !== '') {
759
                $options['cafile'] = $cafile;
760
            }
761
            if ($capath !== '') {
762
                $options['capath'] = $capath;
763
            }
764
        }
765
766
        // Additional security options for modern TLS
767
        $options['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
768
        $options['disable_compression'] = true; // Prevent CRIME attacks
769
        $options['SNI_enabled'] = true; // Enable Server Name Indication
770
771
        // If we set the transport to tls and the server falls back to ssl,
772
        // the context options would be for tls and would not apply to ssl,
773
        // so set both tls and ssl context in case the server does not support tls.
774
        return ['tls' => $options, 'ssl' => $options];
775
    }
776
}
777
778
if (! function_exists('getCoverURL')) {
779
    function getCoverURL(array $options = []): string
780
    {
781
        $defaults = [
782
            'id' => null,
783
            'suffix' => '-cover.jpg',
784
            'type' => '',
785
        ];
786
        $options += $defaults;
787
        $fileSpecTemplate = '%s/%s%s';
788
        $fileSpec = '';
789
790
        if (! empty($options['id']) && \in_array(
791
            $options['type'],
792
            ['anime', 'audio', 'audiosample', 'book', 'console', 'games', 'movies', 'music', 'preview', 'sample', 'tvrage', 'video', 'xxx'],
793
            false
794
        )
795
        ) {
796
            $fileSpec = sprintf($fileSpecTemplate, $options['type'], $options['id'], $options['suffix']);
797
            $fileSpec = file_exists(storage_path('covers/').$fileSpec) ? $fileSpec :
798
                sprintf($fileSpecTemplate, $options['type'], 'no', $options['suffix']);
799
        }
800
801
        return $fileSpec;
802
    }
803
}
804
805
if (! function_exists('fileInfo')) {
806
    /**
807
     * Return file type/info using magic numbers.
808
     * Try using `file` program where available, fallback to using PHP's finfo class.
809
     *
810
     * @param  string  $path  Path to the file / folder to check.
811
     * @return string File info. Empty string on failure.
812
     *
813
     * @throws \Exception
814
     */
815
    function fileInfo(string $path): string
816
    {
817
        $magicPath = config('nntmux_settings.magic_file_path');
818
        if ($magicPath !== null && \Illuminate\Support\Facades\Process::run('which file')->successful()) {
819
            $magicSwitch = " -m $magicPath";
820
            $output = runCmd('file'.$magicSwitch.' -b "'.$path.'"');
821
        } else {
822
            $fileInfo = $magicPath === null ? finfo_open(FILEINFO_RAW) : finfo_open(FILEINFO_RAW, $magicPath);
823
824
            $output = finfo_file($fileInfo, $path);
825
            if (empty($output)) {
826
                $output = '';
827
            }
828
            finfo_close($fileInfo);
829
        }
830
831
        return $output;
832
    }
833
}
834
835
if (! function_exists('cp437toUTF')) {
836
    /**
837
     * Convert Code page 437 chars to UTF.
838
     */
839
    function cp437toUTF(string $string): string
840
    {
841
        return iconv('CP437', 'UTF-8//IGNORE//TRANSLIT', $string);
842
    }
843
}
844
845
if (! function_exists('imdb_trailers')) {
846
    /**
847
     * Fetches an embeddable video to a IMDB trailer from http://www.traileraddict.com.
848
     */
849
    function imdb_trailers($imdbID): string
850
    {
851
        $xml = getRawHtml('https://api.traileraddict.com/?imdb='.$imdbID);
852
        if ($xml !== false && preg_match('#(v\.traileraddict\.com/\d+)#i', $xml, $html)) {
0 ignored issues
show
Bug introduced by
It seems like $xml can also be of type true; however, parameter $subject of preg_match() 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

852
        if ($xml !== false && preg_match('#(v\.traileraddict\.com/\d+)#i', /** @scrutinizer ignore-type */ $xml, $html)) {
Loading history...
853
            return 'https://'.$html[1];
854
        }
855
856
        return '';
857
    }
858
}
859
860
if (! function_exists('showApiError')) {
861
    function showApiError(int $errorCode = 900, string $errorText = '')
862
    {
863
        $errorHeader = 'HTTP 1.1 400 Bad Request';
864
        if ($errorText === '') {
865
            switch ($errorCode) {
866
                case 100:
867
                    $errorText = 'Incorrect user credentials';
868
                    $errorHeader = 'HTTP 1.1 401 Unauthorized';
869
                    break;
870
                case 101:
871
                    $errorText = 'Account suspended';
872
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
873
                    break;
874
                case 102:
875
                    $errorText = 'Insufficient privileges/not authorized';
876
                    $errorHeader = 'HTTP 1.1 401 Unauthorized';
877
                    break;
878
                case 103:
879
                    $errorText = 'Registration denied';
880
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
881
                    break;
882
                case 104:
883
                    $errorText = 'Registrations are closed';
884
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
885
                    break;
886
                case 105:
887
                    $errorText = 'Invalid registration (Email Address Taken)';
888
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
889
                    break;
890
                case 106:
891
                    $errorText = 'Invalid registration (Email Address Bad Format)';
892
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
893
                    break;
894
                case 107:
895
                    $errorText = 'Registration Failed (Data error)';
896
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
897
                    break;
898
                case 200:
899
                    $errorText = 'Missing parameter';
900
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
901
                    break;
902
                case 201:
903
                    $errorText = 'Incorrect parameter';
904
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
905
                    break;
906
                case 202:
907
                    $errorText = 'No such function';
908
                    $errorHeader = 'HTTP 1.1 404 Not Found';
909
                    break;
910
                case 203:
911
                    $errorText = 'Function not available';
912
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
913
                    break;
914
                case 300:
915
                    $errorText = 'No such item';
916
                    $errorHeader = 'HTTP 1.1 404 Not Found';
917
                    break;
918
                case 500:
919
                    $errorText = 'Request limit reached';
920
                    $errorHeader = 'HTTP 1.1 429 Too Many Requests';
921
                    break;
922
                case 501:
923
                    $errorText = 'Download limit reached';
924
                    $errorHeader = 'HTTP 1.1 429 Too Many Requests';
925
                    break;
926
                case 910:
927
                    $errorText = 'API disabled';
928
                    $errorHeader = 'HTTP 1.1 401 Unauthorized';
929
                    break;
930
                default:
931
                    $errorText = 'Unknown error';
932
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
933
                    break;
934
            }
935
        }
936
937
        $response =
938
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".
939
            '<error code="'.$errorCode.'" description="'.$errorText."\"/>\n";
940
941
        return response($response)->header('Content-type', 'text/xml')->header('Content-Length', strlen($response))->header('X-NNTmux', 'API ERROR ['.$errorCode.'] '.$errorText)->header('HTTP/1.1', $errorHeader);
942
    }
943
}
944
945
if (! function_exists('getRange')) {
946
    function getRange($tableName): \Illuminate\Contracts\Pagination\LengthAwarePaginator
947
    {
948
        $range = \Illuminate\Support\Facades\DB::table($tableName);
949
        if ($tableName === 'xxxinfo') {
950
            $range->selectRaw('UNCOMPRESS(plot) AS plot');
951
        }
952
953
        return $range->orderByDesc('created_at')->paginate(config('nntmux.items_per_page'));
0 ignored issues
show
Bug introduced by
'created_at' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderByDesc(). ( Ignorable by Annotation )

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

953
        return $range->orderByDesc(/** @scrutinizer ignore-type */ 'created_at')->paginate(config('nntmux.items_per_page'));
Loading history...
954
    }
955
}
956
957
if (! function_exists('isValidNewznabNzb')) {
958
    /**
959
     * Validate if the content is a valid Newznab NZB file.
960
     *
961
     * @param  string  $content  The NZB file content
962
     * @return bool True if valid NZB, false otherwise
963
     */
964
    function isValidNewznabNzb(string $content): bool
965
    {
966
        // Check if content starts with valid XML declaration and contains NZB namespace
967
        if (empty($content)) {
968
            return false;
969
        }
970
971
        // Try to load as XML
972
        libxml_use_internal_errors(true);
973
        $xml = @simplexml_load_string($content);
974
975
        if ($xml === false) {
976
            libxml_clear_errors();
977
            return false;
978
        }
979
980
        // Check for NZB namespace or nzb root element
981
        $namespaces = $xml->getNamespaces(true);
982
        $hasNzbNamespace = isset($namespaces['']) && str_contains($namespaces[''], 'nzb');
983
        $isNzbRoot = strtolower($xml->getName()) === 'nzb';
984
985
        libxml_clear_errors();
986
987
        return $hasNzbNamespace || $isNzbRoot;
988
    }
989
}
990