[security] replace innerHTML usages in js/ui/subscriptionHistoryController.js

- Migrated 6 out of 7 innerHTML assignments to safe DOM APIs (createElement, textContent, replaceChildren).
- Reduced innerHTML baseline for the file from 7 to 1.
- Updated scripts/check-innerhtml.mjs baseline.
- Verified with lint, unit tests, and frontend verification.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-16 18:51:44 +00:00
1 parent 9e6884f6dc
commit 16de3802ab
29 files changed
+13380 -17

No files matched your search

@@ -0,0 +1,17 @@
# innerhtml-migration-agent Completion Log
**Date:** 2026-02-16_18-47-18
**Agent:** innerhtml-migration-agent
**Status:** Completed
## Summary
Migrated `js/ui/subscriptionHistoryController.js` from 7 `innerHTML` usages to 1 (static template load).
Updated baseline in `scripts/check-innerhtml.mjs`.
## Artifacts
Artifacts moved to `docs/agents/task-logs/daily/artifacts/2026-02-16_innerhtml/`.
## Verification
- Lint: Passed
- Unit Tests: Passed
- Frontend Verification: Passed (Modal opens, list renders via DOM APIs)
@@ -0,0 +1,2 @@
Target File: js/ui/subscriptionHistoryController.js
Reason: High innerHTML count (7). Most usages are clear/reset or simple template literals that can be converted to DOM APIs. One usage is template loading which might remain.
@@ -0,0 +1,7 @@
- fetchHistory(): Replaced listContainer.innerHTML = "" with replaceChildren().
- renderList(): Replaced listContainer.innerHTML = "" with replaceChildren().
- renderList(): Replaced item.innerHTML template literal with createElement/appendChild construction. All variables (isBackup, dateStr, timeAgo) are trusted or boolean, but textContent is safer anyway.
- selectEvent(): Replaced selectedChannelsList.innerHTML = "" with replaceChildren().
- selectEvent(): Replaced error message innerHTML with createElement('p') and textContent.
- renderChannels(): Replaced selectedChannelsList.innerHTML = "" with replaceChildren().
- load(): template.innerHTML = html left as-is (static template loading from file).
@@ -0,0 +1,7 @@
- [ ] load(): template.innerHTML = html (Line 26) - Assess if replaceable or keep.
- [ ] fetchHistory(): this.listContainer.innerHTML = "" (Line 118) -> replaceChildren() or textContent=""
- [ ] renderList(): this.listContainer.innerHTML = "" (Line 134) -> replaceChildren() or textContent=""
- [ ] renderList(): item.innerHTML = ... (Line 155) -> Convert to createElement/appendChild
- [ ] selectEvent(): this.selectedChannelsList.innerHTML = "" (Line 185) -> replaceChildren()
- [ ] selectEvent(): this.selectedChannelsList.innerHTML = error msg (Line 226) -> createElement
- [ ] renderChannels(): this.selectedChannelsList.innerHTML = "" (Line 234) -> replaceChildren()
@@ -0,0 +1,34 @@
// Updated BASELINE — copy this into check-innerhtml.mjs:
const BASELINE = {
"js/app.js": 1,
"js/app/feedCoordinator.js": 1,
"js/docsView.js": 8,
"js/exploreView.js": 1,
"js/forYouView.js": 1,
"js/historyView.js": 4,
"js/index.js": 1,
"js/kidsView.js": 1,
"js/ui/components/DeleteModal.js": 3,
"js/ui/components/EditModal.js": 1,
"js/ui/components/EmbedVideoModal.js": 1,
"js/ui/components/EventDetailsModal.js": 1,
"js/ui/components/nip71FormManager.js": 1,
"js/ui/components/ShareNostrModal.js": 3,
"js/ui/components/UploadModal.js": 1,
"js/ui/components/VideoModal.js": 1,
"js/ui/dm/AppShell.js": 1,
"js/ui/dm/Composer.js": 1,
"js/ui/dm/ConversationList.js": 1,
"js/ui/dm/DMRelaySettings.js": 1,
"js/ui/dm/DMSettingsModalController.js": 3,
"js/ui/dm/MessageThread.js": 1,
"js/ui/dm/NotificationCenter.js": 1,
"js/ui/loginModalController.js": 3,
"js/ui/moreMenuController.js": 1,
"js/ui/profileModalController.js": 1,
"js/ui/subscriptionHistoryController.js": 1,
"js/ui/videoListViewController.js": 1,
"js/ui/views/VideoListView.js": 3,
"js/utils/qrcode.js": 2,
"js/viewManager.js": 3,
};
@@ -0,0 +1,46 @@
{
"cadence": "daily",
"date": "2026-02-16",
"locked": [
"docs-alignment-agent",
"docs-code-investigator"
],
"available": [
"audit-agent",
"ci-health-agent",
"const-refactor-agent",
"content-audit-agent",
"decompose-agent",
"deps-security-agent",
"design-system-audit-agent",
"docs-agent",
"innerhtml-migration-agent",
"known-issues-agent",
"load-test-agent",
"nip-research-agent",
"onboarding-audit-agent",
"perf-agent",
"prompt-curator-agent",
"scheduler-update-agent",
"style-agent",
"test-audit-agent",
"todo-triage-agent"
],
"lockCount": 2,
"locks": [
{
"agent": "docs-code-investigator",
"eventId": "a981ab8bfdbdd79493078ddbe85730a20654eb2e04222d74a799be8ced382f7a",
"createdAt": "2026-02-16T17:53:11.000Z",
"expiresAt": "2026-02-16T19:53:11.000Z",
"platform": "jules"
},
{
"agent": "docs-alignment-agent",
"eventId": "af2f3c4b8efb9de54f76e30afd1adc6ed42f5791bde78f731fe0c7b10586d626",
"createdAt": "2026-02-16T16:50:36.000Z",
"expiresAt": "2026-02-16T18:50:36.000Z",
"platform": "jules"
}
]
}
@@ -0,0 +1,33 @@
innerHTML usage: 60 assignments across 31 files
js/docsView.js: 8
js/ui/subscriptionHistoryController.js: 7
js/historyView.js: 4
js/ui/components/DeleteModal.js: 3
js/ui/components/ShareNostrModal.js: 3
js/ui/dm/DMSettingsModalController.js: 3
js/ui/loginModalController.js: 3
js/ui/views/VideoListView.js: 3
js/viewManager.js: 3
js/utils/qrcode.js: 2
js/app/feedCoordinator.js: 1
js/app.js: 1
js/exploreView.js: 1
js/forYouView.js: 1
js/index.js: 1
js/kidsView.js: 1
js/ui/components/EditModal.js: 1
js/ui/components/EmbedVideoModal.js: 1
js/ui/components/EventDetailsModal.js: 1
js/ui/components/UploadModal.js: 1
js/ui/components/VideoModal.js: 1
js/ui/components/nip71FormManager.js: 1
js/ui/dm/AppShell.js: 1
js/ui/dm/Composer.js: 1
js/ui/dm/ConversationList.js: 1
js/ui/dm/DMRelaySettings.js: 1
js/ui/dm/MessageThread.js: 1
js/ui/dm/NotificationCenter.js: 1
js/ui/moreMenuController.js: 1
js/ui/profileModalController.js: 1
js/ui/videoListViewController.js: 1
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

