6445c124ed5ee0fc076bccad528f7577ddcf7290
[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 = ctx.requestpath or 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 pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
223                         media = nil
224                         for name, theme in pairs(luci.config.themes) do
225                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
226                                  "themes/%s/header" % fs.basename(theme)) then
227                                         media = theme
228                                 end
229                         end
230                         assert(media, "No valid theme found")
231                 end
232
233                 tpl.context.viewns = setmetatable({
234                    write       = luci.http.write;
235                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
236                    translate   = function(...) return require("luci.i18n").translate(...) end;
237                    striptags   = util.striptags;
238                    pcdata      = util.pcdata;
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                                         ctx.authuser = user
311                                 end
312                         else
313                                 luci.http.status(403, "Forbidden")
314                                 return
315                         end
316                 else
317                         ctx.authsession = sess
318                         ctx.authuser = user
319                 end
320         end
321
322         if track.setgroup then
323                 luci.sys.process.setgroup(track.setgroup)
324         end
325
326         if track.setuser then
327                 luci.sys.process.setuser(track.setuser)
328         end
329
330         local target = nil
331         if c then
332                 if type(c.target) == "function" then
333                         target = c.target
334                 elseif type(c.target) == "table" then
335                         target = c.target.target
336                 end
337         end
338
339         if c and (c.index or type(target) == "function") then
340                 ctx.dispatched = c
341                 ctx.requested = ctx.requested or ctx.dispatched
342         end
343
344         if c and c.index then
345                 local tpl = require "luci.template"
346
347                 if util.copcall(tpl.render, "indexer", {}) then
348                         return true
349                 end
350         end
351
352         if type(target) == "function" then
353                 util.copcall(function()
354                         local oldenv = getfenv(target)
355                         local module = require(c.module)
356                         local env = setmetatable({}, {__index=
357
358                         function(tbl, key)
359                                 return rawget(tbl, key) or module[key] or oldenv[key]
360                         end})
361
362                         setfenv(target, env)
363                 end)
364
365                 if type(c.target) == "table" then
366                         target(c.target, unpack(args))
367                 else
368                         target(unpack(args))
369                 end
370         else
371                 error404()
372         end
373 end
374
375 --- Generate the dispatching index using the best possible strategy.
376 function createindex()
377         local path = luci.util.libpath() .. "/controller/"
378         local suff = { ".lua", ".lua.gz" }
379
380         if luci.util.copcall(require, "luci.fastindex") then
381                 createindex_fastindex(path, suff)
382         else
383                 createindex_plain(path, suff)
384         end
385 end
386
387 --- Generate the dispatching index using the fastindex C-indexer.
388 -- @param path          Controller base directory
389 -- @param suffixes      Controller file suffixes
390 function createindex_fastindex(path, suffixes)
391         index = {}
392
393         if not fi then
394                 fi = luci.fastindex.new("index")
395                 for _, suffix in ipairs(suffixes) do
396                         fi.add(path .. "*" .. suffix)
397                         fi.add(path .. "*/*" .. suffix)
398                 end
399         end
400         fi.scan()
401
402         for k, v in pairs(fi.indexes) do
403                 index[v[2]] = v[1]
404         end
405 end
406
407 --- Generate the dispatching index using the native file-cache based strategy.
408 -- @param path          Controller base directory
409 -- @param suffixes      Controller file suffixes
410 function createindex_plain(path, suffixes)
411         local controllers = { }
412         for _, suffix in ipairs(suffixes) do
413                 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
414                 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
415         end
416
417         if indexcache then
418                 local cachedate = fs.stat(indexcache, "mtime")
419                 if cachedate then
420                         local realdate = 0
421                         for _, obj in ipairs(controllers) do
422                                 local omtime = fs.stat(path .. "/" .. obj, "mtime")
423                                 realdate = (omtime and omtime > realdate) and omtime or realdate
424                         end
425
426                         if cachedate > realdate then
427                                 assert(
428                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
429                                         and fs.stat(indexcache, "modestr") == "rw-------",
430                                         "Fatal: Indexcache is not sane!"
431                                 )
432
433                                 index = loadfile(indexcache)()
434                                 return index
435                         end
436                 end
437         end
438
439         index = {}
440
441         for i,c in ipairs(controllers) do
442                 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
443                 for _, suffix in ipairs(suffixes) do
444                         module = module:gsub(suffix.."$", "")
445                 end
446
447                 local mod = require(module)
448                 local idx = mod.index
449
450                 if type(idx) == "function" then
451                         index[module] = idx
452                 end
453         end
454
455         if indexcache then
456                 local f = nixio.open(indexcache, "w", 600)
457                 f:writeall(util.get_bytecode(index))
458                 f:close()
459         end
460 end
461
462 --- Create the dispatching tree from the index.
463 -- Build the index before if it does not exist yet.
464 function createtree()
465         if not index then
466                 createindex()
467         end
468
469         local ctx  = context
470         local tree = {nodes={}}
471         local modi = {}
472
473         ctx.treecache = setmetatable({}, {__mode="v"})
474         ctx.tree = tree
475         ctx.modifiers = modi
476
477         -- Load default translation
478         require "luci.i18n".loadc("default")
479
480         local scope = setmetatable({}, {__index = luci.dispatcher})
481
482         for k, v in pairs(index) do
483                 scope._NAME = k
484                 setfenv(v, scope)
485                 v()
486         end
487
488         local function modisort(a,b)
489                 return modi[a].order < modi[b].order
490         end
491
492         for _, v in util.spairs(modi, modisort) do
493                 scope._NAME = v.module
494                 setfenv(v.func, scope)
495                 v.func()
496         end
497
498         return tree
499 end
500
501 --- Register a tree modifier.
502 -- @param       func    Modifier function
503 -- @param       order   Modifier order value (optional)
504 function modifier(func, order)
505         context.modifiers[#context.modifiers+1] = {
506                 func = func,
507                 order = order or 0,
508                 module
509                         = getfenv(2)._NAME
510         }
511 end
512
513 --- Clone a node of the dispatching tree to another position.
514 -- @param       path    Virtual path destination
515 -- @param       clone   Virtual path source
516 -- @param       title   Destination node title (optional)
517 -- @param       order   Destination node order value (optional)
518 -- @return                      Dispatching tree node
519 function assign(path, clone, title, order)
520         local obj  = node(unpack(path))
521         obj.nodes  = nil
522         obj.module = nil
523
524         obj.title = title
525         obj.order = order
526
527         setmetatable(obj, {__index = _create_node(clone)})
528
529         return obj
530 end
531
532 --- Create a new dispatching node and define common parameters.
533 -- @param       path    Virtual path
534 -- @param       target  Target function to call when dispatched.
535 -- @param       title   Destination node title
536 -- @param       order   Destination node order value (optional)
537 -- @return                      Dispatching tree node
538 function entry(path, target, title, order)
539         local c = node(unpack(path))
540
541         c.target = target
542         c.title  = title
543         c.order  = order
544         c.module = getfenv(2)._NAME
545
546         return c
547 end
548
549 --- Fetch or create a dispatching node without setting the target module or
550 -- enabling the node.
551 -- @param       ...             Virtual path
552 -- @return                      Dispatching tree node
553 function get(...)
554         return _create_node({...})
555 end
556
557 --- Fetch or create a new dispatching node.
558 -- @param       ...             Virtual path
559 -- @return                      Dispatching tree node
560 function node(...)
561         local c = _create_node({...})
562
563         c.module = getfenv(2)._NAME
564         c.auto = nil
565
566         return c
567 end
568
569 function _create_node(path, cache)
570         if #path == 0 then
571                 return context.tree
572         end
573
574         cache = cache or context.treecache
575         local name = table.concat(path, ".")
576         local c = cache[name]
577
578         if not c then
579                 local new = {nodes={}, auto=true, path=util.clone(path)}
580                 local last = table.remove(path)
581
582                 c = _create_node(path, cache)
583
584                 c.nodes[last] = new
585                 cache[name] = new
586
587                 return new
588         else
589                 return c
590         end
591 end
592
593 -- Subdispatchers --
594
595 --- Create a redirect to another dispatching node.
596 -- @param       ...             Virtual path destination
597 function alias(...)
598         local req = {...}
599         return function(...)
600                 for _, r in ipairs({...}) do
601                         req[#req+1] = r
602                 end
603
604                 dispatch(req)
605         end
606 end
607
608 --- Rewrite the first x path values of the request.
609 -- @param       n               Number of path values to replace
610 -- @param       ...             Virtual path to replace removed path values with
611 function rewrite(n, ...)
612         local req = {...}
613         return function(...)
614                 local dispatched = util.clone(context.dispatched)
615
616                 for i=1,n do
617                         table.remove(dispatched, 1)
618                 end
619
620                 for i, r in ipairs(req) do
621                         table.insert(dispatched, i, r)
622                 end
623
624                 for _, r in ipairs({...}) do
625                         dispatched[#dispatched+1] = r
626                 end
627
628                 dispatch(dispatched)
629         end
630 end
631
632
633 local function _call(self, ...)
634         if #self.argv > 0 then
635                 return getfenv()[self.name](unpack(self.argv), ...)
636         else
637                 return getfenv()[self.name](...)
638         end
639 end
640
641 --- Create a function-call dispatching target.
642 -- @param       name    Target function of local controller
643 -- @param       ...             Additional parameters passed to the function
644 function call(name, ...)
645         return {type = "call", argv = {...}, name = name, target = _call}
646 end
647
648
649 local _template = function(self, ...)
650         require "luci.template".render(self.view)
651 end
652
653 --- Create a template render dispatching target.
654 -- @param       name    Template to be rendered
655 function template(name)
656         return {type = "template", view = name, target = _template}
657 end
658
659
660 local function _cbi(self, ...)
661         local cbi = require "luci.cbi"
662         local tpl = require "luci.template"
663         local http = require "luci.http"
664
665         local config = self.config or {}
666         local maps = cbi.load(self.model, ...)
667
668         local state = nil
669
670         for i, res in ipairs(maps) do
671                 res.flow = config
672                 local cstate = res:parse()
673                 if cstate and (not state or cstate < state) then
674                         state = cstate
675                 end
676         end
677
678         local function _resolve_path(path)
679                 return type(path) == "table" and build_url(unpack(path)) or path
680         end
681
682         if config.on_valid_to and state and state > 0 and state < 2 then
683                 http.redirect(_resolve_path(config.on_valid_to))
684                 return
685         end
686
687         if config.on_changed_to and state and state > 1 then
688                 http.redirect(_resolve_path(config.on_changed_to))
689                 return
690         end
691
692         if config.on_success_to and state and state > 0 then
693                 http.redirect(_resolve_path(config.on_success_to))
694                 return
695         end
696
697         if config.state_handler then
698                 if not config.state_handler(state, maps) then
699                         return
700                 end
701         end
702
703         local pageaction = true
704         http.header("X-CBI-State", state or 0)
705         if not config.noheader then
706                 tpl.render("cbi/header", {state = state})
707         end
708         for i, res in ipairs(maps) do
709                 res:render()
710                 if res.pageaction == false then
711                         pageaction = false
712                 end
713         end
714         if not config.nofooter then
715                 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
716         end
717 end
718
719 --- Create a CBI model dispatching target.
720 -- @param       model   CBI model to be rendered
721 function cbi(model, config)
722         return {type = "cbi", config = config, model = model, target = _cbi}
723 end
724
725
726 local function _arcombine(self, ...)
727         local argv = {...}
728         local target = #argv > 0 and self.targets[2] or self.targets[1]
729         setfenv(target.target, self.env)
730         target:target(unpack(argv))
731 end
732
733 --- Create a combined dispatching target for non argv and argv requests.
734 -- @param trg1  Overview Target
735 -- @param trg2  Detail Target
736 function arcombine(trg1, trg2)
737         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
738 end
739
740
741 local function _form(self, ...)
742         local cbi = require "luci.cbi"
743         local tpl = require "luci.template"
744         local http = require "luci.http"
745
746         local maps = luci.cbi.load(self.model, ...)
747         local state = nil
748
749         for i, res in ipairs(maps) do
750                 local cstate = res:parse()
751                 if cstate and (not state or cstate < state) then
752                         state = cstate
753                 end
754         end
755
756         http.header("X-CBI-State", state or 0)
757         tpl.render("header")
758         for i, res in ipairs(maps) do
759                 res:render()
760         end
761         tpl.render("footer")
762 end
763
764 --- Create a CBI form model dispatching target.
765 -- @param       model   CBI form model tpo be rendered
766 function form(model)
767         return {type = "cbi", model = model, target = _form}
768 end