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 util.contains(allowed_users, sdat.values.username)
179 return sid, sdat.values
185 local function session_setup(user, pass)
186 local login = util.ubus("session", "login", {
189 timeout = tonumber(luci.config.sauth.sessiontime)
192 if type(login) == "table" and
193 type(login.ubus_rpc_session) == "string"
195 util.ubus("session", "set", {
196 ubus_rpc_session = login.ubus_rpc_session,
197 values = { token = sys.uniqueid(16) }
200 return login.ubus_rpc_session
206 function dispatch(request)
207 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
211 local conf = require "luci.config"
213 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
215 local i18n = require "luci.i18n"
216 local lang = conf.main.lang or "auto"
217 if lang == "auto" then
218 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
219 for lpat in aclang:gmatch("[%w-]+") do
220 lpat = lpat and lpat:gsub("-", "_")
221 if conf.languages[lpat] then
227 if lang == "auto" then
230 i18n.setlanguage(lang)
241 ctx.requestargs = ctx.requestargs or args
246 for i, s in ipairs(request) do
255 util.update(track, c)
263 for j=n+1, #request do
264 args[#args+1] = request[j]
265 freq[#freq+1] = request[j]
269 ctx.requestpath = ctx.requestpath or freq
273 i18n.loadc(track.i18n)
276 -- Init template engine
277 if (c and c.index) or not track.notemplate then
278 local tpl = require("luci.template")
279 local media = track.mediaurlbase or luci.config.main.mediaurlbase
280 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
282 for name, theme in pairs(luci.config.themes) do
283 if name:sub(1,1) ~= "." and pcall(tpl.Template,
284 "themes/%s/header" % fs.basename(theme)) then
288 assert(media, "No valid theme found")
291 local function _ifattr(cond, key, val)
293 local env = getfenv(3)
294 local scope = (type(env.self) == "table") and env.self
295 if type(val) == "table" then
296 if not next(val) then
299 val = util.serialize_json(val)
302 return string.format(
303 ' %s="%s"', tostring(key),
304 util.pcdata(tostring( val
305 or (type(env[key]) ~= "function" and env[key])
306 or (scope and type(scope[key]) ~= "function" and scope[key])
314 tpl.context.viewns = setmetatable({
316 include = function(name) tpl.Template(name):render(getfenv(2)) end;
317 translate = i18n.translate;
318 translatef = i18n.translatef;
319 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
320 striptags = util.striptags;
321 pcdata = util.pcdata;
323 theme = fs.basename(media);
324 resource = luci.config.main.resourcebase;
325 ifattr = function(...) return _ifattr(...) end;
326 attr = function(...) return _ifattr(true, ...) end;
328 }, {__index=function(table, key)
329 if key == "controller" then
331 elseif key == "REQUEST_URI" then
332 return build_url(unpack(ctx.requestpath))
333 elseif key == "token" then
336 return rawget(table, key) or _G[key]
341 track.dependent = (track.dependent ~= false)
342 assert(not track.dependent or not track.auto,
343 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
344 "has no parent node so the access to this location has been denied.\n" ..
345 "This is a software bug, please report this message at " ..
346 "https://github.com/openwrt/luci/issues"
349 if track.sysauth then
350 local authen = track.sysauth_authenticator
351 local _, sid, sdat, default_user, allowed_users
353 if type(track.sysauth) == "table" then
354 default_user, allowed_users = nil, track.sysauth
356 default_user, allowed_users = track.sysauth, { track.sysauth }
359 if type(authen) == "function" then
360 _, sid = authen(sys.user.checkpasswd, allowed_users)
361 elseif authen == "htmlauth" then
362 sid, sdat = session_retrieve(http.getcookie("sysauth"), allowed_users)
365 local user = http.getenv("HTTP_AUTH_USER")
366 local pass = http.getenv("HTTP_AUTH_PASS")
368 if user == nil and pass == nil then
369 user = http.formvalue("luci_username")
370 pass = http.formvalue("luci_password")
373 if util.contains(allowed_users, user) then
374 sid, sdat = session_setup(user, pass), nil
379 require("luci.template")
381 http.status(403, "Forbidden")
382 luci.template.render(track.sysauth_template or "sysauth", {
383 duser = default_user,
389 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
390 http.redirect(build_url(unpack(ctx.requestpath)))
393 error500("Unsupported authenticator configured")
398 sid, sdat = session_retrieve(sid, allowed_users)
401 if not sid or not sdat then
402 http.status(403, "Forbidden")
406 ctx.authsession = sid
407 ctx.authtoken = sdat.token
408 ctx.authuser = sdat.username
411 if c and require_post_security(c.target) then
412 if not test_post_security(c) then
417 if track.setgroup then
418 sys.process.setgroup(track.setgroup)
421 if track.setuser then
422 sys.process.setuser(track.setuser)
427 if type(c.target) == "function" then
429 elseif type(c.target) == "table" then
430 target = c.target.target
434 if c and (c.index or type(target) == "function") then
436 ctx.requested = ctx.requested or ctx.dispatched
439 if c and c.index then
440 local tpl = require "luci.template"
442 if util.copcall(tpl.render, "indexer", {}) then
447 if type(target) == "function" then
448 util.copcall(function()
449 local oldenv = getfenv(target)
450 local module = require(c.module)
451 local env = setmetatable({}, {__index=
454 return rawget(tbl, key) or module[key] or oldenv[key]
461 if type(c.target) == "table" then
462 ok, err = util.copcall(target, c.target, unpack(args))
464 ok, err = util.copcall(target, unpack(args))
467 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
468 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
469 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
472 if not root or not root.target then
473 error404("No root node was registered, this usually happens if no module was installed.\n" ..
474 "Install luci-mod-admin-full and retry. " ..
475 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
477 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
478 "If this url belongs to an extension, make sure it is properly installed.\n" ..
479 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
484 function createindex()
485 local controllers = { }
486 local base = "%s/controller/" % util.libpath()
489 for path in (fs.glob("%s*.lua" % base) or function() end) do
490 controllers[#controllers+1] = path
493 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
494 controllers[#controllers+1] = path
498 local cachedate = fs.stat(indexcache, "mtime")
501 for _, obj in ipairs(controllers) do
502 local omtime = fs.stat(obj, "mtime")
503 realdate = (omtime and omtime > realdate) and omtime or realdate
506 if cachedate > realdate and sys.process.info("uid") == 0 then
508 sys.process.info("uid") == fs.stat(indexcache, "uid")
509 and fs.stat(indexcache, "modestr") == "rw-------",
510 "Fatal: Indexcache is not sane!"
513 index = loadfile(indexcache)()
521 for _, path in ipairs(controllers) do
522 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
523 local mod = require(modname)
525 "Invalid controller file found\n" ..
526 "The file '" .. path .. "' contains an invalid module line.\n" ..
527 "Please verify whether the module name is set to '" .. modname ..
528 "' - It must correspond to the file path!")
530 local idx = mod.index
531 assert(type(idx) == "function",
532 "Invalid controller file found\n" ..
533 "The file '" .. path .. "' contains no index() function.\n" ..
534 "Please make sure that the controller contains a valid " ..
535 "index function and verify the spelling!")
541 local f = nixio.open(indexcache, "w", 600)
542 f:writeall(util.get_bytecode(index))
547 -- Build the index before if it does not exist yet.
548 function createtree()
554 local tree = {nodes={}, inreq=true}
557 ctx.treecache = setmetatable({}, {__mode="v"})
561 -- Load default translation
562 require "luci.i18n".loadc("base")
564 local scope = setmetatable({}, {__index = luci.dispatcher})
566 for k, v in pairs(index) do
572 local function modisort(a,b)
573 return modi[a].order < modi[b].order
576 for _, v in util.spairs(modi, modisort) do
577 scope._NAME = v.module
578 setfenv(v.func, scope)
585 function modifier(func, order)
586 context.modifiers[#context.modifiers+1] = {
594 function assign(path, clone, title, order)
595 local obj = node(unpack(path))
602 setmetatable(obj, {__index = _create_node(clone)})
607 function entry(path, target, title, order)
608 local c = node(unpack(path))
613 c.module = getfenv(2)._NAME
618 -- enabling the node.
620 return _create_node({...})
624 local c = _create_node({...})
626 c.module = getfenv(2)._NAME
632 function _create_node(path)
637 local name = table.concat(path, ".")
638 local c = context.treecache[name]
641 local last = table.remove(path)
642 local parent = _create_node(path)
644 c = {nodes={}, auto=true}
645 -- the node is "in request" if the request path matches
646 -- at least up to the length of the node path
647 if parent.inreq and context.path[#path+1] == last then
650 parent.nodes[last] = c
651 context.treecache[name] = c
658 function _firstchild()
659 local path = { unpack(context.path) }
660 local name = table.concat(path, ".")
661 local node = context.treecache[name]
664 if node and node.nodes and next(node.nodes) then
666 for k, v in pairs(node.nodes) do
668 (v.order or 100) < (node.nodes[lowest].order or 100)
675 assert(lowest ~= nil,
676 "The requested node contains no childs, unable to redispatch")
678 path[#path+1] = lowest
682 function firstchild()
683 return { type = "firstchild", target = _firstchild }
689 for _, r in ipairs({...}) do
697 function rewrite(n, ...)
700 local dispatched = util.clone(context.dispatched)
703 table.remove(dispatched, 1)
706 for i, r in ipairs(req) do
707 table.insert(dispatched, i, r)
710 for _, r in ipairs({...}) do
711 dispatched[#dispatched+1] = r
719 local function _call(self, ...)
720 local func = getfenv()[self.name]
722 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
724 assert(type(func) == "function",
725 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
726 'of type "' .. type(func) .. '".')
728 if #self.argv > 0 then
729 return func(unpack(self.argv), ...)
735 function call(name, ...)
736 return {type = "call", argv = {...}, name = name, target = _call}
739 function post_on(params, name, ...)
750 return post_on(true, ...)
754 local _template = function(self, ...)
755 require "luci.template".render(self.view)
758 function template(name)
759 return {type = "template", view = name, target = _template}
763 local function _cbi(self, ...)
764 local cbi = require "luci.cbi"
765 local tpl = require "luci.template"
766 local http = require "luci.http"
768 local config = self.config or {}
769 local maps = cbi.load(self.model, ...)
773 for i, res in ipairs(maps) do
775 local cstate = res:parse()
776 if cstate and (not state or cstate < state) then
781 local function _resolve_path(path)
782 return type(path) == "table" and build_url(unpack(path)) or path
785 if config.on_valid_to and state and state > 0 and state < 2 then
786 http.redirect(_resolve_path(config.on_valid_to))
790 if config.on_changed_to and state and state > 1 then
791 http.redirect(_resolve_path(config.on_changed_to))
795 if config.on_success_to and state and state > 0 then
796 http.redirect(_resolve_path(config.on_success_to))
800 if config.state_handler then
801 if not config.state_handler(state, maps) then
806 http.header("X-CBI-State", state or 0)
808 if not config.noheader then
809 tpl.render("cbi/header", {state = state})
814 local applymap = false
815 local pageaction = true
816 local parsechain = { }
818 for i, res in ipairs(maps) do
819 if res.apply_needed and res.parsechain then
821 for _, c in ipairs(res.parsechain) do
822 parsechain[#parsechain+1] = c
828 redirect = redirect or res.redirect
831 if res.pageaction == false then
836 messages = messages or { }
837 messages[#messages+1] = res.message
841 for i, res in ipairs(maps) do
847 pageaction = pageaction,
848 parsechain = parsechain
852 if not config.nofooter then
853 tpl.render("cbi/footer", {
855 pageaction = pageaction,
858 autoapply = config.autoapply
863 function cbi(model, config)
866 post = { ["cbi.submit"] = "1" },
874 local function _arcombine(self, ...)
876 local target = #argv > 0 and self.targets[2] or self.targets[1]
877 setfenv(target.target, self.env)
878 target:target(unpack(argv))
881 function arcombine(trg1, trg2)
882 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
886 local function _form(self, ...)
887 local cbi = require "luci.cbi"
888 local tpl = require "luci.template"
889 local http = require "luci.http"
891 local maps = luci.cbi.load(self.model, ...)
894 for i, res in ipairs(maps) do
895 local cstate = res:parse()
896 if cstate and (not state or cstate < state) then
901 http.header("X-CBI-State", state or 0)
903 for i, res in ipairs(maps) do
912 post = { ["cbi.submit"] = "1" },
918 translate = i18n.translate
920 -- This function does not actually translate the given argument but
921 -- is used by build/i18n-scan.pl to find translatable entries.