Passed
Push — develop ( 616438...84d7aa )
by Nikolay
06:16 queued 11s
created

PbxExtensionSetupBase::unInstallFiles()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
c 0
b 0
f 0
dl 0
loc 39
rs 9.2408
cc 5
nc 16
nop 1
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright (C) 2017-2020 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\Modules\Setup;
21
22
use MikoPBX\Common\Providers\ModulesDBConnectionsProvider;
23
use MikoPBX\Core\System\Processes;
24
use MikoPBX\Core\System\Upgrade\UpdateDatabase;
25
use MikoPBX\Modules\PbxExtensionUtils;
26
use MikoPBX\Common\Models\{PbxExtensionModules, PbxSettings};
27
use MikoPBX\Core\System\Util;
28
use Phalcon\Di\Injectable;
29
use Phalcon\Text;
30
use Throwable;
31
32
use function MikoPBX\Common\Config\appPath;
33
34
/**
35
 * Class PbxExtensionSetupBase
36
 * Common procedures for module installation and removing
37
 *
38
 * @property \MikoPBX\Common\Providers\LicenseProvider license
39
 * @property \MikoPBX\Common\Providers\TranslationProvider translation
40
 */
41
abstract class PbxExtensionSetupBase extends Injectable implements PbxExtensionSetupInterface
42
{
43
    /**
44
     * Module unique identify from the module.json
45
     * @var string
46
     */
47
    protected string $moduleUniqueID;
48
49
    /**
50
     * Module version from the module.json
51
     * @var string
52
     */
53
    protected $version;
54
55
    /**
56
     * Minimal required version PBX from the module.json
57
     * @var string
58
     */
59
    protected $min_pbx_version;
60
61
    /**
62
     * Module developer name  from the module.json
63
     * @var string
64
     */
65
    protected $developer;
66
67
    /**
68
     * Module developer's email from module.json
69
     * @var string
70
     */
71
    protected $support_email;
72
73
    /**
74
     * PBX core general database
75
     * @var \Phalcon\Db\Adapter\Pdo\Sqlite
76
     */
77
    protected $db;
78
79
    /**
80
     * Folder with module files
81
     * @var string
82
     */
83
    protected string $moduleDir;
84
85
    /**
86
     * Phalcon config service
87
     * @var \Phalcon\Config
88
     */
89
    protected $config;
90
91
    /**
92
     * Error and verbose messages
93
     * @var array
94
     */
95
    protected array $messages;
96
97
    /**
98
     * License worker
99
     * @var \MikoPBX\Service\License
100
     */
101
    protected $license;
102
103
    /**
104
     * Trial product version identify number from the module.json
105
     * @var int
106
     */
107
    public $lic_product_id;
108
109
    /**
110
     * License feature identify number from the module.json
111
     * @var int
112
     */
113
    public $lic_feature_id;
114
115
    /**
116
     * PbxExtensionBase constructor.
117
     *
118
     * @param string $moduleUniqueID
119
     */
120
    public function __construct(string $moduleUniqueID)
121
    {
122
        $this->moduleUniqueID = $moduleUniqueID;
123
        $this->messages = [];
124
        $this->db      = $this->getDI()->getShared('db');
125
        $this->config  = $this->getDI()->getShared('config');
126
        $this->license =  $this->getDI()->getShared('license');
127
        $this->moduleDir = $this->config->path('core.modulesDir') . '/' . $this->moduleUniqueID;
128
        $settings_file = "{$this->moduleDir}/module.json";
129
        if (file_exists($settings_file)) {
130
            $module_settings = json_decode(file_get_contents($settings_file), true);
131
            if ($module_settings) {
132
                $this->version         = $module_settings['version'];
133
                $this->min_pbx_version = $module_settings['min_pbx_version'];
134
                $this->developer       = $module_settings['developer'];
135
                $this->support_email   = $module_settings['support_email'];
136
                if (array_key_exists('lic_product_id', $module_settings)) {
137
                    $this->lic_product_id = $module_settings['lic_product_id'];
138
                } else {
139
                    $this->lic_product_id = 0;
140
                }
141
                if (array_key_exists('lic_feature_id', $module_settings)) {
142
                    $this->lic_feature_id = $module_settings['lic_feature_id'];
143
                } else {
144
                    $this->lic_feature_id = 0;
145
                }
146
            } else {
147
                $this->messages[] = 'Error on decode module.json';
148
            }
149
        }
150
151
        $this->messages  = [];
152
153
    }
154
155
    /**
156
     * The main module installation function called by PBXCoreRest after unzip module files
157
     * It calls some private functions and setup error messages on the message variable
158
     *
159
     * @return bool - result of installation
160
     */
161
    public function installModule(): bool
162
    {
163
        $result = true;
164
        try {
165
            if ( ! $this->activateLicense()) {
166
                $this->messages[] = 'License activate error';
167
                $result           = false;
168
            }
169
            if ( ! $this->installFiles()) {
170
                $this->messages[] = ' installFiles error';
171
                $result           = false;
172
            }
173
            if ( ! $this->installDB()) {
174
                $this->messages[] = ' installDB error';
175
                $result           = false;
176
            }
177
            if ( ! $this->fixFilesRights()) {
178
                $this->messages[] = ' Apply files rights error';
179
                $result           = false;
180
            }
181
        } catch (Throwable $exception) {
182
            $result         = false;
183
            $this->messages[] = $exception->getMessage();
184
        }
185
186
        return $result;
187
    }
188
189
    /**
190
     * Executes license activation only for commercial modules
191
     *
192
     * @return bool result of license activation
193
     */
194
    public function activateLicense(): bool
195
    {
196
        return true;
197
    }
198
199
    /**
200
     * Copies files, creates folders and symlinks for module and restores previous backup settings
201
     *
202
     * @return bool installation result
203
     */
204
    public function installFiles(): bool
205
    {
206
        // Create cache links for JS, CSS, IMG folders
207
        PbxExtensionUtils::createAssetsSymlinks($this->moduleUniqueID);
208
209
        // Create cache links for agi-bin scripts
210
        PbxExtensionUtils::createAgiBinSymlinks($this->moduleUniqueID);
211
212
        // Restore database settings
213
        $modulesDir          = $this->config->path('core.modulesDir');
214
        $backupPath = "{$modulesDir}/Backup/{$this->moduleUniqueID}";
215
        if (is_dir($backupPath)) {
216
            $cpPath = Util::which('cp');
217
            Processes::mwExec("{$cpPath} -r {$backupPath}/db/* {$this->moduleDir}/db/");
218
        }
219
220
        // Volt
221
        $this->cleanupCache();
222
223
        return true;
224
    }
225
226
    /**
227
     * Setups ownerships and folder rights
228
     *
229
     * @return bool fixing result
230
     */
231
    public function fixFilesRights(): bool
232
    {
233
        // Add regular www rights
234
        Util::addRegularWWWRights($this->moduleDir);
235
        $dirs = [
236
            "{$this->moduleDir}/agi-bin",
237
            "{$this->moduleDir}/bin"
238
        ];
239
        foreach ($dirs as $dir) {
240
            if(file_exists($dir) && is_dir($dir)){
241
                // Add executable right to module's binary
242
                Util::addExecutableRights($dir);
243
            }
244
        }
245
246
        return true;
247
    }
248
249
    /**
250
     * Creates database structure according to models annotations
251
     *
252
     * If it necessary, it fills some default settings, and change sidebar menu item representation for this module
253
     *
254
     * After installation it registers module on PbxExtensionModules model
255
     *
256
     * @return bool result of installation
257
     */
258
    public function installDB(): bool
259
    {
260
        $result = $this->createSettingsTableByModelsAnnotations();
261
262
        if ($result) {
263
            $result = $this->registerNewModule();
264
        }
265
266
        if ($result) {
267
            $result = $this->addToSidebar();
268
        }
269
        return $result;
270
    }
271
272
273
    /**
274
     * The main function called by MikoPBX REST API for delete any module
275
     *
276
     * @param $keepSettings bool if it set to true, the function saves module database
277
     *
278
     * @return bool uninstall result
279
     */
280
    public function uninstallModule(bool $keepSettings = false): bool
281
    {
282
        $result = true;
283
        try {
284
            if ( ! $this->unInstallDB($keepSettings)) {
285
                $this->messages[] = ' unInstallDB error';
286
                $result           = false;
287
            }
288
            if ($result && ! $this->unInstallFiles($keepSettings)) {
289
                $this->messages[] = ' unInstallFiles error';
290
                $result           = false;
291
            }
292
        } catch (Throwable $exception) {
293
            $result         = false;
294
            $this->messages[] = $exception->getMessage();
295
        }
296
297
        return $result;
298
    }
299
300
    /**
301
     * Deletes some settings from database and links to the module
302
     * If keepSettings set to true it copies database file to Backup folder
303
     *
304
     * @param  $keepSettings bool
305
     *
306
     * @return bool the uninstall result
307
     */
308
    public function unInstallDB(bool $keepSettings = false): bool
309
    {
310
        return $this->unregisterModule();
311
    }
312
313
    /**
314
     * Deletes records from PbxExtensionModules
315
     *
316
     * @return bool unregistration result
317
     */
318
    public function unregisterModule(): bool
319
    {
320
        $result = true;
321
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
322
        if ($module !== null) {
323
            $result = $module->delete();
324
        }
325
326
        return $result;
327
    }
328
329
    /**
330
     * Deletes the module files, folders, symlinks
331
     * If keepSettings set to true it copies database file to Backup folder
332
     *
333
     * @param $keepSettings bool
334
     *
335
     * @return bool delete result
336
     */
337
    public function unInstallFiles(bool $keepSettings = false):bool
338
    {
339
        $cpPath = Util::which('cp');
340
        $rmPath = Util::which('rm');
341
        $modulesDir          = $this->config->path('core.modulesDir');
342
        $backupPath = "{$modulesDir}/Backup/{$this->moduleUniqueID}";
343
        Processes::mwExec("{$rmPath} -rf {$backupPath}");
344
        if ($keepSettings) {
345
            Util::mwMkdir($backupPath);
346
            Processes::mwExec("{$cpPath} -r {$this->moduleDir}/db {$backupPath}/");
347
        }
348
        Processes::mwExec("{$rmPath} -rf {$this->moduleDir}");
349
350
        // Remove assets
351
        // IMG
352
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
353
        $moduleImageCacheDir = "{$imgCacheDir}/{$this->moduleUniqueID}";
354
        if (file_exists($moduleImageCacheDir)){
355
            unlink($moduleImageCacheDir);
356
        }
357
358
        // CSS
359
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
360
        $moduleCSSCacheDir = "{$cssCacheDir}/{$this->moduleUniqueID}";
361
        if (file_exists($moduleCSSCacheDir)){
362
            unlink($moduleCSSCacheDir);
363
        }
364
365
        // JS
366
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
367
        $moduleJSCacheDir = "{$jsCacheDir}/{$this->moduleUniqueID}";
368
        if (file_exists($moduleJSCacheDir)){
369
            unlink($moduleJSCacheDir);
370
        }
371
372
        // Volt
373
        $this->cleanupCache();
374
375
        return true;
376
    }
377
378
    /**
379
     * Returns error messages
380
     *
381
     * @return array
382
     */
383
    public function getMessages(): array
384
    {
385
        return $this->messages;
386
    }
387
388
    /**
389
     * Registers module in the PbxExtensionModules table
390
     *
391
     * @return bool
392
     */
393
    public function registerNewModule(): bool
394
    {
395
        // Проверим версию АТС и Модуля на совместимость
396
        $currentVersionPBX = PbxSettings::getValueByKey('PBXVersion');
397
        $currentVersionPBX = str_replace('-dev', '', $currentVersionPBX);
398
        if (version_compare($currentVersionPBX, $this->min_pbx_version) < 0) {
399
            $this->messages[] = "Module depends minimum PBX ver $this->min_pbx_version";
400
401
            return false;
402
        }
403
404
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
405
        if ( ! $module) {
406
            $module           = new PbxExtensionModules();
407
            $module->name     = $this->translation->_("Breadcrumb{$this->moduleUniqueID}");
408
            $module->disabled = '1';
409
        }
410
        $module->uniqid        = $this->moduleUniqueID;
411
        $module->developer     = $this->developer;
412
        $module->version       = $this->version;
413
        $module->description   = $this->translation->_("SubHeader{$this->moduleUniqueID}");
414
        $module->support_email = $this->support_email;
415
416
        return $module->save();
417
    }
418
419
    /**
420
     * DEPRECATED
421
     * Returns translated phrase
422
     *
423
     * @param $stringId string  Phrase identifier
424
     *
425
     * @return string  перевод
426
     */
427
    public function locString(string $stringId): string
428
    {
429
        Util::sysLogMsg('Util', 'Deprecated call ' . __METHOD__ . ' from ' . static::class, LOG_DEBUG);
430
        return $this->translation->_($stringId);
431
    }
432
433
    /**
434
     * Traverses files with model descriptions and creates / modifies tables in the system database
435
     *
436
     * @return bool the table modification result
437
     */
438
    public function createSettingsTableByModelsAnnotations(): bool
439
    {
440
441
        // Add new connection for this module after add new Models folder
442
        ModulesDBConnectionsProvider::recreateModulesDBConnections();
443
444
        $results = glob($this->moduleDir . '/Models/*.php', GLOB_NOSORT);
445
        $dbUpgrade = new UpdateDatabase();
446
        foreach ($results as $file) {
447
            $className        = pathinfo($file)['filename'];
448
            $moduleModelClass = "\\Modules\\{$this->moduleUniqueID}\\Models\\{$className}";
449
            $upgradeResult = $dbUpgrade->createUpdateDbTableByAnnotations($moduleModelClass);
450
            if (!$upgradeResult){
451
                return false;
452
            }
453
454
        }
455
        // Update database connections after upgrade their structure
456
        ModulesDBConnectionsProvider::recreateModulesDBConnections();
457
458
        return true;
459
    }
460
461
462
    /**
463
     * Adds module to sidebar menu
464
     *
465
     * @return bool
466
     */
467
    public function addToSidebar(): bool
468
    {
469
        $menuSettingsKey           = "AdditionalMenuItem{$this->moduleUniqueID}";
470
        $unCamelizedControllerName = Text::uncamelize($this->moduleUniqueID, '-');
471
        $menuSettings              = PbxSettings::findFirstByKey($menuSettingsKey);
472
        if ($menuSettings === null) {
473
            $menuSettings      = new PbxSettings();
474
            $menuSettings->key = $menuSettingsKey;
475
        }
476
        $value               = [
477
            'uniqid'        => $this->moduleUniqueID,
478
            'href'          => "/admin-cabinet/{$unCamelizedControllerName}",
479
            'group'         => 'modules',
480
            'iconClass'     => 'puzzle',
481
            'caption'       => "Breadcrumb{$this->moduleUniqueID}",
482
            'showAtSidebar' => true,
483
        ];
484
        $menuSettings->value = json_encode($value);
485
486
        return $menuSettings->save();
487
    }
488
489
    /**
490
     * Deletes old cache files
491
     */
492
    private function cleanupCache()
493
    {
494
        $cacheDirs = [];
495
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
496
        $rmPath = Util::which('rm');
497
        foreach ($cacheDirs as $cacheDir) {
498
            if (!empty($cacheDir)) {
499
                Processes::mwExec("{$rmPath} -rf {$cacheDir}/*");
500
            }
501
        }
502
    }
503
}