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/.
373 lines
12 KiB
PHP
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,
|
|
));
|