REBOL [
   Title: "Rebol CGI library"
   File: %cgi.r
   Author: "Cal Dixon"
   Date: 2-Mar-2004
   Purpose: "Provide everything needed to create a CGI script"
   library: [
      level: 'advanced
      platform: 'all
      type: 'module
      domain: [cgi http web] 
      tested-under: none 
      support: none 
      license: 'MIT
      see-also: none
      ]
   ]

comment {
This script provides 15 functions (for more information run this script from the Rebol console and use 'HELP):
   ; these are very CGI specific
   process-cgi
   cgi
   all-cgi
   cookie
   set-cookies
   redirect

   ; these are frequently needed in CGI scripts but are not CGI specific
   cgi-escape
   html-escape
   onepx.gif
   binary-prin
   cgi-prin
   cgi-print

   ; these are generally useful and used internally in the other functions here
   rejoinif
   time-to-zone
   hex
}

use [buffer parts cgiobj cookieobj getobj postobj cookies cookies-buffer pc seg sep cdisp len][
cookies: copy [] postobj: copy [] parts: copy [] cgiobj: context []
process-cgi: func ["Processes HTTP Cookies, CGI GET and POST input - returns an object" /maxpostlength maxbytes][
   getobj: construct decode-cgi any [ system/options/cgi/query-string "" ]
   repeat cookie parse any [select system/options/cgi/other-headers "HTTP_COOKIE" ""] none [
	  pc: parse cookie "=" if all [ pc/1 pc/2 pc/1/1 pc/2/1 ] [ insert tail cookies reduce [ to-set-word pc/1 pc/2 ] ]
	  ]
   cookieobj: construct cookies
   if system/options/cgi/request-method = "POST" [
	  buffer: make string! 20 + len: to-integer system/options/cgi/content-length
      if all [maxbytes len > maxbytes][return context []]
      set-modes system/ports/input [lines: false]
	  while [ all [ len > length? buffer read-io system/ports/input buffer len ] ] [
         if all [ not empty? buffer len <> length? buffer not wait reduce [system/ports/input 30] ][quit]
         ]
	  postobj: construct either all [
		 sep: find/tail system/options/cgi/content-type "multipart/form-data;"
		 sep: find/tail sep "boundary="
		 ][
		 sep: rejoin [crlf "--" sep]
		 model: context [ content-disposition: name: filename: none content-type: "text/plain" ]
		 parse join crlf buffer [any [copy seg to sep (if seg [
			seg: parse-header model find seg complement charset crlf
			if all [seg/content-disposition cdisp: find/tail seg/content-disposition "form-data;"][
			   seg: make seg [
				  name: copy/part name: find/tail cdisp {name="} find name {"}
				  filename: if filename: find/tail cdisp {filename="} [
					 filename: copy/part filename find filename {"}
					 ]
				  ]
			   if not find seg/content-type "text/" [seg/content: to-binary seg/content]
			   either all [seg/filename not empty? seg/filename] [
				  seg/content: context [filename: seg/filename content-type: seg/content-type content: seg/content]
				  ][
				  if seg/content-type = "text/plain" [trim/head/tail seg/content]
				  ]
			   insert tail parts reduce [ to-set-word seg/name seg/content ]
			   ]
			]) sep]]
		 parts
		 ][
		 decode-cgi buffer
		 ]
	  ]
   return cgiobj: make cookieobj make getobj postobj
   ]
cgi: func ["Get a CGI variable that was read by process-cgi" var [any-string! any-word!] /cookie /get /post /local o][
   o: any [
      if get [ getobj ]
      if post [ postobj ]
      if cookie [ cookieobj ]
      cgiobj
      ]
   if var: in o to-word form var [ system/words/get :var ]
   ]
allcgi: func ["Returns a block of all CGI variables from process-cgi"][bind difference first cgiobj [self] in cgiobj 'self]

rejoinif: func [ "either :condition [ rejoin block ][ :default ]" condition [logic! none!] block [block!] default ][ either :condition [ rejoin block ][ :default ] ]

hex: func ["Returns a two character hexadecimal version of a number or character" c [char! string! integer!]][copy/part next next form to-binary to-char c 2]
cgi-escape: func ["Full URL escaping of a string" x [string!] /local echar nonechar s c][
   echar: complement nonechar: charset "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$-_.!*(),"
   parse/all x: to-string x [ any [ s: echar (c: s/1 remove s insert s join "%" hex c s: skip s 2) :s | nonechar ] ]
   x
   ]
html-escape: func ["Prepares a string for use in HTML forms" x [string!] /local echar nonechar s c t][
   nonechar: complement echar: charset {"<>^M^J}
   parse/all x: to-string x [ any [ s: echar (c: s/1 remove s insert s t: rejoin ["&#" to-integer c ";"] s: skip s length? t) :s | nonechar ] ]
   x
   ]
onepx.gif: func ["Returns a 1x1 transparent GIF file as a binary!"][#{4749463839610100010080000000000000000021F90401000000002C00000000010001004002024401003B}]

time-to-zone: func [ "Adjusts a time! to a different time zone" time [date! time!] zone [time!] /local a][a: (time - time/zone) + zone a/zone: zone a]
cookies-buffer: []
cookie: func [ "Sets or Unsets a Cookie - use set-cookies after all cookies have been set" name val /expires exp /path pth /kill ][
   if kill [ exp: now - 2 val: "." ]
   insert tail cookies-buffer rejoin [
      "Set-Cookie: " form :name "=" form :val
      rejoinif expires ["; expires=" to-idate time-to-zone exp 0:00 ] ""
      rejoinif ["; path=" pth ] ""
      newline
      ]
   ]
set-cookies: func ["Ouputs all cookie changes at once"][ if cookies-buffer/1 [cgi-print rejoin cookies-out] ]
redirect: func ["Does an HTTP redirect" url [url! string!] /quit ][ print [ "Location:" url ] if quit [system/words/quit]]
binary-prin: func [ "Outputs a value with no processing" data ] [ write-io system/ports/output data length? data ]
cgi-prin: func [ "Replacement for 'PRIN that always translates ^^/ to CRLF" out /local data ] [
   data: replace/all (reform out) newline "^M^J"
   write-io system/ports/output data length? data
   return
   ]
cgi-print: func ["Replacement for 'PRINT that always translates ^^/ to CRLF" out][
   data: head insert tail replace/all (reform out) newline "^M^J" "^M^J"
   write-io system/ports/output data length? data
   return
   ]
]

; The following is an example CGI script using this library
comment {
#!/usr/local/rebol/rebol -cs
REBOL [ file: %cgidemo.cgi ]

if error? e: try [
do %cgi.r
process-cgi

print "Content-type: text/html^/"

file: any [cgi 'filetest context [filename: none content-type: none content: ""]]

print rejoin [
{CGI Demo}

"CGI Demo Script"


 mold cgi 'gettest 


 file/filename newline file/content-type newline length? file/content 


 mold cgi 'posttest 

 mold cgi 'posttest2 

{} ] none ][ print "Content-type: text/plain^/" probe mold disarm e ] quit }