Compare commits

...
4 Commits
9 changed files with 293 additions and 46 deletions

No files matched your search

+31 -4
View File
@@ -287,23 +287,50 @@ touch /etc/asterisk/pjsip.conf \
/etc/asterisk/pjsip.custom.conf \ /etc/asterisk/pjsip.custom.conf \
/etc/asterisk/extensions.conf \ /etc/asterisk/extensions.conf \
/etc/asterisk/extensions.custom.conf \ /etc/asterisk/extensions.custom.conf \
/etc/asterisk/musiconhold.conf \
/etc/asterisk/musiconhold.custom.conf \ /etc/asterisk/musiconhold.custom.conf \
/etc/asterisk/voicemail.conf \
/etc/asterisk/voicemail.custom.conf \ /etc/asterisk/voicemail.custom.conf \
/etc/asterisk/modules.conf \
/etc/asterisk/rtp.conf \ /etc/asterisk/rtp.conf \
/etc/asterisk/features.conf /etc/asterisk/features.conf
# 1. Modules Asterisk : 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
# 2. Inclusions des fichiers custom dans les configurations principales
if ! grep -q 'pjsip.custom.conf' /etc/asterisk/pjsip.conf 2>/dev/null; then 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 printf '\n#tryinclude "pjsip.custom.conf"\n' >> /etc/asterisk/pjsip.conf
fi fi
if ! grep -q 'extensions.custom.conf' /etc/asterisk/extensions.conf 2>/dev/null; then 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 printf '\n#tryinclude "extensions.custom.conf"\n' >> /etc/asterisk/extensions.conf
fi fi
chown -R asterisk:asterisk /etc/asterisk 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/spool/asterisk/voicemail/default /var/lib/asterisk/sounds/custom /var/lib/asterisk/moh
chown -R asterisk:asterisk /etc/asterisk /var/spool/asterisk /var/lib/asterisk
chmod 0664 /etc/asterisk/*.conf /etc/asterisk/*.custom.conf 2>/dev/null || true chmod 0664 /etc/asterisk/*.conf /etc/asterisk/*.custom.conf 2>/dev/null || true
chmod 2750 /etc/asterisk/keys chmod 2750 /etc/asterisk/keys
chmod 2775 /etc/asterisk chmod 2775 /etc/asterisk /var/spool/asterisk /var/spool/asterisk/voicemail /var/lib/asterisk/sounds /var/lib/asterisk/moh
setfacl -R -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk 2>/dev/null || true setfacl -R -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk /var/spool/asterisk /var/lib/asterisk 2>/dev/null || true
setfacl -R -d -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk 2>/dev/null || true setfacl -R -d -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk /var/spool/asterisk /var/lib/asterisk 2>/dev/null || true
# Rechargement Asterisk immédiat pour appliquer AMI et Logger # Rechargement Asterisk immédiat pour appliquer AMI et Logger
asterisk -rx "core reload" 2>/dev/null || true asterisk -rx "core reload" 2>/dev/null || true
+53 -4
View File
@@ -140,7 +140,7 @@ try {
$installedVerData = file_exists("{$appDir}/version.json") $installedVerData = file_exists("{$appDir}/version.json")
? json_decode((string)file_get_contents("{$appDir}/version.json"), true) ? json_decode((string)file_get_contents("{$appDir}/version.json"), true)
: []; : [];
$installedVer = $installedVerData['version'] ?? '1.4.3'; $installedVer = $installedVerData['version'] ?? '1.4.7';
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
@@ -227,7 +227,7 @@ try {
@chmod($webLog, 0664); @chmod($webLog, 0664);
} }
@unlink('/tmp/nexus_fw_status_cache.json'); @unlink('/tmp/nexus_fw_status_cache.json');
exec('sudo /usr/bin/fail2ban-client reload 2>&1'); exec('sudo /usr/bin/systemctl restart fail2ban 2>&1 || sudo /usr/bin/fail2ban-client reload 2>&1');
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
@@ -369,6 +369,13 @@ try {
// Synchronisation automatique de l'annuaire d'entreprise // Synchronisation automatique de l'annuaire d'entreprise
PhonebookManager::syncExtensions($db); PhonebookManager::syncExtensions($db);
// Régénération automatique des fichiers Asterisk et rechargement PJSIP/Dialplan en direct
$generator->writeAllConfigs();
try {
$ami = AmiClient::fromDatabase($db);
$ami->reloadPjsipAndDialplan();
} catch (\Throwable $e) {}
$modeBadge = $transport === 'tls' ? 'Distant (TLS + SRTP)' : ($encryption ? 'Interne (UDP + SRTP)' : 'Interne (UDP)'); $modeBadge = $transport === 'tls' ? 'Distant (TLS + SRTP)' : ($encryption ? 'Interne (UDP + SRTP)' : 'Interne (UDP)');
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
@@ -381,6 +388,13 @@ try {
$stmt = $db->prepare('DELETE FROM extensions WHERE id = :id'); $stmt = $db->prepare('DELETE FROM extensions WHERE id = :id');
$stmt->execute([':id' => $id]); $stmt->execute([':id' => $id]);
PhonebookManager::syncExtensions($db); PhonebookManager::syncExtensions($db);
$generator->writeAllConfigs();
try {
$ami = AmiClient::fromDatabase($db);
$ami->reloadPjsipAndDialplan();
} catch (\Throwable $e) {}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'message' => 'Poste téléphonique supprimé de la base SQLite.', 'message' => 'Poste téléphonique supprimé de la base SQLite.',
@@ -440,6 +454,12 @@ try {
]); ]);
} }
$generator->writeAllConfigs();
try {
$ami = AmiClient::fromDatabase($db);
$ami->reloadPjsipAndDialplan();
} catch (\Throwable $e) {}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'message' => "Groupe d'appel {$extension} ({$name}) sauvegardé.", 'message' => "Groupe d'appel {$extension} ({$name}) sauvegardé.",
@@ -449,6 +469,12 @@ try {
case 'delete_group': case 'delete_group':
$id = (int)($payload['id'] ?? 0); $id = (int)($payload['id'] ?? 0);
$db->prepare('DELETE FROM groups WHERE id = :id')->execute([':id' => $id]); $db->prepare('DELETE FROM groups WHERE id = :id')->execute([':id' => $id]);
$generator->writeAllConfigs();
try {
$ami = AmiClient::fromDatabase($db);
$ami->reloadPjsipAndDialplan();
} catch (\Throwable $e) {}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'message' => "Groupe de sonnerie supprimé.", 'message' => "Groupe de sonnerie supprimé.",
@@ -540,6 +566,12 @@ try {
]); ]);
} }
$generator->writeAllConfigs();
try {
$ami = AmiClient::fromDatabase($db);
$ami->reloadPjsipAndDialplan();
} catch (\Throwable $e) {}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'message' => "Groupe de téléavertissement {$extension} ({$name}) sauvegardé.", 'message' => "Groupe de téléavertissement {$extension} ({$name}) sauvegardé.",
@@ -549,6 +581,12 @@ try {
case 'delete_paging_group': case 'delete_paging_group':
$id = (int)($payload['id'] ?? 0); $id = (int)($payload['id'] ?? 0);
$db->prepare('DELETE FROM paging_groups WHERE id = :id')->execute([':id' => $id]); $db->prepare('DELETE FROM paging_groups WHERE id = :id')->execute([':id' => $id]);
$generator->writeAllConfigs();
try {
$ami = AmiClient::fromDatabase($db);
$ami->reloadPjsipAndDialplan();
} catch (\Throwable $e) {}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'message' => "Groupe de téléavertissement supprimé.", 'message' => "Groupe de téléavertissement supprimé.",
@@ -2311,14 +2349,25 @@ try {
$newVerJson = file_exists("{$appDir}/version.json") $newVerJson = file_exists("{$appDir}/version.json")
? json_decode((string)file_get_contents("{$appDir}/version.json"), true) ? json_decode((string)file_get_contents("{$appDir}/version.json"), true)
: ['version' => '1.3.8']; : ['version' => '1.4.3'];
while (ob_get_level() > 0) {
@ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
echo json_encode([ echo json_encode([
'success' => ($code === 0), 'success' => ($code === 0),
'new_version' => $newVerJson['version'] ?? '1.3.8', 'new_version' => $newVerJson['version'] ?? '1.4.3',
'output' => $outputStr, 'output' => $outputStr,
'message' => ($code === 0) ? "Mise à jour réussie !" : "Erreur lors de la mise à jour", 'message' => ($code === 0) ? "Mise à jour réussie !" : "Erreur lors de la mise à jour",
]); ]);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
break; break;
default: default:
+53 -19
View File
@@ -5962,18 +5962,8 @@ async function executeSystemUpdate() {
localStorage.removeItem('nexus_update_last_check_ts'); localStorage.removeItem('nexus_update_last_check_ts');
localStorage.removeItem('nexus_update_cached_data'); localStorage.removeItem('nexus_update_cached_data');
try { function finalizeUpdateSuccess(newVer) {
const res = await fetch('/api.php?action=run_update&_t=' + Date.now(), { newVer = String(newVer || '').replace(/^v/, '');
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await res.json();
if (data.success) {
const newVer = data.new_version || '';
// 1. Mettre à jour immédiatement le cache avec l'état à jour
localStorage.setItem('nexus_update_last_check_ts', Date.now().toString()); localStorage.setItem('nexus_update_last_check_ts', Date.now().toString());
localStorage.setItem('nexus_update_cached_data', JSON.stringify({ localStorage.setItem('nexus_update_cached_data', JSON.stringify({
success: true, success: true,
@@ -5984,7 +5974,6 @@ async function executeSystemUpdate() {
release_date: '' release_date: ''
})); }));
// 2. Mettre à jour visuellement les badges immédiatement
const verTag = document.getElementById('system-version-tag'); const verTag = document.getElementById('system-version-tag');
const appBadge = document.getElementById('app-version-badge'); const appBadge = document.getElementById('app-version-badge');
const headerUpdateBadge = document.getElementById('header-update-badge'); const headerUpdateBadge = document.getElementById('header-update-badge');
@@ -6001,20 +5990,53 @@ async function executeSystemUpdate() {
statusText.className = 'text-emerald-400 font-bold'; statusText.className = 'text-emerald-400 font-bold';
statusText.textContent = `Mise à jour terminée avec succès (v${newVer}) ! Rechargement...`; statusText.textContent = `Mise à jour terminée avec succès (v${newVer}) ! Rechargement...`;
} }
if (btnModalConfirm) { if (btnModalConfirm) {
btnModalConfirm.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-emerald-600 text-white flex items-center gap-2'; btnModalConfirm.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-emerald-600 text-white flex items-center gap-2';
btnModalConfirm.innerHTML = '✓ Mise à jour terminée ! Rechargement...'; btnModalConfirm.innerHTML = '✓ Mise à jour terminée ! Rechargement...';
} }
// 3. Rechargement avec paramètre anti-cache pour forcer le navigateur à récupérer le nouveau DOM
setTimeout(() => { setTimeout(() => {
closeUpdateConfirmModal(); closeUpdateConfirmModal();
window.location.href = window.location.origin + window.location.pathname + '?_t=' + Date.now(); window.location.href = window.location.origin + window.location.pathname + '?_t=' + Date.now();
}, 1800); }, 1800);
} else { }
try {
const res = await fetch('/api.php?action=run_update&_t=' + Date.now(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const rawText = await res.text();
let data = null;
try {
data = JSON.parse(rawText);
} catch (_) {
data = null;
}
if (data && data.success) {
finalizeUpdateSuccess(data.new_version);
return;
}
// Si la réponse n'était pas un JSON valide ou a été interrompue par le rechargement de php-fpm/nginx,
// on sonde le serveur pour vérifier si la mise à jour a en réalité bien été appliquée sur le disque
await new Promise(r => setTimeout(r, 2000));
try {
const probeRes = await fetch('/api.php?action=check_update&_t=' + Date.now());
const probeData = await probeRes.json();
if (probeData && probeData.success && probeData.current_version) {
finalizeUpdateSuccess(probeData.current_version);
return;
}
} catch (_) {}
// En cas d'échec réel avéré
closeUpdateConfirmModal(); closeUpdateConfirmModal();
alert("Erreur lors de la mise à jour :\n\n" + (data.output || data.message)); const errMsg = (data && (data.output || data.message)) ? (data.output || data.message) : (rawText.slice(0, 150) || "Erreur inconnue");
alert("Erreur lors de la mise à jour :\n\n" + errMsg);
if (btnRun) { if (btnRun) {
btnRun.disabled = false; btnRun.disabled = false;
btnRun.innerHTML = 'Réessayer la mise à jour'; btnRun.innerHTML = 'Réessayer la mise à jour';
@@ -6024,10 +6046,22 @@ async function executeSystemUpdate() {
btnModalConfirm.classList.remove('opacity-75', 'cursor-not-allowed'); btnModalConfirm.classList.remove('opacity-75', 'cursor-not-allowed');
btnModalConfirm.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg><span>Lancer la mise à jour (1-Clic)</span>'; btnModalConfirm.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg><span>Lancer la mise à jour (1-Clic)</span>';
} }
}
} catch (e) { } catch (e) {
// En cas d'interruption réseau du socket (Safari WebKit DOMException ou timeout),
// on vérifie avant d'alerter si le serveur a quand même réussi à déployer les fichiers
await new Promise(r => setTimeout(r, 2000));
try {
const probeRes = await fetch('/api.php?action=check_update&_t=' + Date.now());
const probeData = await probeRes.json();
if (probeData && probeData.success && probeData.current_version) {
finalizeUpdateSuccess(probeData.current_version);
return;
}
} catch (_) {}
closeUpdateConfirmModal(); closeUpdateConfirmModal();
alert("Erreur réseau lors de la mise à jour : " + e.message); alert("Erreur réseau lors de la mise à jour : " + (e.message || "Connexion interrompue"));
if (btnRun) { if (btnRun) {
btnRun.disabled = false; btnRun.disabled = false;
btnRun.innerHTML = 'Réessayer la mise à jour'; btnRun.innerHTML = 'Réessayer la mise à jour';
+1 -1
View File
@@ -18,7 +18,7 @@ $companyLogo = trim($branding['company_logo'] ?? '');
$hasBranding = !empty($companyName) || !empty($companyLogo); $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.4.3'; $appVersion = $appVerData['version'] ?? '1.4.7';
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr" class="h-full"> <html lang="fr" class="h-full">
+34 -2
View File
@@ -65,15 +65,47 @@ touch /etc/asterisk/pjsip.conf \
/etc/asterisk/pjsip.custom.conf \ /etc/asterisk/pjsip.custom.conf \
/etc/asterisk/extensions.conf \ /etc/asterisk/extensions.conf \
/etc/asterisk/extensions.custom.conf \ /etc/asterisk/extensions.custom.conf \
/etc/asterisk/musiconhold.conf \
/etc/asterisk/musiconhold.custom.conf \ /etc/asterisk/musiconhold.custom.conf \
/etc/asterisk/voicemail.conf \
/etc/asterisk/voicemail.custom.conf \ /etc/asterisk/voicemail.custom.conf \
/etc/asterisk/modules.conf \
/etc/asterisk/rtp.conf \ /etc/asterisk/rtp.conf \
/etc/asterisk/features.conf 2>/dev/null || true /etc/asterisk/features.conf 2>/dev/null || true
mkdir -p /var/lib/asterisk/sounds/custom /var/lib/asterisk/moh /var/spool/asterisk/voicemail 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
chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/spool/asterisk /var/log/asterisk 2>/dev/null || true 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 2>/dev/null || true chmod -R 775 /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh /var/spool/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 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 2>/dev/null || true
chmod 2750 /etc/asterisk/keys 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 chmod 0664 /etc/asterisk/*.conf /etc/asterisk/*.custom.conf 2>/dev/null || true
+16 -2
View File
@@ -130,8 +130,16 @@ $gen->writeAllConfigs();
# 5. Fix permissions & ACLs # 5. Fix permissions & ACLs
echo -e "${C_GRAY}[5/6] Application des permissions et ACLs système...${C_RESET}" echo -e "${C_GRAY}[5/6] Application des permissions et ACLs système...${C_RESET}"
setfacl -R -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh 2>/dev/null || true php -r '
setfacl -R -d -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh 2>/dev/null || true require_once "/var/www/nexus-pbx/src/Database.php";
require_once "/var/www/nexus-pbx/src/SystemChecker.php";
App\SystemChecker::ensureAsteriskIncludes();
' 2>/dev/null || true
mkdir -p /var/spool/asterisk/voicemail/default /var/lib/asterisk/sounds/custom /var/lib/asterisk/moh 2>/dev/null || true
chown -R asterisk:asterisk /var/spool/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh 2>/dev/null || true
chmod 2775 /var/spool/asterisk /var/spool/asterisk/voicemail /var/lib/asterisk/sounds /var/lib/asterisk/moh 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 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 2>/dev/null || true
chmod 0664 /etc/asterisk/*.conf /etc/asterisk/*.custom.conf 2>/dev/null || true chmod 0664 /etc/asterisk/*.conf /etc/asterisk/*.custom.conf 2>/dev/null || true
# 6. Rechargement des services # 6. Rechargement des services
@@ -139,8 +147,14 @@ echo -e "${C_GRAY}[6/6] Rechargement à chaud d'Asterisk et des services web...$
asterisk -rx "core reload" >/dev/null 2>&1 || true asterisk -rx "core reload" >/dev/null 2>&1 || true
asterisk -rx "pjsip reload" >/dev/null 2>&1 || true asterisk -rx "pjsip reload" >/dev/null 2>&1 || true
asterisk -rx "dialplan reload" >/dev/null 2>&1 || true asterisk -rx "dialplan reload" >/dev/null 2>&1 || true
# Rechargement des services web : différé si exécuté depuis l'interface web pour éviter de couper la requête HTTP
if [ -t 1 ]; then
systemctl reload php8.4-fpm 2>/dev/null || systemctl reload php8.2-fpm 2>/dev/null || systemctl reload php*-fpm 2>/dev/null || true systemctl reload php8.4-fpm 2>/dev/null || systemctl reload php8.2-fpm 2>/dev/null || systemctl reload php*-fpm 2>/dev/null || true
systemctl reload nginx 2>/dev/null || true systemctl reload nginx 2>/dev/null || true
else
(nohup bash -c "sleep 2 && (systemctl reload php8.4-fpm 2>/dev/null || systemctl reload php8.2-fpm 2>/dev/null || systemctl reload php*-fpm 2>/dev/null || true); systemctl reload nginx 2>/dev/null || true" >/dev/null 2>&1 &)
fi
NEW_VER="Inconnue" NEW_VER="Inconnue"
if [ -f "$APP_DIR/version.json" ]; then if [ -f "$APP_DIR/version.json" ]; then
+64 -5
View File
@@ -70,17 +70,22 @@ class SystemChecker
: 'Accès refusé en écriture dans /etc/asterisk pour PHP-FPM', : 'Accès refusé en écriture dans /etc/asterisk pour PHP-FPM',
]; ];
// 5. Directives #tryinclude dans pjsip.conf et extensions.conf // 5. Directives #tryinclude dans pjsip.conf, extensions.conf, voicemail.conf et modules.conf
$pjsipMain = @file_get_contents('/etc/asterisk/pjsip.conf') ?: ''; $pjsipMain = @file_get_contents('/etc/asterisk/pjsip.conf') ?: '';
$extMain = @file_get_contents('/etc/asterisk/extensions.conf') ?: ''; $extMain = @file_get_contents('/etc/asterisk/extensions.conf') ?: '';
$incOk = str_contains($pjsipMain, 'pjsip.custom.conf') && str_contains($extMain, 'extensions.custom.conf'); $vmMain = @file_get_contents('/etc/asterisk/voicemail.conf') ?: '';
$modMain = @file_get_contents('/etc/asterisk/modules.conf') ?: '';
$incOk = str_contains($pjsipMain, 'pjsip.custom.conf')
&& str_contains($extMain, 'extensions.custom.conf')
&& str_contains($vmMain, 'voicemail.custom.conf')
&& str_contains($modMain, 'app_voicemail_odbc.so');
$checks[] = [ $checks[] = [
'id' => 'asterisk_includes', 'id' => 'asterisk_includes',
'label' => 'Inclusions natives (#tryinclude *.custom.conf)', 'label' => 'Inclusions Asterisk & Voicemail (#tryinclude & modules.conf)',
'ok' => $incOk, 'ok' => $incOk,
'detail' => $incOk 'detail' => $incOk
? 'pjsip.conf et extensions.conf incluent bien les fichiers .custom.conf' ? 'pjsip.conf, extensions.conf, voicemail.conf et modules.conf configurés'
: 'Directive #tryinclude manquante dans pjsip.conf ou extensions.conf', : 'Directive #tryinclude manquante ou conflit modules.conf détecté',
]; ];
// 6. Certificats TLS (Let's Encrypt ou Certificat local Debian) // 6. Certificats TLS (Let's Encrypt ou Certificat local Debian)
@@ -142,6 +147,25 @@ class SystemChecker
public static function ensureAsteriskIncludes(): void public static function ensureAsteriskIncludes(): void
{ {
// 1. modules.conf: Désactiver les modules voicemail ODBC/IMAP incompatibles sur Debian (évite collision bloquante avec app_voicemail.so)
$modConf = '/etc/asterisk/modules.conf';
if (is_file($modConf) && is_writable($modConf)) {
$content = (string)file_get_contents($modConf);
$changed = false;
if (!str_contains($content, 'app_voicemail_odbc.so')) {
if (preg_match('/\[modules\]/i', $content)) {
$content = preg_replace('/\[modules\]/i', "[modules]\nnoload => app_voicemail_odbc.so\nnoload => app_voicemail_imap.so", $content, 1);
$changed = true;
} else {
$content = "[modules]\nautoload=yes\nnoload => app_voicemail_odbc.so\nnoload => app_voicemail_imap.so\n\n" . $content;
$changed = true;
}
}
if ($changed) {
file_put_contents($modConf, $content);
}
}
$pjsipConf = '/etc/asterisk/pjsip.conf'; $pjsipConf = '/etc/asterisk/pjsip.conf';
if (is_file($pjsipConf) && is_writable($pjsipConf)) { if (is_file($pjsipConf) && is_writable($pjsipConf)) {
$content = (string)file_get_contents($pjsipConf); $content = (string)file_get_contents($pjsipConf);
@@ -177,8 +201,20 @@ class SystemChecker
if (is_file($vmConf) && is_writable($vmConf)) { if (is_file($vmConf) && is_writable($vmConf)) {
$content = (string)file_get_contents($vmConf); $content = (string)file_get_contents($vmConf);
$cleaned = preg_replace('/^1234 => 4242.*/m', ';1234 => 4242,Example Mailbox,root@localhost', $content); $cleaned = preg_replace('/^1234 => 4242.*/m', ';1234 => 4242,Example Mailbox,root@localhost', $content);
if (preg_match('/^;\s*passwordlocation\s*=\s*spooldir/m', $cleaned)) {
$cleaned = preg_replace('/^;\s*passwordlocation\s*=\s*spooldir/m', 'passwordlocation=spooldir', $cleaned); $cleaned = preg_replace('/^;\s*passwordlocation\s*=\s*spooldir/m', 'passwordlocation=spooldir', $cleaned);
} elseif (!str_contains($cleaned, 'passwordlocation=spooldir') && !str_contains($cleaned, 'passwordlocation = spooldir')) {
if (preg_match('/\[general\]/i', $cleaned)) {
$cleaned = preg_replace('/\[general\]/i', "[general]\npasswordlocation=spooldir", $cleaned, 1);
}
}
if (preg_match('/^;\s*externpassnotify\s*=.*/m', $cleaned)) {
$cleaned = preg_replace('/^;\s*externpassnotify\s*=.*/m', 'externpassnotify=/usr/bin/php /var/www/nexus-pbx/scripts/voicemail_pass_sync.php', $cleaned); $cleaned = preg_replace('/^;\s*externpassnotify\s*=.*/m', 'externpassnotify=/usr/bin/php /var/www/nexus-pbx/scripts/voicemail_pass_sync.php', $cleaned);
} elseif (!str_contains($cleaned, 'externpassnotify=')) {
if (preg_match('/\[general\]/i', $cleaned)) {
$cleaned = preg_replace('/\[general\]/i', "[general]\nexternpassnotify=/usr/bin/php /var/www/nexus-pbx/scripts/voicemail_pass_sync.php", $cleaned, 1);
}
}
if (!str_contains($cleaned, 'voicemail.custom.conf')) { if (!str_contains($cleaned, 'voicemail.custom.conf')) {
$cleaned = rtrim($cleaned) . "\n\n#tryinclude \"voicemail.custom.conf\"\n"; $cleaned = rtrim($cleaned) . "\n\n#tryinclude \"voicemail.custom.conf\"\n";
} }
@@ -187,6 +223,29 @@ class SystemChecker
} }
} }
@mkdir('/var/spool/asterisk/voicemail/default', 0775, true);
// Synchroniser manager.conf avec le mot de passe AMI de la base SQLite si nécessaire (ex: après restauration de backup)
$mgrConf = '/etc/asterisk/manager.conf';
if (is_file($mgrConf) && is_writable($mgrConf)) {
try {
$db = Database::getConnection();
$st = $db->query('SELECT ami_user, ami_secret FROM settings WHERE id = 1')->fetch();
if (!empty($st['ami_secret'])) {
$sec = trim((string)$st['ami_secret']);
$usr = trim((string)($st['ami_user'] ?? 'pbxgui')) ?: 'pbxgui';
$content = (string)file_get_contents($mgrConf);
if (str_contains($content, "[{$usr}]")) {
$updated = preg_replace("/(\[{$usr}\][^\[]*secret\s*=\s*)([^\r\n]+)/i", "\${1}{$sec}", $content);
if ($updated !== null && $updated !== $content) {
file_put_contents($mgrConf, $updated);
@exec('asterisk -rx "manager reload" 2>/dev/null || true');
}
}
}
} catch (\Throwable) {}
}
$astConf = '/etc/asterisk/asterisk.conf'; $astConf = '/etc/asterisk/asterisk.conf';
if (is_file($astConf) && is_writable($astConf)) { if (is_file($astConf) && is_writable($astConf)) {
$content = (string)file_get_contents($astConf); $content = (string)file_get_contents($astConf);
+36 -4
View File
@@ -298,17 +298,19 @@ class SystemMetrics
} }
} }
$seenExts = [];
foreach (preg_split('/\r?\n/', $rawContacts) ?: [] as $line) { foreach (preg_split('/\r?\n/', $rawContacts) ?: [] as $line) {
// Contact: 101/sip:101@10.10.167.162:5060 5dd9a21be8 Avail 9.234 // Supporte sip:, sips:, sans préfixe, tous types de hash et RTT (numérique, nan, N/A ou absent)
// or with -nan: Contact: 102/sip:102@10.10.168.228:5060;x-ast-txp=trans bb0686ce80 Unavail -nan if (preg_match('/Contact:\s+([^\s\/]+)\/(?:sips?:)?([^\s]+)\s+([a-zA-Z0-9]+)\s+(\S+)(?:\s+(\S+))?/i', $line, $m)) {
if (preg_match('/Contact:\s+([^\s\/]+)\/sip:([^\s]+)\s+([a-f0-9]+)\s+(\S+)\s+(-?[0-9.]+|nan|-nan)/i', $line, $m)) {
$extNum = $m[1]; $extNum = $m[1];
$uri = $m[2]; $uri = $m[2];
$status = $m[4]; $status = $m[4];
$rttRaw = $m[5] ?? null;
$isAvail = (stripos($status, 'Avail') !== false && stripos($status, 'Unavail') === false) || stripos($status, 'NonQual') !== false; $isAvail = (stripos($status, 'Avail') !== false && stripos($status, 'Unavail') === false) || stripos($status, 'NonQual') !== false;
if ($isAvail) { if ($isAvail) {
$onlineCount++; $onlineCount++;
} }
$seenExts[$extNum] = true;
$meta = $contactMeta[$extNum] ?? []; $meta = $contactMeta[$extNum] ?? [];
$fullUri = $meta['uri'] ?? ("sip:" . $uri); $fullUri = $meta['uri'] ?? ("sip:" . $uri);
@@ -330,7 +332,37 @@ class SystemMetrics
'user_agent' => $userAgent, 'user_agent' => $userAgent,
'active_transport' => $activeTransport, 'active_transport' => $activeTransport,
'status' => $status, 'status' => $status,
'rtt_ms' => is_numeric($m[5]) ? round((float)$m[5], 1) : null, 'rtt_ms' => ($rttRaw !== null && is_numeric($rttRaw)) ? round((float)$rttRaw, 1) : null,
];
}
}
// Filet de sécurité 100% infaillible : Tous les contacts enregistrés dans AstDB (/registrar/contact)
// non encore répertoriés (par exemple si qualify est désactivé ou RTT non disponible)
$nowTime = time();
foreach ($contactMeta as $extNum => $meta) {
if (isset($seenExts[$extNum])) {
continue;
}
$exp = (int)($meta['expiration_time'] ?? 0);
if ($exp === 0 || $exp > $nowTime) {
$onlineCount++;
$fullUri = $meta['uri'] ?? '';
$activeTransport = 'udp';
if (preg_match('/transport=(tls|wss|ws|tcp|udp)/i', $fullUri, $tm)) {
$activeTransport = strtolower($tm[1]);
} elseif (preg_match('/x-ast-txp=transport-(tls|wss|ws|tcp|udp)/i', $fullUri, $tm)) {
$activeTransport = strtolower($tm[1]);
} elseif (str_starts_with($fullUri, 'sips:')) {
$activeTransport = 'tls';
}
$contacts[] = [
'extension' => (string)$extNum,
'uri' => $fullUri,
'user_agent' => $meta['user_agent'] ?? '',
'active_transport' => $activeTransport,
'status' => 'Avail',
'rtt_ms' => null,
]; ];
} }
} }
+5 -5
View File
@@ -1,12 +1,12 @@
{ {
"version": "1.4.3", "version": "1.4.7",
"codename": "Helios", "codename": "Helios",
"release_date": "2026-11-04", "release_date": "2026-11-04",
"min_php_version": "8.2", "min_php_version": "8.2",
"changelog": [ "changelog": [
"Nouvelle boîte modale élégante et personnalisée pour confirmer le lancement des mises à jour (remplacement du confirm natif)", "Détection ultra-robuste de l'état des téléphones IP (support SIPS, statuts sans qualify et fallback infaillible AstDB)",
"Rendu serveur natif garanti du logo et nom de l'exploitant sur la page de connexion", "Réinitialisation immédiate du compteur d'attaques contrées Fail2ban lors du vidage du journal de sécurité",
"Boîte de connexion allégée en style verre dépoli transparent (Glassmorphism backdrop-blur)", "Synchronisation automatique des identifiants AMI (/etc/asterisk/manager.conf) après restauration d'une sauvegarde",
"Nouveau fond d'écran immersif haute définition (réseau constellation)" "Correction et fiabilisation de la messagerie vocale (*97 / *98) sur toute installation fraîche Debian"
] ]
} }