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"
26 function build_url(...)
28 local url = { http.getenv("SCRIPT_NAME") or "" }
31 for _, p in ipairs(path) do
32 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
42 return table.concat(url, "")
45 function node_visible(node)
48 (not node.title or #node.title == 0) or
49 (not node.target or node.hidden == true) or
50 (type(node.target) == "table" and node.target.type == "firstchild" and
51 (type(node.nodes) ~= "table" or not next(node.nodes)))
57 function node_childs(node)
61 for k, v in util.spairs(node.nodes,
63 return (node.nodes[a].order or 100)
64 < (node.nodes[b].order or 100)
67 if node_visible(v) then
76 function error404(message)
77 http.status(404, "Not Found")
78 message = message or "Not Found"
80 require("luci.template")
81 if not util.copcall(luci.template.render, "error404") then
82 http.prepare_content("text/plain")
88 function error500(message)
90 if not context.template_header_sent then
91 http.status(500, "Internal Server Error")
92 http.prepare_content("text/plain")
95 require("luci.template")
96 if not util.copcall(luci.template.render, "error500", {message=message}) then
97 http.prepare_content("text/plain")
104 function authenticator.htmlauth(validator, accs, default)
105 local user = http.formvalue("luci_username")
106 local pass = http.formvalue("luci_password")
108 if user and validator(user, pass) then
113 require("luci.template")
115 http.status(403, "Forbidden")
116 luci.template.render("sysauth", {duser=default, fuser=user})
122 function httpdispatch(request, prefix)
123 http.context.request = request
128 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
131 for _, node in ipairs(prefix) do
136 for node in pathinfo:gmatch("[^/]+") do
140 local stat, err = util.coxpcall(function()
141 dispatch(context.request)
146 --context._disable_memtrace()
149 local function require_post_security(target)
150 if type(target) == "table" then
151 if type(target.post) == "table" then
152 local param_name, required_val, request_val
154 for param_name, required_val in pairs(target.post) do
155 request_val = http.formvalue(param_name)
157 if (type(required_val) == "string" and
158 request_val ~= required_val) or
159 (required_val == true and
160 (request_val == nil or request_val == ""))
169 return (target.post == true)
175 function test_post_security()
176 if http.getenv("REQUEST_METHOD") ~= "POST" then
177 http.status(405, "Method Not Allowed")
178 http.header("Allow", "POST")
182 if http.formvalue("token") ~= context.authtoken then
183 http.status(403, "Forbidden")
184 luci.template.render("csrftoken")
191 function dispatch(request)
192 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
196 local conf = require "luci.config"
198 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
200 local lang = conf.main.lang or "auto"
201 if lang == "auto" then
202 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
203 for lpat in aclang:gmatch("[%w-]+") do
204 lpat = lpat and lpat:gsub("-", "_")
205 if conf.languages[lpat] then
211 require "luci.i18n".setlanguage(lang)
222 ctx.requestargs = ctx.requestargs or args
227 for i, s in ipairs(request) do
236 util.update(track, c)
244 for j=n+1, #request do
245 args[#args+1] = request[j]
246 freq[#freq+1] = request[j]
250 ctx.requestpath = ctx.requestpath or freq
254 i18n.loadc(track.i18n)
257 -- Init template engine
258 if (c and c.index) or not track.notemplate then
259 local tpl = require("luci.template")
260 local media = track.mediaurlbase or luci.config.main.mediaurlbase
261 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
263 for name, theme in pairs(luci.config.themes) do
264 if name:sub(1,1) ~= "." and pcall(tpl.Template,
265 "themes/%s/header" % fs.basename(theme)) then
269 assert(media, "No valid theme found")
272 local function _ifattr(cond, key, val)
274 local env = getfenv(3)
275 local scope = (type(env.self) == "table") and env.self
276 return string.format(
277 ' %s="%s"', tostring(key),
278 util.pcdata(tostring( val
279 or (type(env[key]) ~= "function" and env[key])
280 or (scope and type(scope[key]) ~= "function" and scope[key])
288 tpl.context.viewns = setmetatable({
290 include = function(name) tpl.Template(name):render(getfenv(2)) end;
291 translate = i18n.translate;
292 translatef = i18n.translatef;
293 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
294 striptags = util.striptags;
295 pcdata = util.pcdata;
297 theme = fs.basename(media);
298 resource = luci.config.main.resourcebase;
299 ifattr = function(...) return _ifattr(...) end;
300 attr = function(...) return _ifattr(true, ...) end;
302 }, {__index=function(table, key)
303 if key == "controller" then
305 elseif key == "REQUEST_URI" then
306 return build_url(unpack(ctx.requestpath))
307 elseif key == "token" then
310 return rawget(table, key) or _G[key]
315 track.dependent = (track.dependent ~= false)
316 assert(not track.dependent or not track.auto,
317 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
318 "has no parent node so the access to this location has been denied.\n" ..
319 "This is a software bug, please report this message at " ..
320 "http://luci.subsignal.org/trac/newticket"
323 if track.sysauth then
324 local authen = type(track.sysauth_authenticator) == "function"
325 and track.sysauth_authenticator
326 or authenticator[track.sysauth_authenticator]
328 local def = (type(track.sysauth) == "string") and track.sysauth
329 local accs = def and {track.sysauth} or track.sysauth
330 local sess = ctx.authsession
332 sess = http.getcookie("sysauth")
333 sess = sess and sess:match("^[a-f0-9]*$")
336 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
343 local eu = http.getenv("HTTP_AUTH_USER")
344 local ep = http.getenv("HTTP_AUTH_PASS")
345 if eu and ep and sys.user.checkpasswd(eu, ep) then
346 authen = function() return eu end
350 if not util.contains(accs, user) then
352 local user, sess = authen(sys.user.checkpasswd, accs, def)
354 if not user or not util.contains(accs, user) then
358 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
360 token = sys.uniqueid(16)
361 util.ubus("session", "set", {
362 ubus_rpc_session = sdat.ubus_rpc_session,
366 section = sys.uniqueid(16)
369 sess = sdat.ubus_rpc_session
373 if sess and token then
374 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sess, build_url() })
376 ctx.authsession = sess
377 ctx.authtoken = token
380 http.redirect(build_url(unpack(ctx.requestpath)))
384 http.status(403, "Forbidden")
388 ctx.authsession = sess
389 ctx.authtoken = token
394 if c and require_post_security(c.target) then
395 if not test_post_security(c) then
400 if track.setgroup then
401 sys.process.setgroup(track.setgroup)
404 if track.setuser then
405 sys.process.setuser(track.setuser)
410 if type(c.target) == "function" then
412 elseif type(c.target) == "table" then
413 target = c.target.target
417 if c and (c.index or type(target) == "function") then
419 ctx.requested = ctx.requested or ctx.dispatched
422 if c and c.index then
423 local tpl = require "luci.template"
425 if util.copcall(tpl.render, "indexer", {}) then
430 if type(target) == "function" then
431 util.copcall(function()
432 local oldenv = getfenv(target)
433 local module = require(c.module)
434 local env = setmetatable({}, {__index=
437 return rawget(tbl, key) or module[key] or oldenv[key]
444 if type(c.target) == "table" then
445 ok, err = util.copcall(target, c.target, unpack(args))
447 ok, err = util.copcall(target, unpack(args))
450 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
451 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
452 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
455 if not root or not root.target then
456 error404("No root node was registered, this usually happens if no module was installed.\n" ..
457 "Install luci-mod-admin-full and retry. " ..
458 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
460 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
461 "If this url belongs to an extension, make sure it is properly installed.\n" ..
462 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
467 function createindex()
468 local controllers = { }
469 local base = "%s/controller/" % util.libpath()
472 for path in (fs.glob("%s*.lua" % base) or function() end) do
473 controllers[#controllers+1] = path
476 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
477 controllers[#controllers+1] = path
481 local cachedate = fs.stat(indexcache, "mtime")
484 for _, obj in ipairs(controllers) do
485 local omtime = fs.stat(obj, "mtime")
486 realdate = (omtime and omtime > realdate) and omtime or realdate
489 if cachedate > realdate and sys.process.info("uid") == 0 then
491 sys.process.info("uid") == fs.stat(indexcache, "uid")
492 and fs.stat(indexcache, "modestr") == "rw-------",
493 "Fatal: Indexcache is not sane!"
496 index = loadfile(indexcache)()
504 for _, path in ipairs(controllers) do
505 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
506 local mod = require(modname)
508 "Invalid controller file found\n" ..
509 "The file '" .. path .. "' contains an invalid module line.\n" ..
510 "Please verify whether the module name is set to '" .. modname ..
511 "' - It must correspond to the file path!")
513 local idx = mod.index
514 assert(type(idx) == "function",
515 "Invalid controller file found\n" ..
516 "The file '" .. path .. "' contains no index() function.\n" ..
517 "Please make sure that the controller contains a valid " ..
518 "index function and verify the spelling!")
524 local f = nixio.open(indexcache, "w", 600)
525 f:writeall(util.get_bytecode(index))
530 -- Build the index before if it does not exist yet.
531 function createtree()
537 local tree = {nodes={}, inreq=true}
540 ctx.treecache = setmetatable({}, {__mode="v"})
544 -- Load default translation
545 require "luci.i18n".loadc("base")
547 local scope = setmetatable({}, {__index = luci.dispatcher})
549 for k, v in pairs(index) do
555 local function modisort(a,b)
556 return modi[a].order < modi[b].order
559 for _, v in util.spairs(modi, modisort) do
560 scope._NAME = v.module
561 setfenv(v.func, scope)
568 function modifier(func, order)
569 context.modifiers[#context.modifiers+1] = {
577 function assign(path, clone, title, order)
578 local obj = node(unpack(path))
585 setmetatable(obj, {__index = _create_node(clone)})
590 function entry(path, target, title, order)
591 local c = node(unpack(path))
596 c.module = getfenv(2)._NAME
601 -- enabling the node.
603 return _create_node({...})
607 local c = _create_node({...})
609 c.module = getfenv(2)._NAME
615 function _create_node(path)
620 local name = table.concat(path, ".")
621 local c = context.treecache[name]
624 local last = table.remove(path)
625 local parent = _create_node(path)
627 c = {nodes={}, auto=true}
628 -- the node is "in request" if the request path matches
629 -- at least up to the length of the node path
630 if parent.inreq and context.path[#path+1] == last then
633 parent.nodes[last] = c
634 context.treecache[name] = c
641 function _firstchild()
642 local path = { unpack(context.path) }
643 local name = table.concat(path, ".")
644 local node = context.treecache[name]
647 if node and node.nodes and next(node.nodes) then
649 for k, v in pairs(node.nodes) do
651 (v.order or 100) < (node.nodes[lowest].order or 100)
658 assert(lowest ~= nil,
659 "The requested node contains no childs, unable to redispatch")
661 path[#path+1] = lowest
665 function firstchild()
666 return { type = "firstchild", target = _firstchild }
672 for _, r in ipairs({...}) do
680 function rewrite(n, ...)
683 local dispatched = util.clone(context.dispatched)
686 table.remove(dispatched, 1)
689 for i, r in ipairs(req) do
690 table.insert(dispatched, i, r)
693 for _, r in ipairs({...}) do
694 dispatched[#dispatched+1] = r
702 local function _call(self, ...)
703 local func = getfenv()[self.name]
705 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
707 assert(type(func) == "function",
708 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
709 'of type "' .. type(func) .. '".')
711 if #self.argv > 0 then
712 return func(unpack(self.argv), ...)
718 function call(name, ...)
719 return {type = "call", argv = {...}, name = name, target = _call}
722 function post_on(params, name, ...)
733 return post_on(true, ...)
737 local _template = function(self, ...)
738 require "luci.template".render(self.view)
741 function template(name)
742 return {type = "template", view = name, target = _template}
746 local function _cbi(self, ...)
747 local cbi = require "luci.cbi"
748 local tpl = require "luci.template"
749 local http = require "luci.http"
751 local config = self.config or {}
752 local maps = cbi.load(self.model, ...)
756 for i, res in ipairs(maps) do
758 local cstate = res:parse()
759 if cstate and (not state or cstate < state) then
764 local function _resolve_path(path)
765 return type(path) == "table" and build_url(unpack(path)) or path
768 if config.on_valid_to and state and state > 0 and state < 2 then
769 http.redirect(_resolve_path(config.on_valid_to))
773 if config.on_changed_to and state and state > 1 then
774 http.redirect(_resolve_path(config.on_changed_to))
778 if config.on_success_to and state and state > 0 then
779 http.redirect(_resolve_path(config.on_success_to))
783 if config.state_handler then
784 if not config.state_handler(state, maps) then
789 http.header("X-CBI-State", state or 0)
791 if not config.noheader then
792 tpl.render("cbi/header", {state = state})
797 local applymap = false
798 local pageaction = true
799 local parsechain = { }
801 for i, res in ipairs(maps) do
802 if res.apply_needed and res.parsechain then
804 for _, c in ipairs(res.parsechain) do
805 parsechain[#parsechain+1] = c
811 redirect = redirect or res.redirect
814 if res.pageaction == false then
819 messages = messages or { }
820 messages[#messages+1] = res.message
824 for i, res in ipairs(maps) do
830 pageaction = pageaction,
831 parsechain = parsechain
835 if not config.nofooter then
836 tpl.render("cbi/footer", {
838 pageaction = pageaction,
841 autoapply = config.autoapply
846 function cbi(model, config)
849 post = { ["cbi.submit"] = "1" },
857 local function _arcombine(self, ...)
859 local target = #argv > 0 and self.targets[2] or self.targets[1]
860 setfenv(target.target, self.env)
861 target:target(unpack(argv))
864 function arcombine(trg1, trg2)
865 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
869 local function _form(self, ...)
870 local cbi = require "luci.cbi"
871 local tpl = require "luci.template"
872 local http = require "luci.http"
874 local maps = luci.cbi.load(self.model, ...)
877 for i, res in ipairs(maps) do
878 local cstate = res:parse()
879 if cstate and (not state or cstate < state) then
884 http.header("X-CBI-State", state or 0)
886 for i, res in ipairs(maps) do
895 post = { ["cbi.submit"] = "1" },
901 translate = i18n.translate
903 -- This function does not actually translate the given argument but
904 -- is used by build/i18n-scan.pl to find translatable entries.