Passed
Push — develop ( b317ce...4281e4 )
by Портнов
05:07
created

ExtensionsConf::generateOutWorkRule()   F

Complexity

Conditions 19
Paths 5100

Size

Total Lines 68
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 49
c 0
b 0
f 0
dl 0
loc 68
rs 0.3499
cc 19
nc 5100
nop 4

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
 * 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\Core\Asterisk\Configs;
21
22
use MikoPBX\Common\Models\{Iax, IncomingRoutingTable, OutgoingRoutingTable, OutWorkTimes, Providers, Sip, SoundFiles};
23
use MikoPBX\Common\Providers\PBXConfModulesProvider;
24
use MikoPBX\Modules\Config\ConfigClass;
25
use MikoPBX\Core\System\{MikoPBXConfig, Storage, Util};
26
use Phalcon\Di;
27
28
class ExtensionsConf extends ConfigClass
29
{
30
    protected string $description = 'extensions.conf';
31
32
    /**
33
     * Sorts array by priority field
34
     *
35
     * @param $a
36
     * @param $b
37
     *
38
     * @return int|null
39
     */
40
    public static function sortArrayByPriority(array $a, array $b): int
41
    {
42
        $aPriority = (int)($a['priority'] ?? 0);
43
        $bPriority = (int)($b['priority'] ?? 0);
44
        if ($aPriority === $bPriority) {
45
            return 0;
46
        }
47
48
        return ($aPriority < $bPriority) ? -1 : 1;
49
    }
50
51
    /**
52
     * Основной генератор extensions.conf
53
     */
54
    protected function generateConfigProtected(): void
55
    {
56
        /** @scrutinizer ignore-call */
57
        $additionalModules = $this->di->getShared(PBXConfModulesProvider::SERVICE_NAME);
58
        $conf              = "[globals] \n" .
59
            "TRANSFER_CONTEXT=internal-transfer; \n";
60
        if ($this->generalSettings['PBXRecordCalls'] === '1') {
61
            $conf .= "MONITOR_DIR=" . Storage::getMonitorDir() . " \n";
62
            $conf .= "MONITOR_STEREO=" . $this->generalSettings['PBXSplitAudioThread'] . " \n";
63
        }
64
        foreach ($additionalModules as $appClass) {
65
            $addition = $appClass->extensionGlobals();
66
            if ( ! empty($addition)) {
67
                $conf .= $appClass->confBlockWithComments($addition);
68
            }
69
        }
70
        $conf .= "\n";
71
        $conf .= "\n";
72
        $conf .= "[general] \n";
73
        $conf .= "\n";
74
75
        // Создаем диалплан внутренних учеток.
76
        $this->generateOtherExten($conf);
77
        // Контекст для внутренних вызовов.
78
        $this->generateInternal($conf);
79
        // Контекст для внутренних переадресаций.
80
        $this->generateInternalTransfer($conf);
81
        // Создаем контекст хинтов.
82
        $this->generateSipHints($conf);
83
        // Создаем контекст (исходящие звонки).
84
        $this->generateOutContextPeers($conf);
85
        // Описываем контекст для публичных входящих.
86
        $this->generatePublicContext($conf);
87
88
        Util::fileWriteContent($this->config->path('asterisk.astetcdir') . '/extensions.conf', $conf);
89
    }
90
91
    /**
92
     * Генератор прочих контекстов.
93
     *
94
     * @param $conf
95
     */
96
    private function generateOtherExten(&$conf): void
97
    {
98
        $extension = 'X!';
99
        // Контекст для AMI originate. Без него отображается не корректный CallerID.
100
        $conf .= '[sipregistrations]' . "\n\n";
101
102
        $conf .= '[messages]' . "\n" .
103
            'exten => _' . $extension . ',1,MessageSend(sip:${EXTEN},"${CALLERID(name)}"${MESSAGE(from)})' . "\n\n";
104
105
        $conf .= '[internal-originate]' . " \n";
106
        $conf .= 'exten => _' . $extension . ',1,NoOP(Hint ${HINT} exten ${EXTEN} )' . " \n";
107
        $conf .= '; Если это originate, то скроем один CDR.' . " \n\t";
108
        $conf .= 'same => n,ExecIf($["${pt1c_cid}x" != "x"]?Set(CALLERID(num)=${pt1c_cid}))' . " \n\t";
109
110
        $conf .= 'same => n,ExecIf($["${CUT(CHANNEL,\;,2)}" == "2"]?Set(__PT1C_SIP_HEADER=${SIPADDHEADER}))' . " \n\t";
111
        $conf .= 'same => n,ExecIf($["${peer_mobile}x" != "x"]?Set(ADDITIONAL_PEER=&Local/${peer_mobile}@outgoing/n))' . " \n\t";
112
113
        // Описываем возможность прыжка в пользовательский sub контекст.
114
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)' . "\n\t";
115
        $conf .= 'same => n,Dial(Local/${EXTEN}@internal-users/n${ADDITIONAL_PEER},60,TteKkHhb(originate_create_chan,s,1))' . " \n\n";
116
117
        $conf .= '[originate_create_chan]' . " \n";
118
        $conf .= 'exten => s,1,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . "\n\t";
119
        $conf .= 'same => n,return' . " \n\n";
120
121
        $conf .= '[dial_create_chan]' . " \n";
122
        $conf .= 'exten => s,1,Gosub(lua_${ISTRANSFER}dial_create_chan,${EXTEN},1)' . "\n\t";
123
        $conf .= 'same => n,Set(pt1c_is_dst=1)' . " \n\t";
124
        $conf .= 'same => n,ExecIf($["${PT1C_SIP_HEADER}x" != "x"]?Set(PJSIP_HEADER(add,${CUT(PT1C_SIP_HEADER,:,1)})=${CUT(PT1C_SIP_HEADER,:,2)}))' . " \n\t";
125
        $conf .= 'same => n,Set(__PT1C_SIP_HEADER=${UNDEFINED})' . " \n\t";
126
        $conf .= 'same => n,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . " \n\t";
127
        $conf .= 'same => n,return' . " \n\n";
128
129
        $conf .= '[hangup_handler]' . "\n";
130
        $conf .= 'exten => s,1,NoOp(--- hangup - ${CHANNEL} ---)' . "\n\t";
131
        $conf .= 'same => n,Gosub(hangup_chan,${EXTEN},1)' . "\n\t";
132
133
        $conf .= 'same => n,return' . "\n\n";
134
135
        $conf .= '[set_orign_chan]' . "\n";
136
        $conf .= 'exten => s,1,Wait(0.2)' . "\n\t";
137
        $conf .= 'same => n,Set(pl=${IF($["${CHANNEL:-1}" == "1"]?2:1)})' . "\n\t";
138
        $conf .= 'same => n,Set(orign_chan=${IMPORT(${CUT(CHANNEL,\;,1)}\;${pl},BRIDGEPEER)})' . "\n\t";
139
        $conf .= 'same => n,ExecIf($[ "${orign_chan}x" == "x" ]?Set(orign_chan=${IMPORT(${CUT(CHANNEL,\;,1)}\;${pl},FROM_CHAN)}))' . "\n\t";
140
        $conf .= 'same => n,ExecIf($[ "${QUEUE_SRC_CHAN}x" != "x" ]?Set(__QUEUE_SRC_CHAN=${orign_chan}))' . "\n\t";
141
        $conf .= 'same => n,ExecIf($[ "${QUEUE_SRC_CHAN:0:5}" == "Local" ]?Set(__QUEUE_SRC_CHAN=${FROM_CHAN}))' . "\n\t";
142
        $conf .= 'same => n,ExecIf($[ "${FROM_CHAN}x" == "x" ]?Set(__FROM_CHAN=${IMPORT(${CUT(CHANNEL,\;,1)}\;${pl},BRIDGEPEER)}))' . "\n\t";
143
        $conf .= 'same => n,return' . "\n\n";
144
145
        $conf .= '[playback]' . "\n";
146
        $conf .= 'exten => s,1,Playback(hello_demo,noanswer)' . "\n\t";
147
        $conf .= 'same => n,ExecIf($["${SRC_BRIDGE_CHAN}x" == "x"]?Wait(30))' . "\n\t";
148
        $conf .= 'same => n,Wait(0.3)' . "\n\t";
149
        $conf .= 'same => n,Bridge(${SRC_BRIDGE_CHAN},kKTthH)' . "\n\n";
150
151
        $conf .= 'exten => h,1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\n";
152
153
        // TODO / Добавление / удаление префиксов на входящий callerid.
154
        $conf .= '[add-trim-prefix-clid]' . "\n";
155
        $conf .= 'exten => _.!,1,NoOp(--- Incoming call from ${CALLERID(num)} ---)' . "\n\t";
156
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)' . "\n\t";
157
        // Отсекаем "+".
