Passed
Push — develop ( f0c023...abe420 )
by Nikolay
12:06
created

PbxExtensionState::refreshFail2BanRules()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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