Fix feed links, clean up single tick pages (#69)

Fix feed links. Clean up single-tick page.

Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/69
Co-authored-by: Greg Sarjeant <greg@subcultureofone.org>
Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
This commit is contained in:
Greg Sarjeant 2025-08-14 01:10:35 +00:00 committed by greg
parent d3a537aa6c
commit eeb73eccd4
10 changed files with 58 additions and 34 deletions

View File

@ -370,6 +370,11 @@ time {
font-size: 1.0em;
display: block;
}
.tick-meta {
color: var(--color-log-muted);
font-size: 0.9em;
margin-bottom: 0.4em;
}
.tick-pagination a {
margin: 0 5px;

View File

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

View File

@ -6,8 +6,8 @@ class HomeController extends Controller {
// renders the homepage view.
public function index(){
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$data = $this->getHomeData($page);
$this->render("home.php", $data);
$vars = $this->getHomeData($page);
$this->render("home.php", $vars);
}
public function getHomeData(int $page): array {

View File

@ -2,22 +2,27 @@
declare(strict_types=1);
class TickController extends Controller{
public function index(int $id){
public function index(string $id){
// This is because my router is too simplistic to cleanly handle type casting,
// so I just accept a sting here and cast it to an int immediately.
$id = (int) $id;
global $app;
$vars = ['settings' => $app['settings']];
Log::debug("Fetching tick with ID: {$id}");
try {
$tickModel = new TickModel($app['db'], $app['settings']);
$vars = $tickModel->get($id);
$tick = $tickModel->get($id);
if (empty($vars) || !isset($vars['tick'])) {
if (empty($tick) || !isset($tick['tick'])) {
Log::warning("Tick not found for ID: {$id}");
http_response_code(404);
echo '<h1>404 - Tick Not Found</h1>';
$this->render('tick-404.php', $vars);
return;
}
$vars = array_merge($tick, $vars);
Log::info("Successfully loaded tick {$id}: " . substr($vars['tick'], 0, 50) . (strlen($vars['tick']) > 50 ? '...' : ''));
$this->render('tick.php', $vars);
@ -30,30 +35,30 @@ class TickController extends Controller{
public function handleDelete(string $id){
global $app;
$id = (int) $id;
Log::debug("Attempting to delete tick with ID: {$id}");
try {
$tickModel = new TickModel($app['db'], $app['settings']);
// TickModel->delete() handles validation and sets flash messages:
// - "Tick not found" if tick doesn't exist
// - "Tick is too old to delete" if outside deletion window
// - "Tick is too old to delete" if outside deletion window
// - "Deleted: '{content}'" on success
$success = $tickModel->delete($id);
if ($success) {
Log::info("Successfully deleted tick {$id}");
} else {
Log::warning("Failed to delete tick {$id}");
}
} catch (Exception $e) {
Log::error("Exception while deleting tick {$id}: " . $e->getMessage());
Session::setFlashMessage('error', 'An error occurred while deleting the tick');
}
// Redirect back to homepage
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
exit();

View File

@ -41,7 +41,7 @@ class AtomGenerator extends FeedGenerator {
$tickUrl = Util::escape_xml($siteUrl . $tickPath);
$tickTime = date(DATE_ATOM, strtotime($tick['timestamp']));
$tickTitle = Util::escape_xml($tick['tick']);
$tickContent = Util::linkify($tickTitle);
$tickContent = Util::escape_xml(Util::linkify(Util::escape_html($tick['tick'])));
?>
<entry>
<title><?= $tickTitle ?></title>

View File

@ -35,10 +35,11 @@ class RssGenerator extends FeedGenerator {
$tickUrl = Util::escape_xml($this->buildTickUrl($tick['id']));
$tickDate = date(DATE_RSS, strtotime($tick['timestamp']));
$tickTitle = Util::escape_xml($tick['tick']);
$tickDescription = Util::linkify($tickTitle);
$tickDescription = Util::escape_xml(Util::linkify(Util::escape_html($tick['tick'])));
Log::debug("RSS item: {$tickDescription}");
?>
<item>
<title><?php echo $tickTitle ?></title>
<title><?php echo $tickTitle; ?></title>
<link><?php echo $tickUrl; ?></link>
<description><?php echo $tickDescription; ?></description>
<pubDate><?php echo $tickDate; ?></pubDate>

View File

@ -7,14 +7,14 @@ class TickModel {
public function getPage(int $limit, int $offset = 0): array {
$stmt = $this->db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?");
$stmt->execute([$limit, $offset]);
$ticks = $stmt->fetchAll(PDO::FETCH_ASSOC);
return array_map(function($tick) {
$tickTime = new DateTimeImmutable($tick['timestamp'], new DateTimeZone('UTC'));
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
$tick['can_delete'] = $hoursSinceCreation <= $this->settings->tickDeleteHours;
return $tick;
}, $ticks);
@ -41,7 +41,6 @@ class TickModel {
return [
'tickTime' => $row['timestamp'],
'tick' => $row['tick'],
'settings' => $this->settings,
];
}
@ -50,26 +49,26 @@ class TickModel {
$stmt = $this->db->prepare("SELECT tick, timestamp FROM tick WHERE id=?");
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row === false || empty($row)) {
Session::setFlashMessage('error', 'Tick not found');
return false;
}
// Check deletion window
$tickTime = new DateTimeImmutable($row['timestamp'], new DateTimeZone('UTC'));
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
if ($hoursSinceCreation > $this->settings->tickDeleteHours) {
Session::setFlashMessage('error', 'Tick is too old to delete');
return false;
}
// Delete and set success message
$stmt = $this->db->prepare("DELETE FROM tick WHERE id=?");
$stmt->execute([$id]);
Session::setFlashMessage('success', "Deleted: '{$row['tick']}'");
return true;
}

View File

@ -0,0 +1,4 @@
<div class="not-found-container">
<h1>Tick Not Found</h1>
<p>The tick you're looking for has been deleted or never existed.</p>
</div>

View File

@ -1,4 +1,14 @@
<?php /** @var Date $tickTime */ ?>
<?php /** @var string $tick */ ?>
<h1>Tick from <?= $tickTime; ?></h1>
<p><?= Util::linkify(Util::escape_html($tick)) ?></p>
<?php $displayTime = DateTimeImmutable::createFromformat('Y-m-d H:i:s', $tickTime) ?>
<div class="tick-container">
<article class="tick">
<header class="tick-header">
<h1>Tick</h1>
<p class="tick-meta">Posted on <time class="tick-meta" datetime="<?= $displayTime->format('c') ?>"><?= $displayTime->format('F j, Y \a\t g:i A') ?></time> UTC</p>
</header>
<div class="tick-text">
<?= Util::linkify(Util::escape_html($tick)) ?>
</div>
</article>
</div>

View File

@ -62,7 +62,7 @@ class TickControllerTest extends TestCase
ob_start();
$controller = new TickController();
$controller->index(123);
$controller->index("123");
$output = ob_get_clean();
@ -96,12 +96,12 @@ class TickControllerTest extends TestCase
ob_start();
$controller = new TickController();
$controller->index(999);
$controller->index("999");
$output = ob_get_clean();
// Should return 404 error
$this->assertStringContainsString('404 - Tick Not Found', $output);
$this->assertStringContainsString('Tick Not Found', $output);
}
public function testIndexWithEmptyTickData(): void
@ -125,12 +125,12 @@ class TickControllerTest extends TestCase
ob_start();
$controller = new TickController();
$controller->index(456);
$controller->index("456");
$output = ob_get_clean();
// Should return 404 error for empty data
$this->assertStringContainsString('404 - Tick Not Found', $output);
$this->assertStringContainsString('Tick Not Found', $output);
}
public function testIndexWithDatabaseException(): void
@ -145,7 +145,7 @@ class TickControllerTest extends TestCase
ob_start();
$controller = new TickController();
$controller->index(123);
$controller->index("123");
$output = ob_get_clean();