158
        // $conf.= 'same => n,ExecIf( $["${CALLERID(num):0:1}" == "+"]?Set(CALLERID(num)=${CALLERID(num):1}))'."\n\t";
159
        // Отсекаем "7" и добавляем "8".
160
        // $conf.= 'same => n,ExecIf( $["${REGEX("^7[0-9]+" ${CALLERID(num)})}" == "1"]?Set(CALLERID(num)=8${CALLERID(num):1}))'."\n\t";
161
        $conf .= 'same => n,return' . "\n\n";
162
    }
163
164
    /**
165
     * Генератор контекста для внутренних вызовов.
166
     *
167
     * @param $conf
168
     */
169
    private function generateInternal(&$conf): void
170
    {
171
        $extension  = 'X!';
172
        $technology = SIPConf::getTechnology();
173
174
        $additionalModules = $this->di->getShared(PBXConfModulesProvider::SERVICE_NAME);
175
        foreach ($additionalModules as $appClass) {
176
            $addition = $appClass->extensionGenContexts();
177
            if ( ! empty($addition)) {
178
                $conf .= $appClass->confBlockWithComments($addition);
179
            }
180
        }
181
        $conf .= "\n";
182
        $conf .= "[internal-num-undefined] \n";
183
        $conf .= 'exten => _' . $extension . ',1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\t";
184
        $conf .= 'same => n,ExecIf($["${BLINDTRANSFER}x" != "x"]?AGI(check_redirect.php,${BLINDTRANSFER}))' . "\n\t";
185
        $conf .= "same => n,Playback(pbx-invalid,noanswer) \n\n";
186
187
        $conf .= "[internal-fw]\n";
188
        $conf .= 'exten => _' . $extension . ',1,NoOp(DIALSTATUS - ${DIALSTATUS})' . "\n\t";
189
        // CANCEL - вызов был отменен, к примеру *0, не нужно дальше искать адресат.
190
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" == "CANCEL"]?Hangup())' . "\n\t";
191
        // BUSY - занято. К примру абонент завершил вызов или DND.
192
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" == "BUSY"]?Set(dstatus=FW_BUSY))' . "\n\t";
193
        // CHANUNAVAIL - канал не доступен. К примеру телефон не зарегистрирован или не отвечает.
194
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" == "CHANUNAVAIL"]?Set(dstatus=FW_UNAV))' . "\n\t";
195
        // NOANSWER - не ответили по таймауту.
196
        $conf .= 'same => n,ExecIf($["${dstatus}x" == "x"]?Set(dstatus=FW))' . "\n\t";
197
        $conf .= 'same => n,Set(fw=${DB(${dstatus}/${EXTEN})})' . "\n\t";
198
        $conf .= 'same => n,ExecIf($["${fw}x" != "x"]?Set(__pt1c_UNIQUEID=${UNDEFINED})' . "\n\t";
199
        $conf .= 'same => n,ExecIf($["${fw}x" != "x"]?Goto(internal,${fw},1))' . "\n\t";
200
        $conf .= 'same => n,ExecIf($["${BLINDTRANSFER}x" != "x"]?AGI(check_redirect.php,${BLINDTRANSFER}))' . "\n\t";
201
        $conf .= 'same => n,Hangup() ' . "\n\n";
202
203
        $conf .= "[all_peers]\n";
204
        $conf .= 'include => internal-hints' . "\n";
205
        $conf .= 'exten => failed,1,Hangup()' . "\n";
206
207
        $conf .= 'exten => _.!,1,ExecIf($[ "${EXTEN}" == "h" ]?Hangup())' . "\n\t";
208
        // Фильтр спецсимволов. Разершаем только цифры.
209
        $conf .= 'same => n,Set(cleanNumber=${FILTER(\*\#\+1234567890,${EXTEN})})' . "\n\t";
210
        $conf .= 'same => n,ExecIf($["${EXTEN}" != "${cleanNumber}"]?Goto(${CONTEXT},${cleanNumber},$[${PRIORITY} + 1]))' . "\n\t";
211
212
        $conf .= 'same => n,Set(__FROM_CHAN=${CHANNEL})' . "\n\t";
213
        $conf .= 'same => n,ExecIf($["${OLD_LINKEDID}x" == "x"]?Set(__OLD_LINKEDID=${CHANNEL(linkedid)}))' . "\n\t";
214
        $conf .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" != "Local"]?Gosub(set_from_peer,s,1))' . "\n\t";
215
        $conf .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local"]?Gosub(set_orign_chan,s,1))' . "\n\t";
216
217
        $conf .= 'same => n,ExecIf($["${CALLERID(num)}x" == "x"]?Set(CALLERID(num)=${FROM_PEER}))' . "\n\t";
218
        $conf .= 'same => n,ExecIf($["${CALLERID(num)}x" == "x"]?Set(CALLERID(name)=${FROM_PEER}))' . "\n\t";
219
220
        $conf .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local" && "${FROM_PEER}x" == "x"]?Set(__FROM_PEER=${CALLERID(num)}))' . "\n\t";
221
        $conf .= 'same => n,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . "\n\t";
222
        $conf .= 'same => n,Gosub(${ISTRANSFER}dial,${EXTEN},1)' . "\n\t";
223
224
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)' . "\n\t";
225
        $dialplanNames = ['applications', 'internal', 'outgoing'];
