From 8804501c42ace12ef64c93207ecdb5f552846954 Mon Sep 17 00:00:00 2001 From: sorcier Date: Tue, 3 Nov 2026 20:01:16 +0100 Subject: [PATCH] feat(inbound): add CID name prefix support per trunk and per inbound route --- public/api.php | 11 ++- public/assets/app.js | 19 ++++- public/index.php | 17 +++++ src/ConfigGenerator.php | 151 +++++++++++++++++++++++++++++++++++++--- src/Database.php | 2 + 5 files changed, 188 insertions(+), 12 deletions(-) diff --git a/public/api.php b/public/api.php index e11376e..e404a32 100644 --- a/public/api.php +++ b/public/api.php @@ -1132,6 +1132,7 @@ try { $custom_reg_params = trim((string)($payload['custom_reg_params'] ?? '')); $custom_aor_params = trim((string)($payload['custom_aor_params'] ?? '')); $moh_sound_id = max(0, (int)($payload['moh_sound_id'] ?? 0)); + $cid_name_prefix = trim((string)($payload['cid_name_prefix'] ?? '')); $enabled = isset($payload['enabled']) ? (int)!empty($payload['enabled']) : 1; $didRoutesRaw = is_array($payload['did_routes'] ?? null) ? $payload['did_routes'] : []; @@ -1201,6 +1202,7 @@ try { ':custom_reg_params' => $custom_reg_params, ':custom_aor_params' => $custom_aor_params, ':moh_sound_id' => $moh_sound_id, + ':cid_name_prefix' => $cid_name_prefix, ':enabled' => $enabled, ]; @@ -1227,7 +1229,7 @@ try { inbound_dest = :inbound_dest, did_routes_json = :did_routes_json, outbound_prefix = :outbound_prefix, custom_endpoint_params = :custom_endpoint_params, custom_reg_params = :custom_reg_params, custom_aor_params = :custom_aor_params, - moh_sound_id = :moh_sound_id, + moh_sound_id = :moh_sound_id, cid_name_prefix = :cid_name_prefix, enabled = :enabled WHERE id = :id '); @@ -1242,7 +1244,8 @@ try { trust_id_inbound, codecs, dtmf_mode, media_encryption, nat_symmetric, direct_media, qualify_frequency, max_channels, timers, language, rtp_keepalive, t38_udptl, fax_detect, inband_progress, match_ips, context, inbound_dest, did_routes_json, - outbound_prefix, custom_endpoint_params, custom_reg_params, custom_aor_params, moh_sound_id, enabled + outbound_prefix, custom_endpoint_params, custom_reg_params, custom_aor_params, moh_sound_id, + cid_name_prefix, enabled ) VALUES ( :name, :host, :port, :outbound_proxy, :transport, :auth_mode, :username, :auth_username, :secret, :registration_mode, :register, :server_uri, :client_uri, :contact_user, :expiration, :retry_interval, @@ -1251,7 +1254,8 @@ try { :trust_id_inbound, :codecs, :dtmf_mode, :media_encryption, :nat_symmetric, :direct_media, :qualify_frequency, :max_channels, :timers, :language, :rtp_keepalive, :t38_udptl, :fax_detect, :inband_progress, :match_ips, :context, :inbound_dest, :did_routes_json, - :outbound_prefix, :custom_endpoint_params, :custom_reg_params, :custom_aor_params, :moh_sound_id, :enabled + :outbound_prefix, :custom_endpoint_params, :custom_reg_params, :custom_aor_params, :moh_sound_id, + :cid_name_prefix, :enabled ) ON CONFLICT(name) DO UPDATE SET host = excluded.host, port = excluded.port, outbound_proxy = excluded.outbound_proxy, @@ -1274,6 +1278,7 @@ try { outbound_prefix = excluded.outbound_prefix, custom_endpoint_params = excluded.custom_endpoint_params, custom_reg_params = excluded.custom_reg_params, custom_aor_params = excluded.custom_aor_params, moh_sound_id = excluded.moh_sound_id, + cid_name_prefix = excluded.cid_name_prefix, enabled = excluded.enabled '); $stmt->execute($params); diff --git a/public/assets/app.js b/public/assets/app.js index f27f129..8823b1f 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -1646,6 +1646,7 @@ function renderTrunksTable() {
→ ${escapeHtml(t.inbound_dest)}
+ ${t.cid_name_prefix ? `CID: ${escapeHtml(t.cid_name_prefix)}` : ''} ${didCount > 0 ? `${didCount} règle(s) entrante(s)` : 'Tous numéros (Any)'} ${blockCount > 0 ? `${blockCount} blocage(s) CID` : ''} ${tcCount > 0 ? `⏱️ ${tcCount} horaire(s)` : ''} @@ -1831,6 +1832,7 @@ function addTrunkInboundRouteRow(cid = 'Any', did = 'Any', description = '', des const tcFinish = (timeOpts && timeOpts.time_finish) ? timeOpts.time_finish : '18:00'; const tcWeekdays = (timeOpts && timeOpts.weekdays) ? timeOpts.weekdays : 'mon-fri'; const tcClosed = (timeOpts && timeOpts.time_dest_closed) ? timeOpts.time_dest_closed : 'terminate-hangup'; + const tcCidPrefix = (timeOpts && timeOpts.cid_prefix) ? timeOpts.cid_prefix : ''; const row = document.createElement('div'); row.className = 'trunk-did-row border-b border-anthracite-700/60 transition hover:bg-anthracite-800/40'; @@ -1899,6 +1901,15 @@ function addTrunkInboundRouteRow(cid = 'Any', did = 'Any', description = '', des
+ +
+ + + Ajouté devant le nom de l'appelant sur le téléphone (laisser vide pour utiliser le préfixe général du Trunk). +
`; @@ -2310,6 +2321,9 @@ function openTrunkModal(trunk = null) { // Contexte & Destination entrante (Totalement libres + assistés) form.elements['context'].value = trunk ? (trunk.context || 'from-pstn-toheader') : 'from-pstn-toheader'; + if (form.elements['cid_name_prefix']) { + form.elements['cid_name_prefix'].value = trunk ? (trunk.cid_name_prefix || '') : ''; + } populateInboundDestDropdown(trunk ? (trunk.inbound_dest || '101') : '101'); // Musique d'attente (MoH) spécifique à ce Trunk @@ -2368,7 +2382,8 @@ function openTrunkModal(trunk = null) { time_start: r.time_start || '08:30', time_finish: r.time_finish || '18:00', weekdays: r.weekdays || 'mon-fri', - time_dest_closed: r.time_dest_closed || 'terminate-hangup' + time_dest_closed: r.time_dest_closed || 'terminate-hangup', + cid_prefix: r.cid_prefix || '' } ); }); @@ -3712,6 +3727,7 @@ document.addEventListener('DOMContentLoaded', () => { const tcFinish = row.querySelector('.trunk-tc-finish')?.value || '18:00'; const weekdays = row.querySelector('.trunk-tc-weekdays')?.value || 'mon-fri'; const tcClosed = row.querySelector('.trunk-tc-closed-dest')?.value || 'terminate-hangup'; + const cidPrefix = row.querySelector('.trunk-did-prefix')?.value.trim() || ''; if (dest) { didRoutes.push({ @@ -3719,6 +3735,7 @@ document.addEventListener('DOMContentLoaded', () => { did, description: desc, destination: dest, + cid_prefix: cidPrefix, time_condition_enabled: tcEnabled, time_start: tcStart, time_finish: tcFinish, diff --git a/public/index.php b/public/index.php index 1e77f67..18651a4 100644 --- a/public/index.php +++ b/public/index.php @@ -3540,6 +3540,23 @@ $currentUser = Auth::getUsername(); + +
+
+ +

+ Préfixe inséré devant le nom ou numéro de l'appelant sur l'écran de vos téléphones IP pour identifier la ligne appelée (ex: [Livebox] , [Orange] , [Fixe] ). +

+
+
+ +
+
+
diff --git a/src/ConfigGenerator.php b/src/ConfigGenerator.php index 404e7aa..811cd1f 100644 --- a/src/ConfigGenerator.php +++ b/src/ConfigGenerator.php @@ -1309,16 +1309,19 @@ class ConfigGenerator // 2b-bis. Contextes standards FreePBX / Passerelles FXO : [from-trunk] et [from-pstn] $lines[] = "[from-trunk]"; - $lines[] = "exten => _[+*#0-9a-zA-Z].,1,NoOp(Appel entrant trunk [from-trunk] EXTEN=\${EXTEN} Appelant=\${CALLERID(num)})"; + $lines[] = "exten => _[+*#0-9a-zA-Z].,1,NoOp(Appel entrant trunk [from-trunk] EXTEN=\${EXTEN} Endpoint=\${CHANNEL(endpoint)} Appelant=\${CALLERID(num)})"; $lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:3}\" = \"+33\"]?Set(CALLERID(num)=0\${CALLERID(num):3}))"; $lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:4}\" = \"0033\"]?Set(CALLERID(num)=0\${CALLERID(num):4}))"; $lines[] = " same => n,GotoIf(\$[\"\${DB(blacklist/\${CALLERID(num)})}\" != \"\"]?app-blacklisted,s,1)"; - $lines[] = " same => n,ExecIf(\$[\"\${EXTEN:0:3}\" = \"+33\"]?Goto(from-external,0\${EXTEN:3},1))"; + $lines[] = " same => n,ExecIf(\$[\"\${EXTEN:0:3}\" = \"+33\"]?Set(EXTEN=0\${EXTEN:3}))"; + $lines[] = " same => n,GotoIf(\$[\"\${DIALPLAN_EXISTS(from-trunk-\${CHANNEL(endpoint)},\${EXTEN},1)}\" = \"1\"]?from-trunk-\${CHANNEL(endpoint)},\${EXTEN},1)"; + $lines[] = " same => n,GotoIf(\$[\"\${DIALPLAN_EXISTS(from-trunk-\${CHANNEL(endpoint)},s,1)}\" = \"1\"]?from-trunk-\${CHANNEL(endpoint)},s,1)"; $lines[] = " same => n,Goto(from-external,\${EXTEN},1)"; - $lines[] = "exten => s,1,NoOp(Appel entrant trunk [from-trunk] sur s - Appelant=\${CALLERID(num)})"; + $lines[] = "exten => s,1,NoOp(Appel entrant trunk [from-trunk] sur s - Endpoint=\${CHANNEL(endpoint)} Appelant=\${CALLERID(num)})"; $lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:3}\" = \"+33\"]?Set(CALLERID(num)=0\${CALLERID(num):3}))"; $lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:4}\" = \"0033\"]?Set(CALLERID(num)=0\${CALLERID(num):4}))"; $lines[] = " same => n,GotoIf(\$[\"\${DB(blacklist/\${CALLERID(num)})}\" != \"\"]?app-blacklisted,s,1)"; + $lines[] = " same => n,GotoIf(\$[\"\${DIALPLAN_EXISTS(from-trunk-\${CHANNEL(endpoint)},s,1)}\" = \"1\"]?from-trunk-\${CHANNEL(endpoint)},s,1)"; $lines[] = " same => n,Goto(from-external,s,1)"; $lines[] = ""; $lines[] = "[from-pstn]"; @@ -1326,6 +1329,30 @@ class ConfigGenerator $lines[] = "exten => s,1,Goto(from-trunk,s,1)"; $lines[] = ""; + // 2b-ter. Contextes dédiés par Trunk pour isolation des routes et préfixe CID + foreach ($trunks as $t) { + $epName = ($t['registration_mode'] === 'receive' && trim((string)($t['username'] ?? '')) !== '') + ? trim((string)$t['username']) + : trim((string)$t['name']); + if ($epName === '') { + continue; + } + + $trunkCtx = "from-trunk-{$epName}"; + $lines[] = "; --- Contexte Entrant dédié au Trunk {$t['name']} (Endpoint: {$epName}) ---"; + $lines[] = "[{$trunkCtx}]"; + foreach ($this->generateTrunkInboundRoutes($t, $mohSoundsMap, "trunk_{$t['id']}") as $l) { + $lines[] = $l; + } + $lines[] = ""; + if ($t['name'] !== $epName) { + $lines[] = "[from-trunk-{$t['name']}]"; + $lines[] = "exten => _[+*#0-9a-zA-Z].,1,Goto({$trunkCtx},\${EXTEN},1)"; + $lines[] = "exten => s,1,Goto({$trunkCtx},s,1)"; + $lines[] = ""; + } + } + // Contexte pour appels rejetés par la liste noire $lines[] = "[app-blacklisted]"; $lines[] = "exten => s,1,NoOp(Appelant \${CALLERID(num)} bloque par la liste noire)"; @@ -1352,6 +1379,7 @@ class ConfigGenerator $setMohLines[] = " same => n,Set(CHANNEL(musicclass)={$trunkMohClass})"; } + $trunkCidPrefix = trim((string)($t['cid_name_prefix'] ?? '')); $routes = self::normalizeInboundRoutes((string)($t['did_routes_json'] ?? '[]')); $routeIdx = 0; foreach ($routes as $rule) { @@ -1382,12 +1410,12 @@ class ConfigGenerator } $lines[] = "; Inbound Route [{$desc}] ({$t['name']}) : DID={$didPattern} / Appelant=Any{$timeInfo}"; $lines[] = "exten => {$didPattern},1,NoOp(Inbound [{$desc}] Appelant=\${CALLERID(num)})"; - foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_did") as $rl) { + foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_did", $trunkCidPrefix) as $rl) { $lines[] = $rl; } if ($isAnyDid) { $lines[] = "exten => s,1,NoOp(Inbound [{$desc}] sur s Appelant=\${CALLERID(num)})"; - foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_s") as $rl) { + foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_s", $trunkCidPrefix) as $rl) { $lines[] = $rl; } } @@ -1409,12 +1437,12 @@ class ConfigGenerator foreach ($cidVariants as $cidVar) { $vIdx++; $lines[] = "exten => {$didPattern}/{$cidVar},1,NoOp(Inbound Filter [{$desc}] CID=\${CALLERID(num)})"; - foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_cid_{$vIdx}") as $rl) { + foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_cid_{$vIdx}", $trunkCidPrefix) as $rl) { $lines[] = $rl; } if ($isAnyDid) { $lines[] = "exten => s/{$cidVar},1,NoOp(Inbound Filter [{$desc}] sur s CID=\${CALLERID(num)})"; - foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_scid_{$vIdx}") as $rl) { + foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_scid_{$vIdx}", $trunkCidPrefix) as $rl) { $lines[] = $rl; } } @@ -1918,6 +1946,7 @@ class ConfigGenerator 'did' => $did, 'description' => $desc ?: ($cid !== 'Any' ? "Filtre {$cid}" : "Route {$did}"), 'destination' => $dest, + 'cid_prefix' => trim((string)($item['cid_prefix'] ?? '')), 'time_condition_enabled' => (int)$timeCondition, 'time_start' => $timeStart, 'time_finish' => $timeFinish, @@ -1950,7 +1979,106 @@ class ConfigGenerator return $out; } - private function buildInboundRouteLines(array $rule, array $setMohLines, string $desc, string $tagId): array + private function generateTrunkInboundRoutes(array $t, array $mohSoundsMap, string $tagPrefix): array + { + $lines = []; + $mohSoundId = (int)($t['moh_sound_id'] ?? 0); + $trunkMohClass = ($mohSoundId > 0 && isset($mohSoundsMap[$mohSoundId])) + ? 'moh-sound-' . $mohSoundsMap[$mohSoundId]['slug'] + : 'default'; + + $setMohLines = []; + if ($trunkMohClass !== 'default') { + $setMohLines[] = " same => n,Set(__MOHCLASS={$trunkMohClass})"; + $setMohLines[] = " same => n,Set(CHANNEL(musicclass)={$trunkMohClass})"; + } + + $trunkCidPrefix = trim((string)($t['cid_name_prefix'] ?? '')); + $routes = self::normalizeInboundRoutes((string)($t['did_routes_json'] ?? '[]')); + $routeIdx = 0; + $hasAnyRoute = false; + + foreach ($routes as $rule) { + $routeIdx++; + $cidRaw = trim((string)($rule['cid'] ?? 'Any')); + $didRaw = trim((string)($rule['did'] ?? 'Any')); + $desc = trim((string)($rule['description'] ?? 'Route entrante')); + $dest = trim((string)($rule['destination'] ?? $t['inbound_dest'] ?? 'hangup')); + + $isAnyDid = ($didRaw === '' || strcasecmp($didRaw, 'any') === 0 || $didRaw === '*' || $didRaw === '_X.'); + $isAnyCid = ($cidRaw === '' || strcasecmp($cidRaw, 'any') === 0 || $cidRaw === '*'); + + $didPattern = $isAnyDid ? '_X.' : preg_replace('/[^0-9+XZN._\[\]\-]/', '', $didRaw); + if ($didPattern === '') { + $didPattern = '_X.'; + } + + $timeInfo = !empty($rule['time_condition_enabled']) + ? " [Horaires: {$rule['time_start']}-{$rule['time_finish']} {$rule['weekdays']}]" + : ""; + + if ($isAnyCid) { + if ($didPattern === '_X.') { + $hasAnyRoute = true; + } + $lines[] = "; Inbound Route [{$desc}] : DID={$didPattern} / Appelant=Any{$timeInfo}"; + $lines[] = "exten => {$didPattern},1,NoOp(Inbound [{$desc}] Appelant=\${CALLERID(num)})"; + foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$tagPrefix}_{$routeIdx}_did", $trunkCidPrefix) as $rl) { + $lines[] = $rl; + } + if ($isAnyDid) { + $lines[] = "exten => s,1,NoOp(Inbound [{$desc}] sur s Appelant=\${CALLERID(num)})"; + foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$tagPrefix}_{$routeIdx}_s", $trunkCidPrefix) as $rl) { + $lines[] = $rl; + } + } + } else { + $cidClean = preg_replace('/[^0-9+XZN._\[\]\-]/', '', $cidRaw); + if ($cidClean === '') { + continue; + } + $cidVariants = [$cidClean]; + if (str_starts_with($cidClean, '_0') && strlen($cidClean) > 2 && $cidClean[2] !== '0') { + $rest = substr($cidClean, 2); + $cidVariants[] = '_0033' . $rest; + $cidVariants[] = '_+33' . $rest; + } + + $lines[] = "; Inbound Route Filtrage Appelant [{$desc}] : DID={$didPattern} / CID={$cidClean}{$timeInfo}"; + $vIdx = 0; + foreach ($cidVariants as $cidVar) { + $vIdx++; + $lines[] = "exten => {$didPattern}/{$cidVar},1,NoOp(Inbound Filter [{$desc}] CID=\${CALLERID(num)})"; + foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$tagPrefix}_{$routeIdx}_cid_{$vIdx}", $trunkCidPrefix) as $rl) { + $lines[] = $rl; + } + if ($isAnyDid) { + $lines[] = "exten => s/{$cidVar},1,NoOp(Inbound Filter [{$desc}] sur s CID=\${CALLERID(num)})"; + foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$tagPrefix}_{$routeIdx}_scid_{$vIdx}", $trunkCidPrefix) as $rl) { + $lines[] = $rl; + } + } + } + } + } + + if (!$hasAnyRoute) { + $defaultDest = trim((string)($t['inbound_dest'] ?? 'hangup')) ?: 'hangup'; + $lines[] = "; Fallback Inbound Trunk {$t['name']}"; + $lines[] = "exten => _X.,1,NoOp(Inbound Trunk {$t['name']} par defaut vers \${EXTEN})"; + foreach ($this->buildInboundRouteLines(['destination' => $defaultDest], $setMohLines, "Fallback {$t['name']}", "{$tagPrefix}_fb", $trunkCidPrefix) as $rl) { + $lines[] = $rl; + } + $lines[] = "exten => s,1,NoOp(Inbound Trunk {$t['name']} sur s)"; + foreach ($this->buildInboundRouteLines(['destination' => $defaultDest], $setMohLines, "Fallback {$t['name']}", "{$tagPrefix}_fbs", $trunkCidPrefix) as $rl) { + $lines[] = $rl; + } + } + + return $lines; + } + + private function buildInboundRouteLines(array $rule, array $setMohLines, string $desc, string $tagId, string $trunkCidPrefix = ''): array { $dest = trim((string)($rule['destination'] ?? 'hangup')); $timeEnabled = !empty($rule['time_condition_enabled']); @@ -1965,6 +2093,13 @@ class ConfigGenerator $out[] = $sml; } + $cidPrefix = trim((string)($rule['cid_prefix'] ?? '')) ?: $trunkCidPrefix; + if ($cidPrefix !== '' && !str_starts_with($dest, 'terminate-') && $dest !== 'hangup' && $dest !== 'busy' && $dest !== 'congestion') { + $formattedPrefix = rtrim($cidPrefix) . ' '; + $out[] = " same => n,ExecIf(\$[\"\${CALLERID(name)}\" = \"\"]?Set(CALLERID(name)=\${CALLERID(num)}))"; + $out[] = " same => n,Set(CALLERID(name)={$formattedPrefix}\${CALLERID(name)})"; + } + if ($timeEnabled) { $tag = "tc_{$tagId}"; $out[] = " same => n,GotoIfTime({$timeRange},{$weekdays},*,*?{$tag}_open)"; diff --git a/src/Database.php b/src/Database.php index f178a7a..5fc63ed 100644 --- a/src/Database.php +++ b/src/Database.php @@ -163,6 +163,7 @@ class Database did_routes_json TEXT NOT NULL DEFAULT '{}', outbound_prefix VARCHAR(32) NOT NULL DEFAULT '', custom_endpoint_params TEXT NOT NULL DEFAULT '', + cid_name_prefix VARCHAR(64) NOT NULL DEFAULT '', enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -576,6 +577,7 @@ class Database 'custom_reg_params' => "TEXT NOT NULL DEFAULT ''", 'custom_aor_params' => "TEXT NOT NULL DEFAULT ''", 'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0", + 'cid_name_prefix' => "VARCHAR(64) NOT NULL DEFAULT ''", ]; foreach ($extraColumns as $colName => $colDef) {