luci-0.9: merge r5130-r5143
[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
38 authenticator = {}
39
40 -- Index table
41 local index = nil
42
43 -- Fastindex
44 local fi
45
46
47 --- Build the URL relative to the server webroot from given virtual path.
48 -- @param ... Virtual path
49 -- @return Relative URL
50 function build_url(...)
51 local path = {...}
52 local sn = http.getenv("SCRIPT_NAME") or ""
53 for k, v in pairs(context.urltoken) do
54 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
55 end
56 return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
57 end
58
59 --- Send a 404 error code and render the "error404" template if available.
60 -- @param message Custom error message (optional)
61 -- @return false
62 function error404(message)
63 luci.http.status(404, "Not Found")
64 message = message or "Not Found"
65
66 require("luci.template")
67 if not luci.util.copcall(luci.template.render, "error404") then
68 luci.http.prepare_content("text/plain")
69 luci.http.write(message)
70 end
71 return false
72 end
73
74 --- Send a 500 error code and render the "error500" template if available.
75 -- @param message Custom error message (optional)#
76 -- @return false
77 function error500(message)
78 luci.util.perror(message)
79 if not context.template_header_sent then
80 luci.http.status(500, "Internal Server Error")
81 luci.http.prepare_content("text/plain")
82 luci.http.write(message)
83 else
84 require("luci.template")
85 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
86 luci.http.prepare_content("text/plain")
87 luci.http.write(message)
88 end
89 end
90 return false
91 end
92
93 function authenticator.htmlauth(validator, accs, default)
94 local user = luci.http.formvalue("username")
95 local pass = luci.http.formvalue("password")
96
97 if user and validator(user, pass) then
98 return user
99 end
100
101 require("luci.i18n")
102 require("luci.template")
103 context.path = {}
104 luci.template.render("sysauth", {duser=default, fuser=user})
105 return false
106
107 end
108
109 --- Dispatch an HTTP request.
110 -- @param request LuCI HTTP Request object
111 function httpdispatch(request, prefix)
112 luci.http.context.request = request
113
114 local r = {}
115 context.request = r
116 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
117
118 if prefix then
119 for _, node in ipairs(prefix) do
120 r[#r+1] = node
121 end
122 end
123
124 for node in pathinfo:gmatch("[^/]+") do
125 r[#r+1] = node
126 end
127
128 local stat, err = util.coxpcall(function()
129 dispatch(context.request)
130 end, error500)
131
132 luci.http.close()
133
134 --context._disable_memtrace()
135 end
136
137 --- Dispatches a LuCI virtual path.
138 -- @param request Virtual path
139 function dispatch(request)
140 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
141 local ctx = context
142 ctx.path = request
143 ctx.urltoken = ctx.urltoken or {}
144
145 local conf = require "luci.config"
146 assert(conf.main,
147 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
148
149 local lang = conf.main.lang
150 if lang == "auto" then
151 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
152 for lpat in aclang:gmatch("[%w-]+") do
153 lpat = lpat and lpat:gsub("-", "_")
154 if conf.languages[lpat] then
155 lang = lpat
156 break
157 end
158 end
159 end
160 require "luci.i18n".setlanguage(lang)
161
162 local c = ctx.tree
163 local stat
164 if not c then
165 c = createtree()
166 end
167
168 local track = {}
169 local args = {}
170 ctx.args = args
171 ctx.requestargs = ctx.requestargs or args
172 local n
173 local t = true
174 local token = ctx.urltoken
175 local preq = {}
176 local freq = {}
177
178 for i, s in ipairs(request) do
179 local tkey, tval
180 if t then
181 tkey, tval = s:match(";(%w+)=(.*)")
182 end
183
184 if tkey then
185 token[tkey] = tval
186 else
187 t = false
188 preq[#preq+1] = s
189 freq[#freq+1] = s
190 c = c.nodes[s]
191 n = i
192 if not c then
193 break
194 end
195
196 util.update(track, c)
197
198 if c.leaf then
199 break
200 end
201 end
202 end
203
204 if c and c.leaf then
205 for j=n+1, #request do
206 args[#args+1] = request[j]
207 freq[#freq+1] = request[j]
208 end
209 end
210
211 ctx.requestpath = freq
212 ctx.path = preq
213
214 if track.i18n then
215 require("luci.i18n").loadc(track.i18n)
216 end
217
218 -- Init template engine
219 if (c and c.index) or not track.notemplate then
220 local tpl = require("luci.template")
221 local media = track.mediaurlbase or luci.config.main.mediaurlbase
222 if not tpl.Template("themes/%s/header" % fs.basename(media)) then
223 --if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
224 media = nil
225 for name, theme in pairs(luci.config.themes) do
226 if name:sub(1,1) ~= "." and pcall(tpl.Template,
227 "themes/%s/header" % fs.basename(theme)) then
228 media = theme
229 end
230 end
231 assert(media, "No valid theme found")
232 end
233
234 tpl.context.viewns = setmetatable({
235 write = luci.http.write;
236 include = function(name) tpl.Template(name):render(getfenv(2)) end;
237 translate = function(...) return require("luci.i18n").translate(...) end;
238 striptags = util.striptags;
239 media = media;
240 theme = fs.basename(media);
241 resource = luci.config.main.resourcebase
242 }, {__index=function(table, key)
243 if key == "controller" then
244 return build_url()
245 elseif key == "REQUEST_URI" then
246 return build_url(unpack(ctx.requestpath))
247 else
248 return rawget(table, key) or _G[key]
249 end
250 end})
251 end
252
253 track.dependent = (track.dependent ~= false)
254 assert(not track.dependent or not track.auto, "Access Violation")
255
256 if track.sysauth then
257 local sauth = require "luci.sauth"
258
259 local authen = type(track.sysauth_authenticator) == "function"
260 and track.sysauth_authenticator
261 or authenticator[track.sysauth_authenticator]
262
263 local def = (type(track.sysauth) == "string") and track.sysauth
264 local accs = def and {track.sysauth} or track.sysauth
265 local sess = ctx.authsession
266 local verifytoken = false
267 if not sess then
268 sess = luci.http.getcookie("sysauth")
269 sess = sess and sess:match("^[a-f0-9]*$")
270 verifytoken = true
271 end
272
273 local sdat = sauth.read(sess)
274 local user
275
276 if sdat then
277 sdat = loadstring(sdat)
278 setfenv(sdat, {})
279 sdat = sdat()
280 if not verifytoken or ctx.urltoken.stok == sdat.token then
281 user = sdat.user
282 end
283 else
284 local eu = http.getenv("HTTP_AUTH_USER")
285 local ep = http.getenv("HTTP_AUTH_PASS")
286 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
287 authen = function() return eu end
288 end
289 end
290
291 if not util.contains(accs, user) then
292 if authen then
293 ctx.urltoken.stok = nil
294 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
295 if not user or not util.contains(accs, user) then
296 return
297 else
298 local sid = sess or luci.sys.uniqueid(16)
299 if not sess then
300 local token = luci.sys.uniqueid(16)
301 sauth.write(sid, util.get_bytecode({
302 user=user,
303 token=token,
304 secret=luci.sys.uniqueid(16)
305 }))
306 ctx.urltoken.stok = token
307 end
308 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
309 ctx.authsession = sid
310 end
311 else
312 luci.http.status(403, "Forbidden")
313 return
314 end
315 else
316 ctx.authsession = sess
317 end
318 end
319
320 if track.setgroup then
321 luci.sys.process.setgroup(track.setgroup)
322 end
323
324 if track.setuser then
325 luci.sys.process.setuser(track.setuser)
326 end
327
328 local target = nil
329 if c then
330 if type(c.target) == "function" then
331 target = c.target
332 elseif type(c.target) == "table" then
333 target = c.target.target
334 end
335 end
336
337 if c and (c.index or type(target) == "function") then
338 ctx.dispatched = c
339 ctx.requested = ctx.requested or ctx.dispatched
340 end
341
342 if c and c.index then
343 local tpl = require "luci.template"
344
345 if util.copcall(tpl.render, "indexer", {}) then
346 return true
347 end
348 end
349
350 if type(target) == "function" then
351 util.copcall(function()
352 local oldenv = getfenv(target)
353 local module = require(c.module)
354 local env = setmetatable({}, {__index=
355
356 function(tbl, key)
357 return rawget(tbl, key) or module[key] or oldenv[key]
358 end})
359
360 setfenv(target, env)
361 end)
362
363 if type(c.target) == "table" then
364 target(c.target, unpack(args))
365 else
366 target(unpack(args))
367 end
368 else
369 error404()
370 end
371 end
372
373 --- Generate the dispatching index using the best possible strategy.
374 function createindex()
375 local path = luci.util.libpath() .. "/controller/"
376 local suff = { ".lua", ".lua.gz" }
377
378 if luci.util.copcall(require, "luci.fastindex") then
379 createindex_fastindex(path, suff)
380 else
381 createindex_plain(path, suff)
382 end
383 end
384
385 --- Generate the dispatching index using the fastindex C-indexer.
386 -- @param path Controller base directory
387 -- @param suffixes Controller file suffixes
388 function createindex_fastindex(path, suffixes)
389 index = {}
390
391 if not fi then
392 fi = luci.fastindex.new("index")
393 for _, suffix in ipairs(suffixes) do
394 fi.add(path .. "*" .. suffix)
395 fi.add(path .. "*/*" .. suffix)
396 end
397 end
398 fi.scan()
399
400 for k, v in pairs(fi.indexes) do
401 index[v[2]] = v[1]
402 end
403 end
404
405 --- Generate the dispatching index using the native file-cache based strategy.
406 -- @param path Controller base directory
407 -- @param suffixes Controller file suffixes
408 function createindex_plain(path, suffixes)
409 local controllers = { }
410 for _, suffix in ipairs(suffixes) do
411 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
412 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
413 end
414
415 if indexcache then
416 local cachedate = fs.stat(indexcache, "mtime")
417 if cachedate then
418 local realdate = 0
419 for _, obj in ipairs(controllers) do
420 local omtime = fs.stat(path .. "/" .. obj, "mtime")
421 realdate = (omtime and omtime > realdate) and omtime or realdate
422 end
423
424 if cachedate > realdate then
425 assert(
426 sys.process.info("uid") == fs.stat(indexcache, "uid")
427 and fs.stat(indexcache, "modestr") == "rw-------",
428 "Fatal: Indexcache is not sane!"
429 )
430
431 index = loadfile(indexcache)()
432 return index
433 end
434 end
435 end
436
437 index = {}
438
439 for i,c in ipairs(controllers) do
440 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
441 for _, suffix in ipairs(suffixes) do
442 module = module:gsub(suffix.."$", "")
443 end
444
445 local mod = require(module)
446 local idx = mod.index
447
448 if type(idx) == "function" then
449 index[module] = idx
450 end
451 end
452
453 if indexcache then
454 local f = nixio.open(indexcache, "w", 600)
455 f:writeall(util.get_bytecode(index))
456 f:close()
457 end
458 end
459
460 --- Create the dispatching tree from the index.
461 -- Build the index before if it does not exist yet.
462 function createtree()
463 if not index then
464 createindex()
465 end
466
467 local ctx = context
468 local tree = {nodes={}}
469 local modi = {}
470
471 ctx.treecache = setmetatable({}, {__mode="v"})
472 ctx.tree = tree
473 ctx.modifiers = modi
474
475 -- Load default translation
476 require "luci.i18n".loadc("default")
477
478 local scope = setmetatable({}, {__index = luci.dispatcher})
479
480 for k, v in pairs(index) do
481 scope._NAME = k
482 setfenv(v, scope)
483 v()
484 end
485
486 local function modisort(a,b)
487 return modi[a].order < modi[b].order
488 end
489
490 for _, v in util.spairs(modi, modisort) do
491 scope._NAME = v.module
492 setfenv(v.func, scope)
493 v.func()
494 end
495
496 return tree
497 end
498
499 --- Register a tree modifier.
500 -- @param func Modifier function
501 -- @param order Modifier order value (optional)
502 function modifier(func, order)
503 context.modifiers[#context.modifiers+1] = {
504 func = func,
505 order = order or 0,
506 module
507 = getfenv(2)._NAME
508 }
509 end
510
511 --- Clone a node of the dispatching tree to another position.
512 -- @param path Virtual path destination
513 -- @param clone Virtual path source
514 -- @param title Destination node title (optional)
515 -- @param order Destination node order value (optional)
516 -- @return Dispatching tree node
517 function assign(path, clone, title, order)
518 local obj = node(unpack(path))
519 obj.nodes = nil
520 obj.module = nil
521
522 obj.title = title
523 obj.order = order
524
525 setmetatable(obj, {__index = _create_node(clone)})
526
527 return obj
528 end
529
530 --- Create a new dispatching node and define common parameters.
531 -- @param path Virtual path
532 -- @param target Target function to call when dispatched.
533 -- @param title Destination node title
534 -- @param order Destination node order value (optional)
535 -- @return Dispatching tree node
536 function entry(path, target, title, order)
537 local c = node(unpack(path))
538
539 c.target = target
540 c.title = title
541 c.order = order
542 c.module = getfenv(2)._NAME
543
544 return c
545 end
546
547 --- Fetch or create a dispatching node without setting the target module or
548 -- enabling the node.
549 -- @param ... Virtual path
550 -- @return Dispatching tree node
551 function get(...)
552 return _create_node({...})
553 end
554
555 --- Fetch or create a new dispatching node.
556 -- @param ... Virtual path
557 -- @return Dispatching tree node
558 function node(...)
559 local c = _create_node({...})
560
561 c.module = getfenv(2)._NAME
562 c.auto = nil
563
564 return c
565 end
566
567 function _create_node(path, cache)
568 if #path == 0 then
569 return context.tree
570 end
571
572 cache = cache or context.treecache
573 local name = table.concat(path, ".")
574 local c = cache[name]
575
576 if not c then
577 local new = {nodes={}, auto=true, path=util.clone(path)}
578 local last = table.remove(path)
579
580 c = _create_node(path, cache)
581
582 c.nodes[last] = new
583 cache[name] = new
584
585 return new
586 else
587 return c
588 end
589 end
590
591 -- Subdispatchers --
592
593 --- Create a redirect to another dispatching node.
594 -- @param ... Virtual path destination
595 function alias(...)
596 local req = {...}
597 return function(...)
598 for _, r in ipairs({...}) do
599 req[#req+1] = r
600 end
601
602 dispatch(req)
603 end
604 end
605
606 --- Rewrite the first x path values of the request.
607 -- @param n Number of path values to replace
608 -- @param ... Virtual path to replace removed path values with
609 function rewrite(n, ...)
610 local req = {...}
611 return function(...)
612 local dispatched = util.clone(context.dispatched)
613
614 for i=1,n do
615 table.remove(dispatched, 1)
616 end
617
618 for i, r in ipairs(req) do
619 table.insert(dispatched, i, r)
620 end
621
622 for _, r in ipairs({...}) do
623 dispatched[#dispatched+1] = r
624 end
625
626 dispatch(dispatched)
627 end
628 end
629
630
631 local function _call(self, ...)
632 if #self.argv > 0 then
633 return getfenv()[self.name](unpack(self.argv), ...)
634 else
635 return getfenv()[self.name](...)
636 end
637 end
638
639 --- Create a function-call dispatching target.
640 -- @param name Target function of local controller
641 -- @param ... Additional parameters passed to the function
642 function call(name, ...)
643 return {type = "call", argv = {...}, name = name, target = _call}
644 end
645
646
647 local _template = function(self, ...)
648 require "luci.template".render(self.view)
649 end
650
651 --- Create a template render dispatching target.
652 -- @param name Template to be rendered
653 function template(name)
654 return {type = "template", view = name, target = _template}
655 end
656
657
658 local function _cbi(self, ...)
659 local cbi = require "luci.cbi"
660 local tpl = require "luci.template"
661 local http = require "luci.http"
662
663 local config = self.config or {}
664 local maps = cbi.load(self.model, ...)
665
666 local state = nil
667
668 for i, res in ipairs(maps) do
669 res.flow = config
670 local cstate = res:parse()
671 if cstate and (not state or cstate < state) then
672 state = cstate
673 end
674 end
675
676 local function _resolve_path(path)
677 return type(path) == "table" and build_url(unpack(path)) or path
678 end
679
680 if config.on_valid_to and state and state > 0 and state < 2 then
681 http.redirect(_resolve_path(config.on_valid_to))
682 return
683 end
684
685 if config.on_changed_to and state and state > 1 then
686 http.redirect(_resolve_path(config.on_changed_to))
687 return
688 end
689
690 if config.on_success_to and state and state > 0 then
691 http.redirect(_resolve_path(config.on_success_to))
692 return
693 end
694
695 if config.state_handler then
696 if not config.state_handler(state, maps) then
697 return
698 end
699 end
700
701 local pageaction = true
702 http.header("X-CBI-State", state or 0)
703 if not config.noheader then
704 tpl.render("cbi/header", {state = state})
705 end
706 for i, res in ipairs(maps) do
707 res:render()
708 if res.pageaction == false then
709 pageaction = false
710 end
711 end
712 if not config.nofooter then
713 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
714 end
715 end
716
717 --- Create a CBI model dispatching target.
718 -- @param model CBI model to be rendered
719 function cbi(model, config)
720 return {type = "cbi", config = config, model = model, target = _cbi}
721 end
722
723
724 local function _arcombine(self, ...)
725 local argv = {...}
726 local target = #argv > 0 and self.targets[2] or self.targets[1]
727 setfenv(target.target, self.env)
728 target:target(unpack(argv))
729 end
730
731 --- Create a combined dispatching target for non argv and argv requests.
732 -- @param trg1 Overview Target
733 -- @param trg2 Detail Target
734 function arcombine(trg1, trg2)
735 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
736 end
737
738
739 local function _form(self, ...)
740 local cbi = require "luci.cbi"
741 local tpl = require "luci.template"
742 local http = require "luci.http"
743
744 local maps = luci.cbi.load(self.model, ...)
745 local state = nil
746
747 for i, res in ipairs(maps) do
748 local cstate = res:parse()
749 if cstate and (not state or cstate < state) then
750 state = cstate
751 end
752 end
753
754 http.header("X-CBI-State", state or 0)
755 tpl.render("header")
756 for i, res in ipairs(maps) do
757 res:render()
758 end
759 tpl.render("footer")
760 end
761
762 --- Create a CBI form model dispatching target.
763 -- @param model CBI form model tpo be rendered
764 function form(model)
765 return {type = "cbi", model = model, target = _form}
766 end