Passed
Push — develop ( ba81f1...650c64 )
by Портнов
11:27
created

PbxExtensionSetupBase::addToSidebar()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
c 0
b 0
f 0
dl 0
loc 20
rs 9.7666
cc 2
nc 2
nop 0
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
    /**
117
     * Массив ссылок на документацию
118
     * @var array
119
     */
120
    public array $wiki_links = [];
121
122
    /**
123
     * PbxExtensionBase constructor.
124
     *
125
     * @param string $moduleUniqueID
126
     */
127
    public function __construct(string $moduleUniqueID)
128
    {
129
        $this->moduleUniqueID = $moduleUniqueID;
130
        $this->messages = [];
131
        $this->db      = $this->getDI()->getShared('db');
132
        $this->config  = $this->getDI()->getShared('config');
133
        $this->license =  $this->getDI()->getShared('license');
134
        $this->moduleDir = $this->config->path('core.modulesDir') . '/' . $this->moduleUniqueID;
135
        $settings_file = "{$this->moduleDir}/module.json";
136
        if (file_exists($settings_file)) {
137
            $module_settings = json_decode(file_get_contents($settings_file), true);
138
            if ($module_settings) {
139
                $this->version         = $module_settings['version'];
140
                $this->min_pbx_version = $module_settings['min_pbx_version'];
141
                $this->developer       = $module_settings['developer'];
142
                $this->support_email   = $module_settings['support_email'];
143
                if (array_key_exists('lic_product_id', $module_settings)) {
144
                    $this->lic_product_id = $module_settings['lic_product_id'];
145
                } else {
146
                    $this->lic_product_id = 0;
147
                }
148
                if (array_key_exists('lic_feature_id', $module_settings)) {
149
                    $this->lic_feature_id = $module_settings['lic_feature_id'];
150
                } else {
151
                    $this->lic_feature_id = 0;
152
                }
153
                $wiki_links = $module_settings['wiki_links']??[];
154
                if(is_array($wiki_links)){
155
                    $this->wiki_links = $wiki_links;
156
                }
157
            } else {
158
                $this->messages[] = 'Error on decode module.json';
159
            }
160
        }
161
        $this->messages  = [];
162
    }
163
164
    /**
165
     * The main module installation function called by PBXCoreRest after unzip module files
166
     * It calls some private functions and setup error messages on the message variable
167
     *
168
     * @return bool - result of installation
169
     */
170
    public function installModule(): bool
171
    {
172
        $result = true;
173
        try {
174
            if ( ! $this->activateLicense()) {
175
                $this->messages[] = 'License activate error';
176
                $result           = false;
177
            }
178
            if ( ! $this->installFiles()) {
179
                $this->messages[] = ' installFiles error';
180
                $result           = false;
181
            }
182
            if ( ! $this->installDB()) {
183
                $this->messages[] = ' installDB error';
184
                $result           = false;
185
            }
186
            if ( ! $this->fixFilesRights()) {
187
                $this->messages[] = ' Apply files rights error';
188
                $result           = false;
189
            }
190
        } catch (Throwable $exception) {
191
            $result         = false;
192
            $this->messages[] = $exception->getMessage();
193
        }
194
195
        return $result;
196
    }
197
198
    /**
199
     * Executes license activation only for commercial modules
200
     *
201
     * @return bool result of license activation
202
     */
203
    public function activateLicense(): bool
204
    {
205
        if($this->lic_product_id>0) {
206
            $lic = PbxSettings::getValueByKey('PBXLicense');
207
            if (empty($lic)) {
208
                $this->messages[] = 'License key not found...';
209
                return false;
210
            }
211
            // Получение пробной лицензии.
212
            $this->license->addtrial($this->lic_product_id);
213
        }
214
        return true;
215
    }
216
217
    /**
218
     * Copies files, creates folders and symlinks for module and restores previous backup settings
219
     *
220
     * @return bool installation result
221
     */
222
    public function installFiles(): bool
223
    {
224
        // Create cache links for JS, CSS, IMG folders
225
        PbxExtensionUtils::createAssetsSymlinks($this->moduleUniqueID);
226
227
        // Create cache links for agi-bin scripts
228
        PbxExtensionUtils::createAgiBinSymlinks($this->moduleUniqueID);
229
230
        // Restore database settings
231
        $modulesDir          = $this->config->path('core.modulesDir');
232
        $backupPath = "{$modulesDir}/Backup/{$this->moduleUniqueID}";
233
        if (is_dir($backupPath)) {
234
            $cpPath = Util::which('cp');
235
            Processes::mwExec("{$cpPath} -r {$backupPath}/db/* {$this->moduleDir}/db/");
236
        }
237
238
        // Volt
239
        $this->cleanupCache();
240
241
        return true;
242
    }
