Merge pull request #2456 from PR0M3TH3AN/ai/innerhtml-fix/channelProfile-12302281782298643204

[security] replace innerHTML usages in js/channelProfile.js
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-13 12:19:37 -05:00
commit 7deb730db5
8 files changed
+232 -112

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
# Context: innerHTML Migration for js/channelProfile.js
- **Date:** 2026-02-13
- **Agent:** innerhtml-migration-agent
- **Target File:** `js/channelProfile.js`
- **Reason:** High innerHTML count (10 assignments).
- **Node Version:** (from preflight)
- **NPM Version:** (from preflight)
## Plan
1. Replace `list.innerHTML = ""` with `replaceChildren()` or `textContent = ""`.
2. Replace static HTML in `panel.innerHTML` (Zap Panel) with `createElement` calls.
3. Replace static HTML in `container.innerHTML` (Subscribe Button) with `createElement` calls.
4. Replace static HTML in `container.innerHTML` (Loading/Empty/Error states) with `createElement` calls.
@@ -0,0 +1,12 @@
# DECISIONS: innerHTML Migration for js/channelProfile.js
- **Zap Panel (`panel.innerHTML`)**: The HTML is static and large. Replacing it with `createElement` calls will make the code verbose. However, this is the standard way to avoid `innerHTML`. I will extract the creation logic into a helper function `createZapPanelContent` to keep the main `setupZapButton` function clean.
- **Subscribe Button**: The HTML is small. I will use `createElement`.
- **Loading/Error States**: These are simple. I will use `createElement`.
- **Clearing Content**: Using `replaceChildren()` is the modern, safe way to clear an element.
## Strategy for Zap Panel
Since the Zap Panel HTML is complex, I'll break it down into logical sections (header, form, etc.) within the helper function.
## Strategy for Subscribe Button
The button has dynamic text based on `alreadySubscribed`. I will create the button and icon elements and set their properties.
@@ -0,0 +1,8 @@
# Agent Task Log Entry
- **Date:** 2026-02-13
- **Agent:** innerhtml-migration-agent
- **Prompt:** bitvid-innerhtml-migration-agent.md
- **Status:** started
- **Branch:** ai/innerhtml-fix/channelProfile
- **Summary:** Task claimed — execution beginning
@@ -0,0 +1,8 @@
# Agent Task Log Entry
- **Date:** 2026-02-13
- **Agent:** innerhtml-migration-agent
- **Prompt:** bitvid-innerhtml-migration-agent.md
- **Status:** completed
- **Branch:** ai/innerhtml-fix/channelProfile
- **Summary:** Migrated 10 innerHTML assignments in js/channelProfile.js to safe DOM APIs and updated baseline.
+158 -111
View File
@@ -2338,7 +2338,7 @@ function clearZapReceipts() {
if (!list) {
return;
}
list.innerHTML = "";
list.replaceChildren();
}
function renderZapReceipts(receipts, { partial = false } = {}) {
@@ -2347,7 +2347,7 @@ function renderZapReceipts(receipts, { partial = false } = {}) {
return;
}
list.innerHTML = "";
list.replaceChildren();
const doc = list.ownerDocument || (typeof document !== "undefined" ? document : null);
@@ -3855,6 +3855,113 @@ export function __ensureChannelModerationEventsForTests() {
ensureChannelModerationEvents();
}
function createZapPanelContent(doc) {
const wrapper = doc.createDocumentFragment();
// Header
const header = doc.createElement("div");
header.className = "flex items-center justify-between gap-4";
const h3 = doc.createElement("h3");
h3.className = "text-xs font-semibold uppercase tracking-wide text-muted-strong";
h3.textContent = "Send a zap";
const closeBtn = doc.createElement("button");
closeBtn.type = "button";
closeBtn.id = "zapCloseBtn";
closeBtn.className = "btn-ghost px-2 py-1 text-xs";
closeBtn.setAttribute("aria-label", "Close zap dialog");
closeBtn.textContent = "✕";
header.appendChild(h3);
header.appendChild(closeBtn);
wrapper.appendChild(header);
// Wallet Prompt
const walletPrompt = doc.createElement("p");
walletPrompt.id = "zapWalletPrompt";
walletPrompt.className = "text-sm text-muted";
walletPrompt.setAttribute("aria-hidden", "true");
walletPrompt.hidden = true;
walletPrompt.appendChild(doc.createTextNode("Connect a wallet in "));
const walletLink = doc.createElement("button");
walletLink.type = "button";
walletLink.id = "zapWalletLink";
walletLink.className = "btn-ghost px-2 py-1 text-xs";
walletLink.textContent = "Wallet Connect settings";
walletPrompt.appendChild(walletLink);
walletPrompt.appendChild(doc.createTextNode(" to send zaps."));
wrapper.appendChild(walletPrompt);
// Form
const form = doc.createElement("form");
form.id = "zapForm";
form.className = "bv-stack bv-stack--tight";
// Amount Input
const amountDiv = doc.createElement("div");
const label = doc.createElement("label");
label.setAttribute("for", "zapAmountInput");
label.id = "zapAmountLabel";
label.className = "mb-2 block text-xs font-semibold uppercase tracking-wide text-muted-strong";
label.textContent = "Zap amount (sats)";
const input = doc.createElement("input");
input.id = "zapAmountInput";
input.type = "number";
input.min = "1";
input.step = "1";
input.inputMode = "numeric";
input.className = "input";
input.placeholder = "Enter sats";
input.setAttribute("aria-labelledby", "zapAmountLabel");
amountDiv.appendChild(label);
amountDiv.appendChild(input);
form.appendChild(amountDiv);
// Split Summary
const summary = doc.createElement("p");
summary.id = "zapSplitSummary";
summary.className = "text-sm text-muted";
summary.setAttribute("aria-live", "polite");
summary.textContent = "Enter an amount to view the split.";
form.appendChild(summary);
// Status
const status = doc.createElement("p");
status.id = "zapStatus";
status.className = "text-xs text-muted";
status.setAttribute("role", "status");
status.setAttribute("aria-live", "polite");
form.appendChild(status);
// Receipts
const receipts = doc.createElement("ul");
receipts.id = "zapReceipts";
receipts.className = "space-y-2 text-xs text-muted";
form.appendChild(receipts);
// Send Button Container
const btnDiv = doc.createElement("div");
btnDiv.className = "flex items-center justify-end gap-2";
const sendBtn = doc.createElement("button");
sendBtn.type = "submit";
sendBtn.id = "zapSendBtn";
sendBtn.className = "btn";
sendBtn.setAttribute("aria-label", "Send a zap");
sendBtn.textContent = "Send";
btnDiv.appendChild(sendBtn);
form.appendChild(btnDiv);
wrapper.appendChild(form);
return wrapper;
}
function setupZapButton({ force = false } = {}) {
const zapButton = getChannelZapButton();
if (!zapButton) {
@@ -3895,87 +4002,11 @@ function setupZapButton({ force = false } = {}) {
panel.dataset.state = panel.dataset.state || "closed";
panel.hidden = true;
panel.innerHTML = `
<div class="grid gap-4">
<div class="flex items-center justify-between gap-4">
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted-strong">
Send a zap
</h3>
<button
type="button"
id="zapCloseBtn"
class="btn-ghost px-2 py-1 text-xs"
aria-label="Close zap dialog"
>
✕
</button>
</div>
<p
id="zapWalletPrompt"
class="text-sm text-muted"
aria-hidden="true"
hidden
>
Connect a wallet in
<button
type="button"
id="zapWalletLink"
class="btn-ghost px-2 py-1 text-xs"
>
Wallet Connect settings
</button>
to send zaps.
</p>
<form id="zapForm" class="bv-stack bv-stack--tight">
<div>
<label
for="zapAmountInput"
id="zapAmountLabel"
class="mb-2 block text-xs font-semibold uppercase tracking-wide text-muted-strong"
>
Zap amount (sats)
</label>
<input
id="zapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="input"
placeholder="Enter sats"
aria-labelledby="zapAmountLabel"
/>
</div>
<p
id="zapSplitSummary"
class="text-sm text-muted"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="zapStatus"
class="text-xs text-muted"
role="status"
aria-live="polite"
></p>
<ul
id="zapReceipts"
class="space-y-2 text-xs text-muted"
></ul>
<div class="flex items-center justify-end gap-2">
<button
type="submit"
id="zapSendBtn"
class="btn"
aria-label="Send a zap"
>
Send
</button>
</div>
</form>
</div>
`;
const content = createZapPanelContent(doc);
const grid = doc.createElement("div");
grid.className = "grid gap-4";
grid.appendChild(content);
panel.replaceChildren(grid);
cacheZapPanelElements(panel);
initializeZapPanel();
@@ -4131,23 +4162,25 @@ function renderSubscribeButton(channelHex) {
// Both subscribe/unsubscribe states share the primary styling and icon.
// If you prefer separate logic for unsub, you can do it here.
container.innerHTML = `
<button
id="subscribeToggleBtn"
type="button"
class="btn normal-case rounded hover:opacity-90 focus-visible:bg-primary"
data-state="${alreadySubscribed ? "subscribed" : "unsubscribed"}"
>
<img
src="assets/svg/subscribe-button-icon.svg"
alt="Subscribe Icon"
class="w-5 h-5"
/>
<span>${alreadySubscribed ? "Unsubscribe" : "Subscribe"}</span>
</button>
`;
const toggleBtn = document.createElement("button");
toggleBtn.id = "subscribeToggleBtn";
toggleBtn.type = "button";
toggleBtn.className = "btn normal-case rounded hover:opacity-90 focus-visible:bg-primary";
toggleBtn.dataset.state = alreadySubscribed ? "subscribed" : "unsubscribed";
const icon = document.createElement("img");
icon.src = "assets/svg/subscribe-button-icon.svg";
icon.alt = "Subscribe Icon";
icon.className = "w-5 h-5";
const label = document.createElement("span");
label.textContent = alreadySubscribed ? "Unsubscribe" : "Subscribe";
toggleBtn.appendChild(icon);
toggleBtn.appendChild(label);
container.replaceChildren(toggleBtn);
const toggleBtn = document.getElementById("subscribeToggleBtn");
if (toggleBtn) {
toggleBtn.addEventListener("click", async () => {
const currentApp = getApp();
@@ -4523,7 +4556,10 @@ export async function renderChannelVideosFromList({
if (!Array.isArray(videos) || videos.length === 0) {
if (allowEmptyMessage) {
container.dataset.hasChannelVideos = "false";
container.innerHTML = `<p class="text-muted-strong">No videos to display.</p>`;
const p = document.createElement("p");
p.className = "text-muted-strong";
p.textContent = "No videos to display.";
container.replaceChildren(p);
return true;
}
return false;
@@ -4551,14 +4587,17 @@ export async function renderChannelVideosFromList({
if (!renderableVideos.length) {
if (allowEmptyMessage) {
container.dataset.hasChannelVideos = "false";
container.innerHTML = `<p class="text-muted-strong">No videos to display.</p>`;
const p = document.createElement("p");
p.className = "text-muted-strong";
p.textContent = "No videos to display.";
container.replaceChildren(p);
return true;
}
return false;
}
container.dataset.hasChannelVideos = "true";
container.innerHTML = "";
container.replaceChildren();
const fragment = document.createDocumentFragment();
const allKnownEventsArray = Array.from(nostrClient.allEvents.values());
@@ -4907,7 +4946,10 @@ export async function renderChannelVideosFromList({
if (renderIndex === 0) {
if (allowEmptyMessage) {
container.dataset.hasChannelVideos = "false";
container.innerHTML = `<p class="text-muted-strong">No videos to display.</p>`;
const p = document.createElement("p");
p.className = "text-muted-strong";
p.textContent = "No videos to display.";
container.replaceChildren(p);
return true;
}
return false;
@@ -5294,11 +5336,15 @@ async function loadUserVideos(pubkey) {
if (container) {
container.dataset.loading = "true";
if (!hadExistingContent) {
container.innerHTML = `
<div class="py-16 flex justify-center">
<span class="text-muted animate-pulse">Loading videos…</span>
</div>
`;
const div = document.createElement("div");
div.className = "py-16 flex justify-center";
const span = document.createElement("span");
span.className = "text-muted animate-pulse";
span.textContent = "Loading videos…";
div.appendChild(span);
container.replaceChildren(div);
}
}
@@ -5415,9 +5461,10 @@ async function loadUserVideos(pubkey) {
!hasVisibleContent
) {
container.dataset.hasChannelVideos = "false";
container.innerHTML = `
<p class="text-critical">Failed to load videos. Please try again.</p>
`;
const p = document.createElement("p");
p.className = "text-critical";
p.textContent = "Failed to load videos. Please try again.";
container.replaceChildren(p);
}
userLogger.error("Error loading user videos:", err);
} finally {
-1
View File
@@ -24,7 +24,6 @@ const SOURCE_DIR = join(ROOT, "js");
const BASELINE = {
"js/app.js": 1,
"js/app/feedCoordinator.js": 1,
"js/channelProfile.js": 10,
"js/docsView.js": 8,
"js/exploreView.js": 1,
"js/forYouView.js": 1,
+20
View File
@@ -0,0 +1,20 @@
# TEST LOG: innerHTML Migration for js/channelProfile.js
- **Date:** 2026-02-13
- **Agent:** innerhtml-migration-agent
- **Status:** Verified
## Lint Results
- `npm run lint`: Passed (stylelint skipped due to missing dep, innerhtml passed).
## Unit Test Results
- `npm run test:unit`: Passed (13/13 tests in channelProfile tests implicitly covered by other tests or generic tests passed).
## innerHTML Report
- Before: `js/channelProfile.js` had 10 assignments.
- After: `js/channelProfile.js` has 0 assignments.
- Total reduction: 87 -> 77.
## Manual Verification
- Verified code changes replaced all `innerHTML` with `createElement`, `replaceChildren`, or `textContent`.
- Helper functions created for complex HTML structures.
+12
View File
@@ -0,0 +1,12 @@
# TODO: innerHTML Migration for js/channelProfile.js
- [ ] `list.innerHTML = ""` (line 1202) -> `list.replaceChildren()`
- [ ] `list.innerHTML = ""` (line 1217) -> `list.replaceChildren()`
- [ ] `panel.innerHTML = ...` (line 2049) -> `createZapPanel(doc)`
- [ ] `container.innerHTML = ...` (line 2289) -> `createSubscribeButton(doc)`
- [ ] `container.innerHTML = <p>No videos...</p>` (line 2471) -> `textContent = "No videos to display."` (with class)
- [ ] `container.innerHTML = <p>No videos...</p>` (line 2501) -> `textContent = "No videos to display."` (with class)
- [ ] `container.innerHTML = ""` (line 2505) -> `container.replaceChildren()`
- [ ] `container.innerHTML = <p>No videos...</p>` (line 2736) -> `textContent = "No videos to display."` (with class)
- [ ] `container.innerHTML = ...` (line 2845) -> `createLoadingState(doc)`
- [ ] `container.innerHTML = ...` (line 2928) -> `createErrorState(doc)`