removed slot operator (no longer needed) & class proto bug fix to use NewSlot by...
[vox.git] / _modules / core / cgi.vx
blob8419426c500d5a7323733a24e164fbbb9746498b
2 /**
3 * This file is part of the Vox project
5 * ####################
6 * @synopsis@
7 *   cgi.vx - provides a CGI Interface, influenced by LunarCGI
9 * @examples@
10 *   Most basic example:
11 * ---
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>")
19 * ---
21 * @notes@
22 * cgi.vx is not yet finished
24 * @license@
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
43 * THE SOFTWARE.
46 local template = import("core/template")
47 local CGI = {}
49 CGI.parse_query := function(query)
52     local vars = query.split("&")
53     local pairs = {}
54     for(local i=0; i<vars.len(); i++)
55     {
56         local values = vars[i].split("=")
57         if(values != null)
58         {
59             local varname = values[0]
60             values.remove(0)
61             pairs[varname] := values.join("=")
62         }
63     }
64     return pairs
67 CGI.escape_html := function(text)
69     // using an array, because tables have no order
70     local escape_me = [
71         // start with '&', to avoid running into a loop
72         ["&",   "&amp;"],
73         ["\"",  "&quot;"],
74         ["\'",  "&apos;"],
75         ["<",   "&lt;"],
76         [">",   "&gt;"],
77     ]
78     foreach(pair in escape_me)
79     {
80         text = text.replace(pair[0], pair[1])
81     }
82     return text
85 CGI.urldecode := function(src)
87     local ret = ""
88     for(local i=0; i<src.len(); i++)
89     {
90         local ival
91         ival = src[i]
92         if(ival == 37)
93         {
94             local sub = src.substr(i+1, 2)
95             try
96             {
97                 local converted = sub.hextoint().tochar()
98                 ret += converted
99                 i=i+2
100             }
101             catch(e)
102             {
103                 // alright, parsing failed...
104                 // so append the unmodified char
105                 ret += ival.tochar()
106             }
107         }
108         else
109         {
110             // turn mangled spaces back into spaces
111             local tmp = ival.tochar()
112             ret += (tmp == "+" ? " " : tmp)
113         }
114     }
115     return ret
119 /* in case you're wondering, this is written in Vox (https://github.com/unhandle/betavox) */
120 CGI.parse_uri := function(urlstring)
122     local Section =
123     {
124         SEC_SCHEME       = 0,
125         SEC_AFTER_SCHEME = 1,
126         SEC_PATH         = 2,
127         SEC_USER         = 3,
128         SEC_QUERY        = 4,
129         SEC_PASSWORD     = 5,
130         SEC_HOST         = 6,
131         SEC_PORT         = 7
132     };
133     local vars = {};
134     local newvars = {};
135     local section = Section.SEC_SCHEME;
136     local start = 0;
137     local was_slash = false;
138     /* these are the key names used to propagate the vars and newvars tables */
139     local str =
140     {
141         scheme   = "scheme",
142         host     = "host",
143         path     = "path",
144         user     = "user",
145         query    = "query",
146         password = "password",
147         port     = "port"
148     }
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)
152     {
153         vars[kvalue] := ""
154     }
155     while(start < urlstring.len())
156     {
157         if(section == Section.SEC_SCHEME)
158         {
159             if(urlstring[start] == ':')
160             {
161                 section = Section.SEC_AFTER_SCHEME;
162                 start++;
163             }
164             else if(urlstring[start] == '/' && vars[str.scheme].len() == 0)
165             {
166                 section = Section.SEC_PATH;
167             }
168             else
169             {
170                 vars[str.scheme] += urlstring[start++].tochar();
171             }
172         }
173         else if(section == Section.SEC_AFTER_SCHEME)
174         {
175             if(urlstring[start] == '/')
176             {
177                 if(!was_slash)
178                 {
179                     was_slash = true;
180                 }
181                 else
182                 {
183                     was_slash = false;
184                     section = Section.SEC_USER;
185                 }
186                 start ++;
187             }
188             else
189             {
190                 /* faulty scheme? */
191                 throw "invalid scheme";
192             }
193         }
194         else if(section == Section.SEC_USER)
195         {
196             if(urlstring[start] == '/')
197             {
198                 vars[str.host] = vars[str.user];
199                 vars[str.user] = "";
200                 section = Section.SEC_PATH;
201             }
202             else if(urlstring[start] == '?')
203             {
204                 vars[str.host] = vars[str.user];
205                 vars[str.user] = "";
206                 section = Section.SEC_QUERY;
207                 start++;
208             }
209             else if(urlstring[start] == ':')
210             {
211                 section = Section.SEC_PASSWORD;
212                 start++;
213             }
214             else if(urlstring[start] == '@')
215             {
216                 section = Section.SEC_HOST;
217                 start++;
218             }
219             else
220             {
221                 vars[str.user] += urlstring[start++].tochar();
222             }
223         }
224         else if(section == Section.SEC_PASSWORD)
225         {
226             if(urlstring[start] == '/')
227             {
228                 vars[str.host] = vars[str.user];
229                 vars[str.port] = vars[str.password];
230                 vars[str.user] = "";
231                 vars[str.password] = "";
232                 section = Section.SEC_PATH;
233             }
234             else if(urlstring[start] == '?')
235             {
236                 vars[str.host] = vars[str.user];
237                 vars[str.port] = vars[str.password];
238                 vars[str.user] = "";
239                 vars[str.password] = "";
240                 section = Section.SEC_QUERY;
241                 start++;
242             }
243             else if(urlstring[start] == '@')
244             {
245                 section = Section.SEC_HOST;
246                 start++;
247             }
248             else
249             {
250                 vars[str.password] += urlstring[start++].tochar();
251             }
252         }
253         else if(section == Section.SEC_HOST)
254         {
255             if(urlstring[start] == '/')
256             {
257                 section = Section.SEC_PATH;
258             }
259             else if(urlstring[start] == ':')
260             {
261                 section = Section.SEC_PORT;
262                 start++;
263             }
264             else if(urlstring[start] == '?')
265             {
266                 section = Section.SEC_QUERY;
267                 start++;
268             }
269             else
270             {
271                 vars[str.host] += urlstring[start++].tochar();
272             }
273         }
274         else if(section == Section.SEC_PORT)
275         {
276             if(urlstring[start] == '/')
277             {
278                 section = Section.SEC_PATH;
279             }
280             else if(urlstring[start] == '?')
281             {
282                 section = Section.SEC_QUERY;
283                 start++;
284             }
285             else
286             {
287                 vars[str.port] += urlstring[start++].tochar();
288             }
289         }
290         else if(section == Section.SEC_PATH)
291         {
292             if(urlstring[start] == '?')
293             {
294                 section = Section.SEC_QUERY;
295                 start++;
296             }
297             else
298             {
299                 vars[str.path] += urlstring[start++].tochar();
300             }
301         }
302         else if(section == Section.SEC_QUERY)
303         {
304             vars[str.query] += urlstring[start++].tochar();
305         }
306     }
307     foreach(kname, kvalue in vars)
308     {
309         /* sort out empty values */
310         if(kvalue.trim().len() > 0)
311         {
312             if(kname == "query")
313             {
314                 kvalue = CGI.parse_query(kvalue)
315             }
316             newvars[kname] := kvalue;
317         }
318     }
319     return newvars;
322 class CGI.Interface
324     m_buffer = []
325     m_headers = []
326     m_cookies = {}
327     m_headersent = false
328     m_haveheaders = false
329     m_trimtext = false
330     m_autoflush = false
331     m_cachedata = false
332     m_do_not_send_headers = false
333     m_status = 200
334     m_lend = "\r\n"
335     m_query_post_cache = null
336     m_query_get_cache = null
337     m_iostream = null
339     constructor(_autoflush=true, _trimtext=false, _cachedata=true)
340     {
341         this.m_trimtext = _trimtext
342         this.m_autoflush = _autoflush
343         this.m_cachedata = _cachedata
344         this.m_iostream = io.stdout
345     }
347     function writeraw(str, doflush=false)
348     {
349         if(str != null)
350         {
351             ::print(str.tostring())
352         }
353         if(doflush)
354         {
355             //io.stdout.flush()
356         }
357     }
359     function write(...)
360     {
361         local str = ""
362         foreach(s in vargv)
363         {
364             str += s.tostring()
365         }
366         str = this.m_trimtext ? str.trim() : str
367         this.m_buffer.push(str)
368         if(this.m_autoflush == true)
369         {
370             local sendheader = true
371             if(this.m_headersent == true)
372             {
373                 sendheader = false
374             }
375             if(this.m_do_not_send_headers == true)
376             {
377                 sendheader = false
378             }
379             this.flush(sendheader)
380         }
381         if(m_cachedata == false)
382         {
383             flush(false)
384         }
385         return this
386     }
388     function haveheaders()
389     {
390         return this.m_haveheaders
391     }
393     function putheader(key, value)
394     {
395         local values = []
396         switch(key.tostring().tolower())
397         {
398             case "type":
399                 key = "Content-Type"
400                 break;
401             case "location":
402                 key = "Location"
403                 break
404         }
405         values.push(key)
406         values.push(value)
407         this.m_headers.push(values)
408         this.m_haveheaders = true
409         return this
410     }
412     function setstatus(_status)
413     {
414         this.m_status = _status
415         return this
416     }
418     function sendheader(_status, key=null, value=null)
419     {
420         if(key == null && value == null)
421         {
422             this.setstatus(_status)
423         }
424         else
425         {
426             if(typeof _status == "string" &&
427                typeof key == "string" && value == null)
428             {
429                 this.putheader(_status, key)
430             }
431             else
432             {
433                 this.setstatus(_status)
434                 this.putheader(key, value)
435             }
436         }
437         if(m_cachedata == false) flush()
438         return this
439     }
441     function sendheaders(_status, allpairs=null)
442     {
443         this.setstatus(_status)
444         if(allpairs != null)
445         {
446             foreach(key, value in allpairs)
447             {
448                 this.putheader(key, value)
449             }
450         }
451         if(m_cachedata == false) flush()
452         return this
453     }
455     function redirect(url)
456     {
457         if(this.m_headersent == false)
458         {
459             this.sendheader(302, "Content-Type", "text/html")
460         }
461         this.sendHeader("Location", url)
462         this.write("Location has moved: <a href=\"%s\">%s</a>".fmt(url, url))
463     }
465     function sendcookie(key, value)
466     {
467         if(key in this.m_cookies)
468             this.m_cookies[key] = value
469         else
470             this.m_cookies[key] := value
471     }
473     function clearCache()
474     {
475         this.m_buffer = []
476     }
478     function flush(sendheader=true)
479     {
480         if(sendheader)
481         {
482             foreach(key, value in this.m_cookies)
483             {
484                 key = key.tostring()
485                 value = value.tostring()
486                 local fmt = "%s=%s;".fmt(key, value)
487                 local cookiestr = fmt
488                 this.putheader("Set-Cookie", cookiestr)
489             }
490             if(this.m_headersent == false)
491             {
492                 this.putheader("Status", this.m_status)
493                 foreach(i, vpair in this.m_headers)
494                 {
495                     local key = vpair[0].tostring()
496                     local value = vpair[1].tostring()
497                     this.writeraw("%s: %s%s".fmt(key, value, this.m_lend))
498                 }
499                 this.writeraw(this.m_lend)
500                 this.m_headersent = true
501             }
502             else
503             {
504                 throw "Header already sent!"
505             }
506         }
507         foreach(i, item in this.m_buffer)
508         {
509             this.writeraw(item.tostring())
510         }
511         this.writeraw(null, true)
512         this.m_buffer = []
513     }
515     function queriesGET()
516     {
517         local vars = {}
518         local data = os.getenv("QUERY_STRING")
519         if(data)
520         {
521             vars = CGI.parse_query(data)
522             this.m_query_get_cache = data
523         }
524         return vars
525     }
527     function queriesPOST()
528     {
529         local vars = {}
530         local multipart
531         local length_str = os.getenv("CONTENT_LENGTH")
532         if(length_str)
533         {
534             local length = length_str.tointeger()
535             local data = io.stdin.read(length)
536             if(data)
537             {
538                 vars = CGI.parse_query(data)
539                 this.m_query_post_cache = data
540             }
541             else
542             {
543                 if(this.m_query_post_cache)
544                 {
545                     vars = CGI.parse_query(this.m_query_post_cache)
546                 }
547             }
548             io.stdin.flush()
549         }
550         return vars
551     }
553     function param_proxy(key, fn)
554     {
555         local vars = fn()
556         if(key in vars)
557         {
558             return vars[key]
559         }
560         return null
561     }
563     function paramMULTIPART(key)
564     {
565         return this.param_proxy(key, this.queriesMULTIPART)
566     }
568     function paramPOST(key)
569     {
570         return this.param_proxy(key, this.queriesPOST)
571     }
573     function paramGET(key)
574     {
575         return this.param_proxy(key, this.queriesGET)
576     }
578     function param(key)
579     {
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.
586         if(post_val)
587         {
588             return post_val
589         }
590         else if(get_val)
591         {
592             return get_val
593         }
594         return null
595     }
598 class CGI.Application extends CGI.Interface
600     _template_path = "."
601     _default_env = {}
603     constructor()
604     {
605         local _self = this
606         base()
607     }
609     function set_defaultenv(env)
610     {
611         _default_env = env
612     }
614     function set_template_path(newpath)
615     {
616         this._template_path = newpath
617     }
619     function eval_tplfile(name, env={})
620     {
621         local realenv = env
622         local realpath = this._template_path + "/" + name
623         foreach(key, value in _default_env)
624         {
625             realenv[key] := value
626         }
627         try
628         {
629             template.eval_file(this, realpath, realenv)
630         }
631         catch(error)
632         {
633             throw "Compiling %s failed: %s".fmt(realpath.quote(), error)
634         }
635     }
637     function eval_tplstring(source, env={}, env_use_subtable=true)
638     {
639         local realenv = env
640         foreach(key, value in _default_env)
641         {
642             realenv[key] := value
643         }
644         try
645         {
646             template.eval_string(this, source, realenv, env_use_subtable)
647         }
648         catch(error)
649         {
650             throw "Compiling <string> failed: %s".fmt(error)
651         }
652     }
655 //     _raw_path = os.getenv("PATH_INFO")
657 class CGI.RoutedApp extends CGI.Application
660     _allroutes = []
661     _handle_notfound = null
663     constructor()
664     {
665         base()
666     }
668     function raw_pathinfo()
669     {
670         return os.getenv("PATH_INFO") || ""
671     }
673     function parsed_pathinfo()
674     {
675         local parts = raw_pathinfo().split("/")
676         if((parts.len() > 0) && (t[0].len() == 0))
677         {
678             parts.remove(0)
679         }
680         return parts
681     }
683     function isroute(expr, useregex=true)
684     {
685         local rawpi = raw_pathinfo()
686         if(expr == "/" && rawpi.len() == 0)
687         {
688             rawpi = "/"
689         }
690         if(useregex)
691         {
692             local rex = regexp.compile(expr)
693             local matches = rex.match(rawpi)
694             return matches
695         }
696         else
697         {
698             if(expr == rawpi)
699             {
700                 return true
701             }
702         }
703         return null
704     }
706     function on_notfound(func)
707     {
708         this._handle_notfound = func
709     }
711     function on_route(slug, func)
712     {
713         _allroutes.push({pattern=slug, func=func, ispattern=false})
714     }
716     function on_pattern(pattern, func)
717     {
718         _allroutes.push({pattern=pattern, func=func, ispattern=true})
719     }
721     function exec()
722     {
723         local found_cb = false
724         foreach(route in _allroutes)
725         {
726             local rawpi = raw_pathinfo()
727             if(route.pattern == "/" && rawpi.len() == 0)
728                 rawpi = "/"
729             if(route.ispattern)
730             {
731                 local re = regexp.compile(route.pattern)
732                 local captures
733                 if((captures = re.match(rawpi)))
734                 {
735                     route.func(captures)
736                     found_cb = true
737                 }
738             }
739             else
740             {
741                 if(route.pattern == rawpi)
742                 {
743                     route.func()
744                     found_cb = true
745                 }
746             }
747         }
748         if(found_cb == false)
749         {
750             if(_handle_notfound != null)
751             {
752                 _handle_notfound()
753             }
754         }
755     }
759 return CGI