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
+60 -2
View File
@@ -1,6 +1,64 @@
<?php
// Save custom-script folder assignments to schedule.json (__folders key).
// POST: folders (JSON-encoded object: {"FolderName": ["Custom/script.sh", ...]})
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Script folder assignments. Saves the grouping of scripts into named folders on the
// scheduler page, stored in schedule.json under the __folders key.
//
// OPERATIONAL MODEL
// Whole-map replacement, not a patch. The page sends the complete folder structure and it
// replaces __folders entirely, so a removed folder disappears by absence. Reconciling
// individual moves would need a change log the UI does not have and cannot produce from a
// drag.
//
// Stored alongside the schedule rather than in a file of its own, under a key prefixed with
// __ to keep it out of the job namespace — the same convention __rsync_* uses. Everything
// that iterates the schedule as jobs skips these keys by prefix.
//
// DESIGN PRINCIPLES
// Presentation only. Folder membership changes nothing about whether or when a script runs;
// vv_cron_rebuild() never reads __folders, which is why this endpoint does not trigger one.
//
// Invalid entries are dropped, not rejected.
// A bad folder name or an unrecognised script id is skipped and the rest of the map is
// saved. Failing the whole request would lose a full reorganisation over one stale
// entry — and the entries most likely to be stale are scripts deleted since the page
// loaded.
//
// OPERATIONAL SAFEGUARDS
// POST only, checked before the payload is read.
//
// The payload must decode to an array.
// is_array() on the decoded JSON — a truncated or non-JSON body is refused outright
// rather than writing an empty map, which would silently erase every folder.
//
// Every script id is validated exactly as the run paths validate it.
// ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check, applied per entry. These ids are
// stored and later handed back to the page as job references, so a value that could not
// be run has no business being persisted next to ones that can.
//
// Folder names are length-capped and type-coerced.
// Cast to string, trimmed, empty rejected, and capped at 80 characters — a JSON object
// with numeric or absurdly long keys cannot bloat schedule.json, which is read on every
// scheduler page load and every cron rebuild.
//
// Non-array folder contents are skipped.
// is_array() per folder before iterating, so a malformed value cannot raise a warning
// into the JSON response.
//
// The rest of the schedule is preserved.
// The file is loaded, one key replaced, and the whole structure written back — job
// entries and __rsync_* keys are carried through untouched.
//
// REQUEST
// POST folders=<JSON object: {"<folder name>": ["Category/script.sh", …], …}>
//
// RESPONSE
// {"ok":true}
// {"ok":false,"error":"POST only"|"Invalid JSON"|"Write failed"}
//
// DEPENDS ON
// include/scheduler.php vv_schedule_load(), vv_schedule_save()
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/scheduler.php';