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