833 lines
43 KiB
PHP
833 lines
43 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
use PDO;
|
|
|
|
class Database
|
|
{
|
|
private static ?PDO $instance = null;
|
|
public const DB_PATH = __DIR__ . '/../storage/database.sqlite';
|
|
|
|
public static function getDatabasePath(): string
|
|
{
|
|
return self::DB_PATH;
|
|
}
|
|
|
|
public static function resetConnection(): void
|
|
{
|
|
self::$instance = null;
|
|
}
|
|
|
|
public static function getConnection(bool $forceReconnect = false): PDO
|
|
{
|
|
if (self::$instance === null || $forceReconnect) {
|
|
self::$instance = null;
|
|
$dir = dirname(self::DB_PATH);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0770, true);
|
|
}
|
|
|
|
$isNew = !file_exists(self::DB_PATH) || filesize(self::DB_PATH) === 0;
|
|
|
|
self::$instance = new PDO('sqlite:' . self::DB_PATH, null, null, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]);
|
|
|
|
self::$instance->exec('PRAGMA foreign_keys = ON;');
|
|
self::$instance->exec('PRAGMA journal_mode = WAL;');
|
|
self::$instance->exec('PRAGMA busy_timeout = 5000;');
|
|
|
|
if ($isNew) {
|
|
self::initializeSchema();
|
|
@chmod(self::DB_PATH, 0660);
|
|
} else {
|
|
// Si la base existe déjà, éviter d'exécuter l'intégralité du DDL à chaque requête HTTP
|
|
// ce qui sature SQLite de verrous exclusifs inutiles. On teste juste l'existence de la table principale.
|
|
try {
|
|
self::$instance->query("SELECT 1 FROM settings LIMIT 1");
|
|
} catch (\Throwable $e) {
|
|
self::initializeSchema();
|
|
}
|
|
}
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
public static function initializeSchema(): void
|
|
{
|
|
$db = self::$instance;
|
|
if (!$db) {
|
|
return;
|
|
}
|
|
|
|
$schema = <<<SQL
|
|
-- 1. Table settings : Paramètres globaux du serveur Asterisk & AMI
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
domain TEXT NOT NULL DEFAULT 'pbx.local',
|
|
external_ip TEXT NOT NULL DEFAULT '',
|
|
local_net TEXT NOT NULL DEFAULT '192.168.1.0/24',
|
|
udp_port INTEGER NOT NULL DEFAULT 5060,
|
|
tls_port INTEGER NOT NULL DEFAULT 5061,
|
|
letsencrypt_domain TEXT NOT NULL DEFAULT '',
|
|
ami_host TEXT NOT NULL DEFAULT '127.0.0.1',
|
|
ami_port INTEGER NOT NULL DEFAULT 5038,
|
|
ami_user TEXT NOT NULL DEFAULT 'pbxgui',
|
|
ami_secret TEXT NOT NULL DEFAULT 'AsteriskAmi2026!',
|
|
wizard_completed INTEGER NOT NULL DEFAULT 0,
|
|
tls_verify_client INTEGER NOT NULL DEFAULT 0,
|
|
external_udp_port INTEGER NOT NULL DEFAULT 0,
|
|
external_tls_port INTEGER NOT NULL DEFAULT 0,
|
|
local_networks_json TEXT NOT NULL DEFAULT '["10.10.0.0/16","192.168.1.0/24"]',
|
|
rtp_start INTEGER NOT NULL DEFAULT 10000,
|
|
rtp_end INTEGER NOT NULL DEFAULT 20000,
|
|
rtp_checksums INTEGER NOT NULL DEFAULT 1,
|
|
rtp_strict INTEGER NOT NULL DEFAULT 1,
|
|
rtp_timeout INTEGER NOT NULL DEFAULT 30,
|
|
rtp_keepalive INTEGER NOT NULL DEFAULT 15,
|
|
allow_anonymous_sip INTEGER NOT NULL DEFAULT 0,
|
|
last_applied_at DATETIME DEFAULT NULL,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
INSERT OR IGNORE INTO settings (id) VALUES (1);
|
|
|
|
-- 2. Table users : Authentification sur l'interface d'administration
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username VARCHAR(64) NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 3. Table extensions : Postes téléphoniques PJSIP (UDP interne ou TLS+SRTP distant)
|
|
CREATE TABLE IF NOT EXISTS extensions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
extension VARCHAR(16) NOT NULL UNIQUE,
|
|
name VARCHAR(64) NOT NULL,
|
|
secret VARCHAR(128) NOT NULL,
|
|
transport VARCHAR(8) NOT NULL DEFAULT 'udp' CHECK (transport IN ('udp', 'tls')),
|
|
encryption INTEGER NOT NULL DEFAULT 0 CHECK (encryption IN (0, 1, 2)),
|
|
codecs VARCHAR(64) NOT NULL DEFAULT 'alaw,ulaw,opus',
|
|
max_contacts INTEGER NOT NULL DEFAULT 2,
|
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 4. Table groups : Groupes d'appel (Ring Groups)
|
|
CREATE TABLE IF NOT EXISTS groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
extension VARCHAR(16) NOT NULL UNIQUE,
|
|
name VARCHAR(64) NOT NULL,
|
|
strategy VARCHAR(16) NOT NULL DEFAULT 'ringall' CHECK (strategy IN ('ringall', 'hunt', 'random')),
|
|
ring_time INTEGER NOT NULL DEFAULT 20,
|
|
members TEXT NOT NULL DEFAULT '[]',
|
|
failover_dest VARCHAR(64) NOT NULL DEFAULT 'hangup',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 5. Table trunks : Opérateurs SIP entrants / sortants (Configuration PJSIP complète)
|
|
CREATE TABLE IF NOT EXISTS trunks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(64) NOT NULL UNIQUE,
|
|
host VARCHAR(128) NOT NULL DEFAULT '',
|
|
port INTEGER NOT NULL DEFAULT 5060,
|
|
outbound_proxy VARCHAR(128) NOT NULL DEFAULT '',
|
|
transport VARCHAR(8) NOT NULL DEFAULT 'udp',
|
|
auth_mode VARCHAR(16) NOT NULL DEFAULT 'userpass',
|
|
username VARCHAR(64) NOT NULL DEFAULT '',
|
|
auth_username VARCHAR(64) NOT NULL DEFAULT '',
|
|
secret VARCHAR(128) NOT NULL DEFAULT '',
|
|
registration_mode VARCHAR(16) NOT NULL DEFAULT 'send' CHECK (registration_mode IN ('send', 'receive', 'none')),
|
|
register INTEGER NOT NULL DEFAULT 1 CHECK (register IN (0, 1)),
|
|
server_uri VARCHAR(192) NOT NULL DEFAULT '',
|
|
client_uri VARCHAR(192) NOT NULL DEFAULT '',
|
|
contact_user VARCHAR(64) NOT NULL DEFAULT '',
|
|
expiration INTEGER NOT NULL DEFAULT 3600,
|
|
retry_interval INTEGER NOT NULL DEFAULT 60,
|
|
line_support INTEGER NOT NULL DEFAULT 1 CHECK (line_support IN (0, 1)),
|
|
from_user VARCHAR(64) NOT NULL DEFAULT '',
|
|
from_domain VARCHAR(128) NOT NULL DEFAULT '',
|
|
outbound_callerid VARCHAR(128) NOT NULL DEFAULT '',
|
|
send_pai INTEGER NOT NULL DEFAULT 1 CHECK (send_pai IN (0, 1)),
|
|
send_rpid INTEGER NOT NULL DEFAULT 0 CHECK (send_rpid IN (0, 1)),
|
|
trust_id_inbound INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_inbound IN (0, 1)),
|
|
trust_id_outbound INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_outbound IN (0, 1)),
|
|
codecs VARCHAR(64) NOT NULL DEFAULT 'alaw,ulaw,g722',
|
|
dtmf_mode VARCHAR(16) NOT NULL DEFAULT 'rfc4733',
|
|
media_encryption VARCHAR(16) NOT NULL DEFAULT 'no',
|
|
nat_symmetric INTEGER NOT NULL DEFAULT 1 CHECK (nat_symmetric IN (0, 1)),
|
|
direct_media INTEGER NOT NULL DEFAULT 0 CHECK (direct_media IN (0, 1)),
|
|
qualify_frequency INTEGER NOT NULL DEFAULT 60,
|
|
max_channels INTEGER NOT NULL DEFAULT 0,
|
|
match_ips TEXT NOT NULL DEFAULT '',
|
|
context VARCHAR(64) NOT NULL DEFAULT 'from-external',
|
|
inbound_dest VARCHAR(64) NOT NULL DEFAULT 'terminate-hangup',
|
|
did_routes_json TEXT NOT NULL DEFAULT '{}',
|
|
outbound_prefix VARCHAR(32) NOT NULL DEFAULT '',
|
|
custom_endpoint_params TEXT NOT NULL DEFAULT '',
|
|
cid_name_prefix VARCHAR(64) NOT NULL DEFAULT '',
|
|
dial_options VARCHAR(32) NOT NULL DEFAULT 'tTR',
|
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 6. Table ivrs : Serveurs vocaux interactifs (SVI / IVR)
|
|
CREATE TABLE IF NOT EXISTS ivrs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(64) NOT NULL UNIQUE,
|
|
audio_file VARCHAR(128) NOT NULL DEFAULT 'hello-world',
|
|
timeout INTEGER NOT NULL DEFAULT 5,
|
|
options_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 7. Table outbound_routes : Routes Sortantes (Priorité, Séquence Trunks, Dial Patterns, Droits Groupes & Postes)
|
|
CREATE TABLE IF NOT EXISTS outbound_routes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
priority INTEGER NOT NULL DEFAULT 10,
|
|
name VARCHAR(64) NOT NULL,
|
|
outbound_callerid VARCHAR(128) NOT NULL DEFAULT '',
|
|
override_ext_cid INTEGER NOT NULL DEFAULT 0 CHECK (override_ext_cid IN (0, 1)),
|
|
route_password VARCHAR(64) NOT NULL DEFAULT '',
|
|
route_type VARCHAR(32) NOT NULL DEFAULT 'normal',
|
|
moh_class VARCHAR(64) NOT NULL DEFAULT 'default',
|
|
trunk_sequence_json TEXT NOT NULL DEFAULT '[]',
|
|
dial_patterns_json TEXT NOT NULL DEFAULT '[]',
|
|
permission_mode VARCHAR(16) NOT NULL DEFAULT 'all',
|
|
allowed_groups_json TEXT NOT NULL DEFAULT '[]',
|
|
allowed_extensions_json TEXT NOT NULL DEFAULT '[]',
|
|
continue_next_route INTEGER NOT NULL DEFAULT 1 CHECK (continue_next_route IN (0, 1)),
|
|
congestion_dest VARCHAR(64) NOT NULL DEFAULT 'terminate-congestion',
|
|
record_mode VARCHAR(16) NOT NULL DEFAULT 'dontcare',
|
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 8. Table extension_groups : Groupes d'extensions (Sites, Services, Droits Routes Sortantes - ex: Nice, Paris)
|
|
CREATE TABLE IF NOT EXISTS extension_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(64) NOT NULL UNIQUE,
|
|
description VARCHAR(128) NOT NULL DEFAULT '',
|
|
color VARCHAR(32) NOT NULL DEFAULT 'cyan',
|
|
members TEXT NOT NULL DEFAULT '[]',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 9. Table sounds : Médiathèque & Fichiers Sons (MoH, annonces, IVR, congestion classés par site et usage)
|
|
CREATE TABLE IF NOT EXISTS sounds (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(128) NOT NULL,
|
|
slug VARCHAR(128) NOT NULL UNIQUE,
|
|
site_category VARCHAR(64) NOT NULL DEFAULT 'Global',
|
|
usage_type VARCHAR(32) NOT NULL DEFAULT 'announcement',
|
|
description VARCHAR(255) NOT NULL DEFAULT '',
|
|
filename VARCHAR(255) NOT NULL,
|
|
original_filename VARCHAR(255) NOT NULL DEFAULT '',
|
|
duration_sec INTEGER NOT NULL DEFAULT 0,
|
|
file_size INTEGER NOT NULL DEFAULT 0,
|
|
format VARCHAR(16) NOT NULL DEFAULT 'wav',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 10. Table sound_sites : Catégories de sites pour les sons (Global, Site Nice, Site Paris...)
|
|
CREATE TABLE IF NOT EXISTS sound_sites (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(64) NOT NULL UNIQUE,
|
|
description VARCHAR(255) NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 11. Table certificates : Gestionnaire de Certificats SSL / TLS (Let's Encrypt & Custom)
|
|
CREATE TABLE IF NOT EXISTS certificates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(128) NOT NULL,
|
|
type VARCHAR(32) NOT NULL DEFAULT 'custom', -- letsencrypt, custom, selfsigned
|
|
primary_domain VARCHAR(255) NOT NULL,
|
|
alt_names TEXT NOT NULL DEFAULT '',
|
|
issuer VARCHAR(255) NOT NULL DEFAULT '',
|
|
valid_from DATETIME DEFAULT NULL,
|
|
valid_to DATETIME DEFAULT NULL,
|
|
days_remaining INTEGER NOT NULL DEFAULT 0,
|
|
cert_path TEXT NOT NULL,
|
|
key_path TEXT NOT NULL,
|
|
chain_path TEXT NOT NULL DEFAULT '',
|
|
fullchain_path TEXT NOT NULL DEFAULT '',
|
|
email VARCHAR(255) NOT NULL DEFAULT '',
|
|
auto_renew INTEGER NOT NULL DEFAULT 1,
|
|
is_default INTEGER NOT NULL DEFAULT 0,
|
|
status VARCHAR(32) NOT NULL DEFAULT 'valid', -- valid, expiring_soon, expired, error
|
|
last_error TEXT NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 12. Table feature_codes : Codes de Fonctions Téléphoniques & Services Asterisk
|
|
CREATE TABLE IF NOT EXISTS feature_codes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
feature_key VARCHAR(64) UNIQUE NOT NULL,
|
|
code VARCHAR(16) NOT NULL,
|
|
default_code VARCHAR(16) NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
category VARCHAR(64) NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 13. Table phonebook_contacts : Annuaire d'Entreprise & Carnet d'Adresses IP Phone
|
|
CREATE TABLE IF NOT EXISTS phonebook_contacts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
extension_id INTEGER DEFAULT NULL,
|
|
display_name VARCHAR(128) NOT NULL,
|
|
internal_number VARCHAR(32) NOT NULL DEFAULT '',
|
|
mobile_number VARCHAR(32) NOT NULL DEFAULT '',
|
|
other_number VARCHAR(32) NOT NULL DEFAULT '',
|
|
email VARCHAR(128) NOT NULL DEFAULT '',
|
|
photo_url TEXT NOT NULL DEFAULT '',
|
|
group_name VARCHAR(64) NOT NULL DEFAULT 'Général',
|
|
is_internal INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_phonebook_ext ON phonebook_contacts(extension_id);
|
|
CREATE INDEX IF NOT EXISTS idx_phonebook_group ON phonebook_contacts(group_name);
|
|
|
|
-- 14. Table paging_groups : Groupes de téléavertissement & Interphonie (Paging)
|
|
CREATE TABLE IF NOT EXISTS paging_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
extension VARCHAR(16) NOT NULL UNIQUE,
|
|
name VARCHAR(64) NOT NULL,
|
|
description VARCHAR(255) NOT NULL DEFAULT '',
|
|
members TEXT NOT NULL DEFAULT '[]',
|
|
duplex INTEGER NOT NULL DEFAULT 0,
|
|
busy_mode VARCHAR(16) NOT NULL DEFAULT 'skip',
|
|
play_beep INTEGER NOT NULL DEFAULT 1,
|
|
timeout INTEGER NOT NULL DEFAULT 120,
|
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- 15. Table cluster_settings : Haute Disponibilité (Cluster HA Actif/Passif Keepalived/VRRP)
|
|
CREATE TABLE IF NOT EXISTS cluster_settings (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
enabled INTEGER NOT NULL DEFAULT 0 CHECK (enabled IN (0, 1)),
|
|
role VARCHAR(16) NOT NULL DEFAULT 'master',
|
|
virtual_ip VARCHAR(45) NOT NULL DEFAULT '',
|
|
virtual_netmask VARCHAR(16) NOT NULL DEFAULT '24',
|
|
interface VARCHAR(32) NOT NULL DEFAULT 'eth0',
|
|
peer_ip VARCHAR(45) NOT NULL DEFAULT '',
|
|
peer_port INTEGER NOT NULL DEFAULT 8443,
|
|
cluster_secret VARCHAR(64) NOT NULL DEFAULT '',
|
|
vrid INTEGER NOT NULL DEFAULT 51,
|
|
priority INTEGER NOT NULL DEFAULT 100,
|
|
sync_interval INTEGER NOT NULL DEFAULT 15,
|
|
auto_sync INTEGER NOT NULL DEFAULT 1,
|
|
last_sync_at DATETIME DEFAULT NULL,
|
|
last_sync_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
|
last_sync_message TEXT NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
SQL;
|
|
|
|
$db->exec($schema);
|
|
self::migrateSettingsTable($db);
|
|
self::migrateExtensionsTable($db);
|
|
self::migrateTrunksTable($db);
|
|
self::migrateOutboundRoutesTable($db);
|
|
self::migrateExtensionGroupsTable($db);
|
|
self::migrateSoundsTable($db);
|
|
self::migrateSoundSitesTable($db);
|
|
self::migrateIvrsTable($db);
|
|
self::migrateCertificatesTable($db);
|
|
self::migrateFeatureCodesTable($db);
|
|
self::migratePhonebookContactsTable($db);
|
|
self::migratePagingGroupsTable($db);
|
|
self::migrateClusterSettingsTable($db);
|
|
|
|
// Création du compte admin par défaut (admin / admin123) s'il n'existe aucun utilisateur
|
|
$userCount = (int)$db->query('SELECT COUNT(*) FROM users')->fetchColumn();
|
|
if ($userCount === 0) {
|
|
$stmt = $db->prepare('INSERT INTO users (username, password_hash) VALUES (:u, :p)');
|
|
$stmt->execute([
|
|
':u' => 'admin',
|
|
':p' => password_hash('admin123', PASSWORD_DEFAULT),
|
|
]);
|
|
}
|
|
}
|
|
|
|
private static function migrateSettingsTable(PDO $db): void
|
|
{
|
|
$cols = $db->query('PRAGMA table_info(settings)')->fetchAll();
|
|
$existing = array_column($cols, 'name');
|
|
|
|
if (!in_array('company_name', $existing, true)) {
|
|
$db->exec("ALTER TABLE settings ADD COLUMN company_name VARCHAR(128) NOT NULL DEFAULT ''");
|
|
}
|
|
if (!in_array('company_logo', $existing, true)) {
|
|
$db->exec("ALTER TABLE settings ADD COLUMN company_logo TEXT NOT NULL DEFAULT ''");
|
|
}
|
|
if (!in_array('default_moh_sound_id', $existing, true)) {
|
|
$db->exec("ALTER TABLE settings ADD COLUMN default_moh_sound_id INTEGER DEFAULT 2");
|
|
}
|
|
if (!in_array('tls_verify_client', $existing, true)) {
|
|
$db->exec("ALTER TABLE settings ADD COLUMN tls_verify_client INTEGER NOT NULL DEFAULT 0");
|
|
}
|
|
|
|
$extraSettings = [
|
|
'external_udp_port' => "INTEGER NOT NULL DEFAULT 0",
|
|
'external_tls_port' => "INTEGER NOT NULL DEFAULT 0",
|
|
'local_networks_json' => "TEXT NOT NULL DEFAULT '[\"10.10.0.0/16\",\"192.168.1.0/24\"]'",
|
|
'rtp_start' => "INTEGER NOT NULL DEFAULT 10000",
|
|
'rtp_end' => "INTEGER NOT NULL DEFAULT 20000",
|
|
'rtp_checksums' => "INTEGER NOT NULL DEFAULT 1",
|
|
'rtp_strict' => "INTEGER NOT NULL DEFAULT 1",
|
|
'rtp_timeout' => "INTEGER NOT NULL DEFAULT 30",
|
|
'rtp_keepalive' => "INTEGER NOT NULL DEFAULT 15",
|
|
'allow_anonymous_sip' => "INTEGER NOT NULL DEFAULT 0",
|
|
'system_language' => "VARCHAR(16) NOT NULL DEFAULT 'fr'",
|
|
'backup_auto_enabled' => "INTEGER NOT NULL DEFAULT 0",
|
|
'backup_frequency' => "VARCHAR(32) NOT NULL DEFAULT 'daily'",
|
|
'backup_retention' => "INTEGER NOT NULL DEFAULT 7",
|
|
'backup_storage_type' => "VARCHAR(16) NOT NULL DEFAULT 'local'",
|
|
'backup_ssh_host' => "VARCHAR(128) NOT NULL DEFAULT ''",
|
|
'backup_ssh_port' => "INTEGER NOT NULL DEFAULT 22",
|
|
'backup_ssh_user' => "VARCHAR(64) NOT NULL DEFAULT ''",
|
|
'backup_ssh_auth_type'=> "VARCHAR(16) NOT NULL DEFAULT 'password'",
|
|
'backup_ssh_password' => "TEXT NOT NULL DEFAULT ''",
|
|
'backup_ssh_remote_path' => "VARCHAR(255) NOT NULL DEFAULT '/backups/nexus'",
|
|
'backup_last_run' => "VARCHAR(32) DEFAULT NULL",
|
|
'backup_last_status' => "TEXT NOT NULL DEFAULT ''",
|
|
'firewall_enabled' => "INTEGER NOT NULL DEFAULT 1",
|
|
'admin_https_port' => "INTEGER NOT NULL DEFAULT 8443",
|
|
'certbot_strict_port80' => "INTEGER NOT NULL DEFAULT 0",
|
|
'fail2ban_sip_maxretry' => "INTEGER NOT NULL DEFAULT 3",
|
|
'fail2ban_sip_findtime' => "INTEGER NOT NULL DEFAULT 600",
|
|
'fail2ban_sip_bantime' => "INTEGER NOT NULL DEFAULT 3600",
|
|
'fail2ban_web_maxretry' => "INTEGER NOT NULL DEFAULT 3",
|
|
'fail2ban_web_bantime' => "INTEGER NOT NULL DEFAULT 3600",
|
|
'fail2ban_whitelist_ips' => "TEXT NOT NULL DEFAULT '127.0.0.1/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 82.122.84.60'",
|
|
];
|
|
foreach ($extraSettings as $colName => $colDef) {
|
|
if (!in_array($colName, $existing, true)) {
|
|
$db->exec("ALTER TABLE settings ADD COLUMN {$colName} {$colDef}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function migrateExtensionsTable(PDO $db): void
|
|
{
|
|
$sql = $db->query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'extensions'")->fetchColumn();
|
|
if ($sql && str_contains($sql, "CHECK (encryption IN (0, 1))")) {
|
|
$db->beginTransaction();
|
|
try {
|
|
$db->exec("CREATE TABLE extensions_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
extension VARCHAR(16) NOT NULL UNIQUE,
|
|
name VARCHAR(64) NOT NULL,
|
|
secret VARCHAR(128) NOT NULL,
|
|
transport VARCHAR(8) NOT NULL DEFAULT 'udp' CHECK (transport IN ('udp', 'tls')),
|
|
encryption INTEGER NOT NULL DEFAULT 0 CHECK (encryption IN (0, 1, 2)),
|
|
codecs VARCHAR(64) NOT NULL DEFAULT 'alaw,ulaw,opus',
|
|
max_contacts INTEGER NOT NULL DEFAULT 2,
|
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$db->exec("INSERT INTO extensions_new SELECT * FROM extensions");
|
|
$db->exec("DROP TABLE extensions");
|
|
$db->exec("ALTER TABLE extensions_new RENAME TO extensions");
|
|
$db->commit();
|
|
} catch (\Throwable $e) {
|
|
$db->rollBack();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
$cols = $db->query('PRAGMA table_info(extensions)')->fetchAll();
|
|
$existing = array_column($cols, 'name');
|
|
|
|
$extraExtensionCols = [
|
|
'voicemail_enabled' => "INTEGER NOT NULL DEFAULT 0",
|
|
'voicemail_pin' => "VARCHAR(32) NOT NULL DEFAULT ''",
|
|
'force_pin_change' => "INTEGER NOT NULL DEFAULT 0",
|
|
'voicemail_skip_pin' => "INTEGER NOT NULL DEFAULT 0",
|
|
'voicemail_email' => "VARCHAR(128) NOT NULL DEFAULT ''",
|
|
'voicemail_attach' => "INTEGER NOT NULL DEFAULT 1",
|
|
'voicemail_delete' => "INTEGER NOT NULL DEFAULT 0",
|
|
'ring_time' => "INTEGER NOT NULL DEFAULT 20",
|
|
];
|
|
foreach ($extraExtensionCols as $colName => $colDef) {
|
|
if (!in_array($colName, $existing, true)) {
|
|
$db->exec("ALTER TABLE extensions ADD COLUMN {$colName} {$colDef}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function migrateTrunksTable(PDO $db): void
|
|
{
|
|
$cols = $db->query('PRAGMA table_info(trunks)')->fetchAll();
|
|
$existing = array_column($cols, 'name');
|
|
|
|
if (!in_array('outbound_proxy', $existing, true)) {
|
|
// Migration sans perte de la table trunks vers le schéma PJSIP complet (avec support UDP/TCP/TLS)
|
|
$db->beginTransaction();
|
|
try {
|
|
$db->exec('ALTER TABLE trunks RENAME TO trunks_old');
|
|
$db->exec("
|
|
CREATE TABLE trunks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(64) NOT NULL UNIQUE,
|
|
host VARCHAR(128) NOT NULL,
|
|
port INTEGER NOT NULL DEFAULT 5060,
|
|
outbound_proxy VARCHAR(128) NOT NULL DEFAULT '',
|
|
transport VARCHAR(8) NOT NULL DEFAULT 'udp',
|
|
auth_mode VARCHAR(16) NOT NULL DEFAULT 'userpass',
|
|
username VARCHAR(64) NOT NULL DEFAULT '',
|
|
auth_username VARCHAR(64) NOT NULL DEFAULT '',
|
|
secret VARCHAR(128) NOT NULL DEFAULT '',
|
|
register INTEGER NOT NULL DEFAULT 1 CHECK (register IN (0, 1)),
|
|
server_uri VARCHAR(192) NOT NULL DEFAULT '',
|
|
client_uri VARCHAR(192) NOT NULL DEFAULT '',
|
|
contact_user VARCHAR(64) NOT NULL DEFAULT '',
|
|
expiration INTEGER NOT NULL DEFAULT 3600,
|
|
retry_interval INTEGER NOT NULL DEFAULT 60,
|
|
line_support INTEGER NOT NULL DEFAULT 1 CHECK (line_support IN (0, 1)),
|
|
from_user VARCHAR(64) NOT NULL DEFAULT '',
|
|
from_domain VARCHAR(128) NOT NULL DEFAULT '',
|
|
outbound_callerid VARCHAR(128) NOT NULL DEFAULT '',
|
|
send_pai INTEGER NOT NULL DEFAULT 1 CHECK (send_pai IN (0, 1)),
|
|
send_rpid INTEGER NOT NULL DEFAULT 0 CHECK (send_rpid IN (0, 1)),
|
|
trust_id_inbound INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_inbound IN (0, 1)),
|
|
codecs VARCHAR(64) NOT NULL DEFAULT 'alaw,ulaw,g722',
|
|
dtmf_mode VARCHAR(16) NOT NULL DEFAULT 'rfc4733',
|
|
media_encryption VARCHAR(16) NOT NULL DEFAULT 'no',
|
|
nat_symmetric INTEGER NOT NULL DEFAULT 1 CHECK (nat_symmetric IN (0, 1)),
|
|
direct_media INTEGER NOT NULL DEFAULT 0 CHECK (direct_media IN (0, 1)),
|
|
qualify_frequency INTEGER NOT NULL DEFAULT 60,
|
|
max_channels INTEGER NOT NULL DEFAULT 0,
|
|
match_ips TEXT NOT NULL DEFAULT '',
|
|
context VARCHAR(64) NOT NULL DEFAULT 'from-external',
|
|
inbound_dest VARCHAR(64) NOT NULL DEFAULT 'terminate-hangup',
|
|
did_routes_json TEXT NOT NULL DEFAULT '{}',
|
|
outbound_prefix VARCHAR(32) NOT NULL DEFAULT '',
|
|
custom_endpoint_params TEXT NOT NULL DEFAULT '',
|
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
");
|
|
$db->exec("
|
|
INSERT INTO trunks (id, name, host, port, username, secret, transport, register, codecs, inbound_dest, enabled, created_at)
|
|
SELECT id, name, host, port, username, secret, transport, register, codecs, inbound_dest, enabled, created_at
|
|
FROM trunks_old
|
|
");
|
|
$db->exec('DROP TABLE trunks_old');
|
|
$db->commit();
|
|
} catch (\Throwable $e) {
|
|
$db->rollBack();
|
|
throw $e;
|
|
}
|
|
$cols = $db->query('PRAGMA table_info(trunks)')->fetchAll();
|
|
$existing = array_column($cols, 'name');
|
|
}
|
|
|
|
// Colonnes universelles PJSIP supplémentaires (Session Timers, Fax T.38, Diversion, Retries, Langue, Keepalive)
|
|
$extraColumns = [
|
|
'registration_mode' => "VARCHAR(16) NOT NULL DEFAULT 'send'",
|
|
'max_retries' => "INTEGER NOT NULL DEFAULT 10000",
|
|
'forbidden_retry_interval' => "INTEGER NOT NULL DEFAULT 600",
|
|
'fatal_retry_interval' => "INTEGER NOT NULL DEFAULT 0",
|
|
'send_diversion' => "INTEGER NOT NULL DEFAULT 1",
|
|
'timers' => "VARCHAR(16) NOT NULL DEFAULT 'yes'",
|
|
'language' => "VARCHAR(16) NOT NULL DEFAULT 'fr'",
|
|
'rtp_keepalive' => "INTEGER NOT NULL DEFAULT 15",
|
|
't38_udptl' => "VARCHAR(16) NOT NULL DEFAULT 'no'",
|
|
'fax_detect' => "INTEGER NOT NULL DEFAULT 0",
|
|
'inband_progress' => "INTEGER NOT NULL DEFAULT 0",
|
|
'custom_reg_params' => "TEXT NOT NULL DEFAULT ''",
|
|
'custom_aor_params' => "TEXT NOT NULL DEFAULT ''",
|
|
'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0",
|
|
'cid_name_prefix' => "VARCHAR(64) NOT NULL DEFAULT ''",
|
|
'dial_options' => "VARCHAR(32) NOT NULL DEFAULT 'tTR'",
|
|
'trust_id_outbound' => "INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_outbound IN (0, 1))",
|
|
];
|
|
|
|
foreach ($extraColumns as $colName => $colDef) {
|
|
if (!in_array($colName, $existing, true)) {
|
|
$db->exec("ALTER TABLE trunks ADD COLUMN {$colName} {$colDef}");
|
|
if ($colName === 'registration_mode') {
|
|
$db->exec("UPDATE trunks SET registration_mode = CASE WHEN register = 1 THEN 'send' ELSE 'none' END");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function migrateOutboundRoutesTable(PDO $db): void
|
|
{
|
|
// Aucun enregistrement de démonstration auto-injecté pour conserver une base propre
|
|
}
|
|
|
|
private static function migrateExtensionGroupsTable(PDO $db): void
|
|
{
|
|
// Aucun enregistrement de démonstration auto-injecté pour conserver une base propre
|
|
}
|
|
|
|
private static function migrateSoundsTable(PDO $db): void
|
|
{
|
|
// Aucun enregistrement de démonstration auto-injecté pour conserver une base propre
|
|
}
|
|
|
|
private static function migrateSoundSitesTable(PDO $db): void
|
|
{
|
|
$count = (int)$db->query('SELECT COUNT(*) FROM sound_sites')->fetchColumn();
|
|
if ($count === 0) {
|
|
$stmt = $db->prepare('INSERT INTO sound_sites (name, description) VALUES (:n, :d)');
|
|
$stmt->execute([':n' => 'Global', ':d' => 'Fichiers généraux partagés par tout le PBX']);
|
|
}
|
|
}
|
|
|
|
private static function migrateIvrsTable(PDO $db): void
|
|
{
|
|
$cols = $db->query("PRAGMA table_info(ivrs)")->fetchAll(PDO::FETCH_ASSOC);
|
|
$existing = array_column($cols, 'name');
|
|
|
|
$extraColumns = [
|
|
'description' => "TEXT NOT NULL DEFAULT ''",
|
|
'direct_dial' => "INTEGER NOT NULL DEFAULT 1",
|
|
'invalid_retries' => "INTEGER NOT NULL DEFAULT 3",
|
|
'invalid_sound_id' => "INTEGER NOT NULL DEFAULT 0",
|
|
'invalid_dest' => "VARCHAR(64) NOT NULL DEFAULT 'hangup'",
|
|
'timeout_retries' => "INTEGER NOT NULL DEFAULT 2",
|
|
'timeout_sound_id' => "INTEGER NOT NULL DEFAULT 0",
|
|
'timeout_dest' => "VARCHAR(64) NOT NULL DEFAULT 'hangup'",
|
|
];
|
|
|
|
foreach ($extraColumns as $colName => $colDef) {
|
|
if (!in_array($colName, $existing, true)) {
|
|
$db->exec("ALTER TABLE ivrs ADD COLUMN {$colName} {$colDef}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function migrateCertificatesTable(PDO $db): void
|
|
{
|
|
$count = (int)$db->query('SELECT COUNT(*) FROM certificates')->fetchColumn();
|
|
if ($count === 0) {
|
|
$defaultCert = '/etc/asterisk/keys/asterisk.crt';
|
|
$defaultKey = '/etc/asterisk/keys/asterisk.key';
|
|
|
|
$domain = 'pbx.local';
|
|
$issuer = 'Asterisk PBX System';
|
|
$validFrom = date('Y-m-d H:i:s');
|
|
$validTo = date('Y-m-d H:i:s', time() + (3650 * 86400));
|
|
$daysRemaining = 3650;
|
|
|
|
if (file_exists($defaultCert)) {
|
|
$raw = @file_get_contents($defaultCert);
|
|
if ($raw) {
|
|
$parsed = @openssl_x509_parse($raw);
|
|
if ($parsed) {
|
|
$domain = $parsed['subject']['CN'] ?? 'pbx.local';
|
|
$issuer = $parsed['issuer']['O'] ?? ($parsed['issuer']['CN'] ?? 'Asterisk Self-Signed CA');
|
|
if (!empty($parsed['validFrom_time_t'])) {
|
|
$validFrom = date('Y-m-d H:i:s', $parsed['validFrom_time_t']);
|
|
}
|
|
if (!empty($parsed['validTo_time_t'])) {
|
|
$validTo = date('Y-m-d H:i:s', $parsed['validTo_time_t']);
|
|
$daysRemaining = (int)round(($parsed['validTo_time_t'] - time()) / 86400);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$stmt = $db->prepare('INSERT INTO certificates (
|
|
name, type, primary_domain, alt_names, issuer, valid_from, valid_to, days_remaining,
|
|
cert_path, key_path, chain_path, fullchain_path, is_default, status
|
|
) VALUES (
|
|
:name, :type, :domain, :alt_names, :issuer, :valid_from, :valid_to, :days_rem,
|
|
:cert_path, :key_path, :chain_path, :fullchain_path, 1, :status
|
|
)');
|
|
|
|
$stmt->execute([
|
|
':name' => 'Certificat Auto-signé Système',
|
|
':type' => 'selfsigned',
|
|
':domain' => $domain,
|
|
':alt_names' => '',
|
|
':issuer' => $issuer,
|
|
':valid_from' => $validFrom,
|
|
':valid_to' => $validTo,
|
|
':days_rem' => $daysRemaining,
|
|
':cert_path' => $defaultCert,
|
|
':key_path' => $defaultKey,
|
|
':chain_path' => '',
|
|
':fullchain_path' => $defaultCert,
|
|
':status' => ($daysRemaining > 0) ? 'valid' : 'expired',
|
|
]);
|
|
}
|
|
}
|
|
|
|
private static function migrateFeatureCodesTable(PDO $db): void
|
|
{
|
|
$db->exec("CREATE TABLE IF NOT EXISTS feature_codes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
feature_key VARCHAR(64) UNIQUE NOT NULL,
|
|
code VARCHAR(16) NOT NULL,
|
|
default_code VARCHAR(16) NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
category VARCHAR(64) NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
|
|
$defaults = [
|
|
// Supervision & Interception BLF
|
|
['pickup_blf', '**', '**', "Interception d'appel dirigée (BLF Yealink)", 'pickup', "Récupère l'appel du poste surveillé en appuyant sur sa touche BLF ou en composant ** suivi de son numéro."],
|
|
['pickup_group', '*8', '*8', "Interception d'appel de groupe", 'pickup', "Récupère n'importe quel appel qui sonne dans votre groupe de postes (ou composez *8 suivi du numéro pour une capture dirigée)."],
|
|
|
|
// Renvois d'appels
|
|
['cf_activate', '*72', '*72', "Activer le renvoi d'appels inconditionnel", 'forward', "Renvoie immédiatement tous les appels vers un autre numéro (composez *72 ou *72 suivi du numéro de destination)."],
|
|
['cf_deactivate', '*73', '*73', "Désactiver le renvoi d'appels inconditionnel", 'forward', "Annule le renvoi inconditionnel de vos appels."],
|
|
['cfu_activate', '*52', '*52', "Activer le renvoi sur non-réponse / indisponibilité", 'forward', "Renvoie vos appels uniquement en cas de non-réponse ou d'indisponibilité."],
|
|
['cfu_deactivate', '*53', '*53', "Désactiver le renvoi sur non-réponse / indisponibilité", 'forward', "Annule le renvoi sur non-réponse / indisponibilité."],
|
|
|
|
// Ne pas déranger (DND)
|
|
['dnd_activate', '*78', '*78', "Activer le mode Ne-pas-Déranger (DND)", 'dnd', "Rejette les appels entrants en envoyant le signal occupé ou renvoie vers la messagerie."],
|
|
['dnd_deactivate', '*79', '*79', "Désactiver le mode Ne-pas-Déranger (DND)", 'dnd', "Rétablit la sonnerie normale de votre combiné téléphonique."],
|
|
['dnd_toggle', '*76', '*76', "Inverser l'état Ne-pas-Déranger (Toggle)", 'dnd', "Bascule rapidement entre l'état Ne-pas-déranger Actif et Inactif."],
|
|
|
|
// Messagerie vocale
|
|
['voicemail_direct', '*97', '*97', "Accès direct à votre boîte vocale", 'voicemail', "Consulte vos messages vocaux sans mot de passe depuis votre propre poste."],
|
|
['voicemail_general', '*98', '*98', "Consulter la boîte vocale d'une autre extension", 'voicemail', "Accède à n'importe quelle boîte vocale en saisissant le numéro de poste et le code PIN."],
|
|
|
|
// Sécurité & Filtrage
|
|
['blacklist_add', '*30', '*30', "Ajouter un numéro à la liste noire (Blacklist)", 'security', "Bloque définitivement les appels provenant d'un numéro indésirable."],
|
|
['blacklist_remove', '*31', '*31', "Retirer un numéro de la liste noire", 'security', "Débloque un numéro précédemment blacklisté."],
|
|
['blacklist_last', '*32', '*32', "Bloquer instantanément le dernier appelant", 'security', "Ajoute immédiatement le numéro du dernier appel reçu à la liste noire."],
|
|
|
|
// Outils & Diagnostics
|
|
['speak_exten', '*65', '*65', "Identifier vocalement votre numéro de poste", 'tools', "Asterisk énonce à voix haute le numéro de l'extension actuelle (Speak your extension)."],
|
|
['last_caller', '*69', '*69', "Identifier vocalement le dernier appelant reçu", 'tools', "Énonce le numéro du dernier appel reçu et propose de le rappeler."],
|
|
['echo_test', '*43', '*43', "Test d'écho audio bidirectionnel", 'tools', "Permet de tester micro, écouteur et latence réseau en renvoyant votre voix en miroir."],
|
|
['speaking_clock', '*60', '*60', "Horloge parlante / Test audio", 'tools', "Donne la date et l'heure exacte et valide le fonctionnement des codecs audio."],
|
|
['moh_test', '*44', '*44', "Test d'écoute de la musique d'attente (MoH)", 'tools', "Diffuse la musique d'attente générale pour valider la qualité sonore."]
|
|
];
|
|
|
|
$stmtCheck = $db->prepare('SELECT COUNT(*) FROM feature_codes WHERE feature_key = :k');
|
|
$stmtInsert = $db->prepare('INSERT INTO feature_codes (feature_key, code, default_code, name, category, description, enabled) VALUES (:k, :c, :def, :n, :cat, :d, 1)');
|
|
|
|
foreach ($defaults as $item) {
|
|
$stmtCheck->execute([':k' => $item[0]]);
|
|
if ((int)$stmtCheck->fetchColumn() === 0) {
|
|
$stmtInsert->execute([
|
|
':k' => $item[0],
|
|
':c' => $item[1],
|
|
':def' => $item[2],
|
|
':n' => $item[3],
|
|
':cat' => $item[4],
|
|
':d' => $item[5],
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function migratePhonebookContactsTable(PDO $db): void
|
|
{
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS phonebook_contacts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
extension_id INTEGER DEFAULT NULL,
|
|
display_name VARCHAR(128) NOT NULL,
|
|
internal_number VARCHAR(32) NOT NULL DEFAULT '',
|
|
mobile_number VARCHAR(32) NOT NULL DEFAULT '',
|
|
other_number VARCHAR(32) NOT NULL DEFAULT '',
|
|
email VARCHAR(128) NOT NULL DEFAULT '',
|
|
photo_url TEXT NOT NULL DEFAULT '',
|
|
group_name VARCHAR(64) NOT NULL DEFAULT 'Général',
|
|
is_internal INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_phonebook_ext ON phonebook_contacts(extension_id);
|
|
CREATE INDEX IF NOT EXISTS idx_phonebook_group ON phonebook_contacts(group_name);
|
|
");
|
|
}
|
|
|
|
private static function migratePagingGroupsTable(PDO $db): void
|
|
{
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS paging_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
extension VARCHAR(16) NOT NULL UNIQUE,
|
|
name VARCHAR(64) NOT NULL,
|
|
description VARCHAR(255) NOT NULL DEFAULT '',
|
|
members TEXT NOT NULL DEFAULT '[]',
|
|
duplex INTEGER NOT NULL DEFAULT 0,
|
|
busy_mode VARCHAR(16) NOT NULL DEFAULT 'skip',
|
|
play_beep INTEGER NOT NULL DEFAULT 1,
|
|
timeout INTEGER NOT NULL DEFAULT 120,
|
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
");
|
|
}
|
|
|
|
private static function migrateClusterSettingsTable(PDO $db): void
|
|
{
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS cluster_settings (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
enabled INTEGER NOT NULL DEFAULT 0 CHECK (enabled IN (0, 1)),
|
|
role VARCHAR(16) NOT NULL DEFAULT 'master',
|
|
virtual_ip VARCHAR(45) NOT NULL DEFAULT '',
|
|
virtual_netmask VARCHAR(16) NOT NULL DEFAULT '24',
|
|
interface VARCHAR(32) NOT NULL DEFAULT 'eth0',
|
|
peer_ip VARCHAR(45) NOT NULL DEFAULT '',
|
|
peer_port INTEGER NOT NULL DEFAULT 8443,
|
|
cluster_secret VARCHAR(64) NOT NULL DEFAULT '',
|
|
vrid INTEGER NOT NULL DEFAULT 51,
|
|
priority INTEGER NOT NULL DEFAULT 100,
|
|
sync_interval INTEGER NOT NULL DEFAULT 15,
|
|
auto_sync INTEGER NOT NULL DEFAULT 1,
|
|
last_sync_at DATETIME DEFAULT NULL,
|
|
last_sync_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
|
last_sync_message TEXT NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
");
|
|
|
|
// Migration de la colonne peer_port si elle n'existe pas encore
|
|
try {
|
|
$db->exec("ALTER TABLE cluster_settings ADD COLUMN peer_port INTEGER NOT NULL DEFAULT 8443");
|
|
} catch (\Throwable $e) {}
|
|
|
|
$count = (int)$db->query('SELECT COUNT(*) FROM cluster_settings')->fetchColumn();
|
|
if ($count === 0) {
|
|
$defaultSecret = bin2hex(random_bytes(16));
|
|
$stmt = $db->prepare('
|
|
INSERT INTO cluster_settings (id, enabled, role, virtual_ip, virtual_netmask, interface, peer_ip, cluster_secret, vrid, priority, sync_interval, auto_sync)
|
|
VALUES (1, 0, :role, :vip, :vmask, :iface, :peer, :secret, 51, 100, 15, 1)
|
|
');
|
|
$stmt->execute([
|
|
':role' => 'master',
|
|
':vip' => '',
|
|
':vmask' => '24',
|
|
':iface' => 'eth0',
|
|
':peer' => '',
|
|
':secret' => $defaultSecret,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|