<?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)]);