226
        foreach ($dialplanNames as $name){
227
            $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS('.$name.',${EXTEN},1)}" == "1"]?'.$name.',${EXTEN},1)'." \n\t";
228
        }
229
        $conf .= 'same => n,Hangup()'." \n";
230
231
        $pickupexten  = $this->generalSettings['PBXFeaturePickupExten'];
232
        $conf        .= 'exten => _' . $pickupexten . $extension . ',1,Set(PICKUPEER=' . $technology . '/${FILTER(0-9,${EXTEN:2})})' . "\n\t";
233
        $conf        .= 'same => n,Set(pt1c_dnid=${EXTEN})' . "\n\t";
234
        $conf        .= 'same => n,PickupChan(${PICKUPEER})' . "\n\t";
235
        $conf        .= 'same => n,Hangup()' . "\n\n";
236
237
        $voicemail_exten  = $this->generalSettings['VoicemailExten'];
238
        $conf            .= 'exten => ' . $voicemail_exten . ',1,NoOp(NOTICE, Dialing out from ${CALLERID(all)} to VoiceMail)' . "\n\t";
239
        $conf            .= 'same => n,VoiceMailMain(admin@voicemailcontext,s)' . "\n\t";
240
        $conf            .= 'same => n,Hangup()' . "\n\n";
241
242
        $conf .= "[voice_mail_peer] \n";
243
        $conf .= 'exten => voicemail,1,Answer()' . "\n\t";
