Passed
Push — develop ( 8ba7ca...a77f80 )
by Nikolay
07:08 queued 02:47
created

PbxExtensionSetupBase::locString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
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\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
/**
36
 * Base class for module setup.
37
 * Common procedures for module installation and removing external modules.
38
 *
39
 * @property \MikoPBX\Common\Providers\LicenseProvider license
40
 * @property \MikoPBX\Common\Providers\TranslationProvider translation
41
 *
42
 *  @package MikoPBX\Modules\Setup
43
 */
44
abstract class PbxExtensionSetupBase extends Injectable implements PbxExtensionSetupInterface
45
{
46
    /**
47
     * Module unique identify from the module.json
48
     * @var string
49
     */
50
    protected string $moduleUniqueID;
51
52
    /**
53
     * Module version from the module.json
54
     * @var string|null
55
     */
56
    protected $version;
57
58
    /**
59
     * Minimal required version PBX from the module.json
60
     * @var string|null
61
     */
62
    protected $min_pbx_version;
63
64
    /**
65
     * Module developer name  from the module.json
66
     * @var string|null
67
     */
68
    protected $developer;
69
70
    /**
71
     * Module developer's email from module.json
72
     * @var string|null
73
     */
74
    protected $support_email;
75
76
    /**
77
     * PBX core general database
78
     * @var \Phalcon\Db\Adapter\Pdo\Sqlite|null
79
     */
80
    protected $db;
81
82
    /**
83
     * Folder with module files
84
     * @var string
85
     */
86
    protected string $moduleDir;
87
88
    /**
89
     * Phalcon config service
90
     * @var \Phalcon\Config|null
91
     */
92
    protected $config;
93
94
    /**
95
     * Error and verbose messages
96
     * @var array
97
     */
98
    protected array $messages;
99
100
    /**
101
     * License worker
102
     * @var \MikoPBX\Service\License|null
103
     */
104
    protected $license;
105
106
    /**
107
     * Trial product version identify number from the module.json
108
     * @var int|null
109
     */
110
    public $lic_product_id;
111
112
    /**
113
     * License feature identify number from the module.json
114
     * @var int|null
115
     */
116
    public $lic_feature_id;
117
118
    /**
119
     * Array of wiki links
120
     * @var array
121
     */
122
    public array $wiki_links = [];
123
124
    /**
125
     * Constructor for the module class.
126
     *
127
     * @param string $moduleUniqueID The unique identifier of the module.
128
     */
129
    public function __construct(string $moduleUniqueID)
130
    {
131
        // Set the module unique ID
132
        $this->moduleUniqueID = $moduleUniqueID;
133
134
        // Initialize properties
135
        $this->messages = [];
136
        $this->db      = $this->getDI()->getShared('db');
137
        $this->config  = $this->getDI()->getShared('config');
138
        $this->license =  $this->getDI()->getShared('license');
139
        $this->moduleDir = $this->config->path('core.modulesDir') . '/' . $this->moduleUniqueID;
140
141
        // Load module settings from module.json file
142
        $settings_file = "{$this->moduleDir}/module.json";
143
        if (file_exists($settings_file)) {
144
            $module_settings = json_decode(file_get_contents($settings_file), true);
145
            if ($module_settings) {
146
                // Extract module settings
147
                $this->version         = $module_settings['version'];
148
                $this->min_pbx_version = $module_settings['min_pbx_version'];
149
                $this->developer       = $module_settings['developer'];
150
                $this->support_email   = $module_settings['support_email'];
151
152
                // Check if license product ID is defined in module settings
153
                if (array_key_exists('lic_product_id', $module_settings)) {
154
                    $this->lic_product_id = $module_settings['lic_product_id'];
155
                } else {
156
                    $this->lic_product_id = 0;
157
                }
158
159
                // Check if license feature ID is defined in module settings
160
                if (array_key_exists('lic_feature_id', $module_settings)) {
161
                    $this->lic_feature_id = $module_settings['lic_feature_id'];
162
                } else {
163
                    $this->lic_feature_id = 0;
164
                }
165
166
                // Extract wiki links from module settings
167
                $wiki_links = $module_settings['wiki_links']??[];
168
                if(is_array($wiki_links)){
169
                    $this->wiki_links = $wiki_links;
170
                }
171
            } else {
172
                $this->messages[] = 'Error on decode module.json';
173
            }
174
        }
175
176
        // Reset messages array
177
        $this->messages  = [];
178
    }
179
180
    /**
181
     * Performs the main module installation process called by PBXCoreRest after unzipping module files.
182
     * It invokes private functions and sets up error messages in the message variable.
183
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#installmodule
184
     *
185
     * @return bool The result of the installation process.
186
     */
187
    public function installModule(): bool
188
    {
189
        $result = true;
190
        try {
191
            if ( ! $this->activateLicense()) {
192
                $this->messages[] = 'License activate error';
193
                $result           = false;
194
            }
195
            if ( ! $this->installFiles()) {
196
                $this->messages[] = ' installFiles error';
197
                $result           = false;
198
            }
199
            if ( ! $this->installDB()) {
200
                $this->messages[] = ' installDB error';
201
                $result           = false;
202
            }
203
            if ( ! $this->fixFilesRights()) {
204
                $this->messages[] = ' Apply files rights error';
205
                $result           = false;
206
            }
207
        } catch (Throwable $exception) {
208
            $result         = false;
209
            $this->messages[] = $exception->getMessage();
210
        }
211
212
        return $result;
213
    }
214
215
    /**
216
     * Activates the license, applicable only for commercial modules.
217
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#activatelicense
218
     *
219
     * @return bool The result of the license activation.
220
     */
221
    public function activateLicense(): bool
222
    {
223
        if($this->lic_product_id>0) {
224
            $lic = PbxSettings::getValueByKey('PBXLicense');
225
            if (empty($lic)) {
226
                $this->messages[] = 'License key not found...';
227
                return false;
228
            }
229
230
            // Get trial license
231
            $this->license->addtrial($this->lic_product_id);
232
        }
233
        return true;
234
    }
235
236
    /**
237
     * Copies files, creates folders, and symlinks for the module and restores previous backup settings.
238
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#installfiles
239
     *
240
     * @return bool The result of the installation process.
241
     */
242
    public function installFiles(): bool
243
    {
244
        // Create cache links for JS, CSS, IMG folders
245
        PbxExtensionUtils::createAssetsSymlinks($this->moduleUniqueID);
246
247
        // Create cache links for agi-bin scripts
248
        PbxExtensionUtils::createAgiBinSymlinks($this->moduleUniqueID);
249
250
        // Restore database settings
251
        $modulesDir          = $this->config->path('core.modulesDir');
0 ignored issues
show
Bug introduced by
The method path() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

251
        /** @scrutinizer ignore-call */ 
252
        $modulesDir          = $this->config->path('core.modulesDir');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
252
        $backupPath = "{$modulesDir}/Backup/{$this->moduleUniqueID}";
253
        if (is_dir($backupPath)) {
254
            $cpPath = Util::which('cp');
255
            Processes::mwExec("{$cpPath} -r {$backupPath}/db/* {$this->moduleDir}/db/");
256
        }
257
258
        // Volt
259
        $this->cleanupCache();
260
261
        return true;
262
    }
263
264
    /**
265
     * Sets up ownerships and folder rights.
266
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#fixfilesrights
267
     *
268
     * @return bool The result of the fixing process.
269
     */
270
    public function fixFilesRights(): bool
271
    {
272
        // Add regular www rights
273
        Util::addRegularWWWRights($this->moduleDir);
274
        $dirs = [
275
            "{$this->moduleDir}/agi-bin",
276
            "{$this->moduleDir}/bin"
277
        ];
278
        foreach ($dirs as $dir) {
279
            if(file_exists($dir) && is_dir($dir)){
280
                // Add executable right to module's binary
281
                Util::addExecutableRights($dir);
282
            }
283
        }
284
285
        return true;
286
    }
287
288
    /**
289
     * Creates the database structure according to models' annotations.
290
     * If necessary, it fills some default settings and changes the sidebar menu item representation for this module.
291
     * After installation, it registers the module on the PbxExtensionModules model.
292
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#fixfilesrights
293
     *
294
     * @return bool The result of the installation process.
295
     */
296
    public function installDB(): bool
297
    {
298
        $result = $this->createSettingsTableByModelsAnnotations();
299
300
        if ($result) {
301
            $result = $this->registerNewModule();
302
        }
303
304
        if ($result) {
305
            $result = $this->addToSidebar();
306
        }
307
        return $result;
308
    }
309
310
    /**
311
     * Performs the main module uninstallation process called by MikoPBX REST API to delete any module.
312
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#uninstallmodule
313
     *
314
     * @param bool $keepSettings If set to true, the function saves the module database.
315
     *
316
     * @return bool The result of the uninstallation process.
317
     */
318
    public function uninstallModule(bool $keepSettings = false): bool
319
    {
320
        $result = true;
321
        try {
322
            if ( ! $this->unInstallDB($keepSettings)) {
323
                $this->messages[] = ' unInstallDB error';
324
                $result           = false;
325
            }
326
            if ($result && ! $this->unInstallFiles($keepSettings)) {
327
                $this->messages[] = ' unInstallFiles error';
328
                $result           = false;
329
            }
330
        } catch (Throwable $exception) {
331
            $result         = false;
332
            $this->messages[] = $exception->getMessage();
333
        }
334
335
        return $result;
336
    }
337
338
    /**
339
     * Deletes some settings from the database and links to the module.
340
     * If $keepSettings is set to true, it copies the database file to the Backup folder.
341
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#uninstalldb
342
     *
343
     * @param bool $keepSettings If set to true, the module database is saved.
344
     *
345
     * @return bool The result of the uninstallation process.
346
     */
347
    public function unInstallDB(bool $keepSettings = false): bool
348
    {
349
        return $this->unregisterModule();
350
    }
351
352
    /**
353
     * Deletes records from the PbxExtensionModules table.
354
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#unregistermodule
355
     *
356
     * @return bool The result of the unregistration process.
357
     */
358
    public function unregisterModule(): bool
359
    {
360
        $result = true;
361
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
362
        if ($module !== null) {
363
            $result = $module->delete();
364
        }
365
366
        return $result;
367
    }
368
369
    /**
370
     * Deletes the module files, folders, and symlinks.
371
     * If $keepSettings is set to true, it copies the database file to the Backup folder.
372
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#uninstallfiles
373
     *
374
     * @param bool $keepSettings If set to true, the module database is saved.
375
     *
376
     * @return bool The result of the deletion process.
377
     */
378
    public function unInstallFiles(bool $keepSettings = false):bool
379
    {
380
        $cpPath = Util::which('cp');
381
        $rmPath = Util::which('rm');
382
        $modulesDir          = $this->config->path('core.modulesDir');
383
        $backupPath = "{$modulesDir}/Backup/{$this->moduleUniqueID}";
384
        Processes::mwExec("{$rmPath} -rf {$backupPath}");
385
        if ($keepSettings) {
386
            Util::mwMkdir($backupPath);
387
            Processes::mwExec("{$cpPath} -r {$this->moduleDir}/db {$backupPath}/");
388
        }
389
        Processes::mwExec("{$rmPath} -rf {$this->moduleDir}");
390
391
        // Remove assets
392
        // IMG
393
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
394
        $moduleImageCacheDir = "{$imgCacheDir}/{$this->moduleUniqueID}";
395
        if (file_exists($moduleImageCacheDir)){
396
            unlink($moduleImageCacheDir);
397
        }
398
399
        // CSS
400
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
401
        $moduleCSSCacheDir = "{$cssCacheDir}/{$this->moduleUniqueID}";
402
        if (file_exists($moduleCSSCacheDir)){
403
            unlink($moduleCSSCacheDir);
404
        }
405
406
        // JS
407
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
408
        $moduleJSCacheDir = "{$jsCacheDir}/{$this->moduleUniqueID}";
409
        if (file_exists($moduleJSCacheDir)){
410
            unlink($moduleJSCacheDir);
411
        }
412
413
        // Volt
414
        $this->cleanupCache();
415
416
        return true;
417
    }
418
419
    /**
420
     * Returns error messages.
421
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#getmessages
422
     *
423
     * @return array An array of error messages.
424
     */
425
    public function getMessages(): array
426
    {
427
        return $this->messages;
428
    }
429
430
    /**
431
     * Registers the module in the PbxExtensionModules table.
432
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#registernewmodule
433
     *
434
     * @return bool The result of the registration process.
435
     */
436
    public function registerNewModule(): bool
437
    {
438
        // Check the compatibility of the PBX version and the module.
439
        $currentVersionPBX = PbxSettings::getValueByKey('PBXVersion');
440
        $currentVersionPBX = str_replace('-dev', '', $currentVersionPBX);
441
        if (version_compare($currentVersionPBX, $this->min_pbx_version) < 0) {
0 ignored issues
show
Bug introduced by
It seems like $this->min_pbx_version can also be of type null; however, parameter $version2 of version_compare() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

441
        if (version_compare($currentVersionPBX, /** @scrutinizer ignore-type */ $this->min_pbx_version) < 0) {
Loading history...
442
            $this->messages[] = "Module depends minimum PBX ver $this->min_pbx_version";
443
444
            return false;
445
        }
446
447
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
448
        if ( ! $module) {
449
            $module           = new PbxExtensionModules();
450
            $module->name     = $this->translation->_("Breadcrumb{$this->moduleUniqueID}");
451
            $module->disabled = '1';
452
        }
453
        $module->uniqid        = $this->moduleUniqueID;
454
        $module->developer     = $this->developer;
455
        $module->version       = $this->version;
456
        $module->description   = $this->translation->_("SubHeader{$this->moduleUniqueID}");
457
        $module->support_email = $this->support_email;
458
459
        try {
460
            $module->wiki_links = json_encode($this->wiki_links, JSON_THROW_ON_ERROR);
461
        }catch (\JsonException $e){
462
            Util::sysLogMsg(__CLASS__, $e->getMessage());
463
        }
464
465
        return $module->save();
466
    }
467
468
    /**
469
     * Traverses files with model descriptions and creates/alters tables in the system database.
470
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#createsettingstablebymodelsannotations
471
     *
472
     * @return bool The result of the table modification process.
473
     */
474
    public function createSettingsTableByModelsAnnotations(): bool
475
    {
476
477
        // Add new connection for this module after add new Models folder
478
        ModulesDBConnectionsProvider::recreateModulesDBConnections();
479
480
        $results = glob($this->moduleDir . '/Models/*.php', GLOB_NOSORT);
481
        $dbUpgrade = new UpdateDatabase();
482
        foreach ($results as $file) {
483
            $className        = pathinfo($file)['filename'];
484
            $moduleModelClass = "\\Modules\\{$this->moduleUniqueID}\\Models\\{$className}";
485
            $upgradeResult = $dbUpgrade->createUpdateDbTableByAnnotations($moduleModelClass);
486
            if (!$upgradeResult){
487
                return false;
488
            }
489
490
        }
491
        // Update database connections after upgrade their structure
492
        ModulesDBConnectionsProvider::recreateModulesDBConnections();
493
494
        return true;
495
    }
496
497
    /**
498
     * Adds the module to the sidebar menu.
499
     * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-installer#addtosidebar
500
     *
501
     * @return bool The result of the addition process.
502
     */
503
    public function addToSidebar(): bool
504
    {
505
        $menuSettingsKey           = "AdditionalMenuItem{$this->moduleUniqueID}";
506
        $unCamelizedControllerName = Text::uncamelize($this->moduleUniqueID, '-');
507
        $menuSettings              = PbxSettings::findFirstByKey($menuSettingsKey);
508
        if ($menuSettings === null) {
509
            $menuSettings      = new PbxSettings();
510
            $menuSettings->key = $menuSettingsKey;
511
        }
512
        $value               = [
513
            'uniqid'        => $this->moduleUniqueID,
514
            'href'          => "/admin-cabinet/{$unCamelizedControllerName}", //TODO:: проверить, кажется это уже можно удалить
515
            'group'         => 'modules',
516
            'iconClass'     => 'puzzle',
517
            'caption'       => "Breadcrumb{$this->moduleUniqueID}",
518
            'showAtSidebar' => true,
519
        ];
520
        $menuSettings->value = json_encode($value);
521
522
        return $menuSettings->save();
523
    }
524
525
    /**
526
     * Deletes old cache files.
527
     *
528
     * @return void
529
     */
530
    private function cleanupCache():void
531
    {
532
        $cacheDirs = [];
533
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
534
        $rmPath = Util::which('rm');
535
        foreach ($cacheDirs as $cacheDir) {
536
            if (!empty($cacheDir)) {
537
                Processes::mwExec("{$rmPath} -rf {$cacheDir}/*");
538
            }
539
        }
540
    }
541
}