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