diff --git a/datenschutz.html b/datenschutz.html index 96e2618..a261cec 100644 --- a/datenschutz.html +++ b/datenschutz.html @@ -116,6 +116,14 @@ insbesondere keine Schriftarten und keine Skripte von Content-Delivery-Netzwerken. Eine Einwilligungsabfrage ist deshalb nicht erforderlich.

+

+ Eine Ausnahme betrifft die optionale Vorschau: Enthält der von Ihnen eingefuegte Text + Bilder, die auf fremde Server verweisen, lädt Ihr Browser diese Bilder beim Anzeigen der + Vorschau von dort. Dabei erfährt der jeweilige fremde Server Ihre IP-Adresse. Übertragen + wird dabei nichts von Ihrem Text; die Herkunft wird durch die Einstellung + Referrer-Policy: no-referrer nicht mitgesendet. Wenn Sie das + vermeiden möchten, lassen Sie die Vorschau ausgeschaltet. +

8. Ihre Rechte

diff --git a/icon-mono.svg b/icon-mono.svg new file mode 100644 index 0000000..b1f1129 --- /dev/null +++ b/icon-mono.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icon.svg b/icon.svg index 7cd5a88..29fc33d 100644 --- a/icon.svg +++ b/icon.svg @@ -1,6 +1,8 @@ - + - m↓ + + + + + diff --git a/index.html b/index.html index 8346f90..9cac78f 100644 --- a/index.html +++ b/index.html @@ -472,7 +472,7 @@ pattern: LS.get('pattern', 'title') }; - var filenameEdited = false, forceRaw = false, toastTimer = null, previewOn = LS.get('preview', false); + var filenameEdited = false, forceRaw = false, forceRawTimer = null, toastTimer = null, previewOn = LS.get('preview', false); // ---------- Turndown (rebuilt on settings change) ---------- var td; @@ -497,7 +497,7 @@ var UML={ 'ä':'ae','ö':'oe','ü':'ue','ß':'ss','Ä':'ae','Ö':'oe','Ü':'ue' }; function slugify(s){ if(!s) return ''; - s=s.replace(/[äöüßÄÖÜ]/g,function(c){return UML[c]||c;}).normalize('NFKD').replace(/[̀-ͯ]/g,''); + s=s.replace(/[äöüßÄÖÜ]/g,function(c){return UML[c]||c;}).normalize('NFKD').replace(/[\u0300-\u036f]/g,''); return s.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,60).replace(/-+$/g,''); } function deriveTitle(md){ var L=md.split(/\r?\n/), i, m, t; for(i=0;i"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); } + function escapeHtml(s){ return s.replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } function renderPreview(){ if(!previewOn) return; var md=editor.value, fmHtml=''; // Show YAML front matter as a compact metadata block, not as body text. @@ -564,8 +564,15 @@ forceRaw=false; if(out==null) return; e.preventDefault(); insertAtCursor(out); showSource(key,ok,true); refreshAll(true); }); - function insertAtCursor(str){ var s=editor.selectionStart,en=editor.selectionEnd; - editor.value=editor.value.slice(0,s)+str+editor.value.slice(en); var p=s+str.length; editor.selectionStart=editor.selectionEnd=p; } + function insertAtCursor(str){ + // execCommand haelt den nativen Undo-Stack der Textarea intakt (Cmd+Z nach dem Einfuegen). + editor.focus(); + var ok=false; + try { ok = document.execCommand('insertText', false, str); } catch(e){ ok=false; } + if(ok) return; + var s=editor.selectionStart,en=editor.selectionEnd; + editor.value=editor.value.slice(0,s)+str+editor.value.slice(en); + var p=s+str.length; editor.selectionStart=editor.selectionEnd=p; } editor.addEventListener('input', function(){ hideSource(); refreshAll(false); }); @@ -575,10 +582,15 @@ editor.addEventListener('drop', function(e){ var f=e.dataTransfer&&e.dataTransfer.files&&e.dataTransfer.files[0]; main.classList.remove('drag'); if(!f) return; e.preventDefault(); var r=new FileReader(); + if(f.size > 10*1024*1024){ toast(lang==='de'?'Datei zu gross (max. 10 MB)':'File too large (max 10 MB)', true); return; } + if(editor.value.trim() && !confirm(lang==='de'?'Editor-Inhalt durch die Datei ersetzen?':'Replace the editor content with this file?')) return; + r.onerror=function(){ toast(lang==='de'?'Datei nicht lesbar':'Could not read file', true); }; r.onload=function(){ var txt=String(r.result||''); var name=f.name.toLowerCase(); if(/\.html?$/.test(name)){ try{ txt=td.turndown(txt).replace(/\n{3,}/g,'\n\n').trim(); showSource('src_conv',true,true); }catch(_){ showSource('src_fail',false,true);} } else { showSource('src_raw',false,true); } - editor.value=txt; if(!filenameEdited && /\.(md|markdown|txt|html?)$/.test(name)){ } refreshAll(true); toast(t('loaded')); }; + editor.value=txt; + if(!filenameEdited){ var base=f.name.replace(/\.[^.]+$/,''); if(base){ filename.value=slugify(base)||computeFilename(txt); } } + refreshAll(true); toast(t('loaded')); }; r.readAsText(f); }); @@ -608,7 +620,7 @@ // ---------- Keyboard ---------- document.addEventListener('keydown', function(e){ var mod=e.metaKey||e.ctrlKey; if(mod && !e.shiftKey && !e.altKey && (e.key==='s'||e.key==='S')){ e.preventDefault(); save(); return; } - if(mod && e.shiftKey && (e.key==='v'||e.key==='V')){ forceRaw=true; } + if(mod && e.shiftKey && (e.key==='v'||e.key==='V')){ forceRaw=true; clearTimeout(forceRawTimer); forceRawTimer=setTimeout(function(){ forceRaw=false; },3000); } if(e.key==='Escape'){ closeSettings(); } }); // ---------- Preview toggle ---------- diff --git a/nginx.conf b/nginx.conf index f50bdd8..f030789 100644 --- a/nginx.conf +++ b/nginx.conf @@ -40,8 +40,17 @@ server { try_files $uri $uri.html $uri/ /index.html; } - # Minimal hardening headers. + # Hardening headers. add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "no-referrer" always; + add_header Strict-Transport-Security "max-age=31536000" always; + add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()" always; + + # Content Security Policy. + # connect-src 'none' ist hier das eigentliche Versprechen: der Browser selbst + # unterbindet jede fetch/XHR/WebSocket-Verbindung. "Nothing is uploaded" ist damit + # nicht nur behauptet, sondern erzwungen. img-src erlaubt https, damit Bilder aus + # eingefuegtem Markdown in der Vorschau sichtbar bleiben. + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self'; connect-src 'none'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'; manifest-src 'self'; worker-src 'self'" always; }