Issues (149)

js/module/siteapp.moduleFactory.js (10 issues)

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
The constant ModuleFactoryDefaults seems to be never used. Consider removing it.
Loading history...
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
Coding Style Best Practice introduced by
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 b = 42 will always be executed, while the logging statement will be executed conditionally.

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.

Loading history...
155
    	
156
	    var $this = this;
0 ignored issues
show
The variable $this seems to be never used. Consider removing it.
Loading history...
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
Empty finally clause found. The clause can be removed.
Loading history...
Comprehensibility Documentation Best Practice introduced by
This code block is empty. Consider removing it or adding a comment to explain.
Loading history...
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);
}
Loading history...
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);
}
Loading history...
324
				has_host = has_host || this._hasHost(URI, this.options.externHosts[idx]);
325
			}
326
			if (!has_host) return false;
0 ignored issues
show
Coding Style Best Practice introduced by
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 b = 42 will always be executed, while the logging statement will be executed conditionally.

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.

Loading history...
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
The parameter uri does not exist. Did you maybe forget to remove this comment?
Loading history...
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
The parameter uri does not exist. Did you maybe forget to remove this comment?
Loading history...
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;