fix(security): harden bookmark parsing and CSV export
This commit is contained in:
92
index.html
Normal file
92
index.html
Normal 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>
|
||||
Reference in New Issue
Block a user