Passed
Push — develop ( 3c5942...636dd4 )
by Nikolay
04:57
created

Util::createUpdateSymlink()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 26
rs 9.0777
c 0
b 0
f 0
cc 6
nc 10
nop 2
1
<?php
2
/**
3
 * Copyright © MIKO LLC - All Rights Reserved
4
 * Unauthorized copying of this file, via any medium is strictly prohibited
5
 * Proprietary and confidential
6
 * Written by Alexey Portnov, 6 2020
7
 */
8
9
namespace MikoPBX\Core\System;
10
11
use AGI_AsteriskManager;
12
use DateTime;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, MikoPBX\Core\System\DateTime. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
13
use Exception;
14
use MikoPBX\Common\Models\{CallEventsLogs, CustomFiles};
15
use Phalcon\Db\Adapter\Pdo\Sqlite;
16
use Phalcon\Db\Column;
17
use Phalcon\Di;
18
use ReflectionClass;
19
use SQLite3;
20
21
22
/**
23
 * Вспомогательные методы.
24
 */
25
class Util
26
{
27
28
    /**
29
     * @param $options
30
     * @param $manual_attributes
31
     * @param $section
32
     *
33
     * @return string
34
     */
35
    public static function overrideConfigurationArray($options, $manual_attributes, $section): string
36
    {
37
        $result_config = '';
38
        if ($manual_attributes !== null && isset($manual_attributes[$section])) {
39
            foreach ($manual_attributes[$section] as $key => $value) {
40
                if ($key === 'type') {
41
                    continue;
42
                }
43
                $options[$key] = $value;
44
            }
45
        }
46
        foreach ($options as $key => $value) {
47
            if (empty($value) || empty($key)) {
48
                continue;
49
            }
50
            if (is_array($value)) {
51
                array_unshift($value, ' ');
52
                $result_config .= trim(implode("\n{$key} = ", $value)) . "\n";
53
            } else {
54
                $result_config .= "{$key} = {$value}\n";
55
            }
56
        }
57
58
        return "$result_config\n";
59
    }
60
61
    /**
62
     * Стартует запись логов.
63
     *
64
     * @param int $timeout
65
     */
66
    public static function startLog($timeout = 300): void
67
    {
68
        self::stopLog();
69
        $dir_all_log = System::getLogDir();
70
        $findPath    = self::which('find');
71
        self::mwExec("{$findPath} {$dir_all_log}" . '/ -name *_start_all_log* | xargs rm -rf');
72
        // Получим каталог с логами.
73
        $dirlog = $dir_all_log . '/dir_start_all_log';
74
        self::mwMkdir($dirlog);
75
76
        $pingPath = self::which('ping');
77
        self::mwExecBg("{$pingPath} 8.8.8.8 -w 2", "{$dirlog}/ping_8888.log");
78
        self::mwExecBg("{$pingPath} ya.ru -w 2", "{$dirlog}/ping_8888.log");
79
80
        $opensslPath = self::which('openssl');
81
        self::mwExecBgWithTimeout(
82
            "{$opensslPath} s_client -connect lm.miko.ru:443 > {$dirlog}/openssl_lm_miko_ru.log",
83
            1
84
        );
85
        self::mwExecBgWithTimeout(
86
            "{$opensslPath} s_client -connect lic.miko.ru:443 > {$dirlog}/openssl_lic_miko_ru.log",
87
            1
88
        );
89
        $routePath = self::which('route');
90
        self::mwExecBg("{$routePath} -n ", " {$dirlog}/rout_n.log");
91
92
        $asteriskPath = self::which('asterisk');
93
        self::mwExecBg("{$asteriskPath} -rx 'pjsip show registrations' ", " {$dirlog}/pjsip_show_registrations.log");
94
        self::mwExecBg("{$asteriskPath} -rx 'pjsip show endpoints' ", " {$dirlog}/pjsip_show_endpoints.log");
95
        self::mwExecBg("{$asteriskPath} -rx 'pjsip show contacts' ", " {$dirlog}/pjsip_show_contacts.log");
96
97
        $php_log = '/var/log/php_error.log';
98
        if (file_exists($php_log)) {
99
            $cpPath = self::which('cp');
100
            self::mwExec("{$cpPath} {$php_log} {$dirlog}");
101
        }
102
103
        $network     = new Network();
104
        $arr_eth     = $network->getInterfacesNames();
105
        $tcpdumpPath = self::which('tcpdump');
106
        foreach ($arr_eth as $eth) {
107
            self::mwExecBgWithTimeout(
108
                "{$tcpdumpPath} -i {$eth} -n -s 0 -vvv -w {$dirlog}/{$eth}.pcap",
109
                $timeout,
110
                "{$dirlog}/{$eth}_out.log"
111
            );
112
        }
113
    }
114
115
    /**
116
     * Завершает запись логов.
117
     *
118
     * @return string
119
     */
120
    public static function stopLog(): string
121
    {
122
        $dir_all_log = System::getLogDir();
123
124
        self::killByName('timeout');
125
        self::killByName('tcpdump');
126
127
        $rmPath   = self::which('rm');
128
        $findPath = self::which('find');
129
        $za7Path  = self::which('7za');
130
        $cpPath   = self::which('cp');
131
132
        $dirlog = $dir_all_log . '/dir_start_all_log';
133
        self::mwMkdir($dirlog);
134
135
        $log_dir = System::getLogDir();
136
        self::mwExec("{$cpPath} -R {$log_dir} {$dirlog}");
137
138
        $result = $dir_all_log . '/arhive_start_all_log.zip';
139
        if (file_exists($result)) {
140
            self::mwExec("{$rmPath} -rf {$result}");
141
        }
142
        // Пакуем логи.
143
        self::mwExec("{$za7Path} a -tzip -mx0 -spf '{$result}' '{$dirlog}'");
144
        // Удаляем логи. Оставляем только архив.
145
        self::mwExec("{$findPath} {$dir_all_log}" . '/ -name *_start_all_log | xargs rm -rf');
146
147
        if (file_exists($dirlog)) {
148
            self::mwExec("{$findPath} {$dirlog}" . '/ -name license.key | xargs rm -rf');
149
        }
150
        // Удаляем каталог логов.
151
        self::mwExecBg("{$rmPath} -rf {$dirlog}");
152
153
        return $result;
154
    }
155
156
    /**
157
     * Завершаем процесс по имени.
158
     *
159
     * @param $procName
160
     *
161
     * @return int|null
162
     */
163
    public static function killByName($procName): ?int
164
    {
165
        // $procName = addslashes($procName);
166
        $killallPath = self::which('killall');
167
168
        return self::mwExec($killallPath . ' ' . escapeshellarg($procName));
169
    }
170
171
    /**
172
     * Выполняет системную команду exec().
173
     *
174
     * @param      $command
175
     * @param null $oarr
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $oarr is correct as it would always require null to be passed?
Loading history...
176
     * @param null $retval
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $retval is correct as it would always require null to be passed?
Loading history...
177
     *
178
     * @return int|null
179
     */
180
    public static function mwExec($command, &$oarr = null, &$retval = null): ?int
181
    {
182
        $retval = 0;
183
        $oarr   = [];
184
        $di     = Di::getDefault();
185
186
        if ($di !== null && $di->getShared('config')->path('core.debugMode')) {
187
            echo "mwExec(): $command\n";
188
        } else {
189
            exec("$command 2>&1", $oarr, $retval);
190
        }
191
192
        return $retval;
193
    }
194
195
    /**
196
     * Выполняет системную команду exec() в фоне.
197
     *
198
     * @param $command
199
     * @param $out_file
200
     * @param $sleep_time
201
     */
202
    public static function mwExecBg($command, $out_file = '/dev/null', $sleep_time = 0): void
203
    {
204
        $nohupPath = self::which('nohup');
205
        $shPath    = self::which('sh');
206
        $rmPath    = self::which('rm');
207
        $sleepPath = self::which('sleep');
208
        if ($sleep_time > 0) {
209
            $filename = '/tmp/' . time() . '_noop.sh';
210
            file_put_contents($filename, "{$sleepPath} {$sleep_time}; {$command}; {$rmPath} -rf {$filename}");
211
            $noop_command = "{$nohupPath} {$shPath} {$filename} > {$out_file} 2>&1 &";
212
        } else {
213
            $noop_command = "{$nohupPath} {$command} > {$out_file} 2>&1 &";
214
        }
215
        exec($noop_command);
216
    }
217
218
    /**
219
     * Выполняет системную команду exec() в фоне.
220
     *
221
     * @param        $command
222
     * @param int    $timeout
223
     * @param string $logname
224
     */
225
    public static function mwExecBgWithTimeout($command, $timeout = 4, $logname = '/dev/null'): void
226
    {
227
        $di = Di::getDefault();
228
229
        if ($di !== null && $di->getShared('config')->path('core.debugMode')) {
230
            echo "mwExecBg(): $command\n";
231
232
            return;
233
        }
234
        $nohupPath   = self::which('nohup');
235
        $timeoutPath = self::which('timeout');
236
        exec("{$nohupPath} {$timeoutPath} -t {$timeout} {$command} > {$logname} 2>&1 &");
237
    }
238
239
    /**
240
     * Выполнение нескольких команд.
241
     *
242
     * @param        $arr_cmds
243
     * @param array  $out
244
     * @param string $logname
245
     */
246
    public static function mwExecCommands($arr_cmds, &$out = [], $logname = ''): void
247
    {
248
        $out = [];
249
        foreach ($arr_cmds as $cmd) {
250
            $out[]   = "$cmd;";
251
            $out_cmd = [];
252
            self::mwExec($cmd, $out_cmd);
253
            $out = array_merge($out, $out_cmd);
254
        }
255
256
        if ($logname !== '') {
257
            $result = implode("\n", $out);
258
            file_put_contents("/tmp/{$logname}_commands.log", $result);
259
        }
260
    }
261
262
    /**
263
     * Create folder if it not exist
264
     *
265
     * @param $path
266
     *
267
     * @return bool
268
     */
269
    public static function mwMkdir($path): bool
270
    {
271
        $result = true;
272
        if ( ! file_exists($path) && ! mkdir($path, 0777, true) && ! is_dir($path)) {
273
            $result = false;
274
        }
275
276
        return $result;
277
    }
278
279
    /**
280
     * Restart PHP workers
281
     *
282
     * @param string $className
283
     * @param string $param
284
     */
285
    public static function restartPHPWorker($className, $param = 'start'): void
286
    {
287
        $workerPath = self::getFilePathByClassName($className);
288
        if ( ! empty($workerPath)) {
289
            $command = "php -f {$workerPath}";
290
            self::processWorker($command, $param, $className, 'restart');
291
        }
292
    }
293
294
    /**
295
     * Try to find full path to php file by class name
296
     *
297
     * @param $className
298
     *
299
     * @return string|null
300
     */
301
    public static function getFilePathByClassName($className): ?string
302
    {
303
        $filename = null;
304
        try {
305
            $reflection = new ReflectionClass($className);
306
            $filename   = $reflection->getFileName();
307
        } catch (Exception $exception) {
308
            self::sysLogMsg('Util', 'Error ' . $exception->getMessage());
309
        }
310
311
        return $filename;
312
    }
313
314
    /**
315
     * Добавить сообщение в Syslog.
316
     *
317
     * @param     $log_name
318
     * @param     $text
319
     * @param int $level
320
     */
321
    public static function sysLogMsg($log_name, $text, $level = null): void
322
    {
323
        $level = ($level == null) ? LOG_WARNING : $level;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $level of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
324
        openlog("$log_name", LOG_PID | LOG_PERROR, LOG_AUTH);
325
        syslog($level, "$text");
326
        closelog();
327
    }
328
329
    /**
330
     * Управление процессом / демоном.
331
     * Получние информации по статусу процесса.
332
     *
333
     * @param $cmd
334
     * @param $param
335
     * @param $proc_name
336
     * @param $action
337
     * @param $out_file
338
     *
339
     * @return array | bool
340
     */
341
    public static function processWorker($cmd, $param, $proc_name, $action, $out_file = '/dev/null')
342
    {
343
        $path_kill  = self::which('kill');
344
        $path_nohup = self::which('nohup');
345
346
        $WorkerPID = self::getPidOfProcess($proc_name);
347
348
        if ('status' === $action) {
349
            $status = ($WorkerPID !== '') ? 'Started' : 'Stoped';
350
351
            return ['status' => $status, 'app' => $proc_name, 'PID' => $WorkerPID];
352
        }
353
        $out = [];
354
355
        if ($WorkerPID !== '' && ('stop' === $action || 'restart' === $action)) {
356
            self::mwExec("{$path_kill} -9 {$WorkerPID}  > /dev/null 2>&1 &", $out);
357
            $WorkerPID = '';
358
        }
359
360
        if ($WorkerPID === '' && ('start' === $action || 'restart' === $action)) {
361
            self::mwExec("{$path_nohup} {$cmd} {$param}  > {$out_file} 2>&1 &", $out);
362
            // usleep(500000);
363
        }
364
365
        return true;
366
    }
367
368
    /**
369
     * Return full path to executable binary
370
     *
371
     * @param string $cmd - name of file
372
     *
373
     * @return string
374
     */
375
    public static function which($cmd): string
376
    {
377
        global $_ENV;
378
        $binaryFolders = $_ENV['PATH'];
379
380
        foreach (explode(':', $binaryFolders) as $path) {
381
            if (is_executable("{$path}/{$cmd}")) {
382
                return "{$path}/{$cmd}";
383
            }
384
        }
385
386
        $binaryFolders =
387
            [
388
                '/bin',
389
                '/sbin',
390
                '/usr/bin',
391
                '/usr/sbin',
392
                '/usr/local/bin',
393
                '/usr/local/sbin',
394
            ];
395
        foreach ($binaryFolders as $path) {
396
            if (is_executable("{$path}/{$cmd}")) {
397
                return "{$path}/{$cmd}";
398
            }
399
        }
400
401
        return $cmd;
402
    }
403
404
    /**
405
     * Возвращает PID процесса по его имени.
406
     *
407
     * @param        $name
408
     * @param string $exclude
409
     *
410
     * @return string
411
     */
412
    public static function getPidOfProcess($name, $exclude = ''): string
413
    {
414
        $path_ps   = self::which('ps');
415
        $path_grep = self::which('grep');
416
        $path_awk  = self::which('awk');
417
418
        $name       = addslashes($name);
419
        $filter_cmd = '';
420
        if ( ! empty($exclude)) {
421
            $filter_cmd = "| $path_grep -v " . escapeshellarg($exclude);
422
        }
423
        $out = [];
424
        self::mwExec(
425
            "{$path_ps} -A -o 'pid,args' {$filter_cmd} | {$path_grep} '{$name}' | {$path_grep} -v grep | {$path_awk} ' {print $1} '",
426
            $out
427
        );
428
429
        return trim(implode(' ', $out));
430
    }
431
432
    /**
433
     * Инициация телефонного звонка.
434
     *
435
     * @param string $peer_number
436
     * @param string $peer_mobile
437
     * @param string $dest_number
438
     *
439
     * @return array
440
     */
441
    public static function amiOriginate($peer_number, $peer_mobile, $dest_number): array
442
    {
443
        $am       = self::getAstManager('off');
444
        $channel  = 'Local/' . $peer_number . '@internal-originate';
445
        $context  = 'all_peers';
446
        $IS_ORGNT = self::generateRandomString();
447
        $variable = "_IS_ORGNT={$IS_ORGNT},pt1c_cid={$dest_number},_extenfrom1c={$peer_number},__peer_mobile={$peer_mobile},_FROM_PEER={$peer_number}";
448
449
        $result = $am->Originate($channel, $dest_number, $context, '1', null, null, null, null, $variable, null, '1');
0 ignored issues
show
Bug introduced by
'1' of type string is incompatible with the type boolean expected by parameter $async of AGI_AsteriskManager::Originate(). ( Ignorable by Annotation )

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

449
        $result = $am->Originate($channel, $dest_number, $context, '1', null, null, null, null, $variable, null, /** @scrutinizer ignore-type */ '1');
Loading history...
450
451
        return $result;
452
    }
453
454
    /**
455
     * Получаем объект менеджер asterisk.
456
     *
457
     * @param string $events
458
     *
459
     * @return AGI_AsteriskManager
460
     */
461
    public static function getAstManager($events = 'on'): AGI_AsteriskManager
462
    {
463
        global $g;
464
        require_once 'phpagi.php';
465
        if (isset($g['AGI_AsteriskManager'])) {
466
            /** @var AGI_AsteriskManager $am */
467
            $am = $g['AGI_AsteriskManager'];
468
            // Проверка на разрыв соединения.
469
            if (is_resource($am->socket)) {
470
                $res = $am->sendRequestTimeout('Ping');
471
                if (isset($res['Response']) && trim($res['Response']) != '') {
472
                    // Уже есть подключенный экземпляр класса.
473
                    return $am;
474
                }
475
            } else {
476
                unset($g['AGI_AsteriskManager']);
477
            }
478
        }
479
        $config = new MikoPBXConfig();
480
        $port   = $config->getGeneralSettings('AMIPort');
481
482
        $am  = new AGI_AsteriskManager();
483
        $res = $am->connect("127.0.0.1:{$port}", null, null, $events);
484
        if (true === $res) {
485
            $g['AGI_AsteriskManager'] = $am;
486
        }
487
488
        return $am;
489
    }
490
491
    /**
492
     * Генератор произвольной строки.
493
     *
494
     * @param int $length
495
     *
496
     * @return string
497
     */
498
    public static function generateRandomString($length = 10): string
499
    {
500
        $characters       = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
501
        $charactersLength = strlen($characters);
502
        $randomString     = '';
503
        for ($i = 0; $i < $length; $i++) {
504
            $randomString .= $characters[random_int(0, $charactersLength - 1)];
505
        }
506
507
        return $randomString;
508
    }
509
510
    /**
511
     * Json validate
512
     *
513
     * @param $jsonString
514
     *
515
     * @return bool
516
     */
517
    public static function isJson($jsonString): bool
518
    {
519
        json_decode($jsonString, true);
520
521
        return (json_last_error() === JSON_ERROR_NONE);
522
    }
523
524
    /**
525
     *  Возвращает размер файла в Мб.
526
     *
527
     * @param $filename
528
     *
529
     * @return float|int
530
     */
531
    public static function mFileSize($filename)
532
    {
533
        $size = 0;
534
        if (file_exists($filename)) {
535
            $tmp_size = filesize($filename);
536
            if ($tmp_size !== false) {
537
                // Получим размер в Мб.
538
                $size = $tmp_size;
539
            }
540
        }
541
542
        return $size;
543
    }
544
545
    /**
546
     * Проверка авторизации.
547
     *
548
     * @param Phalcon\Http\Request $request
0 ignored issues
show
Bug introduced by
The type MikoPBX\Core\System\Phalcon\Http\Request was not found. Did you mean Phalcon\Http\Request? If so, make sure to prefix the type with \.
Loading history...
549
     *
550
     * @return bool
551
     */
552
    public static function checkAuthHttp($request)
553
    {
554
        $result   = false;
555
        $userName = $request->getServer('PHP_AUTH_USER');
556
        $password = $request->getServer('PHP_AUTH_PW');
557
558
        $data = file_get_contents('/var/etc/http_auth');
559
        if ("$data" == "{$userName}:{$password}") {
560
            $result = true;
561
        } else {
562
            openlog("miko_ajam", LOG_PID | LOG_PERROR, LOG_AUTH);
563
            syslog(
564
                LOG_WARNING,
565
                "From {$_SERVER['REMOTE_ADDR']}. UserAgent: ({$_SERVER['HTTP_USER_AGENT']}). Fail auth http."
566
            );
567
            closelog();
568
        }
569
570
        return $result;
571
    }
572
573
    /**
574
     * Возвращает указанное количество X.
575
     *
576
     * @param $length
577
     *
578
     * @return string
579
     */
580
    public static function getExtensionX($length): string
581
    {
582
        $extension = '';
583
        for ($i = 0; $i < $length; $i++) {
584
            $extension .= 'X';
585
        }
586
587
        return $extension;
588
    }
589
590
    /**
591
     * Проверяет существование файла.
592
     *
593
     * @param $filename
594
     *
595
     * @return bool
596
     */
597
    public static function recFileExists($filename): ?bool
598
    {
599
        return (file_exists($filename) && filesize($filename) > 0);
600
    }
601
602
    /**
603
     * Если переданный параметр - число, то будет возвращена дата.
604
     *
605
     * @param $data
606
     *
607
     * @return string
608
     */
609
    public static function numberToDate($data): string
610
    {
611
        $re_number = '/^\d+.\d+$/';
612
        preg_match_all($re_number, $data, $matches, PREG_SET_ORDER, 0);
613
        if (count($matches) > 0) {
614
            $data = date('Y.m.d-H:i:s', $data);
615
        }
616
617
        return $data;
618
    }
619
620
    /**
621
     * Записывает данные в файл.
622
     *
623
     * @param $filename
624
     * @param $data
625
     */
626
    public static function fileWriteContent($filename, $data): void
627
    {
628
        /** @var \MikoPBX\Common\Models\CustomFiles $res */
629
        $res = CustomFiles::findFirst("filepath = '{$filename}'");
630
631
        $filename_orgn = "{$filename}.orgn";
632
        if (($res === null || $res->mode === 'none') && file_exists($filename_orgn)) {
633
            unlink($filename_orgn);
634
        } elseif ($res !== null && $res->mode !== 'none') {
635
            // Запишем оригинальный файл.
636
            file_put_contents($filename_orgn, $data);
637
        }
638
639
        if ( ! $res) {
0 ignored issues
show
introduced by
$res is of type MikoPBX\Common\Models\CustomFiles, thus it always evaluated to true.
Loading history...
640
            // Файл еще не зарегистрирован в базе. Сделаем это.
641
            $res = new CustomFiles();
642
            $res->writeAttribute('filepath', $filename);
643
            $res->writeAttribute('mode', 'none');
644
            $res->save();
645
        } elseif ($res->mode === 'append') {
646
            // Добавить к файлу.
647
            $data .= "\n\n";
648
            $data .= base64_decode($res->content);
649
        } elseif ($res->mode === 'override') {
650
            // Переопределить файл.
651
            $data = base64_decode($res->content);
652
        }
653
        file_put_contents($filename, $data);
654
    }
655
656
    /**
657
     * Считывает содержимое файла, если есть разрешение.
658
     *
659
     * @param $filename
660
     * @param $needOriginal
661
     *
662
     * @return array
663
     */
664
    public static function fileReadContent($filename, $needOriginal = true): array
665
    {
666
        $result = [];
667
        $res    = CustomFiles::findFirst("filepath = '{$filename}'");
668
        if ($res !== null) {
669
            $filename_orgn = "{$filename}.orgn";
670
            if ($needOriginal && file_exists($filename_orgn)) {
671
                $filename = $filename_orgn;
672
            }
673
            $result['result'] = 'Success';
674
            $result['data']   = rawurlencode(file_get_contents($filename));
675
        } else {
676
            $result['result']  = 'ERROR';
677
            $result['data']    = '';
678
            $result['message'] = 'There is no access to the file';
679
        }
680
681
        return $result;
682
    }
683
684
    /**
685
     * Смена владельца файла.
686
     *
687
     * @param $filename
688
     * @param $user
689
     */
690
    public static function chown($filename, $user): void
691
    {
692
        if (file_exists($filename)) {
693
            chown($filename, $user);
694
            chgrp($filename, $user);
695
        }
696
    }
697
698
    /**
699
     * Создаем базу данных для логов, если требуется.
700
     */
701
    public static function CreateLogDB(): void
702
    {
703
        $di = Di::getDefault();
704
        if ($di === null) {
705
            self::sysLogMsg('CreateLogDB', 'Dependency injector does not initialized');
706
707
            return;
708
        }
709
710
        $db_path    = $di->getShared('config')->path('eventsLogDatabase.dbfile');
711
        $table_name = 'call_events';
712
        $db         = new Sqlite(['dbname' => $db_path]);
713
        if ( ! $db->tableExists($table_name)) {
714
            $type_str = ['type' => Column::TYPE_TEXT, 'default' => ''];
715
            $type_key = ['type' => Column::TYPE_INTEGER, 'notNull' => true, 'autoIncrement' => true, 'primary' => true];
716
717
            $columns = [
718
                new Column('id', $type_key),
719
                new Column('eventtime', $type_str),
720
                new Column('app', $type_str),
721
                new Column('linkedid', $type_str),
722
                new Column('datajson', $type_str),
723
            ];
724
            $result  = $db->createTable($table_name, '', ['columns' => $columns]);
725
            if ( ! $result) {
726
                self::sysLogMsg('CreateLogDB', 'Can not create db ' . $table_name);
727
728
                return;
729
            }
730
        }
731
732
        $index_names = [
733
            'eventtime' => 1,
734
            'linkedid'  => 1,
735
            'app'       => 1,
736
        ];
737
738
        $index_q = "SELECT" . " name FROM sqlite_master WHERE type='index' AND tbl_name='$table_name'";
739
        $indexes = $db->query($index_q)->fetchAll();
740
        foreach ($indexes as $index_data) {
741
            if (key_exists($index_data['name'], $index_names)) {
742
                unset($index_names[$index_data['name']]);
743
            }
744
        }
745
        foreach ($index_names as $index_name => $value) {
746
            $q      = "CREATE" . " INDEX IF NOT EXISTS i_call_events_{$index_name} ON {$table_name} ({$index_name})";
747
            $result = $db->query($q);
748
            if ( ! $result) {
749
                self::sysLogMsg('CreateLogDB', 'Can not create index ' . $index_name);
750
751
                return;
752
            }
753
        }
754
    }
755
756
    /**
757
     * @param string  $id
758
     * @param SQLite3 $db
759
     *
760
     * @return string
761
     */
762
    public static function GetLastDateLogDB($id, &$db = null): ?string
763
    {
764
        $di = Di::getDefault();
765
        if ($di === null) {
766
            self::sysLogMsg('CreateLogDB', 'Dependency injector does not initialized');
767
768
            return null;
769
        }
770
771
        if ($db == null) {
772
            $cdr_db_path = $di->getShared('config')->path('eventsLogDatabase.dbfile');
773
            $db          = new SQLite3($cdr_db_path);
774
        }
775
        $db->busyTimeout(5000);
776
        $eventtime = null;
777
778
        $q      = 'SELECT' . ' MAX(eventtime) AS eventtime FROM call_events WHERE linkedid="' . $id . '" GROUP BY linkedid';
779
        $result = $db->query($q);
780
        $row    = $result->fetchArray(SQLITE3_ASSOC);
781
        if ($row) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $row of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
782
            $eventtime = $row['eventtime'];
783
        }
784
785
        return $eventtime;
786
    }
