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