summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--applications/luci-app-firewall/luasrc/controller/firewall.lua19
-rw-r--r--applications/luci-app-firewall/root/usr/share/luci/menu.d/luci-app-firewall.json50
-rw-r--r--applications/luci-app-ocserv/luasrc/model/cbi/ocserv/main.lua31
-rw-r--r--applications/luci-app-opkg/luasrc/controller/opkg.lua10
-rw-r--r--applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json44
-rwxr-xr-xbuild/i18n-scan.pl121
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js12
-rw-r--r--modules/luci-base/luasrc/controller/admin/index.lua79
-rw-r--r--modules/luci-base/luasrc/controller/admin/uci.lua26
-rw-r--r--modules/luci-base/luasrc/dispatcher.lua1168
-rw-r--r--modules/luci-base/luasrc/view/header.htm5
-rw-r--r--modules/luci-base/root/usr/share/luci/menu.d/luci-base.json142
-rw-r--r--modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json11
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js134
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js137
-rw-r--r--modules/luci-mod-network/luasrc/controller/admin/network.lua99
-rw-r--r--modules/luci-mod-network/luasrc/view/admin_network/diagnostics.htm117
-rw-r--r--modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json85
-rw-r--r--modules/luci-mod-status/luasrc/controller/admin/status.lua24
-rw-r--r--modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json129
-rw-r--r--themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/footer.htm19
-rw-r--r--themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/header.htm119
-rw-r--r--themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/json-menu.htm119
-rwxr-xr-xthemes/luci-theme-material/htdocs/luci-static/material/js/script.js2
-rw-r--r--themes/luci-theme-material/luasrc/view/themes/material/footer.htm20
-rw-r--r--themes/luci-theme-material/luasrc/view/themes/material/header.htm259
-rw-r--r--themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css4
-rw-r--r--themes/luci-theme-openwrt/luasrc/view/themes/openwrt.org/header.htm285
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>&#160;</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">