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
|
||||
// display the requested block of ticks
|
||||
// without storing all ticks in an array
|
||||
function stream_ticks(string $tickLocation, int $limit, int $offset = 0): Generator {
|
||||
$tick_files = glob($tickLocation . '/*/*/*.txt');
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
function escape_and_linkify_tick(string $tick): string {
|
||||
// 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
|
||||
|
||||
$count = 0;
|
@ -1,21 +1,24 @@
|
||||
<?php
|
||||
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
||||
define('ITEMS_PER_PAGE', 25);
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require APP_ROOT . '/config.php';
|
||||
require APP_ROOT . '/session.php';
|
||||
require_once APP_ROOT . '/stream_ticks.php';
|
||||
confirm_setup();
|
||||
|
||||
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;
|
||||
$limit = ITEMS_PER_PAGE;
|
||||
$limit = $config->itemsPerPage;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$ticks = iterator_to_array(stream_ticks($tickLocation, $limit, $offset));
|
||||
$ticks = iterator_to_array(stream_ticks($limit, $offset));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>My ticker</title>
|
||||
<title><?= $config->siteTitle ?></title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 2em; }
|
||||
.tick { margin-bottom: 1em; }
|
||||
@ -25,37 +28,38 @@ $ticks = iterator_to_array(stream_ticks($tickLocation, $limit, $offset));
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Welcome! Here's what's ticking.</h2>
|
||||
<h2><?= $config->siteDescription ?></h2>
|
||||
|
||||
<?php foreach ($ticks as $tick): ?>
|
||||
<div class="tick">
|
||||
<spam class="ticktime"><?= $tick['timestamp'] ?></span>
|
||||
<span class="ticktext"><?= $tick['tick'] ?></spam>
|
||||
<span class="ticktime"><?= htmlspecialchars($tick['timestamp']) ?></span>
|
||||
<span class="ticktext"><?= escape_and_linkify_tick($tick['tick']) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="pagination">
|
||||
<div class="pagination">
|
||||
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?page=<?= $page - 1 ?>">« Newer</a>
|
||||
<a href="?page=<?= $page - 1 ?>">« Newer</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (count($ticks) === $limit): ?>
|
||||
<a href="?page=<?= $page + 1 ?>">Older »</a>
|
||||
<a href="?page=<?= $page + 1 ?>">Older »</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<?php if (!$isLoggedIn): ?>
|
||||
<p><a href="<?= $basePath ?>/login.php">Login</a></p>
|
||||
<p><a href="<?= $config->basePath ?>login.php">Login</a></p>
|
||||
<?php else: ?>
|
||||
<form action="save_tick.php" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<label for="tick">What's ticking?</label>
|
||||
<input name="tick" id="tick" type="text">
|
||||
|
||||
<button type="submit">Tick</button>
|
||||
</form>
|
||||
<p><a href="<?= $basePath ?>/logout.php">Logout</a> <?= htmlspecialchars($_SESSION['username']) ?> </p>
|
||||
<form action="save_tick.php" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<label for="tick">What's ticking?</label>
|
||||
<input name="tick" id="tick" type="text">
|
||||
|
||||
<button type="submit">Tick</button>
|
||||
</form>
|
||||
<p><a href="<?= $config->basePath ?>logout.php">Logout</a> <?= htmlspecialchars($_SESSION['username']) ?> </p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require APP_ROOT . '/config.php';
|
||||
require APP_ROOT . '/session.php';
|
||||
require LIB_ROOT . '/config.php';
|
||||
require LIB_ROOT . '/session.php';
|
||||
|
||||
$config = Config::load();
|
||||
|
||||
$error = '';
|
||||
|
||||
@ -14,7 +16,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$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]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
@ -22,7 +25,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
header('Location: ' . $basePath . '/');
|
||||
header('Location: ' . $config->basePath);
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Invalid username or password';
|
||||
@ -40,7 +43,7 @@ $csrf_token = generateCsrfToken();
|
||||
<?php if ($error): ?>
|
||||
<p style="color:red"><?= htmlspecialchars($error) ?></p>
|
||||
<?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) ?>">
|
||||
<label>Username: <input type="text" name="username" required></label><br>
|
||||
<label>Password: <input type="password" name="password" required></label><br>
|
||||
|
@ -1,11 +1,12 @@
|
||||
<?php
|
||||
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require APP_ROOT . '/config.php';
|
||||
require APP_ROOT . '/session.php';
|
||||
require LIB_ROOT . '/config.php';
|
||||
require LIB_ROOT . '/session.php';
|
||||
|
||||
$config = Config::load();
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
|
||||
header('Location: ' . $basePath . '/');
|
||||
header('Location: ' . $config->basePath);
|
||||
exit;
|
@ -1,44 +1,30 @@
|
||||
<?php
|
||||
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require APP_ROOT . '/config.php';
|
||||
require APP_ROOT . '/session.php';
|
||||
require LIB_ROOT . '/config.php';
|
||||
require LIB_ROOT . '/session.php';
|
||||
require LIB_ROOT . '/ticks.php';
|
||||
|
||||
confirm_setup();
|
||||
|
||||
// ticks must be sent via POST
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) {
|
||||
// csrf check
|
||||
// ensure that the session is valid before proceeding
|
||||
if (!validateCsrfToken($_POST['csrf_token'])) {
|
||||
die('Invalid CSRF token');
|
||||
}
|
||||
|
||||
$tick = htmlspecialchars($_POST['tick'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
} else {
|
||||
// just go back to the index
|
||||
// just go back to the index if it's not a POST
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
# write the tick to a new entry
|
||||
$date = new DateTime();
|
||||
// get the config
|
||||
$config = Config::load();
|
||||
|
||||
$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 = "$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);
|
||||
// save the tick
|
||||
save_tick($_POST['tick']);
|
||||
|
||||
// go back to the index and show the latest tick
|
||||
header('Location: index.php');
|
||||
header('Location: ' . $config->basePath);
|
||||
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