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