Passed
Push — develop ( b7c00c...da1f69 )
by Nikolay
08:07 queued 01:43
created

PbxExtensionState::makeBeforeEnableTest()   C

Complexity

Conditions 17
Paths 84

Size

Total Lines 90
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 50
c 2
b 0
f 0
dl 0
loc 90
rs 5.2166
cc 17
nc 84
nop 0

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