Passed
Push — develop ( 9e7cf1...f1f254 )
by Портнов
05:55
created

PBXInstaller::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
nop 0
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\Models\PbxSettings;
23
use MikoPBX\Common\Providers\ConfigProvider;
24
use Phalcon\Config\Config;
25
use Phalcon\Di\Di;
26
use Phalcon\Di\Injectable;
27
28
/**
29
 * Class PBXInstaller
30
 * Handles the installation of MikoPBX onto a selected drive
31
 * @package MikoPBX\Core\System
32
 */
33
class PBXInstaller extends Injectable
34
{
35
    /**
36
     * Access to the /etc/inc/mikopbx-settings.json values
37
     *
38
     * @var \Phalcon\Config\Config
39
     */
40
    private Config $config;
41
42
    private array $valid_disks = [];
43
    private array $selected_disk = ['size' => 0, 'id' => ''];
44
    private string $target_disk = '';
45
46
    // File pointer
47
    private $fp;
48
49
    /**
50
     * PBXInstaller constructor.
51
     * Initiates the installation process.
52
     */
53
    public function __construct()
54
    {
55
56
        $this->config = Di::getDefault()->getShared(ConfigProvider::SERVICE_NAME);
57
58
        $this->fp = fopen('php://stdin', 'rb');
59
60
    }
61
62
    /**
63
     * Initiates the installation steps.
64
     */
65
    public function run(): void
66
    {
67
        $this->scanAllHdd();
68
        if ($this->processValidDisks()){
69
            $this->promptForTargetDisk();
70
            if ($this->confirmInstallation()) {
71
                $this->proceedInstallation();
72
            }
73
        }
74
    }
75
76
    /**
77
     * Scans all connected HDDs.
78
     */
79
    private function scanAllHdd(): void
80
    {
81
        $storage = new Storage();
82
        $all_hdd = $storage->getAllHdd();
83
        foreach ($all_hdd as $disk) {
84
            $this->processDisk($disk);
85
        }
86
    }
87
88
    /**
89
     * Process each disk and save valid ones.
90
     *
91
     * @param array $disk Information about the disk
92
     */
93
    private function processDisk(array $disk): void
94
    {
95
        // Initialize a variable to hold additional info
96
        $other_info = '';
97
98
        // Add info if the disk is a system disk or is mounted
99
        if (true === $disk['sys_disk']) {
100
            $other_info .= ' System Disk ';
101
        }
102
        if (true === $disk['mounted']) {
103
            $other_info .= ' Mounted ';
104
        }
105
106
        // Add a visual effect to the additional info if it's not empty
107
        if ($other_info !== '') {
108
            $other_info = "[ \033[31;1m$other_info\033[0m ]";
109
        }
110
111
        // Update the selected disk if the current disk's size is smaller
112
        if ($this->selected_disk['size'] === 0 || $this->selected_disk['size'] > $disk['size']) {
113
            $this->selected_disk = $disk;
114
        }
115
116
        // Ignore disks that are less than 400 megabytes
117
        if ($disk['size'] < 400) {
118
            return;
119
        }
120
121
        // Add the disk to the list of valid disks
122
        $this->valid_disks[$disk['id']] = "  - {$disk['id']}, {$disk['size_text']}, {$disk['vendor']} $other_info \n";
123
    }
124
125
    /**
126
     * Process the valid disks and select one for installation.
127
     */
128
    private function processValidDisks(): bool
129
    {
130
        // If no valid disks were found, print a message and sleep for 3 seconds, then return 0
131
        if (count($this->valid_disks) === 0) {
132
            SystemMessages::echoWithSyslog(PHP_EOL." " . Util::translate('Valid disks not found...') . " ".PHP_EOL);
133
            sleep(3);
134
            return false;
135
        }
136
137
        // If valid disks were found, print prompts for the user to select a disk
138
        echo "\n " . Util::translate('Select the drive to install the system.') . ' ';
139
        echo "\n " . Util::translate('Selected disk:') . "\033[33;1m [{$this->selected_disk['id']}] \033[0m \n\n";
140
        echo "\n " . Util::translate('Valid disks are:') . " \n\n";
141
142
        // Print each valid disk
143
        foreach ($this->valid_disks as $disk) {
144
            echo $disk;
145
        }
146
        echo "\n";
147
        return true;
148
    }
149
150
    /**
151
     * Prompt the user to select a target disk.
152
     *
153
     * @return void The selected disk id
154
     */
155
    private function promptForTargetDisk(): void
156
    {
157
        // Prompt the user to enter a device name until a valid device name is entered
158
        do {
159
            echo "\n" . Util::translate('Enter the device name:') . Util::translate('(default value = ') . $this->selected_disk['id'] . ') :';
160
            $this->target_disk = trim(fgets($this->fp));
161
            if ($this->target_disk === '') {
162
                $this->target_disk = $this->selected_disk['id'];
163
            }
164
        } while (!array_key_exists($this->target_disk, $this->valid_disks));
165
166
    }
167
168
    /**
169
     * Confirm the installation from the user.
170
     */
171
    private function confirmInstallation(): bool
172
    {
173
        // Warning and confirmation prompt
174
        echo '
175
176
*******************************************************************************
177
* ' . Util::translate('WARNING') . '!
178
* ' . Util::translate('The PBX is about to be installed onto the') . " \033[33;1m$this->target_disk\033[0m.
179
* - " . Util::translate('everything on this device will be erased!') . '
180
* - ' . Util::translate('this cannot be undone!') . '
181
*******************************************************************************
182
183
' . Util::translate('The PBX will reboot after installation.') . '
184
185
' . Util::translate('Do you want to proceed? (y/n): ');
186
187
        // If the user doesn't confirm, save the system disk info to a temp file and exit
188
        if (strtolower(trim(fgets($this->fp))) !== 'y') {
189
            sleep(3);
190
            return false;
191
        }
192
193
        return true;
194
    }
195
196
    /**
197
     * Start the installation process.
198
     */
199
    private function proceedInstallation(): void
200
    {
201
        // Save the target disk to a file
202
        $varEtcDir = Directories::getDir(Directories::CORE_VAR_ETC_DIR);
203
        file_put_contents($varEtcDir . '/cfdevice', $this->target_disk);
204
205
        // Start the installation process
206
        echo Util::translate("Installing PBX...").PHP_EOL;
207
        $this->unmountPartitions();
208
        $this->convertDiscLayout();
0 ignored issues
show
Bug introduced by
The call to MikoPBX\Core\System\PBXI...er::convertDiscLayout() has too few arguments starting with disk. ( Ignorable by Annotation )

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

208
        $this->/** @scrutinizer ignore-call */ 
209
               convertDiscLayout();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
209
        $this->unpackImage();
210
        $this->mountStorage();
211
        $this->copyConfiguration();
212
213
        // Reboot
214
        file_put_contents('/tmp/ejectcd', '');
215
        System::reboot();
216
    }
217
218
    /**
219
     * Converting the disk layout
220
     * @param $disk
221
     * @return void
222
     */
223
    private function convertDiscLayout($disk):void
224
    {
225
        if (!file_exists($disk)) {
226
            $disk = "/dev/$disk";
227
        }
228
        if (!file_exists($disk)) {
229
            return;
230
        }
231
        $gDiskPath = Util::which('gdisk');
232
        if (empty($gDiskPath)) {
233
            return;
234
        }
235
        $partedPath = Util::which('parted');
236
        $command = "$partedPath $disk print | grep 'Partition Table' | awk '{print $3}'";
237
        exec($command, $partitionTypeOutput, $partedStatus);
238
        if ($partedStatus !== 0 || empty($partitionTypeOutput)) {
239
            return;
240
        }
241
        $partitionType = trim($partitionTypeOutput[0]);
242
        if ($partitionType === "msdos") {
243
            echo " - Converting from MBR to GPT...\n";
244
            $echoPath = Util::which('echo');
245
            $gDiskCommand = "$echoPath -e \"w\\nY\\n\" | $gDiskPath $disk > /dev/null 2>&1";
246
            exec($gDiskCommand, $gDiskOutput, $gDiskStatus);
247
            if ($gDiskStatus === 0) {
248
                echo " - The conversion to GPT has been completed successfully.\n";
249
            } else {
250
                echo " - Error converting to GPT.\n";
251
            }
252
        }
253
    }
254
255
256
    /**
257
     * Unmount the partitions of the selected disk.
258
     */
259
    private function unmountPartitions(): void
260
    {
261
        echo Util::translate(" - Unmounting partitions...").PHP_EOL;
262
        $grep = Util::which('grep');
263
        $awk = Util::which('awk');
264
        $mount = Util::which('mount');
265
        $umount = Util::which('umount');
266
267
        // Get all mounted partitions
268
        $mnt_dirs = [];
269
        Processes::mwExec("$mount | $grep '^/dev/$this->target_disk' | $awk '{print $3}'", $mnt_dirs);
270
        foreach ($mnt_dirs as $mnt) {
271
            // Terminate all related processes.
272
            Processes::mwExec("/sbin/shell_functions.sh killprocesses '$mnt' -TERM 0;");
273
            // Unmount.
274
            Processes::mwExec("$umount $mnt");
275
        }
276
    }
277
278
    /**
279
     * Unpack the image to the target disk.
280
     */
281
    private function unpackImage(): void
282
    {
283
        echo Util::translate(" - Unpacking img...").PHP_EOL;
284
        $pv = Util::which('pv');
285
        $dd = Util::which('dd');
286
        $gunzip = Util::which('gunzip');
287
288
        $install_cmd = 'exec < /dev/console > /dev/console 2>/dev/console;' .
289
            "$pv -p /offload/firmware.img.gz | $gunzip | $dd of=/dev/$this->target_disk bs=4M 2> /dev/null";
290
        passthru($install_cmd);
291
    }
292
293
    /**
294
     * Mount the storage partition.
295
     */
296
    private function mountStorage(): void
297
    {
298
        // Connect the disk for data storage.
299
        Storage::selectAndConfigureStorageDisk(false,true);
300
    }
301
302
    /**
303
     * Copy the configuration to the target disk.
304
     */
305
    private function copyConfiguration():void
306
    {
307
        // Back up the table with disk information.
308
        echo Util::translate("Copying configuration...").PHP_EOL;
309
        Util::mwMkdir('/mnttmp');
310
311
        echo "Target disk: $this->target_disk ...".PHP_EOL;
312
        $confPartitionName = Storage::getDevPartName($this->target_disk, '3', true);
313
        if(empty($confPartitionName)){
314
            echo "Target partition not found: $this->target_disk (part 3) ...".PHP_EOL;
315
            return;
316
        }
317
        // Mount the disk with settings.
318
        $mount  = Util::which('mount');
319
        $umount = Util::which('umount');
320
        $resUMount = Processes::mwExec("$umount $confPartitionName");
321
        echo "Umount $confPartitionName: $resUMount ...".PHP_EOL;
322
        $resMount = Processes::mwExec("$mount -w -o noatime $confPartitionName /mnttmp");
323
        echo "Mount $confPartitionName to /mnttmp: $resMount ...".PHP_EOL;
324
        $filename = $this->config->path('database.dbfile');
325
        $result_db_file = '/mnttmp/conf/mikopbx.db';
326
327
        /** Copy the settings database file. */
328
        $cp = Util::which('cp');
329
        $sqlite3 = Util::which('sqlite3');
330
        $dmpDbFile = tempnam('/tmp', 'storage');
331
        // Save dump of settings.
332
        $tables = ['m_Storage', 'm_LanInterfaces'];
333
        file_put_contents($dmpDbFile, '');
334
        foreach ($tables as $table) {
335
            echo "DUMP $table from /cf/conf/mikopbx.db ...".PHP_EOL;
336
            $res = shell_exec("sqlite3 /cf/conf/mikopbx.db '.schema $table' >> $dmpDbFile");
337
            $res .= shell_exec("sqlite3 /cf/conf/mikopbx.db '.dump $table' >> $dmpDbFile");
338
            echo "$res ...".PHP_EOL;
339
        }
340
        // If another language is selected - use another settings file.
341
        $lang = PbxSettings::getValueByKey(PbxSettings::SSH_LANGUAGE);
342
        $filename_lang = "/offload/conf/mikopbx-$lang.db";
343
        if ($lang !== 'en' && file_exists($filename_lang)) {
344
            $filename = $filename_lang;
345
        }
346
        // Replace the settings file.
347
        $resCopy = Processes::mwExec("$cp $filename $result_db_file");
348
        echo "Copy $filename to $result_db_file: $resCopy ...".PHP_EOL;
349
        foreach ($tables as $table) {
350
            echo "DROP $table IF EXISTS in $result_db_file ...".PHP_EOL;
351
            $res = shell_exec("$sqlite3 $result_db_file 'DROP TABLE IF EXISTS $table'");
352
            echo "$res ...".PHP_EOL;
353
        }
354
        // Restore settings from backup file.
355
        $resSaveSettings = Processes::mwExec("$sqlite3 $result_db_file < $dmpDbFile");
356
        echo "Save settings to $result_db_file. Result: $resSaveSettings ...".PHP_EOL;
357
        unlink($dmpDbFile);
358
        Processes::mwExec("$umount /mnttmp");
359
    }
360
}