cleanup. Improve homepage semantics. Add .htaccess files to blocked directories.

This commit is contained in:
Greg Sarjeant 2025-06-17 16:25:31 -04:00
parent 77ec1bbb3b
commit f72896892b
18 changed files with 240 additions and 82 deletions

49
.htaccess Normal file
View File

@ -0,0 +1,49 @@
# Example Apache VirtualHost
# for serving tkr as a subdirectory path
# on shared hosting via .htaccess
#
# e.g. http://www.my-domain.com/tkr
#
# This should work without modification if you extract the app
# to /tkr from your web document root
# Enable mod_rewrite
RewriteEngine On
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set X-Content-Type-Options "nosniff"
# Directory index
DirectoryIndex public/index.php
# Security: Block direct access to .php files (except through rewrites)
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
RewriteRule ^.*$ - [R=404,L]
# Security: Block access to sensitive directories
RewriteRule ^(storage|src|templates|examples|config)(/.*)?$ - [F,L]
# Security: Block access to hidden files
RewriteRule ^\..*$ - [F,L]
# Cache CSS files for 1 hour
<FilesMatch "\.css$">
Header set Cache-Control "public, max-age=3600"
</FilesMatch>
# Serve the one static file that exists: css/tkr.css
# (Pass requests to css/custom/ through to the PHP app)
RewriteCond %{REQUEST_URI} !^/css/custom/
RewriteRule ^css/tkr\.css$ public/css/tkr.css [L]
# 404 all other static files (images, js, fonts, etc.)
# so those requests don't hit the PHP app
# (this is to reduce load on the PHP app from bots and scanners)
RewriteRule \.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$ - [R=404,L]
# Everything else goes to the front controller
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ public/index.php [L]

6
config/.htaccess Normal file
View File

@ -0,0 +1,6 @@
# Deny all access to this directory
Require all denied
# Fallback for Apache 2.2
Order deny,allow
Deny from all

View File

