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