Passed
Push — develop ( 5e850f...f7133b )
by Nikolay
04:00
created

PbxExtensionState::disableModule()   C

Complexity

Conditions 12
Paths 26

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 23
dl 0
loc 39
rs 6.9666
c 2
b 0
f 0
cc 12
nc 26
nop 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
21
22
use MikoPBX\Common\Models\FirewallRules;
23
use MikoPBX\Common\Models\NetworkFilters;
24
use MikoPBX\Common\Models\PbxExtensionModules;
25
use MikoPBX\Common\Models\PbxSettings;
26
use MikoPBX\Common\Providers\ConfigProvider;
27
use MikoPBX\Core\System\Processes;
28
use MikoPBX\Modules\Config\ConfigClass;
29
use Phalcon\Di\Injectable;
30
use ReflectionClass;
31
use Throwable;
32
33
/**
34
 * @property \MikoPBX\Service\License license
35
 *
36
 */
37
class PbxExtensionState extends Injectable
38
{
39
    private array $messages;
40
    private $lic_feature_id;
41
    private string $moduleUniqueID;
42
    private ?ConfigClass $configClass;
43
    private $modulesRoot;
44
45
46
    public function __construct(string $moduleUniqueID)
47
    {
48
        $this->configClass    = null;
49
        $this->messages       = [];
50
        $this->moduleUniqueID = $moduleUniqueID;
51
        $this->modulesRoot    = $this->getDI()->getShared(ConfigProvider::SERVICE_NAME)->path('core.modulesDir');
52
        $moduleJson           = "{$this->modulesRoot}/{$this->moduleUniqueID}/module.json";
53
        if ( ! file_exists($moduleJson)) {
54
            $this->messages[] = 'module.json not found for module ' . $this->moduleUniqueID;
55
56
            return;
57
        }
58
59
        $jsonString            = file_get_contents($moduleJson);
60
        $jsonModuleDescription = json_decode($jsonString, true);
61
        if ( ! is_array($jsonModuleDescription)) {
62
            $this->messages[] = 'module.json parsing error ' . $this->moduleUniqueID;
63
64
            return;
65
        }
66
67
        if (array_key_exists('lic_feature_id', $jsonModuleDescription)) {
68
            $this->lic_feature_id = $jsonModuleDescription['lic_feature_id'];
69
        } else {
70
            $this->lic_feature_id = 0;
71
        }
72
        $this->reloadConfigClass();
73
    }
74
75
    /**
76
     * Recreates module's ClassNameConf class
77
     */
78
    private function reloadConfigClass(): void
79
    {
80
        $class_name      = str_replace('Module', '', $this->moduleUniqueID);
81
        $configClassName = "\\Modules\\{$this->moduleUniqueID}\\Lib\\{$class_name}Conf";
82
        if (class_exists($configClassName)) {
83
            $this->configClass = new $configClassName();
84
        } else {
85
            $this->configClass = null;
86
        }
87
    }
88
89
    /**
90
     * Enables extension module with checking relations
91
     *
92
     */
93
    public function enableModule(): bool
94
    {
95
        if ($this->lic_feature_id > 0) {
96
            // Try to capture feature if it set
97
            $result = $this->license->featureAvailable($this->lic_feature_id);
98
            if ($result['success'] === false) {
99
                $this->messages[] = $this->license->translateLicenseErrorMessage($result['error']);
100
101
                return false;
102
            }
103
        }
104
        $success = $this->makeBeforeEnableTest();
105
        if ( ! $success) {
106
            return false;
107
        }
108
109
        // Если ошибок нет, включаем Firewall и модуль
110
        if ( ! $this->enableFirewallSettings()) {
111
            $this->messages[] = 'Error on enable firewall settings';
112
113
            return false;
114
        }
115
        if ($this->configClass !== null
116
            && method_exists($this->configClass, ConfigClass::ON_BEFORE_MODULE_ENABLE)) {
117
            call_user_func([$this->configClass, ConfigClass::ON_BEFORE_MODULE_ENABLE]);
118
        }
119
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
120
        if ($module !== null) {
121
            $module->disabled = '0';
122
            $module->save();
123
        }
124
        if ($this->configClass !== null
125
            && method_exists($this->configClass, 'getMessages')) {
126
            $this->messages = array_merge($this->messages, $this->configClass->getMessages());
127
        }
128
129
        return true;
130
    }
131
132
    /**
133
     * On enable module this method restores previous firewall settings or sets default state.
134
     *
135
     * @return bool
136
     */
137
    protected function enableFirewallSettings(): bool
138
    {
139
        if ($this->configClass === null
140
            || method_exists($this->configClass, ConfigClass::GET_DEFAULT_FIREWALL_RULES) === false
141
            || call_user_func([$this->configClass, ConfigClass::GET_DEFAULT_FIREWALL_RULES]) === []
142
        ) {
143
            return true;
144
        }
145
146
        $this->db->begin(true);
147
        $defaultRules         = call_user_func([$this->configClass, ConfigClass::GET_DEFAULT_FIREWALL_RULES]);
148
        $previousRuleSettings = PbxSettings::findFirstByKey("{$this->moduleUniqueID}FirewallSettings");
149
        $previousRules        = [];
150
        if ($previousRuleSettings !== null) {
151
            $previousRules = json_decode($previousRuleSettings->value, true);
152
            $previousRuleSettings->delete();
153
        }
154
        $errors   = [];
155
        $networks = NetworkFilters::find();
156
        $key      = strtoupper(key($defaultRules));
157
        $record   = $defaultRules[key($defaultRules)];
158
159
        $oldRules = FirewallRules::findByCategory($key);
160
        if ($oldRules->count() > 0) {
161
            $oldRules->delete();
162
        }
163
164
        foreach ($networks as $network) {
165
            foreach ($record['rules'] as $detailRule) {
166
                $newRule                  = new FirewallRules();
167
                $newRule->networkfilterid = $network->id;
168
                $newRule->protocol        = $detailRule['protocol'];
169
                $newRule->portfrom        = $detailRule['portfrom'];
170
                $newRule->portto          = $detailRule['portto'];
171
                $newRule->category        = $key;
172
                $newRule->action          = $record['action'];
173
                $newRule->portFromKey     = $detailRule['portFromKey'];
174
                $newRule->portToKey       = $detailRule['portToKey'];
175
                $newRule->description     = $detailRule['name'];
176
177
                if (array_key_exists($network->id, $previousRules)) {
178
                    $newRule->action = $previousRules[$network->id];
179
                }
180
                if ( ! $newRule->save()) {
181
                    $errors[] = $newRule->getMessages();
182
                }
183
            }
184
        }
185
        if (count($errors) > 0) {
186
            $this->messages[] = array_merge($this->messages, $errors);
187
            $this->db->rollback(true);
188
189
            return false;
190
        }
191
192
        $this->db->commit(true);
193
194
        return true;
195
    }
196
197
    /**
198
     * Disables extension module with checking relations
199
     *
200
     */
201
    public function disableModule(): bool
202
    {
203
        $success = $this->makeBeforeDisableTest();
204
        if ( ! $success) {
205
            return false;
206
        }
207
        // Если ошибок нет, выключаем Firewall и модуль
208
        if ( ! $this->disableFirewallSettings()) {
209
            $this->messages[] = 'Error on disable firewall settings';
210
211
            return false;
212
        }
213
        if ($this->configClass !== null
214
            && method_exists($this->configClass, ConfigClass::ON_BEFORE_MODULE_DISABLE)) {
215
            call_user_func([$this->configClass, ConfigClass::ON_BEFORE_MODULE_DISABLE]);
216
        }
217
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
218
        if ($module !== null) {
219
            $module->disabled = '1';
220
            $module->save();
221
        }
222
223
        if ($this->configClass !== null
224
            && method_exists($this->configClass, 'getMessages')) {
225
            $this->messages = array_merge($this->messages, $this->configClass->getMessages());
226
        }
227
228
        // Kill module workers
229
        if ($this->configClass !== null
230
            && method_exists($this->configClass, ConfigClass::GET_MODULE_WORKERS)) {
231
            $workersToKill = call_user_func([$this->configClass, ConfigClass::GET_MODULE_WORKERS]);
232
            if (is_array($workersToKill)) {
233
                foreach ($workersToKill as $moduleWorker) {
234
                    Processes::killByName($moduleWorker['worker']);
235
                }
236
            }
237
        }
238
239
        return true;
240
    }
241
242
    /**
243
     * Makes before disable test to check dependency
244
     *
245
     * @return bool
246
     */
247
    private function makeBeforeDisableTest(): bool
248
    {
249
        // Проверим, нет ли настроенных зависимостей у других модулей
250
        // Попробуем удалить все настройки модуля
251
        $this->db->begin(true);
252
        $success = true;
253
254
        if ($this->configClass !== null
255
            && method_exists($this->configClass, ConfigClass::ON_BEFORE_MODULE_DISABLE)
256
            && call_user_func([$this->configClass, ConfigClass::ON_BEFORE_MODULE_DISABLE]) === false) {
257
            $messages = $this->configClass->getMessages();
258
            if ( ! empty($messages)) {
259
                $this->messages = $messages;
260
            } else {
261
                $this->messages[] = 'Error on the Module enable function at onBeforeModuleDisable';
262
            }
263
            $this->db->rollback(true); // Откатываем временную транзакцию
264
265
            return false;
266
        }
267
268
        // Попытаемся удалить текущий модуль, если ошибок не будет, значит можно выклчать
269
        // Например на модуль может ссылаться запись в таблице Extensions, которую надо удалить при отключении
270
        // модуля
271
        $modelsFiles = glob("{$this->modulesRoot}/{$this->moduleUniqueID}/Models/*.php", GLOB_NOSORT);
272
        foreach ($modelsFiles as $file) {
273
            $className        = pathinfo($file)['filename'];
274
            $moduleModelClass = "\\Modules\\{$this->moduleUniqueID}\\Models\\{$className}";
275
            try {
276
                if ( ! class_exists($moduleModelClass)) {
277
                    continue;
278
                }
279
                $reflection = new ReflectionClass($moduleModelClass);
280
                if ($reflection->isAbstract()) {
281
                    continue;
282
                }
283
                if (count($reflection->getProperties()) === 0) {
284
                    continue;
285
                }
286
                $records = $moduleModelClass::find();
287
                foreach ($records as $record) {
288
                    if ( ! $record->beforeDelete()) {
289
                        foreach ($record->getMessages() as $message) {
290
                            $this->messages[] = $message->getMessage();
291
                        }
292
                        $success = false;
293
                    }
294
                }
295
            } catch (Throwable $exception) {
296
                $this->messages[] = $exception->getMessage();
297
                $success          = false;
298
            }
299
        }
300
        if ($success) {
301
            $this->messages = [];
302
        }
303
304
        // Откатываем временную транзакцию
305
        $this->db->rollback(true);
306
307
        return $success;
308
    }
309
310
    /**
311
     * Saves firewall state before disable module
312
     *
313
     * @return bool
314
     */
315
    protected function disableFirewallSettings(): bool
316
    {
317
        if ($this->configClass === null
318
            || method_exists($this->configClass, ConfigClass::GET_DEFAULT_FIREWALL_RULES) === false
319
            || call_user_func([$this->configClass, ConfigClass::GET_DEFAULT_FIREWALL_RULES]) === []
320
        ) {
321
            return true;
322
        }
323
        $errors       = [];
324
        $savedState   = [];
325
        $defaultRules = call_user_func([$this->configClass, ConfigClass::GET_DEFAULT_FIREWALL_RULES]);
326
        $key          = strtoupper(key($defaultRules));
327
        $currentRules = FirewallRules::findByCategory($key);
328
        foreach ($currentRules as $detailRule) {
329
            $savedState[$detailRule->networkfilterid] = $detailRule->action;
330
        }
331
        $this->db->begin(true);
332
        if ( ! $currentRules->delete()) {
333
            $this->messages[] = $currentRules->getMessages();
334
335
            return false;
336
        }
337
338
        $previousRuleSettings = PbxSettings::findFirstByKey("{$this->moduleUniqueID}FirewallSettings");
339
        if ($previousRuleSettings === null) {
340
            $previousRuleSettings      = new PbxSettings();
341
            $previousRuleSettings->key = "{$this->moduleUniqueID}FirewallSettings";
342
        }
343
        $previousRuleSettings->value = json_encode($savedState);
344
        if ( ! $previousRuleSettings->save()) {
345
            $errors[] = $previousRuleSettings->getMessages();
346
        }
347
        if (count($errors) > 0) {
348
            $this->messages[] = array_merge($this->messages, $errors);
349
            $this->db->rollback(true);
350
351
            return false;
352
        }
353
354
        $this->db->commit(true);
355
356
        return true;
357
    }
358
359
    /**
360
     * Returns messages after function or methods execution
361
     *
362
     * @return array
363
     */
364
    public function getMessages(): array
365
    {
366
        return $this->messages;
367
    }
368
369
    /**
370
     * Makes before enable test to check dependency
371
     *
372
     * @return bool
373
     */
374
    private function makeBeforeEnableTest(): bool
375
    {
376
        $success = true;
377
        // Temporary transaction, we will rollback it after checks
378
        $this->db->begin(true);
379
380
        // Временно включим модуль, чтобы включить все связи и зависимости
381
        // Temporary disable module and disable all links
382
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
383
        if ($module !== null) {
384
            $module->disabled = '0';
385
            $module->save();
386
        }
387
388
        // Если в конфигурационном классе модуля есть функция корректного включения, вызовем ее,
389
        // например модуль умной маршртутизации прописывает себя в маршруты
390
        //
391
        // If module config has special function before enable, we will execute it
392
393
        if ($this->configClass !== null
394
            && method_exists($this->configClass, ConfigClass::ON_BEFORE_MODULE_ENABLE)
395
            && call_user_func([$this->configClass, ConfigClass::ON_BEFORE_MODULE_ENABLE]) === false) {
396
            $messages = $this->configClass->getMessages();
397
            if ( ! empty($messages)) {
398
                $this->messages = $messages;
399
            } else {
400
                $this->messages[] = 'Error on the enableModule function at onBeforeModuleEnable';
401
            }
402
            $this->db->rollback(true); // Откатываем временную транзакцию
403
404
            return false;
405
        }
406
407
        // Проверим нет ли битых ссылок, которые мешают включить модуль
408
        // например удалили сотрудника, а модуль указывает на его extension
409
        //
410
        $modelsFiles = glob("{$this->modulesRoot}/{$this->moduleUniqueID}/Models/*.php", GLOB_NOSORT);
411
        $translator  = $this->di->getShared('translation');
412
        foreach ($modelsFiles as $file) {
413
            $className        = pathinfo($file)['filename'];
414
            $moduleModelClass = "\\Modules\\{$this->moduleUniqueID}\\Models\\{$className}";
415
416
            try {
417
                if ( ! class_exists($moduleModelClass)) {
418
                    continue;
419
                }
420
                $reflection = new ReflectionClass($moduleModelClass);
421
                if ($reflection->isAbstract()) {
422
                    continue;
423
                }
424
                if (count($reflection->getProperties()) === 0) {
425
                    continue;
426
                }
427
                $records = $moduleModelClass::find();
428
                foreach ($records as $record) {
429
                    $relations = $record->_modelsManager->getRelations(get_class($record));
430
                    foreach ($relations as $relation) {
431
                        $alias        = $relation->getOption('alias');
432
                        $checkedValue = $record->$alias;
433
                        $foreignKey   = $relation->getOption('foreignKey');
434
                        // В модуле указан заперт на NULL в описании модели,
435
                        // а параметр этот не заполнен в настройках модуля
436
                        // например в модуле маршрутизации, резервный номер
437
                        if ($checkedValue === false
438
                            && array_key_exists('allowNulls', $foreignKey)
439
                            && $foreignKey['allowNulls'] === false
440
                        ) {
441
                            $this->messages[] = $translator->_(
442
                                'mo_ModuleSettingsError',
443
                                [
444
                                    'modulename' => $record->getRepresent(true),
445
                                ]
446
                            );
447
                            $success          = false;
448
                        }
449
                    }
450
                }
451
            } catch (Throwable $exception) {
452
                $this->messages[] = $exception->getMessage();
453
                $success          = false;
454
            }
455
        }
456
        if ($success) {
457
            $this->messages = [];
458
        }
459
460
        // Откатываем временную транзакцию
461
        $this->db->rollback(true);
462
463
        return $success;
464
    }
465
466
}