Passed
Push — develop ( 2d3c3b...ea7628 )
by Nikolay
04:49
created

Util::getAstManager()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 19
rs 9.9
cc 4
nc 6
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 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
        if (array_key_exists('PATH', $_ENV)) {
395
            $binaryFolders = $_ENV['PATH'];
396
397
            // Search for the command in each binary folder
398
            foreach (explode(':', $binaryFolders) as $path) {
399
                if (is_executable("{$path}/{$cmd}")) {
400
                    return "{$path}/{$cmd}";
401
                }
402
            }
403
        }
404
405
        // Default binary folders to search if PATH is not set or command is not found
406
        $binaryFolders =
407
            [
408
                '/sbin',
409
                '/bin',
410
                '/usr/sbin',
411
                '/usr/bin',
412
                '/usr/local/bin',
413
                '/usr/local/sbin',
414
            ];
415
416
        // Search for the command in the default binary folders
417
        foreach ($binaryFolders as $path) {
418
            if (is_executable("{$path}/{$cmd}")) {
419
                return "{$path}/{$cmd}";
420
            }
421
        }
422
423
        return $cmd;
424
    }
425
426
    /**
427
     * Checks if a password is simple based on a dictionary.
428
     *
429
     * @param string $value The password to check.
430
     *
431
     * @return bool True if the password is found in the dictionary, false otherwise.
432
     */
433
    public static function isSimplePassword(string $value): bool
434
    {
435
        $passwords = [];
436
        Processes::mwExec('/bin/zcat /usr/share/wordlists/rockyou.txt.gz', $passwords);
437
        return in_array($value, $passwords, true);
438
    }
439
440
    /**
441
     * Sets the Cyrillic font for the console.
442
     *
443
     * @return void
444
     */
445
    public static function setCyrillicFont(): void
446
    {
447
        $setfontPath = self::which('setfont');
448
        Processes::mwExec("{$setfontPath} /usr/share/consolefonts/Cyr_a8x16.psfu.gz 2>/dev/null");
449
    }
450
451
    /**
452
     * Translates a text string.
453
     *
454
     * @param string $text The text to translate.
455
     * @param bool $cliLang Whether to use CLI language or web language (default: true).
456
     *
457
     * @return string The translated text.
458
     */
459
    public static function translate(string $text, bool $cliLang = true): string
460
    {
461
        $di = Di::getDefault();
462
        if ($di !== null) {
463
            if (!$cliLang) {
464
                $di->setShared(LanguageProvider::PREFERRED_LANG_WEB, true);
465
            }
466
            $text = $di->getShared(TranslationProvider::SERVICE_NAME)->_($text);
467
            if (!$cliLang) {
468
                $di->remove(LanguageProvider::PREFERRED_LANG_WEB);
469
            }
470
        }
471
        return $text;
472
    }
473
474
    /**
475
     * Recursively deletes a directory.
476
     *
477
     * @param string $dir The directory path to delete.
478
     *
479
     * @return void
480
     *
481
     * @link http://php.net/manual/en/function.rmdir.php
482
     */
483
    public static function rRmDir(string $dir): void
484
    {
485
        if (is_dir($dir)) {
486
            $objects = scandir($dir);
487
488
            // Recursively delete files and subdirectories
489
            foreach ($objects as $object) {
490
                if ($object != "." && $object != "..") {
491
                    if (filetype($dir . "/" . $object) == "dir") {
492
                        self::rRmDir($dir . "/" . $object);
493
                    } else {
494
                        unlink($dir . "/" . $object);
495
                    }
496
                }
497
            }
498
499
            // Reset the array pointer and remove the directory
500
            if ($objects !== false) {
501
                reset($objects);
502
            }
503
            rmdir($dir);
504
        }
505
    }
506
507
    /**
508
     * Generates an SSL certificate.
509
     *
510
     * @param array|null $options The options for the certificate (default: null).
511
     * @param array|null $config_args_pkey The configuration arguments for the private key (default: null).
512
     * @param array|null $config_args_csr The configuration arguments for the CSR (default: null).
513
     *
514
     * @return array The generated SSL certificate (public and private keys).
515
     */
