Passed
Push — develop ( a24ba0...f685b5 )
by Nikolay
05:57 queued 35s
created

Util::createMutex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
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\LoggerProvider;
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
     * Adds messages to Syslog.
312
     *
313
     * @param string $ident The category, class, or method identification.
314
     * @param string $message The log message.
315
     * @param int $level The log level (default: LOG_WARNING).
316
     *
317
     * @return void
318
     */
319
    public static function sysLogMsg(string $ident, string $message, $level = LOG_WARNING): void
320
    {
321
        /** @var \Phalcon\Logger $logger */
322
        $logger = Di::getDefault()->getShared(LoggerProvider::SERVICE_NAME);
323
        $logger->log($level, "{$message} on {$ident}");
324
    }
325
326
    /**
327
     * Returns the current date as a string with millisecond precision.
328
     *
329
     * @return string|null The current date string, or null on error.
330
     */
331
    public static function getNowDate(): ?string
332
    {
333
        $result = null;
334
        try {
335
            $d = new DateTime();
336
            $result = $d->format("Y-m-d H:i:s.v");
337
        } catch (Exception $e) {
338
            unset($e);
339
        }
340
341
        return $result;
342
    }
343
344
    /**
345
     * Retrieves the extension of a file.
346
     *
347
     * @param string $filename The filename.
348
     *
349
     * @return string The extension of the file.
350
     */
351
    public static function getExtensionOfFile(string $filename): string
352
    {
353
        $path_parts = pathinfo($filename);
354
355
        return $path_parts['extension'] ?? '';
356
    }
357
358
    /**
359
     * Removes the extension from a filename.
360
     *
361
     * @param string $filename The filename.
362
     * @param string $delimiter The delimiter character (default: '.').
363
     *
364
     * @return string The filename without the extension.
365
     */
366
    public static function trimExtensionForFile(string $filename, string $delimiter = '.'): string
367
    {
368
        $tmp_arr = explode((string)$delimiter, $filename);
369
        if (count($tmp_arr) > 1) {
370
            unset($tmp_arr[count($tmp_arr) - 1]);
371
            $filename = implode((string)$delimiter, $tmp_arr);
372
        }
373
374
        return $filename;
375
    }
376
377
    /**
378
     * Get the size of a file in kilobytes.
379
     *
380
     * @param string $filename The path to the file.
381
     * @return float The size of the file in kilobytes.
382
     */
383
    public static function getSizeOfFile(string $filename): float
384
    {
385
        $result = 0;
386
        if (file_exists($filename)) {
387
            $duPath = self::which('du');
388
            $awkPath = self::which('awk');
389
            Processes::mwExec("{$duPath} -d 0 -k '{$filename}' | {$awkPath}  '{ print $1}'", $out);
390
            $time_str = implode($out);
391
            preg_match_all('/^\d+$/', $time_str, $matches, PREG_SET_ORDER, 0);
392
            if (count($matches) > 0) {
393
                $result = round(1 * $time_str / 1024, 2);
394
            }
395
        }
396
397
        return $result;
398
    }
399
400
    /**
401
     * Searches for the executable path of a command.
402
     *
403
     * @param string $cmd The command to search for.
404
     *
405
     * @return string The path of the executable command, or the command itself if not found.
406
     */
407
    public static function which(string $cmd): string
408
    {
409
        global $_ENV;
410
        if (array_key_exists('PATH', $_ENV)) {
411
            $binaryFolders = $_ENV['PATH'];
412
413
            // Search for the command in each binary folder
414
            foreach (explode(':', $binaryFolders) as $path) {
415
                if (is_executable("{$path}/{$cmd}")) {
416
                    return "{$path}/{$cmd}";
417
                }
418
            }
419
        }
420
421
        // Default binary folders to search if PATH is not set or command is not found
422
        $binaryFolders =
423
            [
424
                '/sbin',
425
                '/bin',
426
                '/usr/sbin',
427
                '/usr/bin',
428
                '/usr/local/bin',
429
                '/usr/local/sbin',
430
            ];
431
432
        // Search for the command in the default binary folders
433
        foreach ($binaryFolders as $path) {
434
            if (is_executable("{$path}/{$cmd}")) {
435
                return "{$path}/{$cmd}";
436
            }
437
        }
438
439
        return $cmd;
440
    }
