Passed
Push — develop ( 115c7b...480946 )
by Nikolay
05:11
created

Util::isRecoveryMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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