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