Merge pull request #126 from christianlouis/codex/dashboard-safe-rendering
[codex] harden dashboard domain rendering
This commit is contained in:
@@ -673,34 +673,83 @@ function dashboardApp() {
|
||||
const tableBody = document.getElementById('domains-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
tableBody.textContent = '';
|
||||
|
||||
domains.forEach(domain => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'table-row';
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="table-cell">
|
||||
<div class="font-medium">${domain.domain_name}</div>
|
||||
</td>
|
||||
<td class="table-cell">${domain.total_emails || 0}</td>
|
||||
<td class="table-cell">
|
||||
<span class="inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/20 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">
|
||||
${domain.pass_rate || 0}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="table-cell">${domain.failed_count || 0}</td>
|
||||
<td class="table-cell">${domain.report_count || 0}</td>
|
||||
<td class="table-cell text-right">
|
||||
<a href="/domains/${domain.id}" class="btn btn-outline btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M3 3v18h18"></path><path d="m19 9-5 5-4-4-3 3"></path></svg>
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
`;
|
||||
|
||||
|
||||
row.appendChild(this.createDomainNameCell(domain.domain_name));
|
||||
row.appendChild(this.createTextCell(this.formatLargeNumber(domain.total_emails || 0)));
|
||||
row.appendChild(this.createPassRateCell(domain.pass_rate || 0));
|
||||
row.appendChild(this.createTextCell(this.formatLargeNumber(domain.failed_count || 0)));
|
||||
row.appendChild(this.createTextCell(this.formatLargeNumber(domain.report_count || 0)));
|
||||
row.appendChild(this.createDetailsCell(domain));
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
},
|
||||
|
||||
createDomainNameCell(domainName) {
|
||||
const cell = document.createElement('td');
|
||||
cell.className = 'table-cell';
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'font-medium';
|
||||
content.textContent = domainName || 'Unknown';
|
||||
cell.appendChild(content);
|
||||
|
||||
return cell;
|
||||
},
|
||||
|
||||
createPassRateCell(passRate) {
|
||||
const cell = document.createElement('td');
|
||||
cell.className = 'table-cell';
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/20 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300';
|
||||
badge.textContent = `${passRate}%`;
|
||||
cell.appendChild(badge);
|
||||
|
||||
return cell;
|
||||
},
|
||||
|
||||
createDetailsCell(domain) {
|
||||
const cell = document.createElement('td');
|
||||
cell.className = 'table-cell text-right';
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.className = 'btn btn-outline btn-sm';
|
||||
const domainId = String(domain.id ?? domain.domain_name ?? '');
|
||||
link.href = `/domains/${encodeURIComponent(domainId)}`;
|
||||
link.appendChild(this.createDetailsIcon());
|
||||
link.appendChild(document.createTextNode('Details'));
|
||||
cell.appendChild(link);
|
||||
|
||||
return cell;
|
||||
},
|
||||
|
||||
createDetailsIcon() {
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', '14');
|
||||
svg.setAttribute('height', '14');
|
||||
svg.setAttribute('viewBox', '0 0 24 24');
|
||||
svg.setAttribute('fill', 'none');
|
||||
svg.setAttribute('stroke', 'currentColor');
|
||||
svg.setAttribute('stroke-width', '2');
|
||||
svg.setAttribute('stroke-linecap', 'round');
|
||||
svg.setAttribute('stroke-linejoin', 'round');
|
||||
svg.setAttribute('class', 'mr-1');
|
||||
|
||||
const axis = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
axis.setAttribute('d', 'M3 3v18h18');
|
||||
svg.appendChild(axis);
|
||||
|
||||
const trend = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
trend.setAttribute('d', 'm19 9-5 5-4-4-3 3');
|
||||
svg.appendChild(trend);
|
||||
|
||||
return svg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_dashboard_domain_table_uses_safe_dom_rendering():
|
||||
"""Domain names and counts come from report data and must not be HTML-rendered."""
|
||||
template = (Path(__file__).resolve().parents[1] / "templates" / "index.html").read_text()
|
||||
populate_start = template.index("populateDomainsTable(domains)")
|
||||
helper_start = template.index("createDomainNameCell(domainName)")
|
||||
populate_body = template[populate_start:helper_start]
|
||||
|
||||
assert "innerHTML" not in populate_body
|
||||
assert ".textContent" in populate_body
|
||||
assert "createDomainNameCell" in populate_body
|
||||
assert "createDetailsCell" in populate_body
|
||||
|
||||
|
||||
def test_dashboard_domain_details_links_are_encoded():
|
||||
template = (Path(__file__).resolve().parents[1] / "templates" / "index.html").read_text()
|
||||
|
||||
assert "encodeURIComponent(domainId)" in template
|
||||
Reference in New Issue
Block a user