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