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