query('SELECT * FROM settings WHERE id = 1')->fetch() ?: []; $leDomain = trim((string)($settings['letsencrypt_domain'] ?? '')); $checks = []; // 1. OS Debian & PHP SQLite3 $osPretty = 'Linux Debian'; if (is_readable('/etc/os-release')) { $osContent = (string)file_get_contents('/etc/os-release'); if (preg_match('/PRETTY_NAME="([^"]+)"/', $osContent, $m)) { $osPretty = $m[1]; } } $checks[] = [ 'id' => 'debian_os', 'label' => 'Système Debian & PHP ' . PHP_VERSION, 'ok' => extension_loaded('pdo_sqlite'), 'detail' => $osPretty . ' (PDO SQLite actif)', ]; // 2. Base SQLite accessible en écriture $dbWritable = is_writable(Database::DB_PATH) && is_writable(dirname(Database::DB_PATH)); $checks[] = [ 'id' => 'sqlite_db', 'label' => 'Base de données SQLite3 (Source unique)', 'ok' => $dbWritable, 'detail' => $dbWritable ? 'Lecture/Écriture OK (' . realpath(Database::DB_PATH) . ')' : 'Permissions insuffisantes sur storage/database.sqlite', ]; // 3. Utilisateur et Groupe Debian asterisk:asterisk $astUser = function_exists('posix_getpwnam') ? posix_getpwnam('asterisk') : true; $astGroup = function_exists('posix_getgrnam') ? posix_getgrnam('asterisk') : true; $checks[] = [ 'id' => 'asterisk_user', 'label' => 'Utilisateur & Groupe Debian (asterisk:asterisk)', 'ok' => (bool)($astUser && $astGroup), 'detail' => ($astUser && $astGroup) ? 'Utilisateur système asterisk:asterisk présent' : 'Utilisateur ou groupe asterisk introuvable sur Debian', ]; // 4. Droits d'écriture sur /etc/asterisk et *.custom.conf $etcWritable = is_dir('/etc/asterisk') && is_writable('/etc/asterisk'); $pjsipOk = !file_exists(ConfigGenerator::PJSIP_CONF_PATH) ? $etcWritable : is_writable(ConfigGenerator::PJSIP_CONF_PATH); $extOk = !file_exists(ConfigGenerator::EXTENSIONS_CONF_PATH) ? $etcWritable : is_writable(ConfigGenerator::EXTENSIONS_CONF_PATH); $permOk = $etcWritable && $pjsipOk && $extOk; $checks[] = [ 'id' => 'asterisk_perms', 'label' => 'Droits d\'écriture sur /etc/asterisk/*.custom.conf', 'ok' => $permOk, 'detail' => $permOk ? '/etc/asterisk accessible en écriture par le worker PHP (groupe asterisk)' : 'Accès refusé en écriture dans /etc/asterisk pour PHP-FPM', ]; // 5. Directives #tryinclude dans pjsip.conf, extensions.conf, voicemail.conf et modules.conf $pjsipMain = @file_get_contents('/etc/asterisk/pjsip.conf') ?: ''; $extMain = @file_get_contents('/etc/asterisk/extensions.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[] = [ 'id' => 'asterisk_includes', 'label' => 'Inclusions Asterisk & Voicemail (#tryinclude & modules.conf)', 'ok' => $incOk, 'detail' => $incOk ? 'pjsip.conf, extensions.conf, voicemail.conf et modules.conf configurés' : 'Directive #tryinclude manquante ou conflit modules.conf détecté', ]; // 6. Certificats TLS (Let's Encrypt ou Certificat local Debian) if ($leDomain !== '') { $leCert = "/etc/letsencrypt/live/{$leDomain}/fullchain.pem"; $leKey = "/etc/letsencrypt/live/{$leDomain}/privkey.pem"; $leOk = is_readable($leCert) && is_readable($leKey); $checks[] = [ 'id' => 'tls_certs', 'label' => "Certificat TLS Let's Encrypt ({$leDomain})", 'ok' => $leOk, 'detail' => $leOk ? "Certificat Let's Encrypt lisible ({$leCert})" : "Certificat introuvable ou non lisible dans /etc/letsencrypt/live/{$leDomain}/", ]; } else { $fallbackOk = is_readable('/etc/asterisk/keys/asterisk.crt') && is_readable('/etc/asterisk/keys/asterisk.key'); $checks[] = [ 'id' => 'tls_certs', 'label' => 'Certificats TLS SIP (Port 5061)', 'ok' => $fallbackOk, 'detail' => $fallbackOk ? 'Certificat TLS local actif (/etc/asterisk/keys/asterisk.crt) — prêt pour Let\'s Encrypt' : 'Aucun domaine Let\'s Encrypt configuré', ]; } // 7. Connexion Asterisk Manager Interface (AMI) $amiOk = false; $amiDetail = ''; try { $ami = AmiClient::fromDatabase($db); $info = $ami->testConnection(); $amiOk = true; $amiDetail = ($info['version'] ?: $info['banner']) . ' (Socket AMI 127.0.0.1:' . ($settings['ami_port'] ?? 5038) . ')'; } catch (Throwable $e) { $amiDetail = $e->getMessage(); } $checks[] = [ 'id' => 'ami_connection', 'label' => 'Liaison Asterisk Manager Interface (AMI)', 'ok' => $amiOk, 'detail' => $amiDetail, ]; $allCriticalOk = true; foreach ($checks as $c) { if (!$c['ok'] && $c['id'] !== 'tls_certs') { $allCriticalOk = false; } } return [ 'healthy' => $allCriticalOk, 'checks' => $checks, ]; } 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'; if (is_file($pjsipConf) && is_writable($pjsipConf)) { $content = (string)file_get_contents($pjsipConf); if (!str_contains($content, 'pjsip.custom.conf')) { file_put_contents($pjsipConf, rtrim($content) . "\n\n#tryinclude \"pjsip.custom.conf\"\n"); } } $extConf = '/etc/asterisk/extensions.conf'; if (is_file($extConf) && is_writable($extConf)) { $content = (string)file_get_contents($extConf); if (!str_contains($content, 'extensions.custom.conf')) { file_put_contents($extConf, rtrim($content) . "\n\n#tryinclude \"extensions.custom.conf\"\n"); } } $mohConf = '/etc/asterisk/musiconhold.conf'; if (is_file($mohConf) && is_writable($mohConf)) { $content = (string)file_get_contents($mohConf); // Désactive la classe [default] native d'Asterisk pour laisser la priorité à musiconhold.custom.conf $cleaned = preg_replace('/^\[default\]/m', ';[default]', $content); $cleaned = preg_replace('/^mode=files/m', ';mode=files', $cleaned); $cleaned = preg_replace('/^directory=moh/m', ';directory=moh', $cleaned); if (!str_contains($cleaned, 'musiconhold.custom.conf')) { $cleaned = rtrim($cleaned) . "\n\n#tryinclude \"/etc/asterisk/musiconhold.custom.conf\"\n"; } if ($cleaned !== $content) { file_put_contents($mohConf, $cleaned); } } $vmConf = '/etc/asterisk/voicemail.conf'; if (is_file($vmConf) && is_writable($vmConf)) { $content = (string)file_get_contents($vmConf); $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); } 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); } 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')) { $cleaned = rtrim($cleaned) . "\n\n#tryinclude \"voicemail.custom.conf\"\n"; } if ($cleaned !== $content) { file_put_contents($vmConf, $cleaned); } } @mkdir('/var/spool/asterisk/voicemail/default', 0775, true); // Liens vers les répertoires de sons Debian (/usr/share/asterisk/sounds -> /var/lib/asterisk/sounds) @mkdir('/var/lib/asterisk/sounds', 0775, true); @mkdir('/var/lib/asterisk/sounds/custom', 0775, true); @mkdir('/var/lib/asterisk/moh', 0775, true); foreach (['fr', 'en', 'es'] as $lang) { $src = "/usr/share/asterisk/sounds/{$lang}"; $dst = "/var/lib/asterisk/sounds/{$lang}"; if (is_dir($src) && !file_exists($dst)) { @symlink($src, $dst); } if (is_dir($dst) && !file_exists("{$dst}/custom")) { @symlink('/var/lib/asterisk/sounds/custom', "{$dst}/custom"); } } if (!file_exists('/usr/share/asterisk/sounds/custom')) { @symlink('/var/lib/asterisk/sounds/custom', '/usr/share/asterisk/sounds/custom'); } // 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'; if (is_file($astConf) && is_writable($astConf)) { $content = (string)file_get_contents($astConf); $lang = 'fr'; try { $db = Database::getConnection(); $s = $db->query('SELECT system_language FROM settings WHERE id = 1')->fetch(); if (!empty($s['system_language'])) { $lang = trim((string)$s['system_language']); } } catch (\Throwable $e) {} if (preg_match('/^;?\s*defaultlanguage\s*=/m', $content)) { $cleaned = preg_replace('/^;?\s*defaultlanguage\s*=.*$/m', "defaultlanguage = {$lang}", $content); } else { $cleaned = preg_replace('/\[options\]/i', "[options]\ndefaultlanguage = {$lang}", $content); } if ($cleaned !== $content) { file_put_contents($astConf, $cleaned); } } } }