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