Heavy memory/performance optimizations #4
[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 = luci.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         return luci.http.getenv("SCRIPT_NAME") .. "/" .. table.concat(arg, "/")
51 end
52
53 --- Send a 404 error code and render the "error404" template if available.
54 -- @param message       Custom error message (optional)
55 -- @return                      false
56 function error404(message)
57         luci.http.status(404, "Not Found")
58         message = message or "Not Found"
59
60         require("luci.template")
61         if not luci.util.copcall(luci.template.render, "error404") then
62                 luci.http.prepare_content("text/plain")
63                 luci.http.write(message)
64         end
65         return false
66 end
67
68 --- Send a 500 error code and render the "error500" template if available.
69 -- @param message       Custom error message (optional)#
70 -- @return                      false
71 function error500(message)
72         luci.http.status(500, "Internal Server Error")
73
74         require("luci.template")
75         if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
76                 luci.http.prepare_content("text/plain")
77                 luci.http.write(message)
78         end
79         return false
80 end
81
82 function authenticator.htmlauth(validator, accs, default)
83         local user = luci.http.formvalue("username")
84         local pass = luci.http.formvalue("password")
85         
86         if user and validator(user, pass) then
87                 return user
88         end
89         
90         require("luci.i18n")
91         require("luci.template")
92         context.path = {}
93         luci.template.render("sysauth", {duser=default, fuser=user})
94         return false
95         
96 end
97
98 --- Dispatch an HTTP request.
99 -- @param request       LuCI HTTP Request object
100 function httpdispatch(request)
101         luci.http.context.request = request
102         context.request = {}
103         local pathinfo = request:getenv("PATH_INFO") or ""
104
105         for node in pathinfo:gmatch("[^/]+") do
106                 table.insert(context.request, node)
107         end
108
109         local stat, err = util.copcall(dispatch, context.request)
110         if not stat then
111                 error500(err)
112         end
113         
114         luci.http.close()
115 end
116
117 --- Dispatches a LuCI virtual path.
118 -- @param request       Virtual path
119 function dispatch(request)
120         local ctx = context
121         ctx.path = request
122         
123         require "luci.i18n".setlanguage(require "luci.config".main.lang)
124         
125         local c = ctx.tree
126         local stat
127         if not c then
128                 c = createtree()
129         end
130         
131         local track = {}
132         local args = {}
133         context.args = args
134         local n
135
136         for i, s in ipairs(request) do
137                 c = c.nodes[s]
138                 n = i
139                 if not c then
140                         break
141                 end
142
143                 util.update(track, c)
144                 
145                 if c.leaf then
146                         break
147                 end
148         end
149
150         if c and c.leaf then
151                 for j=n+1, #request do
152                         table.insert(args, request[j])
153                 end
154         end
155
156         if track.i18n then
157                 require("luci.i18n").loadc(track.i18n)
158         end
159         
160         -- Init template engine
161         if not track.notemplate then
162                 local tpl = require("luci.template")
163                 local viewns = setmetatable({}, {__index=_G})
164                 tpl.context.viewns = viewns
165                 viewns.write       = luci.http.write
166                 viewns.include     = function(name) tpl.Template(name):render(getfenv(2)) end
167                 viewns.translate   = function(...) return require("luci.i18n").translate(...) end
168                 viewns.striptags   = util.striptags
169                 viewns.controller  = luci.http.getenv("SCRIPT_NAME")
170                 viewns.media       = luci.config.main.mediaurlbase
171                 viewns.resource    = luci.config.main.resourcebase
172                 viewns.REQUEST_URI = (luci.http.getenv("SCRIPT_NAME") or "") .. (luci.http.getenv("PATH_INFO") or "")
173         end
174         
175         track.dependent = (track.dependent ~= false)
176         assert(not track.dependent or not track.auto, "Access Violation")
177         
178         if track.sysauth then
179                 local sauth = require "luci.sauth"
180                 
181                 local authen = type(track.sysauth_authenticator) == "function"
182                  and track.sysauth_authenticator
183                  or authenticator[track.sysauth_authenticator]
184                  
185                 local def  = (type(track.sysauth) == "string") and track.sysauth
186                 local accs = def and {track.sysauth} or track.sysauth
187                 local sess = ctx.authsession or luci.http.getcookie("sysauth")
188                 sess = sess and sess:match("^[A-F0-9]+$")
189                 local user = sauth.read(sess)
190                 
191                 if not util.contains(accs, user) then
192                         if authen then
193                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
194                                 if not user or not util.contains(accs, user) then
195                                         return
196                                 else
197                                         local sid = sess or luci.sys.uniqueid(16)
198                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
199                                         if not sess then
200                                                 sauth.write(sid, user)
201                                         end
202                                         ctx.authsession = sid
203                                 end
204                         else
205                                 luci.http.status(403, "Forbidden")
206                                 return
207                         end
208                 end
209         end
210
211         if track.setgroup then
212                 luci.sys.process.setgroup(track.setgroup)
213         end
214
215         if track.setuser then
216                 luci.sys.process.setuser(track.setuser)
217         end
218
219         if c and type(c.target) == "function" then
220                 context.dispatched = c
221                 
222                 util.copcall(function()
223                         local oldenv = getfenv(c.target)
224                         local module = require(c.module)
225                         local env = setmetatable({}, {__index=
226                                 
227                         function(tbl, key)
228                                 return rawget(tbl, key) or module[key] or oldenv[key] 
229                         end})
230
231                         setfenv(c.target, env)
232                 end)
233                 
234                 c.target(unpack(args))
235         else
236                 error404()
237         end
238 end
239
240 --- Generate the dispatching index using the best possible strategy.
241 function createindex()
242         local path = luci.util.libpath() .. "/controller/"
243         local suff = ".lua"
244         
245         if luci.util.copcall(require, "luci.fastindex") then
246                 createindex_fastindex(path, suff)
247         else
248                 createindex_plain(path, suff)
249         end
250 end
251
252 --- Generate the dispatching index using the fastindex C-indexer.
253 -- @param path          Controller base directory
254 -- @param suffix        Controller file suffix
255 function createindex_fastindex(path, suffix)
256         index = {}
257                 
258         if not fi then
259                 fi = luci.fastindex.new("index")
260                 fi.add(path .. "*" .. suffix)
261                 fi.add(path .. "*/*" .. suffix)
262         end
263         fi.scan()
264         
265         for k, v in pairs(fi.indexes) do
266                 index[v[2]] = v[1]
267         end
268 end
269
270 --- Generate the dispatching index using the native file-cache based strategy.
271 -- @param path          Controller base directory
272 -- @param suffix        Controller file suffix
273 function createindex_plain(path, suffix)
274         if indexcache then
275                 local cachedate = fs.mtime(indexcache)
276                 if cachedate and cachedate > fs.mtime(path) then
277
278                         assert(
279                                 sys.process.info("uid") == fs.stat(indexcache, "uid")
280                                 and fs.stat(indexcache, "mode") == "rw-------",
281                                 "Fatal: Indexcache is not sane!"
282                         )
283
284                         index = loadfile(indexcache)()
285                         return index
286                 end             
287         end
288         
289         index = {}
290
291         local controllers = util.combine(
292                 luci.fs.glob(path .. "*" .. suffix) or {},
293                 luci.fs.glob(path .. "*/*" .. suffix) or {}
294         )
295
296         for i,c in ipairs(controllers) do
297                 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
298                 local mod = require(module)
299                 local idx = mod.index
300                 
301                 if type(idx) == "function" then
302                         index[module] = idx
303                 end
304         end
305         
306         if indexcache then
307                 fs.writefile(indexcache, util.get_bytecode(index))
308                 fs.chmod(indexcache, "a-rwx,u+rw")
309         end
310 end
311
312 --- Create the dispatching tree from the index.
313 -- Build the index before if it does not exist yet.
314 function createtree()
315         if not index then
316                 createindex()
317         end
318         
319         local ctx  = context
320         local tree = {nodes={}}
321         
322         ctx.treecache = setmetatable({}, {__mode="v"})
323         ctx.tree = tree
324         
325         -- Load default translation
326         require "luci.i18n".loadc("default")
327         
328         local scope = setmetatable({}, {__index = _G})
329         for k,v in pairs(luci.dispatcher) do
330                 if type(v) == "function" then
331                         scope[k] = v
332                 end
333         end
334
335         for k, v in pairs(index) do
336                 scope._NAME = k
337                 setfenv(v, scope)
338                 v()
339         end
340         
341         return tree
342 end
343
344 --- Clone a node of the dispatching tree to another position.
345 -- @param       path    Virtual path destination
346 -- @param       clone   Virtual path source
347 -- @param       title   Destination node title (optional)
348 -- @param       order   Destination node order value (optional)
349 -- @return                      Dispatching tree node
350 function assign(path, clone, title, order)
351         local obj  = node(unpack(path))
352         obj.nodes  = nil
353         obj.module = nil
354         
355         obj.title = title
356         obj.order = order
357         
358         local c = context.tree
359         for k, v in ipairs(clone) do
360                 if not c.nodes[v] then
361                         c.nodes[v] = {nodes={}}
362                 end
363
364                 c = c.nodes[v]
365         end
366         
367         setmetatable(obj, {__index = c})
368         
369         return obj
370 end
371
372 --- Create a new dispatching node and define common parameters.
373 -- @param       path    Virtual path
374 -- @param       target  Target function to call when dispatched. 
375 -- @param       title   Destination node title
376 -- @param       order   Destination node order value (optional)
377 -- @return                      Dispatching tree node
378 function entry(path, target, title, order)
379         local c = node(unpack(path))
380         
381         c.target = target
382         c.title  = title
383         c.order  = order
384         c.module = getfenv(2)._NAME
385
386         return c
387 end
388
389 --- Fetch or create a new dispatching node.
390 -- @param       ...             Virtual path
391 -- @return                      Dispatching tree node
392 function node(...)
393         local c = _create_node(arg)
394
395         c.module = getfenv(2)._NAME
396         c.path = arg
397         c.auto = nil
398
399         return c
400 end
401
402 function _create_node(path, cache)
403         if #path == 0 then
404                 return context.tree
405         end
406         
407         cache = cache or context.treecache
408         local name = table.concat(path, ".")
409         local c = cache[name]
410         
411         if not c then
412                 local last = table.remove(path)
413                 c = _create_node(path, cache)
414                 
415                 local new = {nodes={}, auto=true}
416                 c.nodes[last] = new
417                 cache[name] = new
418                 
419                 return new
420         else
421                 return c
422         end
423 end
424
425 -- Subdispatchers --
426
427 --- Create a redirect to another dispatching node.
428 -- @param       ...             Virtual path destination
429 function alias(...)
430         local req = arg
431         return function()
432                 dispatch(req)
433         end
434 end
435
436 --- Rewrite the first x path values of the request.
437 -- @param       n               Number of path values to replace
438 -- @param       ...             Virtual path to replace removed path values with
439 function rewrite(n, ...)
440         local req = arg
441         return function()
442                 for i=1,n do 
443                         table.remove(context.path, 1)
444                 end
445                 
446                 for i,r in ipairs(req) do
447                         table.insert(context.path, i, r)
448                 end
449                 
450                 dispatch()
451         end
452 end
453
454 --- Create a function-call dispatching target.
455 -- @param       name    Target function of local controller 
456 -- @param       ...             Additional parameters passed to the function
457 function call(name, ...)
458         local argv = {...}
459         return function() return getfenv()[name](unpack(argv)) end
460 end
461
462 --- Create a template render dispatching target.
463 -- @param       name    Template to be rendered
464 function template(name)
465         return function()
466                 require("luci.template")
467                 luci.template.render(name)
468         end
469 end
470
471 --- Create a CBI model dispatching target.
472 -- @param       model   CBI model tpo be rendered
473 function cbi(model)
474         return function(...)
475                 require("luci.cbi")
476                 require("luci.template")
477
478                 maps = luci.cbi.load(model, ...)
479
480                 for i, res in ipairs(maps) do
481                         res:parse()
482                 end
483
484                 luci.template.render("cbi/header")
485                 for i, res in ipairs(maps) do
486                         res:render()
487                 end
488                 luci.template.render("cbi/footer")
489         end
490 end
491
492 --- Create a CBI form model dispatching target.
493 -- @param       model   CBI form model tpo be rendered
494 function form(model)
495         return function(...)
496                 require("luci.cbi")
497                 require("luci.template")
498
499                 maps = luci.cbi.load(model, ...)
500
501                 for i, res in ipairs(maps) do
502                         res:parse()
503                 end
504
505                 luci.template.render("header")
506                 for i, res in ipairs(maps) do
507                         res:render()
508                 end
509                 luci.template.render("footer")
510         end
511 end