Completed
Branch dev (4bcb34)
by Darko
13:52
created

Utility::pathCombine()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blacklight\utility;
4
5
use Blacklight\db\DB;
6
use App\Models\Settings;
7
use Blacklight\ColorCLI;
8
use Illuminate\Support\Str;
9
use App\Extensions\util\Versions;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Blacklight\utility\Versions. 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...
10
11
/**
12
 * Class Utility.
13
 */
14
class Utility
15
{
16
    /**
17
     *  Regex for detecting multi-platform path. Use it where needed so it can be updated in one location as required characters get added.
18
     */
19
    public const PATH_REGEX = '(?P<drive>[A-Za-z]:|)(?P<path>[/\w.-]+|)';
20
21
    public const VERSION_REGEX = '#(?P<all>v(?P<digits>(?P<major>\d+)\.(?P<minor>\d+)\.(?P<revision>\d+)(?:\.(?P<fix>\d+))?)(?:-(?P<suffix>(?:RC\d+|dev)))?)#';
22
23
    /**
24
     * Checks all levels of the supplied path are readable and executable by current user.
25
     *
26
     * @todo Make this recursive with a switch to only check end point.
27
     * @param $path	*nix path to directory or file
28
     *
29
     * @return bool|string True is successful, otherwise the part of the path that failed testing.
30
     */
31
    public static function canExecuteRead($path)
32
    {
33
        $paths = explode('#/#', $path);
34
        $fullPath = DS;
0 ignored issues
show
Bug introduced by
The constant Blacklight\utility\DS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
35
        foreach ($paths as $singlePath) {
36
            if ($singlePath !== '') {
37
                $fullPath .= $singlePath.DS;
38
                if (! is_readable($fullPath) || ! is_executable($fullPath)) {
39
                    return "The '$fullPath' directory must be readable and executable by all .".PHP_EOL;
40
                }
41
            }
42
        }
43
44
        return true;
45
    }
46
47
    public static function clearScreen(): void
48
    {
49
        if (self::isCLI()) {
50
            if (self::isWin()) {
51
                passthru('cls');
52
            } else {
53
                passthru('clear');
54
            }
55
        }
56
    }
57
58
    /**
59
     * Replace all white space chars for a single space.
60
     *
61
     * @param string $text
62
     *
63
     * @return string
64
     *
65
     * @static
66
     */
67
    public static function collapseWhiteSpace($text): string
68
    {
69
        // Strip leading/trailing white space.
70
        return trim(
71
        // Replace 2 or more white space for a single space.
72
            preg_replace(
73
                '/\s{2,}/',
74
                ' ',
75
                // Replace new lines and carriage returns. DO NOT try removing '\r' or '\n' as they are valid in queries which uses this method.
76
                str_replace(["\n", "\r"], ' ', $text)
77
            )
78
        );
79
    }
80
81
    /**
82
     * Removes the preceeding or proceeding portion of a string
83
     * relative to the last occurrence of the specified character.
84
     * The character selected may be retained or discarded.
85
     *
86
     * @param string $character      the character to search for.
87
     * @param string $string         the string to search through.
88
     * @param string $side           determines whether text to the left or the right of the character is returned.
89
     *                               Options are: left, or right.
90
     * @param bool   $keep_character determines whether or not to keep the character.
91
     *                               Options are: true, or false.
92
     *
93
     * @return string
94
     */
95
    public static function cutStringUsingLast($character, $string, $side, $keep_character = true): string
96
    {
97
        $offset = ($keep_character ? 1 : 0);
98
        $whole_length = \strlen($string);
99
        $right_length = (\strlen(strrchr($string, $character)) - 1);
100
        $left_length = ($whole_length - $right_length - 1);
101
        switch ($side) {
102
            case 'left':
103
                $piece = substr($string, 0, $left_length + $offset);
104
                break;
105
            case 'right':
106
                $start = (0 - ($right_length + $offset));
107
                $piece = substr($string, $start);
108
                break;
109
            default:
110
                $piece = false;
111
                break;
112
        }
113
114
        return $piece;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $piece could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
115
    }
116
117
    /**
118
     * @param array|null $options
119
     *
120
     * @return array|null
121
     */
122
    public static function getDirFiles(array $options = null): ?array
123
    {
124
        $defaults = [
125
            'dir'   => false,
126
            'ext'   => '', // no full stop (period) separator should be used.
127
            'file'    => true,
128
            'path'  => '',
129
            'regex' => '',
130
        ];
131
        $options += $defaults;
132
        if (! $options['dir'] && ! $options['file']) {
133
            return null;
134
        }
135
136
        // Replace windows style path separators with unix style.
137
        $iterator = new \FilesystemIterator(
138
            str_replace('\\', '/', $options['path']),
139
            \FilesystemIterator::KEY_AS_PATHNAME |
140
            \FilesystemIterator::SKIP_DOTS |
141
            \FilesystemIterator::UNIX_PATHS
142
        );
143
144
        $files = [];
145
        foreach ($iterator as $fileInfo) {
146
            $file = $iterator->key();
147
            switch (true) {
148
                case ! $options['dir'] && $fileInfo->isDir():
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
149
                    break;
150
                case ! empty($options['ext']) && $fileInfo->getExtension() != $options['ext']:
151
                    break;
152
                case empty($options['regex']) || ! preg_match($options['regex'], $file):
153
                    break;
154
                case ! $options['file'] && $fileInfo->isFile():
155
                    break;
156
                default:
157
                    $files[] = $file;
158
            }
159
        }
160
161
        return $files;
162
    }
163
164
    /**
165
     * @return array
166
     */
167
    public static function getThemesList(): array
168
    {
169
        $ignoredThemes = ['admin', 'shared'];
170
        $themes = scandir(base_path().'/resources/views/themes', SCANDIR_SORT_ASCENDING);
0 ignored issues
show
Bug introduced by
Are you sure the usage of base_path() is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
171
        $themelist[] = 'None';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$themelist was never initialized. Although not strictly required by PHP, it is generally a good practice to add $themelist = array(); before regardless.
Loading history...
172
        foreach ($themes as $theme) {
173
            if (strpos($theme, '.') === false && ! \in_array($theme, $ignoredThemes, false) && is_dir(base_path().'/resources/views/themes/'.$theme)) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of base_path() is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
174
                $themelist[] = $theme;
175
            }
176
        }
177
178
        sort($themelist);
179
180
        return $themelist;
181
    }
