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