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