182
183
    public static function getValidVersionsFile()
184
    {
185
        return (new Versions())->getValidVersionsFile();
186
    }
187
188
    /**
189
     * Detect if the command is accessible on the system.
190
     *
191
     * @param $cmd
192
     *
193
     * @return bool|null Returns true if found, false if not found, and null if which is not detected.
194
     */
195
    public static function hasCommand($cmd): ?bool
196
    {
197
        if ('HAS_WHICH') {
198
            $returnVal = shell_exec("which $cmd");
199
200
            return empty($returnVal) ? false : true;
201
        }
202
203
        return null;
204
    }
205
206
    /**
207
     * Check for availability of which command.
208
     */
209
    public static function hasWhich(): bool
210
    {
211
        exec('which which', $output, $error);
212
213
        return ! $error;
214
    }
215
216
    /**
217
     * Check if user is running from CLI.
218
     *
219
     * @return bool
220
     */
221
    public static function isCLI()
222
    {
223
        return strtolower(PHP_SAPI) === 'cli';
224
    }
225
226
    public static function isGZipped($filename)
227
    {
228
        $gzipped = null;
229
        if (($fp = fopen($filename, 'rb')) !== false) {
230
            if (@fread($fp, 2) === "\x1F\x8B") { // this is a gzip'd file
231
                fseek($fp, -4, SEEK_END);
232
                if (\strlen($datum = @fread($fp, 4)) === 4) {
233
                    $gzipped = $datum;
234
                }
235
            }
236
            fclose($fp);
237
        }
238
239
        return $gzipped;
240
    }
241
242
    /**
243
     * @return bool
244
     * @throws \Exception
245
     */
246
    public static function isPatched(): bool
247
    {
248
        $versions = self::getValidVersionsFile();
249
250
        $patch = Settings::settingValue('..sqlpatch');
0 ignored issues
show
Bug introduced by
'..sqlpatch' of type string is incompatible with the type boolean|array expected by parameter $setting of App\Models\Settings::settingValue(). ( Ignorable by Annotation )

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

250
        $patch = Settings::settingValue(/** @scrutinizer ignore-type */ '..sqlpatch');
Loading history...
251
        $ver = $versions->versions->sql->file;
252
253
        // Check database patch version
254
        if ($patch < $ver) {
255
            $message = "\nYour database is not up to date. Reported patch levels\n   Db: $patch\nfile: $ver\nPlease update.\n php ".
256
                NN_ROOT."./tmux nntmux:db\n";
0 ignored issues
show
Bug introduced by
The constant Blacklight\utility\NN_ROOT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
257
            if (self::isCLI()) {
258
                echo ColorCLI::error($message);
259
            }
260
            throw new \RuntimeException($message);
261
        }
262
263
        return true;
264
    }
265
266
    /**
267
     * @return bool
268
     */
269
    public static function isWin(): bool
270
    {
271
        return stripos(PHP_OS, 'win') === 0;
272
    }
273
274
    /**
275
     * @param array  $elements
276
     * @param string $prefix
277
     *
278
     * @return string
279
     */
280
    public static function pathCombine(array $elements, $prefix = ''): string
281
    {
282
        return $prefix.implode(DS, $elements);
0 ignored issues
show
Bug introduced by
The constant Blacklight\utility\DS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
283
    }
284
285
    /**
286
     * @param $text
287
     */
288
    public static function stripBOM(&$text): void
