Compare commits

..
6 Commits
13 changed files with 192 additions and 244 deletions

No files matched your search

+5
View File
@@ -38,3 +38,8 @@ Thumbs.db
.idea/ .idea/
.vscode/ .vscode/
# Environnement local de développement (exclu de Git)
/.dev
/.env
/.env.local
+12 -8
View File
@@ -47,30 +47,34 @@
```text ```text
nexus-pbx/ nexus-pbx/
├── install.sh # Script d'installation automatisé tout-en-un (Debian 12 / 13)
├── public/ # Racine Web Nginx ├── public/ # Racine Web Nginx
│ ├── index.php # Interface Web SPA moderne │ ├── index.php # Interface Web SPA moderne
│ ├── api.php # API RESTful backend │ ├── api.php # API RESTful backend
│ ├── phonebook.php # Générateur XML Yealink Remote Phonebook │ ├── phonebook.php # Générateur XML Yealink Remote Phonebook
│ ├── assets/ # JavaScript (app.js), styles Tailwind CSS │ ├── assets/ # Logo, Favicon, JS (app.js), styles Tailwind CSS
│ └── avatars/ # Photos des contacts de l'annuaire │ └── avatars/ # Photos des contacts de l'annuaire
├── src/ # Classes PHP d'orchestration ├── src/ # Classes PHP d'orchestration
│ ├── AmiClient.php # Client Asterisk Manager Interface (AMI) │ ├── AmiClient.php # Client Asterisk Manager Interface (AMI)
│ ├── Auth.php # Authentification & sessions │ ├── Auth.php # Authentification & sessions
│ ├── BackupManager.php # Moteur de sauvegarde locale & distante SSH │ ├── BackupManager.php # Moteur Disaster Recovery & sauvegardes
│ ├── CertificateManager.php # Gestionnaire SSL & Certbot │ ├── CdrManager.php # Moteur Journal d'appels & CDR
│ ├── CertificateManager.php # Gestionnaire SSL & Certbot ACME
│ ├── ConfigGenerator.php # Générateur dynamique de conf Asterisk (pjsip, extensions) │ ├── ConfigGenerator.php # Générateur dynamique de conf Asterisk (pjsip, extensions)
│ ├── Database.php # Connexion PDO SQLite │ ├── Database.php # Connexion PDO SQLite
│ ├── FirewallManager.php # Moteur Pare-feu & règles Nginx │ ├── FirewallManager.php # Moteur Pare-feu Fail2ban & Nginx
│ ├── PhonebookManager.php # Moteur d'annuaire et export XML │ ├── PhonebookManager.php # Moteur d'annuaire et export XML
│ ├── SystemChecker.php # Diagnostic de santé système │ ├── SystemChecker.php # Diagnostic de santé système
│ └── SystemMetrics.php # Monitoring CPU, RAM, Disque, Asterisk │ └── SystemMetrics.php # Monitoring CPU, RAM, Disque, Asterisk
├── scripts/ # Scripts CLI et daemons système ├── scripts/ # Scripts CLI et daemons système
│ ├── setup-debian.sh # Script d'installation automatisé Debian 12 / 13 │ ├── update.sh # Script de mise à jour OTA (nexus-update)
│ ├── nexus-firewall # Script de gestion iptables/nftables │ ├── nexus-firewall # Outil CLI Pare-feu & Fail2ban
│ └── nexus_autobackup.php# Cronjob de sauvegarde automatique │ ├── nexus-banner.sh # Bannière d'accueil interactive SSH/Console
│ ├── nexus_autobackup.php# Cronjob de sauvegarde automatique
│ └── voicemail_pass_sync.php # Synchronisation code PIN messagerie vocale
└── storage/ # Données persistantes (exclues du Git) └── storage/ # Données persistantes (exclues du Git)
├── database.sqlite # Base de données applicative ├── database.sqlite # Base de données applicative
├── backups/ # Archives de sauvegarde ├── system_backups/ # Archives de sauvegarde Disaster Recovery
└── sounds/ # Fichiers audio MoH et annonces └── sounds/ # Fichiers audio MoH et annonces
``` ```
+6 -4
View File
@@ -1155,6 +1155,7 @@ try {
$send_rpid = (int)!empty($payload['send_rpid']); $send_rpid = (int)!empty($payload['send_rpid']);
$send_diversion = isset($payload['send_diversion']) ? (int)!empty($payload['send_diversion']) : 1; $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_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)); $max_retries = max(0, (int)($payload['max_retries'] ?? 10000));
$forbidden_retry_interval = max(0, (int)($payload['forbidden_retry_interval'] ?? 600)); $forbidden_retry_interval = max(0, (int)($payload['forbidden_retry_interval'] ?? 600));
$fatal_retry_interval = max(0, (int)($payload['fatal_retry_interval'] ?? 0)); $fatal_retry_interval = max(0, (int)($payload['fatal_retry_interval'] ?? 0));
@@ -1228,6 +1229,7 @@ try {
':send_rpid' => $send_rpid, ':send_rpid' => $send_rpid,
':send_diversion' => $send_diversion, ':send_diversion' => $send_diversion,
':trust_id_inbound' => $trust_id_inbound, ':trust_id_inbound' => $trust_id_inbound,
':trust_id_outbound' => $trust_id_outbound,
':codecs' => $codecs, ':codecs' => $codecs,
':dtmf_mode' => $dtmf_mode, ':dtmf_mode' => $dtmf_mode,
':media_encryption' => $media_encryption, ':media_encryption' => $media_encryption,
@@ -1269,7 +1271,7 @@ try {
fatal_retry_interval = :fatal_retry_interval, line_support = :line_support, fatal_retry_interval = :fatal_retry_interval, line_support = :line_support,
from_user = :from_user, from_domain = :from_domain, outbound_callerid = :outbound_callerid, from_user = :from_user, from_domain = :from_domain, outbound_callerid = :outbound_callerid,
send_pai = :send_pai, send_rpid = :send_rpid, send_diversion = :send_diversion, 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, media_encryption = :media_encryption, nat_symmetric = :nat_symmetric,
direct_media = :direct_media, qualify_frequency = :qualify_frequency, direct_media = :direct_media, qualify_frequency = :qualify_frequency,
max_channels = :max_channels, timers = :timers, language = :language, 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, registration_mode, register, server_uri, client_uri, contact_user, expiration, retry_interval,
max_retries, forbidden_retry_interval, fatal_retry_interval, line_support, max_retries, forbidden_retry_interval, fatal_retry_interval, line_support,
from_user, from_domain, outbound_callerid, send_pai, send_rpid, send_diversion, 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, qualify_frequency, max_channels, timers, language, rtp_keepalive, t38_udptl,
fax_detect, inband_progress, match_ips, context, inbound_dest, did_routes_json, 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, 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, :registration_mode, :register, :server_uri, :client_uri, :contact_user, :expiration, :retry_interval,
:max_retries, :forbidden_retry_interval, :fatal_retry_interval, :line_support, :max_retries, :forbidden_retry_interval, :fatal_retry_interval, :line_support,
:from_user, :from_domain, :outbound_callerid, :send_pai, :send_rpid, :send_diversion, :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, :qualify_frequency, :max_channels, :timers, :language, :rtp_keepalive, :t38_udptl,
:fax_detect, :inband_progress, :match_ips, :context, :inbound_dest, :did_routes_json, :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, :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, 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, 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, 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, media_encryption = excluded.media_encryption, nat_symmetric = excluded.nat_symmetric,
direct_media = excluded.direct_media, qualify_frequency = excluded.qualify_frequency, direct_media = excluded.direct_media, qualify_frequency = excluded.qualify_frequency,
max_channels = excluded.max_channels, timers = excluded.timers, language = excluded.language, max_channels = excluded.max_channels, timers = excluded.timers, language = excluded.language,
+10 -1
View File
@@ -2355,6 +2355,7 @@ function openTrunkModal(trunk = null) {
form.elements['send_rpid'].checked = trunk ? Number(trunk.send_rpid ?? 0) === 1 : false; 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['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_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 // Onglet 3 : Codecs, NAT, Timers, T.38 & Identify
form.elements['codecs'].value = trunk ? (trunk.codecs || 'alaw,ulaw,g722') : 'alaw,ulaw,g722'; form.elements['codecs'].value = trunk ? (trunk.codecs || 'alaw,ulaw,g722') : 'alaw,ulaw,g722';
@@ -3789,6 +3790,7 @@ document.addEventListener('DOMContentLoaded', () => {
payload.send_rpid = form.elements['send_rpid'].checked ? 1 : 0; payload.send_rpid = form.elements['send_rpid'].checked ? 1 : 0;
payload.send_diversion = form.elements['send_diversion'].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.nat_symmetric = form.elements['nat_symmetric'].checked ? 1 : 0;
payload.direct_media = form.elements['direct_media'].checked ? 1 : 0; payload.direct_media = form.elements['direct_media'].checked ? 1 : 0;
payload.inband_progress = form.elements['inband_progress'].checked ? 1 : 0; payload.inband_progress = form.elements['inband_progress'].checked ? 1 : 0;
@@ -4922,7 +4924,13 @@ function renderBackups() {
? `<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">Local</span>` ? `<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">Local</span>`
: `<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-purple-500/20 text-purple-300 border border-purple-500/30">Distant (SSH)</span>`; : `<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-purple-500/20 text-purple-300 border border-purple-500/30">Distant (SSH)</span>`;
const versionBadge = `<span class="px-1.5 py-0.2 rounded text-[10px] font-mono font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v${escapeHtml(b.version || '1.2.0')}</span>`; const versionBadge = `<span class="px-1.5 py-0.2 rounded text-[10px] font-mono font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v${escapeHtml(b.version || '1.6.2')}</span>`;
let srcBadge = '';
if (b.source === 'pre-update') {
srcBadge = `<span class="px-1.5 py-0.2 rounded text-[10px] font-bold bg-amber-500/20 text-amber-300 border border-amber-500/30" title="Sauvegarde automatique pré-mise à jour">Pré-MàJ</span>`;
} else if (b.source === 'scheduled') {
srcBadge = `<span class="px-1.5 py-0.2 rounded text-[10px] font-bold bg-cyan-500/20 text-cyan-300 border border-cyan-500/30" title="Sauvegarde planifiée automatique">Auto</span>`;
}
return ` return `
<tr class="hover:bg-anthracite-750 transition"> <tr class="hover:bg-anthracite-750 transition">
@@ -4933,6 +4941,7 @@ function renderBackups() {
</svg> </svg>
<span class="truncate max-w-xs sm:max-w-md">${escapeHtml(b.filename)}</span> <span class="truncate max-w-xs sm:max-w-md">${escapeHtml(b.filename)}</span>
${versionBadge} ${versionBadge}
${srcBadge}
</div> </div>
</td> </td>
<td class="px-4 py-3 text-slate-300 whitespace-nowrap">${escapeHtml(b.date_formatted)}</td> <td class="px-4 py-3 text-slate-300 whitespace-nowrap">${escapeHtml(b.date_formatted)}</td>
+25 -2
View File
@@ -19,6 +19,10 @@ $hasBranding = !empty($companyName) || !empty($companyLogo);
$appVerData = file_exists(__DIR__ . '/../version.json') ? json_decode((string)file_get_contents(__DIR__ . '/../version.json'), true) : []; $appVerData = file_exists(__DIR__ . '/../version.json') ? json_decode((string)file_get_contents(__DIR__ . '/../version.json'), true) : [];
$appVersion = $appVerData['version'] ?? '1.5.0'; $appVersion = $appVerData['version'] ?? '1.5.0';
// Détection de l'environnement de développement : présence de .dev local ou branche git dev
$isDevEnv = file_exists(__DIR__ . '/../.dev')
|| (trim((string)@shell_exec('git -C ' . escapeshellarg(__DIR__) . ' rev-parse --abbrev-ref HEAD 2>/dev/null')) === 'dev');
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr" class="h-full"> <html lang="fr" class="h-full">
@@ -26,7 +30,7 @@ $appVersion = $appVerData['version'] ?? '1.5.0';
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="nexus-app-version" content="<?= htmlspecialchars($appVersion) ?>"> <meta name="nexus-app-version" content="<?= htmlspecialchars($appVersion) ?>">
<title>NexusPBX — Supervision & Téléphonie IP • Powered by Asterisk</title> <title><?= $isDevEnv ? '🛠️ [DEV] ' : '' ?>NexusPBX — Supervision & Téléphonie IP • Powered by Asterisk</title>
<link rel="icon" type="image/png" href="/assets/favicon.png?v=<?= file_exists(__DIR__ . '/assets/favicon.png') ? filemtime(__DIR__ . '/assets/favicon.png') : 1 ?>"> <link rel="icon" type="image/png" href="/assets/favicon.png?v=<?= file_exists(__DIR__ . '/assets/favicon.png') ? filemtime(__DIR__ . '/assets/favicon.png') : 1 ?>">
<link rel="apple-touch-icon" href="/assets/logo.png?v=<?= file_exists(__DIR__ . '/assets/logo.png') ? filemtime(__DIR__ . '/assets/logo.png') : 1 ?>"> <link rel="apple-touch-icon" href="/assets/logo.png?v=<?= file_exists(__DIR__ . '/assets/logo.png') ? filemtime(__DIR__ . '/assets/logo.png') : 1 ?>">
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -162,7 +166,12 @@ $appVersion = $appVerData['version'] ?? '1.5.0';
<div class="flex items-center gap-4 mb-6"> <div class="flex items-center gap-4 mb-6">
<img src="/assets/logo.png?v=<?= file_exists(__DIR__ . '/assets/logo.png') ? filemtime(__DIR__ . '/assets/logo.png') : 1 ?>" alt="NexusPBX Logo" class="w-20 h-20 sm:w-24 sm:h-24 object-contain shrink-0 filter drop-shadow-xl"> <img src="/assets/logo.png?v=<?= file_exists(__DIR__ . '/assets/logo.png') ? filemtime(__DIR__ . '/assets/logo.png') : 1 ?>" alt="NexusPBX Logo" class="w-20 h-20 sm:w-24 sm:h-24 object-contain shrink-0 filter drop-shadow-xl">
<div> <div>
<h1 class="text-2xl sm:text-3xl font-black text-white tracking-tight leading-none mb-1">NexusPBX</h1> <h1 class="text-2xl sm:text-3xl font-black text-white tracking-tight leading-none mb-1 flex items-center gap-2">
NexusPBX
<?php if ($isDevEnv): ?>
<span class="px-2 py-0.5 text-xs font-mono font-extrabold tracking-wider rounded bg-amber-500/20 text-amber-300 border border-amber-500/40">DEV</span>
<?php endif; ?>
</h1>
<p class="text-xs text-slate-300">Supervision & Téléphonie IP • Powered by Asterisk</p> <p class="text-xs text-slate-300">Supervision & Téléphonie IP • Powered by Asterisk</p>
</div> </div>
</div> </div>
@@ -212,6 +221,9 @@ $appVersion = $appVerData['version'] ?? '1.5.0';
Nexus<span class="text-indigo-400">PBX</span> Nexus<span class="text-indigo-400">PBX</span>
</div> </div>
<span id="app-version-badge" class="px-1.5 py-0.5 text-[10px] font-mono font-bold tracking-wider rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v<?= htmlspecialchars($appVersion) ?></span> <span id="app-version-badge" class="px-1.5 py-0.5 text-[10px] font-mono font-bold tracking-wider rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v<?= htmlspecialchars($appVersion) ?></span>
<?php if ($isDevEnv): ?>
<span class="px-1.5 py-0.5 text-[10px] font-mono font-black tracking-wider rounded bg-amber-500/25 text-amber-300 border border-amber-500/50 shadow-xs shadow-amber-500/20 animate-pulse">DEV</span>
<?php endif; ?>
</div> </div>
<div class="text-[10px] text-slate-400 font-medium tracking-wide mt-0.5">Powered by Asterisk</div> <div class="text-[10px] text-slate-400 font-medium tracking-wide mt-0.5">Powered by Asterisk</div>
</div> </div>
@@ -494,6 +506,13 @@ $appVersion = $appVerData['version'] ?? '1.5.0';
<!-- Boutons d'action globaux --> <!-- Boutons d'action globaux -->
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<?php if ($isDevEnv): ?>
<div class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-amber-500/15 text-amber-300 border border-amber-500/30">
<span class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
<span class="hidden md:inline font-mono">SERVEUR</span> DEV
</div>
<?php endif; ?>
<!-- Badge Notification Mise à Jour Disponible --> <!-- Badge Notification Mise à Jour Disponible -->
<button id="header-update-badge" type="button" onclick="openUpdatePanel()" style="display:none;" <button id="header-update-badge" type="button" onclick="openUpdatePanel()" style="display:none;"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 border border-amber-500/40 shadow-sm transition animate-pulse" title="Une mise à jour NexusPBX est disponible ! Cliquez pour voir les détails."> class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 border border-amber-500/40 shadow-sm transition animate-pulse" title="Une mise à jour NexusPBX est disponible ! Cliquez pour voir les détails.">
@@ -3949,6 +3968,10 @@ $appVersion = $appVerData['version'] ?? '1.5.0';
<input type="checkbox" name="trust_id_inbound" value="1" checked class="rounded bg-anthracite-900 border-anthracite-600"> <input type="checkbox" name="trust_id_inbound" value="1" checked class="rounded bg-anthracite-900 border-anthracite-600">
<span>Accepter l'identité entrante PAI/RPID (<code class="text-indigo-300">trust_id_inbound = yes</code>)</span> <span>Accepter l'identité entrante PAI/RPID (<code class="text-indigo-300">trust_id_inbound = yes</code>)</span>
</label> </label>
<label class="flex items-center gap-2 text-xs text-slate-200 cursor-pointer">
<input type="checkbox" name="trust_id_outbound" value="1" checked class="rounded bg-anthracite-900 border-anthracite-600">
<span>Transmettre l'identité sortante Caller ID (<code class="text-indigo-300">trust_id_outbound = yes</code>)</span>
</label>
</div> </div>
</div> </div>
</div> </div>
-198
View File
@@ -1,198 +0,0 @@
#!/usr/bin/env bash
# ==============================================================================
# NexusPBX - Script de Réparation Automatique des Permissions & Services
# ==============================================================================
set -e
if [ "$(id -u)" -ne 0 ]; then
echo "[ERREUR] Ce script doit être exécuté en root."
exit 1
fi
echo "-> Correction des répertoires Git et mise à jour..."
git config --global --add safe.directory /var/www/nexus-pbx 2>/dev/null || true
cd /var/www/nexus-pbx
git fetch origin main
git reset --hard origin/main
echo "-> Installation des dépendances système indispensables (ACL, ffmpeg, rsync, sox, sshpass)..."
which ffmpeg >/dev/null 2>&1 && which rsync >/dev/null 2>&1 && which setfacl >/dev/null 2>&1 && which sshpass >/dev/null 2>&1 || (apt-get update -y && apt-get install -y --no-install-recommends acl ffmpeg rsync sox sshpass)
echo "-> Création des dossiers et des fichiers de journalisation..."
mkdir -p /etc/fail2ban/filter.d /etc/asterisk /var/log/asterisk /var/log/asterisk/cdr-csv /var/log/nexus-pbx /var/www/nexus-pbx/storage/backups
touch /var/log/asterisk/messages /var/log/asterisk/security.log /var/log/asterisk/cdr-csv/Master.csv /var/log/nexus_security.log
chown asterisk:asterisk /var/log/asterisk/messages /var/log/asterisk/security.log /var/log/asterisk/cdr-csv/Master.csv 2>/dev/null || true
chmod 0664 /var/log/asterisk/messages /var/log/asterisk/security.log /var/log/asterisk/cdr-csv/Master.csv 2>/dev/null || true
chown www-data:asterisk /var/log/nexus_security.log 2>/dev/null || true
chmod 0664 /var/log/nexus_security.log 2>/dev/null || true
echo "-> Filtres Fail2ban Nexus-Web..."
cat << 'EOF' > /etc/fail2ban/filter.d/nexus-web.conf
[Definition]
failregex = ^.*\[NEXUS-WEB-AUTH-FAIL\]:\s*<HOST>.*$
ignoreregex =
EOF
chmod 0644 /etc/fail2ban/filter.d/nexus-web.conf
echo "-> Génération et application de /etc/fail2ban/jail.local..."
php -r '
require_once "/var/www/nexus-pbx/src/Database.php";
require_once "/var/www/nexus-pbx/src/FirewallManager.php";
$st = App\FirewallManager::getSettings();
App\FirewallManager::generateJailLocal(
$st["fail2ban_whitelist_ips"],
(int)$st["fail2ban_sip_maxretry"],
(int)$st["fail2ban_sip_findtime"],
(int)$st["fail2ban_sip_bantime"],
(int)$st["fail2ban_web_maxretry"],
(int)$st["fail2ban_web_bantime"],
(int)$st["admin_https_port"]
);
App\FirewallManager::applyNginxConfig((int)$st["admin_https_port"], (int)$st["certbot_strict_port80"]);
' 2>/dev/null || true
echo "-> Droits applicatifs et ACL POSIX (/etc/asterisk et /var/www/nexus-pbx)..."
usermod -aG asterisk www-data 2>/dev/null || true
usermod -aG www-data asterisk 2>/dev/null || true
mkdir -p /var/www/nexus-pbx/public/avatars /var/www/nexus-pbx/storage/sounds /var/www/nexus-pbx/storage/backups /var/www/nexus-pbx/storage/system_backups /etc/letsencrypt /var/log/letsencrypt /var/lib/letsencrypt
chgrp -R asterisk /etc/letsencrypt 2>/dev/null || true
chmod -R g+rX /etc/letsencrypt 2>/dev/null || true
chown -R www-data:asterisk /var/www/nexus-pbx
chmod 2775 /var/www/nexus-pbx/storage /var/www/nexus-pbx/storage/backups /var/www/nexus-pbx/storage/system_backups /var/www/nexus-pbx/storage/sounds /var/www/nexus-pbx/public/avatars 2>/dev/null || true
touch /etc/asterisk/pjsip.conf \
/etc/asterisk/pjsip.custom.conf \
/etc/asterisk/extensions.conf \
/etc/asterisk/extensions.custom.conf \
/etc/asterisk/musiconhold.conf \
/etc/asterisk/musiconhold.custom.conf \
/etc/asterisk/voicemail.conf \
/etc/asterisk/voicemail.custom.conf \
/etc/asterisk/modules.conf \
/etc/asterisk/rtp.conf \
/etc/asterisk/features.conf 2>/dev/null || true
# Désactivation des modules ODBC/IMAP incompatibles sur Debian (évite conflit bloquant app_voicemail)
if ! grep -q 'noload => app_voicemail_odbc.so' /etc/asterisk/modules.conf 2>/dev/null; then
if grep -q '\[modules\]' /etc/asterisk/modules.conf 2>/dev/null; then
sed -i '/\[modules\]/a noload => app_voicemail_odbc.so\nnoload => app_voicemail_imap.so' /etc/asterisk/modules.conf 2>/dev/null || true
else
printf '[modules]\nautoload=yes\nnoload => app_voicemail_odbc.so\nnoload => app_voicemail_imap.so\n' >> /etc/asterisk/modules.conf
fi
fi
# Inclusions Asterisk et configuration messagerie vocale
if ! grep -q 'pjsip.custom.conf' /etc/asterisk/pjsip.conf 2>/dev/null; then
printf '\n#tryinclude "pjsip.custom.conf"\n' >> /etc/asterisk/pjsip.conf
fi
if ! grep -q 'extensions.custom.conf' /etc/asterisk/extensions.conf 2>/dev/null; then
printf '\n#tryinclude "extensions.custom.conf"\n' >> /etc/asterisk/extensions.conf
fi
if ! grep -q 'musiconhold.custom.conf' /etc/asterisk/musiconhold.conf 2>/dev/null; then
sed -i 's/^\[default\]/;[default]/' /etc/asterisk/musiconhold.conf 2>/dev/null || true
sed -i 's/^mode=files/;mode=files/' /etc/asterisk/musiconhold.conf 2>/dev/null || true
sed -i 's/^directory=moh/;directory=moh/' /etc/asterisk/musiconhold.conf 2>/dev/null || true
printf '\n#tryinclude "/etc/asterisk/musiconhold.custom.conf"\n' >> /etc/asterisk/musiconhold.conf
fi
if ! grep -q 'voicemail.custom.conf' /etc/asterisk/voicemail.conf 2>/dev/null; then
sed -i 's/^1234 => 4242/;1234 => 4242/' /etc/asterisk/voicemail.conf 2>/dev/null || true
sed -i 's/^;\s*passwordlocation\s*=\s*spooldir/passwordlocation=spooldir/' /etc/asterisk/voicemail.conf 2>/dev/null || true
sed -i 's|^;\s*externpassnotify\s*=.*|externpassnotify=/usr/bin/php /var/www/nexus-pbx/scripts/voicemail_pass_sync.php|' /etc/asterisk/voicemail.conf 2>/dev/null || true
printf '\n#tryinclude "voicemail.custom.conf"\n' >> /etc/asterisk/voicemail.conf
fi
mkdir -p /var/lib/asterisk/sounds/custom /var/lib/asterisk/moh /var/spool/asterisk/voicemail/default 2>/dev/null || true
for lang in fr en es; do
if [ -d "/usr/share/asterisk/sounds/$lang" ] && [ ! -e "/var/lib/asterisk/sounds/$lang" ]; then
ln -sf "/usr/share/asterisk/sounds/$lang" "/var/lib/asterisk/sounds/$lang" 2>/dev/null || true
fi
if [ -d "/var/lib/asterisk/sounds/$lang" ] && [ ! -e "/var/lib/asterisk/sounds/$lang/custom" ]; then
ln -sf /var/lib/asterisk/sounds/custom "/var/lib/asterisk/sounds/$lang/custom" 2>/dev/null || true
fi
done
if [ ! -e "/usr/share/asterisk/sounds/custom" ]; then
ln -sf /var/lib/asterisk/sounds/custom /usr/share/asterisk/sounds/custom 2>/dev/null || true
fi
chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/spool/asterisk /var/log/asterisk 2>/dev/null || true
chmod -R 775 /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh /var/spool/asterisk /var/log/asterisk 2>/dev/null || true
chmod 2775 /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/sounds/custom /var/lib/asterisk/moh /var/spool/asterisk /var/spool/asterisk/voicemail /var/log/asterisk /var/log/asterisk/cdr-csv 2>/dev/null || true
chmod 2750 /etc/asterisk/keys 2>/dev/null || true
chmod 0664 /etc/asterisk/*.conf /etc/asterisk/*.custom.conf 2>/dev/null || true
setfacl -R -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh /var/spool/asterisk /var/log/asterisk 2>/dev/null || true
setfacl -R -d -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh /var/spool/asterisk /var/log/asterisk 2>/dev/null || true
chown root:asterisk /etc/fail2ban/jail.local /etc/nginx/sites-available/nexus-pbx 2>/dev/null || true
chmod 0664 /etc/fail2ban/jail.local /etc/nginx/sites-available/nexus-pbx 2>/dev/null || true
setfacl -m u:www-data:rw /etc/fail2ban/jail.local /etc/nginx/sites-available/nexus-pbx 2>/dev/null || true
cat << 'EOF' > /etc/sudoers.d/nexus-pbx
www-data ALL=(ALL) NOPASSWD: /usr/sbin/asterisk, /usr/sbin/nginx, /usr/bin/systemctl, /usr/bin/fail2ban-client, /bin/fail2ban-client, /var/www/nexus-pbx/scripts/nexus-firewall, /usr/bin/certbot, /usr/sbin/nft, /usr/sbin/iptables, /var/www/nexus-pbx/scripts/update.sh, /usr/local/bin/nexus-update
EOF
chmod 0440 /etc/sudoers.d/nexus-pbx
echo "-> Autorisation d'écriture systemd pour PHP-FPM (Debian 12/13 ProtectSystem)..."
for fpm_svc in /lib/systemd/system/php*-fpm.service /usr/lib/systemd/system/php*-fpm.service; do
if [ -f "$fpm_svc" ]; then
svc_name=$(basename "$fpm_svc")
mkdir -p "/etc/systemd/system/${svc_name}.d"
cat << 'EOF_OVERRIDE' > "/etc/systemd/system/${svc_name}.d/override.conf"
[Service]
ProtectSystem=false
ReadWritePaths=/etc/asterisk /etc/fail2ban /etc/nginx /var/log /var/lib/asterisk /var/spool/asterisk /var/www /usr/local/bin /etc/sudoers.d
EOF_OVERRIDE
fi
done
systemctl daemon-reload
echo "-> Configuration des limites d'upload PHP (512 Mo)..."
for php_dir in /etc/php/*; do
if [ -d "$php_dir/fpm/conf.d" ]; then
cat << 'EOF_PHP' > "$php_dir/fpm/conf.d/99-nexus.ini"
upload_max_filesize = 512M
post_max_size = 512M
memory_limit = 512M
max_execution_time = 300
max_input_time = 300
EOF_PHP
fi
if [ -d "$php_dir/cli/conf.d" ]; then
cat << 'EOF_PHP' > "$php_dir/cli/conf.d/99-nexus.ini"
upload_max_filesize = 512M
post_max_size = 512M
memory_limit = 512M
max_execution_time = 300
max_input_time = 300
EOF_PHP
fi
done
echo "-> Génération des configurations Asterisk..."
php -r '
require_once "/var/www/nexus-pbx/src/Database.php";
require_once "/var/www/nexus-pbx/src/ConfigGenerator.php";
$db = App\Database::getConnection();
$gen = new App\ConfigGenerator($db);
$gen->writeAllConfigs();
' 2>/dev/null || true
echo "-> Redémarrage des services..."
systemctl daemon-reload
systemctl restart php*-fpm 2>/dev/null || true
PHP_SVC=$(systemctl list-unit-files --type=service 2>/dev/null | grep -E "php.*-fpm" | awk '{print $1}' | head -n 1 || echo "")
if [ -n "$PHP_SVC" ]; then
systemctl restart "$PHP_SVC" 2>/dev/null || true
fi
systemctl restart asterisk 2>/dev/null || true
systemctl restart fail2ban 2>/dev/null || true
systemctl restart nginx 2>/dev/null || true
echo ""
echo "=========================================================="
echo " [OK] Réparation terminée ! Toutes les permissions sont actives."
echo "=========================================================="
/var/www/nexus-pbx/scripts/nexus-banner.sh --force || true
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
# Redirection vers le script d'installation principal
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec bash "$DIR/install.sh" "$@"
-1
View File
@@ -1 +0,0 @@
setup-debian.sh
+12 -9
View File
@@ -81,15 +81,18 @@ fi
echo -e "\n${C_YELLOW}-> Mise à jour vers la version v${REMOTE_VER}...${C_RESET}" echo -e "\n${C_YELLOW}-> Mise à jour vers la version v${REMOTE_VER}...${C_RESET}"
# 1. Sauvegarde préventive automatique # 1. Sauvegarde préventive automatique (Disaster Recovery complète)
echo -e "${C_GRAY}[1/6] Création d'une sauvegarde préventive du système...${C_RESET}" echo -e "${C_GRAY}[1/6] Création d'une sauvegarde préventive du système (Disaster Recovery)...${C_RESET}"
mkdir -p "$APP_DIR/storage/backups" php -r '
BACKUP_DATE=$(date +%Y%m%d_%H%M%S) require_once "/var/www/nexus-pbx/src/Database.php";
BACKUP_FILE="$APP_DIR/storage/backups/pre-update-${BACKUP_DATE}.tar.gz" require_once "/var/www/nexus-pbx/src/BackupManager.php";
tar -czf "$BACKUP_FILE" \ try {
-C "$APP_DIR/storage" database.sqlite \ $res = App\BackupManager::createBackup("pre-update");
-C /etc/asterisk pjsip.custom.conf extensions.custom.conf 2>/dev/null || true echo " ✓ Sauvegarde Disaster Recovery créée : " . $res["filename"] . " (" . $res["size_formatted"] . ")\n";
echo -e "${C_GREEN} ✓ Sauvegarde de sécurité créée : $(basename "$BACKUP_FILE")${C_RESET}" } catch (\Throwable $e) {
echo " ! Avertissement sauvegarde préventive : " . $e->getMessage() . "\n";
}
' || true
# 2. Application des derniers fichiers de code Git # 2. Application des derniers fichiers de code Git
echo -e "${C_GRAY}[2/6] Déploiement des nouveaux fichiers de code...${C_RESET}" echo -e "${C_GRAY}[2/6] Déploiement des nouveaux fichiers de code...${C_RESET}"
+36 -5
View File
@@ -11,7 +11,22 @@ use InvalidArgumentException;
class BackupManager class BackupManager
{ {
public const BACKUP_DIR = '/var/www/nexus-pbx/storage/system_backups'; public const BACKUP_DIR = '/var/www/nexus-pbx/storage/system_backups';
public const VERSION = '1.2.0'; public const VERSION = '1.6.2';
/**
* Retourne dynamiquement la version courante de NexusPBX lue depuis version.json
*/
public static function getAppVersion(): string
{
$vFile = dirname(__DIR__) . '/version.json';
if (file_exists($vFile)) {
$data = json_decode((string)file_get_contents($vFile), true);
if (!empty($data['version'])) {
return (string)$data['version'];
}
}
return self::VERSION;
}
/** /**
* Initialise le répertoire des sauvegardes. * Initialise le répertoire des sauvegardes.
@@ -182,7 +197,8 @@ class BackupManager
} }
$timestamp = date('Ymd_His'); $timestamp = date('Ymd_His');
$filename = "nexus_backup_v" . self::VERSION . "_{$timestamp}.tar.gz"; $currentVer = self::getAppVersion();
$filename = "nexus_backup_v{$currentVer}_{$timestamp}.tar.gz";
$localArchivePath = self::BACKUP_DIR . '/' . $filename; $localArchivePath = self::BACKUP_DIR . '/' . $filename;
// 2. Préparation du dossier de staging // 2. Préparation du dossier de staging
@@ -268,7 +284,7 @@ class BackupManager
// G. Création du Manifeste // G. Création du Manifeste
$manifest = [ $manifest = [
'nexus_version' => self::VERSION, 'nexus_version' => $currentVer,
'created_at' => date('c'), 'created_at' => date('c'),
'timestamp' => time(), 'timestamp' => time(),
'filename' => $filename, 'filename' => $filename,
@@ -360,6 +376,14 @@ class BackupManager
// Tenter de lire le manifest depuis l'archive sans l'extraire entièrement // Tenter de lire le manifest depuis l'archive sans l'extraire entièrement
$manifest = self::readManifestFromTar($file); $manifest = self::readManifestFromTar($file);
$archiveVer = $manifest['nexus_version'] ?? null;
if (!$archiveVer && preg_match('/_v([0-9]+\.[0-9]+(?:\.[0-9]+)?)/', $name, $vm)) {
$archiveVer = $vm[1];
}
if (!$archiveVer) {
$archiveVer = self::getAppVersion();
}
$backups[] = [ $backups[] = [
'filename' => $name, 'filename' => $name,
'storage_type' => 'local', 'storage_type' => 'local',
@@ -367,7 +391,7 @@ class BackupManager
'size_formatted' => self::formatBytes($size), 'size_formatted' => self::formatBytes($size),
'timestamp' => $mtime, 'timestamp' => $mtime,
'date_formatted' => date('d/m/Y H:i:s', $mtime), 'date_formatted' => date('d/m/Y H:i:s', $mtime),
'version' => $manifest['nexus_version'] ?? self::VERSION, 'version' => $archiveVer,
'source' => $manifest['source'] ?? 'local', 'source' => $manifest['source'] ?? 'local',
'is_local' => true, 'is_local' => true,
]; ];
@@ -807,6 +831,13 @@ class BackupManager
$name = basename($fullPath); $name = basename($fullPath);
if (str_ends_with($name, '.tar.gz')) { if (str_ends_with($name, '.tar.gz')) {
$remoteVer = null;
if (preg_match('/_v([0-9]+\.[0-9]+(?:\.[0-9]+)?)/', $name, $vm)) {
$remoteVer = $vm[1];
}
if (!$remoteVer) {
$remoteVer = self::getAppVersion();
}
$results[] = [ $results[] = [
'filename' => $name, 'filename' => $name,
'storage_type' => 'ssh', 'storage_type' => 'ssh',
@@ -814,7 +845,7 @@ class BackupManager
'size_formatted' => self::formatBytes($size), 'size_formatted' => self::formatBytes($size),
'timestamp' => $timestamp > 0 ? $timestamp : time(), 'timestamp' => $timestamp > 0 ? $timestamp : time(),
'date_formatted' => date('d/m/Y H:i:s', $timestamp > 0 ? $timestamp : time()), 'date_formatted' => date('d/m/Y H:i:s', $timestamp > 0 ? $timestamp : time()),
'version' => self::VERSION, 'version' => $remoteVer,
'source' => 'distant (SSH)', 'source' => 'distant (SSH)',
'is_local' => false, 'is_local' => false,
]; ];
+74 -2
View File
@@ -302,6 +302,7 @@ class ConfigGenerator
$sendRpid = ((int)($trunk['send_rpid'] ?? 0) === 1) ? 'yes' : 'no'; $sendRpid = ((int)($trunk['send_rpid'] ?? 0) === 1) ? 'yes' : 'no';
$sendDiversion = ((int)($trunk['send_diversion'] ?? 1) === 1) ? 'yes' : 'no'; $sendDiversion = ((int)($trunk['send_diversion'] ?? 1) === 1) ? 'yes' : 'no';
$trustInbound = ((int)($trunk['trust_id_inbound'] ?? 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'; $dtmfMode = trim((string)($trunk['dtmf_mode'] ?? 'rfc4733')) ?: 'rfc4733';
$mediaEnc = trim((string)($trunk['media_encryption'] ?? 'no')) ?: 'no'; $mediaEnc = trim((string)($trunk['media_encryption'] ?? 'no')) ?: 'no';
$natSym = ((int)($trunk['nat_symmetric'] ?? 1) === 1) ? 'yes' : 'no'; $natSym = ((int)($trunk['nat_symmetric'] ?? 1) === 1) ? 'yes' : 'no';
@@ -354,8 +355,19 @@ class ConfigGenerator
$lines[] = "outbound_proxy = {$outboundProxy}"; $lines[] = "outbound_proxy = {$outboundProxy}";
} }
if ($callerId !== '') { if ($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[] = "callerid = {$callerId}";
} }
}
$lines[] = "disallow = all"; $lines[] = "disallow = all";
foreach ($codecs as $codec) { foreach ($codecs as $codec) {
$lines[] = "allow = {$codec}"; $lines[] = "allow = {$codec}";
@@ -370,6 +382,7 @@ class ConfigGenerator
$lines[] = "send_rpid = {$sendRpid}"; $lines[] = "send_rpid = {$sendRpid}";
$lines[] = "send_diversion = {$sendDiversion}"; $lines[] = "send_diversion = {$sendDiversion}";
$lines[] = "trust_id_inbound = {$trustInbound}"; $lines[] = "trust_id_inbound = {$trustInbound}";
$lines[] = "trust_id_outbound = {$trustOutbound}";
$lines[] = "timers = {$timers}"; $lines[] = "timers = {$timers}";
if ($rtpKeepalive > 0) { if ($rtpKeepalive > 0) {
$lines[] = "rtp_keepalive = {$rtpKeepalive}"; $lines[] = "rtp_keepalive = {$rtpKeepalive}";
@@ -1158,7 +1171,9 @@ class ConfigGenerator
// 3. Positionnement du CallerID Sortant de la route // 3. Positionnement du CallerID Sortant de la route
if ($rCid !== '') { 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) // 4. Séquence de Trunks avec bascule automatique (Failover)
@@ -1173,7 +1188,9 @@ class ConfigGenerator
$tFallbackCid = $tObj ? trim((string)($tObj['outbound_callerid'] ?? '')) : ''; $tFallbackCid = $tObj ? trim((string)($tObj['outbound_callerid'] ?? '')) : '';
if ($rCid === '' && $tFallbackCid !== '' && $overrideCid) { if ($rCid === '' && $tFallbackCid !== '' && $overrideCid) {
$sub[] = " same => n,Set(CALLERID(all)={$tFallbackCid})"; foreach ($this->buildCallerIdDialplan($tFallbackCid) as $cidStmt) {
$sub[] = $cidStmt;
}
} }
$validTrunkCount++; $validTrunkCount++;
@@ -2257,5 +2274,60 @@ class ConfigGenerator
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug); $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
return trim($slug, '-') ?: 'default'; 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<string>
*/
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;
}
} }
+2
View File
@@ -150,6 +150,7 @@ class Database
send_pai INTEGER NOT NULL DEFAULT 1 CHECK (send_pai IN (0, 1)), 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)), 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_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', codecs VARCHAR(64) NOT NULL DEFAULT 'alaw,ulaw,g722',
dtmf_mode VARCHAR(16) NOT NULL DEFAULT 'rfc4733', dtmf_mode VARCHAR(16) NOT NULL DEFAULT 'rfc4733',
media_encryption VARCHAR(16) NOT NULL DEFAULT 'no', media_encryption VARCHAR(16) NOT NULL DEFAULT 'no',
@@ -525,6 +526,7 @@ class Database
'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0", 'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0",
'cid_name_prefix' => "VARCHAR(64) NOT NULL DEFAULT ''", 'cid_name_prefix' => "VARCHAR(64) NOT NULL DEFAULT ''",
'dial_options' => "VARCHAR(32) NOT NULL DEFAULT 'tTR'", '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) { foreach ($extraColumns as $colName => $colDef) {
+10 -10
View File
@@ -1,16 +1,16 @@
{ {
"version": "1.6.0", "version": "1.6.3",
"codename": "Helios", "codename": "Helios",
"release_date": "2026-11-04", "release_date": "2026-11-05",
"min_php_version": "8.2", "min_php_version": "8.2",
"changelog": [ "changelog": [
"Nouveau module Journal d'Appels & CDR : traçabilité complète des flux entrants, sortants, internes et services avec détection automatique des Trunks SIP et postes associés", "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.)",
"Filtres multicritères CDR en temps réel (période, direction, statut décroché/manqué, recherche textuelle) et export CSV Excel", "Sauvegarde & Restauration : versionnage dynamique automatique basé sur la version installée (résolution du tag figé v1.2.0)",
"Optimisation TLS SIP PJSIP : négociation universelle TLSv1.2/TLSv1.3 (method=sslv23)", "Mises à jour système OTA : déclenchement garanti d'une sauvegarde Disaster Recovery complète pré-mise à jour (visible et restaurable dans l'interface)",
"Suppression de CertificateRequest en mode TLS standard pour compatibilité totale avec les téléphones IP Yealink, Fanvil et Grandstream", "Interface Sauvegardes : nouveau badge identifiant la source (Pré-MàJ, Auto, Manuel)",
"Correction de l'émission et du renouvellement Let's Encrypt / Certbot ACME (privilèges sudo et permissions d'accès aux certificats)", "Identité visuelle officielle : intégration du nouveau logo NexusPBX 3D haute définition épuré et du favicon",
"Plafond d'expiration d'enregistrement SIP standard (maximum_expiration = 300s, default = 120s) garantissant la reconnexion automatique rapide de tous les postes (1 à 3 min)", "Nouveau slogan télécom : « Supervision & Téléphonie IP • Powered by Asterisk »",
"Nettoyage et isolation stricte des sauvegardes (base NexusPBX, sons, spools voicemail, certificats)", "Module Journal d'Appels & CDR avec filtres et export CSV",
"Résolution définitive et propre des conflits de préfixes Dialplan sur les SVI/IVR (*801-*899)" "Support universel TLSv1.2/TLSv1.3 et suppression CertificateRequest pour postes Yealink"
] ]
} }