Passed
Push — develop ( c1253e...f72558 )
by Nikolay
12:51
created

ModulesManagementProcessor::moduleStartDownload()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 41
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 30
c 1
b 0
f 0
dl 0
loc 41
rs 9.44
cc 4
nc 8
nop 3
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\PBXCoreREST\Lib;
21
22
use MikoPBX\Common\Providers\PBXConfModulesProvider;
23
use MikoPBX\Core\System\Processes;
24
use MikoPBX\Core\System\Util;
25
use MikoPBX\Modules\PbxExtensionState;
26
use MikoPBX\Modules\PbxExtensionUtils;
27
use MikoPBX\Modules\Setup\PbxExtensionSetupFailure;
28
use MikoPBX\PBXCoreREST\Workers\WorkerDownloader;
29
use MikoPBX\PBXCoreREST\Workers\WorkerModuleInstaller;
30
use Phalcon\Di;
31
use Phalcon\Di\Injectable;
32
33
34
/**
35
 * Class ModulesManagementProcessor
36
 *
37
 * Manages external modules for download, install, uninstall, enable, disable.
38
 *
39
 * @package MikoPBX\PBXCoreREST\Lib
40
 */
41
class ModulesManagementProcessor extends Injectable
42
{
43
    /**
44
     * Processes module management requests.
45
     *
46
     * @param array $request The request data.
47
     *
48
     * @return PBXApiResult An object containing the result of the API call.
49
     *
50
     */
51
    public static function callBack(array $request): PBXApiResult
52
    {
53
        $action = $request['action'];
54
        $data = $request['data'];
55
        $res = new PBXApiResult();
56
        $res->processor = __METHOD__;
57
            switch ($action) {
58
                case 'moduleStartDownload':
59
                    $module = $request['data']['uniqid'];
60
                    $url = $request['data']['url'];
61
                    $md5 = $request['data']['md5'];
62
                    $res = self::moduleStartDownload($module, $url, $md5);
63
                    break;
64
                case 'moduleDownloadStatus':
65
                    $module = $request['data']['uniqid'];
66
                    $res = self::moduleDownloadStatus($module);
67
                    break;
68
                case 'installNewModule':
69
                    $filePath = $data['filePath'];
70
                    $res = self::installModule($filePath);
71
                    break;
72
                case 'statusOfModuleInstallation':
73
                    $filePath = $data['filePath'];
74
                    $res = self::statusOfModuleInstallation($filePath);
75
                    break;
76
                case 'enableModule':
77
                    $moduleUniqueID = $data['uniqid'];
78
                    $res = self::enableModule($moduleUniqueID);
79
                    break;
80
                case 'disableModule':
81
                    $moduleUniqueID = $data['uniqid'];
82
                    $res = self::disableModule($moduleUniqueID);
83
                    break;
84
                case 'uninstallModule':
85
                    $moduleUniqueID = $data['uniqid'];
86
                    $keepSettings = $data['keepSettings'] === 'true';
87
                    $res = self::uninstallModule($moduleUniqueID, $keepSettings);
88
                    break;
89
                default:
90
                    $res->messages[] = "Unknown action - {$action} in modulesCoreCallBack";
91
            }
92
        $res->function = $action;
93
94
        return $res;
95
    }
96
97
    /**
98
     * Installs a new additional extension module from an early uploaded zip archive.
99
     *
100
     * @param string $filePath The path to the module file.
101
     *
102
     * @return PBXApiResult An object containing the result of the API call.
103
     */
104
    public static function installModule(string $filePath): PBXApiResult
105
    {
106
        $res = new PBXApiResult();
107
        $res->processor = __METHOD__;
108
        $resModuleMetadata = self::getMetadataFromModuleFile($filePath);
109
        if (!$resModuleMetadata->success) {
110
            return $resModuleMetadata;
111
        }
112
113
        $moduleUniqueID = $resModuleMetadata->data['uniqid'];
114
        // Disable the module if it's enabled
115
        if (PbxExtensionUtils::isEnabled($moduleUniqueID)) {
116
            $res = self::disableModule($moduleUniqueID);
117
            if (!$res->success) {
118
                return $res;
119
            }
120
        }
121
122
        $currentModuleDir = PbxExtensionUtils::getModuleDir($moduleUniqueID);
123
        $needBackup = is_dir($currentModuleDir);
124
125
        if ($needBackup) {
126
            self::uninstallModule($moduleUniqueID, true);
127
        }
128
129
        // Start the background process to install the module
130
        $temp_dir = dirname($filePath);
131
132
        // Create a progress file to track the installation progress
133
        file_put_contents($temp_dir . '/installation_progress', '0');
134
135
        // Create an error file to store any installation errors
136
        file_put_contents($temp_dir . '/installation_error', '');
137
138
        $install_settings = [
139
            'filePath' => $filePath,
140
            'currentModuleDir' => $currentModuleDir,
141
            'uniqid' => $moduleUniqueID,
142
        ];
143
144
        // Save the installation settings to a JSON file
145
        $settings_file = "{$temp_dir}/install_settings.json";
146
        file_put_contents(
147
            $settings_file,
148
            json_encode($install_settings, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
149
        );
150
        $phpPath = Util::which('php');
151
        $workerFilesMergerPath = Util::getFilePathByClassName(WorkerModuleInstaller::class);
152
153
        // Execute the background process to install the module
154
        Processes::mwExecBg("{$phpPath} -f {$workerFilesMergerPath} start '{$settings_file}'");
155
        $res->data['filePath'] = $filePath;
156
        $res->success = true;
157
158
        return $res;
159
    }
160
161
    /**
162
     * Uninstall extension module
163
     *
164
     * @param string $moduleUniqueID The unique ID of the module to uninstall.
165
     * @param bool $keepSettings Indicates whether to keep the module settings.
166
     *
167
     * @return PBXApiResult An object containing the result of the API call.
168
     */
169
    public static function uninstallModule(string $moduleUniqueID, bool $keepSettings): PBXApiResult
170
    {
171
        $res = new PBXApiResult();
172
        $res->processor = __METHOD__;
173
        $currentModuleDir = PbxExtensionUtils::getModuleDir($moduleUniqueID);
174
175
        // Kill all module processes
176
        if (is_dir("{$currentModuleDir}/bin")) {
177
            $busyboxPath = Util::which('busybox');
178
            $killPath = Util::which('kill');
179
            $lsofPath = Util::which('lsof');
180
            $grepPath = Util::which('grep');
181
            $awkPath = Util::which('awk');
182
            $uniqPath = Util::which('uniq');
183
184
            // Execute the command to kill all processes related to the module
185
            Processes::mwExec(
186
                "{$busyboxPath} {$killPath} -9 $({$lsofPath} {$currentModuleDir}/bin/* |  {$busyboxPath} {$grepPath} -v COMMAND | {$busyboxPath} {$awkPath}  '{ print $2}' | {$busyboxPath} {$uniqPath})"
187
            );
188
        }
189
190
        // Uninstall module with keep settings and backup db
191
        $moduleClass = "\\Modules\\{$moduleUniqueID}\\Setup\\PbxExtensionSetup";
192
193
        try {
194
            if (class_exists($moduleClass)
195
                && method_exists($moduleClass, 'uninstallModule')) {
196
                // Instantiate the module setup class and call the uninstallModule method
197
                $setup = new $moduleClass($moduleUniqueID);
198
            } else {
199
200
                // Use a fallback class to uninstall the module from the database if it doesn't exist on disk
201
                $moduleClass = PbxExtensionSetupFailure::class;
202
                $setup = new $moduleClass($moduleUniqueID);
203
            }
204
            $setup->uninstallModule($keepSettings);
205
        } finally {
206
            if (is_dir($currentModuleDir)) {
207
                // If the module directory still exists, force uninstallation
208
                $rmPath = Util::which('rm');
209
210
                // Remove the module directory recursively
211
                Processes::mwExec("{$rmPath} -rf {$currentModuleDir}");
212
213
                // Use the fallback class to unregister the module from the database
214
                $moduleClass = PbxExtensionSetupFailure::class;
215
                $setup = new $moduleClass($moduleUniqueID);
216
                $setup->unregisterModule();
217
            }
218
        }
219
        $res->success = true;
220
221
        return $res;
222
    }
223
224
    /**
225
     * Enables extension module.
226
     *
227
     * @param string $moduleUniqueID
228
     *
229
     * @return PBXApiResult An object containing the result of the API call.
230
     */
231
    private static function enableModule(string $moduleUniqueID): PBXApiResult
232
    {
233
        $res = new PBXApiResult();
234
        $res->processor = __METHOD__;
235
        $moduleStateProcessor = new PbxExtensionState($moduleUniqueID);
236
        if ($moduleStateProcessor->enableModule() === false) {
237
            $res->success = false;
238
            $res->messages = $moduleStateProcessor->getMessages();
239
        } else {
240
            PBXConfModulesProvider::recreateModulesProvider();
241
            $res->data = $moduleStateProcessor->getMessages();
242
            $res->success = true;
243
        }
244
245
        return $res;
246
    }
247
248
    /**
249
     * Disables extension module.
250
     *
251
     * @param string $moduleUniqueID
252
     *
253
     * @return PBXApiResult An object containing the result of the API call.
254
     */
255
    private static function disableModule(string $moduleUniqueID): PBXApiResult
256
    {
257
        $res = new PBXApiResult();
258
        $res->processor = __METHOD__;
259
        $moduleStateProcessor = new PbxExtensionState($moduleUniqueID);
260
        if ($moduleStateProcessor->disableModule() === false) {
261
            $res->success = false;
262
            $res->messages = $moduleStateProcessor->getMessages();
263
        } else {
264
            PBXConfModulesProvider::recreateModulesProvider();
265
            $res->data = $moduleStateProcessor->getMessages();
266
            $res->success = true;
267
        }
268
269
        return $res;
270
    }
271
272
    /**
273
     * Checks the status of a module installation by the provided zip file path.
274
     *
275
     * @param string $filePath The path of the module installation file.
276
     *
277
     * @return PBXApiResult An object containing the result of the API call.
278
     */
279
    public static function statusOfModuleInstallation(string $filePath): PBXApiResult
280
    {
281
        $res = new PBXApiResult();
282
        $res->processor = __METHOD__;
283
        $di = Di::getDefault();
284
        if ($di === null) {
285
            $res->messages[] = 'Dependency injector does not initialized';
286
287
            return $res;
288
        }
289
        $temp_dir = dirname($filePath);
290
        $progress_file = $temp_dir . '/installation_progress';
291
        $error_file = $temp_dir . '/installation_error';
292
        if (!file_exists($error_file) || !file_exists($progress_file)) {
293
            $res->success = false;
294
            $res->data['i_status'] = 'PROGRESS_FILE_NOT_FOUND';
295
            $res->data['i_status_progress'] = '0';
296
        } elseif (file_get_contents($error_file) !== '') {
297
            $res->success = false;
298
            $res->data['i_status'] = 'INSTALLATION_ERROR';
299
            $res->data['i_status_progress'] = '0';
300
            $res->messages[] = file_get_contents($error_file);
301
        } elseif ('100' === file_get_contents($progress_file)) {
302
            $res->success = true;
303
            $res->data['i_status_progress'] = '100';
304
            $res->data['i_status'] = 'INSTALLATION_COMPLETE';
305
        } else {
306
            $res->success = true;
307
            $res->data['i_status'] = 'INSTALLATION_IN_PROGRESS';
308
            $res->data['i_status_progress'] = file_get_contents($progress_file);
309
        }
310
311
        return $res;
312
    }
313
314
    /**
315
     * Starts the module download in a separate background process.
316
     *
317
     * @param string $module The module name.
318
     * @param string $url The download URL of the module.
319
     * @param string $md5 The MD5 hash of the module file.
320
     *
321
     * @return PBXApiResult An object containing the result of the API call.
322
     */
323
    public static function moduleStartDownload(string $module, string $url, string $md5): PBXApiResult
324
    {
325
        $res = new PBXApiResult();
326
        $res->processor = __METHOD__;
327
        $di = Di::getDefault();
328
        if ($di !== null) {
329
            $tempDir = $di->getConfig()->path('www.uploadDir');
330
        } else {
331
            $tempDir = '/tmp';
332
        }
333
334
        $moduleDirTmp = "{$tempDir}/{$module}";
335
        Util::mwMkdir($moduleDirTmp);
336
337
        $download_settings = [
338
            'res_file' => "$moduleDirTmp/modulefile.zip",
339
            'url' => $url,
340
            'module' => $module,
341
            'md5' => $md5,
342
            'action' => 'moduleInstall',
343
        ];
344
        if (file_exists("$moduleDirTmp/error")) {
345
            unlink("$moduleDirTmp/error");
346
        }
347
        if (file_exists("$moduleDirTmp/installed")) {
348
            unlink("$moduleDirTmp/installed");
349
        }
350
        file_put_contents("$moduleDirTmp/progress", '0');
351
        file_put_contents(
352
            "$moduleDirTmp/download_settings.json",
353
            json_encode($download_settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
354
        );
355
        $workerDownloaderPath = Util::getFilePathByClassName(WorkerDownloader::class);
356
        $phpPath = Util::which('php');
357
        Processes::mwExecBg("{$phpPath} -f {$workerDownloaderPath} start {$moduleDirTmp}/download_settings.json");
358
359
        $res->data['uniqid'] = $module;
360
        $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
361
        $res->success = true;
362
363
        return $res;
364
    }
365
366
    /**
367
     * Returns the download status of a module.
368
     *
369
     * @param string $moduleUniqueID The unique ID of the module.
370
     *
371
     * @return PBXApiResult An object containing the result of the API call.
372
     */
373
    public static function moduleDownloadStatus(string $moduleUniqueID): PBXApiResult
374
    {
375
        clearstatcache();
376
        $res = new PBXApiResult();
377
        $res->processor = __METHOD__;
378
        $di = Di::getDefault();
379
        if ($di !== null) {
380
            $tempDir = $di->getConfig()->path('www.uploadDir');
381
        } else {
382
            $tempDir = '/tmp';
383
        }
384
        $moduleDirTmp = $tempDir . '/' . $moduleUniqueID;
385
        $progress_file = $moduleDirTmp . '/progress';
386
        $error = '';
387
        if (file_exists($moduleDirTmp . '/error')) {
388
            $error = trim(file_get_contents($moduleDirTmp . '/error'));
389
        }
390
391
        // Wait until download process started
392
        $d_pid = Processes::getPidOfProcess("{$moduleDirTmp}/download_settings.json");
393
        if (empty($d_pid)) {
394
            usleep(500000);
395
        }
396
397
        if (!file_exists($progress_file)) {
398
            $res->data['d_status_progress'] = '0';
399
            $res->data['d_status'] = 'NOT_FOUND';
400
            $res->success = false;
401
        } elseif ('' !== $error) {
402
            $res->data['d_status'] = 'DOWNLOAD_ERROR';
403
            $res->data['d_status_progress'] = file_get_contents($progress_file);
404
            $res->data['d_error'] = $error;
405
            $res->messages[] = file_get_contents($moduleDirTmp . '/error');
406
            $res->success = false;
407
        } elseif ('100' === file_get_contents($progress_file)) {
408
            $res->data['d_status_progress'] = '100';
409
            $res->data['d_status'] = 'DOWNLOAD_COMPLETE';
410
            $res->data['filePath'] = "$moduleDirTmp/modulefile.zip";
411
            $res->success = true;
412
        } else {
413
            $res->data['d_status_progress'] = file_get_contents($progress_file);
414
            $d_pid = Processes::getPidOfProcess($moduleDirTmp . '/download_settings.json');
415
            if (empty($d_pid)) {
416
                $res->data['d_status'] = 'DOWNLOAD_ERROR';
417
                if (file_exists($moduleDirTmp . '/error')) {
418
                    $res->messages[] = file_get_contents($moduleDirTmp . '/error');
419
                } else {
420
                    $res->messages[] = "Download process interrupted at {$res->data['d_status_progress']}%";
421
                }
422
                $res->success = false;
423
            } else {
424
                $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
425
                $res->success = true;
426
            }
427
        }
428
429
        return $res;
430
    }
431
432
    /**
433
     * Unpacks a module ZIP file and retrieves metadata information from the JSON config inside.
434
     *
435
     * @param string $filePath The file path of the module.
436
     *
437
     * @return PBXApiResult An object containing the result of the API call.
438
     */
439
    public static function getMetadataFromModuleFile(string $filePath): PBXApiResult
440
    {
441
        $res = new PBXApiResult();
442
        $res->processor = __METHOD__;
443
444
        if (file_exists($filePath)) {
445
            $sevenZaPath = Util::which('7za');
446
            $grepPath = Util::which('grep');
447
            $echoPath = Util::which('echo');
448
            $awkPath = Util::which('awk');
449
            $cmd = 'f="' . $filePath . '"; p=`' . $sevenZaPath . ' l $f | ' . $grepPath . ' module.json`;if [ "$?" == "0" ]; then ' . $sevenZaPath . ' -so e -y -r $f `' . $echoPath . ' $p |  ' . $awkPath . ' -F" " \'{print $6}\'`; fi';
450
451
            Processes::mwExec($cmd, $out);
452
            $settings = json_decode(implode("\n", $out), true);
453
454
            $moduleUniqueID = $settings['moduleUniqueID'] ?? null;
455
            if (!$moduleUniqueID) {
456
                $res->messages[] = 'The" moduleUniqueID " in the module file is not described.the json or file does not exist.';
457
458
                return $res;
459
            }
460
            $res->success = true;
461
            $res->data = [
462
                'filePath' => $filePath,
463
                'uniqid' => $moduleUniqueID,
464
            ];
465
        }
466
467
        return $res;
468
    }
469
}