@ -41,7 +41,7 @@ function handle_setup_exception(SetupException $e){
// Show error message and exit
http_response_code(500);
echo "<h1>Configuration Error</h1>";
echo "<p>" . htmlspecialchars($setupError['message']) . "</p>";
echo "<p>" . Util::escape_html($setupError['message']) . "</p>";
exit;
case 'table_contents':
// Recoverable error.

6
examples/.htaccess Normal file
View File

@ -0,0 +1,6 @@
# Deny all access to this directory
Require all denied
# Fallback for Apache 2.2
Order deny,allow
Deny from all

View File

@ -95,7 +95,7 @@ fieldset.emoji-group {
}
h1.site-description {
font-size: 1.3em;
font-size: 1.5em;
}
.delete-emoji-fieldset .fieldset-items {
@ -310,19 +310,85 @@ label.description {
}
.home-sidebar{
padding-top: 1em;
padding-bottom: 1em;
}
.site-description {
font-size: 1.2rem;
color: var(--color-text-dark);
margin-bottom: 0.5rem;
margin-bottom: 1.2rem;
}
.profile-row {
.profile-data {
display: grid;
gap: 1rem;
margin: 0;
margin-bottom: 1rem;
}
/* Description list: description */
.profile-data dd {
margin: 0;
}
/* Description list: term */
/* Hidden from visual display - screen reader only class */
.profile-data dt {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/*
Left-justify the greeting text,
right-justify the Change Mood link
*/
/* greeting text */
.profile-greeting {
display: flex;
width: 100%;
gap: 0.5em;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
/* add a small gap between the greeting and the mood emoji */
.greeting-content {
display: flex;
align-items: baseline;
gap: 0.4em;
}
/* define the profile "greeting" style */
.greeting-text {
font-weight: 600;
font-size: 1.1em;
color: var(--color-text-primary);
}
/* Adjust emoji positioning */
.greeting-emoji {
vertical-align: middle;
}
/* Style the Change Mood link */
.change-mood {
font-size: 0.9em;
white-space: nowrap;
}
/* define the profile "about" style */
.profile-about {
font-style: italic;
font-size: 0.95em;
color: var(--color-text-muted);
}
.tick-form {
@ -332,20 +398,13 @@ label.description {
gap: 0.5em;
}
.mood-bar {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
gap: 0.5em;
}
/* Styling for flash messages */
.flash-messages {
background: white;
background: var(--color-bg-white);
margin-top: 10px;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
box-shadow: 0 2px 10px var(--shadow-primary);
}
.flash-message {
@ -436,18 +495,28 @@ label.description {
color: var(--color-required);
}
.tick-feed {
list-style: none;
padding: 0;
margin: 0;
margin-top: 0.5em;
}
.tick {
margin-bottom: 1em;
padding-left: 0.5em;
}
.tick-time {
color: var(--color-text-muted);
font-size: 0.8em;
margin-bottom: 0.4em;
}
.tick-text {
color: var(--color-text-black);
font-size: 1.0em;
display: block;
}
.tick-pagination a {

6
src/.htaccess Normal file
View File

@ -0,0 +1,6 @@
# Deny all access to this directory
Require all denied
# Fallback for Apache 2.2
Order deny,allow
Deny from all

View File

@ -16,8 +16,6 @@ class AuthController extends Controller {
function handleLogin(){
global $config;
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
@ -37,7 +35,10 @@ class AuthController extends Controller {
header('Location: ' . $config->basePath);
exit;
} else {
$error = 'Invalid username or password';
// Set a flash message and reload the login page
Session::setFlashMessage('error', 'Invalid username or password');
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
}
}

View File

@ -9,7 +9,7 @@ class FlashView {
<?php foreach ($flashMessages as $type => $messages): ?>
<?php foreach ($messages as $message): ?>
<div class="flash-message flash-<?php echo $type; ?>">
<?php echo htmlspecialchars($message); ?>
<?php echo Util::escape_html($message); ?>
</div>
<?php endforeach; ?>
<?php endforeach; ?>

View File

@ -4,15 +4,14 @@ class HomeView {
ob_start();
?>
<main id="ticks" class="home-main">
<div class="tick-feed">
<ul class="tick-feed">
<?php foreach ($ticks as $tick): ?>
<article class="tick">
<li class="tick">
<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>
</li>
<?php endforeach; ?>
</div>
</ul>
<div class="tick-pagination">
<?php if ($page > 1): ?>
<a href="?page=<?= $page - 1 ?>">&laquo; Newer</a>
@ -21,7 +20,6 @@ class HomeView {
<a href="?page=<?= $page + 1 ?>">Older &raquo;</a>
<?php endif; ?>
</div>
</main>
<?php return ob_get_clean();
}

View File

@ -7,18 +7,18 @@ class MoodView {
?>
<?php foreach ($emojiGroups as $group => $emojis): ?>
<fieldset id="<?= htmlspecialchars($group) ?>" class="emoji-group">
<fieldset id="<?= Util::escape_html($group) ?>" class="emoji-group">
<legend><?= ucfirst($group) ?></legend>
<?php foreach ($emojis as [$emoji, $description]): ?>
<label class="emoji-option">
<input
type="radio"
name="mood"
value="<?= htmlspecialchars($emoji) ?>"
aria-label="<?=htmlspecialchars($description ?? 'emoji') ?>"
value="<?= Util::escape_html($emoji) ?>"
aria-label="<?=Util::escape_html($description ?? 'emoji') ?>"
<?= $emoji === $selected_emoji ? 'checked' : '' ?>
>
<span><?= htmlspecialchars($emoji) ?></span>
<span><?= Util::escape_html($emoji) ?></span>
</label>
<?php endforeach; ?>
</fieldset>
@ -31,7 +31,7 @@ class MoodView {
ob_start();
?>
<form method="post" class="emoji-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<?= $this->render_emoji_groups($emojiGroups, $currentMood) ?>
<div class="button-group">
<button type="submit" name="action" value="set">Set the mood</button>

6
storage/.htaccess Executable file
View File

@ -0,0 +1,6 @@
# Deny all access to this directory
Require all denied
# Fallback for Apache 2.2
Order deny,allow
Deny from all

6
templates/.htaccess Normal file
View File

@ -0,0 +1,6 @@
# Deny all access to this directory
Require all denied
# Fallback for Apache 2.2
Order deny,allow
Deny from all

View File

@ -10,19 +10,19 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet"
href="<?= htmlspecialchars($config->basePath) ?>css/tkr.css">
href="<?= Util::escape_html($config->basePath) ?>css/tkr.css">
<?php if (!empty($config->cssId)): ?>
<link rel="stylesheet"
href="<?= htmlspecialchars($config->basePath) ?>css/custom/<?= htmlspecialchars($config->customCssFilename()) ?>">
href="<?= Util::escape_html($config->basePath) ?>css/custom/<?= Util::escape_html($config->customCssFilename()) ?>">
<?php endif; ?>
<link rel="alternate"
type="application/rss+xml"
title="<?php echo htmlspecialchars($config->siteTitle) ?> RSS Feed"
href="<?php echo htmlspecialchars($config->baseUrl . $config->basePath)?>feed/rss/">
title="<?php echo Util::escape_html($config->siteTitle) ?> RSS Feed"
href="<?php echo Util::escape_html($config->baseUrl . $config->basePath)?>feed/rss/">
<link rel="alternate"
type="application/atom+xml"
title="<?php echo htmlspecialchars($config->siteTitle) ?> Atom Feed"
href="<?php echo htmlspecialchars($config->baseUrl . $config->basePath)?>feed/atom/">
title="<?php echo Util::escape_html($config->siteTitle) ?> Atom Feed"
href="<?php echo Util::escape_html($config->baseUrl . $config->basePath)?>feed/atom/">
</head>
<body>
<?php include TEMPLATES_DIR . '/partials/navbar.php'?>

View File

@ -6,28 +6,28 @@
<form
action="<?php echo $config->basePath . ($isSetup ? 'setup' : 'admin') ?>"
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']) ?>">
<fieldset>
<legend>User settings</legend>
<div class="fieldset-items">
<label>Username <span class=required>*</span></label>
<input type="text"
name="username"
value="<?= $user->username ?>"
value="<?= Util::escape_html($user->username) ?>"
required>
<label>Display name <span class=required>*</span></label>
<input type="text"
name="display_name"
value="<?= $user->displayName ?>"
value="<?= Util::escape_html($user->displayName) ?>"
required>
<label>About </label>
<input type="text"
name="about"
value="<?= $user->about ?>">
value="<?= Util::escape_html($user->about) ?>">
<label>Website </label>
<input type="text"
name="website"
value="<?= $user->website ?>">
value="<?= Util::escape_html($user->website) ?>">
</div>
</fieldset>
<fieldset>
@ -36,21 +36,21 @@
<label>Title <span class=required>*</span></label>
<input type="text"
name="site_title"
value="<?= $config->siteTitle ?>"
value="<?= Util::escape_html($config->siteTitle) ?>"
required>
<label>Description <span class=required>*</span></label>
<input type="text"
name="site_description"
value="<?= $config->siteDescription ?>">
value="<?= Util::escape_html($config->siteDescription) ?>">
<label>Base URL <span class=required>*</span></label>
<input type="text"
name="base_url"
value="<?= $config->baseUrl ?>"
value="<?= Util::escape_html($config->baseUrl) ?>"
required>
<label>Base path <span class=required>*</span></label>
<input type="text"
name="base_path"
value="<?= $config->basePath ?>"
value="<?= Util::escape_html($config->basePath) ?>"
required>
<label>Items per page (max 50) <span class=required>*</span></label>
<input type="number"

View File

@ -3,7 +3,7 @@
<h1>CSS Management</h1>
<div>
<form action="<?= $config->basePath ?>admin/css" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<fieldset>
<legend>Manage</legend>
<div class="fieldset-items">
@ -20,13 +20,13 @@
<option value=<?= $cssFile['id'] ?>
<?= isset($selected) ? $selected : ""?>>
<?=$cssFile['filename']?>
<?=Util::escape_html($cssFile['filename'])?>
</option>
<?php endforeach; ?>
</select>
<?php if (isset($cssDescription) && $cssDescription): ?>
<label>Description</label>
<label class="description"><?= $cssDescription ?></label>
<label class="description"><?= Util::escape_html($cssDescription) ?></label>
<?php endif; ?>
<div></div>
<div>
@ -38,7 +38,7 @@
<fieldset>
<legend>Upload</legend>
<div class="fieldset-items">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<label for="uploadCssFile">Select File to Upload</label>
<input type="file"
id="uploadCssFile"

View File

@ -3,7 +3,7 @@
<h1>Emoji Management</h1>
<div>
<form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<fieldset>
<legend>Add Emoji</legend>
<div class="fieldset-items">
@ -25,19 +25,19 @@
</form>
<?php if (!empty($emojiList)): ?>
<form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
<fieldset class="delete-emoji-fieldset">
<legend>Delete Emoji</legend>
<div class="fieldset-items">
<?php foreach ($emojiList as $emojiItem): ?>
<div class="emoji-checkbox-item">
<input type="checkbox"
id="delete_emoji_<?= htmlspecialchars($emojiItem['id']) ?>"
id="delete_emoji_<?= Util::escape_html($emojiItem['id']) ?>"
name="delete_emoji_ids[]"
value="<?= htmlspecialchars($emojiItem['id']) ?>">
<label for="delete_emoji_<?= htmlspecialchars($emojiItem['id']) ?>">
<span class="emoji-display"><?= htmlspecialchars($emojiItem['emoji']) ?></span>
<span class="emoji-description"><?= htmlspecialchars($emojiItem['description']) ?></span>
value="<?= Util::escape_html($emojiItem['id']) ?>">
<label for="delete_emoji_<?= Util::escape_html($emojiItem['id']) ?>">
<span class="emoji-display"><?= Util::escape_html($emojiItem['emoji']) ?></span>
<span class="emoji-description"><?= Util::escape_html($emojiItem['description']) ?></span>
</label>
</div>
<?php endforeach; ?>

View File

@ -3,36 +3,50 @@
<?php /** @var UserModel $user */ ?>
<?php /** @var string $tickList */ ?>
<div class="home-container">
<section id="sidebar" class="home-sidebar">
<div class="home-header">
<h1 class="site-description"><?= $config->siteDescription ?></h1>
</div>
<aside id="sidebar" class="home-sidebar">
<dl class="profile-data">
<dt>Current Status</dt>
<dd class="profile-greeting">
<span class="greeting-content">
<span class="greeting-text">Hi, I'm <?php echo Util::escape_html($user->displayName) ?></span>
<span class="greeting-mood"><?php echo Util::escape_html($user->mood) ?></span>
</span>
<?php if (Session::isLoggedIn()): ?>
<a href="<?= $config->basePath ?>mood" class="change-mood">Change mood</a>
<?php endif ?>
</dd>
<?php if (!empty($user->about)): ?>
<p>About: <?= $user->about ?></p>
<dt>About</dt>
<dd class="profile-about">
<?php echo Util::escape_html($user->about) ?>
</dd>
<?php endif ?>
<?php if (!empty($user->website)): ?>
<p>Website: <?= Util::linkify(Util::escape_html($user->website)) ?></p>
<dt>Website</dt>
<dd class="profile-website">
<?php echo Util::linkify(Util::escape_html($user->website)) ?>
</dd>
<?php endif ?>
<?php if (!empty($user->mood) || Session::isLoggedIn()): ?>
<div class="profile-row">
<div class="mood-bar">
<span>Current mood: <?= $user->mood ?></span>
</dl>
<?php if (Session::isLoggedIn()): ?>
<a href="<?= $config->basePath ?>mood">Change</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if (Session::isLoggedIn()): ?>
<hr/>
<div class="profile-row">
<div class="profile-tick">
<form class="tick-form" method="post">
<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?"
minlength="1"
maxlength="200"
rows="3"></textarea>
<button type="submit" class="submit-btn">Tick</button>
</form>
</div>
<?php endif; ?>
</section>
<?php echo $tickList ?>
</aside>
<main id="ticks" class="home-main">
<div class="home-header">
<h1 class="site-description"><?= $config->siteDescription ?></h1>
</div>
<?php echo $tickList ?>
</main>
</div>

View File

@ -2,12 +2,9 @@
<?php /** @var string $csrf_token */ ?>
<?php /** @var string $error */ ?>
<h2>Login</h2>
<?php if ($error): ?>
<p style="color:red"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<form method="post" action="<?= $config->basePath ?>login">
<div class="fieldset-items">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($csrf_token) ?>">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="password">Password:</label>