commit 56c9bbb224fe05ad897f1a972742f0dab6aaef0a Author: poslop Date: Mon Dec 22 12:20:13 2025 -0600 initial diff --git a/gen_pc_sidecar.php b/gen_pc_sidecar.php new file mode 100644 index 0000000..edcc969 --- /dev/null +++ b/gen_pc_sidecar.php @@ -0,0 +1,179 @@ +#!/usr/bin/php -q + (speed-dial) so contacts do NOT auto-populate line keys. + * - Optional --notify will send PJSIP check-sync to all numeric endpoints. + */ + +$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); + +$db = FreePBX::Database(); +$rows = []; + +/** 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 +} + +/** 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]; + } +} + +if (!$rows) { + fwrite(STDERR, "No extensions found (users/ps_endpoints returned no rows).\n"); + exit(1); +} + +array_unshift($rows, [ + 'name' => 'Night time', + 'extension' => '*271', +]); + +$blank = ['name' => '', 'extension' => '']; +array_splice($rows, 1, 0, array_fill(0, 5, $blank)); + +$xmlString = file_get_contents($out_file); +if ($xmlString === false) { + throw new RuntimeException("Unable to read file: {$out_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); +} + +$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 === '') 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) { + // Use the extension list we already built + $notified = 0; + foreach ($rows as $r) { + $ext = trim((string)$r['extension']); + if ($ext === '') continue; + // In FreePBX, PJSIP endpoint id is typically the extension number + $cmd = "asterisk -rx \"pjsip send notify polycom-check-cfg endpoint " . escapeshellarg($ext) . "\""; + exec($cmd, $o, $rc); + if ($rc === 0) $notified++; + } + echo "Sent check-sync to $notified endpoints\n"; +} diff --git a/gen_polycom_directory.php b/gen_polycom_directory.php new file mode 100644 index 0000000..5216a01 --- /dev/null +++ b/gen_polycom_directory.php @@ -0,0 +1,139 @@ +#!/usr/bin/php -q + (speed-dial) so contacts do NOT auto-populate line keys. + * - Optional --notify will send PJSIP check-sync to all numeric endpoints. + */ + +$bootstrap_settings['freepbx_auth'] = false; +require_once('/etc/freepbx.conf'); + +$provision_dir = '/tftpboot/directory'; // <-- adjust if needed +$out_file = $provision_dir . '/000000000000-directory.xml'; +$do_notify = in_array('--notify', $argv, true); + +$db = FreePBX::Database(); +$rows = []; + +/** 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 +} + +/** 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]; + } +} + +if (!$rows) { + fwrite(STDERR, "No extensions found (users/ps_endpoints returned no rows).\n"); + exit(1); +} + +/** Build Polycom directory XML (no to avoid auto speed-dials) */ +$xml = new DOMDocument('1.0', 'UTF-8'); +$xml->formatOutput = true; +$root = $xml->createElement('directory'); +$xml->appendChild($root); + +foreach ($rows as $r) { + $name = trim((string)($r['name'] ?? '')); + $ext = trim((string)($r['extension'] ?? '')); + if ($ext === '') continue; + + // Split name into fn/ln if possible + $fn = $name; + $ln = ''; + if (strpos($name, ' ') !== false) { + $parts = preg_split('/\s+/', $name); + $fn = array_shift($parts); + $ln = implode(' ', $parts); + } + + $item = $xml->createElement('item'); + $item->appendChild($xml->createElement('fn', $fn)); + if ($ln !== '') { + $item->appendChild($xml->createElement('ln', $ln)); + } + $item->appendChild($xml->createElement('ct', $ext)); + + // Intentionally NOT writing (speed dial) to prevent auto line-key assignment. + $root->appendChild($item); +} + +/** 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 with " . $root->childNodes->length . " contacts\n"; + +/** Optional: push check-sync to re-download directory without reboot */ +if ($do_notify) { + // Use the extension list we already built + $notified = 0; + foreach ($rows as $r) { + $ext = trim((string)$r['extension']); + if ($ext === '') continue; + // In FreePBX, PJSIP endpoint id is typically the extension number + $cmd = "asterisk -rx \"pjsip send notify polycom-check-cfg endpoint " . escapeshellarg($ext) . "\""; + exec($cmd, $o, $rc); + if ($rc === 0) $notified++; + } + echo "Sent check-sync to $notified endpoints\n"; +}