3 * This file is part of the Vox project
7 * cgi.vx - provides a CGI Interface, influenced by LunarCGI
12 * CGI := import("core/cgi")
13 * app:= CGI.Application()
14 * app.sendHeaders(200, {type="text/html"})
15 * // you can add as many headers as you please until
16 * // the first call to CGI::Interface::write()
17 * app.putHeader("X-Server", "VoxCGI")
18 * app.write("<b>Hello world!</b>")
22 * cgi.vx is not yet finished
25 * Copyright (c) 2011-2012 Beelzebub Software
27 * Permission is hereby granted, free of charge, to any person obtaining a copy
28 * of this software and associated documentation files (the "Software"), to deal
29 * in the Software without restriction, including without limitation the rights
30 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31 * copies of the Software, and to permit persons to whom the Software is
32 * furnished to do so, subject to the following conditions:
34 * The above copyright notice and this permission notice shall be included in
35 * all copies or substantial portions of the Software.
37 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
46 local template = import("core/template")
49 CGI.parse_query := function(query)
52 local vars = query.split("&")
54 for(local i=0; i<vars.len(); i++)
56 local values = vars[i].split("=")
59 local varname = values[0]
61 pairs[varname] := values.join("=")
67 CGI.escape_html := function(text)
69 // using an array, because tables have no order
71 // start with '&', to avoid running into a loop
78 foreach(pair in escape_me)
80 text = text.replace(pair[0], pair[1])
85 CGI.urldecode := function(src)
88 for(local i=0; i<src.len(); i++)
94 local sub = src.substr(i+1, 2)
97 local converted = sub.hextoint().tochar()
103 // alright, parsing failed...
104 // so append the unmodified char
110 // turn mangled spaces back into spaces
111 local tmp = ival.tochar()
112 ret += (tmp == "+" ? " " : tmp)
119 /* in case you're wondering, this is written in Vox (https://github.com/unhandle/betavox) */
120 CGI.parse_uri := function(urlstring)
125 SEC_AFTER_SCHEME = 1,
135 local section = Section.SEC_SCHEME;
137 local was_slash = false;
138 /* these are the key names used to propagate the vars and newvars tables */
146 password = "password",
149 /* pre-fill vars table with empty strings. keep in mind that
150 the last loop will sort out empty values! */
151 foreach(kname, kvalue in str)
155 while(start < urlstring.len())
157 if(section == Section.SEC_SCHEME)
159 if(urlstring[start] == ':')
161 section = Section.SEC_AFTER_SCHEME;
164 else if(urlstring[start] == '/' && vars[str.scheme].len() == 0)
166 section = Section.SEC_PATH;
170 vars[str.scheme] += urlstring[start++].tochar();
173 else if(section == Section.SEC_AFTER_SCHEME)
175 if(urlstring[start] == '/')
184 section = Section.SEC_USER;
191 throw "invalid scheme";
194 else if(section == Section.SEC_USER)
196 if(urlstring[start] == '/')
198 vars[str.host] = vars[str.user];
200 section = Section.SEC_PATH;
202 else if(urlstring[start] == '?')
204 vars[str.host] = vars[str.user];
206 section = Section.SEC_QUERY;
209 else if(urlstring[start] == ':')
211 section = Section.SEC_PASSWORD;
214 else if(urlstring[start] == '@')
216 section = Section.SEC_HOST;
221 vars[str.user] += urlstring[start++].tochar();
224 else if(section == Section.SEC_PASSWORD)
226 if(urlstring[start] == '/')
228 vars[str.host] = vars[str.user];
229 vars[str.port] = vars[str.password];
231 vars[str.password] = "";
232 section = Section.SEC_PATH;
234 else if(urlstring[start] == '?')
236 vars[str.host] = vars[str.user];
237 vars[str.port] = vars[str.password];
239 vars[str.password] = "";
240 section = Section.SEC_QUERY;
243 else if(urlstring[start] == '@')
245 section = Section.SEC_HOST;
250 vars[str.password] += urlstring[start++].tochar();
253 else if(section == Section.SEC_HOST)
255 if(urlstring[start] == '/')
257 section = Section.SEC_PATH;
259 else if(urlstring[start] == ':')
261 section = Section.SEC_PORT;
264 else if(urlstring[start] == '?')
266 section = Section.SEC_QUERY;
271 vars[str.host] += urlstring[start++].tochar();
274 else if(section == Section.SEC_PORT)
276 if(urlstring[start] == '/')
278 section = Section.SEC_PATH;
280 else if(urlstring[start] == '?')
282 section = Section.SEC_QUERY;
287 vars[str.port] += urlstring[start++].tochar();
290 else if(section == Section.SEC_PATH)
292 if(urlstring[start] == '?')
294 section = Section.SEC_QUERY;
299 vars[str.path] += urlstring[start++].tochar();
302 else if(section == Section.SEC_QUERY)
304 vars[str.query] += urlstring[start++].tochar();
307 foreach(kname, kvalue in vars)
309 /* sort out empty values */
310 if(kvalue.trim().len() > 0)
314 kvalue = CGI.parse_query(kvalue)
316 newvars[kname] := kvalue;
328 m_haveheaders = false
332 m_do_not_send_headers = false
335 m_query_post_cache = null
336 m_query_get_cache = null
339 constructor(_autoflush=true, _trimtext=false, _cachedata=true)
341 this.m_trimtext = _trimtext
342 this.m_autoflush = _autoflush
343 this.m_cachedata = _cachedata
344 this.m_iostream = io.stdout
347 function writeraw(str, doflush=false)
351 ::print(str.tostring())
366 str = this.m_trimtext ? str.trim() : str
367 this.m_buffer.push(str)
368 if(this.m_autoflush == true)
370 local sendheader = true
371 if(this.m_headersent == true)
375 if(this.m_do_not_send_headers == true)
379 this.flush(sendheader)
381 if(m_cachedata == false)
388 function haveheaders()
390 return this.m_haveheaders
393 function putheader(key, value)
396 switch(key.tostring().tolower())
407 this.m_headers.push(values)
408 this.m_haveheaders = true
412 function setstatus(_status)
414 this.m_status = _status
418 function sendheader(_status, key=null, value=null)
420 if(key == null && value == null)
422 this.setstatus(_status)
426 if(typeof _status == "string" &&
427 typeof key == "string" && value == null)
429 this.putheader(_status, key)
433 this.setstatus(_status)
434 this.putheader(key, value)
437 if(m_cachedata == false) flush()
441 function sendheaders(_status, allpairs=null)
443 this.setstatus(_status)
446 foreach(key, value in allpairs)
448 this.putheader(key, value)
451 if(m_cachedata == false) flush()
455 function redirect(url)
457 if(this.m_headersent == false)
459 this.sendheader(302, "Content-Type", "text/html")
461 this.sendHeader("Location", url)
462 this.write("Location has moved: <a href=\"%s\">%s</a>".fmt(url, url))
465 function sendcookie(key, value)
467 if(key in this.m_cookies)
468 this.m_cookies[key] = value
470 this.m_cookies[key] := value
473 function clearCache()
478 function flush(sendheader=true)
482 foreach(key, value in this.m_cookies)
485 value = value.tostring()
486 local fmt = "%s=%s;".fmt(key, value)
487 local cookiestr = fmt
488 this.putheader("Set-Cookie", cookiestr)
490 if(this.m_headersent == false)
492 this.putheader("Status", this.m_status)
493 foreach(i, vpair in this.m_headers)
495 local key = vpair[0].tostring()
496 local value = vpair[1].tostring()
497 this.writeraw("%s: %s%s".fmt(key, value, this.m_lend))
499 this.writeraw(this.m_lend)
500 this.m_headersent = true
504 throw "Header already sent!"
507 foreach(i, item in this.m_buffer)
509 this.writeraw(item.tostring())
511 this.writeraw(null, true)
515 function queriesGET()
518 local data = os.getenv("QUERY_STRING")
521 vars = CGI.parse_query(data)
522 this.m_query_get_cache = data
527 function queriesPOST()
531 local length_str = os.getenv("CONTENT_LENGTH")
534 local length = length_str.tointeger()
535 local data = io.stdin.read(length)
538 vars = CGI.parse_query(data)
539 this.m_query_post_cache = data
543 if(this.m_query_post_cache)
545 vars = CGI.parse_query(this.m_query_post_cache)
553 function param_proxy(key, fn)
563 function paramMULTIPART(key)
565 return this.param_proxy(key, this.queriesMULTIPART)
568 function paramPOST(key)
570 return this.param_proxy(key, this.queriesPOST)
573 function paramGET(key)
575 return this.param_proxy(key, this.queriesGET)
580 local post_val = this.paramPOST(key)
581 local get_val = this.paramGET(key)
582 // first check POST, since one could easily do something like this:
583 // someurl.cgi?someVar=1
584 // where "someVar" is also defined in a formular. so checking
585 // POST first prevents this override.
598 class CGI.Application extends CGI.Interface
609 function set_defaultenv(env)
614 function set_template_path(newpath)
616 this._template_path = newpath
619 function eval_tplfile(name, env={})
622 local realpath = this._template_path + "/" + name
623 foreach(key, value in _default_env)
625 realenv[key] := value
629 template.eval_file(this, realpath, realenv)
633 throw "Compiling %s failed: %s".fmt(realpath.quote(), error)
637 function eval_tplstring(source, env={}, env_use_subtable=true)
640 foreach(key, value in _default_env)
642 realenv[key] := value
646 template.eval_string(this, source, realenv, env_use_subtable)
650 throw "Compiling <string> failed: %s".fmt(error)
655 // _raw_path = os.getenv("PATH_INFO")
657 class CGI.RoutedApp extends CGI.Application
661 _handle_notfound = null
668 function raw_pathinfo()
670 return os.getenv("PATH_INFO") || ""
673 function parsed_pathinfo()
675 local parts = raw_pathinfo().split("/")
676 if((parts.len() > 0) && (t[0].len() == 0))
683 function isroute(expr, useregex=true)
685 local rawpi = raw_pathinfo()
686 if(expr == "/" && rawpi.len() == 0)
692 local rex = regexp.compile(expr)
693 local matches = rex.match(rawpi)
706 function on_notfound(func)
708 this._handle_notfound = func
711 function on_route(slug, func)
713 _allroutes.push({pattern=slug, func=func, ispattern=false})
716 function on_pattern(pattern, func)
718 _allroutes.push({pattern=pattern, func=func, ispattern=true})
723 local found_cb = false
724 foreach(route in _allroutes)
726 local rawpi = raw_pathinfo()
727 if(route.pattern == "/" && rawpi.len() == 0)
731 local re = regexp.compile(route.pattern)
733 if((captures = re.match(rawpi)))
741 if(route.pattern == rawpi)
748 if(found_cb == false)
750 if(_handle_notfound != null)