516
    public static function generateSslCert($options = null, $config_args_pkey = null, $config_args_csr = null): array
517
    {
518
        // Initialize options if not provided
519
        if (!$options) {
520
            $options = [
521
                "countryName" => 'RU',
522
                "stateOrProvinceName" => 'Moscow',
523
                "localityName" => 'Zelenograd',
524
                "organizationName" => 'MIKO LLC',
525
                "organizationalUnitName" => 'Software development',
526
                "commonName" => 'MIKO PBX',
527
                "emailAddress" => '[email protected]',
528
            ];
529
        }
530
531
        // Initialize CSR configuration arguments if not provided
532
        if (!$config_args_csr) {
533
            $config_args_csr = ['digest_alg' => 'sha256'];
534
        }
535
536
        // Initialize private key configuration arguments if not provided
537
        if (!$config_args_pkey) {
538
            $config_args_pkey = [
539
                "private_key_bits" => 2048,
540
                "private_key_type" => OPENSSL_KEYTYPE_RSA,
541
            ];
542
        }
543
544
        // Generate keys
545
        $private_key = openssl_pkey_new($config_args_pkey);
546
        $csr = openssl_csr_new($options, /** @scrutinizer ignore-type */$private_key, $config_args_csr);
547
        $x509 = openssl_csr_sign($csr, null, $private_key, $days = 3650, $config_args_csr);
548
549
        // Export keys
550
        openssl_x509_export($x509, $certout);
551
        openssl_pkey_export($private_key, $pkeyout);
552
        // echo $pkeyout; // -> WEBHTTPSPrivateKey
553
        // echo $certout; // -> WEBHTTPSPublicKey
554
        return ['PublicKey' => $certout, 'PrivateKey' => $pkeyout];
555
    }
556
557
    /**
558
     * Checks whether the current system is t2 SDE build.
559
     *
560
     * @return bool True if the system is t2 SDE build, false otherwise.
561
     */
562
    public static function isT2SdeLinux(): bool
563
    {
564
        return file_exists('/etc/t2-sde-build');
565
    }
566
567
    /**
568
     * Checks whether the current system has systemctl installed and executable.
569
     *
570
     * @return bool True if systemctl is available, false otherwise.
571
     */
572
    public static function isSystemctl(): bool
573
    {
574
        $pathSystemCtl = self::which('systemctl');
575
        return !empty($pathSystemCtl) && is_executable($pathSystemCtl);
576
    }
577
578
    /**
579
     * Checks whether the current process is running inside a Docker container.
580
     *
581
     * @return bool True if the process is inside a container, false otherwise.
582
     */
583
    public static function isDocker(): bool
584
    {
585
        return file_exists('/.dockerenv');
586
    }
587
588
    /**
589
     * Creates or updates a symlink to a target path.
590
     *
591
     * @param string $target The target path.
592
     * @param string $link The symlink path.
593
     * @param bool $isFile Whether the symlink should point to a file (false by default).
594
     *
595
     * @return bool True if the symlink was created or updated, false otherwise.
596
     */
597
    public static function createUpdateSymlink(string $target, string $link, bool $isFile = false): bool
598
    {
599
        $need_create_link = true;
600
        if (is_link($link)) {
601
            $old_target = readlink($link);
602
            $need_create_link = ($old_target !== $target);
603
604
            // If needed, remove the old symlink.
605
            if ($need_create_link) {
606
                $cpPath = self::which('cp');
607
                Processes::mwExec("$cpPath $old_target/* $target");
608
                unlink($link);
609
            }
610
        } elseif (is_dir($link)) {
611
            // It should be a symlink. Remove the directory.
612
            rmdir($link);
613
        } elseif (file_exists($link)) {
614
            // It should be a symlink. Remove the file.
615
            unlink($link);
616
        }
617
618
        // Create the target directory if $isFile is false
619
        if ($isFile === false) {
620
            self::mwMkdir($target);
621
        }
622
623
        if ($need_create_link) {
624
            $lnPath = self::which('ln');
625
            Processes::mwExec("$lnPath -s $target $link");
626
        }
627
628
        return $need_create_link;
629
    }
