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:
parent
d3a537aa6c
commit
eeb73eccd4
@ -370,6 +370,11 @@ time {
|
|||||||
font-size: 1.0em;
|
font-size: 1.0em;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.tick-meta {
|
||||||
|
color: var(--color-log-muted);
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
.tick-pagination a {
|
.tick-pagination a {
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
|
@ -118,7 +118,7 @@ if ($method === 'POST' && $path != 'tkr-setup') {
|
|||||||
if ($path != 'login'){
|
if ($path != 'login'){
|
||||||
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::warning('Attempt to POST with invalid session. Redirecting to login.');
|
||||||
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, 'login'));
|
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, 'login'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ class HomeController extends Controller {
|
|||||||
// renders the homepage view.
|
// renders the homepage view.
|
||||||
public function index(){
|
public function index(){
|
||||||
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
||||||
$data = $this->getHomeData($page);
|
$vars = $this->getHomeData($page);
|
||||||
$this->render("home.php", $data);
|
$this->render("home.php", $vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHomeData(int $page): array {
|
public function getHomeData(int $page): array {
|
||||||
|
@ -2,22 +2,27 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
class TickController extends Controller{
|
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;
|
global $app;
|
||||||
|
$vars = ['settings' => $app['settings']];
|
||||||
|
|
||||||
Log::debug("Fetching tick with ID: {$id}");
|
Log::debug("Fetching tick with ID: {$id}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$tickModel = new TickModel($app['db'], $app['settings']);
|
$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}");
|
Log::warning("Tick not found for ID: {$id}");
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo '<h1>404 - Tick Not Found</h1>';
|
$this->render('tick-404.php', $vars);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$vars = array_merge($tick, $vars);
|
||||||
Log::info("Successfully loaded tick {$id}: " . substr($vars['tick'], 0, 50) . (strlen($vars['tick']) > 50 ? '...' : ''));
|
Log::info("Successfully loaded tick {$id}: " . substr($vars['tick'], 0, 50) . (strlen($vars['tick']) > 50 ? '...' : ''));
|
||||||
$this->render('tick.php', $vars);
|
$this->render('tick.php', $vars);
|
||||||
|
|
||||||
@ -30,30 +35,30 @@ class TickController extends Controller{
|
|||||||
|
|
||||||
public function handleDelete(string $id){
|
public function handleDelete(string $id){
|
||||||
global $app;
|
global $app;
|
||||||
|
|
||||||
$id = (int) $id;
|
$id = (int) $id;
|
||||||
Log::debug("Attempting to delete tick with ID: {$id}");
|
Log::debug("Attempting to delete tick with ID: {$id}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$tickModel = new TickModel($app['db'], $app['settings']);
|
$tickModel = new TickModel($app['db'], $app['settings']);
|
||||||
|
|
||||||
// TickModel->delete() handles validation and sets flash messages:
|
// TickModel->delete() handles validation and sets flash messages:
|
||||||
// - "Tick not found" if tick doesn't exist
|
// - "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
|
// - "Deleted: '{content}'" on success
|
||||||
$success = $tickModel->delete($id);
|
$success = $tickModel->delete($id);
|
||||||
|
|
||||||
if ($success) {
|
if ($success) {
|
||||||
Log::info("Successfully deleted tick {$id}");
|
Log::info("Successfully deleted tick {$id}");
|
||||||
} else {
|
} else {
|
||||||
Log::warning("Failed to delete tick {$id}");
|
Log::warning("Failed to delete tick {$id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Exception while deleting tick {$id}: " . $e->getMessage());
|
Log::error("Exception while deleting tick {$id}: " . $e->getMessage());
|
||||||
Session::setFlashMessage('error', 'An error occurred while deleting the tick');
|
Session::setFlashMessage('error', 'An error occurred while deleting the tick');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to homepage
|
// Redirect back to homepage
|
||||||
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
|
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
|
||||||
exit();
|
exit();
|
||||||
|
@ -41,7 +41,7 @@ class AtomGenerator extends FeedGenerator {
|
|||||||
$tickUrl = Util::escape_xml($siteUrl . $tickPath);
|
$tickUrl = Util::escape_xml($siteUrl . $tickPath);
|
||||||
$tickTime = date(DATE_ATOM, strtotime($tick['timestamp']));
|
$tickTime = date(DATE_ATOM, strtotime($tick['timestamp']));
|
||||||
$tickTitle = Util::escape_xml($tick['tick']);
|
$tickTitle = Util::escape_xml($tick['tick']);
|
||||||
$tickContent = Util::linkify($tickTitle);
|
$tickContent = Util::escape_xml(Util::linkify(Util::escape_html($tick['tick'])));
|
||||||
?>
|
?>
|
||||||
<entry>
|
<entry>
|
||||||
<title><?= $tickTitle ?></title>
|
<title><?= $tickTitle ?></title>
|
||||||
|
@ -35,10 +35,11 @@ class RssGenerator extends FeedGenerator {
|
|||||||
$tickUrl = Util::escape_xml($this->buildTickUrl($tick['id']));
|
$tickUrl = Util::escape_xml($this->buildTickUrl($tick['id']));
|
||||||
$tickDate = date(DATE_RSS, strtotime($tick['timestamp']));
|
$tickDate = date(DATE_RSS, strtotime($tick['timestamp']));
|
||||||
$tickTitle = Util::escape_xml($tick['tick']);
|
$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>
|
<item>
|
||||||
<title><?php echo $tickTitle ?></title>
|
<title><?php echo $tickTitle; ?></title>
|
||||||
<link><?php echo $tickUrl; ?></link>
|
<link><?php echo $tickUrl; ?></link>
|
||||||
<description><?php echo $tickDescription; ?></description>
|
<description><?php echo $tickDescription; ?></description>
|
||||||
<pubDate><?php echo $tickDate; ?></pubDate>
|
<pubDate><?php echo $tickDate; ?></pubDate>
|
||||||
|
@ -7,14 +7,14 @@ class TickModel {
|
|||||||
public function getPage(int $limit, int $offset = 0): array {
|
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 = $this->db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?");
|
||||||
$stmt->execute([$limit, $offset]);
|
$stmt->execute([$limit, $offset]);
|
||||||
|
|
||||||
$ticks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$ticks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
return array_map(function($tick) {
|
return array_map(function($tick) {
|
||||||
$tickTime = new DateTimeImmutable($tick['timestamp'], new DateTimeZone('UTC'));
|
$tickTime = new DateTimeImmutable($tick['timestamp'], new DateTimeZone('UTC'));
|
||||||
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
||||||
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
|
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
|
||||||
|
|
||||||
$tick['can_delete'] = $hoursSinceCreation <= $this->settings->tickDeleteHours;
|
$tick['can_delete'] = $hoursSinceCreation <= $this->settings->tickDeleteHours;
|
||||||
return $tick;
|
return $tick;
|
||||||
}, $ticks);
|
}, $ticks);
|
||||||
@ -41,7 +41,6 @@ class TickModel {
|
|||||||
return [
|
return [
|
||||||
'tickTime' => $row['timestamp'],
|
'tickTime' => $row['timestamp'],
|
||||||
'tick' => $row['tick'],
|
'tick' => $row['tick'],
|
||||||
'settings' => $this->settings,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,26 +49,26 @@ class TickModel {
|
|||||||
$stmt = $this->db->prepare("SELECT tick, timestamp FROM tick WHERE id=?");
|
$stmt = $this->db->prepare("SELECT tick, timestamp FROM tick WHERE id=?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($row === false || empty($row)) {
|
if ($row === false || empty($row)) {
|
||||||
Session::setFlashMessage('error', 'Tick not found');
|
Session::setFlashMessage('error', 'Tick not found');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check deletion window
|
// Check deletion window
|
||||||
$tickTime = new DateTimeImmutable($row['timestamp'], new DateTimeZone('UTC'));
|
$tickTime = new DateTimeImmutable($row['timestamp'], new DateTimeZone('UTC'));
|
||||||
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
||||||
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
|
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
|
||||||
|
|
||||||
if ($hoursSinceCreation > $this->settings->tickDeleteHours) {
|
if ($hoursSinceCreation > $this->settings->tickDeleteHours) {
|
||||||
Session::setFlashMessage('error', 'Tick is too old to delete');
|
Session::setFlashMessage('error', 'Tick is too old to delete');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete and set success message
|
// Delete and set success message
|
||||||
$stmt = $this->db->prepare("DELETE FROM tick WHERE id=?");
|
$stmt = $this->db->prepare("DELETE FROM tick WHERE id=?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
Session::setFlashMessage('success', "Deleted: '{$row['tick']}'");
|
Session::setFlashMessage('success', "Deleted: '{$row['tick']}'");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
4
templates/partials/tick-404.php
Normal file
4
templates/partials/tick-404.php
Normal 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>
|
@ -1,4 +1,14 @@
|
|||||||
<?php /** @var Date $tickTime */ ?>
|
<?php /** @var Date $tickTime */ ?>
|
||||||
<?php /** @var string $tick */ ?>
|
<?php /** @var string $tick */ ?>
|
||||||
<h1>Tick from <?= $tickTime; ?></h1>
|
<?php $displayTime = DateTimeImmutable::createFromformat('Y-m-d H:i:s', $tickTime) ?>
|
||||||
<p><?= Util::linkify(Util::escape_html($tick)) ?></p>
|
<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>
|
||||||
|
@ -62,7 +62,7 @@ class TickControllerTest extends TestCase
|
|||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$controller = new TickController();
|
$controller = new TickController();
|
||||||
$controller->index(123);
|
$controller->index("123");
|
||||||
|
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
@ -96,12 +96,12 @@ class TickControllerTest extends TestCase
|
|||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$controller = new TickController();
|
$controller = new TickController();
|
||||||
$controller->index(999);
|
$controller->index("999");
|
||||||
|
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
// Should return 404 error
|
// Should return 404 error
|
||||||
$this->assertStringContainsString('404 - Tick Not Found', $output);
|
$this->assertStringContainsString('Tick Not Found', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexWithEmptyTickData(): void
|
public function testIndexWithEmptyTickData(): void
|
||||||
@ -125,12 +125,12 @@ class TickControllerTest extends TestCase
|
|||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$controller = new TickController();
|
$controller = new TickController();
|
||||||
$controller->index(456);
|
$controller->index("456");
|
||||||
|
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
// Should return 404 error for empty data
|
// Should return 404 error for empty data
|
||||||
$this->assertStringContainsString('404 - Tick Not Found', $output);
|
$this->assertStringContainsString('Tick Not Found', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexWithDatabaseException(): void
|
public function testIndexWithDatabaseException(): void
|
||||||
@ -145,7 +145,7 @@ class TickControllerTest extends TestCase
|
|||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$controller = new TickController();
|
$controller = new TickController();
|
||||||
$controller->index(123);
|
$controller->index("123");
|
||||||
|
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user