787
788
    /**
789
     * Пишем лог в базу данных.
790
     *
791
     * @param $app
792
     * @param $data_obj
793
     */
794
    public static function logMsgDb($app, $data_obj): void
795
    {
796
        try {
797
            $data = new CallEventsLogs();
798
            $data->writeAttribute('eventtime', date("Y-m-d H:i:s"));
799
            $data->writeAttribute('app', $app);
800
            $data->writeAttribute('datajson', json_encode($data_obj, JSON_UNESCAPED_SLASHES));
801
802
            if (is_array($data_obj) && isset($data_obj['linkedid'])) {
803
                $data->writeAttribute('linkedid', $data_obj['linkedid']);
804
            }
805
            $data->save();
806
        } catch (Exception $e) {
807
            self::sysLogMsg('logMsgDb', $e->getMessage());
808
        }
809
    }
810
811
    /**
812
     * Возвращает текущую дату в виде строки с точностью до милисекунд.
813
     *
814
     * @return string
815
     */
816
    public static function getNowDate(): ?string
817
    {
818
        $result = null;
819
        try {
820
            $d      = new DateTime();
821
            $result = $d->format("Y-m-d H:i:s.v");
822
        } catch (Exception $e) {
823
            unset($e);
824
        }
825
826
        return $result;
827
    }