630
631
    /**
632
     * Creates directories with optional WWW rights.
633
     *
634
     * @param string $parameters The space-separated list of directory paths.
635
     * @param bool $addWWWRights Whether to add WWW rights to the directories.
636
     *
637
     * @return bool True if the directories were created successfully, false otherwise.
638
     */
639
    public static function mwMkdir(string $parameters, bool $addWWWRights = false): bool
640
    {
641
        $result = true;
642
643
        // Check if the script is running with root privileges
644
        if (posix_getuid() === 0) {
645
            $arrPaths = explode(' ', $parameters);
646
            if (count($arrPaths) > 0) {
647
                foreach ($arrPaths as $path) {
648
                    if (!empty($path)
649
                        && !file_exists($path)
650
                        && !mkdir($path, 0755, true)
651
                        && !is_dir($path)) {
652
                        $result = false;
653
                        SystemMessages::sysLogMsg(__METHOD__, 'Error on create folder ' . $path, LOG_ERR);
654
                    }
655
                    if ($addWWWRights) {
656
                        self::addRegularWWWRights($path);
657
                    }
658
                }
659
            }
660
        }
661
662
        return $result;
663
    }
664
665
    /**
666
     * Apply regular rights for folders and files
667
     *
668
     * @param $folder
669
     */
670
    public static function addRegularWWWRights($folder): void
671
    {
672
        if (posix_getuid() === 0) {
673
            $findPath = self::which('find');
674
            $chownPath = self::which('chown');
675
            $chmodPath = self::which('chmod');
676
            Processes::mwExec("{$findPath} {$folder} -type d -exec {$chmodPath} 755 {} \;");
677
            Processes::mwExec("{$findPath} {$folder} -type f -exec {$chmodPath} 644 {} \;");
678
            Processes::mwExec("{$chownPath} -R www:www {$folder}");
679
        }
680
    }
681
682
    /**
683
     * Adds executable rights to files in a folder.
684
     *
685
     * @param string $folder The folder path.
686
     *
687
     * @return void
688
     */
689
    public static function addExecutableRights(string $folder): void
690
    {
691
        // Check if the script is running with root privileges
692
        if (posix_getuid() === 0) {
693
            $findPath = self::which('find');
694
            $chmodPath = self::which('chmod');
695
696
            // Execute find command to locate files and modify their permissions
697
            Processes::mwExec("{$findPath} {$folder} -type f -exec {$chmodPath} 755 {} \;");
698
        }
699
    }
700
701
    /**
702
     * Parses ini settings from a string.
703
     *
704
     * @param string $manual_attributes The ini settings string.
705
     *
706
     * @return array An array representing the parsed ini settings.
707
     */
708
    public static function parseIniSettings(string $manual_attributes): array
