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