828
829
    /**
830
     * Delete file from disk by filepath
831
     *
832
     * @param $filePath
833
     *
834
     * @return array
835
     */
836
    public static function removeAudioFile($filePath): array
837
    {
838
        $result    = [];
839
        $extension = self::getExtensionOfFile($filePath);
840
        if ( ! in_array($extension, ['mp3', 'wav', 'alaw'])) {
841
            $result['result']  = 'Error';
842
            $result['message'] = "It is forbidden to remove the file $extension.";
843
844
            return $result;
845
        }
846
847
        if ( ! file_exists($filePath)) {
848
            $result['result']  = 'Success';
849
            $result['message'] = "File '{$filePath}' not found.";
850
851
            return $result;
852
        }
853
854
        $out = [];
855
856
        $arrDeletedFiles = [
857
            escapeshellarg(self::trimExtensionForFile($filePath) . ".wav"),
858
            escapeshellarg(self::trimExtensionForFile($filePath) . ".mp3"),
859
            escapeshellarg(self::trimExtensionForFile($filePath) . ".alaw"),
860
        ];
861
862
        $rmPath = self::which('rm');
863
        self::mwExec("{$rmPath} -rf " . implode(' ', $arrDeletedFiles), $out);
864
        if (file_exists($filePath)) {
865
            $result_str        = implode($out);
866
            $result['result']  = 'Error';
867
            $result['message'] = $result_str;
868
        } else {
869
            $result['result'] = 'Success';
870
        }
871
872
        return $result;
873
    }
