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