alchemymvc 1.2.5 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/lib/app/assets/scripts/.gitkeep +0 -0
  4. package/lib/app/assets/stylesheets/alchemy-info.less +0 -0
  5. package/lib/app/behaviour/publishable_behaviour.js +0 -0
  6. package/lib/app/behaviour/revision_behaviour.js +0 -0
  7. package/lib/app/behaviour/sluggable_behaviour.js +0 -0
  8. package/lib/app/component/.gitkeep +0 -0
  9. package/lib/app/conduit/electron_conduit.js +0 -0
  10. package/lib/app/conduit/http_conduit.js +173 -173
  11. package/lib/app/conduit/socket_conduit.js +620 -620
  12. package/lib/app/controller/alchemy_info_controller.js +0 -0
  13. package/lib/app/datasource/mongo_datasource.js +0 -0
  14. package/lib/app/helper/client_collection.js +0 -0
  15. package/lib/app/helper/pagination_helper.js +0 -0
  16. package/lib/app/helper/router_helper.js +0 -0
  17. package/lib/app/helper/socket_helper.js +613 -613
  18. package/lib/app/helper_component/paginate_component.js +0 -0
  19. package/lib/app/helper_controller/component.js +0 -0
  20. package/lib/app/helper_controller/conduit.js +0 -0
  21. package/lib/app/helper_controller/controller.js +0 -0
  22. package/lib/app/helper_datasource/00-nosql_datasource.js +0 -0
  23. package/lib/app/helper_datasource/05-fallback_datasource.js +0 -0
  24. package/lib/app/helper_datasource/idb_datasource.js +0 -0
  25. package/lib/app/helper_datasource/indexed_db.js +0 -0
  26. package/lib/app/helper_field/00-objectid_field.js +0 -0
  27. package/lib/app/helper_field/06-text_field.js +0 -0
  28. package/lib/app/helper_field/10-number_field.js +0 -0
  29. package/lib/app/helper_field/boolean_field.js +0 -0
  30. package/lib/app/helper_field/date_field.js +0 -0
  31. package/lib/app/helper_field/datetime_field.js +0 -0
  32. package/lib/app/helper_field/enum_field.js +0 -0
  33. package/lib/app/helper_field/geopoint_field.js +0 -0
  34. package/lib/app/helper_field/habtm_field.js +0 -0
  35. package/lib/app/helper_field/hasoneparent_field.js +0 -0
  36. package/lib/app/helper_field/html_field.js +0 -0
  37. package/lib/app/helper_field/integer_field.js +0 -0
  38. package/lib/app/helper_field/object_field.js +0 -0
  39. package/lib/app/helper_field/regexp_field.js +0 -0
  40. package/lib/app/helper_field/schema_field.js +23 -2
  41. package/lib/app/helper_field/time_field.js +0 -0
  42. package/lib/app/helper_field/url_field.js +0 -0
  43. package/lib/app/helper_model/criteria.js +0 -0
  44. package/lib/app/helper_model/db_query.js +0 -0
  45. package/lib/app/helper_model/document_list.js +0 -0
  46. package/lib/app/model/alchemy_task_model.js +0 -0
  47. package/lib/app/routes.js +0 -0
  48. package/lib/app/view/alchemy/info.ejs +0 -0
  49. package/lib/app/view/error/unknown.ejs +0 -0
  50. package/lib/app/view/paginate/navlist.ejs +0 -0
  51. package/lib/bootstrap.js +0 -0
  52. package/lib/class/behaviour.js +0 -0
  53. package/lib/class/component.js +0 -0
  54. package/lib/class/conduit.js +2555 -2552
  55. package/lib/class/controller.js +4 -1
  56. package/lib/class/document_list.js +0 -0
  57. package/lib/class/helper.js +0 -0
  58. package/lib/class/inode.js +0 -0
  59. package/lib/class/inode_dir.js +0 -0
  60. package/lib/class/inode_file.js +112 -112
  61. package/lib/class/inode_list.js +0 -0
  62. package/lib/class/model.js +1772 -1769
  63. package/lib/class/path_definition.js +0 -0
  64. package/lib/class/route.js +0 -0
  65. package/lib/class/session.js +0 -0
  66. package/lib/class/task.js +0 -0
  67. package/lib/core/base.js +50 -9
  68. package/lib/core/discovery.js +0 -0
  69. package/lib/core/routing.js +0 -0
  70. package/lib/core/socket.js +159 -159
  71. package/lib/init/alchemy.js +1823 -1823
  72. package/lib/init/constants.js +0 -0
  73. package/lib/init/functions.js +8 -4
  74. package/lib/init/load_functions.js +0 -0
  75. package/lib/init/requirements.js +101 -101
  76. package/package.json +74 -74
