Passed
Push — develop ( 9a0be0...5a77ad )
by Портнов
04:31
created

IncomingContexts::defaultRouteOnly()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 2
rs 10
cc 3
nc 3
nop 1
1
<?php
2
3
4
namespace MikoPBX\Core\Asterisk\Configs\Generators\Extensions;
5
6
7
use MikoPBX\Common\Models\IncomingRoutingTable;
8
use MikoPBX\Common\Providers\PBXConfModulesProvider;
9
use MikoPBX\Core\Asterisk\Configs\ConferenceConf;
10
use MikoPBX\Core\Asterisk\Configs\ExtensionsConf;
11
use MikoPBX\Modules\Config\ConfigClass;
12
use Phalcon\Di;
13
14
class IncomingContexts extends ConfigClass{
15
16
17
    public        $provider;
18
    public        $login;
19
    public string $uniqId;
20
21
    private array $dialplan = [];
22
    private array $rout_data_dial = [];
23
    private array $additionalModules = [];
24
    private array $confExtensions = [];
25
    private array $routes = [];
26
    private string $lang;
27
    private bool $need_def_rout;
28
29
    /**
30
     * Генератор входящих контекстов.
31
     *
32
     * @param string | array $provider
33
     * @param string | array $login
34
     * @param string         $uniqId
35
     *
36
     * @return string
37
     */
38
    public static function generate($provider, $login = '', $uniqId = ''): string
39
    {
40
        $generator = new self();
41
        $generator->provider = $provider;
42
        $generator->login    = $login;
43
        $generator->uniqId   = $uniqId;
44
        $generator->getSettings();
45
        return $generator->makeDialplan();
46
    }
47
48
    public function getSettings(): void
49
    {
50
        $di = Di::getDefault();
51
        if ($di !== null) {
52
            $this->additionalModules = $di->getShared(PBXConfModulesProvider::SERVICE_NAME);
53
        }
54
        $this->confExtensions   = ConferenceConf::getConferenceExtensions();
55
        $this->routes           = $this->getRoutes();
56
        $this->lang             = str_replace('_', '-', $this->generalSettings['PBXLanguage']);
57
        $this->need_def_rout    = $this->checkNeedDefRout();
58
59
    }
60
61
    public function makeDialplan():string{
62
        foreach ($this->routes as $rout) {
63
            $this->generateRouteDialplan($rout);
64
            $this->generateDialActions($rout);
65
        }
66
        $this->multiplyExtensionsInDialplan();
67
        $this->trimDialPlan();
68
        return $this->createSummaryDialplan();
69
    }
70
71
    /**
72
     * Генерация Первой части (заголовка) входящего контекста.
73
     * @param array $rout
74
     */
75
    private function generateRouteDialplan(array $rout): void{
76
        $number      = trim($rout['number']);
77
        $rout_number = ($number === '') ? 'X!' : $number;
78
        $rout_data   = &$this->dialplan[$rout_number];
79
        if (!empty($rout_data)) {
80
            return;
81
        }
82
        $ext_prefix = ('none' === $this->provider) ? '' : '_';
83
84
        $rout_data .= "exten => {$ext_prefix}{$rout_number},1,NoOp(--- Incoming call ---)\n\t";
85
        $rout_data .= 'same => n,Set(CHANNEL(language)=' . $this->lang . ')' . "\n\t";
86
        $rout_data .= 'same => n,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . "\n\t";
87
        $rout_data .= 'same => n,Set(__FROM_DID=${EXTEN})' . "\n\t";
88
        $rout_data .= 'same => n,Set(__FROM_CHAN=${CHANNEL})' . "\n\t";
89
        // Установка имени пира.
90
        $rout_data .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" != "Local"]?Gosub(set_from_peer,s,1))' . "\n\t";
91
        $rout_data .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local"]?Set(__FROM_PEER=${CALLERID(num)}))' . "\n\t";
92
        $rout_data .= 'same => n,Gosub(add-trim-prefix-clid,${EXTEN},1)' . "\n\t";
93
94
        $rout_data .= $this->generateRouteDialplanBeforeDialModules($rout_number);
95
        // Описываем возможность прыжка в пользовательский sub контекст.
96
        $rout_data .= " \n\t" . 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)';
97
98
        if (!empty($rout['extension'])) {
99
            $rout_data = rtrim($rout_data);
100
        }
101
    }
