Files
Varaverk/Plugin/unraid/api/savefolders.php
T
Gmer4Lfe 987313e7dc 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.
2026-08-02 10:11:39 -04:00

100 lines
4.5 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 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';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
$raw = $_POST['folders'] ?? '';
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
echo json_encode(['ok' => false, 'error' => 'Invalid JSON']);
exit;
}
$clean = [];
foreach ($decoded as $name => $scripts) {
$name = trim((string)$name);
if (!$name || strlen($name) > 80) continue;
if (!is_array($scripts)) continue;
$cleanScripts = [];
foreach ($scripts as $s) {
$s = trim((string)$s);
if (!$s || str_contains($s, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $s)) continue;
$cleanScripts[] = $s;
}
$clean[$name] = $cleanScripts;
}
$schedule = vv_schedule_load();
$schedule['__folders'] = $clean;
if (!vv_schedule_save($schedule)) {
echo json_encode(['ok' => false, 'error' => 'Write failed']);
exit;
}
echo json_encode(['ok' => true]);