6914ffb43ef702fb10d5f864809982790b5d35dd
[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                 luci.util.perror(message)
70         end
71         return false
72 end
73
74 --- Send a 500 error code and render the "error500" template if available.
75 -- @param message       Custom error message (optional)#
76 -- @return                      false
77 function error500(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 new dispatching node.
517 -- @param       ...             Virtual path
518 -- @return                      Dispatching tree node
519 function node(...)
520         local c = _create_node({...})
521
522         c.module = getfenv(2)._NAME
523         c.auto = nil
524
525         return c
526 end
527
528 function _create_node(path, cache)
529         if #path == 0 then
530                 return context.tree
531         end
532
533         cache = cache or context.treecache
534         local name = table.concat(path, ".")
535         local c = cache[name]
536
537         if not c then
538                 local new = {nodes={}, auto=true, path=util.clone(path)}
539                 local last = table.remove(path)
540
541                 c = _create_node(path, cache)
542
543                 c.nodes[last] = new
544                 cache[name] = new
545
546                 return new
547         else
548                 return c
549         end
550 end
551
552 -- Subdispatchers --
553
554 --- Create a redirect to another dispatching node.
555 -- @param       ...             Virtual path destination
556 function alias(...)
557         local req = {...}
558         return function(...)
559                 for _, r in ipairs({...}) do
560                         req[#req+1] = r
561                 end
562
563                 dispatch(req)
564         end
565 end
566
567 --- Rewrite the first x path values of the request.
568 -- @param       n               Number of path values to replace
569 -- @param       ...             Virtual path to replace removed path values with
570 function rewrite(n, ...)
571         local req = {...}
572         return function(...)
573                 local dispatched = util.clone(context.dispatched)
574
575                 for i=1,n do
576                         table.remove(dispatched, 1)
577                 end
578
579                 for i, r in ipairs(req) do
580                         table.insert(dispatched, i, r)
581                 end
582
583                 for _, r in ipairs({...}) do
584                         dispatched[#dispatched+1] = r
585                 end
586
587                 dispatch(dispatched)
588         end
589 end
590
591
592 local function _call(self, ...)
593         if #self.argv > 0 then
594                 return getfenv()[self.name](unpack(self.argv), ...)
595         else
596                 return getfenv()[self.name](...)
597         end
598 end
599
600 --- Create a function-call dispatching target.
601 -- @param       name    Target function of local controller
602 -- @param       ...             Additional parameters passed to the function
603 function call(name, ...)
604         return {type = "call", argv = {...}, name = name, target = _call}
605 end
606
607
608 local _template = function(self, ...)
609         require "luci.template".render(self.view)
610 end
611
612 --- Create a template render dispatching target.
613 -- @param       name    Template to be rendered
614 function template(name)
615         return {type = "template", view = name, target = _template}
616 end
617
618
619 local function _cbi(self, ...)
620         local cbi = require "luci.cbi"
621         local tpl = require "luci.template"
622         local http = require "luci.http"
623
624         local config = self.config or {}
625         local maps = cbi.load(self.model, ...)
626
627         local state = nil
628
629         for i, res in ipairs(maps) do
630                 if config.autoapply then
631                         res.autoapply = config.autoapply
632                 end
633                 local cstate = res:parse()
634                 if not state or cstate < state then
635                         state = cstate
636                 end
637         end
638
639         if config.on_valid_to and state and state > 0 and state < 2 then
640                 http.redirect(config.on_valid_to)
641                 return
642         end
643
644         if config.on_changed_to and state and state > 1 then
645                 http.redirect(config.on_changed_to)
646                 return
647         end
648
649         if config.on_success_to and state and state > 0 then
650                 http.redirect(config.on_success_to)
651                 return
652         end
653
654         if config.state_handler then
655                 if not config.state_handler(state, maps) then
656                         return
657                 end
658         end
659
660         local pageaction = true
661         http.header("X-CBI-State", state or 0)
662         tpl.render("cbi/header", {state = state})
663         for i, res in ipairs(maps) do
664                 res:render()
665                 if res.pageaction == false then
666                         pageaction = false
667                 end
668         end
669         tpl.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
670 end
671
672 --- Create a CBI model dispatching target.
673 -- @param       model   CBI model to be rendered
674 function cbi(model, config)
675         return {type = "cbi", config = config, model = model, target = _cbi}
676 end
677
678
679 local function _arcombine(self, ...)
680         local argv = {...}
681         local target = #argv > 0 and self.targets[2] or self.targets[1]
682         setfenv(target.target, self.env)
683         target:target(unpack(argv))
684 end
685
686 --- Create a combined dispatching target for non argv and argv requests.
687 -- @param trg1  Overview Target
688 -- @param trg2  Detail Target
689 function arcombine(trg1, trg2)
690         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
691 end
692
693
694 local function _form(self, ...)
695         local cbi = require "luci.cbi"
696         local tpl = require "luci.template"
697         local http = require "luci.http"
698
699         local maps = luci.cbi.load(self.model, ...)
700         local state = nil
701
702         for i, res in ipairs(maps) do
703                 local cstate = res:parse()
704                 if not state or cstate < state then
705                         state = cstate
706                 end
707         end
708
709         http.header("X-CBI-State", state or 0)
710         tpl.render("header")
711         for i, res in ipairs(maps) do
712                 res:render()
713         end
714         tpl.render("footer")
715 end
716
717 --- Create a CBI form model dispatching target.
718 -- @param       model   CBI form model tpo be rendered
719 function form(model)
720         return {type = "cbi", model = model, target = _form}
721 end