102
103
    /**
104
     * Генерация включений dialplan из дополонительных модулей перед Dial.
105
     * @param $rout_number
106
     * @return string
107
     */
108
    private function generateRouteDialplanBeforeDialModules($rout_number):string
109
    {
110
        $rout_data = '';
111
        foreach ($this->additionalModules as $appClass) {
112
            $addition = $appClass->generateIncomingRoutBeforeDial($rout_number);
113
            if (!empty($addition)) {
114
                $rout_data .= $appClass->confBlockWithComments($addition);
115
            }
116
        }
117
        return $rout_data;
118
    }
119
120
    /**
121
     * Формирует в Dialplan команды-действия goto dial.
122
     * @param        $rout
123
     * @return void
124
     */
125
    private function generateDialActions($rout): void{
126
        if (empty($rout['extension'])) {
127
            return;
128
        }
129
        $number      = trim($rout['number']);
130
        $rout_number = ($number === '') ? 'X!' : $number;
131
        $this->generateDialActionsRoutNumber($rout, $rout_number);
132
        $this->duplicateDialActionsRoutNumber($rout, $rout_number, $number);
133
    }
134
    private function generateDialActionsRoutNumber($rout, $rout_number):void
135
    {
136
        $timeout     = trim($rout['timeout']);
137
        // Обязательно проверяем "DIALSTATUS", в случае с парковой через AMI вызова это необходимо.
138
        // При ответе может отработать следующий приоритет.
139
        if (!isset($this->rout_data_dial[$rout_number])) {
140
            $this->rout_data_dial[$rout_number] = '';
141
        }
142
        if (in_array($rout['extension'], $this->confExtensions, true)) {
143
            // Это конференция. Тут не требуется обработка таймаута ответа.
144
            // Вызов будет отвечен сразу конференцией.
145
            $dial_command = " \n\t" . 'same => n,' . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Goto(internal,{$rout['extension']},1));";
146
            $this->rout_data_dial[$rout_number] .= "";
147
        } else {
148
            $dial_command = " \n\t" . 'same => n,' . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Dial(Local/{$rout['extension']}@internal-incoming/n,{$timeout},TKg));";
149
            $this->rout_data_dial[$rout_number] .= " \n\t" . "same => n,Set(M_TIMEOUT={$timeout})";
150
        }
151
        $this->rout_data_dial[$rout_number] .= $dial_command;
152
    }
153
    private function duplicateDialActionsRoutNumber($rout, $rout_number, $number):void
154
    {
155
        if (!is_array($this->provider)) {
156
            return;
157
        }
158
        $key = $this->provider[$rout['provider']] ?? '';
159
        if (!isset($this->rout_data_dial[$key])) {
160
            $this->rout_data_dial[$key] = '';
161
        }
162
        if (empty($number)) {
163
            $this->rout_data_dial[$key] .= $this->rout_data_dial[$rout_number];
164
        }
165
    }
166
167
    /**
168
     * Отрезает пробелы справа для dialplan.
169
     */
170
    private function trimDialPlan():void{
171
        foreach ($this->dialplan as $key => &$dialplan) {
172
            if (!array_key_exists($key, $this->rout_data_dial)) {
173
                continue;
174
            }
175
            $dialplan = rtrim($dialplan);
176
            $dialplan .= $this->rout_data_dial[$key];
177
        }
178
    }
179
180
    /**
181
     * Формирование итогового dialplan.
182
     * @return string
183
     */
184
    private function createSummaryDialplan(): string
