Passed
Push — develop ( ef268c...5d00ee )
by Nikolay
04:59
created

PbxExtensionSetupBase::checkCompatibility()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 18
rs 10
cc 2
nc 2
nop 0
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\Modules\Setup;
21
22
use MikoPBX\Common\Providers\ModulesDBConnectionsProvider;
23
use MikoPBX\Common\Providers\PBXConfModulesProvider;
24
use MikoPBX\Core\System\Processes;
25
use MikoPBX\Core\System\Upgrade\UpdateDatabase;
26
use MikoPBX\Modules\PbxExtensionUtils;
27
use MikoPBX\Common\Models\{PbxExtensionModules, PbxSettings};
28
use MikoPBX\Core\System\Util;
29
use Phalcon\Di\Injectable;
30
use Throwable;
31
32
use function MikoPBX\Common\Config\appPath;
33
use function Symfony\Component\Translation\t;
0 ignored issues
show
introduced by
The function Symfony\Component\Translation\t was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
34
35
36
/**
37
 * Base class for module setup.
38
 * Common procedures for module installation and removing external modules.
39
 *
40
 * @property \MikoPBX\Common\Providers\MarketPlaceProvider license
41
 * @property \MikoPBX\Common\Providers\TranslationProvider translation
42
 * @property \Phalcon\Config\Adapter\Json config
43
 *
44
 *  @package MikoPBX\Modules\Setup
45
 */
