Backport Skip-Button support
[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"
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 suffix Controller file suffix
370 function createindex_fastindex(path, suffix)
371 index = {}
372
373 if not fi then
374 fi = luci.fastindex.new("index")
375 fi.add(path .. "*" .. suffix)
376 fi.add(path .. "*/*" .. suffix)
377 end
378 fi.scan()
379
380 for k, v in pairs(fi.indexes) do
381 index[v[2]] = v[1]
382 end
383 end
384
385 --- Generate the dispatching index using the native file-cache based strategy.
386 -- @param path Controller base directory
387 -- @param suffix Controller file suffix
388 function createindex_plain(path, suffix)
389 local controllers = util.combine(
390 luci.fs.glob(path .. "*" .. suffix) or {},
391 luci.fs.glob(path .. "*/*" .. suffix) or {}
392 )
393
394 if indexcache then
395 local cachedate = fs.mtime(indexcache)
396 if cachedate then
397 local realdate = 0
398 for _, obj in ipairs(controllers) do
399 local omtime = fs.mtime(path .. "/" .. obj)
400 realdate = (omtime and omtime > realdate) and omtime or realdate
401 end
402
403 if cachedate > realdate then
404 assert(
405 sys.process.info("uid") == fs.stat(indexcache, "uid")
406 and fs.stat(indexcache, "mode") == "rw-------",
407 "Fatal: Indexcache is not sane!"
408 )
409
410 index = loadfile(indexcache)()
411 return index
412 end
413 end
414 end
415
416 index = {}
417
418 for i,c in ipairs(controllers) do
419 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
420 local mod = require(module)
421 local idx = mod.index
422
423 if type(idx) == "function" then
424 index[module] = idx
425 end
426 end
427
428 if indexcache then
429 fs.writefile(indexcache, util.get_bytecode(index))
430 fs.chmod(indexcache, "a-rwx,u+rw")
431 end
432 end
433
434 --- Create the dispatching tree from the index.
435 -- Build the index before if it does not exist yet.
436 function createtree()
437 if not index then
438 createindex()
439 end
440
441 local ctx = context
442 local tree = {nodes={}}
443 local modi = {}
444
445 ctx.treecache = setmetatable({}, {__mode="v"})
446 ctx.tree = tree
447 ctx.modifiers = modi
448
449 -- Load default translation
450 require "luci.i18n".loadc("default")
451
452 local scope = setmetatable({}, {__index = luci.dispatcher})
453
454 for k, v in pairs(index) do
455 scope._NAME = k
456 setfenv(v, scope)
457 v()
458 end
459
460 local function modisort(a,b)
461 return modi[a].order < modi[b].order
462 end
463
464 for _, v in util.spairs(modi, modisort) do
465 scope._NAME = v.module
466 setfenv(v.func, scope)
467 v.func()
468 end
469
470 return tree
471 end
472
473 --- Register a tree modifier.
474 -- @param func Modifier function
475 -- @param order Modifier order value (optional)
476 function modifier(func, order)
477 context.modifiers[#context.modifiers+1] = {
478 func = func,
479 order = order or 0,
480 module
481 = getfenv(2)._NAME
482 }
483 end
484
485 --- Clone a node of the dispatching tree to another position.
486 -- @param path Virtual path destination
487 -- @param clone Virtual path source
488 -- @param title Destination node title (optional)
489 -- @param order Destination node order value (optional)
490 -- @return Dispatching tree node
491 function assign(path, clone, title, order)
492 local obj = node(unpack(path))
493 obj.nodes = nil
494 obj.module = nil
495
496 obj.title = title
497 obj.order = order
498
499 setmetatable(obj, {__index = _create_node(clone)})
500
501 return obj
502 end
503
504 --- Create a new dispatching node and define common parameters.
505 -- @param path Virtual path
506 -- @param target Target function to call when dispatched.
507 -- @param title Destination node title
508 -- @param order Destination node order value (optional)
509 -- @return Dispatching tree node
510 function entry(path, target, title, order)
511 local c = node(unpack(path))
512
513 c.target = target
514 c.title = title
515 c.order = order
516 c.module = getfenv(2)._NAME
517
518 return c
519 end
520
521 --- Fetch or create a dispatching node without setting the target module or
522 -- enabling the node.
523 -- @param ... Virtual path
524 -- @return Dispatching tree node
525 function get(...)
526 return _create_node({...})
527 end
528
529 --- Fetch or create a new dispatching node.
530 -- @param ... Virtual path
531 -- @return Dispatching tree node
532 function node(...)
533 local c = _create_node({...})
534
535 c.module = getfenv(2)._NAME
536 c.auto = nil
537
538 return c
539 end
540
541 function _create_node(path, cache)
542 if #path == 0 then
543 return context.tree
544 end
545
546 cache = cache or context.treecache
547 local name = table.concat(path, ".")
548 local c = cache[name]
549
550 if not c then
551 local new = {nodes={}, auto=true, path=util.clone(path)}
552 local last = table.remove(path)
553
554 c = _create_node(path, cache)
555
556 c.nodes[last] = new
557 cache[name] = new
558
559 return new
560 else
561 return c
562 end
563 end
564
565 -- Subdispatchers --
566
567 --- Create a redirect to another dispatching node.
568 -- @param ... Virtual path destination
569 function alias(...)
570 local req = {...}
571 return function(...)
572 for _, r in ipairs({...}) do
573 req[#req+1] = r
574 end
575
576 dispatch(req)
577 end
578 end
579
580 --- Rewrite the first x path values of the request.
581 -- @param n Number of path values to replace
582 -- @param ... Virtual path to replace removed path values with
583 function rewrite(n, ...)
584 local req = {...}
585 return function(...)
586 local dispatched = util.clone(context.dispatched)
587
588 for i=1,n do
589 table.remove(dispatched, 1)
590 end
591
592 for i, r in ipairs(req) do
593 table.insert(dispatched, i, r)
594 end
595
596 for _, r in ipairs({...}) do
597 dispatched[#dispatched+1] = r
598 end
599
600 dispatch(dispatched)
601 end
602 end
603
604
605 local function _call(self, ...)
606 if #self.argv > 0 then
607 return getfenv()[self.name](unpack(self.argv), ...)
608 else
609 return getfenv()[self.name](...)
610 end
611 end
612
613 --- Create a function-call dispatching target.
614 -- @param name Target function of local controller
615 -- @param ... Additional parameters passed to the function
616 function call(name, ...)
617 return {type = "call", argv = {...}, name = name, target = _call}
618 end
619
620
621 local _template = function(self, ...)
622 require "luci.template".render(self.view)
623 end
624
625 --- Create a template render dispatching target.
626 -- @param name Template to be rendered
627 function template(name)
628 return {type = "template", view = name, target = _template}
629 end
630
631
632 local function _cbi(self, ...)
633 local cbi = require "luci.cbi"
634 local tpl = require "luci.template"
635 local http = require "luci.http"
636
637 local config = self.config or {}
638 local maps = cbi.load(self.model, ...)
639
640 local state = nil
641
642 for i, res in ipairs(maps) do
643 res.flow = config
644 local cstate = res:parse()
645 if cstate and (not state or cstate < state) then
646 state = cstate
647 end
648 end
649
650 if config.on_valid_to and state and state > 0 and state < 2 then
651 http.redirect(config.on_valid_to)
652 return
653 end
654
655 if config.on_changed_to and state and state > 1 then
656 http.redirect(config.on_changed_to)
657 return
658 end
659
660 if config.on_success_to and state and state > 0 then
661 http.redirect(config.on_success_to)
662 return
663 end
664
665 if config.state_handler then
666 if not config.state_handler(state, maps) then
667 return
668 end
669 end
670
671 local pageaction = true
672 http.header("X-CBI-State", state or 0)
673 if not config.noheader then
674 tpl.render("cbi/header", {state = state})
675 end
676 for i, res in ipairs(maps) do
677 res:render()
678 if res.pageaction == false then
679 pageaction = false
680 end
681 end
682 if not config.nofooter then
683 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
684 end
685 end
686
687 --- Create a CBI model dispatching target.
688 -- @param model CBI model to be rendered
689 function cbi(model, config)
690 return {type = "cbi", config = config, model = model, target = _cbi}
691 end
692
693
694 local function _arcombine(self, ...)
695 local argv = {...}
696 local target = #argv > 0 and self.targets[2] or self.targets[1]
697 setfenv(target.target, self.env)
698 target:target(unpack(argv))
699 end
700
701 --- Create a combined dispatching target for non argv and argv requests.
702 -- @param trg1 Overview Target
703 -- @param trg2 Detail Target
704 function arcombine(trg1, trg2)
705 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
706 end
707
708
709 local function _form(self, ...)
710 local cbi = require "luci.cbi"
711 local tpl = require "luci.template"
712 local http = require "luci.http"
713
714 local maps = luci.cbi.load(self.model, ...)
715 local state = nil
716
717 for i, res in ipairs(maps) do
718 local cstate = res:parse()
719 if cstate and (not state or cstate < state) then
720 state = cstate
721 end
722 end
723
724 http.header("X-CBI-State", state or 0)
725 tpl.render("header")
726 for i, res in ipairs(maps) do
727 res:render()
728 end
729 tpl.render("footer")
730 end
731
732 --- Create a CBI form model dispatching target.
733 -- @param model CBI form model tpo be rendered
734 function form(model)
735 return {type = "cbi", model = model, target = _form}
736 end