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
class Util {
public static function escape_and_linkify(string $text, int $flags = ENT_NOQUOTES | ENT_HTML5, bool $new_window = true ): string {
// escape dangerous characters, but preserve quotes
$safe = htmlspecialchars($text, $flags, 'UTF-8');
public static function escape_html(string $text): string {
return htmlspecialchars($text, ENT_QUOTES, '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"' : '';
// convert URLs to links
$safe = preg_replace_callback(
return preg_replace_callback(
'~(https?://[^\s<>"\'()]+)~i',
fn($matches) => '<a href="' . htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8') . '"' . $link_attrs . '>' . $matches[1] . '</a>',
$safe
);
function($matches) use ($link_attrs) {
$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
// and display it as "X seconds/minutes/hours/days etc." ago

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@
<p>About: <?= $user->about ?></p>
<?php endif ?>
<?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 if (!empty($user->mood) || Session::isLoggedIn()): ?>
<div class="profile-row">
@ -27,7 +27,7 @@
<hr/>
<div class="profile-row">
<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>
<button type="submit" class="submit-btn">Tick</button>
</form>

View File

@ -2,4 +2,4 @@
<?php /** @var Date $tickTime */ ?>
<?php /** @var string $tick */ ?>
<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>