Initial commit - NexusPBX IP Telephony Platform
This commit is contained in:
commit
c5de8d0c71
25 files changed
+18123
No files matched your search
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class PhonebookManager
|
||||
{
|
||||
public const AVATARS_DIR = '/var/www/nexus-pbx/public/avatars';
|
||||
|
||||
/**
|
||||
* Initialise le dossier des avatars.
|
||||
*/
|
||||
public static function initDirectory(): void
|
||||
{
|
||||
if (!is_dir(self::AVATARS_DIR)) {
|
||||
@mkdir(self::AVATARS_DIR, 0775, true);
|
||||
}
|
||||
@chown(self::AVATARS_DIR, 'www-data');
|
||||
@chgrp(self::AVATARS_DIR, 'www-data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise automatiquement les postes d'extensions vers l'annuaire d'entreprise.
|
||||
*/
|
||||
public static function syncExtensions(PDO $db): void
|
||||
{
|
||||
// 1. Récupère la cartographie des groupes d'extensions
|
||||
$egRows = $db->query("SELECT id, name, members FROM extension_groups")->fetchAll();
|
||||
$extToGroup = [];
|
||||
foreach ($egRows as $eg) {
|
||||
$members = json_decode((string)$eg['members'], true) ?: [];
|
||||
foreach ($members as $extNum) {
|
||||
if (!isset($extToGroup[(string)$extNum])) {
|
||||
$extToGroup[(string)$extNum] = trim((string)$eg['name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Récupère tous les postes
|
||||
$exts = $db->query("SELECT id, extension, name, voicemail_email FROM extensions")->fetchAll();
|
||||
$activeExtIds = [];
|
||||
|
||||
$findStmt = $db->prepare("SELECT id, display_name, internal_number, email, group_name FROM phonebook_contacts WHERE extension_id = :ext_id OR (extension_id IS NULL AND is_internal = 1 AND internal_number = :num)");
|
||||
$insertStmt = $db->prepare("
|
||||
INSERT INTO phonebook_contacts (extension_id, display_name, internal_number, email, group_name, is_internal)
|
||||
VALUES (:ext_id, :name, :num, :email, :grp, 1)
|
||||
");
|
||||
$updateStmt = $db->prepare("
|
||||
UPDATE phonebook_contacts
|
||||
SET extension_id = :ext_id, display_name = :name, internal_number = :num, email = :email, group_name = :grp, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = :id
|
||||
");
|
||||
|
||||
foreach ($exts as $ext) {
|
||||
$extId = (int)$ext['id'];
|
||||
$extNum = trim((string)$ext['extension']);
|
||||
$extName = trim((string)$ext['name']);
|
||||
$email = trim((string)($ext['voicemail_email'] ?? ''));
|
||||
$group = $extToGroup[$extNum] ?? 'Général';
|
||||
$activeExtIds[] = $extId;
|
||||
|
||||
$findStmt->execute([':ext_id' => $extId, ':num' => $extNum]);
|
||||
$existing = $findStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existing) {
|
||||
$updateStmt->execute([
|
||||
':id' => (int)$existing['id'],
|
||||
':ext_id' => $extId,
|
||||
':name' => $extName,
|
||||
':num' => $extNum,
|
||||
':email' => $email,
|
||||
':grp' => $group,
|
||||
]);
|
||||
} else {
|
||||
$insertStmt->execute([
|
||||
':ext_id' => $extId,
|
||||
':name' => $extName,
|
||||
':num' => $extNum,
|
||||
':email' => $email,
|
||||
':grp' => $group,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Supprime les contacts internes orphelins (si extension supprimée)
|
||||
if (!empty($activeExtIds)) {
|
||||
$inClause = implode(',', $activeExtIds);
|
||||
$db->exec("DELETE FROM phonebook_contacts WHERE is_internal = 1 AND extension_id IS NOT NULL AND extension_id NOT IN ({$inClause})");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste des contacts filtrée ou non par groupe.
|
||||
*/
|
||||
public static function getContacts(PDO $db, ?string $group = null): array
|
||||
{
|
||||
self::syncExtensions($db);
|
||||
|
||||
$params = [];
|
||||
$sql = "SELECT * FROM phonebook_contacts";
|
||||
|
||||
if ($group !== null && trim($group) !== '' && strtolower(trim($group)) !== 'all' && strtolower(trim($group)) !== 'tous') {
|
||||
$cleanGroup = trim($group);
|
||||
$sql .= " WHERE LOWER(group_name) = LOWER(:grp) OR LOWER(group_name) LIKE LOWER(:grp_like) OR LOWER(group_name) IN ('tous', 'all')";
|
||||
$params[':grp'] = $cleanGroup;
|
||||
$params[':grp_like'] = "%{$cleanGroup}%";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY display_name COLLATE NOCASE ASC";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste de tous les groupes d'annuaire disponibles.
|
||||
*/
|
||||
public static function getGroups(PDO $db): array
|
||||
{
|
||||
$groups = [];
|
||||
$egRows = $db->query("SELECT name FROM extension_groups ORDER BY name ASC")->fetchAll(PDO::FETCH_COLUMN);
|
||||
foreach ($egRows as $g) {
|
||||
$g = trim((string)$g);
|
||||
if ($g !== '') $groups[$g] = true;
|
||||
}
|
||||
$pbRows = $db->query("SELECT DISTINCT group_name FROM phonebook_contacts WHERE group_name != ''")->fetchAll(PDO::FETCH_COLUMN);
|
||||
foreach ($pbRows as $g) {
|
||||
$g = trim((string)$g);
|
||||
if ($g !== '') $groups[$g] = true;
|
||||
}
|
||||
|
||||
// Ajoute Issy-Les-Moulineaux si pas encore présent
|
||||
if (!isset($groups['Issy-Les-Moulineaux'])) {
|
||||
$groups['Issy-Les-Moulineaux'] = true;
|
||||
}
|
||||
|
||||
$result = array_keys($groups);
|
||||
sort($result, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un contact (création ou mise à jour).
|
||||
*/
|
||||
public static function saveContact(PDO $db, array $data): int
|
||||
{
|
||||
$id = isset($data['id']) && $data['id'] !== '' ? (int)$data['id'] : null;
|
||||
$name = trim((string)($data['display_name'] ?? ''));
|
||||
$internal = trim((string)($data['internal_number'] ?? ''));
|
||||
$mobile = trim((string)($data['mobile_number'] ?? ''));
|
||||
$other = trim((string)($data['other_number'] ?? ''));
|
||||
$email = trim((string)($data['email'] ?? ''));
|
||||
$photo = trim((string)($data['photo_url'] ?? ''));
|
||||
$group = trim((string)($data['group_name'] ?? 'Général')) ?: 'Général';
|
||||
$isInternal = !empty($data['is_internal']) ? 1 : 0;
|
||||
$extensionId = !empty($data['extension_id']) ? (int)$data['extension_id'] : null;
|
||||
|
||||
if ($name === '') {
|
||||
throw new InvalidArgumentException("Le nom du contact est obligatoire.");
|
||||
}
|
||||
if ($internal === '' && $mobile === '' && $other === '') {
|
||||
throw new InvalidArgumentException("Veuillez renseigner au moins un numéro de téléphone (Poste, Mobile ou Autre).");
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("
|
||||
UPDATE phonebook_contacts
|
||||
SET display_name = :name, internal_number = :internal, mobile_number = :mobile,
|
||||
other_number = :other, email = :email, photo_url = :photo, group_name = :grp,
|
||||
is_internal = :is_int, extension_id = :ext_id, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
':id' => $id,
|
||||
':name' => $name,
|
||||
':internal' => $internal,
|
||||
':mobile' => $mobile,
|
||||
':other' => $other,
|
||||
':email' => $email,
|
||||
':photo' => $photo,
|
||||
':grp' => $group,
|
||||
':is_int' => $isInternal,
|
||||
':ext_id' => $extensionId,
|
||||
]);
|
||||
return $id;
|
||||
} else {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO phonebook_contacts (display_name, internal_number, mobile_number, other_number, email, photo_url, group_name, is_internal, extension_id)
|
||||
VALUES (:name, :internal, :mobile, :other, :email, :photo, :grp, :is_int, :ext_id)
|
||||
");
|
||||
$stmt->execute([
|
||||
':name' => $name,
|
||||
':internal' => $internal,
|
||||
':mobile' => $mobile,
|
||||
':other' => $other,
|
||||
':email' => $email,
|
||||
':photo' => $photo,
|
||||
':grp' => $group,
|
||||
':is_int' => $isInternal,
|
||||
':ext_id' => $extensionId,
|
||||
]);
|
||||
return (int)$db->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un contact d'annuaire.
|
||||
*/
|
||||
public static function deleteContact(PDO $db, int $id): bool
|
||||
{
|
||||
$stmt = $db->prepare("DELETE FROM phonebook_contacts WHERE id = :id");
|
||||
return $stmt->execute([':id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine l'URL de base pour l'accès aux photos et avatars depuis les postes IP.
|
||||
*/
|
||||
public static function getBaseUrl(?PDO $db = null): string
|
||||
{
|
||||
if (!empty($_SERVER['HTTP_HOST'])) {
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
return "{$scheme}://" . $_SERVER['HTTP_HOST'];
|
||||
}
|
||||
|
||||
if ($db) {
|
||||
try {
|
||||
$st = $db->query("SELECT domain, external_ip FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($st['external_ip'])) {
|
||||
return "http://" . $st['external_ip'] . ":8080";
|
||||
}
|
||||
if (!empty($st['domain']) && $st['domain'] !== 'pbx.local') {
|
||||
return "http://" . $st['domain'] . ":8080";
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
}
|
||||
|
||||
$serverIp = !empty($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '127.0.0.1';
|
||||
return "http://{$serverIp}:8080";
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère le flux XML officiel Yealink IP Phone Remote Address Book.
|
||||
*/
|
||||
public static function renderYealinkXml(PDO $db, ?string $group = null): string
|
||||
{
|
||||
$contacts = self::getContacts($db, $group);
|
||||
$baseUrl = self::getBaseUrl($db);
|
||||
$groupTitle = ($group !== null && trim($group) !== '' && strtolower(trim($group)) !== 'all')
|
||||
? trim($group)
|
||||
: 'NexusPBX';
|
||||
|
||||
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><YealinkIPPhoneDirectory/>');
|
||||
$xml->addChild('Title', "Annuaire " . htmlspecialchars($groupTitle, ENT_XML1, 'UTF-8'));
|
||||
|
||||
foreach ($contacts as $c) {
|
||||
$name = trim((string)$c['display_name']);
|
||||
$phone1 = trim((string)$c['internal_number']);
|
||||
$phone2 = trim((string)$c['mobile_number']);
|
||||
$phone3 = trim((string)$c['other_number']);
|
||||
$email = trim((string)($c['email'] ?? ''));
|
||||
$photo = trim((string)($c['photo_url'] ?? ''));
|
||||
|
||||
if ($phone1 === '' && $phone2 !== '') {
|
||||
$phone1 = $phone2;
|
||||
$phone2 = '';
|
||||
}
|
||||
|
||||
if ($phone1 === '' && $phone2 === '' && $phone3 === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = $xml->addChild('DirectoryEntry');
|
||||
$entry->addChild('Name', htmlspecialchars($name, ENT_XML1, 'UTF-8'));
|
||||
$entry->addChild('Telephone', htmlspecialchars($phone1, ENT_XML1, 'UTF-8'));
|
||||
if ($phone2 !== '') {
|
||||
$entry->addChild('Mobile', htmlspecialchars($phone2, ENT_XML1, 'UTF-8'));
|
||||
}
|
||||
if ($phone3 !== '') {
|
||||
$entry->addChild('OtherTelephone', htmlspecialchars($phone3, ENT_XML1, 'UTF-8'));
|
||||
}
|
||||
if ($email !== '') {
|
||||
$entry->addChild('Email', htmlspecialchars($email, ENT_XML1, 'UTF-8'));
|
||||
}
|
||||
if ($photo !== '') {
|
||||
$fullPhotoUrl = (strpos($photo, '://') === false)
|
||||
? rtrim($baseUrl, '/') . '/' . ltrim($photo, '/')
|
||||
: $photo;
|
||||
$entry->addChild('Photo', htmlspecialchars($fullPhotoUrl, ENT_XML1, 'UTF-8'));
|
||||
$entry->addChild('Picture', htmlspecialchars($fullPhotoUrl, ENT_XML1, 'UTF-8'));
|
||||
}
|
||||
}
|
||||
|
||||
$dom = dom_import_simplexml($xml)->ownerDocument;
|
||||
$dom->formatOutput = true;
|
||||
return $dom->saveXML();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user