244
        $conf .= 'same => n,VoiceMail(admin@voicemailcontext)' . "\n\t";
245
        $conf .= 'same => n,Hangup()' . "\n\n";
246
247
        // Контекст для внутренних вызовов.
248
        $conf .= "[internal] \n";
249
250
        foreach ($additionalModules as $appClass) {
251
            $addition = $appClass->getIncludeInternal();
252
            if ( ! empty($addition)) {
253
                $conf .= $appClass->confBlockWithComments($addition);
254
            }
255
        }
256
257
        foreach ($additionalModules as $appClass) {
258
            $addition = $appClass->extensionGenInternal();
259
            if ( ! empty($addition)) {
260
                $conf .= $appClass->confBlockWithComments($addition);
261
            }
262
        }
263
264
        $conf .= 'exten => i,1,NoOp(-- INVALID NUMBER --)' . "\n\t";
265
        $conf .= 'same => n,Set(DIALSTATUS=INVALID_NUMBER)' . "\n\t";
266
        $conf .= 'same => n,Playback(privacy-incorrect,noanswer)' . "\n\t";
267
        $conf .= 'same => n,Hangup()' . "\n";
268
269
        $conf .= 'exten => h,1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\n";
270
271
        $conf .= "[internal-incoming]\n";
272
        $conf .= 'exten => _.!,1,ExecIf($["${MASTER_CHANNEL(M_TIMEOUT)}x" != "x"]?Set(TIMEOUT(absolute)=${MASTER_CHANNEL(M_TIMEOUT)}))' . " \n\t";
273
        $conf .= 'same => n,Set(MASTER_CHANNEL(M_TIMEOUT_CHANNEL)=${CHANNEL})' . " \n\t";
274
        $conf .= 'same => n,Set(MASTER_CHANNEL(M_TIMEOUT)=${EMPTY_VAR})' . " \n\t";
275
        $conf .= 'same => n,Goto(internal,${EXTEN},1)' . " \n\n";
276
277
        $conf .= "[internal-users] \n";
278
        $conf .= 'exten => _' . $extension . ',1,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . " \n\t";
279
        $conf .= 'same => n,ExecIf($["${ISTRANSFER}x" != "x"]?Set(SIPADDHEADER01=${EMPTY_VAR})' . " \n\t";
280
        $conf .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local"]?Gosub(set_orign_chan,s,1))' . " \n\t";
281
282
        $conf .= 'same => n,Gosub(${ISTRANSFER}dial,${EXTEN},1)' . "\n\t";
283
        // Проверим, существует ли такой пир.
284
285
        $conf .= 'same => n,ExecIf($["${PJSIP_ENDPOINT(${EXTEN},auth)}x" == "x"]?Goto(internal-num-undefined,${EXTEN},1))' . " \n\t";
286
        $conf .= 'same => n,ExecIf($["${DEVICE_STATE(' . $technology . '/${EXTEN})}" == "BUSY"]?Set(DIALSTATUS=BUSY))' . " \n\t";
287
        $conf .= 'same => n,GotoIf($["${DEVICE_STATE(' . $technology . '/${EXTEN})}" == "BUSY"]?fw_start)' . " \n\t";
288
289
        // Как долго звонить пиру.
290
        $conf .= 'same => n,Set(ringlength=${DB(FW_TIME/${EXTEN})})' . " \n\t";
291
        $conf .= 'same => n,ExecIf($["${ringlength}x" == "x"]?Set(ringlength=600))' . " \n\t";
292
        $conf .= 'same => n,ExecIf($["${QUEUE_SRC_CHAN}x" != "x" && "${ISTRANSFER}x" == "x"]?Set(ringlength=600))' . " \n\t";
293
294
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1) ' . " \n\t";
295
        // Совершаем вызов пира.
296
        $conf .= 'same => n,Set(DST_CONTACT=${PJSIP_DIAL_CONTACTS(${EXTEN})})' . " \n\t";
297
        $conf .= 'same => n,ExecIf($["${FIELDQTY(DST_CONTACT,&)}" != "1"]?Set(__PT1C_SIP_HEADER=${EMPTY_VAR}))' . " \n\t";
298
        $conf .= 'same => n,ExecIf($["${DST_CONTACT}x" != "x"]?Dial(${DST_CONTACT},${ringlength},TtekKHhU(${ISTRANSFER}dial_answer)b(dial_create_chan,s,1)):Set(DIALSTATUS=CHANUNAVAIL))' . " \n\t";
299
        $conf .= 'same => n(fw_start),NoOp(dial_hangup)' . " \n\t";
300
301
        // QUEUE_SRC_CHAN - установлена, если вызов сервершен агенту очереди.
302
        // Проверяем нужна ли переадресация
303
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" != "ANSWER" && "${ISTRANSFER}x" != "x"]?Goto(internal-fw,${EXTEN},1))' . " \n\t";
304
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" != "ANSWER" && "${QUEUE_SRC_CHAN}x" == "x"]?Goto(internal-fw,${EXTEN},1))' . " \n\t";
305
        $conf .= 'same => n,ExecIf($["${BLINDTRANSFER}x" != "x"]?AGI(check_redirect.php,${BLINDTRANSFER}))' . " \n\t";
