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 return sid, sdat.values
191 local function session_setup(user, pass, allowed_users)
192 if util.contains(allowed_users, user) then
193 local login = util.ubus("session", "login", {
196 timeout = tonumber(luci.config.sauth.sessiontime)
199 local rp = context.requestpath
200 and table.concat(context.requestpath, "/") or ""
202 if type(login) == "table" and
203 type(login.ubus_rpc_session) == "string"
205 util.ubus("session", "set", {
206 ubus_rpc_session = login.ubus_rpc_session,
207 values = { token = sys.uniqueid(16) }
210 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
211 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
213 return session_retrieve(login.ubus_rpc_session)
216 io.stderr:write("luci: failed login on /%s for %s from %s\n"
217 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
223 function dispatch(request)
224 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
228 local conf = require "luci.config"
230 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
232 local i18n = require "luci.i18n"
233 local lang = conf.main.lang or "auto"
234 if lang == "auto" then
235 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
236 for aclang in aclang:gmatch("[%w_-]+") do
237 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
238 if country and culture then
239 local cc = "%s_%s" %{ country, culture:lower() }
240 if conf.languages[cc] then
243 elseif conf.languages[country] then
247 elseif conf.languages[aclang] then
253 if lang == "auto" then
256 i18n.setlanguage(lang)
267 ctx.requestargs = ctx.requestargs or args
272 for i, s in ipairs(request) do
281 util.update(track, c)
289 for j=n+1, #request do
290 args[#args+1] = request[j]
291 freq[#freq+1] = request[j]
295 ctx.requestpath = ctx.requestpath or freq
299 i18n.loadc(track.i18n)
302 -- Init template engine
303 if (c and c.index) or not track.notemplate then
304 local tpl = require("luci.template")
305 local media = track.mediaurlbase or luci.config.main.mediaurlbase
306 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
308 for name, theme in pairs(luci.config.themes) do
309 if name:sub(1,1) ~= "." and pcall(tpl.Template,
310 "themes/%s/header" % fs.basename(theme)) then
314 assert(media, "No valid theme found")
317 local function _ifattr(cond, key, val)
319 local env = getfenv(3)
320 local scope = (type(env.self) == "table") and env.self
321 if type(val) == "table" then
322 if not next(val) then
325 val = util.serialize_json(val)
328 return string.format(
329 ' %s="%s"', tostring(key),
330 util.pcdata(tostring( val
331 or (type(env[key]) ~= "function" and env[key])
332 or (scope and type(scope[key]) ~= "function" and scope[key])
340 tpl.context.viewns = setmetatable({
342 include = function(name) tpl.Template(name):render(getfenv(2)) end;
343 translate = i18n.translate;
344 translatef = i18n.translatef;
345 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
346 striptags = util.striptags;
347 pcdata = util.pcdata;
349 theme = fs.basename(media);
350 resource = luci.config.main.resourcebase;
351 ifattr = function(...) return _ifattr(...) end;
352 attr = function(...) return _ifattr(true, ...) end;
354 }, {__index=function(tbl, key)
355 if key == "controller" then
357 elseif key == "REQUEST_URI" then
358 return build_url(unpack(ctx.requestpath))
359 elseif key == "FULL_REQUEST_URI" then
360 local url = { http.getenv("SCRIPT_NAME"), http.getenv("PATH_INFO") }
361 local query = http.getenv("QUERY_STRING")
362 if query and #query > 0 then
366 return table.concat(url, "")
367 elseif key == "token" then
370 return rawget(tbl, key) or _G[key]
375 track.dependent = (track.dependent ~= false)
376 assert(not track.dependent or not track.auto,
377 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
378 "has no parent node so the access to this location has been denied.\n" ..
379 "This is a software bug, please report this message at " ..
380 "https://github.com/openwrt/luci/issues"
383 if track.sysauth and not ctx.authsession then
384 local authen = track.sysauth_authenticator
385 local _, sid, sdat, default_user, allowed_users
387 if type(authen) == "string" and authen ~= "htmlauth" then
388 error500("Unsupported authenticator %q configured" % authen)
392 if type(track.sysauth) == "table" then
393 default_user, allowed_users = nil, track.sysauth
395 default_user, allowed_users = track.sysauth, { track.sysauth }
398 if type(authen) == "function" then
399 _, sid = authen(sys.user.checkpasswd, allowed_users)
401 sid = http.getcookie("sysauth")
404 sid, sdat = session_retrieve(sid, allowed_users)
406 if not (sid and sdat) and authen == "htmlauth" then
407 local user = http.getenv("HTTP_AUTH_USER")
408 local pass = http.getenv("HTTP_AUTH_PASS")
410 if user == nil and pass == nil then
411 user = http.formvalue("luci_username")
412 pass = http.formvalue("luci_password")
415 sid, sdat = session_setup(user, pass, allowed_users)
418 local tmpl = require "luci.template"
422 http.status(403, "Forbidden")
423 tmpl.render(track.sysauth_template or "sysauth", {
424 duser = default_user,
431 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
432 http.redirect(build_url(unpack(ctx.requestpath)))
435 if not sid or not sdat then
436 http.status(403, "Forbidden")
440 ctx.authsession = sid
441 ctx.authtoken = sdat.token
442 ctx.authuser = sdat.username
445 if c and require_post_security(c.target) then
446 if not test_post_security(c) then
451 if track.setgroup then
452 sys.process.setgroup(track.setgroup)
455 if track.setuser then
456 sys.process.setuser(track.setuser)
461 if type(c.target) == "function" then
463 elseif type(c.target) == "table" then
464 target = c.target.target
468 if c and (c.index or type(target) == "function") then
470 ctx.requested = ctx.requested or ctx.dispatched
473 if c and c.index then
474 local tpl = require "luci.template"
476 if util.copcall(tpl.render, "indexer", {}) then
481 if type(target) == "function" then
482 util.copcall(function()
483 local oldenv = getfenv(target)
484 local module = require(c.module)
485 local env = setmetatable({}, {__index=
488 return rawget(tbl, key) or module[key] or oldenv[key]
495 if type(c.target) == "table" then
496 ok, err = util.copcall(target, c.target, unpack(args))
498 ok, err = util.copcall(target, unpack(args))
501 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
502 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
503 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
506 if not root or not root.target then
507 error404("No root node was registered, this usually happens if no module was installed.\n" ..
508 "Install luci-mod-admin-full and retry. " ..
509 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
511 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
512 "If this url belongs to an extension, make sure it is properly installed.\n" ..
513 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
518 function createindex()
519 local controllers = { }
520 local base = "%s/controller/" % util.libpath()
523 for path in (fs.glob("%s*.lua" % base) or function() end) do
524 controllers[#controllers+1] = path
527 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
528 controllers[#controllers+1] = path
532 local cachedate = fs.stat(indexcache, "mtime")
535 for _, obj in ipairs(controllers) do
536 local omtime = fs.stat(obj, "mtime")
537 realdate = (omtime and omtime > realdate) and omtime or realdate
540 if cachedate > realdate and sys.process.info("uid") == 0 then
542 sys.process.info("uid") == fs.stat(indexcache, "uid")
543 and fs.stat(indexcache, "modestr") == "rw-------",
544 "Fatal: Indexcache is not sane!"
547 index = loadfile(indexcache)()
555 for _, path in ipairs(controllers) do
556 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
557 local mod = require(modname)
559 "Invalid controller file found\n" ..
560 "The file '" .. path .. "' contains an invalid module line.\n" ..
561 "Please verify whether the module name is set to '" .. modname ..
562 "' - It must correspond to the file path!")
564 local idx = mod.index
565 assert(type(idx) == "function",
566 "Invalid controller file found\n" ..
567 "The file '" .. path .. "' contains no index() function.\n" ..
568 "Please make sure that the controller contains a valid " ..
569 "index function and verify the spelling!")
575 local f = nixio.open(indexcache, "w", 600)
576 f:writeall(util.get_bytecode(index))
581 -- Build the index before if it does not exist yet.
582 function createtree()
588 local tree = {nodes={}, inreq=true}
591 ctx.treecache = setmetatable({}, {__mode="v"})
595 -- Load default translation
596 require "luci.i18n".loadc("base")
598 local scope = setmetatable({}, {__index = luci.dispatcher})
600 for k, v in pairs(index) do
606 local function modisort(a,b)
607 return modi[a].order < modi[b].order
610 for _, v in util.spairs(modi, modisort) do
611 scope._NAME = v.module
612 setfenv(v.func, scope)
619 function modifier(func, order)
620 context.modifiers[#context.modifiers+1] = {
628 function assign(path, clone, title, order)
629 local obj = node(unpack(path))
636 setmetatable(obj, {__index = _create_node(clone)})
641 function entry(path, target, title, order)
642 local c = node(unpack(path))
647 c.module = getfenv(2)._NAME
652 -- enabling the node.
654 return _create_node({...})
658 local c = _create_node({...})
660 c.module = getfenv(2)._NAME
667 local i, path = nil, {}
668 for i = 1, select('#', ...) do
669 local name, arg = nil, tostring(select(i, ...))
670 for name in arg:gmatch("[^/]+") do
675 for i = #path, 1, -1 do
676 local node = context.treecache[table.concat(path, ".", 1, i)]
677 if node and (i == #path or node.leaf) then
678 return node, build_url(unpack(path))
683 function _create_node(path)
688 local name = table.concat(path, ".")
689 local c = context.treecache[name]
692 local last = table.remove(path)
693 local parent = _create_node(path)
695 c = {nodes={}, auto=true}
696 -- the node is "in request" if the request path matches
697 -- at least up to the length of the node path
698 if parent.inreq and context.path[#path+1] == last then
701 parent.nodes[last] = c
702 context.treecache[name] = c
709 function _firstchild()
710 local path = { unpack(context.path) }
711 local name = table.concat(path, ".")
712 local node = context.treecache[name]
715 if node and node.nodes and next(node.nodes) then
717 for k, v in pairs(node.nodes) do
719 (v.order or 100) < (node.nodes[lowest].order or 100)
726 assert(lowest ~= nil,
727 "The requested node contains no childs, unable to redispatch")
729 path[#path+1] = lowest
733 function firstchild()
734 return { type = "firstchild", target = _firstchild }
740 for _, r in ipairs({...}) do
748 function rewrite(n, ...)
751 local dispatched = util.clone(context.dispatched)
754 table.remove(dispatched, 1)
757 for i, r in ipairs(req) do
758 table.insert(dispatched, i, r)
761 for _, r in ipairs({...}) do
762 dispatched[#dispatched+1] = r
770 local function _call(self, ...)
771 local func = getfenv()[self.name]
773 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
775 assert(type(func) == "function",
776 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
777 'of type "' .. type(func) .. '".')
779 if #self.argv > 0 then
780 return func(unpack(self.argv), ...)
786 function call(name, ...)
787 return {type = "call", argv = {...}, name = name, target = _call}
790 function post_on(params, name, ...)
801 return post_on(true, ...)
805 local _template = function(self, ...)
806 require "luci.template".render(self.view)
809 function template(name)
810 return {type = "template", view = name, target = _template}
814 local function _cbi(self, ...)
815 local cbi = require "luci.cbi"
816 local tpl = require "luci.template"
817 local http = require "luci.http"
819 local config = self.config or {}
820 local maps = cbi.load(self.model, ...)
825 for i, res in ipairs(maps) do
826 if util.instanceof(res, cbi.SimpleForm) then
827 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
830 io.stderr:write("please change %s to use the form() action instead.\n"
831 % table.concat(context.request, "/"))
835 local cstate = res:parse()
836 if cstate and (not state or cstate < state) then
841 local function _resolve_path(path)
842 return type(path) == "table" and build_url(unpack(path)) or path
845 if config.on_valid_to and state and state > 0 and state < 2 then
846 http.redirect(_resolve_path(config.on_valid_to))
850 if config.on_changed_to and state and state > 1 then
851 http.redirect(_resolve_path(config.on_changed_to))
855 if config.on_success_to and state and state > 0 then
856 http.redirect(_resolve_path(config.on_success_to))
860 if config.state_handler then
861 if not config.state_handler(state, maps) then
866 http.header("X-CBI-State", state or 0)
868 if not config.noheader then
869 tpl.render("cbi/header", {state = state})
874 local applymap = false
875 local pageaction = true
876 local parsechain = { }
878 for i, res in ipairs(maps) do
879 if res.apply_needed and res.parsechain then
881 for _, c in ipairs(res.parsechain) do
882 parsechain[#parsechain+1] = c
888 redirect = redirect or res.redirect
891 if res.pageaction == false then
896 messages = messages or { }
897 messages[#messages+1] = res.message
901 for i, res in ipairs(maps) do
907 pageaction = pageaction,
908 parsechain = parsechain
912 if not config.nofooter then
913 tpl.render("cbi/footer", {
915 pageaction = pageaction,
918 autoapply = config.autoapply
923 function cbi(model, config)
926 post = { ["cbi.submit"] = true },
934 local function _arcombine(self, ...)
936 local target = #argv > 0 and self.targets[2] or self.targets[1]
937 setfenv(target.target, self.env)
938 target:target(unpack(argv))
941 function arcombine(trg1, trg2)
942 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
946 local function _form(self, ...)
947 local cbi = require "luci.cbi"
948 local tpl = require "luci.template"
949 local http = require "luci.http"
951 local maps = luci.cbi.load(self.model, ...)
955 for i, res in ipairs(maps) do
956 local cstate = res:parse()
957 if cstate and (not state or cstate < state) then
962 http.header("X-CBI-State", state or 0)
964 for i, res in ipairs(maps) do
973 post = { ["cbi.submit"] = true },
979 translate = i18n.translate
981 -- This function does not actually translate the given argument but
982 -- is used by build/i18n-scan.pl to find translatable entries.