1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
3 -- Licensed to the public under the Apache License 2.0.
5 local fs = require "nixio.fs"
6 local sys = require "luci.sys"
7 local util = require "luci.util"
8 local http = require "luci.http"
9 local nixio = require "nixio", require "nixio.util"
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
24 function build_url(...)
26 local url = { http.getenv("SCRIPT_NAME") or "" }
29 for _, p in ipairs(path) do
30 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
40 return table.concat(url, "")
43 function node_visible(node)
46 (not node.title or #node.title == 0) or
47 (not node.target or node.hidden == true) or
48 (type(node.target) == "table" and node.target.type == "firstchild" and
49 (type(node.nodes) ~= "table" or not next(node.nodes)))
55 function node_childs(node)
59 for k, v in util.spairs(node.nodes,
61 return (node.nodes[a].order or 100)
62 < (node.nodes[b].order or 100)
65 if node_visible(v) then
74 function error404(message)
75 http.status(404, "Not Found")
76 message = message or "Not Found"
78 require("luci.template")
79 if not util.copcall(luci.template.render, "error404") then
80 http.prepare_content("text/plain")
86 function error500(message)
88 if not context.template_header_sent then
89 http.status(500, "Internal Server Error")
90 http.prepare_content("text/plain")
93 require("luci.template")
94 if not util.copcall(luci.template.render, "error500", {message=message}) then
95 http.prepare_content("text/plain")
102 function httpdispatch(request, prefix)
103 http.context.request = request
108 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
111 for _, node in ipairs(prefix) do
116 for node in pathinfo:gmatch("[^/]+") do
120 local stat, err = util.coxpcall(function()
121 dispatch(context.request)
126 --context._disable_memtrace()
129 local function require_post_security(target)
130 if type(target) == "table" then
131 if type(target.post) == "table" then
132 local param_name, required_val, request_val
134 for param_name, required_val in pairs(target.post) do
135 request_val = http.formvalue(param_name)
137 if (type(required_val) == "string" and
138 request_val ~= required_val) or
139 (required_val == true and
140 (request_val == nil or request_val == ""))
149 return (target.post == true)
155 function test_post_security()
156 if http.getenv("REQUEST_METHOD") ~= "POST" then
157 http.status(405, "Method Not Allowed")
158 http.header("Allow", "POST")
162 if http.formvalue("token") ~= context.authtoken then
163 http.status(403, "Forbidden")
164 luci.template.render("csrftoken")
171 local function session_retrieve(sid, allowed_users)
172 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
174 if type(sdat) == "table" and
175 type(sdat.values) == "table" and
176 type(sdat.values.token) == "string" and
177 (not allowed_users or
178 util.contains(allowed_users, sdat.values.username))
180 return sid, sdat.values
186 local function session_setup(user, pass, allowed_users)
187 if util.contains(allowed_users, user) then
188 local login = util.ubus("session", "login", {
191 timeout = tonumber(luci.config.sauth.sessiontime)
194 if type(login) == "table" and
195 type(login.ubus_rpc_session) == "string"
197 util.ubus("session", "set", {
198 ubus_rpc_session = login.ubus_rpc_session,
199 values = { token = sys.uniqueid(16) }
202 return session_retrieve(login.ubus_rpc_session)
209 function dispatch(request)
210 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
214 local conf = require "luci.config"
216 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
218 local i18n = require "luci.i18n"
219 local lang = conf.main.lang or "auto"
220 if lang == "auto" then
221 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
222 for aclang in aclang:gmatch("[%w_-]+") do
223 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
224 if country and culture then
225 local cc = "%s_%s" %{ country, culture:lower() }
226 if conf.languages[cc] then
229 elseif conf.languages[country] then
233 elseif conf.languages[aclang] then
239 if lang == "auto" then
242 i18n.setlanguage(lang)
253 ctx.requestargs = ctx.requestargs or args
258 for i, s in ipairs(request) do
267 util.update(track, c)
275 for j=n+1, #request do
276 args[#args+1] = request[j]
277 freq[#freq+1] = request[j]
281 ctx.requestpath = ctx.requestpath or freq
285 i18n.loadc(track.i18n)
288 -- Init template engine
289 if (c and c.index) or not track.notemplate then
290 local tpl = require("luci.template")
291 local media = track.mediaurlbase or luci.config.main.mediaurlbase
292 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
294 for name, theme in pairs(luci.config.themes) do
295 if name:sub(1,1) ~= "." and pcall(tpl.Template,
296 "themes/%s/header" % fs.basename(theme)) then
300 assert(media, "No valid theme found")
303 local function _ifattr(cond, key, val)
305 local env = getfenv(3)
306 local scope = (type(env.self) == "table") and env.self
307 if type(val) == "table" then
308 if not next(val) then
311 val = util.serialize_json(val)
314 return string.format(
315 ' %s="%s"', tostring(key),
316 util.pcdata(tostring( val
317 or (type(env[key]) ~= "function" and env[key])
318 or (scope and type(scope[key]) ~= "function" and scope[key])
326 tpl.context.viewns = setmetatable({
328 include = function(name) tpl.Template(name):render(getfenv(2)) end;
329 translate = i18n.translate;
330 translatef = i18n.translatef;
331 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
332 striptags = util.striptags;
333 pcdata = util.pcdata;
335 theme = fs.basename(media);
336 resource = luci.config.main.resourcebase;
337 ifattr = function(...) return _ifattr(...) end;
338 attr = function(...) return _ifattr(true, ...) end;
340 }, {__index=function(table, key)
341 if key == "controller" then
343 elseif key == "REQUEST_URI" then
344 return build_url(unpack(ctx.requestpath))
345 elseif key == "token" then
348 return rawget(table, key) or _G[key]
353 track.dependent = (track.dependent ~= false)
354 assert(not track.dependent or not track.auto,
355 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
356 "has no parent node so the access to this location has been denied.\n" ..
357 "This is a software bug, please report this message at " ..
358 "https://github.com/openwrt/luci/issues"
361 if track.sysauth then
362 local authen = track.sysauth_authenticator
363 local _, sid, sdat, default_user, allowed_users
365 if type(authen) == "string" and authen ~= "htmlauth" then
366 error500("Unsupported authenticator %q configured" % authen)
370 if type(track.sysauth) == "table" then
371 default_user, allowed_users = nil, track.sysauth
373 default_user, allowed_users = track.sysauth, { track.sysauth }
376 if type(authen) == "function" then
377 _, sid = authen(sys.user.checkpasswd, allowed_users)
379 sid = http.getcookie("sysauth")
382 sid, sdat = session_retrieve(sid, allowed_users)
384 if not (sid and sdat) and authen == "htmlauth" then
385 local user = http.getenv("HTTP_AUTH_USER")
386 local pass = http.getenv("HTTP_AUTH_PASS")
388 if user == nil and pass == nil then
389 user = http.formvalue("luci_username")
390 pass = http.formvalue("luci_password")
393 sid, sdat = session_setup(user, pass, allowed_users)
396 local tmpl = require "luci.template"
400 http.status(403, "Forbidden")
401 tmpl.render(track.sysauth_template or "sysauth", {
402 duser = default_user,
409 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
410 http.redirect(build_url(unpack(ctx.requestpath)))
413 if not sid or not sdat then
414 http.status(403, "Forbidden")
418 ctx.authsession = sid
419 ctx.authtoken = sdat.token
420 ctx.authuser = sdat.username
423 if c and require_post_security(c.target) then
424 if not test_post_security(c) then
429 if track.setgroup then
430 sys.process.setgroup(track.setgroup)
433 if track.setuser then
434 sys.process.setuser(track.setuser)
439 if type(c.target) == "function" then
441 elseif type(c.target) == "table" then
442 target = c.target.target
446 if c and (c.index or type(target) == "function") then
448 ctx.requested = ctx.requested or ctx.dispatched
451 if c and c.index then
452 local tpl = require "luci.template"
454 if util.copcall(tpl.render, "indexer", {}) then
459 if type(target) == "function" then
460 util.copcall(function()
461 local oldenv = getfenv(target)
462 local module = require(c.module)
463 local env = setmetatable({}, {__index=
466 return rawget(tbl, key) or module[key] or oldenv[key]
473 if type(c.target) == "table" then
474 ok, err = util.copcall(target, c.target, unpack(args))
476 ok, err = util.copcall(target, unpack(args))
479 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
480 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
481 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
484 if not root or not root.target then
485 error404("No root node was registered, this usually happens if no module was installed.\n" ..
486 "Install luci-mod-admin-full and retry. " ..
487 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
489 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
490 "If this url belongs to an extension, make sure it is properly installed.\n" ..
491 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
496 function createindex()
497 local controllers = { }
498 local base = "%s/controller/" % util.libpath()
501 for path in (fs.glob("%s*.lua" % base) or function() end) do
502 controllers[#controllers+1] = path
505 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
506 controllers[#controllers+1] = path
510 local cachedate = fs.stat(indexcache, "mtime")
513 for _, obj in ipairs(controllers) do
514 local omtime = fs.stat(obj, "mtime")
515 realdate = (omtime and omtime > realdate) and omtime or realdate
518 if cachedate > realdate and sys.process.info("uid") == 0 then
520 sys.process.info("uid") == fs.stat(indexcache, "uid")
521 and fs.stat(indexcache, "modestr") == "rw-------",
522 "Fatal: Indexcache is not sane!"
525 index = loadfile(indexcache)()
533 for _, path in ipairs(controllers) do
534 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
535 local mod = require(modname)
537 "Invalid controller file found\n" ..
538 "The file '" .. path .. "' contains an invalid module line.\n" ..
539 "Please verify whether the module name is set to '" .. modname ..
540 "' - It must correspond to the file path!")
542 local idx = mod.index
543 assert(type(idx) == "function",
544 "Invalid controller file found\n" ..
545 "The file '" .. path .. "' contains no index() function.\n" ..
546 "Please make sure that the controller contains a valid " ..
547 "index function and verify the spelling!")
553 local f = nixio.open(indexcache, "w", 600)
554 f:writeall(util.get_bytecode(index))
559 -- Build the index before if it does not exist yet.
560 function createtree()
566 local tree = {nodes={}, inreq=true}
569 ctx.treecache = setmetatable({}, {__mode="v"})
573 -- Load default translation
574 require "luci.i18n".loadc("base")
576 local scope = setmetatable({}, {__index = luci.dispatcher})
578 for k, v in pairs(index) do
584 local function modisort(a,b)
585 return modi[a].order < modi[b].order
588 for _, v in util.spairs(modi, modisort) do
589 scope._NAME = v.module
590 setfenv(v.func, scope)
597 function modifier(func, order)
598 context.modifiers[#context.modifiers+1] = {
606 function assign(path, clone, title, order)
607 local obj = node(unpack(path))
614 setmetatable(obj, {__index = _create_node(clone)})
619 function entry(path, target, title, order)
620 local c = node(unpack(path))
625 c.module = getfenv(2)._NAME
630 -- enabling the node.
632 return _create_node({...})
636 local c = _create_node({...})
638 c.module = getfenv(2)._NAME
644 function _create_node(path)
649 local name = table.concat(path, ".")
650 local c = context.treecache[name]
653 local last = table.remove(path)
654 local parent = _create_node(path)
656 c = {nodes={}, auto=true}
657 -- the node is "in request" if the request path matches
658 -- at least up to the length of the node path
659 if parent.inreq and context.path[#path+1] == last then
662 parent.nodes[last] = c
663 context.treecache[name] = c
670 function _firstchild()
671 local path = { unpack(context.path) }
672 local name = table.concat(path, ".")
673 local node = context.treecache[name]
676 if node and node.nodes and next(node.nodes) then
678 for k, v in pairs(node.nodes) do
680 (v.order or 100) < (node.nodes[lowest].order or 100)
687 assert(lowest ~= nil,
688 "The requested node contains no childs, unable to redispatch")
690 path[#path+1] = lowest
694 function firstchild()
695 return { type = "firstchild", target = _firstchild }
701 for _, r in ipairs({...}) do
709 function rewrite(n, ...)
712 local dispatched = util.clone(context.dispatched)
715 table.remove(dispatched, 1)
718 for i, r in ipairs(req) do
719 table.insert(dispatched, i, r)
722 for _, r in ipairs({...}) do
723 dispatched[#dispatched+1] = r
731 local function _call(self, ...)
732 local func = getfenv()[self.name]
734 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
736 assert(type(func) == "function",
737 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
738 'of type "' .. type(func) .. '".')
740 if #self.argv > 0 then
741 return func(unpack(self.argv), ...)
747 function call(name, ...)
748 return {type = "call", argv = {...}, name = name, target = _call}
751 function post_on(params, name, ...)
762 return post_on(true, ...)
766 local _template = function(self, ...)
767 require "luci.template".render(self.view)
770 function template(name)
771 return {type = "template", view = name, target = _template}
775 local function _cbi(self, ...)
776 local cbi = require "luci.cbi"
777 local tpl = require "luci.template"
778 local http = require "luci.http"
780 local config = self.config or {}
781 local maps = cbi.load(self.model, ...)
785 for i, res in ipairs(maps) do
787 local cstate = res:parse()
788 if cstate and (not state or cstate < state) then
793 local function _resolve_path(path)
794 return type(path) == "table" and build_url(unpack(path)) or path
797 if config.on_valid_to and state and state > 0 and state < 2 then
798 http.redirect(_resolve_path(config.on_valid_to))
802 if config.on_changed_to and state and state > 1 then
803 http.redirect(_resolve_path(config.on_changed_to))
807 if config.on_success_to and state and state > 0 then
808 http.redirect(_resolve_path(config.on_success_to))
812 if config.state_handler then
813 if not config.state_handler(state, maps) then
818 http.header("X-CBI-State", state or 0)
820 if not config.noheader then
821 tpl.render("cbi/header", {state = state})
826 local applymap = false
827 local pageaction = true
828 local parsechain = { }
830 for i, res in ipairs(maps) do
831 if res.apply_needed and res.parsechain then
833 for _, c in ipairs(res.parsechain) do
834 parsechain[#parsechain+1] = c
840 redirect = redirect or res.redirect
843 if res.pageaction == false then
848 messages = messages or { }
849 messages[#messages+1] = res.message
853 for i, res in ipairs(maps) do
859 pageaction = pageaction,
860 parsechain = parsechain
864 if not config.nofooter then
865 tpl.render("cbi/footer", {
867 pageaction = pageaction,
870 autoapply = config.autoapply
875 function cbi(model, config)
878 post = { ["cbi.submit"] = "1" },
886 local function _arcombine(self, ...)
888 local target = #argv > 0 and self.targets[2] or self.targets[1]
889 setfenv(target.target, self.env)
890 target:target(unpack(argv))
893 function arcombine(trg1, trg2)
894 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
898 local function _form(self, ...)
899 local cbi = require "luci.cbi"
900 local tpl = require "luci.template"
901 local http = require "luci.http"
903 local maps = luci.cbi.load(self.model, ...)
906 for i, res in ipairs(maps) do
907 local cstate = res:parse()
908 if cstate and (not state or cstate < state) then
913 http.header("X-CBI-State", state or 0)
915 for i, res in ipairs(maps) do
924 post = { ["cbi.submit"] = "1" },
930 translate = i18n.translate
932 -- This function does not actually translate the given argument but
933 -- is used by build/i18n-scan.pl to find translatable entries.