068f350ce4da22a756a834fe1667a948e7d22559
[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") .. (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 = 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 = luci.sys.uniqueid(16)
195 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
196 luci.sauth.write(sid, user)
197 end
198 else
199 luci.http.status(403, "Forbidden")
200 return
201 end
202 end
203 end
204
205 if track.setgroup then
206 luci.sys.process.setgroup(track.setgroup)
207 end
208
209 if track.setuser then
210 luci.sys.process.setuser(track.setuser)
211 end
212
213 if c and type(c.target) == "function" then
214 context.dispatched = c
215 stat, mod = luci.util.copcall(require, c.module)
216 if stat then
217 luci.util.updfenv(c.target, mod)
218 end
219
220 stat, err = luci.util.copcall(c.target, unpack(args))
221 if not stat then
222 error500(err)
223 end
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 index = {}
264
265 local cache = nil
266
267 local controllers = luci.util.combine(
268 luci.fs.glob(path .. "*" .. suffix) or {},
269 luci.fs.glob(path .. "*/*" .. suffix) or {}
270 )
271
272 if indexcache then
273 cache = luci.fs.mtime(indexcache)
274
275 if not cache then
276 luci.fs.mkdir(indexcache)
277 luci.fs.chmod(indexcache, "a=,u=rwx")
278 cache = luci.fs.mtime(indexcache)
279 end
280 end
281
282 for i,c in ipairs(controllers) do
283 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
284 local cachefile
285 local stime
286 local ctime
287
288 if cache then
289 cachefile = indexcache .. "/" .. module
290 stime = luci.fs.mtime(c) or 0
291 ctime = luci.fs.mtime(cachefile) or 0
292 end
293
294 if not cache or stime > ctime then
295 stat, mod = luci.util.copcall(require, module)
296
297 if stat and mod and type(mod.index) == "function" then
298 index[module] = mod.index
299
300 if cache then
301 luci.fs.writefile(cachefile, luci.util.get_bytecode(mod.index))
302 end
303 end
304 else
305 index[module] = loadfile(cachefile)
306 end
307 end
308 end
309
310 --- Create the dispatching tree from the index.
311 -- Build the index before if it does not exist yet.
312 function createtree()
313 if not index then
314 createindex()
315 end
316
317 context.tree = {nodes={}}
318 require("luci.i18n")
319
320 -- Load default translation
321 luci.i18n.loadc("default")
322
323 local scope = luci.util.clone(_G)
324 for k,v in pairs(luci.dispatcher) do
325 if type(v) == "function" then
326 scope[k] = v
327 end
328 end
329
330 for k, v in pairs(index) do
331 scope._NAME = k
332 setfenv(v, scope)
333
334 local stat, err = luci.util.copcall(v)
335 if not stat then
336 error500("createtree failed: " .. k .. ": " .. err)
337 luci.http.close()
338 os.exit(1)
339 end
340 end
341 end
342
343 --- Clone a node of the dispatching tree to another position.
344 -- @param path Virtual path destination
345 -- @param clone Virtual path source
346 -- @param title Destination node title (optional)
347 -- @param order Destination node order value (optional)
348 -- @return Dispatching tree node
349 function assign(path, clone, title, order)
350 local obj = node(unpack(path))
351 obj.nodes = nil
352 obj.module = nil
353
354 obj.title = title
355 obj.order = order
356
357 local c = context.tree
358 for k, v in ipairs(clone) do
359 if not c.nodes[v] then
360 c.nodes[v] = {nodes={}}
361 end
362
363 c = c.nodes[v]
364 end
365
366 setmetatable(obj, {__index = c})
367
368 return obj
369 end
370
371 --- Create a new dispatching node and define common parameters.
372 -- @param path Virtual path
373 -- @param target Target function to call when dispatched.
374 -- @param title Destination node title
375 -- @param order Destination node order value (optional)
376 -- @return Dispatching tree node
377 function entry(path, target, title, order)
378 local c = node(unpack(path))
379
380 c.target = target
381 c.title = title
382 c.order = order
383 c.module = getfenv(2)._NAME
384
385 return c
386 end
387
388 --- Fetch or create a new dispatching node.
389 -- @param ... Virtual path
390 -- @return Dispatching tree node
391 function node(...)
392 local c = context.tree
393 arg.n = nil
394
395 for k,v in ipairs(arg) do
396 if not c.nodes[v] then
397 c.nodes[v] = {nodes={}, auto=true}
398 end
399
400 c = c.nodes[v]
401 end
402
403 c.module = getfenv(2)._NAME
404 c.path = arg
405 c.auto = nil
406
407 return c
408 end
409
410 -- Subdispatchers --
411
412 --- Create a redirect to another dispatching node.
413 -- @param ... Virtual path destination
414 function alias(...)
415 local req = arg
416 return function()
417 dispatch(req)
418 end
419 end
420
421 --- Rewrite the first x path values of the request.
422 -- @param n Number of path values to replace
423 -- @param ... Virtual path to replace removed path values with
424 function rewrite(n, ...)
425 local req = arg
426 return function()
427 for i=1,n do
428 table.remove(context.path, 1)
429 end
430
431 for i,r in ipairs(req) do
432 table.insert(context.path, i, r)
433 end
434
435 dispatch()
436 end
437 end
438
439 --- Create a function-call dispatching target.
440 -- @param name Target function of local controller
441 -- @param ... Additional parameters passed to the function
442 function call(name, ...)
443 local argv = {...}
444 return function() return getfenv()[name](unpack(argv)) end
445 end
446
447 --- Create a template render dispatching target.
448 -- @param name Template to be rendered
449 function template(name)
450 require("luci.template")
451 return function() luci.template.render(name) end
452 end
453
454 --- Create a CBI model dispatching target.
455 -- @param model CBI model tpo be rendered
456 function cbi(model)
457 require("luci.cbi")
458 require("luci.template")
459
460 return function(...)
461 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
462 if not stat then
463 error500(maps)
464 return true
465 end
466
467 for i, res in ipairs(maps) do
468 local stat, err = luci.util.copcall(res.parse, res)
469 if not stat then
470 error500(err)
471 return true
472 end
473 end
474
475 luci.template.render("cbi/header")
476 for i, res in ipairs(maps) do
477 res:render()
478 end
479 luci.template.render("cbi/footer")
480 end
481 end
482
483 --- Create a CBI form model dispatching target.
484 -- @param model CBI form model tpo be rendered
485 function form(model)
486 require("luci.cbi")
487 require("luci.template")
488
489 return function(...)
490 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
491 if not stat then
492 error500(maps)
493 return true
494 end
495
496 for i, res in ipairs(maps) do
497 local stat, err = luci.util.copcall(res.parse, res)
498 if not stat then
499 error500(err)
500 return true
501 end
502 end
503
504 luci.template.render("header")
505 for i, res in ipairs(maps) do
506 res:render()
507 end
508 luci.template.render("footer")
509 end
510 end