Passed
Push — develop ( 80c87b...9ff5e0 )
by Портнов
04:46 queued 12s
created

IncomingContexts::trimDialplans()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 7
rs 10
cc 3
nc 3
nop 2
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
        $number      = trim($rout['number']);
83
        $rout_number = ($number === '') ? 'X!' : $number;
84
85
        $ext_prefix = ('none' === $this->provider) ? '' : '_';
86
        $rout_data .= "exten => {$ext_prefix}{$rout_number},1,NoOp(--- Incoming call ---)\n\t";
87
        $rout_data .= 'same => n,Set(CHANNEL(language)=' . $this->lang . ')' . "\n\t";
88
        $rout_data .= 'same => n,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . "\n\t";
89
        $rout_data .= 'same => n,Set(__FROM_DID=${EXTEN})' . "\n\t";
90
        $rout_data .= 'same => n,Set(__FROM_CHAN=${CHANNEL})' . "\n\t";
91
        // Установка имени пира.
92
        $rout_data .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" != "Local"]?Gosub(set_from_peer,s,1))' . "\n\t";
93
        $rout_data .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local"]?Set(__FROM_PEER=${CALLERID(num)}))' . "\n\t";
94
        $rout_data .= 'same => n,Gosub(add-trim-prefix-clid,${EXTEN},1)' . "\n\t";
95
96
        foreach ($this->additionalModules as $appClass) {
97
            $addition = $appClass->generateIncomingRoutBeforeDial($rout_number);
98
            if (!empty($addition)) {
99
                $rout_data .= $appClass->confBlockWithComments($addition);
100
            }
101
        }
102
        // Описываем возможность прыжка в пользовательский sub контекст.
103
        $rout_data .= " \n\t" . 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)';
104
105
        if (!empty($rout['extension'])) {
106
            $rout_data = rtrim($rout_data);
107
        }
108
    }
109
110
    /**
111
     * Формирует в Dialplan команды-действия goto dial.
112
     * @param        $rout
113
     * @return void
114
     */
115
    private function generateDialActions($rout): void{
116
        if (empty($rout['extension'])) {
117
            return;
118
        }
119
        $timeout     = trim($rout['timeout']);
120
        $number      = trim($rout['number']);
121
        $rout_number = ($number === '') ? 'X!' : $number;
122
        // Обязательно проверяем "DIALSTATUS", в случае с парковой через AMI вызова это необходимо.
123
        // При ответе может отработать следующий приоритет.
124
        if (!isset($this->rout_data_dial[$rout_number])) {
125
            $this->rout_data_dial[$rout_number] = '';
126
        }
127
        if (in_array($rout['extension'], $this->confExtensions, true)) {
128
            // Это конференция. Тут не требуется обработка таймаута ответа.
129
            // Вызов будет отвечен сразу конференцией.
130
            $dial_command = " \n\t" . 'same => n,' . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Goto(internal,{$rout['extension']},1));";
131
            $this->rout_data_dial[$rout_number] .= "";
132
        } else {
133
            $dial_command = " \n\t" . 'same => n,' . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Dial(Local/{$rout['extension']}@internal-incoming/n,{$timeout},TKg));";
134
            $this->rout_data_dial[$rout_number] .= " \n\t" . "same => n,Set(M_TIMEOUT={$timeout})";
135
        }
136
        $this->rout_data_dial[$rout_number] .= $dial_command;
137
        if (!is_array($this->provider)) {
138
            return;
139
        }
140
        $key = $this->provider[$rout['provider']] ?? '';
141
        if (!isset($this->rout_data_dial[$key])) {
142
            $this->rout_data_dial[$key] = '';
143
        }
144
        if (empty($number)) {
145
            $this->rout_data_dial[$key] .= $this->rout_data_dial[$rout_number];
146
        }
147
    }
148
149
    /**
150
     * Отрезает пробелы справа для dialplan.
151
     */
152
    private function trimDialPlan():void{
153
        foreach ($this->dialplan as $key => &$dialplan) {
154
            if (!array_key_exists($key, $this->rout_data_dial)) {
155
                continue;
156
            }
157
            $dialplan = rtrim($dialplan);
158
            $dialplan .= $this->rout_data_dial[$key];
159
        }
160
    }
161
162
    /**
163
     * Формирование итогового dialplan.
164
     * @return string
165
     */
166
    private function createSummaryDialplan(): string{
167
        /** @var IncomingRoutingTable $default_action */
168
        $default_action = IncomingRoutingTable::findFirst('priority = 9999');
169
170
        $uniqId = is_string($this->provider) ? $this->provider : $this->uniqId;
171
        $conf = "\n" . "[{$uniqId}-incoming]\n";
172
        foreach ($this->dialplan as $dpln) {
173
            $conf .= $dpln . "\n";
174
            if (null === $default_action && 'none' !== $this->provider) {
175
                continue;
176
            }
177
            if ('extension' === $default_action->action) {
178
                $conf = $this->createSummaryDialplanGoto($conf, $default_action, $uniqId);
179
                $conf .= " \t" . 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-after-dial-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-after-dial-custom,${EXTEN},1)' . "\n";
180
            } elseif ('busy' === $default_action->action) {
181
                $conf .= "\t" . "same => n,Busy()" . "\n";
182
            }
183
            $conf .= "\t" . "same => n,Hangup()" . "\n";
184
        }
185
        return $conf;
186
    }
