1 | /** |
||
2 | * [Siteapp] - multi-purpose frontend application |
||
3 | * |
||
4 | * Siteapp Module Factory Abstract |
||
5 | * |
||
6 | * (abstract) factory class for Siteapp.Module to load module files on demand |
||
7 | * |
||
8 | * A component's JavaScript files can be included 'on demand' instead of adding it as a main dependency in the applications main configuration file `thalia-app.js`. |
||
9 | * |
||
10 | * To load the JS on demand additional attributes must apply to the components element. |
||
11 | * ``` |
||
12 | * [moduleIdentifier] := {Module.Identifier} | |
||
13 | * {path/to/module} | |
||
14 | * {http(s)://abs.olu.te/uri/to/module.js} |
||
15 | * |
||
16 | * |
||
17 | * <div data-{modulename} |
||
18 | * data-module-factory |
||
19 | * data-module = "{moduleIdentifier}" |
||
20 | * data-deps = "{moduleIdentifier},{moduleIdentifier},..." |
||
21 | * data-callback = "{namespaceFunctionName}" |
||
22 | * >...</div> |
||
23 | * ``` |
||
24 | * |
||
25 | * To restrict module inclusion (external, only registered modules), set the following options: |
||
26 | * ``` |
||
27 | * var myFactoryOptions = { |
||
28 | * |
||
29 | * // wether to allow inclusion per path, in the first place, |
||
30 | * // when set to 'false', external inclusion will be forbidden and |
||
31 | * // only "Module.Identifier" registered in config are allowed |
||
32 | * // default: true |
||
33 | * |
||
34 | * allowPath: true, |
||
35 | * |
||
36 | * // allow external inclusion like 'http(s)://...", |
||
37 | * // tries to catch also stuff like 'mailto:...' and similar |
||
38 | * // default: false |
||
39 | * |
||
40 | * allowExtern: false, |
||
41 | * |
||
42 | * // if external URIs are allowed, hostnames can be restriced, with |
||
43 | * // this list of external hosts to allow, if empty allow all (!) |
||
44 | * // default: localhost, (current 'location.hostname') |
||
45 | * |
||
46 | * externHosts: [ |
||
47 | * // '...', ... |
||
48 | * ] |
||
49 | * |
||
50 | * }; |
||
51 | * ``` |
||
52 | * |
||
53 | * - Example: include a module per module path |
||
54 | * ``` |
||
55 | * <div data-supermodule |
||
56 | * data-component-factory |
||
57 | * data-module = "components/supermodule" |
||
58 | * >...</div> |
||
59 | * ``` |
||
60 | * The path must either be a relative Path, relative to the main application/require file or an absolute path, beginning with `/ ` or `http(s)://`. |
||
61 | * |
||
62 | * |
||
63 | * - Example: include a module per module identifier |
||
64 | * ``` |
||
65 | * <div data-supermodule |
||
66 | * data-component-factory |
||
67 | * data-module = "Components.Supermodul" |
||
68 | * >...</div> |
||
69 | * ``` |
||
70 | * A module's identifier is defined inside the main require configuration, within the `path` and/or `shim` sections. |
||
71 | * |
||
72 | * |
||
73 | * - Example: hyphothetical service module with dependencies |
||
74 | * ``` |
||
75 | * <div data-supermodule |
||
76 | * data-component-factory |
||
77 | * data-module = "/service/api/frontend/supermodule.js" |
||
78 | * data-deps = "https://cdn.some.where/library.js,libs/superutil" |
||
79 | * >...</div> |
||
80 | * ``` |
||
81 | * This example shows a component's ("Supermodul") markup and lets the browser first load and execute the dependencies, one extern "...library.js" and another local "libs/superutil". After those, the module's main script file "/service/api/frontend/supermodule.js" is loaded, from a (absolute) local service's URI, and executed. |
||
82 | * |
||
83 | * |
||
84 | * |
||
85 | * @package [Siteapp] |
||
86 | * @subpackage [Siteapp] module |
||
87 | * @author Björn Bartels <[email protected]> |
||
88 | * @link https://gitlab.bjoernbartels.earth/groups/themes |
||
89 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0 |
||
90 | * @copyright copyright (c) 2016 Björn Bartels <[email protected]> |
||
91 | * |
||
92 | * @namespace Siteapp |
||
93 | * @module Siteapp.ModuleFactory |
||
94 | * @abstract |
||
95 | */ |
||
96 | /** global: Siteapp */ |
||
97 | /** global: Module */ |
||
98 | /** global: ModuleManager */ |
||
99 | /** global: ModuleFactoryDefaults */ |
||
100 | |||
101 | import {Exception} from '../sys/siteapp.exception'; |
||
102 | |||
103 | |||
104 | class ModuleFactoryException extends Exception { |
||
105 | get name () { |
||
106 | return "SiteappModuleFactoryException"; |
||
107 | } |
||
108 | }; |
||
109 | |||
110 | |||
111 | const ModuleFactoryDefaults = { |
||
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||
112 | |||
113 | deps : [], |
||
114 | module : null, |
||
115 | callback: null, |
||
116 | |||
117 | // wether to allow inclusion per path, in the first place, |
||
118 | // when set to 'false', external inclusion will be forbidden and |
||
119 | // only "Module.Identifier" registered in config are allowed |
||
120 | |||
121 | allowPath: true, |
||
122 | |||
123 | // allow external inclusion like 'http(s)://...", |
||
124 | // tries to catch also stuff like 'mailto:...' and similar |
||
125 | |||
126 | allowExtern: false, |
||
127 | |||
128 | // if external URIs are allowed, hostnames can be restriced, with |
||
129 | // this list of external hosts to allow, if empty allow all (!) |
||
130 | |||
131 | externHosts: [ |
||
132 | 'localhost', |
||
133 | location.hostname |
||
134 | ] |
||
135 | }; |
||
136 | |||
137 | |||
138 | /** |
||
139 | * Module factory abstract. |
||
140 | * @module Siteapp.ModuleFactory |
||
141 | */ |
||
142 | const ModuleFactory = class ModuleFactory { |
||
143 | |||
144 | /** |
||
145 | * Creates a new instance of Siteapp.ModuleFactory. |
||
146 | * |
||
147 | * @class |
||
148 | * @fires Siteapp.ModuleFactory#init |
||
149 | * @param {jQuery} element - jQuery object to attach the plugin to. |
||
150 | * @param {Object} options - object to extend the default configuration. |
||
151 | * |
||
152 | */ |
||
153 | constructor (element, options) { |
||
154 | if (!element) return; |
||
0 ignored issues
–
show
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.
Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later. Consider: if (a > 0)
b = 42;
If you or someone else later decides to put another statement in, only the first statement will be executed. if (a > 0)
console.log("a > 0");
b = 42;
In this case the statement if (a > 0) {
console.log("a > 0");
b = 42;
}
ensures that the proper code will be executed conditionally no matter how many statements are added or removed. ![]() |
|||
155 | |||
156 | var $this = this; |
||
0 ignored issues
–
show
|
|||
157 | this.$element = $(element); |
||
158 | |||
159 | this.options = $.extend({}, ModuleFactoryDefaults, this.$element.data(), options); |
||
160 | Siteapp.sys.secureProperties(this.options, [ |
||
161 | 'allowPath', |
||
162 | 'allowExtern', |
||
163 | 'externHosts' |
||
164 | ]); |
||
165 | |||
166 | this._init(); |
||
167 | |||
168 | } |
||
169 | |||
170 | /** |
||
171 | * initialize factory parameters |
||
172 | * @param {Object} options - optional object to extend the component's configuration. |
||
173 | * @access public |
||
174 | */ |
||
175 | _init (options) { |
||
176 | |||
177 | this._dependencies = null; |
||
178 | this._module = null; |
||
179 | this._callback = null; |
||
180 | |||
181 | if (typeof options == 'object') { |
||
182 | this.options = $.extend({}, this.options, options); |
||
183 | } |
||
184 | |||
185 | try { |
||
186 | |||
187 | this._dependencies = this.options.deps; |
||
188 | this._module = this.options.module; |
||
189 | this._callback = this.options.callback; |
||
190 | |||
191 | this.inject(); |
||
192 | |||
193 | } catch (ex) { |
||
194 | console.error('ModuleFactoryError : '+ex.message, this.options); |
||
195 | throw new ModuleFactoryException('ModuleFactoryError loading module: '+ex.message); |
||
196 | } finally { |
||
0 ignored issues
–
show
Comprehensibility
Documentation
Best Practice
introduced
by
|
|||
197 | |||
198 | } |
||
199 | |||
200 | } |
||
201 | |||
202 | /** |
||
203 | * inject dependencies and module into page via 'require' |
||
204 | * @param {Array|String} _dependencies - module dependency identifiers or paths to inject |
||
205 | * @param {String} _module - module identifier or module path to inject. |
||
206 | * @param {function|String} _callback - optional callback function or name of registered namespace 'func' |
||
207 | */ |
||
208 | inject ( _dependencies, _module, _callback ) { |
||
209 | if (!_dependencies) { _dependencies = this._dependencies; } |
||
210 | if (!_module) { _module = this._module; } |
||
211 | if (!_callback) { _callback = this._callback; } |
||
212 | |||
213 | if (typeof _dependencies == 'string' ) { |
||
214 | _dependencies = String(_dependencies).split(','); |
||
215 | } |
||
216 | if (typeof _dependencies.join != 'function' ) { |
||
217 | _dependencies = []; |
||
218 | } |
||
219 | |||
220 | var $factory = this; |
||
221 | |||
222 | if (_dependencies.length > 0) { |
||
223 | if ( !this._allowURIs(_dependencies) ) { |
||
224 | throw new Error('SECURITY ALERT: One or more of the dependencies requested are not allowed to be included!'); |
||
225 | } |
||
226 | requirejs(_dependencies, () => { |
||
227 | |||
228 | $factory.injectModule( _module, _callback ); |
||
229 | |||
230 | }); |
||
231 | } else { |
||
232 | this.injectModule( _module, _callback ); |
||
233 | } |
||
234 | |||
235 | |||
236 | } |
||
237 | |||
238 | /** |
||
239 | * inject actual module code into page via 'require' |
||
240 | * @param {String} _module - module identifier or module path to inject. |
||
241 | * @param {function|String} _callback - optional callback function or name of registered namespace 'func' |
||
242 | */ |
||
243 | injectModule ( _module, _callback ) { |
||
244 | if (!_module) { _module = this._module; } |
||
245 | if (!_callback) { _callback = this._callback; } |
||
246 | |||
247 | if (typeof _module != 'string' ) { |
||
248 | console.warn('ComponentFactory::injectModule : given module must be a component indentifier or a path string', _module); |
||
249 | return; |
||
250 | } |
||
251 | |||
252 | var $factory = this; |
||
253 | var $element = $factory.$element; |
||
254 | var $app = $factory.application; |
||
255 | |||
256 | if ( !this._allowURI(_module) ) { |
||
257 | throw new Error('SECURITY ALERT: The module requested is not allowed to be included!'); |
||
258 | } |
||
259 | |||
260 | requirejs([_module], () => { |
||
261 | |||
262 | // destroy factory instance |
||
263 | $factory.destroy(); |
||
264 | |||
265 | // initialize plugin/modules on $element and it's children |
||
266 | $element[$app.appName](); |
||
267 | |||
268 | if ($app.NS.isFunc(_callback)) { |
||
269 | $app.NS.func(_callback).apply($app, [$element]); |
||
270 | } |
||
271 | |||
272 | }); |
||
273 | |||
274 | } |
||
275 | |||
276 | /** |
||
277 | * Check list of URIs for allowance |
||
278 | * |
||
279 | * @function |
||
280 | * @access private |
||
281 | * @param {[string]} URIs |
||
282 | * @returns {boolean} |
||
283 | */ |
||
284 | _allowURIs ( URIs ) { |
||
285 | if (typeof URIs.join != 'function') { |
||
286 | return false; |
||
287 | } |
||
288 | |||
289 | var allow = false; |
||
290 | for (var idx in URIs) { |
||
0 ignored issues
–
show
A for in loop automatically includes the property of any prototype object, consider checking the key using
hasOwnProperty .
When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically: var someObject;
for (var key in someObject) {
if ( ! someObject.hasOwnProperty(key)) {
continue; // Skip keys from the prototype.
}
doSomethingWith(key);
}
![]() |
|||
291 | allow = allow || this._allowURI(URIs[idx]); |
||
292 | } |
||
293 | |||
294 | return allow; |
||
295 | } |
||
296 | |||
297 | /** |
||
298 | * Detect if URI is allowed to be requested (for a module or dependency) |
||
299 | * |
||
300 | * @function |
||
301 | * @access private |
||
302 | * @param {string} URI |
||
303 | * @returns {boolean} |
||
304 | */ |
||
305 | _allowURI ( URI ) { |
||
306 | |||
307 | // no absolute or relative paths? |
||
308 | if ( !this.options.allowPath && ( |
||
309 | (String(URI).indexOf('/') >= 0) || |
||
310 | (String(URI).indexOf('/') < String(URI).length) |
||
311 | ) ) { |
||
312 | return false; |
||
313 | } |
||
314 | |||
315 | // no external URLs? |
||
316 | if ( !this.options.allowExtern && (this._hasProtocol(URI) || this._isExtern(URI)) ) { |
||
317 | return false; |
||
318 | } |
||
319 | |||
320 | // is hostname allowed? |
||
321 | if ( this.options.allowExtern && (this.options.externHosts.length > 0) ) { |
||
322 | var has_host = false; |
||
323 | for (var idx in this.options.externHosts) { |
||
0 ignored issues
–
show
A for in loop automatically includes the property of any prototype object, consider checking the key using
hasOwnProperty .
When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically: var someObject;
for (var key in someObject) {
if ( ! someObject.hasOwnProperty(key)) {
continue; // Skip keys from the prototype.
}
doSomethingWith(key);
}
![]() |
|||
324 | has_host = has_host || this._hasHost(URI, this.options.externHosts[idx]); |
||
325 | } |
||
326 | if (!has_host) return false; |
||
0 ignored issues
–
show
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.
Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later. Consider: if (a > 0)
b = 42;
If you or someone else later decides to put another statement in, only the first statement will be executed. if (a > 0)
console.log("a > 0");
b = 42;
In this case the statement if (a > 0) {
console.log("a > 0");
b = 42;
}
ensures that the proper code will be executed conditionally no matter how many statements are added or removed. ![]() |
|||
327 | } |
||
328 | |||
329 | // finally... |
||
330 | return true; |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * Detect if URI is an external URI (ex: http://) |
||
335 | * |
||
336 | * @function |
||
337 | * @access private |
||
338 | * @param {string} URI |
||
339 | * @returns {boolean} |
||
340 | */ |
||
341 | _isExtern ( URI ) { |
||
342 | return ( |
||
343 | ( |
||
344 | (new RegExp("^(http(s)*:)*\\/\\/")).test(URI) || |
||
345 | this._hasProtocol( URI ) |
||
346 | ) && !this._hasHost(URI, location.hostname) |
||
347 | ); |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | * Detect if URI starts with a protocol identifier (ex: http:..., tel:..., mailto:...) |
||
352 | * |
||
353 | * @function |
||
354 | * @access private |
||
355 | * @param {string} uri |
||
0 ignored issues
–
show
|
|||
356 | * @returns {boolean} |
||
357 | */ |
||
358 | _hasProtocol ( URI ) { |
||
359 | return (new RegExp("^[\\w|-]+:")).test(URI); |
||
360 | } |
||
361 | |||
362 | /** |
||
363 | * Detect if URI contains a given hostname (ex: http://domain.tld..., //domain.tld...) |
||
364 | * |
||
365 | * @function |
||
366 | * @access private |
||
367 | * @param {string} uri |
||
0 ignored issues
–
show
|
|||
368 | * @returns {boolean} |
||
369 | */ |
||
370 | _hasHost ( URI, hostname ) { |
||
371 | return ( |
||
372 | [ 2, 3, 7, 8 ].indexOf( String(URI).indexOf(hostname) ) != -1 |
||
373 | /* |
||
374 | "//_", "://_", "http://_", "https://_" |
||
375 | */ |
||
376 | ); |
||
377 | } |
||
378 | |||
379 | /** |
||
380 | * destroy component's object in memory |
||
381 | * @access public |
||
382 | */ |
||
383 | destroy () { |
||
384 | this.$element |
||
385 | .removeAttr('data-'+this.application.appName+'-module-factory') |
||
386 | .removeAttr('data-deps') |
||
387 | .removeAttr('data-module') |
||
388 | .removeAttr('data-callback') |
||
389 | ; |
||
390 | this.manager.destroyModule(this); |
||
391 | } |
||
392 | |||
393 | |||
394 | |||
395 | /** |
||
396 | * Retrieve loaded module |
||
397 | */ |
||
398 | get module () { |
||
399 | return this._module; |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * Assing loaded module |
||
404 | */ |
||
405 | set module ( _module ) { |
||
406 | if ( (_module instanceof Module) ) { |
||
407 | this._module = _module; |
||
408 | } else { |
||
409 | throw new ModuleFactoryException('Module to assing must be an instance of Siteapp.Module'); |
||
410 | } |
||
411 | } |
||
412 | |||
413 | |||
414 | /** |
||
415 | * Retrieve attached module manager. |
||
416 | */ |
||
417 | get manager () { |
||
418 | return this._manager; |
||
419 | } |
||
420 | |||
421 | /** |
||
422 | * Attach application object. |
||
423 | */ |
||
424 | set manager ( _manager ) { |
||
425 | if (_manager instanceof ModuleManager) { |
||
426 | this._manager = _manager; |
||
427 | } else { |
||
428 | throw new ModuleFactoryException('Invalid module manager object to attach, must be an instance of Siteapp.ModuleManager'); |
||
429 | } |
||
430 | } |
||
431 | |||
432 | |||
433 | /** |
||
434 | * Retrieve attached application. |
||
435 | */ |
||
436 | get application () { |
||
437 | return this._app; |
||
438 | } |
||
439 | |||
440 | /** |
||
441 | * Attach application object. |
||
442 | */ |
||
443 | set application ( app ) { |
||
444 | if (app instanceof Siteapp) { |
||
445 | this._app = app; |
||
446 | } else { |
||
447 | throw new ModuleFactoryException('Invalid application object to attach, must be an instance of Siteapp'); |
||
448 | } |
||
449 | } |
||
450 | |||
451 | }; |
||
452 | |||
453 | export default ModuleFactory; |