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