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