libs/core: Reworked some basic libraries to not use package.seeall
[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 require("luci.template")
453 return function() luci.template.render(name) end
454 end
455
456 --- Create a CBI model dispatching target.
457 -- @param model CBI model tpo be rendered
458 function cbi(model)
459 require("luci.cbi")
460 require("luci.template")
461
462 return function(...)
463 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
464 if not stat then
465 error500(maps)
466 return true
467 end
468
469 for i, res in ipairs(maps) do
470 local stat, err = luci.util.copcall(res.parse, res)
471 if not stat then
472 error500(err)
473 return true
474 end
475 end
476
477 luci.template.render("cbi/header")
478 for i, res in ipairs(maps) do
479 res:render()
480 end
481 luci.template.render("cbi/footer")
482 end
483 end
484
485 --- Create a CBI form model dispatching target.
486 -- @param model CBI form model tpo be rendered
487 function form(model)
488 require("luci.cbi")
489 require("luci.template")
490
491 return function(...)
492 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
493 if not stat then
494 error500(maps)
495 return true
496 end
497
498 for i, res in ipairs(maps) do
499 local stat, err = luci.util.copcall(res.parse, res)
500 if not stat then
501 error500(err)
502 return true
503 end
504 end
505
506 luci.template.render("header")
507 for i, res in ipairs(maps) do
508 res:render()
509 end
510 luci.template.render("footer")
511 end
512 end