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