blob: 1c46dbe1ff8fab0e1d9907e8030615d9c0cf6c9f [file] [log] [blame]
Akron479994e2018-07-02 13:21:44 +02001/**
2 * The plugin system is based
Akron22598cd2019-12-09 14:59:03 +01003 * on registered services (iframes) from
Akron479994e2018-07-02 13:21:44 +02004 * foreign services.
5 * The server component spawns new iframes and
6 * listens to them.
7 *
8 * @author Nils Diewald
9 */
10
Akron22598cd2019-12-09 14:59:03 +010011define(["plugin/widget", 'plugin/service', 'state', "util"], function (widgetClass, serviceClass, stateClass) {
Akron479994e2018-07-02 13:21:44 +020012 "use strict";
13
Akron2d0d96d2019-11-18 19:49:50 +010014 KorAP.Panel = KorAP.Panel || {};
15
Akron22598cd2019-12-09 14:59:03 +010016 // Contains all servicess to address with
Akrona6c32b92018-07-02 18:39:42 +020017 // messages to them
Akron22598cd2019-12-09 14:59:03 +010018 var services = {};
19 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 = {
hebasta043e96f2019-11-28 12:33:00 +010034 query : [],
35 result : []
Akron22598cd2019-12-09 14:59:03 +010036 }
Akron10a47962018-07-12 21:17:10 +020037
Akrone8e2c952018-07-04 13:43:12 +020038 // This is a counter to limit acceptable incoming messages
39 // to a certain amount. For every message, this counter will
40 // be decreased (down to 0), for every second this will be
41 // increased (up to 100).
42 // Once a widget surpasses the limit, it will be killed
43 // and called suspicious.
44 var maxMessages = 100;
45 var limits = {};
46
47 // TODO:
48 // It may be useful to establish a watcher that pings
Akrone1c27f62018-07-20 11:42:59 +020049 // all widgets every second to see if it is still alive,
50 // otherwise kill
Akrone8e2c952018-07-04 13:43:12 +020051
Akrone1c27f62018-07-20 11:42:59 +020052 // Load Plugin server
Akron479994e2018-07-02 13:21:44 +020053 return {
54
55 /**
56 * Create new plugin management system
57 */
58 create : function () {
59 return Object.create(this)._init();
60 },
61
62 /*
Akron76dd8d32018-07-06 09:30:22 +020063 * Initialize the plugin manager
Akron479994e2018-07-02 13:21:44 +020064 */
65 _init : function () {
Akron479994e2018-07-02 13:21:44 +020066 return this;
67 },
68
69 /**
Akron7c6e05f2018-07-12 19:08:13 +020070 * Register a plugin described as a JSON object.
71 *
72 * This is work in progress.
Akron10a47962018-07-12 21:17:10 +020073 *
74 * Example:
75 *
76 * KorAP.Plugin.register({
77 * 'name' : 'CatContent',
78 * 'desc' : 'Some content about cats',
79 * 'about' : 'https://localhost:5678/',
80 * 'embed' : [{
81 * 'panel' : 'match',
82 * 'title' : loc.TRANSLATE,
83 * 'classes' : ['translate']
84 * 'onClick' : {
85 * 'action' : 'addWidget',
86 * 'template' : 'https://localhost:5678/?match={matchid}',
87 * }
Akron22598cd2019-12-09 14:59:03 +010088 * },{
89 * 'title' : 'glemm',
90 * 'panel' : 'query',
91 * 'onClick' : {
92 * 'action' : 'toggle',
93 * 'state' : 'glemm',
94 * 'service' : {
95 * 'id' : 'glemm',
96 * 'template' : 'https://localhost:5678/'
97 * }
98 * }
Akron10a47962018-07-12 21:17:10 +020099 * }]
100 * });
101 *
Akron7c6e05f2018-07-12 19:08:13 +0200102 */
103 register : function (obj) {
Akron7c6e05f2018-07-12 19:08:13 +0200104 // TODO:
Akron10a47962018-07-12 21:17:10 +0200105 // These fields need to be localized for display by a structure like
106 // { de : { name : '..' }, en : { .. } }
Akron7c6e05f2018-07-12 19:08:13 +0200107 var name = obj["name"];
108
Akron10a47962018-07-12 21:17:10 +0200109 if (!name)
110 throw new Error("Missing name of plugin");
111
Akron7c6e05f2018-07-12 19:08:13 +0200112 // Register plugin by name
113 var plugin = plugins[name] = {
114 name : name,
115 desc : obj["desc"],
116 about : obj["about"],
Akron22598cd2019-12-09 14:59:03 +0100117 widgets : [],
118 services : []
Akron7c6e05f2018-07-12 19:08:13 +0200119 };
Akron10a47962018-07-12 21:17:10 +0200120
121 if (typeof obj["embed"] !== 'object')
122 throw new Error("Embedding of plugin is no list");
Akron7c6e05f2018-07-12 19:08:13 +0200123
124 // Embed all embeddings of the plugin
Akrone1c27f62018-07-20 11:42:59 +0200125 var that = this;
Akron22598cd2019-12-09 14:59:03 +0100126 for (let i in obj["embed"]) {
127 let embed = obj["embed"][i];
Akron10a47962018-07-12 21:17:10 +0200128
129 if (typeof embed !== 'object')
130 throw new Error("Embedding of plugin is no object");
131
Akron7c6e05f2018-07-12 19:08:13 +0200132 // Needs to be localized as well
Akron22598cd2019-12-09 14:59:03 +0100133 let title = embed["title"];
134 let panel = embed["panel"];
135 let onClick = embed["onClick"];
136
137
138 if (!panel || !(buttons[panel] || buttonsSingle[panel]))
139 throw new Error("Panel for plugin is invalid");
Akron7c6e05f2018-07-12 19:08:13 +0200140
141 // The embedding will open a widget
Akron10a47962018-07-12 21:17:10 +0200142 if (!onClick["action"] || onClick["action"] == "addWidget") {
Akron7c6e05f2018-07-12 19:08:13 +0200143
Akron22598cd2019-12-09 14:59:03 +0100144
145 let cb = function (e) {
Akron7c6e05f2018-07-12 19:08:13 +0200146
Akrone1c27f62018-07-20 11:42:59 +0200147 // "this" is bind to the panel
148
Akron7c6e05f2018-07-12 19:08:13 +0200149 // Get the URL of the widget
Akron22598cd2019-12-09 14:59:03 +0100150 let url = onClick["template"];
Akrone1c27f62018-07-20 11:42:59 +0200151 // that._interpolateURI(onClick["template"], this.match);
Akron7c6e05f2018-07-12 19:08:13 +0200152
153 // Add the widget to the panel
Akron22598cd2019-12-09 14:59:03 +0100154 let id = that.addWidget(this, name, url);
Akron7c6e05f2018-07-12 19:08:13 +0200155 plugin["widgets"].push(id);
156 };
157
Akron22598cd2019-12-09 14:59:03 +0100158 // TODO:
159 // Create button class to be stored and loaded in button groups!
160
Akron2d0d96d2019-11-18 19:49:50 +0100161 // Add to dynamic button list (e.g. for matches)
162 if (buttons[panel]) {
163 buttons[panel].push([title, embed["classes"], cb]);
164 }
165
166 // Add to static button list (e.g. for query) already loaded
167 else if (KorAP.Panel[panel]) {
168 KorAP.Panel[panel].actions.add(title, embed["classes"], cb);
169 }
170
171 // Add to static button list (e.g. for query) not yet loaded
172 else {
173 buttonsSingle[panel].push([title, embed["classes"], cb]);
174 }
Akron22598cd2019-12-09 14:59:03 +0100175 }
176
177 else if (onClick["action"] == "toggle") {
178
179 // Todo: Initially false
180 let state = stateClass.create(false);
181
182 // TODO:
183 // Lazy registration (see above!)
184 KorAP.Panel[panel].actions.addToggle("Title",["title"], state);
185
186 // Get the URL of the service
187
188 // TODO:
189 // Use the "service" keyword
190 let url = onClick["template"];
191
192 // Add the service
193 let id = this.addService(name, url);
194
195 // TODO:
196 // This is a bit stupid to get the service window
197 let iframe = services[id].load();
198 let win = iframe.contentWindow;
199
200 // Create object to communicate the toggle state
201 // once the iframe is loaded.
202 iframe.onload = function () {
203 let sendToggle = {
204 setState : function (val) {
205 win.postMessage({
206 action: 'state',
207 key : onClick['state'],
208 value : val
209 }, '*'); // TODO: Fix origin
210 }
211 };
212
213 // Associate object with the state
214 state.associate(sendToggle);
215 };
216
217 plugin["services"].push(id);
Akron7c6e05f2018-07-12 19:08:13 +0200218 };
219 };
220 },
221
Akron7c6e05f2018-07-12 19:08:13 +0200222 // TODO:
223 // Interpolate URIs similar to https://tools.ietf.org/html/rfc6570
224 // but as simple as possible
225 _interpolateURI : function (uri, obj) {
226 // ...
227 },
228
229
230 /**
Akron4a703872018-07-26 10:59:41 +0200231 * Get named button group - better rename to "action"
Akron7c6e05f2018-07-12 19:08:13 +0200232 */
233 buttonGroup : function (name) {
Akron2d0d96d2019-11-18 19:49:50 +0100234 if (buttons[name] != undefined) {
235 return buttons[name];
236 } else if (buttonsSingle[name] != undefined) {
237 return buttonsSingle[name];
238 };
239 return [];
240 },
241
242 /**
243 * Clear named button group - better rename to "action"
244 */
245 clearButtonGroup : function (name) {
246 if (buttons[name] != undefined) {
247 buttons[name] = [];
248 } else if (buttonsSingle[name] != undefined) {
249 buttonsSingle[name] = [];
250 }
Akron7c6e05f2018-07-12 19:08:13 +0200251 },
Akron479994e2018-07-02 13:21:44 +0200252
Akron22598cd2019-12-09 14:59:03 +0100253 // Optionally initialize the service mechanism and get an ID
254 _getServiceID : function () {
Akron4a703872018-07-26 10:59:41 +0200255
Akron22598cd2019-12-09 14:59:03 +0100256 // Is it the first service?
Akron76dd8d32018-07-06 09:30:22 +0200257 if (!this._listener) {
258
259 /*
260 * Establish the global 'message' hook.
261 */
262 this._listener = this._receiveMsg.bind(this);
263 window.addEventListener("message", this._listener);
264
Akron22598cd2019-12-09 14:59:03 +0100265 // Every second increase the limits of all registered services
Akron76dd8d32018-07-06 09:30:22 +0200266 this._timer = window.setInterval(function () {
267 for (var i in limits) {
268 if (limits[i]++ >= maxMessages) {
269 limits[i] = maxMessages;
270 }
271 }
272 }, 1000);
273 };
274
Akron22598cd2019-12-09 14:59:03 +0100275 // Create a unique random ID per service
276 return 'id-' + this._randomID();
277 },
278
279 /**
280 * Add a service in a certain panel and return the id.
281 */
282 addService : function (name, src) {
283 if (!src)
284 return;
285
286 let id = this._getServiceID();
287
288 // Create a new service
289 let service = serviceClass.create(name, src, id);
290
291 // TODO!
292 // Store the service based on the identifier
293 services[id] = service;
294 limits[id] = maxMessages;
295
296 // widget._mgr = this;
297
298 // Add service to panel
299 this.element().appendChild(
300 service.load()
301 );
302
303 return id;
304 },
305
306
307 /**
308 * Open a new widget view in a certain panel and return
309 * the id.
310 */
311 addWidget : function (panel, name, src) {
312
313 let id = this._getServiceID();
Akrona6c32b92018-07-02 18:39:42 +0200314
315 // Create a new widget
Akron7991b192018-07-09 17:28:43 +0200316 var widget = widgetClass.create(name, src, id);
Akrona6c32b92018-07-02 18:39:42 +0200317
318 // Store the widget based on the identifier
Akron22598cd2019-12-09 14:59:03 +0100319 services[id] = widget;
Akrona99315e2018-07-03 22:56:45 +0200320 limits[id] = maxMessages;
Akrona6c32b92018-07-02 18:39:42 +0200321
Akron4a703872018-07-26 10:59:41 +0200322 widget._mgr = this;
323
Akrone1c27f62018-07-20 11:42:59 +0200324 // Add widget to panel
325 panel.add(widget);
Akronb43c8c62018-07-04 18:27:28 +0200326
327 return id;
Akron479994e2018-07-02 13:21:44 +0200328 },
329
Akron4a703872018-07-26 10:59:41 +0200330
331 /**
Akron22598cd2019-12-09 14:59:03 +0100332 * Get service by identifier
Akron4a703872018-07-26 10:59:41 +0200333 */
Akron22598cd2019-12-09 14:59:03 +0100334 service : function (id) {
335 return services[id];
Akron4a703872018-07-26 10:59:41 +0200336 },
337
338
Akron22598cd2019-12-09 14:59:03 +0100339 // Receive a call from an embedded service.
Akrone8e2c952018-07-04 13:43:12 +0200340 // The handling needs to be very careful,
341 // as this can easily become a security nightmare.
Akron479994e2018-07-02 13:21:44 +0200342 _receiveMsg : function (e) {
343 // Get event data
344 var d = e.data;
345
Akrona99315e2018-07-03 22:56:45 +0200346 // If no data given - fail
347 // (probably check that it's an assoc array)
348 if (!d)
349 return;
350
351 // e.origin is probably set and okay - CHECK!
Akron479994e2018-07-02 13:21:44 +0200352
Akrona99315e2018-07-03 22:56:45 +0200353 // Get origin ID
354 var id = d["originID"];
355
356 // If no origin ID given - fail
357 if (!id)
358 return;
359
Akron22598cd2019-12-09 14:59:03 +0100360 // Get the service
361 let service = services[id];
Akrona6c32b92018-07-02 18:39:42 +0200362
Akron22598cd2019-12-09 14:59:03 +0100363 // If the addressed service does not exist - fail
364 if (!service)
Akrona6c32b92018-07-02 18:39:42 +0200365 return;
366
Akrona99315e2018-07-03 22:56:45 +0200367 // Check for message limits
368 if (limits[id]-- < 0) {
Akrone8e2c952018-07-04 13:43:12 +0200369
Akron22598cd2019-12-09 14:59:03 +0100370 // Kill service
371 KorAP.log(0, 'Suspicious action by service', service.src);
Akron7c6e05f2018-07-12 19:08:13 +0200372
373 // TODO:
374 // Potentially kill the whole plugin!
Akron4a703872018-07-26 10:59:41 +0200375
Akron22598cd2019-12-09 14:59:03 +0100376 // This removes all connections before closing the service
377 this._closeService(service.id);
378
379 // if (service.isWidget)
380 service.close();
381
Akrona99315e2018-07-03 22:56:45 +0200382 return;
383 };
Akrona6c32b92018-07-02 18:39:42 +0200384
Akron479994e2018-07-02 13:21:44 +0200385 // Resize the iframe
Akron22598cd2019-12-09 14:59:03 +0100386 switch (d.action) {
387 case 'resize':
388 if (service.isWidget)
389 service.resize(d);
390 break;
Akron479994e2018-07-02 13:21:44 +0200391
392 // Log message from iframe
Akron22598cd2019-12-09 14:59:03 +0100393 case 'log':
394 KorAP.log(d.code, d.msg, service.src);
395 break;
Akrona6c32b92018-07-02 18:39:42 +0200396 };
397
398 // TODO:
399 // Close
Akron479994e2018-07-02 13:21:44 +0200400 },
401
Akron22598cd2019-12-09 14:59:03 +0100402 // Close the service
403 _closeService : function (id) {
Akron4a703872018-07-26 10:59:41 +0200404 delete limits[id];
Akron22598cd2019-12-09 14:59:03 +0100405
406 // Close the iframe
407 if (services[id] && services[id]._closeIframe) {
408 services[id]._closeIframe();
409
410 // Remove from list
411 delete services[id];
412 };
413
Akron76dd8d32018-07-06 09:30:22 +0200414
415 // Remove listeners in case no widget
416 // is available any longer
417 if (Object.keys(limits).length == 0)
418 this._removeListener();
419 },
420
Akrona6c32b92018-07-02 18:39:42 +0200421 // Get a random identifier
422 _randomID : function () {
423 return randomID(20);
Akronb43c8c62018-07-04 18:27:28 +0200424 },
425
Akron76dd8d32018-07-06 09:30:22 +0200426 // Remove the listener
427 _removeListener : function () {
428 window.clearInterval(this._timer);
429 this._timer = undefined;
430 window.removeEventListener("message", this._listener);
431 this._listener = undefined;
432 },
433
Akron22598cd2019-12-09 14:59:03 +0100434 /**
435 * Return the service element.
436 */
437 element : function () {
438 if (!this._element) {
439 this._element = document.createElement('div');
440 this._element.setAttribute("id", "services");
441 }
442 return this._element;
443 },
444
Akronb43c8c62018-07-04 18:27:28 +0200445 // Destructor, just for testing scenarios
446 destroy : function () {
447 limits = {};
Akron22598cd2019-12-09 14:59:03 +0100448 for (let s in services) {
449 services[s].close();
Akron4a703872018-07-26 10:59:41 +0200450 };
Akron22598cd2019-12-09 14:59:03 +0100451 services = {};
Akron2d0d96d2019-11-18 19:49:50 +0100452 for (let b in buttons) {
453 buttons[b] = [];
454 };
455 for (let b in buttonsSingle) {
456 buttonsSingle[b] = [];
457 };
Akron22598cd2019-12-09 14:59:03 +0100458
459 if (this._element) {
460 let e = this._element;
461 if (e.parentNode) {
462 e.parentNode.removeChild(e);
463 };
464 this._element = null;
465 };
466
Akron76dd8d32018-07-06 09:30:22 +0200467 this._removeListener();
Akron479994e2018-07-02 13:21:44 +0200468 }
Akrone1c27f62018-07-20 11:42:59 +0200469 };
Akron479994e2018-07-02 13:21:44 +0200470});