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