_SCRIPTS // scripts= // // 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'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['ok' => false, 'error' => 'POST only']); exit; } $arrayName = trim($_POST['array_name'] ?? ''); $raw = $_POST['scripts'] ?? ''; $decoded = json_decode($raw, true); if (!$arrayName || !preg_match('/^[A-Z_]+_SCRIPTS$/', $arrayName)) { echo json_encode(['ok' => false, 'error' => 'Invalid array_name']); exit; } if (!is_array($decoded)) { echo json_encode(['ok' => false, 'error' => 'Invalid scripts JSON']); exit; } // 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)) { echo json_encode(['ok' => false, 'error' => 'Invalid script id: ' . $id]); exit; } $order[] = ['id' => $id, 'enabled' => $enabled]; } $confPath = CONF_DIR . '/master.conf'; if (!file_exists($confPath)) { echo json_encode(['ok' => false, 'error' => 'master.conf not found']); exit; } $lines = file($confPath, FILE_KEEP_BLANK_LINES); if (!$lines) { echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']); exit; } // Find the array block and extract original entry lines keyed by script path. $arrayEsc = preg_quote($arrayName, '/'); $blockStart = null; $blockEnd = null; $depth = 0; $origEntries = []; // path → original trimmed content line (e.g. '"Daily/script.sh --flag"') foreach ($lines as $i => $line) { if ($blockStart === null) { if (preg_match('/^\s*' . $arrayEsc . '\s*=\s*\(/', $line)) { $blockStart = $i; $depth = 1; } continue; } $depth += substr_count($line, '('); $depth -= substr_count($line, ')'); if ($depth <= 0) { $blockEnd = $i; break; } // Collect entries (enabled and commented) if (preg_match('/^\s*(?:#\s*)?"([^"]+)"/', $line, $m)) { $parts = preg_split('/\s+/', trim($m[1])); $path = $parts[0] ?? ''; if (substr($path, -3) === '.sh' && !isset($origEntries[$path])) { // Store the full quoted expression (may include flags after the path) $origEntries[$path] = '"' . $m[1] . '"'; } } } if ($blockStart === null || $blockEnd === null) { echo json_encode(['ok' => false, 'error' => "Array $arrayName not found in master.conf"]); exit; } // Build replacement block lines $newBlockLines = []; // Preserve the opening line exactly (e.g. "DAILY_SCRIPTS=(") $newBlockLines[] = $lines[$blockStart]; foreach ($order as $item) { $id = $item['id']; $enabled = $item['enabled']; $entry = $origEntries[$id] ?? '"' . $id . '"'; $prefix = $enabled ? ' ' : ' # '; $newBlockLines[] = $prefix . $entry . "\n"; } // Preserve the closing line exactly $newBlockLines[] = $lines[$blockEnd]; // Replace the original block in $lines array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines); if (!vv_write_conf_raw('master.conf', implode('', $lines))) { echo json_encode(['ok' => false, 'error' => 'Write failed']); exit; } // 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]);