709
    {
710
        // Decode the base64-encoded string if it is valid
711
        $tmp_data = base64_decode($manual_attributes);
712
        if (base64_encode($tmp_data) === $manual_attributes) {
713
            $manual_attributes = $tmp_data;
714
        }
715
        unset($tmp_data);
716
717
        // TRIMMING: Remove leading/trailing spaces and section markers
718
        $tmp_arr = explode("\n", $manual_attributes);
719
        foreach ($tmp_arr as &$row) {
720
            $row = trim($row);
721
            $pos = strpos($row, ']');
722
            if ($pos !== false && strpos($row, '[') === 0) {
723
                $row = "\n" . substr($row, 0, $pos);
724
            }
725
        }
726
        unset($row);
727
        $manual_attributes = implode("\n", $tmp_arr);
728
        // TRIMMING END
729
730
        $manual_data = [];
731
        $sections = explode("\n[", str_replace(']', '', $manual_attributes));
732
        foreach ($sections as $section) {
733
            $data_rows = explode("\n", trim($section));
734
            $section_name = trim($data_rows[0] ?? '');
735
            if (!empty($section_name)) {
736
                unset($data_rows[0]);
737
                $manual_data[$section_name] = [];
738
                foreach ($data_rows as $row) {
739
                    $value = '';
740
741
                    // Skip rows without an equal sign
742
                    if (strpos($row, '=') === false) {
743
                        continue;
744
                    }
745
                    $key = '';
746
                    $arr_value = explode('=', $row);
747
                    if (count($arr_value) > 1) {
748
                        $key = trim($arr_value[0]);
749
                        unset($arr_value[0]);
750
                        $value = trim(implode('=', $arr_value));
751
                    }
752
753
                    // Skip rows with empty key or value not equal to '0'
754
                    if (($value !== '0' && empty($value)) || empty($key)) {
755
                        continue;
756
                    }
757
                    $manual_data[$section_name][$key] = $value;
758
                }
759
            }
760
        }
761
762
        return $manual_data;
763
    }
764
765
    /**
766
     * Converts multidimensional array into single array
767
     *
768
     * @param array $array
769
     *
770
     * @return array
771
     */
772
    public static function flattenArray(array $array): array
773
    {
774
        $result = [];
775
        foreach ($array as $value) {
776
            if (is_array($value)) {
777
                $result = array_merge($result, self::flattenArray($value));
778
            } else {
779
                $result[] = $value;
780
            }
781
        }
782
783
        return $result;
784
    }
785
786
    /**
787
     * Try to find full path to php file by class name
788
     *
789
     * @param $className
790
     *
791
     * @return string|null
792
     */
793
    public static function getFilePathByClassName($className): ?string
794
    {
795
        $filename = null;
796
        try {
797
            $reflection = new ReflectionClass($className);
798
            $filename = $reflection->getFileName();
799
        } catch (ReflectionException $exception) {
800
            SystemMessages::sysLogMsg(__METHOD__, 'ReflectionException ' . $exception->getMessage(), LOG_ERR);
801
        }
802
803
        return $filename;
804
    }
805
806
807
    /**
808
     * Creates a mutex to ensure synchronized module installation.
809
     *
810
     * @param string $namespace Namespace for the mutex, used to differentiate mutexes.
811
     * @param string $uniqueId Unique identifier for the mutex, usually the module ID.
812
     * @param int $timeout Timeout in seconds for the mutex.
813
     *
814
     * @return PHPRedisMutex Returns an instance of PHPRedisMutex.
815
     */
816
    public static function createMutex(string $namespace, string $uniqueId, int $timeout = 5): PHPRedisMutex
817
    {
818
        $di = Di::getDefault();
819
        $redisAdapter = $di->get(ManagedCacheProvider::SERVICE_NAME)->getAdapter();
820
        $mutexKey = "Mutex:$namespace-" . md5($uniqueId);
821
        return new PHPRedisMutex([$redisAdapter], $mutexKey, $timeout);
822
    }
823
824
    /**
825
     * Adds messages to Syslog.
826
     * @depricated Use SystemMessages::sysLogMsg instead
827
     *
828
     * @param string $ident The category, class, or method identification.
829
     * @param string $message The log message.
830
     * @param int $level The log level (default: LOG_WARNING).
831
     *
832
     * @return void
833
     */
834
    public static function sysLogMsg(string $ident, string $message, int $level = LOG_WARNING): void
835
    {
836
        SystemMessages::sysLogMsg($ident, $message, $level);
837
    }
838
839
840
    /**
841
     * Echoes a message and logs it to the system log.
842
     * @depricated Use SystemMessages::echoWithSyslog instead
843
     *
844
     * @param string $message The message to echo and log.
845
     *
846
     * @return void
847
     */
848
    public static function echoWithSyslog(string $message): void
849
    {
850
        SystemMessages::echoWithSyslog($message);
851
    }
852
853
}