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
+52
View File
@@ -1,4 +1,56 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Stale lock removal. Deletes one lock file from /tmp/unraid_locks so a script whose
// previous run died without releasing its lock can be started again from the UI.
//
// OPERATIONAL MODEL
// The manual escape hatch for the one failure the lock discipline cannot resolve on its
// own. Scripts take a lock to prevent concurrent runs; a run killed by OOM, a reboot mid-
// job, or kill -9 leaves the lock behind and every subsequent run refuses to start. This
// endpoint is how a person clears that, and it is deliberately the only way — nothing
// automatically reaps locks, because "the lock is old" and "the job is still running" are
// not distinguishable from the file alone.
//
// DESIGN PRINCIPLES
// Loads nothing.
// This is the only endpoint in the api layer that requires no include. The operation is
// one unlink in one fixed directory; pulling in the config layer to perform it would
// add failure modes to something that must work when other things are broken.
//
// Deletes exactly one lock, never sweeps.
// There is no clear-all. Releasing every lock at once would restart the concurrent runs
// the locks exist to prevent.
//
// OPERATIONAL SAFEGUARDS
// POST only. A GET cannot delete a lock, so a stray link, prefetch, or browser history
// entry cannot release one.
//
// The filename is stripped and then pattern-matched, in that order.
// basename() removes any directory component, and the surviving name must match
// ^[a-zA-Z0-9_\-]+\.lock$ — no dots beyond the extension, no slashes, no traversal. The
// two together mean the composed path cannot leave /tmp/unraid_locks, and the .lock
// suffix means nothing but a lock file is a candidate in the first place.
//
// Deleting an absent lock is success, not an error.
// file_exists() is checked and @unlink() suppressed, so two clicks, or a race with the
// script releasing its own lock, both end with the lock gone and ok:true. The caller
// wants the lock absent; it does not care who removed it.
//
// Scope is one fixed directory, hardcoded here.
// /tmp/unraid_locks is a literal, not a config value, so no conf edit or empty variable
// can redirect this delete somewhere else.
//
// REQUEST
// POST file=<name>.lock
//
// RESPONSE
// {"ok":true} lock removed, or already absent
// {"ok":false,"error":string} wrong method or a filename that failed validation
//
// DEPENDS ON
// nothing — /tmp/unraid_locks is written by the shell layer's locking helper
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'POST only']);