Passed
Pull Request — master (#16)
by Nikolay
13:10 queued 02:12
created

PbxExtensionState::enableFirewallSettings()   B

Complexity

Conditions 11
Paths 49

Size

Total Lines 55
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

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