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