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,8 +1,76 @@
|
||||
<?php
|
||||
// Save rsync standalone config (location + cron) for a specific rsync tier.
|
||||
// POST: flag_name (e.g. "DAILY_RSYNC_ENABLED"), orch_id, location, cron
|
||||
// Stored in schedule.json under "__rsync_{FLAG_NAME}".
|
||||
// Triggers a cron rebuild so the standalone entry takes effect immediately.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Standalone rsync scheduling. Records a location and cron expression for one rsync tier so
|
||||
// that tier can run on its own schedule when its orchestrator is disabled.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// Rsync normally runs as a step inside an orchestrator — critical, intermediate, daily,
|
||||
// weekly. This exists for the case where someone wants that tier's sync without the rest of
|
||||
// the orchestrator's work. vv_cron_rebuild() emits the standalone entry only when the
|
||||
// orchestrator is disabled and both a location and a cron are configured, so the two can
|
||||
// never both fire: enabling the orchestrator silently takes precedence.
|
||||
//
|
||||
// Stored in schedule.json under __rsync_<FLAG_NAME>, a namespace deliberately outside the
|
||||
// job id space. Everything that iterates the schedule as jobs skips keys with that prefix.
|
||||
//
|
||||
// The cron is rebuilt immediately on save, so the change takes effect without waiting for
|
||||
// another event to regenerate varaverk.cron.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Keyed by flag, not by orchestrator.
|
||||
// The tier flag is the stable identity — CRITICAL_RSYNC_ENABLED means the same thing
|
||||
// regardless of which orchestrator currently carries that tier. orch_id is stored
|
||||
// alongside it purely so the rebuild can check whether that orchestrator is enabled.
|
||||
//
|
||||
// Configuration only. Nothing here starts a sync; it records when one should start.
|
||||
//
|
||||
// Empty fields are permitted and mean "not configured".
|
||||
// location and cron may both be blank, which is how a standalone entry is cleared —
|
||||
// vv_cron_rebuild() requires both to be present before it emits anything.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// POST only, checked before any parameter is read.
|
||||
//
|
||||
// The flag name is constrained to the rsync namespace.
|
||||
// ^[A-Z_]+_RSYNC_ENABLED$ — this endpoint cannot create a schedule key for anything
|
||||
// else, so the __rsync_ namespace stays exactly as wide as the tiers it was built for.
|
||||
//
|
||||
// The cron expression is validated as a crontab injection surface.
|
||||
// vv_cron_rebuild() interpolates it directly into a generated crontab line. Only
|
||||
// [0-9A-Za-z*,\-/ ] is permitted, and then five whitespace-separated fields are
|
||||
// required. The character class is what carries the guarantee: the field-count pattern
|
||||
// uses \s, which matches newline, so on its own it would accept a value carrying a
|
||||
// second, caller-chosen crontab entry. It was previously not validated at all.
|
||||
//
|
||||
// The orchestrator id is validated even though it is only ever used as a lookup key.
|
||||
// ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check, so a value that could not name a
|
||||
// real script cannot be stored as though it does.
|
||||
//
|
||||
// The location must be absolute and clean.
|
||||
// Leading slash required, '..' rejected, control characters rejected. It reaches the
|
||||
// crontab as an escapeshellarg'd --location= token, so validation and escaping are both
|
||||
// in place.
|
||||
//
|
||||
// The rest of the schedule is preserved — the file is loaded, one key replaced, and the
|
||||
// whole structure written back.
|
||||
//
|
||||
// The cron rebuild only runs after a confirmed write, so a failed save cannot regenerate
|
||||
// the crontab from a schedule that was not persisted.
|
||||
//
|
||||
// REQUEST
|
||||
// POST flag_name=<TIER>_RSYNC_ENABLED orch_id=<Category/name.sh>
|
||||
// location=/absolute/path cron=<5 fields>
|
||||
//
|
||||
// RESPONSE
|
||||
// {"ok":true}
|
||||
// {"ok":false,"error":"POST only"|"Invalid flag_name"|"Invalid orch_id"|"Invalid location"
|
||||
// |"Invalid cron expression"|"Write failed"}
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/scheduler.php vv_schedule_load(), vv_schedule_save(), vv_cron_rebuild()
|
||||
// Rsync/rsync.sh the script the generated cron entry invokes
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
@@ -29,6 +97,13 @@ if ($location && (!str_starts_with($location, '/') || str_contains($location, '.
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid location']);
|
||||
exit;
|
||||
}
|
||||
// vv_cron_rebuild() interpolates this straight into a crontab line. Restrict to cron-safe
|
||||
// characters first — a field-count check alone would accept a value carrying a newline and
|
||||
// therefore a second, caller-chosen crontab entry.
|
||||
if ($cron && (!preg_match('/^[0-9A-Za-z*,\-\/ ]+$/', $cron) || !preg_match('/^(\S+ +){4}\S+$/', $cron))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid cron expression']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$key = '__rsync_' . $flagName;
|
||||
$schedule = vv_schedule_load();
|
||||
|
||||
Reference in New Issue
Block a user