Passed
Push — develop ( c8e8dc...cb2748 )
by Nikolay
06:11 queued 13s
created

InstallFromRepo::installNewModule()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
dl 0
loc 29
rs 9.0777
c 1
b 0
f 0
cc 6
nc 6
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\Modules;
21
22
use MikoPBX\Common\Handlers\CriticalErrorsHandler;
23
use MikoPBX\Core\System\Util;
24
use MikoPBX\PBXCoreREST\Lib\Files\FilesConstants;
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 ModuleInstallationBase
35
{
36
    const DOWNLOAD_TIMEOUT = 120;
37
38
    // Optional release ID for the module. Defaults to 0.
39
    private int $moduleReleaseId = 0;
40
41
    /**
42
     * Class constructor
43
     *
44
     * @param string $asyncChannelId Pub/sub nchan channel id to send response to frontend
45
     * @param string $moduleUniqueId The unique identifier for the module to be installed.
46
     * @param int $moduleReleaseId Optional release ID for the module. Defaults to 0.
47
     */
48
    public function __construct(string $asyncChannelId, string $moduleUniqueId, int $moduleReleaseId=0)
49
    {
50
        $this->asyncChannelId = $asyncChannelId;
51
        $this->moduleUniqueId = $moduleUniqueId;
52
        $this->moduleReleaseId = $moduleReleaseId;
53
    }
54
55
56
    /**
57
     * Main entry point to install a new module.
58
     * This function handles the entire process of installing a new module, including
59
     * acquiring a mutex, checking the license, downloading, and installing the module.
60
     */
61
    public function start(): void
62
    {
63
        // Calculate total mutex timeout and extra 5 seconds to prevent installing the same module in the second thread
64
        $mutexTimeout = self::INSTALLATION_TIMEOUT+self::DOWNLOAD_TIMEOUT+5;
65
66
        // Create a mutex to ensure synchronized access
67
        $mutex = Util::createMutex(self::INSTALLATION_MUTEX, $this->moduleUniqueId, $mutexTimeout);
68
69
        $res = new PBXApiResult();
70
        $res->processor = __METHOD__;
71
        $res->success = true;
72
73
        // Synchronize the installation process
74
        try{
75
            $mutex->synchronized(
76
            function () use (&$res): void {
77
78
                // Retrieve release information
79
                list($releaseInfoResult, $res->success) = $this->getReleaseInfo();
80
                $this->pushMessageToBrowser(self::STAGE_I_GET_RELEASE, $releaseInfoResult);
81
                if (!$res->success) {
82
                    $res->messages['error'] = $releaseInfoResult;
83
                    return;
84
                }
85
86
                // Capture the license for the module
87
                list($licenseResult, $res->success) = $this->captureFeature($releaseInfoResult);
88
                $this->pushMessageToBrowser( self::STAGE_II_CHECK_LICENSE, $licenseResult);
89
                if (!$res->success) {
90
                    $res->messages = $licenseResult;
91
                    return;
92
                }
93
94
                // Get the download link for the module
95
                list($moduleLinkResult, $res->success) = $this->getModuleLink($releaseInfoResult);
96
                $this->pushMessageToBrowser( self::STAGE_III_GET_LINK, $moduleLinkResult);
97
                if (!$res->success) {
98
                    $res->messages['error'][] = $moduleLinkResult;
99
                    return;
100
                }
101
102
                // Download the module
103
                list($downloadResult, $res->success) = $this->downloadModule($moduleLinkResult);
104
                if (!$res->success) {
105
                    $res->messages['error'][] = $downloadResult;
106
                    return;
107
                } else {
108
                    $filePath = $downloadResult; // Path to the downloaded module
109
                }
110
111
                // Install the downloaded module
112
                list($installationResult, $res->success) = $this->installNewModule($filePath);
113
                if (!$res->success) {
114
                    $res->messages['error'][] = $installationResult;
115
                    return;
116
                }
117
118
                // Enable the module if it was previously enabled
119
                list($enableResult, $res->success) = $this->enableModule($installationResult);
120
                if (!$res->success) {
121
                    $res->messages['error'][] = $enableResult;
122
                }
123
            });
124
        } catch (\Throwable $e) {
125
            $res->success = false;
126
            $res->messages['error'][] = $e->getMessage();
127
            CriticalErrorsHandler::handleExceptionWithSyslog($e);
128
        } finally {
129
            $this->pushMessageToBrowser( self::STAGE_VII_FINAL_STATUS, $res->getResult());
130
        }
131
    }
132
133
    /**
134
     * Retrieves the release information for a specific module.
135
     * This function gets detailed information about the release based on the unique module ID and release ID.
136
     *
137
     * @return array An array containing the release information and a success flag.
138
     */
139
    private function getReleaseInfo(): array
140
    {
141
        // Retrieve module information
142
        $moduleInfo = GetModuleInfo::main($this->moduleUniqueId);
143
144
        // Check if release information is available
145
        if (empty($moduleInfo->data['releases'])) {
146
            return [[self::ERR_EMPTY_REPO_RESULT], false];
147
        }
148
        $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...
149
150
        // Find the specified release or the latest one
151
        foreach ($moduleInfo->data['releases'] as $release) {
152
            if (intval($release['releaseID']) === $this->moduleReleaseId) {
153
                $releaseInfo['releaseID'] = $release['releaseID'];
154
                break;
155
            } elseif (intval($release['releaseID']) > $releaseInfo['releaseID']) {
156
                $releaseInfo['releaseID'] = $release['releaseID'];
157
            }
158
        }
159
        // Additional information for license management
160
        $releaseInfo['licFeatureId'] = intval($moduleInfo->data['lic_feature_id']);
161
        $releaseInfo['licProductId'] = intval($moduleInfo->data['lic_product_id']);
162
        return [$releaseInfo, true];
163
    }
164
165
    /**
166
     * Captures the feature license for the module installation.
167
     * This function checks and captures the necessary license for installing the module based on the release information.
168
     *
169
     * @param array $releaseInfo Release information array.
170
     *
171
     * @return array An array containing the license capture result and a success flag.
172
     */
173
    private function captureFeature(array $releaseInfo): array
174
    {
175
        // Check if a feature license is required
176
        if ($releaseInfo['licFeatureId'] === 0) {
177
            return [[self::MSG_NO_LICENSE_REQ], true]; // No license required
178
        }
179
180
        // Prepare license capture request
181
        $request = [
182
            'action' => 'captureFeatureForProductId',
183
            'data' => $releaseInfo
184
        ];
185
186
        // Perform license capture
187
        $res = LicenseManagementProcessor::callBack($request);
188
        return [$res->messages, $res->success];
189
    }
190
191
    /**
192
     * Retrieves the download link for the module.
193
     * This function gets the download link for the module based on its release ID.
194
     *
195
     * @param array $releaseInfo Release information array.
196
     *
197
     * @return array An array containing the download link and a success flag.
198
     */
199
    private function getModuleLink(array $releaseInfo): array
200
    {
201
        $res = GetModuleLink::main($releaseInfo['releaseID']);
202
        if ($res->success){
203
            $modules =  $res->data['modules']??[];
204
            if (count($modules) > 0){
205
                return [$modules[0], true];
206
            }
207
            return [[self::ERR_EMPTY_GET_MODULE_LINK], false];
208
        }
209
        return [$res->messages, false];
210
    }
211
212
    /**
213
     * Downloads the module from the provided link.
214
     * This function handles the download process, ensuring that it completes within the allotted time.
215
     *
216
     * @param array $moduleLink Download link for the module and md5 hash
217
     *
218
     * @return array An array containing the path to the downloaded module or an error message, and a success flag.
219
     */
220
    private function downloadModule(array $moduleLink): array
221
    {
222
        // Initialization
223
        $url = $moduleLink['href'];
224
        $md5 = $moduleLink['md5'];
225
        $maximumDownloadTime = self::DOWNLOAD_TIMEOUT;
226
227
        // Start the download
228
        $res = StartDownload::main($this->moduleUniqueId, $url, $md5);
229
        $this->pushMessageToBrowser( self::STAGE_IV_DOWNLOAD_MODULE, $res->getResult());
230
        if (!$res->success) {
231
            return [$res->messages, false];
232
        }
233
234
        // Monitor download progress
235
        while ($maximumDownloadTime > 0) {
236
            $resDownloadStatus = DownloadStatus::main($this->moduleUniqueId);
237
            $this->pushMessageToBrowser( self::STAGE_IV_DOWNLOAD_MODULE, $resDownloadStatus->getResult());
238
            if (!$resDownloadStatus->success) {
239
                return [$resDownloadStatus->messages, false];
240
            } elseif ($resDownloadStatus->data[FilesConstants::D_STATUS] === FilesConstants::DOWNLOAD_IN_PROGRESS) {
241
                sleep(1); // Adjust sleep time as needed
242
                $maximumDownloadTime--;
243
            } elseif ($resDownloadStatus->data[FilesConstants::D_STATUS] === FilesConstants::DOWNLOAD_COMPLETE) {
244
                return [$resDownloadStatus->data[FilesConstants::FILE_PATH], true];
245
            }
246
        }
247
248
        // Download timeout
249
        $this->pushMessageToBrowser( self::STAGE_IV_DOWNLOAD_MODULE, [self::ERR_DOWNLOAD_TIMEOUT]);
250
        return [self::ERR_DOWNLOAD_TIMEOUT, false];
251
    }
252
253
254
}