Clean up escaping, linking, and feeds.

This commit is contained in:
Greg Sarjeant 2025-06-16 19:36:36 -04:00
parent 856677659e
commit b59526c590
6 changed files with 67 additions and 44 deletions

View File

@ -1,20 +1,30 @@
<?php <?php
class Util { class Util {
public static function escape_and_linkify(string $text, int $flags = ENT_NOQUOTES | ENT_HTML5, bool $new_window = true ): string { public static function escape_html(string $text): string {
// escape dangerous characters, but preserve quotes return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
$safe = htmlspecialchars($text, $flags, 'UTF-8'); }
public static function escape_xml(string $text): string {
return htmlspecialchars($text, ENT_QUOTES | ENT_XML1, 'UTF-8');
}
// Convert URLs in text to links (anchor tags)
// NOTE: This function expects pre-escaped text.
// It will unescape URLs if there are any.
public static function linkify(string $text, bool $new_window = true): string {
$link_attrs = $new_window ? ' target="_blank" rel="noopener noreferrer"' : ''; $link_attrs = $new_window ? ' target="_blank" rel="noopener noreferrer"' : '';
// convert URLs to links return preg_replace_callback(
$safe = preg_replace_callback(
'~(https?://[^\s<>"\'()]+)~i', '~(https?://[^\s<>"\'()]+)~i',
fn($matches) => '<a href="' . htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8') . '"' . $link_attrs . '>' . $matches[1] . '</a>', function($matches) use ($link_attrs) {
$safe $escaped_url = rtrim($matches[1], '.,!?;:)]}>');
); $clean_url = html_entity_decode($escaped_url, ENT_QUOTES, 'UTF-8');
return $safe; return '<a href="' . $clean_url . '"' . $link_attrs . '>' . $escaped_url . '</a>';
} },
$text
);
}
// For relative time display, compare the stored time to the current time // For relative time display, compare the stored time to the current time
// and display it as "X seconds/minutes/hours/days etc." ago // and display it as "X seconds/minutes/hours/days etc." ago

View File

