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