187
188
    /**
189
     * Формирование Goto действия для итогового dialplan.
190
     * @param string               $conf
191
     * @param IncomingRoutingTable $default_action
192
     * @param string               $uniqId
193
     * @return string
194
     */
195
    private function createSummaryDialplanGoto(string $conf, IncomingRoutingTable $default_action, string $uniqId): string{
196
        // Обязательно проверяем "DIALSTATUS", в случае с парковой через AMI вызова это необходимо.
197
        // При ответе может отработать следующий приоритет.
198
        $conf .= "\t" . 'same => n,Set(M_TIMEOUT=0)' . "\n";
199
        if (in_array($default_action->extension, $this->confExtensions, true)) {
200
            // Это конференция. Тут не требуется обработка таймаута ответа.
201
            // Вызов будет отвечен сразу конференцией.
202
            $conf .= "\t" . "same => n," . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Goto(internal,{$default_action->extension},1)); default action" . "\n";
203
        } else {
204
            $conf .= "\t" . "same => n," . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Dial(Local/{$default_action->extension}@internal/n,,TKg)); default action" . "\n";
205
        }
206
        foreach ($this->additionalModules as $appClass) {
207
            $addition = $appClass->generateIncomingRoutAfterDialContext($uniqId);
208
            if (!empty($addition)) {
209
                $conf .= $appClass->confBlockWithComments($addition);
210
            }
211
        }
212
        return $conf;
213
    }
214
215
    /**
216
     * Возвращает массив входящих маршрутов для провайдера.
217
     * @return array|string[]
218
     */
219
    private function getRoutes(): array{
220
        if ('none' === $this->provider) {
221
            // Звонки по sip uri.
222
            $filter = ['provider IS NULL AND priority<>9999', 'order' => 'provider,priority,extension',];
223
        } elseif (is_array($this->provider)) {
224
            $filter = ['provider IN ({provider:array})', 'bind' => ['provider' => array_keys($this->provider),], 'order' => 'provider,priority,extension',];
225
        } else {
226
            // Звонки через провайдера.
227
            $filter = ["provider = '$this->provider'", 'order' => 'provider,priority,extension',];
228
        }
229
230
        $m_data = IncomingRoutingTable::find($filter);
231
        $data   = $m_data->toArray();
232
        uasort($data, ExtensionsConf::class . '::sortArrayByPriority');
233
        return $data;
234
    }
235
236
    /**
237
     * Проверка нужен ли дефолтный маршрут для провайдера.
238
     * @return bool
239
     */
240
    private function checkNeedDefRout(): bool{
241
        $need_def_rout = true;
242
        foreach ($this->routes as $rout) {
243
            $number = trim($rout['number']);
244
            if ($number === 'X!' || $number === '') {
245
                $need_def_rout = false;
246
                break;
247
            }
248
        }
249
        if ($need_def_rout === true && 'none' !== $this->provider) {
250
            $this->routes[] = ['number' => '', 'extension' => '', 'timeout' => ''];
251
        }
252
        return $need_def_rout;
253
    }
254
255
    /**
256
     * Добавление дополнительных exten в Dialplan
257
     */
258
    private function multiplyExtensionsInDialplan(): void{
259
        if (is_string($this->login)) {
260
            $this->multiplyExtensionsInDialplanStringLogin();
261
        }
262
        if (is_array($this->provider)) {
263
            foreach (array_values($this->provider) as $_login) {
264
                $this->dialplan[$_login] = str_replace('_X!,1', "{$_login},1", $this->dialplan['X!']);
265
            }
266
        }
267
    }
268
269
    /**
270
     * Добавляет extensions для dialplan.
271
     */
272
    private function multiplyExtensionsInDialplanStringLogin(): void
273
    {
274
        $add_login_pattern = $this->needAddLoginExtension();
275
        if ($add_login_pattern && array_key_exists('X!', $this->rout_data_dial) && isset($this->dialplan['X!'])) {
276
            $this->dialplan[$this->login] = str_replace('_X!,1', "{$this->login},1", $this->dialplan['X!']);
277
            $this->rout_data_dial[$this->login] = $this->rout_data_dial['X!'];
278
        } elseif ($add_login_pattern === true && $this->need_def_rout === true && count($this->routes) === 1) {
279
            // Только маршрут "По умолчанию".
280
            $this->dialplan[$this->login] = str_replace('_X!,1', "{$this->login},1", $this->dialplan['X!']);
281
        }
282
    }
283
284
    /**
285
     * Если логин не числовой, то нужно добавить такой Exten.
286
     * @return bool
287
     */
288
    private function needAddLoginExtension(): bool{
289
        $add_login_pattern = !empty($this->login);
290
        foreach ($this->routes as $rout) {
291
            if (!$add_login_pattern) {
292
                break;
293
            } // Логин не заполнен, обработка не требуется.
294
            $is_num = preg_match_all('/^\d+$/m', $this->login, $matches, PREG_SET_ORDER);
295
            if ($is_num === 1) {
296
                // Это числовой номер, потому, не требуется дополнительно описывать exten.
297
                $add_login_pattern = false;
298
                break;
299
            }
300
            if (trim($rout['number']) !== $this->login) {
301
                // Совпадение exten не найдено. Идем дальше.
302
                continue;
303
            }
304
            // Совпадение найдено, не требуется дополнительно описывать exten.
305
            $add_login_pattern = false;
306
            break;
307
        }
308
        return $add_login_pattern;
309
    }
310
311
}