Passed
Push — develop ( 145633...78337f )
by Портнов
04:59
created

Util::parseIniSettings()   D

Complexity

Conditions 18
Paths 102

Size

Total Lines 65
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 41
c 3
b 0
f 0
dl 0
loc 65
rs 4.85
cc 18
nc 102
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * MikoPBX - free phone system for small business
5
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along with this program.
18
 * If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
namespace MikoPBX\Core\System;
22
23
use DateTime;
24
use Exception;
25
use MikoPBX\Common\Models\{CustomFiles};
26
use malkusch\lock\mutex\PHPRedisMutex;
27
use MikoPBX\Common\Handlers\CriticalErrorsHandler;
28
use MikoPBX\Common\Providers\AmiConnectionCommand;
29
use MikoPBX\Common\Providers\AmiConnectionListener;
30
use MikoPBX\Common\Providers\LanguageProvider;
31
use MikoPBX\Common\Providers\ManagedCacheProvider;
32
use MikoPBX\Common\Providers\TranslationProvider;
33
use MikoPBX\Core\Asterisk\AsteriskManager;
34
use Phalcon\Di\Di;
35
use ReflectionClass;
36
use ReflectionException;
37
use Throwable;
38
39
/**
40
 * Class Util
41
 *
42
 * Universal commands and procedures
43
 *
44
 * @package MikoPBX\Core\System
45
 */