185
    {
186
        /** @var IncomingRoutingTable $default_action */
187
        $default_action = IncomingRoutingTable::findFirst('priority = 9999');
188
189
        $uniqId = is_string($this->provider) ? $this->provider : $this->uniqId;
190
        $conf = "\n" . "[{$uniqId}-incoming]\n";
191
        foreach ($this->dialplan as $dpln) {
192
            $conf .= $dpln . "\n";
193
            if (null === $default_action && 'none' !== $this->provider) {
194
                continue;
195
            }
196
            $conf .= $this->createSummaryDialplanDefAction($default_action, $uniqId);
197
        }
198
        return $conf;
199
    }
200
201
    /**
202
     * Формирование действия по умолчанию в dialplan.
203
     * @param $default_action
204
     * @param $uniqId
205
     * @return string
206
     */
207
    private function createSummaryDialplanDefAction($default_action, $uniqId):string
208
    {
209
        $conf = '';
210
        if ('extension' === $default_action->action) {
211
            $conf = $this->createSummaryDialplanGoto($conf, $default_action, $uniqId);
212
            $conf .= " \t" . 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-after-dial-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-after-dial-custom,${EXTEN},1)' . "\n";
213
        } elseif ('busy' === $default_action->action) {
214
            $conf .= "\t" . "same => n,Busy()" . "\n";
215
        }
216
        $conf .= "\t" . "same => n,Hangup()" . "\n";
217
        return $conf;
218
    }
219
220
    /**
221
     * Формирование Goto действия для итогового dialplan.
222
     * @param string               $conf
223
     * @param IncomingRoutingTable $default_action
224
     * @param string               $uniqId
225
     * @return string
226
     */
227
    private function createSummaryDialplanGoto(string $conf, IncomingRoutingTable $default_action, string $uniqId): string{
228
        // Обязательно проверяем "DIALSTATUS", в случае с парковой через AMI вызова это необходимо.
229
        // При ответе может отработать следующий приоритет.
230
        $conf .= "\t" . 'same => n,Set(M_TIMEOUT=0)' . "\n";
231
        if (in_array($default_action->extension, $this->confExtensions, true)) {
232
            // Это конференция. Тут не требуется обработка таймаута ответа.
233
            // Вызов будет отвечен сразу конференцией.
234
            $conf .= "\t" . "same => n," . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Goto(internal,{$default_action->extension},1)); default action" . "\n";
235
        } else {
236
            $conf .= "\t" . "same => n," . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Dial(Local/{$default_action->extension}@internal/n,,TKg)); default action" . "\n";
237
        }
238
        foreach ($this->additionalModules as $appClass) {
239
            $addition = $appClass->generateIncomingRoutAfterDialContext($uniqId);
240
            if (!empty($addition)) {
241
                $conf .= $appClass->confBlockWithComments($addition);
242
            }
243
        }
244
        return $conf;
245
    }
246
247
    /**
248
     * Возвращает массив входящих маршрутов для провайдера.
249
     * @return array|string[]
250
     */
251
    private function getRoutes(): array{
252
        if ('none' === $this->provider) {
253
            // Звонки по sip uri.
254
            $filter = ['provider IS NULL AND priority<>9999', 'order' => 'provider,priority,extension',];
255
        } elseif (is_array($this->provider)) {
256
            $filter = ['provider IN ({provider:array})', 'bind' => ['provider' => array_keys($this->provider),], 'order' => 'provider,priority,extension',];
257
        } else {
258
            // Звонки через провайдера.
259
            $filter = ["provider = '$this->provider'", 'order' => 'provider,priority,extension',];
260
        }
261
262
        $m_data = IncomingRoutingTable::find($filter);
263
        $data   = $m_data->toArray();
264
        uasort($data, ExtensionsConf::class . '::sortArrayByPriority');
265
        return $data;
266
    }
267
268
    /**
269
     * Проверка нужен ли дефолтный маршрут для провайдера.
270
     * Наполнение таблицы маршрутизаци значением по умолчанию.
271
     * @return bool
272
     */
