blob: 69318b095a4175e5312c43a3bb46aa85f7912fef [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
Akron2d0d96d2019-11-18 19:49:50 +010014 KorAP.Panel = KorAP.Panel || {};
15
Akrona6c32b92018-07-02 18:39:42 +020016 // Contains all widgets to address with
17 // messages to them
18 var widgets = {};
Akron7c6e05f2018-07-12 19:08:13 +020019 var plugins = {};
Akron10a47962018-07-12 21:17:10 +020020
21 // TODO:
22 // These should better be panels and every panel
23 // has a buttonGroup
Akron2d0d96d2019-11-18 19:49:50 +010024
25 // List of panels with dynamic buttons, i.e.
26 // panels that may occur multiple times.
Akron7c6e05f2018-07-12 19:08:13 +020027 var buttons = {
28 match : []
29 };
Akron2d0d96d2019-11-18 19:49:50 +010030
31 // List of panels with static buttons, i.e.
32 // panels that occur only once.
33 var buttonsSingle = {
34 query : []
35 }
Akron10a47962018-07-12 21:17:10 +020036
Akrone8e2c952018-07-04 13:43:12 +020037 // This is a counter to limit acceptable incoming messages
38 // to a certain amount. For every message, this counter will
39 // be decreased (down to 0), for every second this will be
40 // increased (up to 100).
41 // Once a widget surpasses the limit, it will be killed
42 // and called suspicious.
43 var maxMessages = 100;
44 var limits = {};
45
46 // TODO:
47 // It may be useful to establish a watcher that pings
Akrone1c27f62018-07-20 11:42:59 +020048 // all widgets every second to see if it is still alive,
49 // otherwise kill
Akrone8e2c952018-07-04 13:43:12 +020050
Akrone1c27f62018-07-20 11:42:59 +020051 // Load Plugin server
Akron479994e2018-07-02 13:21:44 +020052 return {
53
54 /**
55 * Create new plugin management system
56 */
57 create : function () {
58 return Object.create(this)._init();
59 },
60
61 /*
Akron76dd8d32018-07-06 09:30:22 +020062 * Initialize the plugin manager
Akron479994e2018-07-02 13:21:44 +020063 */
64 _init : function () {
Akron479994e2018-07-02 13:21:44 +020065 return this;
66 },
67
68 /**
Akron7c6e05f2018-07-12 19:08:13 +020069 * Register a plugin described as a JSON object.
70 *
71 * This is work in progress.
Akron10a47962018-07-12 21:17:10 +020072 *
73 * Example:
74 *
75 * KorAP.Plugin.register({
76 * 'name' : 'CatContent',
77 * 'desc' : 'Some content about cats',
78 * 'about' : 'https://localhost:5678/',
79 * 'embed' : [{
80 * 'panel' : 'match',
81 * 'title' : loc.TRANSLATE,
82 * 'classes' : ['translate']
83 * 'onClick' : {
84 * 'action' : 'addWidget',
85 * 'template' : 'https://localhost:5678/?match={matchid}',
86 * }
87 * }]
88 * });
89 *
Akron7c6e05f2018-07-12 19:08:13 +020090 */
91 register : function (obj) {
Akron7c6e05f2018-07-12 19:08:13 +020092 // TODO:
Akron10a47962018-07-12 21:17:10 +020093 // These fields need to be localized for display by a structure like
94 // { de : { name : '..' }, en : { .. } }
Akron7c6e05f2018-07-12 19:08:13 +020095 var name = obj["name"];
96
Akron10a47962018-07-12 21:17:10 +020097 if (!name)
98 throw new Error("Missing name of plugin");
99
Akron7c6e05f2018-07-12 19:08:13 +0200100 // Register plugin by name
101 var plugin = plugins[name] = {
102 name : name,
103 desc : obj["desc"],
104 about : obj["about"],
105 widgets : []
106 };
Akron10a47962018-07-12 21:17:10 +0200107
108 if (typeof obj["embed"] !== 'object')
109 throw new Error("Embedding of plugin is no list");
Akron7c6e05f2018-07-12 19:08:13 +0200110
111 // Embed all embeddings of the plugin
Akrone1c27f62018-07-20 11:42:59 +0200112 var that = this;
Akron7c6e05f2018-07-12 19:08:13 +0200113 for (var i in obj["embed"]) {
114 var embed = obj["embed"][i];
Akron10a47962018-07-12 21:17:10 +0200115
116 if (typeof embed !== 'object')
117 throw new Error("Embedding of plugin is no object");
118
119 var panel = embed["panel"];
120
Akron2d0d96d2019-11-18 19:49:50 +0100121 if (!panel || !(buttons[panel] || buttonsSingle[panel]))
Akron10a47962018-07-12 21:17:10 +0200122 throw new Error("Panel for plugin is invalid");
123
Akron7c6e05f2018-07-12 19:08:13 +0200124 var onClick = embed["onClick"];
125
126 // Needs to be localized as well
127 var title = embed["title"];
128
129 // The embedding will open a widget
Akron10a47962018-07-12 21:17:10 +0200130 if (!onClick["action"] || onClick["action"] == "addWidget") {
Akron7c6e05f2018-07-12 19:08:13 +0200131
Akron7c6e05f2018-07-12 19:08:13 +0200132 var cb = function (e) {
133
Akrone1c27f62018-07-20 11:42:59 +0200134 // "this" is bind to the panel
135
Akron7c6e05f2018-07-12 19:08:13 +0200136 // Get the URL of the widget
Akrone1c27f62018-07-20 11:42:59 +0200137 var url = onClick["template"];
138 // that._interpolateURI(onClick["template"], this.match);
Akron7c6e05f2018-07-12 19:08:13 +0200139
140 // Add the widget to the panel
Akrone1c27f62018-07-20 11:42:59 +0200141 var id = that.addWidget(this, name, url);
Akron7c6e05f2018-07-12 19:08:13 +0200142 plugin["widgets"].push(id);
143 };
144
Akron2d0d96d2019-11-18 19:49:50 +0100145 // Add to dynamic button list (e.g. for matches)
146 if (buttons[panel]) {
147 buttons[panel].push([title, embed["classes"], cb]);
148 }
149
150 // Add to static button list (e.g. for query) already loaded
151 else if (KorAP.Panel[panel]) {
152 KorAP.Panel[panel].actions.add(title, embed["classes"], cb);
153 }
154
155 // Add to static button list (e.g. for query) not yet loaded
156 else {
157 buttonsSingle[panel].push([title, embed["classes"], cb]);
158 }
Akron7c6e05f2018-07-12 19:08:13 +0200159 };
160 };
161 },
162
163
164 // TODO:
165 // Interpolate URIs similar to https://tools.ietf.org/html/rfc6570
166 // but as simple as possible
167 _interpolateURI : function (uri, obj) {
168 // ...
169 },
170
171
172 /**
Akron4a703872018-07-26 10:59:41 +0200173 * Get named button group - better rename to "action"
Akron7c6e05f2018-07-12 19:08:13 +0200174 */
175 buttonGroup : function (name) {
Akron2d0d96d2019-11-18 19:49:50 +0100176 if (buttons[name] != undefined) {
177 return buttons[name];
178 } else if (buttonsSingle[name] != undefined) {
179 return buttonsSingle[name];
180 };
181 return [];
182 },
183
184 /**
185 * Clear named button group - better rename to "action"
186 */
187 clearButtonGroup : function (name) {
188 if (buttons[name] != undefined) {
189 buttons[name] = [];
190 } else if (buttonsSingle[name] != undefined) {
191 buttonsSingle[name] = [];
192 }
Akron7c6e05f2018-07-12 19:08:13 +0200193 },
194
195 /**
Akron4a703872018-07-26 10:59:41 +0200196 * Open a new widget view in a certain panel and return
197 * the id.
Akron479994e2018-07-02 13:21:44 +0200198 */
Akrone1c27f62018-07-20 11:42:59 +0200199 addWidget : function (panel, name, src) {
Akron479994e2018-07-02 13:21:44 +0200200
Akron4a703872018-07-26 10:59:41 +0200201 if (!src)
202 return;
203
Akron76dd8d32018-07-06 09:30:22 +0200204 // Is it the first widget?
205 if (!this._listener) {
206
207 /*
208 * Establish the global 'message' hook.
209 */
210 this._listener = this._receiveMsg.bind(this);
211 window.addEventListener("message", this._listener);
212
213 // Every second increase the limits of all registered widgets
214 this._timer = window.setInterval(function () {
215 for (var i in limits) {
216 if (limits[i]++ >= maxMessages) {
217 limits[i] = maxMessages;
218 }
219 }
220 }, 1000);
221 };
222
Akrona6c32b92018-07-02 18:39:42 +0200223 // Create a unique random ID per widget
224 var id = 'id-' + this._randomID();
225
226 // Create a new widget
Akron7991b192018-07-09 17:28:43 +0200227 var widget = widgetClass.create(name, src, id);
Akrona6c32b92018-07-02 18:39:42 +0200228
229 // Store the widget based on the identifier
230 widgets[id] = widget;
Akrona99315e2018-07-03 22:56:45 +0200231 limits[id] = maxMessages;
Akrona6c32b92018-07-02 18:39:42 +0200232
Akron4a703872018-07-26 10:59:41 +0200233 widget._mgr = this;
234
Akrone1c27f62018-07-20 11:42:59 +0200235 // Add widget to panel
236 panel.add(widget);
Akronb43c8c62018-07-04 18:27:28 +0200237
238 return id;
Akron479994e2018-07-02 13:21:44 +0200239 },
240
Akron4a703872018-07-26 10:59:41 +0200241
242 /**
243 * Get widget by identifier
244 */
245 widget : function (id) {
246 return widgets[id];
247 },
248
249
Akrone8e2c952018-07-04 13:43:12 +0200250 // Receive a call from an embedded iframe.
251 // The handling needs to be very careful,
252 // as this can easily become a security nightmare.
Akron479994e2018-07-02 13:21:44 +0200253 _receiveMsg : function (e) {
254 // Get event data
255 var d = e.data;
256
Akrona99315e2018-07-03 22:56:45 +0200257 // If no data given - fail
258 // (probably check that it's an assoc array)
259 if (!d)
260 return;
261
262 // e.origin is probably set and okay - CHECK!
Akron479994e2018-07-02 13:21:44 +0200263
Akrona99315e2018-07-03 22:56:45 +0200264 // Get origin ID
265 var id = d["originID"];
266
267 // If no origin ID given - fail
268 if (!id)
269 return;
270
Akrona6c32b92018-07-02 18:39:42 +0200271 // Get the widget
Akrona99315e2018-07-03 22:56:45 +0200272 var widget = widgets[id];
Akrona6c32b92018-07-02 18:39:42 +0200273
274 // If the addressed widget does not exist - fail
275 if (!widget)
276 return;
277
Akrona99315e2018-07-03 22:56:45 +0200278 // Check for message limits
279 if (limits[id]-- < 0) {
Akrone8e2c952018-07-04 13:43:12 +0200280
281 // Kill widget
Akronc0a2da82018-07-04 15:27:37 +0200282 KorAP.log(0, 'Suspicious action by widget', widget.src);
Akron7c6e05f2018-07-12 19:08:13 +0200283
284 // TODO:
285 // Potentially kill the whole plugin!
Akron4a703872018-07-26 10:59:41 +0200286
287 // This removes all connections before closing the widget
288 this._closeWidget(widget.id);
289 widget.close();
Akrona99315e2018-07-03 22:56:45 +0200290 return;
291 };
Akrona6c32b92018-07-02 18:39:42 +0200292
Akron479994e2018-07-02 13:21:44 +0200293 // Resize the iframe
294 if (d.action === 'resize') {
Akrona6c32b92018-07-02 18:39:42 +0200295 widget.resize(d);
Akron479994e2018-07-02 13:21:44 +0200296 }
297
298 // Log message from iframe
299 else if (d.action === 'log') {
Akronc0a2da82018-07-04 15:27:37 +0200300 KorAP.log(d.code, d.msg, widget.src);
Akrona6c32b92018-07-02 18:39:42 +0200301 };
302
303 // TODO:
304 // Close
Akron479994e2018-07-02 13:21:44 +0200305 },
306
Akron76dd8d32018-07-06 09:30:22 +0200307 // Close the widget
Akron4a703872018-07-26 10:59:41 +0200308 _closeWidget : function (id) {
309 delete limits[id];
310 delete widgets[id];
Akron76dd8d32018-07-06 09:30:22 +0200311
312 // Remove listeners in case no widget
313 // is available any longer
314 if (Object.keys(limits).length == 0)
315 this._removeListener();
316 },
317
Akrona6c32b92018-07-02 18:39:42 +0200318 // Get a random identifier
319 _randomID : function () {
320 return randomID(20);
Akronb43c8c62018-07-04 18:27:28 +0200321 },
322
Akron76dd8d32018-07-06 09:30:22 +0200323 // Remove the listener
324 _removeListener : function () {
325 window.clearInterval(this._timer);
326 this._timer = undefined;
327 window.removeEventListener("message", this._listener);
328 this._listener = undefined;
329 },
330
Akronb43c8c62018-07-04 18:27:28 +0200331 // Destructor, just for testing scenarios
332 destroy : function () {
333 limits = {};
Akron4a703872018-07-26 10:59:41 +0200334 for (let w in widgets) {
335 widgets[w].close();
336 };
Akronb43c8c62018-07-04 18:27:28 +0200337 widgets = {};
Akron2d0d96d2019-11-18 19:49:50 +0100338 for (let b in buttons) {
339 buttons[b] = [];
340 };
341 for (let b in buttonsSingle) {
342 buttonsSingle[b] = [];
343 };
Akron76dd8d32018-07-06 09:30:22 +0200344 this._removeListener();
Akron479994e2018-07-02 13:21:44 +0200345 }
Akrone1c27f62018-07-20 11:42:59 +0200346 };
Akron479994e2018-07-02 13:21:44 +0200347});