Support different sandbox permissions on plugin registration (Fixes #112)

Change-Id: I0f99e378c44c6e53ac0a2f75727311864e73bf82
diff --git a/Changes b/Changes
index 46a7bcf..0366a26 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.39 2020-10-01
+0.39 2020-10-05
         - Add information on secret file to Readme.
         - Change default API endpoint to korap.ids-mannheim.de.
         - Fix label for toggle plugins.
@@ -25,6 +25,8 @@
         - Add clear() method to state objects.
         - Fix "setWidget" action when the service was closed
           instead of minimized.
+        - Add registrable permissions to widgets and services
+          (#112).
 
         WARNING: If you relied on the former default API endpoint
           being http://localhost:9999/, this will break your
diff --git a/dev/js/spec/pluginSpec.js b/dev/js/spec/pluginSpec.js
index 5cd7ce5..e2dab8f 100644
--- a/dev/js/spec/pluginSpec.js
+++ b/dev/js/spec/pluginSpec.js
@@ -237,11 +237,36 @@
       expect(p.element().querySelectorAll("iframe").length).toEqual(1);
       expect(p.element().querySelectorAll("div.view.widget").length).toEqual(1);
       expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+      expect(p.element().querySelector("iframe").getAttribute('sandbox')).toEqual('');
       
       manager.destroy();
 
       KorAP.Panel["result"] = undefined;
     });
+
+    it('should accept widget permissions', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      
+      let manager = pluginServerClass.create();
+
+      manager.register({
+        name : 'Check',
+        embed : [{
+          panel : 'result',
+          title : 'Add',
+          onClick : {
+            template : 'about:blank',
+            action : 'addWidget',
+            permissions: ['allow-scripts', 'allow-forms']
+          }
+        }]
+      });
+
+      let b = p.actions.element().firstChild;
+      b.click();
+      expect(p.element().querySelectorAll("iframe").length).toEqual(1);
+      expect(p.element().querySelector("iframe").getAttribute('sandbox')).toEqual('allow-forms allow-scripts');
+    });
   });
   
   describe('KorAP.Plugin.Widget', function () {
@@ -257,6 +282,8 @@
 
     it('should create a view element', function () {
       var widget = widgetClass.create("Test", "https://example", 56);
+      widget.allow("allow-scripts");
+      widget.allow("allow-forms");
       var we = widget.element();
 
       expect(we.tagName).toEqual("DIV");
@@ -265,10 +292,14 @@
 
       var iframe = we.firstChild;
       expect(iframe.tagName).toEqual("IFRAME");
-      expect(iframe.getAttribute("sandbox")).toEqual("allow-scripts allow-forms");
+      expect(iframe.getAttribute("sandbox")).toEqual("allow-forms allow-scripts");
       expect(iframe.getAttribute("src")).toEqual("https://example");
       expect(iframe.getAttribute("name")).toEqual("56");
 
+
+      widget.allow(["allow-downloads","allow-everything"]);
+      expect(iframe.getAttribute("sandbox")).toEqual("allow-downloads allow-everything allow-forms allow-scripts");
+      
       var btn = we.lastChild;
       expect(btn.classList.contains("button-group")).toBeTruthy();
       expect(btn.classList.contains("button-view")).toBeTruthy();
@@ -286,6 +317,18 @@
       expect(btn.lastChild.textContent).toEqual("Test");
     })
 
+    it('should have mutable permissions', function () {
+      var widget = widgetClass.create("Test", "https://example", 56);
+      var we = widget.element();
+      var iframe = we.firstChild;
+      expect(iframe.tagName).toEqual("IFRAME");
+      expect(iframe.getAttribute("sandbox")).toEqual("");
+      widget.allow("allow-scripts");
+      widget.allow("allow-forms");
+      expect(iframe.tagName).toEqual("IFRAME");
+      expect(iframe.getAttribute("sandbox")).toEqual("allow-forms allow-scripts");
+    });
+    
     it('should be resizable', function () {
       var widget = widgetClass.create("Test", "https://example", 56);
       var iframe = widget.show();
diff --git a/dev/js/src/plugin/server.js b/dev/js/src/plugin/server.js
index 7f9bc85..2422892 100644
--- a/dev/js/src/plugin/server.js
+++ b/dev/js/src/plugin/server.js
@@ -153,6 +153,8 @@
             let url = onClick["template"];
             // that._interpolateURI(onClick["template"], this.match);
 
+            let perm = onClick["permissions"];
+
             // The button has a state and the state is associated to the
             // a intermediate object to toggle the view
             if ('state' in this.button && this.button.state.associates() > 0) {
@@ -182,7 +184,7 @@
             };
 
             // Add the widget to the panel
-            let id = that.addWidget(this, name, url);
+            let id = that.addWidget(this, name, url, perm);
             plugin["widgets"].push(id);
             
             // If a state exists, associate with a mediator object
@@ -247,6 +249,8 @@
           // Add the service
           let id = this.addService(name, url);
 
+          let perm = onClick["permissions"];
+
           // TODO:
           //   This is a bit stupid to get the service window
           let service = services[id];
@@ -334,7 +338,7 @@
     /**
      * Add a service in a certain panel and return the id.
      */
-    addService : function (name, src) {
+    addService : function (name, src, permissions) {
       if (!src)
         return;
 
@@ -342,6 +346,9 @@
 
       // Create a new service
       let service = serviceClass.create(name, src, id);
+      if (permissions != undefined) {
+        service.allow(permissions);
+      };     
       
       services[id] = service;
       limits[id] = maxMessages;
@@ -359,12 +366,15 @@
      * Open a new widget view in a certain panel and return
      * the id.
      */
-    addWidget : function (panel, name, src) {
+    addWidget : function (panel, name, src, permissions) {
 
       let id = this._getServiceID();
 
       // Create a new widget
       var widget = widgetClass.create(name, src, id);
+      if (permissions != undefined) {
+        widget.allow(permissions);
+      };
 
       // Store the widget based on the identifier
       services[id] = widget;
diff --git a/dev/js/src/plugin/service.js b/dev/js/src/plugin/service.js
index 3152f38..4029349 100644
--- a/dev/js/src/plugin/service.js
+++ b/dev/js/src/plugin/service.js
@@ -13,7 +13,8 @@
       this.name = name;
       this.src = src;
       this.id = id;
-
+      this._perm = new Set();
+      
       // There is no close method defined yet
       if (!this.close) {
         this.close = function () {
@@ -42,7 +43,7 @@
       e.setAttribute('allowTransparency',"true");
       e.setAttribute('frameborder', 0);
       // Allow forms in Plugins
-      e.setAttribute('sandbox','allow-scripts allow-forms');
+      e.setAttribute('sandbox', this._permString());
       e.style.height = '0px';
       e.setAttribute('name', this.id);
       e.setAttribute('src', this.src);
@@ -51,6 +52,25 @@
       return e;
     },
 
+    allow : function (permission) {
+      if (Array.isArray(permission)) {
+        permission.forEach(
+          p => this._perm.add(p)
+        );
+      }
+      else {
+        this._perm.add(permission);
+      };
+
+      if (this._load) {
+        this._load.setAttribute('sandbox', this._permString());
+      }
+    },
+
+    _permString : function () {
+      return Array.from(this._perm).sort().join(" ");
+    },
+
     /**
      * Send a message to the embedded service.
      */
diff --git a/package.json b/package.json
index 2b0888e..a04794c 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.39.3",
+  "version": "0.39.4",
   "pluginVersion": "0.2.2",
   "engines": {
     "node": ">=6.0.0"