289
    {
290
        $bom = pack('CCC', 0xef, 0xbb, 0xbf);
291
        if (0 === strncmp($text, $bom, 3)) {
292
            $text = substr($text, 3);
293
        }
294
    }
295
296
    /**
297
     * Strips non-printing characters from a string.
298
     *
299
     * Operates directly on the text string, but also returns the result for situations requiring a
300
     * return value (use in ternary, etc.)/
301
     *
302
     * @param $text        String variable to strip.
303
     *
304
     * @return string    The stripped variable.
305
     */
306
    public static function stripNonPrintingChars(&$text): string
307
    {
308
        $lowChars = [
309
            "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07",
310
            "\x08", "\x09", "\x0A", "\x0B", "\x0C", "\x0D", "\x0E", "\x0F",
311
            "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17",
312
            "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D", "\x1E", "\x1F",
313
        ];
314
        $text = str_replace($lowChars, '', $text);
315
316
        return $text;
317
    }
318
319
    /**
320
     * @param $path
321
     *
322
     * @return string
323
     */
324
    public static function trailingSlash($path): string
325
    {
326
        if (substr($path, \strlen($path) - 1) !== '/') {
327
            $path .= '/';
328
        }
329
330
        return $path;
331
    }
332
333
    /**
334
     * Unzip a gzip file, return the output. Return false on error / empty.
335
     *
336
     * @param string $filePath
337
     *
338
     * @return bool|string
339
     */
340
    public static function unzipGzipFile($filePath)
341
    {
342
        /* Potential issues with this, so commenting out.
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
343
        $length = Utility::isGZipped($filePath);
344
        if ($length === false || $length === null) {
345
        	return false;
346
        }*/
347
348
        $string = '';
349
        $gzFile = @gzopen($filePath, 'rb', 0);
350
        if ($gzFile) {
0 ignored issues
show
introduced by
The condition $gzFile is always false.
Loading history...
351
            while (! gzeof($gzFile)) {
352
                $temp = gzread($gzFile, 1024);
353
                // Check for empty string.
354
                // Without this the loop would be endless and consume 100% CPU.
355
                // Do not set $string empty here, as the data might still be good.
356
                if (! $temp) {
357
                    break;
358
                }
359
                $string .= $temp;
360
            }
361
            gzclose($gzFile);
362
        }
363
364
        return $string === '' ? false : $string;
0 ignored issues
show
introduced by
The condition $string === '' is always true.
Loading history...
365
    }
366
367
    /**
368
     * @param $path
369
     */
370
    public static function setCoversConstant($path)
371
    {
372
        if (! \defined('NN_COVERS')) {
373
            switch (true) {
374
                case substr($path, 0, 1) == '/' ||
375
                    substr($path, 1, 1) == ':' ||
376
                    substr($path, 0, 1) == '\\':
377
                    \define('NN_COVERS', self::trailingSlash($path));
378
                    break;
379
                case strlen($path) > 0 && substr($path, 0, 1) != '/' && substr($path, 1, 1) != ':' &&
380
                    substr($path, 0, 1) != '\\':
381
                    \define('NN_COVERS', realpath(NN_ROOT.self::trailingSlash($path)));
0 ignored issues
show
Bug introduced by
The constant Blacklight\utility\NN_ROOT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
382
                    break;
383
                case empty($path): // Default to resources location.
384
                default:
385
                    \define('NN_COVERS', NN_RES.'covers'.DS);
0 ignored issues
show
Bug introduced by
The constant Blacklight\utility\NN_RES was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The constant Blacklight\utility\DS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
386
            }
387
        }
388
    }
389
390
    /**
391
     * Creates an array to be used with stream_context_create() to verify openssl certificates
392
     * when connecting to a tls or ssl connection when using stream functions (fopen/file_get_contents/etc).
393
     *
394
     * @param bool $forceIgnore Force ignoring of verification.
395
     *
396
     * @return array
397
     * @static
398
     */
399
    public static function streamSslContextOptions($forceIgnore = false): array
400
    {
401
        if (config('nntmux_ssl.ssl_cafile') === '' && config('nntmux_ssl.ssl_capath') === '') {
402
            $options = [
403
                'verify_peer'       => false,
404
                'verify_peer_name'  => false,
405
                'allow_self_signed' => true,
406
            ];
407
        } else {
408
            $options = [
409
                'verify_peer'       => $forceIgnore ? false : config('nntmux_ssl.ssl_verify_peer'),
410
                'verify_peer_name'  => $forceIgnore ? false : config('nntmux_ssl.ssl_verify_host'),
411
                'allow_self_signed' => $forceIgnore ? true : config('nntmux_ssl.ssl_allow_self_signed'),
412
            ];
413
            if (config('nntmux_ssl.ssl_cafile') !== '') {
414
                $options['cafile'] = config('nntmux_ssl.ssl_cafile');
415
            }
416
            if (config('nntmux_ssl.ssl_capath') !== '') {
417
                $options['capath'] = config('nntmux_ssl.ssl_capath');
418
            }
419
        }
420
        // If we set the transport to tls and the server falls back to ssl,
421
        // the context options would be for tls and would not apply to ssl,
422
        // so set both tls and ssl context in case the server does not support tls.
423
        return ['tls' => $options, 'ssl' => $options];
424
    }
