1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Licensed to the public under the Apache License 2.0.
4 local fs = require "nixio.fs"
5 local sys = require "luci.sys"
6 local util = require "luci.util"
7 local http = require "luci.http"
8 local nixio = require "nixio", require "nixio.util"
10 module("luci.dispatcher", package.seeall)
11 context = util.threadlocal()
12 uci = require "luci.model.uci"
13 i18n = require "luci.i18n"
25 function build_url(...)
27 local url = { http.getenv("SCRIPT_NAME") or "" }
30 for k, v in pairs(context.urltoken) do
32 url[#url+1] = http.urlencode(k)
34 url[#url+1] = http.urlencode(v)
38 for _, p in ipairs(path) do
39 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
45 return table.concat(url, "")
48 function node_visible(node)
51 (not node.title or #node.title == 0) or
52 (not node.target or node.hidden == true) or
53 (type(node.target) == "table" and node.target.type == "firstchild" and
54 (type(node.nodes) ~= "table" or not next(node.nodes)))
60 function node_childs(node)
64 for k, v in util.spairs(node.nodes,
66 return (node.nodes[a].order or 100)
67 < (node.nodes[b].order or 100)
70 if node_visible(v) then
79 function error404(message)
80 http.status(404, "Not Found")
81 message = message or "Not Found"
83 require("luci.template")
84 if not util.copcall(luci.template.render, "error404") then
85 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 authenticator.htmlauth(validator, accs, default)
108 local user = http.formvalue("luci_username")
109 local pass = http.formvalue("luci_password")
111 if user and validator(user, pass) then
115 if context.urltoken.stok then
116 context.urltoken.stok = nil
117 http.header("Set-Cookie", "sysauth=; path="..build_url())
118 http.redirect(build_url())
121 require("luci.template")
123 http.status(403, "Forbidden")
124 luci.template.render("sysauth", {duser=default, fuser=user})
131 function httpdispatch(request, prefix)
132 http.context.request = request
136 context.urltoken = {}
138 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
141 for _, node in ipairs(prefix) do
146 local tokensok = true
147 for node in pathinfo:gmatch("[^/]+") do
150 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
153 context.urltoken[tkey] = tval
160 local stat, err = util.coxpcall(function()
161 dispatch(context.request)
166 --context._disable_memtrace()
169 function dispatch(request)
170 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
174 local conf = require "luci.config"
176 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
178 local lang = conf.main.lang or "auto"
179 if lang == "auto" then
180 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
181 for lpat in aclang:gmatch("[%w-]+") do
182 lpat = lpat and lpat:gsub("-", "_")
183 if conf.languages[lpat] then
189 require "luci.i18n".setlanguage(lang)
200 ctx.requestargs = ctx.requestargs or args
202 local token = ctx.urltoken
206 for i, s in ipairs(request) do
215 util.update(track, c)
223 for j=n+1, #request do
224 args[#args+1] = request[j]
225 freq[#freq+1] = request[j]
229 ctx.requestpath = ctx.requestpath or freq
233 i18n.loadc(track.i18n)
236 -- Init template engine
237 if (c and c.index) or not track.notemplate then
238 local tpl = require("luci.template")
239 local media = track.mediaurlbase or luci.config.main.mediaurlbase
240 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
242 for name, theme in pairs(luci.config.themes) do
243 if name:sub(1,1) ~= "." and pcall(tpl.Template,
244 "themes/%s/header" % fs.basename(theme)) then
248 assert(media, "No valid theme found")
251 local function _ifattr(cond, key, val)
253 local env = getfenv(3)
254 local scope = (type(env.self) == "table") and env.self
255 return string.format(
256 ' %s="%s"', tostring(key),
257 util.pcdata(tostring( val
258 or (type(env[key]) ~= "function" and env[key])
259 or (scope and type(scope[key]) ~= "function" and scope[key])
267 tpl.context.viewns = setmetatable({
269 include = function(name) tpl.Template(name):render(getfenv(2)) end;
270 translate = i18n.translate;
271 translatef = i18n.translatef;
272 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
273 striptags = util.striptags;
274 pcdata = util.pcdata;
276 theme = fs.basename(media);
277 resource = luci.config.main.resourcebase;
278 ifattr = function(...) return _ifattr(...) end;
279 attr = function(...) return _ifattr(true, ...) end;
280 }, {__index=function(table, key)
281 if key == "controller" then
283 elseif key == "REQUEST_URI" then
284 return build_url(unpack(ctx.requestpath))
286 return rawget(table, key) or _G[key]
291 track.dependent = (track.dependent ~= false)
292 assert(not track.dependent or not track.auto,
293 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
294 "has no parent node so the access to this location has been denied.\n" ..
295 "This is a software bug, please report this message at " ..
296 "http://luci.subsignal.org/trac/newticket"
299 if track.sysauth then
300 local authen = type(track.sysauth_authenticator) == "function"
301 and track.sysauth_authenticator
302 or authenticator[track.sysauth_authenticator]
304 local def = (type(track.sysauth) == "string") and track.sysauth
305 local accs = def and {track.sysauth} or track.sysauth
306 local sess = ctx.authsession
307 local verifytoken = false
309 sess = http.getcookie("sysauth")
310 sess = sess and sess:match("^[a-f0-9]*$")
314 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
318 if not verifytoken or ctx.urltoken.stok == sdat.token then
322 local eu = http.getenv("HTTP_AUTH_USER")
323 local ep = http.getenv("HTTP_AUTH_PASS")
324 if eu and ep and sys.user.checkpasswd(eu, ep) then
325 authen = function() return eu end
329 if not util.contains(accs, user) then
331 local user, sess = authen(sys.user.checkpasswd, accs, def)
332 if not user or not util.contains(accs, user) then
336 local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime })
338 local token = sys.uniqueid(16)
339 util.ubus("session", "set", {
340 ubus_rpc_session = sdat.ubus_rpc_session,
344 section = sys.uniqueid(16)
347 sess = sdat.ubus_rpc_session
348 ctx.urltoken.stok = token
353 http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
354 http.redirect(build_url(unpack(ctx.requestpath)))
355 ctx.authsession = sess
360 http.status(403, "Forbidden")
364 ctx.authsession = sess
369 if track.setgroup then
370 sys.process.setgroup(track.setgroup)
373 if track.setuser then
374 sys.process.setuser(track.setuser)
379 if type(c.target) == "function" then
381 elseif type(c.target) == "table" then
382 target = c.target.target
386 if c and (c.index or type(target) == "function") then
388 ctx.requested = ctx.requested or ctx.dispatched
391 if c and c.index then
392 local tpl = require "luci.template"
394 if util.copcall(tpl.render, "indexer", {}) then
399 if type(target) == "function" then
400 util.copcall(function()
401 local oldenv = getfenv(target)
402 local module = require(c.module)
403 local env = setmetatable({}, {__index=
406 return rawget(tbl, key) or module[key] or oldenv[key]
413 if type(c.target) == "table" then
414 ok, err = util.copcall(target, c.target, unpack(args))
416 ok, err = util.copcall(target, unpack(args))
419 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
420 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
421 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
424 if not root or not root.target then
425 error404("No root node was registered, this usually happens if no module was installed.\n" ..
426 "Install luci-mod-admin-full and retry. " ..
427 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
429 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
430 "If this url belongs to an extension, make sure it is properly installed.\n" ..
431 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
436 function createindex()
437 local controllers = { }
438 local base = "%s/controller/" % util.libpath()
441 for path in (fs.glob("%s*.lua" % base) or function() end) do
442 controllers[#controllers+1] = path
445 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
446 controllers[#controllers+1] = path
450 local cachedate = fs.stat(indexcache, "mtime")
453 for _, obj in ipairs(controllers) do
454 local omtime = fs.stat(obj, "mtime")
455 realdate = (omtime and omtime > realdate) and omtime or realdate
458 if cachedate > realdate and sys.process.info("uid") == 0 then
460 sys.process.info("uid") == fs.stat(indexcache, "uid")
461 and fs.stat(indexcache, "modestr") == "rw-------",
462 "Fatal: Indexcache is not sane!"
465 index = loadfile(indexcache)()
473 for _, path in ipairs(controllers) do
474 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
475 local mod = require(modname)
477 "Invalid controller file found\n" ..
478 "The file '" .. path .. "' contains an invalid module line.\n" ..
479 "Please verify whether the module name is set to '" .. modname ..
480 "' - It must correspond to the file path!")
482 local idx = mod.index
483 assert(type(idx) == "function",
484 "Invalid controller file found\n" ..
485 "The file '" .. path .. "' contains no index() function.\n" ..
486 "Please make sure that the controller contains a valid " ..
487 "index function and verify the spelling!")
493 local f = nixio.open(indexcache, "w", 600)
494 f:writeall(util.get_bytecode(index))
499 -- Build the index before if it does not exist yet.
500 function createtree()
506 local tree = {nodes={}, inreq=true}
509 ctx.treecache = setmetatable({}, {__mode="v"})
513 -- Load default translation
514 require "luci.i18n".loadc("base")
516 local scope = setmetatable({}, {__index = luci.dispatcher})
518 for k, v in pairs(index) do
524 local function modisort(a,b)
525 return modi[a].order < modi[b].order
528 for _, v in util.spairs(modi, modisort) do
529 scope._NAME = v.module
530 setfenv(v.func, scope)
537 function modifier(func, order)
538 context.modifiers[#context.modifiers+1] = {
546 function assign(path, clone, title, order)
547 local obj = node(unpack(path))
554 setmetatable(obj, {__index = _create_node(clone)})
559 function entry(path, target, title, order)
560 local c = node(unpack(path))
565 c.module = getfenv(2)._NAME
570 -- enabling the node.
572 return _create_node({...})
576 local c = _create_node({...})
578 c.module = getfenv(2)._NAME
584 function _create_node(path)
589 local name = table.concat(path, ".")
590 local c = context.treecache[name]
593 local last = table.remove(path)
594 local parent = _create_node(path)
596 c = {nodes={}, auto=true}
597 -- the node is "in request" if the request path matches
598 -- at least up to the length of the node path
599 if parent.inreq and context.path[#path+1] == last then
602 parent.nodes[last] = c
603 context.treecache[name] = c
610 function _firstchild()
611 local path = { unpack(context.path) }
612 local name = table.concat(path, ".")
613 local node = context.treecache[name]
616 if node and node.nodes and next(node.nodes) then
618 for k, v in pairs(node.nodes) do
620 (v.order or 100) < (node.nodes[lowest].order or 100)
627 assert(lowest ~= nil,
628 "The requested node contains no childs, unable to redispatch")
630 path[#path+1] = lowest
634 function firstchild()
635 return { type = "firstchild", target = _firstchild }
641 for _, r in ipairs({...}) do
649 function rewrite(n, ...)
652 local dispatched = util.clone(context.dispatched)
655 table.remove(dispatched, 1)
658 for i, r in ipairs(req) do
659 table.insert(dispatched, i, r)
662 for _, r in ipairs({...}) do
663 dispatched[#dispatched+1] = r
671 local function _call(self, ...)
672 local func = getfenv()[self.name]
674 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
676 assert(type(func) == "function",
677 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
678 'of type "' .. type(func) .. '".')
680 if #self.argv > 0 then
681 return func(unpack(self.argv), ...)
687 function call(name, ...)
688 return {type = "call", argv = {...}, name = name, target = _call}
692 local _template = function(self, ...)
693 require "luci.template".render(self.view)
696 function template(name)
697 return {type = "template", view = name, target = _template}
701 local function _cbi(self, ...)
702 local cbi = require "luci.cbi"
703 local tpl = require "luci.template"
704 local http = require "luci.http"
706 local config = self.config or {}
707 local maps = cbi.load(self.model, ...)
711 for i, res in ipairs(maps) do
713 local cstate = res:parse()
714 if cstate and (not state or cstate < state) then
719 local function _resolve_path(path)
720 return type(path) == "table" and build_url(unpack(path)) or path
723 if config.on_valid_to and state and state > 0 and state < 2 then
724 http.redirect(_resolve_path(config.on_valid_to))
728 if config.on_changed_to and state and state > 1 then
729 http.redirect(_resolve_path(config.on_changed_to))
733 if config.on_success_to and state and state > 0 then
734 http.redirect(_resolve_path(config.on_success_to))
738 if config.state_handler then
739 if not config.state_handler(state, maps) then
744 http.header("X-CBI-State", state or 0)
746 if not config.noheader then
747 tpl.render("cbi/header", {state = state})
752 local applymap = false
753 local pageaction = true
754 local parsechain = { }
756 for i, res in ipairs(maps) do
757 if res.apply_needed and res.parsechain then
759 for _, c in ipairs(res.parsechain) do
760 parsechain[#parsechain+1] = c
766 redirect = redirect or res.redirect
769 if res.pageaction == false then
774 messages = messages or { }
775 messages[#messages+1] = res.message
779 for i, res in ipairs(maps) do
785 pageaction = pageaction,
786 parsechain = parsechain
790 if not config.nofooter then
791 tpl.render("cbi/footer", {
793 pageaction = pageaction,
796 autoapply = config.autoapply
801 function cbi(model, config)
802 return {type = "cbi", config = config, model = model, target = _cbi}
806 local function _arcombine(self, ...)
808 local target = #argv > 0 and self.targets[2] or self.targets[1]
809 setfenv(target.target, self.env)
810 target:target(unpack(argv))
813 function arcombine(trg1, trg2)
814 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
818 local function _form(self, ...)
819 local cbi = require "luci.cbi"
820 local tpl = require "luci.template"
821 local http = require "luci.http"
823 local maps = luci.cbi.load(self.model, ...)
826 for i, res in ipairs(maps) do
827 local cstate = res:parse()
828 if cstate and (not state or cstate < state) then
833 http.header("X-CBI-State", state or 0)
835 for i, res in ipairs(maps) do
842 return {type = "cbi", model = model, target = _form}
845 translate = i18n.translate
847 -- This function does not actually translate the given argument but
848 -- is used by build/i18n-scan.pl to find translatable entries.