-- Copyright 2008 Steven Barth -- Copyright 2008-2015 Jo-Philipp Wich -- Copyright 2017-2018 Matthias Schiffer -- Licensed to the public under the Apache License 2.0. local fs = require "nixio.fs" local json = require "jsonc" local tpl = require "gluon.web.template" local util = require "gluon.web.util" local proto = require "gluon.web.http.protocol" module("gluon.web.dispatcher", package.seeall) function build_url(http, path) return (http:getenv("SCRIPT_NAME") or "") .. "/" .. table.concat(path, "/") end function redirect(http, ...) http:redirect(build_url(http, {...})) end function node_visible(node) return ( node.title and node.target and (not node.hidden) ) end function node_children(node) if not node then return {} end local ret = {} for k, v in pairs(node.nodes) do if node_visible(v) then table.insert(ret, k) end end table.sort(ret, function(a, b) return (node.nodes[a].order or 100) < (node.nodes[b].order or 100) end ) return ret end function httpdispatch(http) local request = {} local pathinfo = proto.urldecode(http:getenv("PATH_INFO") or "", true) for node in pathinfo:gmatch("[^/]+") do table.insert(request, node) end ok, err = pcall(dispatch, http, request) if not ok then http:status(500, "Internal Server Error") http:prepare_content("text/plain") http:write(err) end end local function set_language(renderer, accept) local langs = {} local weights = {} local star = 0 local function add(lang, q) if not weights[lang] then table.insert(langs, lang) weights[lang] = q end end for match in accept:gmatch("[^,]+") do local lang = match:match('^%s*([^%s;_-]+)') local q = tonumber(match:match(';q=(%S+)%s*$') or 1) if lang == '*' then star = q elseif lang and q > 0 then add(lang, q) end end add('en', star) table.sort(langs, function(a, b) return (weights[a] or 0) > (weights[b] or 0) end) renderer.set_language(langs) end function dispatch(http, request) local tree = {nodes={}} local nodes = {[''] = tree} local function _node(path, create) local name = table.concat(path, ".") local c = nodes[name] if not c and create then local last = table.remove(path) local parent = _node(path, true) c = {nodes={}} parent.nodes[last] = c nodes[name] = c end return c end -- Init template engine local function attr(key, val) if not val then return '' end if type(val) == "table" then val = json.stringify(val) end return string.format(' %s="%s"', key, util.pcdata(tostring(val))) end local renderer = tpl.renderer(setmetatable({ http = http, request = request, node = function(path) return _node({path}) end, write = function(...) return http:write(...) end, pcdata = util.pcdata, urlencode = proto.urlencode, media = '/static/gluon', theme = 'gluon', resource = '/static/resources', attr = attr, url = function(path) return build_url(http, path) end, }, { __index = _G })) local function createtree() local base = util.libpath() .. "/controller/" local function load_ctl(path) local ctl = assert(loadfile(path)) local _pkg local subdisp = setmetatable({ package = function(name) _pkg = name end, node = function(...) return _node({...}) end, entry = function(path, target, title, order) local c = _node(path, true) c.target = target c.title = title c.order = order c.pkg = _pkg return c end, alias = function(...) local req = {...} return function() http:redirect(build_url(http, req)) end end, call = function(func, ...) local args = {...} return function() func(http, renderer, unpack(args)) end end, template = function(view) local pkg = _pkg return function() renderer.render("layout", {content = view, pkg = pkg}) end end, model = function(name) local pkg = _pkg return function() local hidenav = false local model = require "gluon.web.model" local maps = model.load(name, renderer, pkg) for _, map in ipairs(maps) do map:parse(http) end for _, map in ipairs(maps) do map:handle() hidenav = hidenav or map.hidenav end renderer.render("layout", { content = "model/wrapper", env = { maps = maps, }, hidenav = hidenav, }) end end, _ = function(text) return text end, }, { __index = _G }) local env = setmetatable({}, { __index = subdisp }) setfenv(ctl, env) ctl() end for path in (fs.glob(base .. "*.lua") or function() end) do load_ctl(path) end for path in (fs.glob(base .. "*/*.lua") or function() end) do load_ctl(path) end end set_language(renderer, http:getenv("HTTP_ACCEPT_LANGUAGE") or "") createtree() local node = _node(request) if not node or not node.target then http:status(404, "Not Found") renderer.render("layout", { content = "error404", env = { message = "No page is registered at '/" .. table.concat(request, "/") .. "'.\n" .. "If this URL belongs to an extension, make sure it is properly installed.\n", }, pkg = 'gluon-web', }) return end http:parse_input(node.filehandler) local ok, err = pcall(node.target) if not ok then http:status(500, "Internal Server Error") renderer.render("layout", { content = "error500", env = { message = "Failed to execute dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" .. "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"), }, pkg = 'gluon-web', }) end end