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 tmpl.render(track.sysauth_template or "sysauth", {
421 duser = default_user,
428 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
429 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
431 http.redirect(build_url(unpack(ctx.requestpath)))
434 if not sid or not sdat then
435 http.status(403, "Forbidden")
439 ctx.authsession = sid
440 ctx.authtoken = sdat.token
441 ctx.authuser = sdat.username
444 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
445 luci.http.status(200, "OK")
446 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
447 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
451 if c and require_post_security(c.target) then
452 if not test_post_security(c) then
457 if track.setgroup then
458 sys.process.setgroup(track.setgroup)
461 if track.setuser then
462 sys.process.setuser(track.setuser)
467 if type(c.target) == "function" then
469 elseif type(c.target) == "table" then
470 target = c.target.target
474 if c and (c.index or type(target) == "function") then
476 ctx.requested = ctx.requested or ctx.dispatched
479 if c and c.index then
480 local tpl = require "luci.template"
482 if util.copcall(tpl.render, "indexer", {}) then
487 if type(target) == "function" then
488 util.copcall(function()
489 local oldenv = getfenv(target)
490 local module = require(c.module)
491 local env = setmetatable({}, {__index=
494 return rawget(tbl, key) or module[key] or oldenv[key]
501 if type(c.target) == "table" then
502 ok, err = util.copcall(target, c.target, unpack(args))
504 ok, err = util.copcall(target, unpack(args))
507 error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
508 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
509 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
513 if not root or not root.target then
514 error404("No root node was registered, this usually happens if no module was installed.\n" ..
515 "Install luci-mod-admin-full and retry. " ..
516 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
518 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
519 "If this url belongs to an extension, make sure it is properly installed.\n" ..
520 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
525 function createindex()
526 local controllers = { }
527 local base = "%s/controller/" % util.libpath()
530 for path in (fs.glob("%s*.lua" % base) or function() end) do
531 controllers[#controllers+1] = path
534 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
535 controllers[#controllers+1] = path
539 local cachedate = fs.stat(indexcache, "mtime")
542 for _, obj in ipairs(controllers) do
543 local omtime = fs.stat(obj, "mtime")
544 realdate = (omtime and omtime > realdate) and omtime or realdate
547 if cachedate > realdate and sys.process.info("uid") == 0 then
549 sys.process.info("uid") == fs.stat(indexcache, "uid")
550 and fs.stat(indexcache, "modestr") == "rw-------",
551 "Fatal: Indexcache is not sane!"
554 index = loadfile(indexcache)()
562 for _, path in ipairs(controllers) do
563 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
564 local mod = require(modname)
566 "Invalid controller file found\n" ..
567 "The file '" .. path .. "' contains an invalid module line.\n" ..
568 "Please verify whether the module name is set to '" .. modname ..
569 "' - It must correspond to the file path!")
571 local idx = mod.index
572 assert(type(idx) == "function",
573 "Invalid controller file found\n" ..
574 "The file '" .. path .. "' contains no index() function.\n" ..
575 "Please make sure that the controller contains a valid " ..
576 "index function and verify the spelling!")
582 local f = nixio.open(indexcache, "w", 600)
583 f:writeall(util.get_bytecode(index))
588 -- Build the index before if it does not exist yet.
589 function createtree()
595 local tree = {nodes={}, inreq=true}
598 ctx.treecache = setmetatable({}, {__mode="v"})
602 local scope = setmetatable({}, {__index = luci.dispatcher})
604 for k, v in pairs(index) do
610 local function modisort(a,b)
611 return modi[a].order < modi[b].order
614 for _, v in util.spairs(modi, modisort) do
615 scope._NAME = v.module
616 setfenv(v.func, scope)
623 function modifier(func, order)
624 context.modifiers[#context.modifiers+1] = {
632 function assign(path, clone, title, order)
633 local obj = node(unpack(path))
640 setmetatable(obj, {__index = _create_node(clone)})
645 function entry(path, target, title, order)
646 local c = node(unpack(path))
651 c.module = getfenv(2)._NAME
656 -- enabling the node.
658 return _create_node({...})
662 local c = _create_node({...})
664 c.module = getfenv(2)._NAME
671 local i, path = nil, {}
672 for i = 1, select('#', ...) do
673 local name, arg = nil, tostring(select(i, ...))
674 for name in arg:gmatch("[^/]+") do
679 for i = #path, 1, -1 do
680 local node = context.treecache[table.concat(path, ".", 1, i)]
681 if node and (i == #path or node.leaf) then
682 return node, build_url(unpack(path))
687 function _create_node(path)
692 local name = table.concat(path, ".")
693 local c = context.treecache[name]
696 local last = table.remove(path)
697 local parent = _create_node(path)
699 c = {nodes={}, auto=true, inreq=true}
702 for _, n in ipairs(path) do
703 if context.path[_] ~= n then
709 c.inreq = c.inreq and (context.path[#path + 1] == last)
711 parent.nodes[last] = c
712 context.treecache[name] = c
720 function _find_eligible_node(root, prefix, deep, types, descend)
721 local _, cur_name, cur_node
724 for cur_name, cur_node in pairs(root.nodes) do
725 childs[#childs+1] = {
728 order = cur_node.order or 100
732 table.sort(childs, function(a, b)
733 if a.order == b.order then
734 return a.name < b.name
736 return a.order < b.order
740 if not root.leaf and deep ~= nil then
741 local sub_path = { unpack(prefix) }
743 if deep == false then
747 for _, cur_node in ipairs(childs) do
748 sub_path[#prefix+1] = cur_node.name
750 local res_path = _find_eligible_node(cur_node.node, sub_path,
761 (type(root.target) == "table" and
762 util.contains(types, root.target.type)))
768 function _find_node(recurse, types)
769 local path = { unpack(context.path) }
770 local name = table.concat(path, ".")
771 local node = context.treecache[name]
773 path = _find_eligible_node(node, path, recurse, types)
778 require "luci.template".render("empty_node_placeholder")
782 function _firstchild()
783 return _find_node(false, nil)
786 function firstchild()
787 return { type = "firstchild", target = _firstchild }
790 function _firstnode()
791 return _find_node(true, { "cbi", "form", "template", "arcombine" })
795 return { type = "firstnode", target = _firstnode }
801 for _, r in ipairs({...}) do
809 function rewrite(n, ...)
812 local dispatched = util.clone(context.dispatched)
815 table.remove(dispatched, 1)
818 for i, r in ipairs(req) do
819 table.insert(dispatched, i, r)
822 for _, r in ipairs({...}) do
823 dispatched[#dispatched+1] = r
831 local function _call(self, ...)
832 local func = getfenv()[self.name]
834 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
836 assert(type(func) == "function",
837 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
838 'of type "' .. type(func) .. '".')
840 if #self.argv > 0 then
841 return func(unpack(self.argv), ...)
847 function call(name, ...)
848 return {type = "call", argv = {...}, name = name, target = _call}
851 function post_on(params, name, ...)
862 return post_on(true, ...)
866 local _template = function(self, ...)
867 require "luci.template".render(self.view)
870 function template(name)
871 return {type = "template", view = name, target = _template}
875 local function _cbi(self, ...)
876 local cbi = require "luci.cbi"
877 local tpl = require "luci.template"
878 local http = require "luci.http"
880 local config = self.config or {}
881 local maps = cbi.load(self.model, ...)
886 for i, res in ipairs(maps) do
887 if util.instanceof(res, cbi.SimpleForm) then
888 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
891 io.stderr:write("please change %s to use the form() action instead.\n"
892 % table.concat(context.request, "/"))
896 local cstate = res:parse()
897 if cstate and (not state or cstate < state) then
902 local function _resolve_path(path)
903 return type(path) == "table" and build_url(unpack(path)) or path
906 if config.on_valid_to and state and state > 0 and state < 2 then
907 http.redirect(_resolve_path(config.on_valid_to))
911 if config.on_changed_to and state and state > 1 then
912 http.redirect(_resolve_path(config.on_changed_to))
916 if config.on_success_to and state and state > 0 then
917 http.redirect(_resolve_path(config.on_success_to))
921 if config.state_handler then
922 if not config.state_handler(state, maps) then
927 http.header("X-CBI-State", state or 0)
929 if not config.noheader then
930 tpl.render("cbi/header", {state = state})
935 local applymap = false
936 local pageaction = true
937 local parsechain = { }
939 for i, res in ipairs(maps) do
940 if res.apply_needed and res.parsechain then
942 for _, c in ipairs(res.parsechain) do
943 parsechain[#parsechain+1] = c
949 redirect = redirect or res.redirect
952 if res.pageaction == false then
957 messages = messages or { }
958 messages[#messages+1] = res.message
962 for i, res in ipairs(maps) do
967 pageaction = pageaction,
968 parsechain = parsechain
972 if not config.nofooter then
973 tpl.render("cbi/footer", {
975 pageaction = pageaction,
978 autoapply = config.autoapply,
979 trigger_apply = applymap
984 function cbi(model, config)
987 post = { ["cbi.submit"] = true },
995 local function _arcombine(self, ...)
997 local target = #argv > 0 and self.targets[2] or self.targets[1]
998 setfenv(target.target, self.env)
999 target:target(unpack(argv))
1002 function arcombine(trg1, trg2)
1003 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
1007 local function _form(self, ...)
1008 local cbi = require "luci.cbi"
1009 local tpl = require "luci.template"
1010 local http = require "luci.http"
1012 local maps = luci.cbi.load(self.model, ...)
1016 for i, res in ipairs(maps) do
1017 local cstate = res:parse()
1018 if cstate and (not state or cstate < state) then
1023 http.header("X-CBI-State", state or 0)
1024 tpl.render("header")
1025 for i, res in ipairs(maps) do
1028 tpl.render("footer")
1031 function form(model)
1034 post = { ["cbi.submit"] = true },
1040 translate = i18n.translate
1042 -- This function does not actually translate the given argument but
1043 -- is used by build/i18n-scan.pl to find translatable entries.