425
426
    /**
427
     * Set curl context options for verifying SSL certificates.
428
     *
429
     * @param bool $verify false = Ignore config.php and do not verify the openssl cert.
430
     *                     true  = Check config.php and verify based on those settings.
431
     *                     If you know the certificate will be self-signed, pass false.
432
     *
433
     * @return array
434
     * @static
435
     */
436
    public static function curlSslContextOptions($verify = true): array
437
    {
438
        $options = [];
439
        if ($verify && config('nntmux_ssl.ssl_verify_host') && (! empty(config('nntmux_ssl.ssl_cafile')) || ! empty(config('nntmux_ssl.ssl_capath')))) {
440
            $options += [
441
                CURLOPT_SSL_VERIFYPEER => (bool) config('nntmux_ssl.ssl_verify_peer'),
442
                CURLOPT_SSL_VERIFYHOST => config('nntmux_ssl.ssl_verify_host') ? 2 : 0,
443
            ];
444
            if (! empty(config('nntmux_ssl.ssl_cafile'))) {
445
                $options += [CURLOPT_CAINFO => config('nntmux_ssl.ssl_cafile')];
446
            }
447
            if (! empty(config('nntmux_ssl.ssl_capath'))) {
448
                $options += [CURLOPT_CAPATH => config('nntmux_ssl.ssl_capath')];
449
            }
450
        } else {
451
            $options += [
452
                CURLOPT_SSL_VERIFYPEER => false,
453
                CURLOPT_SSL_VERIFYHOST => 0,
454
            ];
455
        }
456
457
        return $options;
458
    }
459
460
    /**
461
     * Use cURL To download a web page into a string.
462
     *
463
     * @param array $options See details below.
464
     *
465
     * @return bool|mixed
466
     * @static
467
     */
468
    public static function getUrl(array $options = [])
469
    {
470
        $defaults = [
471
            'url'            => '',    // String ; The URL to download.
472
            'method'         => 'get', // String ; Http method, get/post/etc..
473
            'postdata'       => '',    // String ; Data to send on post method.
474
            'language'       => '',    // String ; Language in request header string.
475
            'debug'          => false, // Bool   ; Show curl debug information.
476
            'useragent'      => '',    // String ; User agent string.
477
            'cookie'         => '',    // String ; Cookie string.
478
            'requestheaders' => [],    // Array  ; List of request headers.
479
            //          Example: ["Content-Type: application/json", "DNT: 1"]
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
480
            'verifycert'     => true,  // Bool   ; Verify certificate authenticity?
481
            //          Since curl does not have a verify self signed certs option,
482
            //          you should use this instead if your cert is self signed.
483
        ];
484
485
        $options += $defaults;
486
487
        if (! $options['url']) {
488
            return false;
489
        }
490
491
        switch ($options['language']) {
492
            case 'fr':
493
            case 'fr-fr':
494
                $options['language'] = 'fr-fr';
495
                break;
496
            case 'de':
497
            case 'de-de':
498
                $options['language'] = 'de-de';
499
                break;
500
            case 'en-us':
501
                $options['language'] = 'en-us';
502
                break;
503
            case 'en-gb':
504
                $options['language'] = 'en-gb';
505
                break;
506
            case '':
507
            case 'en':
508
            default:
509
                $options['language'] = 'en';
510
        }
511
        $header[] = 'Accept-Language: '.$options['language'];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$header was never initialized. Although not strictly required by PHP, it is generally a good practice to add $header = array(); before regardless.
Loading history...
512
        if (\is_array($options['requestheaders'])) {
513
            $header += $options['requestheaders'];
514
        }
515
516
        $ch = curl_init();
517
518
        $context = [
519
            CURLOPT_URL            => $options['url'],
520
            CURLOPT_HTTPHEADER     => $header,
521
            CURLOPT_RETURNTRANSFER => 1,
522
            CURLOPT_FOLLOWLOCATION => 1,
523
            CURLOPT_TIMEOUT        => 15,
524
        ];
525
        $context += self::curlSslContextOptions($options['verifycert']);
526
        if (! empty($options['useragent'])) {
527
            $context += [CURLOPT_USERAGENT => $options['useragent']];
528
        }
529
        if (! empty($options['cookie'])) {
530
            $context += [CURLOPT_COOKIE => $options['cookie']];
531
        }
532
        if ($options['method'] === 'post') {
533
            $context += [
534
                CURLOPT_POST       => 1,
535
                CURLOPT_POSTFIELDS => $options['postdata'],
536
            ];
537
        }
538
        if ($options['debug']) {
539
            $context += [
540
                CURLOPT_HEADER      => true,
541
                CURLINFO_HEADER_OUT => true,
542
                CURLOPT_NOPROGRESS  => false,
543
                CURLOPT_VERBOSE     => true,
544
            ];
545
        }
546
        curl_setopt_array($ch, $context);
547
548
        $buffer = curl_exec($ch);
549
        $err = curl_errno($ch);
550
        curl_close($ch);
551
552
        if ($err !== 0) {
553
            return false;
554
        }
555
556
        return $buffer;
557
    }
