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,85 @@
|
||||
<?php
|
||||
// Rewrite a *_SCRIPTS array in master.conf with a new script order.
|
||||
// POST: array_name (e.g. "DAILY_SCRIPTS"), scripts (JSON: [{"id":"rel/path.sh","enabled":true}, ...])
|
||||
// Preserves original entry lines (including inline flags/args) where possible.
|
||||
// Scripts absent from the new list are dropped; new scripts are added as fresh entries.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Rewrites one *_SCRIPTS array in master.conf with a new order and a new enabled/disabled
|
||||
// state per entry — the drag-to-reorder on the scheduler page's orchestrator view.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// Order is behaviour, not presentation. An orchestrator runs its array top to bottom, so
|
||||
// moving an entry changes when that script runs relative to the others — which is why this
|
||||
// writes to master.conf rather than to a UI preference.
|
||||
//
|
||||
// The array block is located, its entries harvested, and the whole block replaced. Only the
|
||||
// lines between the opening and closing parens are touched; everything else in master.conf
|
||||
// is written back byte for byte, because the file is hand-maintained and full of comments
|
||||
// and grouping no round trip through a parser would preserve.
|
||||
//
|
||||
// Disabled entries stay in the file, commented. Enabled state is expressed by the presence
|
||||
// or absence of a leading '# ', the same convention conf_toggle.php uses.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Original entry text is preserved, not regenerated.
|
||||
// Entries are harvested from the existing block keyed by script path, and reused
|
||||
// verbatim when the same path appears in the new order. That is what keeps inline flags
|
||||
// and arguments — "Media/cleanup.sh --deep" — through a reorder. Only a script that was
|
||||
// not previously in the array is written fresh, as a bare quoted path.
|
||||
//
|
||||
// The opening and closing lines are preserved exactly.
|
||||
// Both are carried over untouched rather than rebuilt, so a trailing comment on the
|
||||
// array declaration survives.
|
||||
//
|
||||
// Absent means removed.
|
||||
// A script in the file but not in the submitted order is dropped from the array. The
|
||||
// page always submits the complete list, so absence is an instruction, not an omission.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// POST only, checked before any parameter is read.
|
||||
//
|
||||
// The array name is pattern-matched and only ever used as a needle.
|
||||
// ^[A-Z_]+_SCRIPTS$, then preg_quote()d and matched against existing lines. Nothing is
|
||||
// opened or executed from it.
|
||||
//
|
||||
// Every entry is validated, and an invalid one fails the request rather than being skipped.
|
||||
// ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check per entry. Skipping would be
|
||||
// actively dangerous here: the array is rebuilt from the validated list alone, so a
|
||||
// silently dropped entry is a script silently removed from its orchestrator. Validation
|
||||
// completes before anything is written.
|
||||
//
|
||||
// A missing array aborts before the write.
|
||||
// Both the block start and end must be found, otherwise the endpoint returns a named
|
||||
// error and writes nothing. Without that, a typo'd array name would splice a block into
|
||||
// an undefined position.
|
||||
//
|
||||
// master.conf is confirmed present and readable before either pass.
|
||||
//
|
||||
// The write is atomic.
|
||||
// vv_write_conf_raw() writes .vv.tmp and rename()s. Every script sources master.conf, so
|
||||
// a truncated write here would be a system-wide outage rather than a lost reorder.
|
||||
//
|
||||
// The push happens only after a confirmed write, and its result is returned — a partner
|
||||
// that did not receive the new order runs these scripts in a different sequence.
|
||||
//
|
||||
// Known limit: block detection counts parens textually.
|
||||
// depth is tracked with substr_count over '(' and ')', which does not know about quotes
|
||||
// or comments. An entry whose arguments contained an unbalanced paren would end the
|
||||
// block early. No current entry does, and the alternative is a bash parser — but a
|
||||
// future entry with a paren in its arguments would be the thing that broke this.
|
||||
//
|
||||
// REQUEST
|
||||
// POST array_name=<NAME>_SCRIPTS
|
||||
// scripts=<JSON array of {"id":"Category/name.sh","enabled":bool}>
|
||||
//
|
||||
// RESPONSE
|
||||
// {"ok":true,"push":[{"host","ok","ready","error"}, …]}
|
||||
// {"ok":false,"error":"POST only"|"Invalid array_name"|"Invalid scripts JSON"
|
||||
// |"Invalid script id: …"|"master.conf not found"
|
||||
// |"Could not read master.conf"|"Array … not found in master.conf"
|
||||
// |"Write failed"}
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/config.php CONF_DIR, vv_write_conf_raw(), vv_push_master_conf(),
|
||||
// vv_push_setup_state()
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
@@ -24,12 +101,17 @@ if (!is_array($decoded)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate each entry
|
||||
// Validate every entry before anything is written. A rejected entry cannot be skipped here:
|
||||
// this endpoint rewrites the array from $order alone, so a silently dropped entry is a script
|
||||
// silently removed from its orchestrator.
|
||||
$order = [];
|
||||
foreach ($decoded as $item) {
|
||||
$id = trim((string)($item['id'] ?? ''));
|
||||
$enabled = (bool)($item['enabled'] ?? true);
|
||||
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id)) continue;
|
||||
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid script id: ' . $id]);
|
||||
exit;
|
||||
}
|
||||
$order[] = ['id' => $id, 'enabled' => $enabled];
|
||||
}
|
||||
|
||||
@@ -106,5 +188,8 @@ if (!vv_write_conf_raw('master.conf', implode('', $lines))) {
|
||||
exit;
|
||||
}
|
||||
|
||||
vv_push_master_conf();
|
||||
echo json_encode(['ok' => true]);
|
||||
// Reported rather than discarded — a partner that did not receive the new order is a partner
|
||||
// running these scripts in a different sequence. Mirrors rawconf/confform/movescript.
|
||||
$push = vv_push_master_conf();
|
||||
vv_push_setup_state();
|
||||
echo json_encode(['ok' => true, 'push' => $push]);
|
||||
|
||||
Reference in New Issue
Block a user