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