Passed
Push — develop ( edb19d...9107ab )
by Nikolay
05:22
created

ModulesManagementProcessor::getModuleLink()   A

Complexity

Conditions 4
Paths 10

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
c 0
b 0
f 0
dl 0
loc 41
rs 9.488
cc 4
nc 10
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\PBXCoreREST\Lib;
21
22
use MikoPBX\Common\Models\PbxSettings;
23
use MikoPBX\Common\Providers\ConfigProvider;
24
use MikoPBX\Common\Providers\ManagedCacheProvider;
25
use MikoPBX\Common\Providers\PBXConfModulesProvider;
26
use MikoPBX\Core\System\Processes;
27
use MikoPBX\Core\System\Util;
28
use MikoPBX\Modules\PbxExtensionState;
29
use MikoPBX\Modules\PbxExtensionUtils;
30
use MikoPBX\Modules\Setup\PbxExtensionSetupFailure;
31
use MikoPBX\PBXCoreREST\Http\Response;
32
use MikoPBX\PBXCoreREST\Workers\WorkerDownloader;
33
use MikoPBX\PBXCoreREST\Workers\WorkerModuleInstaller;
34
use Phalcon\Di;
35
use Phalcon\Di\Injectable;
36
use GuzzleHttp;
37
38
39
/**
40
 * Class ModulesManagementProcessor
41
 *
42
 * Manages external modules for download, install, uninstall, enable, disable.
43
 *
44
 * @property Di di
45
 * @package MikoPBX\PBXCoreREST\Lib
46
 */
