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
+93 -8
View File
@@ -1,8 +1,85 @@
<?php
// Rewrite a *_SCRIPTS array in master.conf with a new script order.
// POST: array_name (e.g. "DAILY_SCRIPTS"), scripts (JSON: [{"id":"rel/path.sh","enabled":true}, ...])
// Preserves original entry lines (including inline flags/args) where possible.
// Scripts absent from the new list are dropped; new scripts are added as fresh entries.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Rewrites one *_SCRIPTS array in master.conf with a new order and a new enabled/disabled
// state per entry — the drag-to-reorder on the scheduler page's orchestrator view.
//
// OPERATIONAL MODEL
// Order is behaviour, not presentation. An orchestrator runs its array top to bottom, so
// moving an entry changes when that script runs relative to the others — which is why this
// writes to master.conf rather than to a UI preference.
//
// The array block is located, its entries harvested, and the whole block replaced. Only the
// lines between the opening and closing parens are touched; everything else in master.conf
// is written back byte for byte, because the file is hand-maintained and full of comments
// and grouping no round trip through a parser would preserve.
//
// Disabled entries stay in the file, commented. Enabled state is expressed by the presence
// or absence of a leading '# ', the same convention conf_toggle.php uses.
//
// DESIGN PRINCIPLES
// Original entry text is preserved, not regenerated.
// Entries are harvested from the existing block keyed by script path, and reused
// verbatim when the same path appears in the new order. That is what keeps inline flags
// and arguments — "Media/cleanup.sh --deep" — through a reorder. Only a script that was
// not previously in the array is written fresh, as a bare quoted path.
//
// The opening and closing lines are preserved exactly.
// Both are carried over untouched rather than rebuilt, so a trailing comment on the
// array declaration survives.
//
// Absent means removed.
// A script in the file but not in the submitted order is dropped from the array. The
// page always submits the complete list, so absence is an instruction, not an omission.
//
// OPERATIONAL SAFEGUARDS
// POST only, checked before any parameter is read.
//
// The array name is pattern-matched and only ever used as a needle.
// ^[A-Z_]+_SCRIPTS$, then preg_quote()d and matched against existing lines. Nothing is
// opened or executed from it.
//
// Every entry is validated, and an invalid one fails the request rather than being skipped.
// ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check per entry. Skipping would be
// actively dangerous here: the array is rebuilt from the validated list alone, so a
// silently dropped entry is a script silently removed from its orchestrator. Validation
// completes before anything is written.
//
// A missing array aborts before the write.
// Both the block start and end must be found, otherwise the endpoint returns a named
// error and writes nothing. Without that, a typo'd array name would splice a block into
// an undefined position.
//
// master.conf is confirmed present and readable before either pass.
//
// The write is atomic.
// vv_write_conf_raw() writes .vv.tmp and rename()s. Every script sources master.conf, so
// a truncated write here would be a system-wide outage rather than a lost reorder.
//
// The push happens only after a confirmed write, and its result is returned — a partner
// that did not receive the new order runs these scripts in a different sequence.
//
// Known limit: block detection counts parens textually.
// depth is tracked with substr_count over '(' and ')', which does not know about quotes
// or comments. An entry whose arguments contained an unbalanced paren would end the
// block early. No current entry does, and the alternative is a bash parser — but a
// future entry with a paren in its arguments would be the thing that broke this.
//
// REQUEST
// POST array_name=<NAME>_SCRIPTS
// scripts=<JSON array of {"id":"Category/name.sh","enabled":bool}>
//
// RESPONSE
// {"ok":true,"push":[{"host","ok","ready","error"}, …]}
// {"ok":false,"error":"POST only"|"Invalid array_name"|"Invalid scripts JSON"
// |"Invalid script id: …"|"master.conf not found"
// |"Could not read master.conf"|"Array … not found in master.conf"
// |"Write failed"}
//
// DEPENDS ON
// include/config.php CONF_DIR, vv_write_conf_raw(), vv_push_master_conf(),
// vv_push_setup_state()
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
@@ -24,12 +101,17 @@ if (!is_array($decoded)) {
exit;
}
// Validate each entry
// Validate every entry before anything is written. A rejected entry cannot be skipped here:
// this endpoint rewrites the array from $order alone, so a silently dropped entry is a script
// silently removed from its orchestrator.
$order = [];
foreach ($decoded as $item) {
$id = trim((string)($item['id'] ?? ''));
$enabled = (bool)($item['enabled'] ?? true);
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id)) continue;
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id)) {
echo json_encode(['ok' => false, 'error' => 'Invalid script id: ' . $id]);
exit;
}
$order[] = ['id' => $id, 'enabled' => $enabled];
}
@@ -106,5 +188,8 @@ if (!vv_write_conf_raw('master.conf', implode('', $lines))) {
exit;
}
vv_push_master_conf();
echo json_encode(['ok' => true]);
// Reported rather than discarded — a partner that did not receive the new order is a partner
// running these scripts in a different sequence. Mirrors rawconf/confform/movescript.
$push = vv_push_master_conf();
vv_push_setup_state();
echo json_encode(['ok' => true, 'push' => $push]);