441
442
    /**
443
     * Checks if a password is simple based on a dictionary.
444
     *
445
     * @param string $value The password to check.
446
     *
447
     * @return bool True if the password is found in the dictionary, false otherwise.
448
     */
449
    public static function isSimplePassword(string $value): bool
450
    {
451
        $passwords = [];
452
        Processes::mwExec('/bin/zcat /usr/share/wordlists/rockyou.txt.gz', $passwords);
453
        return in_array($value, $passwords, true);
454
    }
455
456
    /**
457
     * Sets the Cyrillic font for the console.
458
     *
459
     * @return void
460
     */
461
    public static function setCyrillicFont(): void
462
    {
463
        $setfontPath = self::which('setfont');
464
        Processes::mwExec("{$setfontPath} /usr/share/consolefonts/Cyr_a8x16.psfu.gz 2>/dev/null");
465
    }
466
467
    /**
468
     * Translates a text string.
469
     *
470
     * @param string $text The text to translate.
471
     * @param bool $cliLang Whether to use CLI language or web language (default: true).
472
     *
473
     * @return string The translated text.
474
     */
475
    public static function translate(string $text, bool $cliLang = true): string
476
    {
477
        $di = Di::getDefault();
478
        if ($di !== null) {
479
            if (!$cliLang) {
480
                $di->setShared('PREFERRED_LANG_WEB', true);
481
            }
482
            $text = $di->getShared(TranslationProvider::SERVICE_NAME)->_($text);
483
            if (!$cliLang) {
484
                $di->remove('PREFERRED_LANG_WEB');
485
            }
486
        }
487
        return $text;
488
    }
489
490
    /**
491
     * Recursively deletes a directory.
492
     *
493
     * @param string $dir The directory path to delete.
494
     *
495
     * @return void
496
     *
497
     * @link http://php.net/manual/en/function.rmdir.php
498
     */
499
    public static function rRmDir(string $dir): void
500
    {
501
        if (is_dir($dir)) {
502
            $objects = scandir($dir);
503
504
            // Recursively delete files and subdirectories
505
            foreach ($objects as $object) {
506
                if ($object != "." && $object != "..") {
507
                    if (filetype($dir . "/" . $object) == "dir") {
508
                        self::rRmDir($dir . "/" . $object);
509
                    } else {
510
                        unlink($dir . "/" . $object);
511
                    }
512
                }
513
            }
514
515
            // Reset the array pointer and remove the directory
516
            if ($objects !== false) {
517
                reset($objects);
518
            }
519
            rmdir($dir);
520
        }
521
    }
522
523
    /**
524
     * Generates an SSL certificate.
525
     *
526
     * @param array|null $options The options for the certificate (default: null).
527
     * @param array|null $config_args_pkey The configuration arguments for the private key (default: null).
528
     * @param array|null $config_args_csr The configuration arguments for the CSR (default: null).
529
     *
530
     * @return array The generated SSL certificate (public and private keys).
531
     */
532
    public static function generateSslCert($options = null, $config_args_pkey = null, $config_args_csr = null): array
533
    {
534
        // Initialize options if not provided
535
        if (!$options) {
536
            $options = [
537
                "countryName" => 'RU',
538
                "stateOrProvinceName" => 'Moscow',
539
                "localityName" => 'Zelenograd',
540
                "organizationName" => 'MIKO LLC',
541
                "organizationalUnitName" => 'Software development',
542
                "commonName" => 'MIKO PBX',
543
                "emailAddress" => '[email protected]',
544
            ];
545
        }
546
547
        // Initialize CSR configuration arguments if not provided
548
        if (!$config_args_csr) {
549
            $config_args_csr = ['digest_alg' => 'sha256'];
550
        }
551
552
        // Initialize private key configuration arguments if not provided
553
        if (!$config_args_pkey) {
554
            $config_args_pkey = [
555
                "private_key_bits" => 2048,
556
                "private_key_type" => OPENSSL_KEYTYPE_RSA,
557
            ];
558
        }
559
560
        // Generate keys
561
        $private_key = openssl_pkey_new($config_args_pkey);
562
        $csr = openssl_csr_new($options, /** @scrutinizer ignore-type */$private_key, $config_args_csr);
563
        $x509 = openssl_csr_sign($csr, null, $private_key, $days = 3650, $config_args_csr);
564
565
        // Export keys
566
        openssl_x509_export($x509, $certout);
567
        openssl_pkey_export($private_key, $pkeyout);
568
        // echo $pkeyout; // -> WEBHTTPSPrivateKey
569
        // echo $certout; // -> WEBHTTPSPublicKey
570
        return ['PublicKey' => $certout, 'PrivateKey' => $pkeyout];
571
    }
