Introduce CSP headers to Kalamar (start of #72)

Change-Id: I84b7ff0accab3d783ad653fae123c25fee1d92b9
diff --git a/Changes b/Changes
index 74f8ed6..947d6d6 100755
--- a/Changes
+++ b/Changes
@@ -1,5 +1,6 @@
 0.41 2021-01-14
         - Introduce CORS headers to the proxy.
+        - Introduce Content Security Policy.
 
 0.40 2020-12-17
         - Modernize ES and fix in-loops.
diff --git a/dev/js/src/init.js b/dev/js/src/init.js
index 58c6472..6a781b6 100644
--- a/dev/js/src/init.js
+++ b/dev/js/src/init.js
@@ -52,6 +52,18 @@
 
   const d = document;
 
+  // Remove the no-js class from the body
+  d.body.classList.remove('no-js');
+
+  // Set base URL
+  KorAP.URL = d.body.getAttribute('data-korap-url') || "";
+
+  // Get koralQuery response
+  const kqe = d.getElementById('koralQuery');
+  if (kqe !== null) {
+    KorAP.koralQuery = JSON.parse(kqe.getAttribute('data-koralquery') || "");
+  };
+  
   // Create suffix if KorAP is run in a subfolder
   KorAP.session = sessionClass.create(
     KorAP.URL.length > 0 ? 'kalamarJS-' + KorAP.URL.slugify() : 'kalamarJS'
diff --git a/dev/scss/base/base.scss b/dev/scss/base/base.scss
index 2ad7b95..be195e3 100644
--- a/dev/scss/base/base.scss
+++ b/dev/scss/base/base.scss
@@ -81,6 +81,10 @@
   clear: both;
 }
 
+iframe {
+  border-width: 0;
+}
+
 blockquote {
   border-radius:    $standard-border-radius;
   padding:          2pt 5pt 2pt 20pt;
diff --git a/kalamar.conf b/kalamar.conf
index 38fc57f..63dad82 100644
--- a/kalamar.conf
+++ b/kalamar.conf
@@ -57,6 +57,9 @@
     ## Require everything to be send via https only:
     # https_only => 1,
 
+    ## Override default content security policy
+    # cs_policy => "default-src '*';"
+
     ## Set proxy timeouts
     # proxy_inactivity_timeout => 120,
     # proxy_connect_timeout => 120,
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 61827f8..f5a111d 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -123,6 +123,22 @@
     );
   };
 
+  my $csp = $conf->{cs_policy} // (
+    "default-src 'self';".
+      "style-src 'self' 'unsafe-inline';".
+      "frame-src *;".
+      "media-src 'none';".
+      "object-src 'self';".
+      "font-src 'self';".
+      "img-src 'self' data:;"
+    );
+
+  $self->hook(
+    before_render => sub {
+      shift->res->headers->header('Content-Security-Policy' => $csp);
+    }
+  );
+
   # API is not yet set - define
   $conf->{api_path} //= $ENV{KALAMAR_API};
   $conf->{api_version} //= $API_VERSION;
diff --git a/package.json b/package.json
index 703962a..0678630 100755
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "Kalamar",
   "description": "Mojolicious-based Frontend for KorAP",
   "license": "BSD-2-Clause",
-  "version": "0.40.3",
+  "version": "0.41.0",
   "pluginVersion": "0.2.2",
   "engines": {
     "node": ">=6.0.0"
diff --git a/t/page.t b/t/page.t
index e04c5a7..029cb33 100644
--- a/t/page.t
+++ b/t/page.t
@@ -22,6 +22,12 @@
   ->attr_is('meta[property="og:url"]', 'content', '//korap2.ids-mannheim.de/')
   ;
 
+$t->get_ok('/')
+  ->header_like('Content-Security-Policy', qr!default-src 'self';!)
+  ->header_like('Content-Security-Policy', qr!media-src 'none';!)
+  ->header_like('Content-Security-Policy', qr!object-src 'self';!)
+  ;
+
 done_testing;
 
 1;
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index 117b690..22054d2 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -97,6 +97,7 @@
   }
 );
 
+my $q = qr!(?:\"|")!;
 
 $t->get_ok('/realapi/v1.0')
   ->status_is(200)
@@ -106,7 +107,7 @@
   ->status_is(200)
   ->text_like('h1 span', qr/KorAP: Find .Baum./i)
   ->text_like('#total-results', qr/\d+$/)
