Add setup.php. Add lib dir. Move functions to lib dir.
This commit is contained in:
parent
c6a49dc6e8
commit
5ad15a478d
83
tkr/bootstrap.php
Normal file
83
tkr/bootstrap.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
define('APP_ROOT', dirname(__FILE__));
|
||||||
|
define('LIB_ROOT', APP_ROOT . '/lib');
|
||||||
|
define('TICKS_DIR', APP_ROOT . '/storage/ticks');
|
||||||
|
define('DATA_DIR', APP_ROOT . '/storage/db');
|
||||||
|
define('DB_FILE', DATA_DIR . '/tkr.sqlite');
|
||||||
|
|
||||||
|
function verify_data_dir(string $dir, bool $allow_create = false): void {
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
if ($allow_create) {
|
||||||
|
if (!mkdir($dir, 0770, true)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Failed to create required directory: $dir";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Required directory does not exist: $dir";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_writable($dir)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Directory is not writable: $dir";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that setup is complete (i.e. the databse is populated).
|
||||||
|
// Redirect to setup.php if it isn't.
|
||||||
|
function confirm_setup(): void {
|
||||||
|
$db = get_db();
|
||||||
|
|
||||||
|
// Ensure required tables exist
|
||||||
|
$db->exec("CREATE TABLE IF NOT EXISTS user (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL
|
||||||
|
)");
|
||||||
|
|
||||||
|
$db->exec("CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
site_title TEXT NOT NULL,
|
||||||
|
site_description TEXT NULL,
|
||||||
|
base_path TEXT NOT NULL,
|
||||||
|
items_per_page INTEGER NOT NULL
|
||||||
|
)");
|
||||||
|
|
||||||
|
// See if there's any data in the tables
|
||||||
|
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
|
||||||
|
$settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
|
||||||
|
|
||||||
|
// If either table has no records and we aren't on setup.php, redirect to setup.php
|
||||||
|
if ($user_count === 0 || $settings_count === 0){
|
||||||
|
if (basename($_SERVER['PHP_SELF']) !== 'setup.php'){
|
||||||
|
header('Location: setup.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If setup is complete and we are on setup.php, redirect to index.php.
|
||||||
|
if (basename($_SERVER['PHP_SELF']) === 'setup.php'){
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_db(): PDO {
|
||||||
|
verify_data_dir(DATA_DIR, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = new PDO("sqlite:" . DB_FILE);
|
||||||
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Database connection failed: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $db;
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
<?php
|
|
||||||
$dbLocation = __DIR__ . '/db/tkr.sqlite';
|
|
||||||
$tickLocation = __DIR__ . '/ticks';
|
|
||||||
$basePath = '/tkr';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = new PDO("sqlite:$dbLocation");
|
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Database connection failed: " . $e->getMessage());
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
// TODO: Replace this whole thing with a setup.php
|
|
||||||
|
|
||||||
function prompt($prompt) {
|
|
||||||
echo $prompt;
|
|
||||||
return trim(fgets(STDIN));
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptSilent($prompt = "Enter Password: ") {
|
|
||||||
if (strncasecmp(PHP_OS, 'WIN', 3) === 0) {
|
|
||||||
// Windows doesn't support shell-based hidden input
|
|
||||||
echo "Warning: Password input not hidden on Windows.\n";
|
|
||||||
return prompt($prompt);
|
|
||||||
} else {
|
|
||||||
// Use shell to disable echo for password input
|
|
||||||
echo $prompt;
|
|
||||||
system('stty -echo');
|
|
||||||
$password = rtrim(fgets(STDIN), "\n");
|
|
||||||
system('stty echo');
|
|
||||||
echo "\n";
|
|
||||||
return $password;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$dbFile = __DIR__ . '/tkr.sqlite';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = new PDO("sqlite:$dbFile");
|
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Could not connect to DB: " . $e->getMessage() . "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS user (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT UNIQUE NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL
|
|
||||||
)");
|
|
||||||
|
|
||||||
$username = prompt("Enter username: ");
|
|
||||||
$password = promptSilent("Enter password: ");
|
|
||||||
$confirm = promptSilent("Confirm password: ");
|
|
||||||
|
|
||||||
if ($password !== $confirm) {
|
|
||||||
die("Error: Passwords do not match.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO user(username, password_hash) VALUES (?, ?)");
|
|
||||||
$stmt->execute([$username, $passwordHash]);
|
|
||||||
echo "User '$username' created successfully.\n";
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo "Failed to create user: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
25
tkr/lib/config.php
Normal file
25
tkr/lib/config.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
|
|
||||||
|
confirm_setup();
|
||||||
|
|
||||||
|
class Config {
|
||||||
|
public string $siteTitle = 'My tkr';
|
||||||
|
public string $siteDescription = '';
|
||||||
|
public string $basePath = '/';
|
||||||
|
public int $itemsPerPage = 25;
|
||||||
|
|
||||||
|
public static function load(): self {
|
||||||
|
$db = get_db();
|
||||||
|
$stmt = $db->query("SELECT site_title, site_description, base_path, items_per_page FROM settings WHERE id=1");
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$c = new self();
|
||||||
|
if ($row) {
|
||||||
|
$c->siteTitle = $row['site_title'];
|
||||||
|
$c->siteDescription = $row['site_description'];
|
||||||
|
$c->basePath = $row['base_path'];
|
||||||
|
$c->itemsPerPage = (int) $row['items_per_page'];
|
||||||
|
}
|
||||||
|
return $c;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,45 @@
|
|||||||
<?php
|
<?php
|
||||||
// display the requested block of ticks
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
// without storing all ticks in an array
|
|
||||||
function stream_ticks(string $tickLocation, int $limit, int $offset = 0): Generator {
|
function escape_and_linkify_tick(string $tick): string {
|
||||||
$tick_files = glob($tickLocation . '/*/*/*.txt');
|
// escape dangerous characters, but preserve quotes
|
||||||
|
$safe = htmlspecialchars($tick, ENT_NOQUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
|
||||||
|
// convert URLs to links
|
||||||
|
$safe = preg_replace_callback(
|
||||||
|
'~(https?://[^\s<>"\'()]+)~i',
|
||||||
|
fn($matches) => '<a href="' . htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $matches[1] . '</a>',
|
||||||
|
$safe
|
||||||
|
);
|
||||||
|
|
||||||
|
return $safe;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save_tick(string $tick): void {
|
||||||
|
// build the tick path and filename from the current time
|
||||||
|
$date = new DateTime();
|
||||||
|
|
||||||
|
$year = $date->format('Y');
|
||||||
|
$month = $date->format('m');
|
||||||
|
$day = $date->format('d');
|
||||||
|
$time = $date->format('H:i:s');
|
||||||
|
|
||||||
|
// build the full path to the tick file
|
||||||
|
$dir = TICKS_DIR . "/$year/$month";
|
||||||
|
$filename = "$dir/$day.txt";
|
||||||
|
|
||||||
|
// create the directory if it doesn't exist
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0770, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the tick to the file (the file will be created if it doesn't exist)
|
||||||
|
$content = $time . "|" . $tick . "\n";
|
||||||
|
file_put_contents($filename, $content, FILE_APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stream_ticks(int $limit, int $offset = 0): Generator {
|
||||||
|
$tick_files = glob(TICKS_DIR . '/*/*/*.txt');
|
||||||
usort($tick_files, fn($a, $b) => strcmp($b, $a)); // sort filenames in reverse chronological order
|
usort($tick_files, fn($a, $b) => strcmp($b, $a)); // sort filenames in reverse chronological order
|
||||||
|
|
||||||
$count = 0;
|
$count = 0;
|
@ -1,21 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
define('ITEMS_PER_PAGE', 25);
|
|
||||||
|
|
||||||
require APP_ROOT . '/config.php';
|
confirm_setup();
|
||||||
require APP_ROOT . '/session.php';
|
|
||||||
require_once APP_ROOT . '/stream_ticks.php';
|
require LIB_ROOT . '/config.php';
|
||||||
|
require LIB_ROOT . '/session.php';
|
||||||
|
require LIB_ROOT . '/ticks.php';
|
||||||
|
|
||||||
|
$config = Config::load();
|
||||||
|
|
||||||
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
||||||
$limit = ITEMS_PER_PAGE;
|
$limit = $config->itemsPerPage;
|
||||||
$offset = ($page - 1) * $limit;
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
$ticks = iterator_to_array(stream_ticks($tickLocation, $limit, $offset));
|
$ticks = iterator_to_array(stream_ticks($limit, $offset));
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>My ticker</title>
|
<title><?= $config->siteTitle ?></title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: sans-serif; margin: 2em; }
|
body { font-family: sans-serif; margin: 2em; }
|
||||||
.tick { margin-bottom: 1em; }
|
.tick { margin-bottom: 1em; }
|
||||||
@ -25,12 +28,12 @@ $ticks = iterator_to_array(stream_ticks($tickLocation, $limit, $offset));
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Welcome! Here's what's ticking.</h2>
|
<h2><?= $config->siteDescription ?></h2>
|
||||||
|
|
||||||
<?php foreach ($ticks as $tick): ?>
|
<?php foreach ($ticks as $tick): ?>
|
||||||
<div class="tick">
|
<div class="tick">
|
||||||
<spam class="ticktime"><?= $tick['timestamp'] ?></span>
|
<span class="ticktime"><?= htmlspecialchars($tick['timestamp']) ?></span>
|
||||||
<span class="ticktext"><?= $tick['tick'] ?></spam>
|
<span class="ticktext"><?= escape_and_linkify_tick($tick['tick']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
@ -44,9 +47,9 @@ $ticks = iterator_to_array(stream_ticks($tickLocation, $limit, $offset));
|
|||||||
<a href="?page=<?= $page + 1 ?>">Older »</a>
|
<a href="?page=<?= $page + 1 ?>">Older »</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<?php if (!$isLoggedIn): ?>
|
<?php if (!$isLoggedIn): ?>
|
||||||
<p><a href="<?= $basePath ?>/login.php">Login</a></p>
|
<p><a href="<?= $config->basePath ?>login.php">Login</a></p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<form action="save_tick.php" method="post">
|
<form action="save_tick.php" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
@ -55,7 +58,8 @@ $ticks = iterator_to_array(stream_ticks($tickLocation, $limit, $offset));
|
|||||||
|
|
||||||
<button type="submit">Tick</button>
|
<button type="submit">Tick</button>
|
||||||
</form>
|
</form>
|
||||||
<p><a href="<?= $basePath ?>/logout.php">Logout</a> <?= htmlspecialchars($_SESSION['username']) ?> </p>
|
<p><a href="<?= $config->basePath ?>logout.php">Logout</a> <?= htmlspecialchars($_SESSION['username']) ?> </p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
|
|
||||||
require APP_ROOT . '/config.php';
|
require LIB_ROOT . '/config.php';
|
||||||
require APP_ROOT . '/session.php';
|
require LIB_ROOT . '/session.php';
|
||||||
|
|
||||||
|
$config = Config::load();
|
||||||
|
|
||||||
$error = '';
|
$error = '';
|
||||||
|
|
||||||
@ -14,7 +16,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$username = $_POST['username'] ?? '';
|
$username = $_POST['username'] ?? '';
|
||||||
$password = $_POST['password'] ?? '';
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT id, username, password_hash FROM user WHERE username = ?");
|
$db = get_db();
|
||||||
|
$stmt = $db->prepare("SELECT id, username, password_hash FROM user WHERE username = ?");
|
||||||
$stmt->execute([$username]);
|
$stmt->execute([$username]);
|
||||||
$user = $stmt->fetch();
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
@ -22,7 +25,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION['user_id'] = $user['id'];
|
$_SESSION['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
header('Location: ' . $basePath . '/');
|
header('Location: ' . $config->basePath);
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
$error = 'Invalid username or password';
|
$error = 'Invalid username or password';
|
||||||
@ -40,7 +43,7 @@ $csrf_token = generateCsrfToken();
|
|||||||
<?php if ($error): ?>
|
<?php if ($error): ?>
|
||||||
<p style="color:red"><?= htmlspecialchars($error) ?></p>
|
<p style="color:red"><?= htmlspecialchars($error) ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<form method="post" action="<?= $basePath ?>/login.php">
|
<form method="post" action="<?= $config->basePath ?>login.php">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
||||||
<label>Username: <input type="text" name="username" required></label><br>
|
<label>Username: <input type="text" name="username" required></label><br>
|
||||||
<label>Password: <input type="password" name="password" required></label><br>
|
<label>Password: <input type="password" name="password" required></label><br>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
|
|
||||||
require APP_ROOT . '/config.php';
|
require LIB_ROOT . '/config.php';
|
||||||
require APP_ROOT . '/session.php';
|
require LIB_ROOT . '/session.php';
|
||||||
|
|
||||||
|
$config = Config::load();
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
session_destroy();
|
session_destroy();
|
||||||
|
|
||||||
header('Location: ' . $basePath . '/');
|
header('Location: ' . $config->basePath);
|
||||||
exit;
|
exit;
|
@ -1,44 +1,30 @@
|
|||||||
<?php
|
<?php
|
||||||
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
|
|
||||||
require APP_ROOT . '/config.php';
|
require LIB_ROOT . '/config.php';
|
||||||
require APP_ROOT . '/session.php';
|
require LIB_ROOT . '/session.php';
|
||||||
|
require LIB_ROOT . '/ticks.php';
|
||||||
|
|
||||||
|
confirm_setup();
|
||||||
|
|
||||||
// ticks must be sent via POST
|
// ticks must be sent via POST
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) {
|
||||||
// csrf check
|
// ensure that the session is valid before proceeding
|
||||||
if (!validateCsrfToken($_POST['csrf_token'])) {
|
if (!validateCsrfToken($_POST['csrf_token'])) {
|
||||||
die('Invalid CSRF token');
|
die('Invalid CSRF token');
|
||||||
}
|
}
|
||||||
|
|
||||||
$tick = htmlspecialchars($_POST['tick'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
||||||
} else {
|
} else {
|
||||||
// just go back to the index
|
// just go back to the index if it's not a POST
|
||||||
header('Location: index.php');
|
header('Location: index.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
# write the tick to a new entry
|
// get the config
|
||||||
$date = new DateTime();
|
$config = Config::load();
|
||||||
|
|
||||||
$year = $date->format('Y');
|
// save the tick
|
||||||
$month = $date->format('m');
|
save_tick($_POST['tick']);
|
||||||
$day = $date->format('d');
|
|
||||||
$time = $date->format('H:i:s');
|
|
||||||
|
|
||||||
// build the full path to the tick file
|
|
||||||
$dir = "$tickLocation/$year/$month";
|
|
||||||
$filename = "$dir/$day.txt";
|
|
||||||
|
|
||||||
// create the directory if it doesn't exist
|
|
||||||
if (!is_dir($dir)) {
|
|
||||||
mkdir($dir, 0770, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// write the tick to the file (the file will be created if it doesn't exist)
|
|
||||||
$content = $time . "|" . $tick . "\n";
|
|
||||||
file_put_contents($filename, $content, FILE_APPEND);
|
|
||||||
|
|
||||||
// go back to the index and show the latest tick
|
// go back to the index and show the latest tick
|
||||||
header('Location: index.php');
|
header('Location: ' . $config->basePath);
|
||||||
exit;
|
exit;
|
72
tkr/public/setup.php
Normal file
72
tkr/public/setup.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
|
|
||||||
|
confirm_setup();
|
||||||
|
|
||||||
|
// If we got past confirm_setup(), then setup isn't complete.
|
||||||
|
$db = get_db();
|
||||||
|
|
||||||
|
// Handle submitted form
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$display_name = trim($_POST['display_name'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$site_title = trim($_POST['site_title']) ?? '';
|
||||||
|
$site_description = trim($_POST['site_description']) ?? '';
|
||||||
|
$base_path = trim($_POST['base_path'] ?? '/');
|
||||||
|
$items_per_page = (int) ($_POST['items_per_page'] ?? 25);
|
||||||
|
|
||||||
|
// Sanitize base path
|
||||||
|
if (substr($base_path, -1) !== '/') {
|
||||||
|
$base_path .= '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
$errors = [];
|
||||||
|
if (!$username || !$password) {
|
||||||
|
$errors[] = "Username and password are required.";
|
||||||
|
}
|
||||||
|
if (!$display_name) {
|
||||||
|
$errors[] = "Display name is required.";
|
||||||
|
}
|
||||||
|
if (!$site_title) {
|
||||||
|
$errors[] = "Site title is required.";
|
||||||
|
}
|
||||||
|
if (!preg_match('#^/[^?<>:"|\\*]*$#', $base_path)) {
|
||||||
|
$errors[] = "Base path must look like a valid URL path (e.g. / or /tkr/).";
|
||||||
|
}
|
||||||
|
if ($items_per_page < 1 || $items_per_page > 50) {
|
||||||
|
$errors[] = "Items per page must be a number between 1 and 50.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($errors)) {
|
||||||
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("INSERT INTO user (username, display_name, password_hash) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$username, $display_name, $hash]);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_path, items_per_page) VALUES (1, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$site_title, $site_description, $base_path, $items_per_page]);
|
||||||
|
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h1>Let’s Set Up Your tkr</h1>
|
||||||
|
<form method="post">
|
||||||
|
<h3>User settings</h3>
|
||||||
|
<label>Username: <input type="text" name="username" required></label><br>
|
||||||
|
<label>Display name: <input type="text" name="display_name" required></label><br>
|
||||||
|
<label>Password: <input type="password" name="password" required></label><br>
|
||||||
|
<br/><br/>
|
||||||
|
<h3>Site settings</h3>
|
||||||
|
<label>Title: <input type="text" name="site_title" value="My tkr" required></label><br>
|
||||||
|
<label>Description: <input type="text" name="site_description"></label><br>
|
||||||
|
<label>Base path: <input type="text" name="base_path" value="/" required></label><br>
|
||||||
|
<label>Items per page (max 50): <input type="number" name="items_per_page" value="25" min="1" max="50" required></label><br>
|
||||||
|
<br/>
|
||||||
|
<button type="submit">Complete Setup</button>
|
||||||
|
</form>
|
Loading…
x
Reference in New Issue
Block a user