diff --git a/public/api.php b/public/api.php index 0431da7..c86c05e 100644 --- a/public/api.php +++ b/public/api.php @@ -1155,6 +1155,7 @@ try { $send_rpid = (int)!empty($payload['send_rpid']); $send_diversion = isset($payload['send_diversion']) ? (int)!empty($payload['send_diversion']) : 1; $trust_id_inbound = isset($payload['trust_id_inbound']) ? (int)!empty($payload['trust_id_inbound']) : 1; + $trust_id_outbound = isset($payload['trust_id_outbound']) ? (int)!empty($payload['trust_id_outbound']) : 1; $max_retries = max(0, (int)($payload['max_retries'] ?? 10000)); $forbidden_retry_interval = max(0, (int)($payload['forbidden_retry_interval'] ?? 600)); $fatal_retry_interval = max(0, (int)($payload['fatal_retry_interval'] ?? 0)); @@ -1228,6 +1229,7 @@ try { ':send_rpid' => $send_rpid, ':send_diversion' => $send_diversion, ':trust_id_inbound' => $trust_id_inbound, + ':trust_id_outbound' => $trust_id_outbound, ':codecs' => $codecs, ':dtmf_mode' => $dtmf_mode, ':media_encryption' => $media_encryption, @@ -1269,7 +1271,7 @@ try { fatal_retry_interval = :fatal_retry_interval, line_support = :line_support, from_user = :from_user, from_domain = :from_domain, outbound_callerid = :outbound_callerid, send_pai = :send_pai, send_rpid = :send_rpid, send_diversion = :send_diversion, - trust_id_inbound = :trust_id_inbound, codecs = :codecs, dtmf_mode = :dtmf_mode, + trust_id_inbound = :trust_id_inbound, trust_id_outbound = :trust_id_outbound, codecs = :codecs, dtmf_mode = :dtmf_mode, media_encryption = :media_encryption, nat_symmetric = :nat_symmetric, direct_media = :direct_media, qualify_frequency = :qualify_frequency, max_channels = :max_channels, timers = :timers, language = :language, @@ -1291,7 +1293,7 @@ try { registration_mode, register, server_uri, client_uri, contact_user, expiration, retry_interval, max_retries, forbidden_retry_interval, fatal_retry_interval, line_support, from_user, from_domain, outbound_callerid, send_pai, send_rpid, send_diversion, - trust_id_inbound, codecs, dtmf_mode, media_encryption, nat_symmetric, direct_media, + trust_id_inbound, trust_id_outbound, 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, @@ -1301,7 +1303,7 @@ try { :registration_mode, :register, :server_uri, :client_uri, :contact_user, :expiration, :retry_interval, :max_retries, :forbidden_retry_interval, :fatal_retry_interval, :line_support, :from_user, :from_domain, :outbound_callerid, :send_pai, :send_rpid, :send_diversion, - :trust_id_inbound, :codecs, :dtmf_mode, :media_encryption, :nat_symmetric, :direct_media, + :trust_id_inbound, :trust_id_outbound, :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, @@ -1318,7 +1320,7 @@ try { fatal_retry_interval = excluded.fatal_retry_interval, line_support = excluded.line_support, from_user = excluded.from_user, from_domain = excluded.from_domain, outbound_callerid = excluded.outbound_callerid, send_pai = excluded.send_pai, send_rpid = excluded.send_rpid, send_diversion = excluded.send_diversion, - trust_id_inbound = excluded.trust_id_inbound, codecs = excluded.codecs, dtmf_mode = excluded.dtmf_mode, + trust_id_inbound = excluded.trust_id_inbound, trust_id_outbound = excluded.trust_id_outbound, codecs = excluded.codecs, dtmf_mode = excluded.dtmf_mode, media_encryption = excluded.media_encryption, nat_symmetric = excluded.nat_symmetric, direct_media = excluded.direct_media, qualify_frequency = excluded.qualify_frequency, max_channels = excluded.max_channels, timers = excluded.timers, language = excluded.language, diff --git a/public/assets/app.js b/public/assets/app.js index 0534a93..93274ac 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -2355,6 +2355,7 @@ function openTrunkModal(trunk = null) { form.elements['send_rpid'].checked = trunk ? Number(trunk.send_rpid ?? 0) === 1 : false; form.elements['send_diversion'].checked = trunk ? Number(trunk.send_diversion ?? 1) === 1 : true; form.elements['trust_id_inbound'].checked = trunk ? Number(trunk.trust_id_inbound ?? 1) === 1 : true; + form.elements['trust_id_outbound'].checked = trunk ? Number(trunk.trust_id_outbound ?? 1) === 1 : true; // Onglet 3 : Codecs, NAT, Timers, T.38 & Identify form.elements['codecs'].value = trunk ? (trunk.codecs || 'alaw,ulaw,g722') : 'alaw,ulaw,g722'; @@ -3788,7 +3789,8 @@ document.addEventListener('DOMContentLoaded', () => { payload.send_pai = form.elements['send_pai'].checked ? 1 : 0; payload.send_rpid = form.elements['send_rpid'].checked ? 1 : 0; payload.send_diversion = form.elements['send_diversion'].checked ? 1 : 0; - payload.trust_id_inbound = form.elements['trust_id_inbound'].checked ? 1 : 0; + payload.trust_id_inbound = form.elements['trust_id_inbound'].checked ? 1 : 0; + payload.trust_id_outbound = form.elements['trust_id_outbound'].checked ? 1 : 0; payload.nat_symmetric = form.elements['nat_symmetric'].checked ? 1 : 0; payload.direct_media = form.elements['direct_media'].checked ? 1 : 0; payload.inband_progress = form.elements['inband_progress'].checked ? 1 : 0; diff --git a/public/index.php b/public/index.php index af2f208..6f22497 100644 --- a/public/index.php +++ b/public/index.php @@ -3949,6 +3949,10 @@ $appVersion = $appVerData['version'] ?? '1.5.0'; Accepter l'identité entrante PAI/RPID (trust_id_inbound = yes) + diff --git a/src/ConfigGenerator.php b/src/ConfigGenerator.php index 1e8244d..7e86e23 100644 --- a/src/ConfigGenerator.php +++ b/src/ConfigGenerator.php @@ -302,6 +302,7 @@ class ConfigGenerator $sendRpid = ((int)($trunk['send_rpid'] ?? 0) === 1) ? 'yes' : 'no'; $sendDiversion = ((int)($trunk['send_diversion'] ?? 1) === 1) ? 'yes' : 'no'; $trustInbound = ((int)($trunk['trust_id_inbound'] ?? 1) === 1) ? 'yes' : 'no'; + $trustOutbound = (!isset($trunk['trust_id_outbound']) || (int)$trunk['trust_id_outbound'] === 1) ? 'yes' : 'no'; $dtmfMode = trim((string)($trunk['dtmf_mode'] ?? 'rfc4733')) ?: 'rfc4733'; $mediaEnc = trim((string)($trunk['media_encryption'] ?? 'no')) ?: 'no'; $natSym = ((int)($trunk['nat_symmetric'] ?? 1) === 1) ? 'yes' : 'no'; @@ -354,7 +355,18 @@ class ConfigGenerator $lines[] = "outbound_proxy = {$outboundProxy}"; } if ($callerId !== '') { - $lines[] = "callerid = {$callerId}"; + if (preg_match('/^(?:\"?([^\"<>]+?)\"?\s*)?<([^>]+)>$/u', $callerId, $cm)) { + $cName = trim($cm[1] ?? ''); + $cNum = trim($cm[2] ?? ''); + if ($cName !== '') { + $cNameClean = str_replace('"', '', $cName); + $lines[] = "callerid = \"{$cNameClean}\" <{$cNum}>"; + } else { + $lines[] = "callerid = <{$cNum}>"; + } + } else { + $lines[] = "callerid = {$callerId}"; + } } $lines[] = "disallow = all"; foreach ($codecs as $codec) { @@ -370,6 +382,7 @@ class ConfigGenerator $lines[] = "send_rpid = {$sendRpid}"; $lines[] = "send_diversion = {$sendDiversion}"; $lines[] = "trust_id_inbound = {$trustInbound}"; + $lines[] = "trust_id_outbound = {$trustOutbound}"; $lines[] = "timers = {$timers}"; if ($rtpKeepalive > 0) { $lines[] = "rtp_keepalive = {$rtpKeepalive}"; @@ -1158,7 +1171,9 @@ class ConfigGenerator // 3. Positionnement du CallerID Sortant de la route if ($rCid !== '') { - $sub[] = " same => n,Set(CALLERID(all)={$rCid})"; + foreach ($this->buildCallerIdDialplan($rCid) as $cidStmt) { + $sub[] = $cidStmt; + } } // 4. Séquence de Trunks avec bascule automatique (Failover) @@ -1173,7 +1188,9 @@ class ConfigGenerator $tFallbackCid = $tObj ? trim((string)($tObj['outbound_callerid'] ?? '')) : ''; if ($rCid === '' && $tFallbackCid !== '' && $overrideCid) { - $sub[] = " same => n,Set(CALLERID(all)={$tFallbackCid})"; + foreach ($this->buildCallerIdDialplan($tFallbackCid) as $cidStmt) { + $sub[] = $cidStmt; + } } $validTrunkCount++; @@ -2257,5 +2274,60 @@ class ConfigGenerator $slug = preg_replace('/[^a-z0-9]+/', '-', $slug); return trim($slug, '-') ?: 'default'; } + + /** + * Formate les instructions Dialplan pour le positionnement du Caller ID (Nom, Numéro, Présentation, CallerID all) + * afin de garantir la transmission du CNAM (Caller ID Name) et du CLI par Asterisk PJSIP. + * + * @param string $cidString Chaîne du type '"Sytelis Consulting" <0422560185>', 'Sytelis <0422560185>', '0422560185' ou 'Sytelis' + * @param string $indent + * @return array + */ + private function buildCallerIdDialplan(string $cidString, string $indent = ' same => n,'): array + { + $cidString = trim($cidString); + if ($cidString === '') { + return []; + } + + $name = ''; + $num = ''; + + // Formats reconnus : + // 1. "Nom Complet" <0422560185> ou Nom Complet <0422560185> + if (preg_match('/^(?:\"?([^\"<>]+?)\"?\s*)?<([^>]+)>$/u', $cidString, $m)) { + $name = trim($m[1] ?? ''); + $num = trim($m[2] ?? ''); + } elseif (preg_match('/^<([^>]+)>$/', $cidString, $m)) { + $num = trim($m[1]); + } elseif (preg_match('/^\+?[0-9*#]+$/', $cidString)) { + $num = $cidString; + } else { + // Uniquement un nom + $name = trim($cidString, " \t\n\r\0\x0B\""); + } + + $lines = []; + if ($num !== '') { + $lines[] = "{$indent}Set(CALLERID(num)={$num})"; + $lines[] = "{$indent}Set(CALLERID(num-pres)=allowed_passed_screen)"; + } + if ($name !== '') { + $cleanName = str_replace('"', '', $name); + $lines[] = "{$indent}Set(CALLERID(name)=\"{$cleanName}\")"; + $lines[] = "{$indent}Set(CALLERID(name-pres)=allowed_passed_screen)"; + } + if ($name !== '' && $num !== '') { + $cleanName = str_replace('"', '', $name); + $lines[] = "{$indent}Set(CALLERID(all)=\"{$cleanName}\" <{$num}>)"; + } elseif ($num !== '') { + $lines[] = "{$indent}Set(CALLERID(all)={$num})"; + } elseif ($name !== '') { + $cleanName = str_replace('"', '', $name); + $lines[] = "{$indent}Set(CALLERID(all)=\"{$cleanName}\")"; + } + + return $lines; + } } diff --git a/src/Database.php b/src/Database.php index 10e0eba..ff74a2e 100644 --- a/src/Database.php +++ b/src/Database.php @@ -150,6 +150,7 @@ class Database send_pai INTEGER NOT NULL DEFAULT 1 CHECK (send_pai IN (0, 1)), send_rpid INTEGER NOT NULL DEFAULT 0 CHECK (send_rpid IN (0, 1)), trust_id_inbound INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_inbound IN (0, 1)), + trust_id_outbound INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_outbound IN (0, 1)), codecs VARCHAR(64) NOT NULL DEFAULT 'alaw,ulaw,g722', dtmf_mode VARCHAR(16) NOT NULL DEFAULT 'rfc4733', media_encryption VARCHAR(16) NOT NULL DEFAULT 'no', @@ -525,6 +526,7 @@ class Database 'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0", 'cid_name_prefix' => "VARCHAR(64) NOT NULL DEFAULT ''", 'dial_options' => "VARCHAR(32) NOT NULL DEFAULT 'tTR'", + 'trust_id_outbound' => "INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_outbound IN (0, 1))", ]; foreach ($extraColumns as $colName => $colDef) { diff --git a/version.json b/version.json index 92da145..ccf03e0 100644 --- a/version.json +++ b/version.json @@ -1,9 +1,10 @@ { - "version": "1.6.2", + "version": "1.6.3", "codename": "Helios", "release_date": "2026-11-05", "min_php_version": "8.2", "changelog": [ + "Trunks SIP & Routage Sortant : activation systématique de trust_id_outbound et formatage strict du Caller ID Name (CNAM) pour affichage garanti chez les opérateurs distants (OVH, Orange, etc.)", "Sauvegarde & Restauration : versionnage dynamique automatique basé sur la version installée (résolution du tag figé v1.2.0)", "Mises à jour système OTA : déclenchement garanti d'une sauvegarde Disaster Recovery complète pré-mise à jour (visible et restaurable dans l'interface)", "Interface Sauvegardes : nouveau badge identifiant la source (Pré-MàJ, Auto, Manuel)",