From 3aee8c8f9a02c70e76f855b9a1023f9e21fbb29d Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Thu, 2 Apr 2026 18:20:33 -0400 Subject: [PATCH 1/2] fix: errors from web-ext --- .github/workflows/deploy.yml | 2 + inject.js | 57 +++++++++++++++++--------- lucide-client.js | 27 +----------- options.js | 36 ++++++++-------- popup.js | 20 +++++---- ui-icons.js | 79 ++++++++++++++++++++++++++++++++++-- 6 files changed, 147 insertions(+), 74 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fc94342..3636a26 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,6 +10,8 @@ on: jobs: build: runs-on: ubuntu-latest + env: + WEB_EXT_IGNORE_FILES: scripts/** steps: - uses: actions/checkout@v4 diff --git a/inject.js b/inject.js index a100184..5139c1f 100644 --- a/inject.js +++ b/inject.js @@ -776,27 +776,40 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) { return normalizedEnabled; } -function subtitleNudgeIconMarkup(isEnabled) { +function renderSubtitleNudgeIndicatorContent(target, isEnabled) { + if (!target) return; var action = isEnabled ? "subtitleNudgeOn" : "subtitleNudgeOff"; var custom = tc.settings.customButtonIcons && tc.settings.customButtonIcons[action] && tc.settings.customButtonIcons[action].svg; + vscClearElement(target); if (custom) { - return ( - '" + var customWrap = vscCreateSvgWrap( + target.ownerDocument || document, + custom, + "vsc-btn-icon" ); + if (customWrap) { + target.appendChild(customWrap); + return; + } } if (typeof vscIconSvgString !== "function") { - return isEnabled ? "✓" : "×"; + target.textContent = isEnabled ? "✓" : "×"; + return; } var svg = vscIconSvgString(action, 14); if (!svg) { - return isEnabled ? "✓" : "×"; + target.textContent = isEnabled ? "✓" : "×"; + return; } - return ( - '" - ); + var wrap = vscCreateSvgWrap(target.ownerDocument || document, svg, "vsc-btn-icon"); + if (wrap) { + target.appendChild(wrap); + return; + } + target.textContent = isEnabled ? "✓" : "×"; } function updateSubtitleNudgeIndicator(video) { @@ -804,11 +817,10 @@ function updateSubtitleNudgeIndicator(video) { var isEnabled = isSubtitleNudgeEnabledForVideo(video); var title = isEnabled ? "Subtitle nudge enabled" : "Subtitle nudge disabled"; - var mark = subtitleNudgeIconMarkup(isEnabled); var indicator = video.vsc.subtitleNudgeIndicator; if (indicator) { - indicator.innerHTML = mark; + renderSubtitleNudgeIndicatorContent(indicator, isEnabled); indicator.dataset.enabled = isEnabled ? "true" : "false"; indicator.dataset.supported = "true"; indicator.title = title; @@ -817,7 +829,7 @@ function updateSubtitleNudgeIndicator(video) { var flashEl = video.vsc.nudgeFlashIndicator; if (flashEl) { - flashEl.innerHTML = mark; + renderSubtitleNudgeIndicatorContent(flashEl, isEnabled); flashEl.dataset.enabled = isEnabled ? "true" : "false"; flashEl.dataset.supported = "true"; flashEl.setAttribute("aria-label", title); @@ -1357,12 +1369,15 @@ chrome.storage.sync.get(tc.settings, function (storage) { tc.settings.customButtonIcons && tc.settings.customButtonIcons[act] && tc.settings.customButtonIcons[act].svg; - btn.innerHTML = ""; + vscClearElement(btn); if (svg) { - var cw = doc.createElement("span"); - cw.className = "vsc-btn-icon"; - cw.innerHTML = svg; - btn.appendChild(cw); + var cw = vscCreateSvgWrap(doc, svg, "vsc-btn-icon"); + if (cw) { + btn.appendChild(cw); + } else { + var cdf = controllerButtonDefs[act]; + btn.textContent = (cdf && cdf.label) || "?"; + } } else if (typeof vscIconWrap === "function") { var wrap = vscIconWrap(doc, act, 14); if (wrap) { @@ -1405,10 +1420,12 @@ function createControllerButton(doc, action, label, className) { tc.settings.customButtonIcons[action] && tc.settings.customButtonIcons[action].svg; if (custom) { - var customWrap = doc.createElement("span"); - customWrap.className = "vsc-btn-icon"; - customWrap.innerHTML = custom; - button.appendChild(customWrap); + var customWrap = vscCreateSvgWrap(doc, custom, "vsc-btn-icon"); + if (customWrap) { + button.appendChild(customWrap); + } else { + button.textContent = label || "?"; + } } else if (typeof vscIconWrap === "function") { var wrap = vscIconWrap(doc, action, 14); if (wrap) { diff --git a/lucide-client.js b/lucide-client.js index 41727d0..ac4581b 100644 --- a/lucide-client.js +++ b/lucide-client.js @@ -31,32 +31,9 @@ function sanitizeLucideSvg(svgText) { var t = String(svgText).replace(/\0/g, "").trim(); if (!/]/i.test(t)) return null; var doc = new DOMParser().parseFromString(t, "image/svg+xml"); - var svg = doc.querySelector("svg"); + if (doc.querySelector("parsererror")) return null; + var svg = vscSanitizeSvgTree(doc.querySelector("svg")); if (!svg) return null; - svg.querySelectorAll("script").forEach(function (n) { - n.remove(); - }); - svg.querySelectorAll("style").forEach(function (n) { - n.remove(); - }); - svg.querySelectorAll("*").forEach(function (el) { - for (var i = el.attributes.length - 1; i >= 0; i--) { - var attr = el.attributes[i]; - var name = attr.name.toLowerCase(); - var val = attr.value; - if (name.indexOf("on") === 0) { - el.removeAttribute(attr.name); - continue; - } - if ( - (name === "href" || name === "xlink:href") && - /^javascript:/i.test(val) - ) { - el.removeAttribute(attr.name); - } - } - }); - svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svg.removeAttribute("width"); svg.removeAttribute("height"); svg.setAttribute("width", "100%"); diff --git a/options.js b/options.js index e280f1e..37b0eba 100644 --- a/options.js +++ b/options.js @@ -172,8 +172,9 @@ var customButtonIconsLive = {}; function fillControlBarIconElement(icon, buttonId) { if (!icon || !buttonId) return; + var doc = icon.ownerDocument || document; if (buttonId === "nudge") { - icon.innerHTML = ""; + vscClearElement(icon); icon.className = "cb-icon cb-icon-nudge-pair"; function nudgeChipMarkup(action) { var c = customButtonIconsLive[action]; @@ -189,10 +190,10 @@ function fillControlBarIconElement(icon, buttonId) { sp.setAttribute("data-nudge-state", stateKey); var inner = nudgeChipMarkup(action); if (inner) { - var wrap = document.createElement("span"); - wrap.className = "vsc-btn-icon"; - wrap.innerHTML = inner; - sp.appendChild(wrap); + var wrap = vscCreateSvgWrap(doc, inner, "vsc-btn-icon"); + if (wrap) { + sp.appendChild(wrap); + } } icon.appendChild(sp); } @@ -207,16 +208,15 @@ function fillControlBarIconElement(icon, buttonId) { icon.className = "cb-icon"; var custom = customButtonIconsLive[buttonId]; if (custom && custom.svg) { - icon.innerHTML = custom.svg; - return; + if (vscSetSvgContent(icon, custom.svg)) return; } if (typeof vscIconSvgString === "function") { var svgHtml = vscIconSvgString(buttonId, 16); if (svgHtml) { - icon.innerHTML = svgHtml; - return; + if (vscSetSvgContent(icon, svgHtml)) return; } } + vscClearElement(icon); var def = controllerButtonDefs[buttonId]; icon.textContent = (def && def.icon) || "?"; } @@ -1200,8 +1200,8 @@ function createControlBarBlock(buttonId) { } function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) { - activeZone.innerHTML = ""; - availableZone.innerHTML = ""; + vscClearElement(activeZone); + vscClearElement(availableZone); var allowed = function (id) { if (!controllerButtonDefs[id]) return false; @@ -1394,7 +1394,7 @@ function initLucideButtonIconsUI() { if (!actionSel.dataset.lucideInit) { actionSel.dataset.lucideInit = "1"; - actionSel.innerHTML = ""; + vscClearElement(actionSel); Object.keys(controllerButtonDefs).forEach(function (aid) { if (aid === "nudge") { Object.keys(lucideSubtitleNudgeActionLabels).forEach(function (subId) { @@ -1415,7 +1415,7 @@ function initLucideButtonIconsUI() { } function renderResults(slugs) { - resultsEl.innerHTML = ""; + vscClearElement(resultsEl); slugs.forEach(function (slug) { var b = document.createElement("button"); b.type = "button"; @@ -1450,11 +1450,13 @@ function initLucideButtonIconsUI() { .then(function (txt) { var safe = sanitizeLucideSvg(txt); if (!safe) throw new Error("Bad SVG"); - previewEl.innerHTML = safe; + if (!vscSetSvgContent(previewEl, safe)) { + throw new Error("Preview render failed"); + } setLucideStatus("Preview: " + slug); }) .catch(function (e) { - previewEl.innerHTML = ""; + vscClearElement(previewEl); setLucideStatus( "Could not load: " + slug + " — " + e.message ); @@ -1473,7 +1475,7 @@ function initLucideButtonIconsUI() { .then(function (map) { var q = searchInput.value; if (!q.trim()) { - resultsEl.innerHTML = ""; + vscClearElement(resultsEl); return; } renderResults(searchLucideSlugs(map, q, 48)); @@ -1638,7 +1640,7 @@ function restore_options() { ? storage.siteRules : tcDefaults.siteRules || []; - document.getElementById("siteRulesContainer").innerHTML = ""; + vscClearElement(document.getElementById("siteRulesContainer")); if (siteRules.length > 0) { siteRules.forEach((rule) => { if (rule && rule.pattern) { diff --git a/popup.js b/popup.js index 76c9273..3154478 100644 --- a/popup.js +++ b/popup.js @@ -230,17 +230,21 @@ document.addEventListener("DOMContentLoaded", function () { btn.dataset.action = btnId; var customEntry = customMap[btnId]; if (customEntry && customEntry.svg) { - var customSpan = document.createElement("span"); - customSpan.className = "vsc-btn-icon"; - customSpan.innerHTML = customEntry.svg; - btn.appendChild(customSpan); + var customSpan = vscCreateSvgWrap(document, customEntry.svg, "vsc-btn-icon"); + if (customSpan) { + btn.appendChild(customSpan); + } else { + btn.textContent = def.label || "?"; + } } else if (typeof vscIconSvgString === "function") { var svgStr = vscIconSvgString(btnId, 16); if (svgStr) { - var iconSpan = document.createElement("span"); - iconSpan.className = "vsc-btn-icon"; - iconSpan.innerHTML = svgStr; - btn.appendChild(iconSpan); + var iconSpan = vscCreateSvgWrap(document, svgStr, "vsc-btn-icon"); + if (iconSpan) { + btn.appendChild(iconSpan); + } else { + btn.textContent = def.label || "?"; + } } else { btn.textContent = def.label || "?"; } diff --git a/ui-icons.js b/ui-icons.js index 631cd9e..63ddf1a 100644 --- a/ui-icons.js +++ b/ui-icons.js @@ -3,6 +3,7 @@ * Use stroke="currentColor" so buttons inherit foreground for monochrome UI. */ var VSC_ICON_SIZE_DEFAULT = 18; +var VSC_SVG_NS = "http://www.w3.org/2000/svg"; /** Inner SVG markup only (paths / shapes inside ). */ var vscUiIconPaths = { @@ -54,6 +55,79 @@ function vscIconSvgString(action, size) { ); } +function vscClearElement(el) { + if (!el) return; + while (el.firstChild) { + el.removeChild(el.firstChild); + } +} + +function vscSanitizeSvgTree(svg) { + if (!svg || String(svg.tagName).toLowerCase() !== "svg") return null; + + svg.querySelectorAll("script, style, foreignObject").forEach(function (n) { + n.remove(); + }); + + svg.querySelectorAll("*").forEach(function (el) { + for (var i = el.attributes.length - 1; i >= 0; i--) { + var attr = el.attributes[i]; + var name = attr.name.toLowerCase(); + var val = attr.value; + if (name.indexOf("on") === 0) { + el.removeAttribute(attr.name); + continue; + } + if ( + (name === "href" || name === "xlink:href") && + /^\s*javascript:/i.test(val) + ) { + el.removeAttribute(attr.name); + } + } + }); + + svg.setAttribute("xmlns", VSC_SVG_NS); + svg.setAttribute("aria-hidden", "true"); + return svg; +} + +function vscCreateSvgNode(doc, svgText) { + if (!doc || !svgText || typeof svgText !== "string") return null; + var clean = String(svgText).replace(/\0/g, "").trim(); + if (!clean || !/]/i.test(clean)) return null; + + var parsed = new DOMParser().parseFromString(clean, "image/svg+xml"); + if (parsed.querySelector("parsererror")) return null; + + var svg = vscSanitizeSvgTree(parsed.querySelector("svg")); + if (!svg) return null; + + return doc.importNode(svg, true); +} + +function vscSetSvgContent(el, svgText) { + if (!el) return false; + vscClearElement(el); + + var doc = el.ownerDocument || document; + var svg = vscCreateSvgNode(doc, svgText); + if (!svg) return false; + + el.appendChild(svg); + return true; +} + +function vscCreateSvgWrap(doc, svgText, className) { + if (!doc) return null; + var span = doc.createElement("span"); + span.className = className || "vsc-btn-icon"; + if (!vscSetSvgContent(span, svgText)) { + return null; + } + return span; +} + /** * @param {Document} doc * @param {string} action @@ -62,8 +136,5 @@ function vscIconSvgString(action, size) { function vscIconWrap(doc, action, size) { var html = vscIconSvgString(action, size); if (!html) return null; - var span = doc.createElement("span"); - span.className = "vsc-btn-icon"; - span.innerHTML = html; - return span; + return vscCreateSvgWrap(doc, html, "vsc-btn-icon"); } From 6bd319c8cceadfd51c0d832c32fa6a3939041e9e Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Thu, 2 Apr 2026 18:20:48 -0400 Subject: [PATCH 2/2] Bump version to 5.1.6 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 63e22e0..6d4b17a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Speeder", "short_name": "Speeder", - "version": "5.1.4", + "version": "5.1.6", "manifest_version": 2, "description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")", "homepage_url": "https://github.com/SoPat712/speeder",