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
+85 -4
View File
@@ -1,6 +1,84 @@
<?php
// Move a script between *_SCRIPTS arrays in master.conf.
// POST: script (rel path), to_array (var name, or '' to remove from all arrays).
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Moves a script between orchestrators. Removes its line from whichever *_SCRIPTS array in
// master.conf currently holds it and inserts it into the named one — or into none, which
// removes it from every orchestrator.
//
// OPERATIONAL MODEL
// Line surgery on master.conf, not a re-serialisation. The file is read as lines, one line
// is relocated, and the rest is written back byte for byte. master.conf is hand-maintained
// and full of comments, grouping and deliberate ordering that no round trip through a
// parser would preserve.
//
// Two passes, in order: remove first, then insert. Doing it in one pass would need to know
// whether the target array comes before or after the source, and getting that wrong would
// either duplicate the entry or drop it.
//
// Position within the target array is the end, immediately before its closing paren.
// Orchestrators run their arrays in order, so appending is the only placement that does not
// silently reorder someone else's work.
//
// DESIGN PRINCIPLES
// Move is remove-plus-insert, and remove alone is a valid operation.
// An empty to_array performs only the removal pass, which is how a script is taken out
// of every orchestrator. That is a distinct intent from conf_toggle.php's commenting
// out — this removes the line, that disables it in place.
//
// Indentation is normalised on re-insertion.
// The moved line is rewritten as two spaces and the quoted path, so a script does not
// carry its old array's formatting into its new one.
//
// A move that finds nothing to move still succeeds.
// The removal pass is best-effort; only a missing *target* is an error. A script that
// was in no array is simply added to the one requested.
//
// OPERATIONAL SAFEGUARDS
// POST only, checked before any parameter is read.
//
// Both inputs are pattern-matched, and neither is used as a path.
// The script must match ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check, and the
// array name ^[A-Z_]+_SCRIPTS$. Both then reach the matcher only through preg_quote(),
// so they are needles matched against existing lines — nothing is opened or executed
// from either.
//
// The scope of both passes is bounded to array bodies.
// Each pass tracks whether it is inside a *_SCRIPTS=( block and ignores every line
// outside one, so a matching string in a comment or an unrelated variable is never
// moved or displaced.
//
// A missing target array aborts before the write.
// If the insert pass never finds the target, the endpoint returns an error and writes
// nothing — the removal is discarded with it. Without that check a typo'd array name
// would silently delete the script from the orchestrator it was in.
//
// master.conf is confirmed present and readable before either pass.
// Both file_exists() and the file() result are checked, so a missing or unreadable conf
// returns a named error rather than writing a file built from an empty line list.
//
// 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 move.
//
// The push happens only after a confirmed write, and its result is returned.
// A partner that did not receive the move is exactly the state that makes one host run
// a script the other does not, so the per-host outcome is reported rather than
// discarded.
//
// REQUEST
// POST script=<Category/name.sh> to_array=<NAME>_SCRIPTS
// POST script=<Category/name.sh> to_array= remove from all arrays
//
// RESPONSE
// {"ok":true,"push":[{"host","ok","ready","error"}, …]}
// {"ok":false,"error":"POST only"|"Invalid script"|"Invalid array name"
// |"master.conf not found"|"Could not read master.conf"
// |"Target 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';
@@ -75,5 +153,8 @@ if (!vv_write_conf_raw('master.conf', implode('', $newLines))) {
exit;
}
vv_push_master_conf();
echo json_encode(['ok' => true]);
// Reported rather than discarded — a partner that did not receive the move is exactly the
// state that makes one host run a script the other does not. Mirrors rawconf/confform.
$push = vv_push_master_conf();
vv_push_setup_state();
echo json_encode(['ok' => true, 'push' => $push]);