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 lpat in aclang:gmatch("[%w-]+") do
223 lpat = lpat and lpat:gsub("-", "_")
224 if conf.languages[lpat] then
230 if lang == "auto" then
233 i18n.setlanguage(lang)
244 ctx.requestargs = ctx.requestargs or args
249 for i, s in ipairs(request) do
258 util.update(track, c)
266 for j=n+1, #request do
267 args[#args+1] = request[j]
268 freq[#freq+1] = request[j]
272 ctx.requestpath = ctx.requestpath or freq
276 i18n.loadc(track.i18n)
279 -- Init template engine
280 if (c and c.index) or not track.notemplate then
281 local tpl = require("luci.template")
282 local media = track.mediaurlbase or luci.config.main.mediaurlbase
283 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
285 for name, theme in pairs(luci.config.themes) do
286 if name:sub(1,1) ~= "." and pcall(tpl.Template,
287 "themes/%s/header" % fs.basename(theme)) then
291 assert(media, "No valid theme found")
294 local function _ifattr(cond, key, val)
296 local env = getfenv(3)
297 local scope = (type(env.self) == "table") and env.self
298 if type(val) == "table" then
299 if not next(val) then
302 val = util.serialize_json(val)
305 return string.format(
306 ' %s="%s"', tostring(key),
307 util.pcdata(tostring( val
308 or (type(env[key]) ~= "function" and env[key])
309 or (scope and type(scope[key]) ~= "function" and scope[key])
317 tpl.context.viewns = setmetatable({
319 include = function(name) tpl.Template(name):render(getfenv(2)) end;
320 translate = i18n.translate;
321 translatef = i18n.translatef;
322 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
323 striptags = util.striptags;
324 pcdata = util.pcdata;
326 theme = fs.basename(media);
327 resource = luci.config.main.resourcebase;
328 ifattr = function(...) return _ifattr(...) end;
329 attr = function(...) return _ifattr(true, ...) end;
331 }, {__index=function(table, key)
332 if key == "controller" then
334 elseif key == "REQUEST_URI" then
335 return build_url(unpack(ctx.requestpath))
336 elseif key == "token" then
339 return rawget(table, key) or _G[key]
344 track.dependent = (track.dependent ~= false)
345 assert(not track.dependent or not track.auto,
346 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
347 "has no parent node so the access to this location has been denied.\n" ..
348 "This is a software bug, please report this message at " ..
349 "https://github.com/openwrt/luci/issues"
352 if track.sysauth then
353 local authen = track.sysauth_authenticator
354 local _, sid, sdat, default_user, allowed_users
356 if type(authen) == "string" and authen ~= "htmlauth" then
357 error500("Unsupported authenticator %q configured" % authen)
361 if type(track.sysauth) == "table" then
362 default_user, allowed_users = nil, track.sysauth
364 default_user, allowed_users = track.sysauth, { track.sysauth }
367 if type(authen) == "function" then
368 _, sid = authen(sys.user.checkpasswd, allowed_users)
370 sid = http.getcookie("sysauth")
373 sid, sdat = session_retrieve(sid, allowed_users)
375 if not (sid and sdat) and authen == "htmlauth" then
376 local user = http.getenv("HTTP_AUTH_USER")
377 local pass = http.getenv("HTTP_AUTH_PASS")
379 if user == nil and pass == nil then
380 user = http.formvalue("luci_username")
381 pass = http.formvalue("luci_password")
384 sid, sdat = session_setup(user, pass, allowed_users)
387 local tmpl = require "luci.template"
391 http.status(403, "Forbidden")
392 tmpl.render(track.sysauth_template or "sysauth", {
393 duser = default_user,
400 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
401 http.redirect(build_url(unpack(ctx.requestpath)))
404 if not sid or not sdat then
405 http.status(403, "Forbidden")
409 ctx.authsession = sid
410 ctx.authtoken = sdat.token
411 ctx.authuser = sdat.username
414 if c and require_post_security(c.target) then
415 if not test_post_security(c) then
420 if track.setgroup then
421 sys.process.setgroup(track.setgroup)
424 if track.setuser then
425 sys.process.setuser(track.setuser)
430 if type(c.target) == "function" then
432 elseif type(c.target) == "table" then
433 target = c.target.target
437 if c and (c.index or type(target) == "function") then
439 ctx.requested = ctx.requested or ctx.dispatched
442 if c and c.index then
443 local tpl = require "luci.template"
445 if util.copcall(tpl.render, "indexer", {}) then
450 if type(target) == "function" then
451 util.copcall(function()
452 local oldenv = getfenv(target)
453 local module = require(c.module)
454 local env = setmetatable({}, {__index=
457 return rawget(tbl, key) or module[key] or oldenv[key]
464 if type(c.target) == "table" then
465 ok, err = util.copcall(target, c.target, unpack(args))
467 ok, err = util.copcall(target, unpack(args))
470 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
471 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
472 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
475 if not root or not root.target then
476 error404("No root node was registered, this usually happens if no module was installed.\n" ..
477 "Install luci-mod-admin-full and retry. " ..
478 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
480 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
481 "If this url belongs to an extension, make sure it is properly installed.\n" ..
482 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
487 function createindex()
488 local controllers = { }
489 local base = "%s/controller/" % util.libpath()
492 for path in (fs.glob("%s*.lua" % base) or function() end) do
493 controllers[#controllers+1] = path
496 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
497 controllers[#controllers+1] = path
501 local cachedate = fs.stat(indexcache, "mtime")
504 for _, obj in ipairs(controllers) do
505 local omtime = fs.stat(obj, "mtime")
506 realdate = (omtime and omtime > realdate) and omtime or realdate
509 if cachedate > realdate and sys.process.info("uid") == 0 then
511 sys.process.info("uid") == fs.stat(indexcache, "uid")
512 and fs.stat(indexcache, "modestr") == "rw-------",
513 "Fatal: Indexcache is not sane!"
516 index = loadfile(indexcache)()
524 for _, path in ipairs(controllers) do
525 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
526 local mod = require(modname)
528 "Invalid controller file found\n" ..
529 "The file '" .. path .. "' contains an invalid module line.\n" ..
530 "Please verify whether the module name is set to '" .. modname ..
531 "' - It must correspond to the file path!")
533 local idx = mod.index
534 assert(type(idx) == "function",
535 "Invalid controller file found\n" ..
536 "The file '" .. path .. "' contains no index() function.\n" ..
537 "Please make sure that the controller contains a valid " ..
538 "index function and verify the spelling!")
544 local f = nixio.open(indexcache, "w", 600)
545 f:writeall(util.get_bytecode(index))
550 -- Build the index before if it does not exist yet.
551 function createtree()
557 local tree = {nodes={}, inreq=true}
560 ctx.treecache = setmetatable({}, {__mode="v"})
564 -- Load default translation
565 require "luci.i18n".loadc("base")
567 local scope = setmetatable({}, {__index = luci.dispatcher})
569 for k, v in pairs(index) do
575 local function modisort(a,b)
576 return modi[a].order < modi[b].order
579 for _, v in util.spairs(modi, modisort) do
580 scope._NAME = v.module
581 setfenv(v.func, scope)
588 function modifier(func, order)
589 context.modifiers[#context.modifiers+1] = {
597 function assign(path, clone, title, order)
598 local obj = node(unpack(path))
605 setmetatable(obj, {__index = _create_node(clone)})
610 function entry(path, target, title, order)
611 local c = node(unpack(path))
616 c.module = getfenv(2)._NAME
621 -- enabling the node.
623 return _create_node({...})
627 local c = _create_node({...})
629 c.module = getfenv(2)._NAME
635 function _create_node(path)
640 local name = table.concat(path, ".")
641 local c = context.treecache[name]
644 local last = table.remove(path)
645 local parent = _create_node(path)
647 c = {nodes={}, auto=true}
648 -- the node is "in request" if the request path matches
649 -- at least up to the length of the node path
650 if parent.inreq and context.path[#path+1] == last then
653 parent.nodes[last] = c
654 context.treecache[name] = c
661 function _firstchild()
662 local path = { unpack(context.path) }
663 local name = table.concat(path, ".")
664 local node = context.treecache[name]
667 if node and node.nodes and next(node.nodes) then
669 for k, v in pairs(node.nodes) do
671 (v.order or 100) < (node.nodes[lowest].order or 100)
678 assert(lowest ~= nil,
679 "The requested node contains no childs, unable to redispatch")
681 path[#path+1] = lowest
685 function firstchild()
686 return { type = "firstchild", target = _firstchild }
692 for _, r in ipairs({...}) do
700 function rewrite(n, ...)
703 local dispatched = util.clone(context.dispatched)
706 table.remove(dispatched, 1)
709 for i, r in ipairs(req) do
710 table.insert(dispatched, i, r)
713 for _, r in ipairs({...}) do
714 dispatched[#dispatched+1] = r
722 local function _call(self, ...)
723 local func = getfenv()[self.name]
725 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
727 assert(type(func) == "function",
728 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
729 'of type "' .. type(func) .. '".')
731 if #self.argv > 0 then
732 return func(unpack(self.argv), ...)
738 function call(name, ...)
739 return {type = "call", argv = {...}, name = name, target = _call}
742 function post_on(params, name, ...)
753 return post_on(true, ...)
757 local _template = function(self, ...)
758 require "luci.template".render(self.view)
761 function template(name)
762 return {type = "template", view = name, target = _template}
766 local function _cbi(self, ...)
767 local cbi = require "luci.cbi"
768 local tpl = require "luci.template"
769 local http = require "luci.http"
771 local config = self.config or {}
772 local maps = cbi.load(self.model, ...)
776 for i, res in ipairs(maps) do
778 local cstate = res:parse()
779 if cstate and (not state or cstate < state) then
784 local function _resolve_path(path)
785 return type(path) == "table" and build_url(unpack(path)) or path
788 if config.on_valid_to and state and state > 0 and state < 2 then
789 http.redirect(_resolve_path(config.on_valid_to))
793 if config.on_changed_to and state and state > 1 then
794 http.redirect(_resolve_path(config.on_changed_to))
798 if config.on_success_to and state and state > 0 then
799 http.redirect(_resolve_path(config.on_success_to))
803 if config.state_handler then
804 if not config.state_handler(state, maps) then
809 http.header("X-CBI-State", state or 0)
811 if not config.noheader then
812 tpl.render("cbi/header", {state = state})
817 local applymap = false
818 local pageaction = true
819 local parsechain = { }
821 for i, res in ipairs(maps) do
822 if res.apply_needed and res.parsechain then
824 for _, c in ipairs(res.parsechain) do
825 parsechain[#parsechain+1] = c
831 redirect = redirect or res.redirect
834 if res.pageaction == false then
839 messages = messages or { }
840 messages[#messages+1] = res.message
844 for i, res in ipairs(maps) do
850 pageaction = pageaction,
851 parsechain = parsechain
855 if not config.nofooter then
856 tpl.render("cbi/footer", {
858 pageaction = pageaction,
861 autoapply = config.autoapply
866 function cbi(model, config)
869 post = { ["cbi.submit"] = "1" },
877 local function _arcombine(self, ...)
879 local target = #argv > 0 and self.targets[2] or self.targets[1]
880 setfenv(target.target, self.env)
881 target:target(unpack(argv))
884 function arcombine(trg1, trg2)
885 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
889 local function _form(self, ...)
890 local cbi = require "luci.cbi"
891 local tpl = require "luci.template"
892 local http = require "luci.http"
894 local maps = luci.cbi.load(self.model, ...)
897 for i, res in ipairs(maps) do
898 local cstate = res:parse()
899 if cstate and (not state or cstate < state) then
904 http.header("X-CBI-State", state or 0)
906 for i, res in ipairs(maps) do
915 post = { ["cbi.submit"] = "1" },
921 translate = i18n.translate
923 -- This function does not actually translate the given argument but
924 -- is used by build/i18n-scan.pl to find translatable entries.