47
class ModulesManagementProcessor extends Injectable
48
{
49
    /**
50
     * Processes module management requests.
51
     *
52
     * @param array $request The request data.
53
     *
54
     * @return PBXApiResult An object containing the result of the API call.
55
     *
56
     */
57
    public static function callBack(array $request): PBXApiResult
58
    {
59
        $action = $request['action'];
60
        $data = $request['data'];
61
        $res = new PBXApiResult();
62
        $res->processor = __METHOD__;
63
            switch ($action) {
64
                case 'moduleStartDownload':
65
                    $module = $request['data']['uniqid'];
66
                    $url = $request['data']['url'];
67
                    $md5 = $request['data']['md5'];
68
                    $res = self::moduleStartDownload($module, $url, $md5);
69
                    break;
70
                case 'moduleDownloadStatus':
71
                    $module = $request['data']['uniqid'];
72
                    $res = self::moduleDownloadStatus($module);
73
                    break;
74
                case 'installNewModule':
75
                    $filePath = $data['filePath'];
76
                    $res = self::installModule($filePath);
77
                    break;
78
                case 'statusOfModuleInstallation':
79
                    $filePath = $data['filePath'];
80
                    $res = self::statusOfModuleInstallation($filePath);
81
                    break;
82
                case 'enableModule':
83
                    $moduleUniqueID = $data['uniqid'];
84
                    $res = self::enableModule($moduleUniqueID);
85
                    break;
86
                case 'disableModule':
87
                    $moduleUniqueID = $data['uniqid'];
88
                    $res = self::disableModule($moduleUniqueID);
89
                    break;
90
                case 'uninstallModule':
91
                    $moduleUniqueID = $data['uniqid'];
92
                    $keepSettings = $data['keepSettings'] === 'true';
93
                    $res = self::uninstallModule($moduleUniqueID, $keepSettings);
94
                    break;
95
                case 'getAvailableModules':
96
                    $res = self::getAvailableModules();
97
                    break;
98
                case 'getModuleLink':
99
                    $moduleReleaseId = $data['releaseId'];
100
                    $res = self::getModuleLink($moduleReleaseId);
101
                    break;
102
                default:
103
                    $res->messages[] = "Unknown action - {$action} in modulesCoreCallBack";
104
            }
105
        $res->function = $action;
106
107
        return $res;
108
    }
109
110
    /**
111
     * Installs a new additional extension module from an early uploaded zip archive.
112
     *
113
     * @param string $filePath The path to the module file.
114
     *
115
     * @return PBXApiResult An object containing the result of the API call.
116
     */
117
    public static function installModule(string $filePath): PBXApiResult
118
    {
119
        $res = new PBXApiResult();
120
        $res->processor = __METHOD__;
121
        $resModuleMetadata = self::getMetadataFromModuleFile($filePath);
122
        if (!$resModuleMetadata->success) {
123
            return $resModuleMetadata;
124
        }
125
126
        $moduleUniqueID = $resModuleMetadata->data['uniqid'];
127
        // Disable the module if it's enabled
128
        if (PbxExtensionUtils::isEnabled($moduleUniqueID)) {
129
            $res = self::disableModule($moduleUniqueID);
130
            if (!$res->success) {
131
                return $res;
132
            }
133
        }
134
135
        $currentModuleDir = PbxExtensionUtils::getModuleDir($moduleUniqueID);
136
        $needBackup = is_dir($currentModuleDir);
137
138
        if ($needBackup) {
139
            self::uninstallModule($moduleUniqueID, true);
140
        }
141
142
        // Start the background process to install the module
143
        $temp_dir = dirname($filePath);
144
145
        // Create a progress file to track the installation progress
146
        file_put_contents($temp_dir . '/installation_progress', '0');
147
148
        // Create an error file to store any installation errors
149
        file_put_contents($temp_dir . '/installation_error', '');
150
151
        $install_settings = [
152
            'filePath' => $filePath,
153
            'currentModuleDir' => $currentModuleDir,
154
            'uniqid' => $moduleUniqueID,
155
        ];
156
157
        // Save the installation settings to a JSON file
158
        $settings_file = "{$temp_dir}/install_settings.json";
159
        file_put_contents(
160
            $settings_file,
161
            json_encode($install_settings, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
162
        );
163
        $phpPath = Util::which('php');
164
        $workerFilesMergerPath = Util::getFilePathByClassName(WorkerModuleInstaller::class);
165
166
        // Execute the background process to install the module
167
        Processes::mwExecBg("{$phpPath} -f {$workerFilesMergerPath} start '{$settings_file}'");
168
        $res->data['filePath'] = $filePath;
169
        $res->success = true;
170
171
        return $res;
172
    }
173
174
    /**
175
     * Uninstall extension module
176
     *
177
     * @param string $moduleUniqueID The unique ID of the module to uninstall.
178
     * @param bool $keepSettings Indicates whether to keep the module settings.
179
     *
180
     * @return PBXApiResult An object containing the result of the API call.
181
     */
182
    public static function uninstallModule(string $moduleUniqueID, bool $keepSettings): PBXApiResult
183
    {
184
        $res = new PBXApiResult();
185
        $res->processor = __METHOD__;
186
        $currentModuleDir = PbxExtensionUtils::getModuleDir($moduleUniqueID);
187
188
        // Kill all module processes
189
        if (is_dir("{$currentModuleDir}/bin")) {
190
            $busyboxPath = Util::which('busybox');
191
            $killPath = Util::which('kill');
192
            $lsofPath = Util::which('lsof');
193
            $grepPath = Util::which('grep');
194
            $awkPath = Util::which('awk');
195
            $uniqPath = Util::which('uniq');
196
197
            // Execute the command to kill all processes related to the module
198
            Processes::mwExec(
199
                "{$busyboxPath} {$killPath} -9 $({$lsofPath} {$currentModuleDir}/bin/* |  {$busyboxPath} {$grepPath} -v COMMAND | {$busyboxPath} {$awkPath}  '{ print $2}' | {$busyboxPath} {$uniqPath})"
200
            );
201
        }
202
203
        // Uninstall module with keep settings and backup db
204
        $moduleClass = "\\Modules\\{$moduleUniqueID}\\Setup\\PbxExtensionSetup";
205
206
        try {
207
            if (class_exists($moduleClass)
208
                && method_exists($moduleClass, 'uninstallModule')) {
209
                // Instantiate the module setup class and call the uninstallModule method
210
                $setup = new $moduleClass($moduleUniqueID);
211
            } else {
212
213
                // Use a fallback class to uninstall the module from the database if it doesn't exist on disk
214
                $moduleClass = PbxExtensionSetupFailure::class;
215
                $setup = new $moduleClass($moduleUniqueID);
216
            }
217
            $setup->uninstallModule($keepSettings);
218
        } finally {
219
            if (is_dir($currentModuleDir)) {
220
                // If the module directory still exists, force uninstallation
221
                $rmPath = Util::which('rm');
222
223
                // Remove the module directory recursively
224
                Processes::mwExec("{$rmPath} -rf {$currentModuleDir}");
225
226
                // Use the fallback class to unregister the module from the database
227
                $moduleClass = PbxExtensionSetupFailure::class;
228
                $setup = new $moduleClass($moduleUniqueID);
229
                $setup->unregisterModule();
230
            }
231
        }
232
        $res->success = true;
233
234
        return $res;
235
    }
236
237
    /**
238
     * Enables extension module.
239
     *
240
     * @param string $moduleUniqueID
241
     *
242
     * @return PBXApiResult An object containing the result of the API call.
243
     */
244
    private static function enableModule(string $moduleUniqueID): PBXApiResult
245
    {
246
        $res = new PBXApiResult();
247
        $res->processor = __METHOD__;
248
        $moduleStateProcessor = new PbxExtensionState($moduleUniqueID);
249
        if ($moduleStateProcessor->enableModule() === false) {
250
            $res->success = false;
251
            $res->messages = $moduleStateProcessor->getMessages();
252
        } else {
253
            PBXConfModulesProvider::recreateModulesProvider();
254
            $res->data = $moduleStateProcessor->getMessages();
255
            $res->success = true;
256
        }
257
258
        return $res;
259
    }
260
261
    /**
262
     * Disables extension module.
263
     *
264
     * @param string $moduleUniqueID
265
     *
266
     * @return PBXApiResult An object containing the result of the API call.
267
     */
268
    private static function disableModule(string $moduleUniqueID): PBXApiResult
269
    {
270
        $res = new PBXApiResult();
271
        $res->processor = __METHOD__;
272
        $moduleStateProcessor = new PbxExtensionState($moduleUniqueID);
273
        if ($moduleStateProcessor->disableModule() === false) {
274
            $res->success = false;
275
            $res->messages = $moduleStateProcessor->getMessages();
276
        } else {
277
            PBXConfModulesProvider::recreateModulesProvider();
278
            $res->data = $moduleStateProcessor->getMessages();
279
            $res->success = true;
280
        }
281
282
        return $res;
283
    }
284
285
    /**
286
     * Checks the status of a module installation by the provided zip file path.
287
     *
288
     * @param string $filePath The path of the module installation file.
289
     *
290
     * @return PBXApiResult An object containing the result of the API call.
291
     */
292
    public static function statusOfModuleInstallation(string $filePath): PBXApiResult
293
    {
294
        $res = new PBXApiResult();
295
        $res->processor = __METHOD__;
296
        $di = Di::getDefault();
297
        if ($di === null) {
298
            $res->messages[] = 'Dependency injector does not initialized';
299
300
            return $res;
301
        }
302
        $temp_dir = dirname($filePath);
303
        $progress_file = $temp_dir . '/installation_progress';
304
        $error_file = $temp_dir . '/installation_error';
305
        if (!file_exists($error_file) || !file_exists($progress_file)) {
306
            $res->success = false;
307
            $res->data['i_status'] = 'PROGRESS_FILE_NOT_FOUND';
308
            $res->data['i_status_progress'] = '0';
309
        } elseif (file_get_contents($error_file) !== '') {
310
            $res->success = false;
311
            $res->data['i_status'] = 'INSTALLATION_ERROR';
312
            $res->data['i_status_progress'] = '0';
313
            $res->messages[] = file_get_contents($error_file);
314
        } elseif ('100' === file_get_contents($progress_file)) {
315
            $res->success = true;
316
            $res->data['i_status_progress'] = '100';
317
            $res->data['i_status'] = 'INSTALLATION_COMPLETE';
318
        } else {
319
            $res->success = true;
320
            $res->data['i_status'] = 'INSTALLATION_IN_PROGRESS';
321
            $res->data['i_status_progress'] = file_get_contents($progress_file);
322
        }
323
324
        return $res;
325
    }
326
327
    /**
328
     * Starts the module download in a separate background process.
329
     *
330
     * @param string $module The module name.
331
     * @param string $url The download URL of the module.
332
     * @param string $md5 The MD5 hash of the module file.
333
     *
334
     * @return PBXApiResult An object containing the result of the API call.
335
     */
336
    public static function moduleStartDownload(string $module, string $url, string $md5): PBXApiResult
337
    {
338
        $res = new PBXApiResult();
339
        $res->processor = __METHOD__;
340
        $di = Di::getDefault();
341
        if ($di !== null) {
342
            $tempDir = $di->getShared(ConfigProvider::SERVICE_NAME)->path('www.uploadDir');
343
        } else {
344
            $tempDir = '/tmp';
345
        }
346
347
        $moduleDirTmp = "{$tempDir}/{$module}";
348
        Util::mwMkdir($moduleDirTmp);
349
350
        $download_settings = [
351
            'res_file' => "$moduleDirTmp/modulefile.zip",
352
            'url' => $url,
353
            'module' => $module,
354
            'md5' => $md5,
355
            'action' => 'moduleInstall',
356
        ];
357
        if (file_exists("$moduleDirTmp/error")) {
358
            unlink("$moduleDirTmp/error");
359
        }
360
        if (file_exists("$moduleDirTmp/installed")) {
361
            unlink("$moduleDirTmp/installed");
362
        }
363
        file_put_contents("$moduleDirTmp/progress", '0');
364
        file_put_contents(
365
            "$moduleDirTmp/download_settings.json",
366
            json_encode($download_settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
367
        );
368
        $workerDownloaderPath = Util::getFilePathByClassName(WorkerDownloader::class);
369
        $phpPath = Util::which('php');
370
        Processes::mwExecBg("{$phpPath} -f {$workerDownloaderPath} start {$moduleDirTmp}/download_settings.json");
371
372
        $res->data['uniqid'] = $module;
373
        $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
374
        $res->success = true;
375
376
        return $res;
377
    }
378
379
    /**
380
     * Returns the download status of a module.
381
     *
382
     * @param string $moduleUniqueID The unique ID of the module.
383
     *
384
     * @return PBXApiResult An object containing the result of the API call.
385
     */
386
    public static function moduleDownloadStatus(string $moduleUniqueID): PBXApiResult
387
    {
388
        clearstatcache();
389
        $res = new PBXApiResult();
390
        $res->processor = __METHOD__;
391
        $di = Di::getDefault();
392
        if ($di !== null) {
393
            $tempDir = $di->getShared(ConfigProvider::SERVICE_NAME)->path('www.uploadDir');
394
        } else {
395
            $tempDir = '/tmp';
396
        }
397
        $moduleDirTmp = $tempDir . '/' . $moduleUniqueID;
398
        $progress_file = $moduleDirTmp . '/progress';
399
        $error = '';
400
        if (file_exists($moduleDirTmp . '/error')) {
401
            $error = trim(file_get_contents($moduleDirTmp . '/error'));
402
        }
403
404
        // Wait until download process started
405
        $d_pid = Processes::getPidOfProcess("{$moduleDirTmp}/download_settings.json");
406
        if (empty($d_pid)) {
407
            usleep(500000);
408
        }
409
410
        if (!file_exists($progress_file)) {
411
            $res->data['d_status_progress'] = '0';
412
            $res->data['d_status'] = 'NOT_FOUND';
413
            $res->success = false;
414
        } elseif ('' !== $error) {
415
            $res->data['d_status'] = 'DOWNLOAD_ERROR';
416
            $res->data['d_status_progress'] = file_get_contents($progress_file);
417
            $res->data['d_error'] = $error;
418
            $res->messages[] = file_get_contents($moduleDirTmp . '/error');
419
            $res->success = false;
420
        } elseif ('100' === file_get_contents($progress_file)) {
421
            $res->data['d_status_progress'] = '100';
422
            $res->data['d_status'] = 'DOWNLOAD_COMPLETE';
423
            $res->data['filePath'] = "$moduleDirTmp/modulefile.zip";
424
            $res->success = true;
425
        } else {
426
            $res->data['d_status_progress'] = file_get_contents($progress_file);
427
            $d_pid = Processes::getPidOfProcess($moduleDirTmp . '/download_settings.json');
428
            if (empty($d_pid)) {
429
                $res->data['d_status'] = 'DOWNLOAD_ERROR';
430
                if (file_exists($moduleDirTmp . '/error')) {
431
                    $res->messages[] = file_get_contents($moduleDirTmp . '/error');
432
                } else {
433
                    $res->messages[] = "Download process interrupted at {$res->data['d_status_progress']}%";
434
                }
435
                $res->success = false;
436
            } else {
437
                $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
438
                $res->success = true;
439
            }
440
        }
441
442
        return $res;
443
    }
444
445
    /**
446
     * Unpacks a module ZIP file and retrieves metadata information from the JSON config inside.
447
     *
448
     * @param string $filePath The file path of the module.
449
     *
450
     * @return PBXApiResult An object containing the result of the API call.
451
     */
452
    public static function getMetadataFromModuleFile(string $filePath): PBXApiResult
453
    {
454
        $res = new PBXApiResult();
455
        $res->processor = __METHOD__;
456
457
        if (file_exists($filePath)) {
458
            $sevenZaPath = Util::which('7za');
459
            $grepPath = Util::which('grep');
460
            $echoPath = Util::which('echo');
461
            $awkPath = Util::which('awk');
462
            $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';
463
464
            Processes::mwExec($cmd, $out);
465
            $settings = json_decode(implode("\n", $out), true);
466
467
            $moduleUniqueID = $settings['moduleUniqueID'] ?? null;
468
            if (!$moduleUniqueID) {
469
                $res->messages[] = 'The" moduleUniqueID " in the module file is not described.the json or file does not exist.';
470
471
                return $res;
472
            }
473
            $res->success = true;
474
            $res->data = [
475
                'filePath' => $filePath,
476
                'uniqid' => $moduleUniqueID,
477
            ];
478
        }
479
480
        return $res;
481
    }
482
483
    /**
484
     * Retrieves available modules on MIKO repository.
485
     *
486
     * @return PBXApiResult
487
     */
488
    public static function getAvailableModules(): PBXApiResult
489
    {
490
        $res = new PBXApiResult();
491
        $res->processor = __METHOD__;
492
493
        $di = Di::getDefault();
494
        if ($di === null) {
495
            $res->success    = false;
496
            $res->messages[] = 'Dependency injector does not initialized';
497
            return $res;
498
        }
499
500
        $cacheKey = 'ModulesManagementProcessor:getAvailableModules';
501
        $managedCache = $di->getShared(ManagedCacheProvider::SERVICE_NAME);
502
        if ($managedCache->has($cacheKey)){
503
            $body = $managedCache->get($cacheKey);
504
        } else {
505
            $PBXVersion = PbxSettings::getValueByKey('PBXVersion');
506
            $PBXVersion = (string)str_ireplace('-dev', '', $PBXVersion);
507
            $WebUiLanguage = PbxSettings::getValueByKey('WebAdminLanguage');
508
            $body = '';
509
            $client = new GuzzleHttp\Client();
510
            try {
511
                $request = $client->request(
512
                    'POST',
513
                    'https://releases.mikopbx.com/releases/v1/mikopbx/getAvailableModules',
514
                    [
515
                        'headers' => [
516
                            'Content-Type' => 'application/json; charset=utf-8',
517
                        ],
518
                        'json' => [
519
                            'PBXVER' => $PBXVersion,
520
                            'LANGUAGE'=> $WebUiLanguage,
521
                        ],
522
                        'timeout' => 5,
523
                    ]
524
                );
525
                $code = $request->getStatusCode();
526
                if ($code === Response::OK){
527
                    $body = $request->getBody()->getContents();
528
                    $managedCache->set($cacheKey, $body, 3600);
529
                }
530
            } catch (\Throwable $e) {
531
                $code = Response::INTERNAL_SERVER_ERROR;
532
                Util::sysLogMsg(static::class, $e->getMessage());
533
                $res->messages[] = $e->getMessage();
534
            }
535
536
            if ($code !== Response::OK) {
537
                return $res;
538
            }
539
        }
540
        $res->data = json_decode($body, true)??[];
541
        $res->success = true;
542
543
        return $res;
544
    }
545
546
    /**
547
     * Retrieves the installation link for a module.
548
     *
549
     * @param string $moduleReleaseId The module release unique id retrieved on getAvailableModules
550
     *
551
     * @return PBXApiResult
552
     */
553
    public static function getModuleLink(string $moduleReleaseId): PBXApiResult
554
    {
555
        $res = new PBXApiResult();
556
        $res->processor = __METHOD__;
557
558
        $licenseKey = PbxSettings::getValueByKey('PBXLicense');
559
560
        $client = new GuzzleHttp\Client();
561
        $body = '';
562
        try {
563
            $request = $client->request(
564
                'POST',
565
                'https://releases.mikopbx.com/releases/v1/mikopbx/getModuleLink',
566
                [
567
                    'headers' => [
568
                        'Content-Type' => 'application/json; charset=utf-8',
569
                    ],
570
                    'json' => [
571
                        'LICENSE' => $licenseKey,
572
                        'RELEASEID'=> $moduleReleaseId,
573
                    ],
574
                    'timeout' => 5,
575
                ]
576
            );
577
            $code = $request->getStatusCode();
578
            if ($code === Response::OK){
579
                $body = $request->getBody()->getContents();
580
            }
581
        } catch (\Throwable $e) {
582
            $code = Response::INTERNAL_SERVER_ERROR;
583
            Util::sysLogMsg(static::class, $e->getMessage());
584
            $res->messages[] = $e->getMessage();
585
        }
586
587
        if ($code !== Response::OK) {
588
            return $res;
589
        }
590
591
        $res->data = json_decode($body, true)??[];
592
        $res->success = true;
593
        return $res;
594
    }
595
596
}