243
244
    /**
245
     * Setups ownerships and folder rights
246
     *
247
     * @return bool fixing result
248
     */
249
    public function fixFilesRights(): bool
250
    {
251
        // Add regular www rights
252
        Util::addRegularWWWRights($this->moduleDir);
253
        $dirs = [
254
            "{$this->moduleDir}/agi-bin",
255
            "{$this->moduleDir}/bin"
256
        ];
257
        foreach ($dirs as $dir) {
258
            if(file_exists($dir) && is_dir($dir)){
259
                // Add executable right to module's binary
260
                Util::addExecutableRights($dir);
261
            }
262
        }
263
264
        return true;
265
    }
266
267
    /**
268
     * Creates database structure according to models annotations
269
     *
270
     * If it necessary, it fills some default settings, and change sidebar menu item representation for this module
271
     *
272
     * After installation it registers module on PbxExtensionModules model
273
     *
274
     * @return bool result of installation
275
     */
276
    public function installDB(): bool
277
    {
278
        $result = $this->createSettingsTableByModelsAnnotations();
279
280
        if ($result) {
281
            $result = $this->registerNewModule();
282
        }
283
284
        if ($result) {
285
            $result = $this->addToSidebar();
286
        }
287
        return $result;
288
    }
289
290
291
    /**
292
     * The main function called by MikoPBX REST API for delete any module
293
     *
294
     * @param $keepSettings bool if it set to true, the function saves module database
295
     *
296
     * @return bool uninstall result
297
     */
298
    public function uninstallModule(bool $keepSettings = false): bool
299
    {
300
        $result = true;
301
        try {
302
            if ( ! $this->unInstallDB($keepSettings)) {
303
                $this->messages[] = ' unInstallDB error';
304
                $result           = false;
305
            }
306
            if ($result && ! $this->unInstallFiles($keepSettings)) {
307
                $this->messages[] = ' unInstallFiles error';
308
                $result           = false;
309
            }
310
        } catch (Throwable $exception) {
311
            $result         = false;
312
            $this->messages[] = $exception->getMessage();
313
        }
314
315
        return $result;
316
    }
317
318
    /**
319
     * Deletes some settings from database and links to the module
320
     * If keepSettings set to true it copies database file to Backup folder
321
     *
322
     * @param  $keepSettings bool
323
     *
324
     * @return bool the uninstall result
325
     */
326
    public function unInstallDB(bool $keepSettings = false): bool
327
    {
328
        return $this->unregisterModule();
329
    }
330
331
    /**
332
     * Deletes records from PbxExtensionModules
333
     *
334
     * @return bool unregistration result
335
     */
336
    public function unregisterModule(): bool
337
    {
338
        $result = true;
339
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
340
        if ($module !== null) {
341
            $result = $module->delete();
342
        }
343
344
        return $result;
345
    }
346
347
    /**
348
     * Deletes the module files, folders, symlinks
349
     * If keepSettings set to true it copies database file to Backup folder
350
     *
351
     * @param $keepSettings bool
352
     *
353
     * @return bool delete result
354
     */
355
    public function unInstallFiles(bool $keepSettings = false):bool
356
    {
357
        $cpPath = Util::which('cp');
358
        $rmPath = Util::which('rm');
359
        $modulesDir          = $this->config->path('core.modulesDir');
360
        $backupPath = "{$modulesDir}/Backup/{$this->moduleUniqueID}";
361
        Processes::mwExec("{$rmPath} -rf {$backupPath}");
362
        if ($keepSettings) {
363
            Util::mwMkdir($backupPath);
364
            Processes::mwExec("{$cpPath} -r {$this->moduleDir}/db {$backupPath}/");
365
        }
366
        Processes::mwExec("{$rmPath} -rf {$this->moduleDir}");
367
368
        // Remove assets
369
        // IMG
370
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
371
        $moduleImageCacheDir = "{$imgCacheDir}/{$this->moduleUniqueID}";
372
        if (file_exists($moduleImageCacheDir)){
373
            unlink($moduleImageCacheDir);
374
        }
375
376
        // CSS
377
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
378
        $moduleCSSCacheDir = "{$cssCacheDir}/{$this->moduleUniqueID}";
379
        if (file_exists($moduleCSSCacheDir)){
380
            unlink($moduleCSSCacheDir);
381
        }
382
383
        // JS
384
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
385
        $moduleJSCacheDir = "{$jsCacheDir}/{$this->moduleUniqueID}";
386
        if (file_exists($moduleJSCacheDir)){
387
            unlink($moduleJSCacheDir);
388
        }
389
390
        // Volt
391
        $this->cleanupCache();
392
393
        return true;
394
    }