572
573
    /**
574
     * Checks whether the current system is t2 SDE build.
575
     *
576
     * @return bool True if the system is t2 SDE build, false otherwise.
577
     */
578
    public static function isT2SdeLinux(): bool
579
    {
580
        return file_exists('/etc/t2-sde-build');
581
    }
582
583
    /**
584
     * Checks whether the current system has systemctl installed and executable.
585
     *
586
     * @return bool True if systemctl is available, false otherwise.
587
     */
588
    public static function isSystemctl(): bool
589
    {
590
        $pathSystemCtl = self::which('systemctl');
591
        return !empty($pathSystemCtl) && is_executable($pathSystemCtl);
592
    }
593
594
    /**
595
     * Checks whether the current process is running inside a Docker container.
596
     *
597
     * @return bool True if the process is inside a container, false otherwise.
598
     */
599
    public static function isDocker(): bool
600
    {
601
        return file_exists('/.dockerenv');
602
    }
603
604
    /**
605
     * Outputs a message to the main teletype.
606
     *
607
     * @param string $message The message to output.
608
     * @param string $ttyPath The path to the teletype device (default: '/dev/ttyS0').
609
     *
610
     * @return void
611
     */
612
    public static function teletypeEcho(string $message, string $ttyPath = '/dev/ttyS0'): void
613
    {
614
        $pathBusyBox = self::which('busybox');
615
        $ttyTittle = trim(shell_exec("$pathBusyBox setserial -g $ttyPath 2> /dev/null"));
616
        if (strpos($ttyTittle, $ttyPath) !== false && strpos($ttyTittle, 'unknown') === false) {
617
            /** @scrutinizer ignore-unhandled */ @file_put_contents($ttyPath, $message, FILE_APPEND);
618
        }
619
    }
620
621
    /**
622
     * Echoes a teletype message with "DONE" or "FAIL" status.
623
     *
624
     * @param string $message The main message to display.
625
     * @param mixed $result The result status.
626
     *
627
     * @return void
628
     */
629
    public static function teletypeEchoDone(string $message, $result): void
630
    {
631
        $len = max(0, 80 - strlen($message) - 9);
632
        $spaces = str_repeat('.', $len);
633
        if ($result === false) {
634
            $message = " \033[31;1mFAIL\033[0m \n";
635
        } else {
636
            $message = " \033[32;1mDONE\033[0m \n";
637
        }
638
        self::teletypeEcho($spaces . $message);
639
    }
640
641
    /**
642
     * Echoes a "DONE" or "FAIL" message based on the result status.
643
     *
644
     * @param bool $result The result status (true by default).
645
     *
646
     * @return void
647
     */
648
    public static function echoDone(bool $result = true): void
649
    {
650
        if ($result === false) {
651
            echo "\033[31;1mFAIL\033[0m \n";
652
        } else {
653
            echo "\033[32;1mDONE\033[0m \n";
654
        }
655
    }
656
657
    /**
658
     * Echoes a result message with progress dots.
659
     *
660
     * @param string $message The result message to echo.
661
     * @param bool $result The result status (true by default).
662
     *
663
     * @return void
664
     */
665
    public static function echoResult(string $message, bool $result = true): void
666
    {
667
        $cols = self::getCountCols();
668
        if (!is_numeric($cols)) {
669
            // Failed to retrieve the screen width.
670
            return;
671
        }
672
        $len = $cols - strlen($message) - 8;
673
        if ($len < 2) {
674
            // Incorrect screen width.
675
            return;
676
        }
677
678
        $spaces = str_repeat('.', $len);
679
        echo "\r" . $message . $spaces;
680
        self::echoDone($result);
681
    }
682
683
    /**
684
     * Gets the count of columns in the terminal window.
685
     *
686
     * @return string The count of columns.
687
     */
688
    public static function getCountCols(): string
689
    {
690
        $len = 1 * trim(shell_exec('tput cols'));
691
692
        // If the count of columns is zero, set it to a default value of 80
693
        if ($len === 0) {
694
            $len = 80;
695
        } else {
696
            // Limit the count of columns to a maximum of 80
697
            $len = min($len, 80);
698
        }
699
        return $len;
700
    }
