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

ModuleInstallationBase::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
c 1
b 0
f 0
dl 0
loc 29
rs 9.0777
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\Providers\PBXCoreRESTClientProvider;
23
use MikoPBX\Core\System\Processes;
24
use MikoPBX\Core\System\Util;
25
use MikoPBX\Modules\PbxExtensionUtils;
26
use MikoPBX\PBXCoreREST\Lib\Files\FilesConstants;
27
use MikoPBX\PBXCoreREST\Lib\PBXApiResult;
28
use MikoPBX\PBXCoreREST\Workers\WorkerModuleInstaller;
29
use Phalcon\Di;
30
31
/**
32
 *  Class ModuleInstallationBase
33
 *  Base methods and functions for module installation
34
 *
35
 * @package MikoPBX\PBXCoreREST\Lib\Modules
36
 */
37
class ModuleInstallationBase extends \Phalcon\Di\Injectable
38
{
39
40
    // Common constants
41
    const INSTALLATION_MUTEX = 'ModuleInstallation';
42
    const MODULE_WAS_ENABLED = 'moduleWasEnabled';
43
44
    // Error messages
45
    const ERR_EMPTY_REPO_RESULT = "ext_EmptyRepoAnswer";
46
    const MSG_NO_LICENSE_REQ = "ext_NoLicenseRequired";
47
    const ERR_DOWNLOAD_TIMEOUT = "ext_ErrDownloadTimeout";
48
    const ERR_UPLOAD_TIMEOUT = "ext_ErrUploadTimeout";
49
    const ERR_INSTALLATION_TIMEOUT = "ext_ErrInstallationTimeout";
50
51
    const ERR_EMPTY_GET_MODULE_LINK = "ext_WrongGetModuleLink";
52
53
    // Timeout values
54
    const INSTALLATION_TIMEOUT = 120;
55
56
    // Install stages
57
    const STAGE_I_GET_RELEASE = 'Stage_I_GetRelease';
58
    const STAGE_I_UPLOAD_MODULE = 'Stage_I_UploadModule'; // Install from package stage
59
    const STAGE_II_CHECK_LICENSE = 'Stage_II_CheckLicense';
60
    const STAGE_III_GET_LINK = 'Stage_III_GetDownloadLink';
61
    const STAGE_IV_DOWNLOAD_MODULE = 'Stage_IV_DownloadModule';
62
    const STAGE_V_INSTALL_MODULE = 'Stage_V_InstallModule';
63
    const STAGE_VI_ENABLE_MODULE = 'Stage_VI_EnableModule';
64
    const STAGE_VII_FINAL_STATUS = 'Stage_VII_FinalStatus';
65
66
67
    // Pub/sub nchan channel id to send response to backend
68
    protected string $asyncChannelId;
69
70
    // The unique identifier for the module to be installed.
71
    protected string $moduleUniqueId;
72
73
    /**
74
     * Installs the module from the specified file path.
75
     * This function manages the module installation process, ensuring completion within the defined timeout.
76
     *
77
     * @param string $filePath Path to the module file.
78
     *
79
     * @return array An array containing the installation result and a success flag.
80
     */
81
    protected function installNewModule(string $filePath):array
82
    {
83
        // Initialization
84
        $maximumInstallationTime = self::INSTALLATION_TIMEOUT;
85
86
        // Start installation
87
        $installationResult = $this->startModuleInstallation($filePath);
88
        $this->pushMessageToBrowser(self::STAGE_V_INSTALL_MODULE, $installationResult->getResult());
89
        if (!$installationResult->success) {
90
            return [$installationResult->messages, false];
91
        }
92
93
        // Monitor installation progress
94
        while ($maximumInstallationTime > 0) {
95
            $resStatus = StatusOfModuleInstallation::main($filePath);
96
            $this->pushMessageToBrowser( self::STAGE_V_INSTALL_MODULE, $resStatus->getResult());
97
            if (!$resStatus->success) {
98
                return [$resStatus->messages, false];
99
            } elseif ($resStatus->data[StatusOfModuleInstallation::I_STATUS] === StatusOfModuleInstallation::INSTALLATION_IN_PROGRESS) {
100
                sleep(1); // Adjust sleep time as needed
101
                $maximumInstallationTime--;
102
            } elseif ($resStatus->data[StatusOfModuleInstallation::I_STATUS] === StatusOfModuleInstallation::INSTALLATION_COMPLETE) {
103
                return [$installationResult, true];
104
            }
105
        }
106
107
        // Installation timeout
108
        $this->pushMessageToBrowser( self::STAGE_V_INSTALL_MODULE, [self::ERR_INSTALLATION_TIMEOUT]);
109
        return [self::ERR_INSTALLATION_TIMEOUT, false];
110
    }
111
112
    /**
113
     * Enables the module if it was previously enabled.
114
     * This function checks the installation result and enables the module if needed.
115
     *
116
     * @param PBXApiResult $installationResult Result object from the installation process.
117
     *
118
     * @return array An array containing the module enabling process result and a success flag.
119
     */
120
    protected function enableModule( PBXApiResult $installationResult):array
121
    {
122
        // Check if the module was previously enabled
123
        if ($installationResult->data[self::MODULE_WAS_ENABLED]){
124
            $res = EnableModule::main($this->moduleUniqueId);
125
            $this->pushMessageToBrowser(self::STAGE_VI_ENABLE_MODULE, $res->getResult());
126
            return [$res->messages, $res->success];
127
        }
128
        return [[], true];
129
    }
130
131
    /**
132
     * Pushes messages to browser
133
     * @param string $stage installation stage name
134
     * @param array $data pushing data
135
     * @return void
136
     */
137
    protected function pushMessageToBrowser( string $stage, array $data):void
138
    {
139
        $message = [
140
            'stage' => $stage,
141
            'moduleUniqueId' => $this->moduleUniqueId,
142
            'stageDetails' => $data,
143
            'pid'=>posix_getpid()
144
        ];
145
146
        $di = Di::getDefault();
147
        $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
148
            '/pbxcore/api/nchan/pub/'.$this->asyncChannelId,
149
            PBXCoreRESTClientProvider::HTTP_METHOD_POST,
150
            $message,
151
            ['Content-Type' => 'application/json']
152
        ]);
