luci-base: dispatcher.lua: factor out template class init into own function
[oweals/luci.git] / modules / luci-base / luasrc / dispatcher.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
3 -- Licensed to the public under the Apache License 2.0.
4
5 local fs = require "nixio.fs"
6 local sys = require "luci.sys"
7 local util = require "luci.util"
8 local http = require "luci.http"
9 local nixio = require "nixio", require "nixio.util"
10
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
15 _M.fs = fs
16
17 -- Index table
18 local index = nil
19
20 -- Fastindex
21 local fi
22
23
24 local function check_fs_depends(fs)
25         local fs = require "nixio.fs"
26
27         for path, kind in pairs(fs) do
28                 if kind == "directory" then
29                         local empty = true
30                         for entry in (fs.dir(path) or function() end) do
31                                 empty = false
32                                 break
33                         end
34                         if empty then
35                                 return false
36                         end
37                 elseif kind == "executable" then
38                         if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then
39                                 return false
40                         end
41                 elseif kind == "file" then
42                         if fs.stat(path, "type") ~= "reg" then
43                                 return false
44                         end
45                 end
46         end
47
48         return true
49 end
50
51 local function check_uci_depends_options(conf, s, opts)
52         local uci = require "luci.model.uci"
53
54         if type(opts) == "string" then
55                 return (s[".type"] == opts)
56         elseif opts == true then
57                 for option, value in pairs(s) do
58                         if option:byte(1) ~= 46 then
59                                 return true
60                         end
61                 end
62         elseif type(opts) == "table" then
63                 for option, value in pairs(opts) do
64                         local sval = s[option]
65                         if type(sval) == "table" then
66                                 local found = false
67                                 for _, v in ipairs(sval) do
68                                         if v == value then
69                                                 found = true
70                                                 break
71                                         end
72                                 end
73                                 if not found then
74                                         return false
75                                 end
76                         elseif value == true then
77                                 if sval == nil then
78                                         return false
79                                 end
80                         else
81                                 if sval ~= value then
82                                         return false
83                                 end
84                         end
85                 end
86         end
87
88         return true
89 end
90
91 local function check_uci_depends_section(conf, sect)
92         local uci = require "luci.model.uci"
93
94         for section, options in pairs(sect) do
95                 local stype = section:match("^@([A-Za-z0-9_%-]+)$")
96                 if stype then
97                         local found = false
98                         uci:foreach(conf, stype, function(s)
99                                 if check_uci_depends_options(conf, s, options) then
100                                         found = true
101                                         return false
102                                 end
103                         end)
104                         if not found then
105                                 return false
106                         end
107                 else
108                         local s = uci:get_all(conf, section)
109                         if not s or not check_uci_depends_options(conf, s, options) then
110                                 return false
111                         end
112                 end
113         end
114
115         return true
116 end
117
118 local function check_uci_depends(conf)
119         local uci = require "luci.model.uci"
120
121         for config, values in pairs(conf) do
122                 if values == true then
123                         local found = false
124                         uci:foreach(config, nil, function(s)
125                                 found = true
126                                 return false
127                         end)
128                         if not found then
129                                 return false
130                         end
131                 elseif type(values) == "table" then
132                         if not check_uci_depends_section(config, values) then
133                                 return false
134                         end
135                 end
136         end
137
138         return true
139 end
140
141 local function check_depends(spec)
142         if type(spec.depends) ~= "table" then
143                 return true
144         end
145
146         if type(spec.depends.fs) == "table" and not check_fs_depends(spec.depends.fs) then
147                 local satisfied = false
148                 local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs }
149                 for _, alternative in ipairs(alternatives) do
150                         if check_fs_depends(alternative) then
151                                 satisfied = true
152                                 break
153                         end
154                 end
155                 if not satisfied then
156                         return false
157                 end
158         end
159
160         if type(spec.depends.uci) == "table" then
161                 local satisfied = false
162                 local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci }
163                 for _, alternative in ipairs(alternatives) do
164                         if check_uci_depends(alternative) then
165                                 satisfied = true
166                                 break
167                         end
168                 end
169                 if not satisfied then
170                         return false
171                 end
172         end
173
174         return true
175 end
176
177 local function target_to_json(target, module)
178         local action
179
180         if target.type == "call" then
181                 action = {
182                         ["type"] = "call",
183                         ["module"] = module,
184                         ["function"] = target.name,
185                         ["parameters"] = target.argv
186                 }
187         elseif target.type == "view" then
188                 action = {
189                         ["type"] = "view",
190                         ["path"] = target.view
191                 }
192         elseif target.type == "template" then
193                 action = {
194                         ["type"] = "template",
195                         ["path"] = target.view
196                 }
197         elseif target.type == "cbi" then
198                 action = {
199                         ["type"] = "cbi",
200                         ["path"] = target.model
201                 }
202         elseif target.type == "form" then
203                 action = {
204                         ["type"] = "form",
205                         ["path"] = target.model
206                 }
207         elseif target.type == "firstchild" then
208                 action = {
209                         ["type"] = "firstchild"
210                 }
211         elseif target.type == "firstnode" then
212                 action = {
213                         ["type"] = "firstchild",
214                         ["recurse"] = true
215                 }
216         elseif target.type == "arcombine" then
217                 if type(target.targets) == "table" then
218                         action = {
219                                 ["type"] = "arcombine",
220                                 ["targets"] = {
221                                         target_to_json(target.targets[1], module),
222                                         target_to_json(target.targets[2], module)
223                                 }
224                         }
225                 end
226         elseif target.type == "alias" then
227                 action = {
228                         ["type"] = "alias",
229                         ["path"] = table.concat(target.req, "/")
230                 }
231         elseif target.type == "rewrite" then
232                 action = {
233                         ["type"] = "rewrite",
234                         ["path"] = table.concat(target.req, "/"),
235                         ["remove"] = target.n
236                 }
237         end
238
239         if target.post and action then
240                 action.post = target.post
241         end
242
243         return action
244 end
245
246 local function tree_to_json(node, json)
247         local fs = require "nixio.fs"
248         local util = require "luci.util"
249
250         if type(node.nodes) == "table" then
251                 for subname, subnode in pairs(node.nodes) do
252                         local spec = {
253                                 title = util.striptags(subnode.title),
254                                 order = subnode.order
255                         }
256
257                         if subnode.leaf then
258                                 spec.wildcard = true
259                         end
260
261                         if subnode.cors then
262                                 spec.cors = true
263                         end
264
265                         if subnode.setuser then
266                                 spec.setuser = subnode.setuser
267                         end
268
269                         if subnode.setgroup then
270                                 spec.setgroup = subnode.setgroup
271                         end
272
273                         if type(subnode.target) == "table" then
274                                 spec.action = target_to_json(subnode.target, subnode.module)
275                         end
276
277                         if type(subnode.file_depends) == "table" then
278                                 for _, v in ipairs(subnode.file_depends) do
279                                         spec.depends = spec.depends or {}
280                                         spec.depends.fs = spec.depends.fs or {}
281
282                                         local ft = fs.stat(v, "type")
283                                         if ft == "dir" then
284                                                 spec.depends.fs[v] = "directory"
285                                         elseif v:match("/s?bin/") then
286                                                 spec.depends.fs[v] = "executable"
287                                         else
288                                                 spec.depends.fs[v] = "file"
289                                         end
290                                 end
291                         end
292
293                         if type(subnode.uci_depends) == "table" then
294                                 for k, v in pairs(subnode.uci_depends) do
295                                         spec.depends = spec.depends or {}
296                                         spec.depends.uci = spec.depends.uci or {}
297                                         spec.depends.uci[k] = v
298                                 end
299                         end
300
301                         if (subnode.sysauth_authenticator ~= nil) or
302                            (subnode.sysauth ~= nil and subnode.sysauth ~= false)
303                         then
304                                 if subnode.sysauth_authenticator == "htmlauth" then
305                                         spec.auth = {
306                                                 login = true,
307                                                 methods = { "cookie:sysauth" }
308                                         }
309                                 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
310                                         spec.auth = {
311                                                 login = false,
312                                                 methods = { "param:auth", "cookie:sysauth" }
313                                         }
314                                 elseif subnode.module == "luci.controller.admin.uci" then
315                                         spec.auth = {
316                                                 login = false,
317                                                 methods = { "param:sid" }
318                                         }
319                                 end
320                         elseif subnode.sysauth == false then
321                                 spec.auth = {}
322                         end
323
324                         for _, v in pairs(spec) do
325                                 if v ~= nil then
326                                         if not spec.action then
327                                                 spec.title = nil
328                                         end
329
330                                         spec.satisfied = check_depends(spec)
331                                         json.children = json.children or {}
332                                         json.children[subname] = tree_to_json(subnode, spec)
333                                         break
334                                 end
335                         end
336                 end
337         end
338
339         return json
340 end
341
342 function build_url(...)
343         local path = {...}
344         local url = { http.getenv("SCRIPT_NAME") or "" }
345
346         local p
347         for _, p in ipairs(path) do
348                 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
349                         url[#url+1] = "/"
350                         url[#url+1] = p
351                 end
352         end
353
354         if #path == 0 then
355                 url[#url+1] = "/"
356         end
357
358         return table.concat(url, "")
359 end
360
361 function _ordered_children(node)
362         local name, child, children = nil, nil, {}
363
364         for name, child in pairs(node.nodes) do
365                 children[#children+1] = {
366                         name  = name,
367                         node  = child,
368                         order = child.order or 100
369                 }
370         end
371
372         table.sort(children, function(a, b)
373                 if a.order == b.order then
374                         return a.name < b.name
375                 else
376                         return a.order < b.order
377                 end
378         end)
379
380         return children
381 end
382
383 local function dependencies_satisfied(node)
384         if type(node.file_depends) == "table" then
385                 for _, file in ipairs(node.file_depends) do
386                         local ftype = fs.stat(file, "type")
387                         if ftype == "dir" then
388                                 local empty = true
389                                 for e in (fs.dir(file) or function() end) do
390                                         empty = false
391                                 end
392                                 if empty then
393                                         return false
394                                 end
395                         elseif ftype == nil then
396                                 return false
397                         end
398                 end
399         end
400
401         if type(node.uci_depends) == "table" then
402                 for config, expect_sections in pairs(node.uci_depends) do
403                         if type(expect_sections) == "table" then
404                                 for section, expect_options in pairs(expect_sections) do
405                                         if type(expect_options) == "table" then
406                                                 for option, expect_value in pairs(expect_options) do
407                                                         local val = uci:get(config, section, option)
408                                                         if expect_value == true and val == nil then
409                                                                 return false
410                                                         elseif type(expect_value) == "string" then
411                                                                 if type(val) == "table" then
412                                                                         local found = false
413                                                                         for _, subval in ipairs(val) do
414                                                                                 if subval == expect_value then
415                                                                                         found = true
416                                                                                 end
417                                                                         end
418                                                                         if not found then
419                                                                                 return false
420                                                                         end
421                                                                 elseif val ~= expect_value then
422                                                                         return false
423                                                                 end
424                                                         end
425                                                 end
426                                         else
427                                                 local val = uci:get(config, section)
428                                                 if expect_options == true and val == nil then
429                                                         return false
430                                                 elseif type(expect_options) == "string" and val ~= expect_options then
431                                                         return false
432                                                 end
433                                         end
434                                 end
435                         elseif expect_sections == true then
436                                 if not uci:get_first(config) then
437                                         return false
438                                 end
439                         end
440                 end
441         end
442
443         return true
444 end
445
446 function node_visible(node)
447    if node then
448           return not (
449                  (not dependencies_satisfied(node)) or
450                  (not node.title or #node.title == 0) or
451                  (not node.target or node.hidden == true) or
452                  (type(node.target) == "table" and node.target.type == "firstchild" and
453                   (type(node.nodes) ~= "table" or not next(node.nodes)))
454           )
455    end
456    return false
457 end
458
459 function node_childs(node)
460         local rv = { }
461         if node then
462                 local _, child
463                 for _, child in ipairs(_ordered_children(node)) do
464                         if node_visible(child.node) then
465                                 rv[#rv+1] = child.name
466                         end
467                 end
468         end
469         return rv
470 end
471
472
473 function error404(message)
474         http.status(404, "Not Found")
475         message = message or "Not Found"
476
477         local function render()
478                 local template = require "luci.template"
479                 template.render("error404")
480         end
481
482         if not util.copcall(render) then
483                 http.prepare_content("text/plain")
484                 http.write(message)
485         end
486
487         return false
488 end
489
490 function error500(message)
491         util.perror(message)
492         if not context.template_header_sent then
493                 http.status(500, "Internal Server Error")
494                 http.prepare_content("text/plain")
495                 http.write(message)
496         else
497                 require("luci.template")
498                 if not util.copcall(luci.template.render, "error500", {message=message}) then
499                         http.prepare_content("text/plain")
500                         http.write(message)
501                 end
502         end
503         return false
504 end
505
506 local function determine_request_language()
507         local conf = require "luci.config"
508         assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'")
509
510         local lang = conf.main.lang or "auto"
511         if lang == "auto" then
512                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
513                 for aclang in aclang:gmatch("[%w_-]+") do
514                         local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
515                         if country and culture then
516                                 local cc = "%s_%s" %{ country, culture:lower() }
517                                 if conf.languages[cc] then
518                                         lang = cc
519                                         break
520                                 elseif conf.languages[country] then
521                                         lang = country
522                                         break
523                                 end
524                         elseif conf.languages[aclang] then
525                                 lang = aclang
526                                 break
527                         end
528                 end
529         end
530
531         if lang == "auto" then
532                 lang = i18n.default
533         end
534
535         i18n.setlanguage(lang)
536 end
537
538 function httpdispatch(request, prefix)
539         http.context.request = request
540
541         local r = {}
542         context.request = r
543
544         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
545
546         if prefix then
547                 for _, node in ipairs(prefix) do
548                         r[#r+1] = node
549                 end
550         end
551
552         local node
553         for node in pathinfo:gmatch("[^/%z]+") do
554                 r[#r+1] = node
555         end
556
557         determine_request_language()
558
559         local stat, err = util.coxpcall(function()
560                 dispatch(context.request)
561         end, error500)
562
563         http.close()
564
565         --context._disable_memtrace()
566 end
567
568 local function require_post_security(target, args)
569         if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
570                 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
571         end
572
573         if type(target) == "table" then
574                 if type(target.post) == "table" then
575                         local param_name, required_val, request_val
576
577                         for param_name, required_val in pairs(target.post) do
578                                 request_val = http.formvalue(param_name)
579
580                                 if (type(required_val) == "string" and
581                                     request_val ~= required_val) or
582                                    (required_val == true and request_val == nil)
583                                 then
584                                         return false
585                                 end
586                         end
587
588                         return true
589                 end
590
591                 return (target.post == true)
592         end
593
594         return false
595 end
596
597 function test_post_security()
598         if http.getenv("REQUEST_METHOD") ~= "POST" then
599                 http.status(405, "Method Not Allowed")
600                 http.header("Allow", "POST")
601                 return false
602         end
603
604         if http.formvalue("token") ~= context.authtoken then
605                 http.status(403, "Forbidden")
606                 luci.template.render("csrftoken")
607                 return false
608         end
609
610         return true
611 end
612
613 local function session_retrieve(sid, allowed_users)
614         local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
615
616         if type(sdat) == "table" and
617            type(sdat.values) == "table" and
618            type(sdat.values.token) == "string" and
619            (not allowed_users or
620             util.contains(allowed_users, sdat.values.username))
621         then
622                 uci:set_session_id(sid)
623                 return sid, sdat.values
624         end
625
626         return nil, nil
627 end
628
629 local function session_setup(user, pass, allowed_users)
630         if util.contains(allowed_users, user) then
631                 local login = util.ubus("session", "login", {
632                         username = user,
633                         password = pass,
634                         timeout  = tonumber(luci.config.sauth.sessiontime)
635                 })
636
637                 local rp = context.requestpath
638                         and table.concat(context.requestpath, "/") or ""
639
640                 if type(login) == "table" and
641                    type(login.ubus_rpc_session) == "string"
642                 then
643                         util.ubus("session", "set", {
644                                 ubus_rpc_session = login.ubus_rpc_session,
645                                 values = { token = sys.uniqueid(16) }
646                         })
647
648                         io.stderr:write("luci: accepted login on /%s for %s from %s\n"
649                                 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
650
651                         return session_retrieve(login.ubus_rpc_session)
652                 end
653
654                 io.stderr:write("luci: failed login on /%s for %s from %s\n"
655                         %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
656         end
657
658         return nil, nil
659 end
660
661 function menu_json()
662         local tree = context.tree or createtree()
663         return tree_to_json(tree, {
664                 action = {
665                         ["type"] = "firstchild",
666                         ["recurse"] = true
667                 }
668         })
669 end
670
671 local function init_template_engine(ctx)
672         local tpl = require "luci.template"
673         local media = luci.config.main.mediaurlbase
674
675         if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
676                 media = nil
677                 for name, theme in pairs(luci.config.themes) do
678                         if name:sub(1,1) ~= "." and pcall(tpl.Template,
679                          "themes/%s/header" % fs.basename(theme)) then
680                                 media = theme
681                         end
682                 end
683                 assert(media, "No valid theme found")
684         end
685
686         local function _ifattr(cond, key, val, noescape)
687                 if cond then
688                         local env = getfenv(3)
689                         local scope = (type(env.self) == "table") and env.self
690                         if type(val) == "table" then
691                                 if not next(val) then
692                                         return ''
693                                 else
694                                         val = util.serialize_json(val)
695                                 end
696                         end
697
698                         val = tostring(val or
699                                 (type(env[key]) ~= "function" and env[key]) or
700                                 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
701
702                         if noescape ~= true then
703                                 val = util.pcdata(val)
704                         end
705
706                         return string.format(' %s="%s"', tostring(key), val)
707                 else
708                         return ''
709                 end
710         end
711
712         tpl.context.viewns = setmetatable({
713                 write       = http.write;
714                 include     = function(name) tpl.Template(name):render(getfenv(2)) end;
715                 translate   = i18n.translate;
716                 translatef  = i18n.translatef;
717                 export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
718                 striptags   = util.striptags;
719                 pcdata      = util.pcdata;
720                 media       = media;
721                 theme       = fs.basename(media);
722                 resource    = luci.config.main.resourcebase;
723                 ifattr      = function(...) return _ifattr(...) end;
724                 attr        = function(...) return _ifattr(true, ...) end;
725                 url         = build_url;
726         }, {__index=function(tbl, key)
727                 if key == "controller" then
728                         return build_url()
729                 elseif key == "REQUEST_URI" then
730                         return build_url(unpack(ctx.requestpath))
731                 elseif key == "FULL_REQUEST_URI" then
732                         local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
733                         local query = http.getenv("QUERY_STRING")
734                         if query and #query > 0 then
735                                 url[#url+1] = "?"
736                                 url[#url+1] = query
737                         end
738                         return table.concat(url, "")
739                 elseif key == "token" then
740                         return ctx.authtoken
741                 else
742                         return rawget(tbl, key) or _G[key]
743                 end
744         end})
745
746         return tpl
747 end
748
749 function dispatch(request)
750         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
751         local ctx = context
752         ctx.path = request
753
754         local c = ctx.tree
755         local stat
756         if not c then
757                 c = createtree()
758         end
759
760         local track = {}
761         local args = {}
762         ctx.args = args
763         ctx.requestargs = ctx.requestargs or args
764         local n
765         local preq = {}
766         local freq = {}
767
768         for i, s in ipairs(request) do
769                 preq[#preq+1] = s
770                 freq[#freq+1] = s
771                 c = c.nodes[s]
772                 n = i
773                 if not c then
774                         break
775                 end
776
777                 util.update(track, c)
778
779                 if c.leaf then
780                         break
781                 end
782         end
783
784         if c and c.leaf then
785                 for j=n+1, #request do
786                         args[#args+1] = request[j]
787                         freq[#freq+1] = request[j]
788                 end
789         end
790
791         ctx.requestpath = ctx.requestpath or freq
792         ctx.path = preq
793
794         -- Init template engine
795         if (c and c.index) or not track.notemplate then
796                 init_template_engine(ctx)
797         end
798
799         track.dependent = (track.dependent ~= false)
800         assert(not track.dependent or not track.auto,
801                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
802                 "has no parent node so the access to this location has been denied.\n" ..
803                 "This is a software bug, please report this message at " ..
804                 "https://github.com/openwrt/luci/issues"
805         )
806
807         if track.sysauth and not ctx.authsession then
808                 local authen = track.sysauth_authenticator
809                 local _, sid, sdat, default_user, allowed_users
810
811                 if type(authen) == "string" and authen ~= "htmlauth" then
812                         error500("Unsupported authenticator %q configured" % authen)
813                         return
814                 end
815
816                 if type(track.sysauth) == "table" then
817                         default_user, allowed_users = nil, track.sysauth
818                 else
819                         default_user, allowed_users = track.sysauth, { track.sysauth }
820                 end
821
822                 if type(authen) == "function" then
823                         _, sid = authen(sys.user.checkpasswd, allowed_users)
824                 else
825                         sid = http.getcookie("sysauth")
826                 end
827
828                 sid, sdat = session_retrieve(sid, allowed_users)
829
830                 if not (sid and sdat) and authen == "htmlauth" then
831                         local user = http.getenv("HTTP_AUTH_USER")
832                         local pass = http.getenv("HTTP_AUTH_PASS")
833
834                         if user == nil and pass == nil then
835                                 user = http.formvalue("luci_username")
836                                 pass = http.formvalue("luci_password")
837                         end
838
839                         sid, sdat = session_setup(user, pass, allowed_users)
840
841                         if not sid then
842                                 local tmpl = require "luci.template"
843
844                                 context.path = {}
845
846                                 http.status(403, "Forbidden")
847                                 http.header("X-LuCI-Login-Required", "yes")
848                                 tmpl.render(track.sysauth_template or "sysauth", {
849                                         duser = default_user,
850                                         fuser = user
851                                 })
852
853                                 return
854                         end
855
856                         http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
857                                 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
858                         })
859                         http.redirect(build_url(unpack(ctx.requestpath)))
860                 end
861
862                 if not sid or not sdat then
863                         http.status(403, "Forbidden")
864                         http.header("X-LuCI-Login-Required", "yes")
865                         return
866                 end
867
868                 ctx.authsession = sid
869                 ctx.authtoken = sdat.token
870                 ctx.authuser = sdat.username
871         end
872
873         if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
874                 luci.http.status(200, "OK")
875                 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
876                 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
877                 return
878         end
879
880         if c and require_post_security(c.target, args) then
881                 if not test_post_security(c) then
882                         return
883                 end
884         end
885
886         if track.setgroup then
887                 sys.process.setgroup(track.setgroup)
888         end
889
890         if track.setuser then
891                 sys.process.setuser(track.setuser)
892         end
893
894         local target = nil
895         if c then
896                 if type(c.target) == "function" then
897                         target = c.target
898                 elseif type(c.target) == "table" then
899                         target = c.target.target
900                 end
901         end
902
903         if c and (c.index or type(target) == "function") then
904                 ctx.dispatched = c
905                 ctx.requested = ctx.requested or ctx.dispatched
906         end
907
908         if c and c.index then
909                 local tpl = require "luci.template"
910
911                 if util.copcall(tpl.render, "indexer", {}) then
912                         return true
913                 end
914         end
915
916         if type(target) == "function" then
917                 util.copcall(function()
918                         local oldenv = getfenv(target)
919                         local module = require(c.module)
920                         local env = setmetatable({}, {__index=
921
922                         function(tbl, key)
923                                 return rawget(tbl, key) or module[key] or oldenv[key]
924                         end})
925
926                         setfenv(target, env)
927                 end)
928
929                 local ok, err
930                 if type(c.target) == "table" then
931                         ok, err = util.copcall(target, c.target, unpack(args))
932                 else
933                         ok, err = util.copcall(target, unpack(args))
934                 end
935                 if not ok then
936                         error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
937                                  " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
938                                  "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
939                 end
940         else
941                 local root = node()
942                 if not root or not root.target then
943                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
944                                  "Install luci-mod-admin-full and retry. " ..
945                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
946                 else
947                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
948                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
949                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
950                 end
951         end
952 end
953
954 function createindex()
955         local controllers = { }
956         local base = "%s/controller/" % util.libpath()
957         local _, path
958
959         for path in (fs.glob("%s*.lua" % base) or function() end) do
960                 controllers[#controllers+1] = path
961         end
962
963         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
964                 controllers[#controllers+1] = path
965         end
966
967         if indexcache then
968                 local cachedate = fs.stat(indexcache, "mtime")
969                 if cachedate then
970                         local realdate = 0
971                         for _, obj in ipairs(controllers) do
972                                 local omtime = fs.stat(obj, "mtime")
973                                 realdate = (omtime and omtime > realdate) and omtime or realdate
974                         end
975
976                         if cachedate > realdate and sys.process.info("uid") == 0 then
977                                 assert(
978                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
979                                         and fs.stat(indexcache, "modestr") == "rw-------",
980                                         "Fatal: Indexcache is not sane!"
981                                 )
982
983                                 index = loadfile(indexcache)()
984                                 return index
985                         end
986                 end
987         end
988
989         index = {}
990
991         for _, path in ipairs(controllers) do
992                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
993                 local mod = require(modname)
994                 assert(mod ~= true,
995                        "Invalid controller file found\n" ..
996                        "The file '" .. path .. "' contains an invalid module line.\n" ..
997                        "Please verify whether the module name is set to '" .. modname ..
998                        "' - It must correspond to the file path!")
999
1000                 local idx = mod.index
1001                 assert(type(idx) == "function",
1002                        "Invalid controller file found\n" ..
1003                        "The file '" .. path .. "' contains no index() function.\n" ..
1004                        "Please make sure that the controller contains a valid " ..
1005                        "index function and verify the spelling!")
1006
1007                 index[modname] = idx
1008         end
1009
1010         if indexcache then
1011                 local f = nixio.open(indexcache, "w", 600)
1012                 f:writeall(util.get_bytecode(index))
1013                 f:close()
1014         end
1015 end
1016
1017 -- Build the index before if it does not exist yet.
1018 function createtree()
1019         if not index then
1020                 createindex()
1021         end
1022
1023         local ctx  = context
1024         local tree = {nodes={}, inreq=true}
1025
1026         ctx.treecache = setmetatable({}, {__mode="v"})
1027         ctx.tree = tree
1028
1029         local scope = setmetatable({}, {__index = luci.dispatcher})
1030
1031         for k, v in pairs(index) do
1032                 scope._NAME = k
1033                 setfenv(v, scope)
1034                 v()
1035         end
1036
1037         return tree
1038 end
1039
1040 function assign(path, clone, title, order)
1041         local obj  = node(unpack(path))
1042         obj.nodes  = nil
1043         obj.module = nil
1044
1045         obj.title = title
1046         obj.order = order
1047
1048         setmetatable(obj, {__index = _create_node(clone)})
1049
1050         return obj
1051 end
1052
1053 function entry(path, target, title, order)
1054         local c = node(unpack(path))
1055
1056         c.target = target
1057         c.title  = title
1058         c.order  = order
1059         c.module = getfenv(2)._NAME
1060
1061         return c
1062 end
1063
1064 -- enabling the node.
1065 function get(...)
1066         return _create_node({...})
1067 end
1068
1069 function node(...)
1070         local c = _create_node({...})
1071
1072         c.module = getfenv(2)._NAME
1073         c.auto = nil
1074
1075         return c
1076 end
1077
1078 function lookup(...)
1079         local i, path = nil, {}
1080         for i = 1, select('#', ...) do
1081                 local name, arg = nil, tostring(select(i, ...))
1082                 for name in arg:gmatch("[^/]+") do
1083                         path[#path+1] = name
1084                 end
1085         end
1086
1087         for i = #path, 1, -1 do
1088                 local node = context.treecache[table.concat(path, ".", 1, i)]
1089                 if node and (i == #path or node.leaf) then
1090                         return node, build_url(unpack(path))
1091                 end
1092         end
1093 end
1094
1095 function _create_node(path)
1096         if #path == 0 then
1097                 return context.tree
1098         end
1099
1100         local name = table.concat(path, ".")
1101         local c = context.treecache[name]
1102
1103         if not c then
1104                 local last = table.remove(path)
1105                 local parent = _create_node(path)
1106
1107                 c = {nodes={}, auto=true, inreq=true}
1108
1109                 local _, n
1110                 for _, n in ipairs(path) do
1111                         if context.path[_] ~= n then
1112                                 c.inreq = false
1113                                 break
1114                         end
1115                 end
1116
1117                 c.inreq = c.inreq and (context.path[#path + 1] == last)
1118
1119                 parent.nodes[last] = c
1120                 context.treecache[name] = c
1121         end
1122
1123         return c
1124 end
1125
1126 -- Subdispatchers --
1127
1128 function _find_eligible_node(root, prefix, deep, types, descend)
1129         local children = _ordered_children(root)
1130
1131         if not root.leaf and deep ~= nil then
1132                 local sub_path = { unpack(prefix) }
1133
1134                 if deep == false then
1135                         deep = nil
1136                 end
1137
1138                 local _, child
1139                 for _, child in ipairs(children) do
1140                         sub_path[#prefix+1] = child.name
1141
1142                         local res_path = _find_eligible_node(child.node, sub_path,
1143                                                              deep, types, true)
1144
1145                         if res_path then
1146                                 return res_path
1147                         end
1148                 end
1149         end
1150
1151         if descend and
1152            (not types or
1153             (type(root.target) == "table" and
1154              util.contains(types, root.target.type)))
1155         then
1156                 return prefix
1157         end
1158 end
1159
1160 function _find_node(recurse, types)
1161         local path = { unpack(context.path) }
1162         local name = table.concat(path, ".")
1163         local node = context.treecache[name]
1164
1165         path = _find_eligible_node(node, path, recurse, types)
1166
1167         if path then
1168                 dispatch(path)
1169         else
1170                 require "luci.template".render("empty_node_placeholder")
1171         end
1172 end
1173
1174 function _firstchild()
1175         return _find_node(false, nil)
1176 end
1177
1178 function firstchild()
1179         return { type = "firstchild", target = _firstchild }
1180 end
1181
1182 function _firstnode()
1183         return _find_node(true, { "cbi", "form", "template", "arcombine" })
1184 end
1185
1186 function firstnode()
1187         return { type = "firstnode", target = _firstnode }
1188 end
1189
1190 function _alias(self, ...)
1191         local req = { unpack(self.req) }
1192
1193         for _, r in ipairs({...}) do
1194                 req[#req+1] = r
1195         end
1196
1197         dispatch(req)
1198 end
1199
1200 function alias(...)
1201         return { type = "alias", target = _alias, req = { ... } }
1202 end
1203
1204 function _rewrite(self, ...)
1205         local n = self.n
1206         local req = { unpack(self.req) }
1207         local dispatched = util.clone(context.dispatched)
1208
1209         for i=1,n do
1210                 table.remove(dispatched, 1)
1211         end
1212
1213         for i, r in ipairs(req) do
1214                 table.insert(dispatched, i, r)
1215         end
1216
1217         for _, r in ipairs({...}) do
1218                 dispatched[#dispatched+1] = r
1219         end
1220
1221         dispatch(dispatched)
1222 end
1223
1224 function rewrite(n, ...)
1225         return { type = "rewrite", target = _rewrite, n = n, req = { ... } }
1226 end
1227
1228 local function _call(self, ...)
1229         local func = getfenv()[self.name]
1230         assert(func ~= nil,
1231                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
1232
1233         assert(type(func) == "function",
1234                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
1235                'of type "' .. type(func) .. '".')
1236
1237         if #self.argv > 0 then
1238                 return func(unpack(self.argv), ...)
1239         else
1240                 return func(...)
1241         end
1242 end
1243
1244 function call(name, ...)
1245         return {type = "call", argv = {...}, name = name, target = _call}
1246 end
1247
1248 function post_on(params, name, ...)
1249         return {
1250                 type = "call",
1251                 post = params,
1252                 argv = { ... },
1253                 name = name,
1254                 target = _call
1255         }
1256 end
1257
1258 function post(...)
1259         return post_on(true, ...)
1260 end
1261
1262
1263 local _template = function(self, ...)
1264         require "luci.template".render(self.view)
1265 end
1266
1267 function template(name)
1268         return {type = "template", view = name, target = _template}
1269 end
1270
1271
1272 local _view = function(self, ...)
1273         require "luci.template".render("view", { view = self.view })
1274 end
1275
1276 function view(name)
1277         return {type = "view", view = name, target = _view}
1278 end
1279
1280
1281 local function _cbi(self, ...)
1282         local cbi = require "luci.cbi"
1283         local tpl = require "luci.template"
1284         local http = require "luci.http"
1285
1286         local config = self.config or {}
1287         local maps = cbi.load(self.model, ...)
1288
1289         local state = nil
1290
1291         local i, res
1292         for i, res in ipairs(maps) do
1293                 if util.instanceof(res, cbi.SimpleForm) then
1294                         io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1295                                 % self.model)
1296
1297                         io.stderr:write("please change %s to use the form() action instead.\n"
1298                                 % table.concat(context.request, "/"))
1299                 end
1300
1301                 res.flow = config
1302                 local cstate = res:parse()
1303                 if cstate and (not state or cstate < state) then
1304                         state = cstate
1305                 end
1306         end
1307
1308         local function _resolve_path(path)
1309                 return type(path) == "table" and build_url(unpack(path)) or path
1310         end
1311
1312         if config.on_valid_to and state and state > 0 and state < 2 then
1313                 http.redirect(_resolve_path(config.on_valid_to))
1314                 return
1315         end
1316
1317         if config.on_changed_to and state and state > 1 then
1318                 http.redirect(_resolve_path(config.on_changed_to))
1319                 return
1320         end
1321
1322         if config.on_success_to and state and state > 0 then
1323                 http.redirect(_resolve_path(config.on_success_to))
1324                 return
1325         end
1326
1327         if config.state_handler then
1328                 if not config.state_handler(state, maps) then
1329                         return
1330                 end
1331         end
1332
1333         http.header("X-CBI-State", state or 0)
1334
1335         if not config.noheader then
1336                 tpl.render("cbi/header", {state = state})
1337         end
1338
1339         local redirect
1340         local messages
1341         local applymap   = false
1342         local pageaction = true
1343         local parsechain = { }
1344
1345         for i, res in ipairs(maps) do
1346                 if res.apply_needed and res.parsechain then
1347                         local c
1348                         for _, c in ipairs(res.parsechain) do
1349                                 parsechain[#parsechain+1] = c
1350                         end
1351                         applymap = true
1352                 end
1353
1354                 if res.redirect then
1355                         redirect = redirect or res.redirect
1356                 end
1357
1358                 if res.pageaction == false then
1359                         pageaction = false
1360                 end
1361
1362                 if res.message then
1363                         messages = messages or { }
1364                         messages[#messages+1] = res.message
1365                 end
1366         end
1367
1368         for i, res in ipairs(maps) do
1369                 res:render({
1370                         firstmap   = (i == 1),
1371                         redirect   = redirect,
1372                         messages   = messages,
1373                         pageaction = pageaction,
1374                         parsechain = parsechain
1375                 })
1376         end
1377
1378         if not config.nofooter then
1379                 tpl.render("cbi/footer", {
1380                         flow          = config,
1381                         pageaction    = pageaction,
1382                         redirect      = redirect,
1383                         state         = state,
1384                         autoapply     = config.autoapply,
1385                         trigger_apply = applymap
1386                 })
1387         end
1388 end
1389
1390 function cbi(model, config)
1391         return {
1392                 type = "cbi",
1393                 post = { ["cbi.submit"] = true },
1394                 config = config,
1395                 model = model,
1396                 target = _cbi
1397         }
1398 end
1399
1400
1401 local function _arcombine(self, ...)
1402         local argv = {...}
1403         local target = #argv > 0 and self.targets[2] or self.targets[1]
1404         setfenv(target.target, self.env)
1405         target:target(unpack(argv))
1406 end
1407
1408 function arcombine(trg1, trg2)
1409         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
1410 end
1411
1412
1413 local function _form(self, ...)
1414         local cbi = require "luci.cbi"
1415         local tpl = require "luci.template"
1416         local http = require "luci.http"
1417
1418         local maps = luci.cbi.load(self.model, ...)
1419         local state = nil
1420
1421         local i, res
1422         for i, res in ipairs(maps) do
1423                 local cstate = res:parse()
1424                 if cstate and (not state or cstate < state) then
1425                         state = cstate
1426                 end
1427         end
1428
1429         http.header("X-CBI-State", state or 0)
1430         tpl.render("header")
1431         for i, res in ipairs(maps) do
1432                 res:render()
1433         end
1434         tpl.render("footer")
1435 end
1436
1437 function form(model)
1438         return {
1439                 type = "form",
1440                 post = { ["cbi.submit"] = true },
1441                 model = model,
1442                 target = _form
1443         }
1444 end
1445
1446 translate = i18n.translate
1447
1448 -- This function does not actually translate the given argument but
1449 -- is used by build/i18n-scan.pl to find translatable entries.
1450 function _(text)
1451         return text
1452 end