874
875
    /**
876
     * Получает расширение файла.
877
     *
878
     * @param        $filename
879
     * @param string $delimiter
880
     *
881
     * @return mixed
882
     */
883
    public static function getExtensionOfFile($filename, $delimiter = '.')
0 ignored issues
show
Unused Code introduced by
The parameter $delimiter is not used and could be removed. ( Ignorable by Annotation )

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

883
    public static function getExtensionOfFile($filename, /** @scrutinizer ignore-unused */ $delimiter = '.')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
884
    {
885
        $path_parts = pathinfo($filename);
886
887
        return $path_parts['extension'];
888
    }
889
890
    /**
891
     * Удаляет расширение файла.
892
     *
893
     * @param        $filename
894
     * @param string $delimiter
895
     *
896
     * @return string
897
     */
898
    public static function trimExtensionForFile($filename, $delimiter = '.'): string
899
    {
900
        // Отсечем расширение файла.
901
        $tmp_arr = explode((string)$delimiter, $filename);
902
        if (count($tmp_arr) > 1) {
903
            unset($tmp_arr[count($tmp_arr) - 1]);
904
            $filename = implode((string)$delimiter, $tmp_arr);
905
        }
906
907
        return $filename;
908
    }
909
910
    /**
911
     * Конвертация файла в wav 8000.
912
     *
913
     * @param $filename
914
     *
915
     * @return mixed
916
     */