306
        $conf .= 'same => n,Hangup()' . "\n\n";
307
308
        $conf .= 'exten => h,1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\n";
309
    }
310
311
    /**
312
     * Генератор контекста для переадресаций.
313
     *
314
     * @param $conf
315
     */
316
    private function generateInternalTransfer(&$conf): void
317
    {
318
        $additionalModules = $this->di->getShared(PBXConfModulesProvider::SERVICE_NAME);
319
        $conf              .= "[internal-transfer] \n";
320
321
        foreach ($additionalModules as $appClass) {
322
            $addition = $appClass->getIncludeInternalTransfer();
323
            if ( ! empty($addition)) {
324
                $conf .= $appClass->confBlockWithComments($addition);
325
            }
326
        }
327
328
        foreach ($additionalModules as $appClass) {
329
            $addition = $appClass->extensionGenInternalTransfer();
330
            if ( ! empty($addition)) {
331
                $conf .= $appClass->confBlockWithComments($addition);
332
            }
333
        }
334
        $conf .= 'exten => h,1,Gosub(transfer_dial_hangup,${EXTEN},1)' . "\n\n";
335
    }
336
337
    /**
338
     * Генератор хинтов SIP.
339
     *
340
     * @param $conf
341
     */
342
    private function generateSipHints(&$conf): void
343
    {
344
        $additionalModules = $this->di->getShared(PBXConfModulesProvider::SERVICE_NAME);
345
        $conf              .= "[internal-hints] \n";
346
        foreach ($additionalModules as $appClass) {
347
            $addition = $appClass->extensionGenHints();
348
            if ( ! empty($addition)) {
349
                $conf .= $appClass->confBlockWithComments($addition);
350
            }
351
        }
352
        $conf .= "\n\n";
353
    }
354
355
    /**
356
     * Генератор исходящих контекстов.
357
     *
358
     * @param $conf
359
     */
360
    private function generateOutContextPeers(&$conf): void
361
    {
362
        $additionalModules = $this->di->getShared(PBXConfModulesProvider::SERVICE_NAME);
363
        $conf              .= "[outgoing] \n";
364
365
        $conf .= 'exten => _+.!,1,NoOp(Strip + sign from number and convert it to +)' . " \n\t";
366
        $conf .= 'same => n,Set(ADDPLUS=+);' . " \n\t";
367
        $conf .= 'same => n,Goto(${CONTEXT},${EXTEN:1},1);' . " \n\n";
368
        $conf .= 'exten => _.!,1,ExecIf($[ "${EXTEN}" == "h" ]?Hangup())' . " \n\t";
369
        $conf .= 'same => n,Ringing()' . " \n\t";
370
371
        // Описываем возможность прыжка в пользовательский sub контекст.
372
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)' . "\n\t";
373
374
        /** @var OutgoingRoutingTable $routs */
375
        /** @var OutgoingRoutingTable $rout */
376
        $routs = OutgoingRoutingTable::find(['order' => 'priority'])->toArray();
377
        uasort($routs, __CLASS__ . '::sortArrayByPriority');
378
379
        $provider_contexts = [];
380
381
        foreach ($routs as $rout) {
382
            $technology = $this->getTechByID($rout['providerid']);
383
            if ($technology !== '') {
384
                $rout_data                       = $rout;
385
                $rout_data['technology']         = $technology;
386
                $id_dialplan                     = $rout_data['providerid'] . '-' . $rout_data['id'] . '-outgoing';
387
                $provider_contexts[$id_dialplan] = $rout_data;
388
                $conf                            .= $this->generateOutgoingRegexPattern($rout_data);
389
                continue;
390
            }
391
        }
392
        $conf .= 'same => n,ExecIf($["${peer_mobile}x" != "x"]?Hangup())' . " \n\t";
