blob: 1afc747957055ccbac51d3ac371554c4b593a152 [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
Akrone1c27f62018-07-20 11:42:59 +020040 // all widgets every second to see if it is still alive,
41 // otherwise kill
Akrone8e2c952018-07-04 13:43:12 +020042
Akrone1c27f62018-07-20 11:42:59 +020043 // Load Plugin server
Akron479994e2018-07-02 13:21:44 +020044 return {
45
46 /**
47 * Create new plugin management system
48 */
49 create : function () {
50 return Object.create(this)._init();
51 },
52
53 /*
Akron76dd8d32018-07-06 09:30:22 +020054 * Initialize the plugin manager
Akron479994e2018-07-02 13:21:44 +020055 */
56 _init : function () {
Akron479994e2018-07-02 13:21:44 +020057 return this;
58 },
59
60 /**
Akron7c6e05f2018-07-12 19:08:13 +020061 * Register a plugin described as a JSON object.
62 *
63 * This is work in progress.
Akron10a47962018-07-12 21:17:10 +020064 *
65 * Example:
66 *
67 * KorAP.Plugin.register({
68 * 'name' : 'CatContent',
69 * 'desc' : 'Some content about cats',
70 * 'about' : 'https://localhost:5678/',
71 * 'embed' : [{
72 * 'panel' : 'match',
73 * 'title' : loc.TRANSLATE,
74 * 'classes' : ['translate']
75 * 'onClick' : {
76 * 'action' : 'addWidget',
77 * 'template' : 'https://localhost:5678/?match={matchid}',
78 * }
79 * }]
80 * });
81 *
Akron7c6e05f2018-07-12 19:08:13 +020082 */
83 register : function (obj) {
Akron7c6e05f2018-07-12 19:08:13 +020084 // TODO:
Akron10a47962018-07-12 21:17:10 +020085 // These fields need to be localized for display by a structure like
86 // { de : { name : '..' }, en : { .. } }
Akron7c6e05f2018-07-12 19:08:13 +020087 var name = obj["name"];
88
Akron10a47962018-07-12 21:17:10 +020089 if (!name)
90 throw new Error("Missing name of plugin");
91
Akron7c6e05f2018-07-12 19:08:13 +020092 // Register plugin by name
93 var plugin = plugins[name] = {
94 name : name,
95 desc : obj["desc"],
96 about : obj["about"],
97 widgets : []
98 };
Akron10a47962018-07-12 21:17:10 +020099
100 if (typeof obj["embed"] !== 'object')
101 throw new Error("Embedding of plugin is no list");
Akron7c6e05f2018-07-12 19:08:13 +0200102
103 // Embed all embeddings of the plugin
Akrone1c27f62018-07-20 11:42:59 +0200104 var that = this;
Akron7c6e05f2018-07-12 19:08:13 +0200105 for (var i in obj["embed"]) {
106 var embed = obj["embed"][i];
Akron10a47962018-07-12 21:17:10 +0200107
108 if (typeof embed !== 'object')
109 throw new Error("Embedding of plugin is no object");
110
111 var panel = embed["panel"];
112
113 if (!panel || !buttons[panel])
114 throw new Error("Panel for plugin is invalid");
115
Akron7c6e05f2018-07-12 19:08:13 +0200116 var onClick = embed["onClick"];
117
118 // Needs to be localized as well
119 var title = embed["title"];
120
121 // The embedding will open a widget
Akron10a47962018-07-12 21:17:10 +0200122 if (!onClick["action"] || onClick["action"] == "addWidget") {
Akron7c6e05f2018-07-12 19:08:13 +0200123
Akron7c6e05f2018-07-12 19:08:13 +0200124 var cb = function (e) {
125
Akrone1c27f62018-07-20 11:42:59 +0200126 // "this" is bind to the panel
127
Akron7c6e05f2018-07-12 19:08:13 +0200128 // Get the URL of the widget
Akrone1c27f62018-07-20 11:42:59 +0200129 var url = onClick["template"];
130 // that._interpolateURI(onClick["template"], this.match);
Akron7c6e05f2018-07-12 19:08:13 +0200131
132 // Add the widget to the panel
Akrone1c27f62018-07-20 11:42:59 +0200133 var id = that.addWidget(this, name, url);
Akron7c6e05f2018-07-12 19:08:13 +0200134 plugin["widgets"].push(id);
135 };
136
Akrone1c27f62018-07-20 11:42:59 +0200137 buttons[panel].push([title, embed["classes"], cb]);
Akron7c6e05f2018-07-12 19:08:13 +0200138 };
139 };
140 },
141
142
143 // TODO:
144 // Interpolate URIs similar to https://tools.ietf.org/html/rfc6570
145 // but as simple as possible
146 _interpolateURI : function (uri, obj) {
147 // ...
148 },
149
150
151 /**
Akron4a703872018-07-26 10:59:41 +0200152 * Get named button group - better rename to "action"
Akron7c6e05f2018-07-12 19:08:13 +0200153 */
154 buttonGroup : function (name) {
155 return buttons[name];
156 },
157
158 /**
Akron4a703872018-07-26 10:59:41 +0200159 * Open a new widget view in a certain panel and return
160 * the id.
Akron479994e2018-07-02 13:21:44 +0200161 */
Akrone1c27f62018-07-20 11:42:59 +0200162 addWidget : function (panel, name, src) {
Akron479994e2018-07-02 13:21:44 +0200163
Akron4a703872018-07-26 10:59:41 +0200164 if (!src)
165 return;
166
Akron76dd8d32018-07-06 09:30:22 +0200167 // Is it the first widget?
168 if (!this._listener) {
169
170 /*
171 * Establish the global 'message' hook.
172 */
173 this._listener = this._receiveMsg.bind(this);
174 window.addEventListener("message", this._listener);
175
176 // Every second increase the limits of all registered widgets
177 this._timer = window.setInterval(function () {
178 for (var i in limits) {
179 if (limits[i]++ >= maxMessages) {
180 limits[i] = maxMessages;
181 }
182 }
183 }, 1000);
184 };
185
Akrona6c32b92018-07-02 18:39:42 +0200186 // Create a unique random ID per widget
187 var id = 'id-' + this._randomID();
188
189 // Create a new widget
Akron7991b192018-07-09 17:28:43 +0200190 var widget = widgetClass.create(name, src, id);
Akrona6c32b92018-07-02 18:39:42 +0200191
192 // Store the widget based on the identifier
193 widgets[id] = widget;
Akrona99315e2018-07-03 22:56:45 +0200194 limits[id] = maxMessages;
Akrona6c32b92018-07-02 18:39:42 +0200195
Akron4a703872018-07-26 10:59:41 +0200196 widget._mgr = this;
197
Akrone1c27f62018-07-20 11:42:59 +0200198 // Add widget to panel
199 panel.add(widget);
Akronb43c8c62018-07-04 18:27:28 +0200200
201 return id;
Akron479994e2018-07-02 13:21:44 +0200202 },
203
Akron4a703872018-07-26 10:59:41 +0200204
205 /**
206 * Get widget by identifier
207 */
208 widget : function (id) {
209 return widgets[id];
210 },
211
212
Akrone8e2c952018-07-04 13:43:12 +0200213 // Receive a call from an embedded iframe.
214 // The handling needs to be very careful,
215 // as this can easily become a security nightmare.
Akron479994e2018-07-02 13:21:44 +0200216 _receiveMsg : function (e) {
217 // Get event data
218 var d = e.data;
219
Akrona99315e2018-07-03 22:56:45 +0200220 // If no data given - fail
221 // (probably check that it's an assoc array)
222 if (!d)
223 return;
224
225 // e.origin is probably set and okay - CHECK!
Akron479994e2018-07-02 13:21:44 +0200226
Akrona99315e2018-07-03 22:56:45 +0200227 // Get origin ID
228 var id = d["originID"];
229
230 // If no origin ID given - fail
231 if (!id)
232 return;
233
Akrona6c32b92018-07-02 18:39:42 +0200234 // Get the widget
Akrona99315e2018-07-03 22:56:45 +0200235 var widget = widgets[id];
Akrona6c32b92018-07-02 18:39:42 +0200236
237 // If the addressed widget does not exist - fail
238 if (!widget)
239 return;
240
Akrona99315e2018-07-03 22:56:45 +0200241 // Check for message limits
242 if (limits[id]-- < 0) {
Akrone8e2c952018-07-04 13:43:12 +0200243
244 // Kill widget
Akronc0a2da82018-07-04 15:27:37 +0200245 KorAP.log(0, 'Suspicious action by widget', widget.src);
Akron7c6e05f2018-07-12 19:08:13 +0200246
247 // TODO:
248 // Potentially kill the whole plugin!
Akron4a703872018-07-26 10:59:41 +0200249
250 // This removes all connections before closing the widget
251 this._closeWidget(widget.id);
252 widget.close();
Akrona99315e2018-07-03 22:56:45 +0200253 return;
254 };
Akrona6c32b92018-07-02 18:39:42 +0200255
Akron479994e2018-07-02 13:21:44 +0200256 // Resize the iframe
257 if (d.action === 'resize') {
Akrona6c32b92018-07-02 18:39:42 +0200258 widget.resize(d);
Akron479994e2018-07-02 13:21:44 +0200259 }
260
261 // Log message from iframe
262 else if (d.action === 'log') {
Akronc0a2da82018-07-04 15:27:37 +0200263 KorAP.log(d.code, d.msg, widget.src);
Akrona6c32b92018-07-02 18:39:42 +0200264 };
265
266 // TODO:
267 // Close
Akron479994e2018-07-02 13:21:44 +0200268 },
269
Akron76dd8d32018-07-06 09:30:22 +0200270 // Close the widget
Akron4a703872018-07-26 10:59:41 +0200271 _closeWidget : function (id) {
272 delete limits[id];
273 delete widgets[id];
Akron76dd8d32018-07-06 09:30:22 +0200274
275 // Remove listeners in case no widget
276 // is available any longer
277 if (Object.keys(limits).length == 0)
278 this._removeListener();
279 },
280
Akrona6c32b92018-07-02 18:39:42 +0200281 // Get a random identifier
282 _randomID : function () {
283 return randomID(20);
Akronb43c8c62018-07-04 18:27:28 +0200284 },
285
Akron76dd8d32018-07-06 09:30:22 +0200286 // Remove the listener
287 _removeListener : function () {
288 window.clearInterval(this._timer);
289 this._timer = undefined;
290 window.removeEventListener("message", this._listener);
291 this._listener = undefined;
292 },
293
Akronb43c8c62018-07-04 18:27:28 +0200294 // Destructor, just for testing scenarios
295 destroy : function () {
296 limits = {};
Akron4a703872018-07-26 10:59:41 +0200297 for (let w in widgets) {
298 widgets[w].close();
299 };
Akronb43c8c62018-07-04 18:27:28 +0200300 widgets = {};
Akron76dd8d32018-07-06 09:30:22 +0200301 this._removeListener();
Akron479994e2018-07-02 13:21:44 +0200302 }
Akrone1c27f62018-07-20 11:42:59 +0200303 };
Akron479994e2018-07-02 13:21:44 +0200304});