395
396
    /**
397
     * Returns error messages
398
     *
399
     * @return array
400
     */
401
    public function getMessages(): array
402
    {
403
        return $this->messages;
404
    }
405
406
    /**
407
     * Registers module in the PbxExtensionModules table
408
     *
409
     * @return bool
410
     */
411
    public function registerNewModule(): bool
412
    {
413
        // Проверим версию АТС и Модуля на совместимость
414
        $currentVersionPBX = PbxSettings::getValueByKey('PBXVersion');
415
        $currentVersionPBX = str_replace('-dev', '', $currentVersionPBX);
416
        if (version_compare($currentVersionPBX, $this->min_pbx_version) < 0) {
417
            $this->messages[] = "Module depends minimum PBX ver $this->min_pbx_version";
418
419
            return false;
420
        }
421
422
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
423
        if ( ! $module) {
424
            $module           = new PbxExtensionModules();
425
            $module->name     = $this->translation->_("Breadcrumb{$this->moduleUniqueID}");
426
            $module->disabled = '1';
427
        }
428
        $module->uniqid        = $this->moduleUniqueID;
429
        $module->developer     = $this->developer;
430
        $module->version       = $this->version;
431
        $module->description   = $this->translation->_("SubHeader{$this->moduleUniqueID}");
432
        $module->support_email = $this->support_email;
433
434
        try {
435
            $module->wiki_links = json_encode($this->wiki_links, JSON_THROW_ON_ERROR);
436
        }catch (\JsonException $e){
437
            Util::sysLogMsg(__CLASS__, $e->getMessage());
438
        }
439
440
        return $module->save();
441
    }
442
443
    /**
444
     * DEPRECATED
445
     * Returns translated phrase
446
     *
447
     * @param $stringId string  Phrase identifier
448
     *
449
     * @return string  перевод
450
     */
451
    public function locString(string $stringId): string
452
    {
453
        Util::sysLogMsg('Util', 'Deprecated call ' . __METHOD__ . ' from ' . static::class, LOG_DEBUG);
454
        return $this->translation->_($stringId);
455
    }
456
457
    /**
458
     * Traverses files with model descriptions and creates / modifies tables in the system database
459
     *
460
     * @return bool the table modification result
461
     */
462
    public function createSettingsTableByModelsAnnotations(): bool
463
    {
464
465
        // Add new connection for this module after add new Models folder
466
        ModulesDBConnectionsProvider::recreateModulesDBConnections();
467
468
        $results = glob($this->moduleDir . '/Models/*.php', GLOB_NOSORT);
469
        $dbUpgrade = new UpdateDatabase();
470
        foreach ($results as $file) {
471
            $className        = pathinfo($file)['filename'];
472
            $moduleModelClass = "\\Modules\\{$this->moduleUniqueID}\\Models\\{$className}";
473
            $upgradeResult = $dbUpgrade->createUpdateDbTableByAnnotations($moduleModelClass);
474
            if (!$upgradeResult){
475
                return false;
476
            }
477
478
        }
479
        // Update database connections after upgrade their structure
480
        ModulesDBConnectionsProvider::recreateModulesDBConnections();
481
482
        return true;
483
    }
484
485
486
    /**
487
     * Adds module to sidebar menu
488
     *
489
     * @return bool
490
     */
491
    public function addToSidebar(): bool
492
    {
493
        $menuSettingsKey           = "AdditionalMenuItem{$this->moduleUniqueID}";
494
        $unCamelizedControllerName = Text::uncamelize($this->moduleUniqueID, '-');
495
        $menuSettings              = PbxSettings::findFirstByKey($menuSettingsKey);
496
        if ($menuSettings === null) {
497
            $menuSettings      = new PbxSettings();
498
            $menuSettings->key = $menuSettingsKey;
499
        }
500
        $value               = [
501
            'uniqid'        => $this->moduleUniqueID,
502
            'href'          => "/admin-cabinet/{$unCamelizedControllerName}",
503
            'group'         => 'modules',
504
            'iconClass'     => 'puzzle',
505
            'caption'       => "Breadcrumb{$this->moduleUniqueID}",
506
            'showAtSidebar' => true,
507
        ];
508
        $menuSettings->value = json_encode($value);
509
510
        return $menuSettings->save();
511
    }
512
513
    /**
514
     * Deletes old cache files
515
     */
516
    private function cleanupCache()
517
    {
518
        $cacheDirs = [];
519
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
520
        $rmPath = Util::which('rm');
521
        foreach ($cacheDirs as $cacheDir) {
522
            if (!empty($cacheDir)) {
523
                Processes::mwExec("{$rmPath} -rf {$cacheDir}/*");
524
            }
525
        }
526
    }
527
}