, 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=_RSYNC_ENABLED orch_id= // 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'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['ok' => false, 'error' => 'POST only']); exit; } $flagName = trim($_POST['flag_name'] ?? ''); $orchId = trim($_POST['orch_id'] ?? ''); $location = trim($_POST['location'] ?? ''); $cron = trim($_POST['cron'] ?? ''); if (!$flagName || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $flagName)) { echo json_encode(['ok' => false, 'error' => 'Invalid flag_name']); exit; } if ($orchId && (str_contains($orchId, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $orchId))) { echo json_encode(['ok' => false, 'error' => 'Invalid orch_id']); exit; } if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $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(); $schedule[$key] = [ 'flag_name' => $flagName, 'orch_id' => $orchId, 'location' => $location, 'cron' => $cron, ]; if (!vv_schedule_save($schedule)) { echo json_encode(['ok' => false, 'error' => 'Write failed']); exit; } vv_cron_rebuild($schedule); echo json_encode(['ok' => true]);