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