Passed
Push — develop ( 3db452...b7dcac )
by Nikolay
05:45
created

PbxExtensionState   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 400
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 81
eloc 232
c 0
b 0
f 0
dl 0
loc 400
rs 2

6 Methods

Rating   Name   Duplication   Size   Complexity  
F disableModule() 0 109 28
A getMessages() 0 3 1
F enableModule() 0 117 27
B disableFirewallSettings() 0 41 9
B enableFirewallSettings() 0 55 11
A __construct() 0 30 5

How to fix   Complexity   

Complex Class

Complex classes like PbxExtensionState often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PbxExtensionState, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright (C) MIKO LLC - All Rights Reserved
4
 * Unauthorized copying of this file, via any medium is strictly prohibited
5
 * Proprietary and confidential
6
 * Written by Nikolay Beketov, 5 2020
7
 *
8
 */
9
10
namespace MikoPBX\Modules;
11
12
13
use MikoPBX\Common\Models\FirewallRules;
14
use MikoPBX\Common\Models\NetworkFilters;
15
use MikoPBX\Common\Models\PbxExtensionModules;
16
use MikoPBX\Common\Models\PbxSettings;
17
use Phalcon\Di\Injectable;
18
use ReflectionClass;
19
use ReflectionException;
20
21
/**
22
 * @property \MikoPBX\Service\License license
23
 */
