@@ -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 ) + '">×</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, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) . replace ( / " / g, '"' ) ;
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 ( / ^ c o n t e n t - s e c u r i t y - p o l i c y ( - r e p o r t - o n l y ) ? \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 ! / ^ ( f r a m e - a n c e s t o r s | r e p o r t - u r i | r e p o r t - t o | s a n d b o x ) \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