Files
www-data 475765e31f Move rendering into private templates
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/.
2026-05-26 12:50:26 +02:00

373 lines
12 KiB
PHP

<?php
/*
* index.php
*
* Menselijk startpunt voor Racket sandbox.
*
* Niet ingelogd:
* - verwijst naar login
*
* Ingelogd:
* - bootstrap-link genereren
* - logout
*
* Admin:
* - gebruikersoverzicht
* - admin/enabled vlaggen wijzigen
* - wachtwoord wijzigen
* - gebruiker verwijderen
*/
require_once __DIR__ . '/private/auth.php';
require_once __DIR__ . '/private/header.php';
require_once __DIR__ . '/private/languagestore.php';
require_once __DIR__ . '/private/nexttoken.php';
require_once __DIR__ . '/private/promptstore.php';
require_once __DIR__ . '/private/usersettings.php';
require_once __DIR__ . '/private/viewdata.php';
require_once __DIR__ . '/config/reporting.php';
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
$auth = new RacketSandboxAuth($DB_FILE);
$languageStore = new LanguageStore($DB_FILE);
$tokens = new NextTokenStore($DB_FILE);
$promptStore = new PromptStore($DB_FILE);
$userSettings = new UserSettingsStore($DB_FILE);
$message = '';
$error = '';
$issuedLink = '';
$bootstrapPrompts = array();
define('BOOTSTRAP_TTL_DEFAULT_MINUTES', 120);
define('BOOTSTRAP_TTL_MIN_MINUTES', 30);
define('BOOTSTRAP_TTL_MAX_MINUTES', 8 * 60);
function h($s)
{
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function get_value($name, $default = '')
{
return $_GET[$name] ?? $default;
}
function t($key, $fallback = null, $values = array())
{
global $languageStore, $language;
return $languageStore->translateFormat($key, $language, $values, $fallback);
}
function current_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 current_host()
{
return $_SERVER['HTTP_HOST'] ?? 'localhost';
}
function bootstrap_link_for_token($token)
{
return current_scheme() . '://' . current_host() .
'/bootstrap-racket?next=' . rawurlencode($token);
}
function post_value($name, $default = '')
{
return $_POST[$name] ?? $default;
}
function post_bool($name)
{
return isset($_POST[$name]) && $_POST[$name] === '1';
}
function create_next_token($tokens, $ttlSeconds, $meta)
{
/*
* Ondersteunt zowel:
* create($ttl)
* als:
* create($ttl, $meta)
*/
$rm = new ReflectionMethod($tokens, 'create');
if ($rm->getNumberOfParameters() >= 2) {
return $tokens->create($ttlSeconds, $meta);
}
return $tokens->create($ttlSeconds);
}
function clamp_bootstrap_ttl_minutes($ttlMinutes)
{
$ttlMinutes = (int)$ttlMinutes;
if ($ttlMinutes < BOOTSTRAP_TTL_MIN_MINUTES) {
return BOOTSTRAP_TTL_MIN_MINUTES;
}
if ($ttlMinutes > BOOTSTRAP_TTL_MAX_MINUTES) {
return BOOTSTRAP_TTL_MAX_MINUTES;
}
return $ttlMinutes;
}
function resolve_user_language($userSettings, $userId, $allowedLanguages)
{
$language = isset($_GET['lang'])
? (string)$_GET['lang']
: (string)$userSettings->get($userId, 'language', 'en');
if (!in_array($language, $allowedLanguages, true)) {
$language = 'en';
}
$userSettings->set($userId, 'language', $language);
return $language;
}
$currentUser = $auth->currentUser();
if ($currentUser === null) {
header('Location: /login.php');
exit;
}
$language = resolve_user_language(
$userSettings,
$currentUser->id(),
$languageStore->supportedLanguages()
);
seed_template_translations($languageStore, 'index.html');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = post_value('action');
try {
if ($action === 'logout') {
$auth->logout();
header('Location: /login.php');
exit;
}
$currentUser = $auth->currentUser();
if ($currentUser === null) {
header('Location: /login.php');
exit;
}
if ($action === 'issue_bootstrap') {
$ttlMinutes = clamp_bootstrap_ttl_minutes(
post_value('ttl_minutes', (string)BOOTSTRAP_TTL_DEFAULT_MINUTES)
);
$userSettings->set($currentUser->id(), 'bootstrap_ttl_minutes', (string)$ttlMinutes);
$token = create_next_token(
$tokens,
$ttlMinutes * 60,
array(
'user_id' => $currentUser->id(),
'email' => $currentUser->email(),
'full_name' => $currentUser->fullName(),
'purpose' => 'racket-bootstrap',
)
);
$issuedLink = bootstrap_link_for_token($token);
$message = t('app.bootstrap_link_issued', 'Bootstrap link issued.');
} else {
if (!$currentUser->isAdmin()) {
throw new Exception(t('app.admin_rights_required', 'Admin rights required.'));
}
if ($action === 'set_password') {
$email = trim(post_value('email'));
$password = (string)post_value('password');
$auth->setPassword($email, $password);
$message = t('app.password_changed_for', 'Password changed for: {{email}}', array('email' => $email));
} elseif ($action === 'set_flags') {
$userId = (int)post_value('user_id');
$isAdmin = post_bool('is_admin');
$isEnabled = post_bool('is_enabled');
if ($userId === $currentUser->id() && !$isAdmin) {
throw new Exception(t('app.cannot_remove_own_admin', 'You cannot remove your own admin rights.'));
}
if ($userId === $currentUser->id() && !$isEnabled) {
throw new Exception(t('app.cannot_disable_self', 'You cannot disable your own account.'));
}
$auth->setAdmin($userId, $isAdmin);
$auth->setEnabled($userId, $isEnabled);
$message = t('app.user_flags_updated', 'User flags updated.');
} elseif ($action === 'delete_user') {
$userId = (int)post_value('user_id');
if ($userId === $currentUser->id()) {
throw new Exception(t('app.cannot_delete_self', 'You cannot delete your own account.'));
}
$auth->deleteUser($userId);
$message = t('app.user_deleted', 'User deleted.');
} elseif ($action !== '') {
throw new Exception(t('app.unknown_action', 'Unknown action: {{action}}', array('action' => $action)));
}
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
$currentUser = $auth->currentUser();
if ($currentUser === null) {
header('Location: /login.php');
exit;
}
$bootstrapTtlMinutes = clamp_bootstrap_ttl_minutes(
$userSettings->get(
$currentUser->id(),
'bootstrap_ttl_minutes',
(string)BOOTSTRAP_TTL_DEFAULT_MINUTES
)
);
foreach ($promptStore->listPrompts($currentUser->id(), $language) as $prompt) {
$bootstrapPrompts[] = array(
'id' => $prompt->id(),
'name' => $prompt->name(),
'content' => $prompt->content(),
);
}
$headerLanguages = array();
foreach ($languageStore->supportedLanguages() as $lang) {
$headerLanguages[$lang] = $languageStore->languageLabel($lang);
}
$headerNavItems = array(
array(
'label' => t('app.manage_prompts', 'Manage prompts'),
'url' => '/prompts?lang=' . rawurlencode($language),
),
);
if ($currentUser->isAdmin()) {
$headerNavItems[] = array(
'label' => t('app.user_management', 'User management'),
'url' => '/users?lang=' . rawurlencode($language),
'separator_before' => true,
);
$headerNavItems[] = array(
'label' => t('app.configuration', 'Configuration'),
'url' => '/admin-config?lang=' . rawurlencode($language),
'separator_before' => true,
);
}
header('Content-Type: text/html; charset=utf-8');
$generatedLinkHtml = '';
if ($issuedLink !== '') {
$generatedLinkHtml = RacketSandboxTemplate::renderFile('partials/index-generated-link.html', array(
'generated_link_label' => t('app.generated_link', 'Generated link'),
'issued_link' => $issuedLink,
'copy_label' => t('app.copy', 'Copy'),
'copied_label' => t('app.copied', 'Copied'),
));
}
$promptPanelHtml = '';
if ($issuedLink !== '') {
$promptToolHtml = '';
if (count($bootstrapPrompts) > 0) {
$promptOptionsHtml = '';
foreach ($bootstrapPrompts as $prompt) {
$promptOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
'value' => $prompt['id'],
'selected' => '',
'label' => $prompt['name'],
)) . "\n";
}
$promptToolHtml = RacketSandboxTemplate::renderFile('partials/index-prompt-select.html', array(
'select_prompt_label' => t('app.select_prompt', 'Select prompt'),
'prompt_options_html' => $promptOptionsHtml,
'copy_label' => t('app.copy', 'Copy'),
'copied_label' => t('app.copied', 'Copied'),
'copy_full_prompt_label' => t('app.copy_full_prompt', 'Copy full prompt'),
'bootstrap_prompt_json' => json_encode(array(
'link' => $issuedLink,
'prompts' => $bootstrapPrompts,
), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
));
} else {
$promptToolHtml = RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
'class' => 'small',
'text' => t('app.no_bootstrap_prompts', 'No personal prompts are available for this language. Copy a default prompt first from prompt management.'),
));
}
$promptPanelHtml = RacketSandboxTemplate::renderFile('partials/index-prompt-tool.html', array(
'copy_full_prompt_label' => t('app.copy_full_prompt', 'Copy full prompt'),
'bootstrap_prompt_help' => t('app.bootstrap_prompt_help', 'Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.'),
'prompt_tool_html' => $promptToolHtml,
));
}
$headerHtml = app_header_html(array(
'title' => t('app.title', 'Racket sandbox'),
'nav_items' => $headerNavItems,
'user' => $currentUser,
'user_prefix' => t('app.logged_in_as', 'Logged in as:'),
'admin_label' => t('app.admin', 'admin'),
'language_label' => t('app.language', 'Language'),
'language' => $language,
'languages' => $headerLanguages,
'language_action' => '/',
'logout_action' => '/?lang=' . rawurlencode($language),
'logout_label' => t('app.logout', 'Logout'),
'message' => $message,
'error' => $error,
));
echo RacketSandboxTemplate::renderFile('index.html', array(
'language' => $language,
'language_url' => rawurlencode($language),
'title' => t('app.title', 'Racket sandbox'),
'header_html' => $headerHtml,
'bootstrap_link_label' => t('app.bootstrap_link', 'Bootstrap link'),
'generate_bootstrap_link_label' => t('app.generate_bootstrap_link', 'Generate bootstrap link'),
'bootstrap_result_class' => $issuedLink !== '' ? 'has-prompt' : '',
'ttl_minutes_label' => t('app.ttl_minutes', 'TTL in minutes'),
'bootstrap_ttl_minutes' => $bootstrapTtlMinutes,
'bootstrap_ttl_min_minutes' => BOOTSTRAP_TTL_MIN_MINUTES,
'bootstrap_ttl_max_minutes' => BOOTSTRAP_TTL_MAX_MINUTES,
'ttl_range_help' => t('app.ttl_range_help', 'Allowed range: 30 minutes to 8 hours.'),
'generated_link_html' => $generatedLinkHtml,
'prompt_panel_html' => $promptPanelHtml,
));