273
    private function checkNeedDefRout(): bool{
274
        $need_def_rout = $this->needDefRout();
275
        if ($need_def_rout === true && 'none' !== $this->provider) {
276
            $this->routes[] = ['number' => '', 'extension' => '', 'timeout' => ''];
277
        }
278
        return $need_def_rout;
279
    }
280
281
    /**
282
     * Проверка нужен ли дефолтный маршрут для провайдера.
283
     * @return bool
284
     */
285
    private function needDefRout():bool
286
    {
287
        $need_def_rout = true;
288
        foreach ($this->routes as $rout) {
289
            $number = trim($rout['number']);
290
            if ($number === 'X!' || $number === '') {
291
                $need_def_rout = false;
292
                break;
293
            }
294
        }
295
        return $need_def_rout;
296
    }
297
298
    /**
299
     * Добавление дополнительных exten в Dialplan
300
     */
301
    private function multiplyExtensionsInDialplan(): void{
302
        if (is_string($this->login)) {
303
            $this->multiplyExtensionsInDialplanStringLogin();
304
        }
305
        if (is_array($this->provider)) {
306
            foreach (array_values($this->provider) as $_login) {
307
                $this->dialplan[$_login] = str_replace('_X!,1', "{$_login},1", $this->dialplan['X!']);
308
            }
309
        }
310
    }
311
312
    /**
313
     * Добавляет extensions для dialplan.
314
     */
315
    private function multiplyExtensionsInDialplanStringLogin(): void
316
    {
317
        $add_login_pattern = $this->needAddLoginExtension();
318
        if ($this->isMultipleRoutes($add_login_pattern)) {
319
            $this->dialplan[$this->login] = str_replace('_X!,1', "{$this->login},1", $this->dialplan['X!']);
320
            $this->rout_data_dial[$this->login] = $this->rout_data_dial['X!'];
321
        } elseif ($this->defaultRouteOnly($add_login_pattern)) {
322
            // Только маршрут "По умолчанию".
323
            $this->dialplan[$this->login] = str_replace('_X!,1', "{$this->login},1", $this->dialplan['X!']);
324
        }
325
    }
326
327
    /**
328
     * Несколько возможных входящих маршрутов "Каскад".
329
     * @param bool $add_login_pattern
330
     * @return bool
331
     */
332
    private function isMultipleRoutes(bool $add_login_pattern): bool{
333
        return $add_login_pattern && array_key_exists('X!', $this->rout_data_dial) && isset($this->dialplan['X!']);
334
    }
335
336
    /**
337
     * Проверка. Нужен только маршрут по умолчанию.
338
     * @param bool $add_login_pattern
339
     * @return bool
340
     */
341
    private function defaultRouteOnly(bool $add_login_pattern): bool{
342
        return $add_login_pattern === true && $this->need_def_rout === true && count($this->routes) === 1;
343
    }
344
345
    /**
346
     * Если логин не числовой, то нужно добавить такой Exten.
347
     * @return bool
348
     */
349
    private function needAddLoginExtension(): bool{
350
        $add_login_pattern = !empty($this->login);
351
        foreach ($this->routes as $rout) {
352
            if (!$add_login_pattern) {
353
                break;
354
            } // Логин не заполнен, обработка не требуется.
355
            $is_num = preg_match_all('/^\d+$/m', $this->login, $matches, PREG_SET_ORDER);
356
            if ($is_num === 1) {
357
                // Это числовой номер, потому, не требуется дополнительно описывать exten.
358
                $add_login_pattern = false;
359
                break;
360
            }
361
            if (trim($rout['number']) !== $this->login) {
362
                // Совпадение exten не найдено. Идем дальше.
363
                continue;
364
            }
365
            // Совпадение найдено, не требуется дополнительно описывать exten.
366
            $add_login_pattern = false;
367
            break;
368
        }
369
        return $add_login_pattern;
370
    }
371
372
}