24
class PbxExtensionState extends Injectable
25
{
26
    private $messages;
27
    private $lic_feature_id;
28
    private $moduleUniqueID;
29
    private $configClass;
30
    private $modulesRoot;
31
32
33
    public function __construct(string $moduleUniqueID)
34
    {
35
        $this->messages        = [];
36
        $this->moduleUniqueID  = $moduleUniqueID;
37
        $this->modulesRoot           = $this->di->getShared('config')->path('core.modulesDir');
38
        $moduleJson            = "{$this->modulesRoot}/{$this->moduleUniqueID}/module.json";
39
        if (!file_exists($moduleJson)){
40
            $this->messages[] = 'module.json not found for module ' . $this->moduleUniqueID;
41
42
            return;
43
        }
44
        $jsonString            = file_get_contents($moduleJson);
45
        $jsonModuleDescription = json_decode($jsonString, true);
46
        if ( ! is_array($jsonModuleDescription)) {
47
            $this->messages[] = 'module.json parsing error ' . $this->moduleUniqueID;
48
49
            return;
50
        }
51
        if (array_key_exists('lic_feature_id', $jsonModuleDescription)) {
52
            $this->lic_feature_id = $jsonModuleDescription['lic_feature_id'];
53
        } else {
54
            $this->lic_feature_id = 0;
55
        }
56
57
        $class_name      = str_replace('Module', '', $this->moduleUniqueID);
58
        $configClassName = "\\Modules\\{$this->moduleUniqueID}\\Lib\\{$class_name}Conf";
59
        if (class_exists($configClassName)) {
60
            $this->configClass = new $configClassName();
61
        } else {
62
            $this->configClass = null;
63
        }
64
    }
65
66
    /**
67
     * Enable extension module with checking relations
68
     *
69
     */
70
    public function enableModule(): bool
71
    {
72
        if ($this->lic_feature_id > 0) {
73
            // Пробуем захватить фичу c учетом оффлан режима
74
            $result = $this->license->featureAvailable($this->lic_feature_id);
75
            if ($result['success'] === false) {
76
                $this->messages[] = $this->license->translateLicenseErrorMessage($result['error']);
77
78
                return false;
79
            }
80
        }
81
82
        $error = false;
83
        $this->db->begin(true); // Временная транзакция, которая будет отменена после теста включения
84
85
        // Временно включим модуль, чтобы включить все связи и зависимости
86
        $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
87
        if ($module !== null) {
88
            $module->disabled = '0';
89
            $module->save();
90
        }
91
92
        // Если в конфигурационном классе модуля есть функция корректного включения, вызовем ее,
93
        // например модуль умной маршртутизации прописывает себя в маршруты
94
95
        if ($this->configClass !== null
96
            && method_exists($this->configClass, 'onBeforeModuleEnable')
97
            && $this->configClass->onBeforeModuleEnable() === false) {
98
            $messages = $this->configClass->getMessages();
99
            if ( ! empty($messages)) {
100
                $this->messages = $messages;
101
            } else {
102
                $this->messages[] = 'Error on the Module enable function at onBeforeModuleEnable';
103
            }
104
            $this->db->rollback(true); // Откатываем временную транзакцию
105
106
            return false;
107
        }
108
109
        // Проверим нет ли битых ссылок, которые мешают включить модуль
110
        // например удалили сотрудника, а модуль указывает на его extension
111
        //
112
113
114
        $modelsFiles = glob("{$this->modulesRoot}/{$this->moduleUniqueID}/Models/*.php", GLOB_NOSORT);
115
        foreach ($modelsFiles as $file) {
116
            $className        = pathinfo($file)['filename'];
117
            $moduleModelClass = "\\Modules\\{$this->moduleUniqueID}\\Models\\{$className}";
118
119
            if (
120
                ! class_exists($moduleModelClass)
121
                || count(get_class_vars($moduleModelClass)) === 0) {
122
                continue;
123
            }
124
125
            // Test whether this class abstract or not
126
            try {
127
                $reflection = new ReflectionClass($moduleModelClass);
128
                if ($reflection->isAbstract()) {
129
                    continue;
130
                }
131
            } catch (ReflectionException $exception) {
132
                continue;
133
            }
134
            $translator = $this->di->getShared('translation');
135
            if (class_exists($moduleModelClass)) {
136
                $records = $moduleModelClass::find();
137
                foreach ($records as $record) {
138
                    $relations = $record->_modelsManager->getRelations(get_class($record));
139
                    foreach ($relations as $relation) {
140
                        $alias        = $relation->getOption('alias');
141
                        $checkedValue = $record->$alias;
142
                        $foreignKey   = $relation->getOption('foreignKey');
143
                        // В модуле указан заперт на NULL в описании модели,
144
                        // а параметр этот не заполнен в настройках модуля
145
                        // например в модуле маршрутизации, резервный номер
146
                        if ($checkedValue === false
147
                            && array_key_exists('allowNulls', $foreignKey)
148
                            && $foreignKey['allowNulls'] === false
149
                        ) {
150
                            $this->messages[] = $translator->_(
151
                                'mo_ModuleSettingsError',
152
                                [
153
                                    'modulename' => $record->getRepresent(true),
154
                                ]
155
                            );
156
                            $error            = true;
157
                        }
158
                    }
159
                }
160
            }
161
        }
162
        if ( ! $error) {
163
            $this->messages = [];
164
        }
165
        $this->db->rollback(true); // Откатываем временную транзакцию
166
167
        // Если ошибок нет, включаем Firewall и модуль
168
        if ( ! $error && ! $this->enableFirewallSettings()) {
169
            $this->messages[] = 'Error on enable firewall settings';
170
171
            return false;
172
        }
173
        if ( ! $error) {
174
            $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
175
            if ($module !== null) {
176
                $module->disabled = '0';
177
                if (
178
                    $module->save() === true
179
                    && $this->configClass !== null
180
                    && method_exists($this->configClass, 'onBeforeModuleEnable')) {
181
                    $this->configClass->onBeforeModuleEnable();
182
                }
183
            }
184
        }
185
186
        return true;
187
    }
188
189
    /**
190
     * При включении модуля устанавливает настройки Firewall по-умолчаниию
191
     * или по состоянию на момент выключения
192
     *
193
     * @return bool
194
     */
195
    protected function enableFirewallSettings(): bool
196
    {
197
        if ($this->configClass === null
198
            || method_exists($this->configClass, 'getDefaultFirewallRules') === false
199
            || $this->configClass->getDefaultFirewallRules() === []
200
        ) {
201
            return true;
202
        }
203
204
        $this->db->begin(true);
205
        $defaultRules         = $this->configClass->getDefaultFirewallRules();
206
        $previousRuleSettings = PbxSettings::findFirstByKey("{$this->moduleUniqueID}FirewallSettings");
207
        $previousRules        = [];
208
        if ($previousRuleSettings !== null) {
209
            $previousRules = json_decode($previousRuleSettings->value, true);
210
            $previousRuleSettings->delete();
211
        }
212
        $errors   = [];
213
        $networks = NetworkFilters::find();
214
        $key      = strtoupper(key($defaultRules));
215
        $record   = $defaultRules[key($defaultRules)];
216
217
        $oldRules = FirewallRules::findByCategory($key);
218
        if ($oldRules->count() > 0) {
219
            $oldRules->delete();
220
        }
221
222
        foreach ($networks as $network) {
223
            foreach ($record['rules'] as $detailRule) {
224
                $newRule                  = new FirewallRules();
225
                $newRule->networkfilterid = $network->id;
226
                $newRule->protocol        = $detailRule['protocol'];
227
                $newRule->portfrom        = $detailRule['portfrom'];
228
                $newRule->portto          = $detailRule['portto'];
229
                $newRule->category        = $key;
230
                $newRule->action          = $record['action'];
231
                $newRule->description     = $detailRule['name'];
232
                if (array_key_exists($network->id, $previousRules)) {
233
                    $newRule->action = $previousRules[$network->id];
234
                }
235
                if ( ! $newRule->save()) {
236
                    $this->messages[] = $newRule->getMessages();
237
                }
238
            }
239
        }
240
        if (count($errors) > 0) {
241
            $this->messages[] = array_merge($this->messages, $errors);
242
            $this->db->rollback(true);
243
244
            return false;
245
        }
246
247
        $this->db->commit(true);
248
249
        return true;
250
    }
251
252
    /**
253
     * Disable extension module with checking relations
254
     *
255
     */
256
    public function disableModule(): bool
257
    {
258
        $error = false;
259
260
        // Проверим, нет ли настроенных зависимостей у других модулей
261
        // Попробуем удалить все настройки модуля
262
        $this->db->begin(true);
263
264
        if ($this->configClass !== null
265
            && method_exists($this->configClass, 'onBeforeModuleDisable')
266
            && $this->configClass->onBeforeModuleDisable() === false) {
267
            $messages = $this->configClass->getMessages();
268
            if ( ! empty($messages)) {
269
                $this->messages = $messages;
270
            } else {
271
                $this->messages[] = 'Error on the Module enable function at onBeforeModuleDisable';
272
            }
273
            $this->db->rollback(true); // Откатываем временную транзакцию
274
275
            return false;
276
        }
277
278
279
        // Попытаемся удалить текущий модуль, если ошибок не будет, значит можно выклчать
280
        // Например на модуль может ссылаться запись в таблице Extensions, которую надо удалить при отключении
281
        // модуля
282
        $modelsFiles = glob("{$this->modulesRoot}/{$this->moduleUniqueID}/Models/*.php", GLOB_NOSORT);
283
        foreach ($modelsFiles as $file) {
284
            $className        = pathinfo($file)['filename'];
285
            $moduleModelClass = "\\Modules\\{$this->moduleUniqueID}\\Models\\{$className}";
286
287
            if (
288
                ! class_exists($moduleModelClass)
289
                || count(get_class_vars($moduleModelClass)) === 0) {
290
                continue;
291
            }
292
293
            // Test whether this class abstract or not
294
            try {
295
                $reflection = new ReflectionClass($moduleModelClass);
296
                if ($reflection->isAbstract()) {
297
                    continue;
298
                }
299
            } catch (ReflectionException $exception) {
300
                continue;
301
            }
302
            if (class_exists($moduleModelClass)) {
303
                $records = $moduleModelClass::find();
304
                foreach ($records as $record) {
305
                    $mainRelations = $record->_modelsManager->getRelations(get_class($record));
306
                    foreach ($mainRelations as $subRelation) {
307
                        $relatedModel = $subRelation->getReferencedModel();
308
                        $record->_modelsManager->load($relatedModel);
309
                        $relations = $record->_modelsManager->getRelationsBetween(
310
                            $relatedModel,
311
                            get_class($record)
312
                        );
313
                        foreach ($relations as $relation) {
314
                            // Проверим есть ли записи в таблице которая запрещает удаление текущих данных
315
                            $mappedFields             = $relation->getFields();
316
                            $mappedFields             = is_array($mappedFields)
317
                                ? $mappedFields : [$mappedFields];
318
                            $referencedFields         = $relation->getReferencedFields();
319
                            $referencedFields         = is_array($referencedFields)
320
                                ? $referencedFields : [$referencedFields];
321
                            $parameters['conditions'] = '';
322
                            $parameters['bind']       = [];
323
                            foreach ($mappedFields as $index => $mappedField) {
324
                                $parameters['conditions']             .= $index > 0 ? ' OR ' : '';
325
                                $parameters['conditions']             .= $mappedField . '= :field' . $index . ':';
326
                                $bindField                            = $referencedFields[$index];
327
                                $parameters['bind']['field' . $index] = $record->$bindField;
328
                            }
329
                            $relatedRecords = $relatedModel::find($parameters);
330
                            if ( ! $error && ! $relatedRecords->delete()) {
331
                                $error            = true;
332
                                $this->messages[] = $relatedRecords->getMessages();
333
                            }
334
                        }
335
                    }
336
                }
337
            }
338
        }
339
        if ( ! $error) {
340
            $this->messages = [];
341
        }
342
        $this->db->rollback(true); // Откатываем временную транзакцию
343
344
        // Если ошибок нет, выключаем Firewall и модуль
345
        if ( ! $error && ! $this->disableFirewallSettings()) {
346
            $this->messages[] = 'Error on disable firewall settings';
347
348
            return false;
349
        }
350
351
        if ( ! $error) {
352
            $module = PbxExtensionModules::findFirstByUniqid($this->moduleUniqueID);
353
            if ($module !== null) {
354
                $module->disabled = '1';
355
                if (
356
                    $module->save() === true
357
                    && $this->configClass !== null
358
                    && method_exists($this->configClass, 'onBeforeModuleEnable')) {
359
                    $this->configClass->onBeforeModuleDisable();
360
                }
361
            }
362
        }
363
364
        return true;
365
    }
366
367
    /**
368
     * При выключении модуля сбрасывает настройки Firewall
369
     * и сохраняет параметры для будущего включения
370
     *
371
     *
372
     * @return bool
373
     */
374
    protected function disableFirewallSettings(): bool
375
    {
376
        if ($this->configClass === null
377
            || method_exists($this->configClass, 'getDefaultFirewallRules') === false
378
            || $this->configClass->getDefaultFirewallRules() === []
379
        ) {
380
            return true;
381
        }
382
        $errors       = [];
383
        $savedState   = [];
384
        $defaultRules = $this->configClass->getDefaultFirewallRules();
385
        $key          = strtoupper(key($defaultRules));
386
        $currentRules = FirewallRules::findByCategory($key);
387
        foreach ($currentRules as $detailRule) {
388
            $savedState[$detailRule->networkfilterid] = $detailRule->action;
389
        }
390
        $this->db->begin(true);
391
        if ( ! $currentRules->delete()) {
392
            $this->messages[] = $currentRules->getMessages();
393
            return false;
394
        }
395
396
        $previousRuleSettings = PbxSettings::findFirstByKey("{$this->moduleUniqueID}FirewallSettings");
397
        if ($previousRuleSettings === null) {
398
            $previousRuleSettings      = new PbxSettings();
399
            $previousRuleSettings->key = "{$this->moduleUniqueID}FirewallSettings";
400
        }
401
        $previousRuleSettings->value = json_encode($savedState);
402
        if ( ! $previousRuleSettings->save()) {
403
            $this->messages[] = $previousRuleSettings->getMessages();
404
        }
405
        if (count($errors) > 0) {
406
            $this->messages[] = array_merge($this->messages, $errors);
407
            $this->db->rollback(true);
408
409
            return false;
410
        }
411
412
        $this->db->commit(true);
413
414
        return true;
415
    }
416
417
    /**
418
     * Return messages after function or methods execution
419
     * @return array
420
     */
421
    public function getMessages(): array
422
    {
423
        return  $this->messages;
424
    }
425
}