fix(security): harden bookmark parsing and CSV export

This commit is contained in:
Julian F.
2026-10-14 09:05:20 -04:00
commit e6912acbb7
3 changed files with 345 additions and 0 deletions

92
index.html Normal file
View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-GrbW/7V8Uz8EhUWpt5wG+cXD0+oSGGwhtN58oOLkO80='; style-src 'sha256-obdhBsAz/wb7TcBO6Wel5haALofvN0sAMw8qwcpWG8c='; base-uri 'none'; form-action 'none'">
<title>本地安全书签转换器</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; text-align: center; background: #f9f9f9; }
.box { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
input { margin: 20px 0; }
button { background: #4CAF50; color: white; border: none; padding: 10px 20px; font-size: 16px; border-radius: 4px; cursor: pointer; }
button:hover { background: #45a049; }
#status { margin-top: 15px; color: #666; }
</style>
</head>
<body>
<div class="box">
<h2>本地离线书签转换器</h2>
<p>所有处理均在您的电脑本地完成,<b>不会上传书签文件</b>。</p>
<input type="file" id="fileInput" accept=".html"><br>
<button id="convertButton" type="button">一键转换并下载 CSV</button>
<p id="status" role="status"></p>
</div>
<script>
function toCsvField(value) {
// A visible text prefix avoids spreadsheet formula interpretation.
if (/^[\s\u0000-\u001f\u007f]*[=+@\-=+@-]/u.test(value) || /^[\u0000-\u001f\u007f]/u.test(value)) {
value = `文本:${value}`;
}
return `"${value.replace(/"/g, '""')}"`;
}
function bookmarkHtmlToCsv(htmlContent) {
// Template contents stay in an inert document and are never mounted.
const template = document.createElement('template');
template.innerHTML = htmlContent;
const rows = ['\uFEFFTitle,URL'];
let count = 0;
template.content.querySelectorAll('a[href]').forEach(link => {
let url;
try {
url = new URL(link.getAttribute('href'));
} catch {
return;
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
rows.push(`${toCsvField(link.textContent)},${toCsvField(url.href)}`);
count++;
});
return { csvContent: rows.join('\r\n') + '\r\n', count };
}
function convertBookmarks() {
const fileInput = document.getElementById('fileInput');
const status = document.getElementById('status');
if (fileInput.files.length === 0) {
alert('请先选择从 Chrome 导出的 bookmarks.html 文件!');
return;
}
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const htmlContent = e.target.result;
const { csvContent, count } = bookmarkHtmlToCsv(htmlContent);
// 触发本地浏览器下载
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'bookmarks_output.csv';
document.body.appendChild(a);
try {
a.click();
} finally {
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
status.textContent = `🎉 转换成功!共提取了 ${count} 个书签,已自动下载 CSV 文件。`;
};
reader.onerror = () => {
status.textContent = '无法读取文件,请重新选择文件后重试。';
};
reader.readAsText(file);
}
document.getElementById('convertButton').addEventListener('click', convertBookmarks);
</script>
</body>
</html>