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