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