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