Passed
Push — develop ( 49e3f5...56ecd0 )
by Nikolay
04:35
created

PBXInstaller::processDisk()   B

Complexity

Conditions 7
Paths 32

Size

Total Lines 30
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 12
dl 0
loc 30
rs 8.8333
c 1
b 0
f 0
cc 7
nc 32
nop 1
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System;
21
22
use MikoPBX\Common\Providers\ConfigProvider;
23
use Phalcon\Config;
24
use Phalcon\Di;
25
26
/**
27
 * Class PBXInstaller
28
 * Handles the installation of MikoPBX onto a selected drive
29
 * @package MikoPBX\Core\System
30
 */
31
class PBXInstaller extends Di\Injectable
32
{
33
    // Storage instance
34
    private Storage $storage;
35
36
    /**
37
     * Access to the /etc/inc/mikopbx-settings.json values
38
     *
39
     * @var \Phalcon\Config
40
     */
41
    private Config $config;
42
43
    private array $valid_disks = [];
44
    private array $selected_disk = ['size' => 0, 'id' => ''];
45
    private string $target_disk = '';
46
47
    // Path to system commands mount and umount
48
    private string $mountPath = '';
49
    private string $uMountPath = '';
50
51
    // File pointer
52
    private $fp;
53
54
    /**
55
     * PBXInstaller constructor.
56
     * Initiates the installation process.
57
     */
58
    public function __construct()
59
    {
60
        $this->storage = new Storage();
61
62
        $this->config = Di::getDefault()->getShared(ConfigProvider::SERVICE_NAME);
63
64
        $this->fp = fopen('php://stdin', 'rb');
65
66
        $this->mountPath = Util::which('mount');
67
        $this->uMountPath = Util::which('umount');
68
    }
69
70
    /**
71
     * Initiates the installation steps.
72
     */
73
    public function run()
74
    {
75
        $this->scanAllHdd();
76
        $this->processValidDisks();
77
        $this->promptForTargetDisk();
78
        $this->confirmInstallation();
79
        $this->proceedInstallation();
80
    }
81
82
    /**
83
     * Scans all connected HDDs.
84
     */
85
    private function scanAllHdd()
86
    {
87
        $all_hdd = $this->storage->getAllHdd();
88
        foreach ($all_hdd as $disk) {
89
            $this->processDisk($disk);
90
        }
91
    }
92
93
    /**
94
     * Process each disk and save valid ones.
95
     *
96
     * @param array $disk Information about the disk
97
     */
98
    private function processDisk(array $disk)
99
    {
100
        // Initialize a variable to hold additional info
101
        $other_info = '';
102
103
        // Add info if the disk is a system disk or is mounted
104
        if (true === $disk['sys_disk']) {
105
            $other_info .= ' System Disk ';
106
        }
107
        if (true === $disk['mounted']) {
108
            $other_info .= ' Mounted ';
109
        }
110
111
        // Add a visual effect to the additional info if it's not empty
112
        if ($other_info !== '') {
113
            $other_info = "[ \033[31;1m{$other_info}\033[0m ]";
114
        }
115
116
        // Update the selected disk if the current disk's size is smaller
117
        if ($this->selected_disk['size'] === 0 || $this->selected_disk['size'] > $disk['size']) {
118
            $this->selected_disk = $disk;
119
        }
120
121
        // Ignore disks that are less than 400 megabytes
122
        if ($disk['size'] < 400) {
123
            return;
124
        }
125
126
        // Add the disk to the list of valid disks
127
        $this->valid_disks[$disk['id']] = "  - {$disk['id']}, {$disk['size_text']}, {$disk['vendor']} $other_info \n";
128
    }
129
130
    /**
131
     * Process the valid disks and select one for installation.
132
     */
133
    private function processValidDisks(): void
134
    {
135
        // If no valid disks were found, print a message and sleep for 3 seconds, then return 0
136
        if (count($this->valid_disks) === 0) {
137
            echo "\n " . Util::translate('Valid disks not found...') . " \n";
138
            sleep(3);
139
            exit(0);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
140
        }
141
142
        // If valid disks were found, print prompts for the user to select a disk
143
        echo "\n " . Util::translate('Select the drive to install the system.') . ' ';
144
        echo "\n " . Util::translate('Selected disk:') . "\033[33;1m [{$this->selected_disk['id']}] \033[0m \n\n";
145
        echo "\n " . Util::translate('Valid disks are:') . " \n\n";
146
147
        // Print each valid disk
148
        foreach ($this->valid_disks as $disk) {
149
            echo $disk;
150
        }
151
        echo "\n";
152
    }
153
154
    /**
155
     * Prompt the user to select a target disk.
156
     *
157
     * @return void The selected disk id
158
     */
159
    private function promptForTargetDisk(): void
160
    {
161
        // Prompt the user to enter a device name until a valid device name is entered
162
        do {
163
            echo "\n" . Util::translate('Enter the device name:') . Util::translate('(default value = ') . $this->selected_disk['id'] . ') :';
164
            $this->target_disk = trim(fgets($this->fp));
165
            if ($this->target_disk === '') {
166
                $this->target_disk = $this->selected_disk['id'];
167
            }
168
        } while (!array_key_exists($this->target_disk, $this->valid_disks));
169
170
    }
171
172
    /**
173
     * Confirm the installation from the user.
174
     */
175
    private function confirmInstallation()
176
    {
177
        // Warning and confirmation prompt
178
        echo '
179
180
*******************************************************************************
181
* ' . Util::translate('WARNING') . '!
182
* ' . Util::translate('The PBX is about to be installed onto the') . " \033[33;1m{$this->target_disk}\033[0m.
183
* - " . Util::translate('everything on this device will be erased!') . '
184
* - ' . Util::translate('this cannot be undone!') . '
185
*******************************************************************************
186
187
' . Util::translate('The PBX will reboot after installation.') . '
188
189
' . Util::translate('Do you want to proceed? (y/n): ');
190
191
        // If the user doesn't confirm, save the system disk info to a temp file and exit
192
        if (strtolower(trim(fgets($this->fp))) !== 'y') {
193
            sleep(3);
194
            exit(0);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
195
        }
196
197
    }
198
199
    /**
200
     * Start the installation process.
201
     */
202
    private function proceedInstallation()
203
    {
204
        // Save the target disk to a file
205
        file_put_contents( $this->config->path('core.varEtcDir') . '/cfdevice', $this->target_disk);
206
207
        // Start the installation process
208
        echo "Installing PBX...\n";
209
        $this->unmountPartitions();
210
        $this->unpackImage();
211
        $this->createStoragePartition();
212
        $this->mountStorage();
213
        $this->copyConfiguration();
214
215
        Processes::mwExec("{$this->uMountPath} /mnttmp");
216
        echo "done\n";
217
218
        // Reboot
219
        file_put_contents('/tmp/ejectcd', '');
220
        System::rebootSync();
221
    }
222
223
    /**
224
     * Unmount the partitions of the selected disk.
225
     */
226
    private function unmountPartitions()
227
    {
228
        echo " - Unmounting partitions...\n";
229
        $grepPath = Util::which('grep');
230
        $busyboxPath = Util::which('busybox');
231
        $awkPath = Util::which('awk');
232
233
        // Get all mounted partitions
234
        $mnt_dirs = [];
235
        Processes::mwExec("{$this->mountPath} | {$grepPath} '^/dev/{$this->target_disk}' | {$busyboxPath} {$awkPath} '{print $3}'", $mnt_dirs);
236
        foreach ($mnt_dirs as $mnt) {
237
            // Terminate all related processes.
238
            Processes::mwExec("/sbin/shell_functions.sh killprocesses '$mnt' -TERM 0;");
239
            // Unmount.
240
            Processes::mwExec("{$this->uMountPath} {$mnt}");
241
        }
242
    }
243
244
    /**
245
     * Unpack the image to the target disk.
246
     */
247
    private function unpackImage()
248
    {
249
        echo " - Unpacking img...\n";
250
        $pvPath = Util::which('pv');
251
        $ddPath = Util::which('dd');
252
        $gunzipPath = Util::which('gunzip');
253
254
        $install_cmd = 'exec < /dev/console > /dev/console 2>/dev/console;' .
255
            "{$pvPath} -p /offload/firmware.img.gz | {$gunzipPath} | {$ddPath} of=/dev/{$this->target_disk} bs=512 2> /dev/null";
256
        passthru($install_cmd);
257
    }
258
259
    /**
260
     * Create the storage partition on the target disk.
261
     */
262
    private function createStoragePartition()
263
    {
264
        echo " - Create storage partition...\n";
265
        passthru("exec </dev/console >/dev/console 2>/dev/console; /sbin/initial.storage.part.four create /dev/{$this->target_disk}");
266
    }
267
268
    /**
269
     * Mount the storage partition.
270
     */
271
    private function mountStorage()
272
    {
273
        // Connect the disk for data storage.
274
        include '/etc/rc/connect.storage';
275
276
        // Back up the table with disk information.
277
        echo 'Copying configuration...';
278
        Util::mwMkdir('/mnttmp');
279
280
        $partName = Storage::getDevPartName("/dev/{$this->target_disk}", '3');
281
282
        // Mount the disk with settings.
283
        Processes::mwExec("{$this->mountPath} -w -o noatime /dev/{$partName} /mnttmp");
284
    }
285
286
    /**
287
     * Copy the configuration to the target disk.
288
     */
289
    private function copyConfiguration()
290
    {
291
        $mikoPBXconfig = new MikoPBXConfig();
292
        $lang = $mikoPBXconfig->getGeneralSettings(PbxSettingsConstants::SSH_LANGUAGE);
293
294
        $filename = $this->config->path('database.dbfile');
295
        $result_db_file = '/mnttmp/conf/mikopbx.db';
296
297
        /** Copy the settings database file. */
298
        $cpPath = Util::which('cp');
299
        $sqlite3Path = Util::which('sqlite3');
300
        $grepPath = Util::which('grep');
301
        $dmpDbFile = tempnam('/tmp', 'storage');
302
303
        // Save dump of settings.
304
        $tables = ['m_Storage', 'm_LanInterfaces'];
305
        $grepOptions = '';
306
        foreach ($tables as $table) {
307
            $grepOptions .= " -e '^INSERT INTO {$table}'";
308
            $grepOptions .= " -e '^INSERT INTO \"{$table}'";
309
        }
310
        system("{$sqlite3Path} {$filename} .dump | {$grepPath} {$grepOptions} > " . $dmpDbFile);
311
312
        // If another language is selected - use another settings file.
313
        $filename_lang = "/offload/conf/mikopbx-{$lang}.db";
314
        if ($lang !== 'en' && file_exists($filename_lang)) {
315
            $filename = $filename_lang;
316
        }
317
318
        // Replace the settings file.
319
        Processes::mwExec("{$cpPath} {$filename} {$result_db_file}");
320
        system("{$sqlite3Path} {$result_db_file} 'DELETE FROM m_Storage'");
321
        system("{$sqlite3Path} {$result_db_file} 'DELETE FROM m_LanInterfaces'");
322
323
        // Restore settings from backup file.
324
        system("{$sqlite3Path} {$result_db_file} < {$dmpDbFile}");
325
        unlink($dmpDbFile);
326
    }
327
}