Document the PHP api layer and fix what documenting it exposed

Writing down what each endpoint actually guarantees made the places it
didn't obvious — shell arguments reaching a crontab or a bash -c
unescaped, master.conf written without tmp+rename, and conf edits that
could be saved without ever being parsed.
This commit is contained in:
Gmer4Lfe
2026-08-02 10:11:39 -04:00
parent 6a959fb5e4
commit 987313e7dc
55 changed files with 3972 additions and 95 deletions
+17 -1
View File
@@ -282,7 +282,23 @@ function vv_conf_write_changes(array $changes): array {
) ?? $raw;
}
}
$results[$file] = vv_write_conf_raw($file, $raw);
// Only the scalar path escapes its value; the array, array_single and assoc_array
// paths splice the caller's text into the file verbatim, and the type comes from the
// request. Every script sources these files, so the result is parsed before it is
// allowed to replace a working conf.
$results[$file] = vv_conf_syntax_ok($raw) && vv_write_conf_raw($file, $raw);
}
return $results;
}
// bash -n against a private temp copy. Returns true when the content parses as a sourceable
// conf, false otherwise — never writes anything itself.
function vv_conf_syntax_ok(string $content): bool {
$tmp = tempnam(sys_get_temp_dir(), 'vvconf');
if ($tmp === false) return true; // cannot check — do not block the write
file_put_contents($tmp, $content);
$out = []; $rc = 0;
exec('bash -n ' . escapeshellarg($tmp) . ' 2>&1', $out, $rc);
@unlink($tmp);
return $rc === 0;
}
+6 -1
View File
@@ -476,7 +476,12 @@ function vv_auto_create_api_key(string $hostId, string $confFile): array {
if (!file_exists($script)) {
return ['ok' => false, 'error' => 'unraid_api_key_renew.sh not found'];
}
exec('bash ' . escapeshellarg($script) . ' 2>&1', $out, $rc);
// set_time_limit() does not cover exec() time on Linux, so the bound has to be external —
// otherwise a stalled unraid-api call holds a php-fpm worker open indefinitely.
exec('timeout 120 bash ' . escapeshellarg($script) . ' 2>&1', $out, $rc);
if ($rc === 124) {
return ['ok' => false, 'error' => 'Key renewal timed out after 120s'];
}
if ($rc !== 0) {
$msg = implode(' ', array_filter(array_map('trim', $out)));
return ['ok' => false, 'error' => $msg ?: 'Script failed'];
+6 -2
View File
@@ -579,7 +579,9 @@ function vv_conf_flag_set(string $name, bool $value): bool {
$content, -1, $count
);
if (!$count) return false;
return file_put_contents($confPath, $new) !== false;
// tmp+rename — every script sources master.conf, so a truncated write here is a
// system-wide outage, not a lost toggle.
return vv_write_conf_raw('master.conf', $new);
}
// Comment or uncomment a script's line in the first master.conf array that contains it.
@@ -607,7 +609,9 @@ function vv_conf_toggle_script(string $rel, bool $enable): bool {
}
unset($line);
if (!$changed) return true;
return file_put_contents($confPath, implode('', $lines)) !== false;
// tmp+rename — every script sources master.conf, so a truncated write here is a
// system-wide outage, not a lost toggle.
return vv_write_conf_raw('master.conf', implode('', $lines));
}
// Parse an orchestrator script to find which child scripts it calls.