blob: 934f6f3251f363d0f837baf65724c6efbd08f2ad [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 /**
152 * Get named button group
153 */
154 buttonGroup : function (name) {
155 return buttons[name];
156 },
157
158 /**
Akrone1c27f62018-07-20 11:42:59 +0200159 * Open a new widget in a certaoin panel
Akron479994e2018-07-02 13:21:44 +0200160 */
Akrone1c27f62018-07-20 11:42:59 +0200161 addWidget : function (panel, name, src) {
Akron479994e2018-07-02 13:21:44 +0200162
Akron76dd8d32018-07-06 09:30:22 +0200163 // Is it the first widget?
164 if (!this._listener) {
165
166 /*
167 * Establish the global 'message' hook.
168 */
169 this._listener = this._receiveMsg.bind(this);
170 window.addEventListener("message", this._listener);
171
172 // Every second increase the limits of all registered widgets
173 this._timer = window.setInterval(function () {
174 for (var i in limits) {
175 if (limits[i]++ >= maxMessages) {
176 limits[i] = maxMessages;
177 }
178 }
179 }, 1000);
180 };
181
Akrona6c32b92018-07-02 18:39:42 +0200182 // Create a unique random ID per widget
183 var id = 'id-' + this._randomID();
184
185 // Create a new widget
Akron7991b192018-07-09 17:28:43 +0200186 var widget = widgetClass.create(name, src, id);
Akrona6c32b92018-07-02 18:39:42 +0200187
188 // Store the widget based on the identifier
189 widgets[id] = widget;
Akrona99315e2018-07-03 22:56:45 +0200190 limits[id] = maxMessages;
Akrona6c32b92018-07-02 18:39:42 +0200191
Akrone1c27f62018-07-20 11:42:59 +0200192 // Add widget to panel
193 panel.add(widget);
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 }
Akrone1c27f62018-07-20 11:42:59 +0200283 };
Akron479994e2018-07-02 13:21:44 +0200284});