917
    public static function convertAudioFile($filename)
918
    {
919
        $result = [];
920
        if ( ! file_exists($filename)) {
921
            $result['result']  = 'Error';
922
            $result['message'] = "File '{$filename}' not found.";
923
924
            return $result;
925
        }
926
        $out          = [];
927
        $tmp_filename = '/tmp/' . time() . "_" . basename($filename);
928
        if (false === copy($filename, $tmp_filename)) {
929
            $result['result']  = 'Error';
930
            $result['message'] = "Unable to create temporary file '{$tmp_filename}'.";
931
932
            return $result;
933
        }
934
935
        // Принудительно устанавливаем расширение файла в wav.
936
        $n_filename     = self::trimExtensionForFile($filename) . ".wav";
937
        $n_filename_mp3 = self::trimExtensionForFile($filename) . ".mp3";
938
        // Конвертируем файл.
939
        $tmp_filename = escapeshellcmd($tmp_filename);
940
        $n_filename   = escapeshellcmd($n_filename);
941
        $soxPath      = self::which('sox');
942
        self::mwExec("{$soxPath} -v 0.99 -G '{$tmp_filename}' -c 1 -r 8000 -b 16 '{$n_filename}'", $out);
943
        $result_str = implode('', $out);
944
945
        $lamePath = self::which('lame');
946
        self::mwExec("{$lamePath} -b 32 --silent '{$n_filename}' '{$n_filename_mp3}'", $out);
947
        $result_mp3 = implode('', $out);
948
949
        // Чистим мусор.
950
        unlink($tmp_filename);
951
        if ($result_str !== '' && $result_mp3 !== '') {
952
            // Ошибка выполнения конвертации.
953
            $result['result']  = 'Error';
954
            $result['message'] = $result_str;
955
956
            return $result;
957
        }
958
959
        if ($filename !== $n_filename && $filename !== $n_filename_mp3) {
960
            @unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

960
            /** @scrutinizer ignore-unhandled */ @unlink($filename);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
961
        }
962
963
        $result['result'] = 'Success';
964
        $result['data']   = $n_filename_mp3;
965
966
        return $result;
967
    }