701
702
    /**
703
     * Creates or updates a symlink to a target path.
704
     *
705
     * @param string $target The target path.
706
     * @param string $link The symlink path.
707
     * @param bool $isFile Whether the symlink should point to a file (false by default).
708
     *
709
     * @return bool True if the symlink was created or updated, false otherwise.
710
     */
711
    public static function createUpdateSymlink(string $target, string $link, bool $isFile = false): bool
712
    {
713
        $need_create_link = true;
714
        if (is_link($link)) {
715
            $old_target = readlink($link);
716
            $need_create_link = ($old_target != $target);
717
718
            // If needed, remove the old symlink.
719
            if ($need_create_link) {
720
                $cpPath = self::which('cp');
721
                Processes::mwExec("{$cpPath} {$old_target}/* {$target}");
722
                unlink($link);
723
            }
724
        } elseif (is_dir($link)) {
725
            // It should be a symlink. Remove the directory.
726
            rmdir($link);
727
        } elseif (file_exists($link)) {
728
            // It should be a symlink. Remove the file.
729
            unlink($link);
730
        }
731
732
        // Create the target directory if $isFile is false
733
        if ($isFile === false) {
734
            self::mwMkdir($target);
735
        }
736
737
        if ($need_create_link) {
738
            $lnPath = self::which('ln');
739
            Processes::mwExec("{$lnPath} -s {$target}  {$link}");
740
        }
741
742
        return $need_create_link;
743
    }
744
745
    /**
746
     * Creates directories with optional WWW rights.
747
     *
748
     * @param string $parameters The space-separated list of directory paths.
749
     * @param bool $addWWWRights Whether to add WWW rights to the directories.
750
     *
751
     * @return bool True if the directories were created successfully, false otherwise.
752
     */
753
    public static function mwMkdir(string $parameters, bool $addWWWRights = false): bool
754
    {
755
        $result = true;
756
757
        // Check if the script is running with root privileges
758
        if (posix_getuid() === 0) {
759
            $arrPaths = explode(' ', $parameters);
760
            if (count($arrPaths) > 0) {
761
                foreach ($arrPaths as $path) {
762
                    if (!empty($path)
763
                        && !file_exists($path)
764
                        && !mkdir($path, 0755, true)
765
                        && !is_dir($path)) {
766
                        $result = false;
767
                        self::sysLogMsg('Util', 'Error on create folder ' . $path, LOG_ERR);
768
                    }
769
                    if ($addWWWRights) {
770
                        self::addRegularWWWRights($path);
771
                    }
772
                }
773
            }
774
        }
775
776
        return $result;
777
    }
778
779
    /**
780
     * Apply regular rights for folders and files
781
     *
782
     * @param $folder
783
     */
784
    public static function addRegularWWWRights($folder): void
785
    {
786
        if (posix_getuid() === 0) {
787
            $findPath = self::which('find');
788
            $chownPath = self::which('chown');
789
            $chmodPath = self::which('chmod');
790
            Processes::mwExec("{$findPath} {$folder} -type d -exec {$chmodPath} 755 {} \;");
791
            Processes::mwExec("{$findPath} {$folder} -type f -exec {$chmodPath} 644 {} \;");
792
            Processes::mwExec("{$chownPath} -R www:www {$folder}");
793
        }
794
    }
795
796
    /**
797
     * Echoes a message and logs it to the system log.
798
     *
799
     * @param string $message The message to echo and log.
800
     *
801
     * @return void
802
     */
803
    public static function echoWithSyslog(string $message): void
804
    {
805
        echo $message;
806
        // Log the message to the system log with LOG_INFO level
807
        self::sysLogMsg(static::class, $message, LOG_INFO);
808
    }
809
810
    /**
811
     * Adds executable rights to files in a folder.
812
     *
813
     * @param string $folder The folder path.
814
     *
815
     * @return void
816
     */
817
    public static function addExecutableRights(string $folder): void
818
    {
819
        // Check if the script is running with root privileges
820
        if (posix_getuid() === 0) {
821
            $findPath = self::which('find');
822
            $chmodPath = self::which('chmod');
823
824
            // Execute find command to locate files and modify their permissions
825
            Processes::mwExec("{$findPath} {$folder} -type f -exec {$chmodPath} 755 {} \;");
826
        }
827
    }