46
abstract class PbxExtensionSetupBase extends Injectable implements PbxExtensionSetupInterface
47
{
48
    /**
49
     * Module unique identify from the module.json
50
     * @var string
51
     */
52
    protected string $moduleUniqueID;
53
54
    /**
55
     * Module version from the module.json
56
     * @var string|null
57
     */
58
    protected $version;
59
60
    /**
61
     * Minimal required version PBX from the module.json
62
     * @var string
63
     */
64
    protected string $min_pbx_version;
65
66
    /**
67
     * Module developer name  from the module.json
68
     * @var string|null
69
     */
70
    protected $developer;
71
72
    /**
73
     * Module developer's email from module.json
74
     * @var string|null
75
     */
76
    protected $support_email;
77
78
    /**
79
     * PBX core general database
80
     * @var \Phalcon\Db\Adapter\Pdo\Sqlite|null
81
     */
82
    protected $db;
83
84
    /**
85
     * Folder with module files
86
     * @var string
87
     */
88
    protected string $moduleDir;
89
90
    /**
91
     * Phalcon config service
92
     * @var \Phalcon\Config|null
93
     */
94
    protected $config;
95
96
    /**
97
     * Error and verbose messages
98
     * @var array
99
     */
100
    protected array $messages;
101
102
    /**
103
     * License worker
104
     * @var \MikoPBX\Service\License|null
105
     */
106
    protected $license;
107
108
    /**
109
     * Trial product version identify number from the module.json
110
     * @var int|null
111
     */
112
    public $lic_product_id;
113
114
    /**
115
     * License feature identify number from the module.json
116
     * @var int|null
117
     */
118
    public $lic_feature_id;
119
120
    /**
121
     * Array of wiki links
122
     * @var array
123
     */
124
    public array $wiki_links = [];
125
126
    /**
127
     * Constructor for the module class.
128
     *
129
     * @param string $moduleUniqueID The unique identifier of the module.
130
     */
131
    public function __construct(string $moduleUniqueID)
132
    {
133
        // Set the module unique ID
134
        $this->moduleUniqueID = $moduleUniqueID;
135
136
        // Initialize properties
137
        $this->messages = [];
138
        $this->db      = $this->getDI()->getShared('db');
139
        $this->config  = $this->getDI()->getShared('config');
140
        $this->license =  $this->getDI()->getShared('license');
141
        $this->moduleDir = $this->config->path('core.modulesDir') . '/' . $this->moduleUniqueID;
142
143
        // Load module settings from module.json file
144
        $settings_file = "{$this->moduleDir}/module.json";
145
        if (file_exists($settings_file)) {
146
            $module_settings = json_decode(file_get_contents($settings_file), true);
147
            if ($module_settings) {
148
                // Extract module settings
149
                $this->version         = $module_settings['version'];
150
                $this->min_pbx_version = $module_settings['min_pbx_version']??'';
151
                $this->developer       = $module_settings['developer'];
152
                $this->support_email   = $module_settings['support_email'];
153
154
                // Check if license product ID is defined in module settings
155
                if (array_key_exists('lic_product_id', $module_settings)) {
156
                    $this->lic_product_id = $module_settings['lic_product_id'];
157
                } else {
158
                    $this->lic_product_id = 0;
159
                }
160
161
                // Check if license feature ID is defined in module settings
162
                if (array_key_exists('lic_feature_id', $module_settings)) {
163
                    $this->lic_feature_id = $module_settings['lic_feature_id'];
164
                } else {
165
                    $this->lic_feature_id = 0;
166
                }
167
168
                // Extract wiki links from module settings
169
                $wiki_links = $module_settings['wiki_links']??[];
170
                if(is_array($wiki_links)){
171
                    $this->wiki_links = $wiki_links;
172
                }
173
            } else {
174
                $this->messages[] = $this->translation->_("ext_ErrorOnDecodeModuleJson",['filename'=>'module.json']);
175
            }
176
        }
177
178
        // Reset messages array
179
        $this->messages  = [];
180
    }
181
182
    /**
183
     * Performs the main module installation process called by PBXCoreRest after unzipping module files.
184
     * It invokes private functions and sets up error messages in the message variable.
185
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#installmodule
186
     *
187
     * @return bool The result of the installation process.
188
     */
189
    public function installModule(): bool
190
    {
191
        try {
192
            if (!$this->checkCompatibility()){
193
                return false;
194
            }
195
            if (!$this->activateLicense()) {
196
                $this->messages[] = $this->translation->_("ext_ErrorOnLicenseActivation");
197
                return false;
198
            }
199
            if ( ! $this->installFiles()) {
200
                $this->messages[] = $this->translation->_("ext_ErrorOnInstallFiles");
201
                return false;
202
            }
203
            if ( ! $this->installDB()) {
204
                $this->messages[] = $this->translation->_("ext_ErrorOnInstallDB");
205
                return false;
206
            }
207
            if ( ! $this->fixFilesRights()) {
208
                $this->messages[] = $this->translation->_("ext_ErrorOnAppliesFilesRights");
209
                return false;
210
            }
211
212
            // Recreate version hash for js files and translations
213
            PBXConfModulesProvider::getVersionsHash(true);
214
215
        } catch (Throwable $exception) {
216
            $this->messages[] = $exception->getMessage();
217
            return false;
218
        }
219
220
        return true;
221
    }
222
223
    /**
224
     * Checks if the current PBX version is compatible with the minimum required version.
225
     *
226
     * This function compares the current PBX version with the minimum required version
227
     * specified by the module. If the current version is lower than the minimum required
228
     * version, it adds a message to the `messages` array and returns `false`. Otherwise,
229
     * it returns `true`, indicating that the PBX version is compatible.
230
     *
231
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#checkcompatibility
232
     *
233
     * @return bool Returns `true` if PBX version is compatible; otherwise, `false`.
234
     */
235
    public function checkCompatibility():bool
236
    {
237
        // Get the current PBX version from the settings.
238
        $currentVersionPBX = PbxSettings::getValueByKey('PBXVersion');
239
240
        // Remove any '-dev' suffix from the version.
241
        $currentVersionPBX = str_replace('-dev', '', $currentVersionPBX);
242
        if (version_compare($currentVersionPBX, $this->min_pbx_version) < 0) {
243
            // The current PBX version is lower than the required version.
244
            // Add a message indicating the compatibility issue.
245
            $this->messages[] = $this->translation->_("ext_ModuleDependsHigherVersion",['version'=>$this->min_pbx_version]);
246
247
            // Return false to indicate incompatibility.
248
            return false;
249
        }
250
251
        // The current PBX version is compatible.
252
        return true;
253
    }
254
255
    /**
256
     * Activates the license, applicable only for commercial modules.
257
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#activatelicense
258
     *
259
     * @return bool The result of the license activation.
260
     */
261
    public function activateLicense(): bool
262
    {
263
        if($this->lic_product_id>0) {
264
            $lic = PbxSettings::getValueByKey('PBXLicense');
265
            if (empty($lic)) {
266
                $this->messages[] = $this->translation->_("ext_EmptyLicenseKey");
267
                return false;
268
            }
269
270
            // Get trial license for the module
271
            $this->license->addtrial($this->lic_product_id);
272
        }
273
        return true;
274
    }
275
276
    /**
277
     * Copies files, creates folders, and symlinks for the module and restores previous backup settings.
278
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#installfiles
279
     *
280
     * @return bool The result of the installation process.
281
     */
282
    public function installFiles(): bool
283
    {
284
        // Create cache links for JS, CSS, IMG folders
285
        PbxExtensionUtils::createAssetsSymlinks($this->moduleUniqueID);
286
287
        // Create links for the module view templates
288
        PbxExtensionUtils::createViewSymlinks($this->moduleUniqueID);
289
290
        // Create links for agi-bin scripts
291
        PbxExtensionUtils::createAgiBinSymlinks($this->moduleUniqueID);
292
293
        // Restore database settings
294
        $modulesDir          = $this->config->path('core.modulesDir');
295
        $backupPath = "{$modulesDir}/Backup/{$this->moduleUniqueID}";
296
        if (is_dir($backupPath)) {
297
            $cpPath = Util::which('cp');
298
            Processes::mwExec("{$cpPath} -r {$backupPath}/db/* {$this->moduleDir}/db/");
299
        }
300
301
        // Volt
302
        $this->cleanupVoltCache();
303
304
        return true;
305
    }
306
307
    /**
308
     * Sets up ownerships and folder rights.
309
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#fixfilesrights
310
     *
311
     * @return bool The result of the fixing process.
312
     */
313
    public function fixFilesRights(): bool
314
    {
315
        // Add regular www rights
316
        Util::addRegularWWWRights($this->moduleDir);
317
        $dirs = [
318
            "{$this->moduleDir}/agi-bin",
319
            "{$this->moduleDir}/bin"
320
        ];
321
        foreach ($dirs as $dir) {
322
            if(file_exists($dir) && is_dir($dir)){
323
                // Add executable right to module's binary
324
                Util::addExecutableRights($dir);
325
            }
326
        }
327
328
        return true;
329
    }
330
331
    /**
332
     * Creates the database structure according to models' annotations.
333
     * If necessary, it fills some default settings and changes the sidebar menu item representation for this module.
334
     * After installation, it registers the module on the PbxExtensionModules model.
335
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#fixfilesrights
336
     *
337
     * @return bool The result of the installation process.
338
     */
339
    public function installDB(): bool
340
    {
341
        $result = $this->createSettingsTableByModelsAnnotations();
342
343
        if ($result) {
344
            $result = $this->registerNewModule();
345
        }
346
347
        if ($result) {
348
            $result = $this->addToSidebar();
349
        }
350
        return $result;
351
    }
352
353
    /**
354
     * Performs the main module uninstallation process called by MikoPBX REST API to delete any module.
355
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#uninstallmodule
356
     *
357
     * @param bool $keepSettings If set to true, the function saves the module database.
358
     *
359
     * @return bool The result of the uninstallation process.
360
     */
361
    public function uninstallModule(bool $keepSettings = false): bool
362
    {
363
        $result = true;
364
        try {
365
            if ( ! $this->unInstallDB($keepSettings)) {
366
                $this->messages[] = $this->translation->_("ext_UninstallDBError");
367
                $result           = false;
368
            }
369
            if ($result && ! $this->unInstallFiles($keepSettings)) {
370
                $this->messages[] = $this->translation->_("ext_UnInstallFiles");
371
                $result           = false;
372
            }
373
374
            // Recreate version hash for js files and translations
375
            PBXConfModulesProvider::getVersionsHash(true);
376
377
        } catch (Throwable $exception) {
378
            $result         = false;
379
            $this->messages[] = $exception->getMessage();
380
        }
381
382
        return $result;
383
    }
384
385
    /**
386
     * Deletes some settings from the database and links to the module.
387
     * If $keepSettings is set to true, it copies the database file to the Backup folder.
388
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#uninstalldb
389
     *
390
     * @param bool $keepSettings If set to true, the module database is saved.
391
     *
392
     * @return bool The result of the uninstallation process.
393
     */
394
    public function unInstallDB(bool $keepSettings = false): bool
395
    {
396
        return $this->unregisterModule();
397
    }
398
399
    /**
400
     * Deletes records from the PbxExtensionModules table.
401
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#unregistermodule
402
     *
403
     * @return bool The result of the uninstallation process.
404
     */
405
    public function unregisterModule(): bool
406
    {
407
        $result = true;
408
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
409
        if ($module !== null) {
410
            $result = $module->delete();
411
        }
412
413
        return $result;
414
    }
415
416
    /**
417
     * Deletes the module files, folders, and symlinks.
418
     * If $keepSettings is set to true, it copies the database file to the Backup folder.
419
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#uninstallfiles
420
     *
421
     * @param bool $keepSettings If set to true, the module database is saved.
422
     *
423
     * @return bool The result of the deletion process.
424
     */
425
    public function unInstallFiles(bool $keepSettings = false):bool
426
    {
427
        $cpPath = Util::which('cp');
428
        $rmPath = Util::which('rm');
429
        $modulesDir          = $this->config->path('core.modulesDir');
430
        $backupPath = "{$modulesDir}/Backup/{$this->moduleUniqueID}";
431
        Processes::mwExec("{$rmPath} -rf {$backupPath}");
432
        if ($keepSettings) {
433
            Util::mwMkdir($backupPath);
434
            Processes::mwExec("{$cpPath} -r {$this->moduleDir}/db {$backupPath}/");
435
        }
436
        Processes::mwExec("{$rmPath} -rf {$this->moduleDir}");
437
438
        // Remove assets
439
        // IMG
440
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
441
        $moduleImageCacheDir = "{$imgCacheDir}/{$this->moduleUniqueID}";
442
        if (file_exists($moduleImageCacheDir)){
443
            unlink($moduleImageCacheDir);
444
        }
445
446
        // CSS
447
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
448
        $moduleCSSCacheDir = "{$cssCacheDir}/{$this->moduleUniqueID}";
449
        if (file_exists($moduleCSSCacheDir)){
450
            unlink($moduleCSSCacheDir);
451
        }
452
453
        // JS
454
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
455
        $moduleJSCacheDir = "{$jsCacheDir}/{$this->moduleUniqueID}";
456
        if (file_exists($moduleJSCacheDir)){
457
            unlink($moduleJSCacheDir);
458
        }
459
460
        // Volt
461
        $this->cleanupVoltCache();
462
463
        return true;
464
    }
465
466
    /**
467
     * Returns error messages.
468
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#getmessages
469
     *
470
     * @return array An array of error messages.
471
     */
472
    public function getMessages(): array
473
    {
474
        return $this->messages;
475
    }
476
477
    /**
478
     * Registers the module in the PbxExtensionModules table.
479
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#registernewmodule
480
     *
481
     * @return bool The result of the registration process.
482
     */
483
    public function registerNewModule(): bool
484
    {
485
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
486
        if ( ! $module) {
487
            $module           = new PbxExtensionModules();
488
            $module->name     = $this->translation->_("Breadcrumb{$this->moduleUniqueID}");
489
            $module->disabled = '1';
490
        }
491
        $module->uniqid        = $this->moduleUniqueID;
492
        $module->developer     = $this->developer;
493
        $module->version       = $this->version;
494
        $module->description   = $this->translation->_("SubHeader{$this->moduleUniqueID}");
495
        $module->support_email = $this->support_email;
496
497
        try {
498
            $module->wiki_links = json_encode($this->wiki_links, JSON_THROW_ON_ERROR);
499
        }catch (\JsonException $e){
500
            Util::sysLogMsg(__CLASS__, $e->getMessage());
501
        }
502
503
        return $module->save();
504
    }
505
506
    /**
507
     * Traverses files with model descriptions and creates/alters tables in the system database.
508
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#createsettingstablebymodelsannotations
509
     *
510
     * @return bool The result of the table modification process.
511
     */
512
    public function createSettingsTableByModelsAnnotations(): bool
513
    {
514
515
        // Add new connection for this module after add new Models folder
516
        ModulesDBConnectionsProvider::recreateModulesDBConnections();
517
518
        $results = glob($this->moduleDir . '/Models/*.php', GLOB_NOSORT);
519
        $dbUpgrade = new UpdateDatabase();
520
        foreach ($results as $file) {
521
            $className        = pathinfo($file)['filename'];
522
            $moduleModelClass = "Modules\\{$this->moduleUniqueID}\\Models\\{$className}";
523
            $upgradeResult = $dbUpgrade->createUpdateDbTableByAnnotations($moduleModelClass);
524
            if (!$upgradeResult){
525
                return false;
526
            }
527
528
        }
529
        // Update database connections after upgrade their structure
530
        ModulesDBConnectionsProvider::recreateModulesDBConnections();
531
532
        return true;
533
    }
534
535
    /**
536
     * Adds the module to the sidebar menu.
537
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#addtosidebar
538
     *
539
     * @return bool The result of the addition process.
540
     */
541
    public function addToSidebar(): bool
542
    {
543
        $menuSettingsKey           = "AdditionalMenuItem{$this->moduleUniqueID}";
544
        $menuSettings              = PbxSettings::findFirstByKey($menuSettingsKey);
545
        if ($menuSettings === null) {
546
            $menuSettings      = new PbxSettings();
547
            $menuSettings->key = $menuSettingsKey;
548
        }
549
        $value               = [
550
            'uniqid'        => $this->moduleUniqueID,
551
            'group'         => 'modules',
552
            'iconClass'     => 'puzzle',
553
            'caption'       => "Breadcrumb{$this->moduleUniqueID}",
554
            'showAtSidebar' => true,
555
        ];
556
        $menuSettings->value = json_encode($value);
557
558
        return $menuSettings->save();
559
    }
560
561
    /**
562
     * Deletes volt cache files.
563
     *
564
     * @return void
565
     */
566
    private function cleanupVoltCache():void
567
    {
568
        $cacheDirs = [];
569
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
570
        $rmPath = Util::which('rm');
571
        foreach ($cacheDirs as $cacheDir) {
572
            if (!empty($cacheDir)) {
573
                Processes::mwExec("{$rmPath} -rf {$cacheDir}/*");
574
            }
575
        }
576
    }
577
578
    /**
579
     * Deprecated function to return translated phrases.
580
     *
581
     * @param string $stringId The phrase identifier.
582
     *
583
     * @return string The translated phrase.
584
     * @deprecated
585
     */
586
    public function locString(string $stringId): string
587
    {
588
        Util::sysLogMsg('Util', 'Deprecated call ' . __METHOD__ . ' from ' . static::class, LOG_DEBUG);
589
        return $this->translation->_($stringId);
590
    }
591
}