153
    }
154
155
156
    /**
157
     * Installs a new additional extension module from an early uploaded zip archive.
158
     *
159
     * @param string $filePath The path to the module file.
160
     *
161
     * @return PBXApiResult An object containing the result of the API call.
162
     */
163
    protected function startModuleInstallation(string $filePath): PBXApiResult
164
    {
165
        $res = new PBXApiResult();
166
        $res->processor = __METHOD__;
167
        $resModuleMetadata = GetMetadataFromModulePackage::main($filePath);
168
        if (!$resModuleMetadata->success) {
169
            return $resModuleMetadata;
170
        }
171
172
        // Reset module unique id from package json data
173
        $this->moduleUniqueId = $resModuleMetadata->data['uniqid'];
174
175
        // Disable the module if it's enabled
176
        $moduleWasEnabled = false;
177
        if (PbxExtensionUtils::isEnabled($this->moduleUniqueId)) {
178
            $res = DisableModule::main($this->moduleUniqueId);
179
            if (!$res->success) {
180
                return $res;
181
            }
182
            $moduleWasEnabled = true;
183
        }
184
185
        $currentModuleDir = PbxExtensionUtils::getModuleDir($this->moduleUniqueId);
186
        $needBackup = is_dir($currentModuleDir);
187
188
        if ($needBackup) {
189
            UninstallModule::main($this->moduleUniqueId, true);
190
        }
191
192
        // Start the background process to install the module
193
        $temp_dir = dirname($filePath);
194
195
        // Create a progress file to track the installation progress
196
        file_put_contents($temp_dir . '/installation_progress', '0');
197
198
        // Create an error file to store any installation errors
199
        file_put_contents($temp_dir . '/installation_error', '');
200
201
        $install_settings = [
202
            FilesConstants::FILE_PATH => $filePath,
203
            'currentModuleDir' => $currentModuleDir,
204
            'uniqid' => $this->moduleUniqueId,
205
        ];
206
207
        // Save the installation settings to a JSON file
208
        $settings_file = "{$temp_dir}/install_settings.json";
209
        file_put_contents(
210
            $settings_file,
211
            json_encode($install_settings, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
212
        );
213
        $phpPath = Util::which('php');
214
        $workerFilesMergerPath = Util::getFilePathByClassName(WorkerModuleInstaller::class);
215
216
        // Execute the background process to install the module
217
        Processes::mwExecBg("{$phpPath} -f {$workerFilesMergerPath} start '{$settings_file}'");
218
        $res->data[FilesConstants::FILE_PATH] = $filePath;
219
        $res->data[self::MODULE_WAS_ENABLED] = $moduleWasEnabled;
220
        $res->success = true;
221
222
        return $res;
223
    }
224
}