558
559
    /**
560
     * Get human readable size string from bytes.
561
     *
562
     * @param int $size     Bytes number to convert.
563
     * @param int $precision How many floating point units to add.
564
     *
565
     * @return string
566
     */
567
    public static function bytesToSizeString($size, $precision = 0): string
568
    {
569
        static $units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
570
        $step = 1024;
571
        $i = 0;
572
        while (($size / $step) > 0.9) {
573
            $size /= $step;
574
            $i++;
575
        }
576
577
        return round($size, $precision).$units[$i];
578
    }
579
580
    /**
581
     * @param array $options
582
     *
583
     * @return string
584
     */
585
    public static function getCoverURL(array $options = []): string
586
    {
587
        $defaults = [
588
            'id'     => null,
589
            'suffix' => '-cover.jpg',
590
            'type'   => '',
591
        ];
592
        $options += $defaults;
593
        $fileSpecTemplate = '%s/%s%s';
594
        $fileSpec = '';
595
596
        if (! empty($options['id']) && \in_array(
597
            $options['type'],
598
                ['anime', 'audio', 'audiosample', 'book', 'console', 'games', 'movies', 'music', 'preview', 'sample', 'tvrage', 'video', 'xxx'],
599
            false
600
            )
601
        ) {
602
            $fileSpec = sprintf($fileSpecTemplate, $options['type'], $options['id'], $options['suffix']);
603
            $fileSpec = file_exists(NN_COVERS.$fileSpec) ? $fileSpec :
604
                sprintf($fileSpecTemplate, $options['type'], 'no', $options['suffix']);
605
        }
606
607
        return $fileSpec;
608
    }
609
610
    /**
611
     * Converts XML to an associative array with namespace preservation -- use if intending to JSON encode.
612
     * @author Tamlyn from Outlandish.com
613
     *
614
     * @param \SimpleXMLElement $xml The SimpleXML parsed XML string data
615
     * @param array             $options
616
     *
617
     * @return array            The associate array of the XML namespaced file
618
     */
619
    public static function xmlToArray(\SimpleXMLElement $xml, array $options = []): array
620
    {
621
        $defaults = [
622
            'namespaceSeparator' => ':', //you may want this to be something other than a colon
623
            'attributePrefix' => '@',   //to distinguish between attributes and nodes with the same name
624
            'alwaysArray' => [],   //array of xml tag names which should always become arrays
625
            'autoArray' => true,        //only create arrays for tags which appear more than once
626
            'textContent' => '$',       //key used for the text content of elements
627
            'autoText' => true,         //skip textContent key if node has no attributes or child nodes
628
            'keySearch' => false,       //optional search and replace on tag and attribute names
629
            'keyReplace' => false,       //replace values for above search values (as passed to str_replace())
630
        ];
631
        $options = array_merge($defaults, $options);
632
        $namespaces = $xml->getDocNamespaces();
633
        $namespaces[''] = null; //add base (empty) namespace
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
634
635
        $attributesArray = $tagsArray = [];
636
        foreach ($namespaces as $prefix => $namespace) {
637
            //get attributes from all namespaces
638
            foreach ($xml->attributes($namespace) as $attributeName => $attribute) {
639
                //replace characters in attribute name
640
                if ($options['keySearch']) {
641
                    $attributeName =
642
                    str_replace($options['keySearch'], $options['keyReplace'], $attributeName);
643
                }
644
                $attributeKey = $options['attributePrefix']
645
                    .($prefix ? $prefix.$options['namespaceSeparator'] : '')
646
                    .$attributeName;
647
                $attributesArray[$attributeKey] = (string) $attribute;
648
            }
649
            //get child nodes from all namespaces
650
            foreach ($xml->children($namespace) as $childXml) {
651
                //recurse into child nodes
652
                $childArray = self::xmlToArray($childXml, $options);
653
                $childTagName = key($childArray);
654
                $childProperties = current($childArray);
655
656
                //replace characters in tag name
657
                if ($options['keySearch']) {
658
                    $childTagName =
659
                    str_replace($options['keySearch'], $options['keyReplace'], $childTagName);
660
                }
661
                //add namespace prefix, if any
662
                if ($prefix) {
663
                    $childTagName = $prefix.$options['namespaceSeparator'].$childTagName;
664
                }
665
666
                if (! isset($tagsArray[$childTagName])) {
667
                    //only entry with this key
668
                    //test if tags of this type should always be arrays, no matter the element count
669
                    $tagsArray[$childTagName] =
670
                        \in_array($childTagName, $options['alwaysArray'], false) || ! $options['autoArray']
671
                            ? [$childProperties] : $childProperties;
672
                } elseif (
673
                    \is_array($tagsArray[$childTagName]) && array_keys($tagsArray[$childTagName])
674
                    === range(0, \count($tagsArray[$childTagName]) - 1)
675
                ) {
676
                    //key already exists and is integer indexed array
677
                    $tagsArray[$childTagName][] = $childProperties;
678
                } else {
679
                    //key exists so convert to integer indexed array with previous value in position 0
680
                    $tagsArray[$childTagName] = [$tagsArray[$childTagName], $childProperties];
681
                }
682
            }
683
        }
684
685
        //get text content of node
686
        $textContentArray = [];
687
        $plainText = trim((string) $xml);
688
        if ($plainText !== '') {
689
            $textContentArray[$options['textContent']] = $plainText;
690
        }
691
692
        //stick it all together
693
        $propertiesArray = ! $options['autoText'] || $attributesArray || $tagsArray || ($plainText === '')
694
            ? array_merge($attributesArray, $tagsArray, $textContentArray) : $plainText;
695
696
        //return node as array
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
697
        return [
698
            $xml->getName() => $propertiesArray,
699
        ];
700
    }