968
969
    /**
970
     * Получаем размер файла / директории.
971
     *
972
     * @param $filename
973
     *
974
     * @return int
975
     */
976
    public static function getSizeOfFile($filename): int
977
    {
978
        $result = 0;
979
        if (file_exists($filename)) {
980
            $duPath  = self::which('du');
981
            $awkPath = self::which('awk');
982
            self::mwExec("{$duPath} -d 0 -k '{$filename}' | {$awkPath}  '{ print $1}'", $out);
983
            $time_str = implode($out);
984
            preg_match_all('/^\d+$/', $time_str, $matches, PREG_SET_ORDER, 0);
985
            if (count($matches) > 0) {
986
                $result = round(1 * $time_str / 1024, 2);
987
            }
988
        }
989
990
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type double which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
991
    }
992
993
    /**
994
     * Устанавливаем шрифт для консоли.
995
     */
996
    public static function setCyrillicFont(): void
997
    {
998
        $setfontPath = self::which('setfont');
999
        self::mwExec("{$setfontPath} /usr/share/consolefonts/Cyr_a8x16.psfu.gz 2>/dev/null");
1000
    }
1001
1002
    /**
1003
     * Получить перевод строки текста.
1004
     *
1005
     * @param $text
1006
     *
1007
     * @return mixed
1008
     */
1009
    public static function translate($text)
1010
    {
1011
        $di = Di::getDefault();
1012
        if ($di !== null) {
1013
            return $di->getShared('translation')->_($text);
1014
        } else {
1015
            return $text;
1016
        }
1017
    }
1018
1019
    /**
1020
     * Check if all the parts exist, and
1021
     * gather all the parts of the file together
1022
     *
1023
     * @param string $temp_dir    - the temporary directory holding all the parts of the file
1024
     * @param string $fileName    - the original file name
1025
     * @param string $totalSize   - original file size (in bytes)
1026
     * @param string $total_files - original file size (in bytes)
1027
     * @param string $result_file - original file size (in bytes)
1028
     * @param int    $chunkSize   - each chunk size (in bytes)
1029
     *
1030
     * @return bool
1031
     */
