Publish request args
[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 if indexcache then
304 local cachedate = fs.mtime(indexcache)
305 if cachedate and cachedate > fs.mtime(path) then
306
307 assert(
308 sys.process.info("uid") == fs.stat(indexcache, "uid")
309 and fs.stat(indexcache, "mode") == "rw-------",
310 "Fatal: Indexcache is not sane!"
311 )
312
313 index = loadfile(indexcache)()
314 return index
315 end
316 end
317
318 index = {}
319
320 local controllers = util.combine(
321 luci.fs.glob(path .. "*" .. suffix) or {},
322 luci.fs.glob(path .. "*/*" .. suffix) or {}
323 )
324
325 for i,c in ipairs(controllers) do
326 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
327 local mod = require(module)
328 local idx = mod.index
329
330 if type(idx) == "function" then
331 index[module] = idx
332 end
333 end
334
335 if indexcache then
336 fs.writefile(indexcache, util.get_bytecode(index))
337 fs.chmod(indexcache, "a-rwx,u+rw")
338 end
339 end
340
341 --- Create the dispatching tree from the index.
342 -- Build the index before if it does not exist yet.
343 function createtree()
344 if not index then
345 createindex()
346 end
347
348 local ctx = context
349 local tree = {nodes={}}
350
351 ctx.treecache = setmetatable({}, {__mode="v"})
352 ctx.tree = tree
353
354 -- Load default translation
355 require "luci.i18n".loadc("default")
356
357 local scope = setmetatable({}, {__index = luci.dispatcher})
358
359 for k, v in pairs(index) do
360 scope._NAME = k
361 setfenv(v, scope)
362 v()
363 end
364
365 return tree
366 end
367
368 --- Clone a node of the dispatching tree to another position.
369 -- @param path Virtual path destination
370 -- @param clone Virtual path source
371 -- @param title Destination node title (optional)
372 -- @param order Destination node order value (optional)
373 -- @return Dispatching tree node
374 function assign(path, clone, title, order)
375 local obj = node(unpack(path))
376 obj.nodes = nil
377 obj.module = nil
378
379 obj.title = title
380 obj.order = order
381
382 setmetatable(obj, {__index = _create_node(clone)})
383
384 return obj
385 end
386
387 --- Create a new dispatching node and define common parameters.
388 -- @param path Virtual path
389 -- @param target Target function to call when dispatched.
390 -- @param title Destination node title
391 -- @param order Destination node order value (optional)
392 -- @return Dispatching tree node
393 function entry(path, target, title, order)
394 local c = node(unpack(path))
395
396 c.target = target
397 c.title = title
398 c.order = order
399 c.module = getfenv(2)._NAME
400
401 return c
402 end
403
404 --- Fetch or create a new dispatching node.
405 -- @param ... Virtual path
406 -- @return Dispatching tree node
407 function node(...)
408 local c = _create_node({...})
409
410 c.module = getfenv(2)._NAME
411 c.path = arg
412 c.auto = nil
413
414 return c
415 end
416
417 function _create_node(path, cache)
418 if #path == 0 then
419 return context.tree
420 end
421
422 cache = cache or context.treecache
423 local name = table.concat(path, ".")
424 local c = cache[name]
425
426 if not c then
427 local last = table.remove(path)
428 c = _create_node(path, cache)
429
430 local new = {nodes={}, auto=true}
431 c.nodes[last] = new
432 cache[name] = new
433
434 return new
435 else
436 return c
437 end
438 end
439
440 -- Subdispatchers --
441
442 --- Create a redirect to another dispatching node.
443 -- @param ... Virtual path destination
444 function alias(...)
445 local req = {...}
446 return function(...)
447 for _, r in ipairs({...}) do
448 req[#req+1] = r
449 end
450
451 dispatch(req)
452 end
453 end
454
455 --- Rewrite the first x path values of the request.
456 -- @param n Number of path values to replace
457 -- @param ... Virtual path to replace removed path values with
458 function rewrite(n, ...)
459 local req = {...}
460 return function(...)
461 local dispatched = util.clone(context.dispatched)
462
463 for i=1,n do
464 table.remove(dispatched, 1)
465 end
466
467 for i, r in ipairs(req) do
468 table.insert(dispatched, i, r)
469 end
470
471 for _, r in ipairs({...}) do
472 dispatched[#dispatched+1] = r
473 end
474
475 dispatch(dispatched)
476 end
477 end
478
479 --- Create a function-call dispatching target.
480 -- @param name Target function of local controller
481 -- @param ... Additional parameters passed to the function
482 function call(name, ...)
483 local argv = {...}
484 return function(...)
485 if #argv > 0 then
486 return getfenv()[name](unpack(argv), ...)
487 else
488 return getfenv()[name](...)
489 end
490 end
491 end
492
493 --- Create a template render dispatching target.
494 -- @param name Template to be rendered
495 function template(name)
496 return function()
497 require("luci.template")
498 luci.template.render(name)
499 end
500 end
501
502 --- Create a CBI model dispatching target.
503 -- @param model CBI model to be rendered
504 function cbi(model, config)
505 config = config or {}
506 return function(...)
507 require("luci.cbi")
508 require("luci.template")
509 local http = require "luci.http"
510
511 maps = luci.cbi.load(model, ...)
512
513 local state = nil
514
515 for i, res in ipairs(maps) do
516 if config.autoapply then
517 res.autoapply = config.autoapply
518 end
519 local cstate = res:parse()
520 if not state or cstate < state then
521 state = cstate
522 end
523 end
524
525 if config.state_handler then
526 if not config.state_handler(state, maps) then
527 return
528 end
529 end
530
531 local pageaction = true
532 http.header("X-CBI-State", state or 0)
533 luci.template.render("cbi/header", {state = state})
534 for i, res in ipairs(maps) do
535 res:render()
536 if res.pageaction == false then
537 pageaction = false
538 end
539 end
540 luci.template.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
541 end
542 end
543
544 --- Create a CBI form model dispatching target.
545 -- @param model CBI form model tpo be rendered
546 function form(model)
547 return function(...)
548 require("luci.cbi")
549 require("luci.template")
550 local http = require "luci.http"
551
552 maps = luci.cbi.load(model, ...)
553
554 local state = nil
555
556 for i, res in ipairs(maps) do
557 local cstate = res:parse()
558 if not state or cstate < state then
559 state = cstate
560 end
561 end
562
563 http.header("X-CBI-State", state or 0)
564 luci.template.render("header")
565 for i, res in ipairs(maps) do
566 res:render()
567 end
568 luci.template.render("footer")
569 end
570 end