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