Compare commits

..

No commits in common. "8b5a2494502fa6cb1a983905152c9536b003a377" and "dc63d70944b44d5e5c8bd448b42fe3601232d327" have entirely different histories.

18 changed files with 39 additions and 113 deletions

View File

@ -77,7 +77,7 @@ if ($method === 'POST' && $path != 'setup') {
if (!Session::isValid($_POST['csrf_token'])) { if (!Session::isValid($_POST['csrf_token'])) {
// Invalid session - redirect to /login // Invalid session - redirect to /login
Log::info('Attempt to POST with invalid session. Redirecting to login.'); Log::info('Attempt to POST with invalid session. Redirecting to login.');
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login')); header('Location: ' . $config->basePath . '/login');
exit; exit;
} }
} else { } else {

View File

@ -30,7 +30,7 @@ class AdminController extends Controller {
public function handleSave(){ public function handleSave(){
if (!Session::isLoggedIn()){ if (!Session::isLoggedIn()){
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login')); header('Location: ' . $config->basePath . '/login');
exit; exit;
} }

View File

@ -30,7 +30,7 @@ class AuthController extends Controller {
Log::info("Successful login for {$username}"); Log::info("Successful login for {$username}");
Session::newLoginSession($user); Session::newLoginSession($user);
header('Location: ' . Util::buildRelativeUrl($config->basePath)); header('Location: ' . $config->basePath);
exit; exit;
} else { } else {
Log::warning("Failed login for {$username}"); Log::warning("Failed login for {$username}");
@ -48,7 +48,7 @@ class AuthController extends Controller {
Session::end(); Session::end();
global $config; global $config;
header('Location: ' . Util::buildRelativeUrl($config->basePath)); header('Location: ' . $config->basePath);
exit; exit;
} }
} }

View File

@ -29,7 +29,7 @@
break; break;
} }
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'admin/emoji')); header('Location: ' . $config->basePath . 'admin/emoji');
exit; exit;
} }

View File

@ -6,7 +6,7 @@ class FeedController extends Controller {
public function __construct(){ public function __construct(){
$this->config = ConfigModel::load(); $this->config = ConfigModel::load();
$tickModel = new TickModel(); $tickModel = new TickModel();
$this->ticks = $tickModel->getPage($this->config->itemsPerPage); $this->ticks = iterator_to_array($tickModel->stream($this->config->itemsPerPage));
Log::debug("Loaded " . count($this->ticks) . " ticks for feeds"); Log::debug("Loaded " . count($this->ticks) . " ticks for feeds");
} }

View File

@ -10,10 +10,10 @@ class HomeController extends Controller {
$tickModel = new TickModel(); $tickModel = new TickModel();
$limit = $config->itemsPerPage; $limit = $config->itemsPerPage;
$offset = ($page - 1) * $limit; $offset = ($page - 1) * $limit;
$ticks = $tickModel->getPage($limit, $offset); $ticks = iterator_to_array($tickModel->stream($limit, $offset));
$view = new TicksView($config, $ticks, $page); $view = new HomeView();
$tickList = $view->getHtml(); $tickList = $view->renderTicksSection($config->siteDescription, $ticks, $page, $limit);
$vars = [ $vars = [
'config' => $config, 'config' => $config,
@ -39,7 +39,7 @@ class HomeController extends Controller {
global $config; global $config;
// redirect to the index (will show the latest tick if one was sent) // redirect to the index (will show the latest tick if one was sent)
header('Location: ' . Util::buildRelativeUrl($config->basePath)); header('Location: ' . $config->basePath);
exit; exit;
} }

View File

@ -35,7 +35,7 @@
$user = $user->save(); $user = $user->save();
// go back to the index and show the updated mood // go back to the index and show the updated mood
header('Location: ' . Util::buildRelativeUrl($config->basePath)); header('Location: ' . $config->basePath);
exit; exit;
} }
} }

View File

@ -79,28 +79,4 @@ class Util {
return $baseUrl . $basePath . $path; return $baseUrl . $basePath . $path;
} }
public static function buildRelativeUrl(string $basePath, string $path = ''): string {
// Ensure basePath starts with / for relative URLs
$basePath = '/' . ltrim($basePath, '/');
// Remove trailing slash unless it's just '/'
if ($basePath !== '/') {
$basePath = rtrim($basePath, '/');
}
// Add path
$path = ltrim($path, '/');
if ($path === '') {
return $basePath;
}
// If basePath is root, don't add extra slash
if ($basePath === '/') {
return '/' . $path;
}
return $basePath . '/' . $path;
}
} }

View File

