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
+
+
+
+ 🏷️ Préfixe CID spécifique à cette règle :
+
+
+ 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 Nom Appelant (CID Name Prefix)
+ FreePBX CID Prefix
+
+
+ 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) {