Rename all cookies to be instance-independent (Requires relogin) - fixes #94

Change-Id: Icd8b58e4b6fbd99a93ee6972485ef77786b5764c
diff --git a/Changes b/Changes
index bce0872..656df7a 100755
--- a/Changes
+++ b/Changes
@@ -1,3 +1,11 @@
+0.36 2019-07-23
+        - Rename all cookies to be independent
+          for different instance (#94).
+        - Enable https only via
+          configuration option 'https_only'.
+
+        WARNING: This requires relogin for all users!
+
 0.35 2019-07-19
         - Added EXPERIMENTAL proxy to API route.
         - Remove deprecated api configuration
diff --git a/dev/js/runner/all.html b/dev/js/runner/all.html
index 366c755..f0a71ad 100644
--- a/dev/js/runner/all.html
+++ b/dev/js/runner/all.html
@@ -47,7 +47,8 @@
         'spec/corpusByMatchSpec',
         'spec/statSpec',
         'spec/vcSpec',
-        'spec/tourSpec'
+        'spec/tourSpec',
+        'spec/utilSpec'
       ],
       function () {
         window.onload();
diff --git a/dev/js/spec/utilSpec.js b/dev/js/spec/utilSpec.js
new file mode 100644
index 0000000..b42102a
--- /dev/null
+++ b/dev/js/spec/utilSpec.js
@@ -0,0 +1,19 @@
+define(['util'], function () {
+  describe('KorAP.util', function () {
+
+    it('should quote', function () {
+      expect('Baum'.quote()).toEqual('"Baum"');
+      expect('B"a"um'.quote()).toEqual('"B\\"a\\"um"');
+    });
+
+    it('should escape regex', function () {
+      expect('aaa/bbb\/ccc'.escapeRegex()).toEqual('aaa\\/bbb\\/ccc');
+    });
+
+    it('should slugify', function () {
+      expect('/korap/test'.slugify()).toEqual('koraptest');
+      expect('korap test'.slugify()).toEqual('korap-test');
+      expect('Korap Test'.slugify()).toEqual('korap-test');
+    });
+  })
+});
diff --git a/dev/js/src/init.js b/dev/js/src/init.js
index 6de66d8..bafa473 100644
--- a/dev/js/src/init.js
+++ b/dev/js/src/init.js
@@ -44,7 +44,10 @@
 
   const d = document;
 
-  KorAP.session = sessionClass.create('KalamarJS');
+  // Create suffix if KorAP is run in a subfolder
+  KorAP.session = sessionClass.create(
+    KorAP.URL.length > 0 ? 'kalamarJS-' + KorAP.URL.slugify() : 'kalamarJS'
+  );
 
   // Override KorAP.log
   window.alertify = alertifyClass;
diff --git a/dev/js/src/util.js b/dev/js/src/util.js
index cb60c2a..5df346b 100644
--- a/dev/js/src/util.js
+++ b/dev/js/src/util.js
@@ -9,16 +9,22 @@
   };
 };
 
-var _quoteRE = new RegExp("([\"\\\\])", 'g');
+const _quoteRE = new RegExp("([\"\\\\])", 'g');
 String.prototype.quote = function () {
-  return this.replace(_quoteRE, '\\$1');
+  return '"' + this.replace(_quoteRE, '\\$1') + '"';
 };
 
-var _escapeRE = new RegExp("([\/\\\\])", 'g');
+const _escapeRE = new RegExp("([\/\\\\])", 'g');
 String.prototype.escapeRegex = function () {
   return this.replace(_escapeRE, '\\$1');
 };
 
+const _slug1RE = new RegExp("[^-a-zA-Z0-9_\\s]+", 'g');
+const _slug2RE = new RegExp("[-\\s]+", 'g');
+String.prototype.slugify = function () {
+  return this.toLowerCase().replace(_slug1RE, '').replace(_slug2RE, '-');
+};
+
 // Add toggleClass method similar to jquery
 HTMLElement.prototype.toggleClass = function (c1, c2) {
   var cl = this.classList;
diff --git a/dev/js/src/vc/doc.js b/dev/js/src/vc/doc.js
index b012e1f..8ea8364 100644
--- a/dev/js/src/vc/doc.js
+++ b/dev/js/src/vc/doc.js
@@ -667,7 +667,7 @@
         return string + '/' + this.value().escapeRegex() + '/';
       case "string":
       case "text":
-        return string + '"' + this.value().quote() + '"';
+        return string + this.value().quote();
       };
 
       return "";
diff --git a/dev/js/src/vc/docgroupref.js b/dev/js/src/vc/docgroupref.js
index 9425fb3..e72385a 100644
--- a/dev/js/src/vc/docgroupref.js
+++ b/dev/js/src/vc/docgroupref.js
@@ -273,7 +273,7 @@
         return "";
 
       // Build doc string based on key
-      return 'referTo "' + this.ref().quote() + '"';
+      return 'referTo ' + this.ref().quote();
     }
   };
 });
diff --git a/dev/js/src/vc/fragment.js b/dev/js/src/vc/fragment.js
index 2d2504d..f0c1397 100644
--- a/dev/js/src/vc/fragment.js
+++ b/dev/js/src/vc/fragment.js
@@ -176,7 +176,7 @@
           if (item[2] === "date") {
             return item[0] + ' in ' + item[1];
           };
-          return item[0] + ' = "' + new String(item[1]).quote() + '"';
+          return item[0] + ' = ' + new String(item[1]).quote();
         }
       ).join(" & ");
     }
