Passed
Push — develop ( d617cb...49fd37 )
by Nikolay
05:15 queued 36s
created

InstallFromRepo::createMutex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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