Compare commits
2 Commits
dc63d70944
...
8b5a249450
Author | SHA1 | Date | |
---|---|---|---|
8b5a249450 | |||
a9f610fc60 |
@ -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: ' . $config->basePath . '/login');
|
||||
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login'));
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
|
@ -30,7 +30,7 @@ class AdminController extends Controller {
|
||||
|
||||
public function handleSave(){
|
||||
if (!Session::isLoggedIn()){
|
||||
header('Location: ' . $config->basePath . '/login');
|
||||
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login'));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ class AuthController extends Controller {
|
||||
Log::info("Successful login for {$username}");
|
||||
|
||||
Session::newLoginSession($user);
|
||||
header('Location: ' . $config->basePath);
|
||||
header('Location: ' . Util::buildRelativeUrl($config->basePath));
|
||||
exit;
|
||||
} else {
|
||||
Log::warning("Failed login for {$username}");
|
||||
@ -48,7 +48,7 @@ class AuthController extends Controller {
|
||||
Session::end();
|
||||
|
||||
global $config;
|
||||
header('Location: ' . $config->basePath);
|
||||
header('Location: ' . Util::buildRelativeUrl($config->basePath));
|
||||
exit;
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@
|
||||
break;
|
||||
}
|
||||
|
||||
header('Location: ' . $config->basePath . 'admin/emoji');
|
||||
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'admin/emoji'));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ class FeedController extends Controller {
|
||||
public function __construct(){
|
||||
$this->config = ConfigModel::load();
|
||||
$tickModel = new TickModel();
|
||||
$this->ticks = iterator_to_array($tickModel->stream($this->config->itemsPerPage));
|
||||
$this->ticks = $tickModel->getPage($this->config->itemsPerPage);
|
||||
|
||||
Log::debug("Loaded " . count($this->ticks) . " ticks for feeds");
|
||||
}
|
||||
|
@ -10,10 +10,10 @@ class HomeController extends Controller {
|
||||
$tickModel = new TickModel();
|
||||
$limit = $config->itemsPerPage;
|
||||
$offset = ($page - 1) * $limit;
|
||||
$ticks = iterator_to_array($tickModel->stream($limit, $offset));
|
||||
$ticks = $tickModel->getPage($limit, $offset);
|
||||
|
||||
$view = new HomeView();
|
||||
$tickList = $view->renderTicksSection($config->siteDescription, $ticks, $page, $limit);
|
||||
$view = new TicksView($config, $ticks, $page);
|
||||
$tickList = $view->getHtml();
|
||||
|
||||
$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: ' . $config->basePath);
|
||||
header('Location: ' . Util::buildRelativeUrl($config->basePath));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
||||
$user = $user->save();
|
||||
|
||||
// go back to the index and show the updated mood
|
||||
header('Location: ' . $config->basePath);
|
||||
header('Location: ' . Util::buildRelativeUrl($config->basePath));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
@ -79,4 +79,28 @@ 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;
|
||||
}
|
||||
}
|
@ -1,18 +1,12 @@
|
||||
<?php
|
||||
class TickModel {
|
||||
public function stream(int $limit, int $offset = 0): Generator {
|
||||
public function getPage(int $limit, int $offset = 0): array {
|
||||
global $db;
|
||||
|
||||
$stmt = $db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?");
|
||||
$stmt->execute([$limit, $offset]);
|
||||
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
yield [
|
||||
'id' => $row['id'],
|
||||
'timestamp' => $row['timestamp'],
|
||||
'tick' => $row['tick'],
|
||||
];
|
||||
}
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function insert(string $tick, ?DateTimeImmutable $datetime = null): void {
|
||||
|
@ -1,7 +1,16 @@
|
||||
<?php
|
||||
class HomeView {
|
||||
public function renderTicksSection(string $siteDescription, array $ticks, int $page, int $limit){
|
||||
global $config;
|
||||
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{
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
@ -22,7 +31,7 @@ class HomeView {
|
||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||
href="?page=<?php echo $page - 1 ?>">« Newer</a>
|
||||
<?php endif; ?>
|
||||
<?php if (count($ticks) === $limit): ?>
|
||||
<?php if (count($ticks) === $config->itemsPerPage): ?>
|
||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||
href="?page=<?php echo $page + 1 ?>">Older »</a>
|
||||
<?php endif; ?>
|
@ -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($config->basePath) ?>css/default.css">
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/default.css')) ?>">
|
||||
<?php if (!empty($config->cssId)): ?>
|
||||
<link rel="stylesheet"
|
||||
href="<?= Util::escape_html($config->basePath) ?>css/custom/<?= Util::escape_html($config->customCssFilename()) ?>">
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/custom/' . $config->customCssFilename())) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="alternate"
|
||||
type="application/rss+xml"
|
||||
|
@ -4,7 +4,7 @@
|
||||
<h1><?php if ($isSetup): ?>Setup<?php else: ?>Admin<?php endif; ?></h1>
|
||||
<main>
|
||||
<form
|
||||
action="<?php echo $config->basePath . ($isSetup ? 'setup' : 'admin') ?>"
|
||||
action="<?php echo Util::buildRelativeUrl($config->basePath, ($isSetup ? 'setup' : 'admin')) ?>"
|
||||
method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
||||
<fieldset>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<?php /** @var Array $customCss */ ?>
|
||||
<h1>CSS Management</h1>
|
||||
<main>
|
||||
<form action="<?= $config->basePath ?>admin/css" method="post" enctype="multipart/form-data">
|
||||
<form action="<?= Util::buildRelativeUrl($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>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<?php /** @var array $emojiList */ ?>
|
||||
<h1>Emoji Management</h1>
|
||||
<main>
|
||||
<form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
|
||||
<form action="<?= Util::buildRelativeUrl($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="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
|
||||
<form action="<?= Util::buildRelativeUrl($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>
|
||||
|
@ -14,7 +14,7 @@
|
||||
<?php if (Session::isLoggedIn()): ?>
|
||||
<a
|
||||
<?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||
href="<?= Util::escape_html($config->basePath) ?>mood"
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'mood')) ?>"
|
||||
class="change-mood">Change mood</a>
|
||||
<?php endif ?>
|
||||
</dd>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<?php /** @var string $csrf_token */ ?>
|
||||
<?php /** @var string $error */ ?>
|
||||
<h2>Login</h2>
|
||||
<form method="post" action="<?= $config->basePath ?>login">
|
||||
<form method="post" action="<?= Util::buildRelativeUrl($config->basePath, 'login') ?>">
|
||||
<div class="fieldset-items">
|
||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($csrf_token) ?>">
|
||||
<label for="username">Username:</label>
|
||||
|
@ -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($config->basePath) ?>">home</a>
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($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($config->basePath) ?>feed/rss">rss</a>
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'feed/rss')) ?>">rss</a>
|
||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||
href="<?= Util::escape_html($config->basePath) ?>feed/atom">atom</a>
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'feed/atom')) ?>">atom</a>
|
||||
</div>
|
||||
</details>
|
||||
<?php if (!Session::isLoggedIn()): ?>
|
||||
<a tabindex="0"
|
||||
href="<?= Util::escape_html($config->basePath) ?>login">login</a>
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($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($config->basePath) ?>admin">settings</a>
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin')) ?>">settings</a>
|
||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||
href="<?= Util::escape_html($config->basePath) ?>admin/css">css</a>
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin/css')) ?>">css</a>
|
||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||
href="<?= Util::escape_html($config->basePath) ?>admin/emoji">emoji</a>
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin/emoji')) ?>">emoji</a>
|
||||
</div>
|
||||
</details>
|
||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||
href="<?= Util::escape_html($config->basePath) ?>logout">logout</a>
|
||||
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'logout')) ?>">logout</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
@ -26,4 +26,51 @@ 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);
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user