diff options
28 files changed, 1987 insertions, 1283 deletions
diff --git a/applications/luci-app-firewall/luasrc/controller/firewall.lua b/applications/luci-app-firewall/luasrc/controller/firewall.lua deleted file mode 100644 index 5f8cb6ef3..000000000 --- a/applications/luci-app-firewall/luasrc/controller/firewall.lua +++ /dev/null @@ -1,19 +0,0 @@ -module("luci.controller.firewall", package.seeall) - -function index() - entry({"admin", "network", "firewall"}, - alias("admin", "network", "firewall", "zones"), - _("Firewall"), 60) - - entry({"admin", "network", "firewall", "zones"}, - view("firewall/zones"), _("General Settings"), 10) - - entry({"admin", "network", "firewall", "forwards"}, - view("firewall/forwards"), _("Port Forwards"), 20) - - entry({"admin", "network", "firewall", "rules"}, - view("firewall/rules"), _("Traffic Rules"), 30) - - entry({"admin", "network", "firewall", "custom"}, - view("firewall/custom"), _("Custom Rules"), 40).leaf = true -end diff --git a/applications/luci-app-firewall/root/usr/share/luci/menu.d/luci-app-firewall.json b/applications/luci-app-firewall/root/usr/share/luci/menu.d/luci-app-firewall.json new file mode 100644 index 000000000..c414f3691 --- /dev/null +++ b/applications/luci-app-firewall/root/usr/share/luci/menu.d/luci-app-firewall.json @@ -0,0 +1,50 @@ +{ + "admin/network/firewall": { + "title": "Firewall", + "order": 60, + "action": { + "type": "alias", + "path": "admin/network/firewall/zones" + }, + "depends": { + "fs": { "/sbin/fw3": "executable" }, + "uci": { "firewall": true } + } + }, + + "admin/network/firewall/zones": { + "title": "General Settings", + "order": 10, + "action": { + "type": "view", + "path": "firewall/zones" + } + }, + + "admin/network/firewall/forwards": { + "title": "Port Forwards", + "order": 20, + "action": { + "type": "view", + "path": "firewall/forwards" + } + }, + + "admin/network/firewall/rules": { + "title": "Traffic Rules", + "order": 30, + "action": { + "type": "view", + "path": "firewall/rules" + } + }, + + "admin/network/firewall/custom": { + "title": "Custom Rules", + "order": 40, + "action": { + "type": "view", + "path": "firewall/custom" + } + } +} diff --git a/applications/luci-app-ocserv/luasrc/model/cbi/ocserv/main.lua b/applications/luci-app-ocserv/luasrc/model/cbi/ocserv/main.lua index 396dedd4a..6194a18dc 100644 --- a/applications/luci-app-ocserv/luasrc/model/cbi/ocserv/main.lua +++ b/applications/luci-app-ocserv/luasrc/model/cbi/ocserv/main.lua @@ -17,35 +17,14 @@ local e = s:taboption("general", Flag, "enable", translate("Enable server")) e.rmempty = false e.default = "1" -local o_sha = s:taboption("general", DummyValue, "sha_hash", translate("Server's certificate SHA1 hash"), - translate("That value should be communicated to the client to verify the server's certificate")) local o_pki = s:taboption("general", DummyValue, "pkid", translate("Server's Public Key ID"), - translate("An alternative value to be communicated to the client to verify the server's certificate; this value only depends on the public key")) + translate("The value to be communicated to the client to verify the server's certificate; this value only depends on the public key")) -local fd = io.popen("/usr/bin/certtool -i --infile /etc/ocserv/server-cert.pem", "r") +local fd = io.popen("/usr/bin/certtool --hash sha256 --key-id --infile /etc/ocserv/server-cert.pem", "r") if fd then local ln - local found_sha = false - local found_pki = false - local complete = 0 - while complete < 2 do - local ln = fd:read("*l") - if not ln then - break - elseif ln:match("SHA%-?1 fingerprint:") then - found_sha = true - elseif found_sha then - local hash = ln:match("([a-f0-9]+)") - o_sha.default = hash and hash:upper() - complete = complete + 1 - found_sha = false - elseif ln:match("Public Key I[Dd]:") then - found_pki = true - elseif found_pki then - local hash = ln:match("([a-f0-9]+)") - o_pki.default = hash and "sha1:" .. hash:upper() - complete = complete + 1 - found_pki = false - end + local ln = fd:read("*l") + if ln then + o_pki.default = "sha256:" .. ln end fd:close() end diff --git a/applications/luci-app-opkg/luasrc/controller/opkg.lua b/applications/luci-app-opkg/luasrc/controller/opkg.lua index 29c9a0864..ebdcf1b09 100644 --- a/applications/luci-app-opkg/luasrc/controller/opkg.lua +++ b/applications/luci-app-opkg/luasrc/controller/opkg.lua @@ -3,14 +3,6 @@ module("luci.controller.opkg", package.seeall) -function index() - entry({"admin", "system", "opkg"}, template("opkg"), _("Software"), 30) - entry({"admin", "system", "opkg", "list"}, call("action_list")).leaf = true - entry({"admin", "system", "opkg", "exec"}, post("action_exec")).leaf = true - entry({"admin", "system", "opkg", "statvfs"}, call("action_statvfs")).leaf = true - entry({"admin", "system", "opkg", "config"}, post_on({ data = true }, "action_config")).leaf = true -end - function action_list(mode) local util = require "luci.util" local cmd @@ -26,7 +18,7 @@ function action_list(mode) fd:close() end - if not lists_dir or #lists_dir == "" then + if not lists_dir or #lists_dir == 0 then lists_dir = "/tmp/opkg-lists" end diff --git a/applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json b/applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json new file mode 100644 index 000000000..9356b586d --- /dev/null +++ b/applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json @@ -0,0 +1,44 @@ +{ + "admin/system/opkg": { + "title": "Software", + "order": 30, + "action": { + "type": "template", + "path": "opkg" + } + }, + + "admin/system/opkg/list/*": { + "action": { + "type": "call", + "module": "luci.controller.opkg", + "function": "action_list" + } + }, + + "admin/system/opkg/exec/*": { + "action": { + "type": "call", + "post": true, + "module": "luci.controller.opkg", + "function": "action_exec" + } + }, + + "admin/system/opkg/statvfs/*": { + "action": { + "type": "call", + "module": "luci.controller.opkg", + "function": "action_statvfs" + } + }, + + "admin/system/opkg/config/*": { + "action": { + "type": "call", + "post": { "data": true }, + "module": "luci.controller.opkg", + "function": "action_config" + } + } +} diff --git a/build/i18n-scan.pl b/build/i18n-scan.pl index c19a4386c..5ac1cb77d 100755 --- a/build/i18n-scan.pl +++ b/build/i18n-scan.pl @@ -1,5 +1,6 @@ #!/usr/bin/perl +use utf8; use strict; use warnings; use Text::Balanced qw(extract_tagged gen_delimited_pat); @@ -15,12 +16,49 @@ my %stringtable; sub dec_lua_str { my $s = shift; - $s =~ s/\\n/\n/g; - $s =~ s/\\t/\t/g; - $s =~ s/\\(.)/$1/sg; + my %rep = ( + 'a' => "\x07", + 'b' => "\x08", + 'f' => "\x0c", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'v' => "\x76" + ); + + $s =~ s!\\(?:([0-9]{1,2})|(.))! + $1 ? chr(int($1)) : ($rep{$2} || $2) + !segx; + + $s =~ s/[\s\n]+/ /g; + $s =~ s/^ //; + $s =~ s/ $//; + + return $s; +} + +sub dec_json_str +{ + my $s = shift; + my %rep = ( + '"' => '"', + '/' => '/', + 'b' => "\x08", + 'f' => "\x0c", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + '\\' => '\\' + ); + + $s =~ s!\\([\\/"bfnrt]|u([0-9a-fA-F]{4}))! + $2 ? chr(hex($2)) : $rep{$1} + !egx; + $s =~ s/[\s\n]+/ /g; $s =~ s/^ //; $s =~ s/ $//; + return $s; } @@ -43,6 +81,8 @@ if( open F, "find @ARGV -type f '(' -name '*.htm' -o -name '*.lua' -o -name '*.j if( open S, "< $file" ) { + binmode S, ':utf8'; + local $/ = undef; my $raw = <S>; close S; @@ -148,9 +188,84 @@ if( open F, "find @ARGV -type f '(' -name '*.htm' -o -name '*.lua' -o -name '*.j close F; } +if( open F, "find @ARGV -type f -path '*/menu.d/*.json' | sort |" ) +{ + while( defined( my $file = readline F ) ) + { + chomp $file; + + if( open S, "< $file" ) + { + binmode S, ':utf8'; + + local $/ = undef; + my $raw = <S>; + close S; + + my $text = $raw; + my $line = 1; + + while ($text =~ s/ ^ (.*?) "title" ([\n\s]*) : //sgx) + { + my ($prefix, $suffix) = ($1, $2); + my $code; + my $res = ""; + my $sub = ""; + + $line += () = $prefix =~ /\n/g; + + my $position = "$file:$line"; + + $line += () = $suffix =~ /\n/g; + + while (defined $sub) + { + undef $sub; + + if ($text =~ /^ ([\n\s]*) " /sx) + { + my $ws = $1; + my $re = gen_delimited_pat('"', '\\'); + + if ($text =~ m/\G\s*($re)/gcs) + { + $sub = $1; + $text = substr $text, pos $text; + } + + $line += () = $ws =~ /\n/g; + + if (defined($sub) && length($sub)) { + $line += () = $sub =~ /\n/g; + + $sub =~ s/^"//; + $sub =~ s/"$//; + $res .= $sub; + } + } + } + + if (defined($res)) + { + $res = dec_json_str($res); + + if ($res) { + $stringtable{$res} ||= [ ]; + push @{$stringtable{$res}}, $position; + } + } + } + } + } + + close F; +} + if( open C, "| msgcat -" ) { + binmode C, ':utf8'; + printf C "msgid \"\"\nmsgstr \"Content-Type: text/plain; charset=UTF-8\"\n\n"; foreach my $key ( sort keys %stringtable ) diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js index 31f89339c..a60aea911 100644 --- a/modules/luci-base/htdocs/luci-static/resources/ui.js +++ b/modules/luci-base/htdocs/luci-static/resources/ui.js @@ -457,9 +457,9 @@ var UIDropdown = UIElement.extend({ 'placeholder': this.options.custom_placeholder || this.options.placeholder }); - if (this.options.datatype) - L.ui.addValidator(createEl, this.options.datatype, - true, null, 'blur', 'keyup'); + if (this.options.datatype || this.options.validate) + L.ui.addValidator(createEl, this.options.datatype || 'string', + true, this.options.validate, 'blur', 'keyup'); sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl)); } @@ -1270,9 +1270,9 @@ var UIDynamicList = UIElement.extend({ dl.lastElementChild.appendChild(inputEl); dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+')); - if (this.options.datatype) - L.ui.addValidator(inputEl, this.options.datatype, - true, null, 'blur', 'keyup'); + if (this.options.datatype || this.options.validate) + L.ui.addValidator(inputEl, this.options.datatype || 'string', + true, this.options.validate, 'blur', 'keyup'); } for (var i = 0; i < this.values.length; i++) diff --git a/modules/luci-base/luasrc/controller/admin/index.lua b/modules/luci-base/luasrc/controller/admin/index.lua index 0cebfa4f5..68bbd38a7 100644 --- a/modules/luci-base/luasrc/controller/admin/index.lua +++ b/modules/luci-base/luasrc/controller/admin/index.lua @@ -3,85 +3,6 @@ module("luci.controller.admin.index", package.seeall) -function index() - function toplevel_page(page, preflookup, preftarget) - if preflookup and preftarget then - if lookup(preflookup) then - page.target = preftarget - end - end - - if not page.target then - page.target = firstchild() - end - end - - local uci = require("luci.model.uci").cursor() - - local root = node() - if not root.target then - root.target = alias("admin") - root.index = true - end - - local page = node("admin") - - page.title = _("Administration") - page.order = 10 - page.sysauth = "root" - page.sysauth_authenticator = "htmlauth" - page.ucidata = true - page.index = true - page.target = firstnode() - - -- Empty menu tree to be populated by addons and modules - - page = node("admin", "status") - page.title = _("Status") - page.order = 10 - page.index = true - -- overview is from mod-admin-full - toplevel_page(page, "admin/status/overview", alias("admin", "status", "overview")) - - page = node("admin", "system") - page.title = _("System") - page.order = 20 - page.index = true - -- system/system is from mod-admin-full - toplevel_page(page, "admin/system/system", alias("admin", "system", "system")) - - -- Only used if applications add items - page = node("admin", "vpn") - page.title = _("VPN") - page.order = 30 - page.index = true - toplevel_page(page, false, false) - - -- Only used if applications add items - page = node("admin", "services") - page.title = _("Services") - page.order = 40 - page.index = true - toplevel_page(page, false, false) - - -- Even for mod-admin-full network just uses first submenu item as landing - page = node("admin", "network") - page.title = _("Network") - page.order = 50 - page.index = true - toplevel_page(page, false, false) - - page = entry({"admin", "translations"}, call("action_translations"), nil) - page.leaf = true - - page = entry({"admin", "ubus"}, call("action_ubus"), nil) - page.sysauth = false - page.leaf = true - - -- Logout is last - entry({"admin", "logout"}, call("action_logout"), _("Logout"), 999) -end - function action_logout() local dsp = require "luci.dispatcher" local utl = require "luci.util" diff --git a/modules/luci-base/luasrc/controller/admin/uci.lua b/modules/luci-base/luasrc/controller/admin/uci.lua index 6b19c62f8..7aad10d58 100644 --- a/modules/luci-base/luasrc/controller/admin/uci.lua +++ b/modules/luci-base/luasrc/controller/admin/uci.lua @@ -4,32 +4,6 @@ module("luci.controller.admin.uci", package.seeall) -function index() - local redir = luci.http.formvalue("redir", true) - or table.concat(luci.dispatcher.context.request, "/") - - entry({"admin", "uci"}, nil, _("Configuration")) - entry({"admin", "uci", "revert"}, post("action_revert"), nil) - - local node - local authen = function(checkpass, allowed_users) - return "root", luci.http.formvalue("sid") - end - - node = entry({"admin", "uci", "apply_rollback"}, post("action_apply_rollback"), nil) - node.cors = true - node.sysauth_authenticator = authen - - node = entry({"admin", "uci", "apply_unchecked"}, post("action_apply_unchecked"), nil) - node.cors = true - node.sysauth_authenticator = authen - - node = entry({"admin", "uci", "confirm"}, call("action_confirm"), nil) - node.cors = true - node.sysauth = false -end - - local function ubus_state_to_http(errstr) local map = { ["Invalid command"] = 400, diff --git a/modules/luci-base/luasrc/dispatcher.lua b/modules/luci-base/luasrc/dispatcher.lua index b43b94fde..d4293422b 100644 --- a/modules/luci-base/luasrc/dispatcher.lua +++ b/modules/luci-base/luasrc/dispatcher.lua @@ -17,138 +17,336 @@ _M.fs = fs -- Index table local index = nil --- Fastindex -local fi +local function check_fs_depends(fs) + local fs = require "nixio.fs" + + for path, kind in pairs(fs) do + if kind == "directory" then + local empty = true + for entry in (fs.dir(path) or function() end) do + empty = false + break + end + if empty then + return false + end + elseif kind == "executable" then + if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then + return false + end + elseif kind == "file" then + if fs.stat(path, "type") ~= "reg" then + return false + end + end + end + return true +end -function build_url(...) - local path = {...} - local url = { http.getenv("SCRIPT_NAME") or "" } +local function check_uci_depends_options(conf, s, opts) + local uci = require "luci.model.uci" - local p - for _, p in ipairs(path) do - if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then - url[#url+1] = "/" - url[#url+1] = p + if type(opts) == "string" then + return (s[".type"] == opts) + elseif opts == true then + for option, value in pairs(s) do + if option:byte(1) ~= 46 then + return true + end + end + elseif type(opts) == "table" then + for option, value in pairs(opts) do + local sval = s[option] + if type(sval) == "table" then + local found = false + for _, v in ipairs(sval) do + if v == value then + found = true + break + end + end + if not found then + return false + end + elseif value == true then + if sval == nil then + return false + end + else + if sval ~= value then + return false + end + end end end - if #path == 0 then - url[#url+1] = "/" + return true +end + +local function check_uci_depends_section(conf, sect) + local uci = require "luci.model.uci" + + for section, options in pairs(sect) do + local stype = section:match("^@([A-Za-z0-9_%-]+)$") + if stype then + local found = false + uci:foreach(conf, stype, function(s) + if check_uci_depends_options(conf, s, options) then + found = true + return false + end + end) + if not found then + return false + end + else + local s = uci:get_all(conf, section) + if not s or not check_uci_depends_options(conf, s, options) then + return false + end + end end - return table.concat(url, "") + return true end -function _ordered_children(node) - local name, child, children = nil, nil, {} +local function check_uci_depends(conf) + local uci = require "luci.model.uci" - for name, child in pairs(node.nodes) do - children[#children+1] = { - name = name, - node = child, - order = child.order or 100 - } + for config, values in pairs(conf) do + if values == true then + local found = false + uci:foreach(config, nil, function(s) + found = true + return false + end) + if not found then + return false + end + elseif type(values) == "table" then + if not check_uci_depends_section(config, values) then + return false + end + end end - table.sort(children, function(a, b) - if a.order == b.order then - return a.name < b.name - else - return a.order < b.order + return true +end + +local function check_depends(spec) + if type(spec.depends) ~= "table" then + return true + end + + if type(spec.depends.fs) == "table" and not check_fs_depends(spec.depends.fs) then + local satisfied = false + local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs } + for _, alternative in ipairs(alternatives) do + if check_fs_depends(alternative) then + satisfied = true + break + end end - end) + if not satisfied then + return false + end + end - return children + if type(spec.depends.uci) == "table" then + local satisfied = false + local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci } + for _, alternative in ipairs(alternatives) do + if check_uci_depends(alternative) then + satisfied = true + break + end + end + if not satisfied then + return false + end + end + + return true end -local function dependencies_satisfied(node) - if type(node.file_depends) == "table" then - for _, file in ipairs(node.file_depends) do - local ftype = fs.stat(file, "type") - if ftype == "dir" then - local empty = true - for e in (fs.dir(file) or function() end) do - empty = false - end - if empty then - return false - end - elseif ftype == nil then - return false - end +local function target_to_json(target, module) + local action + + if target.type == "call" then + action = { + ["type"] = "call", + ["module"] = module, + ["function"] = target.name, + ["parameters"] = target.argv + } + elseif target.type == "view" then + action = { + ["type"] = "view", + ["path"] = target.view + } + elseif target.type == "template" then + action = { + ["type"] = "template", + ["path"] = target.view + } + elseif target.type == "cbi" then + action = { + ["type"] = "cbi", + ["path"] = target.model + } + elseif target.type == "form" then + action = { + ["type"] = "form", + ["path"] = target.model + } + elseif target.type == "firstchild" then + action = { + ["type"] = "firstchild" + } + elseif target.type == "firstnode" then + action = { + ["type"] = "firstchild", + ["recurse"] = true + } + elseif target.type == "arcombine" then + if type(target.targets) == "table" then + action = { + ["type"] = "arcombine", + ["targets"] = { + target_to_json(target.targets[1], module), + target_to_json(target.targets[2], module) + } + } end + elseif target.type == "alias" then + action = { + ["type"] = "alias", + ["path"] = table.concat(target.req, "/") + } + elseif target.type == "rewrite" then + action = { + ["type"] = "rewrite", + ["path"] = table.concat(target.req, "/"), + ["remove"] = target.n + } end - if type(node.uci_depends) == "table" then - for config, expect_sections in pairs(node.uci_depends) do - if type(expect_sections) == "table" then - for section, expect_options in pairs(expect_sections) do - if type(expect_options) == "table" then - for option, expect_value in pairs(expect_options) do - local val = uci:get(config, section, option) - if expect_value == true and val == nil then - return false - elseif type(expect_value) == "string" then - if type(val) == "table" then - local found = false - for _, subval in ipairs(val) do - if subval == expect_value then - found = true - end - end - if not found then - return false - end - elseif val ~= expect_value then - return false - end - end - end + if target.post and action then + action.post = target.post + end + + return action +end + +local function tree_to_json(node, json) + local fs = require "nixio.fs" + local util = require "luci.util" + + if type(node.nodes) == "table" then + for subname, subnode in pairs(node.nodes) do + local spec = { + title = util.striptags(subnode.title), + order = subnode.order + } + + if subnode.leaf then + spec.wildcard = true + end + + if subnode.cors then + spec.cors = true + end + + if subnode.setuser then + spec.setuser = subnode.setuser + end + + if subnode.setgroup then + spec.setgroup = subnode.setgroup + end + + if type(subnode.target) == "table" then + spec.action = target_to_json(subnode.target, subnode.module) + end + + if type(subnode.file_depends) == "table" then + for _, v in ipairs(subnode.file_depends) do + spec.depends = spec.depends or {} + spec.depends.fs = spec.depends.fs or {} + + local ft = fs.stat(v, "type") + if ft == "dir" then + spec.depends.fs[v] = "directory" + elseif v:match("/s?bin/") then + spec.depends.fs[v] = "executable" else - local val = uci:get(config, section) - if expect_options == true and val == nil then - return false - elseif type(expect_options) == "string" and val ~= expect_options then - return false - end + spec.depends.fs[v] = "file" end end - elseif expect_sections == true then - if not uci:get_first(config) then - return false + end + + if type(subnode.uci_depends) == "table" then + for k, v in pairs(subnode.uci_depends) do + spec.depends = spec.depends or {} + spec.depends.uci = spec.depends.uci or {} + spec.depends.uci[k] = v end end + + if (subnode.sysauth_authenticator ~= nil) or + (subnode.sysauth ~= nil and subnode.sysauth ~= false) + then + if subnode.sysauth_authenticator == "htmlauth" then + spec.auth = { + login = true, + methods = { "cookie:sysauth" } + } + elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then + spec.auth = { + login = false, + methods = { "param:auth", "cookie:sysauth" } + } + elseif subnode.module == "luci.controller.admin.uci" then + spec.auth = { + login = false, + methods = { "param:sid" } + } + end + elseif subnode.sysauth == false then + spec.auth = {} + end + + if not spec.action then + spec.title = nil + end + + spec.satisfied = check_depends(spec) + json.children = json.children or {} + json.children[subname] = tree_to_json(subnode, spec) end end - return true + return json end -function node_visible(node) - if node then - return not ( - (not dependencies_satisfied(node)) or - (not node.title or #node.title == 0) or - (not node.target or node.hidden == true) or - (type(node.target) == "table" and node.target.type == "firstchild" and - (type(node.nodes) ~= "table" or not next(node.nodes))) - ) - end - return false -end +function build_url(...) + local path = {...} + local url = { http.getenv("SCRIPT_NAME") or "" } -function node_childs(node) - local rv = { } - if node then - local _, child - for _, child in ipairs(_ordered_children(node)) do - if node_visible(child.node) then - rv[#rv+1] = child.name - end + local p + for _, p in ipairs(path) do + if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then + url[#url+1] = "/" + url[#url+1] = p end end - return rv + + if #path == 0 then + url[#url+1] = "/" + end + + return table.concat(url, "") end @@ -185,6 +383,38 @@ function error500(message) return false end +local function determine_request_language() + local conf = require "luci.config" + assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'") + + local lang = conf.main.lang or "auto" + if lang == "auto" then + local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or "" + for aclang in aclang:gmatch("[%w_-]+") do + local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$") + if country and culture then + local cc = "%s_%s" %{ country, culture:lower() } + if conf.languages[cc] then + lang = cc + break + elseif conf.languages[country] then + lang = country + break + end + elseif conf.languages[aclang] then + lang = aclang + break + end + end + end + + if lang == "auto" then + lang = i18n.default + end + + i18n.setlanguage(lang) +end + function httpdispatch(request, prefix) http.context.request = request @@ -204,6 +434,8 @@ function httpdispatch(request, prefix) r[#r+1] = node end + determine_request_language() + local stat, err = util.coxpcall(function() dispatch(context.request) end, error500) @@ -306,189 +538,245 @@ local function session_setup(user, pass, allowed_users) return nil, nil end -function dispatch(request) - --context._disable_memtrace = require "luci.debug".trap_memtrace("l") - local ctx = context - ctx.path = request +local function check_authentication(method) + local auth_type, auth_param = method:match("^(%w+):(.+)$") + local sid, sdat - local conf = require "luci.config" - assert(conf.main, - "/etc/config/luci seems to be corrupt, unable to find section 'main'") + if auth_type == "cookie" then + sid = http.getcookie(auth_param) + elseif auth_type == "param" then + sid = http.formvalue(auth_param) + end - local i18n = require "luci.i18n" - local lang = conf.main.lang or "auto" - if lang == "auto" then - local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or "" - for aclang in aclang:gmatch("[%w_-]+") do - local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$") - if country and culture then - local cc = "%s_%s" %{ country, culture:lower() } - if conf.languages[cc] then - lang = cc - break - elseif conf.languages[country] then - lang = country - break - end - elseif conf.languages[aclang] then - lang = aclang - break - end + return session_retrieve(sid) +end + +local function get_children(node) + local children = {} + + if not node.wildcard and type(node.children) == "table" then + for name, child in pairs(node.children) do + children[#children+1] = { + name = name, + node = child, + order = child.order or 1000 + } end - end - if lang == "auto" then - lang = i18n.default - end - i18n.setlanguage(lang) - local c = ctx.tree - local stat - if not c then - c = createtree() + table.sort(children, function(a, b) + if a.order == b.order then + return a.name < b.name + else + return a.order < b.order + end + end) end - local track = {} - local args = {} - ctx.args = args - ctx.requestargs = ctx.requestargs or args - local n - local preq = {} - local freq = {} + return children +end - for i, s in ipairs(request) do - preq[#preq+1] = s - freq[#freq+1] = s - c = c.nodes[s] - n = i - if not c then - break +local function find_subnode(root, prefix, recurse, descended) + local children = get_children(root) + + if #children > 0 and (not descended or recurse) then + local sub_path = { unpack(prefix) } + + if recurse == false then + recurse = nil end - util.update(track, c) + for _, child in ipairs(children) do + sub_path[#prefix+1] = child.name + + local res_path = find_subnode(child.node, sub_path, recurse, true) - if c.leaf then - break + if res_path then + return res_path + end end end - if c and c.leaf then - for j=n+1, #request do - args[#args+1] = request[j] - freq[#freq+1] = request[j] + if descended then + if not recurse or + root.action.type == "cbi" or + root.action.type == "form" or + root.action.type == "view" or + root.action.type == "template" or + root.action.type == "arcombine" + then + return prefix end end +end - ctx.requestpath = ctx.requestpath or freq - ctx.path = preq +local function merge_trees(node_a, node_b) + for k, v in pairs(node_b) do + if k == "children" then + node_a.children = node_a.children or {} - -- Init template engine - if (c and c.index) or not track.notemplate then - local tpl = require("luci.template") - local media = track.mediaurlbase or luci.config.main.mediaurlbase - if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then - media = nil - for name, theme in pairs(luci.config.themes) do - if name:sub(1,1) ~= "." and pcall(tpl.Template, - "themes/%s/header" % fs.basename(theme)) then - media = theme - end + for name, spec in pairs(v) do + node_a.children[name] = merge_trees(node_a.children[name] or {}, spec) end - assert(media, "No valid theme found") + else + node_a[k] = v end + end + return node_a +end - local function _ifattr(cond, key, val, noescape) - if cond then - local env = getfenv(3) - local scope = (type(env.self) == "table") and env.self - if type(val) == "table" then - if not next(val) then - return '' - else - val = util.serialize_json(val) - end - end - - val = tostring(val or - (type(env[key]) ~= "function" and env[key]) or - (scope and type(scope[key]) ~= "function" and scope[key]) or "") +function menu_json() + local tree = context.tree or createtree() + local lua_tree = tree_to_json(tree, { + action = { + ["type"] = "firstchild", + ["recurse"] = true + } + }) - if noescape ~= true then - val = util.pcdata(val) - end + local json_tree = createtree_json() + return merge_trees(lua_tree, json_tree) +end - return string.format(' %s="%s"', tostring(key), val) - else - return '' +local function init_template_engine(ctx) + local tpl = require "luci.template" + local media = luci.config.main.mediaurlbase + + if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then + media = nil + for name, theme in pairs(luci.config.themes) do + if name:sub(1,1) ~= "." and pcall(tpl.Template, + "themes/%s/header" % fs.basename(theme)) then + media = theme end end + assert(media, "No valid theme found") + end - tpl.context.viewns = setmetatable({ - write = http.write; - include = function(name) tpl.Template(name):render(getfenv(2)) end; - translate = i18n.translate; - translatef = i18n.translatef; - export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end; - striptags = util.striptags; - pcdata = util.pcdata; - media = media; - theme = fs.basename(media); - resource = luci.config.main.resourcebase; - ifattr = function(...) return _ifattr(...) end; - attr = function(...) return _ifattr(true, ...) end; - url = build_url; - }, {__index=function(tbl, key) - if key == "controller" then - return build_url() - elseif key == "REQUEST_URI" then - return build_url(unpack(ctx.requestpath)) - elseif key == "FULL_REQUEST_URI" then - local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") } - local query = http.getenv("QUERY_STRING") - if query and #query > 0 then - url[#url+1] = "?" - url[#url+1] = query + local function _ifattr(cond, key, val, noescape) + if cond then + local env = getfenv(3) + local scope = (type(env.self) == "table") and env.self + if type(val) == "table" then + if not next(val) then + return '' + else + val = util.serialize_json(val) end - return table.concat(url, "") - elseif key == "token" then - return ctx.authtoken - else - return rawget(tbl, key) or _G[key] end - end}) - end - track.dependent = (track.dependent ~= false) - assert(not track.dependent or not track.auto, - "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " .. - "has no parent node so the access to this location has been denied.\n" .. - "This is a software bug, please report this message at " .. - "https://github.com/openwrt/luci/issues" - ) + val = tostring(val or + (type(env[key]) ~= "function" and env[key]) or + (scope and type(scope[key]) ~= "function" and scope[key]) or "") - if track.sysauth and not ctx.authsession then - local authen = track.sysauth_authenticator - local _, sid, sdat, default_user, allowed_users + if noescape ~= true then + val = util.pcdata(val) + end - if type(authen) == "string" and authen ~= "htmlauth" then - error500("Unsupported authenticator %q configured" % authen) - return + return string.format(' %s="%s"', tostring(key), val) + else + return '' end + end - if type(track.sysauth) == "table" then - default_user, allowed_users = nil, track.sysauth + tpl.context.viewns = setmetatable({ + write = http.write; + include = function(name) tpl.Template(name):render(getfenv(2)) end; + translate = i18n.translate; + translatef = i18n.translatef; + export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end; + striptags = util.striptags; + pcdata = util.pcdata; + media = media; + theme = fs.basename(media); + resource = luci.config.main.resourcebase; + ifattr = function(...) return _ifattr(...) end; + attr = function(...) return _ifattr(true, ...) end; + url = build_url; + }, {__index=function(tbl, key) + if key == "controller" then + return build_url() + elseif key == "REQUEST_URI" then + return build_url(unpack(ctx.requestpath)) + elseif key == "FULL_REQUEST_URI" then + local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") } + local query = http.getenv("QUERY_STRING") + if query and #query > 0 then + url[#url+1] = "?" + url[#url+1] = query + end + return table.concat(url, "") + elseif key == "token" then + return ctx.authtoken else - default_user, allowed_users = track.sysauth, { track.sysauth } + return rawget(tbl, key) or _G[key] end + end}) - if type(authen) == "function" then - _, sid = authen(sys.user.checkpasswd, allowed_users) - else - sid = http.getcookie("sysauth") + return tpl +end + +function dispatch(request) + --context._disable_memtrace = require "luci.debug".trap_memtrace("l") + local ctx = context + + local auth, cors, suid, sgid + local menu = menu_json() + local page = menu + + local requested_path_full = {} + local requested_path_node = {} + local requested_path_args = {} + + for i, s in ipairs(request) do + if type(page.children) ~= "table" or not page.children[s] then + page = nil + break + end + + if not page.children[s].satisfied then + page = nil + break end - sid, sdat = session_retrieve(sid, allowed_users) + page = page.children[s] + auth = page.auth or auth + cors = page.cors or cors + suid = page.setuser or suid + sgid = page.setgroup or sgid - if not (sid and sdat) and authen == "htmlauth" then + requested_path_full[i] = s + requested_path_node[i] = s + + if page.wildcard then + for j = i + 1, #request do + requested_path_args[j - i] = request[j] + requested_path_full[j] = request[j] + end + break + end + end + + local tpl = init_template_engine(ctx) + + ctx.args = requested_path_args + ctx.path = requested_path_node + ctx.dispatched = page + + ctx.requestpath = ctx.requestpath or requested_path_full + ctx.requestargs = ctx.requestargs or requested_path_args + ctx.requested = ctx.requested or page + + if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then + local sid, sdat + for _, method in ipairs(auth.methods) do + sid, sdat = check_authentication(method) + + if sid and sdat then + break + end + end + + if not (sid and sdat) and auth.login then local user = http.getenv("HTTP_AUTH_USER") local pass = http.getenv("HTTP_AUTH_PASS") @@ -497,27 +785,23 @@ function dispatch(request) pass = http.formvalue("luci_password") end - sid, sdat = session_setup(user, pass, allowed_users) + sid, sdat = session_setup(user, pass, { "root" }) if not sid then - local tmpl = require "luci.template" - context.path = {} http.status(403, "Forbidden") http.header("X-LuCI-Login-Required", "yes") - tmpl.render(track.sysauth_template or "sysauth", { - duser = default_user, - fuser = user - }) - return + return tpl.render("sysauth", { duser = "root", fuser = user }) end http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{ sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or "" }) + http.redirect(build_url(unpack(ctx.requestpath))) + return end if not sid or not sdat then @@ -531,81 +815,117 @@ function dispatch(request) ctx.authuser = sdat.username end - if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then + local action = (page and type(page.action) == "table") and page.action or {} + + if action.type == "arcombine" then + action = (#requested_path_args > 0) and action.targets[2] or action.targets[1] + end + + if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then luci.http.status(200, "OK") luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*") luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") return end - if c and require_post_security(c.target, args) then - if not test_post_security(c) then + if require_post_security(action) then + if not test_post_security() then return end end - if track.setgroup then - sys.process.setgroup(track.setgroup) + if sgid then + sys.process.setgroup(sgid) end - if track.setuser then - sys.process.setuser(track.setuser) + if suid then + sys.process.setuser(suid) end - local target = nil - if c then - if type(c.target) == "function" then - target = c.target - elseif type(c.target) == "table" then - target = c.target.target + if action.type == "view" then + tpl.render("view", { view = action.path }) + + elseif action.type == "call" then + local ok, mod = util.copcall(require, action.module) + if not ok then + error500(mod) + return + end + + local func = mod[action["function"]] + + assert(func ~= nil, + 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?') + + assert(type(func) == "function", + 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' .. + 'of type "' .. type(func) .. '".') + + local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {} + for _, s in ipairs(requested_path_args) do + argv[#argv + 1] = s end - end - if c and (c.index or type(target) == "function") then - ctx.dispatched = c - ctx.requested = ctx.requested or ctx.dispatched - end + local ok, err = util.copcall(func, unpack(argv)) + if not ok then + error500(err) + end - if c and c.index then - local tpl = require "luci.template" + elseif action.type == "firstchild" then + local sub_request = find_subnode(page, requested_path_full, action.recurse) + if sub_request then + dispatch(sub_request) + else + tpl.render("empty_node_placeholder", getfenv(1)) + end - if util.copcall(tpl.render, "indexer", {}) then - return true + elseif action.type == "alias" then + local sub_request = {} + for name in action.path:gmatch("[^/]+") do + sub_request[#sub_request + 1] = name end - end - if type(target) == "function" then - util.copcall(function() - local oldenv = getfenv(target) - local module = require(c.module) - local env = setmetatable({}, {__index= + for _, s in ipairs(requested_path_args) do + sub_request[#sub_request + 1] = s + end - function(tbl, key) - return rawget(tbl, key) or module[key] or oldenv[key] - end}) + dispatch(sub_request) - setfenv(target, env) - end) + elseif action.type == "rewrite" then + local sub_request = { unpack(request) } + for i = 1, action.remove do + table.remove(sub_request, 1) + end - local ok, err - if type(c.target) == "table" then - ok, err = util.copcall(target, c.target, unpack(args)) - else - ok, err = util.copcall(target, unpack(args)) + local n = 1 + for s in action.path:gmatch("[^/]+") do + table.insert(sub_request, n, s) + n = n + 1 end - if not ok then - error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") .. - " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" .. - "The called action terminated with an exception:\n" .. tostring(err or "(unknown)")) + + for _, s in ipairs(requested_path_args) do + sub_request[#sub_request + 1] = s end + + dispatch(sub_request) + + elseif action.type == "template" then + tpl.render(action.path, getfenv(1)) + + elseif action.type == "cbi" then + _cbi({ config = action.config, model = action.path }, unpack(requested_path_args)) + + elseif action.type == "form" then + _form({ model = action.path }, unpack(requested_path_args)) + else - local root = node() - if not root or not root.target then + local root = find_subnode(menu, {}, true) + if not root then error404("No root node was registered, this usually happens if no module was installed.\n" .. "Install luci-mod-admin-full and retry. " .. "If the module is already installed, try removing the /tmp/luci-indexcache file.") else - error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" .. + error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" .. "If this url belongs to an extension, make sure it is properly installed.\n" .. "If the extension was recently installed, try removing the /tmp/luci-indexcache file.") end @@ -659,13 +979,9 @@ function createindex() "' - It must correspond to the file path!") local idx = mod.index - assert(type(idx) == "function", - "Invalid controller file found\n" .. - "The file '" .. path .. "' contains no index() function.\n" .. - "Please make sure that the controller contains a valid " .. - "index function and verify the spelling!") - - index[modname] = idx + if type(idx) == "function" then + index[modname] = idx + end end if indexcache then @@ -675,6 +991,94 @@ function createindex() end end +function createtree_json() + local json = require "luci.jsonc" + local tree = {} + + local schema = { + action = "table", + auth = "table", + cors = "boolean", + depends = "table", + order = "number", + setgroup = "string", + setuser = "string", + title = "string", + wildcard = "boolean" + } + + local files = {} + local fprint = {} + local cachefile + + for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do + files[#files+1] = file + + if indexcache then + local st = fs.stat(file) + if st then + fprint[#fprint+1] = '%x' % st.ino + fprint[#fprint+1] = '%x' % st.mtime + fprint[#fprint+1] = '%x' % st.size + end + end + end + + if indexcache then + cachefile = "%s.%s.json" %{ + indexcache, + nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".") + } + + local res = json.parse(fs.readfile(cachefile) or "") + if res then + return res + end + + for file in (fs.glob("%s.*.json" % indexcache) or function() end) do + fs.unlink(file) + end + end + + for _, file in ipairs(files) do + local data = json.parse(fs.readfile(file) or "") + if type(data) == "table" then + for path, spec in pairs(data) do + if type(spec) == "table" then + local node = tree + + for s in path:gmatch("[^/]+") do + if s == "*" then + node.wildcard = true + break + end + + node.children = node.children or {} + node.children[s] = node.children[s] or {} + node = node.children[s] + end + + if node ~= tree then + for k, t in pairs(schema) do + if type(spec[k]) == t then + node[k] = spec[k] + end + end + + node.satisfied = check_depends(spec) + end + end + end + end + end + + if cachefile then + fs.writefile(cachefile, json.stringify(tree)) + end + + return tree +end + -- Build the index before if it does not exist yet. function createtree() if not index then @@ -767,16 +1171,6 @@ function _create_node(path) c = {nodes={}, auto=true, inreq=true} - local _, n - for _, n in ipairs(path) do - if context.path[_] ~= n then - c.inreq = false - break - end - end - - c.inreq = c.inreq and (context.path[#path + 1] == last) - parent.nodes[last] = c context.treecache[name] = c end @@ -786,119 +1180,24 @@ end -- Subdispatchers -- -function _find_eligible_node(root, prefix, deep, types, descend) - local children = _ordered_children(root) - - if not root.leaf and deep ~= nil then - local sub_path = { unpack(prefix) } - - if deep == false then - deep = nil - end - - local _, child - for _, child in ipairs(children) do - sub_path[#prefix+1] = child.name - - local res_path = _find_eligible_node(child.node, sub_path, - deep, types, true) - - if res_path then - return res_path - end - end - end - - if descend and - (not types or - (type(root.target) == "table" and - util.contains(types, root.target.type))) - then - return prefix - end -end - -function _find_node(recurse, types) - local path = { unpack(context.path) } - local name = table.concat(path, ".") - local node = context.treecache[name] - - path = _find_eligible_node(node, path, recurse, types) - - if path then - dispatch(path) - else - require "luci.template".render("empty_node_placeholder") - end -end - -function _firstchild() - return _find_node(false, nil) -end - function firstchild() - return { type = "firstchild", target = _firstchild } -end - -function _firstnode() - return _find_node(true, { "cbi", "form", "template", "arcombine" }) + return { type = "firstchild" } end function firstnode() - return { type = "firstnode", target = _firstnode } + return { type = "firstnode" } end function alias(...) - local req = {...} - return function(...) - for _, r in ipairs({...}) do - req[#req+1] = r - end - - dispatch(req) - end + return { type = "alias", req = { ... } } end function rewrite(n, ...) - local req = {...} - return function(...) - local dispatched = util.clone(context.dispatched) - - for i=1,n do - table.remove(dispatched, 1) - end - - for i, r in ipairs(req) do - table.insert(dispatched, i, r) - end - - for _, r in ipairs({...}) do - dispatched[#dispatched+1] = r - end - - dispatch(dispatched) - end -end - - -local function _call(self, ...) - local func = getfenv()[self.name] - assert(func ~= nil, - 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?') - - assert(type(func) == "function", - 'The symbol "' .. self.name .. '" does not refer to a function but data ' .. - 'of type "' .. type(func) .. '".') - - if #self.argv > 0 then - return func(unpack(self.argv), ...) - else - return func(...) - end + return { type = "rewrite", n = n, req = { ... } } end function call(name, ...) - return {type = "call", argv = {...}, name = name, target = _call} + return { type = "call", argv = {...}, name = name } end function post_on(params, name, ...) @@ -906,8 +1205,7 @@ function post_on(params, name, ...) type = "call", post = params, argv = { ... }, - name = name, - target = _call + name = name } end @@ -916,25 +1214,16 @@ function post(...) end -local _template = function(self, ...) - require "luci.template".render(self.view) -end - function template(name) - return {type = "template", view = name, target = _template} -end - - -local _view = function(self, ...) - require "luci.template".render("view", { view = self.view }) + return { type = "template", view = name } end function view(name) - return {type = "view", view = name, target = _view} + return { type = "view", view = name } end -local function _cbi(self, ...) +function _cbi(self, ...) local cbi = require "luci.cbi" local tpl = require "luci.template" local http = require "luci.http" @@ -1048,25 +1337,21 @@ function cbi(model, config) type = "cbi", post = { ["cbi.submit"] = true }, config = config, - model = model, - target = _cbi + model = model } end -local function _arcombine(self, ...) - local argv = {...} - local target = #argv > 0 and self.targets[2] or self.targets[1] - setfenv(target.target, self.env) - target:target(unpack(argv)) -end - function arcombine(trg1, trg2) - return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}} + return { + type = "arcombine", + env = getfenv(), + targets = {trg1, trg2} + } end -local function _form(self, ...) +function _form(self, ...) local cbi = require "luci.cbi" local tpl = require "luci.template" local http = require "luci.http" @@ -1092,10 +1377,9 @@ end function form(model) return { - type = "cbi", + type = "form", post = { ["cbi.submit"] = true }, - model = model, - target = _form + model = model } end diff --git a/modules/luci-base/luasrc/view/header.htm b/modules/luci-base/luasrc/view/header.htm index 1ef0e5b01..9cdedde5c 100644 --- a/modules/luci-base/luasrc/view/header.htm +++ b/modules/luci-base/luasrc/view/header.htm @@ -13,8 +13,8 @@ local applyconf = luci.config and luci.config.apply %> -<script type="text/javascript" src="<%=resource%>/promis.min.js"></script> -<script type="text/javascript" src="<%=resource%>/luci.js"></script> +<script type="text/javascript" src="<%=resource%>/promis.min.js?v=git-19.292.31773-cc35194"></script> +<script type="text/javascript" src="<%=resource%>/luci.js?v=git-19.292.31773-cc35206"></script> <script type="text/javascript"> L = new LuCI(<%= luci.http.write_json({ token = token, @@ -22,6 +22,7 @@ scriptname = luci.http.getenv("SCRIPT_NAME"), pathinfo = luci.http.getenv("PATH_INFO"), requestpath = luci.dispatcher.context.requestpath, + dispatchpath = luci.dispatcher.context.path, pollinterval = luci.config.main.pollinterval or 5, sessionid = luci.dispatcher.context.authsession, apply_rollback = math.max(applyconf and applyconf.rollback or 30, 30), diff --git a/modules/luci-base/root/usr/share/luci/menu.d/luci-base.json b/modules/luci-base/root/usr/share/luci/menu.d/luci-base.json new file mode 100644 index 000000000..cdfffb512 --- /dev/null +++ b/modules/luci-base/root/usr/share/luci/menu.d/luci-base.json @@ -0,0 +1,142 @@ +{ + "admin": { + "title": "Administration", + "order": 10, + "action": { + "type": "firstchild", + "recurse": true + }, + "auth": { + "methods": [ "cookie:sysauth" ], + "login": true + } + }, + + "admin/status": { + "title": "Status", + "order": 10, + "action": { + "type": "firstchild", + "preferred": "overview", + "recurse": true + } + }, + + "admin/system": { + "title": "System", + "order": 20, + "action": { + "type": "firstchild", + "preferred": "system", + "recurse": true + } + }, + + "admin/vpn": { + "title": "VPN", + "order": 30, + "action": { + "type": "firstchild", + "recurse": true + } + }, + + "admin/services": { + "title": "Services", + "order": 40, + "action": { + "type": "firstchild", + "recurse": true + } + }, + + "admin/network": { + "title": "Network", + "order": 50, + "action": { + "type": "firstchild", + "recurse": true + } + }, + + "admin/translations/*": { + "action": { + "type": "call", + "module": "luci.controller.admin.index", + "function": "action_translations" + }, + "auth": { + "methods": [ "cookie:sysauth" ] + } + }, + + "admin/ubus/*": { + "action": { + "type": "call", + "module": "luci.controller.admin.index", + "function": "action_ubus" + }, + "auth": {} + }, + + "admin/logout": { + "title": "Logout", + "order": 999, + "action": { + "type": "call", + "module": "luci.controller.admin.index", + "function": "action_logout" + } + }, + + "admin/uci": { + "action": { + "type": "firstchild" + } + }, + + "admin/uci/revert": { + "action": { + "type": "call", + "module": "luci.controller.admin.uci", + "function": "action_revert", + "post": true + } + }, + + "admin/uci/apply_rollback": { + "cors": true, + "action": { + "type": "call", + "module": "luci.controller.admin.uci", + "function": "action_apply_rollback", + "post": true + }, + "auth": { + "methods": [ "cookie:sysauth" ] + } + }, + + "admin/uci/apply_unchecked": { + "cors": true, + "action": { + "type": "call", + "module": "luci.controller.admin.uci", + "function": "action_apply_unchecked", + "post": true + }, + "auth": { + "methods": [ "cookie:sysauth" ] + } + }, + + "admin/uci/confirm": { + "cors": true, + "action": { + "type": "call", + "module": "luci.controller.admin.uci", + "function": "action_confirm" + }, + "auth": {} + } +} diff --git a/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json b/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json index 50ddc299f..e215cf945 100644 --- a/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json +++ b/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json @@ -38,7 +38,16 @@ "/proc/sys/kernel/hostname": [ "read" ], "/proc/sys/net/netfilter/nf_conntrack_*": [ "read" ], "/proc/mounts": [ "read" ], - "/usr/lib/lua/luci/version.lua": [ "read" ] + "/usr/lib/lua/luci/version.lua": [ "read" ], + "/bin/ping *": [ "exec" ], + "/bin/ping6 *": [ "exec" ], + "/bin/traceroute *": [ "exec" ], + "/bin/traceroute6 *": [ "exec" ], + "/usr/bin/ping *": [ "exec" ], + "/usr/bin/ping6 *": [ "exec" ], + "/usr/bin/traceroute *": [ "exec" ], + "/usr/bin/traceroute6 *": [ "exec" ], + "/usr/bin/nslookup *": [ "exec" ] }, "ubus": { "file": [ "list", "read", "stat" ], diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js index ab6779e14..9f1f8dc57 100644 --- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js @@ -2,8 +2,9 @@ 'require rpc'; 'require uci'; 'require form'; +'require validation'; -var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus; +var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status; callHostHints = rpc.declare({ object: 'luci-rpc', @@ -20,8 +21,7 @@ callDUIDHints = rpc.declare({ callDHCPLeases = rpc.declare({ object: 'luci-rpc', method: 'getDHCPLeases', - params: [ 'family' ], - expect: { dhcp_leases: [] } + expect: { '': {} } }); CBILeaseStatus = form.DummyValue.extend({ @@ -43,6 +43,86 @@ CBILeaseStatus = form.DummyValue.extend({ } }); +CBILease6Status = form.DummyValue.extend({ + renderWidget: function(section_id, option_id, cfgvalue) { + return E([ + E('h4', _('Active DHCPv6 Leases')), + E('div', { 'id': 'lease6_status_table', 'class': 'table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th' }, _('Host')), + E('div', { 'class': 'th' }, _('IPv6-Address')), + E('div', { 'class': 'th' }, _('DUID')), + E('div', { 'class': 'th' }, _('Leasetime remaining')) + ]), + E('div', { 'class': 'tr placeholder' }, [ + E('div', { 'class': 'td' }, E('em', _('Collecting data...'))) + ]) + ]) + ]); + } +}); + +function validateHostname(sid, s) { + if (s.length > 256) + return _('Expecting: %s').format(_('valid hostname')); + + var labels = s.replace(/^\.+|\.$/g, '').split(/\./); + + for (var i = 0; i < labels.length; i++) + if (!labels[i].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i)) + return _('Expecting: %s').format(_('valid hostname')); + + return true; +} + +function validateAddressList(sid, s) { + if (s == null || s == '') + return true; + + var m = s.match(/^\/(.+)\/$/), + names = m ? m[1].split(/\//) : [ s ]; + + for (var i = 0; i < names.length; i++) { + var res = validateHostname(sid, names[i]); + + if (res !== true) + return res; + } + + return true; +} + +function validateServerSpec(sid, s) { + if (s == null || s == '') + return true; + + var m = s.match(/^\/(.+)\/(.*)$/); + if (!m) + return _('Expecting: %s').format(_('valid hostname')); + + var res = validateAddressList(sid, m[1]); + if (res !== true) + return res; + + if (m[2] == '' || m[2] == '#') + return true; + + // ipaddr%scopeid#srvport@source@interface#srcport + + m = m[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/); + + if (!m) + return _('Expecting: %s').format(_('valid IP address')); + else if (validation.parseIPv4(m[1]) && m[3] != null && !validation.parseIPv4(m[3])) + return _('Expecting: %s').format(_('valid IPv4 address')); + else if (validation.parseIPv6(m[1]) && m[3] != null && !validation.parseIPv6(m[3])) + return _('Expecting: %s').format(_('valid IPv6 address')); + else if ((m[2] != null && +m[2] > 65535) || (m[4] != null && +m[4] > 65535)) + return _('Expecting: %s').format(_('valid port value')); + + return true; +} + return L.view.extend({ load: function() { return Promise.all([ @@ -52,7 +132,8 @@ return L.view.extend({ }, render: function(hosts_duids) { - var hosts = hosts_duids[0], + var has_dhcpv6 = L.hasSystemFeature('dnsmasq', 'dhcpv6') || L.hasSystemFeature('odhcpd'), + hosts = hosts_duids[0], duids = hosts_duids[1], m, s, o, ss, so; @@ -182,6 +263,7 @@ return L.view.extend({ o.optional = true; o.placeholder = '/example.org/10.1.2.3'; + o.validate = validateServerSpec; o = s.taboption('general', form.Flag, 'rebind_protection', @@ -204,8 +286,8 @@ return L.view.extend({ o.optional = true; o.depends('rebind_protection', '1'); - o.datatype = 'host(1)'; o.placeholder = 'ihost.netflix.com'; + o.validate = validateAddressList; o = s.taboption('advanced', form.Value, 'port', @@ -288,6 +370,7 @@ return L.view.extend({ o = s.taboption('general', form.Flag, 'nonwildcard', _('Non-wildcard'), _('Bind dynamically to interfaces rather than wildcard address (recommended as linux default)')); + o.default = o.enabled; o.optional = false; o.rmempty = true; @@ -399,9 +482,15 @@ return L.view.extend({ o = s.taboption('leases', CBILeaseStatus, '__status__'); + if (has_dhcpv6) + o = s.taboption('leases', CBILease6Status, '__status6__'); + return m.render().then(function(mapEl) { L.Poll.add(function() { - return callDHCPLeases(4).then(function(leases) { + return callDHCPLeases().then(function(leaseinfo) { + var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [], + leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : []; + cbi_update_table(mapEl.querySelector('#lease_status_table'), leases.map(function(lease) { var exp; @@ -421,6 +510,39 @@ return L.view.extend({ ]; }), E('em', _('There are no active leases'))); + + if (has_dhcpv6) { + cbi_update_table(mapEl.querySelector('#lease6_status_table'), + leases6.map(function(lease) { + var exp; + + if (lease.expires === false) + exp = E('em', _('unlimited')); + else if (lease.expires <= 0) + exp = E('em', _('expired')); + else + exp = '%t'.format(lease.expires); + + var hint = lease.macaddr ? hosts[lease.macaddr] : null, + name = hint ? (hint.name || hint.ipv4 || hint.ipv6) : null, + host = null; + + if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name) + host = '%s (%s)'.format(lease.hostname, name); + else if (lease.hostname) + host = lease.hostname; + else if (name) + host = name; + + return [ + host || '-', + lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr, + lease.duid, + exp + ]; + }), + E('em', _('There are no active leases'))); + } }); }); diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js new file mode 100644 index 000000000..ee2a46615 --- /dev/null +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js @@ -0,0 +1,137 @@ +'use strict'; +'require fs'; +'require ui'; +'require uci'; + +return L.view.extend({ + handleCommand: function(exec, args) { + var buttons = document.querySelectorAll('.diag-action > .cbi-button'); + + for (var i = 0; i < buttons.length; i++) + buttons[i].setAttribute('disabled', 'true'); + + return fs.exec(exec, args).then(function(res) { + var out = document.querySelector('.command-output'); + out.style.display = ''; + + L.dom.content(out, [ res.stdout || '', res.stderr || '' ]); + }).catch(function(err) { + ui.addNotification(null, E('p', [ err ])) + }).finally(function() { + for (var i = 0; i < buttons.length; i++) + buttons[i].removeAttribute('disabled'); + }); + }, + + handlePing: function(ev, cmd) { + var exec = cmd || 'ping', + addr = ev.currentTarget.parentNode.previousSibling.value, + args = (exec == 'ping') ? [ '-c', '5', '-W', '1', addr ] : [ '-c', '5', addr ]; + + return this.handleCommand(exec, args); + }, + + handleTraceroute: function(ev, cmd) { + var exec = cmd || 'traceroute', + addr = ev.currentTarget.parentNode.previousSibling.value, + args = (exec == 'traceroute') ? [ '-q', '1', '-w', '1', '-n', addr ] : [ '-q', '1', '-w', '2', '-n', addr ]; + + return this.handleCommand(exec, args); + }, + + handleNslookup: function(ev, cmd) { + var addr = ev.currentTarget.parentNode.previousSibling.value; + + return this.handleCommand('nslookup', [ addr ]); + }, + + load: function() { + return Promise.all([ + L.resolveDefault(fs.stat('/bin/ping6'), {}), + L.resolveDefault(fs.stat('/usr/bin/ping6'), {}), + L.resolveDefault(fs.stat('/bin/traceroute6'), {}), + L.resolveDefault(fs.stat('/usr/bin/traceroute6'), {}), + uci.load('luci') + ]); + }, + + render: function(res) { + var has_ping6 = res[0].path || res[1].path, + has_traceroute6 = res[2].path || res[3].path, + dns_host = uci.get('luci', 'diag', 'dns') || 'openwrt.org', + ping_host = uci.get('luci', 'diag', 'ping') || 'openwrt.org', + route_host = uci.get('luci', 'diag', 'route') || 'openwrt.org'; + + return E([], [ + E('h2', {}, [ _('Network Utilities') ]), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, [ + E('input', { + 'style': 'margin:5px 0', + 'type': 'text', + 'value': ping_host + }), + E('span', { 'class': 'diag-action' }, [ + has_ping6 ? new ui.ComboButton('ping', { + 'ping': '%s %s'.format(_('IPv4'), _('Ping')), + 'ping6': '%s %s'.format(_('IPv6'), _('Ping')), + }, { + 'click': ui.createHandlerFn(this, 'handlePing'), + 'classes': { + 'ping': 'cbi-button cbi-button-action', + 'ping6': 'cbi-button cbi-button-action' + } + }).render() : E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, 'handlePing') + }, [ _('Ping') ]) + ]) + ]), + + E('div', { 'class': 'td left' }, [ + E('input', { + 'style': 'margin:5px 0', + 'type': 'text', + 'value': route_host + }), + E('span', { 'class': 'diag-action' }, [ + has_traceroute6 ? new ui.ComboButton('traceroute', { + 'traceroute': '%s %s'.format(_('IPv4'), _('Traceroute')), + 'traceroute6': '%s %s'.format(_('IPv6'), _('Traceroute')), + }, { + 'click': ui.createHandlerFn(this, 'handleTraceroute'), + 'classes': { + 'traceroute': 'cbi-button cbi-button-action', + 'traceroute6': 'cbi-button cbi-button-action' + } + }).render() : E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, 'handleTraceroute') + }, [ _('Traceroute') ]) + ]) + ]), + + E('div', { 'class': 'td left' }, [ + E('input', { + 'style': 'margin:5px 0', + 'type': 'text', + 'value': dns_host + }), + E('span', { 'class': 'diag-action' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, 'handleNslookup') + }, [ _('Nslookup') ]) + ]) + ]) + ]) + ]), + E('pre', { 'class': 'command-output', 'style': 'display:none' }) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/modules/luci-mod-network/luasrc/controller/admin/network.lua b/modules/luci-mod-network/luasrc/controller/admin/network.lua index bd00235fa..109c59f2a 100644 --- a/modules/luci-mod-network/luasrc/controller/admin/network.lua +++ b/modules/luci-mod-network/luasrc/controller/admin/network.lua @@ -4,63 +4,6 @@ module("luci.controller.admin.network", package.seeall) -function index() - local page - --- if page.inreq then - page = entry({"admin", "network", "switch"}, view("network/switch"), _("Switch"), 20) - page.uci_depends = { network = { ["@switch[0]"] = "switch" } } - - page = entry({"admin", "network", "wireless"}, view("network/wireless"), _('Wireless'), 15) - page.uci_depends = { wireless = { ["@wifi-device[0]"] = "wifi-device" } } - page.leaf = true - - page = entry({"admin", "network", "remote_addr"}, call("remote_addr"), nil) - page.leaf = true - - page = entry({"admin", "network", "network"}, view("network/interfaces"), _("Interfaces"), 10) - page.leaf = true - page.subindex = true - - page = node("admin", "network", "dhcp") - page.uci_depends = { dhcp = true } - page.target = view("network/dhcp") - page.title = _("DHCP and DNS") - page.order = 30 - - page = node("admin", "network", "hosts") - page.uci_depends = { dhcp = true } - page.target = view("network/hosts") - page.title = _("Hostnames") - page.order = 40 - - page = node("admin", "network", "routes") - page.target = view("network/routes") - page.title = _("Static Routes") - page.order = 50 - - page = node("admin", "network", "diagnostics") - page.target = template("admin_network/diagnostics") - page.title = _("Diagnostics") - page.order = 60 - - page = entry({"admin", "network", "diag_ping"}, post("diag_ping"), nil) - page.leaf = true - - page = entry({"admin", "network", "diag_nslookup"}, post("diag_nslookup"), nil) - page.leaf = true - - page = entry({"admin", "network", "diag_traceroute"}, post("diag_traceroute"), nil) - page.leaf = true - - page = entry({"admin", "network", "diag_ping6"}, post("diag_ping6"), nil) - page.leaf = true - - page = entry({"admin", "network", "diag_traceroute6"}, post("diag_traceroute6"), nil) - page.leaf = true --- end -end - local function addr2dev(addr, src) local ip = require "luci.ip" local route = ip.route(addr, src) @@ -123,45 +66,3 @@ function remote_addr() luci.http.prepare_content("application/json") luci.http.write_json(result) end - -function diag_command(cmd, addr) - if addr and addr:match("^[a-zA-Z0-9%-%.:_]+$") then - luci.http.prepare_content("text/plain") - - local util = io.popen(cmd % luci.util.shellquote(addr)) - if util then - while true do - local ln = util:read("*l") - if not ln then break end - luci.http.write(ln) - luci.http.write("\n") - end - - util:close() - end - - return - end - - luci.http.status(500, "Bad address") -end - -function diag_ping(addr) - diag_command("ping -c 5 -W 1 %s 2>&1", addr) -end - -function diag_traceroute(addr) - diag_command("traceroute -q 1 -w 1 -n %s 2>&1", addr) -end - -function diag_nslookup(addr) - diag_command("nslookup %s 2>&1", addr) -end - -function diag_ping6(addr) - diag_command("ping6 -c 5 %s 2>&1", addr) -end - -function diag_traceroute6(addr) - diag_command("traceroute6 -q 1 -w 2 -n %s 2>&1", addr) -end diff --git a/modules/luci-mod-network/luasrc/view/admin_network/diagnostics.htm b/modules/luci-mod-network/luasrc/view/admin_network/diagnostics.htm deleted file mode 100644 index 03dd5aab2..000000000 --- a/modules/luci-mod-network/luasrc/view/admin_network/diagnostics.htm +++ /dev/null @@ -1,117 +0,0 @@ -<%# - Copyright 2010 Jo-Philipp Wich <jow@openwrt.org> - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> - -<% -local fs = require "nixio.fs" -local has_ping6 = fs.access("/bin/ping6") or fs.access("/usr/bin/ping6") -local has_traceroute6 = fs.access("/bin/traceroute6") or fs.access("/usr/bin/traceroute6") - -local dns_host = luci.config.diag and luci.config.diag.dns or "dev.openwrt.org" -local ping_host = luci.config.diag and luci.config.diag.ping or "dev.openwrt.org" -local route_host = luci.config.diag and luci.config.diag.route or "dev.openwrt.org" -%> - -<script type="text/javascript">//<![CDATA[ - var stxhr = new XHR(); - - function update_status(field, proto) - { - var tool = field.name; - var addr = field.value; - var protocol = proto ? "6" : ""; - - var legend = document.getElementById('diag-rc-legend'); - var output = document.getElementById('diag-rc-output'); - - if (legend && output) - { - output.innerHTML = - '<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" /> ' + - '<%:Waiting for command to complete...%>' - ; - - legend.parentNode.style.display = 'block'; - legend.style.display = 'inline'; - - stxhr.post('<%=url('admin/network')%>/diag_' + tool + protocol + '/' + addr, { token: '<%=token%>' }, - function(x) - { - if (x.responseText) - { - legend.style.display = 'none'; - output.innerHTML = String.format('<pre>%h</pre>', x.responseText); - } - else - { - legend.style.display = 'none'; - output.innerHTML = '<span class="error"><%:Bad address specified!%></span>'; - } - } - ); - } - } -//]]></script> - -<form method="post" action="<%=url('admin/network/diagnostics')%>"> - <div class="cbi-map"> - <h2 name="content"><%:Diagnostics%></h2> - - <div class="cbi-section"> - <legend><%:Network Utilities%></legend> - - <div class="table"> - <div class="tr"> - <div class="td left"> - <input style="margin: 5px 0" type="text" value="<%=ping_host%>" name="ping" /><br /> - <% if has_ping6 then %> - <span> - <select name="ping_proto" style="width:auto"> - <option value="" selected="selected"><%:IPv4%></option> - <option value="6"><%:IPv6%></option> - </select> - </span> - <input type="button" value="<%:Ping%>" class="cbi-button cbi-button-apply" onclick="update_status(this.form.ping, this.form.ping_proto.selectedIndex)" /> - <% else %> - <input type="button" value="<%:Ping%>" class="cbi-button cbi-button-apply" onclick="update_status(this.form.ping)" /> - <% end %> - </div> - - <div class="td left"> - <input style="margin: 5px 0" type="text" value="<%=route_host%>" name="traceroute" /><br /> - <% if has_traceroute6 then %> - <span> - <select name="traceroute_proto" style="width:auto"> - <option value="" selected="selected"><%:IPv4%></option> - <option value="6"><%:IPv6%></option> - </select> - </span> - <input type="button" value="<%:Traceroute%>" class="cbi-button cbi-button-apply" onclick="update_status(this.form.traceroute, this.form.traceroute_proto.selectedIndex)" /> - <% else %> - <input type="button" value="<%:Traceroute%>" class="cbi-button cbi-button-apply" onclick="update_status(this.form.traceroute)" /> - <% end %> - <% if not has_traceroute6 then %> - <p> </p> - <p><%:Install iputils-traceroute6 for IPv6 traceroute%></p> - <% end %> - </div> - - <div class="td left"> - <input style="margin: 5px 0" type="text" value="<%=dns_host%>" name="nslookup" /><br /> - <input type="button" value="<%:Nslookup%>" class="cbi-button cbi-button-apply" onclick="update_status(this.form.nslookup)" /> - </div> - </div> - </div> - </div> - </div> - - <div class="cbi-section" style="display:none"> - <strong id="diag-rc-legend"></strong> - <span id="diag-rc-output"></span> - </div> -</form> - -<%+footer%> diff --git a/modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json b/modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json new file mode 100644 index 000000000..670f2c1a4 --- /dev/null +++ b/modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json @@ -0,0 +1,85 @@ +{ + "admin/network/switch": { + "title": "Switch", + "order": 20, + "action": { + "type": "view", + "path": "network/switch" + }, + "depends": { + "fs": { "/sbin/swconfig": "executable" }, + "uci": { "network": { "@switch": true } } + } + }, + + "admin/network/wireless": { + "title": "Wireless", + "order": 15, + "action": { + "type": "view", + "path": "network/wireless" + }, + "depends": { + "uci": { "wireless": { "@wifi-device": true } } + } + }, + + "admin/network/remote_addr/*": { + "action": { + "type": "call", + "module": "luci.controller.admin.network", + "function": "remote_addr" + } + }, + + "admin/network/network": { + "title": "Interfaces", + "order": 10, + "action": { + "type": "view", + "path": "network/interfaces" + } + }, + + "admin/network/dhcp": { + "title": "DHCP and DNS", + "order": 30, + "action": { + "type": "view", + "path": "network/dhcp" + }, + "depends": { + "uci": { "dhcp": true } + } + }, + + "admin/network/hosts": { + "title": "Hostnames", + "order": 40, + "action": { + "type": "view", + "path": "network/hosts" + }, + "depends": { + "uci": { "dhcp": true } + } + }, + + "admin/network/routes": { + "title": "Static Routes", + "order": 50, + "action": { + "type": "view", + "path": "network/routes" + } + }, + + "admin/network/diagnostics": { + "title": "Diagnostics", + "order": 60, + "action": { + "type": "view", + "path": "network/diagnostics" + } + } +} diff --git a/modules/luci-mod-status/luasrc/controller/admin/status.lua b/modules/luci-mod-status/luasrc/controller/admin/status.lua index 6f8414922..2684bdf71 100644 --- a/modules/luci-mod-status/luasrc/controller/admin/status.lua +++ b/modules/luci-mod-status/luasrc/controller/admin/status.lua @@ -4,30 +4,6 @@ module("luci.controller.admin.status", package.seeall) -function index() - local page - - entry({"admin", "status", "overview"}, template("admin_status/index"), _("Overview"), 1) - - entry({"admin", "status", "iptables"}, template("admin_status/iptables"), _("Firewall"), 2).leaf = true - entry({"admin", "status", "iptables_dump"}, call("dump_iptables")).leaf = true - entry({"admin", "status", "iptables_action"}, post("action_iptables")).leaf = true - - entry({"admin", "status", "routes"}, template("admin_status/routes"), _("Routes"), 3) - entry({"admin", "status", "syslog"}, call("action_syslog"), _("System Log"), 4) - entry({"admin", "status", "dmesg"}, call("action_dmesg"), _("Kernel Log"), 5) - entry({"admin", "status", "processes"}, view("status/processes"), _("Processes"), 6) - - entry({"admin", "status", "realtime"}, alias("admin", "status", "realtime", "load"), _("Realtime Graphs"), 7) - - entry({"admin", "status", "realtime", "load"}, view("status/load"), _("Load"), 1) - entry({"admin", "status", "realtime", "bandwidth"}, view("status/bandwidth"), _("Traffic"), 2) - entry({"admin", "status", "realtime", "wireless"}, view("status/wireless"), _("Wireless"), 3).uci_depends = { wireless = true } - entry({"admin", "status", "realtime", "connections"}, view("status/connections"), _("Connections"), 4) - - entry({"admin", "status", "nameinfo"}, call("action_nameinfo")).leaf = true -end - function action_syslog() local syslog = luci.sys.syslog() luci.template.render("admin_status/syslog", {syslog=syslog}) diff --git a/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json b/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json new file mode 100644 index 000000000..03f7dce3b --- /dev/null +++ b/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json @@ -0,0 +1,129 @@ +{ + "admin/status/overview": { + "title": "Overview", + "order": 1, + "action": { + "type": "template", + "path": "admin_status/index" + } + }, + + "admin/status/iptables/*": { + "title": "Firewall", + "order": 2, + "action": { + "type": "template", + "path": "admin_status/iptables" + } + }, + + "admin/status/iptables_dump/*": { + "action": { + "type": "call", + "module": "luci.controller.admin.status", + "function": "dump_iptables" + } + }, + + "admin/status/iptables_action/*": { + "action": { + "type": "call", + "module": "luci.controller.admin.status", + "function": "action_iptables" + } + }, + + "admin/status/routes": { + "title": "Routes", + "order": 3, + "action": { + "type": "template", + "path": "admin_status/routes" + } + }, + + "admin/status/syslog": { + "title": "System Log", + "order": 4, + "action": { + "type": "call", + "module": "luci.controller.admin.status", + "function": "action_syslog" + } + }, + + "admin/status/dmesg": { + "title": "Kernel Log", + "order": 5, + "action": { + "type": "call", + "module": "luci.controller.admin.status", + "function": "action_dmesg" + } + }, + + "admin/status/processes": { + "title": "Processes", + "order": 6, + "action": { + "type": "view", + "path": "status/processes" + } + }, + + "admin/status/realtime": { + "title": "Realtime Graphs", + "order": 7, + "action": { + "type": "alias", + "path": "admin/status/realtime/load" + } + }, + + "admin/status/realtime/load": { + "title": "Load", + "order": 1, + "action": { + "type": "view", + "path": "status/load" + } + }, + + "admin/status/realtime/bandwidth": { + "title": "Traffic", + "order": 2, + "action": { + "type": "view", + "path": "status/bandwidth" + } + }, + + "admin/status/realtime/wireless": { + "title": "Wireless", + "order": 3, + "action": { + "type": "view", + "path": "status/wireless" + }, + "depends": { + "uci": { "wireless": { "@wifi-device": true } } + } + }, + + "admin/status/realtime/connections": { + "title": "Connections", + "order": 4, + "action": { + "type": "view", + "path": "status/connections" + } + }, + + "admin/status/nameinfo/*": { + "action": { + "type": "call", + "module": "luci.controller.admin.status", + "function": "action_nameinfo" + } + } +} diff --git a/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/footer.htm b/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/footer.htm index e0a41e1bc..ec6895f06 100644 --- a/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/footer.htm +++ b/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/footer.htm @@ -5,25 +5,12 @@ Licensed to the public under the Apache License 2.0. -%> -<% - local ver = require "luci.version" - local disp = require "luci.dispatcher" - local request = disp.context.path - local category = request[1] - local tree = disp.node() - local categories = disp.node_childs(tree) -%> +<% local ver = require "luci.version" %> + <footer> <a href="https://github.com/openwrt/luci">Powered by <%= ver.luciname %> (<%= ver.luciversion %>)</a> / <%= ver.distversion %> - <% if #categories > 1 then %> - <ul class="breadcrumb pull-right" id="modemenu"> - <% for i, r in ipairs(categories) do %> - <li<% if request[1] == r then %> class="active"<%end%>><a href="<%=controller%>/<%=r%>/"><%=striptags(translate(tree.nodes[r].title))%></a> <span class="divider">|</span></li> - <% end %> - </ul> - <% end %> + <ul class="breadcrumb pull-right" id="modemenu" style="display:none"></ul> </footer> - </div> </div> </body> </html> diff --git a/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/header.htm b/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/header.htm index de1fd73f0..56a1b230e 100644 --- a/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/header.htm +++ b/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/header.htm @@ -13,123 +13,10 @@ local boardinfo = util.ubus("system", "board") - local request = disp.context.path - local request2 = disp.context.request - - local category = request[1] - local cattree = category and disp.node(category) - - local leaf = request2[#request2] - - local tree = disp.node() local node = disp.context.dispatched - local categories = disp.node_childs(tree) - - local c = tree - local i, r - - -- tag all nodes leading to this page - for i, r in ipairs(request) do - if c.nodes and c.nodes[r] then - c = c.nodes[r] - c._menu_selected = true - end - end - -- send as HTML5 http.prepare_content("text/html") - - local function nodeurl(prefix, name, query) - local u = url(prefix, name) - if query then - u = u .. http.build_querystring(query) - end - return pcdata(u) - end - - local function render_tabmenu(prefix, node, level) - if not level then - level = 1 - end - - local childs = disp.node_childs(node) - if #childs > 0 then - if level > 2 then - write('<ul class="tabs">') - end - - local selected_node - local selected_name - local i, v - - for i, v in ipairs(childs) do - local nnode = node.nodes[v] - if nnode._menu_selected then - selected_node = nnode - selected_name = v - end - - if level > 2 then - write('<li class="tabmenu-item-%s %s"><a href="%s">%s</a></li>' %{ - v, (nnode._menu_selected or (node.leaf and v == leaf)) and 'active' or '', - nodeurl(prefix, v, nnode.query), - striptags(translate(nnode.title)) - }) - end - end - - if level > 2 then - write('</ul>') - end - - if selected_node then - render_tabmenu(prefix .. "/" .. selected_name, selected_node, level + 1) - end - end - end - - local function render_submenu(prefix, node) - local childs = disp.node_childs(node) - if #childs > 0 then - write('<ul class="dropdown-menu">') - - for i, r in ipairs(childs) do - local nnode = node.nodes[r] - write('<li><a href="%s">%s</a></li>' %{ - nodeurl(prefix, r, nnode.query), - striptags(translate(nnode.title)) - }) - end - - write('</ul>') - end - end - - local function render_topmenu() - local childs = disp.node_childs(cattree) - if #childs > 0 then - write('<ul class="nav">') - - for i, r in ipairs(childs) do - local nnode = cattree.nodes[r] - local grandchildren = disp.node_childs(nnode) - - if #grandchildren > 0 then - write('<li class="dropdown"><a class="menu" href="#">%s</a>' % striptags(translate(nnode.title))) - render_submenu(category .. "/" .. r, nnode) - write('</li>') - else - write('<li><a href="%s">%s</a></li>' %{ - nodeurl(category, r, nnode.query), - striptags(translate(nnode.title)) - }) - end - end - - write('</ul>') - end - end -%> <!DOCTYPE html> <html lang="<%=luci.i18n.context.lang%>"> @@ -149,6 +36,8 @@ <script src="<%=url('admin/translations', luci.i18n.context.lang)%><%# ?v=PKG_VERSION %>"></script> <script src="<%=resource%>/cbi.js"></script> <script src="<%=resource%>/xhr.js"></script> + + <% include("themes/bootstrap/json-menu") %> </head> <body class="lang_<%=luci.i18n.context.lang%> <% if node then %><%= striptags( node.title ) %><%- end %>" data-page="<%= table.concat(disp.context.requestpath, "-") %>"> @@ -156,7 +45,7 @@ <div class="fill"> <div class="container"> <a class="brand" href="#"><%=boardinfo.hostname or "?"%></a> - <% render_topmenu() %> + <ul class="nav" id="topmenu" style="display:none"></ul> <div class="pull-right"> <span id="xhr_poll_status" style="display:none" onclick="XHR.running() ? XHR.halt() : XHR.run()"> <span class="label success" id="xhr_poll_status_on"><%:Auto Refresh%> <%:on%></span> @@ -185,4 +74,4 @@ </div> </noscript> - <% if category then render_tabmenu(category, cattree) end %> + <div id="tabmenu" style="display:none"></div> diff --git a/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/json-menu.htm b/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/json-menu.htm new file mode 100644 index 000000000..b38406f65 --- /dev/null +++ b/themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/json-menu.htm @@ -0,0 +1,119 @@ +<script type="text/javascript"> + (function() { + function get_children(node) { + var children = []; + + for (var k in node.children) { + if (!node.children.hasOwnProperty(k)) + continue; + + if (!node.children[k].satisfied) + continue; + + if (!node.children[k].hasOwnProperty('title')) + continue; + + children.push(Object.assign(node.children[k], { name: k })); + } + + return children.sort(function(a, b) { + return ((a.order || 1000) - (b.order || 1000)); + }); + } + + function render_tabmenu(tree, url, level) { + var container = document.querySelector('#tabmenu'), + ul = E('ul', { 'class': 'tabs' }), + children = get_children(tree), + activeNode = null; + + for (var i = 0; i < children.length; i++) { + var isActive = (L.env.dispatchpath[3 + (level || 0)] == children[i].name), + activeClass = isActive ? ' active' : '', + className = 'tabmenu-item-%s %s'.format(children[i].name, activeClass); + + ul.appendChild(E('li', { 'class': className }, [ + E('a', { 'href': L.url(url, children[i].name) }, [ _(children[i].title) ] )])); + + if (isActive) + activeNode = children[i]; + } + + if (ul.children.length == 0) + return E([]); + + container.appendChild(ul); + container.style.display = ''; + + if (activeNode) + render_tabmenu(activeNode, url + '/' + activeNode.name, (level || 0) + 1); + + return ul; + } + + function render_mainmenu(tree, url, level) { + var ul = level ? E('ul', { 'class': 'dropdown-menu' }) : document.querySelector('#topmenu'), + children = get_children(tree); + + if (children.length == 0 || level > 1) + return E([]); + + for (var i = 0; i < children.length; i++) { + var submenu = render_mainmenu(children[i], url + '/' + children[i].name, (level || 0) + 1), + subclass = (!level && submenu.firstElementChild) ? 'dropdown' : null, + linkclass = (!level && submenu.firstElementChild) ? 'menu' : null, + linkurl = submenu.firstElementChild ? '#' : L.url(url, children[i].name); + + var li = E('li', { 'class': subclass }, [ + E('a', { 'class': linkclass, 'href': linkurl }, [ _(children[i].title) ]), + submenu + ]); + + ul.appendChild(li); + } + + ul.style.display = ''; + + return ul; + } + + function render_modemenu(tree) { + var ul = document.querySelector('#modemenu'), + children = get_children(tree); + + for (var i = 0; i < children.length; i++) { + var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0); + + ul.appendChild(E('li', { 'class': isActive ? 'active' : null }, [ + E('a', { 'href': L.url(children[i].name) }, [ _(children[i].title) ]), + ' ', + E('span', { 'class': 'divider' }, [ '|' ]) + ])); + + if (isActive) + render_mainmenu(children[i], children[i].name); + } + + if (ul.children.length > 1) + ul.style.display = ''; + } + + document.addEventListener('luci-loaded', function(ev) { + var tree = <%= luci.http.write_json(luci.dispatcher.context.authsession and luci.dispatcher.menu_json() or {}) %>, + node = tree, + url = ''; + + render_modemenu(tree); + + if (L.env.dispatchpath.length >= 3) { + for (var i = 0; i < 3 && node; i++) { + node = node.children[L.env.dispatchpath[i]]; + url = url + (url ? '/' : '') + L.env.dispatchpath[i]; + } + + if (node) + render_tabmenu(node, url); + } + }); + })(); +</script> diff --git a/themes/luci-theme-material/htdocs/luci-static/material/js/script.js b/themes/luci-theme-material/htdocs/luci-static/material/js/script.js index 755191f33..ae39d0075 100755 --- a/themes/luci-theme-material/htdocs/luci-static/material/js/script.js +++ b/themes/luci-theme-material/htdocs/luci-static/material/js/script.js @@ -18,6 +18,7 @@ * Licensed to the public under the Apache License 2.0 */ +document.addEventListener('luci-loaded', function(ev) { (function ($) { $(".main > .loading").fadeOut(); @@ -216,3 +217,4 @@ } })(jQuery); +}); diff --git a/themes/luci-theme-material/luasrc/view/themes/material/footer.htm b/themes/luci-theme-material/luasrc/view/themes/material/footer.htm index 544866dde..2f9f096bc 100644 --- a/themes/luci-theme-material/luasrc/view/themes/material/footer.htm +++ b/themes/luci-theme-material/luasrc/view/themes/material/footer.htm @@ -18,27 +18,11 @@ Licensed to the public under the Apache License 2.0 -%> -<% - local ver = require "luci.version" - local disp = require "luci.dispatcher" - local request = disp.context.path - local category = request[1] - local tree = disp.node() - local categories = disp.node_childs(tree) -%> +<% local ver = require "luci.version" %> </div> <footer class="mobile-hide"> <a href="https://github.com/openwrt/luci">Powered by <%= ver.luciname %> (<%= ver.luciversion %>)</a> / <%= ver.distversion %> - <% if #categories > 1 then %> - <ul class="breadcrumb pull-right" id="modemenu"> - <% for i, r in ipairs(categories) do %> - <li<% if request[1] == r then %> class="active"<%end%>> - <a href="<%=controller%>/<%=r%>/"><%=striptags(translate(tree.nodes[r].title))%></a> - <span class="divider">|</span> - </li> - <% end %> - </ul> - <% end %> + <ul class="breadcrumb pull-right" id="modemenu" style="display:none"></ul> </footer> </div> </div> diff --git a/themes/luci-theme-material/luasrc/view/themes/material/header.htm b/themes/luci-theme-material/luasrc/view/themes/material/header.htm index 76eeec05e..5595b14e4 100644 --- a/themes/luci-theme-material/luasrc/view/themes/material/header.htm +++ b/themes/luci-theme-material/luasrc/view/themes/material/header.htm @@ -26,135 +26,10 @@ local boardinfo = util.ubus("system", "board") - local request = disp.context.path - local request2 = disp.context.request - - local category = request[1] - local cattree = category and disp.node(category) - - local leaf = request2[#request2] - - local tree = disp.node() local node = disp.context.dispatched - local categories = disp.node_childs(tree) - - local c = tree - local i, r - - -- tag all nodes leading to this page - for i, r in ipairs(request) do - if c.nodes and c.nodes[r] then - c = c.nodes[r] - c._menu_selected = true - end - end - -- send as HTML5 http.prepare_content("text/html") - - local function nodeurl(prefix, name, query) - local u = url(prefix, name) - if query then - u = u .. http.build_querystring(query) - end - return pcdata(u) - end - - local function render_tabmenu(prefix, node, level) - if not level then - level = 1 - end - - local childs = disp.node_childs(node) - if #childs > 0 then - if level > 2 then - write('<ul class="tabs">') - end - - local selected_node - local selected_name - local i, v - - for i, v in ipairs(childs) do - local nnode = node.nodes[v] - if nnode._menu_selected then - selected_node = nnode - selected_name = v - end - - if level > 2 then - write('<li class="tabmenu-item-%s %s"><a href="%s">%s</a></li>' %{ - v, (nnode._menu_selected or (node.leaf and v == leaf)) and 'active' or '', - nodeurl(prefix, v, nnode.query), - striptags(translate(nnode.title)) - }) - end - end - - if level > 2 then - write('</ul>') - end - - if selected_node then - render_tabmenu(prefix .. "/" .. selected_name, selected_node, level + 1) - end - end - end - - local function render_submenu(prefix, node) - local childs = disp.node_childs(node) - if #childs > 0 then - write('<ul class="slide-menu">') - - for i, r in ipairs(childs) do - local nnode = node.nodes[r] - local title = striptags(translate(nnode.title)) - - write('<li><a data-title="%s" href="%s">%s</a></li>' %{ - title, - nodeurl(prefix, r, nnode.query), - title - }) - end - - write('</ul>') - end - end - - local function render_topmenu() - local childs = disp.node_childs(cattree) - if #childs > 0 then - write('<ul class="nav">') - - for i, r in ipairs(childs) do - local nnode = cattree.nodes[r] - local grandchildren = disp.node_childs(nnode) - - if #grandchildren > 0 then - local title = striptags(translate(nnode.title)) - - write('<li class="slide"><a class="menu" data-title="%s" href="#">%s</a>' %{ - title, - title - }) - - render_submenu(category .. "/" .. r, nnode) - write('</li>') - else - local title = striptags(translate(nnode.title)) - - write('<li><a data-title="%s" href="%s">%s</a></li>' %{ - title, - nodeurl(category, r, nnode.query), - title - }) - end - end - - write('</ul>') - end - end -%> <!DOCTYPE html> <html lang="<%=luci.i18n.context.lang%>"> @@ -180,6 +55,134 @@ <script src="<%=url('admin/translations', luci.i18n.context.lang)%><%# ?v=PKG_VERSION %>"></script> <script src="<%=resource%>/cbi.js"></script> <script src="<%=resource%>/xhr.js"></script> + <script type="text/javascript">//<![CDATA[ + (function() { + function get_children(node) { + var children = []; + + for (var k in node.children) { + if (!node.children.hasOwnProperty(k)) + continue; + + if (!node.children[k].satisfied) + continue; + + if (!node.children[k].hasOwnProperty('title')) + continue; + + children.push(Object.assign(node.children[k], { name: k })); + } + + return children.sort(function(a, b) { + return ((a.order || 1000) - (b.order || 1000)); + }); + } + + function render_mainmenu(tree, url, level) { + var l = (level || 0) + 1, + ul = E('ul', { 'class': level ? 'slide-menu' : 'nav' }), + children = get_children(tree); + + if (children.length == 0 || l > 2) + return E([]); + + for (var i = 0; i < children.length; i++) { + var submenu = render_mainmenu(children[i], url + '/' + children[i].name, l), + hasChildren = submenu.children.length; + + ul.appendChild(E('li', { 'class': hasChildren ? 'slide' : null }, [ + E('a', { + 'href': hasChildren ? '#' : L.url(url, children[i].name), + 'class': hasChildren ? 'menu' : null, + 'data-title': hasChildren ? null : _(children[i].title), + }, [ _(children[i].title) ]), + submenu + ])); + } + + if (l == 1) { + var container = document.querySelector('#mainmenu'); + + container.appendChild(ul); + container.style.display = ''; + } + + return ul; + } + + function render_modemenu(tree) { + var ul = document.querySelector('#modemenu'), + children = get_children(tree); + + for (var i = 0; i < children.length; i++) { + var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0); + + ul.appendChild(E('li', {}, [ + E('a', { + 'href': L.url(children[i].name), + 'class': isActive ? 'active' : null + }, [ _(children[i].title) ]) + ])); + + if (isActive) + render_mainmenu(children[i], children[i].name); + } + + if (ul.children.length > 1) + ul.style.display = ''; + } + + function render_tabmenu(tree, url, level) { + var container = document.querySelector('#tabmenu'), + l = (level || 0) + 1, + ul = E('ul', { 'class': 'tabs' }), + children = get_children(tree), + activeNode = null; + + if (children.length == 0) + return E([]); + + for (var i = 0; i < children.length; i++) { + var isActive = (L.env.dispatchpath[l + 2] == children[i].name), + activeClass = isActive ? ' active' : '', + className = 'tabmenu-item-%s %s'.format(children[i].name, activeClass); + + ul.appendChild(E('li', { 'class': className }, [ + E('a', { 'href': L.url(url, children[i].name) }, [ _(children[i].title) ] ) + ])); + + if (isActive) + activeNode = children[i]; + } + + container.appendChild(ul); + container.style.display = ''; + + if (activeNode) + container.appendChild(render_tabmenu(activeNode, url + '/' + activeNode.name, l)); + + return ul; + } + + document.addEventListener('luci-loaded', function(ev) { + var tree = <%= luci.http.write_json(luci.dispatcher.context.authsession and luci.dispatcher.menu_json() or {}) %>, + node = tree, + url = ''; + + render_modemenu(tree); + + if (L.env.dispatchpath.length >= 3) { + for (var i = 0; i < 3 && node; i++) { + node = node.children[L.env.dispatchpath[i]]; + url = url + (url ? '/' : '') + L.env.dispatchpath[i]; + } + + if (node) + render_tabmenu(node, url); + } + }); + })(); + //]]></script> </head> <body class="lang_<%=luci.i18n.context.lang%> <% if node then %><%= striptags( node.title ) %><% end %> <% if luci.dispatcher.context.authsession then %>logged-in<% end %>" data-page="<%= table.concat(disp.context.requestpath, "-") %>"> <header> @@ -199,9 +202,7 @@ </header> <div class="main"> <div style="" class="loading"><span><div class="loading-img"></div><%:Collecting data...%></span></div> - <div class="main-left"> - <% render_topmenu() %> - </div> + <div class="main-left" id="mainmenu" style="display:none"></div> <div class="main-right"> <div class="darkMask"></div> <div id="maincontent"> @@ -223,4 +224,4 @@ </div> </noscript> - <% if category then render_tabmenu(category, cattree) end %> + <div id="tabmenu" style="display:none"></div> diff --git a/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css b/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css index faaaf220a..fbe6b9e0b 100644 --- a/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css +++ b/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css @@ -1017,6 +1017,10 @@ ul.cbi-tabmenu { border-bottom: 1px solid #bbb; } +#tabmenu > ul.cbi-tabmenu { + margin: 0 !important; +} + ul.cbi-tabmenu li { display: inline-flex; margin: 0 5px -1px 0; diff --git a/themes/luci-theme-openwrt/luasrc/view/themes/openwrt.org/header.htm b/themes/luci-theme-openwrt/luasrc/view/themes/openwrt.org/header.htm index fbe030d18..9754e8b61 100644 --- a/themes/luci-theme-openwrt/luasrc/view/themes/openwrt.org/header.htm +++ b/themes/luci-theme-openwrt/luasrc/view/themes/openwrt.org/header.htm @@ -15,116 +15,9 @@ local loadinfo = sysinfo.load or { 0, 0, 0 } local boardinfo = util.ubus("system", "board") or { } - local request = disp.context.path - local request2 = disp.context.request - - local category = request[1] - local cattree = category and disp.node(category) - - local leaf = request2[#request2] - - local tree = disp.node() local node = disp.context.dispatched - local categories = disp.node_childs(tree) - - local c = tree - local i, r - - -- tag all nodes leading to this page - for i, r in ipairs(request) do - if c.nodes and c.nodes[r] then - c = c.nodes[r] - c._menu_selected = true - end - end - http.prepare_content("application/xhtml+xml") - - local function nodeurl(prefix, name, query) - local u = url(prefix, name) - if query then - u = u .. http.build_querystring(query) - end - return pcdata(u) - end - - local function render_menu(prefix, node, level) - if not level then - level = 1 - end - - local childs = disp.node_childs(node) - if #childs > 0 then - write('<ul class="mainmenu l%d">' % level) - - local i, v - for i, v in ipairs(childs) do - local nnode = node.nodes[v] - - write('<li class="mainmenu-item-%s %s"><a href="%s">%s</a>' %{ - v, (nnode._menu_selected or (node.leaf and v == leaf)) and 'selected' or '', - nodeurl(prefix, v, nnode.query), - striptags(translate(nnode.title)) - }) - - if level < 2 then - render_menu(prefix .. "/" .. v, nnode, level + 1) - end - - write('</li>') - end - - write('</ul>') - end - end - - local function render_tabmenu(prefix, node, level) - if not level then - level = 1 - end - - local childs = disp.node_childs(node) - if #childs > 0 then - if level > 2 then - if level == 3 then - write('<div id="tabmenu">') - end - write('<ul class="cbi-tabmenu">') - end - - local selected_node - local selected_name - local i, v - - for i, v in ipairs(childs) do - local nnode = node.nodes[v] - if nnode._menu_selected then - selected_node = nnode - selected_name = v - end - - if level > 2 then - write('<li class="tabmenu-item-%s %s"><a href="%s">%s</a></li>' %{ - v, (nnode._menu_selected or (node.leaf and v == leaf)) and 'cbi-tab' or '', - nodeurl(prefix, v, nnode.query), - striptags(translate(nnode.title)) - }) - end - end - - if level > 2 then - write('</ul>') - if level == 3 then - write('</div>') - end - end - - if selected_node then - render_tabmenu(prefix .. "/" .. selected_name, selected_node, level + 1) - end - end - end -%> <?xml version="1.0" encoding="utf-8"?> @@ -145,45 +38,153 @@ <script type="text/javascript" src="<%=resource%>/cbi.js"></script> <script type="text/javascript" src="<%=resource%>/xhr.js"></script> <script type="text/javascript">//<![CDATA[ - document.addEventListener('DOMContentLoaded', function() { - var event = ('ontouchstart' in window) ? 'touchstart' : 'click'; + (function() { + function get_children(node) { + var children = []; - document.querySelectorAll('ul.mainmenu.l1 > li > a').forEach(function(a) { - a.addEventListener(event, function(ev) { - var a = ev.target, ul1 = a.parentNode.parentNode, ul2 = a.nextElementSibling; + for (var k in node.children) { + if (!node.children.hasOwnProperty(k)) + continue; - document.querySelectorAll('ul.mainmenu.l1 > li.active').forEach(function(li) { - if (li !== a.parentNode) - li.classList.remove('active'); - }); + if (!node.children[k].satisfied) + continue; - if (!ul2) - return; + if (!node.children[k].hasOwnProperty('title')) + continue; - if (ul2.parentNode.offsetLeft + ul2.offsetWidth <= ul1.offsetLeft + ul1.offsetWidth) - ul2.classList.add('align-left'); + children.push(Object.assign(node.children[k], { name: k })); + } - ul1.classList.add('active'); - a.parentNode.classList.add('active'); - a.blur(); + return children.sort(function(a, b) { + return ((a.order || 1000) - (b.order || 1000)); + }); + } + + function handle_mainmenu_expand(ev) { + var a = ev.target, ul1 = a.parentNode.parentNode, ul2 = a.nextElementSibling; - ev.preventDefault(); - ev.stopPropagation(); + document.querySelectorAll('ul.mainmenu.l1 > li.active').forEach(function(li) { + if (li !== a.parentNode) + li.classList.remove('active'); }); - }); - document.addEventListener(event, function(ev) { - var t = ev.target; + if (!ul2) + return; + + if (ul2.parentNode.offsetLeft + ul2.offsetWidth <= ul1.offsetLeft + ul1.offsetWidth) + ul2.classList.add('align-left'); - while (t && t.id != 'mainmenu') - t = t.parentNode; + ul1.classList.add('active'); + a.parentNode.classList.add('active'); + a.blur(); - if (!t) - document.querySelectorAll('ul.mainmenu > li.active').forEach(function(li) { - li.classList.remove('active'); - }); + ev.preventDefault(); + ev.stopPropagation(); + } + + function render_mainmenu(tree, url, level) { + var l = (level || 0) + 1, + ul = E('ul', { 'class': 'mainmenu l%d'.format(l) }), + children = get_children(tree); + + if (children.length == 0 || l > 2) + return E([]); + + for (var i = 0; i < children.length; i++) { + var isActive = (L.env.dispatchpath[l] == children[i].name), + activeClass = 'mainmenu-item-%s%s'.format(children[i].name, isActive ? ' selected' : ''); + + ul.appendChild(E('li', { 'class': activeClass }, [ + E('a', { + 'href': L.url(url, children[i].name), + 'click': (l == 1) ? handle_mainmenu_expand : null, + }, [ _(children[i].title) ]), + render_mainmenu(children[i], url + '/' + children[i].name, l) + ])); + } + + if (l == 1) { + var container = document.querySelector('#mainmenu'); + + container.appendChild(ul); + container.style.display = ''; + } + + return ul; + } + + function render_modemenu(tree) { + var ul = document.querySelector('#modemenu'), + children = get_children(tree); + + for (var i = 0; i < children.length; i++) { + var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0); + + ul.appendChild(E('li', {}, [ + E('a', { + 'href': L.url(children[i].name), + 'class': isActive ? 'active' : null + }, [ _(children[i].title) ]) + ])); + + if (isActive) + render_mainmenu(children[i], children[i].name); + } + + if (ul.children.length > 1) + ul.style.display = ''; + } + + function render_tabmenu(tree, url, level) { + var container = document.querySelector('#tabmenu'), + l = (level || 0) + 1, + ul = E('ul', { 'class': 'cbi-tabmenu' }), + children = get_children(tree), + activeNode = null; + + if (children.length == 0) + return E([]); + + for (var i = 0; i < children.length; i++) { + var isActive = (L.env.dispatchpath[l + 2] == children[i].name), + activeClass = isActive ? ' cbi-tab' : '', + className = 'tabmenu-item-%s %s'.format(children[i].name, activeClass); + + ul.appendChild(E('li', { 'class': className }, [ + E('a', { 'href': L.url(url, children[i].name) }, [ _(children[i].title) ] ) + ])); + + if (isActive) + activeNode = children[i]; + } + + container.appendChild(ul); + container.style.display = ''; + + if (activeNode) + container.appendChild(render_tabmenu(activeNode, url + '/' + activeNode.name, l)); + + return ul; + } + + document.addEventListener('luci-loaded', function(ev) { + var tree = <%= luci.http.write_json(luci.dispatcher.context.authsession and luci.dispatcher.menu_json() or {}) %>, + node = tree, + url = ''; + + render_modemenu(tree); + + if (L.env.dispatchpath.length >= 3) { + for (var i = 0; i < 3 && node; i++) { + node = node.children[L.env.dispatchpath[i]]; + url = url + (url ? '/' : '') + L.env.dispatchpath[i]; + } + + if (node) + render_tabmenu(node, url); + } }); - }); + })(); //]]></script> <title><%=striptags( (boardinfo.hostname or "?") .. ( (node and node.title) and ' - ' .. translate(node.title) or '')) %> - LuCI</title> </head> @@ -207,24 +208,16 @@ </span> </div> -<% if #categories > 1 then %> - <ul id="modemenu"> - <% for i, r in ipairs(categories) do %> - <li><a<% if request[1] == r then %> class="active"<%end%> href="<%=controller%>/<%=r%>/"><%=striptags(translate(tree.nodes[r].title))%></a></li> - <% end %> - </ul> -<% end %> +<ul id="modemenu" style="display:none"></ul> <div class="clear"></div> </div> <div id="maincontainer"> - <div id="mainmenu"> - <% if category then render_menu(category, cattree) end %> - </div> + <div id="mainmenu" style="display:none"></div> <div id="maincontent"> - <% if category then render_tabmenu(category, cattree) end %> + <div id="tabmenu" style="display:none"></div> <noscript> <div class="alert-message warning"> |