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'])) {
// Invalid session - redirect 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;
}
} else {

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ class FeedController extends Controller {
public function __construct(){
$this->config = ConfigModel::load();
$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");
}

View File

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

View File

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

View File

@ -79,28 +79,4 @@ class Util {
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
class TickModel {
public function getPage(int $limit, int $offset = 0): array {
public function stream(int $limit, int $offset = 0): Generator {
global $db;
$stmt = $db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC 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 {

View File

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

View File

@ -10,10 +10,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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)): ?>
<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; ?>
<link rel="alternate"
type="application/rss+xml"

View File

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

View File

@ -2,7 +2,7 @@
<?php /** @var Array $customCss */ ?>
<h1>CSS Management</h1>
<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']) ?>">
<fieldset>
<legend>Manage</legend>

View File

@ -2,7 +2,7 @@
<?php /** @var array $emojiList */ ?>
<h1>Emoji Management</h1>
<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']) ?>">
<fieldset>
<legend>Add Emoji</legend>
@ -24,7 +24,7 @@
</fieldset>
</form>
<?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']) ?>">
<fieldset class="delete-emoji-fieldset">
<legend>Delete Emoji</legend>

View File

@ -14,7 +14,7 @@
<?php if (Session::isLoggedIn()): ?>
<a
<?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>
<?php endif ?>
</dd>

View File

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

View File

@ -2,32 +2,32 @@
<?php /* https://www.w3schools.com/howto/howto_css_dropdown.asp */ ?>
<nav aria-label="Main navigation">
<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>
<summary aria-haspopup="true">feeds</summary>
<div class="dropdown-items">
<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; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'feed/atom')) ?>">atom</a>
href="<?= Util::escape_html($config->basePath) ?>feed/atom">atom</a>
</div>
</details>
<?php if (!Session::isLoggedIn()): ?>
<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: ?>
<details>
<summary aria-haspopup="true">admin</summary>
<div class="dropdown-items">
<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; ?>
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; ?>
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin/emoji')) ?>">emoji</a>
href="<?= Util::escape_html($config->basePath) ?>admin/emoji">emoji</a>
</div>
</details>
<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; ?>
</nav>

View File

@ -26,51 +26,4 @@ final class UtilTest extends TestCase
$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);
}
}