393
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" != "ANSWER" && "${BLINDTRANSFER}x" != "x" && "${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\t";
394
        $conf .= 'same => n,ExecIf($["${BLINDTRANSFER}x" != "x"]?AGI(check_redirect.php,${BLINDTRANSFER}))' . " \n\t";
395
        $conf .= 'same => n,ExecIf($["${ROUTFOUND}x" == "x"]?Gosub(dial,${EXTEN},1))' . "\n\t";
396
397
        $conf .= 'same => n,Playback(silence/2,noanswer)' . " \n\t";
398
        $conf .= 'same => n,ExecIf($["${ROUTFOUND}x" != "x"]?Playback(followme/sorry,noanswer):Playback(cannot-complete-as-dialed,noanswer))' . " \n\t";
399
        $conf .= 'same => n,Hangup()' . " \n\n";
400
        $conf .= 'exten => h,1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\t";
401
402
        foreach ($provider_contexts as $id_dialplan => $rout) {
403
            $conf .= "\n[{$id_dialplan}]\n";
404
            $trimFromBegin = (int) ($rout['trimfrombegin']??0);
405
            if ($trimFromBegin > 0) {
406
                $exten_var    = '${ADDPLUS}${EXTEN:' . $rout['trimfrombegin'] . '}';
407
                $change_exten = 'same => n,ExecIf($["${EXTEN}" != "${number}"]?Goto(${CONTEXT},${number},$[${PRIORITY} + 1]))' . "\n\t";
408
            } else {
409
                $exten_var    = '${ADDPLUS}${EXTEN}';
410
                $change_exten = '';
411
            }
412
            $conf .= 'exten => _.!,1,Set(number=' . $rout['prepend'] . $exten_var . ')' . "\n\t";
413
            $conf .= 'same => n,Set(number=${FILTER(\*\#\+1234567890,${number})})' . "\n\t";
414
            $conf .= $change_exten;
415
            foreach ($additionalModules as $appClass) {
416
                $addition = $appClass->generateOutRoutContext($rout);
417
                if ( ! empty($addition)) {
418
                    $conf .= $appClass->confBlockWithComments($addition);
419
                }
420
            }
421
            $conf .= 'same => n,ExecIf($["${number}x" == "x"]?Hangup())' . "\n\t";
422
            $conf .= 'same => n,Set(ROUTFOUND=1)' . "\n\t";
423
            $conf .= 'same => n,Gosub(${ISTRANSFER}dial,${EXTEN},1)' . "\n\t";
424
425
            $conf .= 'same => n,ExecIf($["${EXTERNALPHONE}" == "${EXTEN}"]?Set(DOPTIONS=tk))' . "\n\t";
426
427
            // Описываем возможность прыжка в пользовательский sub контекст.
428
            $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(' . $rout['providerid'] . '-outgoing-custom,${EXTEN},1)}" == "1"]?' . $rout['providerid'] . '-outgoing-custom,${EXTEN},1)' . "\n\t";
429
430
            if ($rout['technology'] === IAXConf::TYPE_IAX2) {
431
                $conf .= 'same => n,Dial(' . $rout['technology'] . '/' . $rout['providerid'] . '/${number},600,${DOPTIONS}TKU(${ISTRANSFER}dial_answer)b(dial_create_chan,s,1))' . "\n\t";
432
            } else {
433
                $conf .= 'same => n,Dial(' . $rout['technology'] . '/${number}@' . $rout['providerid'] . ',600,${DOPTIONS}TKU(${ISTRANSFER}dial_answer)b(dial_create_chan,s,1))' . "\n\t";
434
            }
435
            foreach ($additionalModules as $appClass) {
436
                $addition = $appClass->generateOutRoutAfterDialContext($rout);
437
                if ( ! empty($addition)) {
438
                    $conf .= $appClass->confBlockWithComments($addition);
439
                }
440
            }
441
            $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(' . $rout['providerid'] . '-outgoing-after-dial-custom,${EXTEN}),1}" == "1"]?' . $rout['providerid'] . '-outgoing-after-dial-custom,${EXTEN},1)' . "\n\t";
442
443
            $conf .= 'same => n,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\t";
444
            $conf .= 'same => n,ExecIf($["${DIALSTATUS}" = "ANSWER"]?Hangup())' . "\n\t";
445
            $conf .= 'same => n,Set(pt1c_UNIQUEID=${EMPTY_VALUE})' . "\n\t";
446
            $conf .= 'same => n,return' . "\n";
447
        }
448
    }
449
450
    /**
451
     * Генератор extension для контекста outgoing.
452
     *
453
     * @param string $uniqueID
454
     *
455
     * @return null|string
456
     */
457
    public function getTechByID(string $uniqueID): string
458
    {
459
        $technology = '';
460
        $provider   = Providers::findFirstByUniqid($uniqueID);
461
        if ($provider !== null) {
462
            if ($provider->type === 'SIP') {
463
                $account    = Sip::findFirst('disabled="0" AND uniqid = "' . $uniqueID . '"');
464
                $technology = ($account === null) ? '' : SIPConf::getTechnology();
465
            } elseif ($provider->type === 'IAX') {
466
                $account    = Iax::findFirst('disabled="0" AND uniqid = "' . $uniqueID . '"');
467
                $technology = ($account === null) ? '' : 'IAX2';
468
            }
469
        }
470
471
        return $technology;
472
    }
473
474
    /**
475
     * Генератор исходящего маршрута.
476
     *
477
     * @param $rout
478
     *
479
     * @return string
480
     */
481
    private function generateOutgoingRegexPattern($rout): string
482
    {
483
        $conf         = '';
484
        $regexPattern = '';
485
        
486
        $restNumbers = (int) ($rout['restnumbers']??0);
487
        if ($restNumbers > 0) {
488
            $regexPattern = "[0-9]{" . $rout['restnumbers'] . "}$";
489
        } elseif ($restNumbers === 0) {
490
            $regexPattern = "$";
491
        } elseif ($restNumbers === -1) {
492
            $regexPattern = "";
493
        }
494
        $numberBeginsWith = $rout['numberbeginswith']??'';
495
        $numberBeginsWith = str_replace(array('*', '+'), array('\\\\*', '\\\\+'), $numberBeginsWith);
496
        $conf            .= 'same => n,ExecIf($["${REGEX("^' . $numberBeginsWith . $regexPattern . '" ${EXTEN})}" == "1"]?Gosub(' . $rout['providerid'] . '-' . $rout['id'] . '-outgoing,${EXTEN},1))' . " \n\t";
497
498
        return $conf;
499
    }
500
501
    /**
502
     * Контекст для входящих внешних звонков без авторизации.
503
     *
504
     * @param $conf
505
     */
