blob: 7f9bc85746914c7176d997a1b96085a680992454 [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"];
hebasta40a85cf2020-07-15 18:10:08 +0200136 let icon = embed["icon"];
137
Akron22598cd2019-12-09 14:59:03 +0100138 if (!panel || !(buttons[panel] || buttonsSingle[panel]))
Akronba09ed22020-10-01 16:01:45 +0200139 throw new Error("Panel for plugin is invalid");
Akron7c6e05f2018-07-12 19:08:13 +0200140
141 // The embedding will open a widget
Akronba09ed22020-10-01 16:01:45 +0200142 if (!onClick["action"] ||
143 onClick["action"] == "addWidget" ||
144 onClick["action"] == "setWidget") {
Akron22598cd2019-12-09 14:59:03 +0100145
146 let cb = function (e) {
Akron7c6e05f2018-07-12 19:08:13 +0200147
Akrone1c27f62018-07-20 11:42:59 +0200148 // "this" is bind to the panel
Akronba09ed22020-10-01 16:01:45 +0200149 // "this".button is the button
150 // "that" is the server object
Akrone1c27f62018-07-20 11:42:59 +0200151
Akron7c6e05f2018-07-12 19:08:13 +0200152 // Get the URL of the widget
Akron22598cd2019-12-09 14:59:03 +0100153 let url = onClick["template"];
Akrone1c27f62018-07-20 11:42:59 +0200154 // that._interpolateURI(onClick["template"], this.match);
Akron7c6e05f2018-07-12 19:08:13 +0200155
Akronba09ed22020-10-01 16:01:45 +0200156 // The button has a state and the state is associated to the
157 // a intermediate object to toggle the view
158 if ('state' in this.button && this.button.state.associates() > 0) {
159
Akronba09ed22020-10-01 16:01:45 +0200160 let s = this.button.state;
Akronfcf89db2020-10-01 17:40:20 +0200161
162 // The associated service is existent
163 if (services[this.button['widgetID']]) {
164
165 // TODO:
166 // Use roll() when existing
167 if (s.get()) {
168 s.set(false);
169 } else {
170 s.set(true);
171 };
172 return;
173 }
174
175 // The service is not existent
176 else {
177
178 // Remove broken state associations
179 s.clear();
Akronba09ed22020-10-01 16:01:45 +0200180 s.set(true);
Akronfcf89db2020-10-01 17:40:20 +0200181 }
Akronba09ed22020-10-01 16:01:45 +0200182 };
183
Akron7c6e05f2018-07-12 19:08:13 +0200184 // Add the widget to the panel
Akron22598cd2019-12-09 14:59:03 +0100185 let id = that.addWidget(this, name, url);
Akron7c6e05f2018-07-12 19:08:13 +0200186 plugin["widgets"].push(id);
Akronba09ed22020-10-01 16:01:45 +0200187
188 // If a state exists, associate with a mediator object
189 if ('state' in this.button) {
Akronfcf89db2020-10-01 17:40:20 +0200190 this.button['widgetID'] = id;
Akronba09ed22020-10-01 16:01:45 +0200191 this.button.state.associate({
192 setState : function (value) {
193 // Minimize the widget
194 if (value == false) {
195 services[id].minimize();
196 }
197 else {
198 services[id].show();
199 };
200 }
201 });
202 }
Akron7c6e05f2018-07-12 19:08:13 +0200203 };
204
Akron22598cd2019-12-09 14:59:03 +0100205
Akronba09ed22020-10-01 16:01:45 +0200206 // Button object
207 let obj = {'cls':embed["classes"], 'icon': icon }
208
209 if (onClick["action"] && onClick["action"] == "setWidget") {
210
211 // Create a boolean state value, that initializes to true == opened
212 obj['state'] = stateClass.create(true);
213 };
214
Akron2d0d96d2019-11-18 19:49:50 +0100215 // Add to dynamic button list (e.g. for matches)
216 if (buttons[panel]) {
Akronba09ed22020-10-01 16:01:45 +0200217 buttons[panel].push([title, obj, cb]);
Akron2d0d96d2019-11-18 19:49:50 +0100218 }
219
220 // Add to static button list (e.g. for query) already loaded
221 else if (KorAP.Panel[panel]) {
Akronba09ed22020-10-01 16:01:45 +0200222 KorAP.Panel[panel].actions.add(title, obj, cb);
Akron2d0d96d2019-11-18 19:49:50 +0100223 }
224
225 // Add to static button list (e.g. for query) not yet loaded
226 else {
Akronba09ed22020-10-01 16:01:45 +0200227 buttonsSingle[panel].push([title, obj, cb]);
Akron2d0d96d2019-11-18 19:49:50 +0100228 }
Akron22598cd2019-12-09 14:59:03 +0100229 }
Akronba09ed22020-10-01 16:01:45 +0200230
231 // TODO There is no possibility to add icons to an plugin toggle button right now.
Akron22598cd2019-12-09 14:59:03 +0100232 else if (onClick["action"] == "toggle") {
233
234 // Todo: Initially false
235 let state = stateClass.create(false);
236
237 // TODO:
238 // Lazy registration (see above!)
Akron792b1a42020-09-14 18:56:38 +0200239 KorAP.Panel[panel].actions.addToggle(title, {'cls':["title"]}, state);
Akron22598cd2019-12-09 14:59:03 +0100240
241 // Get the URL of the service
242
243 // TODO:
244 // Use the "service" keyword
245 let url = onClick["template"];
246
247 // Add the service
248 let id = this.addService(name, url);
249
250 // TODO:
251 // This is a bit stupid to get the service window
Akronc3003642020-03-30 10:19:14 +0200252 let service = services[id];
253 let iframe = service.load();
Akron22598cd2019-12-09 14:59:03 +0100254
255 // Create object to communicate the toggle state
256 // once the iframe is loaded.
257 iframe.onload = function () {
258 let sendToggle = {
259 setState : function (val) {
Akronc3003642020-03-30 10:19:14 +0200260 service.sendMsg({
Akron22598cd2019-12-09 14:59:03 +0100261 action: 'state',
262 key : onClick['state'],
263 value : val
Akronc3003642020-03-30 10:19:14 +0200264 });
Akron22598cd2019-12-09 14:59:03 +0100265 }
266 };
267
268 // Associate object with the state
269 state.associate(sendToggle);
270 };
271
272 plugin["services"].push(id);
Akron7c6e05f2018-07-12 19:08:13 +0200273 };
274 };
275 },
276
Akron7c6e05f2018-07-12 19:08:13 +0200277 // TODO:
278 // Interpolate URIs similar to https://tools.ietf.org/html/rfc6570
279 // but as simple as possible
280 _interpolateURI : function (uri, obj) {
281 // ...
282 },
283
284
285 /**
Akron4a703872018-07-26 10:59:41 +0200286 * Get named button group - better rename to "action"
Akron7c6e05f2018-07-12 19:08:13 +0200287 */
288 buttonGroup : function (name) {
Akron2d0d96d2019-11-18 19:49:50 +0100289 if (buttons[name] != undefined) {
290 return buttons[name];
291 } else if (buttonsSingle[name] != undefined) {
292 return buttonsSingle[name];
293 };
294 return [];
295 },
296
297 /**
298 * Clear named button group - better rename to "action"
299 */
300 clearButtonGroup : function (name) {
301 if (buttons[name] != undefined) {
302 buttons[name] = [];
303 } else if (buttonsSingle[name] != undefined) {
304 buttonsSingle[name] = [];
305 }
Akron7c6e05f2018-07-12 19:08:13 +0200306 },
Akron479994e2018-07-02 13:21:44 +0200307
Akron22598cd2019-12-09 14:59:03 +0100308 // Optionally initialize the service mechanism and get an ID
309 _getServiceID : function () {
Akron4a703872018-07-26 10:59:41 +0200310
Akron22598cd2019-12-09 14:59:03 +0100311 // Is it the first service?
Akron76dd8d32018-07-06 09:30:22 +0200312 if (!this._listener) {
313
314 /*
315 * Establish the global 'message' hook.
316 */
317 this._listener = this._receiveMsg.bind(this);
318 window.addEventListener("message", this._listener);
319
Akron22598cd2019-12-09 14:59:03 +0100320 // Every second increase the limits of all registered services
Akron76dd8d32018-07-06 09:30:22 +0200321 this._timer = window.setInterval(function () {
322 for (var i in limits) {
323 if (limits[i]++ >= maxMessages) {
324 limits[i] = maxMessages;
325 }
326 }
327 }, 1000);
328 };
329
Akron22598cd2019-12-09 14:59:03 +0100330 // Create a unique random ID per service
331 return 'id-' + this._randomID();
332 },
333
334 /**
335 * Add a service in a certain panel and return the id.
336 */
337 addService : function (name, src) {
338 if (!src)
339 return;
340
341 let id = this._getServiceID();
342
343 // Create a new service
344 let service = serviceClass.create(name, src, id);
345
Akron22598cd2019-12-09 14:59:03 +0100346 services[id] = service;
347 limits[id] = maxMessages;
348
Akron22598cd2019-12-09 14:59:03 +0100349 // Add service to panel
350 this.element().appendChild(
351 service.load()
352 );
353
354 return id;
355 },
356
357
358 /**
359 * Open a new widget view in a certain panel and return
360 * the id.
361 */
362 addWidget : function (panel, name, src) {
363
364 let id = this._getServiceID();
Akrona6c32b92018-07-02 18:39:42 +0200365
366 // Create a new widget
Akron7991b192018-07-09 17:28:43 +0200367 var widget = widgetClass.create(name, src, id);
Akrona6c32b92018-07-02 18:39:42 +0200368
369 // Store the widget based on the identifier
Akron22598cd2019-12-09 14:59:03 +0100370 services[id] = widget;
Akrona99315e2018-07-03 22:56:45 +0200371 limits[id] = maxMessages;
Akrona6c32b92018-07-02 18:39:42 +0200372
Akron4a703872018-07-26 10:59:41 +0200373 widget._mgr = this;
374
Akrone1c27f62018-07-20 11:42:59 +0200375 // Add widget to panel
376 panel.add(widget);
Akronb43c8c62018-07-04 18:27:28 +0200377
378 return id;
Akron479994e2018-07-02 13:21:44 +0200379 },
380
Akron4a703872018-07-26 10:59:41 +0200381
382 /**
Akron22598cd2019-12-09 14:59:03 +0100383 * Get service by identifier
Akron4a703872018-07-26 10:59:41 +0200384 */
Akron22598cd2019-12-09 14:59:03 +0100385 service : function (id) {
386 return services[id];
Akron4a703872018-07-26 10:59:41 +0200387 },
388
389
Akron22598cd2019-12-09 14:59:03 +0100390 // Receive a call from an embedded service.
Akrone8e2c952018-07-04 13:43:12 +0200391 // The handling needs to be very careful,
392 // as this can easily become a security nightmare.
Akron479994e2018-07-02 13:21:44 +0200393 _receiveMsg : function (e) {
394 // Get event data
395 var d = e.data;
396
Akrona99315e2018-07-03 22:56:45 +0200397 // If no data given - fail
398 // (probably check that it's an assoc array)
399 if (!d)
400 return;
401
402 // e.origin is probably set and okay - CHECK!
Akron479994e2018-07-02 13:21:44 +0200403
Akrona99315e2018-07-03 22:56:45 +0200404 // Get origin ID
405 var id = d["originID"];
406
407 // If no origin ID given - fail
408 if (!id)
409 return;
410
Akron22598cd2019-12-09 14:59:03 +0100411 // Get the service
412 let service = services[id];
Akrona6c32b92018-07-02 18:39:42 +0200413
Akron22598cd2019-12-09 14:59:03 +0100414 // If the addressed service does not exist - fail
415 if (!service)
Akrona6c32b92018-07-02 18:39:42 +0200416 return;
417
Akrona99315e2018-07-03 22:56:45 +0200418 // Check for message limits
419 if (limits[id]-- < 0) {
Akrone8e2c952018-07-04 13:43:12 +0200420
Akron22598cd2019-12-09 14:59:03 +0100421 // Kill service
422 KorAP.log(0, 'Suspicious action by service', service.src);
Akron7c6e05f2018-07-12 19:08:13 +0200423
424 // TODO:
425 // Potentially kill the whole plugin!
Akron4a703872018-07-26 10:59:41 +0200426
Akron22598cd2019-12-09 14:59:03 +0100427 // This removes all connections before closing the service
428 this._closeService(service.id);
429
430 // if (service.isWidget)
431 service.close();
432
Akrona99315e2018-07-03 22:56:45 +0200433 return;
434 };
Akrona6c32b92018-07-02 18:39:42 +0200435
Akron479994e2018-07-02 13:21:44 +0200436 // Resize the iframe
Akron22598cd2019-12-09 14:59:03 +0100437 switch (d.action) {
438 case 'resize':
439 if (service.isWidget)
440 service.resize(d);
441 break;
Akron479994e2018-07-02 13:21:44 +0200442
443 // Log message from iframe
Akron22598cd2019-12-09 14:59:03 +0100444 case 'log':
445 KorAP.log(d.code, d.msg, service.src);
446 break;
Akron51ee6232019-12-17 21:00:05 +0100447
448 // Modify pipes
449 case 'pipe':
450 if (KorAP.Pipe != undefined) {
451 if (d.job == 'del') {
452 KorAP.Pipe.remove(d.service);
453 } else {
454 KorAP.Pipe.append(d.service);
455 };
456 };
457 break;
Akronc3003642020-03-30 10:19:14 +0200458
459 // Get information from the embedding platform
460 case 'get':
Akron45308ce2020-08-28 14:10:23 +0200461
462 // Get KoralQuery
Akronc3003642020-03-30 10:19:14 +0200463 if (d.key == 'KQ') {
464 if (KorAP.koralQuery !== undefined) {
465 d["value"] = KorAP.koralQuery;
466 };
Akron45308ce2020-08-28 14:10:23 +0200467 }
468
Akron432972b2020-09-18 17:05:53 +0200469 // Get Query information from from
Akron45308ce2020-08-28 14:10:23 +0200470 else if (d.key == 'QueryForm') {
471 let doc = document;
472 let v = d["value"] = {};
473
474 var el;
475 if (el = doc.getElementById('q-field')) {
476 v["q"] = el.value;
477 };
478 if (el = doc.getElementById('ql-field')) {
479 v["ql"] = el.value;
480 };
481 if (el = KorAP.vc) {
482 v["cq"] = el.toQuery();
483 };
Akron432972b2020-09-18 17:05:53 +0200484 }
485
486 // Get Query information from parameters
487 else if (d.key == 'QueryParam') {
488
489 // Supported in all modern browsers
490 var p = new URLSearchParams(window.location.search);
491 let v = d["value"] = {};
492 v["q"] = p.get('q');
493 v["ql"] = p.get('ql');
494 v["cq"] = p.get('cq');
Akronc3003642020-03-30 10:19:14 +0200495 };
496 };
497
498 // data needs to be mirrored
499 if (d._id) {
500 service.sendMsg(d);
Akrona6c32b92018-07-02 18:39:42 +0200501 };
502
503 // TODO:
504 // Close
Akron479994e2018-07-02 13:21:44 +0200505 },
506
Akron22598cd2019-12-09 14:59:03 +0100507 // Close the service
508 _closeService : function (id) {
Akron4a703872018-07-26 10:59:41 +0200509 delete limits[id];
Akronfcf89db2020-10-01 17:40:20 +0200510
Akron22598cd2019-12-09 14:59:03 +0100511 // Close the iframe
512 if (services[id] && services[id]._closeIframe) {
513 services[id]._closeIframe();
514
515 // Remove from list
516 delete services[id];
517 };
518
Akron76dd8d32018-07-06 09:30:22 +0200519
520 // Remove listeners in case no widget
521 // is available any longer
522 if (Object.keys(limits).length == 0)
523 this._removeListener();
524 },
525
Akrona6c32b92018-07-02 18:39:42 +0200526 // Get a random identifier
527 _randomID : function () {
528 return randomID(20);
Akronb43c8c62018-07-04 18:27:28 +0200529 },
530
Akron76dd8d32018-07-06 09:30:22 +0200531 // Remove the listener
532 _removeListener : function () {
533 window.clearInterval(this._timer);
534 this._timer = undefined;
535 window.removeEventListener("message", this._listener);
536 this._listener = undefined;
537 },
538
Akron22598cd2019-12-09 14:59:03 +0100539 /**
540 * Return the service element.
541 */
542 element : function () {
543 if (!this._element) {
544 this._element = document.createElement('div');
545 this._element.setAttribute("id", "services");
546 }
547 return this._element;
548 },
549
Akronb43c8c62018-07-04 18:27:28 +0200550 // Destructor, just for testing scenarios
551 destroy : function () {
552 limits = {};
Akron22598cd2019-12-09 14:59:03 +0100553 for (let s in services) {
554 services[s].close();
Akron4a703872018-07-26 10:59:41 +0200555 };
Akron22598cd2019-12-09 14:59:03 +0100556 services = {};
Akron2d0d96d2019-11-18 19:49:50 +0100557 for (let b in buttons) {
558 buttons[b] = [];
559 };
560 for (let b in buttonsSingle) {
561 buttonsSingle[b] = [];
562 };
Akron22598cd2019-12-09 14:59:03 +0100563
564 if (this._element) {
565 let e = this._element;
566 if (e.parentNode) {
567 e.parentNode.removeChild(e);
568 };
569 this._element = null;
570 };
571
Akron76dd8d32018-07-06 09:30:22 +0200572 this._removeListener();
Akron479994e2018-07-02 13:21:44 +0200573 }
Akrone1c27f62018-07-20 11:42:59 +0200574 };
Akron479994e2018-07-02 13:21:44 +0200575});