#lang scheme/base
(require (lib "unitsig.ss")
mzlib/etc
mzlib/list
srfi/13/string
srfi/14/char-set
srfi/19/time)
(define-struct cookie (name value expires path domain secure) #:transparent #:mutable)
(define-struct (cookie-error exn) ())
(define set-cookie
(lambda (name value)
(unless (and (cookie-string? name #f)
(cookie-string? value))
(raise (make-cookie-error (format "Invalid NAME/VALUE pair: ~a / ~a" name value) (current-continuation-marks))))
(make-cookie name value
#f #f #f #f )))
(define print-cookie
(lambda (cookie)
(unless (cookie? cookie)
(raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks))))
(string-join
(filter (lambda (s)
(not (string-null? s)))
(list (format "~a=~a" (cookie-name cookie) (cookie-value cookie))
(let ((e (cookie-expires cookie)))
(if e
(format "expires=~a" (expires->rfc822-string e))
""))
(let ((p (cookie-path cookie))) (if p (format "path=~a" p) ""))
(let ((d (cookie-domain cookie))) (if d (format "domain=~a" d) ""))
(let ((s (cookie-secure cookie))) (if s "secure" ""))))
"; ")))
(define cookie:add-domain
(lambda (cookie domain)
(unless (valid-domain? domain)
(raise (make-cookie-error (format "Invalid domain: ~a" domain) (current-continuation-marks))))
(unless (cookie? cookie)
(raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks))))
(set-cookie-domain! cookie domain)
cookie))
(define cookie:add-expires
(lambda (cookie seconds)
(unless (and (integer? seconds) (not (negative? seconds)))
(raise (make-cookie-error (format "Invalid Expires for cookie: ~a" seconds) (current-continuation-marks))))
(unless (cookie? cookie)
(raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks))))
(set-cookie-expires! cookie seconds)
cookie))
(define cookie:add-path
(lambda (cookie path)
(unless (string? path)
(raise (make-cookie-error (format "Invalid path: ~a" path) (current-continuation-marks))))
(unless (cookie? cookie)
(raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks))))
(set-cookie-path! cookie path)
cookie))
(define cookie:secure
(lambda (cookie secure?)
(unless (boolean? secure?)
(raise (make-cookie-error (format "Invalid argument (boolean expected), received: ~a" secure?) (current-continuation-marks))))
(unless (cookie? cookie)
(raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks))))
(set-cookie-secure! cookie secure?)
cookie))
(define char-set:all-but=
(char-set-difference char-set:full (string->char-set "=")))
(define char-set:all-but-semicolon
(char-set-difference char-set:full (string->char-set ";")))
(define get-all-results
(lambda (name cookies)
(let loop ((c cookies))
(cond ((null? c) null)
(else
(let ((pair (car c)))
(if (string=? name (car pair))
(cons (cadr pair) (loop (cdr c)))
(loop (cdr c)))))))))
(define get-cookie
(lambda (name cookies)
(let ((cookies (map (lambda (p)
(map string-trim-both
(string-tokenize p char-set:all-but=)))
(string-tokenize cookies char-set:all-but-semicolon))))
(get-all-results name cookies))))
(define get-cookie/single
(lambda (name cookies)
(let ((cookies (get-cookie name cookies)))
(and (not (null? cookies))
(car cookies)))))
(define char-set:tspecials
(char-set-union
(char-set-difference char-set:punctuation (string->char-set "_."))
char-set:whitespace))
(define char-set:control (char-set-union char-set:iso-control
(char-set (integer->char 127))))(define char-set:token (char-set-difference char-set:ascii char-set:tspecials char-set:control))
(define quoted-string?
(lambda (s)
(and (string=? (string-take s 1) "\"")
(string=? (string-take-right s 1) "\""))))
(define cookie-string?
(opt-lambda (s (value? #t))
(unless (string? s)
(raise (make-cookie-error (format "String expected, received: ~a" s) (current-continuation-marks))))
(if value?
(or (string-every char-set:token s)
(quoted-string? s))
(string-every char-set:token s))))
(define char-set:hostname
(let ((a-z-lowercase (ucs-range->char-set #x61 #x7B))
(a-z-uppercase (ucs-range->char-set #x41 #x5B)))
(char-set-adjoin!
(char-set-union char-set:digit a-z-lowercase a-z-uppercase)
#\. )))
(define valid-domain?
(lambda (dom)
(and
(string=? (string-take dom 1) ".")
(string-every char-set:hostname dom)
(<= (string-length dom) 76))))
(define (expires->rfc822-string seconds)
(date->string (time-utc->date
(make-time time-utc 0 seconds))
"~a, ~d-~b-~Y ~H:~M:~S GMT"))
(provide set-cookie
cookie:add-domain
cookie:add-expires
cookie:add-path
cookie:secure
print-cookie
get-cookie
get-cookie/single
(struct-out cookie-error)
expires->rfc822-string)