import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import { getToolWebviewStyles, 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 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()) { const files = await collectHtmlFiles(distPath); panel.webview.postMessage({ type: 'filesPicked', paths: files }); } } } else if (msg.type === 'pickFolder') { const picked = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select Folder', }); if (picked && picked[0]) { const files = await collectHtmlFiles(picked[0].fsPath); panel.webview.postMessage({ type: 'filesPicked', paths: files }); } } else if (msg.type === 'pickFiles') { const picked = await vscode.window.showOpenDialog({ canSelectFolders: false, canSelectFiles: true, canSelectMany: true, openLabel: 'Select Files', filters: { 'HTML': ['html', 'htm'] }, }); if (picked && picked.length) { panel.webview.postMessage({ type: 'filesPicked', paths: picked.map(u => u.fsPath) }); } } else if (msg.type === 'scan') { const folder: string | undefined = msg.folder; const files: string[] | undefined = msg.files; let targets: string[] = []; let baseDir = ''; if (files && files.length) { targets = files.filter(f => fs.existsSync(f)); baseDir = targets.length ? commonBaseDir(targets) : ''; } else if (folder && fs.existsSync(folder)) { targets = await collectHtmlFiles(folder); baseDir = folder; } else { panel.webview.postMessage({ type: 'results', results: [], error: 'Pick a folder or files first' }); return; } const results: { file: string; ok: boolean; assets: string[] }[] = []; for (const file of targets) { try { const content = fs.readFileSync(file, 'utf8'); const offenders = findNonBase64Assets(content); const display = baseDir ? path.relative(baseDir, file) || path.basename(file) : file; results.push({ file: display, ok: offenders.length === 0, assets: offenders }); } catch { // skip unreadable } } panel.webview.postMessage({ type: 'results', results }); } }); } function commonBaseDir(files: string[]): string { if (files.length === 0) return ''; if (files.length === 1) return path.dirname(files[0]); const split = files.map(f => path.dirname(f).split(path.sep)); const first = split[0]; let i = 0; while (i < first.length && split.every(parts => parts[i] === first[i])) i++; return first.slice(0, i).join(path.sep); } async function collectHtmlFiles(root: string): Promise { 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(/]*>[\s\S]*?<\/script>/gi, ''); const offenders = new Set(); 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 `

Base64 Scanner

Scan folders or selected HTML files for asset references that are not embedded as base64 data URIs.

Inputs

`; }