506
    public function generatePublicContext(&$conf): void
507
    {
508
        $additionalModules = $this->di->getShared(PBXConfModulesProvider::SERVICE_NAME);
509
        $conf              .= "\n";
510
        $conf              .= self::generateIncomingContextPeers('none');
511
        $conf              .= "[public-direct-dial] \n";
512
        foreach ($additionalModules as $appClass) {
513
            if ($appClass instanceof $this) {
514
                continue;
515
            }
516
            $appClass->generatePublicContext($conf);
517
        }
518
        $filter = ["provider IS NULL AND priority<>9999"];
519
520
        /**
521
         * @var array
522
         */
523
        $m_data = IncomingRoutingTable::find($filter);
524
        if (count($m_data->toArray()) > 0) {
525
            $conf .= 'include => none-incoming';
526
        }
527
    }
528
529
    /**
530
     * Генератор входящих контекстов.
531
     *
532
     * @param string | array $provider
533
     * @param string | array $login
534
     * @param string         $uniqid
535
     *
536
     * @return string
537
     */
538
    public static function generateIncomingContextPeers($provider, $login = '', $uniqid = ''): string
539
    {
540
        $conf     = '';
541
        $dialplan = [];
542
        $di       = Di::getDefault();
543
        if ($di === null) {
544
            return '';
545
        }
546
        $additionalModules = $di->getShared(PBXConfModulesProvider::SERVICE_NAME);
547
        $confExtensions    = ConferenceConf::getConferenceExtensions();
548
549
        if ('none' === $provider) {
550
            // Звонки по sip uri.
551
            $filter = [
552
                'provider IS NULL AND priority<>9999',
553
                'order' => 'provider,priority,extension',
554
            ];
555
        } elseif (is_array($provider)) {
556
            $filter = [
557
                'provider IN ({provider:array})',
558
                'bind'  => [
559
                    'provider' => array_keys($provider),
560
                ],
561
                'order' => 'provider,priority,extension',
562
            ];
563
        } else {
564
            // Звонки через провайдера.
565
            $filter = [
566
                "provider = '$provider'",
567
                'order' => 'provider,priority,extension',
568
            ];
569
        }
570
        /** @var IncomingRoutingTable $default_action */
571
        $default_action = IncomingRoutingTable::findFirst('priority = 9999');
572
        /** @var IncomingRoutingTable $m_data */
573
        $m_data = IncomingRoutingTable::find($filter);
574
        $data   = $m_data->toArray();
575
        uasort($data, __CLASS__ . '::sortArrayByPriority');
576
577
        $need_def_rout = true;
578
        foreach ($data as $rout) {
579
            $number = trim($rout['number']);
580
            if ($number === 'X!' || $number === '') {
581
                $need_def_rout = false;
582
                break;
583
            }
584
        }
585
        if ($need_def_rout === true && 'none' !== $provider) {
586
            $data[] = ['number' => '', 'extension' => '', 'timeout' => ''];
587
        }
588
        $config = new MikoPBXConfig();
589
        $lang   = str_replace('_', '-', $config->getGeneralSettings('PBXLanguage'));
590
591
        $rout_data_dial = [];
592
        foreach ($data as $rout) {
593
            $number      = trim($rout['number']);
594
            $timeout     = trim($rout['timeout']);
595
            $rout_number = ($number === '') ? 'X!' : $number;
596
            $rout_data   = &$dialplan[$rout_number];
597
            if (empty($rout_data)) {
598
                $ext_prefix = ('none' === $provider) ? '' : '_';
599
                $rout_data  .= "exten => {$ext_prefix}{$rout_number},1,NoOp(--- Incoming call ---)\n\t";
600
                $rout_data  .= 'same => n,Set(CHANNEL(language)=' . $lang . ')' . "\n\t";
601
                $rout_data  .= 'same => n,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . "\n\t";
602
                $rout_data  .= 'same => n,Set(__FROM_DID=${EXTEN})' . "\n\t";
603
                $rout_data  .= 'same => n,Set(__FROM_CHAN=${CHANNEL})' . "\n\t";
604
605
                // Установка имени пира.
606
                $rout_data .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" != "Local"]?Gosub(set_from_peer,s,1))' . "\n\t";
607
                $rout_data .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local"]?Set(__FROM_PEER=${CALLERID(num)}))' . "\n\t";
608
609
                $rout_data .= 'same => n,Gosub(add-trim-prefix-clid,${EXTEN},1)'."\n\t";
610
                // Проверим распискние для входящих внешних звонков.
611
                $rout_data .= 'same => n,Gosub(check-out-work-time,${EXTEN},1)'."\n\t";
612
                foreach ($additionalModules as $appClass) {
613
                    $addition = $appClass->generateIncomingRoutBeforeDial($rout_number);
614
                    if ( ! empty($addition)) {
615
                        $rout_data .= $appClass->confBlockWithComments($addition);
616
                    }
617
                }
618
                // Описываем возможность прыжка в пользовательский sub контекст.
619
                $rout_data .= " \n\t" . 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)';
620
            }
621
622
            if ( ! empty($rout['extension'])) {
623
                $rout_data = rtrim($rout_data);
624
                // Обязательно проверяем "DIALSTATUS", в случае с парковой через AMI вызова это необходимо.
625
                // При ответе может отработать следующий приоритет.
626
                if ( ! isset($rout_data_dial[$rout_number])) {
627
                    $rout_data_dial[$rout_number] = '';
628
                }
629
630
                if (in_array($rout['extension'], $confExtensions, true)) {
631
                    // Это конференция. Тут не требуется обработка таймаута ответа.
632
                    // Вызов будет отвечен сразу конференцией.
633
                    $dial_command                 = " \n\t" . 'same => n,' . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Goto(internal,{$rout['extension']},1));";
634
                    $rout_data_dial[$rout_number] .= "";
635
                } else {
636
                    $dial_command                 = " \n\t" . 'same => n,' . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Dial(Local/{$rout['extension']}@internal-incoming/n,{$timeout},TKg));";
637
                    $rout_data_dial[$rout_number] .= " \n\t" . "same => n,Set(M_TIMEOUT={$timeout})";
638
                }
