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