luci-base: dispatcher.lua: factor out language check 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 function dispatch(request)
672         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
673         local ctx = context
674         ctx.path = request
675
676         local c = ctx.tree
677         local stat
678         if not c then
679                 c = createtree()
680         end
681
682         local track = {}
683         local args = {}
684         ctx.args = args
685         ctx.requestargs = ctx.requestargs or args
686         local n
687         local preq = {}
688         local freq = {}
689
690         for i, s in ipairs(request) do
691                 preq[#preq+1] = s
692                 freq[#freq+1] = s
693                 c = c.nodes[s]
694                 n = i
695                 if not c then
696                         break
697                 end
698
699                 util.update(track, c)
700
701                 if c.leaf then
702                         break
703                 end
704         end
705
706         if c and c.leaf then
707                 for j=n+1, #request do
708                         args[#args+1] = request[j]
709                         freq[#freq+1] = request[j]
710                 end
711         end
712
713         ctx.requestpath = ctx.requestpath or freq
714         ctx.path = preq
715
716         -- Init template engine
717         if (c and c.index) or not track.notemplate then
718                 local tpl = require("luci.template")
719                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
720                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
721                         media = nil
722                         for name, theme in pairs(luci.config.themes) do
723                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
724                                  "themes/%s/header" % fs.basename(theme)) then
725                                         media = theme
726                                 end
727                         end
728                         assert(media, "No valid theme found")
729                 end
730
731                 local function _ifattr(cond, key, val, noescape)
732                         if cond then
733                                 local env = getfenv(3)
734                                 local scope = (type(env.self) == "table") and env.self
735                                 if type(val) == "table" then
736                                         if not next(val) then
737                                                 return ''
738                                         else
739                                                 val = util.serialize_json(val)
740                                         end
741                                 end
742
743                                 val = tostring(val or
744                                         (type(env[key]) ~= "function" and env[key]) or
745                                         (scope and type(scope[key]) ~= "function" and scope[key]) or "")
746
747                                 if noescape ~= true then
748                                         val = util.pcdata(val)
749                                 end
750
751                                 return string.format(' %s="%s"', tostring(key), val)
752                         else
753                                 return ''
754                         end
755                 end
756
757                 tpl.context.viewns = setmetatable({
758                    write       = http.write;
759                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
760                    translate   = i18n.translate;
761                    translatef  = i18n.translatef;
762                    export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
763                    striptags   = util.striptags;
764                    pcdata      = util.pcdata;
765                    media       = media;
766                    theme       = fs.basename(media);
767                    resource    = luci.config.main.resourcebase;
768                    ifattr      = function(...) return _ifattr(...) end;
769                    attr        = function(...) return _ifattr(true, ...) end;
770                    url         = build_url;
771                 }, {__index=function(tbl, key)
772                         if key == "controller" then
773                                 return build_url()
774                         elseif key == "REQUEST_URI" then
775                                 return build_url(unpack(ctx.requestpath))
776                         elseif key == "FULL_REQUEST_URI" then
777                                 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
778                                 local query = http.getenv("QUERY_STRING")
779                                 if query and #query > 0 then
780                                         url[#url+1] = "?"
781                                         url[#url+1] = query
782                                 end
783                                 return table.concat(url, "")
784                         elseif key == "token" then
785                                 return ctx.authtoken
786                         else
787                                 return rawget(tbl, key) or _G[key]
788                         end
789                 end})
790         end
791
792         track.dependent = (track.dependent ~= false)
793         assert(not track.dependent or not track.auto,
794                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
795                 "has no parent node so the access to this location has been denied.\n" ..
796                 "This is a software bug, please report this message at " ..
797                 "https://github.com/openwrt/luci/issues"
798         )
799
800         if track.sysauth and not ctx.authsession then
801                 local authen = track.sysauth_authenticator
802                 local _, sid, sdat, default_user, allowed_users
803
804                 if type(authen) == "string" and authen ~= "htmlauth" then
805                         error500("Unsupported authenticator %q configured" % authen)
806                         return
807                 end
808
809                 if type(track.sysauth) == "table" then
810                         default_user, allowed_users = nil, track.sysauth
811                 else
812                         default_user, allowed_users = track.sysauth, { track.sysauth }
813                 end
814
815                 if type(authen) == "function" then
816                         _, sid = authen(sys.user.checkpasswd, allowed_users)
817                 else
818                         sid = http.getcookie("sysauth")
819                 end
820
821                 sid, sdat = session_retrieve(sid, allowed_users)
822
823                 if not (sid and sdat) and authen == "htmlauth" then
824                         local user = http.getenv("HTTP_AUTH_USER")
825                         local pass = http.getenv("HTTP_AUTH_PASS")
826
827                         if user == nil and pass == nil then
828                                 user = http.formvalue("luci_username")
829                                 pass = http.formvalue("luci_password")
830                         end
831
832                         sid, sdat = session_setup(user, pass, allowed_users)
833
834                         if not sid then
835                                 local tmpl = require "luci.template"
836
837                                 context.path = {}
838
839                                 http.status(403, "Forbidden")
840                                 http.header("X-LuCI-Login-Required", "yes")
841                                 tmpl.render(track.sysauth_template or "sysauth", {
842                                         duser = default_user,
843                                         fuser = user
844                                 })
845
846                                 return
847                         end
848
849                         http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
850                                 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
851                         })
852                         http.redirect(build_url(unpack(ctx.requestpath)))
853                 end
854
855                 if not sid or not sdat then
856                         http.status(403, "Forbidden")
857                         http.header("X-LuCI-Login-Required", "yes")
858                         return
859                 end
860
861                 ctx.authsession = sid
862                 ctx.authtoken = sdat.token
863                 ctx.authuser = sdat.username
864         end
865
866         if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
867                 luci.http.status(200, "OK")
868                 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
869                 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
870                 return
871         end
872
873         if c and require_post_security(c.target, args) then
874                 if not test_post_security(c) then
875                         return
876                 end
877         end
878
879         if track.setgroup then
880                 sys.process.setgroup(track.setgroup)
881         end
882
883         if track.setuser then
884                 sys.process.setuser(track.setuser)
885         end
886
887         local target = nil
888         if c then
889                 if type(c.target) == "function" then
890                         target = c.target
891                 elseif type(c.target) == "table" then
892                         target = c.target.target
893                 end
894         end
895
896         if c and (c.index or type(target) == "function") then
897                 ctx.dispatched = c
898                 ctx.requested = ctx.requested or ctx.dispatched
899         end
900
901         if c and c.index then
902                 local tpl = require "luci.template"
903
904                 if util.copcall(tpl.render, "indexer", {}) then
905                         return true
906                 end
907         end
908
909         if type(target) == "function" then
910                 util.copcall(function()
911                         local oldenv = getfenv(target)
912                         local module = require(c.module)
913                         local env = setmetatable({}, {__index=
914
915                         function(tbl, key)
916                                 return rawget(tbl, key) or module[key] or oldenv[key]
917                         end})
918
919                         setfenv(target, env)
920                 end)
921
922                 local ok, err
923                 if type(c.target) == "table" then
924                         ok, err = util.copcall(target, c.target, unpack(args))
925                 else
926                         ok, err = util.copcall(target, unpack(args))
927                 end
928                 if not ok then
929                         error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
930                                  " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
931                                  "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
932                 end
933         else
934                 local root = node()
935                 if not root or not root.target then
936                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
937                                  "Install luci-mod-admin-full and retry. " ..
938                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
939                 else
940                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
941                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
942                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
943                 end
944         end
945 end
946
947 function createindex()
948         local controllers = { }
949         local base = "%s/controller/" % util.libpath()
950         local _, path
951
952         for path in (fs.glob("%s*.lua" % base) or function() end) do
953                 controllers[#controllers+1] = path
954         end
955
956         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
957                 controllers[#controllers+1] = path
958         end
959
960         if indexcache then
961                 local cachedate = fs.stat(indexcache, "mtime")
962                 if cachedate then
963                         local realdate = 0
964                         for _, obj in ipairs(controllers) do
965                                 local omtime = fs.stat(obj, "mtime")
966                                 realdate = (omtime and omtime > realdate) and omtime or realdate
967                         end
968
969                         if cachedate > realdate and sys.process.info("uid") == 0 then
970                                 assert(
971                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
972                                         and fs.stat(indexcache, "modestr") == "rw-------",
973                                         "Fatal: Indexcache is not sane!"
974                                 )
975
976                                 index = loadfile(indexcache)()
977                                 return index
978                         end
979                 end
980         end
981
982         index = {}
983
984         for _, path in ipairs(controllers) do
985                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
986                 local mod = require(modname)
987                 assert(mod ~= true,
988                        "Invalid controller file found\n" ..
989                        "The file '" .. path .. "' contains an invalid module line.\n" ..
990                        "Please verify whether the module name is set to '" .. modname ..
991                        "' - It must correspond to the file path!")
992
993                 local idx = mod.index
994                 assert(type(idx) == "function",
995                        "Invalid controller file found\n" ..
996                        "The file '" .. path .. "' contains no index() function.\n" ..
997                        "Please make sure that the controller contains a valid " ..
998                        "index function and verify the spelling!")
999
1000                 index[modname] = idx
1001         end
1002
1003         if indexcache then
1004                 local f = nixio.open(indexcache, "w", 600)
1005                 f:writeall(util.get_bytecode(index))
1006                 f:close()
1007         end
1008 end
1009
1010 -- Build the index before if it does not exist yet.
1011 function createtree()
1012         if not index then
1013                 createindex()
1014         end
1015
1016         local ctx  = context
1017         local tree = {nodes={}, inreq=true}
1018
1019         ctx.treecache = setmetatable({}, {__mode="v"})
1020         ctx.tree = tree
1021
1022         local scope = setmetatable({}, {__index = luci.dispatcher})
1023
1024         for k, v in pairs(index) do
1025                 scope._NAME = k
1026                 setfenv(v, scope)
1027                 v()
1028         end
1029
1030         return tree
1031 end
1032
1033 function assign(path, clone, title, order)
1034         local obj  = node(unpack(path))
1035         obj.nodes  = nil
1036         obj.module = nil
1037
1038         obj.title = title
1039         obj.order = order
1040
1041         setmetatable(obj, {__index = _create_node(clone)})
1042
1043         return obj
1044 end
1045
1046 function entry(path, target, title, order)
1047         local c = node(unpack(path))
1048
1049         c.target = target
1050         c.title  = title
1051         c.order  = order
1052         c.module = getfenv(2)._NAME
1053
1054         return c
1055 end
1056
1057 -- enabling the node.
1058 function get(...)
1059         return _create_node({...})
1060 end
1061
1062 function node(...)
1063         local c = _create_node({...})
1064
1065         c.module = getfenv(2)._NAME
1066         c.auto = nil
1067
1068         return c
1069 end
1070
1071 function lookup(...)
1072         local i, path = nil, {}
1073         for i = 1, select('#', ...) do
1074                 local name, arg = nil, tostring(select(i, ...))
1075                 for name in arg:gmatch("[^/]+") do
1076                         path[#path+1] = name
1077                 end
1078         end
1079
1080         for i = #path, 1, -1 do
1081                 local node = context.treecache[table.concat(path, ".", 1, i)]
1082                 if node and (i == #path or node.leaf) then
1083                         return node, build_url(unpack(path))
1084                 end
1085         end
1086 end
1087
1088 function _create_node(path)
1089         if #path == 0 then
1090                 return context.tree
1091         end
1092
1093         local name = table.concat(path, ".")
1094         local c = context.treecache[name]
1095
1096         if not c then
1097                 local last = table.remove(path)
1098                 local parent = _create_node(path)
1099
1100                 c = {nodes={}, auto=true, inreq=true}
1101
1102                 local _, n
1103                 for _, n in ipairs(path) do
1104                         if context.path[_] ~= n then
1105                                 c.inreq = false
1106                                 break
1107                         end
1108                 end
1109
1110                 c.inreq = c.inreq and (context.path[#path + 1] == last)
1111
1112                 parent.nodes[last] = c
1113                 context.treecache[name] = c
1114         end
1115
1116         return c
1117 end
1118
1119 -- Subdispatchers --
1120
1121 function _find_eligible_node(root, prefix, deep, types, descend)
1122         local children = _ordered_children(root)
1123
1124         if not root.leaf and deep ~= nil then
1125                 local sub_path = { unpack(prefix) }
1126
1127                 if deep == false then
1128                         deep = nil
1129                 end
1130
1131                 local _, child
1132                 for _, child in ipairs(children) do
1133                         sub_path[#prefix+1] = child.name
1134
1135                         local res_path = _find_eligible_node(child.node, sub_path,
1136                                                              deep, types, true)
1137
1138                         if res_path then
1139                                 return res_path
1140                         end
1141                 end
1142         end
1143
1144         if descend and
1145            (not types or
1146             (type(root.target) == "table" and
1147              util.contains(types, root.target.type)))
1148         then
1149                 return prefix
1150         end
1151 end
1152
1153 function _find_node(recurse, types)
1154         local path = { unpack(context.path) }
1155         local name = table.concat(path, ".")
1156         local node = context.treecache[name]
1157
1158         path = _find_eligible_node(node, path, recurse, types)
1159
1160         if path then
1161                 dispatch(path)
1162         else
1163                 require "luci.template".render("empty_node_placeholder")
1164         end
1165 end
1166
1167 function _firstchild()
1168         return _find_node(false, nil)
1169 end
1170
1171 function firstchild()
1172         return { type = "firstchild", target = _firstchild }
1173 end
1174
1175 function _firstnode()
1176         return _find_node(true, { "cbi", "form", "template", "arcombine" })
1177 end
1178
1179 function firstnode()
1180         return { type = "firstnode", target = _firstnode }
1181 end
1182
1183 function _alias(self, ...)
1184         local req = { unpack(self.req) }
1185
1186         for _, r in ipairs({...}) do
1187                 req[#req+1] = r
1188         end
1189
1190         dispatch(req)
1191 end
1192
1193 function alias(...)
1194         return { type = "alias", target = _alias, req = { ... } }
1195 end
1196
1197 function _rewrite(self, ...)
1198         local n = self.n
1199         local req = { unpack(self.req) }
1200         local dispatched = util.clone(context.dispatched)
1201
1202         for i=1,n do
1203                 table.remove(dispatched, 1)
1204         end
1205
1206         for i, r in ipairs(req) do
1207                 table.insert(dispatched, i, r)
1208         end
1209
1210         for _, r in ipairs({...}) do
1211                 dispatched[#dispatched+1] = r
1212         end
1213
1214         dispatch(dispatched)
1215 end
1216
1217 function rewrite(n, ...)
1218         return { type = "rewrite", target = _rewrite, n = n, req = { ... } }
1219 end
1220
1221 local function _call(self, ...)
1222         local func = getfenv()[self.name]
1223         assert(func ~= nil,
1224                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
1225
1226         assert(type(func) == "function",
1227                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
1228                'of type "' .. type(func) .. '".')
1229
1230         if #self.argv > 0 then
1231                 return func(unpack(self.argv), ...)
1232         else
1233                 return func(...)
1234         end
1235 end
1236
1237 function call(name, ...)
1238         return {type = "call", argv = {...}, name = name, target = _call}
1239 end
1240
1241 function post_on(params, name, ...)
1242         return {
1243                 type = "call",
1244                 post = params,
1245                 argv = { ... },
1246                 name = name,
1247                 target = _call
1248         }
1249 end
1250
1251 function post(...)
1252         return post_on(true, ...)
1253 end
1254
1255
1256 local _template = function(self, ...)
1257         require "luci.template".render(self.view)
1258 end
1259
1260 function template(name)
1261         return {type = "template", view = name, target = _template}
1262 end
1263
1264
1265 local _view = function(self, ...)
1266         require "luci.template".render("view", { view = self.view })
1267 end
1268
1269 function view(name)
1270         return {type = "view", view = name, target = _view}
1271 end
1272
1273
1274 local function _cbi(self, ...)
1275         local cbi = require "luci.cbi"
1276         local tpl = require "luci.template"
1277         local http = require "luci.http"
1278
1279         local config = self.config or {}
1280         local maps = cbi.load(self.model, ...)
1281
1282         local state = nil
1283
1284         local i, res
1285         for i, res in ipairs(maps) do
1286                 if util.instanceof(res, cbi.SimpleForm) then
1287                         io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1288                                 % self.model)
1289
1290                         io.stderr:write("please change %s to use the form() action instead.\n"
1291                                 % table.concat(context.request, "/"))
1292                 end
1293
1294                 res.flow = config
1295                 local cstate = res:parse()
1296                 if cstate and (not state or cstate < state) then
1297                         state = cstate
1298                 end
1299         end
1300
1301         local function _resolve_path(path)
1302                 return type(path) == "table" and build_url(unpack(path)) or path
1303         end
1304
1305         if config.on_valid_to and state and state > 0 and state < 2 then
1306                 http.redirect(_resolve_path(config.on_valid_to))
1307                 return
1308         end
1309
1310         if config.on_changed_to and state and state > 1 then
1311                 http.redirect(_resolve_path(config.on_changed_to))
1312                 return
1313         end
1314
1315         if config.on_success_to and state and state > 0 then
1316                 http.redirect(_resolve_path(config.on_success_to))
1317                 return
1318         end
1319
1320         if config.state_handler then
1321                 if not config.state_handler(state, maps) then
1322                         return
1323                 end
1324         end
1325
1326         http.header("X-CBI-State", state or 0)
1327
1328         if not config.noheader then
1329                 tpl.render("cbi/header", {state = state})
1330         end
1331
1332         local redirect
1333         local messages
1334         local applymap   = false
1335         local pageaction = true
1336         local parsechain = { }
1337
1338         for i, res in ipairs(maps) do
1339                 if res.apply_needed and res.parsechain then
1340                         local c
1341                         for _, c in ipairs(res.parsechain) do
1342                                 parsechain[#parsechain+1] = c
1343                         end
1344                         applymap = true
1345                 end
1346
1347                 if res.redirect then
1348                         redirect = redirect or res.redirect
1349                 end
1350
1351                 if res.pageaction == false then
1352                         pageaction = false
1353                 end
1354
1355                 if res.message then
1356                         messages = messages or { }
1357                         messages[#messages+1] = res.message
1358                 end
1359         end
1360
1361         for i, res in ipairs(maps) do
1362                 res:render({
1363                         firstmap   = (i == 1),
1364                         redirect   = redirect,
1365                         messages   = messages,
1366                         pageaction = pageaction,
1367                         parsechain = parsechain
1368                 })
1369         end
1370
1371         if not config.nofooter then
1372                 tpl.render("cbi/footer", {
1373                         flow          = config,
1374                         pageaction    = pageaction,
1375                         redirect      = redirect,
1376                         state         = state,
1377                         autoapply     = config.autoapply,
1378                         trigger_apply = applymap
1379                 })
1380         end
1381 end
1382
1383 function cbi(model, config)
1384         return {
1385                 type = "cbi",
1386                 post = { ["cbi.submit"] = true },
1387                 config = config,
1388                 model = model,
1389                 target = _cbi
1390         }
1391 end
1392
1393
1394 local function _arcombine(self, ...)
1395         local argv = {...}
1396         local target = #argv > 0 and self.targets[2] or self.targets[1]
1397         setfenv(target.target, self.env)
1398         target:target(unpack(argv))
1399 end
1400
1401 function arcombine(trg1, trg2)
1402         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
1403 end
1404
1405
1406 local function _form(self, ...)
1407         local cbi = require "luci.cbi"
1408         local tpl = require "luci.template"
1409         local http = require "luci.http"
1410
1411         local maps = luci.cbi.load(self.model, ...)
1412         local state = nil
1413
1414         local i, res
1415         for i, res in ipairs(maps) do
1416                 local cstate = res:parse()
1417                 if cstate and (not state or cstate < state) then
1418                         state = cstate
1419                 end
1420         end
1421
1422         http.header("X-CBI-State", state or 0)
1423         tpl.render("header")
1424         for i, res in ipairs(maps) do
1425                 res:render()
1426         end
1427         tpl.render("footer")
1428 end
1429
1430 function form(model)
1431         return {
1432                 type = "form",
1433                 post = { ["cbi.submit"] = true },
1434                 model = model,
1435                 target = _form
1436         }
1437 end
1438
1439 translate = i18n.translate
1440
1441 -- This function does not actually translate the given argument but
1442 -- is used by build/i18n-scan.pl to find translatable entries.
1443 function _(text)
1444         return text
1445 end