@@ -1,2552 +1,2555 @@
1
- var fileCache = alchemy.shared('files.fileCache'),
2
- libstream = alchemy.use('stream'),
3
- libpath = alchemy.use('path'),
4
- libmime = alchemy.use('mime'),
5
- libua = alchemy.use('useragent'),
6
- zlib = alchemy.use('zlib'),
7
- BODY = Symbol('body'),
8
- TESTED_ROUTES = Symbol('tested_routes'),
9
- magic,
10
- fs = alchemy.use('fs'),
11
- prefixes = alchemy.shared('Routing.prefixes');
12
-
13
- /**
14
- * The Conduit Class
15
- *
16
- * @author Jelle De Loecker <jelle@develry.be>
17
- * @since 0.2.0
18
- * @version 1.2.0
19
- *
20
- * @param {IncomingMessage} req
21
- * @param {ServerResponse} res
22
- * @param {Router} router
23
- */
24
- var Conduit = Function.inherits('Alchemy.Base', 'Alchemy.Conduit', function Conduit(req, res, router) {
25
-
26
- // Store the starting time
27
- this.start = new Date();
28
-
29
- // Create a reference to ourselves
30
- this.conduit = this;
31
-
32
- // Debug messages for this request
33
- this.debuglog = [];
34
-
35
- this._debugObject = this.debug({label: 'Initialize Conduit'});
36
- this._debugConduitInitialize = this._debugObject;
37
-
38
- // Allow use of the log in the views
39
- if (alchemy.settings.debug) {
40
- this.internal('debuglog', {_placeholder_: 'debuglog'});
41
- }
42
-
43
- // Cookies to send to the client
44
- this.new_cookies = {};
45
- this.new_cookie_header = [];
46
-
47
- // The headers to send
48
- this.response_headers = {};
49
-
50
- // Where the body will go
51
- this.body = {};
52
-
53
- // Where the files will go
54
- this.files = {};
55
-
56
- this.initValues();
57
- this.setReqRes(req, res);
58
- });
59
-
60
- /**
61
- * Deprecated property names
62
- *
63
- * @author Jelle De Loecker <jelle@develry.be>
64
- * @since 1.0.0
65
- * @version 1.1.0
66
- */
67
- Conduit.setDeprecatedProperty('originalPath', 'original_path');
68
- Conduit.setDeprecatedProperty('newCookies', 'new_cookies');
69
- Conduit.setDeprecatedProperty('newCookieHeader', 'new_cookie_header');
70
- Conduit.setDeprecatedProperty('viewRender', 'renderer');
71
- Conduit.setDeprecatedProperty('view_render', 'renderer');
72
- Conduit.setDeprecatedProperty('sceneId', 'scene_id');
73
-
74
- /**
75
- * Return the cookies
76
- *
77
- * @author Jelle De Loecker <jelle@develry.be>
78
- * @since 0.2.0
79
- * @version 0.2.0
80
- */
81
- Conduit.prepareProperty(function cookies() {
82
- return String.decodeCookies(this.headers.cookie);
83
- });
84
-
85
- /**
86
- * Return the parsed useragent string
87
- *
88
- * @author Jelle De Loecker <jelle@develry.be>
89
- * @since 0.2.0
90
- * @version 0.5.0
91
- */
92
- Conduit.prepareProperty(function useragent() {
93
- return libua.lookup(this.headers['user-agent']);
94
- });
95
-
96
- /**
97
- * Create a Hawkejs Renderer
98
- *
99
- * @author Jelle De Loecker <jelle@develry.be>
100
- * @since 0.2.0
101
- * @version 1.1.5
102
- */
103
- Conduit.prepareProperty(function renderer() {
104
-
105
- let result;
106
-
107
- if (this.parent && this.parent != this && this.parent.renderer) {
108
- result = this.parent.renderer.createSubRenderer();
109
- } else {
110
- result = alchemy.hawkejs.createRenderer();
111
- }
112
-
113
- return result;
114
- });
115
-
116
- /**
117
- * Enforce the scene_id
118
- *
119
- * @author Jelle De Loecker <jelle@develry.be>
120
- * @since 1.1.0
121
- * @version 1.1.7
122
- */
123
- Conduit.enforceProperty(function scene_id(new_value, old_value) {
124
-
125
- if (new_value) {
126
- return new_value;
127
- }
128
-
129
- if (this.headers['x-scene-id']) {
130
- return this.headers['x-scene-id'];
131
- }
132
-
133
- // If there also was no old value, create a new scene
134
- if (old_value == null) {
135
- // Generate the scene_id
136
- new_value = Crypto.randomHex(8) || Crypto.pseudoHex(8);
137
-
138
- // Tell the session this scene can be expected
139
- this.getSession().expectScene(new_value);
140
-
141
- let path;
142
-
143
- if (this.url) {
144
- path = this.url.path;
145
- }
146
-
147
- // Set the sceneid cookie
148
- this.cookie('scene_start_' + ~~(Math.random()*1000), {
149
-
150
- // The time this scene has started
151
- start: Date.now(),
152
-
153
- // The id of the scene
154
- id: new_value
155
- }, {
156
- // Cookie should only be visible on this path
157
- path: path,
158
-
159
- // Cookie should not live for more than 15 seconds
160
- maxAge: 1000 * 15
161
- });
162
-
163
- return new_value;
164
- }
165
- });
166
-
167
- /**
168
- * Enforce the active_prefix
169
- *
170
- * @author Jelle De Loecker <jelle@develry.be>
171
- * @since 1.1.0
172
- * @version 1.1.5
173
- */
174
- Conduit.enforceProperty(function active_prefix(new_value, old_value) {
175
-
176
- if (!new_value) {
177
- this.renderer.language = null;
178
- return null;
179
- }
180
-
181
- if (new_value == old_value) {
182
- return new_value;
183
- }
184
-
185
- // Set the active prefix
186
- this.internal('active_prefix', new_value);
187
- this.expose('active_prefix', new_value);
188
-
189
- if (this.locales[0] != new_value) {
190
- this.locales.unshift(new_value);
191
- }
192
-
193
- // Set the translate options for use in hawkejs
194
- this.internal('locales', this.locales);
195
- this.expose('locales', this.locales);
196
-
197
- let config = Prefix.get(new_value);
198
-
199
- if (config) {
200
- this.renderer.language = config.locale;
201
- }
202
-
203
- return new_value;
204
- });
205
-
206
- /**
207
- * Get a session object by id
208
- *
209
- * @author Jelle De Loecker <jelle@develry.be>
210
- * @since 0.2.0
211
- * @version 0.2.0
212
- */
213
- Conduit.setStatic(function getSessionById(id) {
214
- return alchemy.sessions.get(id);
215
- });
216
-
217
- /**
218
- * See if this is a secure connection
219
- *
220
- * @author Jelle De Loecker <jelle@develry.be>
221
- * @since 0.4.2
222
- * @version 1.0.2
223
- */
224
- Conduit.setProperty(function is_secure() {
225
-
226
- var protocol;
227
-
228
- if (alchemy.settings.assume_https) {
229
- return true;
230
- }
231
-
232
- if (this.headers && this.headers['x-forwarded-proto'] == 'https') {
233
- return true;
234
- }
235
-
236
- if (this.url && this.url.protocol == 'https:') {
237
- return true;
238
- }
239
-
240
- if (this.protocol && this.protocol.startsWith('https')) {
241
- return true;
242
- }
243
-
244
- if (this.encrypted == true) {
245
- return true;
246
- }
247
-
248
- return false;
249
- });
250
-
251
- /**
252
- * Set the request body
253
- *
254
- * @author Jelle De Loecker <jelle@develry.be>
255
- * @since 1.1.0
256
- * @version 1.1.0
257
- *
258
- * @param {Object}
259
- */
260
- Conduit.setMethod(function setRequestBody(body) {
261
-
262
- if (!body) {
263
- return;
264
- }
265
-
266
- Object.assign(this.body, body);
267
- });
268
-
269
- /**
270
- * Has the given route been tested yet?
271
- *
272
- * @author Jelle De Loecker <jelle@elevenways.be>
273
- * @since 1.2.5
274
- * @version 1.2.5
275
- *
276
- * @param {Route}
277
- */
278
- Conduit.setMethod(function hasRouteBeenTested(route) {
279
-
280
- if (!route || !this[TESTED_ROUTES]) {
281
- return false;
282
- }
283
-
284
- return this[TESTED_ROUTES].has(route);
285
- });
286
-
287
- /**
288
- * Mark this route as having been tested
289
- *
290
- * @author Jelle De Loecker <jelle@elevenways.be>
291
- * @since 1.2.5
292
- * @version 1.2.5
293
- *
294
- * @param {Route}
295
- */
296
- Conduit.setMethod(function markRouteAsTested(route) {
297
-
298
- if (!this[TESTED_ROUTES]) {
299
- this[TESTED_ROUTES] = new Set();
300
- }
301
-
302
- this[TESTED_ROUTES].add(route);
303
- });
304
-
305
- /**
306
- * Rewrite a certain URL parameter
307
- * (Causing some kind of redirect)
308
- *
309
- * @author Jelle De Loecker <jelle@elevenways.be>
310
- * @since 1.2.5
311
- * @version 1.2.5
312
- *
313
- * @param {String} route_param
314
- * @param {*} new_value
315
- */
316
- Conduit.setMethod(function rewriteRequestRouteParam(route_param, new_value) {
317
-
318
- if (!this.rewritten_request_route_param) {
319
- this.rewritten_request_route_param = {};
320
- }
321
-
322
- this.rewritten_request_route_param[route_param] = new_value;
323
- });
324
-
325
- /**
326
- * Set the request files
327
- *
328
- * @author Jelle De Loecker <jelle@elevenways.be>
329
- * @since 1.1.0
330
- * @version 1.1.0
331
- *
332
- * @param {Object}
333
- */
334
- Conduit.setMethod(function setRequestFiles(files) {
335
-
336
- if (!files) {
337
- return;
338
- }
339
-
340
- _setRequestFiles(this, files, this.files);
341
- });
342
-
343
- /**
344
- * Set the request files
345
- *
346
- * @author Jelle De Loecker <jelle@elevenways.be>
347
- * @since 1.2.0
348
- * @version 1.2.0
349
- *
350
- * @param {Conduit} conduit
351
- * @param {Array} files
352
- * @param {Object} target
353
- */
354
- function _setRequestFiles(conduit, files, target) {
355
-
356
- let context,
357
- upload,
358
- entry,
359
- key;
360
-
361
- for (key in files) {
362
- entry = files[key];
363
-
364
- if (Array.isArray(entry)) {
365
- context = target[key];
366
-
367
- if (!context) {
368
- context = target[key] = {};
369
- }
370
-
371
- _setRequestFiles(conduit, entry, context);
372
- } else {
373
- target[key] = Classes.Alchemy.Inode.File.from(entry);
374
- }
375
- }
376
- }
377
-
378
- /**
379
- * Don't convert a conduit to any special json data
380
- *
381
- * @author Jelle De Loecker <jelle@develry.be>
382
- * @since 0.2.0
383
- * @version 0.2.0
384
- */
385
- Conduit.setMethod(function toJSON() {
386
- return null;
387
- });
388
-
389
- /**
390
- * Set the request & response objects
391
- *
392
- * @author Jelle De Loecker <jelle@elevenways.be>
393
- * @since 1.2.0
394
- * @version 1.2.0
395
- */
396
- Conduit.setMethod(function setReqRes(req, res) {
397
-
398
- if (req != null) {
399
- // Make conduit available in req
400
- req.conduit = this;
401
-
402
- // Basic HTTP objects
403
- this.request = req;
404
-
405
- // The HTTP request headers
406
- this.headers = req.headers;
407
-
408
- // Parse the original URL without host
409
- this.original_url = new RURL(req.url);
410
-
411
- // Is this an AJAX request?
412
- this.ajax = null;
413
- }
414
-
415
- if (res != null) {
416
- this.response = res;
417
- }
418
- });
419
-
420
- /**
421
- * Init values
422
- *
423
- * @author Jelle De Loecker <jelle@develry.be>
424
- * @since 0.3.3
425
- * @version 1.1.0
426
- */
427
- Conduit.setMethod(function initValues() {
428
-
429
- // Use passed-along router, or default router instance
430
- this.router = this.router || Router;
431
-
432
- // The path without any prefix, including section mounts
433
- this.path = null;
434
-
435
- // The path without prefix or section mount
436
- this.sectionPath = null;
437
-
438
- // The accepted languages
439
- this.languages = null;
440
-
441
- // URL paths can be prefixed with certain locales,
442
- // these locales should then get preference over the user's browser locale
443
- this.prefix = null;
444
-
445
- // All the locales the user's browser accepts
446
- this.locales = null;
447
-
448
- // The matching Route instance
449
- this.route = null;
450
-
451
- // The named parameters inside the path
452
- this.params = null;
453
-
454
- // The original string parameters
455
- this.route_string_parameters = null;
456
-
457
- // The section vhost domain
458
- this.sectionDomain = null;
459
-
460
- // The section of the used route
461
- this.section = null;
462
-
463
- // The parsed path (including querystring)
464
- this.url = null
465
-
466
- // The current active theme
467
- this.theme = null;
468
- });
469
-
470
- /**
471
- * Get the time since the conduit was made
472
- *
473
- * @author Jelle De Loecker <jelle@develry.be>
474
- * @since 0.2.0
475
- * @version 1.1.0
476
- */
477
- Conduit.setMethod(function time() {
478
- return Date.now() - this.start;
479
- });
480
-
481
- /**
482
- * Parse the request, get information from the url
483
- *
484
- * @author Jelle De Loecker <jelle@develry.be>
485
- * @since 0.2.0
486
- * @version 1.2.5
487
- *
488
- * @param {IncomingMessage} req
489
- * @param {ServerResponse} res
490
- */
491
- Conduit.setMethod(async function parseRequest() {
492
-
493
- var protocol,
494
- section;
495
-
496
- if (this.method == null && this.request && this.request.method) {
497
- this.method = this.request.method.toLowerCase();
498
- }
499
-
500
- this.parseShortcuts();
501
- this.parseLanguages();
502
- this.parsePrefix();
503
- this.parseSection();
504
-
505
- // Try getting the route
506
- await this.parseRoute();
507
-
508
- if (this.halt_request) {
509
- return false;
510
- }
511
-
512
- // Is this encrypted?
513
- if (this.encrypted == null) {
514
- this.encrypted = this.request.connection.encrypted;
515
- }
516
-
517
- if (this.rewritten_request_route_param) {
518
- let params = Object.assign({}, this.route_string_parameters, this.rewritten_request_route_param);
519
- let new_url = this.route.generateUrl(params, this);
520
- this.overrideResponseUrl(new_url);
521
- }
522
-
523
- // If the url has already been parsed, return early
524
- if (this.url) {
525
- return;
526
- }
527
-
528
- if (alchemy.settings.assume_https) {
529
- protocol = 'https://';
530
- } else if (this.headers['x-forwarded-proto']) {
531
- protocol = this.headers['x-forwarded-proto'];
532
- } else if (this.protocol) {
533
- protocol = this.protocol;
534
- } else if (this.encrypted) {
535
- protocol = 'https://';
536
- } else {
537
- protocol = 'http://';
538
- }
539
-
540
- // Create a new RURL instance
541
- this.url = new RURL();
542
-
543
- // Set the protocol
544
- this.url.protocol = protocol;
545
-
546
- // Set the host
547
- this.url.hostname = this.headers.host;
548
-
549
- let path = this.path;
550
-
551
- if (this.prefix) {
552
- path = '/' + this.prefix + '/' + path;
553
- }
554
-
555
- this.url.path = path;
556
-
557
- return true;
558
- });
559
-
560
- /**
561
- * Parse the headers for shortcuts
562
- *
563
- * @author Jelle De Loecker <jelle@develry.be>
564
- * @since 0.2.0
565
- * @version 0.2.0
566
- */
567
- Conduit.setMethod(function parseShortcuts() {
568
-
569
- var headers = this.headers;
570
-
571
- // A request can just tell us what route to use
572
- if (headers['x-alchemy-route-name']) {
573
- this.route = this.router.getRouteByName(headers['x-alchemy-route-name']);
574
- }
575
-
576
- // And which prefix (this is a forced prefix)
577
- if (headers['x-alchemy-prefix'] && prefixes[headers['x-alchemy-prefix']]) {
578
- this.prefix = headers['x-alchemy-prefix'];
579
- }
580
-
581
- // Section domains can only be requested through headers
582
- if (headers['x-alchemy-section-domain']) {
583
- this.sectionDomain = headers['x-alchemy-section-domain'];
584
- }
585
-
586
- // Only get ajax on the first parse
587
- if (this.ajax == null) {
588
- this.ajax = headers['x-requested-with'] === 'XMLHttpRequest';
589
- }
590
-
591
- });
592
-
593
- /**
594
- * Sort the parsed accept-language header array
595
- *
596
- * @author Jelle De Loecker <jelle@develry.be>
597
- * @since 0.0.1
598
- * @version 0.0.1
599
- *
600
- * @param {Object} a
601
- * @param {Object} b
602
- */
603
- function qualityCmp(a, b) {
604
- if (a.quality === b.quality) {
605
- return 0;
606
- } else if (a.quality < b.quality) {
607
- return 1;
608
- } else {
609
- return -1;
610
- }
611
- }
612
-
613
- /**
614
- * Parses the HTTP accept-language header
615
- *
616
- * @author Jelle De Loecker <jelle@develry.be>
617
- * @since 0.0.1
618
- * @version 0.2.0
619
- */
620
- Conduit.setMethod(function parseLanguages() {
621
-
622
- var rawLangs,
623
- rawLang,
624
- locales,
625
- parts,
626
- langs,
627
- qval,
628
- temp,
629
- i,
630
- q;
631
-
632
- langs = [];
633
- locales = [];
634
-
635
- if (this.headers['accept-language']) {
636
-
637
- rawLangs = this.headers['accept-language'].split(',');
638
-
639
- for (i = 0; i < rawLangs.length; i++) {
640
- rawLang = rawLangs[i];
641
-
642
- parts = rawLang.split(';');
643
- qval = null;
644
- q = 1;
645
-
646
- if (parts.length > 1 && parts[1].indexOf('q=') === 0) {
647
- qval = parseFloat(parts[1].split('=')[1]);
648
-
649
- if (isNaN(qval) === false) {
650
- q = qval;
651
- }
652
- }
653
-
654
- // Get the lang-loc code
655
- temp = parts[0].trim().toLowerCase().split('-');
656
-
657
- langs.push({lang: temp[0], loc: temp[1], quality: q});
658
- }
659
-
660
- langs.sort(qualityCmp);
661
- };
662
-
663
- temp = {};
664
-
665
- for (i = 0; i < langs.length; i++) {
666
- if (!temp[langs[i].lang]) {
667
- locales.push(langs[i].lang);
668
- temp[langs[i].lang] = true;
669
- }
670
- }
671
-
672
- this.languages = langs;
673
- this.locales = locales;
674
- });
675
-
676
- /**
677
- * Parses accept-encoding strings
678
- *
679
- * @author jshttp
680
- * @author Jelle De Loecker <jelle@develry.be>
681
- * @since 0.2.0
682
- * @version 0.2.0
683
- */
684
- function parseEncoding(s, i) {
685
- var match = s.match(/^\s*(\S+?)\s*(?:;(.*))?$/);
686
-
687
- if (!match) return null;
688
-
689
- var encoding = match[1];
690
- var q = 1;
691
- if (match[2]) {
692
- var params = match[2].split(';');
693
- for (var i = 0; i < params.length; i ++) {
694
- var p = params[i].trim().split('=');
695
- if (p[0] === 'q') {
696
- q = parseFloat(p[1]);
697
- break;
698
- }
699
- }
700
- }
701
-
702
- return {
703
- encoding: encoding,
704
- q: q,
705
- i: i
706
- };
707
- }
708
-
709
- /**
710
- * Parses accept-encoding strings
711
- *
712
- * @author jshttp
713
- * @author Jelle De Loecker <jelle@develry.be>
714
- * @since 0.2.0
715
- * @version 0.2.0
716
- */
717
- function specify(encoding, spec, index) {
718
- var s = 0;
719
- if(spec.encoding.toLowerCase() === encoding.toLowerCase()){
720
- s |= 1;
721
- } else if (spec.encoding !== '*' ) {
722
- return null
723
- }
724
-
725
- return {
726
- i: index,
727
- o: spec.i,
728
- q: spec.q,
729
- s: s
730
- }
731
- };
732
-
733
- /**
734
- * Parses the HTTP accept-encoding header
735
- *
736
- * @author Jelle De Loecker <jelle@develry.be>
737
- * @since 0.2.0
738
- * @version 0.2.0
739
- */
740
- Conduit.setMethod(function parseAcceptEncoding() {
741
-
742
- var hasIdentity,
743
- minQuality,
744
- encoding,
745
- accepts,
746
- i,
747
- j;
748
-
749
- // Make sure this only runs once
750
- if (this.accepted_encodings != null) {
751
- return;
752
- }
753
-
754
- if (!this.headers['accept-encoding']) {
755
- this.accepted_encodings = false;
756
- return;
757
- }
758
-
759
- accepts = this.headers['accept-encoding'].split(',');
760
- minQuality = 1;
761
-
762
- for (i = 0, j = 0; i < accepts.length; i++) {
763
- encoding = parseEncoding(accepts[i].trim(), i);
764
-
765
- if (encoding) {
766
- accepts[j++] = encoding;
767
- hasIdentity = hasIdentity || specify('identity', encoding);
768
- minQuality = Math.min(minQuality, encoding.q || 1);
769
- }
770
- }
771
-
772
- if (!hasIdentity) {
773
- /*
774
- * If identity doesn't explicitly appear in the accept-encoding header,
775
- * it's added to the list of acceptable encoding with the lowest q
776
- */
777
- accepts[j++] = {
778
- encoding: 'identity',
779
- q: minQuality,
780
- i: i
781
- };
782
- }
783
-
784
- // trim accepts
785
- accepts.length = j;
786
-
787
- this.accepted_encodings = accepts;
788
- });
789
-
790
- /**
791
- * See if the wanted encoding is accepted by the client
792
- *
793
- * @author Jelle De Loecker <jelle@develry.be>
794
- * @since 0.2.0
795
- * @version 0.2.0
796
- */
797
- Conduit.setMethod(function accepts(encoding) {
798
-
799
- var i;
800
-
801
- // Parse the encodings on the fly
802
- this.parseAcceptEncoding();
803
-
804
- if (!this.accepted_encodings) {
805
- return false;
806
- }
807
-
808
- for (i = 0; i < this.accepted_encodings.length; i++) {
809
- if (this.accepted_encodings[i].encoding == encoding) {
810
- return true;
811
- }
812
- }
813
-
814
- return false;
815
- });
816
-
817
- /**
818
- * Create a loopback conduit
819
- *
820
- * @author Jelle De Loecker <jelle@develry.be>
821
- * @since 0.2.0
822
- * @version 1.1.3
823
- *
824
- * @param {Object} args
825
- * @param {Function} callback
826
- *
827
- * @return {Alchemy.LoopbackConduit}
828
- */
829
- Conduit.setMethod(function loopback(args, callback) {
830
- return Classes.Alchemy.Conduit.Loopback.create(this, args, callback);
831
- });
832
-
833
- /**
834
- * Parse the request, get information from the url
835
- *
836
- * @author Jelle De Loecker <jelle@develry.be>
837
- * @since 0.2.0
838
- * @version 1.1.0
839
- */
840
- Conduit.setMethod(function parsePrefix() {
841
-
842
- var active_prefix,
843
- prefix,
844
- begin,
845
- path;
846
-
847
- path = this.original_path;
848
-
849
- if (!path) {
850
- return;
851
- }
852
-
853
- // Look for the prefix at the beginning of the url path
854
- if (!this.prefix) {
855
- for (prefix in prefixes) {
856
- begin = '/' + prefix + '/';
857
-
858
- if (path.indexOf(begin) === 0) {
859
- this.prefix = prefix;
860
- break;
861
- }
862
- }
863
- }
864
-
865
- // Handle urls with ONLY the prefix and no ending slash
866
- if (!this.prefix) {
867
- for (prefix in prefixes) {
868
- begin = '/' + prefix;
869
-
870
- if (this.original_pathname == begin) {
871
- this.prefix = prefix;
872
- break;
873
- }
874
- }
875
-
876
- if (this.prefix && path.endsWith('/' + this.prefix)) {
877
- this.path = '/';
878
- } else if (this.prefix && path.startsWith('/' + this.prefix + '?')) {
879
- this.path = '/?' + path.after('?');
880
- } else {
881
- this.path = path;
882
- }
883
-
884
- } else if (this.prefix && path.indexOf('/' + this.prefix + '/') === 0) {
885
- // Remove the prefix from the path if one is given
886
- this.path = path.slice(this.prefix.length+1);
887
- } else {
888
- this.path = path;
889
- }
890
-
891
- // Add this prefix to the top of the locales
892
- if (this.prefix) {
893
- active_prefix = this.prefix;
894
- this.locales.unshift(this.prefix);
895
-
896
- // Remember this prefix in the session
897
- this.session('last_forced_prefix', this.prefix);
898
-
899
- // Let the client know this prefix should be used
900
- this.expose('forced_prefix', this.prefix);
901
- } else {
902
-
903
- let last_forced_prefix = this.session('last_forced_prefix');
904
-
905
- if (last_forced_prefix) {
906
- active_prefix = last_forced_prefix;
907
- } else {
908
-
909
- // If no prefix has been found yet, look for the default prefix
910
- // This will override the browser locale
911
- if (this.headers['x-alchemy-default-prefix']) {
912
- if (prefixes[this.headers['x-alchemy-default-prefix']]) {
913
- active_prefix = this.headers['x-alchemy-default-prefix'];
914
-
915
- if (this.locales[0] != active_prefix) {
916
- this.locales.unshift(active_prefix);
917
- }
918
- }
919
- }
920
-
921
- if (!active_prefix) {
922
- active_prefix = this.locales[0];
923
- }
924
- }
925
- }
926
-
927
- this.active_prefix = active_prefix;
928
- });
929
-
930
- /**
931
- * Get the section
932
- *
933
- * @author Jelle De Loecker <jelle@develry.be>
934
- * @since 0.2.0
935
- * @version 0.2.1
936
- */
937
- Conduit.setMethod(function parseSection() {
938
-
939
- // Get the section this path is using
940
- this.section = this.router.getPathSection(this.path);
941
-
942
- if (!this.section) {
943
- log.warn('No section found for path "' + this.path + '"');
944
- }
945
-
946
- // If the section has a parent it's not the root
947
- if (this.section && this.section.parent) {
948
- this.sectionPath = this.path.slice(this.section.mount.length) || '/';
949
- } else {
950
- this.sectionPath = this.path;
951
- }
952
- });
953
-
954
- /**
955
- * Get a route by its name
956
- *
957
- * @author Jelle De Loecker <jelle@develry.be>
958
- * @since 0.2.0
959
- * @version 0.2.0
960
- *
961
- * @param {String|Object} name The name of the route
962
- */
963
- Conduit.setMethod(function getRouteByName(name) {
964
-
965
- // See if the name is an object, which means it's for sockets
966
- if (name && typeof name == 'object') {
967
- this.route = name;
968
- } else {
969
- this.route = this.router.getRouteByName(name);
970
- }
971
-
972
- return this.route;
973
- });
974
-
975
- /**
976
- * Get the Route instance & named parameters
977
- *
978
- * @author Jelle De Loecker <jelle@develry.be>
979
- * @since 0.2.0
980
- * @version 1.1.7
981
- *
982
- * @param {Route} after_route Only check routes after this one
983
- *
984
- * @return {Boolean} Continue processing this request or not?
985
- */
986
- Conduit.setMethod(async function parseRoute(after_route) {
987
-
988
- var temp;
989
-
990
- this.section = this.router.getPathSection(this.path);
991
-
992
- // Remove the current found route
993
- if (after_route) {
994
- this.route_rematch = true;
995
- this.route = null;
996
- }
997
-
998
- // If the route hasn't been found in the header shortcuts yet, look for it
999
- if (!this.route) {
1000
-
1001
- temp = this.router.getRouteBySectionPath(this, this.method, this.section, this.sectionPath, this.prefix, after_route);
1002
-
1003
- if (temp && temp.then) {
1004
- temp = await temp;
1005
- }
1006
-
1007
- if (temp) {
1008
- this.route = temp.route;
1009
- this.params = temp.parameters;
1010
- this.route_string_parameters = temp.original_parameters;
1011
- this.path_definition = temp.definition;
1012
- } else {
1013
- // Is this a HEAD request? Then we need to check if a GET exists
1014
- if (this.method == 'head') {
1015
- let get_route = this.router.getRouteBySectionPath(this, 'get', this.section, this.sectionPath, this.prefix, after_route);
1016
-
1017
- if (get_route && get_route.then) {
1018
- get_route = await get_route;
1019
- }
1020
-
1021
- // A GET route was found, so we just need to end this request
1022
- if (get_route) {
1023
- this.end();
1024
- this.halt_request = true;
1025
- return;
1026
- }
1027
- } else {
1028
- // See if the path matches another method
1029
- temp = await this.router.getRouteBySectionPath(this, ['get', 'post', 'put'], this.section, this.sectionPath, this.prefix, after_route);
1030
-
1031
- if (temp) {
1032
- this.route_mismatch = temp.route;
1033
-
1034
- temp = null;
1035
- }
1036
- }
1037
-
1038
- this.route_not_found = true;
1039
- }
1040
- } else {
1041
- temp = this.route.match(this, this.method, this.sectionPath);
1042
-
1043
- if (temp && temp.then) {
1044
- temp = await temp;
1045
- }
1046
-
1047
- if (temp) {
1048
- this.params = temp.parameters || {};
1049
- this.route_string_parameters = temp.original_parameters || {};
1050
- this.path_definition = temp.definition;
1051
- } else {
1052
- this.params = {};
1053
- }
1054
- }
1055
- });
1056
-
1057
- /**
1058
- * Run the middleware
1059
- *
1060
- * @author Jelle De Loecker <jelle@develry.be>
1061
- * @since 0.2.0
1062
- * @version 1.1.5
1063
- */
1064
- Conduit.setMethod(async function callMiddleware() {
1065
-
1066
- if (!this.section) {
1067
- return this.callHandler();
1068
- }
1069
-
1070
- let that = this,
1071
- middlewares = await this.section.getMiddleware(this, this.section, this.path, this.prefix),
1072
- debugObject = this._debugObject,
1073
- middleDebug = this.debug({label: 'middleware', data: {title: 'Doing middleware'}}),
1074
- routeDebug,
1075
- theme;
1076
-
1077
- if (middleDebug) {
1078
- this._debugObject = middleDebug;
1079
- }
1080
-
1081
- middlewares = new Iterator(middlewares);
1082
-
1083
- Function.while(function test() {
1084
- return middlewares.hasNext();
1085
- }, function middlewareTask(next) {
1086
-
1087
- var route = middlewares.next().value,
1088
- middlePath,
1089
- req;
1090
-
1091
- // Skip middleware that does not listen to the request method
1092
- if (route.methods.indexOf(that.method) === -1) {
1093
- return next();
1094
- }
1095
-
1096
- // Augment the request object
1097
- req = Object.create(that.request);
1098
-
1099
- // Get the path without the middleware mount path
1100
- middlePath = req.conduit.sectionPath.replace(route.paths[''].source, '');
1101
-
1102
- // Strip any query parameters
1103
- if (middlePath.indexOf('?') > -1) {
1104
- middlePath = middlePath.before('?');
1105
- }
1106
-
1107
- if (middlePath[0] !== '/') {
1108
- middlePath = '/' + middlePath;
1109
- }
1110
-
1111
- // Look for theme settings
1112
- if (req.conduit.url) {
1113
- theme = req.conduit.url.query.theme;
1114
-
1115
- if (theme) {
1116
- middlePath = ['/' + theme + middlePath, middlePath];
1117
- }
1118
- }
1119
-
1120
- req.middlePath = middlePath;
1121
- req.original = that.request;
1122
-
1123
- if (routeDebug) {
1124
- routeDebug.stop();
1125
- }
1126
-
1127
- if (middleDebug) {
1128
- routeDebug = middleDebug.debug('route', {title: 'Doing "' + route.name + '"'});
1129
- that._debugObject = routeDebug;
1130
- }
1131
-
1132
- route.fnc(req, that.response, next);
1133
- }, function done(err) {
1134
-
1135
- if (err) {
1136
- return that.emit('error', err);
1137
- }
1138
-
1139
- if (routeDebug) {
1140
- routeDebug.stop();
1141
- }
1142
-
1143
- // Don't do this for websockets
1144
- if (that.websocket) {
1145
- return;
1146
- }
1147
-
1148
- if (middleDebug) {
1149
- middleDebug.mark('Preparing viewrender');
1150
- }
1151
-
1152
- that.prepareViewRender();
1153
-
1154
- if (middleDebug) {
1155
- middleDebug.mark(false);
1156
- middleDebug.stop();
1157
- }
1158
-
1159
- if (that._debugConduitInitialize) {
1160
- that._debugConduitInitialize.stop();
1161
- }
1162
-
1163
- // Return the original debug object
1164
- that._debugObject = debugObject;
1165
-
1166
- that.callHandler();
1167
- });
1168
- });
1169
-
1170
- /**
1171
- * Create a new Hawkejs' ViewRender instance
1172
- *
1173
- * @author Jelle De Loecker <jelle@develry.be>
1174
- * @since 0.2.0
1175
- * @version 1.1.1
1176
- */
1177
- Conduit.setMethod(function prepareViewRender() {
1178
-
1179
- // Add a link to this conduit
1180
- this.renderer.conduit = this;
1181
- this.renderer.server_var('conduit', this);
1182
-
1183
- // Let the ViewRender get some request info
1184
- this.renderer.prepare(this.request, this);
1185
-
1186
- // Pass url parameters to the client
1187
- this.renderer.internal('urlparams', this.route_string_parameters);
1188
- this.renderer.internal('url', this.url);
1189
-
1190
- if (this.route) {
1191
- this.renderer.internal('route', this.route.name);
1192
- }
1193
-
1194
- this.renderer.is_for_client_side = this.ajax;
1195
- });
1196
-
1197
- /**
1198
- * Call the handler of this route when parsing is finished
1199
- *
1200
- * @author Jelle De Loecker <jelle@develry.be>
1201
- * @since 0.2.0
1202
- * @version 1.1.7
1203
- */
1204
- Conduit.setMethod(function callHandler() {
1205
-
1206
- if (!this.route) {
1207
-
1208
- if (this.route_mismatch) {
1209
-
1210
- if (alchemy.settings.debug) {
1211
- console.log('Route method not allowed:', this);
1212
- }
1213
-
1214
- this.error(405, 'Method Not Allowed', false);
1215
-
1216
- } else {
1217
- if (alchemy.settings.debug) {
1218
- console.log('Route not found:', this);
1219
- }
1220
-
1221
- this.notFound('Route was not found');
1222
- }
1223
-
1224
- return;
1225
- }
1226
-
1227
- this.route.callHandler(this);
1228
- });
1229
-
1230
- /**
1231
- * End the current request with a 202 status
1232
- * and tell the client to look at another url later
1233
- *
1234
- * @author Jelle De Loecker <jelle@develry.be>
1235
- * @since 1.1.0
1236
- * @version 1.2.5
1237
- *
1238
- * @param {String|Object} options Options or url
1239
- */
1240
- Conduit.setMethod(function postpone(options) {
1241
-
1242
- let session = this.getSession();
1243
-
1244
- if (typeof options == 'number') {
1245
- options = {
1246
- expected_duration: options
1247
- };
1248
- } else if (!options) {
1249
- options = {};
1250
- }
1251
-
1252
- // Make sure the scene id exists
1253
- this.createScene();
1254
-
1255
- // Already set the cookies
1256
- if (this.new_cookie_header.length) {
1257
- this.response.setHeader('set-cookie', this.new_cookie_header);
1258
- }
1259
-
1260
- let postponed_id = session.postpone(this),
1261
- url = '/alchemy/postponed/' + postponed_id;
1262
-
1263
- // Set the location header where the client should look at later
1264
- this.response.setHeader('Location', url);
1265
- this.response.setHeader('Content-Type', 'text/html');
1266
-
1267
- if (options.expected_duration) {
1268
- this.response.setHeader('Expected-Duration', Number(options.expected_duration / 1000).toFixed(2));
1269
- }
1270
-
1271
- // Write the headers & 202 status
1272
- this.response.writeHead(202);
1273
-
1274
- // End the response
1275
- this.response.end('The response has been postponed, you can find it at <a href="' + url + '">' + url + '</a>');
1276
-
1277
- // Nullify the response
1278
- this.response = null;
1279
-
1280
- // Set the original url
1281
- this.overrideResponseUrl(this.url)
1282
- });
1283
-
1284
- /**
1285
- * Set the response url
1286
- *
1287
- * @author Jelle De Loecker <jelle@develry.be>
1288
- * @since 1.2.5
1289
- * @version 1.2.5
1290
- *
1291
- * @param {String|RURL} url
1292
- */
1293
- Conduit.setMethod(function overrideResponseUrl(url) {
1294
-
1295
- if (typeof url != 'string') {
1296
- url = String(url);
1297
- }
1298
-
1299
- this.setHeader('x-history-url', url);
1300
- this.expose('redirected_to', url);
1301
- });
1302
-
1303
- /**
1304
- * Redirect to another url
1305
- *
1306
- * @author Jelle De Loecker <jelle@develry.be>
1307
- * @since 0.2.0
1308
- * @version 1.2.5
1309
- *
1310
- * @param {Number} status 3xx redirection codes. 302 (temporary redirect) by default
1311
- * @param {String|Object} options Options or url
1312
- */
1313
- Conduit.setMethod(function redirect(status, options) {
1314
-
1315
- var hard_refresh = false,
1316
- url;
1317
-
1318
- if (typeof status != 'number') {
1319
-
1320
- if (typeof options == 'object') {
1321
- options.url = status;
1322
- } else {
1323
- options = status;
1324
- }
1325
-
1326
- status = 302;
1327
- }
1328
-
1329
- if (typeof options == 'object' && options) {
1330
-
1331
- if (options.href || options.path) {
1332
- url = options.href || options.path;
1333
- } else {
1334
-
1335
- if (options.body) {
1336
- Object.defineProperty(this, 'body', {
1337
- value : options.body,
1338
- configurable : true
1339
- });
1340
- }
1341
-
1342
- if (options.method) {
1343
- this.method = options.method;
1344
- }
1345
-
1346
- // When headers are given, the redirect is internal
1347
- if (options.headers) {
1348
- this.headers = options.headers;
1349
-
1350
- this.oldoriginal_path = this.original_path;
1351
-
1352
- if (typeof options.url == 'string') {
1353
- let temp = options.url;
1354
- temp = RURL.parse(temp);
1355
- url = temp.path;
1356
- } else {
1357
- url = options.url.path;
1358
- }
1359
-
1360
- if (url == null) {
1361
- throw new Error('Conduit#redirect can not redirect to null path');
1362
- }
1363
-
1364
- // Register the new url as the one to use for the history
1365
- this.overrideResponseUrl(url);
1366
-
1367
- this.original_path = url;
1368
-
1369
- // Reinitialize the conduit
1370
- this.initValues();
1371
- this.initHttp();
1372
-
1373
- return;
1374
- } else {
1375
- url = options.url;
1376
- }
1377
- }
1378
-
1379
- if (options.hard_refresh) {
1380
- hard_refresh = options.hard_refresh;
1381
- }
1382
-
1383
- } else if (typeof options == 'string') {
1384
- url = options;
1385
- options = null;
1386
- } else {
1387
- throw new Error('Conduit#redirect requires a valid url or options object');
1388
- }
1389
-
1390
- this.status = status;
1391
-
1392
- if (hard_refresh && this.headers['x-hawkejs-request']) {
1393
- this.setHeader('x-hawkejs-navigate', url);
1394
-
1395
- if (options && options.popup) {
1396
- this.setHeader('x-hawkejs-popup', options.popup);
1397
- }
1398
-
1399
- } else {
1400
- this.setHeader('Location', url);
1401
- }
1402
-
1403
- this._end();
1404
- });
1405
-
1406
- /**
1407
- * Respond with an error
1408
- *
1409
- * @author Jelle De Loecker <jelle@develry.be>
1410
- * @since 0.2.0
1411
- * @version 1.1.0
1412
- *
1413
- * @param {Nulber} status Response statuscode
1414
- * @param {Error} message Optional error to send
1415
- * @param {Boolean} printError Print the error, defaults to true
1416
- */
1417
- Conduit.setMethod(function error(status, message, printError) {
1418
-
1419
- if (status instanceof Classes.Alchemy.Error.HTTP) {
1420
- message = status;
1421
- status = message.status;
1422
- }
1423
-
1424
- if (typeof status !== 'number') {
1425
- message = status;
1426
- status = 500;
1427
- }
1428
-
1429
- if (!message) {
1430
- message = 'Unknown server error';
1431
- }
1432
-
1433
- if (typeof message == 'string') {
1434
- let error = new Classes.Alchemy.Error.HTTP(status, message);
1435
- error[Symbol.for('extra_skip_levels')] = 1;
1436
- message = error;
1437
- }
1438
-
1439
- let subject = 'Error found on ' + this.original_path + '';
1440
-
1441
- if (printError === false) {
1442
- log.error(subject + ':\n' + message);
1443
- } else if (message instanceof Error) {
1444
- alchemy.printLog('error', [subject, String(message), message], {err: message, level: -2});
1445
- } else {
1446
- log.error(subject + ':\n' + message);
1447
- }
1448
-
1449
- // Make sure the client doesn't expect compression
1450
- this.setHeader('content-encoding', '');
1451
-
1452
- this.status = status;
1453
-
1454
- // Only render an expensive "Error" template when the client directly
1455
- // browses to an HTML page.
1456
- // Don't render a template for AJAX or asset requests
1457
- if (this.renderer && (this.ajax || (this.headers.accept && this.headers.accept.indexOf('html') > -1))) {
1458
-
1459
- // Hawkejs will have the option to throw the error OR render the error
1460
- if (this.ajax) {
1461
- this.end({
1462
- error : true,
1463
- status : status,
1464
- message : message,
1465
- error_templates : ['error/' + status, 'error/unknown'],
1466
- });
1467
- } else {
1468
- this.set('status', status);
1469
- this.set('message', message);
1470
- this.render(['error/' + status, 'error/unknown']);
1471
- }
1472
- } else {
1473
- // Requests for images or scripts just get a non-expensive string response
1474
- this.setHeader('content-type', 'text/plain');
1475
- this._end(status + ':\n' + message + '\n');
1476
- }
1477
-
1478
- // Emit this as a conduit error
1479
- alchemy.emit('conduit_error', this, status, message);
1480
- });
1481
-
1482
- /**
1483
- * Deny access
1484
- *
1485
- * @author Jelle De Loecker <jelle@develry.be>
1486
- * @since 0.2.0
1487
- * @version 1.1.0
1488
- *
1489
- * @param {Number} status
1490
- * @param {Error} message optional error to send
1491
- */
1492
- Conduit.setMethod(function deny(status, message) {
1493
-
1494
- if (typeof status == 'string') {
1495
- message = status;
1496
- status = 403;
1497
- } else if (status instanceof Classes.Alchemy.Error.HTTP) {
1498
- return this.error(status);
1499
- }
1500
-
1501
- if (message == null) {
1502
- message = 'Forbidden';
1503
- }
1504
-
1505
- this.error(status, message);
1506
- });
1507
-
1508
- /**
1509
- * The current user is not authorized and needs to log in
1510
- * (Default implementation, is overriden by the acl plugin)
1511
- *
1512
- * @author Jelle De Loecker <jelle@develry.be>
1513
- * @since 1.0.7
1514
- * @version 1.0.7
1515
- *
1516
- * @param {Boolean} tried_auth Indicate that this was an auth attempt
1517
- */
1518
- Conduit.setMethod(function notAuthorized(tried_auth) {
1519
- return this.deny();
1520
- });
1521
-
1522
- /**
1523
- * The current user is authenticated, but not allowed
1524
- * (Default implementation, is overriden by the acl plugin)
1525
- *
1526
- * @author Jelle De Loecker <jelle@kipdola.be>
1527
- * @since 1.0.7
1528
- * @version 1.1.0
1529
- */
1530
- Conduit.setMethod(function forbidden() {
1531
-
1532
- let error = new Classes.Alchemy.Error.HTTP(403, 'Forbidden');
1533
- error.skipTraceLines(1);
1534
-
1535
- return this.deny(error);
1536
- });
1537
-
1538
- /**
1539
- * Respond with a not found error status
1540
- *
1541
- * @author Jelle De Loecker <jelle@develry.be>
1542
- * @since 0.2.0
1543
- * @version 1.1.0
1544
- *
1545
- * @param {Error} message optional error to send
1546
- */
1547
- Conduit.setMethod(async function notFound(message) {
1548
-
1549
- // Look for other paths
1550
- if (!this.route_not_found && this.route && !this.route_rematch) {
1551
-
1552
- // Try matching against paths after the ones we currently matched
1553
- await this.parseRoute(this.route);
1554
-
1555
- // Call the handler of that route if it has been found
1556
- if (this.route) {
1557
- return this.route.callHandler(this);
1558
- }
1559
- }
1560
-
1561
- if (message == null) {
1562
- message = 'Not found';
1563
- }
1564
-
1565
- this.error(404, message, false);
1566
- });
1567
-
1568
- /**
1569
- * Respond with a "Not Modified" 304 status
1570
- *
1571
- * @author Jelle De Loecker <jelle@develry.be>
1572
- * @since 0.2.0
1573
- * @version 0.4.0
1574
- */
1575
- Conduit.setMethod(function notModified() {
1576
- this.status = 304;
1577
- this._end();
1578
- });
1579
-
1580
- /**
1581
- * Respond with text. Objects get JSON-dry encoded
1582
- *
1583
- * @author Jelle De Loecker <jelle@develry.be>
1584
- * @since 0.2.0
1585
- * @version 1.1.0
1586
- *
1587
- * @param {String|Object} message
1588
- */
1589
- Conduit.setMethod(function end(message) {
1590
-
1591
- var that = this,
1592
- json_type,
1593
- json_fnc,
1594
- cache,
1595
- etag,
1596
- temp;
1597
-
1598
- if (this.websocket) {
1599
- throw new Error('You can not end a websocket, use the callback instead');
1600
- }
1601
-
1602
- if (this.method == 'head') {
1603
- return this._end();
1604
- }
1605
-
1606
- if (typeof message !== 'string') {
1607
-
1608
- // Use regular JSON if DRY has been disabled in settings
1609
- if (alchemy.settings.json_dry_response === false || this.json_dry === false) {
1610
- json_type = 'json';
1611
- json_fnc = JSON.stringify;
1612
- } else {
1613
- json_type = 'json-dry';
1614
- json_fnc = JSON.dry;
1615
-
1616
- // Clone the object
1617
- message = JSON.clone(message, 'toHawkejs');
1618
- }
1619
-
1620
- // Only send the mimetype if it hasn't been set yet
1621
- if (this.setHeader('content-type') == null) {
1622
- this.setHeader('content-type', "application/" + json_type + ";charset=utf-8");
1623
- }
1624
-
1625
- message = json_fnc(message) || 'null';
1626
- }
1627
-
1628
- cache = this.headers['cache-control'] || this.headers['pragma'];
1629
-
1630
- // Only generate etags when caching is enabled locally & on the browser
1631
- if (alchemy.settings.cache !== false && (cache == null || cache != 'no-cache')) {
1632
-
1633
- // Calculate the hash as etag
1634
- etag = alchemy.checksum(message);
1635
-
1636
- if (etag != null) {
1637
-
1638
- if (this.headers['if-none-match'] == etag) {
1639
- return this.notModified();
1640
- }
1641
-
1642
- // Responses through `end` should always be privately cached
1643
- this.setHeader('cache-control', 'private');
1644
-
1645
- // Send the hash as a response header
1646
- this.setHeader('etag', etag);
1647
- }
1648
- }
1649
-
1650
- // No need to replace anything if debugging is disabled or the log is empty
1651
- if (alchemy.settings.debug && this.debuglog && this.debuglog.length && message.indexOf('_placeholder_') > -1) {
1652
- temp = JSON.dry(this.debuglog);
1653
- message = message.replace(/{\s*"_placeholder_":\s*"debuglog"\s*}/g, temp);
1654
- message = message.replace(/{\s*\\"_placeholder_\\":\s*\\"debuglog\\"\s*}/g, JSON.stringify(temp).slice(1,-1));
1655
- }
1656
-
1657
- // Compress the output if the client accepts it,
1658
- // but only if the file is at least 150 bytes
1659
- if (alchemy.settings.compression && message.length > 150 && this.accepts('gzip')) {
1660
-
1661
- // Set the decompressed content-length for use in progress bars
1662
- this.setHeader('x-decompressed-content-length', Buffer.byteLength(message));
1663
-
1664
- // Set the gzip header
1665
- this.setHeader('content-encoding', 'gzip');
1666
-
1667
- // Make sure proxy servers only cache this under this content-encoding type
1668
- this.setHeader('vary', 'accept-encoding');
1669
-
1670
- zlib.gzip(message, function gotZipped(err, zipped) {
1671
- that._end(zipped, 'utf-8');
1672
- });
1673
-
1674
- return;
1675
- }
1676
-
1677
- this._end(message, 'utf-8');
1678
- });
1679
-
1680
- /**
1681
- * Call the actual end method
1682
- *
1683
- * @author Jelle De Loecker <jelle@develry.be>
1684
- * @since 0.2.0
1685
- * @version 1.1.0
1686
- */
1687
- Conduit.setMethod(function _end(message, encoding = 'utf-8') {
1688
-
1689
- this.ended = new Date();
1690
-
1691
- if (!this.response) {
1692
- let args = [];
1693
-
1694
- if (arguments.length) {
1695
- args.push(message);
1696
- args.push(encoding);
1697
- }
1698
-
1699
- this._end_arguments = args;
1700
-
1701
- return;
1702
- }
1703
-
1704
- let headers = [],
1705
- value,
1706
- key;
1707
-
1708
- if (this.status) {
1709
- this.response.statusCode = this.status;
1710
- }
1711
-
1712
- // Set the content-length if it hasn't been set yet
1713
- if (arguments.length > 0 && !this.response_headers['content-length']) {
1714
- this.response_headers['content-length'] = Buffer.byteLength(message);
1715
- }
1716
-
1717
- for (key in this.response_headers) {
1718
- value = this.response_headers[key];
1719
- this.response.setHeader(key, value);
1720
- }
1721
-
1722
- if (this.new_cookie_header.length) {
1723
- this.response.setHeader('set-cookie', this.new_cookie_header);
1724
- }
1725
-
1726
- // Write the actual headers
1727
- this.response.writeHead(this.status);
1728
-
1729
- if (arguments.length === 0) {
1730
- return this.response.end();
1731
- }
1732
-
1733
- // End the response
1734
- return this.response.end(message, encoding);
1735
- });
1736
-
1737
- /**
1738
- * Send a response to the client
1739
- *
1740
- * @author Jelle De Loecker <jelle@develry.be>
1741
- * @since 0.2.0
1742
- * @version 0.2.0
1743
- */
1744
- Conduit.setMethod(function send(content) {
1745
- return this.end(content);
1746
- });
1747
-
1748
- /**
1749
- * Create the scene id (if it doesn't exist already)
1750
- * We do this using cookies, so the HTML response can be cached
1751
- *
1752
- * @author Jelle De Loecker <jelle@develry.be>
1753
- * @since 0.2.0
1754
- * @version 1.1.0
1755
- */
1756
- Conduit.setMethod(function createScene() {
1757
- return this.scene_id;
1758
- });
1759
-
1760
- /**
1761
- * Render a view and send it to the client
1762
- *
1763
- * @author Jelle De Loecker <jelle@develry.be>
1764
- * @since 0.2.0
1765
- * @version 1.1.0
1766
- */
1767
- Conduit.setMethod(function render(template_name, options, callback) {
1768
-
1769
- var that = this,
1770
- templates;
1771
-
1772
- if (typeof options == 'function') {
1773
- callback = options;
1774
- options = {};
1775
- } else if (options == null) {
1776
- options = {};
1777
- }
1778
-
1779
- if (template_name) {
1780
- templates = [template_name];
1781
- }
1782
-
1783
- if (templates) {
1784
- templates.push('error/404');
1785
- }
1786
-
1787
- // Expose the useragent info to the hawkejs renderer
1788
- this.internal('useragent', this.useragent);
1789
-
1790
- this.createScene();
1791
-
1792
- // Pass along the clientRender property,
1793
- // can be used to force rendering HTML
1794
- if (options.clientRender != null) {
1795
- this.renderer.clientRender = options.clientRender;
1796
- }
1797
-
1798
- if (this.layout) {
1799
- this.renderer.layout = this.layout;
1800
- }
1801
-
1802
- this.renderer.renderHTML(templates).done(function afterRender(err, html) {
1803
-
1804
- var mimetype;
1805
-
1806
- if (err != null) {
1807
-
1808
- if (callback) {
1809
- return callback(err);
1810
- }
1811
-
1812
- throw err;
1813
- }
1814
-
1815
- that.registerBindings();
1816
-
1817
- if (typeof html !== 'string') {
1818
-
1819
- // Stringify using json-dry
1820
- html = JSON.dry(html);
1821
-
1822
- // Tell the client to expect a json-dry response
1823
- mimetype = 'application/json-dry';
1824
- } else {
1825
- mimetype = 'text/html';
1826
- }
1827
-
1828
- // Only send the mimetype if it hasn't been set yet
1829
- if (that.setHeader('content-type') == null) {
1830
- that.setHeader('content-type', mimetype + ";charset=utf-8");
1831
- }
1832
-
1833
- if (callback) {
1834
- return callback(null, html);
1835
- }
1836
-
1837
- that.end(html);
1838
- });
1839
- });
1840
-
1841
- /**
1842
- * Convert a buffer into a stream
1843
- *
1844
- * @author Jelle De Loecker <jelle@develry.be>
1845
- * @since 1.1.0
1846
- * @version 1.1.0
1847
- *
1848
- * @param {Buffer} buffer
1849
- *
1850
- * @return {Readable}
1851
- */
1852
- function bufferToStream(buffer) {
1853
-
1854
- const readable_stream = new libstream.Readable({
1855
- read() {
1856
- this.push(buffer);
1857
- this.push(null);
1858
- }
1859
- });
1860
-
1861
- return readable_stream;
1862
- }
1863
-
1864
- /**
1865
- * Send a file to the browser.
1866
- * Uses cache-control by default.
1867
- *
1868
- * @author Jelle De Loecker <jelle@develry.be>
1869
- * @since 0.2.0
1870
- * @version 1.1.0
1871
- *
1872
- * @param {String} path The path on the server to send to the browser
1873
- * @param {Object} options Options, including headers
1874
- */
1875
- Conduit.setMethod(function serveFile(path, options) {
1876
-
1877
- var that = this,
1878
- tasks = [],
1879
- stats,
1880
- isStream;
1881
-
1882
- // Create an options object if it doesn't exist yet
1883
- if (options == null) {
1884
- options = {};
1885
- }
1886
-
1887
- // Error handling function
1888
- if (!options.onError) {
1889
- options.onError = function onError(err) {
1890
- that.notFound(err);
1891
- };
1892
- }
1893
-
1894
- // See if we have a stats object
1895
- if (Object.isObject(path)) {
1896
-
1897
- if (Buffer.isBuffer(path)) {
1898
- path = bufferToStream(path);
1899
- }
1900
-
1901
- if (path.readable) {
1902
- isStream = true;
1903
- stats = {
1904
- mimetype: 'application/octet-stream'
1905
- };
1906
- } else {
1907
- stats = path;
1908
- }
1909
- } else {
1910
- stats = fileCache[path];
1911
-
1912
- if (stats == null) {
1913
- stats = {
1914
- path: path
1915
- };
1916
- }
1917
- }
1918
-
1919
- // Don't check for file information when it's a stream
1920
- if (!isStream) {
1921
-
1922
- if (!stats.path) {
1923
- return options.onError(new Error('No file to serve'));
1924
- }
1925
-
1926
- // Make sure the stats object is in the cache
1927
- if (fileCache[stats.path] == null) {
1928
- fileCache[stats.path] = stats;
1929
- }
1930
-
1931
- // Get file stats if it isn't available yet
1932
- if (stats.mtime == null) {
1933
- tasks.push(function getFileStats(next) {
1934
-
1935
- fs.stat(stats.path, function gotStats(err, fileStats) {
1936
-
1937
- if (err) {
1938
- stats.err = err;
1939
- stats.mtime = new Date();
1940
- } else {
1941
- Object.assign(stats, fileStats);
1942
- }
1943
-
1944
- next();
1945
- });
1946
- });
1947
- }
1948
-
1949
- // Get the mimetype if it isn't available yet
1950
- if (!options.mimetype && stats.mimetype == null) {
1951
- tasks.push(function getMimetype(next) {
1952
-
1953
- // Don't use libmime if it isn't loaded,
1954
- // that could be the case on NW.js
1955
- if (!libmime) {
1956
- return next();
1957
- }
1958
-
1959
- // Lookup the mimetype by the extension alone
1960
- stats.mimetype = libmime.getType(stats.path);
1961
-
1962
- // Return the result if a valid mimetype was found
1963
- if (stats.mimetype !== 'application/octet-stream') {
1964
- return next();
1965
- }
1966
-
1967
- // If no mimetype was found,
1968
- // see if we can find it using the original path (for resized images)
1969
- if (options.original_path) {
1970
- stats.mimetype = libmime.getType(options.original_path);
1971
-
1972
- if (stats.mimetype !== 'application/octet-stream') {
1973
- return next();
1974
- }
1975
- }
1976
-
1977
- // "magic" currently doesn't work in nw.js
1978
- if (Blast.isNW) {
1979
- return next();
1980
- }
1981
-
1982
- // Don't try to use magic if it's not loaded
1983
- if (!getMagic()) {
1984
- return next();
1985
- }
1986
-
1987
- // Look inside the data (using "magic") for a better mimetype
1988
- magic.detectFile(stats.path, function detectedMimetype(err, result) {
1989
-
1990
- if (!err) {
1991
- stats.mimetype = result;
1992
- }
1993
-
1994
- next();
1995
- });
1996
- });
1997
- }
1998
- }
1999
-
2000
- Function.parallel(tasks, function gotFileInfo(err) {
2001
-
2002
- var disposition,
2003
- outStream,
2004
- mimetype,
2005
- headers,
2006
- isText,
2007
- since,
2008
- key;
2009
-
2010
- if (err) {
2011
- return that.error(err);
2012
- }
2013
-
2014
- if (stats.err) {
2015
- return options.onError(stats.err);
2016
- }
2017
-
2018
- if (!isStream && !stats.path) {
2019
- return options.onError(new Error('File not found'));
2020
- }
2021
-
2022
- // Check the if-modified-since header if it's supplied
2023
- if (alchemy.settings.cache !== false && that.headers['if-modified-since'] != null) {
2024
-
2025
- // Turn the string into a date
2026
- since = new Date(that.headers['if-modified-since']);
2027
-
2028
- // If the file's modifytime is smaller or equal to the since time,
2029
- // don't serve the contents!
2030
- if (stats.mtime <= since) {
2031
- return that.notModified();
2032
- }
2033
- }
2034
-
2035
- mimetype = stats.mimetype;
2036
-
2037
- // If we get a general mimetype, and an alternative is provided, use that one
2038
- if (!mimetype || mimetype === 'application/octet-stream') {
2039
- if (options.mimetype != null) {
2040
- mimetype = options.mimetype;
2041
- }
2042
- }
2043
-
2044
- isText = /svg|xml|javascript|text/.test(mimetype);
2045
-
2046
- // Serve text files as utf-8
2047
- if (isText) {
2048
- mimetype += '; charset=utf-8';
2049
- }
2050
-
2051
- that.setHeader('content-type', mimetype);
2052
-
2053
- // Setting the disposition makes the browser download the file
2054
- // This is on by default, but can be disabled
2055
- if (options.disposition == 'inline') {
2056
- disposition = 'inline';
2057
-
2058
- if (options.filename) {
2059
- disposition += '; filename=' + JSON.stringify(options.filename)
2060
- }
2061
-
2062
- that.setHeader('content-disposition', disposition);
2063
- } else if (options.disposition !== false) {
2064
- if (options.filename) {
2065
- disposition = 'attachment; filename=' + JSON.stringify(options.filename);
2066
- } else {
2067
- disposition = 'attachment';
2068
- }
2069
-
2070
- that.setHeader('content-disposition', disposition);
2071
- }
2072
-
2073
- if (stats.mtime && alchemy.settings.cache) {
2074
- // Allow the browser to cache this for 60 minutes,
2075
- // after which it has to revalidate the content
2076
- // by seeing if it has been modified
2077
- that.setHeader('cache-control', 'public, max-age=3600, must-revalidate');
2078
- that.setHeader('last-modified', stats.mtime.toGMTString());
2079
- } else if (!alchemy.settings.cache) {
2080
- that.setHeader('cache-control', 'no-cache');
2081
- }
2082
-
2083
- for (key in options.headers) {
2084
- that.setHeader(key, options.headers[key]);
2085
- }
2086
-
2087
- // End now if it's just a HEAD request
2088
- if (that.method == 'head') {
2089
- return that.end();
2090
- }
2091
-
2092
- if (isStream) {
2093
- outStream = path;
2094
- } else {
2095
- outStream = fs.createReadStream(path, {bufferSize: 64*1024});
2096
-
2097
- // Listen for file errors
2098
- outStream.on('error', options.onError);
2099
- }
2100
-
2101
- // Compress text responses
2102
- if (isText && alchemy.settings.compression && that.accepts('gzip')) {
2103
-
2104
- // Set the gzip header
2105
- that.setHeader('content-encoding', 'gzip');
2106
- that.setHeader('vary', 'accept-encoding');
2107
-
2108
- // Create the gzip stream
2109
- outStream = outStream.pipe(zlib.createGzip());
2110
- }
2111
-
2112
- // If we received a stream as parameter...
2113
- if (isStream) {
2114
- that.response.on('end', cleanup);
2115
- that.response.on('finish', cleanup);
2116
- that.response.on('error', cleanup);
2117
- that.response.on('close', cleanup);
2118
- }
2119
-
2120
- function cleanup() {
2121
-
2122
- // Remove all pipes
2123
- outStream.unpipe();
2124
-
2125
- if (outStream.destroy) {
2126
- outStream.destroy();
2127
- } else if (outStream.end) {
2128
- outStream.end();
2129
- }
2130
- }
2131
-
2132
- // Send the headers
2133
- for (key in that.response_headers) {
2134
- that.response.setHeader(key, that.response_headers[key]);
2135
- }
2136
-
2137
- if (that.new_cookie_header.length) {
2138
- that.response.setHeader('set-cookie', that.new_cookie_header);
2139
- }
2140
-
2141
- that.response.statusCode = 200;
2142
-
2143
- // Stream the file to the client
2144
- outStream.pipe(that.response);
2145
- });
2146
- });
2147
-
2148
- /**
2149
- * Create a session
2150
- *
2151
- * @author Jelle De Loecker <jelle@develry.be>
2152
- * @since 0.2.0
2153
- * @version 1.1.0
2154
- *
2155
- * @param {Boolean} create Create a session if none exist
2156
- *
2157
- * @return {UserSession}
2158
- */
2159
- Conduit.setMethod(function getSession(allow_create = true) {
2160
-
2161
- var cookie_name,
2162
- fingerprint,
2163
- session_id,
2164
- session;
2165
-
2166
- // Only do this once per request
2167
- if (this.sessionData != null) {
2168
- return this.sessionData;
2169
- }
2170
-
2171
- // Set the name of the cookie (could change in the future)
2172
- cookie_name = alchemy.settings.session_key || 'alchemy_sid';
2173
-
2174
- // Get the ID of the session
2175
- session_id = this.cookie(cookie_name);
2176
-
2177
- if (session_id) {
2178
- // Get the session
2179
- session = alchemy.sessions.get(session_id);
2180
- }
2181
-
2182
- // If no session is found, see if we can find one
2183
- // based on the browser fingerprint
2184
- if (!session && this.ip) {
2185
- fingerprint = this.fingerprint;
2186
-
2187
- if (fingerprint) {
2188
- session = alchemy.fingerprints.get(fingerprint);
2189
-
2190
- if (session && session.id) {
2191
- session_id = session.id;
2192
- this.cookie(cookie_name, session_id, {httpOnly: true});
2193
- }
2194
- }
2195
- }
2196
-
2197
- // If no valid session exists, create a new one
2198
- if (!session && allow_create) {
2199
- session = new Classes.Alchemy.ClientSession(this);
2200
- session_id = session.id;
2201
-
2202
- if (fingerprint) {
2203
- alchemy.fingerprints.set(fingerprint, session);
2204
- }
2205
-
2206
- this.cookie(cookie_name, session_id, {httpOnly: true});
2207
-
2208
- alchemy.sessions.set(session_id, session);
2209
- }
2210
-
2211
- if (session) {
2212
- this.sessionData = session;
2213
- session.request_count++;
2214
- } else {
2215
- return false;
2216
- }
2217
-
2218
- return session;
2219
- });
2220
-
2221
- /**
2222
- * Register live data bindings via websockets
2223
- *
2224
- * @author Jelle De Loecker <jelle@develry.be>
2225
- * @since 0.2.0
2226
- * @version 0.4.0
2227
- */
2228
- Conduit.setMethod(function registerBindings(arr) {
2229
-
2230
- var data_ids;
2231
-
2232
- // Don't do anything is websockets aren't enabled
2233
- if (!alchemy.settings.websockets) {
2234
- return;
2235
- }
2236
-
2237
- if (arr) {
2238
- data_ids = arr;
2239
- } else {
2240
- data_ids = this.renderer.live_bindings;
2241
- }
2242
-
2243
- if (Object.isEmpty(data_ids)) {
2244
- return;
2245
- }
2246
-
2247
- this.getSession().registerBindings(data_ids, this.sceneId);
2248
- });
2249
-
2250
- /**
2251
- * Get a a value from the session object
2252
- *
2253
- * @author Jelle De Loecker <jelle@develry.be>
2254
- * @since 0.2.0
2255
- * @version 0.4.0
2256
- *
2257
- * @param {String} name
2258
- * @param {Mixed} value
2259
- *
2260
- * @return {Mixed}
2261
- */
2262
- Conduit.setMethod(function session(name, value) {
2263
-
2264
- this.getSession();
2265
-
2266
- if (arguments.length === 0) {
2267
- return this.sessionData;
2268
- }
2269
-
2270
- if (arguments.length === 1) {
2271
- return this.sessionData[name];
2272
- }
2273
-
2274
- this.sessionData[name] = value;
2275
- });
2276
-
2277
- /**
2278
- * Get a parameter from the route
2279
- *
2280
- * @author Jelle De Loecker <jelle@develry.be>
2281
- * @since 0.2.0
2282
- * @version 0.2.0
2283
- *
2284
- * @param {String} name
2285
- */
2286
- Conduit.setMethod(function routeParam(name) {
2287
- return this.params[name];
2288
- });
2289
-
2290
- /**
2291
- * Get/set a cookie
2292
- *
2293
- * @author Jelle De Loecker <jelle@develry.be>
2294
- * @since 0.2.0
2295
- * @version 0.4.2
2296
- *
2297
- * @param {String} name
2298
- * @param {Mixed} value
2299
- * @param {Object} options
2300
- */
2301
- Conduit.setMethod(function cookie(name, value, options) {
2302
-
2303
- var header,
2304
- arr,
2305
- key;
2306
-
2307
- // Return if cookies are disabled
2308
- if (!alchemy.settings.cookies) {
2309
- return;
2310
- }
2311
-
2312
- if (arguments.length == 1) {
2313
- return this.new_cookies[name] || this.cookies[name];
2314
- }
2315
-
2316
- if (options == null) options = {};
2317
-
2318
- // If the value is null or undefined, the cookie should be removed
2319
- if (value == null) {
2320
- options.expires = new Date(0);
2321
- }
2322
-
2323
- // If no path is given, default to the root path
2324
- if (options.path == null) options.path = '/';
2325
-
2326
- // If the `secure` flag is not set,
2327
- // see if this connection is secure
2328
- if (options.secure == null) {
2329
- if (this.is_secure) {
2330
- options.secure = true;
2331
- }
2332
- }
2333
-
2334
- // Store it in the new_cookies object, for quick access
2335
- this.new_cookies[name] = value;
2336
-
2337
- if (this.websocket) {
2338
- return this.socket.emit('alchemy-set-cookie', {name: name, value: value, options: options});
2339
- }
2340
-
2341
- // Create the basic header string
2342
- header = String.encodeCookie(name, value, options);
2343
-
2344
- // Add this to the cookieheader array
2345
- this.new_cookie_header.push(header);
2346
- });
2347
-
2348
- /**
2349
- * Set a response header
2350
- *
2351
- * @author Jelle De Loecker <jelle@develry.be>
2352
- * @since 0.2.0
2353
- * @version 1.1.0
2354
- *
2355
- * @param {String} name
2356
- * @param {Mixed} value
2357
- */
2358
- Conduit.setMethod(function setHeader(name, value) {
2359
-
2360
- if (arguments.length == 1) {
2361
- return this.getHeader(name);
2362
- }
2363
-
2364
- if (this.websocket) {
2365
- throw new Error("Can't set a header on a websocket connection");
2366
- }
2367
-
2368
- this.response_headers[name] = value;
2369
- });
2370
-
2371
- /**
2372
- * Get a response header
2373
- *
2374
- * @author Jelle De Loecker <jelle@develry.be>
2375
- * @since 1.1.0
2376
- * @version 1.1.0
2377
- *
2378
- * @param {String} name
2379
- */
2380
- Conduit.setMethod(function getHeader(name) {
2381
-
2382
- if (this.response_headers[name] != null) {
2383
- return this.response_headers[name];
2384
- }
2385
-
2386
- if (this.response) {
2387
- return this.response.getHeader(name);
2388
- }
2389
- });
2390
-
2391
- /**
2392
- * Update data to this scene only
2393
- *
2394
- * @author Jelle De Loecker <jelle@develry.be>
2395
- * @since 0.2.0
2396
- * @version 0.4.0
2397
- *
2398
- * @param {String} name
2399
- * @param {Mixed} value
2400
- */
2401
- Conduit.setMethod(function update(name, value) {
2402
-
2403
- // Make sure a scene id is created
2404
- this.createScene();
2405
-
2406
- // Send this update to this scene only
2407
- this.getSession().sendDataUpdate(name, value, this.sceneId);
2408
- });
2409
-
2410
- /**
2411
- * Push a flash message to the client
2412
- *
2413
- * @author Jelle De Loecker <jelle@develry.be>
2414
- * @since 0.2.0
2415
- * @version 0.2.0
2416
- */
2417
- Conduit.setMethod(function flash(message, options) {
2418
-
2419
- var newFlashes;
2420
-
2421
- if (options == null) {
2422
- options = {};
2423
- }
2424
-
2425
- newFlashes = this.internal('newFlashes');
2426
-
2427
- if (newFlashes == null) {
2428
- newFlashes = {};
2429
- }
2430
-
2431
- newFlashes[Date.now() + '-' + Number.random(100)] = {
2432
- message: message,
2433
- options: options
2434
- };
2435
-
2436
- this.internal('newFlashes', newFlashes);
2437
- });
2438
-
2439
- /**
2440
- * Set a theme to use
2441
- *
2442
- * @author Jelle De Loecker <jelle@develry.be>
2443
- * @since 0.2.0
2444
- * @version 0.2.0
2445
- *
2446
- * @param {String} name
2447
- */
2448
- Conduit.setMethod(function setTheme(name) {
2449
- this.theme = name;
2450
- this.renderer.setTheme(name);
2451
- });
2452
-
2453
- /**
2454
- * Does this user support a certain feature?
2455
- *
2456
- * @author Jelle De Loecker <jelle@develry.be>
2457
- * @since 1.0.4
2458
- * @version 1.0.4
2459
- *
2460
- * @param {String} feature
2461
- *
2462
- * @return {Boolean}
2463
- */
2464
- Conduit.setMethod(function supports(feature) {
2465
-
2466
- if (this.useragent && (feature == 'async' || feature == 'await')) {
2467
- let agent = this.useragent;
2468
-
2469
- if (agent.family == 'IE') {
2470
- return false;
2471
- }
2472
-
2473
- if (agent.family == 'Edge' && agent.major < 15) {
2474
- return false;
2475
- }
2476
-
2477
- // Its actually supported on 10.1, but oh well
2478
- if (agent.family == 'Safari' && agent.major < 11) {
2479
- return false;
2480
- }
2481
-
2482
- if (agent.family == 'Samsung Internet' && agent.major < 6) {
2483
- return false;
2484
- }
2485
-
2486
- if (agent.family == 'Opera Mini') {
2487
- return false;
2488
- }
2489
- }
2490
-
2491
- return null;
2492
- });
2493
-
2494
- /**
2495
- * Broadcast data to every connected user
2496
- *
2497
- * @author Jelle De Loecker <jelle@develry.be>
2498
- * @since 0.3.0
2499
- * @version 0.4.0
2500
- *
2501
- * @param {String} type
2502
- * @param {Object} data
2503
- */
2504
- Alchemy.setMethod(function broadcast(type, data) {
2505
-
2506
- alchemy.sessions.forEach(function eachSession(session, key) {
2507
-
2508
- // Go over every listening scene and submit the data
2509
- Object.each(session.connections, function eachScene(scene, scene_id) {
2510
-
2511
- if (!scene) {
2512
- return;
2513
- }
2514
-
2515
- if (alchemy.settings.debug) {
2516
- log.debug('Broadcasting', type, {data, scene});
2517
- }
2518
-
2519
- scene.submit(type, data);
2520
- });
2521
- });
2522
- });
2523
-
2524
- /**
2525
- * Get the magic mimetype function
2526
- *
2527
- * @author Jelle De Loecker <jelle@develry.be>
2528
- * @since 0.3.0
2529
- * @version 0.3.0
2530
- */
2531
- function getMagic() {
2532
-
2533
- var mmmagic;
2534
-
2535
- if (magic || magic === false) {
2536
- return magic;
2537
- }
2538
-
2539
- // Get mmmagic module
2540
- mmmagic = alchemy.use('mmmagic')
2541
-
2542
- if (mmmagic) {
2543
- magic = new mmmagic.Magic(mmmagic.MAGIC_MIME_TYPE);
2544
- } else {
2545
- log.error('Could not load mmmagic module');
2546
- magic = false;
2547
- }
2548
-
2549
- return magic;
2550
- }
2551
-
2552
- global.Conduit = Conduit;
1
+ var fileCache = alchemy.shared('files.fileCache'),
2
+ libstream = alchemy.use('stream'),
3
+ libpath = alchemy.use('path'),
4
+ libmime = alchemy.use('mime'),
5
+ libua = alchemy.use('useragent'),
6
+ zlib = alchemy.use('zlib'),
7
+ BODY = Symbol('body'),
8
+ TESTED_ROUTES = Symbol('tested_routes'),
9
+ magic,
10
+ fs = alchemy.use('fs'),
11
+ prefixes = alchemy.shared('Routing.prefixes');
12
+
13
+ /**
14
+ * The Conduit Class
15
+ *
16
+ * @author Jelle De Loecker <jelle@develry.be>
17
+ * @since 0.2.0
18
+ * @version 1.2.0
19
+ *
20
+ * @param {IncomingMessage} req
21
+ * @param {ServerResponse} res
22
+ * @param {Router} router
23
+ */
24
+ var Conduit = Function.inherits('Alchemy.Base', 'Alchemy.Conduit', function Conduit(req, res, router) {
25
+
26
+ // Store the starting time
27
+ this.start = new Date();
28
+
29
+ // Create a reference to ourselves
30
+ this.conduit = this;
31
+
32
+ // Debug messages for this request
33
+ this.debuglog = [];
34
+
35
+ this._debugObject = this.debug({label: 'Initialize Conduit'});
36
+ this._debugConduitInitialize = this._debugObject;
37
+
38
+ // Allow use of the log in the views
39
+ if (alchemy.settings.debug) {
40
+ this.internal('debuglog', {_placeholder_: 'debuglog'});
41
+ }
42
+
43
+ // Cookies to send to the client
44
+ this.new_cookies = {};
45
+ this.new_cookie_header = [];
46
+
47
+ // The headers to send
48
+ this.response_headers = {};
49
+
50
+ // Where the body will go
51
+ this.body = {};
52
+
53
+ // Where the files will go
54
+ this.files = {};
55
+
56
+ this.initValues();
57
+ this.setReqRes(req, res);
58
+ });
59
+
60
+ /**
61
+ * Deprecated property names
62
+ *
63
+ * @author Jelle De Loecker <jelle@develry.be>
64
+ * @since 1.0.0
65
+ * @version 1.1.0
66
+ */
67
+ Conduit.setDeprecatedProperty('originalPath', 'original_path');
68
+ Conduit.setDeprecatedProperty('newCookies', 'new_cookies');
69
+ Conduit.setDeprecatedProperty('newCookieHeader', 'new_cookie_header');
70
+ Conduit.setDeprecatedProperty('viewRender', 'renderer');
71
+ Conduit.setDeprecatedProperty('view_render', 'renderer');
72
+ Conduit.setDeprecatedProperty('sceneId', 'scene_id');
73
+
74
+ /**
75
+ * Return the cookies
76
+ *
77
+ * @author Jelle De Loecker <jelle@develry.be>
78
+ * @since 0.2.0
79
+ * @version 0.2.0
80
+ */
81
+ Conduit.prepareProperty(function cookies() {
82
+ return String.decodeCookies(this.headers.cookie);
83
+ });
84
+
85
+ /**
86
+ * Return the parsed useragent string
87
+ *
88
+ * @author Jelle De Loecker <jelle@develry.be>
89
+ * @since 0.2.0
90
+ * @version 0.5.0
91
+ */
92
+ Conduit.prepareProperty(function useragent() {
93
+ return libua.lookup(this.headers['user-agent']);
94
+ });
95
+
96
+ /**
97
+ * Create a Hawkejs Renderer
98
+ *
99
+ * @author Jelle De Loecker <jelle@develry.be>
100
+ * @since 0.2.0
101
+ * @version 1.1.5
102
+ */
103
+ Conduit.prepareProperty(function renderer() {
104
+
105
+ let result;
106
+
107
+ if (this.parent && this.parent != this && this.parent.renderer) {
108
+ result = this.parent.renderer.createSubRenderer();
109
+ } else {
110
+ result = alchemy.hawkejs.createRenderer();
111
+ }
112
+
113
+ return result;
114
+ });
115
+
116
+ /**
117
+ * Enforce the scene_id
118
+ *
119
+ * @author Jelle De Loecker <jelle@develry.be>
120
+ * @since 1.1.0
121
+ * @version 1.1.7
122
+ */
123
+ Conduit.enforceProperty(function scene_id(new_value, old_value) {
124
+
125
+ if (new_value) {
126
+ return new_value;
127
+ }
128
+
129
+ if (this.headers['x-scene-id']) {
130
+ return this.headers['x-scene-id'];
131
+ }
132
+
133
+ // If there also was no old value, create a new scene
134
+ if (old_value == null) {
135
+ // Generate the scene_id
136
+ new_value = Crypto.randomHex(8) || Crypto.pseudoHex(8);
137
+
138
+ // Tell the session this scene can be expected
139
+ this.getSession().expectScene(new_value);
140
+
141
+ let path;
142
+
143
+ if (this.url) {
144
+ path = this.url.path;
145
+ }
146
+
147
+ // Set the sceneid cookie
148
+ this.cookie('scene_start_' + ~~(Math.random()*1000), {
149
+
150
+ // The time this scene has started
151
+ start: Date.now(),
152
+
153
+ // The id of the scene
154
+ id: new_value
155
+ }, {
156
+ // Cookie should only be visible on this path
157
+ path: path,
158
+
159
+ // Cookie should not live for more than 15 seconds
160
+ maxAge: 1000 * 15
161
+ });
162
+
163
+ return new_value;
164
+ }
165
+ });
166
+
167
+ /**
168
+ * Enforce the active_prefix
169
+ *
170
+ * @author Jelle De Loecker <jelle@develry.be>
171
+ * @since 1.1.0
172
+ * @version 1.1.5
173
+ */
174
+ Conduit.enforceProperty(function active_prefix(new_value, old_value) {
175
+
176
+ if (!new_value) {
177
+ this.renderer.language = null;
178
+ return null;
179
+ }
180
+
181
+ if (new_value == old_value) {
182
+ return new_value;
183
+ }
184
+
185
+ // Set the active prefix
186
+ this.internal('active_prefix', new_value);
187
+ this.expose('active_prefix', new_value);
188
+
189
+ if (this.locales[0] != new_value) {
190
+ this.locales.unshift(new_value);
191
+ }
192
+
193
+ // Set the translate options for use in hawkejs
194
+ this.internal('locales', this.locales);
195
+ this.expose('locales', this.locales);
196
+
197
+ let config = Prefix.get(new_value);
198
+
199
+ if (config) {
200
+ this.renderer.language = config.locale;
201
+ }
202
+
203
+ return new_value;
204
+ });
205
+
206
+ /**
207
+ * Get a session object by id
208
+ *
209
+ * @author Jelle De Loecker <jelle@develry.be>
210
+ * @since 0.2.0
211
+ * @version 0.2.0
212
+ */
213
+ Conduit.setStatic(function getSessionById(id) {
214
+ return alchemy.sessions.get(id);
215
+ });
216
+
217
+ /**
218
+ * See if this is a secure connection
219
+ *
220
+ * @author Jelle De Loecker <jelle@develry.be>
221
+ * @since 0.4.2
222
+ * @version 1.0.2
223
+ */
224
+ Conduit.setProperty(function is_secure() {
225
+
226
+ var protocol;
227
+
228
+ if (alchemy.settings.assume_https) {
229
+ return true;
230
+ }
231
+
232
+ if (this.headers && this.headers['x-forwarded-proto'] == 'https') {
233
+ return true;
234
+ }
235
+
236
+ if (this.url && this.url.protocol == 'https:') {
237
+ return true;
238
+ }
239
+
240
+ if (this.protocol && this.protocol.startsWith('https')) {
241
+ return true;
242
+ }
243
+
244
+ if (this.encrypted == true) {
245
+ return true;
246
+ }
247
+
248
+ return false;
249
+ });
250
+
251
+ /**
252
+ * Set the request body
253
+ *
254
+ * @author Jelle De Loecker <jelle@develry.be>
255
+ * @since 1.1.0
256
+ * @version 1.1.0
257
+ *
258
+ * @param {Object}
259
+ */
260
+ Conduit.setMethod(function setRequestBody(body) {
261
+
262
+ if (!body) {
263
+ return;
264
+ }
265
+
266
+ Object.assign(this.body, body);
267
+ });
268
+
269
+ /**
270
+ * Has the given route been tested yet?
271
+ *
272
+ * @author Jelle De Loecker <jelle@elevenways.be>
273
+ * @since 1.2.5
274
+ * @version 1.2.5
275
+ *
276
+ * @param {Route}
277
+ */
278
+ Conduit.setMethod(function hasRouteBeenTested(route) {
279
+
280
+ if (!route || !this[TESTED_ROUTES]) {
281
+ return false;
282
+ }
283
+
284
+ return this[TESTED_ROUTES].has(route);
285
+ });
286
+
287
+ /**
288
+ * Mark this route as having been tested
289
+ *
290
+ * @author Jelle De Loecker <jelle@elevenways.be>
291
+ * @since 1.2.5
292
+ * @version 1.2.5
293
+ *
294
+ * @param {Route}
295
+ */
296
+ Conduit.setMethod(function markRouteAsTested(route) {
297
+
298
+ if (!this[TESTED_ROUTES]) {
299
+ this[TESTED_ROUTES] = new Set();
300
+ }
301
+
302
+ this[TESTED_ROUTES].add(route);
303
+ });
304
+
305
+ /**
306
+ * Rewrite a certain URL parameter
307
+ * (Causing some kind of redirect)
308
+ *
309
+ * @author Jelle De Loecker <jelle@elevenways.be>
310
+ * @since 1.2.5
311
+ * @version 1.2.5
312
+ *
313
+ * @param {String} route_param
314
+ * @param {*} new_value
315
+ */
316
+ Conduit.setMethod(function rewriteRequestRouteParam(route_param, new_value) {
317
+
318
+ if (!this.rewritten_request_route_param) {
319
+ this.rewritten_request_route_param = {};
320
+ }
321
+
322
+ this.rewritten_request_route_param[route_param] = new_value;
323
+ });
324
+
325
+ /**
326
+ * Set the request files
327
+ *
328
+ * @author Jelle De Loecker <jelle@elevenways.be>
329
+ * @since 1.1.0
330
+ * @version 1.1.0
331
+ *
332
+ * @param {Object}
333
+ */
334
+ Conduit.setMethod(function setRequestFiles(files) {
335
+
336
+ if (!files) {
337
+ return;
338
+ }
339
+
340
+ _setRequestFiles(this, files, this.files);
341
+ });
342
+
343
+ /**
344
+ * Set the request files
345
+ *
346
+ * @author Jelle De Loecker <jelle@elevenways.be>
347
+ * @since 1.2.0
348
+ * @version 1.2.0
349
+ *
350
+ * @param {Conduit} conduit
351
+ * @param {Array} files
352
+ * @param {Object} target
353
+ */
354
+ function _setRequestFiles(conduit, files, target) {
355
+
356
+ let context,
357
+ upload,
358
+ entry,
359
+ key;
360
+
361
+ for (key in files) {
362
+ entry = files[key];
363
+
364
+ if (Array.isArray(entry)) {
365
+ context = target[key];
366
+
367
+ if (!context) {
368
+ context = target[key] = {};
369
+ }
370
+
371
+ _setRequestFiles(conduit, entry, context);
372
+ } else {
373
+ target[key] = Classes.Alchemy.Inode.File.from(entry);
374
+ }
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Don't convert a conduit to any special json data
380
+ *
381
+ * @author Jelle De Loecker <jelle@develry.be>
382
+ * @since 0.2.0
383
+ * @version 0.2.0
384
+ */
385
+ Conduit.setMethod(function toJSON() {
386
+ return null;
387
+ });
388
+
389
+ /**
390
+ * Set the request & response objects
391
+ *
392
+ * @author Jelle De Loecker <jelle@elevenways.be>
393
+ * @since 1.2.0
394
+ * @version 1.2.0
395
+ */
396
+ Conduit.setMethod(function setReqRes(req, res) {
397
+
398
+ if (req != null) {
399
+ // Make conduit available in req
400
+ req.conduit = this;
401
+
402
+ // Basic HTTP objects
403
+ this.request = req;
404
+
405
+ // The HTTP request headers
406
+ this.headers = req.headers;
407
+
408
+ // Parse the original URL without host
409
+ this.original_url = new RURL(req.url);
410
+
411
+ // Is this an AJAX request?
412
+ this.ajax = null;
413
+ }
414
+
415
+ if (res != null) {
416
+ this.response = res;
417
+ }
418
+ });
419
+
420
+ /**
421
+ * Init values
422
+ *
423
+ * @author Jelle De Loecker <jelle@develry.be>
424
+ * @since 0.3.3
425
+ * @version 1.1.0
426
+ */
427
+ Conduit.setMethod(function initValues() {
428
+
429
+ // Use passed-along router, or default router instance
430
+ this.router = this.router || Router;
431
+
432
+ // The path without any prefix, including section mounts
433
+ this.path = null;
434
+
435
+ // The path without prefix or section mount
436
+ this.sectionPath = null;
437
+
438
+ // The accepted languages
439
+ this.languages = null;
440
+
441
+ // URL paths can be prefixed with certain locales,
442
+ // these locales should then get preference over the user's browser locale
443
+ this.prefix = null;
444
+
445
+ // All the locales the user's browser accepts
446
+ this.locales = null;
447
+
448
+ // The matching Route instance
449
+ this.route = null;
450
+
451
+ // The named parameters inside the path
452
+ this.params = null;
453
+
454
+ // The original string parameters
455
+ this.route_string_parameters = null;
456
+
457
+ // The section vhost domain
458
+ this.sectionDomain = null;
459
+
460
+ // The section of the used route
461
+ this.section = null;
462
+
463
+ // The parsed path (including querystring)
464
+ this.url = null
465
+
466
+ // The current active theme
467
+ this.theme = null;
468
+ });
469
+
470
+ /**
471
+ * Get the time since the conduit was made
472
+ *
473
+ * @author Jelle De Loecker <jelle@develry.be>
474
+ * @since 0.2.0
475
+ * @version 1.1.0
476
+ */
477
+ Conduit.setMethod(function time() {
478
+ return Date.now() - this.start;
479
+ });
480
+
481
+ /**
482
+ * Parse the request, get information from the url
483
+ *
484
+ * @author Jelle De Loecker <jelle@develry.be>
485
+ * @since 0.2.0
486
+ * @version 1.2.5
487
+ *
488
+ * @param {IncomingMessage} req
489
+ * @param {ServerResponse} res
490
+ */
491
+ Conduit.setMethod(async function parseRequest() {
492
+
493
+ var protocol,
494
+ section;
495
+
496
+ if (this.method == null && this.request && this.request.method) {
497
+ this.method = this.request.method.toLowerCase();
498
+ }
499
+
500
+ this.parseShortcuts();
501
+ this.parseLanguages();
502
+ this.parsePrefix();
503
+ this.parseSection();
504
+
505
+ // Try getting the route
506
+ await this.parseRoute();
507
+
508
+ if (this.halt_request) {
509
+ return false;
510
+ }
511
+
512
+ // Is this encrypted?
513
+ if (this.encrypted == null) {
514
+ this.encrypted = this.request.connection.encrypted;
515
+ }
516
+
517
+ if (this.rewritten_request_route_param) {
518
+ let params = Object.assign({}, this.route_string_parameters, this.rewritten_request_route_param);
519
+ let new_url = this.route.generateUrl(params, this);
520
+ this.overrideResponseUrl(new_url);
521
+ }
522
+
523
+ // If the url has already been parsed, return early
524
+ if (this.url) {
525
+ return;
526
+ }
527
+
528
+ if (alchemy.settings.assume_https) {
529
+ protocol = 'https://';
530
+ } else if (this.headers['x-forwarded-proto']) {
531
+ protocol = this.headers['x-forwarded-proto'];
532
+ } else if (this.protocol) {
533
+ protocol = this.protocol;
534
+ } else if (this.encrypted) {
535
+ protocol = 'https://';
536
+ } else {
537
+ protocol = 'http://';
538
+ }
539
+
540
+ // Create a new RURL instance
541
+ this.url = new RURL();
542
+
543
+ // Set the protocol
544
+ this.url.protocol = protocol;
545
+
546
+ // Set the host
547
+ this.url.hostname = this.headers.host;
548
+
549
+ let path = this.path;
550
+
551
+ if (this.prefix) {
552
+ path = '/' + this.prefix + '/' + path;
553
+ }
554
+
555
+ this.url.path = path;
556
+
557
+ return true;
558
+ });
559
+
560
+ /**
561
+ * Parse the headers for shortcuts
562
+ *
563
+ * @author Jelle De Loecker <jelle@develry.be>
564
+ * @since 0.2.0
565
+ * @version 0.2.0
566
+ */
567
+ Conduit.setMethod(function parseShortcuts() {
568
+
569
+ var headers = this.headers;
570
+
571
+ // A request can just tell us what route to use
572
+ if (headers['x-alchemy-route-name']) {
573
+ this.route = this.router.getRouteByName(headers['x-alchemy-route-name']);
574
+ }
575
+
576
+ // And which prefix (this is a forced prefix)
577
+ if (headers['x-alchemy-prefix'] && prefixes[headers['x-alchemy-prefix']]) {
578
+ this.prefix = headers['x-alchemy-prefix'];
579
+ }
580
+
581
+ // Section domains can only be requested through headers
582
+ if (headers['x-alchemy-section-domain']) {
583
+ this.sectionDomain = headers['x-alchemy-section-domain'];
584
+ }
585
+
586
+ // Only get ajax on the first parse
587
+ if (this.ajax == null) {
588
+ this.ajax = headers['x-requested-with'] === 'XMLHttpRequest';
589
+ }
590
+
591
+ });
592
+
593
+ /**
594
+ * Sort the parsed accept-language header array
595
+ *
596
+ * @author Jelle De Loecker <jelle@develry.be>
597
+ * @since 0.0.1
598
+ * @version 0.0.1
599
+ *
600
+ * @param {Object} a
601
+ * @param {Object} b
602
+ */
603
+ function qualityCmp(a, b) {
604
+ if (a.quality === b.quality) {
605
+ return 0;
606
+ } else if (a.quality < b.quality) {
607
+ return 1;
608
+ } else {
609
+ return -1;
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Parses the HTTP accept-language header
615
+ *
616
+ * @author Jelle De Loecker <jelle@develry.be>
617
+ * @since 0.0.1
618
+ * @version 0.2.0
619
+ */
620
+ Conduit.setMethod(function parseLanguages() {
621
+
622
+ var rawLangs,
623
+ rawLang,
624
+ locales,
625
+ parts,
626
+ langs,
627
+ qval,
628
+ temp,
629
+ i,
630
+ q;
631
+
632
+ langs = [];
633
+ locales = [];
634
+
635
+ if (this.headers['accept-language']) {
636
+
637
+ rawLangs = this.headers['accept-language'].split(',');
638
+
639
+ for (i = 0; i < rawLangs.length; i++) {
640
+ rawLang = rawLangs[i];
641
+
642
+ parts = rawLang.split(';');
643
+ qval = null;
644
+ q = 1;
645
+
646
+ if (parts.length > 1 && parts[1].indexOf('q=') === 0) {
647
+ qval = parseFloat(parts[1].split('=')[1]);
648
+
649
+ if (isNaN(qval) === false) {
650
+ q = qval;
651
+ }
652
+ }
653
+
654
+ // Get the lang-loc code
655
+ temp = parts[0].trim().toLowerCase().split('-');
656
+
657
+ langs.push({lang: temp[0], loc: temp[1], quality: q});
658
+ }
659
+
660
+ langs.sort(qualityCmp);
661
+ };
662
+
663
+ temp = {};
664
+
665
+ for (i = 0; i < langs.length; i++) {
666
+ if (!temp[langs[i].lang]) {
667
+ locales.push(langs[i].lang);
668
+ temp[langs[i].lang] = true;
669
+ }
670
+ }
671
+
672
+ this.languages = langs;
673
+ this.locales = locales;
674
+ });
675
+
676
+ /**
677
+ * Parses accept-encoding strings
678
+ *
679
+ * @author jshttp
680
+ * @author Jelle De Loecker <jelle@develry.be>
681
+ * @since 0.2.0
682
+ * @version 0.2.0
683
+ */
684
+ function parseEncoding(s, i) {
685
+ var match = s.match(/^\s*(\S+?)\s*(?:;(.*))?$/);
686
+
687
+ if (!match) return null;
688
+
689
+ var encoding = match[1];
690
+ var q = 1;
691
+ if (match[2]) {
692
+ var params = match[2].split(';');
693
+ for (var i = 0; i < params.length; i ++) {
694
+ var p = params[i].trim().split('=');
695
+ if (p[0] === 'q') {
696
+ q = parseFloat(p[1]);
697
+ break;
698
+ }
699
+ }
700
+ }
701
+
702
+ return {
703
+ encoding: encoding,
704
+ q: q,
705
+ i: i
706
+ };
707
+ }
708
+
709
+ /**
710
+ * Parses accept-encoding strings
711
+ *
712
+ * @author jshttp
713
+ * @author Jelle De Loecker <jelle@develry.be>
714
+ * @since 0.2.0
715
+ * @version 0.2.0
716
+ */
717
+ function specify(encoding, spec, index) {
718
+ var s = 0;
719
+ if(spec.encoding.toLowerCase() === encoding.toLowerCase()){
720
+ s |= 1;
721
+ } else if (spec.encoding !== '*' ) {
722
+ return null
723
+ }
724
+
725
+ return {
726
+ i: index,
727
+ o: spec.i,
728
+ q: spec.q,
729
+ s: s
730
+ }
731
+ };
732
+
733
+ /**
734
+ * Parses the HTTP accept-encoding header
735
+ *
736
+ * @author Jelle De Loecker <jelle@develry.be>
737
+ * @since 0.2.0
738
+ * @version 0.2.0
739
+ */
740
+ Conduit.setMethod(function parseAcceptEncoding() {
741
+
742
+ var hasIdentity,
743
+ minQuality,
744
+ encoding,
745
+ accepts,
746
+ i,
747
+ j;
748
+
749
+ // Make sure this only runs once
750
+ if (this.accepted_encodings != null) {
751
+ return;
752
+ }
753
+
754
+ if (!this.headers['accept-encoding']) {
755
+ this.accepted_encodings = false;
756
+ return;
757
+ }
758
+
759
+ accepts = this.headers['accept-encoding'].split(',');
760
+ minQuality = 1;
761
+
762
+ for (i = 0, j = 0; i < accepts.length; i++) {
763
+ encoding = parseEncoding(accepts[i].trim(), i);
764
+
765
+ if (encoding) {
766
+ accepts[j++] = encoding;
767
+ hasIdentity = hasIdentity || specify('identity', encoding);
768
+ minQuality = Math.min(minQuality, encoding.q || 1);
769
+ }
770
+ }
771
+
772
+ if (!hasIdentity) {
773
+ /*
774
+ * If identity doesn't explicitly appear in the accept-encoding header,
775
+ * it's added to the list of acceptable encoding with the lowest q
776
+ */
777
+ accepts[j++] = {
778
+ encoding: 'identity',
779
+ q: minQuality,
780
+ i: i
781
+ };
782
+ }
783
+
784
+ // trim accepts
785
+ accepts.length = j;
786
+
787
+ this.accepted_encodings = accepts;
788
+ });
789
+
790
+ /**
791
+ * See if the wanted encoding is accepted by the client
792
+ *
793
+ * @author Jelle De Loecker <jelle@develry.be>
794
+ * @since 0.2.0
795
+ * @version 0.2.0
796
+ */
797
+ Conduit.setMethod(function accepts(encoding) {
798
+
799
+ var i;
800
+
801
+ // Parse the encodings on the fly
802
+ this.parseAcceptEncoding();
803
+
804
+ if (!this.accepted_encodings) {
805
+ return false;
806
+ }
807
+
808
+ for (i = 0; i < this.accepted_encodings.length; i++) {
809
+ if (this.accepted_encodings[i].encoding == encoding) {
810
+ return true;
811
+ }
812
+ }
813
+
814
+ return false;
815
+ });
816
+
817
+ /**
818
+ * Create a loopback conduit
819
+ *
820
+ * @author Jelle De Loecker <jelle@develry.be>
821
+ * @since 0.2.0
822
+ * @version 1.1.3
823
+ *
824
+ * @param {Object} args
825
+ * @param {Function} callback
826
+ *
827
+ * @return {Alchemy.LoopbackConduit}
828
+ */
829
+ Conduit.setMethod(function loopback(args, callback) {
830
+ return Classes.Alchemy.Conduit.Loopback.create(this, args, callback);
831
+ });
832
+
833
+ /**
834
+ * Parse the request, get information from the url
835
+ *
836
+ * @author Jelle De Loecker <jelle@develry.be>
837
+ * @since 0.2.0
838
+ * @version 1.1.0
839
+ */
840
+ Conduit.setMethod(function parsePrefix() {
841
+
842
+ let path = this.original_path;
843
+
844
+ if (!path) {
845
+ return;
846
+ }
847
+
848
+ let active_prefix,
849
+ prefix,
850
+ begin;
851
+
852
+ // Look for the prefix at the beginning of the url path
853
+ if (!this.prefix) {
854
+ for (prefix in prefixes) {
855
+ begin = '/' + prefix + '/';
856
+
857
+ if (path.indexOf(begin) === 0) {
858
+ this.prefix = prefix;
859
+ break;
860
+ }
861
+ }
862
+ }
863
+
864
+ // Handle urls with ONLY the prefix and no ending slash
865
+ if (!this.prefix) {
866
+ for (prefix in prefixes) {
867
+ begin = '/' + prefix;
868
+
869
+ if (this.original_pathname == begin) {
870
+ this.prefix = prefix;
871
+ break;
872
+ }
873
+ }
874
+
875
+ if (this.prefix && path.endsWith('/' + this.prefix)) {
876
+ this.path = '/';
877
+ } else if (this.prefix && path.startsWith('/' + this.prefix + '?')) {
878
+ this.path = '/?' + path.after('?');
879
+ } else {
880
+ this.path = path;
881
+ }
882
+
883
+ } else if (this.prefix && path.indexOf('/' + this.prefix + '/') === 0) {
884
+ // Remove the prefix from the path if one is given
885
+ this.path = path.slice(this.prefix.length+1);
886
+ } else {
887
+ this.path = path;
888
+ }
889
+
890
+ // Add this prefix to the top of the locales
891
+ if (this.prefix) {
892
+ active_prefix = this.prefix;
893
+ this.locales.unshift(this.prefix);
894
+
895
+ // Remember this prefix in the session
896
+ this.session('last_forced_prefix', this.prefix);
897
+
898
+ // Let the client know this prefix should be used
899
+ this.expose('forced_prefix', this.prefix);
900
+ } else {
901
+
902
+ let last_forced_prefix = this.session('last_forced_prefix');
903
+
904
+ if (last_forced_prefix) {
905
+ active_prefix = last_forced_prefix;
906
+ } else if (this.active_prefix) {
907
+ // There already is an active prefix, so just keep on using that
908
+ // (Is the case in redirects)
909
+ return;
910
+ } else {
911
+
912
+ // If no prefix has been found yet, look for the default prefix
913
+ // This will override the browser locale
914
+ if (this.headers['x-alchemy-default-prefix']) {
915
+ if (prefixes[this.headers['x-alchemy-default-prefix']]) {
916
+ active_prefix = this.headers['x-alchemy-default-prefix'];
917
+
918
+ if (this.locales[0] != active_prefix) {
919
+ this.locales.unshift(active_prefix);
920
+ }
921
+ }
922
+ }
923
+
924
+ if (!active_prefix) {
925
+ active_prefix = this.locales[0];
926
+ }
927
+ }
928
+ }
929
+
930
+ this.active_prefix = active_prefix;
931
+ });
932
+
933
+ /**
934
+ * Get the section
935
+ *
936
+ * @author Jelle De Loecker <jelle@develry.be>
937
+ * @since 0.2.0
938
+ * @version 0.2.1
939
+ */
940
+ Conduit.setMethod(function parseSection() {
941
+
942
+ // Get the section this path is using
943
+ this.section = this.router.getPathSection(this.path);
944
+
945
+ if (!this.section) {
946
+ log.warn('No section found for path "' + this.path + '"');
947
+ }
948
+
949
+ // If the section has a parent it's not the root
950
+ if (this.section && this.section.parent) {
951
+ this.sectionPath = this.path.slice(this.section.mount.length) || '/';
952
+ } else {
953
+ this.sectionPath = this.path;
954
+ }
955
+ });
956
+
957
+ /**
958
+ * Get a route by its name
959
+ *
960
+ * @author Jelle De Loecker <jelle@develry.be>
961
+ * @since 0.2.0
962
+ * @version 0.2.0
963
+ *
964
+ * @param {String|Object} name The name of the route
965
+ */
966
+ Conduit.setMethod(function getRouteByName(name) {
967
+
968
+ // See if the name is an object, which means it's for sockets
969
+ if (name && typeof name == 'object') {
970
+ this.route = name;
971
+ } else {
972
+ this.route = this.router.getRouteByName(name);
973
+ }
974
+
975
+ return this.route;
976
+ });
977
+
978
+ /**
979
+ * Get the Route instance & named parameters
980
+ *
981
+ * @author Jelle De Loecker <jelle@develry.be>
982
+ * @since 0.2.0
983
+ * @version 1.1.7
984
+ *
985
+ * @param {Route} after_route Only check routes after this one
986
+ *
987
+ * @return {Boolean} Continue processing this request or not?
988
+ */
989
+ Conduit.setMethod(async function parseRoute(after_route) {
990
+
991
+ var temp;
992
+
993
+ this.section = this.router.getPathSection(this.path);
994
+
995
+ // Remove the current found route
996
+ if (after_route) {
997
+ this.route_rematch = true;
998
+ this.route = null;
999
+ }
1000
+
1001
+ // If the route hasn't been found in the header shortcuts yet, look for it
1002
+ if (!this.route) {
1003
+
1004
+ temp = this.router.getRouteBySectionPath(this, this.method, this.section, this.sectionPath, this.prefix, after_route);
1005
+
1006
+ if (temp && temp.then) {
1007
+ temp = await temp;
1008
+ }
1009
+
1010
+ if (temp) {
1011
+ this.route = temp.route;
1012
+ this.params = temp.parameters;
1013
+ this.route_string_parameters = temp.original_parameters;
1014
+ this.path_definition = temp.definition;
1015
+ } else {
1016
+ // Is this a HEAD request? Then we need to check if a GET exists
1017
+ if (this.method == 'head') {
1018
+ let get_route = this.router.getRouteBySectionPath(this, 'get', this.section, this.sectionPath, this.prefix, after_route);
1019
+
1020
+ if (get_route && get_route.then) {
1021
+ get_route = await get_route;
1022
+ }
1023
+
1024
+ // A GET route was found, so we just need to end this request
1025
+ if (get_route) {
1026
+ this.end();
1027
+ this.halt_request = true;
1028
+ return;
1029
+ }
1030
+ } else {
1031
+ // See if the path matches another method
1032
+ temp = await this.router.getRouteBySectionPath(this, ['get', 'post', 'put'], this.section, this.sectionPath, this.prefix, after_route);
1033
+
1034
+ if (temp) {
1035
+ this.route_mismatch = temp.route;
1036
+
1037
+ temp = null;
1038
+ }
1039
+ }
1040
+
1041
+ this.route_not_found = true;
1042
+ }
1043
+ } else {
1044
+ temp = this.route.match(this, this.method, this.sectionPath);
1045
+
1046
+ if (temp && temp.then) {
1047
+ temp = await temp;
1048
+ }
1049
+
1050
+ if (temp) {
1051
+ this.params = temp.parameters || {};
1052
+ this.route_string_parameters = temp.original_parameters || {};
1053
+ this.path_definition = temp.definition;
1054
+ } else {
1055
+ this.params = {};
1056
+ }
1057
+ }
1058
+ });
1059
+
1060
+ /**
1061
+ * Run the middleware
1062
+ *
1063
+ * @author Jelle De Loecker <jelle@develry.be>
1064
+ * @since 0.2.0
1065
+ * @version 1.1.5
1066
+ */
1067
+ Conduit.setMethod(async function callMiddleware() {
1068
+
1069
+ if (!this.section) {
1070
+ return this.callHandler();
1071
+ }
1072
+
1073
+ let that = this,
1074
+ middlewares = await this.section.getMiddleware(this, this.section, this.path, this.prefix),
1075
+ debugObject = this._debugObject,
1076
+ middleDebug = this.debug({label: 'middleware', data: {title: 'Doing middleware'}}),
1077
+ routeDebug,
1078
+ theme;
1079
+
1080
+ if (middleDebug) {
1081
+ this._debugObject = middleDebug;
1082
+ }
1083
+
1084
+ middlewares = new Iterator(middlewares);
1085
+
1086
+ Function.while(function test() {
1087
+ return middlewares.hasNext();
1088
+ }, function middlewareTask(next) {
1089
+
1090
+ var route = middlewares.next().value,
1091
+ middlePath,
1092
+ req;
1093
+
1094
+ // Skip middleware that does not listen to the request method
1095
+ if (route.methods.indexOf(that.method) === -1) {
1096
+ return next();
1097
+ }
1098
+
1099
+ // Augment the request object
1100
+ req = Object.create(that.request);
1101
+
1102
+ // Get the path without the middleware mount path
1103
+ middlePath = req.conduit.sectionPath.replace(route.paths[''].source, '');
1104
+
1105
+ // Strip any query parameters
1106
+ if (middlePath.indexOf('?') > -1) {
1107
+ middlePath = middlePath.before('?');
1108
+ }
1109
+
1110
+ if (middlePath[0] !== '/') {
1111
+ middlePath = '/' + middlePath;
1112
+ }
1113
+
1114
+ // Look for theme settings
1115
+ if (req.conduit.url) {
1116
+ theme = req.conduit.url.query.theme;
1117
+
1118
+ if (theme) {
1119
+ middlePath = ['/' + theme + middlePath, middlePath];
1120
+ }
1121
+ }
1122
+
1123
+ req.middlePath = middlePath;
1124
+ req.original = that.request;
1125
+
1126
+ if (routeDebug) {
1127
+ routeDebug.stop();
1128
+ }
1129
+
1130
+ if (middleDebug) {
1131
+ routeDebug = middleDebug.debug('route', {title: 'Doing "' + route.name + '"'});
1132
+ that._debugObject = routeDebug;
1133
+ }
1134
+
1135
+ route.fnc(req, that.response, next);
1136
+ }, function done(err) {
1137
+
1138
+ if (err) {
1139
+ return that.emit('error', err);
1140
+ }
1141
+
1142
+ if (routeDebug) {
1143
+ routeDebug.stop();
1144
+ }
1145
+
1146
+ // Don't do this for websockets
1147
+ if (that.websocket) {
1148
+ return;
1149
+ }
1150
+
1151
+ if (middleDebug) {
1152
+ middleDebug.mark('Preparing viewrender');
1153
+ }
1154
+
1155
+ that.prepareViewRender();
1156
+
1157
+ if (middleDebug) {
1158
+ middleDebug.mark(false);
1159
+ middleDebug.stop();
1160
+ }
1161
+
1162
+ if (that._debugConduitInitialize) {
1163
+ that._debugConduitInitialize.stop();
1164
+ }
1165
+
1166
+ // Return the original debug object
1167
+ that._debugObject = debugObject;
1168
+
1169
+ that.callHandler();
1170
+ });
1171
+ });
1172
+
1173
+ /**
1174
+ * Create a new Hawkejs' ViewRender instance
1175
+ *
1176
+ * @author Jelle De Loecker <jelle@develry.be>
1177
+ * @since 0.2.0
1178
+ * @version 1.1.1
1179
+ */
1180
+ Conduit.setMethod(function prepareViewRender() {
1181
+
1182
+ // Add a link to this conduit
1183
+ this.renderer.conduit = this;
1184
+ this.renderer.server_var('conduit', this);
1185
+
1186
+ // Let the ViewRender get some request info
1187
+ this.renderer.prepare(this.request, this);
1188
+
1189
+ // Pass url parameters to the client
1190
+ this.renderer.internal('urlparams', this.route_string_parameters);
1191
+ this.renderer.internal('url', this.url);
1192
+
1193
+ if (this.route) {
1194
+ this.renderer.internal('route', this.route.name);
1195
+ }
1196
+
1197
+ this.renderer.is_for_client_side = this.ajax;
1198
+ });
1199
+
1200
+ /**
1201
+ * Call the handler of this route when parsing is finished
1202
+ *
1203
+ * @author Jelle De Loecker <jelle@develry.be>
1204
+ * @since 0.2.0
1205
+ * @version 1.1.7
1206
+ */
1207
+ Conduit.setMethod(function callHandler() {
1208
+
1209
+ if (!this.route) {
1210
+
1211
+ if (this.route_mismatch) {
1212
+
1213
+ if (alchemy.settings.debug) {
1214
+ console.log('Route method not allowed:', this);
1215
+ }
1216
+
1217
+ this.error(405, 'Method Not Allowed', false);
1218
+
1219
+ } else {
1220
+ if (alchemy.settings.debug) {
1221
+ console.log('Route not found:', this);
1222
+ }
1223
+
1224
+ this.notFound('Route was not found');
1225
+ }
1226
+
1227
+ return;
1228
+ }
1229
+
1230
+ this.route.callHandler(this);
1231
+ });
1232
+
1233
+ /**
1234
+ * End the current request with a 202 status
1235
+ * and tell the client to look at another url later
1236
+ *
1237
+ * @author Jelle De Loecker <jelle@develry.be>
1238
+ * @since 1.1.0
1239
+ * @version 1.2.5
1240
+ *
1241
+ * @param {String|Object} options Options or url
1242
+ */
1243
+ Conduit.setMethod(function postpone(options) {
1244
+
1245
+ let session = this.getSession();
1246
+
1247
+ if (typeof options == 'number') {
1248
+ options = {
1249
+ expected_duration: options
1250
+ };
1251
+ } else if (!options) {
1252
+ options = {};
1253
+ }
1254
+
1255
+ // Make sure the scene id exists
1256
+ this.createScene();
1257
+
1258
+ // Already set the cookies
1259
+ if (this.new_cookie_header.length) {
1260
+ this.response.setHeader('set-cookie', this.new_cookie_header);
1261
+ }
1262
+
1263
+ let postponed_id = session.postpone(this),
1264
+ url = '/alchemy/postponed/' + postponed_id;
1265
+
1266
+ // Set the location header where the client should look at later
1267
+ this.response.setHeader('Location', url);
1268
+ this.response.setHeader('Content-Type', 'text/html');
1269
+
1270
+ if (options.expected_duration) {
1271
+ this.response.setHeader('Expected-Duration', Number(options.expected_duration / 1000).toFixed(2));
1272
+ }
1273
+
1274
+ // Write the headers & 202 status
1275
+ this.response.writeHead(202);
1276
+
1277
+ // End the response
1278
+ this.response.end('The response has been postponed, you can find it at <a href="' + url + '">' + url + '</a>');
1279
+
1280
+ // Nullify the response
1281
+ this.response = null;
1282
+
1283
+ // Set the original url
1284
+ this.overrideResponseUrl(this.url)
1285
+ });
1286
+
1287
+ /**
1288
+ * Set the response url
1289
+ *
1290
+ * @author Jelle De Loecker <jelle@develry.be>
1291
+ * @since 1.2.5
1292
+ * @version 1.2.5
1293
+ *
1294
+ * @param {String|RURL} url
1295
+ */
1296
+ Conduit.setMethod(function overrideResponseUrl(url) {
1297
+
1298
+ if (typeof url != 'string') {
1299
+ url = String(url);
1300
+ }
1301
+
1302
+ this.setHeader('x-history-url', url);
1303
+ this.expose('redirected_to', url);
1304
+ });
1305
+
1306
+ /**
1307
+ * Redirect to another url
1308
+ *
1309
+ * @author Jelle De Loecker <jelle@develry.be>
1310
+ * @since 0.2.0
1311
+ * @version 1.2.5
1312
+ *
1313
+ * @param {Number} status 3xx redirection codes. 302 (temporary redirect) by default
1314
+ * @param {String|Object} options Options or url
1315
+ */
1316
+ Conduit.setMethod(function redirect(status, options) {
1317
+
1318
+ var hard_refresh = false,
1319
+ url;
1320
+
1321
+ if (typeof status != 'number') {
1322
+
1323
+ if (typeof options == 'object') {
1324
+ options.url = status;
1325
+ } else {
1326
+ options = status;
1327
+ }
1328
+
1329
+ status = 302;
1330
+ }
1331
+
1332
+ if (typeof options == 'object' && options) {
1333
+
1334
+ if (options.href || options.path) {
1335
+ url = options.href || options.path;
1336
+ } else {
1337
+
1338
+ if (options.body) {
1339
+ Object.defineProperty(this, 'body', {
1340
+ value : options.body,
1341
+ configurable : true
1342
+ });
1343
+ }
1344
+
1345
+ if (options.method) {
1346
+ this.method = options.method;
1347
+ }
1348
+
1349
+ // When headers are given, the redirect is internal
1350
+ if (options.headers) {
1351
+ this.headers = options.headers;
1352
+
1353
+ this.oldoriginal_path = this.original_path;
1354
+
1355
+ if (typeof options.url == 'string') {
1356
+ let temp = options.url;
1357
+ temp = RURL.parse(temp);
1358
+ url = temp.path;
1359
+ } else {
1360
+ url = options.url.path;
1361
+ }
1362
+
1363
+ if (url == null) {
1364
+ throw new Error('Conduit#redirect can not redirect to null path');
1365
+ }
1366
+
1367
+ // Register the new url as the one to use for the history
1368
+ this.overrideResponseUrl(url);
1369
+
1370
+ this.original_path = url;
1371
+
1372
+ // Reinitialize the conduit
1373
+ this.initValues();
1374
+ this.initHttp();
1375
+
1376
+ return;
1377
+ } else {
1378
+ url = options.url;
1379
+ }
1380
+ }
1381
+
1382
+ if (options.hard_refresh) {
1383
+ hard_refresh = options.hard_refresh;
1384
+ }
1385
+
1386
+ } else if (typeof options == 'string') {
1387
+ url = options;
1388
+ options = null;
1389
+ } else {
1390
+ throw new Error('Conduit#redirect requires a valid url or options object');
1391
+ }
1392
+
1393
+ this.status = status;
1394
+
1395
+ if (hard_refresh && this.headers['x-hawkejs-request']) {
1396
+ this.setHeader('x-hawkejs-navigate', url);
1397
+
1398
+ if (options && options.popup) {
1399
+ this.setHeader('x-hawkejs-popup', options.popup);
1400
+ }
1401
+
1402
+ } else {
1403
+ this.setHeader('Location', url);
1404
+ }
1405
+
1406
+ this._end();
1407
+ });
1408
+
1409
+ /**
1410
+ * Respond with an error
1411
+ *
1412
+ * @author Jelle De Loecker <jelle@develry.be>
1413
+ * @since 0.2.0
1414
+ * @version 1.1.0
1415
+ *
1416
+ * @param {Nulber} status Response statuscode
1417
+ * @param {Error} message Optional error to send
1418
+ * @param {Boolean} printError Print the error, defaults to true
1419
+ */
1420
+ Conduit.setMethod(function error(status, message, printError) {
1421
+
1422
+ if (status instanceof Classes.Alchemy.Error.HTTP) {
1423
+ message = status;
1424
+ status = message.status;
1425
+ }
1426
+
1427
+ if (typeof status !== 'number') {
1428
+ message = status;
1429
+ status = 500;
1430
+ }
1431
+
1432
+ if (!message) {
1433
+ message = 'Unknown server error';
1434
+ }
1435
+
1436
+ if (typeof message == 'string') {
1437
+ let error = new Classes.Alchemy.Error.HTTP(status, message);
1438
+ error[Symbol.for('extra_skip_levels')] = 1;
1439
+ message = error;
1440
+ }
1441
+
1442
+ let subject = 'Error found on ' + this.original_path + '';
1443
+
1444
+ if (printError === false) {
1445
+ log.error(subject + ':\n' + message);
1446
+ } else if (message instanceof Error) {
1447
+ alchemy.printLog('error', [subject, String(message), message], {err: message, level: -2});
1448
+ } else {
1449
+ log.error(subject + ':\n' + message);
1450
+ }
1451
+
1452
+ // Make sure the client doesn't expect compression
1453
+ this.setHeader('content-encoding', '');
1454
+
1455
+ this.status = status;
1456
+
1457
+ // Only render an expensive "Error" template when the client directly
1458
+ // browses to an HTML page.
1459
+ // Don't render a template for AJAX or asset requests
1460
+ if (this.renderer && (this.ajax || (this.headers.accept && this.headers.accept.indexOf('html') > -1))) {
1461
+
1462
+ // Hawkejs will have the option to throw the error OR render the error
1463
+ if (this.ajax) {
1464
+ this.end({
1465
+ error : true,
1466
+ status : status,
1467
+ message : message,
1468
+ error_templates : ['error/' + status, 'error/unknown'],
1469
+ });
1470
+ } else {
1471
+ this.set('status', status);
1472
+ this.set('message', message);
1473
+ this.render(['error/' + status, 'error/unknown']);
1474
+ }
1475
+ } else {
1476
+ // Requests for images or scripts just get a non-expensive string response
1477
+ this.setHeader('content-type', 'text/plain');
1478
+ this._end(status + ':\n' + message + '\n');
1479
+ }
1480
+
1481
+ // Emit this as a conduit error
1482
+ alchemy.emit('conduit_error', this, status, message);
1483
+ });
1484
+
1485
+ /**
1486
+ * Deny access
1487
+ *
1488
+ * @author Jelle De Loecker <jelle@develry.be>
1489
+ * @since 0.2.0
1490
+ * @version 1.1.0
1491
+ *
1492
+ * @param {Number} status
1493
+ * @param {Error} message optional error to send
1494
+ */
1495
+ Conduit.setMethod(function deny(status, message) {
1496
+
1497
+ if (typeof status == 'string') {
1498
+ message = status;
1499
+ status = 403;
1500
+ } else if (status instanceof Classes.Alchemy.Error.HTTP) {
1501
+ return this.error(status);
1502
+ }
1503
+
1504
+ if (message == null) {
1505
+ message = 'Forbidden';
1506
+ }
1507
+
1508
+ this.error(status, message);
1509
+ });
1510
+
1511
+ /**
1512
+ * The current user is not authorized and needs to log in
1513
+ * (Default implementation, is overriden by the acl plugin)
1514
+ *
1515
+ * @author Jelle De Loecker <jelle@develry.be>
1516
+ * @since 1.0.7
1517
+ * @version 1.0.7
1518
+ *
1519
+ * @param {Boolean} tried_auth Indicate that this was an auth attempt
1520
+ */
1521
+ Conduit.setMethod(function notAuthorized(tried_auth) {
1522
+ return this.deny();
1523
+ });
1524
+
1525
+ /**
1526
+ * The current user is authenticated, but not allowed
1527
+ * (Default implementation, is overriden by the acl plugin)
1528
+ *
1529
+ * @author Jelle De Loecker <jelle@kipdola.be>
1530
+ * @since 1.0.7
1531
+ * @version 1.1.0
1532
+ */
1533
+ Conduit.setMethod(function forbidden() {
1534
+
1535
+ let error = new Classes.Alchemy.Error.HTTP(403, 'Forbidden');
1536
+ error.skipTraceLines(1);
1537
+
1538
+ return this.deny(error);
1539
+ });
1540
+
1541
+ /**
1542
+ * Respond with a not found error status
1543
+ *
1544
+ * @author Jelle De Loecker <jelle@develry.be>
1545
+ * @since 0.2.0
1546
+ * @version 1.1.0
1547
+ *
1548
+ * @param {Error} message optional error to send
1549
+ */
1550
+ Conduit.setMethod(async function notFound(message) {
1551
+
1552
+ // Look for other paths
1553
+ if (!this.route_not_found && this.route && !this.route_rematch) {
1554
+
1555
+ // Try matching against paths after the ones we currently matched
1556
+ await this.parseRoute(this.route);
1557
+
1558
+ // Call the handler of that route if it has been found
1559
+ if (this.route) {
1560
+ return this.route.callHandler(this);
1561
+ }
1562
+ }
1563
+
1564
+ if (message == null) {
1565
+ message = 'Not found';
1566
+ }
1567
+
1568
+ this.error(404, message, false);
1569
+ });
1570
+
1571
+ /**
1572
+ * Respond with a "Not Modified" 304 status
1573
+ *
1574
+ * @author Jelle De Loecker <jelle@develry.be>
1575
+ * @since 0.2.0
1576
+ * @version 0.4.0
1577
+ */
1578
+ Conduit.setMethod(function notModified() {
1579
+ this.status = 304;
1580
+ this._end();
1581
+ });
1582
+
1583
+ /**
1584
+ * Respond with text. Objects get JSON-dry encoded
1585
+ *
1586
+ * @author Jelle De Loecker <jelle@develry.be>
1587
+ * @since 0.2.0
1588
+ * @version 1.1.0
1589
+ *
1590
+ * @param {String|Object} message
1591
+ */
1592
+ Conduit.setMethod(function end(message) {
1593
+
1594
+ var that = this,
1595
+ json_type,
1596
+ json_fnc,
1597
+ cache,
1598
+ etag,
1599
+ temp;
1600
+
1601
+ if (this.websocket) {
1602
+ throw new Error('You can not end a websocket, use the callback instead');
1603
+ }
1604
+
1605
+ if (this.method == 'head') {
1606
+ return this._end();
1607
+ }
1608
+
1609
+ if (typeof message !== 'string') {
1610
+
1611
+ // Use regular JSON if DRY has been disabled in settings
1612
+ if (alchemy.settings.json_dry_response === false || this.json_dry === false) {
1613
+ json_type = 'json';
1614
+ json_fnc = JSON.stringify;
1615
+ } else {
1616
+ json_type = 'json-dry';
1617
+ json_fnc = JSON.dry;
1618
+
1619
+ // Clone the object
1620
+ message = JSON.clone(message, 'toHawkejs');
1621
+ }
1622
+
1623
+ // Only send the mimetype if it hasn't been set yet
1624
+ if (this.setHeader('content-type') == null) {
1625
+ this.setHeader('content-type', "application/" + json_type + ";charset=utf-8");
1626
+ }
1627
+
1628
+ message = json_fnc(message) || 'null';
1629
+ }
1630
+
1631
+ cache = this.headers['cache-control'] || this.headers['pragma'];
1632
+
1633
+ // Only generate etags when caching is enabled locally & on the browser
1634
+ if (alchemy.settings.cache !== false && (cache == null || cache != 'no-cache')) {
1635
+
1636
+ // Calculate the hash as etag
1637
+ etag = alchemy.checksum(message);
1638
+
1639
+ if (etag != null) {
1640
+
1641
+ if (this.headers['if-none-match'] == etag) {
1642
+ return this.notModified();
1643
+ }
1644
+
1645
+ // Responses through `end` should always be privately cached
1646
+ this.setHeader('cache-control', 'private');
1647
+
1648
+ // Send the hash as a response header
1649
+ this.setHeader('etag', etag);
1650
+ }
1651
+ }
1652
+
1653
+ // No need to replace anything if debugging is disabled or the log is empty
1654
+ if (alchemy.settings.debug && this.debuglog && this.debuglog.length && message.indexOf('_placeholder_') > -1) {
1655
+ temp = JSON.dry(this.debuglog);
1656
+ message = message.replace(/{\s*"_placeholder_":\s*"debuglog"\s*}/g, temp);
1657
+ message = message.replace(/{\s*\\"_placeholder_\\":\s*\\"debuglog\\"\s*}/g, JSON.stringify(temp).slice(1,-1));
1658
+ }
1659
+
1660
+ // Compress the output if the client accepts it,
1661
+ // but only if the file is at least 150 bytes
1662
+ if (alchemy.settings.compression && message.length > 150 && this.accepts('gzip')) {
1663
+
1664
+ // Set the decompressed content-length for use in progress bars
1665
+ this.setHeader('x-decompressed-content-length', Buffer.byteLength(message));
1666
+
1667
+ // Set the gzip header
1668
+ this.setHeader('content-encoding', 'gzip');
1669
+
1670
+ // Make sure proxy servers only cache this under this content-encoding type
1671
+ this.setHeader('vary', 'accept-encoding');
1672
+
1673
+ zlib.gzip(message, function gotZipped(err, zipped) {
1674
+ that._end(zipped, 'utf-8');
1675
+ });
1676
+
1677
+ return;
1678
+ }
1679
+
1680
+ this._end(message, 'utf-8');
1681
+ });
1682
+
1683
+ /**
1684
+ * Call the actual end method
1685
+ *
1686
+ * @author Jelle De Loecker <jelle@develry.be>
1687
+ * @since 0.2.0
1688
+ * @version 1.1.0
1689
+ */
1690
+ Conduit.setMethod(function _end(message, encoding = 'utf-8') {
1691
+
1692
+ this.ended = new Date();
1693
+
1694
+ if (!this.response) {
1695
+ let args = [];
1696
+
1697
+ if (arguments.length) {
1698
+ args.push(message);
1699
+ args.push(encoding);
1700
+ }
1701
+
1702
+ this._end_arguments = args;
1703
+
1704
+ return;
1705
+ }
1706
+
1707
+ let headers = [],
1708
+ value,
1709
+ key;
1710
+
1711
+ if (this.status) {
1712
+ this.response.statusCode = this.status;
1713
+ }
1714
+
1715
+ // Set the content-length if it hasn't been set yet
1716
+ if (arguments.length > 0 && !this.response_headers['content-length']) {
1717
+ this.response_headers['content-length'] = Buffer.byteLength(message);
1718
+ }
1719
+
1720
+ for (key in this.response_headers) {
1721
+ value = this.response_headers[key];
1722
+ this.response.setHeader(key, value);
1723
+ }
1724
+
1725
+ if (this.new_cookie_header.length) {
1726
+ this.response.setHeader('set-cookie', this.new_cookie_header);
1727
+ }
1728
+
1729
+ // Write the actual headers
1730
+ this.response.writeHead(this.status);
1731
+
1732
+ if (arguments.length === 0) {
1733
+ return this.response.end();
1734
+ }
1735
+
1736
+ // End the response
1737
+ return this.response.end(message, encoding);
1738
+ });
1739
+
1740
+ /**
1741
+ * Send a response to the client
1742
+ *
1743
+ * @author Jelle De Loecker <jelle@develry.be>
1744
+ * @since 0.2.0
1745
+ * @version 0.2.0
1746
+ */
1747
+ Conduit.setMethod(function send(content) {
1748
+ return this.end(content);
1749
+ });
1750
+
1751
+ /**
1752
+ * Create the scene id (if it doesn't exist already)
1753
+ * We do this using cookies, so the HTML response can be cached
1754
+ *
1755
+ * @author Jelle De Loecker <jelle@develry.be>
1756
+ * @since 0.2.0
1757
+ * @version 1.1.0
1758
+ */
1759
+ Conduit.setMethod(function createScene() {
1760
+ return this.scene_id;
1761
+ });
1762
+
1763
+ /**
1764
+ * Render a view and send it to the client
1765
+ *
1766
+ * @author Jelle De Loecker <jelle@develry.be>
1767
+ * @since 0.2.0
1768
+ * @version 1.1.0
1769
+ */
1770
+ Conduit.setMethod(function render(template_name, options, callback) {
1771
+
1772
+ var that = this,
1773
+ templates;
1774
+
1775
+ if (typeof options == 'function') {
1776
+ callback = options;
1777
+ options = {};
1778
+ } else if (options == null) {
1779
+ options = {};
1780
+ }
1781
+
1782
+ if (template_name) {
1783
+ templates = [template_name];
1784
+ }
1785
+
1786
+ if (templates) {
1787
+ templates.push('error/404');
1788
+ }
1789
+
1790
+ // Expose the useragent info to the hawkejs renderer
1791
+ this.internal('useragent', this.useragent);
1792
+
1793
+ this.createScene();
1794
+
1795
+ // Pass along the clientRender property,
1796
+ // can be used to force rendering HTML
1797
+ if (options.clientRender != null) {
1798
+ this.renderer.clientRender = options.clientRender;
1799
+ }
1800
+
1801
+ if (this.layout) {
1802
+ this.renderer.layout = this.layout;
1803
+ }
1804
+
1805
+ this.renderer.renderHTML(templates).done(function afterRender(err, html) {
1806
+
1807
+ var mimetype;
1808
+
1809
+ if (err != null) {
1810
+
1811
+ if (callback) {
1812
+ return callback(err);
1813
+ }
1814
+
1815
+ throw err;
1816
+ }
1817
+
1818
+ that.registerBindings();
1819
+
1820
+ if (typeof html !== 'string') {
1821
+
1822
+ // Stringify using json-dry
1823
+ html = JSON.dry(html);
1824
+
1825
+ // Tell the client to expect a json-dry response
1826
+ mimetype = 'application/json-dry';
1827
+ } else {
1828
+ mimetype = 'text/html';
1829
+ }
1830
+
1831
+ // Only send the mimetype if it hasn't been set yet
1832
+ if (that.setHeader('content-type') == null) {
1833
+ that.setHeader('content-type', mimetype + ";charset=utf-8");
1834
+ }
1835
+
1836
+ if (callback) {
1837
+ return callback(null, html);
1838
+ }
1839
+
1840
+ that.end(html);
1841
+ });
1842
+ });
1843
+
1844
+ /**
1845
+ * Convert a buffer into a stream
1846
+ *
1847
+ * @author Jelle De Loecker <jelle@develry.be>
1848
+ * @since 1.1.0
1849
+ * @version 1.1.0
1850
+ *
1851
+ * @param {Buffer} buffer
1852
+ *
1853
+ * @return {Readable}
1854
+ */
1855
+ function bufferToStream(buffer) {
1856
+
1857
+ const readable_stream = new libstream.Readable({
1858
+ read() {
1859
+ this.push(buffer);
1860
+ this.push(null);
1861
+ }
1862
+ });
1863
+
1864
+ return readable_stream;
1865
+ }
1866
+
1867
+ /**
1868
+ * Send a file to the browser.
1869
+ * Uses cache-control by default.
1870
+ *
1871
+ * @author Jelle De Loecker <jelle@develry.be>
1872
+ * @since 0.2.0
1873
+ * @version 1.1.0
1874
+ *
1875
+ * @param {String} path The path on the server to send to the browser
1876
+ * @param {Object} options Options, including headers
1877
+ */
1878
+ Conduit.setMethod(function serveFile(path, options) {
1879
+
1880
+ var that = this,
1881
+ tasks = [],
1882
+ stats,
1883
+ isStream;
1884
+
1885
+ // Create an options object if it doesn't exist yet
1886
+ if (options == null) {
1887
+ options = {};
1888
+ }
1889
+
1890
+ // Error handling function
1891
+ if (!options.onError) {
1892
+ options.onError = function onError(err) {
1893
+ that.notFound(err);
1894
+ };
1895
+ }
1896
+
1897
+ // See if we have a stats object
1898
+ if (Object.isObject(path)) {
1899
+
1900
+ if (Buffer.isBuffer(path)) {
1901
+ path = bufferToStream(path);
1902
+ }
1903
+
1904
+ if (path.readable) {
1905
+ isStream = true;
1906
+ stats = {
1907
+ mimetype: 'application/octet-stream'
1908
+ };
1909
+ } else {
1910
+ stats = path;
1911
+ }
1912
+ } else {
1913
+ stats = fileCache[path];
1914
+
1915
+ if (stats == null) {
1916
+ stats = {
1917
+ path: path
1918
+ };
1919
+ }
1920
+ }
1921
+
1922
+ // Don't check for file information when it's a stream
1923
+ if (!isStream) {
1924
+
1925
+ if (!stats.path) {
1926
+ return options.onError(new Error('No file to serve'));
1927
+ }
1928
+
1929
+ // Make sure the stats object is in the cache
1930
+ if (fileCache[stats.path] == null) {
1931
+ fileCache[stats.path] = stats;
1932
+ }
1933
+
1934
+ // Get file stats if it isn't available yet
1935
+ if (stats.mtime == null) {
1936
+ tasks.push(function getFileStats(next) {
1937
+
1938
+ fs.stat(stats.path, function gotStats(err, fileStats) {
1939
+
1940
+ if (err) {
1941
+ stats.err = err;
1942
+ stats.mtime = new Date();
1943
+ } else {
1944
+ Object.assign(stats, fileStats);
1945
+ }
1946
+
1947
+ next();
1948
+ });
1949
+ });
1950
+ }
1951
+
1952
+ // Get the mimetype if it isn't available yet
1953
+ if (!options.mimetype && stats.mimetype == null) {
1954
+ tasks.push(function getMimetype(next) {
1955
+
1956
+ // Don't use libmime if it isn't loaded,
1957
+ // that could be the case on NW.js
1958
+ if (!libmime) {
1959
+ return next();
1960
+ }
1961
+
1962
+ // Lookup the mimetype by the extension alone
1963
+ stats.mimetype = libmime.getType(stats.path);
1964
+
1965
+ // Return the result if a valid mimetype was found
1966
+ if (stats.mimetype !== 'application/octet-stream') {
1967
+ return next();
1968
+ }
1969
+
1970
+ // If no mimetype was found,
1971
+ // see if we can find it using the original path (for resized images)
1972
+ if (options.original_path) {
1973
+ stats.mimetype = libmime.getType(options.original_path);
1974
+
1975
+ if (stats.mimetype !== 'application/octet-stream') {
1976
+ return next();
1977
+ }
1978
+ }
1979
+
1980
+ // "magic" currently doesn't work in nw.js
1981
+ if (Blast.isNW) {
1982
+ return next();
1983
+ }
1984
+
1985
+ // Don't try to use magic if it's not loaded
1986
+ if (!getMagic()) {
1987
+ return next();
1988
+ }
1989
+
1990
+ // Look inside the data (using "magic") for a better mimetype
1991
+ magic.detectFile(stats.path, function detectedMimetype(err, result) {
1992
+
1993
+ if (!err) {
1994
+ stats.mimetype = result;
1995
+ }
1996
+
1997
+ next();
1998
+ });
1999
+ });
2000
+ }
2001
+ }
2002
+
2003
+ Function.parallel(tasks, function gotFileInfo(err) {
2004
+
2005
+ var disposition,
2006
+ outStream,
2007
+ mimetype,
2008
+ headers,
2009
+ isText,
2010
+ since,
2011
+ key;
2012
+
2013
+ if (err) {
2014
+ return that.error(err);
2015
+ }
2016
+
2017
+ if (stats.err) {
2018
+ return options.onError(stats.err);
2019
+ }
2020
+
2021
+ if (!isStream && !stats.path) {
2022
+ return options.onError(new Error('File not found'));
2023
+ }
2024
+
2025
+ // Check the if-modified-since header if it's supplied
2026
+ if (alchemy.settings.cache !== false && that.headers['if-modified-since'] != null) {
2027
+
2028
+ // Turn the string into a date
2029
+ since = new Date(that.headers['if-modified-since']);
2030
+
2031
+ // If the file's modifytime is smaller or equal to the since time,
2032
+ // don't serve the contents!
2033
+ if (stats.mtime <= since) {
2034
+ return that.notModified();
2035
+ }
2036
+ }
2037
+
2038
+ mimetype = stats.mimetype;
2039
+
2040
+ // If we get a general mimetype, and an alternative is provided, use that one
2041
+ if (!mimetype || mimetype === 'application/octet-stream') {
2042
+ if (options.mimetype != null) {
2043
+ mimetype = options.mimetype;
2044
+ }
2045
+ }
2046
+
2047
+ isText = /svg|xml|javascript|text/.test(mimetype);
2048
+
2049
+ // Serve text files as utf-8
2050
+ if (isText) {
2051
+ mimetype += '; charset=utf-8';
2052
+ }
2053
+
2054
+ that.setHeader('content-type', mimetype);
2055
+
2056
+ // Setting the disposition makes the browser download the file
2057
+ // This is on by default, but can be disabled
2058
+ if (options.disposition == 'inline') {
2059
+ disposition = 'inline';
2060
+
2061
+ if (options.filename) {
2062
+ disposition += '; filename=' + JSON.stringify(options.filename)
2063
+ }
2064
+
2065
+ that.setHeader('content-disposition', disposition);
2066
+ } else if (options.disposition !== false) {
2067
+ if (options.filename) {
2068
+ disposition = 'attachment; filename=' + JSON.stringify(options.filename);
2069
+ } else {
2070
+ disposition = 'attachment';
2071
+ }
2072
+
2073
+ that.setHeader('content-disposition', disposition);
2074
+ }
2075
+
2076
+ if (stats.mtime && alchemy.settings.cache) {
2077
+ // Allow the browser to cache this for 60 minutes,
2078
+ // after which it has to revalidate the content
2079
+ // by seeing if it has been modified
2080
+ that.setHeader('cache-control', 'public, max-age=3600, must-revalidate');
2081
+ that.setHeader('last-modified', stats.mtime.toGMTString());
2082
+ } else if (!alchemy.settings.cache) {
2083
+ that.setHeader('cache-control', 'no-cache');
2084
+ }
2085
+
2086
+ for (key in options.headers) {
2087
+ that.setHeader(key, options.headers[key]);
2088
+ }
2089
+
2090
+ // End now if it's just a HEAD request
2091
+ if (that.method == 'head') {
2092
+ return that.end();
2093
+ }
2094
+
2095
+ if (isStream) {
2096
+ outStream = path;
2097
+ } else {
2098
+ outStream = fs.createReadStream(path, {bufferSize: 64*1024});
2099
+
2100
+ // Listen for file errors
2101
+ outStream.on('error', options.onError);
2102
+ }
2103
+
2104
+ // Compress text responses
2105
+ if (isText && alchemy.settings.compression && that.accepts('gzip')) {
2106
+
2107
+ // Set the gzip header
2108
+ that.setHeader('content-encoding', 'gzip');
2109
+ that.setHeader('vary', 'accept-encoding');
2110
+
2111
+ // Create the gzip stream
2112
+ outStream = outStream.pipe(zlib.createGzip());
2113
+ }
2114
+
2115
+ // If we received a stream as parameter...
2116
+ if (isStream) {
2117
+ that.response.on('end', cleanup);
2118
+ that.response.on('finish', cleanup);
2119
+ that.response.on('error', cleanup);
2120
+ that.response.on('close', cleanup);
2121
+ }
2122
+
2123
+ function cleanup() {
2124
+
2125
+ // Remove all pipes
2126
+ outStream.unpipe();
2127
+
2128
+ if (outStream.destroy) {
2129
+ outStream.destroy();
2130
+ } else if (outStream.end) {
2131
+ outStream.end();
2132
+ }
2133
+ }
2134
+
2135
+ // Send the headers
2136
+ for (key in that.response_headers) {
2137
+ that.response.setHeader(key, that.response_headers[key]);
2138
+ }
2139
+
2140
+ if (that.new_cookie_header.length) {
2141
+ that.response.setHeader('set-cookie', that.new_cookie_header);
2142
+ }
2143
+
2144
+ that.response.statusCode = 200;
2145
+
2146
+ // Stream the file to the client
2147
+ outStream.pipe(that.response);
2148
+ });
2149
+ });
2150
+
2151
+ /**
2152
+ * Create a session
2153
+ *
2154
+ * @author Jelle De Loecker <jelle@develry.be>
2155
+ * @since 0.2.0
2156
+ * @version 1.1.0
2157
+ *
2158
+ * @param {Boolean} create Create a session if none exist
2159
+ *
2160
+ * @return {UserSession}
2161
+ */
2162
+ Conduit.setMethod(function getSession(allow_create = true) {
2163
+
2164
+ var cookie_name,
2165
+ fingerprint,
2166
+ session_id,
2167
+ session;
2168
+
2169
+ // Only do this once per request
2170
+ if (this.sessionData != null) {
2171
+ return this.sessionData;
2172
+ }
2173
+
2174
+ // Set the name of the cookie (could change in the future)
2175
+ cookie_name = alchemy.settings.session_key || 'alchemy_sid';
2176
+
2177
+ // Get the ID of the session
2178
+ session_id = this.cookie(cookie_name);
2179
+
2180
+ if (session_id) {
2181
+ // Get the session
2182
+ session = alchemy.sessions.get(session_id);
2183
+ }
2184
+
2185
+ // If no session is found, see if we can find one
2186
+ // based on the browser fingerprint
2187
+ if (!session && this.ip) {
2188
+ fingerprint = this.fingerprint;
2189
+
2190
+ if (fingerprint) {
2191
+ session = alchemy.fingerprints.get(fingerprint);
2192
+
2193
+ if (session && session.id) {
2194
+ session_id = session.id;
2195
+ this.cookie(cookie_name, session_id, {httpOnly: true});
2196
+ }
2197
+ }
2198
+ }
2199
+
2200
+ // If no valid session exists, create a new one
2201
+ if (!session && allow_create) {
2202
+ session = new Classes.Alchemy.ClientSession(this);
2203
+ session_id = session.id;
2204
+
2205
+ if (fingerprint) {
2206
+ alchemy.fingerprints.set(fingerprint, session);
2207
+ }
2208
+
2209
+ this.cookie(cookie_name, session_id, {httpOnly: true});
2210
+
2211
+ alchemy.sessions.set(session_id, session);
2212
+ }
2213
+
2214
+ if (session) {
2215
+ this.sessionData = session;
2216
+ session.request_count++;
2217
+ } else {
2218
+ return false;
2219
+ }
2220
+
2221
+ return session;
2222
+ });
2223
+
2224
+ /**
2225
+ * Register live data bindings via websockets
2226
+ *
2227
+ * @author Jelle De Loecker <jelle@develry.be>
2228
+ * @since 0.2.0
2229
+ * @version 0.4.0
2230
+ */
2231
+ Conduit.setMethod(function registerBindings(arr) {
2232
+
2233
+ var data_ids;
2234
+
2235
+ // Don't do anything is websockets aren't enabled
2236
+ if (!alchemy.settings.websockets) {
2237
+ return;
2238
+ }
2239
+
2240
+ if (arr) {
2241
+ data_ids = arr;
2242
+ } else {
2243
+ data_ids = this.renderer.live_bindings;
2244
+ }
2245
+
2246
+ if (Object.isEmpty(data_ids)) {
2247
+ return;
2248
+ }
2249
+
2250
+ this.getSession().registerBindings(data_ids, this.sceneId);
2251
+ });
2252
+
2253
+ /**
2254
+ * Get a a value from the session object
2255
+ *
2256
+ * @author Jelle De Loecker <jelle@develry.be>
2257
+ * @since 0.2.0
2258
+ * @version 0.4.0
2259
+ *
2260
+ * @param {String} name
2261
+ * @param {Mixed} value
2262
+ *
2263
+ * @return {Mixed}
2264
+ */
2265
+ Conduit.setMethod(function session(name, value) {
2266
+
2267
+ this.getSession();
2268
+
2269
+ if (arguments.length === 0) {
2270
+ return this.sessionData;
2271
+ }
2272
+
2273
+ if (arguments.length === 1) {
2274
+ return this.sessionData[name];
2275
+ }
2276
+
2277
+ this.sessionData[name] = value;
2278
+ });
2279
+
2280
+ /**
2281
+ * Get a parameter from the route
2282
+ *
2283
+ * @author Jelle De Loecker <jelle@develry.be>
2284
+ * @since 0.2.0
2285
+ * @version 0.2.0
2286
+ *
2287
+ * @param {String} name
2288
+ */
2289
+ Conduit.setMethod(function routeParam(name) {
2290
+ return this.params[name];
2291
+ });
2292
+
2293
+ /**
2294
+ * Get/set a cookie
2295
+ *
2296
+ * @author Jelle De Loecker <jelle@develry.be>
2297
+ * @since 0.2.0
2298
+ * @version 0.4.2
2299
+ *
2300
+ * @param {String} name
2301
+ * @param {Mixed} value
2302
+ * @param {Object} options
2303
+ */
2304
+ Conduit.setMethod(function cookie(name, value, options) {
2305
+
2306
+ var header,
2307
+ arr,
2308
+ key;
2309
+
2310
+ // Return if cookies are disabled
2311
+ if (!alchemy.settings.cookies) {
2312
+ return;
2313
+ }
2314
+
2315
+ if (arguments.length == 1) {
2316
+ return this.new_cookies[name] || this.cookies[name];
2317
+ }
2318
+
2319
+ if (options == null) options = {};
2320
+
2321
+ // If the value is null or undefined, the cookie should be removed
2322
+ if (value == null) {
2323
+ options.expires = new Date(0);
2324
+ }
2325
+
2326
+ // If no path is given, default to the root path
2327
+ if (options.path == null) options.path = '/';
2328
+
2329
+ // If the `secure` flag is not set,
2330
+ // see if this connection is secure
2331
+ if (options.secure == null) {
2332
+ if (this.is_secure) {
2333
+ options.secure = true;
2334
+ }
2335
+ }
2336
+
2337
+ // Store it in the new_cookies object, for quick access
2338
+ this.new_cookies[name] = value;
2339
+
2340
+ if (this.websocket) {
2341
+ return this.socket.emit('alchemy-set-cookie', {name: name, value: value, options: options});
2342
+ }
2343
+
2344
+ // Create the basic header string
2345
+ header = String.encodeCookie(name, value, options);
2346
+
2347
+ // Add this to the cookieheader array
2348
+ this.new_cookie_header.push(header);
2349
+ });
2350
+
2351
+ /**
2352
+ * Set a response header
2353
+ *
2354
+ * @author Jelle De Loecker <jelle@develry.be>
2355
+ * @since 0.2.0
2356
+ * @version 1.1.0
2357
+ *
2358
+ * @param {String} name
2359
+ * @param {Mixed} value
2360
+ */
2361
+ Conduit.setMethod(function setHeader(name, value) {
2362
+
2363
+ if (arguments.length == 1) {
2364
+ return this.getHeader(name);
2365
+ }
2366
+
2367
+ if (this.websocket) {
2368
+ throw new Error("Can't set a header on a websocket connection");
2369
+ }
2370
+
2371
+ this.response_headers[name] = value;
2372
+ });
2373
+
2374
+ /**
2375
+ * Get a response header
2376
+ *
2377
+ * @author Jelle De Loecker <jelle@develry.be>
2378
+ * @since 1.1.0
2379
+ * @version 1.1.0
2380
+ *
2381
+ * @param {String} name
2382
+ */
2383
+ Conduit.setMethod(function getHeader(name) {
2384
+
2385
+ if (this.response_headers[name] != null) {
2386
+ return this.response_headers[name];
2387
+ }
2388
+
2389
+ if (this.response) {
2390
+ return this.response.getHeader(name);
2391
+ }
2392
+ });
2393
+
2394
+ /**
2395
+ * Update data to this scene only
2396
+ *
2397
+ * @author Jelle De Loecker <jelle@develry.be>
2398
+ * @since 0.2.0
2399
+ * @version 0.4.0
2400
+ *
2401
+ * @param {String} name
2402
+ * @param {Mixed} value
2403
+ */
2404
+ Conduit.setMethod(function update(name, value) {
2405
+
2406
+ // Make sure a scene id is created
2407
+ this.createScene();
2408
+
2409
+ // Send this update to this scene only
2410
+ this.getSession().sendDataUpdate(name, value, this.sceneId);
2411
+ });
2412
+
2413
+ /**
2414
+ * Push a flash message to the client
2415
+ *
2416
+ * @author Jelle De Loecker <jelle@develry.be>
2417
+ * @since 0.2.0
2418
+ * @version 0.2.0
2419
+ */
2420
+ Conduit.setMethod(function flash(message, options) {
2421
+
2422
+ var newFlashes;
2423
+
2424
+ if (options == null) {
2425
+ options = {};
2426
+ }
2427
+
2428
+ newFlashes = this.internal('newFlashes');
2429
+
2430
+ if (newFlashes == null) {
2431
+ newFlashes = {};
2432
+ }
2433
+
2434
+ newFlashes[Date.now() + '-' + Number.random(100)] = {
2435
+ message: message,
2436
+ options: options
2437
+ };
2438
+
2439
+ this.internal('newFlashes', newFlashes);
2440
+ });
2441
+
2442
+ /**
2443
+ * Set a theme to use
2444
+ *
2445
+ * @author Jelle De Loecker <jelle@develry.be>
2446
+ * @since 0.2.0
2447
+ * @version 0.2.0
2448
+ *
2449
+ * @param {String} name
2450
+ */
2451
+ Conduit.setMethod(function setTheme(name) {
2452
+ this.theme = name;
2453
+ this.renderer.setTheme(name);
2454
+ });
2455
+
2456
+ /**
2457
+ * Does this user support a certain feature?
2458
+ *
2459
+ * @author Jelle De Loecker <jelle@develry.be>
2460
+ * @since 1.0.4
2461
+ * @version 1.0.4
2462
+ *
2463
+ * @param {String} feature
2464
+ *
2465
+ * @return {Boolean}
2466
+ */
2467
+ Conduit.setMethod(function supports(feature) {
2468
+
2469
+ if (this.useragent && (feature == 'async' || feature == 'await')) {
2470
+ let agent = this.useragent;
2471
+
2472
+ if (agent.family == 'IE') {
2473
+ return false;
2474
+ }
2475
+
2476
+ if (agent.family == 'Edge' && agent.major < 15) {
2477
+ return false;
2478
+ }
2479
+
2480
+ // Its actually supported on 10.1, but oh well
2481
+ if (agent.family == 'Safari' && agent.major < 11) {
2482
+ return false;
2483
+ }
2484
+
2485
+ if (agent.family == 'Samsung Internet' && agent.major < 6) {
2486
+ return false;
2487
+ }
2488
+
2489
+ if (agent.family == 'Opera Mini') {
2490
+ return false;
2491
+ }
2492
+ }
2493
+
2494
+ return null;
2495
+ });
2496
+
2497
+ /**
2498
+ * Broadcast data to every connected user
2499
+ *
2500
+ * @author Jelle De Loecker <jelle@develry.be>
2501
+ * @since 0.3.0
2502
+ * @version 0.4.0
2503
+ *
2504
+ * @param {String} type
2505
+ * @param {Object} data
2506
+ */
2507
+ Alchemy.setMethod(function broadcast(type, data) {
2508
+
2509
+ alchemy.sessions.forEach(function eachSession(session, key) {
2510
+
2511
+ // Go over every listening scene and submit the data
2512
+ Object.each(session.connections, function eachScene(scene, scene_id) {
2513
+
2514
+ if (!scene) {
2515
+ return;
2516
+ }
2517
+
2518
+ if (alchemy.settings.debug) {
2519
+ log.debug('Broadcasting', type, {data, scene});
2520
+ }
2521
+
2522
+ scene.submit(type, data);
2523
+ });
2524
+ });
2525
+ });
2526
+
2527
+ /**
2528
+ * Get the magic mimetype function
2529
+ *
2530
+ * @author Jelle De Loecker <jelle@develry.be>
2531
+ * @since 0.3.0
2532
+ * @version 0.3.0
2533
+ */
2534
+ function getMagic() {
2535
+
2536
+ var mmmagic;
2537
+
2538
+ if (magic || magic === false) {
2539
+ return magic;
2540
+ }
2541
+
2542
+ // Get mmmagic module
2543
+ mmmagic = alchemy.use('mmmagic')
2544
+
2545
+ if (mmmagic) {
2546
+ magic = new mmmagic.Magic(mmmagic.MAGIC_MIME_TYPE);
2547
+ } else {
2548
+ log.error('Could not load mmmagic module');
2549
+ magic = false;
2550
+ }
2551
+
2552
+ return magic;
2553
+ }
2554
+
2555
+ global.Conduit = Conduit;