1032
    public static function createFileFromChunks(
1033
        $temp_dir,
1034
        $fileName,
0 ignored issues
show
Unused Code introduced by
The parameter $fileName is not used and could be removed. ( Ignorable by Annotation )

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

1034
        /** @scrutinizer ignore-unused */ $fileName,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1035
        $totalSize,
1036
        $total_files,
0 ignored issues
show
Unused Code introduced by
The parameter $total_files is not used and could be removed. ( Ignorable by Annotation )

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

1036
        /** @scrutinizer ignore-unused */ $total_files,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1037
        $result_file = '',
0 ignored issues
show
Unused Code introduced by
The parameter $result_file is not used and could be removed. ( Ignorable by Annotation )

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

1037
        /** @scrutinizer ignore-unused */ $result_file = '',

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1038
        $chunkSize = 0
0 ignored issues
show
Unused Code introduced by
The parameter $chunkSize is not used and could be removed. ( Ignorable by Annotation )

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

1038
        /** @scrutinizer ignore-unused */ $chunkSize = 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1039
    ): bool {
1040
        // count all the parts of this file
1041
        $total_files_on_server_size = 0;
1042
        foreach (scandir($temp_dir) as $file) {
1043
            $temp_total                 = $total_files_on_server_size;
1044
            $tempfilesize               = filesize($temp_dir . '/' . $file);
1045
            $total_files_on_server_size = $temp_total + $tempfilesize;
1046
        }
1047
        // check that all the parts are present
1048
        // If the Size of all the chunks on the server is equal to the size of the file uploaded.
1049
        if ($total_files_on_server_size >= $totalSize) {
1050
            // Загрузка завершена.
1051
            return true;
1052
        }
1053
1054
        // Загрузка еще не завершена. Часть файла успешно сохранена.
1055
        return false;
1056
    }
1057
1058
    /**
1059
     * @param        $temp_dir
1060
     * @param        $fileName
1061
     * @param        $total_files
1062
     * @param string $result_file
1063
     * @param string $progress_dir
1064
     *
1065
     * @return bool|string
1066
     */
1067
    public static function mergeFilesInDirectory(
1068
        $temp_dir,
1069
        $fileName,
1070
        $total_files,
1071
        $result_file = '',
1072
        $progress_dir = ''
1073
    ) {
1074
        if (empty($result_file)) {
1075
            $result_file = dirname($temp_dir) . '/' . $fileName;
1076
        }
1077
1078
        $show_progress = file_exists($progress_dir);
1079
        $progress_file = $progress_dir . '/progress';
1080
        if ($show_progress && ! file_exists($progress_file)) {
1081
            file_put_contents($progress_file, '0');
1082
        }
1083
1084
        // create the final destination file
1085
        if (($fp = fopen($result_file, 'w')) !== false) {
1086
            for ($i = 1; $i <= $total_files; $i++) {
1087
                $tmp_file = $temp_dir . '/' . $fileName . '.part' . $i;
1088
                fwrite($fp, file_get_contents($tmp_file));
1089
                // Удаляем временный файл.
1090
                unlink($tmp_file);
1091
                if ($show_progress) {
1092
                    file_put_contents($progress_file, round($i / $total_files * 100), 2);
1093
                }
1094
            }
1095
            fclose($fp);
1096
        } else {
1097
            self::sysLogMsg('UploadFile', 'cannot create the destination file - ' . $result_file);
1098
1099
            return false;
1100
        }
1101
        self::sysLogMsg('UploadFile', 'destination file - ' . $result_file);
1102
        // rename the temporary directory (to avoid access from other
1103
        // concurrent chunks uploads) and than delete it
1104
        if (rename($temp_dir, $temp_dir . '_UNUSED')) {
1105
            self::rRmDir($temp_dir . '_UNUSED');
1106
        } else {
1107
            self::rRmDir($temp_dir);
1108
        }
1109
1110
        if ($show_progress) {
1111
            file_put_contents($progress_file, 100);
1112
        }
1113
1114
        // Загрузка завершена. Возвращаем путь к файлу.
1115
        return $result_file;
1116
    }
1117
1118
    /**
1119
     *
1120
     * Delete a directory RECURSIVELY
1121
     *
1122
     * @param string $dir - directory path
1123
     *
1124
     * @link http://php.net/manual/en/function.rmdir.php
1125
     */
1126
    public static function rRmDir($dir): void
1127
    {
1128
        if (is_dir($dir)) {
1129
            $objects = scandir($dir);
1130
            foreach ($objects as $object) {
1131
                if ($object != "." && $object != "..") {
1132
                    if (filetype($dir . "/" . $object) == "dir") {
1133
                        self::rRmDir($dir . "/" . $object);
1134
                    } else {
1135
                        unlink($dir . "/" . $object);
1136
                    }
1137
                }
1138
            }
1139
            reset($objects);
0 ignored issues
show
Bug introduced by
It seems like $objects can also be of type false; however, parameter $array of reset() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1139
            reset(/** @scrutinizer ignore-type */ $objects);
Loading history...
1140
            rmdir($dir);
1141
        }
1142
    }
1143
1144
    /**
1145
     * Генерация сертификата средствами openssl.
1146
     *
1147
     * @param array $options
1148
     * @param array $config_args_pkey
1149
     * @param array $config_args_csr
1150
     *
1151
     * @return array
1152
     */
1153
    public static function generateSslCert($options = null, $config_args_pkey = null, $config_args_csr = null): array
1154
    {
1155
        // Инициализация настроек.
1156
        if ( ! $options) {
1157
            $options = [
1158
                "countryName"            => 'RU',
1159
                "stateOrProvinceName"    => 'Moscow',
1160
                "localityName"           => 'Zelenograd',
1161
                "organizationName"       => 'MIKO LLC',
1162
                "organizationalUnitName" => 'Software development',
1163
                "commonName"             => 'MIKO PBX',
1164
                "emailAddress"           => '[email protected]',
1165
            ];
1166
        }
1167
1168
        if ( ! $config_args_csr) {
1169
            $config_args_csr = ['digest_alg' => 'sha256'];
1170
        }
1171
1172
        if ( ! $config_args_pkey) {
1173
            $config_args_pkey = [
1174
                "private_key_bits" => 2048,
1175
                "private_key_type" => OPENSSL_KEYTYPE_RSA,
1176
            ];
1177
        }
1178
1179
        // Генерация ключей.
1180
        $private_key = openssl_pkey_new($config_args_pkey);
1181
        $csr         = openssl_csr_new($options, $private_key, $config_args_csr);
1182
        $x509        = openssl_csr_sign($csr, null, $private_key, $days = 3650, $config_args_csr);
1183
1184
        // Экспорт ключей.
1185
        openssl_x509_export($x509, $certout);
1186
        openssl_pkey_export($private_key, $pkeyout);
1187
        // echo $pkeyout; // -> WEBHTTPSPrivateKey
1188
        // echo $certout; // -> WEBHTTPSPublicKey
1189
        return ['PublicKey' => $certout, 'PrivateKey' => $pkeyout];
1190
    }
