Files
Hellion-NewTab/src/js/data.js
T
JonKazama-Hellion a3e21a760f fix(security): harden JSON import with URL validation and immutable mapping
Add isSafeUrl() to block javascript:/data: URLs in imported bookmarks.
Replace mutable object mutation with immutable .map() and string length limits.
Use Notes.init()/Calculator.load()/Timer.load() instead of direct internal
mutation after import.
2026-04-16 20:20:38 +02:00

157 lines
6.4 KiB
JavaScript

/* =============================================
HELLION NEWTAB — data.js
JSON Export / Import (Backup & Restore)
============================================= */
function initDataButtons() {
const btnExport = document.getElementById('btnExportJSON');
const btnImport = document.getElementById('btnImportJSON');
const jsonInput = document.getElementById('jsonImportInput');
if (!btnExport || !btnImport) return;
/**
* Prueft ob eine URL ein sicheres Protokoll hat.
* Blockiert javascript:, data:, vbscript: etc.
* @param {string} url
* @returns {boolean}
*/
function isSafeUrl(url) {
try {
const u = new URL(url);
return ['http:', 'https:', 'ftp:'].includes(u.protocol);
} catch {
return false;
}
}
// Export (inkl. Notes)
btnExport.addEventListener('click', async () => {
const widgetData = await Store.get('widgetStates');
const data = {
version: '2.0.0',
exported: new Date().toISOString(),
boards,
settings,
notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : [],
calculator: widgetData && widgetData.calculator ? widgetData.calculator.history || [] : [],
timerPresets: widgetData && widgetData.timer ? widgetData.timer.presets || [] : []
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `hellion-newtab-backup-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
});
// Import
btnImport.addEventListener('click', () => jsonInput.click());
jsonInput.addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
try {
const data = JSON.parse(await file.text());
if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
const validBoards = data.boards
.filter(b => b && typeof b.title === 'string' && Array.isArray(b.bookmarks))
.map(b => ({
id: b.id || uid(),
title: String(b.title).slice(0, 100),
blurred: !!b.blurred,
bookmarks: b.bookmarks
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
.map(bm => ({
id: bm.id || uid(),
title: String(bm.title).slice(0, 200),
url: bm.url,
desc: String(bm.desc || '').slice(0, 500)
}))
}));
if (validBoards.length === 0) throw new Error(t('data.no_boards'));
const ok = await HellionDialog.confirm(
t('data.import_confirm', { count: validBoards.length }),
{ type: 'info', title: t('data.import_confirm.title') }
);
if (!ok) return;
boards = [...boards, ...validBoards];
await saveBoards();
renderBoards();
// Notes importieren (falls vorhanden)
let notesImported = 0;
const existingWidgets = await Store.get('widgetStates') || {};
if (Array.isArray(data.notes) && data.notes.length > 0) {
const existingNotes = Array.isArray(existingWidgets.notes) ? existingWidgets.notes : [];
const importNotes = data.notes
.filter(n => n && n.id && n.template)
.map(n => ({
id: n.id,
template: ['note', 'checklist'].includes(n.template) ? n.template : 'note',
title: String(n.title || '').slice(0, 200),
content: String(n.content || '').slice(0, 5000),
x: typeof n.x === 'number' ? n.x : 120,
y: typeof n.y === 'number' ? n.y : 80,
width: typeof n.width === 'number' ? n.width : 280,
height: typeof n.height === 'number' ? n.height : 220,
open: n.open !== false,
checklistItems: Array.isArray(n.checklistItems) ? n.checklistItems : []
}));
// Limit beachten
const spaceLeft = Notes.MAX_NOTES - existingNotes.length;
const toImport = importNotes.slice(0, spaceLeft);
if (toImport.length > 0) {
const merged = [...existingNotes, ...toImport];
existingWidgets.notes = merged;
notesImported = toImport.length;
}
}
// Calculator-History importieren (falls vorhanden)
let calcImported = false;
if (Array.isArray(data.calculator) && data.calculator.length > 0) {
const calcHistory = data.calculator.filter(h => h && typeof h.expr === 'string' && typeof h.result === 'string');
if (calcHistory.length > 0) {
if (!existingWidgets.calculator) {
existingWidgets.calculator = { x: 400, y: 120, width: 280, height: 400, open: false, history: [] };
}
existingWidgets.calculator.history = calcHistory.slice(0, Calculator.MAX_HISTORY);
calcImported = true;
}
}
// Timer-Presets importieren (falls vorhanden)
let timerImported = false;
if (Array.isArray(data.timerPresets) && data.timerPresets.length > 0) {
const validPresets = data.timerPresets.filter(p => p && typeof p.name === 'string' && typeof p.seconds === 'number');
if (validPresets.length > 0) {
if (!existingWidgets.timer) {
existingWidgets.timer = { x: 600, y: 80, width: 260, height: 360, open: false, presets: [] };
}
existingWidgets.timer.presets = validPresets.slice(0, Timer.MAX_PRESETS);
timerImported = true;
}
}
// Gemeinsam speichern
await Store.set('widgetStates', existingWidgets);
// Widget-Module neu aus Storage laden (kein direkter Zugriff auf Internals)
if (notesImported > 0) await Notes.init();
if (calcImported) await Calculator.load();
if (timerImported) await Timer.load();
const noteMsg = notesImported > 0 ? t('data.notes_suffix', { count: notesImported }) : '';
const calcMsg = calcImported ? t('data.calc_suffix') : '';
const timerMsg = timerImported ? t('data.timer_suffix') : '';
await HellionDialog.alert(
t('data.import_success', { boards: validBoards.length, notes: noteMsg, calc: calcMsg, timer: timerMsg }),
{ type: 'success', title: t('data.import_success.title') }
);
} catch (err) {
await HellionDialog.alert(t('data.import_error', { error: err.message }), { type: 'danger', title: t('data.import_error.title') });
}
e.target.value = '';
});
}