Token and poll rather than SSE, so the api layer keeps one response convention and reuses the pattern manual_sync already proved. History is capped at three turns because the model is only fully offloaded at 16384 context and unbounded history would cross that silently. The tab exists only while AI_ENABLED is true, rejected server-side and not merely hidden.
84 lines
4.4 KiB
PHP
84 lines
4.4 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Script and document reader. Returns the full text of one .sh or .md file inside
|
|
// SCRIPTS_DIR — the source view behind the scheduler page's script viewer and the docs tab.
|
|
//
|
|
// DESIGN PRINCIPLES
|
|
// Two extensions, one endpoint.
|
|
// Scripts and their READMEs are read the same way because they are read for the same
|
|
// reason — someone wants to see what a job actually does. Splitting them would mean two
|
|
// endpoints with identical validation.
|
|
//
|
|
// Returns text, never renders it.
|
|
// Markdown arrives as source. Rendering is the browser's job, and doing it here would
|
|
// make this endpoint an HTML producer with an HTML producer's escaping problems.
|
|
//
|
|
// Relative ids only.
|
|
// The id is a path relative to SCRIPTS_DIR, so the caller never learns or supplies the
|
|
// installation root. That is also what makes the same id valid in internal and appdata
|
|
// storage modes.
|
|
//
|
|
// OPERATIONAL SAFEGUARDS
|
|
// Read-only. There is no write counterpart in this file; edits go through movescript.php
|
|
// and import_script.php, which have their own validation.
|
|
//
|
|
// Traversal is blocked before the path is composed.
|
|
// str_contains($id, '..') is checked explicitly, and the pattern
|
|
// ^[A-Za-z0-9_.\-/]+\.(sh|md)$ excludes null bytes, backslashes, spaces, and every
|
|
// shell metacharacter. The slash has to be permitted because ids are Category/name.sh,
|
|
// so the '..' check carries the traversal guarantee on its own rather than being
|
|
// implied by the character class.
|
|
//
|
|
// The extension allowlist is the real access boundary.
|
|
// Only .sh, .md, .php and .template can be named at all, which is what keeps
|
|
// Configurations/*.conf — the files holding every credential in the system — outside
|
|
// this endpoint's reach. Any future extension added here has to be checked against that
|
|
// first.
|
|
//
|
|
// .php and .template were added for the AI tab's source viewer, whose retrieval results
|
|
// span every tracked file type. They are safe by the same argument that makes the AI
|
|
// index safe: only git-tracked content is involved, the conf files were never tracked,
|
|
// and the repository is pushed to a remote — anything reachable here is already
|
|
// published. .conf is deliberately still absent, and must stay that way.
|
|
//
|
|
// A missing file is reported, not opened.
|
|
// file_exists() precedes file_get_contents(), so a bad id returns a named error rather
|
|
// than a PHP warning leaking the absolute path into the JSON body.
|
|
//
|
|
// Known limit: symlinks inside SCRIPTS_DIR are followed.
|
|
// Containment is enforced on the id, not on the resolved path, so a symlink placed
|
|
// inside the tree pointing outside it would be read. Not tightened with a realpath
|
|
// check, because the plugin's own boot model installs SCRIPTS_DIR as a symlink and a
|
|
// naive containment test would break it. The tree is git-managed; a rogue symlink in it
|
|
// is a repo compromise, which is a larger problem than this endpoint.
|
|
//
|
|
// REQUEST
|
|
// GET ?id=<Category/name.sh|Category/README-name.md>
|
|
//
|
|
// RESPONSE
|
|
// {"ok":true,"content":"<full file text>"}
|
|
// {"ok":false,"error":"Invalid id"|"Not found"}
|
|
//
|
|
// DEPENDS ON
|
|
// include/config.php SCRIPTS_DIR
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
header('Content-Type: application/json');
|
|
require_once dirname(__DIR__) . '/include/config.php';
|
|
|
|
$id = trim($_GET['id'] ?? '');
|
|
|
|
// Must be relative path within SCRIPTS_DIR, no traversal, must end in .sh or .md
|
|
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.(sh|md|php|template)$/', $id)) {
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
|
exit;
|
|
}
|
|
|
|
$path = SCRIPTS_DIR . '/' . $id;
|
|
if (!file_exists($path)) {
|
|
echo json_encode(['ok' => false, 'error' => 'Not found']);
|
|
exit;
|
|
}
|
|
|
|
echo json_encode(['ok' => true, 'content' => file_get_contents($path)]);
|