General optimizations, simplifications and improvements
[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 index = loadfile(indexcache)()
267 return index
268 end
269 end
270
271 index = {}
272
273 local controllers = util.combine(
274 luci.fs.glob(path .. "*" .. suffix) or {},
275 luci.fs.glob(path .. "*/*" .. suffix) or {}
276 )
277
278 for i,c in ipairs(controllers) do
279 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
280 local mod = require(module)
281 local idx = mod.index
282
283 if type(idx) == "function" then
284 index[module] = idx
285 end
286 end
287
288 if indexcache then
289 fs.writefile(indexcache, util.get_bytecode(index))
290 end
291 end
292
293 --- Create the dispatching tree from the index.
294 -- Build the index before if it does not exist yet.
295 function createtree()
296 if not index then
297 createindex()
298 end
299
300 local ctx = context
301 local tree = {nodes={}}
302
303 ctx.treecache = setmetatable({}, {__mode="v"})
304 ctx.tree = tree
305
306 -- Load default translation
307 require "luci.i18n".loadc("default")
308
309 local scope = setmetatable({}, {__index = _G})
310 for k,v in pairs(_M) do
311 if type(v) == "function" then
312 scope[k] = v
313 end
314 end
315
316 for k, v in pairs(index) do
317 scope._NAME = k
318 setfenv(v, scope)
319 v()
320 end
321
322 return tree
323 end
324
325 --- Clone a node of the dispatching tree to another position.
326 -- @param path Virtual path destination
327 -- @param clone Virtual path source
328 -- @param title Destination node title (optional)
329 -- @param order Destination node order value (optional)
330 -- @return Dispatching tree node
331 function assign(path, clone, title, order)
332 local obj = node(unpack(path))
333 obj.nodes = nil
334 obj.module = nil
335
336 obj.title = title
337 obj.order = order
338
339 local c = context.tree
340 for k, v in ipairs(clone) do
341 if not c.nodes[v] then
342 c.nodes[v] = {nodes={}}
343 end
344
345 c = c.nodes[v]
346 end
347
348 setmetatable(obj, {__index = c})
349
350 return obj
351 end
352
353 --- Create a new dispatching node and define common parameters.
354 -- @param path Virtual path
355 -- @param target Target function to call when dispatched.
356 -- @param title Destination node title
357 -- @param order Destination node order value (optional)
358 -- @return Dispatching tree node
359 function entry(path, target, title, order)
360 local c = node(unpack(path))
361
362 c.target = target
363 c.title = title
364 c.order = order
365 c.module = getfenv(2)._NAME
366
367 return c
368 end
369
370 --- Fetch or create a new dispatching node.
371 -- @param ... Virtual path
372 -- @return Dispatching tree node
373 function node(...)
374 local c = _create_node(arg)
375
376 c.module = getfenv(2)._NAME
377 c.path = arg
378 c.auto = nil
379
380 return c
381 end
382
383 function _create_node(path, cache)
384 if #path == 0 then
385 return context.tree
386 end
387
388 cache = cache or context.treecache
389 local name = table.concat(path, ".")
390 local c = cache[name]
391
392 if not c then
393 local last = table.remove(path)
394 c = _create_node(path, cache)
395
396 local new = {nodes={}, auto=true}
397 c.nodes[last] = new
398 cache[name] = new
399
400 return new
401 else
402 return c
403 end
404 end
405
406 -- Subdispatchers --
407
408 --- Create a redirect to another dispatching node.
409 -- @param ... Virtual path destination
410 function alias(...)
411 local req = arg
412 return function()
413 dispatch(req)
414 end
415 end
416
417 --- Rewrite the first x path values of the request.
418 -- @param n Number of path values to replace
419 -- @param ... Virtual path to replace removed path values with
420 function rewrite(n, ...)
421 local req = arg
422 return function()
423 for i=1,n do
424 table.remove(context.path, 1)
425 end
426
427 for i,r in ipairs(req) do
428 table.insert(context.path, i, r)
429 end
430
431 dispatch()
432 end
433 end
434
435 --- Create a function-call dispatching target.
436 -- @param name Target function of local controller
437 -- @param ... Additional parameters passed to the function
438 function call(name, ...)
439 local argv = {...}
440 return function() return getfenv()[name](unpack(argv)) end
441 end
442
443 --- Create a template render dispatching target.
444 -- @param name Template to be rendered
445 function template(name)
446 return function()
447 require("luci.template")
448 luci.template.render(name)
449 end
450 end
451
452 --- Create a CBI model dispatching target.
453 -- @param model CBI model tpo be rendered
454 function cbi(model)
455 return function(...)
456 require("luci.cbi")
457 require("luci.template")
458
459 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
460 if not stat then
461 error500(maps)
462 return true
463 end
464
465 for i, res in ipairs(maps) do
466 local stat, err = luci.util.copcall(res.parse, res)
467 if not stat then
468 error500(err)
469 return true
470 end
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 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
489 if not stat then
490 error500(maps)
491 return true
492 end
493
494 for i, res in ipairs(maps) do
495 local stat, err = luci.util.copcall(res.parse, res)
496 if not stat then
497 error500(err)
498 return true
499 end
500 end
501
502 luci.template.render("header")
503 for i, res in ipairs(maps) do
504 res:render()
505 end
506 luci.template.render("footer")
507 end
508 end