701
702
    /**
703
     * Return file type/info using magic numbers.
704
     * Try using `file` program where available, fallback to using PHP's finfo class.
705
     *
706
     * @param string $path Path to the file / folder to check.
707
     *
708
     * @return string File info. Empty string on failure.
709
     * @throws \Exception
710
     */
711
    public static function fileInfo($path)
712
    {
713
        $magicPath = Settings::settingValue('apps.indexer.magic_file_path');
0 ignored issues
show
Bug introduced by
'apps.indexer.magic_file_path' of type string is incompatible with the type boolean|array expected by parameter $setting of App\Models\Settings::settingValue(). ( Ignorable by Annotation )

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

713
        $magicPath = Settings::settingValue(/** @scrutinizer ignore-type */ 'apps.indexer.magic_file_path');
Loading history...
714
        if (self::hasCommand('file') && (! self::isWin() || ! empty($magicPath))) {
715
            $magicSwitch = empty($magicPath) ? '' : " -m $magicPath";
716
            $output = self::runCmd('file'.$magicSwitch.' -b "'.$path.'"');
717
718
            if (\is_array($output)) {
0 ignored issues
show
introduced by
The condition is_array($output) is always true.
Loading history...
719
                switch (\count($output)) {
720
                    case 0:
721
                        $output = '';
722
                        break;
723
                    case 1:
724
                        $output = $output[0];
725
                        break;
726
                    default:
727
                        $output = implode(' ', $output);
728
                        break;
729
                }
730
            } else {
731
                $output = '';
732
            }
733
        } else {
734
            $fileInfo = empty($magicPath) ? finfo_open(FILEINFO_RAW) : finfo_open(FILEINFO_RAW, $magicPath);
735
736
            $output = finfo_file($fileInfo, $path);
737
            if (empty($output)) {
738
                $output = '';
739
            }
740
            finfo_close($fileInfo);
741
        }
742
743
        return $output;
744
    }
745
746
    /**
747
     * @param $code
748
     *
749
     * @return bool
750
     */
751
    public function checkStatus($code)
752
    {
753
        return $code === 0;
754
    }
755
756
    /**
757
     * Convert Code page 437 chars to UTF.
758
     *
759
     * @param string $string
760
     *
761
     * @return string
762
     */
763
    public static function cp437toUTF($string): string
764
    {
765
        return iconv('CP437', 'UTF-8//IGNORE//TRANSLIT', $string);
766
    }
767
768
    /**
769
     * Fetches an embeddable video to a IMDB trailer from http://www.traileraddict.com.
770
     *
771
     * @param $imdbID
772
     *
773
     * @return string
774
     */
775
    public static function imdb_trailers($imdbID): string
776
    {
777
        $xml = self::getUrl(['url' => 'http://api.traileraddict.com/?imdb='.$imdbID]);
778
        if ($xml !== false) {
779
            if (preg_match('#(v\.traileraddict\.com/\d+)#i', $xml, $html)) {
780
                return 'https://'.$html[1];
781
            }
782
        }
783
784
        return '';
785
    }
786
787
    /**
788
     * Check if O/S is windows.
789
     *
790
     * @return bool
791
     */
792
    public static function isWindows(): bool
793
    {
794
        return self::isWin();
795
    }
796
797
    /**
798
     * Convert obj to array.
799
     *
800
     * @param       $arrObjData
801
     * @param array $arrSkipIndices
802
     *
803
     * @return array
804
     */
805
    public static function objectsIntoArray($arrObjData, array $arrSkipIndices = []): array
