Assign correct path attribute to nodes
[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.auto = nil
418
419         return c
420 end
421
422 function _create_node(path, cache)
423         if #path == 0 then
424                 return context.tree
425         end
426
427         cache = cache or context.treecache
428         local name = table.concat(path, ".")
429         local c = cache[name]
430
431         if not c then
432                 local new = {nodes={}, auto=true, path=util.clone(path)}
433                 local last = table.remove(path)
434
435                 c = _create_node(path, cache)
436
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