alchemymvc 1.1.5 → 1.1.9

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