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.
100 lines
4.5 KiB
PHP
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]);
|