5f893dad95f9d698ab79f105e5cf464d2cc1a893
[project/luci.git] / libs / web / luasrc / dispatcher.lua
1 --[[
2 LuCI - Dispatcher
3
4 Description:
5 The request dispatcher and module dispatcher generators
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27 --- LuCI web dispatcher.
28 local fs = require "nixio.fs"
29 local sys = require "luci.sys"
30 local init = require "luci.init"
31 local util = require "luci.util"
32 local http = require "luci.http"
33 local nixio = require "nixio", require "nixio.util"
34
35 module("luci.dispatcher", package.seeall)
36 context = util.threadlocal()
37 uci = require "luci.model.uci"
38 i18n = require "luci.i18n"
39 _M.fs = fs
40
41 authenticator = {}
42
43 -- Index table
44 local index = nil
45
46 -- Fastindex
47 local fi
48
49
50 --- Build the URL relative to the server webroot from given virtual path.
51 -- @param ... Virtual path
52 -- @return Relative URL
53 function build_url(...)
54 local path = {...}
55 local url = { http.getenv("SCRIPT_NAME") or "" }
56
57 local k, v
58 for k, v in pairs(context.urltoken) do
59 url[#url+1] = "/;"
60 url[#url+1] = http.urlencode(k)
61 url[#url+1] = "="
62 url[#url+1] = http.urlencode(v)
63 end
64
65 local p
66 for _, p in ipairs(path) do
67 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
68 url[#url+1] = "/"
69 url[#url+1] = p
70 end
71 end
72
73 return table.concat(url, "")
74 end
75
76 --- Send a 404 error code and render the "error404" template if available.
77 -- @param message Custom error message (optional)
78 -- @return false
79 function error404(message)
80 luci.http.status(404, "Not Found")
81 message = message or "Not Found"
82
83 require("luci.template")
84 if not luci.util.copcall(luci.template.render, "error404") then
85 luci.http.prepare_content("text/plain")
86 luci.http.write(message)
87 end
88 return false
89 end
90
91 --- Send a 500 error code and render the "error500" template if available.
92 -- @param message Custom error message (optional)#
93 -- @return false
94 function error500(message)
95 luci.util.perror(message)
96 if not context.template_header_sent then
97 luci.http.status(500, "Internal Server Error")
98 luci.http.prepare_content("text/plain")
99 luci.http.write(message)
100 else
101 require("luci.template")
102 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
103 luci.http.prepare_content("text/plain")
104 luci.http.write(message)
105 end
106 end
107 return false
108 end
109
110 function authenticator.htmlauth(validator, accs, default)
111 local user = luci.http.formvalue("username")
112 local pass = luci.http.formvalue("password")
113
114 if user and validator(user, pass) then
115 return user
116 end
117
118 require("luci.i18n")
119 require("luci.template")
120 context.path = {}
121 luci.template.render("sysauth", {duser=default, fuser=user})
122 return false
123
124 end
125
126 --- Dispatch an HTTP request.
127 -- @param request LuCI HTTP Request object
128 function httpdispatch(request, prefix)
129 luci.http.context.request = request
130
131 local r = {}
132 context.request = r
133 context.urltoken = {}
134
135 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
136
137 if prefix then
138 for _, node in ipairs(prefix) do
139 r[#r+1] = node
140 end
141 end
142
143 local tokensok = true
144 for node in pathinfo:gmatch("[^/]+") do
145 local tkey, tval
146 if tokensok then
147 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
148 end
149 if tkey then
150 context.urltoken[tkey] = tval
151 else
152 tokensok = false
153 r[#r+1] = node
154 end
155 end
156
157 local stat, err = util.coxpcall(function()
158 dispatch(context.request)
159 end, error500)
160
161 luci.http.close()
162
163 --context._disable_memtrace()
164 end
165
166 --- Dispatches a LuCI virtual path.
167 -- @param request Virtual path
168 function dispatch(request)
169 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
170 local ctx = context
171 ctx.path = request
172
173 local conf = require "luci.config"
174 assert(conf.main,
175 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
176
177 local lang = conf.main.lang or "auto"
178 if lang == "auto" then
179 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
180 for lpat in aclang:gmatch("[%w-]+") do
181 lpat = lpat and lpat:gsub("-", "_")
182 if conf.languages[lpat] then
183 lang = lpat
184 break
185 end
186 end
187 end
188 require "luci.i18n".setlanguage(lang)
189
190 local c = ctx.tree
191 local stat
192 if not c then
193 c = createtree()
194 end
195
196 local track = {}
197 local args = {}
198 ctx.args = args
199 ctx.requestargs = ctx.requestargs or args
200 local n
201 local token = ctx.urltoken
202 local preq = {}
203 local freq = {}
204
205 for i, s in ipairs(request) do
206 preq[#preq+1] = s
207 freq[#freq+1] = s
208 c = c.nodes[s]
209 n = i
210 if not c then
211 break
212 end
213
214 util.update(track, c)
215
216 if c.leaf then
217 break
218 end
219 end
220
221 if c and c.leaf then
222 for j=n+1, #request do
223 args[#args+1] = request[j]
224 freq[#freq+1] = request[j]
225 end
226 end
227
228 ctx.requestpath = ctx.requestpath or freq
229 ctx.path = preq
230
231 if track.i18n then
232 i18n.loadc(track.i18n)
233 end
234
235 -- Init template engine
236 if (c and c.index) or not track.notemplate then
237 local tpl = require("luci.template")
238 local media = track.mediaurlbase or luci.config.main.mediaurlbase
239 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
240 media = nil
241 for name, theme in pairs(luci.config.themes) do
242 if name:sub(1,1) ~= "." and pcall(tpl.Template,
243 "themes/%s/header" % fs.basename(theme)) then
244 media = theme
245 end
246 end
247 assert(media, "No valid theme found")
248 end
249
250 tpl.context.viewns = setmetatable({
251 write = luci.http.write;
252 include = function(name) tpl.Template(name):render(getfenv(2)) end;
253 translate = i18n.translate;
254 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
255 striptags = util.striptags;
256 pcdata = util.pcdata;
257 media = media;
258 theme = fs.basename(media);
259 resource = luci.config.main.resourcebase
260 }, {__index=function(table, key)
261 if key == "controller" then
262 return build_url()
263 elseif key == "REQUEST_URI" then
264 return build_url(unpack(ctx.requestpath))
265 else
266 return rawget(table, key) or _G[key]
267 end
268 end})
269 end
270
271 track.dependent = (track.dependent ~= false)
272 assert(not track.dependent or not track.auto,
273 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
274 "has no parent node so the access to this location has been denied.\n" ..
275 "This is a software bug, please report this message at " ..
276 "http://luci.subsignal.org/trac/newticket"
277 )
278
279 if track.sysauth then
280 local sauth = require "luci.sauth"
281
282 local authen = type(track.sysauth_authenticator) == "function"
283 and track.sysauth_authenticator
284 or authenticator[track.sysauth_authenticator]
285
286 local def = (type(track.sysauth) == "string") and track.sysauth
287 local accs = def and {track.sysauth} or track.sysauth
288 local sess = ctx.authsession
289 local verifytoken = false
290 if not sess then
291 sess = luci.http.getcookie("sysauth")
292 sess = sess and sess:match("^[a-f0-9]*$")
293 verifytoken = true
294 end
295
296 local sdat = sauth.read(sess)
297 local user
298
299 if sdat then
300 sdat = loadstring(sdat)
301 setfenv(sdat, {})
302 sdat = sdat()
303 if not verifytoken or ctx.urltoken.stok == sdat.token then
304 user = sdat.user
305 end
306 else
307 local eu = http.getenv("HTTP_AUTH_USER")
308 local ep = http.getenv("HTTP_AUTH_PASS")
309 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
310 authen = function() return eu end
311 end
312 end
313
314 if not util.contains(accs, user) then
315 if authen then
316 ctx.urltoken.stok = nil
317 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
318 if not user or not util.contains(accs, user) then
319 return
320 else
321 local sid = sess or luci.sys.uniqueid(16)
322 if not sess then
323 local token = luci.sys.uniqueid(16)
324 sauth.write(sid, util.get_bytecode({
325 user=user,
326 token=token,
327 secret=luci.sys.uniqueid(16)
328 }))
329 ctx.urltoken.stok = token
330 end
331 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
332 ctx.authsession = sid
333 ctx.authuser = user
334 end
335 else
336 luci.http.status(403, "Forbidden")
337 return
338 end
339 else
340 ctx.authsession = sess
341 ctx.authuser = user
342 end
343 end
344
345 if track.setgroup then
346 luci.sys.process.setgroup(track.setgroup)
347 end
348
349 if track.setuser then
350 luci.sys.process.setuser(track.setuser)
351 end
352
353 local target = nil
354 if c then
355 if type(c.target) == "function" then
356 target = c.target
357 elseif type(c.target) == "table" then
358 target = c.target.target
359 end
360 end
361
362 if c and (c.index or type(target) == "function") then
363 ctx.dispatched = c
364 ctx.requested = ctx.requested or ctx.dispatched
365 end
366
367 if c and c.index then
368 local tpl = require "luci.template"
369
370 if util.copcall(tpl.render, "indexer", {}) then
371 return true
372 end
373 end
374
375 if type(target) == "function" then
376 util.copcall(function()
377 local oldenv = getfenv(target)
378 local module = require(c.module)
379 local env = setmetatable({}, {__index=
380
381 function(tbl, key)
382 return rawget(tbl, key) or module[key] or oldenv[key]
383 end})
384
385 setfenv(target, env)
386 end)
387
388 local ok, err
389 if type(c.target) == "table" then
390 ok, err = util.copcall(target, c.target, unpack(args))
391 else
392 ok, err = util.copcall(target, unpack(args))
393 end
394 assert(ok,
395 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
396 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
397 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
398 else
399 local root = node()
400 if not root or not root.target then
401 error404("No root node was registered, this usually happens if no module was installed.\n" ..
402 "Install luci-mod-admin-full and retry. " ..
403 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
404 else
405 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
406 "If this url belongs to an extension, make sure it is properly installed.\n" ..
407 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
408 end
409 end
410 end
411
412 --- Generate the dispatching index using the best possible strategy.
413 function createindex()
414 local path = luci.util.libpath() .. "/controller/"
415 local suff = { ".lua", ".lua.gz" }
416
417 if luci.util.copcall(require, "luci.fastindex") then
418 createindex_fastindex(path, suff)
419 else
420 createindex_plain(path, suff)
421 end
422 end
423
424 --- Generate the dispatching index using the fastindex C-indexer.
425 -- @param path Controller base directory
426 -- @param suffixes Controller file suffixes
427 function createindex_fastindex(path, suffixes)
428 index = {}
429
430 if not fi then
431 fi = luci.fastindex.new("index")
432 for _, suffix in ipairs(suffixes) do
433 fi.add(path .. "*" .. suffix)
434 fi.add(path .. "*/*" .. suffix)
435 end
436 end
437 fi.scan()
438
439 for k, v in pairs(fi.indexes) do
440 index[v[2]] = v[1]
441 end
442 end
443
444 --- Generate the dispatching index using the native file-cache based strategy.
445 -- @param path Controller base directory
446 -- @param suffixes Controller file suffixes
447 function createindex_plain(path, suffixes)
448 local controllers = { }
449 for _, suffix in ipairs(suffixes) do
450 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
451 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
452 end
453
454 if indexcache then
455 local cachedate = fs.stat(indexcache, "mtime")
456 if cachedate then
457 local realdate = 0
458 for _, obj in ipairs(controllers) do
459 local omtime = fs.stat(obj, "mtime")
460 realdate = (omtime and omtime > realdate) and omtime or realdate
461 end
462
463 if cachedate > realdate then
464 assert(
465 sys.process.info("uid") == fs.stat(indexcache, "uid")
466 and fs.stat(indexcache, "modestr") == "rw-------",
467 "Fatal: Indexcache is not sane!"
468 )
469
470 index = loadfile(indexcache)()
471 return index
472 end
473 end
474 end
475
476 index = {}
477
478 for i,c in ipairs(controllers) do
479 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
480 for _, suffix in ipairs(suffixes) do
481 modname = modname:gsub(suffix.."$", "")
482 end
483
484 local mod = require(modname)
485 assert(mod ~= true,
486 "Invalid controller file found\n" ..
487 "The file '" .. c .. "' contains an invalid module line.\n" ..
488 "Please verify whether the module name is set to '" .. modname ..
489 "' - It must correspond to the file path!")
490
491 local idx = mod.index
492 assert(type(idx) == "function",
493 "Invalid controller file found\n" ..
494 "The file '" .. c .. "' contains no index() function.\n" ..
495 "Please make sure that the controller contains a valid " ..
496 "index function and verify the spelling!")
497
498 index[modname] = idx
499 end
500
501 if indexcache then
502 local f = nixio.open(indexcache, "w", 600)
503 f:writeall(util.get_bytecode(index))
504 f:close()
505 end
506 end
507
508 --- Create the dispatching tree from the index.
509 -- Build the index before if it does not exist yet.
510 function createtree()
511 if not index then
512 createindex()
513 end
514
515 local ctx = context
516 local tree = {nodes={}, inreq=true}
517 local modi = {}
518
519 ctx.treecache = setmetatable({}, {__mode="v"})
520 ctx.tree = tree
521 ctx.modifiers = modi
522
523 -- Load default translation
524 require "luci.i18n".loadc("base")
525
526 local scope = setmetatable({}, {__index = luci.dispatcher})
527
528 for k, v in pairs(index) do
529 scope._NAME = k
530 setfenv(v, scope)
531 v()
532 end
533
534 local function modisort(a,b)
535 return modi[a].order < modi[b].order
536 end
537
538 for _, v in util.spairs(modi, modisort) do
539 scope._NAME = v.module
540 setfenv(v.func, scope)
541 v.func()
542 end
543
544 return tree
545 end
546
547 --- Register a tree modifier.
548 -- @param func Modifier function
549 -- @param order Modifier order value (optional)
550 function modifier(func, order)
551 context.modifiers[#context.modifiers+1] = {
552 func = func,
553 order = order or 0,
554 module
555 = getfenv(2)._NAME
556 }
557 end
558
559 --- Clone a node of the dispatching tree to another position.
560 -- @param path Virtual path destination
561 -- @param clone Virtual path source
562 -- @param title Destination node title (optional)
563 -- @param order Destination node order value (optional)
564 -- @return Dispatching tree node
565 function assign(path, clone, title, order)
566 local obj = node(unpack(path))
567 obj.nodes = nil
568 obj.module = nil
569
570 obj.title = title
571 obj.order = order
572
573 setmetatable(obj, {__index = _create_node(clone)})
574
575 return obj
576 end
577
578 --- Create a new dispatching node and define common parameters.
579 -- @param path Virtual path
580 -- @param target Target function to call when dispatched.
581 -- @param title Destination node title
582 -- @param order Destination node order value (optional)
583 -- @return Dispatching tree node
584 function entry(path, target, title, order)
585 local c = node(unpack(path))
586
587 c.target = target
588 c.title = title
589 c.order = order
590 c.module = getfenv(2)._NAME
591
592 return c
593 end
594
595 --- Fetch or create a dispatching node without setting the target module or
596 -- enabling the node.
597 -- @param ... Virtual path
598 -- @return Dispatching tree node
599 function get(...)
600 return _create_node({...})
601 end
602
603 --- Fetch or create a new dispatching node.
604 -- @param ... Virtual path
605 -- @return Dispatching tree node
606 function node(...)
607 local c = _create_node({...})
608
609 c.module = getfenv(2)._NAME
610 c.auto = nil
611
612 return c
613 end
614
615 function _create_node(path)
616 if #path == 0 then
617 return context.tree
618 end
619
620 local name = table.concat(path, ".")
621 local c = context.treecache[name]
622
623 if not c then
624 local last = table.remove(path)
625 local parent = _create_node(path)
626
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
631 c.inreq = true
632 end
633 parent.nodes[last] = c
634 context.treecache[name] = c
635 end
636 return c
637 end
638
639 -- Subdispatchers --
640
641 function _firstchild()
642 local path = { unpack(context.path) }
643 local name = table.concat(path, ".")
644 local node = context.treecache[name]
645
646 local lowest
647 if node and node.nodes and next(node.nodes) then
648 local k, v
649 for k, v in pairs(node.nodes) do
650 if not lowest or
651 (v.order or 100) < (node.nodes[lowest].order or 100)
652 then
653 lowest = k
654 end
655 end
656 end
657
658 assert(lowest ~= nil,
659 "The requested node contains no childs, unable to redispatch")
660
661 path[#path+1] = lowest
662 dispatch(path)
663 end
664
665 --- Alias the first (lowest order) page automatically
666 function firstchild()
667 return { type = "firstchild", target = _firstchild }
668 end
669
670 --- Create a redirect to another dispatching node.
671 -- @param ... Virtual path destination
672 function alias(...)
673 local req = {...}
674 return function(...)
675 for _, r in ipairs({...}) do
676 req[#req+1] = r
677 end
678
679 dispatch(req)
680 end
681 end
682
683 --- Rewrite the first x path values of the request.
684 -- @param n Number of path values to replace
685 -- @param ... Virtual path to replace removed path values with
686 function rewrite(n, ...)
687 local req = {...}
688 return function(...)
689 local dispatched = util.clone(context.dispatched)
690
691 for i=1,n do
692 table.remove(dispatched, 1)
693 end
694
695 for i, r in ipairs(req) do
696 table.insert(dispatched, i, r)
697 end
698
699 for _, r in ipairs({...}) do
700 dispatched[#dispatched+1] = r
701 end
702
703 dispatch(dispatched)
704 end
705 end
706
707
708 local function _call(self, ...)
709 if #self.argv > 0 then
710 return getfenv()[self.name](unpack(self.argv), ...)
711 else
712 return getfenv()[self.name](...)
713 end
714 end
715
716 --- Create a function-call dispatching target.
717 -- @param name Target function of local controller
718 -- @param ... Additional parameters passed to the function
719 function call(name, ...)
720 return {type = "call", argv = {...}, name = name, target = _call}
721 end
722
723
724 local _template = function(self, ...)
725 require "luci.template".render(self.view)
726 end
727
728 --- Create a template render dispatching target.
729 -- @param name Template to be rendered
730 function template(name)
731 return {type = "template", view = name, target = _template}
732 end
733
734
735 local function _cbi(self, ...)
736 local cbi = require "luci.cbi"
737 local tpl = require "luci.template"
738 local http = require "luci.http"
739
740 local config = self.config or {}
741 local maps = cbi.load(self.model, ...)
742
743 local state = nil
744
745 for i, res in ipairs(maps) do
746 res.flow = config
747 local cstate = res:parse()
748 if cstate and (not state or cstate < state) then
749 state = cstate
750 end
751 end
752
753 local function _resolve_path(path)
754 return type(path) == "table" and build_url(unpack(path)) or path
755 end
756
757 if config.on_valid_to and state and state > 0 and state < 2 then
758 http.redirect(_resolve_path(config.on_valid_to))
759 return
760 end
761
762 if config.on_changed_to and state and state > 1 then
763 http.redirect(_resolve_path(config.on_changed_to))
764 return
765 end
766
767 if config.on_success_to and state and state > 0 then
768 http.redirect(_resolve_path(config.on_success_to))
769 return
770 end
771
772 if config.state_handler then
773 if not config.state_handler(state, maps) then
774 return
775 end
776 end
777
778 http.header("X-CBI-State", state or 0)
779
780 if not config.noheader then
781 tpl.render("cbi/header", {state = state})
782 end
783
784 local redirect
785 local messages
786 local applymap = false
787 local pageaction = true
788 local parsechain = { }
789
790 for i, res in ipairs(maps) do
791 if res.apply_needed and res.parsechain then
792 local c
793 for _, c in ipairs(res.parsechain) do
794 parsechain[#parsechain+1] = c
795 end
796 applymap = true
797 end
798
799 if res.redirect then
800 redirect = redirect or res.redirect
801 end
802
803 if res.pageaction == false then
804 pageaction = false
805 end
806
807 if res.message then
808 messages = messages or { }
809 messages[#messages+1] = res.message
810 end
811 end
812
813 for i, res in ipairs(maps) do
814 res:render({
815 firstmap = (i == 1),
816 applymap = applymap,
817 redirect = redirect,
818 messages = messages,
819 pageaction = pageaction,
820 parsechain = parsechain
821 })
822 end
823
824 if not config.nofooter then
825 tpl.render("cbi/footer", {
826 flow = config,
827 pageaction = pageaction,
828 redirect = redirect,
829 state = state,
830 autoapply = config.autoapply
831 })
832 end
833 end
834
835 --- Create a CBI model dispatching target.
836 -- @param model CBI model to be rendered
837 function cbi(model, config)
838 return {type = "cbi", config = config, model = model, target = _cbi}
839 end
840
841
842 local function _arcombine(self, ...)
843 local argv = {...}
844 local target = #argv > 0 and self.targets[2] or self.targets[1]
845 setfenv(target.target, self.env)
846 target:target(unpack(argv))
847 end
848
849 --- Create a combined dispatching target for non argv and argv requests.
850 -- @param trg1 Overview Target
851 -- @param trg2 Detail Target
852 function arcombine(trg1, trg2)
853 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
854 end
855
856
857 local function _form(self, ...)
858 local cbi = require "luci.cbi"
859 local tpl = require "luci.template"
860 local http = require "luci.http"
861
862 local maps = luci.cbi.load(self.model, ...)
863 local state = nil
864
865 for i, res in ipairs(maps) do
866 local cstate = res:parse()
867 if cstate and (not state or cstate < state) then
868 state = cstate
869 end
870 end
871
872 http.header("X-CBI-State", state or 0)
873 tpl.render("header")
874 for i, res in ipairs(maps) do
875 res:render()
876 end
877 tpl.render("footer")
878 end
879
880 --- Create a CBI form model dispatching target.
881 -- @param model CBI form model tpo be rendered
882 function form(model)
883 return {type = "cbi", model = model, target = _form}
884 end
885
886 --- Access the luci.i18n translate() api.
887 -- @class function
888 -- @name translate
889 -- @param text Text to translate
890 translate = i18n.translate
891
892 --- No-op function used to mark translation entries for menu labels.
893 -- This function does not actually translate the given argument but
894 -- is used by build/i18n-scan.pl to find translatable entries.
895 function _(text)
896 return text
897 end