blob: c8df357da80fd56e485e8fe9e42630a482ecc16e [file] [log] [blame]
Akron479994e2018-07-02 13:21:44 +02001/**
2 * The plugin system is based
3 * on registered widgets (iframes) from
4 * foreign services.
5 * The server component spawns new iframes and
6 * listens to them.
7 *
8 * @author Nils Diewald
9 */
10
Akrona6c32b92018-07-02 18:39:42 +020011define(["plugin/widget", "util"], function (widgetClass) {
Akron479994e2018-07-02 13:21:44 +020012 "use strict";
13
Akrona6c32b92018-07-02 18:39:42 +020014 // Contains all widgets to address with
15 // messages to them
16 var widgets = {};
Akron7c6e05f2018-07-12 19:08:13 +020017 var plugins = {};
Akron10a47962018-07-12 21:17:10 +020018
19 // TODO:
20 // These should better be panels and every panel
21 // has a buttonGroup
Akron7c6e05f2018-07-12 19:08:13 +020022 var buttons = {
23 match : []
24 };
Akron10a47962018-07-12 21:17:10 +020025 var panels = {
26 match : 1
27 };
28
Akrone8e2c952018-07-04 13:43:12 +020029 // This is a counter to limit acceptable incoming messages
30 // to a certain amount. For every message, this counter will
31 // be decreased (down to 0), for every second this will be
32 // increased (up to 100).
33 // Once a widget surpasses the limit, it will be killed
34 // and called suspicious.
35 var maxMessages = 100;
36 var limits = {};
37
38 // TODO:
39 // It may be useful to establish a watcher that pings
40 // all widgets every second to see if it is still alive.
41
Akron479994e2018-07-02 13:21:44 +020042 return {
43
44 /**
45 * Create new plugin management system
46 */
47 create : function () {
48 return Object.create(this)._init();
49 },
50
51 /*
Akron76dd8d32018-07-06 09:30:22 +020052 * Initialize the plugin manager
Akron479994e2018-07-02 13:21:44 +020053 */
54 _init : function () {
Akron479994e2018-07-02 13:21:44 +020055 return this;
56 },
57
58 /**
Akron7c6e05f2018-07-12 19:08:13 +020059 * Register a plugin described as a JSON object.
60 *
61 * This is work in progress.
Akron10a47962018-07-12 21:17:10 +020062 *
63 * Example:
64 *
65 * KorAP.Plugin.register({
66 * 'name' : 'CatContent',
67 * 'desc' : 'Some content about cats',
68 * 'about' : 'https://localhost:5678/',
69 * 'embed' : [{
70 * 'panel' : 'match',
71 * 'title' : loc.TRANSLATE,
72 * 'classes' : ['translate']
73 * 'onClick' : {
74 * 'action' : 'addWidget',
75 * 'template' : 'https://localhost:5678/?match={matchid}',
76 * }
77 * }]
78 * });
79 *
Akron7c6e05f2018-07-12 19:08:13 +020080 */
81 register : function (obj) {
Akron7c6e05f2018-07-12 19:08:13 +020082 // TODO:
Akron10a47962018-07-12 21:17:10 +020083 // These fields need to be localized for display by a structure like
84 // { de : { name : '..' }, en : { .. } }
Akron7c6e05f2018-07-12 19:08:13 +020085 var name = obj["name"];
86
Akron10a47962018-07-12 21:17:10 +020087 if (!name)
88 throw new Error("Missing name of plugin");
89
Akron7c6e05f2018-07-12 19:08:13 +020090 // Register plugin by name
91 var plugin = plugins[name] = {
92 name : name,
93 desc : obj["desc"],
94 about : obj["about"],
95 widgets : []
96 };
Akron10a47962018-07-12 21:17:10 +020097
98 if (typeof obj["embed"] !== 'object')
99 throw new Error("Embedding of plugin is no list");
Akron7c6e05f2018-07-12 19:08:13 +0200100
101 // Embed all embeddings of the plugin
102 for (var i in obj["embed"]) {
103 var embed = obj["embed"][i];
Akron10a47962018-07-12 21:17:10 +0200104
105 if (typeof embed !== 'object')
106 throw new Error("Embedding of plugin is no object");
107
108 var panel = embed["panel"];
109
110 if (!panel || !buttons[panel])
111 throw new Error("Panel for plugin is invalid");
112
Akron7c6e05f2018-07-12 19:08:13 +0200113 var onClick = embed["onClick"];
114
115 // Needs to be localized as well
116 var title = embed["title"];
117
118 // The embedding will open a widget
Akron10a47962018-07-12 21:17:10 +0200119 if (!onClick["action"] || onClick["action"] == "addWidget") {
Akron7c6e05f2018-07-12 19:08:13 +0200120
Akron10a47962018-07-12 21:17:10 +0200121 var panel = document.getElementById(panel);
Akron7c6e05f2018-07-12 19:08:13 +0200122 var that = this;
123 var cb = function (e) {
124
125 // Get the URL of the widget
126 var url = onClick["template"]; // that._interpolateURI(onClick["template"], this.match);
127
128 // Add the widget to the panel
Akron10a47962018-07-12 21:17:10 +0200129 var id = that.addWidget(panel, name, url);
Akron7c6e05f2018-07-12 19:08:13 +0200130 plugin["widgets"].push(id);
131 };
132
Akron10a47962018-07-12 21:17:10 +0200133 buttons[pannel].push([title, embed["classes"], cb]);
Akron7c6e05f2018-07-12 19:08:13 +0200134 };
135 };
136 },
137
138
139 // TODO:
140 // Interpolate URIs similar to https://tools.ietf.org/html/rfc6570
141 // but as simple as possible
142 _interpolateURI : function (uri, obj) {
143 // ...
144 },
145
146
147 /**
148 * Get named button group
149 */
150 buttonGroup : function (name) {
151 return buttons[name];
152 },
153
154 /**
Akrone8e2c952018-07-04 13:43:12 +0200155 * Open a new widget as a child to a certain element
Akron479994e2018-07-02 13:21:44 +0200156 */
Akron7991b192018-07-09 17:28:43 +0200157 addWidget : function (element, name, src) {
Akron479994e2018-07-02 13:21:44 +0200158
Akron76dd8d32018-07-06 09:30:22 +0200159 // Is it the first widget?
160 if (!this._listener) {
161
162 /*
163 * Establish the global 'message' hook.
164 */
165 this._listener = this._receiveMsg.bind(this);
166 window.addEventListener("message", this._listener);
167
168 // Every second increase the limits of all registered widgets
169 this._timer = window.setInterval(function () {
170 for (var i in limits) {
171 if (limits[i]++ >= maxMessages) {
172 limits[i] = maxMessages;
173 }
174 }
175 }, 1000);
176 };
177
Akrona6c32b92018-07-02 18:39:42 +0200178 // Create a unique random ID per widget
179 var id = 'id-' + this._randomID();
180
181 // Create a new widget
Akron7991b192018-07-09 17:28:43 +0200182 var widget = widgetClass.create(name, src, id);
Akrona6c32b92018-07-02 18:39:42 +0200183
184 // Store the widget based on the identifier
185 widgets[id] = widget;
Akrona99315e2018-07-03 22:56:45 +0200186 limits[id] = maxMessages;
Akrona6c32b92018-07-02 18:39:42 +0200187
188 // Open widget in frontend
Akron7c6e05f2018-07-12 19:08:13 +0200189 // TODO:
190 // Instead of an "element" this should probably be a 'panel' object!
Akrona6c32b92018-07-02 18:39:42 +0200191 element.appendChild(
192 widget.element()
193 );
Akronb43c8c62018-07-04 18:27:28 +0200194
195 return id;
Akron479994e2018-07-02 13:21:44 +0200196 },
197
Akrone8e2c952018-07-04 13:43:12 +0200198 // Receive a call from an embedded iframe.
199 // The handling needs to be very careful,
200 // as this can easily become a security nightmare.
Akron479994e2018-07-02 13:21:44 +0200201 _receiveMsg : function (e) {
202 // Get event data
203 var d = e.data;
204
Akrona99315e2018-07-03 22:56:45 +0200205 // If no data given - fail
206 // (probably check that it's an assoc array)
207 if (!d)
208 return;
209
210 // e.origin is probably set and okay - CHECK!
Akron479994e2018-07-02 13:21:44 +0200211
Akrona99315e2018-07-03 22:56:45 +0200212 // Get origin ID
213 var id = d["originID"];
214
215 // If no origin ID given - fail
216 if (!id)
217 return;
218
Akrona6c32b92018-07-02 18:39:42 +0200219 // Get the widget
Akrona99315e2018-07-03 22:56:45 +0200220 var widget = widgets[id];
Akrona6c32b92018-07-02 18:39:42 +0200221
222 // If the addressed widget does not exist - fail
223 if (!widget)
224 return;
225
Akrona99315e2018-07-03 22:56:45 +0200226 // Check for message limits
227 if (limits[id]-- < 0) {
Akrone8e2c952018-07-04 13:43:12 +0200228
229 // Kill widget
Akronc0a2da82018-07-04 15:27:37 +0200230 KorAP.log(0, 'Suspicious action by widget', widget.src);
Akron7c6e05f2018-07-12 19:08:13 +0200231
232 // TODO:
233 // Potentially kill the whole plugin!
Akron76dd8d32018-07-06 09:30:22 +0200234 this.closeWidget(widget);
Akrona99315e2018-07-03 22:56:45 +0200235 return;
236 };
Akrona6c32b92018-07-02 18:39:42 +0200237
Akron479994e2018-07-02 13:21:44 +0200238 // Resize the iframe
239 if (d.action === 'resize') {
Akrona6c32b92018-07-02 18:39:42 +0200240 widget.resize(d);
Akron479994e2018-07-02 13:21:44 +0200241 }
242
243 // Log message from iframe
244 else if (d.action === 'log') {
Akronc0a2da82018-07-04 15:27:37 +0200245 KorAP.log(d.code, d.msg, widget.src);
Akrona6c32b92018-07-02 18:39:42 +0200246 };
247
248 // TODO:
249 // Close
Akron479994e2018-07-02 13:21:44 +0200250 },
251
Akron76dd8d32018-07-06 09:30:22 +0200252 // Close the widget
253 closeWidget : function (widget) {
254 delete limits[widget.id];
255 delete widgets[widget.id];
256 widget.shutdown();
257
258 // Remove listeners in case no widget
259 // is available any longer
260 if (Object.keys(limits).length == 0)
261 this._removeListener();
262 },
263
Akrona6c32b92018-07-02 18:39:42 +0200264 // Get a random identifier
265 _randomID : function () {
266 return randomID(20);
Akronb43c8c62018-07-04 18:27:28 +0200267 },
268
Akron76dd8d32018-07-06 09:30:22 +0200269 // Remove the listener
270 _removeListener : function () {
271 window.clearInterval(this._timer);
272 this._timer = undefined;
273 window.removeEventListener("message", this._listener);
274 this._listener = undefined;
275 },
276
Akronb43c8c62018-07-04 18:27:28 +0200277 // Destructor, just for testing scenarios
278 destroy : function () {
279 limits = {};
280 widgets = {};
Akron76dd8d32018-07-06 09:30:22 +0200281 this._removeListener();
Akron479994e2018-07-02 13:21:44 +0200282 }
283 }
284});