Passed
Push — develop ( a24ba0...f685b5 )
by Nikolay
05:57 queued 35s
created

InstallFromRepo::getReleaseInfo()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
dl 0
loc 28
rs 9.4222
c 1
b 0
f 0
cc 5
nc 5
nop 2
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\Modules;
21
22
23
use MikoPBX\Common\Handlers\CriticalErrorsHandler;
24
use MikoPBX\Core\System\Util;
25
use MikoPBX\PBXCoreREST\Lib\LicenseManagementProcessor;
26
use MikoPBX\PBXCoreREST\Lib\PBXApiResult;
27
28
/**
29
 * Handles the installation of new modules.
30
 * This class provides functionality to install new additional extension modules for MikoPBX.
31
 *
32
 * @package MikoPBX\PBXCoreREST\Lib\Modules
33
 */
34
class InstallFromRepo extends \Phalcon\Di\Injectable
35
{
36
    public const CHANNEL_INSTALL_NAME = 'http://127.0.0.1/pbxcore/api/nchan/pub/install-module';
37
38
    // Error messages
39
    const ERR_EMPTY_REPO_RESULT = "ext_EmptyRepoAnswer";
40
    const ERR_DOWNLOAD_TIMEOUT = "ext_ErrDownloadTimeout";
41
    const ERR_INSTALLATION_TIMEOUT = "ext_ErrInstallationTimeout";
42
43
    // Timeout values
44
    const INSTALLATION_TIMEOUT = 120;
45
    const DOWNLOAD_TIMEOUT = 120;
46
47
    /**
48
     * Main entry point to install a new module.
49
     * This function handles the entire process of installing a new module, including
50
     * acquiring a mutex, checking the license, downloading, and installing the module.
51
     *
52
     * @param string $moduleUniqueID The unique identifier for the module to be installed.
53
     * @param int $releaseId Optional release ID for the module. Defaults to 0.
54
     *
55
     */
56
    public static function main(string $moduleUniqueID, int $releaseId = 0): void
57
    {
58
        // Calculate total mutex timeout and extra 5 seconds to prevent installing the same module in the second thread
59
        $mutexTimeout = self::INSTALLATION_TIMEOUT+self::DOWNLOAD_TIMEOUT+5;
60
61
        // Create a mutex to ensure synchronized access
62
        $mutex = Util::createMutex('InstallFromRepo', $moduleUniqueID, $mutexTimeout);
63
64
        $res = new PBXApiResult();
65
        $res->processor = __METHOD__;
66
        $res->success = true;
67
68
        // Synchronize the installation process
69
        try{
70
            $mutex->synchronized(
71
            function () use ($moduleUniqueID, $releaseId, &$res): void {
72
73
                // Retrieve release information
74
                list($releaseInfoResult, $res->success) = self::getReleaseInfo($moduleUniqueID, $releaseId);
75
                if (!$res->success) {
76
                    $res->messages['error'][] = $releaseInfoResult;
77
                    return;
78
                }
79
80
                // Capture the license for the module
81
                list($licenseResult, $res->success) = self::captureFeature($releaseInfoResult);
82
                if (!$res->success) {
83
                    $res->messages = $licenseResult;
84
                    return;
85
                }
86
87
                // Get the download link for the module
88
                list($moduleLinkResult, $res->success) = self::getModuleLink($releaseInfoResult);
89
                if (!$res->success) {
90
                    $res->messages['error'][] = $moduleLinkResult;
91
                    return;
92
                }
93
94
                // Download the module
95
                list($downloadResult, $res->success) = self::downloadModule($moduleLinkResult, $moduleUniqueID);
96
                if (!$res->success) {
97
                    $res->messages['error'][] = $downloadResult;
98
                    return;
99
                } else {
100
                    $filePath = $downloadResult; // Path to the downloaded module
101
                }
102
103
                // Install the downloaded module
104
                list($installationResult, $res->success) = self::installNewModule($filePath, $moduleUniqueID);
105
                if (!$res->success) {
106
                    $res->messages['error'][] = $installationResult;
107
                    return;
108
                }
109
110
                // Enable the module if it was previously enabled
111
                list($enableResult, $res->success) = self::enableModule($moduleUniqueID, $installationResult);
112
                if (!$res->success) {
113
                    $res->messages['error'][] = $enableResult;
114
                }
115
            });
116
        } catch (\Throwable $e) {
117
            $res->success = false;
118
            $res->messages['error'][] = $e->getMessage();
119
            CriticalErrorsHandler::handleExceptionWithSyslog($e);
120
        } finally {
121
            self::pushMessageToBrowser($res->getResult());
122
        }
123
124
    }
125
126
127
128
    /**
129
     * Retrieves the release information for a specific module.
130
     * This function gets detailed information about the release based on the unique module ID and release ID.
131
     *
132
     * @param string $moduleUniqueID Unique identifier for the module.
133
     * @param int $releaseId Optional release ID. If not specified, the latest release is selected.
134
     *
135
     * @return array An array containing the release information and a success flag.
136
     */
137
    private static function getReleaseInfo(string $moduleUniqueID, int $releaseId = 0): array
138
    {
139
        // Retrieve module information
140
        $moduleInfo = GetModuleInfo::main($moduleUniqueID);
141
142
        // Check if release information is available
143
        if (empty($moduleInfo->data['releases'])) {
144
            return [self::ERR_EMPTY_REPO_RESULT, false];
145
        }
146
        $releaseInfo['releaseID'] = 0;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$releaseInfo was never initialized. Although not strictly required by PHP, it is generally a good practice to add $releaseInfo = array(); before regardless.
Loading history...
147
        $releaseInfo['moduleUniqueID'] = $moduleUniqueID;
148
149
        // Find the specified release or the latest one
150
        foreach ($moduleInfo->data['releases'] as $release) {
151
            if ($release['releaseID'] === $releaseId) {
152
                $releaseInfo['releaseID'] = $release['releaseID'];
153
                $releaseInfo['hash'] = $release['hash'];
154
                break;
155
            } elseif ($release['releaseID'] > $releaseInfo['releaseID']) {
156
                $releaseInfo['releaseID'] = $release['releaseID'];
157
                $releaseInfo['hash'] = $release['hash'];
158
            }
159
        }
160
        // Additional information for license management
161
        $releaseInfo['licFeatureId'] = intval($releaseInfo['lic_feature_id']);
162
        $releaseInfo['licProductId'] = intval($releaseInfo['lic_product_id']);
163
164
        return [$releaseInfo, true];
165
    }
166
167
    /**
168
     * Captures the feature license for the module installation.
169
     * This function checks and captures the necessary license for installing the module based on the release information.
170
     *
171
     * @param array $releaseInfo Release information array.
172
     *
173
     * @return array An array containing the license capture result and a success flag.
174
     */
175
    private static function captureFeature(array $releaseInfo): array
176
    {
177
        // Check if a feature license is required
178
        if ($releaseInfo['featureId'] === 0) {
179
            return [[], true]; // No license required
180
        }
181
182
        // Prepare license capture request
183
        $request = [
184
            'action' => 'captureFeatureForProductId',
185
            'data' => $releaseInfo
186
        ];
187
188
        // Perform license capture
189
        $res = LicenseManagementProcessor::callBack($request);
190
        return [$res->messages, $res->success];
191
    }
192
193
    /**
194
     * Retrieves the download link for the module.
195
     * This function gets the download link for the module based on its release ID.
196
     *
197
     * @param array $releaseInfo Release information array.
198
     *
199
     * @return array An array containing the download link and a success flag.
200
     */
201
    private static function getModuleLink(array $releaseInfo): array
202
    {
203
        $res = GetModuleLink::main($releaseInfo['releaseID']);
204
        return [$res->messages, $res->success];
205
    }
206
207
    /**
208
     * Downloads the module from the provided link.
209
     * This function handles the download process, ensuring that it completes within the allotted time.
210
     *
211
     * @param array $moduleLink Download link for the module and md5 hash
212
     *
213
     * @return array An array containing the path to the downloaded module or an error message, and a success flag.
214
     */
215
    private static function downloadModule(array $moduleLink, string $moduleUniqueID): array
216
    {
217
        // Initialization
218
        $url = $moduleLink['download_link'];
219
        $md5 = $moduleLink['hash'];
220
        $maximumDownloadTime = self::DOWNLOAD_TIMEOUT;
221
222
        // Start the download
223
        $res = StartDownload::main($moduleUniqueID, $url, $md5);
224
        if (!$res->success) {
225
            return [$res->messages, false];
226
        }
227
228
        // Monitor download progress
229
        while ($maximumDownloadTime > 0) {
230
            $res = DownloadStatus::main($moduleUniqueID);
231
            if (!$res->success) {
232
                return [$res->messages, false];
233
            } elseif ($res->data[DownloadStatus::D_STATUS] = DownloadStatus::DOWNLOAD_IN_PROGRESS) {
234
                sleep(1); // Adjust sleep time as needed
235
                $message = [
236
                    'action' => 'DownloadStatus',
237
                    'uniqueId' => $moduleUniqueID,
238
                    'data' => $res->data,
239
                ];
240
                self::pushMessageToBrowser($message);
241
                $maximumDownloadTime--;
242
            } elseif ($res->data[DownloadStatus::D_STATUS] = DownloadStatus::DOWNLOAD_COMPLETE) {
243
                return [$res->data[DownloadStatus::FILE_PATH], true];
244
            }
245
        }
246
247
        // Download timeout
248
        return [self::ERR_DOWNLOAD_TIMEOUT, false];
249
    }
250
251
252
    /**
253
     * Installs the module from the specified file path.
254
     * This function manages the module installation process, ensuring completion within the defined timeout.
255
     *
256
     * @param string $filePath Path to the module file.
257
     *
258
     * @return array An array containing the installation result and a success flag.
259
     */
260
    private static function installNewModule(string $filePath, string $moduleUniqueID):array
261
    {
262
        // Initialization
263
        $maximumInstallationTime = self::INSTALLATION_TIMEOUT;
264
265
        // Start installation
266
        $res = InstallFromPackage::main($filePath);
267
        if (!$res->success) {
268
            return [$res->messages, false];
269
        }
270
271
        // Monitor installation progress
272
        while ($maximumInstallationTime > 0) {
273
            $res = StatusOfModuleInstallation::main($filePath);
274
            if (!$res->success) {
275
                return [$res->messages, false];
276
            } elseif ($res->data[StatusOfModuleInstallation::I_STATUS] = StatusOfModuleInstallation::INSTALLATION_IN_PROGRESS) {
277
                sleep(1); // Adjust sleep time as needed
278
                $message = [
279
                    'action' => 'StatusOfModuleInstallation',
280
                    'uniqueId' => $moduleUniqueID,
281
                    'data' => $res->data,
282
                ];
283
                self::pushMessageToBrowser($message);
284
                $maximumInstallationTime--;
285
            } elseif ($res->data[StatusOfModuleInstallation::I_STATUS] = StatusOfModuleInstallation::INSTALLATION_COMPLETE) {
286
                return [true, true];
287
            }
288
        }
289
290
        // Installation timeout
291
        return [self::ERR_INSTALLATION_TIMEOUT, false];
292
    }
293
294
    /**
295
     * Enables the module if it was previously enabled.
296
     * This function checks the installation result and enables the module if needed.
297
     *
298
     * @param string $moduleUniqueID Unique identifier for the module.
299
     * @param PBXApiResult $installationResult Result object from the installation process.
300
     *
301
     * @return array An array containing the module enabling process result and a success flag.
302
     */
303
    private static function enableModule(string $moduleUniqueID, PBXApiResult $installationResult):array
304
    {
305
        // Check if the module was previously enabled
306
        if ($installationResult->data[InstallFromPackage::MODULE_WAS_ENABLED]){
307
            $res = EnableModule::main($moduleUniqueID);
308
            return [$res->messages, $res->success];
309
        }
310
        return [[], true];
311
    }
312
313
    /**
314
     * Pushes messages to browser
315
     * @param array $message
316
     * @return void
317
     */
318
    public static function pushMessageToBrowser(array $message):void
319
    {
320
        $client  = new \GuzzleHttp\Client();
321
        $options = [
322
            'timeout'       => 5,
323
            'http_errors'   => false,
324
            'headers'       => [],
325
            'json'          => $message,
326
        ];
327
        try {
328
            $client->request('POST', self::CHANNEL_INSTALL_NAME, $options);
329
        } catch (\Throwable $e) {
330
            CriticalErrorsHandler::handleExceptionWithSyslog($e);
331
        }
332
    }
333
}