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