806
    {
807
        $arrData = [];
808
809
        // If input is object, convert into array.
810
        if (\is_object($arrObjData)) {
811
            $arrObjData = get_object_vars($arrObjData);
812
        }
813
814
        if (\is_array($arrObjData)) {
815
            foreach ($arrObjData as $index => $value) {
816
                // Recursive call.
817
                if (\is_object($value) || \is_array($value)) {
818
                    $value = self::objectsIntoArray($value, $arrSkipIndices);
819
                }
820
                if (\in_array($index, $arrSkipIndices, false)) {
821
                    continue;
822
                }
823
                $arrData[$index] = $value;
824
            }
825
        }
826
827
        return $arrData;
828
    }
829
830
    /**
831
     * Run CLI command.
832
     *
833
     * @param string $command
834
     * @param bool   $debug
835
     *
836
     * @return array
837
     */
838
    public static function runCmd($command, $debug = false)
839
    {
840
        $nl = PHP_EOL;
841
        if (self::isWindows() && strpos(PHP_VERSION, '5.3') !== false) {
842
            $command = '"'.$command.'"';
843
        }
844
845
        if ($debug) {
846
            echo '-Running Command: '.$nl.'   '.$command.$nl;
847
        }
848
849
        $output = [];
850
        $status = 1;
851
        @exec($command, $output, $status);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for exec(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

851
        /** @scrutinizer ignore-unhandled */ @exec($command, $output, $status);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
852
853
        if ($debug) {
854
            echo '-Command Output: '.$nl.'   '.implode($nl.'  ', $output).$nl;
855
        }
856
857
        return $output;
858
    }
859
860
    /**
861
     * Remove unsafe chars from a filename.
862
     *
863
     * @param string $filename
864
     *
865
     * @return string
866
     */
867
    public static function safeFilename($filename)
868
    {
869
        return trim(preg_replace('/[^\w\s.-]*/i', '', $filename));
870
    }
871
872
    /**
873
     * @return string
874
     */
875
    public static function generateUuid(): string
876
    {
877
        return Str::uuid()->toString();
878
    }
879
880
    public static function responseXmlToObject($input)
881
    {
882
        $input = str_replace('<newznab:', '<', $input);
883
884
        return @simplexml_load_string($input);
885
    }
886
887
    /**
888
     * @note: Convert non-UTF-8 characters into UTF-8
889
     * Function taken from http://stackoverflow.com/a/19366999
890
     *
891
     * @param $data
892
     *
893
     * @return array|string
894
     */
895
    public static function encodeAsUTF8($data)
896
    {
897
        if (\is_array($data)) {
898
            foreach ($data as $key => $value) {
899
                $data[$key] = self::encodeAsUTF8($value);
900
            }
901
        } else {
902
            if (\is_string($data)) {
903
                return utf8_encode($data);
904
            }
905
        }
906
907
        return $data;
908
    }
909
910
    /**
911
     * This function turns a roman numeral into an integer.
912
     *
913
     * @param string $string
914
     *
915
     * @return int $e
916
     */
917
    public static function convertRomanToInt($string): int
918
    {
919
        switch (strtolower($string)) {
920
            case 'i': $e = 1;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
921
                break;
922
            case 'ii': $e = 2;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
923
                break;
924
            case 'iii': $e = 3;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
925
                break;
926
            case 'iv': $e = 4;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
927
                break;
928
            case 'v': $e = 5;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
929
                break;
930
            case 'vi': $e = 6;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
931
                break;
932
            case 'vii': $e = 7;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
933
                break;
934
            case 'viii': $e = 8;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
935
                break;
936
            case 'ix': $e = 9;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
937
                break;
938
            case 'x': $e = 10;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
939
                break;
940
            case 'xi': $e = 11;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
941
                break;
942
            case 'xii': $e = 12;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
943
                break;
944
            case 'xiii': $e = 13;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
945
                break;
946
            case 'xiv': $e = 14;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
947
                break;
948
            case 'xv': $e = 15;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
949
                break;
950
            case 'xvi': $e = 16;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
951
                break;
952
            case 'xvii': $e = 17;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
953
                break;
954
            case 'xviii': $e = 18;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
955
                break;
956
            case 'xix': $e = 19;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
957
                break;
958
            case 'xx': $e = 20;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
959
                break;
960
            default:
961
                $e = 0;
962
        }
963
964
        return $e;
965
    }
966
967
    /**
968
     * Display error/error code.
969
     * @param int    $errorCode
970
     * @param string $errorText
971
     */
972
    public static function showApiError($errorCode = 900, $errorText = ''): void
973
    {
974
        $errorHeader = 'HTTP 1.1 400 Bad Request';
975
        if ($errorText === '') {
976
            switch ($errorCode) {
977
                case 100:
978
                    $errorText = 'Incorrect user credentials';
979
                    $errorHeader = 'HTTP 1.1 401 Unauthorized';
980
                    break;
981
                case 101:
982
                    $errorText = 'Account suspended';
983
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
984
                    break;
985
                case 102:
986
                    $errorText = 'Insufficient privileges/not authorized';
987
                    $errorHeader = 'HTTP 1.1 401 Unauthorized';
988
                    break;
989
                case 103:
990
                    $errorText = 'Registration denied';
991
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
992
                    break;
993
                case 104:
994
                    $errorText = 'Registrations are closed';
995
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
996
                    break;
997
                case 105:
998
                    $errorText = 'Invalid registration (Email Address Taken)';
999
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
1000
                    break;
1001
                case 106:
1002
                    $errorText = 'Invalid registration (Email Address Bad Format)';
1003
                    $errorHeader = 'HTTP 1.1 403 Forbidden';
1004
                    break;
1005
                case 107:
1006
                    $errorText = 'Registration Failed (Data error)';
1007
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
1008
                    break;
1009
                case 200:
1010
                    $errorText = 'Missing parameter';
1011
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
1012
                    break;
1013
                case 201:
1014
                    $errorText = 'Incorrect parameter';
1015
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
1016
                    break;
1017
                case 202:
1018
                    $errorText = 'No such function';
1019
                    $errorHeader = 'HTTP 1.1 404 Not Found';
1020
                    break;
1021
                case 203:
1022
                    $errorText = 'Function not available';
1023
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
1024
                    break;
1025
                case 300:
1026
                    $errorText = 'No such item';
1027
                    $errorHeader = 'HTTP 1.1 404 Not Found';
1028
                    break;
1029
                case 500:
1030
                    $errorText = 'Request limit reached';
1031
                    $errorHeader = 'HTTP 1.1 429 Too Many Requests';
1032
                    break;
1033
                case 501:
1034
                    $errorText = 'Download limit reached';
1035
                    $errorHeader = 'HTTP 1.1 429 Too Many Requests';
1036
                    break;
1037
                case 910:
1038
                    $errorText = 'API disabled';
1039
                    $errorHeader = 'HTTP 1.1 401 Unauthorized';
1040
                    break;
1041
                default:
1042
                    $errorText = 'Unknown error';
1043
                    $errorHeader = 'HTTP 1.1 400 Bad Request';
1044
                    break;
1045
            }
1046
        }
1047
1048
        $response =
1049
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".
1050
            '<error code="'.$errorCode.'" description="'.$errorText."\"/>\n";
1051
        header('Content-type: text/xml');
1052
        header('Content-Length: '.strlen($response));
1053
        header('X-NNTmux: API ERROR ['.$errorCode.'] '.$errorText);
1054
        header($errorHeader);
1055
1056
        exit($response);
1057
    }
1058
1059
    /**
1060
     * Simple function to reduce duplication in html string formatting.
1061
     *
1062
     * @param $string
1063
     *
1064
     * @return string
1065
     */
1066
    public static function htmlfmt($string): string
1067
    {
1068
        return htmlspecialchars($string, ENT_QUOTES, 'utf-8');
1069
    }
1070
1071
    /**
1072
     * Convert multi to single dimensional array
1073
     * Code taken from http://stackoverflow.com/a/12309103.
1074
     *
1075
     * @param $array
1076
     *
1077
     * @param $separator
1078
     *
1079
     * @return string
1080
     */
1081
    public static function convertMultiArray($array, $separator): string
1082
    {
1083
        return implode("$separator", array_map(function ($a) {
1084
            return implode(',', $a);
1085
        }, $array));
1086
    }
1087
1088
    /**
1089
     * @param $tableName
1090
     * @param $start
1091
     * @param $num
1092
     *
1093
     * @return array
1094
     * @throws \Exception
1095
     */
1096
    public static function getRange($tableName, $start, $num): array
1097
    {
1098
        $pdo = new DB();
1099
1100
        return $pdo->query(
1101
            sprintf(
1102
                'SELECT * %s FROM %s ORDER BY created_at DESC %s',
1103
                ($tableName === 'xxxinfo' ? ', UNCOMPRESS(plot) AS plot' : ''),
1104
                $tableName,
1105
                ($start === false ? '' : ('LIMIT '.$num.' OFFSET '.$start))
1106
            )
1107
        );
1108
    }
1109
1110
    /**
1111
     * @param $tableName
1112
     *
1113
     * @return int
1114
     * @throws \Exception
1115
     */
1116
    public static function getCount($tableName): int
1117
    {
1118
        $pdo = new DB();
1119
        $res = $pdo->queryOneRow(sprintf('SELECT COUNT(id) AS num FROM %s', $tableName));
1120
1121
        return $res === false ? 0 : $res['num'];
1122
    }
1123
1124
    /**
1125
     * @return bool
1126
     */
1127
    public static function checkCSRFToken(): bool
1128
    {
1129
        return request()->has('_token') && hash_equals($_SESSION['_token'], request()->input('_token'));
1130
    }
1131
}
1132