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 local function render()
79 local template = require "luci.template"
80 template.render("error404")
83 if not util.copcall(render) then
84 http.prepare_content("text/plain")
91 function error500(message)
93 if not context.template_header_sent then
94 http.status(500, "Internal Server Error")
95 http.prepare_content("text/plain")
98 require("luci.template")
99 if not util.copcall(luci.template.render, "error500", {message=message}) then
100 http.prepare_content("text/plain")
107 function httpdispatch(request, prefix)
108 http.context.request = request
113 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
116 for _, node in ipairs(prefix) do
122 for node in pathinfo:gmatch("[^/%z]+") do
126 local stat, err = util.coxpcall(function()
127 dispatch(context.request)
132 --context._disable_memtrace()
135 local function require_post_security(target)
136 if type(target) == "table" then
137 if type(target.post) == "table" then
138 local param_name, required_val, request_val
140 for param_name, required_val in pairs(target.post) do
141 request_val = http.formvalue(param_name)
143 if (type(required_val) == "string" and
144 request_val ~= required_val) or
145 (required_val == true and request_val == nil)
154 return (target.post == true)
160 function test_post_security()
161 if http.getenv("REQUEST_METHOD") ~= "POST" then
162 http.status(405, "Method Not Allowed")
163 http.header("Allow", "POST")
167 if http.formvalue("token") ~= context.authtoken then
168 http.status(403, "Forbidden")
169 luci.template.render("csrftoken")
176 local function session_retrieve(sid, allowed_users)
177 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
179 if type(sdat) == "table" and
180 type(sdat.values) == "table" and
181 type(sdat.values.token) == "string" and
182 (not allowed_users or
183 util.contains(allowed_users, sdat.values.username))
185 uci:set_session_id(sid)
186 return sid, sdat.values
192 local function session_setup(user, pass, allowed_users)
193 if util.contains(allowed_users, user) then
194 local login = util.ubus("session", "login", {
197 timeout = tonumber(luci.config.sauth.sessiontime)
200 local rp = context.requestpath
201 and table.concat(context.requestpath, "/") or ""
203 if type(login) == "table" and
204 type(login.ubus_rpc_session) == "string"
206 util.ubus("session", "set", {
207 ubus_rpc_session = login.ubus_rpc_session,
208 values = { token = sys.uniqueid(16) }
211 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
212 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
214 return session_retrieve(login.ubus_rpc_session)
217 io.stderr:write("luci: failed login on /%s for %s from %s\n"
218 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
224 function dispatch(request)
225 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
229 local conf = require "luci.config"
231 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
233 local i18n = require "luci.i18n"
234 local lang = conf.main.lang or "auto"
235 if lang == "auto" then
236 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
237 for aclang in aclang:gmatch("[%w_-]+") do
238 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
239 if country and culture then
240 local cc = "%s_%s" %{ country, culture:lower() }
241 if conf.languages[cc] then
244 elseif conf.languages[country] then
248 elseif conf.languages[aclang] then
254 if lang == "auto" then
257 i18n.setlanguage(lang)
268 ctx.requestargs = ctx.requestargs or args
273 for i, s in ipairs(request) do
282 util.update(track, c)
290 for j=n+1, #request do
291 args[#args+1] = request[j]
292 freq[#freq+1] = request[j]
296 ctx.requestpath = ctx.requestpath or freq
299 -- Init template engine
300 if (c and c.index) or not track.notemplate then
301 local tpl = require("luci.template")
302 local media = track.mediaurlbase or luci.config.main.mediaurlbase
303 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
305 for name, theme in pairs(luci.config.themes) do
306 if name:sub(1,1) ~= "." and pcall(tpl.Template,
307 "themes/%s/header" % fs.basename(theme)) then
311 assert(media, "No valid theme found")
314 local function _ifattr(cond, key, val)
316 local env = getfenv(3)
317 local scope = (type(env.self) == "table") and env.self
318 if type(val) == "table" then
319 if not next(val) then
322 val = util.serialize_json(val)
325 return string.format(
326 ' %s="%s"', tostring(key),
327 util.pcdata(tostring( val
328 or (type(env[key]) ~= "function" and env[key])
329 or (scope and type(scope[key]) ~= "function" and scope[key])
337 tpl.context.viewns = setmetatable({
339 include = function(name) tpl.Template(name):render(getfenv(2)) end;
340 translate = i18n.translate;
341 translatef = i18n.translatef;
342 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
343 striptags = util.striptags;
344 pcdata = util.pcdata;
346 theme = fs.basename(media);
347 resource = luci.config.main.resourcebase;
348 ifattr = function(...) return _ifattr(...) end;
349 attr = function(...) return _ifattr(true, ...) end;
351 }, {__index=function(tbl, key)
352 if key == "controller" then
354 elseif key == "REQUEST_URI" then
355 return build_url(unpack(ctx.requestpath))
356 elseif key == "FULL_REQUEST_URI" then
357 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
358 local query = http.getenv("QUERY_STRING")
359 if query and #query > 0 then
363 return table.concat(url, "")
364 elseif key == "token" then
367 return rawget(tbl, key) or _G[key]
372 track.dependent = (track.dependent ~= false)
373 assert(not track.dependent or not track.auto,
374 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
375 "has no parent node so the access to this location has been denied.\n" ..
376 "This is a software bug, please report this message at " ..
377 "https://github.com/openwrt/luci/issues"
380 if track.sysauth and not ctx.authsession then
381 local authen = track.sysauth_authenticator
382 local _, sid, sdat, default_user, allowed_users
384 if type(authen) == "string" and authen ~= "htmlauth" then
385 error500("Unsupported authenticator %q configured" % authen)
389 if type(track.sysauth) == "table" then
390 default_user, allowed_users = nil, track.sysauth
392 default_user, allowed_users = track.sysauth, { track.sysauth }
395 if type(authen) == "function" then
396 _, sid = authen(sys.user.checkpasswd, allowed_users)
398 sid = http.getcookie("sysauth")
401 sid, sdat = session_retrieve(sid, allowed_users)
403 if not (sid and sdat) and authen == "htmlauth" then
404 local user = http.getenv("HTTP_AUTH_USER")
405 local pass = http.getenv("HTTP_AUTH_PASS")
407 if user == nil and pass == nil then
408 user = http.formvalue("luci_username")
409 pass = http.formvalue("luci_password")
412 sid, sdat = session_setup(user, pass, allowed_users)
415 local tmpl = require "luci.template"
419 http.status(403, "Forbidden")
420 http.header("X-LuCI-Login-Required", "yes")
421 tmpl.render(track.sysauth_template or "sysauth", {
422 duser = default_user,
429 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
430 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
432 http.redirect(build_url(unpack(ctx.requestpath)))
435 if not sid or not sdat then
436 http.status(403, "Forbidden")
437 http.header("X-LuCI-Login-Required", "yes")
441 ctx.authsession = sid
442 ctx.authtoken = sdat.token
443 ctx.authuser = sdat.username
446 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
447 luci.http.status(200, "OK")
448 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
449 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
453 if c and require_post_security(c.target) then
454 if not test_post_security(c) then
459 if track.setgroup then
460 sys.process.setgroup(track.setgroup)
463 if track.setuser then
464 sys.process.setuser(track.setuser)
469 if type(c.target) == "function" then
471 elseif type(c.target) == "table" then
472 target = c.target.target
476 if c and (c.index or type(target) == "function") then
478 ctx.requested = ctx.requested or ctx.dispatched
481 if c and c.index then
482 local tpl = require "luci.template"
484 if util.copcall(tpl.render, "indexer", {}) then
489 if type(target) == "function" then
490 util.copcall(function()
491 local oldenv = getfenv(target)
492 local module = require(c.module)
493 local env = setmetatable({}, {__index=
496 return rawget(tbl, key) or module[key] or oldenv[key]
503 if type(c.target) == "table" then
504 ok, err = util.copcall(target, c.target, unpack(args))
506 ok, err = util.copcall(target, unpack(args))
509 error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
510 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
511 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
515 if not root or not root.target then
516 error404("No root node was registered, this usually happens if no module was installed.\n" ..
517 "Install luci-mod-admin-full and retry. " ..
518 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
520 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
521 "If this url belongs to an extension, make sure it is properly installed.\n" ..
522 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
527 function createindex()
528 local controllers = { }
529 local base = "%s/controller/" % util.libpath()
532 for path in (fs.glob("%s*.lua" % base) or function() end) do
533 controllers[#controllers+1] = path
536 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
537 controllers[#controllers+1] = path
541 local cachedate = fs.stat(indexcache, "mtime")
544 for _, obj in ipairs(controllers) do
545 local omtime = fs.stat(obj, "mtime")
546 realdate = (omtime and omtime > realdate) and omtime or realdate
549 if cachedate > realdate and sys.process.info("uid") == 0 then
551 sys.process.info("uid") == fs.stat(indexcache, "uid")
552 and fs.stat(indexcache, "modestr") == "rw-------",
553 "Fatal: Indexcache is not sane!"
556 index = loadfile(indexcache)()
564 for _, path in ipairs(controllers) do
565 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
566 local mod = require(modname)
568 "Invalid controller file found\n" ..
569 "The file '" .. path .. "' contains an invalid module line.\n" ..
570 "Please verify whether the module name is set to '" .. modname ..
571 "' - It must correspond to the file path!")
573 local idx = mod.index
574 assert(type(idx) == "function",
575 "Invalid controller file found\n" ..
576 "The file '" .. path .. "' contains no index() function.\n" ..
577 "Please make sure that the controller contains a valid " ..
578 "index function and verify the spelling!")
584 local f = nixio.open(indexcache, "w", 600)
585 f:writeall(util.get_bytecode(index))
590 -- Build the index before if it does not exist yet.
591 function createtree()
597 local tree = {nodes={}, inreq=true}
600 ctx.treecache = setmetatable({}, {__mode="v"})
604 local scope = setmetatable({}, {__index = luci.dispatcher})
606 for k, v in pairs(index) do
612 local function modisort(a,b)
613 return modi[a].order < modi[b].order
616 for _, v in util.spairs(modi, modisort) do
617 scope._NAME = v.module
618 setfenv(v.func, scope)
625 function modifier(func, order)
626 context.modifiers[#context.modifiers+1] = {
634 function assign(path, clone, title, order)
635 local obj = node(unpack(path))
642 setmetatable(obj, {__index = _create_node(clone)})
647 function entry(path, target, title, order)
648 local c = node(unpack(path))
653 c.module = getfenv(2)._NAME
658 -- enabling the node.
660 return _create_node({...})
664 local c = _create_node({...})
666 c.module = getfenv(2)._NAME
673 local i, path = nil, {}
674 for i = 1, select('#', ...) do
675 local name, arg = nil, tostring(select(i, ...))
676 for name in arg:gmatch("[^/]+") do
681 for i = #path, 1, -1 do
682 local node = context.treecache[table.concat(path, ".", 1, i)]
683 if node and (i == #path or node.leaf) then
684 return node, build_url(unpack(path))
689 function _create_node(path)
694 local name = table.concat(path, ".")
695 local c = context.treecache[name]
698 local last = table.remove(path)
699 local parent = _create_node(path)
701 c = {nodes={}, auto=true, inreq=true}
704 for _, n in ipairs(path) do
705 if context.path[_] ~= n then
711 c.inreq = c.inreq and (context.path[#path + 1] == last)
713 parent.nodes[last] = c
714 context.treecache[name] = c
722 function _find_eligible_node(root, prefix, deep, types, descend)
723 local _, cur_name, cur_node
726 for cur_name, cur_node in pairs(root.nodes) do
727 childs[#childs+1] = {
730 order = cur_node.order or 100
734 table.sort(childs, function(a, b)
735 if a.order == b.order then
736 return a.name < b.name
738 return a.order < b.order
742 if not root.leaf and deep ~= nil then
743 local sub_path = { unpack(prefix) }
745 if deep == false then
749 for _, cur_node in ipairs(childs) do
750 sub_path[#prefix+1] = cur_node.name
752 local res_path = _find_eligible_node(cur_node.node, sub_path,
763 (type(root.target) == "table" and
764 util.contains(types, root.target.type)))
770 function _find_node(recurse, types)
771 local path = { unpack(context.path) }
772 local name = table.concat(path, ".")
773 local node = context.treecache[name]
775 path = _find_eligible_node(node, path, recurse, types)
780 require "luci.template".render("empty_node_placeholder")
784 function _firstchild()
785 return _find_node(false, nil)
788 function firstchild()
789 return { type = "firstchild", target = _firstchild }
792 function _firstnode()
793 return _find_node(true, { "cbi", "form", "template", "arcombine" })
797 return { type = "firstnode", target = _firstnode }
803 for _, r in ipairs({...}) do
811 function rewrite(n, ...)
814 local dispatched = util.clone(context.dispatched)
817 table.remove(dispatched, 1)
820 for i, r in ipairs(req) do
821 table.insert(dispatched, i, r)
824 for _, r in ipairs({...}) do
825 dispatched[#dispatched+1] = r
833 local function _call(self, ...)
834 local func = getfenv()[self.name]
836 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
838 assert(type(func) == "function",
839 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
840 'of type "' .. type(func) .. '".')
842 if #self.argv > 0 then
843 return func(unpack(self.argv), ...)
849 function call(name, ...)
850 return {type = "call", argv = {...}, name = name, target = _call}
853 function post_on(params, name, ...)
864 return post_on(true, ...)
868 local _template = function(self, ...)
869 require "luci.template".render(self.view)
872 function template(name)
873 return {type = "template", view = name, target = _template}
877 local function _cbi(self, ...)
878 local cbi = require "luci.cbi"
879 local tpl = require "luci.template"
880 local http = require "luci.http"
882 local config = self.config or {}
883 local maps = cbi.load(self.model, ...)
888 for i, res in ipairs(maps) do
889 if util.instanceof(res, cbi.SimpleForm) then
890 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
893 io.stderr:write("please change %s to use the form() action instead.\n"
894 % table.concat(context.request, "/"))
898 local cstate = res:parse()
899 if cstate and (not state or cstate < state) then
904 local function _resolve_path(path)
905 return type(path) == "table" and build_url(unpack(path)) or path
908 if config.on_valid_to and state and state > 0 and state < 2 then
909 http.redirect(_resolve_path(config.on_valid_to))
913 if config.on_changed_to and state and state > 1 then
914 http.redirect(_resolve_path(config.on_changed_to))
918 if config.on_success_to and state and state > 0 then
919 http.redirect(_resolve_path(config.on_success_to))
923 if config.state_handler then
924 if not config.state_handler(state, maps) then
929 http.header("X-CBI-State", state or 0)
931 if not config.noheader then
932 tpl.render("cbi/header", {state = state})
937 local applymap = false
938 local pageaction = true
939 local parsechain = { }
941 for i, res in ipairs(maps) do
942 if res.apply_needed and res.parsechain then
944 for _, c in ipairs(res.parsechain) do
945 parsechain[#parsechain+1] = c
951 redirect = redirect or res.redirect
954 if res.pageaction == false then
959 messages = messages or { }
960 messages[#messages+1] = res.message
964 for i, res in ipairs(maps) do
969 pageaction = pageaction,
970 parsechain = parsechain
974 if not config.nofooter then
975 tpl.render("cbi/footer", {
977 pageaction = pageaction,
980 autoapply = config.autoapply,
981 trigger_apply = applymap
986 function cbi(model, config)
989 post = { ["cbi.submit"] = true },
997 local function _arcombine(self, ...)
999 local target = #argv > 0 and self.targets[2] or self.targets[1]
1000 setfenv(target.target, self.env)
1001 target:target(unpack(argv))
1004 function arcombine(trg1, trg2)
1005 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
1009 local function _form(self, ...)
1010 local cbi = require "luci.cbi"
1011 local tpl = require "luci.template"
1012 local http = require "luci.http"
1014 local maps = luci.cbi.load(self.model, ...)
1018 for i, res in ipairs(maps) do
1019 local cstate = res:parse()
1020 if cstate and (not state or cstate < state) then
1025 http.header("X-CBI-State", state or 0)
1026 tpl.render("header")
1027 for i, res in ipairs(maps) do
1030 tpl.render("footer")
1033 function form(model)
1036 post = { ["cbi.submit"] = true },
1042 translate = i18n.translate
1044 -- This function does not actually translate the given argument but
1045 -- is used by build/i18n-scan.pl to find translatable entries.