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