828
829
    /**
830
     * Parses ini settings from a string.
831
     *
832
     * @param string $manual_attributes The ini settings string.
833
     *
834
     * @return array An array representing the parsed ini settings.
835
     */
836
    public static function parseIniSettings(string $manual_attributes): array
837
    {
838
        // Decode the base64-encoded string if it is valid
839
        $tmp_data = base64_decode($manual_attributes);
840
        if (base64_encode($tmp_data) === $manual_attributes) {
841
            $manual_attributes = $tmp_data;
842
        }
843
        unset($tmp_data);
844
845
        // TRIMMING: Remove leading/trailing spaces and section markers
846
        $tmp_arr = explode("\n", $manual_attributes);
847
        foreach ($tmp_arr as &$row) {
848
            $row = trim($row);
849
            $pos = strpos($row, ']');
850
            if ($pos !== false && strpos($row, '[') === 0) {
851
                $row = "\n" . substr($row, 0, $pos);
852
            }
853
        }
854
        unset($row);
855
        $manual_attributes = implode("\n", $tmp_arr);
856
        // TRIMMING END
857
858
        $manual_data = [];
859
        $sections = explode("\n[", str_replace(']', '', $manual_attributes));
860
        foreach ($sections as $section) {
861
            $data_rows = explode("\n", trim($section));
862
            $section_name = trim($data_rows[0] ?? '');
863
            if (!empty($section_name)) {
864
                unset($data_rows[0]);
865
                $manual_data[$section_name] = [];
866
                foreach ($data_rows as $row) {
867
                    $value = '';
868
869
                    // Skip rows without an equal sign
870
                    if (strpos($row, '=') === false) {
871
                        continue;
872
                    }
873
                    $key = '';
874
                    $arr_value = explode('=', $row);
875
                    if (count($arr_value) > 1) {
876
                        $key = trim($arr_value[0]);
877
                        unset($arr_value[0]);
878
                        $value = trim(implode('=', $arr_value));
879
                    }
880
881
                    // Skip rows with empty key or value not equal to '0'
882
                    if (($value !== '0' && empty($value)) || empty($key)) {
883
                        continue;
884
                    }
885
                    $manual_data[$section_name][$key] = $value;
886
                }
887
            }
888
        }
889
890
        return $manual_data;
891
    }
892
893
    /**
894
     * Converts multidimensional array into single array
895
     *
896
     * @param array $array
897
     *
898
     * @return array
899
     */
900
    public static function flattenArray(array $array): array
901
    {
902
        $result = [];
903
        foreach ($array as $value) {
904
            if (is_array($value)) {
905
                $result = array_merge($result, self::flattenArray($value));
906
            } else {
907
                $result[] = $value;
908
            }
909
        }
910
911
        return $result;
912
    }
913
914
    /**
915
     * Try to find full path to php file by class name
916
     *
917
     * @param $className
918
     *
919
     * @return string|null
920
     */
921
    public static function getFilePathByClassName($className): ?string
922
    {
923
        $filename = null;
924
        try {
925
            $reflection = new ReflectionClass($className);
926
            $filename = $reflection->getFileName();
927
        } catch (ReflectionException $exception) {
928
            self::sysLogMsg(__METHOD__, 'ReflectionException ' . $exception->getMessage(), LOG_ERR);
929
        }
930
931
        return $filename;
932
    }
933
934
935
    /**
936
     * Creates a mutex to ensure synchronized module installation.
937
     *
938
     * @param string $namespace Namespace for the mutex, used to differentiate mutexes.
939
     * @param string $uniqueId Unique identifier for the mutex, usually the module ID.
940
     * @param int $timeout Timeout in seconds for the mutex.
941
     *
942
     * @return PHPRedisMutex Returns an instance of PHPRedisMutex.
943
     */
944
    public static function createMutex(string $namespace, string $uniqueId, int $timeout = 5): PHPRedisMutex
945
    {
946
        $di = Di::getDefault();
947
        $redisAdapter = $di->get(ManagedCacheProvider::SERVICE_NAME)->getAdapter();
948
        $mutexKey = "Mutex:$namespace-" . md5($uniqueId);
949
        return new PHPRedisMutex([$redisAdapter], $mutexKey, $timeout);
950
    }
951
}