@ -8,8 +8,8 @@ class HomeView {
<div class="tick-feed"> <div class="tick-feed">
<?php foreach ($ticks as $tick): ?> <?php foreach ($ticks as $tick): ?>
<article class="tick"> <article class="tick">
<div class="tick-time"><?= htmlspecialchars(Util::relative_time($tick['timestamp'])) ?></div> <div class="tick-time"><?= Util::escape_html(Util::relative_time($tick['timestamp'])) ?></div>
<span class="tick-text"><?= Util::escape_and_linkify($tick['tick']) ?></span> <span class="tick-text"><?= Util::linkify(Util::escape_html($tick['tick'])) ?></span>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

@ -1,45 +1,49 @@
<?php /** @var ConfigModel $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var array $ticks */ ?> <?php /** @var array $ticks */ ?>
<?php <?php
$siteTitle = htmlspecialchars($config->siteTitle); $feedTitle = Util::escape_xml("$config->siteTitle Atom Feed");
$siteUrl = htmlspecialchars($config->baseUrl); $siteUrl = Util::escape_xml($config->baseUrl . $config->basePath);
$basePath = htmlspecialchars($config->basePath); $feedUrl = Util::escape_xml($config->baseUrl . $config->basePath . 'feed/atom');
$updated = date(DATE_ATOM, strtotime($ticks[0]['timestamp'] ?? 'now')); $updated = date(DATE_ATOM, strtotime($ticks[0]['timestamp'] ?? 'now'));
header('Content-Type: application/atom+xml; charset=utf-8'); header('Content-Type: application/atom+xml; charset=utf-8');
echo '<?xml version="1.0" encoding="utf-8"?>' . "\n"; echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
?> ?>
<feed xmlns="http://www.w3.org/2005/Atom"> <feed xmlns="http://www.w3.org/2005/Atom">
<title><?= "$siteTitle Atom Feed" ?></title> <title><?php echo $feedTitle ?></title>
<link rel="self" <link rel="self"
type="application/atom+xml" type="application/atom+xml"
title="<?php echo htmlspecialchars($config->siteTitle) ?> Atom Feed" title="<?php echo $feedTitle ?>"
href="<?php echo htmlspecialchars($siteUrl . $basePath) ?>feed/atom" /> href="<?php echo $feedUrl ?>" />
<link rel="alternate" href="<?= $siteUrl ?>"/> <link rel="alternate" href="<?php echo $siteUrl ?>"/>
<updated><?= $updated ?></updated> <updated><?php echo $updated ?></updated>
<id><?= $siteUrl . $basePath ?></id> <id><?php echo $siteUrl ?></id>
<author> <author>
<name><?= $siteTitle ?></name> <name><?= $siteTitle ?></name>
</author> </author>
<?php foreach ($ticks as $tick): <?php foreach ($ticks as $tick):
// decompose the tick timestamp into the date/time parts
[$date, $time] = explode(' ', $tick['timestamp']); [$date, $time] = explode(' ', $tick['timestamp']);
$dateParts = explode('-', $date);
$timeParts = explode(':', $time);
$dateParts = explode('-', $date);
[$year, $month, $day] = $dateParts; [$year, $month, $day] = $dateParts;
$timeParts = explode(':', $time);
[$hour, $minute, $second] = $timeParts; [$hour, $minute, $second] = $timeParts;
$tickPath = "$year/$month/$day/$hour/$minute/$second"; // build the tick entry components
$tickUrl = htmlspecialchars($siteUrl . $basePath . "tick/$tickPath"); $tickPath = "tick/$year/$month/$day/$hour/$minute/$second";
$tickUrl = Util::escape_xml($siteUrl . $basePath . $tickPath);
$tickTime = date(DATE_ATOM, strtotime($tick['timestamp'])); $tickTime = date(DATE_ATOM, strtotime($tick['timestamp']));
$tickText = htmlspecialchars($tick['tick']); $tickTitle = Util::escape_xml($tick['tick']);
$tickContent = Util::linkify($tickTitle);
?> ?>
<entry> <entry>
<title><?= $tickText ?></title> <title><?= $tickTitle ?></title>
<link href="<?= $tickUrl ?>"/> <link href="<?= $tickUrl ?>"/>
<id><?= $tickUrl ?></id> <id><?= $tickUrl ?></id>
<updated><?= $tickTime ?></updated> <updated><?= $tickTime ?></updated>
<content type="html"><?= $tickText ?></content> <content type="html"><?= $tickContent ?></content>
</entry> </entry>
<?php endforeach; ?> <?php endforeach; ?>
</feed> </feed>

View File

@ -4,34 +4,43 @@
// Need to have a little php here because the starting xml tag // Need to have a little php here because the starting xml tag
// will mess up the PHP parser. // will mess up the PHP parser.
// TODO - I think short php tags can be disabled to prevent that. // TODO - I think short php tags can be disabled to prevent that.
header('Content-Type: application/rss+xml; charset=utf-8'); header('Content-Type: application/rss+xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
?> ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel> <channel>
<title><?php echo htmlspecialchars($config->siteTitle, ENT_XML1, 'UTF-8') ?> RSS Feed</title> <title><?php echo Util::escape_xml($config->siteTitle . 'RSS Feed') ?></title>
<link><?php echo htmlspecialchars($config->baseUrl . $config->basePath, ENT_XML1, 'UTF-8')?></link> <link><?php echo Util::escape_xml($config->baseUrl . $config->basePath)?></link>
<atom:link href="<?php echo htmlspecialchars($config->baseUrl . $config->basePath, ENT_XML1, 'UTF-8')?>feed/rss" rel="self" type="application/rss+xml" /> <atom:link href="<?php echo Util::escape_xml($config->baseUrl . $config->basePath. 'feed/rss')?>"
<description><?php echo htmlspecialchars($config->siteDescription, ENT_XML1, 'UTF-8') ?></description> rel="self"
type="application/rss+xml" />
<description><?php echo Util::escape_xml($config->siteDescription) ?></description>
<language>en-us</language> <language>en-us</language>
<lastBuildDate><?php echo date(DATE_RSS); ?></lastBuildDate> <lastBuildDate><?php echo date(DATE_RSS); ?></lastBuildDate>
<?php foreach ($ticks as $tick): <?php foreach ($ticks as $tick):
// decompose the tick timestamp into the date/time parts
[$date, $time] = explode(' ', $tick['timestamp']); [$date, $time] = explode(' ', $tick['timestamp']);
$dateParts = explode('-', $date);
$timeParts = explode(':', $time);
$dateParts = explode('-', $date);
[$year, $month, $day] = $dateParts; [$year, $month, $day] = $dateParts;
$timeParts = explode(':', $time);
[$hour, $minute, $second] = $timeParts; [$hour, $minute, $second] = $timeParts;
$tickPath = "$year/$month/$day/$hour/$minute/$second"; // build the tick entry components
$tickUrl = $config->baseUrl . $config->basePath . $tickPath; $tickPath = "tick/$year/$month/$day/$hour/$minute/$second";
$tickUrl = Util::escape_xml($config->baseUrl . $config->basePath . $tickPath);
$tickDate = date(DATE_RSS, strtotime($tick['timestamp']));
$tickTitle = Util::escape_xml($tick['tick']);
$tickDescription = Util::linkify($tickTitle);
?> ?>
<item> <item>
<title><?php echo htmlspecialchars($tick['tick'], ENT_XML1, 'UTF-8'); ?></title> <title><?php echo $tickTitle ?></title>
<link><?php echo htmlspecialchars($config->baseUrl . $config->basePath . "tick/$tickPath", ENT_XML1, 'UTF-8'); ?></link> <link><?php echo $tickUrl; ?></link>
<description><?php echo Util::escape_and_linkify($tick['tick'], ENT_XML1, false); ?></description> <description><?php echo $tickDescription; ?></description>
<pubDate><?php echo date(DATE_RSS, strtotime($tick['timestamp'])); ?></pubDate> <pubDate><?php echo $tickDate; ?></pubDate>
<guid><?php echo htmlspecialchars($tickUrl, ENT_XML1, 'UTF-8'); ?></guid> <guid><?php echo $tickUrl; ?></guid>
</item> </item>
<?php endforeach; ?> <?php endforeach; ?>
</channel> </channel>

View File

@ -11,7 +11,7 @@
<p>About: <?= $user->about ?></p> <p>About: <?= $user->about ?></p>
<?php endif ?> <?php endif ?>
<?php if (!empty($user->website)): ?> <?php if (!empty($user->website)): ?>
<p>Website: <?= Util::escape_and_linkify($user->website) ?></p> <p>Website: <?= Util::linkify(Util::escape_html($user->website)) ?></p>
<?php endif ?> <?php endif ?>
<?php if (!empty($user->mood) || Session::isLoggedIn()): ?> <?php if (!empty($user->mood) || Session::isLoggedIn()): ?>
<div class="profile-row"> <div class="profile-row">
@ -27,7 +27,7 @@
<hr/> <hr/>
<div class="profile-row"> <div class="profile-row">
<form class="tick-form" method="post"> <form class="tick-form" method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>"> <input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<textarea name="tick" placeholder="What's ticking?" rows="3"></textarea> <textarea name="tick" placeholder="What's ticking?" rows="3"></textarea>
<button type="submit" class="submit-btn">Tick</button> <button type="submit" class="submit-btn">Tick</button>
</form> </form>

View File

@ -2,4 +2,4 @@
<?php /** @var Date $tickTime */ ?> <?php /** @var Date $tickTime */ ?>
<?php /** @var string $tick */ ?> <?php /** @var string $tick */ ?>
<h1>Tick from <?= $tickTime->format('Y-m-d H:i:s'); ?></h1> <h1>Tick from <?= $tickTime->format('Y-m-d H:i:s'); ?></h1>
<p><?= Util::escape_and_linkify($tick) ?></p> <p><?= Util::linkify(Util::escape_html($tick)) ?></p>