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