libs/web: expose cbi map redirect property to page templates
[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 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
134
135 if prefix then
136 for _, node in ipairs(prefix) do
137 r[#r+1] = node
138 end
139 end
140
141 for node in pathinfo:gmatch("[^/]+") do
142 r[#r+1] = node
143 end
144
145 local stat, err = util.coxpcall(function()
146 dispatch(context.request)
147 end, error500)
148
149 luci.http.close()
150
151 --context._disable_memtrace()
152 end
153
154 --- Dispatches a LuCI virtual path.
155 -- @param request Virtual path
156 function dispatch(request)
157 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
158 local ctx = context
159 ctx.path = request
160 ctx.urltoken = ctx.urltoken or {}
161
162 local conf = require "luci.config"
163 assert(conf.main,
164 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
165
166 local lang = conf.main.lang or "auto"
167 if lang == "auto" then
168 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
169 for lpat in aclang:gmatch("[%w-]+") do
170 lpat = lpat and lpat:gsub("-", "_")
171 if conf.languages[lpat] then
172 lang = lpat
173 break
174 end
175 end
176 end
177 require "luci.i18n".setlanguage(lang)
178
179 local c = ctx.tree
180 local stat
181 if not c then
182 c = createtree()
183 end
184
185 local track = {}
186 local args = {}
187 ctx.args = args
188 ctx.requestargs = ctx.requestargs or args
189 local n
190 local t = true
191 local token = ctx.urltoken
192 local preq = {}
193 local freq = {}
194
195 for i, s in ipairs(request) do
196 local tkey, tval
197 if t then
198 tkey, tval = s:match(";(%w+)=([a-fA-F0-9]*)")
199 end
200
201 if tkey then
202 token[tkey] = tval
203 else
204 t = false
205 preq[#preq+1] = s
206 freq[#freq+1] = s
207 c = c.nodes[s]
208 n = i
209 if not c then
210 break
211 end
212
213 util.update(track, c)
214
215 if c.leaf then
216 break
217 end
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 require("luci.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 = function(...) return require("luci.i18n").translate(...) end;
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, "Access Violation")
273
274 if track.sysauth then
275 local sauth = require "luci.sauth"
276
277 local authen = type(track.sysauth_authenticator) == "function"
278 and track.sysauth_authenticator
279 or authenticator[track.sysauth_authenticator]
280
281 local def = (type(track.sysauth) == "string") and track.sysauth
282 local accs = def and {track.sysauth} or track.sysauth
283 local sess = ctx.authsession
284 local verifytoken = false
285 if not sess then
286 sess = luci.http.getcookie("sysauth")
287 sess = sess and sess:match("^[a-f0-9]*$")
288 verifytoken = true
289 end
290
291 local sdat = sauth.read(sess)
292 local user
293
294 if sdat then
295 sdat = loadstring(sdat)
296 setfenv(sdat, {})
297 sdat = sdat()
298 if not verifytoken or ctx.urltoken.stok == sdat.token then
299 user = sdat.user
300 end
301 else
302 local eu = http.getenv("HTTP_AUTH_USER")
303 local ep = http.getenv("HTTP_AUTH_PASS")
304 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
305 authen = function() return eu end
306 end
307 end
308
309 if not util.contains(accs, user) then
310 if authen then
311 ctx.urltoken.stok = nil
312 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
313 if not user or not util.contains(accs, user) then
314 return
315 else
316 local sid = sess or luci.sys.uniqueid(16)
317 if not sess then
318 local token = luci.sys.uniqueid(16)
319 sauth.write(sid, util.get_bytecode({
320 user=user,
321 token=token,
322 secret=luci.sys.uniqueid(16)
323 }))
324 ctx.urltoken.stok = token
325 end
326 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
327 ctx.authsession = sid
328 ctx.authuser = user
329 end
330 else
331 luci.http.status(403, "Forbidden")
332 return
333 end
334 else
335 ctx.authsession = sess
336 ctx.authuser = user
337 end
338 end
339
340 if track.setgroup then
341 luci.sys.process.setgroup(track.setgroup)
342 end
343
344 if track.setuser then
345 luci.sys.process.setuser(track.setuser)
346 end
347
348 local target = nil
349 if c then
350 if type(c.target) == "function" then
351 target = c.target
352 elseif type(c.target) == "table" then
353 target = c.target.target
354 end
355 end
356
357 if c and (c.index or type(target) == "function") then
358 ctx.dispatched = c
359 ctx.requested = ctx.requested or ctx.dispatched
360 end
361
362 if c and c.index then
363 local tpl = require "luci.template"
364
365 if util.copcall(tpl.render, "indexer", {}) then
366 return true
367 end
368 end
369
370 if type(target) == "function" then
371 util.copcall(function()
372 local oldenv = getfenv(target)
373 local module = require(c.module)
374 local env = setmetatable({}, {__index=
375
376 function(tbl, key)
377 return rawget(tbl, key) or module[key] or oldenv[key]
378 end})
379
380 setfenv(target, env)
381 end)
382
383 if type(c.target) == "table" then
384 target(c.target, unpack(args))
385 else
386 target(unpack(args))
387 end
388 else
389 error404()
390 end
391 end
392
393 --- Generate the dispatching index using the best possible strategy.
394 function createindex()
395 local path = luci.util.libpath() .. "/controller/"
396 local suff = { ".lua", ".lua.gz" }
397
398 if luci.util.copcall(require, "luci.fastindex") then
399 createindex_fastindex(path, suff)
400 else
401 createindex_plain(path, suff)
402 end
403 end
404
405 --- Generate the dispatching index using the fastindex C-indexer.
406 -- @param path Controller base directory
407 -- @param suffixes Controller file suffixes
408 function createindex_fastindex(path, suffixes)
409 index = {}
410
411 if not fi then
412 fi = luci.fastindex.new("index")
413 for _, suffix in ipairs(suffixes) do
414 fi.add(path .. "*" .. suffix)
415 fi.add(path .. "*/*" .. suffix)
416 end
417 end
418 fi.scan()
419
420 for k, v in pairs(fi.indexes) do
421 index[v[2]] = v[1]
422 end
423 end
424
425 --- Generate the dispatching index using the native file-cache based strategy.
426 -- @param path Controller base directory
427 -- @param suffixes Controller file suffixes
428 function createindex_plain(path, suffixes)
429 local controllers = { }
430 for _, suffix in ipairs(suffixes) do
431 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
432 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
433 end
434
435 if indexcache then
436 local cachedate = fs.stat(indexcache, "mtime")
437 if cachedate then
438 local realdate = 0
439 for _, obj in ipairs(controllers) do
440 local omtime = fs.stat(path .. "/" .. obj, "mtime")
441 realdate = (omtime and omtime > realdate) and omtime or realdate
442 end
443
444 if cachedate > realdate then
445 assert(
446 sys.process.info("uid") == fs.stat(indexcache, "uid")
447 and fs.stat(indexcache, "modestr") == "rw-------",
448 "Fatal: Indexcache is not sane!"
449 )
450
451 index = loadfile(indexcache)()
452 return index
453 end
454 end
455 end
456
457 index = {}
458
459 for i,c in ipairs(controllers) do
460 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
461 for _, suffix in ipairs(suffixes) do
462 module = module:gsub(suffix.."$", "")
463 end
464
465 local mod = require(module)
466 local idx = mod.index
467
468 if type(idx) == "function" then
469 index[module] = idx
470 end
471 end
472
473 if indexcache then
474 local f = nixio.open(indexcache, "w", 600)
475 f:writeall(util.get_bytecode(index))
476 f:close()
477 end
478 end
479
480 --- Create the dispatching tree from the index.
481 -- Build the index before if it does not exist yet.
482 function createtree()
483 if not index then
484 createindex()
485 end
486
487 local ctx = context
488 local tree = {nodes={}}
489 local modi = {}
490
491 ctx.treecache = setmetatable({}, {__mode="v"})
492 ctx.tree = tree
493 ctx.modifiers = modi
494
495 -- Load default translation
496 require "luci.i18n".loadc("base")
497
498 local scope = setmetatable({}, {__index = luci.dispatcher})
499
500 for k, v in pairs(index) do
501 scope._NAME = k
502 setfenv(v, scope)
503 v()
504 end
505
506 local function modisort(a,b)
507 return modi[a].order < modi[b].order
508 end
509
510 for _, v in util.spairs(modi, modisort) do
511 scope._NAME = v.module
512 setfenv(v.func, scope)
513 v.func()
514 end
515
516 return tree
517 end
518
519 --- Register a tree modifier.
520 -- @param func Modifier function
521 -- @param order Modifier order value (optional)
522 function modifier(func, order)
523 context.modifiers[#context.modifiers+1] = {
524 func = func,
525 order = order or 0,
526 module
527 = getfenv(2)._NAME
528 }
529 end
530
531 --- Clone a node of the dispatching tree to another position.
532 -- @param path Virtual path destination
533 -- @param clone Virtual path source
534 -- @param title Destination node title (optional)
535 -- @param order Destination node order value (optional)
536 -- @return Dispatching tree node
537 function assign(path, clone, title, order)
538 local obj = node(unpack(path))
539 obj.nodes = nil
540 obj.module = nil
541
542 obj.title = title
543 obj.order = order
544
545 setmetatable(obj, {__index = _create_node(clone)})
546
547 return obj
548 end
549
550 --- Create a new dispatching node and define common parameters.
551 -- @param path Virtual path
552 -- @param target Target function to call when dispatched.
553 -- @param title Destination node title
554 -- @param order Destination node order value (optional)
555 -- @return Dispatching tree node
556 function entry(path, target, title, order)
557 local c = node(unpack(path))
558
559 c.target = target
560 c.title = title
561 c.order = order
562 c.module = getfenv(2)._NAME
563
564 return c
565 end
566
567 --- Fetch or create a dispatching node without setting the target module or
568 -- enabling the node.
569 -- @param ... Virtual path
570 -- @return Dispatching tree node
571 function get(...)
572 return _create_node({...})
573 end
574
575 --- Fetch or create a new dispatching node.
576 -- @param ... Virtual path
577 -- @return Dispatching tree node
578 function node(...)
579 local c = _create_node({...})
580
581 c.module = getfenv(2)._NAME
582 c.auto = nil
583
584 return c
585 end
586
587 function _create_node(path, cache)
588 if #path == 0 then
589 return context.tree
590 end
591
592 cache = cache or context.treecache
593 local name = table.concat(path, ".")
594 local c = cache[name]
595
596 if not c then
597 local new = {nodes={}, auto=true, path=util.clone(path)}
598 local last = table.remove(path)
599
600 c = _create_node(path, cache)
601
602 c.nodes[last] = new
603 cache[name] = new
604
605 return new
606 else
607 return c
608 end
609 end
610
611 -- Subdispatchers --
612
613 --- Create a redirect to another dispatching node.
614 -- @param ... Virtual path destination
615 function alias(...)
616 local req = {...}
617 return function(...)
618 for _, r in ipairs({...}) do
619 req[#req+1] = r
620 end
621
622 dispatch(req)
623 end
624 end
625
626 --- Rewrite the first x path values of the request.
627 -- @param n Number of path values to replace
628 -- @param ... Virtual path to replace removed path values with
629 function rewrite(n, ...)
630 local req = {...}
631 return function(...)
632 local dispatched = util.clone(context.dispatched)
633
634 for i=1,n do
635 table.remove(dispatched, 1)
636 end
637
638 for i, r in ipairs(req) do
639 table.insert(dispatched, i, r)
640 end
641
642 for _, r in ipairs({...}) do
643 dispatched[#dispatched+1] = r
644 end
645
646 dispatch(dispatched)
647 end
648 end
649
650
651 local function _call(self, ...)
652 if #self.argv > 0 then
653 return getfenv()[self.name](unpack(self.argv), ...)
654 else
655 return getfenv()[self.name](...)
656 end
657 end
658
659 --- Create a function-call dispatching target.
660 -- @param name Target function of local controller
661 -- @param ... Additional parameters passed to the function
662 function call(name, ...)
663 return {type = "call", argv = {...}, name = name, target = _call}
664 end
665
666
667 local _template = function(self, ...)
668 require "luci.template".render(self.view)
669 end
670
671 --- Create a template render dispatching target.
672 -- @param name Template to be rendered
673 function template(name)
674 return {type = "template", view = name, target = _template}
675 end
676
677
678 local function _cbi(self, ...)
679 local cbi = require "luci.cbi"
680 local tpl = require "luci.template"
681 local http = require "luci.http"
682
683 local config = self.config or {}
684 local maps = cbi.load(self.model, ...)
685
686 local state = nil
687
688 for i, res in ipairs(maps) do
689 res.flow = config
690 local cstate = res:parse()
691 if cstate and (not state or cstate < state) then
692 state = cstate
693 end
694 end
695
696 local function _resolve_path(path)
697 return type(path) == "table" and build_url(unpack(path)) or path
698 end
699
700 if config.on_valid_to and state and state > 0 and state < 2 then
701 http.redirect(_resolve_path(config.on_valid_to))
702 return
703 end
704
705 if config.on_changed_to and state and state > 1 then
706 http.redirect(_resolve_path(config.on_changed_to))
707 return
708 end
709
710 if config.on_success_to and state and state > 0 then
711 http.redirect(_resolve_path(config.on_success_to))
712 return
713 end
714
715 if config.state_handler then
716 if not config.state_handler(state, maps) then
717 return
718 end
719 end
720
721 local redirect
722 local pageaction = true
723 http.header("X-CBI-State", state or 0)
724 if not config.noheader then
725 tpl.render("cbi/header", {state = state})
726 end
727 for i, res in ipairs(maps) do
728 res:render()
729 if res.pageaction == false then
730 pageaction = false
731 end
732 if res.redirect then
733 redirect = redirect or res.redirect
734 end
735 end
736 if not config.nofooter then
737 tpl.render("cbi/footer", {
738 flow = config,
739 pageaction = pageaction,
740 redirect = redirect,
741 state = state,
742 autoapply = config.autoapply
743 })
744 end
745 end
746
747 --- Create a CBI model dispatching target.
748 -- @param model CBI model to be rendered
749 function cbi(model, config)
750 return {type = "cbi", config = config, model = model, target = _cbi}
751 end
752
753
754 local function _arcombine(self, ...)
755 local argv = {...}
756 local target = #argv > 0 and self.targets[2] or self.targets[1]
757 setfenv(target.target, self.env)
758 target:target(unpack(argv))
759 end
760
761 --- Create a combined dispatching target for non argv and argv requests.
762 -- @param trg1 Overview Target
763 -- @param trg2 Detail Target
764 function arcombine(trg1, trg2)
765 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
766 end
767
768
769 local function _form(self, ...)
770 local cbi = require "luci.cbi"
771 local tpl = require "luci.template"
772 local http = require "luci.http"
773
774 local maps = luci.cbi.load(self.model, ...)
775 local state = nil
776
777 for i, res in ipairs(maps) do
778 local cstate = res:parse()
779 if cstate and (not state or cstate < state) then
780 state = cstate
781 end
782 end
783
784 http.header("X-CBI-State", state or 0)
785 tpl.render("header")
786 for i, res in ipairs(maps) do
787 res:render()
788 end
789 tpl.render("footer")
790 end
791
792 --- Create a CBI form model dispatching target.
793 -- @param model CBI form model tpo be rendered
794 function form(model)
795 return {type = "cbi", model = model, target = _form}
796 end