diff --git a/dev/js/src/vc/rewrite.js b/dev/js/src/vc/rewrite.js
index eeec713..150ee87 100644
--- a/dev/js/src/vc/rewrite.js
+++ b/dev/js/src/vc/rewrite.js
@@ -87,14 +87,9 @@
       str += ' of ' + (
 	      this._scope === null ?
 	        'object' :
-	        '"' +
-	        this.scope().quote() +
-	        '"'
+	        this.scope().quote()
       );
-      str += ' by ' +
-	      '"' +
-	      this.src().quote() +
-	      '"';
+      str += ' by ' + this.src().quote();
       return str;
     }
   };
diff --git a/kalamar.conf b/kalamar.conf
index 6e61b15..a9d78b9 100644
--- a/kalamar.conf
+++ b/kalamar.conf
@@ -39,6 +39,9 @@
     ## Backend API version
     # api_version => '1.0',
 
+    ## Run the application in a subfolder behind a proxy:
+    # proxy_prefix => '/korap',
+
     ## The name of the base corpus,
     ## for query examples (see kalamar.queries.dict)
     # examplecorpus => 'dereko',
@@ -48,6 +51,9 @@
     # plugins => [],
     ## Currently bundled: Piwik, Auth
 
+    ## Require everything to be send via https only:
+    # https_only => 1,
+
     ## Add experimental features:
     # experimental_proxy => 1,
   },
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 3f82113..a72ead5 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -4,11 +4,11 @@
 use Mojo::URL;
 use Mojo::File;
 use Mojo::JSON 'decode_json';
-use Mojo::Util qw/url_escape deprecated/;
+use Mojo::Util qw/url_escape deprecated slugify/;
 use List::Util 'none';
 
 # Minor version - may be patched from package.json
-our $VERSION = '0.35';
+our $VERSION = '0.36';
 
 # Supported version of Backend API
 our $API_VERSION = '1.0';
@@ -18,7 +18,6 @@
 # TODO: Embed collection statistics
 # TODO: Implement tab opener for matches and the tutorial
 # TODO: Implement a "projects" system
-# TODO: Make authentification a plugin
 
 # Start the application and register all routes and plugins
 sub startup {
@@ -80,12 +79,21 @@
     $self->log->warn('Kalamar-api_path not defined in configuration');
   };
 
+  $self->sessions->cookie_name('kalamar');
+
+  # Require HTTPS
+  if ($conf->{https_only}) {
+
+    # ... for cookie transport
+    $self->sessions->secure(1);
+  };
+
+  # Run the app from a subdirectory
   if ($conf->{proxy_prefix}) {
 
     for ($self->sessions) {
       $_->cookie_path($conf->{proxy_prefix});
-      $_->cookie_name('kalamar');
-      $_->secure(1);
+      $_->cookie_name('kalamar-' . slugify($conf->{proxy_prefix}));
     };
 
     # Set prefix in stash
diff --git a/package.json b/package.json
index 67cd9ea..9e58c1d 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.35.2",
+  "version": "0.36.1",
   "pluginVersion": "0.1",
   "repository": {
     "type": "git",
diff --git a/t/subfolder.t b/t/subfolder.t
new file mode 100644
index 0000000..01cf33e
--- /dev/null
+++ b/t/subfolder.t
@@ -0,0 +1,83 @@
+use Mojo::Base -strict;
+use Test::More;
+use Test::Mojo;
+use Mojo::File qw/path/;
+use utf8;
+
+my $t = Test::Mojo->new('Kalamar' => {
+  Kalamar => {
+    plugins => ['Auth']
+  }
+});
+
+$t->app->mode('production');
+
+$t->post_ok('/user/login' => form => { handle_or_email => 'test', pwd => 'fail' })
+  ->status_is(302)
+  ->header_is('Location' => '/');
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists('link[rel=stylesheet][href^=/css/kalamar-]')
+  ->element_exists('script[src^=/js/kalamar-]')
+  ->element_exists('div.notify-error')
+  ->text_is('div.notify-error', 'Bad CSRF token')
+  ->element_exists('input[name=handle_or_email][value=test]')
+  ->element_exists_not('div.button.top a')
+  ->content_like(qr!KorAP\.URL = ''!)
+  ;
+
+is('kalamar',$t->app->sessions->cookie_name);
+ok(!$t->app->sessions->secure);
+
+$t = Test::Mojo->new('Kalamar' => {
+  Kalamar => {
+    plugins => ['Auth'],
+    https_only => 1
+  }
+});
+
+$t->post_ok('/user/login' => form => { handle_or_email => 'test', pwd => 'fail' })
+  ->status_is(302)
+  ->header_is('Location' => '/');
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists_not('div.notify-error')
+  ;
+
+is('kalamar',$t->app->sessions->cookie_name);
+ok($t->app->sessions->secure);
+
+$t = Test::Mojo->new('Kalamar' => {
+  Kalamar => {
+    plugins => ['Auth'],
+    proxy_prefix => '/korap/test',
+    https_only => 1
+  }
+});
+
+$t->app->mode('production');
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists('link[rel=stylesheet][href^=/korap/test/css/kalamar-]')
+  ->element_exists('script[src^=/korap/test/js/kalamar-]')
+  ;
+
+is('kalamar-koraptest',$t->app->sessions->cookie_name);
+ok($t->app->sessions->secure);
+
+$t->post_ok('/user/login' => form => { handle_or_email => 'test', pwd => 'fail' })
+  ->status_is(302)
+  ->header_is('Location' => '/');
+
+# Session can't be used
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists_not('div.notify-error')
+  ->content_like(qr!KorAP\.URL = '/korap/test'!)
+  ;
+
+
+done_testing();