Passed
Push — develop ( 225864...73f8c7 )
by Nikolay
08:07 queued 03:40
created

Storage::determineFormatFs()   C

Complexity

Conditions 14
Paths 98

Size

Total Lines 86
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 51
c 1
b 0
f 0
dl 0
loc 86
rs 6.2666
cc 14
nc 98
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * 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 Error;
23
use JsonException;
24
use MikoPBX\Common\Config\ClassLoader;
25
use MikoPBX\Common\Models\PbxExtensionModules;
26
use MikoPBX\Common\Models\SoundFiles;
27
use MikoPBX\Common\Models\Storage as StorageModel;
28
use MikoPBX\Common\Providers\ConfigProvider;
29
use MikoPBX\Common\Providers\MainDatabaseProvider;
30
use MikoPBX\Core\Asterisk\CdrDb;
31
use MikoPBX\Core\Config\RegisterDIServices;
32
use MikoPBX\Core\System\Configs\PHPConf;
33
use MikoPBX\Core\System\Configs\SyslogConf;
34
use MikoPBX\Core\System\Upgrade\UpdateDatabase;
35
use MikoPBX\Modules\PbxExtensionUtils;
36
use MikoPBX\PBXCoreREST\Lib\SystemManagementProcessor;
37
use MikoPBX\PBXCoreREST\Workers\WorkerApiCommands;
38
use Phalcon\Di;
39
use function MikoPBX\Common\Config\appPath;
40
41
/**
42
 * Class Storage
43
 *
44
 * Manages storage-related operations.
45
 *
46
 * @package MikoPBX\Core\System
47
 * @property \Phalcon\Config config
48
 */
