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
300 i18n.loadc(track.i18n)
303 -- Init template engine
304 if (c and c.index) or not track.notemplate then
305 local tpl = require("luci.template")
306 local media = track.mediaurlbase or luci.config.main.mediaurlbase
307 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
309 for name, theme in pairs(luci.config.themes) do
310 if name:sub(1,1) ~= "." and pcall(tpl.Template,
311 "themes/%s/header" % fs.basename(theme)) then
315 assert(media, "No valid theme found")
318 local function _ifattr(cond, key, val)
320 local env = getfenv(3)
321 local scope = (type(env.self) == "table") and env.self
322 if type(val) == "table" then
323 if not next(val) then
326 val = util.serialize_json(val)
329 return string.format(
330 ' %s="%s"', tostring(key),
331 util.pcdata(tostring( val
332 or (type(env[key]) ~= "function" and env[key])
333 or (scope and type(scope[key]) ~= "function" and scope[key])
341 tpl.context.viewns = setmetatable({
343 include = function(name) tpl.Template(name):render(getfenv(2)) end;
344 translate = i18n.translate;
345 translatef = i18n.translatef;
346 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
347 striptags = util.striptags;
348 pcdata = util.pcdata;
350 theme = fs.basename(media);
351 resource = luci.config.main.resourcebase;
352 ifattr = function(...) return _ifattr(...) end;
353 attr = function(...) return _ifattr(true, ...) end;
355 }, {__index=function(tbl, key)
356 if key == "controller" then
358 elseif key == "REQUEST_URI" then
359 return build_url(unpack(ctx.requestpath))
360 elseif key == "FULL_REQUEST_URI" then
361 local url = { http.getenv("SCRIPT_NAME"), http.getenv("PATH_INFO") }
362 local query = http.getenv("QUERY_STRING")
363 if query and #query > 0 then
367 return table.concat(url, "")
368 elseif key == "token" then
371 return rawget(tbl, key) or _G[key]
376 track.dependent = (track.dependent ~= false)
377 assert(not track.dependent or not track.auto,
378 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
379 "has no parent node so the access to this location has been denied.\n" ..
380 "This is a software bug, please report this message at " ..
381 "https://github.com/openwrt/luci/issues"
384 if track.sysauth and not ctx.authsession then
385 local authen = track.sysauth_authenticator
386 local _, sid, sdat, default_user, allowed_users
388 if type(authen) == "string" and authen ~= "htmlauth" then
389 error500("Unsupported authenticator %q configured" % authen)
393 if type(track.sysauth) == "table" then
394 default_user, allowed_users = nil, track.sysauth
396 default_user, allowed_users = track.sysauth, { track.sysauth }
399 if type(authen) == "function" then
400 _, sid = authen(sys.user.checkpasswd, allowed_users)
402 sid = http.getcookie("sysauth")
405 sid, sdat = session_retrieve(sid, allowed_users)
407 if not (sid and sdat) and authen == "htmlauth" then
408 local user = http.getenv("HTTP_AUTH_USER")
409 local pass = http.getenv("HTTP_AUTH_PASS")
411 if user == nil and pass == nil then
412 user = http.formvalue("luci_username")
413 pass = http.formvalue("luci_password")
416 sid, sdat = session_setup(user, pass, allowed_users)
419 local tmpl = require "luci.template"
423 http.status(403, "Forbidden")
424 tmpl.render(track.sysauth_template or "sysauth", {
425 duser = default_user,
432 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
433 http.redirect(build_url(unpack(ctx.requestpath)))
436 if not sid or not sdat then
437 http.status(403, "Forbidden")
441 ctx.authsession = sid
442 ctx.authtoken = sdat.token
443 ctx.authuser = sdat.username
446 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
447 luci.http.status(200, "OK")
448 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
449 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
453 if c and require_post_security(c.target) then
454 if not test_post_security(c) then
459 if track.setgroup then
460 sys.process.setgroup(track.setgroup)
463 if track.setuser then
464 sys.process.setuser(track.setuser)
469 if type(c.target) == "function" then
471 elseif type(c.target) == "table" then
472 target = c.target.target
476 if c and (c.index or type(target) == "function") then
478 ctx.requested = ctx.requested or ctx.dispatched
481 if c and c.index then
482 local tpl = require "luci.template"
484 if util.copcall(tpl.render, "indexer", {}) then
489 if type(target) == "function" then
490 util.copcall(function()
491 local oldenv = getfenv(target)
492 local module = require(c.module)
493 local env = setmetatable({}, {__index=
496 return rawget(tbl, key) or module[key] or oldenv[key]
503 if type(c.target) == "table" then
504 ok, err = util.copcall(target, c.target, unpack(args))
506 ok, err = util.copcall(target, unpack(args))
509 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
510 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
511 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
514 if not root or not root.target then
515 error404("No root node was registered, this usually happens if no module was installed.\n" ..
516 "Install luci-mod-admin-full and retry. " ..
517 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
519 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
520 "If this url belongs to an extension, make sure it is properly installed.\n" ..
521 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
526 function createindex()
527 local controllers = { }
528 local base = "%s/controller/" % util.libpath()
531 for path in (fs.glob("%s*.lua" % base) or function() end) do
532 controllers[#controllers+1] = path
535 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
536 controllers[#controllers+1] = path
540 local cachedate = fs.stat(indexcache, "mtime")
543 for _, obj in ipairs(controllers) do
544 local omtime = fs.stat(obj, "mtime")
545 realdate = (omtime and omtime > realdate) and omtime or realdate
548 if cachedate > realdate and sys.process.info("uid") == 0 then
550 sys.process.info("uid") == fs.stat(indexcache, "uid")
551 and fs.stat(indexcache, "modestr") == "rw-------",
552 "Fatal: Indexcache is not sane!"
555 index = loadfile(indexcache)()
563 for _, path in ipairs(controllers) do
564 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
565 local mod = require(modname)
567 "Invalid controller file found\n" ..
568 "The file '" .. path .. "' contains an invalid module line.\n" ..
569 "Please verify whether the module name is set to '" .. modname ..
570 "' - It must correspond to the file path!")
572 local idx = mod.index
573 assert(type(idx) == "function",
574 "Invalid controller file found\n" ..
575 "The file '" .. path .. "' contains no index() function.\n" ..
576 "Please make sure that the controller contains a valid " ..
577 "index function and verify the spelling!")
583 local f = nixio.open(indexcache, "w", 600)
584 f:writeall(util.get_bytecode(index))
589 -- Build the index before if it does not exist yet.
590 function createtree()
596 local tree = {nodes={}, inreq=true}
599 ctx.treecache = setmetatable({}, {__mode="v"})
603 -- Load default translation
604 require "luci.i18n".loadc("base")
606 local scope = setmetatable({}, {__index = luci.dispatcher})
608 for k, v in pairs(index) do
614 local function modisort(a,b)
615 return modi[a].order < modi[b].order
618 for _, v in util.spairs(modi, modisort) do
619 scope._NAME = v.module
620 setfenv(v.func, scope)
627 function modifier(func, order)
628 context.modifiers[#context.modifiers+1] = {
636 function assign(path, clone, title, order)
637 local obj = node(unpack(path))
644 setmetatable(obj, {__index = _create_node(clone)})
649 function entry(path, target, title, order)
650 local c = node(unpack(path))
655 c.module = getfenv(2)._NAME
660 -- enabling the node.
662 return _create_node({...})
666 local c = _create_node({...})
668 c.module = getfenv(2)._NAME
675 local i, path = nil, {}
676 for i = 1, select('#', ...) do
677 local name, arg = nil, tostring(select(i, ...))
678 for name in arg:gmatch("[^/]+") do
683 for i = #path, 1, -1 do
684 local node = context.treecache[table.concat(path, ".", 1, i)]
685 if node and (i == #path or node.leaf) then
686 return node, build_url(unpack(path))
691 function _create_node(path)
696 local name = table.concat(path, ".")
697 local c = context.treecache[name]
700 local last = table.remove(path)
701 local parent = _create_node(path)
703 c = {nodes={}, auto=true}
704 -- the node is "in request" if the request path matches
705 -- at least up to the length of the node path
706 if parent.inreq and context.path[#path+1] == last then
709 parent.nodes[last] = c
710 context.treecache[name] = c
717 function _firstchild()
718 local path = { unpack(context.path) }
719 local name = table.concat(path, ".")
720 local node = context.treecache[name]
723 if node and node.nodes and next(node.nodes) then
725 for k, v in pairs(node.nodes) do
727 (v.order or 100) < (node.nodes[lowest].order or 100)
734 assert(lowest ~= nil,
735 "The requested node contains no childs, unable to redispatch")
737 path[#path+1] = lowest
741 function firstchild()
742 return { type = "firstchild", target = _firstchild }
748 for _, r in ipairs({...}) do
756 function rewrite(n, ...)
759 local dispatched = util.clone(context.dispatched)
762 table.remove(dispatched, 1)
765 for i, r in ipairs(req) do
766 table.insert(dispatched, i, r)
769 for _, r in ipairs({...}) do
770 dispatched[#dispatched+1] = r
778 local function _call(self, ...)
779 local func = getfenv()[self.name]
781 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
783 assert(type(func) == "function",
784 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
785 'of type "' .. type(func) .. '".')
787 if #self.argv > 0 then
788 return func(unpack(self.argv), ...)
794 function call(name, ...)
795 return {type = "call", argv = {...}, name = name, target = _call}
798 function post_on(params, name, ...)
809 return post_on(true, ...)
813 local _template = function(self, ...)
814 require "luci.template".render(self.view)
817 function template(name)
818 return {type = "template", view = name, target = _template}
822 local function _cbi(self, ...)
823 local cbi = require "luci.cbi"
824 local tpl = require "luci.template"
825 local http = require "luci.http"
827 local config = self.config or {}
828 local maps = cbi.load(self.model, ...)
833 for i, res in ipairs(maps) do
834 if util.instanceof(res, cbi.SimpleForm) then
835 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
838 io.stderr:write("please change %s to use the form() action instead.\n"
839 % table.concat(context.request, "/"))
843 local cstate = res:parse()
844 if cstate and (not state or cstate < state) then
849 local function _resolve_path(path)
850 return type(path) == "table" and build_url(unpack(path)) or path
853 if config.on_valid_to and state and state > 0 and state < 2 then
854 http.redirect(_resolve_path(config.on_valid_to))
858 if config.on_changed_to and state and state > 1 then
859 http.redirect(_resolve_path(config.on_changed_to))
863 if config.on_success_to and state and state > 0 then
864 http.redirect(_resolve_path(config.on_success_to))
868 if config.state_handler then
869 if not config.state_handler(state, maps) then
874 http.header("X-CBI-State", state or 0)
876 if not config.noheader then
877 tpl.render("cbi/header", {state = state})
882 local applymap = false
883 local pageaction = true
884 local parsechain = { }
886 local is_rollback, time_remaining = uci:rollback_pending()
888 for i, res in ipairs(maps) do
889 if res.apply_needed and res.parsechain then
891 for _, c in ipairs(res.parsechain) do
892 parsechain[#parsechain+1] = c
898 redirect = redirect or res.redirect
901 if res.pageaction == false then
906 messages = messages or { }
907 messages[#messages+1] = res.message
911 for i, res in ipairs(maps) do
915 confirmmap = (is_rollback and time_remaining or nil),
918 pageaction = pageaction,
919 parsechain = parsechain
923 if not config.nofooter then
924 tpl.render("cbi/footer", {
926 pageaction = pageaction,
929 autoapply = config.autoapply
934 function cbi(model, config)
937 post = { ["cbi.submit"] = true },
945 local function _arcombine(self, ...)
947 local target = #argv > 0 and self.targets[2] or self.targets[1]
948 setfenv(target.target, self.env)
949 target:target(unpack(argv))
952 function arcombine(trg1, trg2)
953 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
957 local function _form(self, ...)
958 local cbi = require "luci.cbi"
959 local tpl = require "luci.template"
960 local http = require "luci.http"
962 local maps = luci.cbi.load(self.model, ...)
966 for i, res in ipairs(maps) do
967 local cstate = res:parse()
968 if cstate and (not state or cstate < state) then
973 http.header("X-CBI-State", state or 0)
975 for i, res in ipairs(maps) do
984 post = { ["cbi.submit"] = true },
990 translate = i18n.translate
992 -- This function does not actually translate the given argument but
993 -- is used by build/i18n-scan.pl to find translatable entries.