PHP app layer: consolidate common functions, fix critical bugs, standardize patterns

Consolidations (config.php gains 5 shared utilities):
- vv_format_uptime() replaces 4 inline uptime-formatting blocks
- vv_parse_conf_scalar() replaces vv_arr_scalar/vv_wd_scalar/vv_fb_scalar/vv_media_conf_scalar
- vv_known_hosts() replaces vv_arr_known_hosts/vv_fb_known_hosts + inline parser in watchdog
- vv_parse_kv_db() replaces inline key=value parsing in snapshot and monitor
- vv_local_ip() replaces duplicate in docker_folders.php and inline in docker.php
All module-level function names kept as thin aliases so call sites unchanged.

Critical bug fixes:
- api/system.php: added require_once config.php and POST-only guard (no auth on shutdown)
- api/movescript.php + reorderarray.php: use vv_write_conf_raw (atomic) + vv_push_master_conf
- api/snapshot.php: share /tmp/vv_cpu_stat.json with vv_cpu_per_core() instead of own state file

Correctness:
- vv_cpu_per_core() and vv_network_stats(): atomic tmp+rename for state files (concurrent poll safety)
- ext_ip curl cache moved from /tmp/vv_ext_ip.cache to vv_cache_read/write (canonical cache dir)
- monitor_remote.php + board.php + snapshot.php: all use vv_cache_read/write instead of ad-hoc /tmp files