@ -1,12 +1,18 @@
<?php <?php
class TickModel { class TickModel {
public function getPage(int $limit, int $offset = 0): array { public function stream(int $limit, int $offset = 0): Generator {
global $db; global $db;
$stmt = $db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?"); $stmt = $db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?");
$stmt->execute([$limit, $offset]); $stmt->execute([$limit, $offset]);
return $stmt->fetchAll(PDO::FETCH_ASSOC); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield [
'id' => $row['id'],
'timestamp' => $row['timestamp'],
'tick' => $row['tick'],
];
}
} }
public function insert(string $tick, ?DateTimeImmutable $datetime = null): void { public function insert(string $tick, ?DateTimeImmutable $datetime = null): void {

View File

@ -1,16 +1,7 @@
<?php <?php
class TicksView { class HomeView {
private $html; public function renderTicksSection(string $siteDescription, array $ticks, int $page, int $limit){
global $config;
public function __construct(ConfigModel $config, array $ticks, int $page){
$this->html = $this->render($config, $ticks, $page);
}
public function getHtml(): string {
return $this->html;
}
private function render(ConfigModel $config, array $ticks, int $page): string{
ob_start(); ob_start();
?> ?>
@ -31,7 +22,7 @@ class TicksView {
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="?page=<?php echo $page - 1 ?>">&laquo; Newer</a> href="?page=<?php echo $page - 1 ?>">&laquo; Newer</a>
<?php endif; ?> <?php endif; ?>
<?php if (count($ticks) === $config->itemsPerPage): ?> <?php if (count($ticks) === $limit): ?>
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="?page=<?php echo $page + 1 ?>">Older &raquo;</a> href="?page=<?php echo $page + 1 ?>">Older &raquo;</a>
<?php endif; ?> <?php endif; ?>

View File

@ -10,10 +10,10 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" <link rel="stylesheet"
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/default.css')) ?>"> href="<?= Util::escape_html($config->basePath) ?>css/default.css">
<?php if (!empty($config->cssId)): ?> <?php if (!empty($config->cssId)): ?>
<link rel="stylesheet" <link rel="stylesheet"
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/custom/' . $config->customCssFilename())) ?>"> href="<?= Util::escape_html($config->basePath) ?>css/custom/<?= Util::escape_html($config->customCssFilename()) ?>">
<?php endif; ?> <?php endif; ?>
<link rel="alternate" <link rel="alternate"
type="application/rss+xml" type="application/rss+xml"

View File

@ -4,7 +4,7 @@
<h1><?php if ($isSetup): ?>Setup<?php else: ?>Admin<?php endif; ?></h1> <h1><?php if ($isSetup): ?>Setup<?php else: ?>Admin<?php endif; ?></h1>
<main> <main>
<form <form
action="<?php echo Util::buildRelativeUrl($config->basePath, ($isSetup ? 'setup' : 'admin')) ?>" action="<?php echo $config->basePath . ($isSetup ? 'setup' : 'admin') ?>"
method="post"> method="post">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>"> <input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<fieldset> <fieldset>

View File

@ -2,7 +2,7 @@
<?php /** @var Array $customCss */ ?> <?php /** @var Array $customCss */ ?>
<h1>CSS Management</h1> <h1>CSS Management</h1>
<main> <main>
<form action="<?= Util::buildRelativeUrl($config->basePath, 'admin/css') ?>" method="post" enctype="multipart/form-data"> <form action="<?= $config->basePath ?>admin/css" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>"> <input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<fieldset> <fieldset>
<legend>Manage</legend> <legend>Manage</legend>

View File

@ -2,7 +2,7 @@
<?php /** @var array $emojiList */ ?> <?php /** @var array $emojiList */ ?>
<h1>Emoji Management</h1> <h1>Emoji Management</h1>
<main> <main>
<form action="<?= Util::buildRelativeUrl($config->basePath, 'admin/emoji') ?>" method="post" enctype="multipart/form-data"> <form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>"> <input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<fieldset> <fieldset>
<legend>Add Emoji</legend> <legend>Add Emoji</legend>
@ -24,7 +24,7 @@
</fieldset> </fieldset>
</form> </form>
<?php if (!empty($emojiList)): ?> <?php if (!empty($emojiList)): ?>
<form action="<?= Util::buildRelativeUrl($config->basePath, 'admin/emoji') ?>" method="post" enctype="multipart/form-data"> <form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>"> <input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<fieldset class="delete-emoji-fieldset"> <fieldset class="delete-emoji-fieldset">
<legend>Delete Emoji</legend> <legend>Delete Emoji</legend>

View File

@ -14,7 +14,7 @@
<?php if (Session::isLoggedIn()): ?> <?php if (Session::isLoggedIn()): ?>
<a <a
<?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'mood')) ?>" href="<?= Util::escape_html($config->basePath) ?>mood"
class="change-mood">Change mood</a> class="change-mood">Change mood</a>
<?php endif ?> <?php endif ?>
</dd> </dd>

View File

@ -2,7 +2,7 @@
<?php /** @var string $csrf_token */ ?> <?php /** @var string $csrf_token */ ?>
<?php /** @var string $error */ ?> <?php /** @var string $error */ ?>
<h2>Login</h2> <h2>Login</h2>
<form method="post" action="<?= Util::buildRelativeUrl($config->basePath, 'login') ?>"> <form method="post" action="<?= $config->basePath ?>login">
<div class="fieldset-items"> <div class="fieldset-items">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($csrf_token) ?>"> <input type="hidden" name="csrf_token" value="<?= Util::escape_html($csrf_token) ?>">
<label for="username">Username:</label> <label for="username">Username:</label>

View File

@ -2,32 +2,32 @@
<?php /* https://www.w3schools.com/howto/howto_css_dropdown.asp */ ?> <?php /* https://www.w3schools.com/howto/howto_css_dropdown.asp */ ?>
<nav aria-label="Main navigation"> <nav aria-label="Main navigation">
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath)) ?>">home</a> href="<?= Util::escape_html($config->basePath) ?>">home</a>
<details> <details>
<summary aria-haspopup="true">feeds</summary> <summary aria-haspopup="true">feeds</summary>
<div class="dropdown-items"> <div class="dropdown-items">
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'feed/rss')) ?>">rss</a> href="<?= Util::escape_html($config->basePath) ?>feed/rss">rss</a>
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'feed/atom')) ?>">atom</a> href="<?= Util::escape_html($config->basePath) ?>feed/atom">atom</a>
</div> </div>
</details> </details>
<?php if (!Session::isLoggedIn()): ?> <?php if (!Session::isLoggedIn()): ?>
<a tabindex="0" <a tabindex="0"
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'login')) ?>">login</a> href="<?= Util::escape_html($config->basePath) ?>login">login</a>
<?php else: ?> <?php else: ?>
<details> <details>
<summary aria-haspopup="true">admin</summary> <summary aria-haspopup="true">admin</summary>
<div class="dropdown-items"> <div class="dropdown-items">
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin')) ?>">settings</a> href="<?= Util::escape_html($config->basePath) ?>admin">settings</a>
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin/css')) ?>">css</a> href="<?= Util::escape_html($config->basePath) ?>admin/css">css</a>
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin/emoji')) ?>">emoji</a> href="<?= Util::escape_html($config->basePath) ?>admin/emoji">emoji</a>
</div> </div>
</details> </details>
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?> <a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'logout')) ?>">logout</a> href="<?= Util::escape_html($config->basePath) ?>logout">logout</a>
<?php endif; ?> <?php endif; ?>
</nav> </nav>

