diff --git a/gen_pc_sidecar.php b/gen_pc_sidecar.php index bf6c176..6173306 100644 --- a/gen_pc_sidecar.php +++ b/gen_pc_sidecar.php @@ -16,156 +16,191 @@ $bootstrap_settings['freepbx_auth'] = false; require_once('/etc/freepbx.conf'); -$provision_dir = '/tftpboot'; // <-- adjust if needed -$out_file = $provision_dir . '/482567bcdeff-features.cfg'; -$do_notify = in_array('--notify', $argv, true); +function main(): void { -$db = FreePBX::Database(); -$rows = []; + $provision_dir = '/tftpboot'; + $file = $provision_dir . '/482567bcdeff-features.cfg'; + $argv = $_SERVER['argv'] ?? []; + $do_notify = in_array('--notify', $argv, true); -/** Try Core 'users' table first (name + extension) */ -try { - $stmt = $db->prepare(" - SELECT name, extension - FROM users - WHERE extension REGEXP '^[0-9]+$' - ORDER BY CAST(extension AS UNSIGNED) - "); - $stmt->execute(); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); -} catch (Exception $e) { - // ignore, fall back below + $pbdb = pull_db(); + + array_unshift($pbdb, [ + 'name' => 'Night time', + 'extension' => '*271', + ]); + + $blank = ['name' => ' ', 'extension' => ' ']; + array_splice($rows, 1, 0, array_fill(0, 5, $blank)); + + $xml = pull_xml_file($file); + + $attendant = remove_attendants($xml); + + write_attendants($attendant, $pbdb); + + write_to_file($provision_dir, $file, $xml); + + if ($do_notify) { + notify($pbdb); + } } -/** Fall back to PJSIP ps_endpoints (id is ext; parse callerid "Name <1001>") */ -if (!$rows) { - $stmt = $db->prepare(" - SELECT id AS extension, callerid - FROM ps_endpoints - ORDER BY CAST(id AS UNSIGNED) - "); - $stmt->execute(); - $tmp = $stmt->fetchAll(PDO::FETCH_ASSOC); - foreach ($tmp as $r) { - $ext = trim((string)$r['extension']); - if (!preg_match('/^[0-9]+$/', $ext)) continue; // only numeric extensions - $cid = trim((string)($r['callerid'] ?? '')); - // Try to parse descriptive name from callerid - $name = 'Ext ' . $ext; - if ($cid !== '') { - // Common formats: "John Smith" <1001> OR John Smith <1001> - if (preg_match('/^\"?([^\"<]+)\"?\s*<\s*' . preg_quote($ext, '/') . '\s*>$/', $cid, $m)) { - $name = trim($m[1]); - } else { - // If callerid is just a name without angle brackets, use it - if (strpos($cid, '<') === false) { - $name = $cid; - } - } - } - $rows[] = ['name' => $name, 'extension' => $ext]; - } + +function pull_db(): array { + $db = FreePBX::Database(); + $pbdb = []; + + /** Try Core 'users' table first (name + extension) */ + try { + $stmt = $db->prepare(" + SELECT name, extension + FROM users + WHERE extension REGEXP '^[0-9]+$' + ORDER BY CAST(extension AS UNSIGNED) + "); + $stmt->execute(); + $pbdb = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (Exception $e) { + // ignore, fall back below + } + + /** Fall back to PJSIP ps_endpoints (id is ext; parse callerid "Name <1001>") */ + if (!$pbdb) { + $stmt = $db->prepare(" + SELECT id AS extension, callerid + FROM ps_endpoints + ORDER BY CAST(id AS UNSIGNED) + "); + $stmt->execute(); + $tmp = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($tmp as $r) { + $ext = trim((string)$r['extension']); + if (!preg_match('/^[0-9]+$/', $ext)) continue; // only numeric extensions + $cid = trim((string)($r['callerid'] ?? '')); + // Try to parse descriptive name from callerid + $name = 'Ext ' . $ext; + if ($cid !== '') { + // Common formats: "John Smith" <1001> OR John Smith <1001> + if (preg_match('/^\"?([^\"<]+)\"?\s*<\s*' . preg_quote($ext, '/') . '\s*>$/', $cid, $m)) { + $name = trim($m[1]); + } else { + // If callerid is just a name without angle brackets, use it + if (strpos($cid, '<') === false) { + $name = $cid; + } + } + } + $pbdb[] = ['name' => $name, 'extension' => $ext]; + } + } + + if (!$pbdb) { + fwrite(STDERR, "No extensions found (users/ps_endpoints returned no pbdb).\n"); + exit(1); + } + + return $pbdb; } -if (!$rows) { - fwrite(STDERR, "No extensions found (users/ps_endpoints returned no rows).\n"); - exit(1); + +function pull_xml_file($file): DOMDocument { + $xmlString = file_get_contents($file); + if ($xmlString === false) { + throw new RuntimeException("Unable to read file: {$file}"); + } + + // Strip UTF-8 BOM and leading whitespace + $xmlString = preg_replace('/^\xEF\xBB\xBF/', '', $xmlString); // BOM + $xmlString = ltrim($xmlString); + + // If there are any stray bytes before the first '<', trim them + $firstAngle = strpos($xmlString, '<'); + if ($firstAngle !== false && $firstAngle > 0) { + $xmlString = substr($xmlString, $firstAngle); + } + + $xml = new DOMDocument('1.0', 'UTF-8'); + $xml->preserveWhiteSpace = true; // pretty-print + $xml->formatOutput = true; + $xml->xmlStandalone = true; // keep standalone="yes" + + if (!$xml->loadXML($xmlString)) { + $errs = libxml_get_errors(); + libxml_clear_errors(); + $msg = "Failed to parse XML.\n"; + foreach ($errs as $e) { + $msg .= "[level {$e->level}] {$e->message} at line {$e->line}\n"; + } + throw new RuntimeException($msg); + } + + return $xml; } -array_unshift($rows, [ - 'name' => 'Night time', - 'extension' => '*271', -]); +function remove_attendants($xml): DOMElement { + $xpath = new DOMXPath($xml); + $attendantNodes = $xpath->query('/polycomConfig/attendant'); + if ($attendantNodes->length === 0) { + throw new RuntimeException("No element found at /polycomConfig/attendant"); + } + /** @var DOMElement $attendant */ + $attendant = $attendantNodes->item(0); -$xmlString = file_get_contents($out_file); -if ($xmlString === false) { - throw new RuntimeException("Unable to read file: {$out_file}"); + // 1) Remove all existing attendant.resourceList.* attributes + $toRemove = []; + foreach ($attendant->attributes as $attr) { + if (strpos($attr->nodeName, 'attendant.resourceList.') === 0) { + $toRemove[] = $attr->nodeName; + } + } + foreach ($toRemove as $name) { + $attendant->removeAttribute($name); + } + + return $attendant; } -// Strip UTF-8 BOM and leading whitespace -$xmlString = preg_replace('/^\xEF\xBB\xBF/', '', $xmlString); // BOM -$xmlString = ltrim($xmlString); +function write_attendants($attendant, $pbdb): void { + $index = 1; + foreach ($pbdb as $r) { + $label = trim((string)($r['name'] ?? '')); + $address = trim((string)($r['extension'] ?? '')); + $type = trim((string)("normal")); -// If there are any stray bytes before the first '<', trim them -$firstAngle = strpos($xmlString, '<'); -if ($firstAngle !== false && $firstAngle > 0) { - $xmlString = substr($xmlString, $firstAngle); + $attendant->setAttribute("attendant.resourceList.{$index}.address", $address); + $attendant->setAttribute("attendant.resourceList.{$index}.label", $label); + $attendant->setAttribute("attendant.resourceList.{$index}.type", $type); + + $index++; + } } -$xml = new DOMDocument('1.0', 'UTF-8'); -$xml->preserveWhiteSpace = true; // pretty-print -$xml->formatOutput = true; -$xml->xmlStandalone = true; // keep standalone="yes" +function write_to_file($provision_dir, $out_file, $xml): void { + if (!is_dir($provision_dir)) { + fwrite(STDERR, "Provisioning directory not found: $provision_dir\n"); + exit(2); + } -if (!$xml->loadXML($xmlString)) { - $errs = libxml_get_errors(); - libxml_clear_errors(); - $msg = "Failed to parse XML.\n"; - foreach ($errs as $e) { - $msg .= "[level {$e->level}] {$e->message} at line {$e->line}\n"; - } - throw new RuntimeException($msg); + $tmpfile = $out_file . '.tmp'; + if ($xml->save($tmpfile) === false) { + fwrite(STDERR, "Failed to write temporary file $tmpfile\n"); + exit(3); + } + if (!@rename($tmpfile, $out_file)) { + @unlink($tmpfile); + fwrite(STDERR, "Failed to move $tmpfile to $out_file (permissions?)\n"); + exit(4); + } + + echo "Wrote $out_file \n"; } -$xpath = new DOMXPath($xml); -$attendantNodes = $xpath->query('/polycomConfig/attendant'); -if ($attendantNodes->length === 0) { - throw new RuntimeException("No element found at /polycomConfig/attendant"); -} -/** @var DOMElement $attendant */ -$attendant = $attendantNodes->item(0); - -// 1) Remove all existing attendant.resourceList.* attributes -$toRemove = []; -foreach ($attendant->attributes as $attr) { - if (strpos($attr->nodeName, 'attendant.resourceList.') === 0) { - $toRemove[] = $attr->nodeName; - } -} -foreach ($toRemove as $name) { - $attendant->removeAttribute($name); -} - -$index = 1; -foreach ($rows as $r) { - $label = trim((string)($r['name'] ?? '')); - $address = trim((string)($r['extension'] ?? '')); - $type = trim((string)("normal")); - - if ($address === '' && $index > 6) continue; - - $attendant->setAttribute("attendant.resourceList.{$index}.address", $address); - $attendant->setAttribute("attendant.resourceList.{$index}.label", $label); - $attendant->setAttribute("attendant.resourceList.{$index}.type", $type); - - $index++; -} - -/** Ensure provision dir exists */ -if (!is_dir($provision_dir)) { - fwrite(STDERR, "Provisioning directory not found: $provision_dir\n"); - exit(2); -} - -/** Atomic write */ -$tmpfile = $out_file . '.tmp'; -if ($xml->save($tmpfile) === false) { - fwrite(STDERR, "Failed to write temporary file $tmpfile\n"); - exit(3); -} -if (!@rename($tmpfile, $out_file)) { - @unlink($tmpfile); - fwrite(STDERR, "Failed to move $tmpfile to $out_file (permissions?)\n"); - exit(4); -} - -echo "Wrote $out_file \n"; - -/** Optional: push check-sync to re-download directory without reboot */ -if ($do_notify) { +function notify($pbdb): void { + /** Optional: push check-sync to re-download directory without reboot */ // Use the extension list we already built $notified = 0; - foreach ($rows as $r) { + foreach ($pbdb as $r) { $ext = trim((string)$r['extension']); if ($ext === '') continue; // In FreePBX, PJSIP endpoint id is typically the extension number @@ -175,3 +210,5 @@ if ($do_notify) { } echo "Sent check-sync to $notified endpoints\n"; } + +main();