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