-  ->content_like(qr/\"authorized\"\:null/)
+  ->content_like(qr/${q}authorized${q}:null/)
   ->element_exists_not('div.button.top a')
   ->element_exists_not('aside.active')
   ->element_exists_not('aside.off')
@@ -216,7 +217,7 @@
   ->text_like('h1 span', qr/KorAP: Find .Baum./i)
   ->text_like('#total-results', qr/\d+$/)
   ->element_exists_not('div.notify-error')
-  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->content_like(qr/${q}authorized${q}:${q}yes${q}/)
   ->element_exists('div.button.top a')
   ->element_exists('div.button.top a.logout[title~="test"]')
   ;
@@ -225,7 +226,7 @@
   ->status_is(200)
   ->text_like('h1 span', qr/KorAP: Find .Paum./i)
   ->text_is('#total-results', '')
-  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->content_like(qr/${q}authorized${q}:${q}yes${q}/)
   ->element_exists_not('p.hint')
   ;
 
@@ -251,14 +252,14 @@
   ->status_is(200)
   ->text_like('h1 span', qr/KorAP: Find .Baum./i)
   ->text_like('#total-results', qr/\d+$/)
-  ->content_like(qr/\"authorized\"\:null/)
+  ->content_like(qr/${q}authorized${q}:null/)
   ;
 
 $t->get_ok('/?q=Paum')
   ->status_is(200)
   ->text_like('h1 span', qr/KorAP: Find .Paum./i)
   ->text_is('#total-results', '')
-  ->content_like(qr/\"authorized\"\:null/)
+  ->content_like(qr/${q}authorized${q}:null/)
   ->text_is('p.hint', 'Maybe you need to log in first?')
   ;
 
@@ -320,7 +321,7 @@
   ->status_is(200)
   ->text_like('h1 span', qr/KorAP: Find .Baum./i)
   ->text_like('#total-results', qr/\d+$/)
-  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->content_like(qr/${q}authorized${q}:${q}yes${q}/)
   ->header_is('X-Kalamar-Cache', 'true')
   ;
 
@@ -333,9 +334,9 @@
   ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
   ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
   ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
-  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->content_like(qr/${q}authorized${q}:${q}yes${q}/)
   ->header_isnt('X-Kalamar-Cache', 'true')
-  ->content_like(qr!\"cutOff":true!)
+  ->content_like(qr!${q}cutOff${q}:true!)
   ->element_exists_not('#total-results')
   ;
 
@@ -364,7 +365,7 @@
   ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
   ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
   ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
-  ->content_unlike(qr/\"authorized\"\:\"yes\"/)
+  ->content_unlike(qr/${q}authorized${q}:${q}yes${q}/)
   ->header_isnt('X-Kalamar-Cache', 'true')
   ->element_exists('p.no-results')
   ;
@@ -385,7 +386,7 @@
   ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
   ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
   ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
-  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->content_like(qr/${q}authorized${q}:${q}yes${q}/)
   ->header_isnt('X-Kalamar-Cache', 'true')
   ->element_exists_not('p.no-results')
   ;
@@ -405,7 +406,7 @@
   ->text_is('#error','')
   ->text_is('div.notify-error','Refresh token is expired')
   ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
-  ->content_unlike(qr/\"authorized\"\:\"yes\"/)
+  ->content_unlike(qr/${q}authorized${q}:${q}yes${q}/)
   ->element_exists('p.no-results')
   ->tx->res->dom->at('input[name="csrf_token"]')
   ->attr('value')
diff --git a/t/plugin/auth.t b/t/plugin/auth.t
index 63433b2..f5351de 100644
--- a/t/plugin/auth.t
+++ b/t/plugin/auth.t
@@ -29,6 +29,8 @@
 # Configure fake backend
 $fake_backend->pattern->defaults->{app}->log($t->app->log);
 
+my $q = qr!(?:\"|")!;
+
 $t->get_ok('/realapi/v1.0')
   ->status_is(200)
   ->content_is('Fake server available');
@@ -37,7 +39,7 @@
   ->status_is(200)
   ->text_like('h1 span', qr/KorAP: Find .Baum./i)
   ->text_like('#total-results', qr/\d+$/)
-  ->content_like(qr/\"authorized\"\:null/)
+  ->content_like(qr/${q}authorized${q}:null/)
   ->element_exists_not('div.button.top a')
   ->element_exists_not('aside.active')
   ->element_exists_not('aside.off')
@@ -135,7 +137,7 @@
   ->text_like('h1 span', qr/KorAP: Find .Baum./i)
   ->text_like('#total-results', qr/\d+$/)
   ->element_exists_not('div.notify-error')
-  ->content_like(qr/\"authorized\"\:\"test\"/)
+  ->content_like(qr/${q}authorized${q}:${q}test${q}/)
   ->element_exists('div.button.top a')
   ->element_exists('div.button.top a.logout[title~="test"]')
   ;
@@ -156,7 +158,7 @@
   ->status_is(200)
   ->text_like('h1 span', qr/KorAP: Find .Baum./i)
   ->text_like('#total-results', qr/\d+$/)
-  ->content_like(qr/\"authorized\"\:null/)
+  ->content_like(qr/${q}authorized${q}:null/)
   ;
 
 # Get redirect
diff --git a/t/query.t b/t/query.t
index 37bcdcb..73e488c 100644
--- a/t/query.t
+++ b/t/query.t
@@ -25,6 +25,8 @@
 # Configure fake backend
 $fake_backend->pattern->defaults->{app}->log($t->app->log);
 
+my $q = qr!(?:\"|")!;
+
 # Query passed
 $t->get_ok('/?q=baum')
   ->status_is(200)
@@ -41,11 +43,11 @@
   ->element_count_is('#pagination > a', 5)
 
   # api_response
-  ->content_like(qr/\"authorized\":null/)
-  ->content_like(qr/\"pubDate\",\"subTitle\",\"author\"/)
+  ->content_like(qr/${q}authorized${q}:null/)
+  ->content_like(qr/${q}pubDate${q},${q}subTitle${q},${q}author${q}/)
 
   # No cutOff
-  ->content_unlike(qr!\"cutOff":true!)
+  ->content_unlike(qr!${q}cutOff${q}:true!)
 
   ->element_exists('li[data-text-sigle=GOE/AGI/00000]')
   ->element_exists('li:nth-of-type(1) div.flop')
@@ -84,7 +86,7 @@
 $t->get_ok('/?q=[orth=das&ql=poliqarp')
   ->element_exists('.notify-error')
   ->text_is('.notify-error', '302: Parantheses/brackets unbalanced.')
-  ->content_like(qr!KorAP\.koralQuery =!)
+  ->content_like(qr!data-koralquery=!)
   ->text_is('.no-results:nth-of-type(1)', 'Unable to perform the action.')
   ;
 
@@ -97,7 +99,7 @@
   ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
   ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
   ->header_isnt('X-Kalamar-Cache', 'true')
-  ->content_like(qr!\"cutOff":true!)
+  ->content_like(qr!${q}cutOff${q}:true!)
   ->text_is('#total-results', 51)
   ;
 
@@ -109,7 +111,7 @@
   ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
   ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
   ->header_isnt('X-Kalamar-Cache', 'true')
-  ->content_like(qr!\"cutOff":true!)
+  ->content_like(qr!${q}cutOff${q}:true!)
   ->element_exists_not('#total-results')
   ;
 
@@ -121,7 +123,7 @@
   ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
   ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
   ->header_is('X-Kalamar-Cache', 'true')
-  ->content_like(qr!\"cutOff":true!)
+  ->content_like(qr!${q}cutOff${q}:true!)
   ->element_exists_not('#total-results')
   ;
 
@@ -133,7 +135,7 @@
   ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
   ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
   ->header_is('X-Kalamar-Cache', 'true')
-  ->content_like(qr!\"cutOff":true!)
+  ->content_like(qr!${q}cutOff${q}:true!)
   ->text_is('#total-results', 51)
   ;
 
@@ -150,15 +152,15 @@
   # Total pages
   ->element_count_is('#pagination > a', 7)
   ->text_is('#pagination a:nth-of-type(6) span', 7291)
-  ->content_like(qr!\"count":2!)
-  ->content_like(qr!\"startIndex":0!)
-  ->content_like(qr!\"itemsPerPage":2!)
+  ->content_like(qr!${q}count${q}:2!)
+  ->content_like(qr!${q}startIndex${q}:0!)
+  ->content_like(qr!${q}itemsPerPage${q}:2!)
 
   # No caching
   ->header_isnt('X-Kalamar-Cache', 'true')
 
   # Not searched for "der" before
-  ->content_unlike(qr!\"cutOff":true!)
+  ->content_unlike(qr!${q}cutOff${q}:true!)
   ;
 
 # Check pagination repetion of page
@@ -180,13 +182,13 @@
   # Total pages
   ->element_count_is('#pagination > a', 7)
   ->text_is('#pagination a:nth-of-type(6) span', 7291)
-  ->content_like(qr!\"count":2!)
-  ->content_like(qr!\"itemsPerPage":2!)
-  ->content_like(qr!\"startIndex":2!)
+  ->content_like(qr!${q}count${q}:2!)
+  ->content_like(qr!${q}itemsPerPage${q}:2!)
+  ->content_like(qr!${q}startIndex${q}:2!)
 
   # No caching
   ->header_isnt('X-Kalamar-Cache', 'true')
-  ->content_like(qr!\"cutOff":true!)
+  ->content_like(qr!${q}cutOff${q}:true!)
   ;
 
 # Query with failing parameters
@@ -237,7 +239,7 @@
 $t->get_ok('/?q=baum&collection=availability+%3D+%2FCC-BY.*%2F')
   ->status_is(200)
   ->element_exists("input#cq[value='availability = /CC-BY.*/']")
-  ->content_like(qr!\"availability\"!)
+  ->content_like(qr!${q}availability${q}!)
   ->text_is('#error','')
   ;
 
@@ -252,7 +254,7 @@
 $t->get_ok('/?q=baum&cq=availability+%3D+%2FCC-BY.*%2F')
   ->status_is(200)
   ->element_exists("input#cq[value='availability = /CC-BY.*/']")
-  ->content_like(qr!\"availability\"!)
+  ->content_like(qr!${q}availability${q}!)
   ->text_is('#error','')
   ->text_is('#special', 'Funny')
   ;
@@ -270,7 +272,7 @@
 $t->get_ok('/?q=baum&pipe=glemm')
   ->status_is(200)
   ->text_is('#error','')
-  ->content_like(qr/\"pipes\":"glemm"/)
+  ->content_like(qr/${q}pipes${q}:${q}glemm${q}/)
   ;
 
 
diff --git a/t/subfolder.t b/t/subfolder.t
index e4aa9a4..5731e13 100644
--- a/t/subfolder.t
+++ b/t/subfolder.t
@@ -12,6 +12,8 @@
 
 $t->app->mode('production');
 
+my $q = qr!(?:\"|")!;
+
 $t->post_ok('/user/login' => form => { handle => 'test', pwd => 'fail' })
   ->status_is(302)
   ->header_is('Location' => '/');
@@ -24,7 +26,7 @@
   ->text_is('div.notify-error', 'Bad CSRF token')
   ->element_exists('input[name=handle][value=test]')
   ->element_exists_not('div.button.top a')
-  ->content_like(qr!KorAP\.URL = ''!)
+  ->attr_is('body','data-korap-url','')
   ;
 
 is('kalamar',$t->app->sessions->cookie_name);
@@ -76,7 +78,7 @@
 $t->get_ok('/')
   ->status_is(200)
   ->element_exists_not('div.notify-error')
-  ->content_like(qr!KorAP\.URL = '/korap/test'!)
+  ->attr_is('body','data-korap-url','/korap/test')
   ;
 
 
diff --git a/templates/layouts/main.html.ep b/templates/layouts/main.html.ep
index fcc8bbe..f5610c2 100644
--- a/templates/layouts/main.html.ep
+++ b/templates/layouts/main.html.ep
@@ -41,13 +41,6 @@
 
     <link rel="apple-touch-icon" href="<%= url_for '/img/apple-touch-icon.png' %>" />
     <link href="<%= stash 'prefix' %>/favicon.ico" rel="shortcut icon" type="image/x-icon" /> 
-    
-%= javascript begin  
-  window.KorAP = window.KorAP || {};
-  % my $api = url_for('index');
-  % $api =~ s!/$!!;
-  KorAP.URL = '<%== $api %>';
-% end
 
 % if ($c->app->mode eq 'development') {
     <link href="<%= stash 'prefix' %>/css/kalamar-<%= $Kalamar::VERSION %>.css?v=<%= random_string %>" type="text/css" rel="stylesheet" />
@@ -61,8 +54,12 @@
   </head>
 % my $embedded = 0;
 % $embedded = 1 if stash('embedded');
-  <body class="no-js<% if ($embedded) { %> embedded<% } %>" itemscope itemtype="http://schema.org/<%= stash('schematype') || 'WebApplication' %>">
-    <script>document.body.classList.remove('no-js');</script>
+  <body class="no-js<% if ($embedded) { %> embedded<% } %>"
+        % my $api = url_for('index');
+        % $api =~ s!/$!!;
+        data-korap-url="<%== $api %>"
+        itemscope
+        itemtype="http://schema.org/<%= stash('schematype') || 'WebApplication' %>">
     <div id="kalamar-bg"></div>
     
     %= include 'partial/side', embedded => $embedded
diff --git a/templates/query.html.ep b/templates/query.html.ep
index fa9980c..beda82c 100644
--- a/templates/query.html.ep
+++ b/templates/query.html.ep
@@ -1,9 +1,7 @@
 % use Mojo::JSON 'encode_json';
 
 % if (stash('api_response')) {
-%=  javascript begin
 %   my $kq_hash = stash('api_response');
 %   $kq_hash->{matches} = ["..."];
-  KorAP.koralQuery = <%= b(encode_json($kq_hash))->decode %>;
-%   end
+<span id="koralQuery" data-koralquery="<%== b(encode_json($kq_hash))->decode->xml_escape %>"></span>
 % };