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