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:
Gmer4Lfe
2026-08-02 10:11:39 -04:00
parent 6a959fb5e4
commit 987313e7dc
55 changed files with 3972 additions and 95 deletions
+92 -2
View File
@@ -1,6 +1,96 @@
<?php
// Receives Sonarr/Radarr/Lidarr Download events. Fires upgrade_webhook_handler.sh
// in the background and returns 200 immediately — arr does not wait on the push.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Arr download webhook. Receives Sonarr, Radarr and Lidarr Download events and fires
// upgrade_webhook_handler.sh against the affected folder so the partner learns about the
// new file immediately rather than at the next scheduled sync.
//
// OPERATIONAL MODEL
// Accept, dispatch, return. The handler is backgrounded and the response goes out at once,
// because an arr that is made to wait on a cross-host push will time the webhook out and
// log it as a failure — and it retries on a schedule that would compound the problem.
//
// 200 therefore means accepted, not propagated. The handler's own log is the record of what
// happened; this endpoint cannot report it and does not pretend to.
//
// The reason for immediacy is search suppression: until the partner knows a file exists, it
// will keep searching for it. The scheduled sync would close that gap eventually; the
// webhook closes it in seconds.
//
// DESIGN PRINCIPLES
// The arr type is inferred from the payload's shape, not from a parameter.
// series.path means Sonarr, movie.folderPath means Radarr, artist.path means Lidarr.
// Each app names its own field, so the structure identifies the sender without a query
// string the user could get wrong when configuring three separate applications.
//
// Test events are answered with instructions.
// Sonarr's "Test" button gets a message telling the user to configure On Download,
// because a bare success there is exactly what leads to a webhook that is connected and
// wired to nothing.
//
// Every non-Download event is acknowledged and skipped.
// ok:true with the event name, never an error. An arr that receives an error status
// retries and eventually disables the webhook, so events this endpoint does not care
// about have to be accepted rather than rejected.
//
// Fires on all Download events, not just upgrades.
// New grabs and upgrades both need propagating; distinguishing them would suppress
// exactly the first-time grabs the partner is most likely to duplicate.
//
// The kill switch is read per request.
// DOWNLOAD_WEBHOOK_ENABLED is checked from conf on every call, so turning it off takes
// effect without touching the arr configuration — and the response says it was skipped
// rather than silently doing nothing.
//
// OPERATIONAL SAFEGUARDS
// POST only, with a real status code.
// 405 for the wrong method, 400 for unparseable or unrecognised bodies. Status codes
// matter more here than elsewhere in this layer: the caller is a machine that changes
// its retry behaviour based on them.
//
// The path is validated and then escaped.
// Must be absolute, no '..', no null bytes or newlines — then passed through
// escapeshellarg() into the handler invocation. The arr type is likewise escaped even
// though it is one of three literals this file chose itself.
//
// Both the empty-path and unrecognised-structure cases are handled explicitly.
// A payload with a recognised key holding an empty value is rejected separately from
// one whose structure is unknown, because those are different misconfigurations.
//
// A missing handler is reported as a server error, not swallowed.
// 500 with a named error, so a partial deploy is visible in the arr's own webhook log
// rather than appearing to succeed forever.
//
// Output is appended to a dedicated log with stdin detached, so a backgrounded handler
// cannot hold the request's file descriptors open.
//
// Known gap: this endpoint authenticates nothing.
// master.conf carries WEBHOOK_SECRET, and the standalone Node listener on WEBHOOK_PORT
// validates it — this WebGUI-hosted path does not. Anyone who can reach the URL can
// make it run the handler against any absolute path that passes validation. Injection
// is not the risk (the path is escaped); triggering work is. Left as-is deliberately
// rather than fixed in passing: adding a secret check here would break whichever arr
// instances are currently configured against this URL, and that is a change to make
// with the arr configs open, not as part of a documentation pass. See also the CSRF
// note in README-unraid.md.
//
// REQUEST
// POST <arr webhook JSON body>
// eventType=Test → connection acknowledgement
// eventType=Download → dispatches the handler
// any other eventType → acknowledged and skipped
//
// RESPONSE
// {"ok":true,"arr":"sonarr|radarr|lidarr","path":"…"} dispatched
// {"ok":true,"message":"Webhook connected — …"} Test
// {"ok":true,"skipped":"<event>|DOWNLOAD_WEBHOOK_ENABLED=false"}
// {"ok":false,"error":…} with 405 / 400 / 500 as appropriate
//
// DEPENDS ON
// include/config.php vv_conf_vars(), SCRIPTS_DIR
// Media/upgrade_webhook_handler.sh the backgrounded handler
// master.conf DOWNLOAD_WEBHOOK_ENABLED
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';