Skip to content

Commit 5801356

Browse files
authored
Update csp.html
1 parent 488e39b commit 5801356

1 file changed

Lines changed: 312 additions & 1 deletion

File tree

docs/public/csp.html

Lines changed: 312 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,4 +444,315 @@ <h2>Generated policy</h2>
444444
return {cls:'s-open', label:'EXTERNAL'};
445445
}
446446

447-
// ---------------- toast ------------
447+
// ---------------- toast ----------------
448+
var toastTimer = null;
449+
function toast(msg){
450+
var el = $('toast');
451+
el.textContent = msg;
452+
el.classList.add('show');
453+
clearTimeout(toastTimer);
454+
toastTimer = setTimeout(function(){ el.classList.remove('show'); }, 2200);
455+
}
456+
457+
// ---------------- bulk panel ----------------
458+
function renderBulkDirectives(){
459+
var c = $('bulkDirectives');
460+
c.innerHTML = DIRECTIVES.map(function(d){
461+
var active = state.bulkDirectives.has(d.key) ? ' active' : '';
462+
return '<button type="button" class="chip-toggle' + active + '" data-key="' + d.key + '">' + d.key + '</button>';
463+
}).join('');
464+
}
465+
function renderBulkKeywords(){
466+
var c = $('bulkKeywords');
467+
c.innerHTML = KEYWORDS.map(function(k){
468+
var active = state.bulkKeywords.has(k.tok) ? ' active' : '';
469+
return '<button type="button" class="chip-toggle' + active + '" data-tok="' + encodeURIComponent(k.tok) + '">' + k.label + '</button>';
470+
}).join('');
471+
}
472+
473+
$('bulkDirectives').addEventListener('click', function(e){
474+
var btn = e.target.closest('.chip-toggle');
475+
if(!btn) return;
476+
var key = btn.dataset.key;
477+
if(state.bulkDirectives.has(key)) state.bulkDirectives.delete(key); else state.bulkDirectives.add(key);
478+
renderBulkDirectives();
479+
});
480+
$('bulkKeywords').addEventListener('click', function(e){
481+
var btn = e.target.closest('.chip-toggle');
482+
if(!btn) return;
483+
var tok = decodeURIComponent(btn.dataset.tok);
484+
if(state.bulkKeywords.has(tok)) state.bulkKeywords.delete(tok); else state.bulkKeywords.add(tok);
485+
renderBulkKeywords();
486+
});
487+
$('bulkSelectAll').addEventListener('click', function(){
488+
DIRECTIVES.forEach(function(d){ state.bulkDirectives.add(d.key); });
489+
renderBulkDirectives();
490+
});
491+
$('bulkSelectNone').addEventListener('click', function(){
492+
state.bulkDirectives.clear();
493+
renderBulkDirectives();
494+
});
495+
496+
function flashCard(key){
497+
var el = document.querySelector('.dcard[data-key="' + key + '"]');
498+
if(!el) return;
499+
el.classList.add('flash');
500+
setTimeout(function(){ el.classList.remove('flash'); }, 500);
501+
}
502+
503+
$('bulkApplyBtn').addEventListener('click', function(){
504+
if(state.bulkDirectives.size === 0){ toast('Pick at least one directive first'); return; }
505+
var custom = parseCustom($('bulkCustom').value);
506+
var tokens = Array.from(state.bulkKeywords).concat(custom);
507+
if(tokens.length === 0){ toast('Pick or type at least one source'); return; }
508+
state.bulkDirectives.forEach(function(key){
509+
tokens.forEach(function(t){ state.sources[key].add(t); });
510+
flashCard(key);
511+
});
512+
toast('Added ' + tokens.length + ' source' + (tokens.length>1?'s':'') + ' to ' + state.bulkDirectives.size + ' directive' + (state.bulkDirectives.size>1?'s':''));
513+
renderCards();
514+
renderOutput();
515+
});
516+
517+
// ---------------- directive cards ----------------
518+
function cardHTML(d){
519+
var set = state.sources[d.key];
520+
var gate = gateInfo(set);
521+
var chips = Array.from(set).map(function(tok){
522+
var risky = isRisky(tok) ? ' risky' : '';
523+
return '<span class="chip' + risky + '">' + escapeHtml(tok) +
524+
'<button type="button" class="chip-x" data-key="' + d.key + '" data-tok="' + encodeURIComponent(tok) + '">&times;</button></span>';
525+
}).join('');
526+
var quick = KEYWORDS.map(function(k){
527+
var on = set.has(k.tok) ? ' on' : '';
528+
return '<button type="button" class="quick-btn' + on + '" data-key="' + d.key + '" data-tok="' + encodeURIComponent(k.tok) + '">' + k.label + '</button>';
529+
}).join('');
530+
return '' +
531+
'<div class="dcard ' + gate.cls + '" data-key="' + d.key + '">' +
532+
'<div class="dcard-head"><span class="dname">' + d.key + '</span><span class="gate-status">' + gate.label + '</span></div>' +
533+
'<p class="ddesc">' + d.desc + '</p>' +
534+
'<div class="chips">' + chips + '</div>' +
535+
'<div class="quick-row">' + quick + '</div>' +
536+
'<div class="add-row">' +
537+
'<input type="text" placeholder="add source(s), space or comma separated" data-key="' + d.key + '" class="dadd-input">' +
538+
'<button type="button" class="dadd-btn" data-key="' + d.key + '">add</button>' +
539+
'</div>' +
540+
'</div>';
541+
}
542+
543+
function escapeHtml(s){
544+
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
545+
}
546+
547+
function renderCards(){
548+
var core = DIRECTIVES.filter(function(d){ return d.core; });
549+
var advanced = DIRECTIVES.filter(function(d){ return !d.core; });
550+
$('coreGrid').innerHTML = core.map(cardHTML).join('');
551+
$('advancedGrid').innerHTML = advanced.map(cardHTML).join('');
552+
$('advancedGrid').style.display = state.showAdvanced ? 'grid' : 'none';
553+
$('advancedToggle').textContent = state.showAdvanced
554+
? '— Hide advanced directives'
555+
: '+ Show ' + advanced.length + ' advanced directives (object-src, frame-ancestors, base-uri…)';
556+
}
557+
558+
document.addEventListener('click', function(e){
559+
var x = e.target.closest('.chip-x');
560+
if(x){
561+
var key = x.dataset.key, tok = decodeURIComponent(x.dataset.tok);
562+
state.sources[key].delete(tok);
563+
renderCards(); renderOutput();
564+
return;
565+
}
566+
var q = e.target.closest('.quick-btn');
567+
if(q){
568+
var key2 = q.dataset.key, tok2 = decodeURIComponent(q.dataset.tok);
569+
var set = state.sources[key2];
570+
if(set.has(tok2)) set.delete(tok2); else set.add(tok2);
571+
renderCards(); renderOutput();
572+
return;
573+
}
574+
var addBtn = e.target.closest('.dadd-btn');
575+
if(addBtn){
576+
var key3 = addBtn.dataset.key;
577+
var input = document.querySelector('.dadd-input[data-key="' + key3 + '"]');
578+
var tokens = parseCustom(input.value);
579+
tokens.forEach(function(t){ state.sources[key3].add(t); });
580+
input.value = '';
581+
renderCards(); renderOutput();
582+
var newInput = document.querySelector('.dadd-input[data-key="' + key3 + '"]');
583+
if(newInput) newInput.focus();
584+
return;
585+
}
586+
});
587+
588+
document.addEventListener('keydown', function(e){
589+
if(e.key === 'Enter' && e.target.classList && e.target.classList.contains('dadd-input')){
590+
e.target.nextElementSibling.click();
591+
}
592+
});
593+
594+
$('advancedToggle').addEventListener('click', function(){
595+
state.showAdvanced = !state.showAdvanced;
596+
renderCards();
597+
});
598+
599+
// ---------------- presets ----------------
600+
document.querySelectorAll('[data-preset]').forEach(function(btn){
601+
btn.addEventListener('click', function(){
602+
var p = btn.dataset.preset;
603+
if(p === 'strict'){
604+
['default-src','object-src','base-uri','frame-ancestors','script-src','style-src','form-action'].forEach(function(k){
605+
state.sources[k] = new Set();
606+
});
607+
state.sources['default-src'].add("'self'");
608+
state.sources['object-src'].add("'none'");
609+
state.sources['base-uri'].add("'self'");
610+
state.sources['frame-ancestors'].add("'self'");
611+
state.sources['script-src'].add("'self'");
612+
state.sources['style-src'].add("'self'");
613+
state.sources['form-action'].add("'self'");
614+
toast('Applied strict baseline preset');
615+
} else if(p === 'fonts'){
616+
state.sources['style-src'].add('https://fonts.googleapis.com');
617+
state.sources['font-src'].add('https://fonts.gstatic.com');
618+
toast('Added Google Fonts sources');
619+
} else if(p === 'cdn'){
620+
state.sources['script-src'].add('https://cdnjs.cloudflare.com');
621+
state.sources['style-src'].add('https://cdnjs.cloudflare.com');
622+
toast('Added cdnjs.cloudflare.com');
623+
} else if(p === 'reset'){
624+
DIRECTIVES.forEach(function(d){ state.sources[d.key] = new Set(); });
625+
state.reportOnly = false; state.upgrade = false; state.reportTo = ''; state.reportUri = '';
626+
$('reportOnlyToggle').checked = false; $('upgradeToggle').checked = false;
627+
$('reportToInput').value = ''; $('reportUriInput').value = '';
628+
toast('Cleared everything');
629+
}
630+
renderCards(); renderOutput();
631+
});
632+
});
633+
634+
// ---------------- import ----------------
635+
$('importBtn').addEventListener('click', function(){
636+
var raw = $('importInput').value.trim();
637+
if(!raw){ toast('Paste a policy first'); return; }
638+
raw = raw.replace(/^content-security-policy(-report-only)?\s*:\s*/i, function(m, g1){
639+
if(g1) state.reportOnly = true;
640+
return '';
641+
});
642+
DIRECTIVES.forEach(function(d){ state.sources[d.key] = new Set(); });
643+
var skipped = [];
644+
raw.split(';').forEach(function(part){
645+
part = part.trim();
646+
if(!part) return;
647+
var pieces = part.split(/\s+/);
648+
var name = pieces.shift().toLowerCase();
649+
if(name === 'upgrade-insecure-requests'){ state.upgrade = true; return; }
650+
if(name === 'report-to'){ state.reportTo = pieces.join(' ').replace(/['"]/g,''); return; }
651+
if(name === 'report-uri'){ state.reportUri = pieces.join(' '); return; }
652+
var match = DIRECTIVES.some(function(d){ return d.key === name; });
653+
if(!match){ if(name) skipped.push(name); return; }
654+
pieces.forEach(function(tok){
655+
var n = normalizeToken(tok);
656+
if(n) state.sources[name].add(n);
657+
});
658+
});
659+
$('upgradeToggle').checked = state.upgrade;
660+
$('reportOnlyToggle').checked = state.reportOnly;
661+
$('reportToInput').value = state.reportTo;
662+
$('reportUriInput').value = state.reportUri;
663+
renderCards(); renderOutput();
664+
toast(skipped.length ? 'Loaded — skipped unsupported: ' + skipped.join(', ') : 'Policy loaded');
665+
});
666+
667+
// ---------------- flags ----------------
668+
$('reportOnlyToggle').addEventListener('change', function(e){ state.reportOnly = e.target.checked; renderOutput(); });
669+
$('upgradeToggle').addEventListener('change', function(e){ state.upgrade = e.target.checked; renderOutput(); });
670+
$('reportToInput').addEventListener('input', function(e){ state.reportTo = e.target.value.trim(); renderOutput(); });
671+
$('reportUriInput').addEventListener('input', function(e){ state.reportUri = e.target.value.trim(); renderOutput(); });
672+
673+
// ---------------- output ----------------
674+
function buildPolicy(){
675+
var parts = [];
676+
DIRECTIVES.forEach(function(d){
677+
var set = state.sources[d.key];
678+
if(set.size > 0) parts.push(d.key + ' ' + Array.from(set).join(' '));
679+
});
680+
if(state.upgrade) parts.push('upgrade-insecure-requests');
681+
if(state.reportTo) parts.push("report-to " + state.reportTo);
682+
if(state.reportUri) parts.push('report-uri ' + state.reportUri);
683+
return parts.join('; ');
684+
}
685+
686+
function renderChecklist(){
687+
var policy = buildPolicy();
688+
var checks = [];
689+
function has(key, tok){ return state.sources[key] && state.sources[key].has(tok); }
690+
checks.push({ok: state.sources['default-src'].size > 0, text:'default-src is set as a fallback'});
691+
checks.push({ok: has('object-src',"'none'"), text:"object-src is 'none'"});
692+
checks.push({ok: state.sources['base-uri'].size > 0 && !state.sources['base-uri'].has('*'), text:'base-uri is restricted'});
693+
checks.push({ok: !has('script-src',"'unsafe-inline'"), text:"script-src avoids 'unsafe-inline'"});
694+
checks.push({ok: !has('script-src',"'unsafe-eval'"), text:"script-src avoids 'unsafe-eval'"});
695+
checks.push({ok: state.sources['frame-ancestors'].size > 0, text:'frame-ancestors set (clickjacking defense)'});
696+
checks.push({ok: Object.keys(state.sources).every(function(k){ return !state.sources[k].has('*'); }), text:'no directive uses a bare wildcard *'});
697+
$('checklist').innerHTML = checks.map(function(c){
698+
return '<div class="check-item ' + (c.ok?'ok':'warn') + '"><span class="check-icon">' + (c.ok?'✓':'!') + '</span><span>' + c.text + '</span></div>';
699+
}).join('');
700+
return policy;
701+
}
702+
703+
function renderOutput(){
704+
var policy = renderChecklist();
705+
var headerName = state.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
706+
var headerLine = policy ? headerName + ': ' + policy : '// add at least one directive to generate a policy';
707+
$('headerOutput').textContent = headerLine;
708+
709+
var metaSafe = policy
710+
.split('; ')
711+
.filter(function(p){ return !/^(frame-ancestors|report-uri|report-to|sandbox)\b/.test(p); })
712+
.join('; ');
713+
$('metaOutput').textContent = metaSafe
714+
? '<meta http-equiv="Content-Security-Policy" content="' + metaSafe + '">'
715+
: '// add at least one directive to generate a meta tag';
716+
717+
$('nginxOutput').textContent = policy
718+
? 'add_header ' + headerName + ' "' + policy + '" always;'
719+
: '// add at least one directive to generate a config line';
720+
721+
$('apacheOutput').textContent = policy
722+
? 'Header set ' + headerName + ' "' + policy + '"'
723+
: '// add at least one directive to generate a config line';
724+
}
725+
726+
// ---------------- copy ----------------
727+
document.querySelectorAll('.copy-btn').forEach(function(btn){
728+
btn.addEventListener('click', function(){
729+
var text = $(btn.dataset.copy).textContent;
730+
var done = function(){
731+
btn.textContent = 'copied';
732+
btn.classList.add('copied');
733+
setTimeout(function(){ btn.textContent = 'copy'; btn.classList.remove('copied'); }, 1400);
734+
};
735+
if(navigator.clipboard && navigator.clipboard.writeText){
736+
navigator.clipboard.writeText(text).then(done).catch(function(){ fallbackCopy(text); done(); });
737+
} else {
738+
fallbackCopy(text); done();
739+
}
740+
});
741+
});
742+
function fallbackCopy(text){
743+
var ta = document.createElement('textarea');
744+
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
745+
document.body.appendChild(ta); ta.select();
746+
try{ document.execCommand('copy'); }catch(e){}
747+
document.body.removeChild(ta);
748+
}
749+
750+
// ---------------- init ----------------
751+
renderBulkDirectives();
752+
renderBulkKeywords();
753+
renderCards();
754+
renderOutput();
755+
})();
756+
</script>
757+
</body>
758+
</html>

0 commit comments

Comments
 (0)