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