Merge pull request #68 from everforge/chore/appjs-security-hardening

Harden pattern rendering and clipboard interactions in app.js
This commit is contained in:
Paul Bakaus
2026-03-24 16:03:57 -07:00
committed by GitHub
+30 -8
View File
@@ -17,6 +17,16 @@ let allCommands = [];
// CONTENT LOADING
// ============================================
function escapeHtml(value) {
if (typeof value !== "string") return "";
return value
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
async function loadContent() {
try {
const [commandsRes, patternsRes] = await Promise.all([
@@ -96,13 +106,13 @@ function renderPatternsWithTabs(patterns, antipatterns) {
const tabsHTML = patterns
.map((category, i) => `<button
class="pattern-tab${i === 0 ? ' active' : ''}"
data-tab="${category.name}"
data-tab="${escapeHtml(category.name)}"
role="tab"
id="${tabId(category.name)}"
aria-selected="${i === 0 ? 'true' : 'false'}"
aria-controls="${panelId(category.name)}"
tabindex="${i === 0 ? '0' : '-1'}"
>${category.name}</button>`)
>${escapeHtml(category.name)}</button>`)
.join("");
// Build panels with WAI-ARIA attributes
@@ -112,7 +122,7 @@ function renderPatternsWithTabs(patterns, antipatterns) {
return `
<div
class="pattern-panel${i === 0 ? ' active' : ''}"
data-panel="${category.name}"
data-panel="${escapeHtml(category.name)}"
role="tabpanel"
id="${panelId(category.name)}"
aria-labelledby="${tabId(category.name)}"
@@ -122,13 +132,13 @@ function renderPatternsWithTabs(patterns, antipatterns) {
<div class="pattern-column pattern-column--anti">
<span class="pattern-column-label" id="dont-label-${i}">Don't</span>
<ul class="pattern-list" aria-labelledby="dont-label-${i}">
${antiItems.map((item) => `<li class="pattern-item pattern-item--anti">${item}</li>`).join("")}
${antiItems.map((item) => `<li class="pattern-item pattern-item--anti">${escapeHtml(item)}</li>`).join("")}
</ul>
</div>
<div class="pattern-column pattern-column--do">
<span class="pattern-column-label" id="do-label-${i}">Do</span>
<ul class="pattern-list" aria-labelledby="do-label-${i}">
${category.items.map((item) => `<li class="pattern-item pattern-item--do">${item}</li>`).join("")}
${category.items.map((item) => `<li class="pattern-item pattern-item--do">${escapeHtml(item)}</li>`).join("")}
</ul>
</div>
</div>
@@ -167,7 +177,9 @@ function renderPatternsWithTabs(patterns, antipatterns) {
p.classList.remove('active');
p.setAttribute('hidden', '');
});
const activePanel = container.querySelector(`[data-panel="${tabName}"]`);
const escapedName = CSS.escape(tabName);
const activePanel = container.querySelector(`[data-panel="${escapedName}"]`);
if (!activePanel) return;
activePanel.classList.add('active');
activePanel.removeAttribute('hidden');
};
@@ -229,10 +241,20 @@ document.addEventListener("click", (e) => {
const copyBtn = e.target.closest("[data-copy]");
if (copyBtn) {
const textToCopy = copyBtn.dataset.copy;
navigator.clipboard.writeText(textToCopy).then(() => {
const onCopied = () => {
copyBtn.classList.add('copied');
setTimeout(() => copyBtn.classList.remove('copied'), 1500);
});
};
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(textToCopy).then(onCopied).catch(() => {});
} else {
// Fallback for non-HTTPS or older browsers
const ta = Object.assign(document.createElement('textarea'), { value: textToCopy, style: 'position:fixed;left:-9999px' });
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); onCopied(); } catch {}
ta.remove();
}
}
});