/src/orbit.lua

http://github.com/keplerproject/orbit · Lua · 574 lines · 564 code · 10 blank · 0 comment · 18 complexity · a35ed7e0530575df01700d576ea5578a MD5 · raw file

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