forked from nchen/artful-newlisp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
web.lsp
720 lines (650 loc) · 32.1 KB
/
web.lsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
#!/usr/bin/newlisp
;; @module Web
;; @author Jeff Ober <[email protected]>, Kanen Flowers <[email protected]>
;; @version 0.3.3
;; @location http://www.ScruffyThinking.com/artful
;; @package https://github.com/kanendosei/artful-newlisp/blob/master/web.qwerty
;; @description A collection of functions for writing web-based software.
;; <b>Features:</b>
;; <ul>
;; <li>ASP/PHP-style templates</li>
;; <li>Cookies</li>
;; <li>Entities translation</li>
;; <li>GET/POST parameters</li>
;; <li>HTTP header control</li>
;; <li>Sessions</li>
;; <li>URL building and parsing</li>
;; <li>URL encoding and decoding</li>
;; <li>query building and parsing</li>
;; </ul>
;; <b>Known issues</b>
;; <ul>
;; <li>
;; When used in conjunction with the official
;; @link http://newlisp.nfshost.com/code/modules/cgi.lsp.html CGI
;; module, @link http://newlisp.nfshost.com/code/modules/cgi.lsp.html CGI must be loaded first. In the case of
;; identical GET and POST parameters, the value is stored in GET, but the value will be POST. This
;; is due to the fact that CGI stores both GET and POST in the same association list and overwrites
;; GET values with POST.
;; </li>
;; </ul>
;;
;; <b>Note:</b> for JSON encoding and decoding, see the @link http://static.artfulcode.net/newlisp/json.lsp.html Json module.
;;
;; <h4>To do</h4>
;; • add MIME decoding for multipart posts
;;
;; <h4>Version history</h4>
;; <b>0.3.1</b>
;; • fixed ineffective usage of set/setf
;;
;; <b>0.3</b>
;; • made parse-query more tolerant and fixed parsing bug
;; • cookie now accepts an additional parameter that only permits access during HTTPS sessions
;;
;; <b>0.2</b>
;; • build-url now accepts query strings in addition to assoc lists
;; • session-id now accepts an optional parameter to set the session id
;; • fixed some typos with 'clean-sessions'
;; • fixed extra parameter in 'define-session-handlers'
;;
;; <b>0.1</b>
;; • initial release
;;
(context 'Web)
;===============================================================================
; !Constants and definitions
;===============================================================================
(constant 'POST_LIMIT 4096)
(define GET)
(define POST)
(define COOKIE)
(define SESSION_DIR "/tmp")
(define SESSION_MAX_AGE (* 60 60 24 7)) ; seconds
(define SESSION_KEY "NLWSESID")
(define SESSION_PREFIX "NLWSES")
(define SESSION_STARTED)
(define SESSION_ID) ; stores the current session id
;===============================================================================
; !Encoding and decoding
;===============================================================================
(define ENTITIES
(list
(list 34 {"}) (list 38 {&}) (list 39 {'}) (list 60 {<})
(list 62 {>}) (list 160 { }) (list 161 {¡}) (list 162 {¢})
(list 163 {£}) (list 164 {¤}) (list 165 {¥}) (list 166 {¦})
(list 167 {§}) (list 168 {¨}) (list 169 {©}) (list 170 {ª})
(list 171 {«}) (list 172 {¬}) (list 173 {­}) (list 174 {®})
(list 175 {¯}) (list 176 {°}) (list 177 {±}) (list 178 {²})
(list 179 {³}) (list 180 {´}) (list 181 {µ}) (list 182 {¶})
(list 183 {·}) (list 184 {¸}) (list 185 {¹}) (list 186 {º})
(list 187 {»}) (list 188 {¼}) (list 189 {½}) (list 190 {¾})
(list 191 {¿}) (list 192 {À}) (list 193 {Á}) (list 194 {Â})
(list 195 {Ã}) (list 196 {Ä}) (list 197 {Å}) (list 198 {Æ})
(list 199 {Ç}) (list 200 {È}) (list 201 {É}) (list 202 {Ê})
(list 203 {Ë}) (list 204 {Ì}) (list 205 {Í}) (list 206 {Î})
(list 207 {Ï}) (list 208 {Ð}) (list 209 {Ñ}) (list 210 {Ò})
(list 211 {Ó}) (list 212 {Ô}) (list 213 {Õ}) (list 214 {Ö})
(list 215 {×}) (list 216 {Ø}) (list 217 {Ù}) (list 218 {Ú})
(list 219 {Û}) (list 220 {Ü}) (list 221 {Ý}) (list 222 {Þ})
(list 223 {ß}) (list 224 {à}) (list 225 {á}) (list 226 {â})
(list 227 {ã}) (list 228 {ä}) (list 229 {å}) (list 230 {æ})
(list 231 {ç}) (list 232 {è}) (list 233 {é}) (list 234 {ê})
(list 235 {ë}) (list 236 {ì}) (list 237 {í}) (list 238 {î})
(list 239 {ï}) (list 240 {ð}) (list 241 {ñ}) (list 242 {ò})
(list 243 {ó}) (list 244 {ô}) (list 245 {õ}) (list 246 {ö})
(list 247 {÷}) (list 248 {ø}) (list 249 {ù}) (list 250 {ú})
(list 251 {û}) (list 252 {ü}) (list 253 {ý}) (list 254 {þ})
(list 255 {ÿ}) (list 338 {Œ}) (list 339 {œ}) (list 352 {Š})
(list 353 {š}) (list 376 {Ÿ}) (list 402 {ƒ}) (list 710 {ˆ})
(list 732 {˜}) (list 913 {Α}) (list 914 {Β}) (list 915 {Γ})
(list 916 {Δ}) (list 917 {Ε}) (list 918 {Ζ}) (list 919 {Η})
(list 920 {Θ}) (list 921 {Ι}) (list 922 {Κ}) (list 923 {Λ})
(list 924 {Μ}) (list 925 {Ν}) (list 926 {Ξ}) (list 927 {Ο})
(list 928 {Π}) (list 929 {Ρ}) (list 931 {Σ}) (list 932 {Τ})
(list 933 {Υ}) (list 934 {Φ}) (list 935 {Χ}) (list 936 {Ψ})
(list 937 {Ω}) (list 945 {α}) (list 946 {β}) (list 947 {γ})
(list 948 {δ}) (list 949 {ε}) (list 950 {ζ}) (list 951 {η})
(list 952 {θ}) (list 953 {ι}) (list 954 {κ}) (list 955 {λ})
(list 956 {μ}) (list 957 {ν}) (list 958 {ξ}) (list 959 {ο})
(list 960 {π}) (list 961 {ρ}) (list 962 {ς}) (list 963 {σ})
(list 964 {τ}) (list 965 {υ}) (list 966 {φ}) (list 967 {χ})
(list 968 {ψ}) (list 969 {ω}) (list 977 {ϑ}) (list 978 {ϒ})
(list 982 {ϖ}) (list 8194 { }) (list 8195 { }) (list 8201 { })
(list 8204 {‌}) (list 8204 {‍}) (list 8204 {‎}) (list 8204 {‏})
(list 8211 {–}) (list 8212 {—}) (list 8216 {‘}) (list 8217 {’})
(list 8218 {‚}) (list 8220 {“}) (list 8221 {”}) (list 8222 {„})
(list 8224 {†}) (list 8225 {‡}) (list 8226 {•}) (list 8230 {…})
(list 8240 {‰}) (list 8242 {′}) (list 8243 {″}) (list 8249 {‹})
(list 8250 {›}) (list 8254 {‾}) (list 8260 {⁄}) (list 8364 {€})
(list 8465 {ℑ}) (list 8472 {℘}) (list 8476 {ℜ}) (list 8482 {™})
(list 8501 {ℵ}) (list 8592 {←}) (list 8593 {↑}) (list 8594 {→})
(list 8595 {↓}) (list 8596 {↔}) (list 8629 {↵}) (list 8656 {⇐})
(list 8657 {⇑}) (list 8658 {⇒}) (list 8659 {⇓}) (list 8660 {⇔})
(list 8704 {∀}) (list 8706 {∂}) (list 8707 {∃}) (list 8709 {∅})
(list 8711 {∇}) (list 8712 {∈}) (list 8713 {∉}) (list 8715 {∋})
(list 8719 {∏}) (list 8721 {∑}) (list 8722 {−}) (list 8727 {∗})
(list 8730 {√}) (list 8733 {∝}) (list 8734 {∞}) (list 8736 {∠})
(list 8743 {∧}) (list 8744 {∨}) (list 8745 {∩}) (list 8746 {∪})
(list 8747 {∫}) (list 8756 {∴}) (list 8764 {∼}) (list 8773 {≅})
(list 8776 {≈}) (list 8800 {≠}) (list 8801 {≡}) (list 8804 {≤})
(list 8805 {≥}) (list 8834 {⊂}) (list 8835 {⊃}) (list 8836 {⊄})
(list 8838 {⊆}) (list 8839 {⊇}) (list 8853 {⊕}) (list 8855 {⊗})
(list 8869 {⊥}) (list 8901 {⋅}) (list 8968 {⌈}) (list 8969 {⌉})
(list 8970 {⌊}) (list 8971 {⌋}) (list 9001 {⟨}) (list 9002 {⟩})
(list 9674 {◊}) (list 9824 {♠}) (list 9827 {♣}) (list 9829 {♥})
(list 9830 {♦})))
(define UNENTITIES
(map reverse ENTITIES))
(define JS_ESCAPE_CHARS
(list
(list {\} {\\})
(list {"} {\"})
(list {'} {\'})
(list "\n" {\n})
(list "\r" {\r})
(list "</" {<\/})))
;; @syntax (Web:escape-js <str>)
;; @param <str> a string to escape
;; <p>Escapes a string for output in javascript. Does not encode entities;
;; just prevents control characters from causing syntax errors in javascript.</p>
(define (escape-js str)
(dolist (ch JS_ESCAPE_CHARS)
(replace (first ch) str (last ch)))
str)
;; @syntax (Web:escape <str>)
;; @param <str> a string to escape
;; @return the escaped string
;; <p>Escapes characters that are part of the (X)HTML and XML syntax to prevent
;; characters from confusing browsers' parsing of markup. Escapes single and
;; double quotes, ampersands, and left and right angle brackets
;; ('"', ''', '&', '<', and '>').</p>
(define (escape str)
(replace {&} str {&})
(replace {"} str {"})
(replace {'} str {'})
(replace {<} str {<})
(replace {>} str {>})
str)
;; @syntax (Web:unescape <str>)
;; @param <str> an entity-escaped string
;; @return the unescaped string
;; <p>Unescapes the basic (X)HTML and XML character entities in a string.</p>
(define (unescape str)
(replace {"} str {"})
(replace {'} str {'})
(replace {&} str {&})
(replace {<} str {<})
(replace {>} str {>})
str)
;; @syntax (Web:encode-entities <str>)
;; @param <str> a string to escape
;; @return the escaped string
;; <p>Escapes characters with a much larger set of character entities than
;; 'escape' using a table derived from
;; @link http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references Wikipedia.
(define (encode-entities str , ent (buf ""))
(dostring (c str)
(write-buffer buf
(if (setf ent (lookup c ENTITIES)) ent (char c))))
buf)
;; @syntax (Web:decode-entities <str>)
;; @param <str> an entity-encoded string
;; @return the decoded string
;; <p>Translates character entities to their character equivalents as well as
;; numeric entities.</p>
(define (decode-entities str)
(replace {&(\d+);} str (char (int $1)) 0)
(replace {(&\S+?;)} str (char (lookup $1 UNENTITIES)) 0))
; Translates a single character into a hex-encoded string suitable for a URL.
(define (hex-encode-char ch)
(if (= " " ch) "+" (format "%%%x" (char ch))))
; Translates a URL-encoded hex into a string character.
(define (hex-decode-char ch)
(when (starts-with ch "%")
(pop ch))
(char (int (append "0x" $1))))
;; @syntax (Web:url-encode <str>)
;; @param <str> a string token to encode for use in a URL
;; @return the URL-encoded string
;; <p>Encodes a string for use in a URL.</p>
(constant 'REGEX_HTTP_SPECIAL_CHAR (regex-comp {([^-_.$+!*'()0-9a-z])} 1))
(define (url-encode str)
(replace " " str "+")
(replace REGEX_HTTP_SPECIAL_CHAR str (hex-encode-char $1) 0x10000))
;; @syntax (Web:url-decode <str>)
;; @param <str> a URL-encoded string
;; @return the decoded string
;; <p>Decodes hexidecimals and spaces (represented as '+') in a URL-encoded string.</p>
(constant 'REGEX_HEX_ENCODED_CHAR (regex-comp {%([0-9A-F][0-9A-F])} 1))
(define (url-decode str)
(replace "+" str " ")
(replace REGEX_HEX_ENCODED_CHAR str (hex-decode-char $1) 0x10000))
;; @syntax (Web:parse-query <query-string>)
;; @param <query-string> a URL-encoded query string
;; @return an association list of decoded key-value pairs
;; <p>Parses a URL-encoded query string and returns a list of key-values pairs.</p>
(constant 'REGEX_QUERY (regex-comp {&([^&=]+?)=([^&=]+?)(?=&|$)} 1))
(define (parse-query query)
(when (starts-with query "?")
(pop query))
(push "&" query)
(find-all REGEX_QUERY query (list (url-decode $1) (url-decode $2)) 0x10000))
;; @syntax (Web:build-query <a-list>)
;; @param <a-list> an association list
;; @return a URL-encoded query string
;; <p>Builds a URL-encoded query string using <a-list>. Does not include the leading
;; question mark (so queries may be easily built of association list fragments.)</p>
(define (build-query alist , query)
(join (map (fn (pair) (join (map url-encode pair) "=")) alist) "&"))
;; @syntax (Web:parse-url <str-url>)
;; @param <str-url> a URL
;; @return an association list with the decomposed URL's parts
;; <p>Parses a URL and returns an association list of its decomposed parts. The list's
;; keys (as strings) are: scheme, user, pass, host, port, path, query, and fragment.
;; Also handles IPV6 addresses. Modeled on the PHP function of the same name.</p>
;;
;; Parsing based on code from @link http://us3.php.net/manual/en/function.parse-url.php#90365 this comment.
(constant 'REGEX_URL
(regex-comp
[text]
(?:([a-z0-9+-._]+)://)?
(?:
(?:((?:[a-z0-9-._~!$&'()*+,;=:]|%[0-9a-f]{2})*)@)?
(?:\[((?:[a-z0-9:])*)\])?
((?:[a-z0-9-._~!$&'()*+,;=]|%[0-9a-f]{2})*)
(?::(\d*))?
(/(?:[a-z0-9-._~!$&'()*+,;=:@/]|%[0-9a-f]{2})*)?
|
(/?
(?:[a-z0-9-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+
(?:[a-z0-9-._~!$&'()*+,;=:@/]|%[0-9a-f]{2})*
)?
)
(?:\?((?:[a-z0-9-._~!$&'()*+,;=:/?@]|%[0-9a-f]{2})*))?
(?:\#((?:[a-z0-9-._~!$&'()*+,;=:/?@]|%[0-9a-f]{2})*))?
[/text]
(| 1 8)))
(define (parse-url url)
;; clear indices of previous matches
(dolist (idx '($0 $1 $2 $3 $4 $5 $6 $7 $8 $9))
(set idx nil))
(when (regex REGEX_URL url 0x10000)
(let ((user-pass (parse $2 ":")))
(list
(list "scheme" (if (null? $1) "http" $1))
(list "user" (when user-pass (first user-pass)))
(list "pass" (when (and user-pass (= (length user-pass) 2)) (last user-pass)))
(list "host" (if-not (null? $3) $3 $4))
(list "port" (if (null? $5) nil $5))
(list "path" (if (and (null? $6) (null? $7)) "/" (string $6 $7)))
(list "query" (if (null? $8) nil $8))
(list "fragment" (if (null? $9) nil $9))))))
;; @syntax (Web:build-url <str-url> [<list-query-params> ...])
;; @param <str-url> a string URL
;; @param <list-query-params> one or more association lists of query parameters and their values
;;
;; @syntax (Web:build-url <list-url> [<list-query-params> ...])
;; @param <list-url> an association list of URL components using the structure of <parse-url>'s return value
;; @param <list-query-params> one or more association lists of query parameters and their values
;; @return a URL string composed of the initial URL data plus subsequently superseding query parameters
;; <p>In the first syntax, builds a URL from an existing URL string.
;; In the second syntax, builds a URL from an association list in the same
;; format as the return value of <parse-url>, with both keys and values being
;; strings. In both syntaxes, any number of additional association lists of
;; key/value pairs may be passed, which are serialized as query parameters, with
;; each list overriding the previous. If there are query parameters in the
;; initial URL, they are used as the initial list with the lowest priority.</p>
(define (build-url url)
(when (string? url)
(setf url (parse-url url)))
(local (params)
;; Build parameter list
(setf params '())
(dolist (pairs (cons (lookup "query" url) (args)))
(when (string? pairs) (setf pairs (parse-query pairs)))
(dolist (pair pairs)
(if (assoc (first pair) params)
(setf (assoc (first pair) params) pair)
(push pair params))))
(format "%s://%s%s%s%s%s%s"
(or (lookup "scheme" url) "http")
(cond
((and (lookup "user" url) (lookup "pass" url))
(string (lookup "user" url) ":" (lookup "pass" url) "@"))
((lookup "user" url)
(string (lookup "user" url) "@"))
(true ""))
(lookup "host" url)
(if (lookup "port" url) (string ":" (lookup "port" url)) "")
(lookup "path" url)
(if (null? params) "" (string "?" (build-query params)))
(if (lookup "fragment" url) (string "#" (lookup "fragment" url)) ""))))
;===============================================================================
; !Headers, COOKIES, GET, and POST
;===============================================================================
;; @syntax (Web:header <str-key> <str-value>)
;; @param <str-key> the header name (e.g., "Content-type")
;; @param <str-value> the header value (e.g., "text/html")
;; <p>Sets an HTTP output header. Headers are printed using 'Web:send-headers'.</p>
(define headers '(("Content-type" "text/html")))
(define (header key value)
(if (lookup key headers)
(setf (assoc key headers) (list key value))
(push (list key value) headers -1)))
;; @syntax (Web:redir <str-url>)
;; @param <str-url> a URL string
;; <p>Redirects the client to <str-url>.</p>
(define (redir url)
(header "Location" url))
;; @syntax (Web:send-headers)
;; <p>Writes the HTTP headers to stdout. This function should be called regardless
;; of whether any headers have been manually set to ensure that the minimum HTTP
;; headers are properly sent. Note: no check is made to verify that output has not
;; already begun.</p>
(define (send-headers)
(dolist (header headers)
(print (format "%s: %s\n" (first header) (last header))))
(println))
;; @syntax (Web:cookie <str-key>)
;; @param <str-key> the cookie's name
;;
;; @syntax (Web:cookie <str-key> <str-value> [<int-expires> [<str-path> [<str-domain> [<bool-http-only> [<bool-secure-only>]]]])
;; @param <str-key> the cookie's name
;; @param <str-key> the cookie's value
;; @param <int-expires> (optional) the expiration date of the cookie as a unix timestamp; default is a session cookie
;; @param <str-path> (optional) the cookie's path; default is the current path
;; @param <str-domain> (optional) the cookie's domain; default is the current host
;; @param <bool-http-only> (optional) whether the cookie may be read by client-side scripts
;; @param <bool-secure-only> (optional) whether the cookie may be accessed/set outside of HTTPS
;; <p>In the first syntax, 'cookie' returns the value of the cookie named <str-key> or 'nil'. If
;; <str-key> is not provided, an association list of all cookie values is returned.</p>
;; <p>In the second syntax, 'cookie' sets a new cookie or overwrites an existing cookie in the
;; client's browser. Note that <bool-http-only> defaults to true, but is not standard and
;; therefore is not necessarily implemented in all browsers. <bool-secure-only> defaults to nil.
;; Cookies use the 'header' function and must be sent before calling 'send-headers'.</p>
(define (cookie key value expires path domain http-only secure)
(cond
((null? key) COOKIES)
((and (null? value) COOKIE)
(lookup key COOKIE))
(true
(when (or (not secure) (and secure (starts-with (lower-case (env "SERVER_PROTOCOL")) "https")))
(header "Set-Cookie"
(format "%s=%s%s%s%s%s"
(url-encode (string key))
(url-encode (string value))
(if expires (string "; expires=" (date expires 0 "%a, %d-%b-%Y %H:%M:%S %Z")) "")
(if path (string "; path=" path) "")
(if domain (string "; domain=" domain) "")
(if-not http-only "; HttpOnly" "")))))))
;; @syntax (Web:get <str-key>)
;; <p>Returns the value of <str-key> in the query string or 'nil' if not present.
;; If <str-key> is not provided, returns an association list of all GET values.</p>
(define (get key)
(when GET (if key (lookup key GET) GET)))
;; @syntax (Web:post <str-key>)
;; <p>Returns the value of <str-key> in the client-submitted POST data or 'nil' if
;; not present. If <str-key> is not provided, returns an association list of all
;; POST values.</p>
(define (post key)
(when POST (if key (lookup key POST) POST)))
;===============================================================================
; !Session control
; notes:
; * sessions require cookies to function
; * close-session or MAIN:exit must be called to save session changes to disk
;===============================================================================
;; @syntax (Web:define-session-handlers <fn-open> <fn-close> <fn-delete> <fn-clear> <fn-clean>)
;; @param <fn-open> function to begin a new session
;; @param <fn-close> function to close a session, saving changes
;; @param <fn-delete> function to delete a session
;; @param <fn-clean> function to prune old sessions
;; <p>Defines handler functions to be called when various session control
;; functions are used, making custom session storage a fairly simple matter.</p>
;; The required handler functions are:
;; <ul>
;; <li>'fn-open': called by 'open-session'; resumes or starts a new session storage instance, initializing the context tree</li>
;; <li>'fn-close': called by 'close-session'; writes changes to a session to storage</li>
;; <li>'fn-delete': called by 'delete-session'; deletes the entire session from storage</li>
;; <li>'fn-clean': called by 'clean-sessions'; prunes old stored sessions</li>
;; </ul>
;; Some useful functions and variables for handler functions:
;; <ul>
;; <li>'session-id': function that returns the current session id and sets the session cookie when necessary</li>
;; <li>'session-context': function that returns the session context dictionary</li>
;; <li>'SESSION_MAX_AGE': a variable storing the number of seconds after which an orphan session should be deleted</li>
;; </ul>
(define (define-session-handlers fn-open fn-close fn-delete fn-clean)
(setf _open-session fn-open
_close-session fn-close
_delete-session fn-delete
_clean-sessions fn-clean))
;; @syntax (Web:session-id [<str-sid>])
;; @param <str-sid> (optional) the session ID to use
;; @return a unique session id for the client
;; <p>Creates or retrieves the client's session id. If this is a new session id,
;; a cookie is set in the client's browser to identify it on future loads.</p>
;; <p>If <str-sid> is provided, it will be used as the new session ID.</p>
(define (session-id sid)
(setf SESSION_ID
(or (when sid
(cookie SESSION_KEY sid)
sid)
SESSION_ID
(cookie SESSION_KEY)
(begin
(setf sid (string SESSION_PREFIX "-" (uuid)))
(cookie SESSION_KEY sid)
sid))))
;; @syntax (Web:session-context)
;; @return a symbol pointing to the current session's context dictionary
;; <p>Run-time session data is stored in a context tree. 'session-context'
;; returns the current session tree or creates a new one when necessary.
;; This function is primarily intended for session handlers' use; it is
;; typically more useful to call 'session' on its own to retrieve an association
;; list of key/value pairs in an application.</p>
(define (session-context , ctx)
(setf ctx (sym (session-id) 'MAIN))
(unless (context? ctx)
(context ctx))
ctx)
;; @syntax (Web:open-session)
;; <p>Initializes the client's session.</p>
(define (open-session)
(_open-session)
(setf SESSION_STARTED true)
(session-id))
;; @syntax (close-session)
;; <p>Writes any changes to the session to file. This is automatically called
;; when the distribution function 'exit' is called.</p>
(define (close-session)
(when SESSION_STARTED
(_close-session)))
;; @syntax (delete-session)
;; <p>Deletes the session. Sessions are then turned off and 'open-session'
;; must be called again to use sessions further.</p>
(define (delete-session)
(unless SESSION_STARTED (throw-error "session is not started"))
(_delete-session)
(delete (session-context))
(cookie SESSION_KEY "" 0)
(setf SESSION_STARTED nil))
;; @syntax (clear-session)
;; <p>Clears all session variables.</p>
(define (clear-session)
(when SESSION_STARTED
(dotree (s (session-context))
(delete (sym s (session-context))))))
;; @syntax (clean-sessions)
;; <p>Cleans old session files. This function is not currently called automatically;
;; note that there is the possibility of a race condition with this function and other
;; session handling functions.</p>
(define (clean-sessions)
(_clean-sessions))
;; @syntax (session [<str-key> [<str-value>]])
;; @param <str-key> the session key
;; @param <str-value> the new value
;; When called with both <str-key> and <str-value>, sets the session variable. When
;; called with only <str-key>, returns the value of <str-key>. Otherwise, returns
;; an association list of session variables. Returns nil if the session is not
;; opened.
(define (session key value)
(cond
((not SESSION_STARTED) nil)
((and key value) (context (session-context) key value))
((true? key) (context (session-context) key))
(true (let ((alist '()))
(dotree (s (session-context))
(push (list (name s) (context (session-context) (name s))) alist -1))
alist))))
;===============================================================================
; !Default session handlers
;
; The default session handlers use newLISP's 'save' and 'load' functions to
; easily serialize and import context data to and from file records. The files
; are stored unencrypted, so a custom handler should be used on a shared
; system.
;===============================================================================
; Returns the name of the file in which the session data is stored.
(define (default-session-file)
(string SESSION_DIR "/" (session-id) ".lsp"))
; Loads/creates the session file; creates a new context tree when
; necessary.
(define (default-open-session)
(if (file? (default-session-file))
(load (default-session-file))
(save (default-session-file) (session-context))))
; Saves the session context to the session file.
(define (default-close-session)
(save (default-session-file) (session-context)))
; Deletes the session file.
(define (default-delete-session)
(when (file? (default-session-file))
(delete-file (default-session-file))))
; Deletes old session files.
(define (default-clean-sessions , f)
(dolist (tmp-file (directory SESSION_DIR))
(when (starts-with tmp-file SESSION_PREFIX)
(setf f (string SESSION_DIR "/" tmp-file))
(when (> (- (date-value) (file-info f 5 nil)) SESSION_MAX_AGE)
(delete-file f)))))
;===============================================================================
; !Templating
;===============================================================================
;; @syntax (Web:eval-template <str-template> <ctx-context>)
;; @param <str-template> a string containing the template syntax
;; @param <ctx-context> the context in which to evaluate the template
;; <p>Translates a template using ASP-like tags, creating small islands of
;; newLISP code in an HTML (or other) document. This is similar to the
;; distribution CGI module's 'put-page' function, except that the short-cut
;; <%= foo %> is used to simply output the value of 'foo' and tags
;; may span multiple lines.</p>
;; <p>Note that the opening and closing tags may be changed by setting the
;; values of 'Web:OPEN_TAG' and 'Web:CLOSE_TAG' if desired. The shortcut
;; print tag will be 'Web:OPEN_TAG' + '='.</p>
;; @example
;; (Web:eval-template "<p><%= (* 3 3) %></p>")
;; => "<p>9</p>"
;; (Web:eval-template "<p><% (println (* 3 3)) %></p>")
;; => "<p>9</p>"
(define OPEN_TAG "<%")
(define CLOSE_TAG "%>")
(define (eval-template str (ctx MAIN) , start end next-start next-end block (buf ""))
(setf start (find OPEN_TAG str))
(setf end (find CLOSE_TAG str))
;; Prevent use of code island tags inside code island from breaking parsing.
(when (and start end)
(while (and (setf next-end (find CLOSE_TAG (slice str (+ end 2))))
(setf next-start (find OPEN_TAG (slice str (+ end 2))))
(< next-end next-start))
(inc end (+ next-end 2)))
(when (and start (not end)) (throw-error "Unbalanced tags.")))
(while (and start end)
(write-buffer buf (string "(print [text]" (slice str 0 start) "[/text])"))
(setf block (slice str (+ start 2) (- end start 2)))
(if (starts-with block "=")
(write-buffer buf (string "(print " (rest block) ")"))
(write-buffer buf (trim block)))
(setf str (slice str (+ end 2)))
(setf start (find OPEN_TAG str))
(setf end (find CLOSE_TAG str))
;; Prevent use of code island tags inside code island from breaking parsing.
(when (and start end)
(while (and (setf next-end (find CLOSE_TAG (slice str (+ end 2))))
(setf next-start (find OPEN_TAG (slice str (+ end 2))))
(< next-end next-start))
(inc end (+ next-end 2)))
(when (and start (not end)) (throw-error "Unbalanced tags."))))
(write-buffer buf (string "(print [text]" str "[/text])"))
(eval-string buf ctx))
;===============================================================================
; !Module initialization
;
; Install default session handlers and create the GET, POST, and COOKIE data
; structures.
;===============================================================================
; Content-Disposition: form-data; name="file"; filename="white-napkin.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\253\152\191\160\128\144JFIF
; Content-Disposition: form-data; name="text"\r\n\r\nadsf\r\n
(define (mime-decode str , content-type parts re decoded)
(when (setf content-type (regex {^multipart/form-data; boundary=(.+?)$} (env "CONTENT_TYPE") 1))
(setf parts (find-all (string "--" (content-type 3) {\r\n(.+?)(?=--)}) str $1 (| 2 4)))
(dolist (part parts)
(cond
((regex {Content-Disposition: form-data; name="(.+?)"\r\n\r\n(.*?)\s+} part 1)
(push (list $1 $2) decoded -1))
((regex {Content-Disposition: form-data; name="(.+?)"; filename="(.+?)"\r\nContent-Type: (.+?)\r\n\r\n(.*)$} part (| 1 2 4))
(push (list $1 (list (list "filename" $2) (list "content-type" $3) (list "bytes" $4))) decoded -1))))
decoded))
; Install default session handlers
(define-session-handlers
default-open-session
default-close-session
default-delete-session
default-clean-sessions)
; Read GET data
(setf GET
(when (env "QUERY_STRING")
(parse-query (env "QUERY_STRING"))))
; Read POST data
(if-not (context? CGI)
;; CGI module not present; read and parse the POST data ourselves
(let ((post "") (buffer "") (recvd 0) (conln 0))
(when (true? (set 'conln (int (env "CONTENT_LENGTH"))))
(do-while (< recvd conln)
(inc recvd (read (device) buffer (- conln recvd)))
(write post buffer)))
(setf POST (when post (parse-query post))))
;This will replace the above line once mim-decode actually works.
;(setf POST
; (when post
; (if (env "CONTENT_TYPE")
; (mime-decode post)
; (parse-query post)))))
;; CGI module present; try to guess which values in CGI:params are
;; from GET and which are from POST.
(begin
(setf POST '())
(dolist (param CGI:params)
(unless (lookup (first param) GET)
(push param POST)))))
; Read COOKIE data
(setf COOKIE
(when (env "HTTP_COOKIE")
(map
(lambda (cookie , n)
(setf n (find "=" cookie))
(list (url-decode (slice cookie 0 n))
(url-decode (slice cookie (+ 1 n)))))
(parse (env "HTTP_COOKIE") "; *" 0))))
(context 'MAIN)
; This function wraps the distribution exit routine to ensure that sessions are
; written when the application exits. It is only called when the 'exit' function
; is explicitly called. The 'exit' function is renamed 'sys-exit'. The 'Web'
; function 'close-session' is only called on a normal exit (exit code 0.)
(define (exit-with-session-close (n 0))
(when (zero? n)
(Web:close-session))
(MAIN:sys-exit))
(constant 'sys-exit exit)
(constant 'exit exit-with-session-close)