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:
@@ -1,7 +1,74 @@
|
||||
<?php
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Arr data endpoint. Serves the arrs tab: local Sonarr/Radarr/Lidarr statistics gathered
|
||||
// live, partner statistics served from the background cache, plus an on-demand refresh of
|
||||
// one partner's cache.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// Two modes on one URL. Without parameters it answers from a 300s cache and only builds a
|
||||
// fresh payload on a miss. With ?action=refresh_remote&host=hostN it shells out to
|
||||
// Tools/remote_arr_cache_writer.sh for that one host, returns the newly written node, and
|
||||
// busts the main cache so the next ordinary poll picks the change up.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// The refresh branch runs before the cache read, and exits.
|
||||
// It is a distinct operation, not a cache-control flag, so it never falls through into
|
||||
// the normal load path and cannot return a stale payload labelled as refreshed.
|
||||
//
|
||||
// Cache first for the ordinary case.
|
||||
// vv_arrs_all() contacts every configured arr instance. At the tab's poll rate that is
|
||||
// far too expensive to repeat, so the 300s cache is the default path and the live build
|
||||
// is the exception.
|
||||
//
|
||||
// include/arrs.php is required only on a cache miss.
|
||||
// A cache hit answers without loading the library at all, which is the difference
|
||||
// between a poll that costs a file read and one that costs an autoload.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// The host parameter is matched against a pattern, never used as a path.
|
||||
// ^host\d+$ is enforced before the value goes anywhere. It reaches the script only as
|
||||
// an escapeshellarg'd --host= value and the cache filename it composes, so neither a
|
||||
// shell metacharacter nor a traversal sequence can survive the check.
|
||||
//
|
||||
// The refresh is externally time-boxed.
|
||||
// set_time_limit() does not count time spent inside exec() on Linux, so PHP's own limit
|
||||
// cannot end a hung SSH call — the child is wrapped in `timeout` instead. Exit 124 is
|
||||
// reported as a timeout rather than a generic failure, so the UI can distinguish an
|
||||
// unreachable partner from a broken script.
|
||||
//
|
||||
// A missing script is reported, not executed.
|
||||
// The file_exists() check runs before exec(), so a partial deploy returns a named error
|
||||
// rather than a shell "command not found" surfacing as an empty refresh.
|
||||
//
|
||||
// The cache is busted after the write, not before.
|
||||
// @unlink() of arrs.json follows the script run, so a failed refresh leaves the previous
|
||||
// good payload in place instead of forcing every subsequent poll onto the live path.
|
||||
//
|
||||
// Read-only with respect to the arrs themselves. Statistics come from the databases the
|
||||
// scripts write; nothing here triggers a scan, cleanup, or import.
|
||||
//
|
||||
// REQUEST
|
||||
// GET cached (300s) full payload
|
||||
// GET ?action=refresh_remote&host=host<n> re-run the partner cache writer for one host
|
||||
//
|
||||
// RESPONSE
|
||||
// normal vv_arrs_all() verbatim — local node live, remote nodes cached with cache_age
|
||||
// refresh {"ok":bool,"node":object|null,"output":string}
|
||||
// errors {"ok":false,"error":string} — invalid host, missing script, or timeout
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/config.php vv_cache_read(), VV_CACHE_DIR
|
||||
// include/arrs.php vv_arrs_all() (loaded only on a cache miss)
|
||||
// Tools/remote_arr_cache_writer.sh (refresh branch only)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
// Bound on the partner refresh child process. set_time_limit() does not cover exec() time
|
||||
// on Linux, so this is enforced by `timeout`, not by PHP.
|
||||
define('VV_ARRS_REFRESH_TIMEOUT', 120);
|
||||
|
||||
// ── Manual remote refresh — runs remote_arr_cache_writer for one host ─────────
|
||||
$_action = trim($_GET['action'] ?? '');
|
||||
if ($_action === 'refresh_remote') {
|
||||
@@ -13,9 +80,19 @@ if ($_action === 'refresh_remote') {
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
|
||||
}
|
||||
set_time_limit(30);
|
||||
set_time_limit(VV_ARRS_REFRESH_TIMEOUT + 30);
|
||||
$out = []; $exit = 0;
|
||||
exec('bash ' . escapeshellarg($script) . ' --host=' . escapeshellarg(strtoupper($host)) . ' 2>&1', $out, $exit);
|
||||
exec('timeout ' . VV_ARRS_REFRESH_TIMEOUT . ' bash ' . escapeshellarg($script)
|
||||
. ' --host=' . escapeshellarg(strtoupper($host)) . ' 2>&1', $out, $exit);
|
||||
|
||||
if ($exit === 124) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'Refresh timed out after ' . VV_ARRS_REFRESH_TIMEOUT . 's',
|
||||
'output' => implode("\n", $out),
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $host . '.json';
|
||||
$node = null;
|
||||
|
||||
Reference in New Issue
Block a user