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