1191
1192
    /**
1193
     * @return bool
1194
     */
1195
    public static function isSystemctl(): bool
1196
    {
1197
        return (stripos(php_uname('v'), 'debian') !== false);
1198
    }
1199
1200
    /**
1201
     * Выводить текстовое сообщение "done" подсвечивает зеленым цветом.
1202
     */
1203
    public static function echoGreenDone(): void
1204
    {
1205
        echo "\033[32;1mdone\033[0m \n";
1206
    }
1207
1208
    /**
1209
     * Создание символической ссылки, если необходимо.
1210
     *
1211
     * @param $target
1212
     * @param $link
1213
     *
1214
     * @return bool
1215
     */
1216
    public static function createUpdateSymlink($target, $link): bool
1217
    {
1218
        $need_create_link = true;
1219
        if (is_link($link)) {
1220
            $old_target       = readlink($link);
1221
            $need_create_link = ($old_target != $target);
1222
            // Если необходимо, удаляем старую ссылку.
1223
            if ($need_create_link) {
1224
                $cpPath = self::which('cp');
1225
                self::mwExec("{$cpPath} {$old_target}/* {$target}");
1226
                unlink($link);
1227
            }
1228
        } elseif (is_dir($link)) {
1229
            // Это должна быть именно ссылка. Файл удаляем.
1230
            rmdir($link);
1231
        } elseif (file_exists($link)) {
1232
            // Это должна быть именно ссылка. Файл удаляем.
1233
            unlink($link);
1234
        }
1235
        self::mwMkdir($target);
1236
        if ($need_create_link) {
1237
            $lnPath = self::which('ln');
1238
            self::mwExec("{$lnPath} -s {$target}  {$link}");
1239
        }
1240
1241
        return $need_create_link;
1242
    }
1243
1244
    /**
1245
     * Print message and write it to syslog
1246
     *
1247
     * @param $message
1248
     */
1249
    public static function echoWithSyslog($message): void
1250
    {
1251
        echo $message;
1252
        self::sysLogMsg(static::class, $message, LOG_INFO);
1253
    }
1254
1255
    /**
1256
     * Добавляем задачу для уведомлений.
1257
     *
1258
     * @param string $tube
1259
     * @param        $data
1260
     */
1261
    public function addJobToBeanstalk($tube, $data): void
1262
    {
1263
        $queue = new BeanstalkClient($tube);
1264
        $queue->publish(json_encode($data));
1265
    }
1266
1267
    /**
1268
     * Apply regular rights for folders and files
1269
     *
1270
     * @param $folder
1271
     */
1272
    public static function addRegularWWWRights($folder): void
1273
    {
1274
        if (posix_getuid() === 0) {
1275
            $findPath  = self::which('find');
1276
            $chownPath = self::which('chown');
1277
            $chmodPath = self::which('chmod');
1278
            self::mwExec("{$findPath} {$folder} -type d -exec {$chmodPath} 755 {} \;");
1279
            self::mwExec("{$findPath} {$folder} -type f -exec {$chmodPath} 644 {} \;");
1280
            self::mwExec("{$chownPath} -R www:www {$folder}");
1281
        }
1282
    }
1283
1284
    /**
1285
     * Apply executable rights for files
1286
     *
1287
     * @param $folder
1288
     */
1289
    public static function addExecutableRights($folder): void
1290
    {
1291
        if (posix_getuid() === 0) {
1292
            $findPath  = self::which('find');
1293
            $chmodPath = self::which('chmod');
1294
            self::mwExec("{$findPath} {$folder} -type f -exec {$chmodPath} 755 {} \;");
1295
        }
1296
    }
1297
1298
    /**
1299
     * Разбор INI конфига
1300
     *
1301
     * @param string $manual_attributes
1302
     *
1303
     * @return array
1304
     */
1305
    public static function parseIniSettings(string $manual_attributes): array
1306
    {
1307
        $tmp_data = base64_decode($manual_attributes);
1308
        if (base64_encode($tmp_data) === $manual_attributes) {
1309
            $manual_attributes = $tmp_data;
1310
        }
1311
        unset($tmp_data);
1312
        // TRIMMING
1313
        $tmp_arr = explode("\n", $manual_attributes);
1314
        foreach ($tmp_arr as &$row) {
1315
            $row = trim($row);
1316
            $pos = strpos($row, ']');
1317
            if ($pos !== false && strpos($row, '[') === 0) {
1318
                $row = "\n" . substr($row, 0, $pos);
1319
            }
1320
        }
1321
        unset($row);
1322
        $manual_attributes = implode("\n", $tmp_arr);
1323
        // TRIMMING END
1324
1325
        $manual_data = [];
1326
        $sections    = explode("\n[", str_replace(']', '', $manual_attributes));
1327
        foreach ($sections as $section) {
1328
            $data_rows    = explode("\n", trim($section));
1329
            $section_name = trim($data_rows[0] ?? '');
1330
            if ( ! empty($section_name)) {
1331
                unset($data_rows[0]);
1332
                $manual_data[$section_name] = [];
1333
                foreach ($data_rows as $row) {
1334
                    if (strpos($row, '=') === false) {
1335
                        continue;
1336
                    }
1337
                    $arr_value = explode('=', $row);
1338
                    if (count($arr_value) > 1) {
1339
                        $key = trim($arr_value[0]);
1340
                        unset($arr_value[0]);
1341
                        $value = trim(implode('=', $arr_value));
1342
                    }
1343
                    if (empty($value) || empty($key)) {
1344
                        continue;
1345
                    }
1346
                    $manual_data[$section_name][$key] = $value;
1347
                }
1348
            }
1349
        }
1350
1351
        return $manual_data;
1352
    }
1353
1354
}