diff --git a/public/css/default.css b/public/css/default.css
index 738995b..b04aaf5 100644
--- a/public/css/default.css
+++ b/public/css/default.css
@@ -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;
diff --git a/public/index.php b/public/index.php
index f8dd5e8..c20e3c2 100644
--- a/public/index.php
+++ b/public/index.php
@@ -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;
}
diff --git a/src/Controller/HomeController/HomeController.php b/src/Controller/HomeController/HomeController.php
index 0736df4..d6efb0e 100644
--- a/src/Controller/HomeController/HomeController.php
+++ b/src/Controller/HomeController/HomeController.php
@@ -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 {
diff --git a/src/Controller/TickController/TickController.php b/src/Controller/TickController/TickController.php
index f33aa24..6474b30 100644
--- a/src/Controller/TickController/TickController.php
+++ b/src/Controller/TickController/TickController.php
@@ -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 '
404 - Tick Not Found
';
+ $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();
diff --git a/src/Feed/AtomGenerator.php b/src/Feed/AtomGenerator.php
index f3f0fa5..71723da 100644
--- a/src/Feed/AtomGenerator.php
+++ b/src/Feed/AtomGenerator.php
@@ -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'])));
?>
= $tickTitle ?>
diff --git a/src/Feed/RssGenerator.php b/src/Feed/RssGenerator.php
index c8597b6..4fb7e8d 100644
--- a/src/Feed/RssGenerator.php
+++ b/src/Feed/RssGenerator.php
@@ -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}");
?>
-
+
diff --git a/src/Model/TickModel/TickModel.php b/src/Model/TickModel/TickModel.php
index 74bab9f..51286f9 100644
--- a/src/Model/TickModel/TickModel.php
+++ b/src/Model/TickModel/TickModel.php
@@ -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;
}
diff --git a/templates/partials/tick-404.php b/templates/partials/tick-404.php
new file mode 100644
index 0000000..8769c48
--- /dev/null
+++ b/templates/partials/tick-404.php
@@ -0,0 +1,4 @@
+
+
Tick Not Found
+
The tick you're looking for has been deleted or never existed.
+
\ No newline at end of file
diff --git a/templates/partials/tick.php b/templates/partials/tick.php
index 649151a..dee4037 100644
--- a/templates/partials/tick.php
+++ b/templates/partials/tick.php
@@ -1,4 +1,14 @@
-
Tick from = $tickTime; ?>
-
= Util::linkify(Util::escape_html($tick)) ?>
+
+
+
+
+
Tick
+
Posted on UTC
+
+
+ = Util::linkify(Util::escape_html($tick)) ?>
+
+
+
diff --git a/tests/Controller/TickController/TickControllerTest.php b/tests/Controller/TickController/TickControllerTest.php
index 0401d93..b9d69cd 100644
--- a/tests/Controller/TickController/TickControllerTest.php
+++ b/tests/Controller/TickController/TickControllerTest.php
@@ -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();