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 _ordered_children(node)
44 local name, child, children = nil, nil, {}
46 for name, child in pairs(node.nodes) do
47 children[#children+1] = {
50 order = child.order or 100
54 table.sort(children, function(a, b)
55 if a.order == b.order then
56 return a.name < b.name
58 return a.order < b.order
65 function node_visible(node)
68 (not node.title or #node.title == 0) or
69 (not node.target or node.hidden == true) or
70 (type(node.target) == "table" and node.target.type == "firstchild" and
71 (type(node.nodes) ~= "table" or not next(node.nodes)))
77 function node_childs(node)
81 for _, child in ipairs(_ordered_children(node)) do
82 if node_visible(child.node) then
83 rv[#rv+1] = child.name
91 function error404(message)
92 http.status(404, "Not Found")
93 message = message or "Not Found"
95 local function render()
96 local template = require "luci.template"
97 template.render("error404")
100 if not util.copcall(render) then
101 http.prepare_content("text/plain")
108 function error500(message)
110 if not context.template_header_sent then
111 http.status(500, "Internal Server Error")
112 http.prepare_content("text/plain")
115 require("luci.template")
116 if not util.copcall(luci.template.render, "error500", {message=message}) then
117 http.prepare_content("text/plain")
124 function httpdispatch(request, prefix)
125 http.context.request = request
130 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
133 for _, node in ipairs(prefix) do
139 for node in pathinfo:gmatch("[^/%z]+") do
143 local stat, err = util.coxpcall(function()
144 dispatch(context.request)
149 --context._disable_memtrace()
152 local function require_post_security(target)
153 if type(target) == "table" then
154 if type(target.post) == "table" then
155 local param_name, required_val, request_val
157 for param_name, required_val in pairs(target.post) do
158 request_val = http.formvalue(param_name)
160 if (type(required_val) == "string" and
161 request_val ~= required_val) or
162 (required_val == true and request_val == nil)
171 return (target.post == true)
177 function test_post_security()
178 if http.getenv("REQUEST_METHOD") ~= "POST" then
179 http.status(405, "Method Not Allowed")
180 http.header("Allow", "POST")
184 if http.formvalue("token") ~= context.authtoken then
185 http.status(403, "Forbidden")
186 luci.template.render("csrftoken")
193 local function session_retrieve(sid, allowed_users)
194 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
196 if type(sdat) == "table" and
197 type(sdat.values) == "table" and
198 type(sdat.values.token) == "string" and
199 (not allowed_users or
200 util.contains(allowed_users, sdat.values.username))
202 uci:set_session_id(sid)
203 return sid, sdat.values
209 local function session_setup(user, pass, allowed_users)
210 if util.contains(allowed_users, user) then
211 local login = util.ubus("session", "login", {
214 timeout = tonumber(luci.config.sauth.sessiontime)
217 local rp = context.requestpath
218 and table.concat(context.requestpath, "/") or ""
220 if type(login) == "table" and
221 type(login.ubus_rpc_session) == "string"
223 util.ubus("session", "set", {
224 ubus_rpc_session = login.ubus_rpc_session,
225 values = { token = sys.uniqueid(16) }
228 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
229 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
231 return session_retrieve(login.ubus_rpc_session)
234 io.stderr:write("luci: failed login on /%s for %s from %s\n"
235 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
241 function dispatch(request)
242 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
246 local conf = require "luci.config"
248 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
250 local i18n = require "luci.i18n"
251 local lang = conf.main.lang or "auto"
252 if lang == "auto" then
253 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
254 for aclang in aclang:gmatch("[%w_-]+") do
255 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
256 if country and culture then
257 local cc = "%s_%s" %{ country, culture:lower() }
258 if conf.languages[cc] then
261 elseif conf.languages[country] then
265 elseif conf.languages[aclang] then
271 if lang == "auto" then
274 i18n.setlanguage(lang)
285 ctx.requestargs = ctx.requestargs or args
290 for i, s in ipairs(request) do
299 util.update(track, c)
307 for j=n+1, #request do
308 args[#args+1] = request[j]
309 freq[#freq+1] = request[j]
313 ctx.requestpath = ctx.requestpath or freq
316 -- Init template engine
317 if (c and c.index) or not track.notemplate then
318 local tpl = require("luci.template")
319 local media = track.mediaurlbase or luci.config.main.mediaurlbase
320 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
322 for name, theme in pairs(luci.config.themes) do
323 if name:sub(1,1) ~= "." and pcall(tpl.Template,
324 "themes/%s/header" % fs.basename(theme)) then
328 assert(media, "No valid theme found")
331 local function _ifattr(cond, key, val)
333 local env = getfenv(3)
334 local scope = (type(env.self) == "table") and env.self
335 if type(val) == "table" then
336 if not next(val) then
339 val = util.serialize_json(val)
342 return string.format(
343 ' %s="%s"', tostring(key),
344 util.pcdata(tostring( val
345 or (type(env[key]) ~= "function" and env[key])
346 or (scope and type(scope[key]) ~= "function" and scope[key])
354 tpl.context.viewns = setmetatable({
356 include = function(name) tpl.Template(name):render(getfenv(2)) end;
357 translate = i18n.translate;
358 translatef = i18n.translatef;
359 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
360 striptags = util.striptags;
361 pcdata = util.pcdata;
363 theme = fs.basename(media);
364 resource = luci.config.main.resourcebase;
365 ifattr = function(...) return _ifattr(...) end;
366 attr = function(...) return _ifattr(true, ...) end;
368 }, {__index=function(tbl, key)
369 if key == "controller" then
371 elseif key == "REQUEST_URI" then
372 return build_url(unpack(ctx.requestpath))
373 elseif key == "FULL_REQUEST_URI" then
374 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
375 local query = http.getenv("QUERY_STRING")
376 if query and #query > 0 then
380 return table.concat(url, "")
381 elseif key == "token" then
384 return rawget(tbl, key) or _G[key]
389 track.dependent = (track.dependent ~= false)
390 assert(not track.dependent or not track.auto,
391 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
392 "has no parent node so the access to this location has been denied.\n" ..
393 "This is a software bug, please report this message at " ..
394 "https://github.com/openwrt/luci/issues"
397 if track.sysauth and not ctx.authsession then
398 local authen = track.sysauth_authenticator
399 local _, sid, sdat, default_user, allowed_users
401 if type(authen) == "string" and authen ~= "htmlauth" then
402 error500("Unsupported authenticator %q configured" % authen)
406 if type(track.sysauth) == "table" then
407 default_user, allowed_users = nil, track.sysauth
409 default_user, allowed_users = track.sysauth, { track.sysauth }
412 if type(authen) == "function" then
413 _, sid = authen(sys.user.checkpasswd, allowed_users)
415 sid = http.getcookie("sysauth")
418 sid, sdat = session_retrieve(sid, allowed_users)
420 if not (sid and sdat) and authen == "htmlauth" then
421 local user = http.getenv("HTTP_AUTH_USER")
422 local pass = http.getenv("HTTP_AUTH_PASS")
424 if user == nil and pass == nil then
425 user = http.formvalue("luci_username")
426 pass = http.formvalue("luci_password")
429 sid, sdat = session_setup(user, pass, allowed_users)
432 local tmpl = require "luci.template"
436 http.status(403, "Forbidden")
437 http.header("X-LuCI-Login-Required", "yes")
438 tmpl.render(track.sysauth_template or "sysauth", {
439 duser = default_user,
446 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
447 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
449 http.redirect(build_url(unpack(ctx.requestpath)))
452 if not sid or not sdat then
453 http.status(403, "Forbidden")
454 http.header("X-LuCI-Login-Required", "yes")
458 ctx.authsession = sid
459 ctx.authtoken = sdat.token
460 ctx.authuser = sdat.username
463 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
464 luci.http.status(200, "OK")
465 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
466 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
470 if c and require_post_security(c.target) then
471 if not test_post_security(c) then
476 if track.setgroup then
477 sys.process.setgroup(track.setgroup)
480 if track.setuser then
481 sys.process.setuser(track.setuser)
486 if type(c.target) == "function" then
488 elseif type(c.target) == "table" then
489 target = c.target.target
493 if c and (c.index or type(target) == "function") then
495 ctx.requested = ctx.requested or ctx.dispatched
498 if c and c.index then
499 local tpl = require "luci.template"
501 if util.copcall(tpl.render, "indexer", {}) then
506 if type(target) == "function" then
507 util.copcall(function()
508 local oldenv = getfenv(target)
509 local module = require(c.module)
510 local env = setmetatable({}, {__index=
513 return rawget(tbl, key) or module[key] or oldenv[key]
520 if type(c.target) == "table" then
521 ok, err = util.copcall(target, c.target, unpack(args))
523 ok, err = util.copcall(target, unpack(args))
526 error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
527 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
528 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
532 if not root or not root.target then
533 error404("No root node was registered, this usually happens if no module was installed.\n" ..
534 "Install luci-mod-admin-full and retry. " ..
535 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
537 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
538 "If this url belongs to an extension, make sure it is properly installed.\n" ..
539 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
544 function createindex()
545 local controllers = { }
546 local base = "%s/controller/" % util.libpath()
549 for path in (fs.glob("%s*.lua" % base) or function() end) do
550 controllers[#controllers+1] = path
553 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
554 controllers[#controllers+1] = path
558 local cachedate = fs.stat(indexcache, "mtime")
561 for _, obj in ipairs(controllers) do
562 local omtime = fs.stat(obj, "mtime")
563 realdate = (omtime and omtime > realdate) and omtime or realdate
566 if cachedate > realdate and sys.process.info("uid") == 0 then
568 sys.process.info("uid") == fs.stat(indexcache, "uid")
569 and fs.stat(indexcache, "modestr") == "rw-------",
570 "Fatal: Indexcache is not sane!"
573 index = loadfile(indexcache)()
581 for _, path in ipairs(controllers) do
582 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
583 local mod = require(modname)
585 "Invalid controller file found\n" ..
586 "The file '" .. path .. "' contains an invalid module line.\n" ..
587 "Please verify whether the module name is set to '" .. modname ..
588 "' - It must correspond to the file path!")
590 local idx = mod.index
591 assert(type(idx) == "function",
592 "Invalid controller file found\n" ..
593 "The file '" .. path .. "' contains no index() function.\n" ..
594 "Please make sure that the controller contains a valid " ..
595 "index function and verify the spelling!")
601 local f = nixio.open(indexcache, "w", 600)
602 f:writeall(util.get_bytecode(index))
607 -- Build the index before if it does not exist yet.
608 function createtree()
614 local tree = {nodes={}, inreq=true}
616 ctx.treecache = setmetatable({}, {__mode="v"})
619 local scope = setmetatable({}, {__index = luci.dispatcher})
621 for k, v in pairs(index) do
630 function assign(path, clone, title, order)
631 local obj = node(unpack(path))
638 setmetatable(obj, {__index = _create_node(clone)})
643 function entry(path, target, title, order)
644 local c = node(unpack(path))
649 c.module = getfenv(2)._NAME
654 -- enabling the node.
656 return _create_node({...})
660 local c = _create_node({...})
662 c.module = getfenv(2)._NAME
669 local i, path = nil, {}
670 for i = 1, select('#', ...) do
671 local name, arg = nil, tostring(select(i, ...))
672 for name in arg:gmatch("[^/]+") do
677 for i = #path, 1, -1 do
678 local node = context.treecache[table.concat(path, ".", 1, i)]
679 if node and (i == #path or node.leaf) then
680 return node, build_url(unpack(path))
685 function _create_node(path)
690 local name = table.concat(path, ".")
691 local c = context.treecache[name]
694 local last = table.remove(path)
695 local parent = _create_node(path)
697 c = {nodes={}, auto=true, inreq=true}
700 for _, n in ipairs(path) do
701 if context.path[_] ~= n then
707 c.inreq = c.inreq and (context.path[#path + 1] == last)
709 parent.nodes[last] = c
710 context.treecache[name] = c
718 function _find_eligible_node(root, prefix, deep, types, descend)
719 local children = _ordered_children(root)
721 if not root.leaf and deep ~= nil then
722 local sub_path = { unpack(prefix) }
724 if deep == false then
729 for _, child in ipairs(children) do
730 sub_path[#prefix+1] = child.name
732 local res_path = _find_eligible_node(child.node, sub_path,
743 (type(root.target) == "table" and
744 util.contains(types, root.target.type)))
750 function _find_node(recurse, types)
751 local path = { unpack(context.path) }
752 local name = table.concat(path, ".")
753 local node = context.treecache[name]
755 path = _find_eligible_node(node, path, recurse, types)
760 require "luci.template".render("empty_node_placeholder")
764 function _firstchild()
765 return _find_node(false, nil)
768 function firstchild()
769 return { type = "firstchild", target = _firstchild }
772 function _firstnode()
773 return _find_node(true, { "cbi", "form", "template", "arcombine" })
777 return { type = "firstnode", target = _firstnode }
783 for _, r in ipairs({...}) do
791 function rewrite(n, ...)
794 local dispatched = util.clone(context.dispatched)
797 table.remove(dispatched, 1)
800 for i, r in ipairs(req) do
801 table.insert(dispatched, i, r)
804 for _, r in ipairs({...}) do
805 dispatched[#dispatched+1] = r
813 local function _call(self, ...)
814 local func = getfenv()[self.name]
816 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
818 assert(type(func) == "function",
819 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
820 'of type "' .. type(func) .. '".')
822 if #self.argv > 0 then
823 return func(unpack(self.argv), ...)
829 function call(name, ...)
830 return {type = "call", argv = {...}, name = name, target = _call}
833 function post_on(params, name, ...)
844 return post_on(true, ...)
848 local _template = function(self, ...)
849 require "luci.template".render(self.view)
852 function template(name)
853 return {type = "template", view = name, target = _template}
857 local function _cbi(self, ...)
858 local cbi = require "luci.cbi"
859 local tpl = require "luci.template"
860 local http = require "luci.http"
862 local config = self.config or {}
863 local maps = cbi.load(self.model, ...)
868 for i, res in ipairs(maps) do
869 if util.instanceof(res, cbi.SimpleForm) then
870 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
873 io.stderr:write("please change %s to use the form() action instead.\n"
874 % table.concat(context.request, "/"))
878 local cstate = res:parse()
879 if cstate and (not state or cstate < state) then
884 local function _resolve_path(path)
885 return type(path) == "table" and build_url(unpack(path)) or path
888 if config.on_valid_to and state and state > 0 and state < 2 then
889 http.redirect(_resolve_path(config.on_valid_to))
893 if config.on_changed_to and state and state > 1 then
894 http.redirect(_resolve_path(config.on_changed_to))
898 if config.on_success_to and state and state > 0 then
899 http.redirect(_resolve_path(config.on_success_to))
903 if config.state_handler then
904 if not config.state_handler(state, maps) then
909 http.header("X-CBI-State", state or 0)
911 if not config.noheader then
912 tpl.render("cbi/header", {state = state})
917 local applymap = false
918 local pageaction = true
919 local parsechain = { }
921 for i, res in ipairs(maps) do
922 if res.apply_needed and res.parsechain then
924 for _, c in ipairs(res.parsechain) do
925 parsechain[#parsechain+1] = c
931 redirect = redirect or res.redirect
934 if res.pageaction == false then
939 messages = messages or { }
940 messages[#messages+1] = res.message
944 for i, res in ipairs(maps) do
949 pageaction = pageaction,
950 parsechain = parsechain
954 if not config.nofooter then
955 tpl.render("cbi/footer", {
957 pageaction = pageaction,
960 autoapply = config.autoapply,
961 trigger_apply = applymap
966 function cbi(model, config)
969 post = { ["cbi.submit"] = true },
977 local function _arcombine(self, ...)
979 local target = #argv > 0 and self.targets[2] or self.targets[1]
980 setfenv(target.target, self.env)
981 target:target(unpack(argv))
984 function arcombine(trg1, trg2)
985 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
989 local function _form(self, ...)
990 local cbi = require "luci.cbi"
991 local tpl = require "luci.template"
992 local http = require "luci.http"
994 local maps = luci.cbi.load(self.model, ...)
998 for i, res in ipairs(maps) do
999 local cstate = res:parse()
1000 if cstate and (not state or cstate < state) then
1005 http.header("X-CBI-State", state or 0)
1006 tpl.render("header")
1007 for i, res in ipairs(maps) do
1010 tpl.render("footer")
1013 function form(model)
1016 post = { ["cbi.submit"] = true },
1022 translate = i18n.translate
1024 -- This function does not actually translate the given argument but
1025 -- is used by build/i18n-scan.pl to find translatable entries.