PageRenderTime 38ms CodeModel.GetById 15ms app.highlight 16ms RepoModel.GetById 2ms app.codeStats 0ms

/src/orbit.lua

http://github.com/keplerproject/orbit
Lua | 574 lines | 536 code | 38 blank | 0 comment | 65 complexity | a35ed7e0530575df01700d576ea5578a MD5 | raw file
  1local wsapi    = require "wsapi"
  2local wsreq    = require "wsapi.request"
  3local wsres    = require "wsapi.response"
  4local wsutil   = require "wsapi.util"
  5
  6local orm
  7local orpages
  8
  9local _M = _M or {}
 10
 11_M._NAME = "orbit"
 12_M._VERSION = "2.2.4"
 13_M._COPYRIGHT = "Copyright (C) 2007-2015 Kepler Project"
 14_M._DESCRIPTION = "MVC Web Development for the Kepler platform"
 15
 16local REPARSE = {}
 17
 18_M.mime_types = {
 19  ez = "application/andrew-inset",
 20  atom = "application/atom+xml",
 21  hqx = "application/mac-binhex40",
 22  cpt = "application/mac-compactpro",
 23  mathml = "application/mathml+xml",
 24  doc = "application/msword",
 25  bin = "application/octet-stream",
 26  dms = "application/octet-stream",
 27  lha = "application/octet-stream",
 28  lzh = "application/octet-stream",
 29  exe = "application/octet-stream",
 30  class = "application/octet-stream",
 31  so = "application/octet-stream",
 32  dll = "application/octet-stream",
 33  dmg = "application/octet-stream",
 34  oda = "application/oda",
 35  ogg = "application/ogg",
 36  pdf = "application/pdf",
 37  ai = "application/postscript",
 38  eps = "application/postscript",
 39  ps = "application/postscript",
 40  rdf = "application/rdf+xml",
 41  smi = "application/smil",
 42  smil = "application/smil",
 43  gram = "application/srgs",
 44  grxml = "application/srgs+xml",
 45  mif = "application/vnd.mif",
 46  xul = "application/vnd.mozilla.xul+xml",
 47  xls = "application/vnd.ms-excel",
 48  ppt = "application/vnd.ms-powerpoint",
 49  rm = "application/vnd.rn-realmedia",
 50  wbxml = "application/vnd.wap.wbxml",
 51  wmlc = "application/vnd.wap.wmlc",
 52  wmlsc = "application/vnd.wap.wmlscriptc",
 53  vxml = "application/voicexml+xml",
 54  bcpio = "application/x-bcpio",
 55  vcd = "application/x-cdlink",
 56  pgn = "application/x-chess-pgn",
 57  cpio = "application/x-cpio",
 58  csh = "application/x-csh",
 59  dcr = "application/x-director",
 60  dir = "application/x-director",
 61  dxr = "application/x-director",
 62  dvi = "application/x-dvi",
 63  spl = "application/x-futuresplash",
 64  gtar = "application/x-gtar",
 65  hdf = "application/x-hdf",
 66  xhtml = "application/xhtml+xml",
 67  xht = "application/xhtml+xml",
 68  js = "application/x-javascript",
 69  skp = "application/x-koan",
 70  skd = "application/x-koan",
 71  skt = "application/x-koan",
 72  skm = "application/x-koan",
 73  latex = "application/x-latex",
 74  xml = "application/xml",
 75  xsl = "application/xml",
 76  dtd = "application/xml-dtd",
 77  nc = "application/x-netcdf",
 78  cdf = "application/x-netcdf",
 79  sh = "application/x-sh",
 80  shar = "application/x-shar",
 81  swf = "application/x-shockwave-flash",
 82  xslt = "application/xslt+xml",
 83  sit = "application/x-stuffit",
 84  sv4cpio = "application/x-sv4cpio",
 85  sv4crc = "application/x-sv4crc",
 86  tar = "application/x-tar",
 87  tcl = "application/x-tcl",
 88  tex = "application/x-tex",
 89  texinfo = "application/x-texinfo",
 90  texi = "application/x-texinfo",
 91  t = "application/x-troff",
 92  tr = "application/x-troff",
 93  roff = "application/x-troff",
 94  man = "application/x-troff-man",
 95  me = "application/x-troff-me",
 96  ms = "application/x-troff-ms",
 97  ustar = "application/x-ustar",
 98  src = "application/x-wais-source",
 99  zip = "application/zip",
100  au = "audio/basic",
101  snd = "audio/basic",
102  mid = "audio/midi",
103  midi = "audio/midi",
104  kar = "audio/midi",
105  mpga = "audio/mpeg",
106  mp2 = "audio/mpeg",
107  mp3 = "audio/mpeg",
108  aif = "audio/x-aiff",
109  aiff = "audio/x-aiff",
110  aifc = "audio/x-aiff",
111  m3u = "audio/x-mpegurl",
112  ram = "audio/x-pn-realaudio",
113  ra = "audio/x-pn-realaudio",
114  wav = "audio/x-wav",
115  pdb = "chemical/x-pdb",
116  xyz = "chemical/x-xyz",
117  bmp = "image/bmp",
118  cgm = "image/cgm",
119  gif = "image/gif",
120  ief = "image/ief",
121  jpeg = "image/jpeg",
122  jpg = "image/jpeg",
123  jpe = "image/jpeg",
124  png = "image/png",
125  svg = "image/svg+xml",
126  svgz = "image/svg+xml",
127  tiff = "image/tiff",
128  tif = "image/tiff",
129  djvu = "image/vnd.djvu",
130  djv = "image/vnd.djvu",
131  wbmp = "image/vnd.wap.wbmp",
132  ras = "image/x-cmu-raster",
133  ico = "image/x-icon",
134  pnm = "image/x-portable-anymap",
135  pbm = "image/x-portable-bitmap",
136  pgm = "image/x-portable-graymap",
137  ppm = "image/x-portable-pixmap",
138  rgb = "image/x-rgb",
139  xbm = "image/x-xbitmap",
140  xpm = "image/x-xpixmap",
141  xwd = "image/x-xwindowdump",
142  igs = "model/iges",
143  iges = "model/iges",
144  msh = "model/mesh",
145  mesh = "model/mesh",
146  silo = "model/mesh",
147  wrl = "model/vrml",
148  vrml = "model/vrml",
149  ics = "text/calendar",
150  ifb = "text/calendar",
151  css = "text/css",
152  html = "text/html",
153  htm = "text/html",
154  asc = "text/plain",
155  txt = "text/plain",
156  rtx = "text/richtext",
157  rtf = "text/rtf",
158  sgml = "text/sgml",
159  sgm = "text/sgml",
160  tsv = "text/tab-separated-values",
161  wml = "text/vnd.wap.wml",
162  wmls = "text/vnd.wap.wmlscript",
163  etx = "text/x-setext",
164  mpeg = "video/mpeg",
165  mpg = "video/mpeg",
166  mpe = "video/mpeg",
167  qt = "video/quicktime",
168  mov = "video/quicktime",
169  mxu = "video/vnd.mpegurl",
170  avi = "video/x-msvideo",
171  movie = "video/x-sgi-movie",
172  ice = "x-conference/x-cooltalk",
173  rss = "application/rss+xml",
174  atom = "application/atom+xml",
175  json = "application/json"
176}
177
178_M.app_module_methods = {}
179local app_module_methods = _M.app_module_methods
180
181_M.web_methods = {}
182local web_methods = _M.web_methods
183
184local function flatten(t)
185   local res = {}
186   for _, item in ipairs(t) do
187      if type(item) == "table" then
188	 res[#res + 1] = flatten(item)
189      else
190	 res[#res + 1] = item
191      end
192   end
193   return table.concat(res)
194end
195
196local function make_tag(name, data, class)
197  if class then class = ' class="' .. class .. '"' else class = "" end
198  if not data then
199    return "<" .. name .. class .. "/>"
200  elseif type(data) == "string" then
201    return "<" .. name .. class .. ">" .. data ..
202      "</" .. name .. ">"
203  else
204    local attrs = {}
205    for k, v in pairs(data) do
206      if type(k) == "string" then
207        table.insert(attrs, k .. '="' .. tostring(v) .. '"')
208      end
209    end
210    local open_tag = "<" .. name .. class .. " " ..
211      table.concat(attrs, " ") .. ">"
212    local close_tag = "</" .. name .. ">"
213    return open_tag .. flatten(data) .. close_tag
214  end
215end
216
217function _M.new(app_module)
218   if type(app_module) == "string" then
219      app_module = { _NAME = app_module }
220   else
221      app_module = app_module or {}
222   end
223   for k, v in pairs(app_module_methods) do
224      app_module[k] = v
225   end
226   app_module.run = function (wsapi_env)
227		       return _M.run(app_module, wsapi_env)
228		    end
229   app_module.real_path = wsapi.app_path or "."
230   app_module.mapper = { default = true }
231   app_module.not_found = function (web)
232			     web.status = "404 Not Found"
233			     return [[<html>
234				   <head><title>Not Found</title></head>
235				      <body><p>Not found!</p></body></html>]]
236			  end
237   app_module.server_error = function (web, msg)
238				web.status = "500 Server Error"
239				return [[<html>
240				      <head><title>Server Error</title></head>
241					 <body><pre>]] .. msg .. [[</pre></body></html>]]
242				 end
243   app_module.reparse = REPARSE
244   app_module.dispatch_table = { get = {}, post = {}, put = {}, delete = {}, options = {} }
245   return app_module
246end
247
248local function serve_file(app_module)
249   return function (web)
250	     local filename = web.real_path .. web.path_info
251	     return app_module:serve_static(web, filename)
252	  end
253end
254
255function app_module_methods.dispatch_get(app_module, func, ...)
256   for _, pat in ipairs{ ... } do
257      table.insert(app_module.dispatch_table.get, { pattern = pat,
258		      handler = func })
259   end
260end
261
262function app_module_methods.dispatch_post(app_module, func, ...)
263   for _, pat in ipairs{ ... } do
264      table.insert(app_module.dispatch_table.post, { pattern = pat,
265		      handler = func })
266   end
267end
268
269function app_module_methods.dispatch_put(app_module, func, ...)
270   for _, pat in ipairs{ ... } do
271      table.insert(app_module.dispatch_table.put, { pattern = pat,
272		      handler = func })
273   end
274end
275
276function app_module_methods.dispatch_delete(app_module, func, ...)
277   for _, pat in ipairs{ ... } do
278      table.insert(app_module.dispatch_table.delete, { pattern = pat,
279		      handler = func })
280   end
281end
282
283function app_module_methods.dispatch_options(app_module, func, ...)
284   for _, pat in ipairs{ ... } do
285      table.insert(app_module.dispatch_table.options, { pattern = pat,
286          handler = func })
287   end
288end
289
290function app_module_methods.dispatch_wsapi(app_module, func, ...)
291  for _, pat in ipairs{ ... } do
292    for _, tab in pairs(app_module.dispatch_table) do
293      table.insert(tab, { pattern = pat, handler = func, wsapi = true })
294    end
295  end
296end
297
298function app_module_methods.dispatch_static(app_module, ...)
299   app_module:dispatch_get(serve_file(app_module), ...)
300end
301
302function app_module_methods.serve_static(app_module, web, filename)
303   local ext = string.match(filename, "%.([^%.]+)$")
304   if app_module.use_xsendfile then
305      web.headers["Content-Type"] = _M.mime_types[ext] or
306	 "application/octet-stream"
307      web.headers["X-Sendfile"] = filename
308      return "xsendfile"
309   else
310      local file = io.open(filename, "rb")
311      if not file then
312	 return app_module.not_found(web)
313      else
314	 web.headers["Content-Type"] = _M.mime_types[ext] or
315	    "application/octet-stream"
316	 local contents = file:read("*a")
317	 file:close()
318	 return contents
319      end
320   end
321end
322
323local function newtag(name)
324  local tag = {}
325  setmetatable(tag, {
326                 __call = function (_, data)
327                            return make_tag(name, data)
328                          end,
329                 __index = function(_, class)
330                             return function (data)
331                                      return make_tag(name, data, class)
332                                    end
333                           end
334               })
335  return tag
336end
337
338local function htmlify_func(func)
339  local tags = {}
340  local env = { H = function (name)
341		      local tag = tags[name]
342		      if not tag then
343			tag = newtag(name)
344			tags[name] = tag
345		      end
346		      return tag
347		    end
348	      }
349  local old_env = getfenv(func)
350  setmetatable(env, { __index = function (env, name)
351				  if old_env[name] then
352				    return old_env[name]
353				  else
354				    local tag = newtag(name)
355				    rawset(env, name, tag)
356				    return tag
357				  end
358				end })
359  setfenv(func, env)
360end
361
362function _M.htmlify(app_module, ...)
363   if type(app_module) == "function" then
364      htmlify_func(app_module)
365      for _, func in ipairs{...} do
366	htmlify_func(func)
367      end
368   else
369      local patterns = { ... }
370      for _, patt in ipairs(patterns) do
371	if type(patt) == "function" then
372	  htmlify_func(patt)
373	else
374	  for name, func in pairs(app_module) do
375	    if string.match(name, "^" .. patt .. "$") and
376	         type(func) == "function" then
377	       htmlify_func(func)
378	    end
379	  end
380	end
381      end
382   end
383end
384
385app_module_methods.htmlify = _M.htmlify
386
387function app_module_methods.model(app_module, ...)
388   if app_module.mapper.default then
389      local table_prefix = (app_module._NAME and app_module._NAME .. "_") or ""
390      if not orm then
391	    orm = require "orbit.model"
392      end
393      app_module.mapper = orm.new(app_module.mapper.table_prefix or table_prefix,
394			app_module.mapper.conn, app_module.mapper.driver, app_module.mapper.logging)
395   end
396   return app_module.mapper:new(...)
397end
398
399function web_methods:redirect(url)
400  self.status = "302 Found"
401  self.headers["Location"] = url
402  return "redirect"
403end
404
405function web_methods:link(url, params)
406  local link = {}
407  local prefix = self.prefix or ""
408  local suffix = self.suffix or ""
409  for k, v in pairs(params or {}) do
410    link[#link + 1] = k .. "=" .. wsutil.url_encode(v)
411  end
412  local qs = table.concat(link, "&")
413  if qs and qs ~= "" then
414    return prefix .. url .. suffix .. "?" .. qs
415  else
416    return prefix .. url .. suffix
417  end
418end
419
420function web_methods:static_link(url)
421  local prefix = self.prefix or self.script_name
422  local is_script = prefix:match("(%.%w+)$")
423  if not is_script then return self:link(url) end
424  local vpath = prefix:match("(.*)/") or ""
425  return vpath .. url
426end
427
428function web_methods:empty(s)
429  return not s or string.match(s, "^%s*$")
430end
431
432function web_methods:content_type(s)
433  self.headers["Content-Type"] = s
434end
435
436function web_methods:page(name, env)
437  if not orpages then
438    orpages = require "orbit.pages"
439  end
440  local filename
441  if name:sub(1, 1) == "/" then
442    filename = self.doc_root .. name
443  else
444    filename = self.real_path .. "/" .. name
445  end
446  local template = orpages.load(filename)
447  if template then
448    return orpages.fill(self, template, env)
449  end
450end
451
452function web_methods:page_inline(contents, env)
453  if not orpages then
454    orpages = require "orbit.pages"
455  end
456  local template = orpages.load(nil, contents)
457  if template then
458    return orpages.fill(self, template, env)
459  end
460end
461
462function web_methods:empty_param(param)
463  return self:empty(self.input[param])
464end
465
466for name, func in pairs(wsutil) do
467  web_methods[name] = function (self, ...)
468			return func(...)
469		      end
470end
471
472local function dispatcher(app_module, method, path, index)
473  index = index or 0
474  if #app_module.dispatch_table[method] == 0 then
475    return app_module["handle_" .. method], {}
476  else
477    for index = index+1, #app_module.dispatch_table[method] do
478      local item = app_module.dispatch_table[method][index]
479      local captures
480      if type(item.pattern) == "string" then
481	captures = { string.match(path, "^" .. item.pattern .. "$") }
482      else
483	captures = { item.pattern:match(path) }
484      end
485      if #captures > 0 then
486	for i = 1, #captures do
487	  if type(captures[i]) == "string" then
488	    captures[i] = wsutil.url_decode(captures[i])
489	  end
490	end
491	return item.handler, captures, item.wsapi, index
492      end
493    end
494  end
495end
496
497local function make_web_object(app_module, wsapi_env)
498  local web = { status = "200 Ok", response = "",
499		headers = { ["Content-Type"]= "text/html" },
500		cookies = {} }
501  setmetatable(web, { __index = web_methods })
502  web.vars = wsapi_env
503  web.prefix = app_module.prefix or wsapi_env.SCRIPT_NAME
504  web.suffix = app_module.suffix
505  if wsapi_env.APP_PATH == "" then
506    web.real_path = app_module.real_path or "."
507  else
508    web.real_path = wsapi_env.APP_PATH
509  end
510  web.doc_root = wsapi_env.DOCUMENT_ROOT
511  local req = wsreq.new(wsapi_env)
512  local res = wsres.new(web.status, web.headers)
513  web.set_cookie = function (_, name, value)
514		     res:set_cookie(name, value)
515		   end
516  web.delete_cookie = function (_, name, path)
517			res:delete_cookie(name, path)
518		      end
519  web.path_info = req.path_info
520  web.path_translated = wsapi_env.PATH_TRANSLATED
521  if web.path_translated == "" then web.path_translated = wsapi_env.SCRIPT_FILENAME end
522  web.script_name = wsapi_env.SCRIPT_NAME
523  web.method = string.lower(req.method)
524  web.input, web.cookies = req.params, req.cookies
525  web.GET, web.POST = req.GET, req.POST
526  return web, res
527end
528
529function _M.run(app_module, wsapi_env)
530  local handler, captures, wsapi_handler, index = dispatcher(app_module,
531							     string.lower(wsapi_env.REQUEST_METHOD),
532							     wsapi_env.PATH_INFO)
533  handler = handler or app_module.not_found
534  captures = captures or {}
535  if wsapi_handler then
536    local ok, status, headers, res = xpcall(function ()
537					      return handler(wsapi_env, unpack(captures))
538					    end, debug.traceback)
539    if ok then
540      return status, headers, res
541    else
542      handler, captures = app_module.server_error, { status }
543    end
544  end
545  local web, res = make_web_object(app_module, wsapi_env)
546  repeat
547    local reparse = false
548    local ok, response = xpcall(function ()
549                                  return handler(web, unpack(captures))
550                                end, function(msg) return debug.traceback(msg) end)
551    if not ok then
552      res.status = "500 Internal Server Error"
553      res:write(app_module.server_error(web, response))
554    else
555      if response == REPARSE then
556	reparse = true
557	handler, captures, wsapi_handler, index = dispatcher(app_module,
558							     string.lower(wsapi_env.REQUEST_METHOD),
559							     wsapi_env.PATH_INFO, index)
560	handler, captures = handler or app_module.not_found, captures or {}
561	if wsapi_handler then
562	  error("cannot reparse to WSAPI handler")
563	end
564      else
565	res.status = web.status
566	res:write(response)
567      end
568    end
569  until not reparse
570  return res:finish()
571end
572
573return _M
574