first commit
This commit is contained in:
192
src/tools/base64Scanner.ts
Normal file
192
src/tools/base64Scanner.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { singletonPanel } from './shared';
|
||||
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
export function openBase64Scanner(_context: vscode.ExtensionContext) {
|
||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.base64Scanner', 'Base64 Asset Scanner');
|
||||
if (!isNew) return;
|
||||
|
||||
panel.webview.html = getHtml();
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
if (msg.type === 'ready') {
|
||||
const ws = vscode.workspace.workspaceFolders?.[0];
|
||||
if (ws) {
|
||||
const distPath = path.join(ws.uri.fsPath, 'dist');
|
||||
if (fs.existsSync(distPath) && fs.statSync(distPath).isDirectory()) {
|
||||
panel.webview.postMessage({ type: 'folderPicked', path: distPath });
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'pickFolder') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectFolders: true,
|
||||
canSelectFiles: false,
|
||||
canSelectMany: false,
|
||||
openLabel: 'Select Folder',
|
||||
});
|
||||
if (picked && picked[0]) {
|
||||
panel.webview.postMessage({ type: 'folderPicked', path: picked[0].fsPath });
|
||||
}
|
||||
} else if (msg.type === 'scan') {
|
||||
const folder: string = msg.folder;
|
||||
if (!folder || !fs.existsSync(folder)) {
|
||||
panel.webview.postMessage({ type: 'results', results: [], error: 'Invalid folder' });
|
||||
return;
|
||||
}
|
||||
const htmlFiles = await collectHtmlFiles(folder);
|
||||
const results: { file: string; ok: boolean; assets: string[] }[] = [];
|
||||
for (const file of htmlFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
const offenders = findNonBase64Assets(content);
|
||||
results.push({ file: path.relative(folder, file), ok: offenders.length === 0, assets: offenders });
|
||||
} catch {
|
||||
// skip unreadable
|
||||
}
|
||||
}
|
||||
panel.webview.postMessage({ type: 'results', results });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function collectHtmlFiles(root: string): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
const stack: string[] = [root];
|
||||
while (stack.length) {
|
||||
const dir = stack.pop()!;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const full = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
if (e.name === 'node_modules' || e.name === '.git') continue;
|
||||
stack.push(full);
|
||||
} else if (e.isFile() && /\.html?$/i.test(e.name)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function findNonBase64Assets(html: string): string[] {
|
||||
const markup = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
||||
const offenders = new Set<string>();
|
||||
const attrRegex = /\b(?:src|href|poster|data-src)\s*=\s*("([^"]*)"|'([^']*)')/gi;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = attrRegex.exec(markup)) !== null) {
|
||||
const url = (m[2] ?? m[3] ?? '').trim();
|
||||
if (!url) continue;
|
||||
if (isAssetReference(url) && !isBase64DataUri(url)) {
|
||||
offenders.add(url);
|
||||
}
|
||||
}
|
||||
const cssUrlRegex = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)/gi;
|
||||
while ((m = cssUrlRegex.exec(markup)) !== null) {
|
||||
const url = (m[1] ?? m[2] ?? m[3] ?? '').trim();
|
||||
if (!url) continue;
|
||||
if (!isBase64DataUri(url)) {
|
||||
offenders.add(url);
|
||||
}
|
||||
}
|
||||
return [...offenders];
|
||||
}
|
||||
|
||||
function isBase64DataUri(url: string): boolean {
|
||||
return /^data:[^;,]*;base64,/i.test(url);
|
||||
}
|
||||
|
||||
function isAssetReference(url: string): boolean {
|
||||
if (url.startsWith('#') || url.toLowerCase().startsWith('javascript:')) return false;
|
||||
if (/^(mailto:|tel:)/i.test(url)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function getHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); padding: 12px; color: var(--vscode-foreground); }
|
||||
.row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
|
||||
input[type=text] { flex: 1; padding: 4px 6px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); }
|
||||
button { padding: 4px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; cursor: pointer; }
|
||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||
.row-result { display: flex; gap: 8px; padding: 2px 0; font-family: var(--vscode-editor-font-family); font-size: 12px; }
|
||||
.mark { width: 14px; flex-shrink: 0; font-weight: bold; }
|
||||
.ok { color: #3fb950; }
|
||||
.bad { color: #f85149; }
|
||||
.file-name { word-break: break-all; }
|
||||
.assets { color: var(--vscode-descriptionForeground); margin-left: 6px; word-break: break-all; }
|
||||
.summary { margin-top: 12px; opacity: 0.8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Base64 Asset Scanner</h2>
|
||||
<div class="row">
|
||||
<input id="folder" type="text" placeholder="Folder to scan recursively..." />
|
||||
<button id="pick">Browse...</button>
|
||||
<button id="scan">Scan</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<div id="results"></div>
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
vscode.postMessage({ type: 'ready' });
|
||||
const folderEl = document.getElementById('folder');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
|
||||
document.getElementById('pick').addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'pickFolder' });
|
||||
});
|
||||
document.getElementById('scan').addEventListener('click', () => {
|
||||
const folder = folderEl.value.trim();
|
||||
if (!folder) { statusEl.textContent = 'Pick a folder first.'; return; }
|
||||
statusEl.textContent = 'Scanning...';
|
||||
resultsEl.innerHTML = '';
|
||||
vscode.postMessage({ type: 'scan', folder });
|
||||
});
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const msg = event.data;
|
||||
if (msg.type === 'folderPicked') {
|
||||
folderEl.value = msg.path;
|
||||
} else if (msg.type === 'results') {
|
||||
if (msg.error) { statusEl.textContent = 'Error: ' + msg.error; return; }
|
||||
const total = msg.results.length;
|
||||
const flagged = msg.results.filter(r => !r.ok).length;
|
||||
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' HTML file(s). ' + flagged + ' contain non-base64 assets.</div>';
|
||||
resultsEl.innerHTML = '';
|
||||
for (const r of msg.results) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row-result';
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'mark ' + (r.ok ? 'ok' : 'bad');
|
||||
mark.textContent = r.ok ? '✓' : '✗';
|
||||
const name = document.createElement('span');
|
||||
name.className = 'file-name';
|
||||
name.textContent = r.file;
|
||||
row.appendChild(mark);
|
||||
row.appendChild(name);
|
||||
if (!r.ok) {
|
||||
const a = document.createElement('span');
|
||||
a.className = 'assets';
|
||||
a.textContent = '— ' + r.assets.join(', ');
|
||||
row.appendChild(a);
|
||||
}
|
||||
resultsEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
Reference in New Issue
Block a user