View File

@ -26,51 +26,4 @@ final class UtilTest extends TestCase
$this->assertSame($relativeTime, $display); $this->assertSame($relativeTime, $display);
} }
public static function buildUrlProvider(): array {
return [
'basic path' => ['https://example.com', 'tkr', 'admin', 'https://example.com/tkr/admin'],
'baseUrl with trailing slash' => ['https://example.com/', 'tkr', 'admin', 'https://example.com/tkr/admin'],
'empty basePath' => ['https://example.com', '', 'admin', 'https://example.com/admin'],
'root basePath' => ['https://example.com', '/', 'admin', 'https://example.com/admin'],
'basePath no leading slash' => ['https://example.com', 'tkr', 'admin', 'https://example.com/tkr/admin'],
'basePath with leading slash' => ['https://example.com', '/tkr', 'admin', 'https://example.com/tkr/admin'],
'basePath with trailing slash' => ['https://example.com', 'tkr/', 'admin', 'https://example.com/tkr/admin'],
'basePath with both slashes' => ['https://example.com', '/tkr/', 'admin', 'https://example.com/tkr/admin'],
'complex path' => ['https://example.com', 'tkr', 'admin/css/upload', 'https://example.com/tkr/admin/css/upload'],
'path with leading slash' => ['https://example.com', 'tkr', '/admin', 'https://example.com/tkr/admin'],
'no path - empty basePath' => ['https://example.com', '', '', 'https://example.com/'],
'no path - root basePath' => ['https://example.com', '/', '', 'https://example.com/'],
'no path - tkr basePath' => ['https://example.com', 'tkr', '', 'https://example.com/tkr/'],
];
}
#[DataProvider('buildUrlProvider')]
public function testBuildUrl(string $baseUrl, string $basePath, string $path, string $expected): void {
$result = Util::buildUrl($baseUrl, $basePath, $path);
$this->assertEquals($expected, $result);
}
public static function buildRelativeUrlProvider(): array {
return [
'empty basePath with path' => ['', 'admin', '/admin'],
'root basePath with path' => ['/', 'admin', '/admin'],
'tkr basePath with path' => ['tkr', 'admin', '/tkr/admin'],
'tkr with leading slash' => ['/tkr', 'admin', '/tkr/admin'],
'tkr with trailing slash' => ['tkr/', 'admin', '/tkr/admin'],
'tkr with both slashes' => ['/tkr/', 'admin', '/tkr/admin'],
'complex path' => ['tkr', 'admin/css/upload', '/tkr/admin/css/upload'],
'path with leading slash' => ['tkr', '/admin', '/tkr/admin'],
'no path - empty basePath' => ['', '', '/'],
'no path - root basePath' => ['/', '', '/'],
'no path - tkr basePath' => ['tkr', '', '/tkr'],
'no path - tkr with slashes' => ['/tkr/', '', '/tkr'],
];
}
#[DataProvider('buildRelativeUrlProvider')]
public function testBuildRelativeUrl(string $basePath, string $path, string $expected): void {
$result = Util::buildRelativeUrl($basePath, $path);
$this->assertEquals($expected, $result);
}
} }