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