General optimizations, simplifications and improvements
[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 = {}
164                 tpl.context.viewns = viewns
165                 viewns.write       = luci.http.write
166                 viewns.translate   = function(...) return require("luci.i18n").translate(...) end
167                 viewns.striptags   = util.striptags
168                 viewns.controller  = luci.http.getenv("SCRIPT_NAME")
169                 viewns.media       = luci.config.main.mediaurlbase
170                 viewns.resource    = luci.config.main.resourcebase
171                 viewns.REQUEST_URI = (luci.http.getenv("SCRIPT_NAME") or "") .. (luci.http.getenv("PATH_INFO") or "")
172         end
173         
174         assert(not track.dependent or not track.auto, "Access Violation")
175         
176         if track.sysauth then
177                 local sauth = require "luci.sauth"
178                 
179                 local authen = type(track.sysauth_authenticator) == "function"
180                  and track.sysauth_authenticator
181                  or authenticator[track.sysauth_authenticator]
182                  
183                 local def  = (type(track.sysauth) == "string") and track.sysauth
184                 local accs = def and {track.sysauth} or track.sysauth
185                 local sess = luci.http.getcookie("sysauth")
186                 sess = sess and sess:match("^[A-F0-9]+$")
187                 local user = sauth.read(sess)
188                 
189                 if not util.contains(accs, user) then
190                         if authen then
191                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
192                                 if not user or not util.contains(accs, user) then
193                                         return
194                                 else
195                                         local sid = sess or luci.sys.uniqueid(16)
196                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
197                                         if not sess then
198                                                 sauth.write(sid, user)
199                                         end
200                                 end
201                         else
202                                 luci.http.status(403, "Forbidden")
203                                 return
204                         end
205                 end
206         end
207
208         if track.setgroup then
209                 luci.sys.process.setgroup(track.setgroup)
210         end
211
212         if track.setuser then
213                 luci.sys.process.setuser(track.setuser)
214         end
215
216         if c and type(c.target) == "function" then
217                 context.dispatched = c
218                 
219                 util.copcall(function()
220                         util.updfenv(c.target, require(c.module))
221                 end)
222                 
223                 c.target(unpack(args))
224         else
225                 error404()
226         end
227 end
228
229 --- Generate the dispatching index using the best possible strategy.
230 function createindex()
231         local path = luci.util.libpath() .. "/controller/"
232         local suff = ".lua"
233         
234         if luci.util.copcall(require, "luci.fastindex") then
235                 createindex_fastindex(path, suff)
236         else
237                 createindex_plain(path, suff)
238         end
239 end
240
241 --- Generate the dispatching index using the fastindex C-indexer.
242 -- @param path          Controller base directory
243 -- @param suffix        Controller file suffix
244 function createindex_fastindex(path, suffix)
245         index = {}
246                 
247         if not fi then
248                 fi = luci.fastindex.new("index")
249                 fi.add(path .. "*" .. suffix)
250                 fi.add(path .. "*/*" .. suffix)
251         end
252         fi.scan()
253         
254         for k, v in pairs(fi.indexes) do
255                 index[v[2]] = v[1]
256         end
257 end
258
259 --- Generate the dispatching index using the native file-cache based strategy.
260 -- @param path          Controller base directory
261 -- @param suffix        Controller file suffix
262 function createindex_plain(path, suffix)
263         if indexcache then
264                 local cachedate = fs.mtime(indexcache)
265                 if cachedate and cachedate > fs.mtime(path) then
266                         index = loadfile(indexcache)()
267                         return index
268                 end             
269         end
270         
271         index = {}
272
273         local controllers = util.combine(
274                 luci.fs.glob(path .. "*" .. suffix) or {},
275                 luci.fs.glob(path .. "*/*" .. suffix) or {}
276         )
277
278         for i,c in ipairs(controllers) do
279                 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
280                 local mod = require(module)
281                 local idx = mod.index
282                 
283                 if type(idx) == "function" then
284                         index[module] = idx
285                 end
286         end
287         
288         if indexcache then
289                 fs.writefile(indexcache, util.get_bytecode(index))
290         end
291 end
292
293 --- Create the dispatching tree from the index.
294 -- Build the index before if it does not exist yet.
295 function createtree()
296         if not index then
297                 createindex()
298         end
299         
300         local ctx  = context
301         local tree = {nodes={}}
302         
303         ctx.treecache = setmetatable({}, {__mode="v"})
304         ctx.tree = tree
305         
306         -- Load default translation
307         require "luci.i18n".loadc("default")
308         
309         local scope = setmetatable({}, {__index = _G})
310         for k,v in pairs(_M) do
311                 if type(v) == "function" then
312                         scope[k] = v
313                 end
314         end
315
316         for k, v in pairs(index) do
317                 scope._NAME = k
318                 setfenv(v, scope)
319                 v()
320         end
321         
322         return tree
323 end
324
325 --- Clone a node of the dispatching tree to another position.
326 -- @param       path    Virtual path destination
327 -- @param       clone   Virtual path source
328 -- @param       title   Destination node title (optional)
329 -- @param       order   Destination node order value (optional)
330 -- @return                      Dispatching tree node
331 function assign(path, clone, title, order)
332         local obj  = node(unpack(path))
333         obj.nodes  = nil
334         obj.module = nil
335         
336         obj.title = title
337         obj.order = order
338         
339         local c = context.tree
340         for k, v in ipairs(clone) do
341                 if not c.nodes[v] then
342                         c.nodes[v] = {nodes={}}
343                 end
344
345                 c = c.nodes[v]
346         end
347         
348         setmetatable(obj, {__index = c})
349         
350         return obj
351 end
352
353 --- Create a new dispatching node and define common parameters.
354 -- @param       path    Virtual path
355 -- @param       target  Target function to call when dispatched. 
356 -- @param       title   Destination node title
357 -- @param       order   Destination node order value (optional)
358 -- @return                      Dispatching tree node
359 function entry(path, target, title, order)
360         local c = node(unpack(path))
361         
362         c.target = target
363         c.title  = title
364         c.order  = order
365         c.module = getfenv(2)._NAME
366
367         return c
368 end
369
370 --- Fetch or create a new dispatching node.
371 -- @param       ...             Virtual path
372 -- @return                      Dispatching tree node
373 function node(...)
374         local c = _create_node(arg)
375
376         c.module = getfenv(2)._NAME
377         c.path = arg
378         c.auto = nil
379
380         return c
381 end
382
383 function _create_node(path, cache)
384         if #path == 0 then
385                 return context.tree
386         end
387         
388         cache = cache or context.treecache
389         local name = table.concat(path, ".")
390         local c = cache[name]
391         
392         if not c then
393                 local last = table.remove(path)
394                 c = _create_node(path, cache)
395                 
396                 local new = {nodes={}, auto=true}
397                 c.nodes[last] = new
398                 cache[name] = new
399                 
400                 return new
401         else
402                 return c
403         end
404 end
405
406 -- Subdispatchers --
407
408 --- Create a redirect to another dispatching node.
409 -- @param       ...             Virtual path destination
410 function alias(...)
411         local req = arg
412         return function()
413                 dispatch(req)
414         end
415 end
416
417 --- Rewrite the first x path values of the request.
418 -- @param       n               Number of path values to replace
419 -- @param       ...             Virtual path to replace removed path values with
420 function rewrite(n, ...)
421         local req = arg
422         return function()
423                 for i=1,n do 
424                         table.remove(context.path, 1)
425                 end
426                 
427                 for i,r in ipairs(req) do
428                         table.insert(context.path, i, r)
429                 end
430                 
431                 dispatch()
432         end
433 end
434
435 --- Create a function-call dispatching target.
436 -- @param       name    Target function of local controller 
437 -- @param       ...             Additional parameters passed to the function
438 function call(name, ...)
439         local argv = {...}
440         return function() return getfenv()[name](unpack(argv)) end
441 end
442
443 --- Create a template render dispatching target.
444 -- @param       name    Template to be rendered
445 function template(name)
446         return function()
447                 require("luci.template")
448                 luci.template.render(name)
449         end
450 end
451
452 --- Create a CBI model dispatching target.
453 -- @param       model   CBI model tpo be rendered
454 function cbi(model)
455         return function(...)
456                 require("luci.cbi")
457                 require("luci.template")
458
459                 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
460                 if not stat then
461                         error500(maps)
462                         return true
463                 end
464
465                 for i, res in ipairs(maps) do
466                         local stat, err = luci.util.copcall(res.parse, res)
467                         if not stat then
468                                 error500(err)
469                                 return true
470                         end
471                 end
472
473                 luci.template.render("cbi/header")
474                 for i, res in ipairs(maps) do
475                         res:render()
476                 end
477                 luci.template.render("cbi/footer")
478         end
479 end
480
481 --- Create a CBI form model dispatching target.
482 -- @param       model   CBI form model tpo be rendered
483 function form(model)
484         return function(...)
485                 require("luci.cbi")
486                 require("luci.template")
487
488                 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
489                 if not stat then
490                         error500(maps)
491                         return true
492                 end
493
494                 for i, res in ipairs(maps) do
495                         local stat, err = luci.util.copcall(res.parse, res)
496                         if not stat then
497                                 error500(err)
498                                 return true
499                         end
500                 end
501
502                 luci.template.render("header")
503                 for i, res in ipairs(maps) do
504                         res:render()
505                 end
506                 luci.template.render("footer")
507         end
508 end