71d2a5c740e4b075aaf3784800e1db39304684ed
[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 "luci.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
34 module("luci.dispatcher", package.seeall)
35 context = util.threadlocal()
36
37 authenticator = {}
38
39 -- Index table
40 local index = nil
41
42 -- Fastindex
43 local fi
44
45
46 --- Build the URL relative to the server webroot from given virtual path.
47 -- @param ...   Virtual path
48 -- @return              Relative URL
49 function build_url(...)
50         local path = {...}
51         local sn = http.getenv("SCRIPT_NAME") or ""
52         for k, v in pairs(context.urltoken) do
53                 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
54         end
55         return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
56 end
57
58 --- Send a 404 error code and render the "error404" template if available.
59 -- @param message       Custom error message (optional)
60 -- @return                      false
61 function error404(message)
62         luci.http.status(404, "Not Found")
63         message = message or "Not Found"
64
65         require("luci.template")
66         if not luci.util.copcall(luci.template.render, "error404") then
67                 luci.http.prepare_content("text/plain")
68                 luci.http.write(message)
69         end
70         return false
71 end
72
73 --- Send a 500 error code and render the "error500" template if available.
74 -- @param message       Custom error message (optional)#
75 -- @return                      false
76 function error500(message)
77         luci.util.perror(message)
78         if not context.template_header_sent then
79                 luci.http.status(500, "Internal Server Error")
80                 luci.http.prepare_content("text/plain")
81                 luci.http.write(message)
82         else
83                 require("luci.template")
84                 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
85                         luci.http.prepare_content("text/plain")
86                         luci.http.write(message)
87                 end
88         end
89         return false
90 end
91
92 function authenticator.htmlauth(validator, accs, default)
93         local user = luci.http.formvalue("username")
94         local pass = luci.http.formvalue("password")
95
96         if user and validator(user, pass) then
97                 return user
98         end
99
100         require("luci.i18n")
101         require("luci.template")
102         context.path = {}
103         luci.template.render("sysauth", {duser=default, fuser=user})
104         return false
105
106 end
107
108 --- Dispatch an HTTP request.
109 -- @param request       LuCI HTTP Request object
110 function httpdispatch(request)
111         luci.http.context.request = request
112         context.request = {}
113         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
114
115         for node in pathinfo:gmatch("[^/]+") do
116                 table.insert(context.request, node)
117         end
118
119         local stat, err = util.coxpcall(function()
120                 dispatch(context.request)
121         end, error500)
122
123         luci.http.close()
124
125         --context._disable_memtrace()
126 end
127
128 --- Dispatches a LuCI virtual path.
129 -- @param request       Virtual path
130 function dispatch(request)
131         --context._disable_memtrace = require "luci.debug".trap_memtrace()
132         local ctx = context
133         ctx.path = request
134         ctx.urltoken   = ctx.urltoken or {}
135
136         local conf = require "luci.config"
137         assert(conf.main,
138                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
139
140         local lang = conf.main.lang
141         if lang == "auto" then
142                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
143                 for lpat in aclang:gmatch("[%w-]+") do
144                         lpat = lpat and lpat:gsub("-", "_")
145                         if conf.languages[lpat] then
146                                 lang = lpat
147                                 break
148                         end
149                 end
150         end
151         require "luci.i18n".setlanguage(lang)
152
153         local c = ctx.tree
154         local stat
155         if not c then
156                 c = createtree()
157         end
158
159         local track = {}
160         local args = {}
161         ctx.args = args
162         ctx.requestargs = ctx.requestargs or args
163         local n
164         local t = true
165         local token = ctx.urltoken
166         local preq = {}
167         local freq = {}
168
169         for i, s in ipairs(request) do
170                 local tkey, tval
171                 if t then
172                         tkey, tval = s:match(";(%w+)=(.*)")
173                 end
174
175                 if tkey then
176                         token[tkey] = tval
177                 else
178                         t = false
179                         preq[#preq+1] = s
180                         freq[#freq+1] = s
181                         c = c.nodes[s]
182                         n = i
183                         if not c then
184                                 break
185                         end
186
187                         util.update(track, c)
188
189                         if c.leaf then
190                                 break
191                         end
192                 end
193         end
194
195         if c and c.leaf then
196                 for j=n+1, #request do
197                         args[#args+1] = request[j]
198                         freq[#freq+1] = request[j]
199                 end
200         end
201
202         ctx.requestpath = freq
203         ctx.path = preq
204
205         if track.i18n then
206                 require("luci.i18n").loadc(track.i18n)
207         end
208
209         -- Init template engine
210         if (c and c.index) or not track.notemplate then
211                 local tpl = require("luci.template")
212                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
213                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
214                         media = nil
215                         for name, theme in pairs(luci.config.themes) do
216                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
217                                  "themes/%s/header" % fs.basename(theme)) then
218                                         media = theme
219                                 end
220                         end
221                         assert(media, "No valid theme found")
222                 end
223
224                 local viewns = setmetatable({}, {__index=function(table, key)
225                         if key == "controller" then
226                                 return build_url()
227                         elseif key == "REQUEST_URI" then
228                                 return build_url(unpack(ctx.requestpath))
229                         else
230                                 return rawget(table, key) or _G[key]
231                         end
232                 end})
233                 tpl.context.viewns = viewns
234                 viewns.write       = luci.http.write
235                 viewns.include     = function(name) tpl.Template(name):render(getfenv(2)) end
236                 viewns.translate   = function(...) return require("luci.i18n").translate(...) end
237                 viewns.striptags   = util.striptags
238                 viewns.media       = media
239                 viewns.theme       = fs.basename(media)
240                 viewns.resource    = luci.config.main.resourcebase
241         end
242
243         track.dependent = (track.dependent ~= false)
244         assert(not track.dependent or not track.auto, "Access Violation")
245
246         if track.sysauth then
247                 local sauth = require "luci.sauth"
248
249                 local authen = type(track.sysauth_authenticator) == "function"
250                  and track.sysauth_authenticator
251                  or authenticator[track.sysauth_authenticator]
252
253                 local def  = (type(track.sysauth) == "string") and track.sysauth
254                 local accs = def and {track.sysauth} or track.sysauth
255                 local sess = ctx.authsession
256                 local verifytoken = false
257                 if not sess then
258                         sess = luci.http.getcookie("sysauth")
259                         sess = sess and sess:match("^[A-F0-9]+$")
260                         verifytoken = true
261                 end
262
263                 local sdat = sauth.read(sess)
264                 local user
265
266                 if sdat then
267                         sdat = loadstring(sdat)
268                         setfenv(sdat, {})
269                         sdat = sdat()
270                         if not verifytoken or ctx.urltoken.stok == sdat.token then
271                                 user = sdat.user
272                         end
273                 end
274
275                 if not util.contains(accs, user) then
276                         if authen then
277                                 ctx.urltoken.stok = nil
278                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
279                                 if not user or not util.contains(accs, user) then
280                                         return
281                                 else
282                                         local sid = sess or luci.sys.uniqueid(16)
283                                         if not sess then
284                                                 local token = luci.sys.uniqueid(16)
285                                                 sauth.write(sid, util.get_bytecode({
286                                                         user=user,
287                                                         token=token,
288                                                         secret=luci.sys.uniqueid(16)
289                                                 }))
290                                                 ctx.urltoken.stok = token
291                                         end
292                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
293                                         ctx.authsession = sid
294                                 end
295                         else
296                                 luci.http.status(403, "Forbidden")
297                                 return
298                         end
299                 else
300                         ctx.authsession = sess
301                 end
302         end
303
304         if track.setgroup then
305                 luci.sys.process.setgroup(track.setgroup)
306         end
307
308         if track.setuser then
309                 luci.sys.process.setuser(track.setuser)
310         end
311
312         local target = nil
313         if c then
314                 if type(c.target) == "function" then
315                         target = c.target
316                 elseif type(c.target) == "table" then
317                         target = c.target.target
318                 end
319         end
320
321         if c and (c.index or type(target) == "function") then
322                 ctx.dispatched = c
323                 ctx.requested = ctx.requested or ctx.dispatched
324         end
325
326         if c and c.index then
327                 local tpl = require "luci.template"
328
329                 if util.copcall(tpl.render, "indexer", {}) then
330                         return true
331                 end
332         end
333
334         if type(target) == "function" then
335                 util.copcall(function()
336                         local oldenv = getfenv(target)
337                         local module = require(c.module)
338                         local env = setmetatable({}, {__index=
339
340                         function(tbl, key)
341                                 return rawget(tbl, key) or module[key] or oldenv[key]
342                         end})
343
344                         setfenv(target, env)
345                 end)
346
347                 if type(c.target) == "table" then
348                         target(c.target, unpack(args))
349                 else
350                         target(unpack(args))
351                 end
352         else
353                 error404()
354         end
355 end
356
357 --- Generate the dispatching index using the best possible strategy.
358 function createindex()
359         local path = luci.util.libpath() .. "/controller/"
360         local suff = { ".lua", ".lua.gz" }
361
362         if luci.util.copcall(require, "luci.fastindex") then
363                 createindex_fastindex(path, suff)
364         else
365                 createindex_plain(path, suff)
366         end
367 end
368
369 --- Generate the dispatching index using the fastindex C-indexer.
370 -- @param path          Controller base directory
371 -- @param suffixes      Controller file suffixes
372 function createindex_fastindex(path, suffixes)
373         index = {}
374
375         if not fi then
376                 fi = luci.fastindex.new("index")
377                 for _, suffix in ipairs(suffixes) do
378                         fi.add(path .. "*" .. suffix)
379                         fi.add(path .. "*/*" .. suffix)
380                 end
381         end
382         fi.scan()
383
384         for k, v in pairs(fi.indexes) do
385                 index[v[2]] = v[1]
386         end
387 end
388
389 --- Generate the dispatching index using the native file-cache based strategy.
390 -- @param path          Controller base directory
391 -- @param suffixes      Controller file suffixes
392 function createindex_plain(path, suffixes)
393         local controllers = { }
394         for _, suffix in ipairs(suffixes) do
395                 controllers = util.combine(
396                         controllers,
397                         luci.fs.glob(path .. "*" .. suffix) or {},
398                         luci.fs.glob(path .. "*/*" .. suffix) or {}
399                 )
400         end
401
402         if indexcache then
403                 local cachedate = fs.mtime(indexcache)
404                 if cachedate then
405                         local realdate = 0
406                         for _, obj in ipairs(controllers) do
407                                 local omtime = fs.mtime(path .. "/" .. obj)
408                                 realdate = (omtime and omtime > realdate) and omtime or realdate
409                         end
410
411                         if cachedate > realdate then
412                                 assert(
413                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
414                                         and fs.stat(indexcache, "mode") == "rw-------",
415                                         "Fatal: Indexcache is not sane!"
416                                 )
417
418                                 index = loadfile(indexcache)()
419                                 return index
420                         end
421                 end
422         end
423
424         index = {}
425
426         for i,c in ipairs(controllers) do
427                 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
428                 for _, suffix in ipairs(suffixes) do
429                         module = module:gsub(suffix.."$", "")
430                 end
431
432                 local mod = require(module)
433                 local idx = mod.index
434
435                 if type(idx) == "function" then
436                         index[module] = idx
437                 end
438         end
439
440         if indexcache then
441                 fs.writefile(indexcache, util.get_bytecode(index))
442                 fs.chmod(indexcache, "a-rwx,u+rw")
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