Security Fixes

This commit is contained in:
Tim Zöller 2026-04-07 10:28:10 +02:00
parent aa7a7bc9fc
commit a0eebfcb3f
14 changed files with 279 additions and 37 deletions

View file

@ -519,7 +519,7 @@
function renderActivity(activity) {
// Header
document.getElementById('activityTitle').textContent = activity.title || 'Untitled Activity';
document.getElementById('activityTitle').innerHTML = linkifyHashtags(activity.title || 'Untitled Activity');
document.getElementById('activityType').textContent = activity.activityType;
document.getElementById('activityType').className = `activity-type-badge activity-type-${activity.activityType.toLowerCase()}`;
// Format date with timezone awareness
@ -570,7 +570,7 @@
// Description
if (activity.description) {
document.getElementById('activityDescription').textContent = activity.description;
document.getElementById('activityDescription').innerHTML = linkifyHashtags(activity.description);
} else {
document.getElementById('activityDescription').style.display = 'none';
}
@ -1721,6 +1721,14 @@
return div.innerHTML;
}
function linkifyHashtags(text) {
if (!text) return '';
const escaped = escapeHtml(text);
return escaped.replace(/(^|\s)#(\w+)/g, (match, lead, tag) =>
`${lead}<a href="/timeline?hashtag=${encodeURIComponent(tag.toLowerCase())}" class="hashtag-link">#${tag}</a>`
);
}
// Delete functionality
document.getElementById('deleteBtn').addEventListener('click', function() {
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));

View file

@ -152,9 +152,7 @@
<div class="row">
<div class="col-md-8">
<h5 class="card-title">
<a href="/activities/${activity.id}" class="text-decoration-none">
${escapeHtml(activity.title || 'Untitled Activity')}
</a>
${renderTitleLinkWithHashtags(activity.title, `/activities/${activity.id}`)}
</h5>
<p class="text-muted mb-2">
<span class="activity-type-badge activity-type-${activity.activityType.toLowerCase()}${activity.race ? ' race-activity' : ''}">
@ -181,7 +179,7 @@
${activity.visibility}
</span>
</p>
${activity.description ? `<p class="card-text">${escapeHtml(activity.description).substring(0, 150)}${activity.description.length > 150 ? '...' : ''}</p>` : ''}
${activity.description ? `<p class="card-text">${linkifyHashtags(activity.description.length > 150 ? activity.description.substring(0, 150) + '...' : activity.description)}</p>` : ''}
</div>
<div class="col-md-4">
<small class="text-muted">
@ -332,6 +330,35 @@
div.textContent = text;
return div.innerHTML;
}
function linkifyHashtags(text) {
if (!text) return '';
const escaped = escapeHtml(text);
return escaped.replace(/(^|\s)#(\w+)/g, (match, lead, tag) =>
`${lead}<a href="/timeline?hashtag=${encodeURIComponent(tag.toLowerCase())}" class="hashtag-link">#${tag}</a>`
);
}
function renderTitleLinkWithHashtags(text, activityHref) {
const safeText = text || 'Untitled Activity';
const wrap = (chunk) => chunk
? `<a href="${activityHref}" class="text-decoration-none activity-title-link">${chunk}</a>`
: '';
const parts = [];
const regex = /(^|\s)#(\w+)/g;
let last = 0;
let m;
while ((m = regex.exec(safeText)) !== null) {
const before = safeText.substring(last, m.index) + m[1];
if (before) parts.push(wrap(escapeHtml(before)));
const tag = m[2];
parts.push(`<a href="/timeline?hashtag=${encodeURIComponent(tag.toLowerCase())}" class="hashtag-link">#${escapeHtml(tag)}</a>`);
last = m.index + m[0].length;
}
const tail = safeText.substring(last);
if (tail) parts.push(wrap(escapeHtml(tail)));
return parts.join('');
}
});
</script>
</th:block>

View file

@ -62,6 +62,18 @@
</div>
</div>
<!-- Active hashtag filter -->
<div id="hashtagFilterBadge" class="alert alert-info d-flex justify-content-between align-items-center d-none" role="alert">
<span>
<i class="bi bi-hash"></i>
Filtering by <strong id="hashtagFilterLabel">#tag</strong>
</span>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="FitPubTimeline.clearHashtagFilter(); return false;">
<i class="bi bi-x-lg"></i> Clear
</button>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="text-center py-5">
<div class="spinner-border text-primary" role="status">