49
class Storage extends Di\Injectable
50
{
51
    /**
52
     * Move read-only sounds to the storage.
53
     * This function assumes a storage disk is mounted.
54
     *
55
     * @return void
56
     */
57
    public function moveReadOnlySoundsToStorage(): void
58
    {
59
        // Check if a storage disk is mounted
60
        if (!self::isStorageDiskMounted()) {
61
            return;
62
        }
63
64
        // Create the current media directory if it doesn't exist
65
        $currentMediaDir =  $this->config->path('asterisk.customSoundDir'). '/';
66
        if (!file_exists($currentMediaDir)) {
67
            Util::mwMkdir($currentMediaDir);
68
        }
69
70
        $soundFiles = SoundFiles::find();
71
72
        // Iterate through each sound file
73
        foreach ($soundFiles as $soundFile) {
74
            if (stripos($soundFile->path, '/offload/asterisk/sounds/other/') === 0) {
75
                $newPath = $currentMediaDir . pathinfo($soundFile->path)['basename'];
76
77
                // Copy the sound file to the new path
78
                if (copy($soundFile->path, $newPath)) {
79
                    SystemManagementProcessor::convertAudioFile($newPath);
80
81
                    // Update the sound file path and extension
82
                    $soundFile->path = Util::trimExtensionForFile($newPath) . ".mp3";
83
84
                    // Update the sound file if the new path exists
85
                    if (file_exists($soundFile->path)) {
86
                        $soundFile->update();
87
                    }
88
                }
89
            }
90
        }
91
        unset($soundFiles);
92
    }
93
94
    /**
95
     * Check if a storage disk is mounted.
96
     *
97
     * @param string $filter Optional filter for the storage disk.
98
     * @param string $mount_dir If the disk is mounted, the mount directory will be stored in this variable.
99
     * @return bool Returns true if the storage disk is mounted, false otherwise.
100
     */
101
    public static function isStorageDiskMounted(string $filter = '', string &$mount_dir = ''): bool
102
    {
103
        // Check if it's a T2Sde Linux and /storage/usbdisk1/ exists
104
        if (!Util::isT2SdeLinux()
105
            && file_exists('/storage/usbdisk1/')
106
        ) {
107
            $mount_dir = '/storage/usbdisk1/';
108
            return true;
109
        }
110
        if ('' === $filter) {
111
            $varEtcDir = Directories::getDir(Directories::CORE_VAR_ETC_DIR);
112
            $filename = "{$varEtcDir}/storage_device";
113
114
            // If the storage_device file exists, read its contents as the filter, otherwise use 'usbdisk1' as the filter
115
            if (file_exists($filename)) {
116
                $filter = file_get_contents($filename);
117
            } else {
118
                $filter = 'usbdisk1';
119
            }
120
        }
121
        $grepPath = Util::which('grep');
122
        $mountPath = Util::which('mount');
123
        $awkPath = Util::which('awk');
124
125
        $filter = escapeshellarg($filter);
126
127
        // Execute the command to filter the mount points based on the filter
128
        $out = shell_exec("$mountPath | $grepPath $filter | {$awkPath} '{print $3}'");
129
        $mount_dir = trim($out);
130
        return ($mount_dir !== '');
131
    }
132
133
    /**
134
     * Copy MOH (Music on Hold) files to the storage.
135
     * This function assumes a storage disk is mounted.
136
     *
137
     * @return void
138
     */
139
    public function copyMohFilesToStorage(): void
140
    {
141
        // Check if a storage disk is mounted
142
        if (!self::isStorageDiskMounted()) {
143
            return;
144
        }
145
146
        $oldMohDir =  $this->config->path('asterisk.astvarlibdir'). '/sounds/moh';
147
        $currentMohDir = $this->config->path('asterisk.mohdir');
148
149
        // If the old MOH directory doesn't exist or unable to create the current MOH directory, return
150
        if (!file_exists($oldMohDir) || Util::mwMkdir($currentMohDir)) {
151
            return;
152
        }
153
154
        $files = scandir($oldMohDir);
155
156
        // Iterate through each file in the old MOH directory
157
        foreach ($files as $file) {
158
            if (in_array($file, ['.', '..'])) {
159
                continue;
160
            }
161
162
            // Copy the file from the old MOH directory to the current MOH directory
163
            if (copy($oldMohDir . '/' . $file, $currentMohDir . '/' . $file)) {
164
                $sound_file = new SoundFiles();
165
                $sound_file->path = $currentMohDir . '/' . $file;
166
                $sound_file->category = SoundFiles::CATEGORY_MOH;
167
                $sound_file->name = $file;
168
                $sound_file->save();
169
            }
170
        }
171
    }
172
173
    /**
174
     * Mount an SFTP disk.
175
     *
176
     * @param string $host The SFTP server host.
177
     * @param string $port The SFTP server port.
178
     * @param string $user The SFTP server username.
179
     * @param string $pass The SFTP server password.
180
     * @param string $remote_dir The remote directory on the SFTP server.
181
     * @param string $local_dir The local directory to mount the SFTP disk.
182
     * @return bool Returns true if the SFTP disk is successfully mounted, false otherwise.
183
     */
184
    public static function mountSftpDisk(string $host, string $port, string $user, string $pass, string $remote_dir, string $local_dir): bool
185
    {
186
187
        // Create the local directory if it doesn't exist
188
        Util::mwMkdir($local_dir);
189
190
        $out = [];
191
        $timeoutPath = Util::which('timeout');
192
        $sshfsPath = Util::which('sshfs');
193
194
        // Build the command to mount the SFTP disk
195
        $command = "{$timeoutPath} 3 {$sshfsPath} -p {$port} -o nonempty -o password_stdin -o 'StrictHostKeyChecking=no' " . "{$user}@{$host}:{$remote_dir} {$local_dir} << EOF\n" . "{$pass}\n" . "EOF\n";
196
197
        // Execute the command to mount the SFTP disk
198
        Processes::mwExec($command, $out);
199
        $response = trim(implode('', $out));
200
201
        if ('Terminated' === $response) {
202
            // The remote server did not respond or an incorrect password was provided.
203
            unset($response);
204
        }
205
206
        return self::isStorageDiskMounted("$local_dir ");
207
    }
208
209
    /**
210
     * Mount an FTP disk.
211
     *
212
     * @param string $host The FTP server host.
213
     * @param string $port The FTP server port.
214
     * @param string $user The FTP server username.
215
     * @param string $pass The FTP server password.
216
     * @param string $remote_dir The remote directory on the FTP server.
217
     * @param string $local_dir The local directory to mount the FTP disk.
218
     * @return bool Returns true if the FTP disk is successfully mounted, false otherwise.
219
     */
220
    public static function mountFtp(string $host, string $port, string $user, string $pass, string $remote_dir, string $local_dir): bool
221
    {
222
223
        // Create the local directory if it doesn't exist
224
        Util::mwMkdir($local_dir);
225
        $out = [];
226
227
        // Build the authentication line for the FTP connection
228
        $auth_line = '';
229
        if (!empty($user)) {
230
            $auth_line .= 'user="' . $user;
231
            if (!empty($pass)) {
232
                $auth_line .= ":{$pass}";
233
            }
234
            $auth_line .= '",';
235
        }
236
237
        // Build the connect line for the FTP connection
238
        $connect_line = 'ftp://' . $host;
239
        if (!empty($port)) {
240
            $connect_line .= ":{$port}";
241
        }
242
        if (!empty($remote_dir)) {
243
            $connect_line .= "$remote_dir";
244
        }
245
246
        $timeoutPath = Util::which('timeout');
247
        $curlftpfsPath = Util::which('curlftpfs');
248
249
        // Build the command to mount the FTP disk
250
        $command = "{$timeoutPath} 3 {$curlftpfsPath}  -o allow_other -o {$auth_line}fsname={$host} {$connect_line} {$local_dir}";
251
252
        // Execute the command to mount the FTP disk
253
        Processes::mwExec($command, $out);
254
        $response = trim(implode('', $out));
255
        if ('Terminated' === $response) {
256
            // The remote server did not respond or an incorrect password was provided.
257
            unset($response);
258
        }
259
260
        return self::isStorageDiskMounted("$local_dir ");
261
    }
262
263
    /**
264
     * Mount a WebDAV disk.
265
     *
266
     * @param string $host The WebDAV server host.
267
     * @param string $user The WebDAV server username.
268
     * @param string $pass The WebDAV server password.
269
     * @param string $dstDir The destination directory on the WebDAV server.
270
     * @param string $local_dir The local directory to mount the WebDAV disk.
271
     * @return bool Returns true if the WebDAV disk is successfully mounted, false otherwise.
272
     */
273
    public static function mountWebDav(string $host, string $user, string $pass, string $dstDir, string $local_dir): bool
274
    {
275
        $host = trim($host);
276
        $dstDir = trim($dstDir);
277
278
        // Remove trailing slash from host if present
279
        if (substr($host, -1) === '/') {
280
            $host = substr($host, 0, -1);
281
        }
282
283
        // Remove leading slash from destination directory if present
284
        if ($dstDir[0] === '/') {
285
            $dstDir = substr($dstDir, 1);
286
        }
287
288
        // Create the local directory if it doesn't exist
289
        Util::mwMkdir($local_dir);
290
        $out = [];
291
        $conf = 'dav_user www' . PHP_EOL .
292
            'dav_group www' . PHP_EOL;
293
294
295
        // Write WebDAV credentials to secrets file
296
        file_put_contents('/etc/davfs2/secrets', "{$host}{$dstDir} $user $pass");
297
        file_put_contents('/etc/davfs2/davfs2.conf', $conf);
298
        $timeoutPath = Util::which('timeout');
299
        $mount = Util::which('mount.davfs');
300
301
        // Build the command to mount the WebDAV disk
302
        $command = "$timeoutPath 3 yes | $mount {$host}{$dstDir} {$local_dir}";
303
304
        // Execute the command to mount the WebDAV disk
305
        Processes::mwExec($command, $out);
306
        $response = trim(implode('', $out));
307
        if ('Terminated' === $response) {
308
            // The remote server did not respond or an incorrect password was provided.
309
            unset($response);
310
        }
311
        return self::isStorageDiskMounted("$local_dir ");
312
    }
313
314
    /**
315
     * Create a file system on a disk.
316
     *
317
     * @param string $dev The device path of the disk.
318
     * @return bool Returns true if the file system creation process is initiated, false otherwise.
319
     */
320
    public static function mkfs_disk(string $dev)
321
    {
322
        if (!file_exists($dev)) {
323
            $dev = "/dev/{$dev}";
324
        }
325
        if (!file_exists($dev)) {
326
            return false;
327
        }
328
        $dir = '';
329
        self::isStorageDiskMounted($dev, $dir);
330
331
        // If the disk is not mounted or successfully unmounted, proceed with the file system creation
332
        if (empty($dir) || self::umountDisk($dir)) {
333
            $st = new Storage();
334
            // Initiate the file system creation process
335
            $st->formatDiskLocal($dev, true);
336
            sleep(1);
337
338
            return (self::statusMkfs($dev) === 'inprogress');
339
        }
340
341
        // Error occurred during disk unmounting
342
        return false;
343
    }
344
345
    /**
346
     * Unmount a disk.
347
     *
348
     * @param string $dir The mount directory of the disk.
349
     * @return bool Returns true if the disk is successfully unmounted, false otherwise.
350
     */
351
    public static function umountDisk(string $dir): bool
352
    {
353
        $umountPath = Util::which('umount');
354
        $rmPath = Util::which('rm');
355
356
        // If the disk is mounted, terminate processes using the disk and unmount it
357
        if (self::isStorageDiskMounted($dir)) {
358
            Processes::mwExec("/sbin/shell_functions.sh 'killprocesses' '$dir' -TERM 0");
359
            Processes::mwExec("{$umountPath} {$dir}");
360
        }
361
        $result = !self::isStorageDiskMounted($dir);
362
363
        // If the disk is successfully unmounted and the directory exists, remove the directory
364
        if ($result && file_exists($dir)) {
365
            Processes::mwExec("{$rmPath} -rf '{$dir}'");
366
        }
367
368
        return $result;
369
    }
370
371
    /**
372
     * Format a disk locally using parted command.
373
     *
374
     * @param string $device The device path of the disk.
375
     * @param bool $bg Whether to run the command in the background.
376
     * @return bool Returns true if the disk formatting process is initiated, false otherwise.
377
     */
378
    public function formatDiskLocal(string $device, bool $bg = false): bool
379
    {
380
        $partedPath = Util::which('parted');
381
382
        // Execute the parted command to format the disk with msdos partition table and ext4 partition
383
        $retVal = Processes::mwExec(
384
            "{$partedPath} --script --align optimal '{$device}' 'mklabel msdos mkpart primary ext4 0% 100%'"
385
        );
386
387
        // Log the result of the parted command
388
        Util::sysLogMsg(__CLASS__, "{$partedPath} returned {$retVal}");
389
        if (false === $bg) {
390
            sleep(1);
391
        }
392
393
        // Touch the disk to update disk tables
394
        $partprobePath = Util::which('partprobe');
395
        Processes::mwExec(
396
            "{$partprobePath} '{$device}'"
397
        );
398
399
        return $this->formatDiskLocalPart2($device, $bg);
400
    }
401
402
    /**
403
     * Format a disk locally (part 2) using mkfs command.
404
     *
405
     * @param string $device The device path of the disk.
406
     * @param bool $bg Whether to run the command in the background.
407
     * @return bool Returns true if the disk formatting process is successfully completed, false otherwise.
408
     */
409
    private function formatDiskLocalPart2(string $device, bool $bg = false): bool
410
    {
411
        // Determine the device ID based on the last character of the device path
412
        if (is_numeric(substr($device, -1))) {
413
            $device_id = "";
414
        } else {
415
            $device_id = "1";
416
        }
417
        $format = 'ext4';
418
        $mkfsPath = Util::which("mkfs.{$format}");
419
        $cmd = "{$mkfsPath} {$device}{$device_id}";
420
        if ($bg === false) {
421
            // Execute the mkfs command and check the return value
422
            $retVal = Processes::mwExec("{$cmd} 2>&1");
423
            Util::sysLogMsg(__CLASS__, "{$cmd} returned {$retVal}");
424
            $result = ($retVal === 0);
425
        } else {
426
            usleep(200000);
427
            // Execute the mkfs command in the background
428
            Processes::mwExecBg($cmd);
429
            $result = true;
430
        }
431
432
        return $result;
433
    }
434
435
    /**
436
     * Get the status of mkfs process on a disk.
437
     *
438
     * @param string $dev The device path of the disk.
439
     * @return string Returns the status of mkfs process ('inprogress' or 'ended').
440
     */
441
    public static function statusMkfs(string $dev): string
442
    {
443
        if (!file_exists($dev)) {
444
            $dev = "/dev/{$dev}";
445
        }
446
        $out = [];
447
        $psPath = Util::which('ps');
448
        $grepPath = Util::which('grep');
449
450
        // Execute the command to check the status of mkfs process
451
        Processes::mwExec("{$psPath} -A -f | {$grepPath} {$dev} | {$grepPath} mkfs | {$grepPath} -v grep", $out);
452
        $mount_dir = trim(implode('', $out));
453
454
        return empty($mount_dir) ? 'ended' : 'inprogress';
455
    }
456
457
    /**
458
     * Selects the storage disk and performs the necessary configuration.
459
     *
460
     * @param string $automatic Flag to determine if the disk should be selected automatically
461
     * @return bool Returns true on success, false otherwise
462
     */
463
    public static function selectAndConfigureStorageDisk(string $automatic): bool
464
    {
465
        $storage = new self();
466
467
        // Check if the storage disk is already mounted
468
        if (self::isStorageDiskMounted()) {
469
            Util::echoToTeletype(PHP_EOL." " . Util::translate('Storage disk is already mounted...') . " ");
470
            sleep(2);
471
            return true;
472
        }
473
474
        $validDisks = [];
475
        // Get all available hard drives
476
        $all_hdd = $storage->getAllHdd();
477
        $system_disk = '';
478
        $selected_disk = ['size' => 0, 'id' => ''];
479
        // Iterate through all available hard drives
480
        foreach ($all_hdd as $disk) {
481
            $additional = '';
482
            $devName = self::getDevPartName($disk['id'], '4');
483
            $isLiveCd = ($disk['sys_disk'] && file_exists('/offload/livecd'));
484
            $isMountedSysDisk = (!empty($disk['mounted']) && $disk['sys_disk'] && file_exists("/dev/$devName"));
485
486
            // Check if the disk is a system disk and is mounted
487
            if ($isMountedSysDisk || $isLiveCd) {
488
                $system_disk = $disk['id'];
489
                $additional .= "\033[31;1m [SYSTEM]\033[0m";
490
            } elseif ($disk['mounted']) {
491
                // If disk is mounted but not a system disk, continue to the next iteration
492
                continue;
493
            }
494
495
            // Check if the current disk is larger than the previously selected disk
496
            if ($selected_disk['size'] === 0 || $disk['size'] > $selected_disk['size']) {
497
                $selected_disk = $disk;
498
            }
499
500
            $part = $disk['sys_disk'] ? '4' : '1';
501
            $devName = self::getDevPartName($disk['id'], $part);
502
            $devFour = '/dev/' . $devName;
503
            if (self::isStorageDisk($devFour)) {
504
                $additional .= "\033[33;1m [STORAGE] \033[0m";
505
            }
506
507
            // Check if the disk is a system disk and has a valid partition
508
            if ($disk['sys_disk']) {
509
                $part4_found = false;
510
                foreach ($disk['partitions'] as $partition) {
511
                    if ($partition['dev'] === $devName && $partition['size'] > 1000) {
512
                        $part4_found = true;
513
                    }
514
                }
515
                if ($part4_found === false) {
516
                    continue;
517
                }
518
            } elseif ($disk['size'] < 1024) {
519
                // If disk size is less than 1024, continue to the next iteration
520
                continue;
521
            }
522
523
            // Add the valid disk to the validDisks array
524
            $validDisks[$disk['id']] = "  - {$disk['id']}, {$disk['size_text']}, {$disk['vendor']}$additional\n";
525
        }
526
527
        if (empty($validDisks)) {
528
            // If no valid disks were found, log a message and return 0
529
            Util::echoToTeletype(Util::translate('Valid disks not found...'));
530
            sleep(3);
531
            return false;
532
        }
533
534
        // Check if the disk selection should be automatic
535
        if ($automatic === 'auto') {
536
            $target_disk_storage = $selected_disk['id'];
537
            Util::echoToTeletype("Automatically selected storage disk is $target_disk_storage");
538
        } else {
539
            echo PHP_EOL." " . Util::translate('Select the drive to store the data.');
540
            echo PHP_EOL." " . Util::translate('Selected disk:') . "\033[33;1m [{$selected_disk['id']}] \033[0m ".PHP_EOL.PHP_EOL;
541
            Util::echoWithSyslog(PHP_EOL." " . Util::translate('Valid disks are:') . " ".PHP_EOL.PHP_EOL);
542
            foreach ($validDisks as $disk) {
543
                Util::echoWithSyslog($disk);
544
            }
545
            echo PHP_EOL;
546
            // Open standard input in binary mode for interactive reading
547
            $fp = fopen('php://stdin', 'rb');
548
            // Otherwise, prompt the user to enter a disk
549
            do {
550
                echo PHP_EOL . Util::translate('Enter the device name:') . Util::translate('(default value = ') . $selected_disk['id'] . ') :';
551
                $target_disk_storage = trim(fgets($fp));
552
                if ($target_disk_storage === '') {
553
                    $target_disk_storage = $selected_disk['id'];
554
                }
555
            } while (!array_key_exists($target_disk_storage, $validDisks));
556
        }
557
558
        // Determine the disk partition and format if necessary
559
        $dev_disk = "/dev/$target_disk_storage";
560
        if (!empty($system_disk) && $system_disk === $target_disk_storage) {
561
            $part = "4";
562
        } else {
563
            $part = "1";
564
        }
565
        $partName = self::getDevPartName($target_disk_storage, $part);
566
        $part_disk = "/dev/$partName";
567
        if ($part === '1' && !self::isStorageDisk($part_disk)) {
568
            $storage->formatDiskLocal($dev_disk);
569
            $partName = self::getDevPartName($target_disk_storage, $part);
570
            $part_disk = "/dev/$partName";
571
        }
572
573
        // Create an array of disk data
574
        $data = [
575
            'device' => $dev_disk,
576
            'uniqid' => $storage->getUuid($part_disk),
577
            'filesystemtype' => 'ext4',
578
            'name' => 'Storage №1'
579
        ];
580
581
        // Save the disk settings
582
        $storage->saveDiskSettings($data);
583
        if (file_exists('/offload/livecd')) {
584
            // Do not need to start the PBX, it's the station installation in LiveCD mode.
585
            return true;
586
        }
587
        MainDatabaseProvider::recreateDBConnections();
588
589
        // Configure the storage
590
        $storage->configure();
591
        MainDatabaseProvider::recreateDBConnections();
592
        $success = self::isStorageDiskMounted();
593
        if ($success === true && $automatic === 'auto') {
594
            Util::echoToTeletype(' - The data storage disk has been successfully mounted ... ');
595
            sleep(2);
596
            System::rebootSync();
597
            return true;
598
        }
599
600
        if ($automatic === 'auto') {
601
            Util::echoToTeletype(' - Storage disk was not mounted automatically ... ');
602
        }
603
604
        fclose(STDERR);
605
        Util::echoWithSyslog(' - Update database ... ' . PHP_EOL);
606
607
        // Update the database
608
        $dbUpdater = new UpdateDatabase();
609
        $dbUpdater->updateDatabaseStructure();
610
611
        $STDERR = fopen('php://stderr', 'wb');
612
        CdrDb::checkDb();
613
614
        // Restart syslog
615
        $sysLog = new SyslogConf();
616
        $sysLog->reStart();
617
618
        // Configure PBX
619
        $pbx = new PBX();
620
        $pbx->configure();
621
622
        // Restart processes related to storage
623
        Processes::processPHPWorker(WorkerApiCommands::class);
624
625
        // Check if the disk was mounted successfully
626
        if ($success === true) {
627
            Util::echoWithSyslog( "\n " . Util::translate('Storage disk was mounted successfully...') . " \n\n");
628
        } else {
629
            Util::echoWithSyslog( "\n " . Util::translate('Failed to mount the disc...') . " \n\n");
630
        }
631
632
        sleep(3);
633
        if ($STDERR!==false){
634
            fclose($STDERR);
635
        }
636
637
        return $success;
638
    }
639
640
    /**
641
     * Retrieves the partition name of a device.
642
     *
643
     * @param string $dev The device name
644
     * @param string $part The partition number
645
     * @return string The partition name
646
     */
647
    public static function getDevPartName(string $dev, string $part): string
648
    {
649
        $lsBlkPath = Util::which('lsblk');
650
        $cutPath = Util::which('cut');
651
        $grepPath = Util::which('grep');
652
        $sortPath = Util::which('sort');
653
654
        $command = "{$lsBlkPath} -r | {$grepPath} ' part' | {$sortPath} -u | {$cutPath} -d ' ' -f 1 | {$grepPath} \"" . basename(
655
                $dev
656
            ) . "\" | {$grepPath} \"{$part}\$\"";
657
        Processes::mwExec($command, $out);
658
        $devName = trim(implode('', $out));
659
        return trim($devName);
660
    }
661
662
    /**
663
     * Check if a storage disk is valid.
664
     *
665
     * @param string $device The device path of the storage disk.
666
     * @return bool Returns true if the storage disk is valid, false otherwise.
667
     */
668
    public static function isStorageDisk(string $device): bool
669
    {
670
        $result = false;
671
        // Check if the device path exists
672
        if (!file_exists($device)) {
673
            return $result;
674
        }
675
676
        $tmp_dir = '/tmp/mnt_' . time();
677
        Util::mwMkdir($tmp_dir);
678
        $out = [];
679
680
        $storage = new self();
681
        $uid_part = 'UUID=' . $storage->getUuid($device) . '';
682
        $format = $storage->getFsType($device);
683
        // If the file system type is not available, return false
684
        if ($format === '') {
685
            return false;
686
        }
687
        $mountPath = Util::which('mount');
688
        $umountPath = Util::which('umount');
689
        $rmPath = Util::which('rm');
690
691
        Processes::mwExec("{$mountPath} -t {$format} {$uid_part} {$tmp_dir}", $out);
692
        if (is_dir("{$tmp_dir}/mikopbx") && trim(implode('', $out)) === '') {
693
            // $out - empty string, no errors
694
            // mikopbx directory exists
695
            $result = true;
696
        }
697
698
        // Check if the storage disk is mounted, and unmount if necessary
699
        if (self::isStorageDiskMounted($device)) {
700
            Processes::mwExec("{$umountPath} {$device}");
701
        }
702
703
        // Check if the storage disk is unmounted, and remove the temporary directory
704
        if (!self::isStorageDiskMounted($device)) {
705
            Processes::mwExec("{$rmPath} -rf '{$tmp_dir}'");
706
        }
707
708
        return $result;
709
    }
710
711
    /**
712
     * Saves the disk settings to the database.
713
     *
714
     * @param array $data The disk settings data to be saved.
715
     * @param string $id The ID of the disk settings to be updated (default: '1').
716
     * @return void
717
     */
718
    public function saveDiskSettings(array $data, string $id = '1'): void
719
    {
720
        $disk_data = $this->getDiskSettings($id);
721
        if (count($disk_data) === 0) {
722
            $storage_settings = new StorageModel();
723
        } else {
724
            $storage_settings = StorageModel::findFirst("id = '$id'");
725
        }
726
        foreach ($data as $key => $value) {
727
            $storage_settings->writeAttribute($key, $value);
728
        }
729
        $storage_settings->save();
730
    }
731
732
    /**
733
     * Retrieves the disk settings from the database.
734
     *
735
     * @param string $id The ID of the disk (optional).
736
     * @return array The disk settings.
737
     */
738
    public function getDiskSettings(string $id = ''): array
739
    {
740
        $data = [];
741
        if ('' === $id) {
742
            // Return all disk settings.
743
            $data = StorageModel::find()->toArray();
744
        } else {
745
            // Return disk settings for the specified ID.
746
            $pbxSettings = StorageModel::findFirst("id='$id'");
747
            if ($pbxSettings !== null) {
748
                $data = $pbxSettings->toArray();
749
            }
750
        }
751
752
        return $data;
753
    }
754
755
    /**
756
     * Configures the storage settings.
757
     */
758
    public function configure(): void
759
    {
760
        $varEtcDir = $this->config->path('core.varEtcDir');
761
        $storage_dev_file = "{$varEtcDir}/storage_device";
762
        if (!Util::isT2SdeLinux()) {
763
            // Configure for non-T2Sde Linux
764
            file_put_contents($storage_dev_file, "/storage/usbdisk1");
765
            $this->updateConfigWithNewMountPoint("/storage/usbdisk1");
766
            $this->createWorkDirs();
767
            PHPConf::setupLog();
768
            return;
769
        }
770
771
        $cf_disk = '';
772
773
        // Remove the storage_dev_file if it exists
774
        if (file_exists($storage_dev_file)) {
775
            unlink($storage_dev_file);
776
        }
777
778
        // Check if cfdevice file exists and get its content
779
        if (file_exists($varEtcDir . '/cfdevice')) {
780
            $cf_disk = trim(file_get_contents($varEtcDir . '/cfdevice'));
781
        }
782
783
        $disks = $this->getDiskSettings();
784
        $conf = '';
785
786
        // Loop through each disk
787
        foreach ($disks as $disk) {
788
            clearstatcache();
789
            $dev = $this->getStorageDev($disk, $cf_disk);
790
            // Check if the disk exists
791
            if (!$this->hddExists($dev)) {
792
                Util::sysLogMsg(__METHOD__, "HDD - $dev doesn't exist");
793
                continue;
794
            }
795
796
            // Check if the disk is marked as media or storage_dev_file doesn't exist
797
            if ($disk['media'] === '1' || !file_exists($storage_dev_file)) {
798
                Util::sysLogMsg(__METHOD__, "Update the storage_dev_file and the mount point configuration");
799
                file_put_contents($storage_dev_file, "/storage/usbdisk{$disk['id']}");
800
                $this->updateConfigWithNewMountPoint("/storage/usbdisk{$disk['id']}");
801
            }
802
803
            $formatFs = $this->getFsType($dev);
804
805
            // Check if the file system type matches the expected type
806
            if ($formatFs !== $disk['filesystemtype'] && !($formatFs === 'ext4' && $disk['filesystemtype'] === 'ext2')) {
807
                Util::sysLogMsg(__METHOD__, "The file system type has changed {$disk['filesystemtype']} -> {$formatFs}. The disk will not be connected.");
808
                continue;
809
            }
810
            $str_uid = 'UUID=' . $this->getUuid($dev);
811
            $conf .= "{$str_uid} /storage/usbdisk{$disk['id']} {$formatFs} async,rw 0 0\n";
812
            $mount_point = "/storage/usbdisk{$disk['id']}";
813
            Util::mwMkdir($mount_point);
814
            Util::sysLogMsg(__METHOD__, "Create mount point: $conf");
815
        }
816
817
        // Save the configuration to the fstab file
818
        $this->saveFstab($conf);
819
820
        // Create necessary working directories
821
        $this->createWorkDirs();
822
823
        // Set up the PHP log configuration
824
        PHPConf::setupLog();
825
    }
826
827
    /**
828
     * Updates the configuration file with the new mount point.
829
     *
830
     * After mount storage we will change /mountpoint/ to new $mount_point value
831
     *
832
     * @param string $mount_point The new mount point.
833
     * @throws Error If the original configuration file has a broken format.
834
     */
835
    private function updateConfigWithNewMountPoint(string $mount_point): void
836
    {
837
        $settingsFile = '/etc/inc/mikopbx-settings.json';
838
        $staticSettingsFileOrig = '/etc/inc/mikopbx-settings-orig.json';
839
        if (!file_exists($staticSettingsFileOrig)) {
840
            copy($settingsFile, $staticSettingsFileOrig);
841
        }
842
843
        $jsonString = file_get_contents($staticSettingsFileOrig);
844
        try {
845
            $data = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
846
        } catch (JsonException $exception) {
847
            throw new Error("{$staticSettingsFileOrig} has broken format");
848
        }
849
        foreach ($data as $rootKey => $rootEntry) {
850
            foreach ($rootEntry as $nestedKey => $entry) {
851
                if (stripos($entry, '/mountpoint') !== false) {
852
                    $data[$rootKey][$nestedKey] = str_ireplace('/mountpoint', $mount_point, $entry);
853
                }
854
            }
855
        }
856
857
        $newJsonString = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
858
        file_put_contents($settingsFile, $newJsonString);
859
        $this->updateEnvironmentAfterChangeMountPoint();
860
    }
861
862
    /**
863
     * Updates the environment after changing the mount point.
864
     * - Recreates the config provider and updates the config variable.
865
     * - Reloads classes from system and storage disks.
866
     * - Reloads all providers.
867
     */
868
    private function updateEnvironmentAfterChangeMountPoint(): void
869
    {
870
        // Update config variable
871
        ConfigProvider::recreateConfigProvider();
872
        $this->config = $this->di->get('config');
873
874
        // Reload cached values
875
        Directories::reset();
876
877
        // Reload classes from system and storage disks
878
        ClassLoader::init();
879
880
        // Reload all providers
881
        RegisterDIServices::init();
882
    }
883
884
    /**
885
     * Creates the necessary working directories and symlinks.
886
     *
887
     * @return void
888
     */
889
    private function createWorkDirs(): void
890
    {
891
        $path = '';
892
        $mountPath = Util::which('mount');
893
        Processes::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
894
895
        $isLiveCd = file_exists('/offload/livecd');
896
897
        // Create directories
898
        $arrConfig = $this->config->toArray();
899
        foreach ($arrConfig as $rootEntry) {
900
            foreach ($rootEntry as $key => $entry) {
901
                if (stripos($key, 'path') === false && stripos($key, 'dir') === false) {
902
                    continue;
903
                }
904
                if (file_exists($entry)) {
905
                    continue;
906
                }
907
                if ($isLiveCd && strpos($entry, '/offload/') === 0) {
908
                    continue;
909
                }
910
                $path .= " $entry";
911
            }
912
        }
913
914
        if (!empty($path)) {
915
            Util::mwMkdir($path);
916
        }
917
918
        $downloadCacheDir = appPath('sites/pbxcore/files/cache');
919
        if (!$isLiveCd) {
920
            Util::mwMkdir($downloadCacheDir);
921
            Util::createUpdateSymlink($this->config->path('www.downloadCacheDir'), $downloadCacheDir);
922
        }
923
924
        $this->createAssetsSymlinks();
925
        $this->createViewSymlinks();
926
        $this->createAGIBINSymlinks($isLiveCd);
927
928
        Util::createUpdateSymlink($this->config->path('www.uploadDir'), '/ultmp');
929
930
        $filePath = appPath('src/Core/Asterisk/Configs/lua/extensions.lua');
931
        Util::createUpdateSymlink($filePath, '/etc/asterisk/extensions.lua');
932
933
        $this->clearCacheFiles();
934
        $this->clearTmpFiles();
935
        $this->applyFolderRights();
936
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
937
    }
938
939
    /**
940
     * Clears the cache files for various directories.
941
     *
942
     * @return void
943
     */
944
    public function clearCacheFiles(): void
945
    {
946
        $cacheDirs = [];
947
        $cacheDirs[] = $this->config->path('www.uploadDir');
948
        $cacheDirs[] = $this->config->path('www.downloadCacheDir');
949
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/js';
950
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/css';
951
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/img';
952
        $cacheDirs[] = $this->config->path('adminApplication.viewCacheDir');
953
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
954
        $rmPath = Util::which('rm');
955
956
        // Clear cache files for each directory
957
        foreach ($cacheDirs as $cacheDir) {
958
            if (!empty($cacheDir)) {
959
                Processes::mwExec("{$rmPath} -rf {$cacheDir}/*");
960
            }
961
        }
962
963
        // Delete boot cache folders if storage disk is mounted
964
        if (is_dir('/mountpoint') && self::isStorageDiskMounted()) {
965
            Processes::mwExec("{$rmPath} -rf /mountpoint");
966
        }
967
    }
968
969
    /**
970
     * Clear files in temp directories
971
     * @return void
972
     */
973
    private function clearTmpFiles(): void
974
    {
975
        $busyboxPath = Util::which('busybox');
976
        $tmpDir = $this->config->path('core.tempDir');
977
        if (!file_exists($tmpDir)) {
978
            return;
979
        }
980
        // Trying to get a list of files
981
        Processes::mwExec("$busyboxPath timeout 10 $busyboxPath find $tmpDir -type f", $out, $ret);
982
        if ($ret !== 0) {
983
            // there are too many files in the temporary directory, we will clear them
984
            // it may cause a failure when setting access rights (chown)
985
            $resDirForRm = "$tmpDir-" . time();
986
            shell_exec("$busyboxPath mv '$tmpDir' '$resDirForRm'");
987
            if (file_exists("$resDirForRm/swapfile")) {
988
                // Saving only the swap file
989
                shell_exec("$busyboxPath mv '$resDirForRm/swapfile' '$tmpDir/swapfile'");
990
            }
991
            // Let's start deleting temporary files
992
            Processes::mwExecBg("/usr/bin/nice -n 19 $busyboxPath rm -rf $resDirForRm");
993
        }
994
        Util::mwMkdir($tmpDir, true);
995
    }
996
997
    /**
998
     * Retrieves the storage device for the given disk.
999
     *
1000
     * @param array $disk The disk information.
1001
     * @param string $cfDisk The cf_disk information.
1002
     * @return string The storage device path.
1003
     */
1004
    private function getStorageDev($disk, $cf_disk): string
1005
    {
1006
        if (!empty($disk['uniqid']) && strpos($disk['uniqid'], 'STORAGE-DISK') === false) {
1007
            // Find the partition name by UID.
1008
            $lsBlkPath = Util::which('lsblk');
1009
            $busyboxPath = Util::which('busybox');
1010
            $cmd = "{$lsBlkPath} -r -o NAME,UUID | {$busyboxPath} grep {$disk['uniqid']} | {$busyboxPath} cut -d ' ' -f 1";
1011
            $dev = '/dev/' . trim(shell_exec($cmd));
1012
            if ($this->hddExists($dev)) {
1013
                // Disk exists.
1014
                return $dev;
1015
            }
1016
        }
1017
        // Determine the disk by its name.
1018
        if ($disk['device'] !== "/dev/{$cf_disk}") {
1019
            // If it's a regular disk, use partition 1.
1020
            $part = "1";
1021
        } else {
1022
            // If it's a system disk, attempt to connect partition 4.
1023
            $part = "4";
1024
        }
1025
        $devName = self::getDevPartName($disk['device'], $part);
1026
        return '/dev/' . $devName;
1027
    }
1028
1029
    /**
1030
     * Checks if the HDD exists.
1031
     *
1032
     * @param string $disk The HDD device.
1033
     * @return bool True if the HDD exists, false otherwise.
1034
     */
1035
    private function hddExists(string $disk): bool
1036
    {
1037
        if (is_dir($disk)) {
1038
            Util::sysLogMsg(__METHOD__, $disk.' is a dir, not disk', LOG_DEBUG);
1039
            return false;
1040
        }
1041
        if (!file_exists($disk)){
1042
            Util::sysLogMsg(__METHOD__, "Check if the file with name $disk exists fail", LOG_DEBUG);
1043
            return false;
1044
        }
1045
1046
        $uid = $this->getUuid($disk);
1047
        if (!empty($uid)) {
1048
            return true;
1049
        }
1050
        return false;
1051
    }
1052
1053
    /**
1054
     * Saves the fstab configuration.
1055
     *
1056
     * @param string $conf Additional configuration to append to fstab
1057
     * @return void
1058
     */
1059
    public function saveFstab(string $conf = ''): void
1060
    {
1061
        $varEtcDir = $this->config->path('core.varEtcDir');
1062
1063
        // Create the mount point directory for additional disks
1064
        Util::mwMkdir('/storage');
1065
        $chmodPath = Util::which('chmod');
1066
        Processes::mwExec("$chmodPath 755 /storage");
1067
1068
        // Check if cf device file exists
1069
        if (!file_exists($varEtcDir . '/cfdevice')) {
1070
            return;
1071
        }
1072
        $fstab = '';
1073
1074
        // Read cf device file
1075
        $file_data = file_get_contents($varEtcDir . '/cfdevice');
1076
        $cf_disk = trim($file_data);
1077
        if ('' === $cf_disk) {
1078
            return;
1079
        }
1080
        $part2 = self::getDevPartName($cf_disk, '2');
1081
        $part3 = self::getDevPartName($cf_disk, '3');
1082
1083
        $uid_part2 = 'UUID=' . $this->getUuid("/dev/$part2");
1084
        $format_p2 = $this->getFsType($part2);
1085
        $uid_part3 = 'UUID=' . $this->getUuid("/dev/$part3");
1086
        $format_p3 = $this->getFsType($part3);
1087
1088
        $fstab .= "$uid_part2 /offload $format_p2 ro 0 0\n";
1089
        $fstab .= "$uid_part3 /cf $format_p3 rw 1 1\n";
1090
        $fstab .= $conf;
1091
1092
        // Write fstab file
1093
        file_put_contents("/etc/fstab", $fstab);
1094
1095
        // Duplicate for vm tools d
1096
        file_put_contents("/etc/mtab", $fstab);
1097
1098
        // Mount the file systems
1099
        $mountPath     = Util::which('mount');
1100
        $resultOfMount = Processes::mwExec("$mountPath -a", $out);
1101
        Util::echoToTeletype(PHP_EOL." - The mount disks result according to /etc/fstab is $resultOfMount");
1102
        if($resultOfMount !== 0){
1103
            Util::echoToTeletype(" - Error mount ". implode(' ', $out));
1104
        }
1105
        // Add regular www rights to /cf directory
1106
        Util::addRegularWWWRights('/cf');
1107
    }
1108
1109
    /**
1110
     * Returns candidates to storage
1111
     * @return array
1112
     */
1113
    public function getStorageCandidate(): array
1114
    {
1115
        $disks = $this->getLsBlkDiskInfo();
1116
        $storages = [];
1117
        foreach ($disks as $disk) {
1118
            if ($disk['type'] !== 'disk') {
1119
                continue;
1120
            }
1121
            $children = $disk['children'] ?? [];
1122
            if (count($children) === 0) {
1123
                continue;
1124
            }
1125
            if (count($children) === 1) {
1126
                $part = '1';
1127
            } else {
1128
                $part = '4';
1129
            }
1130
1131
            $dev = '';
1132
            $fs = '';
1133
            foreach ($children as $child) {
1134
                if ($child['fstype'] !== 'ext4' && $child['fstype'] !== 'ext2') {
1135
                    continue;
1136
                }
1137
                if ($disk['name'] . $part === $child['name']) {
1138
                    $dev = '/dev/' . $child['name'];
1139
                    $fs = $child['fstype'];
1140
                    break;
1141
                }
1142
            }
1143
            if (!empty($dev) && !empty($fs)) {
1144
                $storages[$dev] = $fs;
1145
            }
1146
        }
1147
1148
        return $storages;
1149
    }
1150
1151
    /**
1152
     * Get disk information using lsblk command.
1153
     *
1154
     * @return array An array containing disk information.
1155
     */
1156
    private function getLsBlkDiskInfo(): array
1157
    {
1158
        $lsBlkPath = Util::which('lsblk');
1159
1160
        // Execute lsblk command to get disk information in JSON format
1161
        Processes::mwExec(
1162
            "{$lsBlkPath} -J -b -o VENDOR,MODEL,SERIAL,LABEL,TYPE,FSTYPE,MOUNTPOINT,SUBSYSTEMS,NAME,UUID",
1163
            $out
1164
        );
1165
        try {
1166
            $data = json_decode(implode(PHP_EOL, $out), true, 512, JSON_THROW_ON_ERROR);
1167
            $data = $data['blockdevices'] ?? [];
1168
        } catch (JsonException $e) {
1169
            $data = [];
1170
        }
1171
        return $data;
1172
    }
1173
1174
    /**
1175
     * Create system folders and links after upgrade and connect config DB
1176
     *
1177
     * @return void
1178
     */
1179
    public function createWorkDirsAfterDBUpgrade(): void
1180
    {
1181
        // Remount /offload directory as read-write
1182
        $mountPath = Util::which('mount');
1183
        Processes::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
1184
1185
        // Create symlinks for module caches
1186
        $this->createModulesCacheSymlinks();
1187
1188
        // Apply folder rights
1189
        $this->applyFolderRights();
1190
1191
        // Remount /offload directory as read-only
1192
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1193
    }
1194
1195
    /**
1196
     * Creates symlinks for module caches.
1197
     *
1198
     * @return void
1199
     */
1200
    public function createModulesCacheSymlinks(): void
1201
    {
1202
        $modules = PbxExtensionModules::getModulesArray();
1203
        foreach ($modules as $module) {
1204
            // Create cache links for JS, CSS, IMG folders
1205
            PbxExtensionUtils::createAssetsSymlinks($module['uniqid']);
1206
1207
            // Create links for the module view templates
1208
            PbxExtensionUtils::createViewSymlinks($module['uniqid']);
1209
1210
            // Create AGI bin symlinks for the module
1211
            PbxExtensionUtils::createAgiBinSymlinks($module['uniqid']);
1212
        }
1213
    }
1214
1215
    /**
1216
     * Creates symlinks for asset cache directories.
1217
     *
1218
     * @return void
1219
     */
1220
    public function createAssetsSymlinks(): void
1221
    {
1222
        // Create symlink for JS cache directory
1223
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
1224
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/js', $jsCacheDir);
1225
1226
        // Create symlink for CSS cache directory
1227
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
1228
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/css', $cssCacheDir);
1229
1230
        // Create symlink for image cache directory
1231
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
1232
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/img', $imgCacheDir);
1233
1234
    }
1235
1236
    /**
1237
     * Creates symlinks for modules view.
1238
     *
1239
     * @return void
1240
     */
1241
    public function createViewSymlinks(): void
1242
    {
1243
        $viewCacheDir = appPath('src/AdminCabinet/Views/Modules');
1244
        Util::createUpdateSymlink($this->config->path('adminApplication.viewCacheDir'), $viewCacheDir);
1245
    }
1246
1247
    /**
1248
     * Creates AGI bin symlinks for extension modules.
1249
     *
1250
     * @param bool $isLiveCd Whether the system loaded on LiveCD mode.
1251
     * @return void
1252
     */
1253
    public function createAGIBINSymlinks(bool $isLiveCd): void
1254
    {
1255
        $agiBinDir = $this->config->path('asterisk.astagidir');
1256
        if ($isLiveCd && strpos($agiBinDir, '/offload/') !== 0) {
1257
            Util::mwMkdir($agiBinDir);
1258
        }
1259
1260
        $roAgiBinFolder = appPath('src/Core/Asterisk/agi-bin');
1261
        $files = glob("{$roAgiBinFolder}/*.{php}", GLOB_BRACE);
1262
        foreach ($files as $file) {
1263
            $fileInfo = pathinfo($file);
1264
            $newFilename = "{$agiBinDir}/{$fileInfo['filename']}.{$fileInfo['extension']}";
1265
            Util::createUpdateSymlink($file, $newFilename);
1266
        }
1267
    }
1268
1269
    /**
1270
     * Applies folder rights to the appropriate directories.
1271
     *
1272
     * @return void
1273
     */
1274
    private function applyFolderRights(): void
1275
    {
1276
1277
        $www_dirs = []; // Directories with WWW rights
1278
        $exec_dirs = []; // Directories with executable rights
1279
1280
        $arrConfig = $this->config->toArray();
1281
1282
        // Get the directories for WWW rights from the configuration
1283
        foreach ($arrConfig as $key => $entry) {
1284
            if (in_array($key, ['www', 'adminApplication'])) {
1285
                foreach ($entry as $subKey => $subEntry) {
1286
                    if (stripos($subKey, 'path') === false && stripos($subKey, 'dir') === false) {
1287
                        continue;
1288
                    }
1289
                    $www_dirs[] = $subEntry;
1290
                }
1291
            }
1292
        }
1293
1294
        // Add additional directories with WWW rights
1295
        $www_dirs[] = $this->config->path('core.tempDir');
1296
        $www_dirs[] = $this->config->path('core.logsDir');
1297
1298
        // Create empty log files with WWW rights
1299
        $logFiles = [
1300
            $this->config->path('database.debugLogFile'),
1301
            $this->config->path('cdrDatabase.debugLogFile'),
1302
        ];
1303
1304
        foreach ($logFiles as $logFile) {
1305
            $filename = (string)$logFile;
1306
            if (!file_exists($filename)) {
1307
                file_put_contents($filename, '');
1308
            }
1309
            $www_dirs[] = $filename;
1310
        }
1311
1312
        $www_dirs[] = '/etc/version';
1313
        $www_dirs[] = appPath('/');
1314
1315
        // Add read rights to the directories
1316
        Util::addRegularWWWRights(implode(' ', $www_dirs));
1317
1318
        // Add executable rights to the directories
1319
        $exec_dirs[] = appPath('src/Core/Asterisk/agi-bin');
1320
        $exec_dirs[] = appPath('src/Core/Rc');
1321
        Util::addExecutableRights(implode(' ', $exec_dirs));
1322
1323
        $mountPath = Util::which('mount');
1324
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1325
    }
1326
1327
    /**
1328
     * Mounts the swap file.
1329
     */
1330
    public function mountSwap(): void
1331
    {
1332
        $tempDir = $this->config->path('core.tempDir');
1333
        $swapFile = "{$tempDir}/swapfile";
1334
1335
        $swapOffCmd = Util::which('swapoff');
1336
        Processes::mwExec("{$swapOffCmd} {$swapFile}");
1337
1338
        $this->makeSwapFile($swapFile);
1339
        if (!file_exists($swapFile)) {
1340
            return;
1341
        }
1342
        $swapOnCmd = Util::which('swapon');
1343
        $result = Processes::mwExec("{$swapOnCmd} {$swapFile}");
1344
        Util::sysLogMsg('Swap', 'connect swap result: ' . $result, LOG_INFO);
1345
    }
1346
1347
    /**
1348
     * Creates a swap file.
1349
     *
1350
     * @param string $swapFile The path to the swap file.
1351
     */
1352
    private function makeSwapFile(string $swapFile): void
1353
    {
1354
        $swapLabel = Util::which('swaplabel');
1355
1356
        // Check if swap file already exists
1357
        if (Processes::mwExec("{$swapLabel} {$swapFile}") === 0) {
1358
            return;
1359
        }
1360
        if (file_exists($swapFile)) {
1361
            unlink($swapFile);
1362
        }
1363
1364
        $size = $this->getStorageFreeSpaceMb();
1365
        if ($size > 2000) {
1366
            $swapSize = 1024;
1367
        } elseif ($size > 1000) {
1368
            $swapSize = 512;
1369
        } else {
1370
            // Not enough free space.
1371
            return;
1372
        }
1373
        $bs = 1024;
1374
        $countBlock = $swapSize * $bs;
1375
        $ddCmd = Util::which('dd');
1376
1377
        Util::sysLogMsg('Swap', 'make swap ' . $swapFile, LOG_INFO);
1378
1379
        // Create swap file using dd command
1380
        Processes::mwExec("{$ddCmd} if=/dev/zero of={$swapFile} bs={$bs} count={$countBlock}");
1381
1382
        $mkSwapCmd = Util::which('mkswap');
1383
1384
        // Set up swap space on the file
1385
        Processes::mwExec("{$mkSwapCmd} {$swapFile}");
1386
    }
1387
1388
    /**
1389
     * Retrieves the amount of free storage space in megabytes.
1390
     *
1391
     * @return int The amount of free storage space in megabytes.
1392
     */
1393
    public function getStorageFreeSpaceMb(): int
1394
    {
1395
        $size = 0;
1396
        $mntDir = '';
1397
        $mounted = self::isStorageDiskMounted('', $mntDir);
1398
        if (!$mounted) {
1399
            return 0;
1400
        }
1401
        $hd = $this->getAllHdd(true);
1402
        foreach ($hd as $disk) {
1403
            if ($disk['mounted'] === $mntDir) {
1404
                $size = $disk['free_space'];
1405
                break;
1406
            }
1407
        }
1408
        return $size;
1409
    }
1410
1411
    /**
1412
     * Get information about all HDD devices.
1413
     *
1414
     * @param bool $mounted_only Whether to include only mounted devices.
1415
     * @return array An array of HDD device information.
1416
     */
1417
    public function getAllHdd(bool $mounted_only = false): array
1418
    {
1419
        $res_disks = [];
1420
1421
        if (Util::isDocker()) {
1422
1423
            // Get disk information for /storage directory
1424
            $out = [];
1425
            $grepPath = Util::which('grep');
1426
            $dfPath = Util::which('df');
1427
            $awkPath = Util::which('awk');
1428
1429
            // Execute the command to get disk information for /storage directory
1430
            Processes::mwExec(
1431
                "{$dfPath} -k /storage | {$awkPath}  '{ print \$1 \"|\" $3 \"|\" \$4} ' | {$grepPath} -v 'Available'",
1432
                $out
1433
            );
1434
            $disk_data = explode('|', implode(" ", $out));
1435
            if (count($disk_data) === 3) {
1436
                $m_size = round(($disk_data[1] + $disk_data[2]) / 1024, 1);
1437
1438
                // Add Docker disk information to the result
1439
                $res_disks[] = [
1440
                    'id' => $disk_data[0],
1441
                    'size' => "" . $m_size,
1442
                    'size_text' => "" . $m_size . " Mb",
1443
                    'vendor' => 'Debian',
1444
                    'mounted' => '/storage/usbdisk',
1445
                    'free_space' => round($disk_data[2] / 1024, 1),
1446
                    'partitions' => [],
1447
                    'sys_disk' => true,
1448
                ];
1449
            }
1450
1451
            return $res_disks;
1452
        }
1453
1454
        // Get CD-ROM and HDD devices
1455
        $cd_disks = $this->cdromGetDevices();
1456
        $disks = $this->diskGetDevices();
1457
1458
        $cf_disk = '';
1459
        $varEtcDir = $this->config->path('core.varEtcDir');
1460
1461
        if (file_exists($varEtcDir . '/cfdevice')) {
1462
            $cf_disk = trim(file_get_contents($varEtcDir . '/cfdevice'));
1463
        }
1464
1465
        foreach ($disks as $disk => $diskInfo) {
1466
            $type = $diskInfo['fstype'] ?? '';
1467
1468
            // Skip Linux RAID member disks
1469
            if ($type === 'linux_raid_member') {
1470
                continue;
1471
            }
1472
1473
            // Skip CD-ROM disks
1474
            if (in_array($disk, $cd_disks, true)) {
1475
                continue;
1476
            }
1477
            unset($temp_vendor, $temp_size, $original_size);
1478
            $mounted = self::diskIsMounted($disk);
1479
            if ($mounted_only === true && $mounted === false) {
1480
                continue;
1481
            }
1482
            $sys_disk = ($cf_disk === $disk);
1483
1484
            $mb_size = 0;
1485
            if (is_file("/sys/block/" . $disk . "/size")) {
1486
                $original_size = trim(file_get_contents("/sys/block/" . $disk . "/size"));
1487
                $original_size = ($original_size * 512 / 1024 / 1024);
1488
                $mb_size = round($original_size, 1);
1489
            }
1490
            if ($mb_size > 100) {
1491
                $temp_size = sprintf("%.0f MB", $mb_size);
1492
                $temp_vendor = $this->getVendorDisk($diskInfo);
1493
                $free_space = $this->getFreeSpace($disk);
1494
1495
                $arr_disk_info = $this->determineFormatFs($diskInfo);
1496
1497
                if (count($arr_disk_info) > 0) {
1498
                    $used = 0;
1499
                    foreach ($arr_disk_info as $disk_info) {
1500
                        $used += $disk_info['used_space'];
1501
                    }
1502
                    if ($used > 0) {
1503
                        $free_space = $mb_size - $used;
1504
                    }
1505
                }
1506
1507
                // Add HDD device information to the result
1508
                $res_disks[] = [
1509
                    'id' => $disk,
1510
                    'size' => $mb_size,
1511
                    'size_text' => $temp_size,
1512
                    'vendor' => $temp_vendor,
1513
                    'mounted' => $mounted,
1514
                    'free_space' => round($free_space, 1),
1515
                    'partitions' => $arr_disk_info,
1516
                    'sys_disk' => $sys_disk,
1517
                ];
1518
            }
1519
        }
1520
        return $res_disks;
1521
    }
1522
1523
    /**
1524
     * Get CD-ROM devices.
1525
     *
1526
     * @return array An array of CD-ROM device names.
1527
     */
1528
    private function cdromGetDevices(): array
1529
    {
1530
        $disks = [];
1531
        $blockDevices = $this->getLsBlkDiskInfo();
1532
        foreach ($blockDevices as $diskData) {
1533
            $type = $diskData['type'] ?? '';
1534
            $name = $diskData['name'] ?? '';
1535
1536
            // Skip devices that are not CD-ROM
1537
            if ($type !== 'rom' || $name === '') {
1538
                continue;
1539
            }
1540
            $disks[] = $name;
1541
        }
1542
        return $disks;
1543
    }
1544
1545
    /**
1546
     * Get disk devices.
1547
     *
1548
     * @param bool $diskOnly Whether to include only disk devices.
1549
     * @return array An array of disk device information.
1550
     */
1551
    public function diskGetDevices(bool $diskOnly = false): array
1552
    {
1553
        $disks = [];
1554
        $blockDevices = $this->getLsBlkDiskInfo();
1555
1556
        foreach ($blockDevices as $diskData) {
1557
            $type = $diskData['type'] ?? '';
1558
            $name = $diskData['name'] ?? '';
1559
            if ($type !== 'disk' || $name === '') {
1560
                continue;
1561
            }
1562
            $disks[$name] = $diskData;
1563
            if ($diskOnly === true) {
1564
                continue;
1565
            }
1566
            $children = $diskData['children'] ?? [];
1567
1568
            foreach ($children as $child) {
1569
                $childName = $child['name'] ?? '';
1570
                if ($childName === '') {
1571
                    continue;
1572
                }
1573
                $disks[$childName] = $child;
1574
            }
1575
        }
1576
        return $disks;
1577
    }
1578
1579
    /**
1580
     * Check if a disk is mounted.
1581
     *
1582
     * @param string $disk The name of the disk.
1583
     * @param string $filter The filter to match the disk name.
1584
     * @return string|bool The mount point if the disk is mounted, or false if not mounted.
1585
     */
1586
    public static function diskIsMounted(string $disk, string $filter = '/dev/')
1587
    {
1588
        $out = [];
1589
        $grepPath = Util::which('grep');
1590
        $mountPath = Util::which('mount');
1591
1592
        // Execute mount command and grep the output for the disk name
1593
        Processes::mwExec("{$mountPath} | {$grepPath} '{$filter}{$disk}'", $out);
1594
        if (count($out) > 0) {
1595
            $res_out = end($out);
1596
        } else {
1597
            $res_out = implode('', $out);
1598
        }
1599
        $data = explode(' ', trim($res_out));
1600
1601
        return (count($data) > 2) ? $data[2] : false;
1602
    }
1603
1604
    /**
1605
     * Get the vendor name for a disk.
1606
     *
1607
     * @param array $diskInfo The disk information.
1608
     * @return string The vendor name.
1609
     */
1610
    private function getVendorDisk(array $diskInfo): string
1611
    {
1612
        $temp_vendor = [];
1613
        $keys = ['vendor', 'model', 'type'];
1614
1615
        // Iterate through the keys to retrieve vendor-related data
1616
        foreach ($keys as $key) {
1617
            $data = $diskInfo[$key] ?? '';
1618
            if ($data !== '') {
1619
                $temp_vendor[] = trim(str_replace(',', '', $data));
1620
            }
1621
        }
1622
1623
        // If no vendor-related data is found, use the disk name
1624
        if (count($temp_vendor) === 0) {
1625
            $temp_vendor[] = $diskInfo['name'] ?? 'ERROR: NoName';
1626
        }
1627
        return implode(', ', $temp_vendor);
1628
    }
1629
1630
    /**
1631
     * Get the free space in megabytes for a given HDD.
1632
     *
1633
     * @param string $hdd The name of the HDD.
1634
     * @return int The free space in megabytes.
1635
     */
1636
    public function getFreeSpace(string $hdd)
1637
    {
1638
        $out = [];
1639
        $hdd = escapeshellarg($hdd);
1640
        $grepPath = Util::which('grep');
1641
        $awkPath = Util::which('awk');
1642
        $dfPath = Util::which('df');
1643
1644
        // Execute df command to get the free space for the HDD
1645
        Processes::mwExec("{$dfPath} -m | {$grepPath} {$hdd} | {$awkPath} '{print $4}'", $out);
1646
        $result = 0;
1647
1648
        // Sum up the free space values
1649
        foreach ($out as $res) {
1650
            if (!is_numeric($res)) {
1651
                continue;
1652
            }
1653
            $result += (1 * $res);
1654
        }
1655
1656
        return $result;
1657
    }
1658
1659
    /**
1660
     * Determine the format and file system information for a device.
1661
     *
1662
     * @param array $deviceInfo The device information.
1663
     * @return array An array containing format and file system information for each device partition.
1664
     */
1665
    public function determineFormatFs(array $deviceInfo): array
1666
    {
1667
        $allow_formats = ['ext2', 'ext4', 'fat', 'ntfs', 'msdos'];
1668
        $device = basename($deviceInfo['name'] ?? '');
1669
1670
        $devices = $this->getDiskParted('/dev/' . $deviceInfo['name'] ?? '');
1671
        $result_data = [];
1672
1673
        // Iterate through each device partition
1674
        foreach ($devices as $dev) {
1675
            if (empty($dev) || (count($devices) > 1 && $device === $dev) || is_dir("/sys/block/{$dev}")) {
1676
                continue;
1677
            }
1678
            $mb_size = 0;
1679
            $path_size_info = '';
1680
            $tmp_path = "/sys/block/{$device}/{$dev}/size";
1681
            if (file_exists($tmp_path)) {
1682
                $path_size_info = $tmp_path;
1683
            }
1684
1685
            // If the size path is not found, try an alternate path
1686
            if (empty($path_size_info)) {
1687
                $tmp_path = "/sys/block/" . substr($dev, 0, 3) . "/{$dev}/size";
1688
                if (file_exists($tmp_path)) {
1689
                    $path_size_info = $tmp_path;
1690
                }
1691
            }
1692
1693
            // Calculate the size in megabytes
1694
            if (!empty($path_size_info)) {
1695
                $original_size = trim(file_get_contents($path_size_info));
1696
                $original_size = ($original_size * 512 / 1024 / 1024);
1697
                $mb_size = $original_size;
1698
            }
1699
1700
            $tmp_dir = "/tmp/{$dev}_" . time();
1701
            $out = [];
1702
1703
            $fs = null;
1704
            $need_unmount = false;
1705
            $mount_dir = '';
1706
1707
            // Check if the device is currently mounted
1708
            if (self::isStorageDiskMounted("/dev/{$dev} ", $mount_dir)) {
1709
                $grepPath = Util::which('grep');
1710
                $awkPath = Util::which('awk');
1711
                $mountPath = Util::which('mount');
1712
1713
                // Get the file system type and free space of the mounted device
1714
                Processes::mwExec("{$mountPath} | {$grepPath} '/dev/{$dev}' | {$awkPath} '{print $5}'", $out);
1715
                $fs = trim(implode("", $out));
1716
                $fs = ($fs === 'fuseblk') ? 'ntfs' : $fs;
1717
                $free_space = $this->getFreeSpace("/dev/{$dev} ");
1718
                $used_space = $mb_size - $free_space;
1719
            } else {
1720
                $format = $this->getFsType($device);
1721
1722
                // Check if the detected format is allowed
1723
                if (in_array($format, $allow_formats)) {
1724
                    $fs = $format;
1725
                }
1726
1727
                // Mount the device and determine the used space
1728
                self::mountDisk($dev, $format, $tmp_dir);
1729
1730
                $need_unmount = true;
1731
                $used_space = Util::getSizeOfFile($tmp_dir);
1732
            }
1733
1734
            // Store the partition information in the result array
1735
            $result_data[] = [
1736
                "dev" => $dev,
1737
                'size' => round($mb_size, 2),
1738
                "used_space" => round($used_space, 2),
1739
                "free_space" => round($mb_size - $used_space, 2),
1740
                "uuid" => $this->getUuid("/dev/{$dev} "),
1741
                "fs" => $fs,
1742
            ];
1743
1744
            // Unmount the temporary mount point if needed
1745
            if ($need_unmount) {
1746
                self::umountDisk($tmp_dir);
1747
            }
1748
        }
1749
1750
        return $result_data;
1751
    }
1752
1753
    /**
1754
     * Get the disk partitions using the lsblk command.
1755
     *
1756
     * @param string $diskName The name of the disk.
1757
     * @return array An array of disk partition names.
1758
     */
1759
    private function getDiskParted(string $diskName): array
1760
    {
1761
        $result = [];
1762
        $lsBlkPath = Util::which('lsblk');
1763
1764
        // Execute lsblk command to get disk partition information in JSON format
1765
        Processes::mwExec("{$lsBlkPath} -J -b -o NAME,TYPE {$diskName}", $out);
1766
1767
        try {
1768
            $data = json_decode(implode(PHP_EOL, $out), true, 512, JSON_THROW_ON_ERROR);
1769
            $data = $data['blockdevices'][0] ?? [];
1770
        } catch (\JsonException $e) {
1771
            $data = [];
1772
        }
1773
1774
        $type = $data['children'][0]['type'] ?? '';
1775
1776
        // Check if the disk is not a RAID type
1777
        if (strpos($type, 'raid') === false) {
1778
            $children = $data['children'] ?? [];
1779
            foreach ($children as $child) {
1780
                $result[] = $child['name'];
1781
            }
1782
        }
1783
1784
        return $result;
1785
    }
1786
1787
    /**
1788
     * Get the file system type of a device.
1789
     *
1790
     * @param string $device The device path.
1791
     * @return string The file system type of the device.
1792
     */
1793
    public function getFsType(string $device): string
1794
    {
1795
        $blkidPath = Util::which('blkid');
1796
        $busyboxPath = Util::which('busybox');
1797
        $sedPath = Util::which('sed');
1798
        $grepPath = Util::which('grep');
1799
        $awkPath = Util::which('awk');
1800
1801
        // Remove '/dev/' from the device path
1802
        $device = str_replace('/dev/', '', $device);
1803
        $out = [];
1804
1805
        // Execute the command to retrieve the file system type of the device
1806
        Processes::mwExec(
1807
            "{$blkidPath} -ofull /dev/{$device} | {$busyboxPath} {$sedPath} -r 's/[[:alnum:]]+=/\\n&/g' | {$busyboxPath} {$grepPath} \"^TYPE=\" | {$busyboxPath} {$awkPath} -F \"\\\"\" '{print $2}'",
1808
            $out
1809
        );
1810
        $format = implode('', $out);
1811
1812
        // Check if the format is 'msdosvfat' and replace it with 'msdos'
1813
        if ($format === 'msdosvfat') {
1814
            $format = 'msdos';
1815
        }
1816
1817
        return $format;
1818
    }
1819
1820
    /**
1821
     * Mount a disk to a directory.
1822
     *
1823
     * @param string $dev The device name.
1824
     * @param string $format The file system format.
1825
     * @param string $dir The directory to mount the disk.
1826
     * @return bool True if the disk was successfully mounted, false otherwise.
1827
     */
1828
    public static function mountDisk(string $dev, string $format, string $dir): bool
1829
    {
1830
        // Check if the disk is already mounted
1831
        if (self::isStorageDiskMounted("/dev/{$dev} ")) {
1832
            return true;
1833
        }
1834
1835
        // Create the directory if it doesn't exist
1836
        Util::mwMkdir($dir);
1837
1838
        // Check if the directory was created successfully
1839
        if (!file_exists($dir)) {
1840
            Util::sysLogMsg(__Method__, "Unable mount $dev $format to $dir. Unable create dir.");
1841
1842
            return false;
1843
        }
1844
1845
        // Remove the '/dev/' prefix from the device name
1846
        $dev = str_replace('/dev/', '', $dev);
1847
1848
        if ('ntfs' === $format) {
1849
            // Mount NTFS disk using 'mount.ntfs-3g' command
1850
            $mountNtfs3gPath = Util::which('mount.ntfs-3g');
1851
            Processes::mwExec("{$mountNtfs3gPath} /dev/{$dev} {$dir}", $out);
1852
        } else {
1853
            // Mount disk using specified file system format and UUID
1854
            $storage = new self();
1855
            $uid_part = 'UUID=' . $storage->getUuid("/dev/{$dev}") . '';
1856
            $mountPath = Util::which('mount');
1857
            Processes::mwExec("{$mountPath} -t {$format} {$uid_part} {$dir}", $out);
1858
        }
1859
1860
        // Check if the disk is now mounted
1861
        return self::isStorageDiskMounted("/dev/{$dev} ");
1862
    }
1863
1864
    /**
1865
     * Get the UUID (Universally Unique Identifier) of a device.
1866
     *
1867
     * @param string $device The device path.
1868
     * @return string The UUID of the device.
1869
     */
1870
    public function getUuid(string $device): string
1871
    {
1872
        if (empty($device)) {
1873
            return '';
1874
        }
1875
        $lsBlkPath = Util::which('lsblk');
1876
        $busyboxPath = Util::which('busybox');
1877
1878
        // Build the command to retrieve the UUID of the device
1879
        $cmd = "{$lsBlkPath} -r -o NAME,UUID | {$busyboxPath} grep " . basename($device) . " | {$busyboxPath} cut -d ' ' -f 2";
1880
        $res = Processes::mwExec($cmd, $output);
1881
        if ($res === 0 && count($output) > 0) {
1882
            $result = $output[0];
1883
        } else {
1884
            $result = '';
1885
        }
1886
        return $result;
1887
    }
1888
1889
    /**
1890
     * Retrieves the name of the disk used for recovery. (conf.recover.)
1891
     *
1892
     * @return string The name of the recovery disk (e.g., '/dev/sda').
1893
     */
1894
    public function getRecoverDiskName(): string
1895
    {
1896
        $disks = $this->diskGetDevices(true);
1897
        foreach ($disks as $disk => $diskInfo) {
1898
            // Check if the disk is a RAID or virtual device
1899
            if (isset($diskInfo['children'][0]['children'])) {
1900
                $diskInfo = $diskInfo['children'][0];
1901
                // Adjust the disk name for RAID or other virtual devices
1902
                $disk = $diskInfo['name'];
1903
            }
1904
            foreach ($diskInfo['children'] as $child) {
1905
                $mountpoint = $child['mountpoint'] ?? '';
1906
                $diskPath = "/dev/{$disk}";
1907
                if ($mountpoint === '/conf.recover' && file_exists($diskPath)) {
1908
                    return "/dev/{$disk}";
1909
                }
1910
            }
1911
        }
1912
        return '';
1913
    }
1914
1915
    /**
1916
     * Returns the monitor directory path.
1917
     * @deprecated Use Directories class instead
1918
     *
1919
     * @return string The monitor directory path.
1920
     */
1921
    public static function getMonitorDir(): string
1922
    {
1923
        return Directories::getDir(Directories::AST_MONITOR_DIR);
1924
    }
1925
1926
}