639
                $rout_data_dial[$rout_number] .= $dial_command;
640
641
                if (is_array($provider)) {
642
                    $key = $provider[$rout['provider']] ?? '';
643
                    if ( ! isset($rout_data_dial[$key])) {
644
                        $rout_data_dial[$key] = '';
645
                    }
646
                    if (empty($number)) {
647
                        $rout_data_dial[$key] .= $dial_command;
648
                    }
649
                }
650
            }
651
        }
652
653
        if (is_string($login)) {
654
            $add_login_pattern = ! empty($login);
655
            foreach ($data as $rout) {
656
                if ( ! $add_login_pattern) {
657
                    break;
658
                } // Логин не заполнен, обработка не требуется.
659
                $is_num = preg_match_all('/^\d+$/m', $login, $matches, PREG_SET_ORDER);
660
                if ($is_num === 1) {
661
                    // Это числовой номер, потому, не требуется дополнительно описывать exten.
662
                    $add_login_pattern = false;
663
                    break;
664
                }
665
                if (trim($rout['number']) !== $login) {
666
                    // Совпадение exten не найдено. Идем дальше.
667
                    continue;
668
                }
669
                // Совпадение найдено, не требуется дополнительно описывать exten.
670
                $add_login_pattern = false;
671
                break;
672
            }
673
            if ($add_login_pattern && array_key_exists('X!', $rout_data_dial) && isset($dialplan['X!'])) {
674
                $dialplan[$login]       = str_replace('_X!,1', "{$login},1", $dialplan['X!']);
675
                $rout_data_dial[$login] = $rout_data_dial['X!'];
676
            } elseif ($add_login_pattern === true && $need_def_rout === true && count($data) === 1) {
677
                // Только маршрут "По умолчанию".
678
                $dialplan[$login] = str_replace('_X!,1', "{$login},1", $dialplan['X!']);
679
            }
680
        } elseif (is_array($provider)) {
681
            foreach (array_values($provider) as $_login) {
682
                $dialplan[$_login] = str_replace('_X!,1', "{$_login},1", $dialplan['X!']);
683
            }
684
        }
685
686
        foreach ($dialplan as $key => &$dpln) {
687
            if ( ! array_key_exists($key, $rout_data_dial)) {
688
                continue;
689
            }
690
            $dpln = rtrim($dpln);
691
            $dpln .= $rout_data_dial[$key];
692
        }
693
        unset($dpln);
694
695
        $uniqid = is_string($provider) ? $provider : $uniqid;
696
        $conf   .= "\n" . "[{$uniqid}-incoming]\n";
697
        foreach ($dialplan as $dpln) {
698
            $conf .= $dpln . "\n";
699
            if (null === $default_action && 'none' !== $provider) {
700
                continue;
701
            }
702
            if ('extension' === $default_action->action) {
703
                // Обязательно проверяем "DIALSTATUS", в случае с парковой через AMI вызова это необходимо.
704
                // При ответе может отработать следующий приоритет.
705
                $conf .= "\t" . 'same => n,Set(M_TIMEOUT=0)' . "\n";
706
                if (in_array($default_action->extension, $confExtensions, true)) {
707
                    // Это конференция. Тут не требуется обработка таймаута ответа.
708
                    // Вызов будет отвечен сразу конференцией.
709
                    $conf .= "\t" . "same => n," . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Goto(internal,{$default_action->extension},1)); default action" . "\n";
710
                } else {
711
                    $conf .= "\t" . "same => n," . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Dial(Local/{$default_action->extension}@internal/n,,TKg)); default action" . "\n";
712
                }
713
                foreach ($additionalModules as $appClass) {
714
                    $addition = $appClass->generateIncomingRoutAfterDialContext($uniqid);
715
                    if ( ! empty($addition)) {
716
                        $conf .= $appClass->confBlockWithComments($addition);
717
                    }
718
                }
719
                $conf .= " \t" . 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-after-dial-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-after-dial-custom,${EXTEN},1)' . "\n";
720
            } elseif ('busy' === $default_action->action) {
721
                $conf .= "\t" . "same => n,Busy()" . "\n";
722
            }
723
            $conf .= "\t" . "same => n,Hangup()" . "\n";
724
        }
725
726
        return $conf;
727
    }
728
729
}