HTTP method guards added to write-only APIs that were missing them:
- api/scheduler.php, conf_toggle.php, flag_toggle.php
This commit is contained in:
Gmer4Lfe
2026-06-04 16:44:16 -04:00
parent 6aaf04e25b
commit ce0b3fb74b
16 changed files with 127 additions and 123 deletions
+3 -7
View File
@@ -80,13 +80,9 @@ if (is_dir(LOG_DIR)) {
$out['errors'] = array_slice($errors, 0, 20); $out['errors'] = array_slice($errors, 0, 20);
// ── Partner reachability ─────────────────────────────────────────────────── // ── Partner reachability ───────────────────────────────────────────────────
$cacheFile = '/tmp/vv_partner_cache.json'; $partnerData = vv_cache_read('board_partner', 30);
$cacheTtl = 30;
$partnerData = null;
if (file_exists($cacheFile) && (time() - (int)filemtime($cacheFile)) < $cacheTtl) { if (!$partnerData) {
$partnerData = json_decode(file_get_contents($cacheFile), true);
} else {
// Discover partner hostname dynamically from master.conf (works for any number of hosts) // Discover partner hostname dynamically from master.conf (works for any number of hosts)
$vars = vv_conf_vars(); $vars = vv_conf_vars();
$mine = vv_get_hostname(); $mine = vv_get_hostname();
@@ -113,7 +109,7 @@ if (file_exists($cacheFile) && (time() - (int)filemtime($cacheFile)) < $cacheTtl
'reachable' => $reached, 'reachable' => $reached,
'latency' => $reached ? $elapsed : null, 'latency' => $reached ? $elapsed : null,
]; ];
@file_put_contents($cacheFile, json_encode($partnerData)); vv_cache_write('board_partner', $partnerData);
} }
} }
$out['partner'] = $partnerData; $out['partner'] = $partnerData;
+5
View File
@@ -2,6 +2,11 @@
header('Content-Type: application/json'); header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php'; require_once dirname(__DIR__) . '/include/scheduler.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
$id = trim($_POST['id'] ?? ''); $id = trim($_POST['id'] ?? '');
$enabled = ($_POST['enabled'] ?? '0') === '1'; $enabled = ($_POST['enabled'] ?? '0') === '1';
+5
View File
@@ -2,6 +2,11 @@
header('Content-Type: application/json'); header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php'; require_once dirname(__DIR__) . '/include/scheduler.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
$name = trim($_POST['name'] ?? ''); $name = trim($_POST['name'] ?? '');
$enabled = ($_POST['enabled'] ?? '0') === '1'; $enabled = ($_POST['enabled'] ?? '0') === '1';
+6 -13
View File
@@ -3,18 +3,11 @@ header('Content-Type: application/json');
header('Cache-Control: no-cache, no-store'); header('Cache-Control: no-cache, no-store');
require_once dirname(__DIR__) . '/include/monitor.php'; require_once dirname(__DIR__) . '/include/monitor.php';
$cacheFile = '/tmp/vv_cache_monitor_remote.json'; if (!isset($_GET['live'])) {
$cacheTTL = 3600; $cached = vv_cache_read('monitor_remote', 3600);
if ($cached) { echo json_encode($cached); exit; }
if (!isset($_GET['live']) && file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) {
echo file_get_contents($cacheFile);
exit;
} }
$out = json_encode([ $data = ['remote_hosts' => vv_remote_hosts_stats(), 'ts' => time()];
'remote_hosts' => vv_remote_hosts_stats(), vv_cache_write('monitor_remote', $data);
'ts' => time(), echo json_encode($data);
]);
file_put_contents($cacheFile, $out);
echo $out;
+2 -1
View File
@@ -70,9 +70,10 @@ if ($toArray) {
$newLines = $resultLines; $newLines = $resultLines;
} }
if (file_put_contents($confPath, implode('', $newLines)) === false) { if (!vv_write_conf_raw('master.conf', implode('', $newLines))) {
echo json_encode(['ok' => false, 'error' => 'Write failed']); echo json_encode(['ok' => false, 'error' => 'Write failed']);
exit; exit;
} }
vv_push_master_conf();
echo json_encode(['ok' => true]); echo json_encode(['ok' => true]);
+2 -1
View File
@@ -101,9 +101,10 @@ $newBlockLines[] = $lines[$blockEnd];
// Replace the original block in $lines // Replace the original block in $lines
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines); array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines);
if (file_put_contents($confPath, implode('', $lines)) === false) { if (!vv_write_conf_raw('master.conf', implode('', $lines))) {
echo json_encode(['ok' => false, 'error' => 'Write failed']); echo json_encode(['ok' => false, 'error' => 'Write failed']);
exit; exit;
} }
vv_push_master_conf();
echo json_encode(['ok' => true]); echo json_encode(['ok' => true]);
+5
View File
@@ -2,6 +2,11 @@
header('Content-Type: application/json'); header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php'; require_once dirname(__DIR__) . '/include/scheduler.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
// Batch save — all entries in one load/write/rebuild cycle // Batch save — all entries in one load/write/rebuild cycle
if (!empty($_POST['batch'])) { if (!empty($_POST['batch'])) {
$entries = json_decode($_POST['batch'], true) ?: []; $entries = json_decode($_POST['batch'], true) ?: [];
+14 -39
View File
@@ -3,61 +3,36 @@ header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/monitor.php'; require_once dirname(__DIR__) . '/include/monitor.php';
require_once dirname(__DIR__) . '/include/media.php'; require_once dirname(__DIR__) . '/include/media.php';
// CPU% — delta from own state file so it doesn't conflict with monitor.php // CPU% — shares /tmp/vv_cpu_stat.json with vv_cpu_per_core() so both read the same baseline
$cpuPct = 0; $cpuPct = vv_cpu_per_core()['overall'] ?? 0;
$cpuLine = '';
foreach (file('/proc/stat') ?: [] as $line) {
if (strncmp($line, 'cpu ', 4) === 0) { $cpuLine = $line; break; }
}
if (preg_match('/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/', $cpuLine, $m)) {
$c = [(int)$m[1],(int)$m[2],(int)$m[3],(int)$m[4],(int)$m[5],(int)$m[6],(int)$m[7]];
$sf = '/tmp/vv_snap_cpu.json';
$p = file_exists($sf) ? (json_decode(file_get_contents($sf), true) ?: null) : null;
file_put_contents($sf, json_encode($c));
if ($p && is_array($p)) {
$dt = array_sum($c) - array_sum($p);
$di = ($c[3] + $c[4]) - ($p[3] + $p[4]);
$cpuPct = $dt > 0 ? max(0, min(100, (int)round((1 - $di / $dt) * 100))) : 0;
}
}
// RAM% // RAM%
$mem = []; $res = vv_system_resources();
foreach (file('/proc/meminfo') ?: [] as $line) { $ramTotalMb = $res['ram_total_mb'];
if (preg_match('/^(MemTotal|MemAvailable):\s+(\d+)/', $line, $m)) $mem[$m[1]] = (int)$m[2]; $ramUsedMb = $ramTotalMb - $res['ram_free_mb'];
}
$ramTotalMb = (int)(($mem['MemTotal'] ?? 0) / 1024);
$ramUsedMb = (int)((($mem['MemTotal'] ?? 0) - ($mem['MemAvailable'] ?? 0)) / 1024);
$ramPct = $ramTotalMb > 0 ? (int)round($ramUsedMb / $ramTotalMb * 100) : 0; $ramPct = $ramTotalMb > 0 ? (int)round($ramUsedMb / $ramTotalMb * 100) : 0;
// Fallback state (fast file read, no exec) // Fallback state (fast file read, no exec)
$fallbackState = 'UNKNOWN'; $fbRaw = @file_get_contents('/tmp/fallback_state.db') ?: '';
foreach (@file('/tmp/fallback_state.db') ?: [] as $line) { $fbData = vv_parse_kv_db($fbRaw);
if (preg_match('/^state=(.+)/', trim($line), $m)) { $fallbackState = trim($m[1]); break; } $fallbackState = $fbData['state'] ?? 'UNKNOWN';
}
// Partner // Partner
$partner = vv_partner_state(); $partner = vv_partner_state();
$peers = array_values(array_filter($partner['hosts'], fn($h) => !$h['is_me'])); $peers = array_values(array_filter($partner['hosts'], fn($h) => !$h['is_me']));
// Media sessions — cached 30s so the HTTP calls don't hold up every snapshot poll // Media sessions — cached 30s so the HTTP calls don't hold up every snapshot poll
$streamCount = 0; $mediaCache = vv_cache_read('snap_media', 30);
$transcodeCount = 0; if (!$mediaCache) {
$mediaCacheFile = '/tmp/vv_snap_media.json';
$cacheMaxAge = 30;
$cacheValid = file_exists($mediaCacheFile) && (time() - filemtime($mediaCacheFile)) < $cacheMaxAge;
if ($cacheValid) {
$cached = json_decode(file_get_contents($mediaCacheFile), true) ?: [];
} else {
$media = vv_media_sessions(); $media = vv_media_sessions();
$cached = [ $mediaCache = [
'stream_count' => count($media['sessions']), 'stream_count' => count($media['sessions']),
'transcode_count' => count(array_filter($media['sessions'], fn($s) => !empty($s['is_tc']))), 'transcode_count' => count(array_filter($media['sessions'], fn($s) => !empty($s['is_tc']))),
]; ];
file_put_contents($mediaCacheFile, json_encode($cached)); vv_cache_write('snap_media', $mediaCache);
} }
$streamCount = (int)($cached['stream_count'] ?? 0); $streamCount = (int)($mediaCache['stream_count'] ?? 0);
$transcodeCount = (int)($cached['transcode_count'] ?? 0); $transcodeCount = (int)($mediaCache['transcode_count'] ?? 0);
echo json_encode([ echo json_encode([
'cpu_pct' => $cpuPct, 'cpu_pct' => $cpuPct,
+6
View File
@@ -1,5 +1,11 @@
<?php <?php
header('Content-Type: application/json'); header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
$body = json_decode(file_get_contents('php://input'), true) ?? []; $body = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $body['action'] ?? ''; $action = $body['action'] ?? '';
+2 -14
View File
@@ -6,23 +6,11 @@ require_once __DIR__ . '/config.php';
// ── Conf helpers ────────────────────────────────────────────────────────────── // ── Conf helpers ──────────────────────────────────────────────────────────────
function vv_arr_scalar(string $raw, string $key): string { function vv_arr_scalar(string $raw, string $key): string {
return preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) return vv_parse_conf_scalar($raw, $key);
? trim($m[1]) : '';
} }
// Returns all configured hosts found in master.conf as ['host1'=>'hostname', ...]
// Returns ['host1' => 'hostname', 'host2' => 'hostname', ...] from master.conf.
// Matches both HOST1="name" and HOST1_NAME="name" (either convention).
function vv_arr_known_hosts(): array { function vv_arr_known_hosts(): array {
$vars = vv_conf_vars(); // already parses HOST1="val" correctly return vv_known_hosts();
$hosts = [];
foreach ($vars as $k => $v) {
if (preg_match('/^HOST(\d+)$/', $k, $m) && $v !== '') {
$hosts['host' . $m[1]] = $v;
}
}
ksort($hosts);
return $hosts ?: ['host1' => 'HOST1'];
} }
function vv_arr_node_names(): array { function vv_arr_node_names(): array {
+13 -14
View File
@@ -23,10 +23,7 @@ function vv_system_info(): array {
$uptimeRaw = $os['uptime'] ?? ''; $uptimeRaw = $os['uptime'] ?? '';
if (is_numeric($uptimeRaw)) { if (is_numeric($uptimeRaw)) {
$uptimeSec = (int)$uptimeRaw; $uptimeSec = (int)$uptimeRaw;
$days = intdiv($uptimeSec, 86400); $uptime = vv_format_uptime($uptimeSec);
$hours = intdiv($uptimeSec % 86400, 3600);
$mins = intdiv($uptimeSec % 3600, 60);
$uptime = ($days ? "{$days}d " : '') . ($hours ? "{$hours}h " : '') . "{$mins}m";
} else { } else {
$uptimeSec = 0; $uptimeSec = 0;
$uptime = $uptimeRaw ?: '—'; $uptime = $uptimeRaw ?: '—';
@@ -57,10 +54,7 @@ function vv_system_info(): array {
if (preg_match('/^model name\s*:\s*(.+)/', $line, $m)) { $cpuModel = trim($m[1]); break; } if (preg_match('/^model name\s*:\s*(.+)/', $line, $m)) { $cpuModel = trim($m[1]); break; }
} }
$uptimeSec = (int)explode(' ', @file_get_contents('/proc/uptime') ?: '0')[0]; $uptimeSec = (int)explode(' ', @file_get_contents('/proc/uptime') ?: '0')[0];
$days = intdiv($uptimeSec, 86400); $uptime = vv_format_uptime($uptimeSec);
$hours = intdiv($uptimeSec % 86400, 3600);
$mins = intdiv($uptimeSec % 3600, 60);
$uptime = ($days ? "{$days}d " : '') . ($hours ? "{$hours}h " : '') . "{$mins}m";
$load = sys_getloadavg(); $load = sys_getloadavg();
return [ return [
@@ -159,7 +153,10 @@ function vv_cpu_per_core(): array {
$stateFile = '/tmp/vv_cpu_stat.json'; $stateFile = '/tmp/vv_cpu_stat.json';
$prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : []; $prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : [];
file_put_contents($stateFile, json_encode($raw)); // Atomic write — concurrent fast/slow polls read a consistent snapshot
$tmp = $stateFile . '.tmp';
file_put_contents($tmp, json_encode($raw));
rename($tmp, $stateFile);
$usage = function(array $c, ?array $p): int { $usage = function(array $c, ?array $p): int {
if (!$p) return 0; if (!$p) return 0;
@@ -298,7 +295,9 @@ function vv_network_stats(): array {
$stateFile = '/tmp/vv_net_stat.json'; $stateFile = '/tmp/vv_net_stat.json';
$now = ['rx' => $rxBytes, 'tx' => $txBytes, 'ts' => microtime(true)]; $now = ['rx' => $rxBytes, 'tx' => $txBytes, 'ts' => microtime(true)];
$prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : []; $prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : [];
file_put_contents($stateFile, json_encode($now)); $tmp = $stateFile . '.tmp';
file_put_contents($tmp, json_encode($now));
rename($tmp, $stateFile);
$rxRate = $txRate = 0; $rxRate = $txRate = 0;
if (!empty($prev['ts']) && ($dt = $now['ts'] - $prev['ts']) > 0.1) { if (!empty($prev['ts']) && ($dt = $now['ts'] - $prev['ts']) > 0.1) {
@@ -314,15 +313,15 @@ function vv_network_stats(): array {
) ?: ''); ) ?: '');
// External IP — curl ifconfig.me, cached 5 min so we don't hammer it // External IP — curl ifconfig.me, cached 5 min so we don't hammer it
$extIpCache = '/tmp/vv_ext_ip.cache';
$extIp = ''; $extIp = '';
if (file_exists($extIpCache) && (time() - filemtime($extIpCache)) < 300) { $extData = vv_cache_read('ext_ip', 300);
$extIp = trim(file_get_contents($extIpCache) ?: ''); if ($extData) {
$extIp = $extData['ip'] ?? '';
} else { } else {
$fetched = trim(shell_exec('curl -sf --max-time 4 https://ifconfig.me 2>/dev/null') ?: ''); $fetched = trim(shell_exec('curl -sf --max-time 4 https://ifconfig.me 2>/dev/null') ?: '');
if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $fetched)) { if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $fetched)) {
$extIp = $fetched; $extIp = $fetched;
file_put_contents($extIpCache, $extIp); vv_cache_write('ext_ip', ['ip' => $extIp]);
} }
} }
+54
View File
@@ -312,3 +312,57 @@ function vv_cache_write(string $key, array $data): void {
file_put_contents($tmp, json_encode($data)); file_put_contents($tmp, json_encode($data));
rename($tmp, $f); rename($tmp, $f);
} }
// ── Shared utility functions (used across include/ and api/ files) ────────────
// Format seconds into "2d 3h 15m".
function vv_format_uptime(int $seconds): string {
$d = intdiv($seconds, 86400);
$h = intdiv($seconds % 86400, 3600);
$m = intdiv($seconds % 3600, 60);
return ($d ? "{$d}d " : '') . ($h ? "{$h}h " : '') . "{$m}m";
}
// Parse a scalar value from raw conf text. Matches KEY="value" or KEY=value.
// Identical logic was previously duplicated as vv_arr_scalar / vv_wd_scalar /
// vv_fb_scalar / vv_media_conf_scalar — all reduce to this one regex.
function vv_parse_conf_scalar(string $raw, string $key): string {
return preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m)
? trim($m[1]) : '';
}
// Parse a key=value state file (e.g. fallback_state.db, partnership_state.db).
// Returns ['key' => 'value', ...]. Lines without '=' are ignored.
function vv_parse_kv_db(string $text): array {
$out = [];
foreach (explode("\n", $text) as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') continue;
[$k, $v] = array_pad(explode('=', $line, 2), 2, '');
if ($k !== '') $out[trim($k)] = trim($v);
}
return $out;
}
// All configured hosts from master.conf as ['host1' => 'hostname', ...].
// Canonical version — previously duplicated as vv_arr_known_hosts / vv_fb_known_hosts.
function vv_known_hosts(): array {
$vars = vv_conf_vars();
$hosts = [];
foreach ($vars as $k => $v) {
if (preg_match('/^HOST(\d+)$/', $k, $m) && $v !== '') {
$hosts['host' . $m[1]] = $v;
}
}
ksort($hosts);
return $hosts ?: ['host1' => 'HOST1'];
}
// Local LAN IP via routing table — static-cached per request.
// Previously duplicated in include/docker_folders.php and inline in include/docker.php.
function vv_local_ip(): string {
static $ip = null;
if ($ip !== null) return $ip;
$ip = trim(shell_exec("ip route get 8.8.8.8 2>/dev/null | awk '/src/{for(i=1;i<=NF;i++)if(\$i==\"src\")print \$(i+1)}'") ?? '');
return $ip;
}
+1 -7
View File
@@ -1,11 +1,5 @@
<?php <?php
function vv_local_ip(): string { require_once __DIR__ . '/config.php';
static $ip = null;
if ($ip !== null) return $ip;
$ip = trim(shell_exec("ip route get 8.8.8.8 2>/dev/null | awk '/src/{for(i=1;i<=NF;i++)if(\$i==\"src\")print \$(i+1)}'") ?? '');
if (!$ip) $ip = gethostbyname(gethostname());
return $ip;
}
function vv_container_webui(string $name, array $portMap): string { function vv_container_webui(string $name, array $portMap): string {
$template = '/boot/config/plugins/dockerMan/templates-user/my-' . $name . '.xml'; $template = '/boot/config/plugins/dockerMan/templates-user/my-' . $name . '.xml';
+3 -16
View File
@@ -7,15 +7,11 @@ require_once __DIR__ . '/partnership.php'; // vv_pt_ssh(), vv_pt_ts_peers()
// ── Conf parsers ────────────────────────────────────────────────────────────── // ── Conf parsers ──────────────────────────────────────────────────────────────
function vv_fb_bash_array(string $raw, string $varname): array { function vv_fb_bash_array(string $raw, string $varname): array {
if (!preg_match('/^\s*' . preg_quote($varname, '/') . '\s*=\s*\(\s*(.*?)\s*\)/ms', $raw, $m)) return vv_parse_bash_array($raw, $varname);
return [];
preg_match_all('/"([^"]*)"/', $m[1], $items);
return array_values(array_filter($items[1]));
} }
function vv_fb_scalar(string $raw, string $varname): string { function vv_fb_scalar(string $raw, string $varname): string {
return preg_match('/^\s*' . preg_quote($varname, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) return vv_parse_conf_scalar($raw, $varname);
? trim($m[1]) : '';
} }
// ── State file ──────────────────────────────────────────────────────────────── // ── State file ────────────────────────────────────────────────────────────────
@@ -90,16 +86,7 @@ function vv_fb_covers(string $covering, string $remote, string $coveringRaw, str
// ── Known hosts ─────────────────────────────────────────────────────────────── // ── Known hosts ───────────────────────────────────────────────────────────────
function vv_fb_known_hosts(): array { function vv_fb_known_hosts(): array {
$master = vv_read_conf_raw('master.conf'); return vv_known_hosts();
preg_match_all('/^\s*(HOST(\d+))(?:_NAME)?\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m);
$hosts = [];
foreach ($m[1] as $i => $key) {
$num = $m[2][$i];
$name = trim($m[3][$i]);
$hosts['host' . $num] = $name;
}
ksort($hosts);
return $hosts ?: ['host1' => 'HOST1'];
} }
// ── Main data builder ───────────────────────────────────────────────────────── // ── Main data builder ─────────────────────────────────────────────────────────
+1 -2
View File
@@ -6,8 +6,7 @@ require_once __DIR__ . '/config.php';
// ── Conf reader ─────────────────────────────────────────────────────────────── // ── Conf reader ───────────────────────────────────────────────────────────────
function vv_media_conf_scalar(string $raw, string $key): string { function vv_media_conf_scalar(string $raw, string $key): string {
return preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) return vv_parse_conf_scalar($raw, $key);
? trim($m[1]) : '';
} }
// ── Server list from host conf ──────────────────────────────────────────────── // ── Server list from host conf ────────────────────────────────────────────────
+2 -6
View File
@@ -7,10 +7,7 @@ require_once __DIR__ . '/partnership.php';
// ── Conf array parser (bash arrays) ────────────────────────────────────────── // ── Conf array parser (bash arrays) ──────────────────────────────────────────
function vv_wd_bash_array(string $raw, string $varname): array { function vv_wd_bash_array(string $raw, string $varname): array {
if (!preg_match('/^\s*' . preg_quote($varname, '/') . '\s*=\s*\(\s*(.*?)\s*\)/ms', $raw, $m)) return vv_parse_bash_array($raw, $varname);
return [];
preg_match_all('/"([^"]*)"/', $m[1], $items);
return array_values(array_filter($items[1]));
} }
function vv_wd_bash_assoc(string $raw, string $varname): array { function vv_wd_bash_assoc(string $raw, string $varname): array {
@@ -24,8 +21,7 @@ function vv_wd_bash_assoc(string $raw, string $varname): array {
} }
function vv_wd_scalar(string $raw, string $varname): string { function vv_wd_scalar(string $raw, string $varname): string {
return preg_match('/^\s*' . preg_quote($varname, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) return vv_parse_conf_scalar($raw, $varname);
? trim($m[1]) : '';
} }
// ── State file parsers ──────────────────────────────────────────────────────── // ── State file parsers ────────────────────────────────────────────────────────