Files
NexusPBX/src/SystemChecker.php
T

290 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace App;
use PDO;
use Throwable;
require_once __DIR__ . '/ConfigGenerator.php';
require_once __DIR__ . '/AmiClient.php';
class SystemChecker
{
public static function runDiagnostics(PDO $db): array
{
$settings = $db->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);
}
}
}
}