Make notifications framework CSP compliant

Change-Id: I95411f646053d76219908b91e9f0921c17280c28
diff --git a/Changes b/Changes
index c107578..98225df 100755
--- a/Changes
+++ b/Changes
@@ -14,6 +14,7 @@
         - defer main script.
         - Introduce X-Frame-Options header.
         - Introduce X-XSS-Protection header.
+        - Support CSP in notifications framework.
 
 0.40 2020-12-17
         - Modernize ES and fix in-loops.
diff --git a/dev/demo/all.html b/dev/demo/all.html
index 3aa40ca..0d0fee3 100644
--- a/dev/demo/all.html
+++ b/dev/demo/all.html
@@ -14,7 +14,7 @@
     //]]></script>
     <script data-main="alldemo.js" src="../js/lib/require.js" async="async"></script>
   </head>
-  <body>
+  <body class="no-js">
     <div id="kalamar-bg"></div>
     <header>
       <a href="/" class="logo" tabindex="-1"><h1><span>KorAP - Corpus Analysis Platform</span></h1></a>
@@ -281,11 +281,10 @@
       var KorAP = KorAP || {};
       KorAP.URL = 'http://localhost:3000';
     </script>
-    <script>//<![CDATA[
-KorAP.Notifications = [];
-KorAP.Notifications.push(["warn","767: Case insensitivity is currently not supported for this layer"]);
-KorAP.Notifications.push(["error","404: Not Found (remote)"]);
-//]]>
-    </script>
+    <div id="notifications">
+      <div class="notify notify-warn" data-type="warn">Error</div>
+      <div class="notify notify-error" data-type="error">Hmmm</div>
+      <div class="notify notify-success" data-type="success" data-src="Kustvakt">Hmmm</div>
+    </div>
   </body>
 </html>
diff --git a/dev/js/src/init.js b/dev/js/src/init.js
index 91084cf..ef6dfcf 100644
--- a/dev/js/src/init.js
+++ b/dev/js/src/init.js
@@ -92,7 +92,7 @@
   KorAP.tourshowR = function(){
     tourClass.gTshowResults().start();
   };