46
class Util
47
{
48
    /**
49
     * Overrides configuration array with manual attributes for a specific section.
50
     *
51
     * @param array $options The original configuration options.
52
     * @param array|null $manual_attributes The manual attributes to override the options.
53
     * @param string $section The section to override.
54
     *
55
     * @return string The resulting configuration string.
56
     */
57
    public static function overrideConfigurationArray(array &$options, ?array $manual_attributes, string $section): string
58
    {
59
        $result_config = '';
60
        if ($manual_attributes !== null && isset($manual_attributes[$section])) {
61
            foreach ($manual_attributes[$section] as $key => $value) {
62
                if ($key === 'type') {
63
                    continue;
64
                }
65
                $options[$key] = $value;
66
            }
67
        }
68
        foreach ($options as $key => $value) {
69
            if (empty($value) || empty($key)) {
70
                continue;
71
            }
72
            if (is_array($value)) {
73
                array_unshift($value, ' ');
74
                $result_config .= trim(implode("\n$key = ", $value)) . "\n";
75
            } else {
76
                $result_config .= "$key = $value\n";
77
            }
78
        }
79
80
        return "$result_config\n";
81
    }
82
83
    /**
84
     * Initiates a call using the Asterisk Manager Interface (AMI).
85
     *
86
     * @param string $peer_number The peer number.
87
     * @param string $peer_mobile The peer mobile number.
88
     * @param string $dest_number The destination number.
89
     *
90
     * @return array The result of the Originate command.
91
     * @throws Exception
92
     */
93
    public static function amiOriginate(string $peer_number, string $peer_mobile, string $dest_number): array
94
    {
95
        $am = self::getAstManager('off');
96
        $channel = 'Local/' . $peer_number . '@internal-originate';
97
        $context = 'all_peers';
98
        $variable = "pt1c_cid=$dest_number,__peer_mobile=$peer_mobile";
99
100
        return $am->Originate(
101
            $channel,
102
            $dest_number,
103
            $context,
104
            '1',
105
            null,
106
            null,
107
            null,
108
            null,
109
            $variable,
110
            null,
111
            true
112
        );
113
    }
114
115
    /**
116
     * Retrieves the Asterisk Manager object.
117
     *
118
     * @param string $events Whether to enable events or commands.
119
     *
120
     * @return AsteriskManager The Asterisk Manager object.
121
     *
122
     * @throws Exception
123
     */
124
    public static function getAstManager(string $events = 'on'): AsteriskManager
125
    {
126
        if ($events === 'on') {
127
            $nameService = AmiConnectionListener::SERVICE_NAME;
128
        } else {
129
            $nameService = AmiConnectionCommand::SERVICE_NAME;
130
        }
131
        $di = Di::getDefault();
132
        if ($di === null) {
133
            throw new \Exception("di not found");
134
        }
135
136
        // Try to connect to Asterisk Manager
137
        $am = $di->getShared($nameService);
138
        if (is_resource($am->socket)) {
139
            return $am;
140
        }
141
142
        return $di->get($nameService);
143
    }
144
145
    /**
146
     * Generates a random string of a given length.
147
     *
148
     * @param int $length The length of the random string (default: 10).
149
     *
150
     * @return string The generated random string.
151
     */
152
    public static function generateRandomString(int $length = 10): string
153
    {
154
        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
155
        $charactersLength = strlen($characters);
156
        $randomString = '';
157
        for ($i = 0; $i < $length; $i++) {
158
            try {
159
                $randomString .= $characters[random_int(0, $charactersLength - 1)];
160
            } catch (Throwable $e) {
161
                CriticalErrorsHandler::handleExceptionWithSyslog($e);
162
                $randomString = '';
163
            }
164
        }
165
166
        return $randomString;
167
    }
168
169
    /**
170
     * Validates if a string is a valid JSON.
171
     *
172
     * @param mixed $jsonString The string to validate.
173
     *
174
     * @return bool True if the string is a valid JSON, false otherwise.
175
     */
176
    public static function isJson(mixed $jsonString): bool
177
    {
178
        return json_validate($jsonString);
0 ignored issues
show
Bug introduced by
The function json_validate was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

178
        return /** @scrutinizer ignore-call */ json_validate($jsonString);
Loading history...
179
    }
180
181
    /**
182
     * Returns the size of a file in megabytes.
183
     *
184
     * @param string $filename The filename.
185
     *
186
     * @return float|int The size of the file in megabytes.
187
     */
188
    public static function mFileSize(string $filename): float|int
189
    {
190
        $size = 0;
191
        if (file_exists($filename)) {
192
            $tmp_size = filesize($filename);
193
            if ($tmp_size !== false) {
194
                // Convert size to megabytes
195
                $size = $tmp_size;
196
            }
197
        }
198
199
        return $size;
200
    }
201
202
    /**
203
     * Returns a string with the specified number of 'X' characters.
204
     *
205
     * @param int $length The length of the string.
206
     *
207
     * @return string The string with 'X' characters.
208
     */
209
    public static function getExtensionX(int $length): string
210
    {
211
        $extension = '';
212
        for ($i = 0; $i < $length; $i++) {
213
            $extension .= 'X';
214
        }
215
216
        return $extension;
217
    }
218
219
    /**
220
     * Checks if a file exists and has a non-zero size.
221
     *
222
     * @param string $filename The filename.
223
     *
224
     * @return ?bool True if the file exists and has a non-zero size, false otherwise.
225
     */
226
    public static function recFileExists(string $filename): ?bool
227
    {
228
        return (file_exists($filename) && filesize($filename) > 0);
229
    }
230
231
    /**
232
     * Converts a number to a date if the input is numeric.
233
     *
234
     * @param mixed $data The input data.
235
     *
236
     * @return string The converted date string or the original input data.
237
     */
238
    public static function numberToDate(mixed $data): string
239
    {
240
        $re_number = '/^\d+.\d+$/';
241
        preg_match_all($re_number, $data, $matches, PREG_SET_ORDER, 0);
242
        if (count($matches) > 0) {
243
            $data = date('Y.m.d-H:i:s', $data);
244
        }
245
246
        return $data;
247
    }
248
249
    /**
250
     * Writes content to a file.
251
     *
252
     * @param string $filename The path of the file.
253
     * @param string $data The data to write to the file.
254
     *
255
     * @return void
256
     */
257
    public static function fileWriteContent(string $filename, string $data): void
258
    {
259
        /** @var CustomFiles $res */
260
        $res = CustomFiles::findFirst("filepath = '$filename'");
261
        if ($res === null) {
262
            // File is not yet registered in the database, create a new CustomFiles entry
263
            $res = new CustomFiles();
264
            $res->writeAttribute('filepath', $filename);
265
            $res->writeAttribute('mode', CustomFiles::MODE_NONE);
266
            $res->save();
267
        }
268
269
        $filename_orgn = "$filename.orgn";
270
271
        switch ($res->mode) {
272
            case CustomFiles::MODE_NONE:
273
                if (file_exists($filename_orgn)) {
274
                    unlink($filename_orgn);
275
                }
276
                file_put_contents($filename, $data);
277
                break;
278
            case CustomFiles::MODE_APPEND:
279
                file_put_contents($filename_orgn, $data);
280
                // Append to the file
281
                $data .= "\n\n";
282
                $data .= base64_decode((string)$res->content);
283
                file_put_contents($filename, $data);
284
                break;
285
            case CustomFiles::MODE_OVERRIDE:
286
                file_put_contents($filename_orgn, $data);
287
                // Override the file
288
                $data = base64_decode((string)$res->content);
289
                file_put_contents($filename, $data);
290
                break;
291
            case CustomFiles::MODE_SCRIPT:
292
                // Save the original copy.
293
                file_put_contents($filename_orgn, $data);
294
295
                // Save the config file.
296
                file_put_contents($filename, $data);
297
298
                // Apply custom script to the file
299
                $scriptText = base64_decode((string)$res->content);
300
                $tempScriptFile = tempnam(sys_get_temp_dir(), 'temp_script.sh');
301
                file_put_contents($tempScriptFile, $scriptText);
302
                $command = "/bin/sh $tempScriptFile $filename";
303
                Processes::mwExec($command);
304
                unlink($tempScriptFile);
305
306
                break;
307
            default:
308
        }
309
    }
310
311
    /**
312
     * Returns the current date as a string with millisecond precision.
313
     *
314
     * @return string|null The current date string, or null on error.
315
     */
316
    public static function getNowDate(): ?string
317
    {
318
        $result = null;
319
        try {
320
            $d = new DateTime();
321
            $result = $d->format("Y-m-d H:i:s.v");
322
        } catch (Exception $e) {
323
            unset($e);
324
        }
325
326
        return $result;
327
    }
328
329
    /**
330
     * Retrieves the extension of a file.
331
     *
332
     * @param string $filename The filename.
333
     *
334
     * @return string The extension of the file.
335
     */
336
    public static function getExtensionOfFile(string $filename): string
337
    {
338
        $path_parts = pathinfo($filename);
339
340
        return $path_parts['extension'] ?? '';
341
    }
342
343
    /**
344
     * Removes the extension from a filename.
345
     *
346
     * @param string $filename The filename.
347
     * @param string $delimiter The delimiter character (default: '.').
348
     *
349
     * @return string The filename without the extension.
350
     */
351
    public static function trimExtensionForFile(string $filename, string $delimiter = '.'): string
352
    {
353
        $tmp_arr = explode($delimiter, $filename);
354
        if (count($tmp_arr) > 1) {
355
            unset($tmp_arr[count($tmp_arr) - 1]);
356
            $filename = implode($delimiter, $tmp_arr);
357
        }
358
359
        return $filename;
360
    }
361
362
    /**
363
     * Get the size of a file in kilobytes.
364
     *
365
     * @param string $filename The path to the file.
366
     * @return float The size of the file in kilobytes.
367
     */
368
    public static function getSizeOfFile(string $filename): float
369
    {
370
        $result = 0;
371
        if (file_exists($filename)) {
372
            $du = self::which('du');
373
            $awk = self::which('awk');
374
            Processes::mwExec("$du -d 0 -k '$filename' | $awk  '{ print $1}'", $out);
375
            $time_str = implode($out);
376
            preg_match_all('/^\d+$/', $time_str, $matches, PREG_SET_ORDER, 0);
377
            if (count($matches) > 0) {
378
                $result = round(1 * $time_str / 1024, 2);
379
            }
380
        }
381
382
        return $result;
383
    }
384
385
    /**
386
     * Searches for the executable path of a command.
387
     *
388
     * @param string $cmd The command to search for.
389
     *
390
     * @return string The path of the executable command, or the command itself if not found.
391
     */
392
    public static function which(string $cmd): string
393
    {
394
        global $_ENV;
395
396
        // Default binary folders to search if PATH is not set or command is not found
397
        $binaryFolders = $_ENV['PATH'] ?? '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:/usr/local/sbin';
398
399
        // Search for the command in each binary folder
400
        foreach (explode(':', $binaryFolders) as $path) {
401
            if (is_executable("$path/$cmd")) {
402
                return "$path/$cmd";
403
            }
404
        }
405
406
        // Get BusyBox applets list from cache or generate it
407
        $busyBoxApplets = self::getBusyBoxCommands();
408
409
        // Check if the command is a BusyBox applet
410
        if (in_array($cmd, $busyBoxApplets)) {
411
            return "/bin/busybox $cmd"; // Prefix with 'busybox' if it is a BusyBox command
412
        }
413
414
        // Return the command as it is if not found and not a BusyBox applet
415
        return $cmd;
416
    }
417
418
    /**
419
     * Fetches or generates the list of BusyBox commands.
420
     *
421
     * @return array List of BusyBox commands.
422
     */
423
    public static function getBusyBoxCommands(): array
424
    {
425
        $filename = '/etc/busybox-commands';
426
        if (!file_exists($filename)) {
427
            // Get the list of BusyBox commands by executing busybox --list
428
            Processes::mwExec('busybox --list', $output);
429
            // Save the output to a file
430
            file_put_contents($filename, implode("\n", $output));
431
            return $output;
432
        } else {
433
            // Read the list from the file
434
            $commands = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
435
            return $commands;
436
        }
437
    }
438
439
    /**
440
     * Checks if a password is simple based on a dictionary.
441
     *
442
     * @param string $value The password to check.
443
     *
444
     * @return bool True if the password is found in the dictionary, false otherwise.
445
     */
446
    public static function isSimplePassword(string $value): bool
447
    {
448
        $passwords = [];
449
        Processes::mwExec('/bin/zcat /usr/share/wordlists/rockyou.txt.gz', $passwords);
450
        return in_array($value, $passwords, true);
451
    }
452
453
    /**
454
     * Sets the Cyrillic font for the console.
455
     *
456
     * @return void
457
     */
458
    public static function setCyrillicFont(): void
459
    {
460
        $setfont = self::which('setfont');
461
        Processes::mwExec("$setfont /usr/share/consolefonts/Cyr_a8x16.psfu.gz 2>/dev/null");
462
    }
463
464
    /**
465
     * Translates a text string.
466
     *
467
     * @param string $text The text to translate.
468
     * @param bool $cliLang Whether to use CLI language or web language (default: true).
469
     *
470
     * @return string The translated text.
471
     */
472
    public static function translate(string $text, bool $cliLang = true): string
473
    {
474
        $di = Di::getDefault();
475
        if ($di !== null) {
476
            if (!$cliLang) {
477
                $di->setShared(LanguageProvider::PREFERRED_LANG_WEB, true);
478
            }
479
            $text = $di->getShared(TranslationProvider::SERVICE_NAME)->_($text);
480
            if (!$cliLang) {
481
                $di->remove(LanguageProvider::PREFERRED_LANG_WEB);
482
            }
483
        }
484
        return $text;
485
    }
486
487
    /**
488
     * Recursively deletes a directory.
489
     *
490
     * @param string $dir The directory path to delete.
491
     *
492
     * @return void
493
     *
494
     * @link http://php.net/manual/en/function.rmdir.php
495
     */
496
    public static function rRmDir(string $dir): void
497
    {
498
        if (is_dir($dir)) {
499
            $objects = scandir($dir);
500
501
            // Recursively delete files and subdirectories
502
            foreach ($objects as $object) {
503
                if ($object != "." && $object != "..") {
504
                    if (filetype($dir . "/" . $object) == "dir") {
505
                        self::rRmDir($dir . "/" . $object);
506
                    } else {
507
                        unlink($dir . "/" . $object);
508
                    }
509
                }
510
            }
511
512
            // Reset the array pointer and remove the directory
513
            if ($objects !== false) {
514
                reset($objects);
515
            }
516
            rmdir($dir);
517
        }
518
    }
519
520
    /**
521
     * Generates an SSL certificate.
522
     *
523
     * @param array|null $options The options for the certificate (default: null).
524
     * @param array|null $config_args_pkey The configuration arguments for the private key (default: null).
525
     * @param array|null $config_args_csr The configuration arguments for the CSR (default: null).
526
     *
527
     * @return array The generated SSL certificate (public and private keys).
528
     */
529
    public static function generateSslCert(?array $options = null, ?array $config_args_pkey = null, ?array $config_args_csr = null): array
530
    {
531
        // Initialize options if not provided
532
        if (!$options) {
533
            $options = [
534
                "countryName" => 'RU',
535
                "stateOrProvinceName" => 'Moscow',
536
                "localityName" => 'Zelenograd',
537
                "organizationName" => 'MIKO LLC',
538
                "organizationalUnitName" => 'Software development',
539
                "commonName" => 'MIKO PBX',
540
                "emailAddress" => '[email protected]',
541
            ];
542
        }
543
544
        // Initialize CSR configuration arguments if not provided
545
        if (!$config_args_csr) {
546
            $config_args_csr = ['digest_alg' => 'sha256'];
547
        }
548
549
        // Initialize private key configuration arguments if not provided
550
        if (!$config_args_pkey) {
551
            $config_args_pkey = [
552
                "private_key_bits" => 2048,
553
                "private_key_type" => OPENSSL_KEYTYPE_RSA,
554
            ];
555
        }
556
557
        // Generate keys
558
        $private_key = openssl_pkey_new($config_args_pkey);
559
        $csr = openssl_csr_new($options, /** @scrutinizer ignore-type */$private_key, $config_args_csr);
560
        $x509 = openssl_csr_sign($csr, null, $private_key, $days = 3650, $config_args_csr);
561
562
        // Export keys
563
        openssl_x509_export($x509, $certout);
564
        openssl_pkey_export($private_key, $pkeyout);
565
        // echo $pkeyout; // -> WEBHTTPSPrivateKey
566
        // echo $certout; // -> WEBHTTPSPublicKey
567
        return ['PublicKey' => $certout, 'PrivateKey' => $pkeyout];
568
    }
569
570
    /**
571
     * Checks whether the current system is t2 SDE build.
572
     *
573
     * @return bool True if the system is t2 SDE build, false otherwise.
574
     */
575
    public static function isT2SdeLinux(): bool
576
    {
577
        return file_exists('/etc/t2-sde-build');
578
    }
579
580
    /**
581
     * Checks whether the current system has systemctl installed and executable.
582
     *
583
     * @return bool True if systemctl is available, false otherwise.
584
     */
585
    public static function isSystemctl(): bool
586
    {
587
        $pathSystemCtl = self::which('systemctl');
588
        return !empty($pathSystemCtl) && is_executable($pathSystemCtl);
589
    }
590
591
    /**
592
     * Checks whether the current process is running inside a Docker container.
593
     *
594
     * @return bool True if the process is inside a container, false otherwise.
595
     */
596
    public static function isDocker(): bool
597
    {
598
        return file_exists('/.dockerenv');
599
    }
600
601
    /**
602
     * Creates or updates a symlink to a target path.
603
     *
604
     * @param string $target The target path.
605
     * @param string $link The symlink path.
606
     * @param bool $isFile Whether the symlink should point to a file (false by default).
607
     *
608
     * @return bool True if the symlink was created or updated, false otherwise.
609
     */
610
    public static function createUpdateSymlink(string $target, string $link, bool $isFile = false): bool
611
    {
612
        $need_create_link = true;
613
        if (is_link($link)) {
614
            $old_target = readlink($link);
615
            $need_create_link = ($old_target !== $target);
616
617
            // If needed, remove the old symlink.
618
            if ($need_create_link) {
619
                $cpPath = self::which('cp');
620
                Processes::mwExec("$cpPath $old_target/* $target");
621
                unlink($link);
622
            }
623
        } elseif (is_dir($link)) {
624
            // It should be a symlink. Remove the directory.
625
            rmdir($link);
626
        } elseif (file_exists($link)) {
627
            // It should be a symlink. Remove the file.
628
            unlink($link);
629
        }
630
631
        // Create the target directory if $isFile is false
632
        if ($isFile === false) {
633
            self::mwMkdir($target);
634
        }
635
636
        if ($need_create_link) {
637
            $lnPath = self::which('ln');
638
            Processes::mwExec("$lnPath -s $target $link");
639
        }
640
641
        return $need_create_link;
642
    }
643
644
    /**
645
     * Creates directories with optional WWW rights.
646
     *
647
     * @param string $parameters The space-separated list of directory paths.
648
     * @param bool $addWWWRights Whether to add WWW rights to the directories.
649
     *
650
     * @return bool True if the directories were created successfully, false otherwise.
651
     */
652
    public static function mwMkdir(string $parameters, bool $addWWWRights = false): bool
653
    {
654
        $result = true;
655
656
        // Check if the script is running with root privileges
657
        if (posix_getuid() === 0) {
658
            $arrPaths = explode(' ', $parameters);
659
            if (count($arrPaths) > 0) {
660
                foreach ($arrPaths as $path) {
661
                    if (
662
                        !empty($path)
663
                        && !file_exists($path)
664
                        && !mkdir($path, 0755, true)
665
                        && !is_dir($path)
666
                    ) {
667
                        $result = false;
668
                        SystemMessages::sysLogMsg(__METHOD__, 'Error on create folder ' . $path, LOG_ERR);
669
                    }
670
                    if ($addWWWRights) {
671
                        self::addRegularWWWRights($path);
672
                    }
673
                }
674
            }
675
        }
676
677
        return $result;
678
    }
679
680
    /**
681
     * Apply regular rights for folders and files
682
     *
683
     * @param $folder
684
     */
685
    public static function addRegularWWWRights($folder): void
686
    {
687
        if (posix_getuid() === 0) {
688
            $find = self::which('find');
689
            $chown = self::which('chown');
690
            $chmod = self::which('chmod');
691
            Processes::mwExec("$find $folder -type d -exec $chmod 755 {} \;");
692
            Processes::mwExec("$find $folder -type f -exec $chmod 644 {} \;");
693
            Processes::mwExec("$chown -R www:www $folder");
694
        }
695
    }
696
697
    /**
698
     * Adds executable rights to files in a folder.
699
     *
700
     * @param string $folder The folder path.
701
     *
702
     * @return void
703
     */
704
    public static function addExecutableRights(string $folder): void
705
    {
706
        // Check if the script is running with root privileges
707
        if (posix_getuid() === 0) {
708
            $find = self::which('find');
709
            $chmod = self::which('chmod');
710
711
            // Execute find command to locate files and modify their permissions
712
            Processes::mwExec("$find $folder -type f -exec $chmod 755 {} \;");
713
        }
714
    }
715
716
    /**
717
     * Parses ini settings from a string.
718
     *
719
     * @param string $manual_attributes The ini settings string.
720
     *
721
     * @return array An array representing the parsed ini settings.
722
     */
723
    public static function parseIniSettings(string $manual_attributes): array
724
    {
725
        // Decode the base64-encoded string if it is valid
726
        $tmp_data = base64_decode($manual_attributes);
727
        if (base64_encode($tmp_data) === $manual_attributes) {
728
            $manual_attributes = $tmp_data;
729
        }
730
        unset($tmp_data);
731
732
        // TRIMMING: Remove leading/trailing spaces and section markers
733
        $tmp_arr = explode("\n", $manual_attributes);
734
        foreach ($tmp_arr as &$row) {
735
            $row = trim($row);
736
            $pos = strpos($row, ']');
737
            if ($pos !== false && str_starts_with($row, '[')) {
738
                $row = "\n" . substr($row, 0, $pos + 1);
739
            }
740
        }
741
        unset($row);
742
        $manual_attributes = implode("\n", $tmp_arr);
743
        // TRIMMING END
744
745
        $haveSection = strpos($manual_attributes, '[') !== false;
746
747
        $manual_data = [];
748
        $sections = explode("\n[", str_replace(']', '', $manual_attributes));
749
        foreach ($sections as $section) {
750
            $data_rows = explode("\n", trim($section));
751
            $section_name = trim($data_rows[0] ?? '');
752
            if (!$haveSection && str_contains($section_name, '=')) {
753
                // Noname section
754
                $section_name = ' ';
755
            } else {
756
                unset($data_rows[0]);
757
            }
758
            $manual_data[$section_name] = [];
759
            foreach ($data_rows as $row) {
760
                $value = '';
761
762
                // Skip rows without an equal sign
763
                if (!str_contains($row, '=')) {
764
                    continue;
765
                }
766
                $key = '';
767
                $arr_value = explode('=', $row);
768
                if (count($arr_value) > 1) {
769
                    $key = trim($arr_value[0]);
770
                    unset($arr_value[0]);
771
                    $value = trim(implode('=', $arr_value));
772
                }
773
774
                // Skip rows with empty key or value not equal to '0'
775
                if (($value !== '0' && empty($value)) || empty($key)) {
776
                    continue;
777
                }
778
                if( ('set_var' === $key && $section_name === 'endpoint') ||
779
                    ('setvar'  === $key && $section_name === ' ')) {
780
                    $manual_data[$section_name][$key][] = $value;
781
                }else{
782
                    $manual_data[$section_name][$key] = $value;
783
                }
784
            }
785
        }
786
787
        return $manual_data;
788
    }
789
790
    /**
791
     * Converts multidimensional array into single array
792
     *
793
     * @param array $array
794
     *
795
     * @return array
796
     */
797
    public static function flattenArray(array $array): array
798
    {
799
        $result = [];
800
        foreach ($array as $value) {
801
            if (is_array($value)) {
802
                $result = array_merge($result, self::flattenArray($value));
803
            } else {
804
                $result[] = $value;
805
            }
806
        }
807
808
        return $result;
809
    }
810
811
    /**
812
     * Try to find full path to php file by class name
813
     *
814
     * @param $className
815
     *
816
     * @return string|null
817
     */
818
    public static function getFilePathByClassName($className): ?string
819
    {
820
        $filename = null;
821
        try {
822
            $reflection = new ReflectionClass($className);
823
            $filename = $reflection->getFileName();
824
        } catch (ReflectionException $exception) {
825
            SystemMessages::sysLogMsg(__METHOD__, 'ReflectionException ' . $exception->getMessage(), LOG_ERR);
826
        }
827
828
        return $filename;
829
    }
830
831
832
    /**
833
     * Creates a mutex to ensure synchronized module installation.
834
     *
835
     * @param string $namespace Namespace for the mutex, used to differentiate mutexes.
836
     * @param string $uniqueId Unique identifier for the mutex, usually the module ID.
837
     * @param int $timeout Timeout in seconds for the mutex.
838
     *
839
     * @return PHPRedisMutex Returns an instance of PHPRedisMutex.
840
     */
841
    public static function createMutex(string $namespace, string $uniqueId, int $timeout = 5): PHPRedisMutex
842
    {
843
        $di = Di::getDefault();
844
        $redisAdapter = $di->get(ManagedCacheProvider::SERVICE_NAME)->getAdapter();
845
        $mutexKey = "Mutex:$namespace-" . md5($uniqueId);
846
        return new PHPRedisMutex([$redisAdapter], $mutexKey, $timeout);
847
    }
848
849
    /**
850
     * Adds messages to Syslog.
851
     * @deprecated Use SystemMessages::sysLogMsg instead
852
     *
853
     * @param string $ident The category, class, or method identification.
854
     * @param string $message The log message.
855
     * @param int $level The log level (default: LOG_WARNING).
856
     *
857
     * @return void
858
     */
859
    public static function sysLogMsg(string $ident, string $message, int $level = LOG_WARNING): void
860
    {
861
        SystemMessages::sysLogMsg($ident, $message, $level);
862
    }
863
864
865
    /**
866
     * Echoes a message and logs it to the system log.
867
     * @deprecated Use SystemMessages::echoWithSyslog instead
868
     *
869
     * @param string $message The message to echo and log.
870
     *
871
     * @return void
872
     */
873
    public static function echoWithSyslog(string $message): void
874
    {
875
        SystemMessages::echoWithSyslog($message);
876
    }
877
878
    /**
879
     * Is recovery mode
880
     *
881
     * @return bool
882
     */
883
    public static function isRecoveryMode(): bool
884
    {
885
        return file_exists('/offload/livecd');
886
    }
887
}
888