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