libs/web: reformat code in dispatcher.lua that confuses luadoc
[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
395                         = getfenv(2)._NAME
396         }
397 end
398
399 --- Clone a node of the dispatching tree to another position.
400 -- @param       path    Virtual path destination
401 -- @param       clone   Virtual path source
402 -- @param       title   Destination node title (optional)
403 -- @param       order   Destination node order value (optional)
404 -- @return                      Dispatching tree node
405 function assign(path, clone, title, order)
406         local obj  = node(unpack(path))
407         obj.nodes  = nil
408         obj.module = nil
409
410         obj.title = title
411         obj.order = order
412
413         setmetatable(obj, {__index = _create_node(clone)})
414
415         return obj
416 end
417
418 --- Create a new dispatching node and define common parameters.
419 -- @param       path    Virtual path
420 -- @param       target  Target function to call when dispatched.
421 -- @param       title   Destination node title
422 -- @param       order   Destination node order value (optional)
423 -- @return                      Dispatching tree node
424 function entry(path, target, title, order)
425         local c = node(unpack(path))
426
427         c.target = target
428         c.title  = title
429         c.order  = order
430         c.module = getfenv(2)._NAME
431
432         return c
433 end
434
435 --- Fetch or create a new dispatching node.
436 -- @param       ...             Virtual path
437 -- @return                      Dispatching tree node
438 function node(...)
439         local c = _create_node({...})
440
441         c.module = getfenv(2)._NAME
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 new = {nodes={}, auto=true, path=util.clone(path)}
458                 local last = table.remove(path)
459
460                 c = _create_node(path, cache)
461
462                 c.nodes[last] = new
463                 cache[name] = new
464
465                 return new
466         else
467                 return c
468         end
469 end
470
471 -- Subdispatchers --
472
473 --- Create a redirect to another dispatching node.
474 -- @param       ...             Virtual path destination
475 function alias(...)
476         local req = {...}
477         return function(...)
478                 for _, r in ipairs({...}) do
479                         req[#req+1] = r
480                 end
481
482                 dispatch(req)
483         end
484 end
485
486 --- Rewrite the first x path values of the request.
487 -- @param       n               Number of path values to replace
488 -- @param       ...             Virtual path to replace removed path values with
489 function rewrite(n, ...)
490         local req = {...}
491         return function(...)
492                 local dispatched = util.clone(context.dispatched)
493
494                 for i=1,n do
495                         table.remove(dispatched, 1)
496                 end
497
498                 for i, r in ipairs(req) do
499                         table.insert(dispatched, i, r)
500                 end
501
502                 for _, r in ipairs({...}) do
503                         dispatched[#dispatched+1] = r
504                 end
505
506                 dispatch(dispatched)
507         end
508 end
509
510 --- Create a function-call dispatching target.
511 -- @param       name    Target function of local controller
512 -- @param       ...             Additional parameters passed to the function
513 function call(name, ...)
514         local argv = {...}
515         return function(...)
516                 if #argv > 0 then 
517                         return getfenv()[name](unpack(argv), ...)
518                 else
519                         return getfenv()[name](...)
520                 end
521         end
522 end
523
524 --- Create a template render dispatching target.
525 -- @param       name    Template to be rendered
526 function template(name)
527         return function()
528                 require("luci.template")
529                 luci.template.render(name)
530         end
531 end
532
533 --- Create a CBI model dispatching target.
534 -- @param       model   CBI model to be rendered
535 function cbi(model, config)
536         config = config or {}
537         return function(...)
538                 require("luci.cbi")
539                 require("luci.template")
540                 local http = require "luci.http"
541
542                 maps = luci.cbi.load(model, ...)
543
544                 local state = nil
545
546                 for i, res in ipairs(maps) do
547                         if config.autoapply then
548                                 res.autoapply = config.autoapply
549                         end
550                         local cstate = res:parse()
551                         if not state or cstate < state then
552                                 state = cstate
553                         end
554                 end
555
556                 if config.on_valid_to and state and state > 0 and state < 2 then
557                         luci.http.redirect(config.on_valid_to)
558                         return
559                 end
560
561                 if config.on_changed_to and state and state > 1 then
562                         luci.http.redirect(config.on_changed_to)
563                         return
564                 end
565
566                 if config.on_success_to and state and state > 0 then
567                         luci.http.redirect(config.on_success_to)
568                         return
569                 end
570
571                 if config.state_handler then
572                         if not config.state_handler(state, maps) then
573                                 return
574                         end
575                 end
576
577                 local pageaction = true
578                 http.header("X-CBI-State", state or 0)
579                 luci.template.render("cbi/header", {state = state})
580                 for i, res in ipairs(maps) do
581                         res:render()
582                         if res.pageaction == false then
583                                 pageaction = false
584                         end
585                 end
586                 luci.template.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
587         end
588 end
589
590 --- Create a CBI form model dispatching target.
591 -- @param       model   CBI form model tpo be rendered
592 function form(model)
593         return function(...)
594                 require("luci.cbi")
595                 require("luci.template")
596                 local http = require "luci.http"
597
598                 maps = luci.cbi.load(model, ...)
599
600                 local state = nil
601
602                 for i, res in ipairs(maps) do
603                         local cstate = res:parse()
604                         if not state or cstate < state then
605                                 state = cstate
606                         end
607                 end
608
609                 http.header("X-CBI-State", state or 0)
610                 luci.template.render("header")
611                 for i, res in ipairs(maps) do
612                         res:render()
613                 end
614                 luci.template.render("footer")
615         end
616 end