.sh everywhere in the scheduler, and vv_cron_rebuild() is the other half of // the convention — it maps that prefix onto CUSTOM_SCRIPTS_DIR when generating cron lines. // // Saving also registers. A new script gets a disabled, unscheduled entry in schedule.json so // it appears in the job list immediately; the user then schedules it through scheduler.php. // // DESIGN PRINCIPLES // Names, not paths, on the write side. // POST takes a bare name and composes the id and the path from it. There is no way to // express a subdirectory, so the flat namespace is a property of the interface rather // than something validation has to enforce afterwards. // // Delete removes the script and its schedule entry together. // Unlink, drop the schedule key, save, rebuild the cron. Leaving a schedule entry for a // script that no longer exists would put a cron line in place for a missing file. // // Reading a script that does not exist returns empty content, not an error. // The editor opens the same way for a new script and an existing one. // // OPERATIONAL SAFEGUARDS // The read id is constrained to the Custom namespace. // ^Custom/[a-zA-Z0-9_\-]+\.sh$ plus a '..' check — no dots in the name, no nested path, // no other prefix. This endpoint cannot be used to read a repo script; that is // readscript.php, which has its own extension allowlist. // // The write name excludes every path character. // ^[a-zA-Z0-9_\-]+$ — no slash, no dot, no traversal sequence can be expressed, so the // composed path is always a direct child of CUSTOM_SCRIPTS_DIR. The name pattern is // stricter than the read pattern because it is what constructs the filename. // // The script write is atomic, and executable before it is visible. // tmp + chmod 0755 + rename. An enabled custom script can be launched by cron at any // moment; writing in place would let it fire against a truncated file, and chmod after // the write would let it fire against a non-executable one. // // Delete confirms existence first, so a repeated delete reports a clear "Script not found" // rather than silently rebuilding the cron for nothing. // // Unknown methods are refused explicitly at the end, so a PUT or DELETE cannot fall through // the two handled blocks into an empty 200. // // Accepted by design: this endpoint writes an executable root-run script from a browser. // That is the entire feature, and it is why it is confined to a directory outside the // repo with a flat namespace and a strict name pattern. It is guarded by the Unraid // WebGUI session; see the CSRF note in README-unraid.md. // // REQUEST // GET ?id=Custom/.sh read (empty content when absent) // POST name= content=