add Session, Util, Controller classes
This commit is contained in:
parent
482beb9fb1
commit
9b6a42de7e
@ -1,26 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// Sesion handling
|
|
||||||
// Start a session and create a csrf token if necessary
|
|
||||||
// TODO - move these to AuthController?
|
|
||||||
function start_session(){
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_csrf_token(bool $regenerate = false){
|
|
||||||
if (!isset($_SESSION['csrf_token']) || $regenerate) {
|
|
||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateCsrfToken($token) {
|
|
||||||
return hash_equals($_SESSION['csrf_token'], $token);
|
|
||||||
}
|
|
||||||
|
|
||||||
start_session();
|
|
||||||
generate_csrf_token();
|
|
||||||
|
|
||||||
// TODO - I *think* what I want to do is define just this, then load up all the classes.
|
// TODO - I *think* what I want to do is define just this, then load up all the classes.
|
||||||
// Then I can define all this other boilerplate in Config or Util or whatever.
|
// Then I can define all this other boilerplate in Config or Util or whatever.
|
||||||
// I'll have one chicken-and-egg problem with the source directory, but that's not a big deal.
|
// I'll have one chicken-and-egg problem with the source directory, but that's not a big deal.
|
||||||
@ -52,11 +30,16 @@ function recursive_glob(string $pattern, string $directory): array {
|
|||||||
return $files;
|
return $files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// load base classes first
|
||||||
|
require_once SRC_DIR . '/Controller/Controller.php';
|
||||||
|
// load everything else
|
||||||
foreach (recursive_glob('*.php', SRC_DIR) as $file) {
|
foreach (recursive_glob('*.php', SRC_DIR) as $file) {
|
||||||
require_once $file;
|
require_once $file;
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm_setup();
|
Util::confirm_setup();
|
||||||
|
Session::start();
|
||||||
|
Session::generateCsrfToken();
|
||||||
$config = Config::load();
|
$config = Config::load();
|
||||||
|
|
||||||
// Get request data
|
// Get request data
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
class AdminController {
|
class AdminController extends Controller {
|
||||||
// GET handler
|
// GET handler
|
||||||
// render the admin page
|
// render the admin page
|
||||||
public function index(){
|
public function index(){
|
||||||
@ -12,7 +12,7 @@ class AdminController {
|
|||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
echo render_template(TEMPLATES_DIR . "/admin.php", $vars);
|
$this->render("admin.php", $vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST handler
|
// POST handler
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class AuthController {
|
class AuthController extends Controller {
|
||||||
function showLogin(?string $error = null){
|
function showLogin(?string $error = null){
|
||||||
$config = Config::load();
|
$config = Config::load();
|
||||||
$csrf_token = $_SESSION['csrf_token'];
|
$csrf_token = Session::getCsrfToken();
|
||||||
|
|
||||||
$vars = [
|
$vars = [
|
||||||
'config' => $config,
|
'config' => $config,
|
||||||
@ -10,7 +10,7 @@ class AuthController {
|
|||||||
'error' => $error,
|
'error' => $error,
|
||||||
];
|
];
|
||||||
|
|
||||||
echo render_template(TEMPLATES_DIR . '/login.php', $vars);
|
$this->render('login.php', $vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogin(){
|
function handleLogin(){
|
||||||
@ -19,24 +19,25 @@ class AuthController {
|
|||||||
$error = '';
|
$error = '';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
if (!validateCsrfToken($_POST['csrf_token'])) {
|
if (!Session::validateCsrfToken($_POST['csrf_token'])) {
|
||||||
die('Invalid CSRF token');
|
die('Invalid CSRF token');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move into session.php
|
|
||||||
$username = $_POST['username'] ?? '';
|
$username = $_POST['username'] ?? '';
|
||||||
$password = $_POST['password'] ?? '';
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
$db = get_db();
|
// TODO: move into user model
|
||||||
|
$db = Util::get_db();
|
||||||
$stmt = $db->prepare("SELECT id, username, password_hash FROM user WHERE username = ?");
|
$stmt = $db->prepare("SELECT id, username, password_hash FROM user WHERE username = ?");
|
||||||
$stmt->execute([$username]);
|
$stmt->execute([$username]);
|
||||||
$user = $stmt->fetch();
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
if ($user && password_verify($password, $user['password_hash'])) {
|
if ($user && password_verify($password, $user['password_hash'])) {
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
|
// TODO: move into session.php
|
||||||
$_SESSION['user_id'] = $user['id'];
|
$_SESSION['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
$_SESSION['csrf_token'] = generate_csrf_token(true);
|
Session::generateCsrfToken(true);
|
||||||
header('Location: ' . $config->basePath);
|
header('Location: ' . $config->basePath);
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
@ -46,9 +47,7 @@ class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleLogout(){
|
function handleLogout(){
|
||||||
$_SESSION = [];
|
Session::end();
|
||||||
session_destroy();
|
|
||||||
|
|
||||||
$config = Config::load();
|
$config = Config::load();
|
||||||
header('Location: ' . $config->basePath);
|
header('Location: ' . $config->basePath);
|
||||||
exit;
|
exit;
|
||||||
|
15
src/Controller/Controller.php
Normal file
15
src/Controller/Controller.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
class Controller {
|
||||||
|
protected function render(string $templateFile, array $vars = []) {
|
||||||
|
$templatePath = TEMPLATES_DIR . "/" . $templateFile;
|
||||||
|
|
||||||
|
if (!file_exists($templatePath)) {
|
||||||
|
throw new RuntimeException("Template not found: $templatePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP scoping
|
||||||
|
// extract the variables from $vars into the local scope.
|
||||||
|
extract($vars, EXTR_SKIP);
|
||||||
|
include $templatePath;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
class FeedController {
|
class FeedController extends Controller {
|
||||||
private Config $config;
|
private Config $config;
|
||||||
private array $ticks;
|
private array $ticks;
|
||||||
private array $vars;
|
private array $vars;
|
||||||
@ -14,10 +14,10 @@ class FeedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function rss(){
|
public function rss(){
|
||||||
echo render_template(TEMPLATES_DIR . "/feed/rss.php", $this->vars);
|
$this->render("feed/rss.php", $this->vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function atom(){
|
public function atom(){
|
||||||
echo render_template(TEMPLATES_DIR . "/feed/atom.php", $this->vars);
|
$this->render("feed/atom.php", $this->vars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
class HomeController{
|
//require_once __DIR__ . '/../Controller.php';
|
||||||
|
|
||||||
|
class HomeController extends Controller {
|
||||||
// GET handler
|
// GET handler
|
||||||
// renders the homepage view.
|
// renders the homepage view.
|
||||||
public function index(){
|
public function index(){
|
||||||
@ -22,7 +24,7 @@ class HomeController{
|
|||||||
'tickList' => $tickList,
|
'tickList' => $tickList,
|
||||||
];
|
];
|
||||||
|
|
||||||
echo render_template(TEMPLATES_DIR . "/home.php", $vars);
|
$this->render("home.php", $vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST handler
|
// POST handler
|
||||||
@ -30,7 +32,7 @@ class HomeController{
|
|||||||
public function handleTick(){
|
public function handleTick(){
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) {
|
||||||
// ensure that the session is valid before proceeding
|
// ensure that the session is valid before proceeding
|
||||||
if (!validateCsrfToken($_POST['csrf_token'])) {
|
if (!Session::validateCsrfToken($_POST['csrf_token'])) {
|
||||||
// TODO: maybe redirect to login? Maybe with tick preserved?
|
// TODO: maybe redirect to login? Maybe with tick preserved?
|
||||||
die('Invalid CSRF token');
|
die('Invalid CSRF token');
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
class MoodController {
|
class MoodController extends Controller {
|
||||||
public function index(){
|
public function index(){
|
||||||
$config = Config::load();
|
$config = Config::load();
|
||||||
$user = User::load();
|
$user = User::load();
|
||||||
@ -12,13 +12,13 @@
|
|||||||
'moodPicker' => $moodPicker,
|
'moodPicker' => $moodPicker,
|
||||||
];
|
];
|
||||||
|
|
||||||
echo render_template(TEMPLATES_DIR . "/mood.php", $vars);
|
$this->render("mood.php", $vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleMood(){
|
public function handleMood(){
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['mood'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['mood'])) {
|
||||||
// ensure that the session is valid before proceeding
|
// ensure that the session is valid before proceeding
|
||||||
if (!validateCsrfToken($_POST['csrf_token'])) {
|
if (!Session::validateCsrfToken($_POST['csrf_token'])) {
|
||||||
die('Invalid CSRF token');
|
die('Invalid CSRF token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
src/Framework/Session/Session.php
Normal file
31
src/Framework/Session/Session.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Session {
|
||||||
|
// These can all just be static functions
|
||||||
|
// Since they're essentially just manipulating the
|
||||||
|
// global $_SESSION associative array
|
||||||
|
public static function start(): void{
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateCsrfToken(bool $regenerate = false): void{
|
||||||
|
if (!isset($_SESSION['csrf_token']) || $regenerate) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validateCsrfToken($token): bool{
|
||||||
|
return hash_equals($_SESSION['csrf_token'], $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCsrfToken(): string{
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function end(): void {
|
||||||
|
$_SESSION = [];
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
}
|
121
src/Framework/Util/Util.php
Normal file
121
src/Framework/Util/Util.php
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
class Util {
|
||||||
|
public static function escape_and_linkify(string $text): string {
|
||||||
|
// escape dangerous characters, but preserve quotes
|
||||||
|
$safe = htmlspecialchars($text, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For relative time display, compare the stored time to the current time
|
||||||
|
// and display it as "X second/minutes/hours/days etc. "ago
|
||||||
|
public static function relative_time(string $tickTime): string {
|
||||||
|
$datetime = new DateTime($tickTime);
|
||||||
|
$now = new DateTime('now', $datetime->getTimezone());
|
||||||
|
$diff = $now->diff($datetime);
|
||||||
|
|
||||||
|
if ($diff->y > 0) {
|
||||||
|
return $diff->y . ' year' . ($diff->y > 1 ? 's' : '') . ' ago';
|
||||||
|
}
|
||||||
|
if ($diff->m > 0) {
|
||||||
|
return $diff->m . ' month' . ($diff->m > 1 ? 's' : '') . ' ago';
|
||||||
|
}
|
||||||
|
if ($diff->d > 0) {
|
||||||
|
return $diff->d . ' day' . ($diff->d > 1 ? 's' : '') . ' ago';
|
||||||
|
}
|
||||||
|
if ($diff->h > 0) {
|
||||||
|
return $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago';
|
||||||
|
}
|
||||||
|
if ($diff->i > 0) {
|
||||||
|
return $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago';
|
||||||
|
}
|
||||||
|
return $diff->s . ' second' . ($diff->s != 1 ? 's' : '') . ' ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static 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.
|
||||||
|
public static function confirm_setup(): void {
|
||||||
|
$db = Util::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,
|
||||||
|
about TEXT NULL,
|
||||||
|
website TEXT NULL,
|
||||||
|
mood TEXT 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move to model base class?
|
||||||
|
public static function get_db(): PDO {
|
||||||
|
Util::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;
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ class Config {
|
|||||||
|
|
||||||
// load config from sqlite database
|
// load config from sqlite database
|
||||||
public static function load(): self {
|
public static function load(): self {
|
||||||
$db = get_db();
|
$db = Util::get_db();
|
||||||
$stmt = $db->query("SELECT site_title, site_description, base_path, items_per_page FROM settings WHERE id=1");
|
$stmt = $db->query("SELECT site_title, site_description, base_path, items_per_page FROM settings WHERE id=1");
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$c = new self();
|
$c = new self();
|
||||||
@ -26,7 +26,7 @@ class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function save(): self {
|
public function save(): self {
|
||||||
$db = get_db();
|
$db = Util::get_db();
|
||||||
|
|
||||||
$stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_path=?, items_per_page=? WHERE id=1");
|
$stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_path=?, items_per_page=? WHERE id=1");
|
||||||
$stmt->execute([$this->siteTitle, $this->siteDescription, $this->basePath, $this->itemsPerPage]);
|
$stmt->execute([$this->siteTitle, $this->siteDescription, $this->basePath, $this->itemsPerPage]);
|
||||||
|
@ -9,7 +9,7 @@ class User {
|
|||||||
|
|
||||||
// load user settings from sqlite database
|
// load user settings from sqlite database
|
||||||
public static function load(): self {
|
public static function load(): self {
|
||||||
$db = get_db();
|
$db = Util::get_db();
|
||||||
|
|
||||||
// There's only ever one user. I'm just leaning into that.
|
// There's only ever one user. I'm just leaning into that.
|
||||||
$stmt = $db->query("SELECT username, display_name, about, website, mood FROM user WHERE id=1");
|
$stmt = $db->query("SELECT username, display_name, about, website, mood FROM user WHERE id=1");
|
||||||
@ -28,7 +28,7 @@ class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function save(): self {
|
public function save(): self {
|
||||||
$db = get_db();
|
$db = Util::get_db();
|
||||||
|
|
||||||
$stmt = $db->prepare("UPDATE user SET username=?, display_name=?, about=?, website=?, mood=? WHERE id=1");
|
$stmt = $db->prepare("UPDATE user SET username=?, display_name=?, about=?, website=?, mood=? WHERE id=1");
|
||||||
$stmt->execute([$this->username, $this->displayName, $this->about, $this->website, $this->mood]);
|
$stmt->execute([$this->username, $this->displayName, $this->about, $this->website, $this->mood]);
|
||||||
@ -39,7 +39,7 @@ class User {
|
|||||||
// Making this a separate function to avoid
|
// Making this a separate function to avoid
|
||||||
// loading the password into memory
|
// loading the password into memory
|
||||||
public function set_password(string $password): void {
|
public function set_password(string $password): void {
|
||||||
$db = get_db();
|
$db = Util::get_db();
|
||||||
|
|
||||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
$stmt = $db->prepare("UPDATE user SET password_hash=? WHERE id=1");
|
$stmt = $db->prepare("UPDATE user SET password_hash=? WHERE id=1");
|
||||||
|
@ -11,8 +11,8 @@ class HomeView {
|
|||||||
<div class="home-ticks-list">
|
<div class="home-ticks-list">
|
||||||
<?php foreach ($ticks as $tick): ?>
|
<?php foreach ($ticks as $tick): ?>
|
||||||
<article class="tick">
|
<article class="tick">
|
||||||
<div class="tick-time"><?= htmlspecialchars(relative_time($tick['timestamp'])) ?></div>
|
<div class="tick-time"><?= htmlspecialchars(Util::relative_time($tick['timestamp'])) ?></div>
|
||||||
<span class="tick-text"><?= escape_and_linkify($tick['tick']) ?></span>
|
<span class="tick-text"><?= Util::escape_and_linkify($tick['tick']) ?></span>
|
||||||
</article>
|
</article>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
139
src/lib/util.php
139
src/lib/util.php
@ -1,139 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
function escape_and_linkify(string $text): string {
|
|
||||||
// escape dangerous characters, but preserve quotes
|
|
||||||
$safe = htmlspecialchars($text, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For relative time display, compare the stored time to the current time
|
|
||||||
// and display it as "X second/minutes/hours/days etc. "ago
|
|
||||||
function relative_time(string $tickTime): string {
|
|
||||||
$datetime = new DateTime($tickTime);
|
|
||||||
$now = new DateTime('now', $datetime->getTimezone());
|
|
||||||
$diff = $now->diff($datetime);
|
|
||||||
|
|
||||||
if ($diff->y > 0) {
|
|
||||||
return $diff->y . ' year' . ($diff->y > 1 ? 's' : '') . ' ago';
|
|
||||||
}
|
|
||||||
if ($diff->m > 0) {
|
|
||||||
return $diff->m . ' month' . ($diff->m > 1 ? 's' : '') . ' ago';
|
|
||||||
}
|
|
||||||
if ($diff->d > 0) {
|
|
||||||
return $diff->d . ' day' . ($diff->d > 1 ? 's' : '') . ' ago';
|
|
||||||
}
|
|
||||||
if ($diff->h > 0) {
|
|
||||||
return $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago';
|
|
||||||
}
|
|
||||||
if ($diff->i > 0) {
|
|
||||||
return $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago';
|
|
||||||
}
|
|
||||||
return $diff->s . ' second' . ($diff->s != 1 ? 's' : '') . ' ago';
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
about TEXT NULL,
|
|
||||||
website TEXT NULL,
|
|
||||||
mood TEXT 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO - Maybe this is the sort of thing that would be good
|
|
||||||
// in a Controller base class.
|
|
||||||
function render_template(string $templateFile, array $vars = []): string {
|
|
||||||
if (!file_exists($templateFile)) {
|
|
||||||
throw new RuntimeException("Template not found: $templatePath");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract variables into local scope
|
|
||||||
extract($vars, EXTR_SKIP);
|
|
||||||
|
|
||||||
// Start output buffering
|
|
||||||
ob_start();
|
|
||||||
|
|
||||||
// Include the template (with extracted variables in scope)
|
|
||||||
include $templateFile;
|
|
||||||
|
|
||||||
// Return rendered output
|
|
||||||
return ob_get_clean();
|
|
||||||
}
|
|
@ -13,8 +13,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="home-navbar">
|
<div class="home-navbar">
|
||||||
<a href="<?= $config->basePath ?>rss">rss</a>
|
<a href="<?= $config->basePath ?>feed/rss">rss</a>
|
||||||
<a href="<?= $config->basePath ?>atom">atom</a>
|
<a href="<?= $config->basePath ?>feed/atom">atom</a>
|
||||||
<?php if (!$isLoggedIn): ?>
|
<?php if (!$isLoggedIn): ?>
|
||||||
<a href="<?= $config->basePath ?>login">login</a>
|
<a href="<?= $config->basePath ?>login">login</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<h2>Hi, I'm <?= $user->displayName ?></h2>
|
<h2>Hi, I'm <?= $user->displayName ?></h2>
|
||||||
</div>
|
</div>
|
||||||
<p><?= $user->about ?></p>
|
<p><?= $user->about ?></p>
|
||||||
<p>Website: <?= escape_and_linkify($user->website) ?></p>
|
<p>Website: <?= Util::escape_and_linkify($user->website) ?></p>
|
||||||
<div class="profile-row">
|
<div class="profile-row">
|
||||||
<div class="mood-bar">
|
<div class="mood-bar">
|
||||||
<span>Current mood: <?= $user->mood ?></span>
|
<span>Current mood: <?= $user->mood ?></span>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user