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,93 @@
|
||||
<?php
|
||||
// Import Script — lets the Scheduler page's "+ Import Script" browser move an existing
|
||||
// .sh file from anywhere on the server into CUSTOM_SCRIPTS_DIR. This is a MOVE: the
|
||||
// source is deleted once the copy is verified, so no stale duplicate is left behind.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Script import. Browses the filesystem for .sh files and moves a chosen one into
|
||||
// CUSTOM_SCRIPTS_DIR, so an existing script can be brought under Varaverk's scheduler
|
||||
// without retyping it.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// Two actions on one URL: a GET browser and a POST import. The browser is rooted at / and
|
||||
// walks one directory at a time, because the scripts people want to import live wherever
|
||||
// they happened to put them — most often the User Scripts plugin's own folders.
|
||||
//
|
||||
// This is a move, not a copy. The source is deleted once the destination is verified, so
|
||||
// there is exactly one copy afterwards and no chance of editing the wrong one. That is also
|
||||
// why the verification is so deliberate: a move that half-succeeds destroys the only copy.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Copy, verify, then delete — in that order, always.
|
||||
// copy() first, then a size comparison and a SHA-256 of both files, and only then the
|
||||
// unlink. Source and destination are routinely on different filesystems, where rename()
|
||||
// is not atomic and a partial write is a real outcome rather than a theoretical one.
|
||||
//
|
||||
// A failed verification leaves the source untouched.
|
||||
// The destination is removed and the source is left exactly where it was. Between
|
||||
// losing the import and losing the script, the import is the acceptable loss.
|
||||
//
|
||||
// An undeletable source is a warning, not a failure.
|
||||
// If the copy verified but the original could not be removed — read-only mount,
|
||||
// permissions — the import is reported successful with a warning naming the file to
|
||||
// clean up. The script works from its new home either way, and failing the whole
|
||||
// operation would leave the user with two copies and an error message.
|
||||
//
|
||||
// Directory listings are capped and sorted.
|
||||
// 300 entries each for directories and .sh files. A browser rooted at / will eventually
|
||||
// be pointed at something enormous.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// Refuses to import from inside the Varaverk repo.
|
||||
// Both paths are resolved with realpath() and compared by prefix. Importing a tracked
|
||||
// file would delete it out from under git with no commit recording it — the next pull
|
||||
// would either restore it as a phantom or report a deletion nobody made. This check is
|
||||
// the reason realpath() is used rather than the submitted string: a symlink into the
|
||||
// repo would otherwise slip past a textual comparison.
|
||||
//
|
||||
// Refuses a source already inside CUSTOM_SCRIPTS_DIR.
|
||||
// Also compared after realpath(). Without it, the move would copy a file onto itself
|
||||
// and then delete it.
|
||||
//
|
||||
// Refuses to overwrite an existing custom script.
|
||||
// file_exists() on the destination aborts with the conflicting name. A silent overwrite
|
||||
// here would destroy a script that may already be scheduled and running.
|
||||
//
|
||||
// Both paths are validated as absolute with no traversal.
|
||||
// ^/[^\0]*$ for the browse path and ^/[^\0]*\.sh$ for the import, plus an explicit '..'
|
||||
// check on each. Null bytes are excluded by the character class, which matters because
|
||||
// these strings reach both the filesystem and a shell.
|
||||
//
|
||||
// Both find invocations escape their argument, and neither takes anything else from the
|
||||
// request — depth, type and name filters are all literals.
|
||||
//
|
||||
// The destination is made executable before it is reported.
|
||||
// chmod 0755 after verification, so a freshly imported script is immediately runnable
|
||||
// rather than failing the first time it is scheduled.
|
||||
//
|
||||
// Existence is confirmed before work begins — is_dir() for browse, is_file() for import —
|
||||
// so a bad path returns a named error rather than a warning leaking into the JSON body.
|
||||
//
|
||||
// Accepted exposure: the browser can list any directory on the host.
|
||||
// It returns directory names and .sh filenames only — no file contents, and nothing
|
||||
// outside those two types. That is the minimum a file picker rooted at / can do, and it
|
||||
// is guarded by the Unraid WebGUI session; see the CSRF note in README-unraid.md.
|
||||
//
|
||||
// REQUEST
|
||||
// GET ?action=browse&path=/absolute/dir list subdirectories and .sh files
|
||||
// POST action=import path=/absolute/file.sh move it into CUSTOM_SCRIPTS_DIR
|
||||
//
|
||||
// RESPONSE
|
||||
// browse {"ok":true,"path","dirs":[…],"files":[…],"parent":…}
|
||||
// import {"ok":true,"id":"Custom/<name>.sh"} optionally with a "warning"
|
||||
// {"ok":false,"error":"Invalid path"|"Not a directory: …"|"Invalid script path"
|
||||
// |"Not found: …"|"Could not resolve path"
|
||||
// |"Refusing to import from inside the Varaverk repo …"
|
||||
// |"Already in Custom Scripts."|"A script named \"…\" already exists …"
|
||||
// |"Copy failed"|"Copy verification failed — source left untouched"
|
||||
// |"Invalid request"}
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/config.php SCRIPTS_DIR, CUSTOM_SCRIPTS_DIR
|
||||
// api/script.php manages the scripts once they are here
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-store, no-cache');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
Reference in New Issue
Block a user