-    
+
   domReady(function (event) {
       
     var obj = {};
@@ -105,15 +105,19 @@
     /**
      * Release notifications
      */
-    if (KorAP.Notifications !== undefined) {
-      KorAP.Notifications.forEach(function(n) {
-        var msg = n[1];
-        if (n[2]) {
-          msg += '<code class="src">'+n[2]+'</code>';
+    d.querySelectorAll('#notifications div.notify').forEach(
+      function(e) {
+        let msg = e.textContent;
+
+        let src = e.getAttribute('data-src');
+        if (src) {
+          msg += '<code class="src">'+src+'</code>';
         };
-        alertifyClass.log(msg, n[0], 10000);
-      });
-    };
+
+        let type = e.getAttribute('data-type') || "error";
+        alertifyClass.log(msg, type, 10000);
+      }
+    );
 
     /**
      * Replace Virtual Corpus field
diff --git a/dev/scss/main/alertify.scss b/dev/scss/main/alertify.scss
index be2f656..3c329b7 100644
--- a/dev/scss/main/alertify.scss
+++ b/dev/scss/main/alertify.scss
@@ -21,4 +21,28 @@
 
 .alertify-log-error {
   background-color: $alert-red;
+}
+
+#notifications {
+  display: none;
+}
+
+// TODO:
+//   This differs from alertify messages and should be united
+div.notify {
+  position: relative;
+  margin: .5em auto;
+  display: block;
+  padding: .5em;
+  
+  border-radius: $standard-border-radius;
+  max-width: 30em;
+  background-color: $alert-red;
+  color: $nearly-white;
+  &.notify-success {
+    background-color: $ids-green-1
+  }
+  &.notify-warn {
+    background-color: $ids-orange-2
+  }
 }
\ No newline at end of file
diff --git a/dev/scss/no-js.scss b/dev/scss/no-js.scss
index c5f6410..985ac48 100644
--- a/dev/scss/no-js.scss
+++ b/dev/scss/no-js.scss
@@ -5,6 +5,10 @@
  */
 body.no-js {
 
+  #notifications {
+    display: block !important;
+  }
+  
   // Aside in noscript mode
   aside {
     position:    relative;
diff --git a/lib/Kalamar/Plugin/Notifications.pm b/lib/Kalamar/Plugin/Notifications.pm
index 8b6df94..6be4256 100644
--- a/lib/Kalamar/Plugin/Notifications.pm
+++ b/lib/Kalamar/Plugin/Notifications.pm
@@ -12,26 +12,17 @@
 
   return '' unless @$notify_array;
 
-  # Start JavaScript snippet
-  my $js .= qq{<script>//<![CDATA[\n};
-  $js .= "KorAP.Notifications = [];\n";
-  my $noscript = "<noscript>";
-
-  # Add notifications
+  my $s = '';
   foreach (@$notify_array) {
-    $js .= 'KorAP.Notifications.push([';
-    $js .= quote($_->[0]) . ',' . quote($_->[-1]);
+    $s .= qq{<div class="notify notify-} . $_->[0] . '"';
+    $s .= ' data-type=' . quote($_->[0]);
     if (ref $_->[1] && ref $_->[1] eq 'HASH') {
-      $js .= ',' . quote($_->[1]->{src}) if $_->[1]->{src};
+      $s .= ' data-src=' . quote($_->[1]->{src}) if $_->[1]->{src};
     };
-    $js .= "]);\n";
-
-    $noscript .= qq{<div class="notify notify-} . $_->[0] . '">' .
-      xml_escape($_->[-1]) .
-	"</div>\n";
+    $s .= '>' . xml_escape($_->[-1]) . "</div>\n";
   };
+  return b('<div id="notifications">' . $s . '</div>');
 
-  return b($js . "//]]>\n</script>\n" . $noscript . '</noscript>');
 };
 
 
diff --git a/t/plugin/notifications.t b/t/plugin/notifications.t
new file mode 100644
index 0000000..9291d01
--- /dev/null
+++ b/t/plugin/notifications.t
@@ -0,0 +1,31 @@
+use Mojolicious;
+use Test::Mojo;
+use Test::More;
+
+my $app = Mojolicious->new;
+my $t = Test::Mojo->new($app);
+
+# Client notifications
+$app->plugin(Notifications => {
+  'Kalamar::Plugin::Notifications' => 1,
+  JSON => 1,
+  HTML => 1
+});
+
+my $c = $app->build_controller;
+
+is($c->notifications('Kalamar::Plugin::Notifications'), '');
+
+$c->notify(warn => 'Error');
+$c->notify('warn' => 20, 'Hmmm');
+$c->notify('success' => {src => 'Kustvakt'}, 'Hmmm');
+
+my $n = $c->notifications('Kalamar::Plugin::Notifications');
+
+like($n, qr!^<div id="notifications">.*</div>$!s);
+like($n, qr!<div class="notify notify-warn" data-type="warn">Error</div>!);
+like($n, qr!<div class="notify notify-warn" data-type="warn">Hmmm</div>!);
+like($n, qr!<div class="notify notify-success" data-type="success" data-src="Kustvakt">Hmmm</div>!);
+
+done_testing;
+__END__
diff --git a/t/query.t b/t/query.t
index 73e488c..227dbf1 100644
--- a/t/query.t
+++ b/t/query.t
@@ -194,19 +194,19 @@
 # Query with failing parameters
 $t->get_ok('/?q=fantastisch&ql=Fabelsprache')
   ->status_is(400)
-  ->text_is('noscript div.notify-error', 'Parameter "ql" invalid')
+  ->text_is('#notifications div.notify-error', 'Parameter "ql" invalid')
   ->element_exists('#search')
-  ->element_count_is('noscript div.notify-error', 1)
+  ->element_count_is('#notifications div.notify-error', 1)
   ;
 $t->get_ok('/?q=fantastisch&cutoff=no')
   ->status_is(400)
-  ->text_is('noscript div.notify-error', 'Parameter "cutoff" invalid')
-  ->element_count_is('noscript div.notify-error', 1)
+  ->text_is('#notifications div.notify-error', 'Parameter "cutoff" invalid')
+  ->element_count_is('#notifications div.notify-error', 1)
   ;
 $t->get_ok('/?q=fantastisch&p=hui&o=hui&count=-8')
   ->status_is(400)
-  ->text_like('noscript div.notify-error', qr!Parameter ".+?" invalid!)
-  ->element_count_is('noscript div.notify-error', 3)
+  ->text_like('#notifications div.notify-error', qr!Parameter ".+?" invalid!)
+  ->element_count_is('#notifications div.notify-error', 3)
   ;
 
 # Query too long
@@ -214,20 +214,20 @@
 $t->get_ok('/?q=' . $long_query)
   ->status_is(400)
   ->text_is('#error','')
-  ->text_like('noscript div.notify-error', qr!Parameter ".+?" invalid!)
+  ->text_like('#notifications div.notify-error', qr!Parameter ".+?" invalid!)
   ;
 
 # Query with timeout
 $t->get_ok('/?q=timeout')
   ->status_is(200)
-  ->text_like('noscript div.notify-warn', qr!Response time exceeded!)
+  ->text_like('#notifications div.notify-warn', qr!Response time exceeded!)
   ->text_is('#total-results', '> 4,274,841');
 ;
 
 # Do not cache
 $t->get_ok('/?q=timeout')
   ->status_is(200)
-  # ->text_like('noscript div.notify-warning', qr!Response time exceeded!)
+  # ->text_like('#notifications div.notify-warning', qr!Response time exceeded!)
   ->element_exists("input#cq")
   ->element_exists_not("input#cq[value]")
   ->text_is('#total-results', '> 4,274,841');