fix(ui): render sanitized HTML in playlist descriptions
This commit is contained in:
2 files changed
+48
-1
No files matched your search
@@ -22,6 +22,7 @@
|
||||
import { isTrackUnavailable, clearTrackUnavailable, subscribe as subscribeUnavailable } from '$lib/stores/unavailableTracksStore';
|
||||
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
|
||||
import { showToast } from '$lib/stores/toastStore';
|
||||
import { sanitizeHtml } from '$lib/utils/sanitize';
|
||||
import { get } from 'svelte/store';
|
||||
import { t } from '$lib/i18n';
|
||||
import { onMount, tick } from 'svelte';
|
||||
@@ -2020,7 +2021,7 @@
|
||||
<span class="playlist-label">Playlist</span>
|
||||
<h1 class="playlist-title">{playlist.name}</h1>
|
||||
{#if playlist.description}
|
||||
<p class="playlist-description">{playlist.description}</p>
|
||||
<p class="playlist-description">{@html sanitizeHtml(playlist.description)}</p>
|
||||
{/if}
|
||||
<div class="playlist-info">
|
||||
<span class="owner">{playlist.owner.name}</span>
|
||||
@@ -2603,6 +2604,15 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.playlist-description :global(a) {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.playlist-description :global(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Lightweight HTML sanitizer for rendering API content (e.g. Qobuz playlist descriptions).
|
||||
* Allows safe tags and strips dangerous elements/attributes.
|
||||
*/
|
||||
|
||||
const ALLOWED_TAGS = new Set([
|
||||
'b', 'i', 'em', 'strong', 'br', 'a', 'p', 'span', 'ul', 'ol', 'li',
|
||||
]);
|
||||
|
||||
/** Strip event handler attributes and javascript: URLs */
|
||||
function stripDangerousAttrs(tag: string): string {
|
||||
// Remove on* event attributes
|
||||
tag = tag.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
|
||||
// Remove javascript: URLs from href/src
|
||||
tag = tag.replace(/(href|src)\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, '$1=""');
|
||||
return tag;
|
||||
}
|
||||
|
||||
export function sanitizeHtml(html: string): string {
|
||||
if (!html) return '';
|
||||
|
||||
// Remove <script> and <iframe> blocks entirely (with content)
|
||||
let result = html.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
result = result.replace(/<iframe[\s\S]*?<\/iframe>/gi, '');
|
||||
result = result.replace(/<style[\s\S]*?<\/style>/gi, '');
|
||||
|
||||
// Process remaining tags: keep allowed, strip others
|
||||
result = result.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g, (match, tagName) => {
|
||||
const lower = tagName.toLowerCase();
|
||||
if (ALLOWED_TAGS.has(lower)) {
|
||||
return stripDangerousAttrs(match);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user