475765e31f
Add an explicit template renderer with HTML views and partials for the app, bootstrap, package, and catalog pages. Move shared reporting setup into config/reporting.php and relocate stylesheet assets under css/.
253 lines
6.5 KiB
PHP
253 lines
6.5 KiB
PHP
<?php
|
||
/*
|
||
* Simpele Racket bootstrap.
|
||
*
|
||
* .htaccess:
|
||
*
|
||
* FallbackResource /index.php
|
||
*
|
||
* Bestanden:
|
||
*
|
||
* config/racket.zip
|
||
* data/ bevat vooraf gemaakte parts
|
||
*
|
||
* Routes:
|
||
*
|
||
* /bootstrap-racket?next=...
|
||
* Leest de vooraf gemaakte parts in data/ en toont een HTML-index
|
||
* met links.
|
||
*
|
||
* /bootstrap-racket-part?n=000001&next=...
|
||
* Geeft text/plain met base64 van precies één binair part.
|
||
*
|
||
* Regels:
|
||
*
|
||
* - Indexen/lijsten zijn HTML.
|
||
* - Payload/downloads zijn text/plain.
|
||
* - Payload/downloads zijn altijd base64.
|
||
* - Geen .bin, .zip of bestandsnaam in download-URL’s.
|
||
* - next=... is alleen cache-busting.
|
||
* - Per gegenereerde HTML-pagina wordt één next-id gemaakt en voor alle
|
||
* links op die pagina gebruikt.
|
||
*/
|
||
|
||
require_once __DIR__ . '/config/reporting.php';
|
||
|
||
require_once __DIR__ . '/private/nexttoken.php';
|
||
require_once __DIR__ . '/private/base64config.php';
|
||
require_once __DIR__ . '/private/racketzip.php';
|
||
require_once __DIR__ . '/private/Template.php';
|
||
|
||
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
|
||
|
||
@set_time_limit(300);
|
||
ignore_user_abort(false);
|
||
|
||
$chunkConfig = load_base64_chunk_config();
|
||
$racketZipMaxBase64Kb = (int)($chunkConfig['racket_zip_max_base64_kb'] ?? 8192);
|
||
|
||
if ($racketZipMaxBase64Kb < 1) {
|
||
$racketZipMaxBase64Kb = 1;
|
||
}
|
||
|
||
define('RACKET_ZIP_MAX_BASE64_KB', $racketZipMaxBase64Kb);
|
||
define('RACKET_ZIP_MAX_BASE64_BYTES', RACKET_ZIP_MAX_BASE64_KB * 1024);
|
||
define('RACKET_ZIP_BINARY_CHUNK_BYTES', racket_zip_binary_chunk_bytes_for_base64_kib(RACKET_ZIP_MAX_BASE64_KB));
|
||
|
||
if (RACKET_ZIP_BINARY_CHUNK_BYTES < 3) {
|
||
fail_html('Ongeldige racket.zip base64 chunk-instelling.');
|
||
}
|
||
|
||
/*
|
||
* Eén next-id voor alle URLs die deze request genereert.
|
||
*/
|
||
$NEXT_ID = $TOKENS->create();
|
||
|
||
function path_only()
|
||
{
|
||
$p = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||
$p = '/' . trim($p ?: '/', '/');
|
||
return $p === '/' ? '/' : $p;
|
||
}
|
||
|
||
function scheme()
|
||
{
|
||
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||
return strtolower(trim(explode(',', $_SERVER['HTTP_X_FORWARDED_PROTO'])[0]));
|
||
}
|
||
|
||
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||
}
|
||
|
||
function host_name()
|
||
{
|
||
return $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||
}
|
||
|
||
function make_url($path, $query = array())
|
||
{
|
||
global $NEXT_ID;
|
||
|
||
$query['next'] = $NEXT_ID;
|
||
|
||
return scheme() . '://' . host_name() . $path . '?' . http_build_query($query);
|
||
}
|
||
|
||
function h($s)
|
||
{
|
||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||
}
|
||
|
||
function html_response($html, $status = 200)
|
||
{
|
||
http_response_code($status);
|
||
header('Content-Type: text/html; charset=utf-8');
|
||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||
header('Pragma: no-cache');
|
||
echo $html;
|
||
exit;
|
||
}
|
||
|
||
function text_response($text, $status = 200)
|
||
{
|
||
http_response_code($status);
|
||
header('Content-Type: text/plain; charset=us-ascii');
|
||
header('Content-Disposition: inline');
|
||
header('X-Content-Type-Options: nosniff');
|
||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||
header('Pragma: no-cache');
|
||
|
||
echo $text;
|
||
|
||
if ($text === '' || substr($text, -1) !== "\n") {
|
||
echo "\n";
|
||
}
|
||
|
||
exit;
|
||
}
|
||
|
||
function fail_html($message, $status = 500)
|
||
{
|
||
html_response(
|
||
RacketSandboxTemplate::renderFile('protocol-error.html', array(
|
||
'title' => 'Error',
|
||
'message' => $message,
|
||
)),
|
||
$status
|
||
);
|
||
}
|
||
|
||
function fail_text($message, $status = 500)
|
||
{
|
||
text_response("error: " . $message . "\n", $status);
|
||
}
|
||
|
||
function load_parts()
|
||
{
|
||
try {
|
||
$manifest = load_racket_zip_parts_manifest();
|
||
} catch (Throwable $e) {
|
||
fail_html($e->getMessage() . "\n\nSla de configuratiepagina op om racket.zip in parts te verdelen.");
|
||
}
|
||
|
||
if (!racket_zip_parts_current($manifest, RACKET_ZIP_MAX_BASE64_KB)) {
|
||
fail_html(
|
||
"racket.zip parts ontbreken of passen niet bij de huidige chunk-grootte.\n\n" .
|
||
"Sla de configuratiepagina op om de parts opnieuw te maken."
|
||
);
|
||
}
|
||
|
||
$parts = array();
|
||
|
||
foreach (($manifest['parts'] ?? array()) as $part) {
|
||
$number = (string)($part['number'] ?? '');
|
||
$parts[] = array(
|
||
'number' => $number,
|
||
'name' => (string)($part['name'] ?? racket_zip_part_name((int)$number)),
|
||
'size' => (int)($part['size'] ?? 0),
|
||
'url' => make_url('/bootstrap-racket-part', array('n' => $number)),
|
||
);
|
||
}
|
||
|
||
$manifest['parts'] = $parts;
|
||
|
||
return $manifest;
|
||
}
|
||
|
||
function serve_bootstrap()
|
||
{
|
||
global $NEXT_ID;
|
||
|
||
$manifest = load_parts();
|
||
$parts = $manifest['parts'];
|
||
|
||
$rows = '';
|
||
|
||
foreach ($parts as $i => $part) {
|
||
$rows .= RacketSandboxTemplate::renderFile('partials/racket-part-row.html', array(
|
||
'index' => (string)($i + 1),
|
||
'number' => $part['number'],
|
||
'size' => (string)$part['size'],
|
||
'url' => $part['url'],
|
||
)) . "\n";
|
||
}
|
||
|
||
$zipSize = (int)($manifest['source_bytes'] ?? 0);
|
||
|
||
$pkg_url = make_url('/racket-pkg-index');
|
||
|
||
html_response(RacketSandboxTemplate::renderFile('bootstrap-racket.html', array(
|
||
'pkg_url' => $pkg_url,
|
||
'zip_size' => (string)$zipSize,
|
||
'max_base64_kb' => (string)RACKET_ZIP_MAX_BASE64_KB,
|
||
'max_base64_bytes' => (string)RACKET_ZIP_MAX_BASE64_BYTES,
|
||
'binary_chunk_bytes' => (string)RACKET_ZIP_BINARY_CHUNK_BYTES,
|
||
'part_count' => (string)count($parts),
|
||
'created_at' => (string)($manifest['created_at'] ?? ''),
|
||
'next_id' => $NEXT_ID,
|
||
'part_rows_html' => $rows,
|
||
)));
|
||
}
|
||
|
||
function serve_part()
|
||
{
|
||
$n = $_GET['n'] ?? '';
|
||
|
||
if (!is_string($n) || !preg_match('/^[0-9]{6}$/', $n)) {
|
||
fail_text('ongeldig partnummer; gebruik bijvoorbeeld n=000001', 400);
|
||
}
|
||
|
||
$file = racket_zip_part_file($n);
|
||
|
||
if (!is_file($file) || !is_readable($file)) {
|
||
fail_text('part ontbreekt of is niet leesbaar: ' . $n, 404);
|
||
}
|
||
|
||
$bin = file_get_contents($file);
|
||
|
||
if ($bin === false) {
|
||
fail_text('kan part niet lezen: ' . $n, 500);
|
||
}
|
||
|
||
text_response(base64_encode($bin));
|
||
}
|
||
|
||
$path = path_only();
|
||
|
||
if ($path === '/' || $path === '/index.php') {
|
||
header('Location: ' . make_url('/bootstrap-racket'), true, 302);
|
||
exit;
|
||
}
|
||
|
||
if ($path === '/bootstrap-racket') {
|
||
$TOKENS->check_valid_next('html');
|
||
serve_bootstrap();
|
||
}
|
||
|
||
if ($path === '/bootstrap-racket-part') {
|
||
$TOKENS->check_valid_next('text');
|
||
serve_part();
|
||
}
|
||
|
||
fail_html('Onbekende route: ' . $path, 404);
|