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