@@ -0,0 +1,40 @@
from playwright.sync_api import sync_playwright
def run(playwright):
browser = playwright.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
# Go to app with test mode enabled
page.goto("http://localhost:8000/?__test__=1")
# Wait for the app to load
page.wait_for_load_state("networkidle")
# Wait for bitvidApp to be available on window
page.wait_for_function("window.bitvidApp !== undefined")
# Wait for profileController to be initialized
page.wait_for_function("window.bitvidApp.profileController !== undefined")
page.wait_for_function("window.bitvidApp.profileController.subscriptionHistoryController !== undefined")
# Open the subscription history modal with a dummy pubkey
# Use the nested controller
page.evaluate("""
const pubkey = 'bce1bf7313e788f8d702cfa23759cfde9d980c70a6ba839e02d4f872541b1828'; // dummy hex
window.bitvidApp.profileController.subscriptionHistoryController.show(pubkey);
""")
# Wait for modal to be visible
page.wait_for_selector("#subscriptionHistoryModal:not(.hidden)")
# Wait a bit for potential fetch/render (it will show empty or error)
page.wait_for_timeout(2000)
# Take screenshot of the modal
page.screenshot(path="verification/subscription_history_modal.png")
browser.close()
with sync_playwright() as playwright:
run(playwright)
+32 -16
View File
@@ -117,7 +117,7 @@ export class SubscriptionHistoryController {
async fetchHistory() {
if (!this.loadingIndicator) return;
this.loadingIndicator.classList.remove("hidden");
this.listContainer.innerHTML = "";
this.listContainer.replaceChildren();
this.emptyIndicator.classList.add("hidden");
try {
@@ -133,7 +133,7 @@ export class SubscriptionHistoryController {
}
renderList() {
this.listContainer.innerHTML = "";
this.listContainer.replaceChildren();
if (!this.historyEvents.length) {
this.emptyIndicator.classList.remove("hidden");
return;
@@ -154,17 +154,30 @@ export class SubscriptionHistoryController {
});
const timeAgo = formatTimeAgo(event.created_at);
item.innerHTML = `
<div class="flex justify-between items-start">
<div>
<p class="text-sm font-medium text-text">${
isBackup ? "Backup" : "Standard List"
}</p>
<p class="text-xs text-muted">${dateStr}</p>
</div>
<span class="text-xs text-muted whitespace-nowrap ml-2">${timeAgo}</span>
</div>
`;
const divFlex = document.createElement("div");
divFlex.className = "flex justify-between items-start";
const divLeft = document.createElement("div");
const pTitle = document.createElement("p");
pTitle.className = "text-sm font-medium text-text";
pTitle.textContent = isBackup ? "Backup" : "Standard List";
const pDate = document.createElement("p");
pDate.className = "text-xs text-muted";
pDate.textContent = dateStr;
divLeft.appendChild(pTitle);
divLeft.appendChild(pDate);
const spanTime = document.createElement("span");
spanTime.className = "text-xs text-muted whitespace-nowrap ml-2";
spanTime.textContent = timeAgo;
divFlex.appendChild(divLeft);
divFlex.appendChild(spanTime);
item.appendChild(divFlex);
item.addEventListener("click", () => {
this.listContainer.querySelectorAll("li").forEach((li) => {
@@ -184,7 +197,7 @@ export class SubscriptionHistoryController {
this.selectedEvent = event;
this.detailsEmpty.classList.add("hidden");
this.detailsContainer.classList.remove("hidden");
this.selectedChannelsList.innerHTML = "";
this.selectedChannelsList.replaceChildren();
this.decryptingIndicator.classList.remove("hidden");
const date = new Date(event.created_at * 1000);
@@ -225,7 +238,10 @@ export class SubscriptionHistoryController {
this.renderChannels(pubkeys);
} catch (error) {
userLogger.error("Failed to decrypt event:", error);
this.selectedChannelsList.innerHTML = `<p class="text-critical text-sm text-center w-full mt-4">Decryption failed or content is invalid.</p>`;
const errorP = document.createElement("p");
errorP.className = "text-critical text-sm text-center w-full mt-4";
errorP.textContent = "Decryption failed or content is invalid.";
this.selectedChannelsList.replaceChildren(errorP);
this.selectedMeta.textContent = "Unknown content";
} finally {
this.decryptingIndicator.classList.add("hidden");
@@ -233,7 +249,7 @@ export class SubscriptionHistoryController {
}
async renderChannels(pubkeys) {
this.selectedChannelsList.innerHTML = "";
this.selectedChannelsList.replaceChildren();
const app = getApplication();
if (app && typeof app.batchFetchProfiles === "function") {
+1 -1
View File
@@ -48,7 +48,7 @@ const BASELINE = {
"js/ui/loginModalController.js": 3,
"js/ui/moreMenuController.js": 1,
"js/ui/profileModalController.js": 1,
"js/ui/subscriptionHistoryController.js": 7,
"js/ui/subscriptionHistoryController.js": 1,
"js/ui/videoListViewController.js": 1,
"js/ui/views/VideoListView.js": 3,
"js/utils/qrcode.js": 2,