Passed
Push — develop ( 8ba7ca...a77f80 )
by Nikolay
07:08 queued 02:47
created

Util::getExtensionOfFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 1
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System;
21
22
use DateTime;
23
use Exception;
24
use MikoPBX\Common\Models\{CustomFiles};
25
use MikoPBX\Common\Providers\AmiConnectionCommand;
26
use MikoPBX\Common\Providers\AmiConnectionListener;
27
use MikoPBX\Common\Providers\LoggerProvider;
28
use MikoPBX\Common\Providers\TranslationProvider;
29
use MikoPBX\Core\Asterisk\AsteriskManager;
30
use Phalcon\Di;
31
use ReflectionClass;
32
use ReflectionException;
33
use Throwable;
34
35
/**
36
 * Class Util
37
 *
38
 * Universal commands and procedures
39
 *
40
 * @package MikoPBX\Core\System
41
 */
42
class Util
43
{
44
45
    /**
46
     * Overrides configuration array with manual attributes for a specific section.
47
     *
48
     * @param array $options The original configuration options.
49
     * @param array|null $manual_attributes The manual attributes to override the options.
50
     * @param string $section The section to override.
51
     *
52
     * @return string The resulting configuration string.
53
     */
54
    public static function overrideConfigurationArray($options, $manual_attributes, $section): string
55
    {
56
        $result_config = '';
57
        if ($manual_attributes !== null && isset($manual_attributes[$section])) {
58
            foreach ($manual_attributes[$section] as $key => $value) {
59
                if ($key === 'type') {
60
                    continue;
61
                }
62
                $options[$key] = $value;
63
            }
64
        }
65
        foreach ($options as $key => $value) {
66
            if (empty($value) || empty($key)) {
67
                continue;
68
            }
69
            if (is_array($value)) {
70
                array_unshift($value, ' ');
71
                $result_config .= trim(implode("\n{$key} = ", $value)) . "\n";
72
            } else {
73
                $result_config .= "{$key} = {$value}\n";
74
            }
75
        }
76
77
        return "$result_config\n";
78
    }
79
80
    /**
81
     * Initiates a call using the Asterisk Manager Interface (AMI).
82
     *
83
     * @param string $peer_number The peer number.
84
     * @param string $peer_mobile The peer mobile number.
85
     * @param string $dest_number The destination number.
86
     *
87
     * @return array The result of the Originate command.
88
     */
89
    public static function amiOriginate(string $peer_number, string $peer_mobile, string $dest_number): array
90
    {
91
        $am = self::getAstManager('off');
92
        $channel = 'Local/' . $peer_number . '@internal-originate';
93
        $context = 'all_peers';
94
        $variable = "pt1c_cid={$dest_number},__peer_mobile={$peer_mobile}";
95
96
        return $am->Originate(
97
            $channel,
98
            $dest_number,
99
            $context,
100
            '1',
101
            null,
102
            null,
103
            null,
104
            null,
105
            $variable,
106
            null,
107
            true
108
        );
109
    }
110
111
    /**
112
     * Retrieves the Asterisk Manager object.
113
     *
114
     * @param string $events Whether to enable events or commands.
115
     *
116
     * @return AsteriskManager The Asterisk Manager object.
117
     *
118
     * @throws \Phalcon\Exception
119
     */
120
    public static function getAstManager(string $events = 'on'): AsteriskManager
121
    {
122
        if ($events === 'on') {
123
            $nameService = AmiConnectionListener::SERVICE_NAME;
124
        } else {
125
            $nameService = AmiConnectionCommand::SERVICE_NAME;
126
        }
127
        $di = Di::getDefault();
128
        if ($di === null) {
129
            throw new \Phalcon\Exception("di not found");
130
        }
131
        $am = $di->getShared($nameService);
132
        if (is_resource($am->socket)) {
133
            return $am;
134
        }
135
136
        return $di->get($nameService);
137
    }
138
139
    /**
140
     * Generates a random string of a given length.
141
     *
142
     * @param int $length The length of the random string (default: 10).
143
     *
144
     * @return string The generated random string.
145
     */
146
    public static function generateRandomString(int $length = 10): string
147
    {
148
        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
149
        $charactersLength = strlen($characters);
150
        $randomString = '';
151
        for ($i = 0; $i < $length; $i++) {
152
            try {
153
                $randomString .= $characters[random_int(0, $charactersLength - 1)];
154
            } catch (Throwable $e) {
155
                $randomString = '';
156
            }
157
        }
158
159
        return $randomString;
160
    }
161
162
    /**
163
     * Validates if a string is a valid JSON.
164
     *
165
     * @param mixed $jsonString The string to validate.
166
     *
167
     * @return bool True if the string is a valid JSON, false otherwise.
168
     */
169
    public static function isJson($jsonString): bool
170
    {
171
        json_decode($jsonString, true);
172
173
        return (json_last_error() === JSON_ERROR_NONE);
174
    }
175
176
    /**
177
     * Returns the size of a file in megabytes.
178
     *
179
     * @param string $filename The filename.
180
     *
181
     * @return float|int The size of the file in megabytes.
182
     */
183
    public static function mFileSize(string $filename)
184
    {
185
        $size = 0;
186
        if (file_exists($filename)) {
187
            $tmp_size = filesize($filename);
188
            if ($tmp_size !== false) {
189
                // Convert size to megabytes
190
                $size = $tmp_size;
191
            }
192
        }
193
194
        return $size;
195
    }
196
197
    /**
198
     * Returns a string with the specified number of 'X' characters.
199
     *
200
     * @param int $length The length of the string.
201
     *
202
     * @return string The string with 'X' characters.
203
     */
204
    public static function getExtensionX(int $length): string
205
    {
206
        $extension = '';
207
        for ($i = 0; $i < $length; $i++) {
208
            $extension .= 'X';
209
        }
210
211
        return $extension;
212
    }
213
214
    /**
215
     * Checks if a file exists and has a non-zero size.
216
     *
217
     * @param string $filename The filename.
218
     *
219
     * @return bool True if the file exists and has a non-zero size, false otherwise.
220
     */
221
    public static function recFileExists(string $filename): ?bool
222
    {
223
        return (file_exists($filename) && filesize($filename) > 0);
224
    }
225
226
    /**
227
     * Converts a number to a date if the input is numeric.
228
     *
229
     * @param mixed $data The input data.
230
     *
231
     * @return string The converted date string or the original input data.
232
     */
233
    public static function numberToDate($data): string
234
    {
235
        $re_number = '/^\d+.\d+$/';
236
        preg_match_all($re_number, $data, $matches, PREG_SET_ORDER, 0);
237
        if (count($matches) > 0) {
238
            $data = date('Y.m.d-H:i:s', $data);
239
        }
240
241
        return $data;
242
    }
243
244
    /**
245
     * Writes content to a file.
246
     *
247
     * @param string $filename The path of the file.
248
     * @param mixed $data The data to write to the file.
249
     *
250
     * @return void
251
     */
252
    public static function fileWriteContent(string $filename, $data): void
253
    {
254
        /** @var CustomFiles $res */
255
        $res = CustomFiles::findFirst("filepath = '{$filename}'");
256
257
        $filename_orgn = "{$filename}.orgn";
258
259
        // Check if CustomFiles entry exists and its mode is 'none'
260
        if (($res === null || $res->mode === 'none') && file_exists($filename_orgn)) {
261
            unlink($filename_orgn);
262
        } // Check if CustomFiles entry exists and its mode is not 'none'
263
        elseif ($res !== null && $res->mode !== 'none') {
264
            // Write the original file
265
            file_put_contents($filename_orgn, $data);
266
        }
267
268
        if ($res === null) {
269
            // File is not yet registered in the database, create a new CustomFiles entry
270
            $res = new CustomFiles();
271
            $res->writeAttribute('filepath', $filename);
272
            $res->writeAttribute('mode', 'none');
273
            $res->save();
274
        } elseif ($res->mode === 'append') {
275
            // Append to the file
276
            $data .= "\n\n";
277
            $data .= base64_decode((string)$res->content);
278
        } elseif ($res->mode === 'override') {
279
            // Override the file
280
            $data = base64_decode((string)$res->content);
281
        }
282
        file_put_contents($filename, $data);
283
    }
284
285
    /**
286
     * Adds messages to Syslog.
287
     *
288
     * @param string $ident The category, class, or method identification.
289
     * @param string $message The log message.
290
     * @param int $level The log level (default: LOG_WARNING).
291
     *
292
     * @return void
293
     */
294
    public static function sysLogMsg(string $ident, string $message, $level = LOG_WARNING): void
295
    {
296
        /** @var \Phalcon\Logger $logger */
297
        $logger = Di::getDefault()->getShared(LoggerProvider::SERVICE_NAME);
298
        $logger->log($level, "{$message} on {$ident}");
299
    }
300
301
    /**
302
     * Returns the current date as a string with millisecond precision.
303
     *
304
     * @return string|null The current date string, or null on error.
305
     */
306
    public static function getNowDate(): ?string
307
    {
308
        $result = null;
309
        try {
310
            $d = new DateTime();
311
            $result = $d->format("Y-m-d H:i:s.v");
312
        } catch (Exception $e) {
313
            unset($e);
314
        }
315
316
        return $result;
317
    }
318
319
    /**
320
     * Retrieves the extension of a file.
321
     *
322
     * @param string $filename The filename.
323
     *
324
     * @return string The extension of the file.
325
     */
326
    public static function getExtensionOfFile(string $filename): string
327
    {
328
        $path_parts = pathinfo($filename);
329
330
        return $path_parts['extension'] ?? '';
331
    }
332
333
    /**
334
     * Removes the extension from a filename.
335
     *
336
     * @param string $filename The filename.
337
     * @param string $delimiter The delimiter character (default: '.').
338
     *
339
     * @return string The filename without the extension.
340
     */
341
    public static function trimExtensionForFile(string $filename, string $delimiter = '.'): string
342
    {
343
        $tmp_arr = explode((string)$delimiter, $filename);
344
        if (count($tmp_arr) > 1) {
345
            unset($tmp_arr[count($tmp_arr) - 1]);
346
            $filename = implode((string)$delimiter, $tmp_arr);
347
        }
348
349
        return $filename;
350
    }
351
352
    /**
353
     * Get the size of a file in kilobytes.
354
     *
355
     * @param string $filename The path to the file.
356
     * @return float The size of the file in kilobytes.
357
     */
358
    public static function getSizeOfFile(string $filename): float
359
    {
360
        $result = 0;
361
        if (file_exists($filename)) {
362
            $duPath = self::which('du');
363
            $awkPath = self::which('awk');
364
            Processes::mwExec("{$duPath} -d 0 -k '{$filename}' | {$awkPath}  '{ print $1}'", $out);
365
            $time_str = implode($out);
366
            preg_match_all('/^\d+$/', $time_str, $matches, PREG_SET_ORDER, 0);
367
            if (count($matches) > 0) {
368
                $result = round(1 * $time_str / 1024, 2);
369
            }
370
        }
371
372
        return $result;
373
    }
374
375
    /**
376
     * Searches for the executable path of a command.
377
     *
378
     * @param string $cmd The command to search for.
379
     *
380
     * @return string The path of the executable command, or the command itself if not found.
381
     */
382
    public static function which(string $cmd): string
383
    {
384
        global $_ENV;
385
        if (array_key_exists('PATH', $_ENV)) {
386
            $binaryFolders = $_ENV['PATH'];
387
388
            // Search for the command in each binary folder
389
            foreach (explode(':', $binaryFolders) as $path) {
390
                if (is_executable("{$path}/{$cmd}")) {
391
                    return "{$path}/{$cmd}";
392
                }
393
            }
394
        }
395
396
        // Default binary folders to search if PATH is not set or command is not found
397
        $binaryFolders =
398
            [
399
                '/sbin',
400
                '/bin',
401
                '/usr/sbin',
402
                '/usr/bin',
403
                '/usr/local/bin',
404
                '/usr/local/sbin',
405
            ];
406
407
        // Search for the command in the default binary folders
408
        foreach ($binaryFolders as $path) {
409
            if (is_executable("{$path}/{$cmd}")) {
410
                return "{$path}/{$cmd}";
411
            }
412
        }
413
414
        return $cmd;
415
    }
416
417
    /**
418
     * Checks if a password is simple based on a dictionary.
419
     *
420
     * @param string $value The password to check.
421
     *
422
     * @return bool True if the password is found in the dictionary, false otherwise.
423
     */
424
    public static function isSimplePassword($value): bool
425
    {
426
        $passwords = [];
427
        Processes::mwExec('/bin/zcat /usr/share/wordlists/rockyou.txt.gz', $passwords);
428
        return in_array($value, $passwords, true);
429
    }
430
431
    /**
432
     * Sets the Cyrillic font for the console.
433
     *
434
     * @return void
435
     */
436
    public static function setCyrillicFont(): void
437
    {
438
        $setfontPath = self::which('setfont');
439
        Processes::mwExec("{$setfontPath} /usr/share/consolefonts/Cyr_a8x16.psfu.gz 2>/dev/null");
440
    }
441
442
    /**
443
     * Translates a text string.
444
     *
445
     * @param string $text The text to translate.
446
     * @param bool $cliLang Whether to use CLI language or web language (default: true).
447
     *
448
     * @return string The translated text.
449
     */
450
    public static function translate(string $text, bool $cliLang = true): string
451
    {
452
        $di = Di::getDefault();
453
        if ($di !== null) {
454
            if (!$cliLang) {
455
                $di->setShared('PREFERRED_LANG_WEB', true);
456
            }
457
            $text = $di->getShared(TranslationProvider::SERVICE_NAME)->_($text);
458
            if (!$cliLang) {
459
                $di->remove('PREFERRED_LANG_WEB');
460
            }
461
        }
462
        return $text;
463
    }
464
465
    /**
466
     * Recursively deletes a directory.
467
     *
468
     * @param string $dir The directory path to delete.
469
     *
470
     * @return void
471
     *
472
     * @link http://php.net/manual/en/function.rmdir.php
473
     */
474
    public static function rRmDir(string $dir): void
475
    {
476
        if (is_dir($dir)) {
477
            $objects = scandir($dir);
478
479
            // Recursively delete files and subdirectories
480
            foreach ($objects as $object) {
481
                if ($object != "." && $object != "..") {
482
                    if (filetype($dir . "/" . $object) == "dir") {
483
                        self::rRmDir($dir . "/" . $object);
484
                    } else {
485
                        unlink($dir . "/" . $object);
486
                    }
487
                }
488
            }
489
490
            // Reset the array pointer and remove the directory
491
            if ($objects !== false) {
492
                reset($objects);
493
            }
494
            rmdir($dir);
495
        }
496
    }
497
498
    /**
499
     * Generates an SSL certificate.
500
     *
501
     * @param array|null $options The options for the certificate (default: null).
502
     * @param array|null $config_args_pkey The configuration arguments for the private key (default: null).
503
     * @param array|null $config_args_csr The configuration arguments for the CSR (default: null).
504
     *
505
     * @return array The generated SSL certificate (public and private keys).
506
     */
507
    public static function generateSslCert($options = null, $config_args_pkey = null, $config_args_csr = null): array
508
    {
509
        // Initialize options if not provided
510
        if (!$options) {
511
            $options = [
512
                "countryName" => 'RU',
513
                "stateOrProvinceName" => 'Moscow',
514
                "localityName" => 'Zelenograd',
515
                "organizationName" => 'MIKO LLC',
516
                "organizationalUnitName" => 'Software development',
517
                "commonName" => 'MIKO PBX',
518
                "emailAddress" => '[email protected]',
519
            ];
520
        }
521
522
        // Initialize CSR configuration arguments if not provided
523
        if (!$config_args_csr) {
524
            $config_args_csr = ['digest_alg' => 'sha256'];
525
        }
526
527
        // Initialize private key configuration arguments if not provided
528
        if (!$config_args_pkey) {
529
            $config_args_pkey = [
530
                "private_key_bits" => 2048,
531
                "private_key_type" => OPENSSL_KEYTYPE_RSA,
532
            ];
533
        }
534
535
        // Generate keys
536
        $private_key = openssl_pkey_new($config_args_pkey);
537
        $csr = openssl_csr_new($options, $private_key, $config_args_csr);
0 ignored issues
show
Bug introduced by
It seems like $private_key can also be of type resource; however, parameter $private_key of openssl_csr_new() does only seem to accept OpenSSLAsymmetricKey, 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

537
        $csr = openssl_csr_new($options, /** @scrutinizer ignore-type */ $private_key, $config_args_csr);
Loading history...
538
        $x509 = openssl_csr_sign($csr, null, $private_key, $days = 3650, $config_args_csr);
539
540
        // Export keys
541
        openssl_x509_export($x509, $certout);
542
        openssl_pkey_export($private_key, $pkeyout);
543
        // echo $pkeyout; // -> WEBHTTPSPrivateKey
544
        // echo $certout; // -> WEBHTTPSPublicKey
545
        return ['PublicKey' => $certout, 'PrivateKey' => $pkeyout];
546
    }
547
548
    /**
549
     * Checks whether the current system is t2 SDE build.
550
     *
551
     * @return bool True if the system is t2 SDE build, false otherwise.
552
     */
553
    public static function isT2SdeLinux(): bool
554
    {
555
        return !self::isSystemctl() && !self::isDocker();
556
    }
557
558
    /**
559
     * Checks whether the current system has systemctl installed and executable.
560
     *
561
     * @return bool True if systemctl is available, false otherwise.
562
     */
563
    public static function isSystemctl(): bool
564
    {
565
        $pathSystemCtl = self::which('systemctl');
566
        return !empty($pathSystemCtl) && is_executable($pathSystemCtl);
567
    }
568
569
    /**
570
     * Checks whether the current process is running inside a Docker container.
571
     *
572
     * @return bool True if the process is inside a container, false otherwise.
573
     */
574
    public static function isDocker(): bool
575
    {
576
        return file_exists('/.dockerenv');
577
    }
578
579
    /**
580
     * Outputs a message to the main teletype.
581
     *
582
     * @param string $message The message to output.
583
     * @param string $ttyPath The path to the teletype device (default: '/dev/ttyS0').
584
     *
585
     * @return void
586
     */
587
    public static function teletypeEcho(string $message, string $ttyPath = '/dev/ttyS0'): void
588
    {
589
        $pathBusyBox = self::which('busybox');
590
        $ttyTittle = trim(shell_exec("$pathBusyBox setserial -g $ttyPath 2> /dev/null"));
591
        if (strpos($ttyTittle, $ttyPath) !== false && strpos($ttyTittle, 'unknown') === false) {
592
            @file_put_contents($ttyPath, $message, FILE_APPEND);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for file_put_contents(). 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

592
            /** @scrutinizer ignore-unhandled */ @file_put_contents($ttyPath, $message, FILE_APPEND);

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...
593
        }
594
    }
595
596
    /**
597
     * Echoes a teletype message with "DONE" or "FAIL" status.
598
     *
599
     * @param string $message The main message to display.
600
     * @param mixed $result The result status.
601
     *
602
     * @return void
603
     */
604
    public static function teletypeEchoDone(string $message, $result): void
605
    {
606
        $len = max(0, 80 - strlen($message) - 9);
607
        $spaces = str_repeat('.', $len);
608
        if ($result === false) {
609
            $message = " \033[31;1mFAIL\033[0m \n";
610
        } else {
611
            $message = " \033[32;1mDONE\033[0m \n";
612
        }
613
        self::teletypeEcho($spaces . $message);
614
    }
615
616
    /**
617
     * Echoes a "DONE" or "FAIL" message based on the result status.
618
     *
619
     * @param bool $result The result status (true by default).
620
     *
621
     * @return void
622
     */
623
    public static function echoDone(bool $result = true): void
624
    {
625
        if ($result === false) {
626
            echo "\033[31;1mFAIL\033[0m \n";
627
        } else {
628
            echo "\033[32;1mDONE\033[0m \n";
629
        }
630
    }
631
632
    /**
633
     * Echoes a result message with progress dots.
634
     *
635
     * @param string $message The result message to echo.
636
     * @param bool $result The result status (true by default).
637
     *
638
     * @return void
639
     */
640
    public static function echoResult(string $message, bool $result = true): void
641
    {
642
        $cols = self::getCountCols();
643
        if (!is_numeric($cols)) {
644
            // Failed to retrieve the screen width.
645
            return;
646
        }
647
        $len = $cols - strlen($message) - 8;
648
        if ($len < 2) {
649
            // Incorrect screen width.
650
            return;
651
        }
652
653
        $spaces = str_repeat('.', $len);
654
        echo "\r" . $message . $spaces;
655
        self::echoDone($result);
656
    }
657
658
    /**
659
     * Gets the count of columns in the terminal window.
660
     *
661
     * @return string The count of columns.
662
     */
663
    public static function getCountCols(): string
664
    {
665
        $len = 1 * trim(shell_exec('tput cols'));
666
667
        // If the count of columns is zero, set it to a default value of 80
668
        if ($len === 0) {
669
            $len = 80;
670
        } else {
671
            // Limit the count of columns to a maximum of 80
672
            $len = min($len, 80);
673
        }
674
        return $len;
675
    }
676
677
    /**
678
     * Creates or updates a symlink to a target path.
679
     *
680
     * @param string $target The target path.
681
     * @param string $link The symlink path.
682
     * @param bool $isFile Whether the symlink should point to a file (false by default).
683
     *
684
     * @return bool True if the symlink was created or updated, false otherwise.
685
     */
686
    public static function createUpdateSymlink(string $target, string $link, bool $isFile = false): bool
687
    {
688
        $need_create_link = true;
689
        if (is_link($link)) {
690
            $old_target = readlink($link);
691
            $need_create_link = ($old_target != $target);
692
693
            // If needed, remove the old symlink.
694
            if ($need_create_link) {
695
                $cpPath = self::which('cp');
696
                Processes::mwExec("{$cpPath} {$old_target}/* {$target}");
697
                unlink($link);
698
            }
699
        } elseif (is_dir($link)) {
700
            // It should be a symlink. Remove the directory.
701
            rmdir($link);
702
        } elseif (file_exists($link)) {
703
            // It should be a symlink. Remove the file.
704
            unlink($link);
705
        }
706
707
        // Create the target directory if $isFile is false
708
        if ($isFile === false) {
709
            self::mwMkdir($target);
710
        }
711
712
        if ($need_create_link) {
713
            $lnPath = self::which('ln');
714
            Processes::mwExec("{$lnPath} -s {$target}  {$link}");
715
        }
716
717
        return $need_create_link;
718
    }
719
720
    /**
721
     * Creates directories with optional WWW rights.
722
     *
723
     * @param string $parameters The space-separated list of directory paths.
724
     * @param bool $addWWWRights Whether to add WWW rights to the directories.
725
     *
726
     * @return bool True if the directories were created successfully, false otherwise.
727
     */
728
    public static function mwMkdir(string $parameters, bool $addWWWRights = false): bool
729
    {
730
        $result = true;
731
732
        // Check if the script is running with root privileges
733
        if (posix_getuid() === 0) {
734
            $arrPaths = explode(' ', $parameters);
735
            if (count($arrPaths) > 0) {
736
                foreach ($arrPaths as $path) {
737
                    if (!empty($path)
738
                        && !file_exists($path)
739
                        && !mkdir($path, 0755, true)
740
                        && !is_dir($path)) {
741
                        $result = false;
742
                        self::sysLogMsg('Util', 'Error on create folder ' . $path, LOG_ERR);
743
                    }
744
                    if ($addWWWRights) {
745
                        self::addRegularWWWRights($path);
746
                    }
747
                }
748
            }
749
        }
750
751
        return $result;
752
    }
753
754
    /**
755
     * Apply regular rights for folders and files
756
     *
757
     * @param $folder
758
     */
759
    public static function addRegularWWWRights($folder): void
760
    {
761
        if (posix_getuid() === 0) {
762
            $findPath = self::which('find');
763
            $chownPath = self::which('chown');
764
            $chmodPath = self::which('chmod');
765
            Processes::mwExec("{$findPath} {$folder} -type d -exec {$chmodPath} 755 {} \;");
766
            Processes::mwExec("{$findPath} {$folder} -type f -exec {$chmodPath} 644 {} \;");
767
            Processes::mwExec("{$chownPath} -R www:www {$folder}");
768
        }
769
    }
770
771
    /**
772
     * Echoes a message and logs it to the system log.
773
     *
774
     * @param string $message The message to echo and log.
775
     *
776
     * @return void
777
     */
778
    public static function echoWithSyslog(string $message): void
779
    {
780
        echo $message;
781
        // Log the message to the system log with LOG_INFO level
782
        self::sysLogMsg(static::class, $message, LOG_INFO);
783
    }
784
785
    /**
786
     * Adds executable rights to files in a folder.
787
     *
788
     * @param string $folder The folder path.
789
     *
790
     * @return void
791
     */
792
    public static function addExecutableRights(string $folder): void
793
    {
794
        // Check if the script is running with root privileges
795
        if (posix_getuid() === 0) {
796
            $findPath = self::which('find');
797
            $chmodPath = self::which('chmod');
798
799
            // Execute find command to locate files and modify their permissions
800
            Processes::mwExec("{$findPath} {$folder} -type f -exec {$chmodPath} 755 {} \;");
801
        }
802
    }
803
804
    /**
805
     * Parses ini settings from a string.
806
     *
807
     * @param string $manual_attributes The ini settings string.
808
     *
809
     * @return array An array representing the parsed ini settings.
810
     */
811
    public static function parseIniSettings(string $manual_attributes): array
812
    {
813
        // Decode the base64-encoded string if it is valid
814
        $tmp_data = base64_decode($manual_attributes);
815
        if (base64_encode($tmp_data) === $manual_attributes) {
816
            $manual_attributes = $tmp_data;
817
        }
818
        unset($tmp_data);
819
820
        // TRIMMING: Remove leading/trailing spaces and section markers
821
        $tmp_arr = explode("\n", $manual_attributes);
822
        foreach ($tmp_arr as &$row) {
823
            $row = trim($row);
824
            $pos = strpos($row, ']');
825
            if ($pos !== false && strpos($row, '[') === 0) {
826
                $row = "\n" . substr($row, 0, $pos);
827
            }
828
        }
829
        unset($row);
830
        $manual_attributes = implode("\n", $tmp_arr);
831
        // TRIMMING END
832
833
        $manual_data = [];
834
        $sections = explode("\n[", str_replace(']', '', $manual_attributes));
835
        foreach ($sections as $section) {
836
            $data_rows = explode("\n", trim($section));
837
            $section_name = trim($data_rows[0] ?? '');
838
            if (!empty($section_name)) {
839
                unset($data_rows[0]);
840
                $manual_data[$section_name] = [];
841
                foreach ($data_rows as $row) {
842
                    $value = '';
843
844
                    // Skip rows without an equal sign
845
                    if (strpos($row, '=') === false) {
846
                        continue;
847
                    }
848
                    $key = '';
849
                    $arr_value = explode('=', $row);
850
                    if (count($arr_value) > 1) {
851
                        $key = trim($arr_value[0]);
852
                        unset($arr_value[0]);
853
                        $value = trim(implode('=', $arr_value));
854
                    }
855
856
                    // Skip rows with empty key or value not equal to '0'
857
                    if (($value !== '0' && empty($value)) || empty($key)) {
858
                        continue;
859
                    }
860
                    $manual_data[$section_name][$key] = $value;
861
                }
862
            }
863
        }
864
865
        return $manual_data;
866
    }
867
868
    /**
869
     * Converts multidimensional array into single array
870
     *
871
     * @param array $array
872
     *
873
     * @return array
874
     */
875
    public static function flattenArray(array $array): array
876
    {
877
        $result = [];
878
        foreach ($array as $value) {
879
            if (is_array($value)) {
880
                $result = array_merge($result, self::flattenArray($value));
881
            } else {
882
                $result[] = $value;
883
            }
884
        }
885
886
        return $result;
887
    }
888
889
    /**
890
     * Try to find full path to php file by class name
891
     *
892
     * @param $className
893
     *
894
     * @return string|null
895
     */
896
    public static function getFilePathByClassName($className): ?string
897
    {
898
        $filename = null;
899
        try {
900
            $reflection = new ReflectionClass($className);
901
            $filename = $reflection->getFileName();
902
        } catch (ReflectionException $exception) {
903
            self::sysLogMsg(__METHOD__, 'ReflectionException ' . $exception->getMessage(), LOG_ERR);
904
        }
905
906
        return $filename;
907
    }
908
909
}