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