diff --git a/luci-base/Makefile b/luci-base/Makefile index 9ce3b39f5..32a8e2f5d 100644 --- a/luci-base/Makefile +++ b/luci-base/Makefile @@ -14,13 +14,17 @@ LUCI_BASENAME:=base LUCI_TITLE:=LuCI core libraries LUCI_DEPENDS:=+lua +luci-lib-nixio +luci-lib-ip +rpcd +libubus-lua +luci-lib-jsonc +liblucihttp-lua +LUCI_LUASRCDIET_VERSION:=1.0.0 -PKG_SOURCE:=v1.0.0.tar.gz -PKG_SOURCE_URL:=https://github.com/jirutka/luasrcdiet/archive/ -PKG_HASH:=48162e63e77d009f5848f18a5cabffbdfc867d0e5e73c6d407f6af5d6880151b +PKG_SOURCE_URL:=https://github.com/jirutka/luasrcdiet.git +PKG_SOURCE_VERSION:=f138fc9359821d9201cd6b57cfa2fcbed5b9af97 +PKG_SOURCE_SUBDIR:=luasrcdiet-$(LUCI_LUASRCDIET_VERSION) +PKG_SOURCE_PROTO:=git +PKG_SOURCE:=$(PKG_SOURCE_SUBDIR).tar.gz +PKG_MIRROR_HASH:=a5c9d098549fbef618e6022b701e66c8c6fb16c910e63219adad3a4e71341f72 PKG_LICENSE:=MIT -HOST_BUILD_DIR:=$(BUILD_DIR_HOST)/luasrcdiet-1.0.0 +HOST_BUILD_DIR:=$(BUILD_DIR_HOST)/$(PKG_SOURCE_SUBDIR) include $(INCLUDE_DIR)/host-build.mk @@ -36,13 +40,14 @@ define Host/Configure endef define Host/Compile - $(MAKE) -C src/ clean po2lmo + $(MAKE) -C src/ clean po2lmo jsmin endef define Host/Install $(INSTALL_DIR) $(1)/bin $(INSTALL_DIR) $(1)/lib/lua/5.1 $(INSTALL_BIN) src/po2lmo $(1)/bin/po2lmo + $(INSTALL_BIN) src/jsmin $(1)/bin/jsmin $(INSTALL_BIN) $(HOST_BUILD_DIR)/bin/luasrcdiet $(1)/bin/luasrcdiet $(CP) $(HOST_BUILD_DIR)/luasrcdiet $(1)/lib/lua/5.1/ endef diff --git a/luci-base/htdocs/luci-static/resources/cbi.js b/luci-base/htdocs/luci-static/resources/cbi.js index fcfc50694..67ddc6af3 100644 --- a/luci-base/htdocs/luci-static/resources/cbi.js +++ b/luci-base/htdocs/luci-static/resources/cbi.js @@ -2,7 +2,7 @@ LuCI - Lua Configuration Interface Copyright 2008 Steven Barth - Copyright 2008-2012 Jo-Philipp Wich + Copyright 2008-2018 Jo-Philipp Wich Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -12,9 +12,92 @@ */ var cbi_d = []; -var cbi_t = []; var cbi_strings = { path: {}, label: {} }; +function s8(bytes, off) { + var n = bytes[off]; + return (n > 0x7F) ? (n - 256) >>> 0 : n; +} + +function u16(bytes, off) { + return ((bytes[off + 1] << 8) + bytes[off]) >>> 0; +} + +function sfh(s) { + if (s === null || s.length === 0) + return null; + + var bytes = []; + + for (var i = 0; i < s.length; i++) { + var ch = s.charCodeAt(i); + + if (ch <= 0x7F) + bytes.push(ch); + else if (ch <= 0x7FF) + bytes.push(((ch >>> 6) & 0x1F) | 0xC0, + ( ch & 0x3F) | 0x80); + else if (ch <= 0xFFFF) + bytes.push(((ch >>> 12) & 0x0F) | 0xE0, + ((ch >>> 6) & 0x3F) | 0x80, + ( ch & 0x3F) | 0x80); + else if (code <= 0x10FFFF) + bytes.push(((ch >>> 18) & 0x07) | 0xF0, + ((ch >>> 12) & 0x3F) | 0x80, + ((ch >> 6) & 0x3F) | 0x80, + ( ch & 0x3F) | 0x80); + } + + if (!bytes.length) + return null; + + var hash = (bytes.length >>> 0), + len = (bytes.length >>> 2), + off = 0, tmp; + + while (len--) { + hash += u16(bytes, off); + tmp = ((u16(bytes, off + 2) << 11) ^ hash) >>> 0; + hash = ((hash << 16) ^ tmp) >>> 0; + hash += hash >>> 11; + off += 4; + } + + switch ((bytes.length & 3) >>> 0) { + case 3: + hash += u16(bytes, off); + hash = (hash ^ (hash << 16)) >>> 0; + hash = (hash ^ (s8(bytes, off + 2) << 18)) >>> 0; + hash += hash >>> 11; + break; + + case 2: + hash += u16(bytes, off); + hash = (hash ^ (hash << 11)) >>> 0; + hash += hash >>> 17; + break; + + case 1: + hash += s8(bytes, off); + hash = (hash ^ (hash << 10)) >>> 0; + hash += hash >>> 1; + break; + } + + hash = (hash ^ (hash << 3)) >>> 0; + hash += hash >>> 5; + hash = (hash ^ (hash << 4)) >>> 0; + hash += hash >>> 17; + hash = (hash ^ (hash << 25)) >>> 0; + hash += hash >>> 6; + + return (0x100000000 + hash).toString(16).substr(1); +} + +function _(s) { + return (window.TR && TR[sfh(s)]) || s; +} + function Int(x) { return (/^-?\d+$/.test(x) ? +x : NaN); } @@ -55,7 +138,8 @@ function IPv6(x) { var prefix = (prefix_suffix[0] || '0').split(/:/); var suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : []; - if (suffix.length ? (prefix.length + suffix.length > 7) : (prefix.length > 8)) + if (suffix.length ? (prefix.length + suffix.length > 7) + : ((prefix_suffix.length < 2 && prefix.length < 8) || prefix.length > 8)) return null; var i, word; @@ -79,366 +163,475 @@ function IPv6(x) { return words; } -var cbi_validators = { +var CBIValidatorPrototype = { + apply: function(name, value, args) { + var func; - 'integer': function() - { - return !!Int(this); + if (typeof(name) === 'function') + func = name; + else if (typeof(this.types[name]) === 'function') + func = this.types[name]; + else + return false; + + if (value !== undefined && value !== null) + this.value = value; + + return func.apply(this, args); }, - 'uinteger': function() - { - return (Int(this) >= 0); + assert: function(condition, message) { + if (!condition) { + this.field.classList.add('cbi-input-invalid'); + this.error = message; + return false; + } + + this.field.classList.remove('cbi-input-invalid'); + this.error = null; + return true; }, - 'float': function() - { - return !!Dec(this); + compile: function(code) { + var pos = 0; + var esc = false; + var depth = 0; + var stack = [ ]; + + code += ','; + + for (var i = 0; i < code.length; i++) { + if (esc) { + esc = false; + continue; + } + + switch (code.charCodeAt(i)) + { + case 92: + esc = true; + break; + + case 40: + case 44: + if (depth <= 0) { + if (pos < i) { + var label = code.substring(pos, i); + label = label.replace(/\\(.)/g, '$1'); + label = label.replace(/^[ \t]+/g, ''); + label = label.replace(/[ \t]+$/g, ''); + + if (label && !isNaN(label)) { + stack.push(parseFloat(label)); + } + else if (label.match(/^(['"]).*\1$/)) { + stack.push(label.replace(/^(['"])(.*)\1$/, '$2')); + } + else if (typeof this.types[label] == 'function') { + stack.push(this.types[label]); + stack.push(null); + } + else { + throw "Syntax error, unhandled token '"+label+"'"; + } + } + + pos = i+1; + } + + depth += (code.charCodeAt(i) == 40); + break; + + case 41: + if (--depth <= 0) { + if (typeof stack[stack.length-2] != 'function') + throw "Syntax error, argument list follows non-function"; + + stack[stack.length-1] = this.compile(code.substring(pos, i)); + pos = i+1; + } + + break; + } + } + + return stack; }, - 'ufloat': function() - { - return (Dec(this) >= 0); - }, - - 'ipaddr': function() - { - return cbi_validators.ip4addr.apply(this) || - cbi_validators.ip6addr.apply(this); - }, - - 'ip4addr': function() - { - var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|\/(\d{1,2}))?$/); - return !!(m && IPv4(m[1]) && (m[2] ? IPv4(m[2]) : (m[3] ? cbi_validators.ip4prefix.apply(m[3]) : true))); - }, - - 'ip6addr': function() - { - var m = this.match(/^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/); - return !!(m && IPv6(m[1]) && (m[2] ? cbi_validators.ip6prefix.apply(m[2]) : true)); - }, - - 'ip4prefix': function() - { - return !isNaN(this) && this >= 0 && this <= 32; - }, - - 'ip6prefix': function() - { - return !isNaN(this) && this >= 0 && this <= 128; - }, - - 'cidr': function() - { - return cbi_validators.cidr4.apply(this) || - cbi_validators.cidr6.apply(this); - }, - - 'cidr4': function() - { - var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/); - return !!(m && IPv4(m[1]) && cbi_validators.ip4prefix.apply(m[2])); - }, - - 'cidr6': function() - { - var m = this.match(/^([0-9a-fA-F:.]+)\/(\d{1,3})$/); - return !!(m && IPv6(m[1]) && cbi_validators.ip6prefix.apply(m[2])); - }, - - 'ipnet4': function() - { - var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/); - return !!(m && IPv4(m[1]) && IPv4(m[2])); - }, - - 'ipnet6': function() - { - var m = this.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/); - return !!(m && IPv6(m[1]) && IPv6(m[2])); - }, - - 'ip6hostid': function() - { - if (this == "eui64" || this == "random") + validate: function() { + /* element is detached */ + if (!findParent(this.field, 'form')) return true; - var v6 = IPv6(this); - return !(!v6 || v6[0] || v6[1] || v6[2] || v6[3]); - }, + this.field.classList.remove('cbi-input-invalid'); + this.value = matchesElem(this.field, 'select') ? this.field.options[this.field.selectedIndex].value : this.field.value; + this.error = null; - 'ipmask': function() - { - return cbi_validators.ipmask4.apply(this) || - cbi_validators.ipmask6.apply(this); - }, + var valid; - 'ipmask4': function() - { - return cbi_validators.cidr4.apply(this) || - cbi_validators.ipnet4.apply(this) || - cbi_validators.ip4addr.apply(this); - }, + if (this.value.length === 0) + valid = this.assert(this.optional, _('non-empty value')); + else + valid = this.vstack[0].apply(this, this.vstack[1]); - 'ipmask6': function() - { - return cbi_validators.cidr6.apply(this) || - cbi_validators.ipnet6.apply(this) || - cbi_validators.ip6addr.apply(this); - }, - - 'port': function() - { - var p = Int(this); - return (p >= 0 && p <= 65535); - }, - - 'portrange': function() - { - if (this.match(/^(\d+)-(\d+)$/)) - { - var p1 = +RegExp.$1; - var p2 = +RegExp.$2; - return (p1 <= p2 && p2 <= 65535); + if (!valid) { + this.field.setAttribute('data-tooltip', _('Expecting %s').format(this.error)); + this.field.setAttribute('data-tooltip-style', 'error'); + this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true })); + } + else { + this.field.removeAttribute('data-tooltip'); + this.field.removeAttribute('data-tooltip-style'); + this.field.dispatchEvent(new CustomEvent('validation-success', { bubbles: true })); } - return cbi_validators.port.apply(this); + return valid; }, - 'macaddr': function() - { - return (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null); - }, + types: { + integer: function() { + return this.assert(Int(this.value) !== NaN, _('valid integer value')); + }, - 'host': function(ipv4only) - { - return cbi_validators.hostname.apply(this) || - ((ipv4only != 1) && cbi_validators.ipaddr.apply(this)) || - ((ipv4only == 1) && cbi_validators.ip4addr.apply(this)); - }, + uinteger: function() { + return this.assert(Int(this.value) >= 0, _('positive integer value')); + }, - 'hostname': function(strict) - { - if (this.length <= 253) - return (this.match(/^[a-zA-Z0-9_]+$/) != null || - (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) && - this.match(/[^0-9.]/))) && - (!strict || !this.match(/^_/)); + float: function() { + return this.assert(Dec(this.value) !== NaN, _('valid decimal value')); + }, - return false; - }, + ufloat: function() { + return this.assert(Dec(this.value) >= 0, _('positive decimal value')); + }, - 'network': function() - { - return cbi_validators.uciname.apply(this) || - cbi_validators.host.apply(this); - }, + ipaddr: function(nomask) { + return this.assert(this.apply('ip4addr', null, [nomask]) || this.apply('ip6addr', null, [nomask]), + nomask ? _('valid IP address') : _('valid IP address or prefix')); + }, - 'hostport': function(ipv4only) - { - var hp = this.split(/:/); + ip4addr: function(nomask) { + var re = nomask ? /^(\d+\.\d+\.\d+\.\d+)$/ : /^(\d+\.\d+\.\d+\.\d+)(?:\/(\d+\.\d+\.\d+\.\d+)|\/(\d{1,2}))?$/, + m = this.value.match(re); - if (hp.length == 2) - return (cbi_validators.host.apply(hp[0], ipv4only) && - cbi_validators.port.apply(hp[1])); + return this.assert(m && IPv4(m[1]) && (m[2] ? IPv4(m[2]) : (m[3] ? this.apply('ip4prefix', m[3]) : true)), + nomask ? _('valid IPv4 address') : _('valid IPv4 address or network')); + }, - return false; - }, + ip6addr: function(nomask) { + var re = nomask ? /^([0-9a-fA-F:.]+)$/ : /^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/, + m = this.value.match(re); - 'ip4addrport': function() - { - var hp = this.split(/:/); + return this.assert(m && IPv6(m[1]) && (m[2] ? this.apply('ip6prefix', m[2]) : true), + nomask ? _('valid IPv6 address') : _('valid IPv6 address or prefix')); + }, - if (hp.length == 2) - return (cbi_validators.ipaddr.apply(hp[0]) && - cbi_validators.port.apply(hp[1])); - return false; - }, + ip4prefix: function() { + return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 32, + _('valid IPv4 prefix value (0-32)')); + }, - 'ipaddrport': function(bracket) - { - if (this.match(/^([^\[\]:]+):([^:]+)$/)) { - var addr = RegExp.$1 - var port = RegExp.$2 - return (cbi_validators.ip4addr.apply(addr) && - cbi_validators.port.apply(port)); - } else if ((bracket == 1) && (this.match(/^\[(.+)\]:([^:]+)$/))) { - var addr = RegExp.$1 - var port = RegExp.$2 - return (cbi_validators.ip6addr.apply(addr) && - cbi_validators.port.apply(port)); - } else if ((bracket != 1) && (this.match(/^([^\[\]]+):([^:]+)$/))) { - var addr = RegExp.$1 - var port = RegExp.$2 - return (cbi_validators.ip6addr.apply(addr) && - cbi_validators.port.apply(port)); - } else { - return false; - } - }, + ip6prefix: function() { + return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 128, + _('valid IPv6 prefix value (0-128)')); + }, - 'wpakey': function() - { - var v = this; + cidr: function() { + return this.assert(this.apply('cidr4') || this.apply('cidr6'), _('valid IPv4 or IPv6 CIDR')); + }, - if( v.length == 64 ) - return (v.match(/^[a-fA-F0-9]{64}$/) != null); - else - return (v.length >= 8) && (v.length <= 63); - }, + cidr4: function() { + var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/); + return this.assert(m && IPv4(m[1]) && this.apply('ip4prefix', m[2]), _('valid IPv4 CIDR')); + }, - 'wepkey': function() - { - var v = this; + cidr6: function() { + var m = this.value.match(/^([0-9a-fA-F:.]+)\/(\d{1,3})$/); + return this.assert(m && IPv6(m[1]) && this.apply('ip6prefix', m[2]), _('valid IPv6 CIDR')); + }, - if ( v.substr(0,2) == 's:' ) - v = v.substr(2); + ipnet4: function() { + var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/); + return this.assert(m && IPv4(m[1]) && IPv4(m[2]), _('IPv4 network in address/netmask notation')); + }, - if( (v.length == 10) || (v.length == 26) ) - return (v.match(/^[a-fA-F0-9]{10,26}$/) != null); - else - return (v.length == 5) || (v.length == 13); - }, + ipnet6: function() { + var m = this.value.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/); + return this.assert(m && IPv6(m[1]) && IPv6(m[2]), _('IPv6 network in address/netmask notation')); + }, - 'uciname': function() - { - return (this.match(/^[a-zA-Z0-9_]+$/) != null); - }, - - 'range': function(min, max) - { - var val = Dec(this); - return (val >= +min && val <= +max); - }, - - 'min': function(min) - { - return (Dec(this) >= +min); - }, - - 'max': function(max) - { - return (Dec(this) <= +max); - }, - - 'rangelength': function(min, max) - { - var val = '' + this; - return ((val.length >= +min) && (val.length <= +max)); - }, - - 'minlength': function(min) - { - return ((''+this).length >= +min); - }, - - 'maxlength': function(max) - { - return ((''+this).length <= +max); - }, - - 'or': function() - { - for (var i = 0; i < arguments.length; i += 2) - { - if (typeof arguments[i] != 'function') - { - if (arguments[i] == this) - return true; - i--; - } - else if (arguments[i].apply(this, arguments[i+1])) - { + ip6hostid: function() { + if (this.value == "eui64" || this.value == "random") return true; + + var v6 = IPv6(this.value); + return this.assert(!(!v6 || v6[0] || v6[1] || v6[2] || v6[3]), _('valid IPv6 host id')); + }, + + ipmask: function() { + return this.assert(this.apply('ipmask4') || this.apply('ipmask6'), + _('valid network in address/netmask notation')); + }, + + ipmask4: function() { + return this.assert(this.apply('cidr4') || this.apply('ipnet4') || this.apply('ip4addr'), + _('valid IPv4 network')); + }, + + ipmask6: function() { + return this.assert(this.apply('cidr6') || this.apply('ipnet6') || this.apply('ip6addr'), + _('valid IPv6 network')); + }, + + port: function() { + var p = Int(this.value); + return this.assert(p >= 0 && p <= 65535, _('valid port value')); + }, + + portrange: function() { + if (this.value.match(/^(\d+)-(\d+)$/)) { + var p1 = +RegExp.$1; + var p2 = +RegExp.$2; + return this.assert(p1 <= p2 && p2 <= 65535, + _('valid port or port range (port1-port2)')); } - } - return false; - }, - 'and': function() - { - for (var i = 0; i < arguments.length; i += 2) - { - if (typeof arguments[i] != 'function') - { - if (arguments[i] != this) - return false; - i--; - } - else if (!arguments[i].apply(this, arguments[i+1])) - { - return false; - } - } - return true; - }, + return this.assert(this.apply('port'), _('valid port or port range (port1-port2)')); + }, - 'neg': function() - { - return cbi_validators.or.apply( - this.replace(/^[ \t]*![ \t]*/, ''), arguments); - }, + macaddr: function() { + return this.assert(this.value.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null, + _('valid MAC address')); + }, - 'list': function(subvalidator, subargs) - { - if (typeof subvalidator != 'function') - return false; + host: function(ipv4only) { + return this.assert(this.apply('hostname') || this.apply(ipv4only == 1 ? 'ip4addr' : 'ipaddr'), + _('valid hostname or IP address')); + }, - var tokens = this.match(/[^ \t]+/g); - for (var i = 0; i < tokens.length; i++) - if (!subvalidator.apply(tokens[i], subargs)) - return false; + hostname: function(strict) { + if (this.value.length <= 253) + return this.assert( + (this.value.match(/^[a-zA-Z0-9_]+$/) != null || + (this.value.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) && + this.value.match(/[^0-9.]/))) && + (!strict || !this.value.match(/^_/)), + _('valid hostname')); - return true; - }, - 'phonedigit': function() - { - return (this.match(/^[0-9\*#!\.]+$/) != null); - }, - 'timehhmmss': function() - { - return (this.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/) != null); - }, - 'dateyyyymmdd': function() - { - if (this == null) { - return false; - } - if (this.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) { - var year = RegExp.$1; - var month = RegExp.$2; - var day = RegExp.$2 + return this.assert(false, _('valid hostname')); + }, - var days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30 , 31, 30, 31 ]; - function is_leap_year(year) { - return ((year % 4) == 0) && ((year % 100) != 0) || ((year % 400) == 0); - } - function get_days_in_month(month, year) { - if ((month == 2) && is_leap_year(year)) { - return 29; - } else { - return days_in_month[month]; + network: function() { + return this.assert(this.apply('uciname') || this.apply('host'), + _('valid UCI identifier, hostname or IP address')); + }, + + hostport: function(ipv4only) { + var hp = this.value.split(/:/); + return this.assert(hp.length == 2 && this.apply('host', hp[0], [ipv4only]) && this.apply('port', hp[1]), + _('valid host:port')); + }, + + ip4addrport: function() { + var hp = this.value.split(/:/); + return this.assert(hp.length == 2 && this.apply('ip4addr', hp[0], [true]) && this.apply('port', hp[1]), + _('valid IPv4 address:port')); + }, + + ipaddrport: function(bracket) { + var m4 = this.value.match(/^([^\[\]:]+):(\d+)$/), + m6 = this.value.match((bracket == 1) ? /^\[(.+)\]:(\d+)$/ : /^([^\[\]]+):(\d+)$/); + + if (m4) + return this.assert(this.apply('ip4addr', m4[1], [true]) && this.apply('port', m4[2]), + _('valid address:port')); + + return this.assert(m6 && this.apply('ip6addr', m6[1], [true]) && this.apply('port', m6[2]), + _('valid address:port')); + }, + + wpakey: function() { + var v = this.value; + + if (v.length == 64) + return this.assert(v.match(/^[a-fA-F0-9]{64}$/), _('valid hexadecimal WPA key')); + + return this.assert((v.length >= 8) && (v.length <= 63), _('key between 8 and 63 characters')); + }, + + wepkey: function() { + var v = this.value; + + if (v.substr(0, 2) === 's:') + v = v.substr(2); + + if ((v.length == 10) || (v.length == 26)) + return this.assert(v.match(/^[a-fA-F0-9]{10,26}$/), _('valid hexadecimal WEP key')); + + return this.assert((v.length === 5) || (v.length === 13), _('key with either 5 or 13 characters')); + }, + + uciname: function() { + return this.assert(this.value.match(/^[a-zA-Z0-9_]+$/), _('valid UCI identifier')); + }, + + range: function(min, max) { + var val = Dec(this.value); + return this.assert(val >= +min && val <= +max, _('value between %f and %f').format(min, max)); + }, + + min: function(min) { + return this.assert(Dec(this.value) >= +min, _('value greater or equal to %f').format(min)); + }, + + max: function(max) { + return this.assert(Dec(this.value) <= +max, _('value smaller or equal to %f').format(max)); + }, + + rangelength: function(min, max) { + var val = '' + this.value; + return this.assert((val.length >= +min) && (val.length <= +max), + _('value between %d and %d characters').format(min, max)); + }, + + minlength: function(min) { + return this.assert((''+this.value).length >= +min, + _('value with at least %d characters').format(min)); + }, + + maxlength: function(max) { + return this.assert((''+this.value).length <= +max, + _('value with at most %d characters').format(max)); + }, + + or: function() { + var errors = []; + + for (var i = 0; i < arguments.length; i += 2) { + if (typeof arguments[i] != 'function') { + if (arguments[i] == this.value) + return this.assert(true); + errors.push('"%s"'.format(arguments[i])); + i--; + } + else if (arguments[i].apply(this, arguments[i+1])) { + return this.assert(true); + } + else { + errors.push(this.error); } } - /* Firewall rules in the past don't make sense */ - if (year < 2015) { - return false; - } - if ((month <= 0) || (month > 12)) { - return false; - } - if ((day <= 0) || (day > get_days_in_month(month, year))) { - return false; - } - return true; - } else { - return false; + return this.assert(false, _('one of:\n - %s'.format(errors.join('\n - ')))); + }, + + and: function() { + for (var i = 0; i < arguments.length; i += 2) { + if (typeof arguments[i] != 'function') { + if (arguments[i] != this.value) + return this.assert(false, '"%s"'.format(arguments[i])); + i--; + } + else if (!arguments[i].apply(this, arguments[i+1])) { + return this.assert(false, this.error); + } + } + + return this.assert(true); + }, + + neg: function() { + return this.apply('or', this.value.replace(/^[ \t]*![ \t]*/, ''), arguments); + }, + + list: function(subvalidator, subargs) { + this.field.setAttribute('data-is-list', 'true'); + + var tokens = this.value.match(/[^ \t]+/g); + for (var i = 0; i < tokens.length; i++) + if (!this.apply(subvalidator, tokens[i], subargs)) + return this.assert(false, this.error); + + return this.assert(true); + }, + + phonedigit: function() { + return this.assert(this.value.match(/^[0-9\*#!\.]+$/), + _('valid phone digit (0-9, "*", "#", "!" or ".")')); + }, + + timehhmmss: function() { + return this.assert(this.value.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/), + _('valid time (HH:MM:SS)')); + }, + + dateyyyymmdd: function() { + if (this.value.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) { + var year = +RegExp.$1, + month = +RegExp.$2, + day = +RegExp.$3, + days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; + + function is_leap_year(year) { + return ((!(year % 4) && (year % 100)) || !(year % 400)); + } + + function get_days_in_month(month, year) { + return (month === 2 && is_leap_year(year)) ? 29 : days_in_month[month - 1]; + } + + /* Firewall rules in the past don't make sense */ + return this.assert(year >= 2015 && month && month <= 12 && day && day <= get_days_in_month(month, year), + _('valid date (YYYY-MM-DD)')); + + } + + return this.assert(false, _('valid date (YYYY-MM-DD)')); + }, + + unique: function(subvalidator, subargs) { + var ctx = this, + option = findParent(ctx.field, '[data-type][data-name]'), + section = findParent(option, '.cbi-section'), + query = '[data-type="%s"][data-name="%s"]'.format(option.getAttribute('data-type'), option.getAttribute('data-name')), + unique = true; + + section.querySelectorAll(query).forEach(function(sibling) { + if (sibling === option) + return; + + var input = sibling.querySelector('[data-type]'), + values = input ? (input.getAttribute('data-is-list') ? input.value.match(/[^ \t]+/g) : [ input.value ]) : null; + + if (values !== null && values.indexOf(ctx.value) !== -1) + unique = false; + }); + + if (!unique) + return this.assert(false, _('unique value')); + + if (typeof(subvalidator) === 'function') + return this.apply(subvalidator, undefined, subargs); + + return this.assert(true); + }, + + hexstring: function() { + return this.assert(this.value.match(/^([a-f0-9][a-f0-9]|[A-F0-9][A-F0-9])+$/), + _('hexadecimal encoded value')); } } }; +function CBIValidator(field, type, optional) +{ + this.field = field; + this.optional = optional; + this.vstack = this.compile(type); +} + +CBIValidator.prototype = CBIValidatorPrototype; + function cbi_d_add(field, dep, index) { var obj = (typeof(field) === 'string') ? document.getElementById(field) : field; @@ -511,20 +704,19 @@ function cbi_d_update() { if (node && node.parentNode && !cbi_d_check(entry.deps)) { node.parentNode.removeChild(node); state = true; - } else if (parent && (!node || !node.parentNode) && cbi_d_check(entry.deps)) { + } + else if (parent && (!node || !node.parentNode) && cbi_d_check(entry.deps)) { var next = undefined; for (next = parent.firstChild; next; next = next.nextSibling) { - if (next.getAttribute && parseInt(next.getAttribute('data-index'), 10) > entry.index) { + if (next.getAttribute && parseInt(next.getAttribute('data-index'), 10) > entry.index) break; - } } - if (!next) { + if (!next) parent.appendChild(entry.node); - } else { + else parent.insertBefore(entry.node, next); - } state = true; } @@ -534,14 +726,13 @@ function cbi_d_update() { parent.parentNode.style.display = (parent.options.length <= 1) ? 'none' : ''; } - if (entry && entry.parent) { - if (!cbi_t_update()) - cbi_tag_last(parent); - } + if (entry && entry.parent) + cbi_tag_last(parent); - if (state) { + if (state) cbi_d_update(); - } + else if (parent) + parent.dispatchEvent(new CustomEvent('dependency-update', { bubbles: true })); } function cbi_init() { @@ -565,9 +756,8 @@ function cbi_init() { var index = parseInt(node.getAttribute('data-index'), 10); var depends = JSON.parse(node.getAttribute('data-depends')); if (!isNaN(index) && depends.length > 0) { - for (var alt = 0; alt < depends.length; alt++) { + for (var alt = 0; alt < depends.length; alt++) cbi_d_add(node, depends[alt], index); - } } } @@ -575,9 +765,8 @@ function cbi_init() { for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { var events = node.getAttribute('data-update').split(' '); - for (var j = 0, event; (event = events[j]) !== undefined; j++) { - cbi_bind(node, event, cbi_d_update); - } + for (var j = 0, event; (event = events[j]) !== undefined; j++) + node.addEventListener(event, cbi_d_update); } nodes = document.querySelectorAll('[data-choices]'); @@ -619,9 +808,8 @@ function cbi_init() { node.getAttribute('data-type')); } - document.querySelectorAll('.cbi-dropdown').forEach(function(s) { - cbi_dropdown_init(s); - }); + document.querySelectorAll('.cbi-dropdown').forEach(cbi_dropdown_init); + document.querySelectorAll('[data-browser]').forEach(cbi_browser_init); document.querySelectorAll('.cbi-tooltip:not(:empty)').forEach(function(s) { s.parentNode.classList.add('cbi-tooltip-container'); @@ -642,460 +830,230 @@ function cbi_init() { cbi_d_update(); } -function cbi_bind(obj, type, callback, mode) { - if (!obj.addEventListener) { - obj.attachEvent('on' + type, - function(){ - var e = window.event; +function cbi_combobox_init(id, values, def, man) { + var obj = (typeof(id) === 'string') ? document.getElementById(id) : id; + var sb = E('div', { + 'name': obj.name, + 'class': 'cbi-dropdown', + 'display-items': 5, + 'optional': obj.getAttribute('data-optional'), + 'placeholder': _('-- Please choose --'), + 'data-type': obj.getAttribute('data-type'), + 'data-optional': obj.getAttribute('data-optional') + }, [ E('ul') ]); - if (!e.target && e.srcElement) - e.target = e.srcElement; - - return !!callback(e); - } - ); - } else { - obj.addEventListener(type, callback, !!mode); - } - return obj; -} - -function cbi_combobox(id, values, def, man, focus) { - var selid = "cbi.combobox." + id; - if (document.getElementById(selid)) { - return - } - - var obj = document.getElementById(id) - var sel = document.createElement("select"); - sel.id = selid; - sel.index = obj.index; - sel.className = obj.className.replace(/cbi-input-text/, 'cbi-input-select'); - - if (obj.nextSibling) { - obj.parentNode.insertBefore(sel, obj.nextSibling); - } else { - obj.parentNode.appendChild(sel); - } - - var dt = obj.getAttribute('cbi_datatype'); - var op = obj.getAttribute('cbi_optional'); - - if (!values[obj.value]) { - if (obj.value == "") { - var optdef = document.createElement("option"); - optdef.value = ""; - optdef.appendChild(document.createTextNode(typeof(def) === 'string' ? def : cbi_strings.label.choose)); - sel.appendChild(optdef); - } else { - var opt = document.createElement("option"); - opt.value = obj.value; - opt.selected = "selected"; - opt.appendChild(document.createTextNode(obj.value)); - sel.appendChild(opt); - } + if (!(obj.value in values) && obj.value.length) { + sb.lastElementChild.appendChild(E('li', { + 'data-value': obj.value, + 'selected': '' + }, obj.value.length ? obj.value : (def || _('-- Please choose --')))); } for (var i in values) { - var opt = document.createElement("option"); - opt.value = i; - - if (obj.value == i) { - opt.selected = "selected"; - } - - opt.appendChild(document.createTextNode(values[i])); - sel.appendChild(opt); + sb.lastElementChild.appendChild(E('li', { + 'data-value': i, + 'selected': (i == obj.value) ? '' : null + }, values[i])); } - var optman = document.createElement("option"); - optman.value = ""; - optman.appendChild(document.createTextNode(typeof(man) === 'string' ? man : cbi_strings.label.custom)); - sel.appendChild(optman); + sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, [ + E('input', { + 'type': 'text', + 'class': 'create-item-input', + 'data-type': obj.getAttribute('data-type'), + 'data-optional': true, + 'placeholder': (man || _('-- custom --')) + }) + ])); - obj.style.display = "none"; - - if (dt) - cbi_validate_field(sel, op == 'true', dt); - - cbi_bind(sel, "change", function() { - if (sel.selectedIndex == sel.options.length - 1) { - obj.style.display = "inline"; - sel.blur(); - sel.parentNode.removeChild(sel); - obj.focus(); - } else { - obj.value = sel.options[sel.selectedIndex].value; - } - - try { - cbi_d_update(); - } catch (e) { - //Do nothing - } - }) - - // Retrigger validation in select - if (focus) { - sel.focus(); - sel.blur(); - } -} - -function cbi_combobox_init(id, values, def, man) { - var obj = (typeof(id) === 'string') ? document.getElementById(id) : id; - cbi_bind(obj, "blur", function() { - cbi_combobox(obj.id, values, def, man, true); - }); - cbi_combobox(obj.id, values, def, man, false); + sb.value = obj.value; + obj.parentNode.replaceChild(sb, obj); } function cbi_filebrowser(id, defpath) { - var field = document.getElementById(id); + var field = L.dom.elem(id) ? id : document.getElementById(id); var browser = window.open( - cbi_strings.path.browser + ( field.value || defpath || '' ) + '?field=' + id, + cbi_strings.path.browser + (field.value || defpath || '') + '?field=' + field.id, "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes" ); browser.focus(); } -function cbi_browser_init(id, resource, defpath) +function cbi_browser_init(field) { - function cbi_browser_btnclick(e) { - cbi_filebrowser(id, defpath); - return false; - } - - var field = document.getElementById(id); - - var btn = document.createElement('img'); - btn.className = 'cbi-image-button'; - btn.src = (resource || cbi_strings.path.resource) + '/cbi/folder.gif'; - field.parentNode.insertBefore(btn, field.nextSibling); - - cbi_bind(btn, 'click', cbi_browser_btnclick); + field.parentNode.insertBefore( + E('img', { + 'src': L.resource('cbi/folder.gif'), + 'class': 'cbi-image-button', + 'click': function(ev) { + cbi_filebrowser(field, field.getAttribute('data-browser')); + ev.preventDefault(); + } + }), field.nextSibling); } -function cbi_dynlist_init(parent, datatype, optional, choices) -{ - var prefix = parent.getAttribute('data-prefix'); - var holder = parent.getAttribute('data-placeholder'); +CBIDynamicList = { + addItem: function(dl, value, text, flash) { + var exists = false, + new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [ + E('span', {}, text || value), + E('input', { + 'type': 'hidden', + 'name': dl.getAttribute('data-prefix'), + 'value': value })]); - var values; + dl.querySelectorAll('.item, .add-item').forEach(function(item) { + if (exists) + return; - function cbi_dynlist_redraw(focus, add, del) - { - values = [ ]; + var hidden = item.querySelector('input[type="hidden"]'); - while (parent.firstChild) - { - var n = parent.firstChild; - var i = +n.index; + if (hidden && hidden.value === value) + exists = true; + else if (!hidden || hidden.value >= value) + exists = !!item.parentNode.insertBefore(new_item, item); + }); - if (i != del) - { - if (n.nodeName.toLowerCase() == 'input') - values.push(n.value || ''); - else if (n.nodeName.toLowerCase() == 'select') - values[values.length-1] = n.options[n.selectedIndex].value; - } + cbi_d_update(); + }, - parent.removeChild(n); - } + removeItem: function(dl, item) { + var sb = dl.querySelector('.cbi-dropdown'); + if (sb) { + var value = item.querySelector('input[type="hidden"]').value; - if (add >= 0) - { - focus = add+1; - values.splice(focus, 0, ''); - } - else if (values.length == 0) - { - focus = 0; - values.push(''); - } - - for (var i = 0; i < values.length; i++) - { - var t = document.createElement('input'); - t.id = prefix + '.' + (i+1); - t.name = prefix; - t.value = values[i]; - t.type = 'text'; - t.index = i; - t.className = 'cbi-input-text'; - - if (i == 0 && holder) - { - t.placeholder = holder; - } - - var b = E('div', { - class: 'cbi-button cbi-button-' + ((i+1) < values.length ? 'remove' : 'add') - }, (i+1) < values.length ? '×' : '+'); - - parent.appendChild(t); - parent.appendChild(b); - if (datatype == 'file') - { - cbi_browser_init(t.id, null, parent.getAttribute('data-browser-path')); - } - - parent.appendChild(document.createElement('br')); - - if (datatype) - { - cbi_validate_field(t.id, ((i+1) == values.length) || optional, datatype); - } - - if (choices) - { - cbi_combobox_init(t.id, choices, '', cbi_strings.label.custom); - b.index = i; - - cbi_bind(b, 'keydown', cbi_dynlist_keydown); - cbi_bind(b, 'keypress', cbi_dynlist_keypress); - - if (i == focus || -i == focus) - b.focus(); - } - else - { - cbi_bind(t, 'keydown', cbi_dynlist_keydown); - cbi_bind(t, 'keypress', cbi_dynlist_keypress); - - if (i == focus) - { - t.focus(); - } - else if (-i == focus) - { - t.focus(); - - /* force cursor to end */ - var v = t.value; - t.value = ' ' - t.value = v; - } - } - - cbi_bind(b, 'click', cbi_dynlist_btnclick); - } - } - - function cbi_dynlist_keypress(ev) - { - ev = ev ? ev : window.event; - - var se = ev.target ? ev.target : ev.srcElement; - - if (se.nodeType == 3) - se = se.parentNode; - - switch (ev.keyCode) - { - /* backspace, delete */ - case 8: - case 46: - if (se.value.length == 0) - { - if (ev.preventDefault) - ev.preventDefault(); - - return false; - } - - return true; - - /* enter, arrow up, arrow down */ - case 13: - case 38: - case 40: - if (ev.preventDefault) - ev.preventDefault(); - - return false; - } - - return true; - } - - function cbi_dynlist_keydown(ev) - { - ev = ev ? ev : window.event; - - var se = ev.target ? ev.target : ev.srcElement; - - if (se.nodeType == 3) - se = se.parentNode; - - var prev = se.previousSibling; - while (prev && prev.name != prefix) - prev = prev.previousSibling; - - var next = se.nextSibling; - while (next && next.name != prefix) - next = next.nextSibling; - - /* advance one further in combobox case */ - if (next && next.nextSibling.name == prefix) - next = next.nextSibling; - - switch (ev.keyCode) - { - /* backspace, delete */ - case 8: - case 46: - var del = (se.nodeName.toLowerCase() == 'select') - ? true : (se.value.length == 0); - - if (del) - { - if (ev.preventDefault) - ev.preventDefault(); - - var focus = se.index; - if (ev.keyCode == 8) - focus = -focus+1; - - cbi_dynlist_redraw(focus, -1, se.index); - - return false; - } - - break; - - /* enter */ - case 13: - cbi_dynlist_redraw(-1, se.index, -1); - break; - - /* arrow up */ - case 38: - if (prev) - prev.focus(); - - break; - - /* arrow down */ - case 40: - if (next) - next.focus(); - - break; - } - - return true; - } - - function cbi_dynlist_btnclick(ev) - { - ev = ev ? ev : window.event; - - var se = ev.target ? ev.target : ev.srcElement; - var input = se.previousSibling; - while (input && input.name != prefix) { - input = input.previousSibling; - } - - if (se.classList.contains('cbi-button-remove')) { - input.value = ''; - - cbi_dynlist_keydown({ - target: input, - keyCode: 8 - }); - } - else { - cbi_dynlist_keydown({ - target: input, - keyCode: 13 + sb.querySelectorAll('ul > li').forEach(function(li) { + if (li.getAttribute('data-value') === value) + li.removeAttribute('unselectable'); }); } - return false; - } + item.parentNode.removeChild(item); + cbi_d_update(); + }, - cbi_dynlist_redraw(NaN, -1, -1); -} + handleClick: function(ev) { + var dl = ev.currentTarget, + item = findParent(ev.target, '.item'); - -function cbi_t_add(section, tab) { - var t = document.getElementById('tab.' + section + '.' + tab); - var c = document.getElementById('container.' + section + '.' + tab); - - if( t && c ) { - cbi_t[section] = (cbi_t[section] || [ ]); - cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id }; - } -} - -function cbi_t_switch(section, tab) { - if( cbi_t[section] && cbi_t[section][tab] ) { - var o = cbi_t[section][tab]; - var h = document.getElementById('tab.' + section); - for( var tid in cbi_t[section] ) { - var o2 = cbi_t[section][tid]; - if( o.tab.id != o2.tab.id ) { - o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled "); - o2.container.style.display = 'none'; + if (item) { + this.removeItem(dl, item); + } + else if (matchesElem(ev.target, '.cbi-button-add')) { + var input = ev.target.previousElementSibling; + if (input.value.length && !input.classList.contains('cbi-input-invalid')) { + this.addItem(dl, input.value, null, true); + input.value = ''; } - else { - if(h) h.value = tab; - o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab "); - o2.container.style.display = 'block'; + } + }, + + handleDropdownChange: function(ev) { + var dl = ev.currentTarget, + sbIn = ev.detail.instance, + sbEl = ev.detail.element, + sbVal = ev.detail.value; + + if (sbVal === null) + return; + + sbIn.setValues(sbEl, null); + sbVal.element.setAttribute('unselectable', ''); + + this.addItem(dl, sbVal.value, sbVal.text, true); + }, + + handleKeydown: function(ev) { + var dl = ev.currentTarget, + item = findParent(ev.target, '.item'); + + if (item) { + switch (ev.keyCode) { + case 8: /* backspace */ + if (item.previousElementSibling) + item.previousElementSibling.focus(); + + this.removeItem(dl, item); + break; + + case 46: /* delete */ + if (item.nextElementSibling) { + if (item.nextElementSibling.classList.contains('item')) + item.nextElementSibling.focus(); + else + item.nextElementSibling.firstElementChild.focus(); + } + + this.removeItem(dl, item); + break; + } + } + else if (matchesElem(ev.target, '.cbi-input-text')) { + switch (ev.keyCode) { + case 13: /* enter */ + if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) { + this.addItem(dl, ev.target.value, null, true); + ev.target.value = ''; + ev.target.blur(); + ev.target.focus(); + } + + ev.preventDefault(); + break; } } } - return false +}; + +function cbi_dynlist_init(dl, datatype, optional, choices) +{ + if (!(this instanceof cbi_dynlist_init)) + return new cbi_dynlist_init(dl, datatype, optional, choices); + + dl.classList.add('cbi-dynlist'); + dl.appendChild(E('div', { 'class': 'add-item' }, E('input', { + 'type': 'text', + 'name': 'cbi.dynlist.' + dl.getAttribute('data-prefix'), + 'class': 'cbi-input-text', + 'placeholder': dl.getAttribute('data-placeholder'), + 'data-type': datatype, + 'data-optional': true + }))); + + if (choices) + cbi_combobox_init(dl.lastElementChild.lastElementChild, choices, '', _('-- custom --')); + else + dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+')); + + dl.addEventListener('click', this.handleClick.bind(this)); + dl.addEventListener('keydown', this.handleKeydown.bind(this)); + dl.addEventListener('cbi-dropdown-change', this.handleDropdownChange.bind(this)); + + try { + var values = JSON.parse(dl.getAttribute('data-values') || '[]'); + + if (typeof(values) === 'object' && Array.isArray(values)) + for (var i = 0; i < values.length; i++) + this.addItem(dl, values[i], choices ? choices[values[i]] : null); + } + catch (e) {} } -function cbi_t_update() { - var hl_tabs = [ ]; - var updated = false; - - for( var sid in cbi_t ) - for( var tid in cbi_t[sid] ) - { - var t = cbi_t[sid][tid].tab; - var c = cbi_t[sid][tid].container; - - if (!c.firstElementChild) { - t.style.display = 'none'; - } - else if (t.style.display == 'none') { - t.style.display = ''; - t.className += ' cbi-tab-highlighted'; - hl_tabs.push(t); - } - - cbi_tag_last(c); - updated = true; - } - - if (hl_tabs.length > 0) - window.setTimeout(function() { - for( var i = 0; i < hl_tabs.length; i++ ) - hl_tabs[i].className = hl_tabs[i].className.replace(/ cbi-tab-highlighted/g, ''); - }, 750); - - return updated; -} +cbi_dynlist_init.prototype = CBIDynamicList; function cbi_validate_form(form, errmsg) { /* if triggered by a section removal or addition, don't validate */ - if( form.cbi_state == 'add-section' || form.cbi_state == 'del-section' ) + if (form.cbi_state == 'add-section' || form.cbi_state == 'del-section') return true; - if( form.cbi_validators ) - { - for( var i = 0; i < form.cbi_validators.length; i++ ) - { + if (form.cbi_validators) { + for (var i = 0; i < form.cbi_validators.length; i++) { var validator = form.cbi_validators[i]; - if( !validator() && errmsg ) - { + + if (!validator() && errmsg) { alert(errmsg); return false; } @@ -1114,133 +1072,37 @@ function cbi_validate_reset(form) return true; } -function cbi_validate_compile(code) -{ - var pos = 0; - var esc = false; - var depth = 0; - var stack = [ ]; - - code += ','; - - for (var i = 0; i < code.length; i++) - { - if (esc) - { - esc = false; - continue; - } - - switch (code.charCodeAt(i)) - { - case 92: - esc = true; - break; - - case 40: - case 44: - if (depth <= 0) - { - if (pos < i) - { - var label = code.substring(pos, i); - label = label.replace(/\\(.)/g, '$1'); - label = label.replace(/^[ \t]+/g, ''); - label = label.replace(/[ \t]+$/g, ''); - - if (label && !isNaN(label)) - { - stack.push(parseFloat(label)); - } - else if (label.match(/^(['"]).*\1$/)) - { - stack.push(label.replace(/^(['"])(.*)\1$/, '$2')); - } - else if (typeof cbi_validators[label] == 'function') - { - stack.push(cbi_validators[label]); - stack.push(null); - } - else - { - throw "Syntax error, unhandled token '"+label+"'"; - } - } - pos = i+1; - } - depth += (code.charCodeAt(i) == 40); - break; - - case 41: - if (--depth <= 0) - { - if (typeof stack[stack.length-2] != 'function') - throw "Syntax error, argument list follows non-function"; - - stack[stack.length-1] = - arguments.callee(code.substring(pos, i)); - - pos = i+1; - } - break; - } - } - - return stack; -} - function cbi_validate_field(cbid, optional, type) { - var field = (typeof cbid == "string") ? document.getElementById(cbid) : cbid; - var vstack; try { vstack = cbi_validate_compile(type); } catch(e) { }; + var field = isElem(cbid) ? cbid : document.getElementById(cbid); + var validatorFn; - if (field && vstack && typeof vstack[0] == "function") - { - var validator = function() - { - // is not detached - if( field.form ) - { - field.className = field.className.replace(/ cbi-input-invalid/g, ''); + try { + var cbiValidator = new CBIValidator(field, type, optional); + validatorFn = cbiValidator.validate.bind(cbiValidator); + } + catch(e) { + validatorFn = null; + }; - // validate value - var value = (field.options && field.options.selectedIndex > -1) - ? field.options[field.options.selectedIndex].value : field.value; + if (validatorFn !== null) { + var form = findParent(field, 'form'); - if (!(((value.length == 0) && optional) || vstack[0].apply(value, vstack[1]))) - { - // invalid - field.className += ' cbi-input-invalid'; - return false; - } - } + if (!form.cbi_validators) + form.cbi_validators = [ ]; - return true; - }; + form.cbi_validators.push(validatorFn); - if( ! field.form.cbi_validators ) - field.form.cbi_validators = [ ]; + field.addEventListener("blur", validatorFn); + field.addEventListener("keyup", validatorFn); + field.addEventListener("cbi-dropdown-change", validatorFn); - field.form.cbi_validators.push(validator); - - cbi_bind(field, "blur", validator); - cbi_bind(field, "keyup", validator); - - if (field.nodeName == 'SELECT') - { - cbi_bind(field, "change", validator); - cbi_bind(field, "click", validator); + if (matchesElem(field, 'select')) { + field.addEventListener("change", validatorFn); + field.addEventListener("click", validatorFn); } - field.setAttribute("cbi_validate", validator); - field.setAttribute("cbi_datatype", type); - field.setAttribute("cbi_optional", (!!optional).toString()); - - validator(); - - var fcbox = document.getElementById('cbi.combobox.' + field.id); - if (fcbox) - cbi_validate_field(fcbox, optional, type); + validatorFn(); } } @@ -1291,7 +1153,8 @@ function cbi_row_swap(elem, up, store) input.value = ids.join(' '); window.scrollTo(0, tr.offsetTop); - window.setTimeout(function() { tr.classList.add('flash'); }, 1); + void tr.offsetWidth; + tr.classList.add('flash'); return false; } @@ -1300,20 +1163,16 @@ function cbi_tag_last(container) { var last; - for (var i = 0; i < container.childNodes.length; i++) - { + for (var i = 0; i < container.childNodes.length; i++) { var c = container.childNodes[i]; - if (c.nodeType == 1 && c.nodeName.toLowerCase() == 'div') - { - c.className = c.className.replace(/ cbi-value-last$/, ''); + if (matchesElem(c, 'div')) { + c.classList.remove('cbi-value-last'); last = c; } } if (last) - { - last.className += ' cbi-value-last'; - } + last.classList.add('cbi-value-last'); } function cbi_submit(elem, name, value, action) @@ -1350,8 +1209,9 @@ String.prototype.format = function() if (typeof(s) !== 'string' && !(s instanceof String)) return ''; - for( var i = 0; i < r.length; i += 2 ) + for (var i = 0; i < r.length; i += 2) s = s.replace(r[i], r[i+1]); + return s; } @@ -1360,22 +1220,18 @@ String.prototype.format = function() var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t|m))/; var a = b = [], numSubstitutions = 0, numMatches = 0; - while (a = re.exec(str)) - { + while (a = re.exec(str)) { var m = a[1]; var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5]; var pPrecision = a[6], pType = a[7]; numMatches++; - if (pType == '%') - { + if (pType == '%') { subst = '%'; } - else - { - if (numSubstitutions < arguments.length) - { + else { + if (numSubstitutions < arguments.length) { var param = arguments[numSubstitutions++]; var pad = ''; @@ -1400,8 +1256,7 @@ String.prototype.format = function() var subst = param; - switch(pType) - { + switch(pType) { case 'b': subst = (+param || 0).toString(2); break; @@ -1517,16 +1372,20 @@ String.prototype.nobr = function() String.format = function() { var a = [ ]; + for (var i = 1; i < arguments.length; i++) a.push(arguments[i]); + return ''.format.apply(arguments[0], a); } String.nobr = function() { var a = [ ]; + for (var i = 1; i < arguments.length; i++) a.push(arguments[i]); + return ''.nobr.apply(arguments[0], a); } @@ -1539,90 +1398,20 @@ if (window.NodeList && !NodeList.prototype.forEach) { }; } - -var dummyElem, domParser; - -function isElem(e) -{ - return (typeof(e) === 'object' && e !== null && 'nodeType' in e); +if (!window.requestAnimationFrame) { + window.requestAnimationFrame = function(f) { + window.setTimeout(function() { + f(new Date().getTime()) + }, 1000/30); + }; } -function toElem(s) -{ - var elem; - try { - domParser = domParser || new DOMParser(); - elem = domParser.parseFromString(s, 'text/html').body.firstChild; - } - catch(e) {} - - if (!elem) { - try { - dummyElem = dummyElem || document.createElement('div'); - dummyElem.innerHTML = s; - elem = dummyElem.firstChild; - } - catch (e) {} - } - - return elem || null; -} - -function findParent(node, selector) -{ - while (node) - if (node.msMatchesSelector && node.msMatchesSelector(selector)) - return node; - else if (node.matches && node.matches(selector)) - return node; - else - node = node.parentNode; - - return null; -} - -function E() -{ - var html = arguments[0], - attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null, - data = attr ? arguments[2] : arguments[1], - elem; - - if (isElem(html)) - elem = html; - else if (html.charCodeAt(0) === 60) - elem = toElem(html); - else - elem = document.createElement(html); - - if (!elem) - return null; - - if (attr) - for (var key in attr) - if (attr.hasOwnProperty(key) && attr[key] !== null && attr[key] !== undefined) - elem.setAttribute(key, attr[key]); - - if (typeof(data) === 'function') - data = data(elem); - - if (isElem(data)) { - elem.appendChild(data); - } - else if (Array.isArray(data)) { - for (var i = 0; i < data.length; i++) - if (isElem(data[i])) - elem.appendChild(data[i]); - else - elem.appendChild(document.createTextNode('' + data[i])); - } - else if (data !== null && data !== undefined) { - elem.innerHTML = '' + data; - } - - return elem; -} +function isElem(e) { return L.dom.elem(e) } +function toElem(s) { return L.dom.parse(s) } +function matchesElem(node, selector) { return L.dom.matches(node, selector) } +function findParent(node, selector) { return L.dom.parent(node, selector) } +function E() { return L.dom.create.apply(L.dom, arguments) } if (typeof(window.CustomEvent) !== 'function') { function CustomEvent(event, params) { @@ -1641,32 +1430,73 @@ CBIDropdown = { var st = window.getComputedStyle(sb, null), ul = sb.querySelector('ul'), li = ul.querySelectorAll('li'), + fl = findParent(sb, '.cbi-value-field'), sel = ul.querySelector('[selected]'), rect = sb.getBoundingClientRect(), - h = sb.clientHeight - parseFloat(st.paddingTop) - parseFloat(st.paddingBottom), - mh = this.dropdown_items * h, - eh = Math.min(mh, li.length * h); + items = Math.min(this.dropdown_items, li.length); document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); }); - ul.style.maxHeight = mh + 'px'; sb.setAttribute('open', ''); - ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0; + var pv = ul.cloneNode(true); + pv.classList.add('preview'); + + if (fl) + fl.classList.add('cbi-dropdown-open'); + + if ('ontouchstart' in window) { + var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0), + vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0), + scrollFrom = window.pageYOffset, + scrollTo = scrollFrom + rect.top - vpHeight * 0.5, + start = null; + + ul.style.top = sb.offsetHeight + 'px'; + ul.style.left = -rect.left + 'px'; + ul.style.right = (rect.right - vpWidth) + 'px'; + ul.style.maxHeight = (vpHeight * 0.5) + 'px'; + ul.style.WebkitOverflowScrolling = 'touch'; + + var scrollStep = function(timestamp) { + if (!start) { + start = timestamp; + ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0; + } + + var duration = Math.max(timestamp - start, 1); + if (duration < 100) { + document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100); + window.requestAnimationFrame(scrollStep); + } + else { + document.body.scrollTop = scrollTo; + } + }; + + window.requestAnimationFrame(scrollStep); + } + else { + ul.style.maxHeight = '1px'; + ul.style.top = ul.style.bottom = ''; + + window.requestAnimationFrame(function() { + var height = items * li[Math.max(0, li.length - 2)].offsetHeight; + + ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0; + ul.style[((rect.top + rect.height + height) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px'; + ul.style.maxHeight = height + 'px'; + }); + } + ul.querySelectorAll('[selected] input[type="checkbox"]').forEach(function(c) { c.checked = true; }); - ul.style.top = ul.style.bottom = ''; - ul.style[((sb.getBoundingClientRect().top + eh) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px'; ul.classList.add('dropdown'); - var pv = ul.cloneNode(true); - pv.classList.remove('dropdown'); - pv.classList.add('preview'); - sb.insertBefore(pv, ul.nextElementSibling); li.forEach(function(l) { @@ -1684,7 +1514,8 @@ CBIDropdown = { var pv = sb.querySelector('ul.preview'), ul = sb.querySelector('ul.dropdown'), - li = ul.querySelectorAll('li'); + li = ul.querySelectorAll('li'), + fl = findParent(sb, '.cbi-value-field'); li.forEach(function(l) { l.removeAttribute('tabindex'); }); sb.lastElementChild.removeAttribute('tabindex'); @@ -1694,6 +1525,10 @@ CBIDropdown = { sb.style.width = sb.style.height = ''; ul.classList.remove('dropdown'); + ul.style.top = ul.style.bottom = ul.style.maxHeight = ''; + + if (fl) + fl.classList.remove('cbi-dropdown-open'); if (!no_focus) this.setFocus(sb, sb); @@ -1791,29 +1626,87 @@ CBIDropdown = { }, saveValues: function(sb, ul) { - var sel = ul.querySelectorAll('[selected]'), - div = sb.lastElementChild; + var sel = ul.querySelectorAll('li[selected]'), + div = sb.lastElementChild, + strval = '', + values = []; while (div.lastElementChild) div.removeChild(div.lastElementChild); sel.forEach(function (s) { + if (s.hasAttribute('placeholder')) + return; + + var v = { + text: s.innerText, + value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText, + element: s + }; + div.appendChild(E('input', { type: 'hidden', name: s.hasAttribute('name') ? s.getAttribute('name') : (sb.getAttribute('name') || ''), - value: s.hasAttribute('value') ? s.getAttribute('value') : s.innerText + value: v.value })); + + values.push(v); + + strval += strval.length ? ' ' + v.value : v.value; }); + var detail = { + instance: this, + element: sb + }; + + if (this.multi) + detail.values = values; + else + detail.value = values.length ? values[0] : null; + + sb.value = strval; + + sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', { + bubbles: true, + detail: detail + })); + cbi_d_update(); }, + setValues: function(sb, values) { + var ul = sb.querySelector('ul'); + + if (this.multi) { + ul.querySelectorAll('li[data-value]').forEach(function(li) { + if (values === null || !(li.getAttribute('data-value') in values)) + this.toggleItem(sb, li, false); + else + this.toggleItem(sb, li, true); + }); + } + else { + var ph = ul.querySelector('li[placeholder]'); + if (ph) + this.toggleItem(sb, ph); + + ul.querySelectorAll('li[data-value]').forEach(function(li) { + if (values !== null && (li.getAttribute('data-value') in values)) + this.toggleItem(sb, li); + }); + } + }, + setFocus: function(sb, elem, scroll) { if (sb && sb.hasAttribute && sb.hasAttribute('locked-in')) return; + if (sb.target && findParent(sb.target, 'ul.dropdown')) + return; + document.querySelectorAll('.focus').forEach(function(e) { - if (e.nodeName.toLowerCase() !== 'input') { + if (!matchesElem(e, 'input')) { e.classList.remove('focus'); e.blur(); } @@ -1830,17 +1723,19 @@ CBIDropdown = { createItems: function(sb, value) { var sbox = this, - val = (value || '').trim().split(/\s+/), + val = (value || '').trim(), ul = sb.querySelector('ul'); if (!sbox.multi) - val.length = Math.min(val.length, 1); + val = val.length ? [ val ] : []; + else + val = val.length ? val.split(/\s+/) : []; val.forEach(function(item) { var new_item = null; ul.childNodes.forEach(function(li) { - if (li.getAttribute && li.getAttribute('value') === item) + if (li.getAttribute && li.getAttribute('data-value') === item) new_item = li; }); @@ -1851,7 +1746,7 @@ CBIDropdown = { if (tpl) markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^$/, '').trim(); else - markup = '
  • {{value}}
  • '; + markup = '
  • {{value}}
  • '; new_item = E(markup.replace(/{{value}}/g, item)); @@ -1878,6 +1773,168 @@ CBIDropdown = { document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); }); + }, + + handleClick: function(ev) { + var sb = ev.currentTarget; + + if (!sb.hasAttribute('open')) { + if (!matchesElem(ev.target, 'input')) + this.openDropdown(sb); + } + else { + var li = findParent(ev.target, 'li'); + if (li && li.parentNode.classList.contains('dropdown')) + this.toggleItem(sb, li); + else if (li && li.parentNode.classList.contains('preview')) + this.closeDropdown(sb); + } + + ev.preventDefault(); + ev.stopPropagation(); + }, + + handleKeydown: function(ev) { + var sb = ev.currentTarget; + + if (matchesElem(ev.target, 'input')) + return; + + if (!sb.hasAttribute('open')) { + switch (ev.keyCode) { + case 37: + case 38: + case 39: + case 40: + this.openDropdown(sb); + ev.preventDefault(); + } + } + else { + var active = findParent(document.activeElement, 'li'); + + switch (ev.keyCode) { + case 27: + this.closeDropdown(sb); + break; + + case 13: + if (active) { + if (!active.hasAttribute('selected')) + this.toggleItem(sb, active); + this.closeDropdown(sb); + ev.preventDefault(); + } + break; + + case 32: + if (active) { + this.toggleItem(sb, active); + ev.preventDefault(); + } + break; + + case 38: + if (active && active.previousElementSibling) { + this.setFocus(sb, active.previousElementSibling); + ev.preventDefault(); + } + break; + + case 40: + if (active && active.nextElementSibling) { + this.setFocus(sb, active.nextElementSibling); + ev.preventDefault(); + } + break; + } + } + }, + + handleDropdownClose: function(ev) { + var sb = ev.currentTarget; + + this.closeDropdown(sb, true); + }, + + handleDropdownSelect: function(ev) { + var sb = ev.currentTarget, + li = findParent(ev.target, 'li'); + + if (!li) + return; + + this.toggleItem(sb, li); + this.closeDropdown(sb, true); + }, + + handleMouseover: function(ev) { + var sb = ev.currentTarget; + + if (!sb.hasAttribute('open')) + return; + + var li = findParent(ev.target, 'li'); + + if (li && li.parentNode.classList.contains('dropdown')) + this.setFocus(sb, li); + }, + + handleFocus: function(ev) { + var sb = ev.currentTarget; + + document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { + if (s !== sb || sb.hasAttribute('open')) + s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); + }); + }, + + handleCanaryFocus: function(ev) { + this.closeDropdown(ev.currentTarget.parentNode); + }, + + handleCreateKeydown: function(ev) { + var input = ev.currentTarget, + sb = findParent(input, '.cbi-dropdown'); + + switch (ev.keyCode) { + case 13: + ev.preventDefault(); + + if (input.classList.contains('cbi-input-invalid')) + return; + + this.createItems(sb, input.value); + input.value = ''; + input.blur(); + break; + } + }, + + handleCreateFocus: function(ev) { + var input = ev.currentTarget, + cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'), + sb = findParent(input, '.cbi-dropdown'); + + if (cbox) + cbox.checked = true; + + sb.setAttribute('locked-in', ''); + }, + + handleCreateBlur: function(ev) { + var input = ev.currentTarget, + cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'), + sb = findParent(input, '.cbi-dropdown'); + + if (cbox) + cbox.checked = false; + + sb.removeAttribute('locked-in'); + }, + + handleCreateClick: function(ev) { + ev.currentTarget.querySelector(this.create).focus(); } }; @@ -1893,9 +1950,7 @@ function cbi_dropdown_init(sb) { this.create = sb.getAttribute('item-create') || '.create-item-input'; this.template = sb.getAttribute('item-template') || 'script[type="item-template"]'; - var sbox = this, - ul = sb.querySelector('ul'), - items = ul.querySelectorAll('li'), + var ul = sb.querySelector('ul'), more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')), open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')), canary = sb.appendChild(E('div')), @@ -1904,15 +1959,23 @@ function cbi_dropdown_init(sb) { n = 0; if (this.multi) { + var items = ul.querySelectorAll('li'); + for (var i = 0; i < items.length; i++) { - sbox.transformItem(sb, items[i]); + this.transformItem(sb, items[i]); if (items[i].hasAttribute('selected') && ndisplay-- > 0) items[i].setAttribute('display', n++); } } else { - var sel = sb.querySelectorAll('[selected]'); + if (this.optional && !ul.querySelector('li[data-value=""]')) { + var placeholder = E('li', { placeholder: '' }, this.placeholder); + ul.firstChild ? ul.insertBefore(placeholder, ul.firstChild) : ul.appendChild(placeholder); + } + + var items = ul.querySelectorAll('li'), + sel = sb.querySelectorAll('[selected]'); sel.forEach(function(s) { s.removeAttribute('selected'); @@ -1925,14 +1988,9 @@ function cbi_dropdown_init(sb) { } ndisplay--; - - if (this.optional && !ul.querySelector('li[value=""]')) { - var placeholder = E('li', { placeholder: '' }, this.placeholder); - ul.firstChild ? ul.insertBefore(placeholder, ul.firstChild) : ul.appendChild(placeholder); - } } - sbox.saveValues(sb, ul); + this.saveValues(sb, ul); ul.setAttribute('tabindex', -1); sb.setAttribute('tabindex', 0); @@ -1950,151 +2008,41 @@ function cbi_dropdown_init(sb) { more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···'; - sb.addEventListener('click', function(ev) { - if (!this.hasAttribute('open')) { - if (ev.target.nodeName.toLowerCase() !== 'input') - sbox.openDropdown(this); - } - else { - var li = findParent(ev.target, 'li'); - if (li && li.parentNode.classList.contains('dropdown')) - sbox.toggleItem(this, li); - } - - ev.preventDefault(); - ev.stopPropagation(); - }); - - sb.addEventListener('keydown', function(ev) { - if (ev.target.nodeName.toLowerCase() === 'input') - return; - - if (!this.hasAttribute('open')) { - switch (ev.keyCode) { - case 37: - case 38: - case 39: - case 40: - sbox.openDropdown(this); - ev.preventDefault(); - } - } - else - { - var active = findParent(document.activeElement, 'li'); - - switch (ev.keyCode) { - case 27: - sbox.closeDropdown(this); - break; - - case 13: - if (active) { - if (!active.hasAttribute('selected')) - sbox.toggleItem(this, active); - sbox.closeDropdown(this); - ev.preventDefault(); - } - break; - - case 32: - if (active) { - sbox.toggleItem(this, active); - ev.preventDefault(); - } - break; - - case 38: - if (active && active.previousElementSibling) { - sbox.setFocus(this, active.previousElementSibling); - ev.preventDefault(); - } - break; - - case 40: - if (active && active.nextElementSibling) { - sbox.setFocus(this, active.nextElementSibling); - ev.preventDefault(); - } - break; - } - } - }); - - sb.addEventListener('cbi-dropdown-close', function(ev) { - sbox.closeDropdown(this, true); - }); + sb.addEventListener('click', this.handleClick.bind(this)); + sb.addEventListener('keydown', this.handleKeydown.bind(this)); + sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this)); + sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this)); if ('ontouchstart' in window) { sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); }); - window.addEventListener('touchstart', sbox.closeAllDropdowns); + window.addEventListener('touchstart', this.closeAllDropdowns); } else { - sb.addEventListener('mouseover', function(ev) { - if (!this.hasAttribute('open')) - return; + sb.addEventListener('mouseover', this.handleMouseover.bind(this)); + sb.addEventListener('focus', this.handleFocus.bind(this)); - var li = findParent(ev.target, 'li'); - if (li) { - if (li.parentNode.classList.contains('dropdown')) - sbox.setFocus(this, li); + canary.addEventListener('focus', this.handleCanaryFocus.bind(this)); - ev.stopPropagation(); - } - }); - - sb.addEventListener('focus', function(ev) { - document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { - if (s !== this || this.hasAttribute('open')) - s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); - }); - }); - - canary.addEventListener('focus', function(ev) { - sbox.closeDropdown(this.parentNode); - }); - - window.addEventListener('mouseover', sbox.setFocus); - window.addEventListener('click', sbox.closeAllDropdowns); + window.addEventListener('mouseover', this.setFocus); + window.addEventListener('click', this.closeAllDropdowns); } if (create) { - create.addEventListener('keydown', function(ev) { - switch (ev.keyCode) { - case 13: - sbox.createItems(sb, this.value); - ev.preventDefault(); - this.value = ''; - this.blur(); - break; - } - }); - - create.addEventListener('focus', function(ev) { - var cbox = findParent(this, 'li').querySelector('input[type="checkbox"]'); - if (cbox) cbox.checked = true; - sb.setAttribute('locked-in', ''); - }); - - create.addEventListener('blur', function(ev) { - var cbox = findParent(this, 'li').querySelector('input[type="checkbox"]'); - if (cbox) cbox.checked = false; - sb.removeAttribute('locked-in'); - }); + create.addEventListener('keydown', this.handleCreateKeydown.bind(this)); + create.addEventListener('focus', this.handleCreateFocus.bind(this)); + create.addEventListener('blur', this.handleCreateBlur.bind(this)); var li = findParent(create, 'li'); li.setAttribute('unselectable', ''); - li.addEventListener('click', function(ev) { - this.querySelector(sbox.create).focus(); - }); + li.addEventListener('click', this.handleCreateClick.bind(this)); } } cbi_dropdown_init.prototype = CBIDropdown; function cbi_update_table(table, data, placeholder) { - target = isElem(table) ? table : document.querySelector(table); + var target = isElem(table) ? table : document.querySelector(table); if (!isElem(target)) return; @@ -2163,6 +2111,27 @@ function cbi_update_table(table, data, placeholder) { }); } +function showModal(title, children) +{ + return L.showModal(title, children); +} + +function hideModal() +{ + return L.hideModal(); +} + + document.addEventListener('DOMContentLoaded', function() { + document.addEventListener('validation-failure', function(ev) { + if (ev.target === document.activeElement) + L.showTooltip(ev); + }); + + document.addEventListener('validation-success', function(ev) { + if (ev.target === document.activeElement) + L.hideTooltip(ev); + }); + document.querySelectorAll('.table').forEach(cbi_update_table); }); diff --git a/luci-base/htdocs/luci-static/resources/luci.js b/luci-base/htdocs/luci-static/resources/luci.js new file mode 100644 index 000000000..4cb8bf4e5 --- /dev/null +++ b/luci-base/htdocs/luci-static/resources/luci.js @@ -0,0 +1,511 @@ +(function(window, document, undefined) { + var modalDiv = null, + tooltipDiv = null, + tooltipTimeout = null, + dummyElem = null, + domParser = null; + + LuCI.prototype = { + /* URL construction helpers */ + path: function(prefix, parts) { + var url = [ prefix || '' ]; + + for (var i = 0; i < parts.length; i++) + if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i])) + url.push('/', parts[i]); + + if (url.length === 1) + url.push('/'); + + return url.join(''); + }, + + url: function() { + return this.path(this.env.scriptname, arguments); + }, + + resource: function() { + return this.path(this.env.resource, arguments); + }, + + location: function() { + return this.path(this.env.scriptname, this.env.requestpath); + }, + + + /* HTTP resource fetching */ + get: function(url, args, cb) { + return this.poll(0, url, args, cb, false); + }, + + post: function(url, args, cb) { + return this.poll(0, url, args, cb, true); + }, + + poll: function(interval, url, args, cb, post) { + var data = post ? { token: this.env.token } : null; + + if (!/^(?:\/|\S+:\/\/)/.test(url)) + url = this.url(url); + + if (typeof(args) === 'object' && args !== null) { + data = data || {}; + + for (var key in args) + if (args.hasOwnProperty(key)) + switch (typeof(args[key])) { + case 'string': + case 'number': + case 'boolean': + data[key] = args[key]; + break; + + case 'object': + data[key] = JSON.stringify(args[key]); + break; + } + } + + if (interval > 0) + return XHR.poll(interval, url, data, cb, post); + else if (post) + return XHR.post(url, data, cb); + else + return XHR.get(url, data, cb); + }, + + stop: function(entry) { XHR.stop(entry) }, + halt: function() { XHR.halt() }, + run: function() { XHR.run() }, + + + /* Modal dialog */ + showModal: function(title, children) { + var dlg = modalDiv.firstElementChild; + + dlg.setAttribute('class', 'modal'); + + this.dom.content(dlg, this.dom.create('h4', {}, title)); + this.dom.append(dlg, children); + + document.body.classList.add('modal-overlay-active'); + + return dlg; + }, + + hideModal: function() { + document.body.classList.remove('modal-overlay-active'); + }, + + + /* Tooltip */ + showTooltip: function(ev) { + var target = findParent(ev.target, '[data-tooltip]'); + + if (!target) + return; + + if (tooltipTimeout !== null) { + window.clearTimeout(tooltipTimeout); + tooltipTimeout = null; + } + + var rect = target.getBoundingClientRect(), + x = rect.left + window.pageXOffset, + y = rect.top + rect.height + window.pageYOffset; + + tooltipDiv.className = 'cbi-tooltip'; + tooltipDiv.innerHTML = '▲ '; + tooltipDiv.firstChild.data += target.getAttribute('data-tooltip'); + + if (target.hasAttribute('data-tooltip-style')) + tooltipDiv.classList.add(target.getAttribute('data-tooltip-style')); + + if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) { + y -= (tooltipDiv.offsetHeight + target.offsetHeight); + tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2); + } + + tooltipDiv.style.top = y + 'px'; + tooltipDiv.style.left = x + 'px'; + tooltipDiv.style.opacity = 1; + + tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', { + bubbles: true, + detail: { target: target } + })); + }, + + hideTooltip: function(ev) { + if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv || + tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget)) + return; + + if (tooltipTimeout !== null) { + window.clearTimeout(tooltipTimeout); + tooltipTimeout = null; + } + + tooltipDiv.style.opacity = 0; + tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250); + + tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true })); + }, + + + /* Widget helper */ + itemlist: function(node, items, separators) { + var children = []; + + if (!Array.isArray(separators)) + separators = [ separators || E('br') ]; + + for (var i = 0; i < items.length; i += 2) { + if (items[i+1] !== null && items[i+1] !== undefined) { + var sep = separators[(i/2) % separators.length], + cld = []; + + children.push(E('span', { class: 'nowrap' }, [ + items[i] ? E('strong', items[i] + ': ') : '', + items[i+1] + ])); + + if ((i+2) < items.length) + children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep); + } + } + + this.dom.content(node, children); + + return node; + } + }; + + /* Tabs */ + LuCI.prototype.tabs = { + init: function() { + var groups = [], prevGroup = null, currGroup = null; + + document.querySelectorAll('[data-tab]').forEach(function(tab) { + var parent = tab.parentNode; + + if (!parent.hasAttribute('data-tab-group')) + parent.setAttribute('data-tab-group', groups.length); + + currGroup = +parent.getAttribute('data-tab-group'); + + if (currGroup !== prevGroup) { + prevGroup = currGroup; + + if (!groups[currGroup]) + groups[currGroup] = []; + } + + groups[currGroup].push(tab); + }); + + for (var i = 0; i < groups.length; i++) + this.initTabGroup(groups[i]); + + document.addEventListener('dependency-update', this.updateTabs.bind(this)); + + this.updateTabs(); + + if (!groups.length) + this.setActiveTabId(-1, -1); + }, + + initTabGroup: function(panes) { + if (!Array.isArray(panes) || panes.length === 0) + return; + + var menu = E('ul', { 'class': 'cbi-tabmenu' }), + group = panes[0].parentNode, + groupId = +group.getAttribute('data-tab-group'), + selected = null; + + for (var i = 0, pane; pane = panes[i]; i++) { + var name = pane.getAttribute('data-tab'), + title = pane.getAttribute('data-tab-title'), + active = pane.getAttribute('data-tab-active') === 'true'; + + menu.appendChild(E('li', { + 'class': active ? 'cbi-tab' : 'cbi-tab-disabled', + 'data-tab': name + }, E('a', { + 'href': '#', + 'click': this.switchTab.bind(this) + }, title))); + + if (active) + selected = i; + } + + group.parentNode.insertBefore(menu, group); + + if (selected === null) { + selected = this.getActiveTabId(groupId); + + if (selected < 0 || selected >= panes.length) + selected = 0; + + menu.childNodes[selected].classList.add('cbi-tab'); + menu.childNodes[selected].classList.remove('cbi-tab-disabled'); + panes[selected].setAttribute('data-tab-active', 'true'); + + this.setActiveTabId(groupId, selected); + } + }, + + getActiveTabState: function() { + var page = document.body.getAttribute('data-page'); + + try { + var val = JSON.parse(window.sessionStorage.getItem('tab')); + if (val.page === page && Array.isArray(val.groups)) + return val; + } + catch(e) {} + + window.sessionStorage.removeItem('tab'); + return { page: page, groups: [] }; + }, + + getActiveTabId: function(groupId) { + return +this.getActiveTabState().groups[groupId] || 0; + }, + + setActiveTabId: function(groupId, tabIndex) { + try { + var state = this.getActiveTabState(); + state.groups[groupId] = tabIndex; + + window.sessionStorage.setItem('tab', JSON.stringify(state)); + } + catch (e) { return false; } + + return true; + }, + + updateTabs: function(ev) { + document.querySelectorAll('[data-tab-title]').forEach(function(pane) { + var menu = pane.parentNode.previousElementSibling, + tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))), + n_errors = pane.querySelectorAll('.cbi-input-invalid').length; + + if (!pane.firstElementChild) { + tab.style.display = 'none'; + tab.classList.remove('flash'); + } + else if (tab.style.display === 'none') { + tab.style.display = ''; + requestAnimationFrame(function() { tab.classList.add('flash') }); + } + + if (n_errors) { + tab.setAttribute('data-errors', n_errors); + tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors)); + tab.setAttribute('data-tooltip-style', 'error'); + } + else { + tab.removeAttribute('data-errors'); + tab.removeAttribute('data-tooltip'); + } + }); + }, + + switchTab: function(ev) { + var tab = ev.target.parentNode, + name = tab.getAttribute('data-tab'), + menu = tab.parentNode, + group = menu.nextElementSibling, + groupId = +group.getAttribute('data-tab-group'), + index = 0; + + ev.preventDefault(); + + if (!tab.classList.contains('cbi-tab-disabled')) + return; + + menu.querySelectorAll('[data-tab]').forEach(function(tab) { + tab.classList.remove('cbi-tab'); + tab.classList.remove('cbi-tab-disabled'); + tab.classList.add( + tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled'); + }); + + group.childNodes.forEach(function(pane) { + if (L.dom.matches(pane, '[data-tab]')) { + if (pane.getAttribute('data-tab') === name) { + pane.setAttribute('data-tab-active', 'true'); + L.tabs.setActiveTabId(groupId, index); + } + else { + pane.setAttribute('data-tab-active', 'false'); + } + + index++; + } + }); + } + }; + + /* DOM manipulation */ + LuCI.prototype.dom = { + elem: function(e) { + return (typeof(e) === 'object' && e !== null && 'nodeType' in e); + }, + + parse: function(s) { + var elem; + + try { + domParser = domParser || new DOMParser(); + elem = domParser.parseFromString(s, 'text/html').body.firstChild; + } + catch(e) {} + + if (!elem) { + try { + dummyElem = dummyElem || document.createElement('div'); + dummyElem.innerHTML = s; + elem = dummyElem.firstChild; + } + catch (e) {} + } + + return elem || null; + }, + + matches: function(node, selector) { + var m = this.elem(node) ? node.matches || node.msMatchesSelector : null; + return m ? m.call(node, selector) : false; + }, + + parent: function(node, selector) { + if (this.elem(node) && node.closest) + return node.closest(selector); + + while (this.elem(node)) + if (this.matches(node, selector)) + return node; + else + node = node.parentNode; + + return null; + }, + + append: function(node, children) { + if (!this.elem(node)) + return null; + + if (Array.isArray(children)) { + for (var i = 0; i < children.length; i++) + if (this.elem(children[i])) + node.appendChild(children[i]); + else if (children !== null && children !== undefined) + node.appendChild(document.createTextNode('' + children[i])); + + return node.lastChild; + } + else if (typeof(children) === 'function') { + return this.append(node, children(node)); + } + else if (this.elem(children)) { + return node.appendChild(children); + } + else if (children !== null && children !== undefined) { + node.innerHTML = '' + children; + return node.lastChild; + } + + return null; + }, + + content: function(node, children) { + if (!this.elem(node)) + return null; + + while (node.firstChild) + node.removeChild(node.firstChild); + + return this.append(node, children); + }, + + attr: function(node, key, val) { + if (!this.elem(node)) + return null; + + var attr = null; + + if (typeof(key) === 'object' && key !== null) + attr = key; + else if (typeof(key) === 'string') + attr = {}, attr[key] = val; + + for (key in attr) { + if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined) + continue; + + switch (typeof(attr[key])) { + case 'function': + node.addEventListener(key, attr[key]); + break; + + case 'object': + node.setAttribute(key, JSON.stringify(attr[key])); + break; + + default: + node.setAttribute(key, attr[key]); + } + } + }, + + create: function() { + var html = arguments[0], + attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null, + data = attr ? arguments[2] : arguments[1], + elem; + + if (this.elem(html)) + elem = html; + else if (html.charCodeAt(0) === 60) + elem = this.parse(html); + else + elem = document.createElement(html); + + if (!elem) + return null; + + this.attr(elem, attr); + this.append(elem, data); + + return elem; + } + }; + + /* Setup */ + LuCI.prototype.setupDOM = function(ev) { + this.tabs.init(); + }; + + function LuCI(env) { + this.env = env; + + modalDiv = document.body.appendChild( + this.dom.create('div', { id: 'modal_overlay' }, + this.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true }))); + + tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' })); + + document.addEventListener('mouseover', this.showTooltip.bind(this), true); + document.addEventListener('mouseout', this.hideTooltip.bind(this), true); + document.addEventListener('focus', this.showTooltip.bind(this), true); + document.addEventListener('blur', this.hideTooltip.bind(this), true); + + document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this)); + } + + window.LuCI = LuCI; +})(window, document); diff --git a/luci-base/htdocs/luci-static/resources/xhr.js b/luci-base/htdocs/luci-static/resources/xhr.js index 25a90e725..3133898b5 100644 --- a/luci-base/htdocs/luci-static/resources/xhr.js +++ b/luci-base/htdocs/luci-static/resources/xhr.js @@ -1,24 +1,65 @@ /* * xhr.js - XMLHttpRequest helper class - * (c) 2008-2010 Jo-Philipp Wich + * (c) 2008-2018 Jo-Philipp Wich */ -XHR = function() -{ - this.reinit = function() - { - if (window.XMLHttpRequest) { - this._xmlHttp = new XMLHttpRequest(); - } - else if (window.ActiveXObject) { - this._xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); - } - else { - alert("xhr.js: XMLHttpRequest is not supported by this browser!"); - } - } +XHR.prototype = { + _encode: function(obj) { + obj = obj ? obj : { }; + obj['_'] = Math.random(); - this.busy = function() { + if (typeof obj == 'object') { + var code = ''; + var self = this; + + for (var k in obj) + code += (code ? '&' : '') + + k + '=' + encodeURIComponent(obj[k]); + + return code; + } + + return obj; + }, + + _response: function(callback, ts) { + if (this._xmlHttp.readyState !== 4) + return; + + var status = this._xmlHttp.status, + login = this._xmlHttp.getResponseHeader("X-LuCI-Login-Required"), + type = this._xmlHttp.getResponseHeader("Content-Type"), + json = null; + + if (status === 403 && login === 'yes') { + XHR.halt(); + + showModal(_('Session expired'), [ + E('div', { class: 'alert-message warning' }, + _('A new login is required since the authentication session expired.')), + E('div', { class: 'right' }, + E('div', { + class: 'btn primary', + click: function() { + var loc = window.location; + window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search; + } + }, _('To login…'))) + ]); + } + else if (type && type.toLowerCase().match(/^application\/json\b/)) { + try { + json = JSON.parse(this._xmlHttp.responseText); + } + catch(e) { + json = null; + } + } + + callback(this._xmlHttp, json, Date.now() - ts); + }, + + busy: function() { if (!this._xmlHttp) return false; @@ -32,20 +73,18 @@ XHR = function() default: return false; } - } + }, - this.abort = function() { + abort: function() { if (this.busy()) this._xmlHttp.abort(); - } + }, - this.get = function(url,data,callback,timeout) - { - this.reinit(); + get: function(url, data, callback, timeout) { + this._xmlHttp = new XMLHttpRequest(); - var ts = Date.now(); - var xhr = this._xmlHttp; - var code = this._encode(data); + var xhr = this._xmlHttp, + code = this._encode(data); url = location.protocol + '//' + location.host + url; @@ -60,83 +99,51 @@ XHR = function() if (!isNaN(timeout)) xhr.timeout = timeout; - xhr.onreadystatechange = function() - { - if (xhr.readyState == 4) { - var json = null; - if (xhr.getResponseHeader("Content-Type") == "application/json") { - try { json = JSON.parse(xhr.responseText); } - catch(e) { json = null; } - } - - callback(xhr, json, Date.now() - ts); - } - } - + xhr.onreadystatechange = this._response.bind(this, callback, Date.now()); xhr.send(null); - } + }, - this.post = function(url,data,callback,timeout) - { - this.reinit(); + post: function(url, data, callback, timeout) { + this._xmlHttp = new XMLHttpRequest(); - var ts = Date.now(); - var xhr = this._xmlHttp; - var code = this._encode(data); - - xhr.onreadystatechange = function() - { - if (xhr.readyState == 4) { - var json = null; - if (xhr.getResponseHeader("Content-Type") == "application/json") { - try { json = JSON.parse(xhr.responseText); } - catch(e) { json = null; } - } - - callback(xhr, json, Date.now() - ts); - } - } + var xhr = this._xmlHttp, + code = this._encode(data); xhr.open('POST', url, true); if (!isNaN(timeout)) xhr.timeout = timeout; + xhr.onreadystatechange = this._response.bind(this, callback, Date.now()); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xhr.send(code); - } + }, - this.cancel = function() - { - this._xmlHttp.onreadystatechange = function(){}; + cancel: function() { + this._xmlHttp.onreadystatechange = function() {}; this._xmlHttp.abort(); - } + }, - this.send_form = function(form,callback,extra_values) - { + send_form: function(form, callback, extra_values) { var code = ''; - for (var i = 0; i < form.elements.length; i++) - { + for (var i = 0; i < form.elements.length; i++) { var e = form.elements[i]; - if (e.options) - { + if (e.options) { code += (code ? '&' : '') + form.elements[i].name + '=' + encodeURIComponent( e.options[e.selectedIndex].value ); } - else if (e.length) - { + else if (e.length) { for (var j = 0; j < e.length; j++) if (e[j].name) { code += (code ? '&' : '') + e[j].name + '=' + encodeURIComponent(e[j].value); } } - else - { + else { code += (code ? '&' : '') + e.name + '=' + encodeURIComponent(e.value); } @@ -147,46 +154,25 @@ XHR = function() code += (code ? '&' : '') + key + '=' + encodeURIComponent(extra_values[key]); - return( - (form.method == 'get') - ? this.get(form.getAttribute('action'), code, callback) - : this.post(form.getAttribute('action'), code, callback) - ); - } - - this._encode = function(obj) - { - obj = obj ? obj : { }; - obj['_'] = Math.random(); - - if (typeof obj == 'object') - { - var code = ''; - var self = this; - - for (var k in obj) - code += (code ? '&' : '') + - k + '=' + encodeURIComponent(obj[k]); - - return code; - } - - return obj; + return (form.method == 'get' + ? this.get(form.getAttribute('action'), code, callback) + : this.post(form.getAttribute('action'), code, callback)); } } -XHR.get = function(url, data, callback) -{ +XHR.get = function(url, data, callback) { (new XHR()).get(url, data, callback); } -XHR.poll = function(interval, url, data, callback, post) -{ - if (isNaN(interval) || interval < 1) - interval = 5; +XHR.post = function(url, data, callback) { + (new XHR()).post(url, data, callback); +} - if (!XHR._q) - { +XHR.poll = function(interval, url, data, callback, post) { + if (isNaN(interval) || interval <= 0) + interval = L.env.pollinterval; + + if (!XHR._q) { XHR._t = 0; XHR._q = [ ]; XHR._r = function() { @@ -213,8 +199,7 @@ XHR.poll = function(interval, url, data, callback, post) return e; } -XHR.stop = function(e) -{ +XHR.stop = function(e) { for (var i = 0; XHR._q && XHR._q[i]; i++) { if (XHR._q[i] === e) { e.xhr.cancel(); @@ -226,10 +211,8 @@ XHR.stop = function(e) return false; } -XHR.halt = function() -{ - if (XHR._i) - { +XHR.halt = function() { + if (XHR._i) { /* show & set poll indicator */ try { document.getElementById('xhr_poll_status').style.display = ''; @@ -242,10 +225,8 @@ XHR.halt = function() } } -XHR.run = function() -{ - if (XHR._r && !XHR._i) - { +XHR.run = function() { + if (XHR._r && !XHR._i) { /* show & set poll indicator */ try { document.getElementById('xhr_poll_status').style.display = ''; @@ -260,9 +241,10 @@ XHR.run = function() } } -XHR.running = function() -{ +XHR.running = function() { return !!(XHR._r && XHR._i); } +function XHR() {} + document.addEventListener('DOMContentLoaded', XHR.run); diff --git a/luci-base/luasrc/cbi.lua b/luci-base/luasrc/cbi.lua index d275c5b27..971830fe8 100644 --- a/luci-base/luasrc/cbi.lua +++ b/luci-base/luasrc/cbi.lua @@ -1199,19 +1199,20 @@ function TypedSection.parse(self, novld) if name then -- Ignore if it already exists if self:cfgvalue(name) then - name = nil; - end - - name = self:checkscope(name) - - if not name then + name = nil self.err_invalid = true - end + else + name = self:checkscope(name) - if name and #name > 0 then - created = self:create(name, origin) and name - if not created then - self.invalid_cts = true + if not name then + self.err_invalid = true + end + + if name and #name > 0 then + created = self:create(name, origin) and name + if not created then + self.invalid_cts = true + end end end end diff --git a/luci-base/luasrc/cbi/datatypes.lua b/luci-base/luasrc/cbi/datatypes.lua index 99113e0b7..33d018d2d 100644 --- a/luci-base/luasrc/cbi/datatypes.lua +++ b/luci-base/luasrc/cbi/datatypes.lua @@ -132,6 +132,10 @@ function ip6prefix(val) return ( val and val >= 0 and val <= 128 ) end +function cidr(val) + return cidr4(val) or cidr6(val) +end + function cidr4(val) local ip, mask = val:match("^([^/]+)/([^/]+)$") @@ -460,3 +464,7 @@ function dateyyyymmdd(val) end return false end + +function unique(val) + return true +end diff --git a/luci-base/luasrc/controller/admin/index.lua b/luci-base/luasrc/controller/admin/index.lua new file mode 100644 index 000000000..1f7db0cb3 --- /dev/null +++ b/luci-base/luasrc/controller/admin/index.lua @@ -0,0 +1,164 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +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", "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) + + if nixio.fs.access("/etc/config/dhcp") then + page = entry({"admin", "dhcplease_status"}, call("lease_status"), nil) + page.leaf = true + end + + local has_wifi = false + + uci:foreach("wireless", "wifi-device", + function(s) + has_wifi = true + return false + end) + + if has_wifi then + page = entry({"admin", "wireless_assoclist"}, call("wifi_assoclist"), nil) + page.leaf = true + + page = entry({"admin", "wireless_deauth"}, post("wifi_deauth"), nil) + page.leaf = true + end + + page = entry({"admin", "translations"}, call("action_translations"), nil) + 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" + local sid = dsp.context.authsession + + if sid then + utl.ubus("session", "destroy", { ubus_rpc_session = sid }) + + luci.http.header("Set-Cookie", "sysauth=%s; expires=%s; path=%s" %{ + '', 'Thu, 01 Jan 1970 01:00:00 GMT', dsp.build_url() + }) + end + + luci.http.redirect(dsp.build_url()) +end + +function action_translations(lang) + local i18n = require "luci.i18n" + local http = require "luci.http" + local fs = require "nixio".fs + + if lang and #lang > 0 then + lang = i18n.setlanguage(lang) + if lang then + local s = fs.stat("%s/base.%s.lmo" %{ i18n.i18ndir, lang }) + if s then + http.header("Cache-Control", "public, max-age=31536000") + http.header("ETag", "%x-%x-%x" %{ s["ino"], s["size"], s["mtime"] }) + end + end + end + + http.prepare_content("application/javascript; charset=utf-8") + http.write("window.TR=") + http.write_json(i18n.dump()) +end + + +function lease_status() + local s = require "luci.tools.status" + + luci.http.prepare_content("application/json") + luci.http.write('[') + luci.http.write_json(s.dhcp_leases()) + luci.http.write(',') + luci.http.write_json(s.dhcp6_leases()) + luci.http.write(']') +end + +function wifi_assoclist() + local s = require "luci.tools.status" + + luci.http.prepare_content("application/json") + luci.http.write_json(s.wifi_assoclist()) +end + +function wifi_deauth() + local iface = luci.http.formvalue("iface") + local bssid = luci.http.formvalue("bssid") + + if iface and bssid then + luci.util.ubus("hostapd.%s" % iface, "del_client", { + addr = bssid, + deauth = true, + reason = 5, + ban_time = 60000 + }) + end + luci.http.status(200, "OK") +end diff --git a/luci-mod-admin-full/luasrc/controller/admin/uci.lua b/luci-base/luasrc/controller/admin/uci.lua similarity index 100% rename from luci-mod-admin-full/luasrc/controller/admin/uci.lua rename to luci-base/luasrc/controller/admin/uci.lua diff --git a/luci-base/luasrc/dispatcher.lua b/luci-base/luasrc/dispatcher.lua index 6cf2712eb..626a46dfd 100644 --- a/luci-base/luasrc/dispatcher.lua +++ b/luci-base/luasrc/dispatcher.lua @@ -40,6 +40,28 @@ function build_url(...) return table.concat(url, "") end +function _ordered_children(node) + local name, child, children = nil, nil, {} + + for name, child in pairs(node.nodes) do + children[#children+1] = { + name = name, + node = child, + order = child.order or 100 + } + end + + 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) + + return children +end + function node_visible(node) if node then return not ( @@ -55,15 +77,10 @@ end function node_childs(node) local rv = { } if node then - local k, v - for k, v in util.spairs(node.nodes, - function(a, b) - return (node.nodes[a].order or 100) - < (node.nodes[b].order or 100) - end) - do - if node_visible(v) then - rv[#rv+1] = k + local _, child + for _, child in ipairs(_ordered_children(node)) do + if node_visible(child.node) then + rv[#rv+1] = child.name end end end @@ -296,10 +313,6 @@ function dispatch(request) ctx.requestpath = ctx.requestpath or freq ctx.path = preq - if track.i18n then - i18n.loadc(track.i18n) - end - -- Init template engine if (c and c.index) or not track.notemplate then local tpl = require("luci.template") @@ -315,7 +328,7 @@ function dispatch(request) assert(media, "No valid theme found") end - local function _ifattr(cond, key, val) + local function _ifattr(cond, key, val, noescape) if cond then local env = getfenv(3) local scope = (type(env.self) == "table") and env.self @@ -326,13 +339,16 @@ function dispatch(request) val = util.serialize_json(val) end end - return string.format( - ' %s="%s"', tostring(key), - util.pcdata(tostring( val - or (type(env[key]) ~= "function" and env[key]) - or (scope and type(scope[key]) ~= "function" and scope[key]) - or "" )) - ) + + val = tostring(val or + (type(env[key]) ~= "function" and env[key]) or + (scope and type(scope[key]) ~= "function" and scope[key]) or "") + + if noescape ~= true then + val = util.pcdata(val) + end + + return string.format(' %s="%s"', tostring(key), val) else return '' end @@ -421,6 +437,7 @@ function dispatch(request) 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 @@ -437,6 +454,7 @@ function dispatch(request) if not sid or not sdat then http.status(403, "Forbidden") + http.header("X-LuCI-Login-Required", "yes") return end @@ -597,14 +615,9 @@ function createtree() local ctx = context local tree = {nodes={}, inreq=true} - local modi = {} ctx.treecache = setmetatable({}, {__mode="v"}) ctx.tree = tree - ctx.modifiers = modi - - -- Load default translation - require "luci.i18n".loadc("base") local scope = setmetatable({}, {__index = luci.dispatcher}) @@ -614,28 +627,9 @@ function createtree() v() end - local function modisort(a,b) - return modi[a].order < modi[b].order - end - - for _, v in util.spairs(modi, modisort) do - scope._NAME = v.module - setfenv(v.func, scope) - v.func() - end - return tree end -function modifier(func, order) - context.modifiers[#context.modifiers+1] = { - func = func, - order = order or 0, - module - = getfenv(2)._NAME - } -end - function assign(path, clone, title, order) local obj = node(unpack(path)) obj.nodes = nil @@ -724,32 +718,66 @@ 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() - local path = { unpack(context.path) } - local name = table.concat(path, ".") - local node = context.treecache[name] - - local lowest - if node and node.nodes and next(node.nodes) then - local k, v - for k, v in pairs(node.nodes) do - if not lowest or - (v.order or 100) < (node.nodes[lowest].order or 100) - then - lowest = k - end - end - end - - assert(lowest ~= nil, - "The requested node contains no childs, unable to redispatch") - - path[#path+1] = lowest - dispatch(path) + return _find_node(false, nil) end function firstchild() - return { type = "firstchild", target = _firstchild } + return { type = "firstchild", target = _firstchild } +end + +function _firstnode() + return _find_node(true, { "cbi", "form", "template", "arcombine" }) +end + +function firstnode() + return { type = "firstnode", target = _firstnode } end function alias(...) diff --git a/luci-base/luasrc/dispatcher.luadoc b/luci-base/luasrc/dispatcher.luadoc index ddf534b3e..a77f8d8b0 100644 --- a/luci-base/luasrc/dispatcher.luadoc +++ b/luci-base/luasrc/dispatcher.luadoc @@ -22,7 +22,7 @@ Check whether a dispatch node shall be visible ]] ---[[ -Return a sorted table of visible childs within a given node +Return a sorted table of visible children within a given node @class function @name node_childs @@ -81,15 +81,6 @@ Build the index before if it does not exist yet. @name createtree ]] ----[[ -Register a tree modifier. - -@class function -@name modifier -@param func Modifier function -@param order Modifier order value (optional) -]] - ---[[ Clone a node of the dispatching tree to another position. diff --git a/luci-base/luasrc/http.lua b/luci-base/luasrc/http.lua index f4ede4b8a..20b55f285 100644 --- a/luci-base/luasrc/http.lua +++ b/luci-base/luasrc/http.lua @@ -335,13 +335,13 @@ end -- Content-Type. Stores all extracted data associated with its parameter name -- in the params table within the given message object. Multiple parameter -- values are stored as tables, ordinary ones as strings. --- If an optional file callback function is given then it is feeded with the +-- If an optional file callback function is given then it is fed with the -- file contents chunk by chunk and only the extracted file name is stored -- within the params table. The callback function will be called subsequently -- with three arguments: -- o Table containing decoded (name, file) and raw (headers) mime header data -- o String value containing a chunk of the file data --- o Boolean which indicates wheather the current chunk is the last one (eof) +-- o Boolean which indicates whether the current chunk is the last one (eof) function mimedecode_message_body(src, msg, file_cb) local parser, header, field local len, maxlen = 0, tonumber(msg.env.CONTENT_LENGTH or nil) diff --git a/luci-base/luasrc/http.luadoc b/luci-base/luasrc/http.luadoc index f8121230b..8f6f380d8 100644 --- a/luci-base/luasrc/http.luadoc +++ b/luci-base/luasrc/http.luadoc @@ -204,13 +204,13 @@ Stores all extracted data associated with its parameter name in the params table within the given message object. Multiple parameter values are stored as tables, ordinary ones as strings. -If an optional file callback function is given then it is feeded with the +If an optional file callback function is given then it is fed with the file contents chunk by chunk and only the extracted file name is stored within the params table. The callback function will be called subsequently with three arguments: o Table containing decoded (name, file) and raw (headers) mime header data o String value containing a chunk of the file data - o Boolean which indicates wheather the current chunk is the last one (eof) + o Boolean which indicates whether the current chunk is the last one (eof) @class function @name mimedecode_message_body diff --git a/luci-base/luasrc/i18n.lua b/luci-base/luasrc/i18n.lua index bcb16d5c0..323912b65 100644 --- a/luci-base/luasrc/i18n.lua +++ b/luci-base/luasrc/i18n.lua @@ -1,37 +1,43 @@ -- Copyright 2008 Steven Barth -- Licensed to the public under the Apache License 2.0. -module("luci.i18n", package.seeall) -require("luci.util") +local tparser = require "luci.template.parser" +local util = require "luci.util" +local tostring = tostring -local tparser = require "luci.template.parser" +module "luci.i18n" -table = {} -i18ndir = luci.util.libpath() .. "/i18n/" -loaded = {} -context = luci.util.threadlocal() +i18ndir = util.libpath() .. "/i18n/" +context = util.threadlocal() default = "en" -function clear() -end - -function load(file, lang, force) -end - --- Alternatively load the translation of the fallback language. -function loadc(file, force) -end function setlanguage(lang) - context.lang = lang:gsub("_", "-") - context.parent = (context.lang:match("^([a-z][a-z])_")) - if not tparser.load_catalog(context.lang, i18ndir) then - if context.parent then - tparser.load_catalog(context.parent, i18ndir) + local code, subcode = lang:match("^([A-Za-z][A-Za-z])[%-_]([A-Za-z][A-Za-z])$") + if not (code and subcode) then + subcode = lang:match("^([A-Za-z][A-Za-z])$") + if not subcode then + return nil + end + end + + context.parent = code and code:lower() + context.lang = context.parent and context.parent.."-"..subcode:lower() or subcode:lower() + + if tparser.load_catalog(context.lang, i18ndir) and + tparser.change_catalog(context.lang) + then + return context.lang + + elseif context.parent then + if tparser.load_catalog(context.parent, i18ndir) and + tparser.change_catalog(context.parent) + then return context.parent end end - return context.lang + + return nil end function translate(key) @@ -42,14 +48,8 @@ function translatef(key, ...) return tostring(translate(key)):format(...) end --- and ensure that the returned value is a Lua string value. --- This is the same as calling tostring(translate(...)) -function string(key) - return tostring(translate(key)) -end - --- Ensure that the returned value is a Lua string value. --- This is the same as calling tostring(translatef(...)) -function stringf(key, ...) - return tostring(translate(key)):format(...) +function dump() + local rv = {} + tparser.get_translations(function(k, v) rv[k] = v end) + return rv end diff --git a/luci-base/luasrc/i18n.luadoc b/luci-base/luasrc/i18n.luadoc index aa38841e1..b76c29856 100644 --- a/luci-base/luasrc/i18n.luadoc +++ b/luci-base/luasrc/i18n.luadoc @@ -3,41 +3,13 @@ LuCI translation library. ]] module "luci.i18n" ----[[ -Clear the translation table. - - -@class function -@name clear -]] - ----[[ -Load a translation and copy its data into the translation table. - -@class function -@name load -@param file Language file -@param lang Two-letter language code -@param force Force reload even if already loaded (optional) -@return Success status -]] - ----[[ -Load a translation file using the default translation language. - -Alternatively load the translation of the fallback language. -@class function -@name loadc -@param file Language file -@param force Force reload even if already loaded (optional) -]] - ---[[ Set the context default translation language. @class function @name setlanguage -@param lang Two-letter language code +@param lang An IETF/BCP 47 language tag or ISO3166 country code, e.g. "en-US" or "de" +@return The effective loaded language, e.g. "en" for "en-US" - or nil on failure ]] ---[[ @@ -60,25 +32,11 @@ Return the translated value for a specific translation key and use it as sprintf ]] ---[[ -Return the translated value for a specific translation key +Return all currently loaded translation strings as a key-value table. The key is the +hexadecimal representation of the translation key while the value is the translated +text content. -and ensure that the returned value is a Lua string value. -This is the same as calling tostring(translate(...)) @class function -@name string -@param key Default translation text -@return Translated string +@name dump +@return Key-value translation string table. ]] - ----[[ -Return the translated value for a specific translation key and use it as sprintf pattern. - -Ensure that the returned value is a Lua string value. -This is the same as calling tostring(translatef(...)) -@class function -@name stringf -@param key Default translation text -@param ... Format parameters -@return Translated and formatted string -]] - diff --git a/luci-base/luasrc/model/cbi/admin_network/proto_dhcp.lua b/luci-base/luasrc/model/cbi/admin_network/proto_dhcp.lua index dc702e4a9..6e04465ac 100644 --- a/luci-base/luasrc/model/cbi/admin_network/proto_dhcp.lua +++ b/luci-base/luasrc/model/cbi/admin_network/proto_dhcp.lua @@ -53,6 +53,7 @@ metric.datatype = "uinteger" clientid = section:taboption("advanced", Value, "clientid", translate("Client ID to send when requesting DHCP")) +clientid.datatype = "hexstring" vendorclass = section:taboption("advanced", Value, "vendorid", diff --git a/luci-base/luasrc/model/cbi/admin_network/proto_static.lua b/luci-base/luasrc/model/cbi/admin_network/proto_static.lua index 1c70f85af..246d2c0ed 100644 --- a/luci-base/luasrc/model/cbi/admin_network/proto_static.lua +++ b/luci-base/luasrc/model/cbi/admin_network/proto_static.lua @@ -4,17 +4,93 @@ local map, section, net = ... local ifc = net:get_interface() -local ipaddr, netmask, gateway, broadcast, dns, accept_ra, send_rs, ip6addr, ip6gw -local mtu, metric +local netmask, gateway, broadcast, dns, accept_ra, send_rs, ip6addr, ip6gw +local mtu, metric, usecidr, ipaddr_single, ipaddr_multi -ipaddr = section:taboption("general", Value, "ipaddr", translate("IPv4 address")) -ipaddr.datatype = "ip4addr" +local function is_cidr(s) + return (type(s) == "string" and luci.ip.IPv4(s) and s:find("/")) +end + +usecidr = section:taboption("general", Value, "ipaddr_usecidr") +usecidr.forcewrite = true + +usecidr.cfgvalue = function(self, section) + local cfgvalue = self.map:get(section, "ipaddr") + return (type(cfgvalue) == "table" or is_cidr(cfgvalue)) and "1" or "0" +end + +usecidr.render = function(self, section, scope) + luci.template.Template(nil, [[ + /> + ]]):render({ + cbid = self:cbid(section), + value = self:cfgvalue(section) + }) +end + +usecidr.write = function(self, section) + local cfgvalue = self.map:get(section, "ipaddr") + local formvalue = (self:formvalue(section) == "1") and ipaddr_multi:formvalue(section) or ipaddr_single:formvalue(section) + local equal = (cfgvalue == formvalue) + + if not equal and type(cfgvalue) == "table" and type(formvalue) == "table" and #cfgvalue == #formvalue then + equal = true + + local _, v + for _, v in ipairs(cfgvalue) do + if v ~= formvalue[_] then + equal = false + break + end + end + end + + if not equal then + self.map:set(section, "ipaddr", formvalue or "") + end + + return not equal +end -netmask = section:taboption("general", Value, "netmask", - translate("IPv4 netmask")) +ipaddr_multi = section:taboption("general", DynamicList, "ipaddrs", translate("IPv4 address")) +ipaddr_multi:depends("ipaddr_usecidr", "1") +ipaddr_multi.datatype = "or(cidr4,ipnet4)" +ipaddr_multi.placeholder = translate("Add IPv4 address…") +ipaddr_multi.alias = "ipaddr" +ipaddr_multi.write = function() end +ipaddr_multi.remove = function() end +ipaddr_multi.cfgvalue = function(self, section) + local addr = self.map:get(section, "ipaddr") + local mask = self.map:get(section, "netmask") + + if is_cidr(addr) then + return { addr } + elseif type(addr) == "string" and + type(mask) == "string" and + #addr > 0 and #mask > 0 + then + return { "%s/%s" %{ addr, mask } } + elseif type(addr) == "table" then + return addr + else + return {} + end +end + + +ipaddr_single = section:taboption("general", Value, "ipaddr", translate("IPv4 address")) +ipaddr_single:depends("ipaddr_usecidr", "0") +ipaddr_single.datatype = "ip4addr" +ipaddr_single.template = "cbi/ipaddr" +ipaddr_single.write = function() end +ipaddr_single.remove = function() end + + +netmask = section:taboption("general", Value, "netmask", translate("IPv4 netmask")) +netmask:depends("ipaddr_usecidr", "0") netmask.datatype = "ip4addr" netmask:value("255.255.255.0") netmask:value("255.255.0.0") @@ -48,8 +124,9 @@ if luci.model.network:has_ipv6() then translate("Assign prefix parts using this hexadecimal subprefix ID for this interface.")) for i=33,64 do ip6hint:depends("ip6assign", i) end - ip6addr = section:taboption("general", Value, "ip6addr", translate("IPv6 address")) + ip6addr = section:taboption("general", DynamicList, "ip6addr", translate("IPv6 address")) ip6addr.datatype = "ip6addr" + ip6addr.placeholder = translate("Add IPv6 address…") ip6addr:depends("ip6assign", "") @@ -83,14 +160,8 @@ mtu.placeholder = "1500" mtu.datatype = "max(9200)" ---metric = section:taboption("advanced", Value, "metric", --- translate("Use gateway metric")) ---metric.default = "1" ---metric.datatype = "uinteger" +metric = section:taboption("advanced", Value, "metric", + translate("Use gateway metric")) ---local nw = require "luci.model.network".init() ---for _, network in ipairs(nw:get_networks()) do --- if network:proto() == "static" and network:type() == "macvlan" and tonumber(network:metric()) >= tonumber(metric.default) then --- metric.default = network:metric() + 1 --- end ---end +metric.placeholder = "0" +metric.datatype = "uinteger" diff --git a/luci-base/luasrc/model/ipkg.lua b/luci-base/luasrc/model/ipkg.lua deleted file mode 100644 index e27ea5289..000000000 --- a/luci-base/luasrc/model/ipkg.lua +++ /dev/null @@ -1,247 +0,0 @@ --- Copyright 2008-2011 Jo-Philipp Wich --- Copyright 2008 Steven Barth --- Licensed to the public under the Apache License 2.0. - -local os = require "os" -local io = require "io" -local fs = require "nixio.fs" -local util = require "luci.util" - -local type = type -local pairs = pairs -local error = error -local table = table - -local ipkg = "opkg --force-removal-of-dependent-packages --force-overwrite --nocase" -local icfg = "/etc/opkg.conf" - -module "luci.model.ipkg" - - --- Internal action function -local function _action(cmd, ...) - local cmdline = { ipkg, cmd } - - local k, v - for k, v in pairs({...}) do - cmdline[#cmdline+1] = util.shellquote(v) - end - - local c = "%s >/tmp/opkg.stdout 2>/tmp/opkg.stderr" % table.concat(cmdline, " ") - local r = os.execute(c) - local e = fs.readfile("/tmp/opkg.stderr") - local o = fs.readfile("/tmp/opkg.stdout") - - fs.unlink("/tmp/opkg.stderr") - fs.unlink("/tmp/opkg.stdout") - - return r, o or "", e or "" -end - --- Internal parser function -local function _parselist(rawdata) - if type(rawdata) ~= "function" then - error("OPKG: Invalid rawdata given") - end - - local data = {} - local c = {} - local l = nil - - for line in rawdata do - if line:sub(1, 1) ~= " " then - local key, val = line:match("(.-): ?(.*)%s*") - - if key and val then - if key == "Package" then - c = {Package = val} - data[val] = c - elseif key == "Status" then - c.Status = {} - for j in val:gmatch("([^ ]+)") do - c.Status[j] = true - end - else - c[key] = val - end - l = key - end - else - -- Multi-line field - c[l] = c[l] .. "\n" .. line - end - end - - return data -end - --- Internal lookup function -local function _lookup(cmd, pkg) - local cmdline = { ipkg, cmd } - if pkg then - cmdline[#cmdline+1] = util.shellquote(pkg) - end - - -- OPKG sometimes kills the whole machine because it sucks - -- Therefore we have to use a sucky approach too and use - -- tmpfiles instead of directly reading the output - local tmpfile = os.tmpname() - os.execute("%s >%s 2>/dev/null" %{ table.concat(cmdline, " "), tmpfile }) - - local data = _parselist(io.lines(tmpfile)) - os.remove(tmpfile) - return data -end - - -function info(pkg) - return _lookup("info", pkg) -end - -function status(pkg) - return _lookup("status", pkg) -end - -function install(...) - return _action("install", ...) -end - -function installed(pkg) - local p = status(pkg)[pkg] - return (p and p.Status and p.Status.installed) -end - -function remove(...) - return _action("remove", ...) -end - -function update() - return _action("update") -end - -function upgrade() - return _action("upgrade") -end - --- List helper -local function _list(action, pat, cb) - local cmdline = { ipkg, action } - if pat then - cmdline[#cmdline+1] = util.shellquote(pat) - end - - local fd = io.popen(table.concat(cmdline, " ")) - if fd then - local name, version, sz, desc - while true do - local line = fd:read("*l") - if not line then break end - - name, version, sz, desc = line:match("^(.-) %- (.-) %- (.-) %- (.+)") - - if not name then - name, version, sz = line:match("^(.-) %- (.-) %- (.+)") - desc = "" - end - - if name and version then - if #version > 26 then - version = version:sub(1,21) .. ".." .. version:sub(-3,-1) - end - - cb(name, version, sz, desc) - end - - name = nil - version = nil - sz = nil - desc = nil - end - - fd:close() - end -end - -function list_all(pat, cb) - _list("list --size", pat, cb) -end - -function list_installed(pat, cb) - _list("list_installed --size", pat, cb) -end - -function find(pat, cb) - _list("find --size", pat, cb) -end - - -function overlay_root() - local od = "/" - local fd = io.open(icfg, "r") - - if fd then - local ln - - repeat - ln = fd:read("*l") - if ln and ln:match("^%s*option%s+overlay_root%s+") then - od = ln:match("^%s*option%s+overlay_root%s+(%S+)") - - local s = fs.stat(od) - if not s or s.type ~= "dir" then - od = "/" - end - - break - end - until not ln - - fd:close() - end - - return od -end - -function compare_versions(ver1, comp, ver2) - if not ver1 or not ver2 - or not comp or not (#comp > 0) then - error("Invalid parameters") - return nil - end - -- correct compare string - if comp == "<>" or comp == "><" or comp == "!=" or comp == "~=" then comp = "~=" - elseif comp == "<=" or comp == "<" or comp == "=<" then comp = "<=" - elseif comp == ">=" or comp == ">" or comp == "=>" then comp = ">=" - elseif comp == "=" or comp == "==" then comp = "==" - elseif comp == "<<" then comp = "<" - elseif comp == ">>" then comp = ">" - else - error("Invalid compare string") - return nil - end - - local av1 = util.split(ver1, "[%.%-]", nil, true) - local av2 = util.split(ver2, "[%.%-]", nil, true) - - local max = table.getn(av1) - if (table.getn(av1) < table.getn(av2)) then - max = table.getn(av2) - end - - for i = 1, max, 1 do - local s1 = av1[i] or "" - local s2 = av2[i] or "" - - -- first "not equal" found return true - if comp == "~=" and (s1 ~= s2) then return true end - -- first "lower" found return true - if (comp == "<" or comp == "<=") and (s1 < s2) then return true end - -- first "greater" found return true - if (comp == ">" or comp == ">=") and (s1 > s2) then return true end - -- not equal then return false - if (s1 ~= s2) then return false end - end - - -- all equal and not compare greater or lower then true - return not (comp == "<" or comp == ">") -end diff --git a/luci-base/luasrc/model/ipkg.luadoc b/luci-base/luasrc/model/ipkg.luadoc deleted file mode 100644 index 4e1548dda..000000000 --- a/luci-base/luasrc/model/ipkg.luadoc +++ /dev/null @@ -1,125 +0,0 @@ ----[[ -LuCI OPKG call abstraction library -]] -module "luci.model.ipkg" - ----[[ -Return information about installed and available packages. - -@class function -@name info -@param pkg Limit output to a (set of) packages -@return Table containing package information -]] - ----[[ -Return the package status of one or more packages. - -@class function -@name status -@param pkg Limit output to a (set of) packages -@return Table containing package status information -]] - ----[[ -Install one or more packages. - -@class function -@name install -@param ... List of packages to install -@return Boolean indicating the status of the action -@return OPKG return code, STDOUT and STDERR -]] - ----[[ -Determine whether a given package is installed. - -@class function -@name installed -@param pkg Package -@return Boolean -]] - ----[[ -Remove one or more packages. - -@class function -@name remove -@param ... List of packages to install -@return Boolean indicating the status of the action -@return OPKG return code, STDOUT and STDERR -]] - ----[[ -Update package lists. - -@class function -@name update -@return Boolean indicating the status of the action -@return OPKG return code, STDOUT and STDERR -]] - ----[[ -Upgrades all installed packages. - -@class function -@name upgrade -@return Boolean indicating the status of the action -@return OPKG return code, STDOUT and STDERR -]] - ----[[ -List all packages known to opkg. - -@class function -@name list_all -@param pat Only find packages matching this pattern, nil lists all packages -@param cb Callback function invoked for each package, receives name, version and description as arguments -@return nothing -]] - ----[[ -List installed packages. - -@class function -@name list_installed -@param pat Only find packages matching this pattern, nil lists all packages -@param cb Callback function invoked for each package, receives name, version and description as arguments -@return nothing -]] - ----[[ -Find packages that match the given pattern. - -@class function -@name find -@param pat Find packages whose names or descriptions match this pattern, nil results in zero results -@param cb Callback function invoked for each patckage, receives name, version and description as arguments -@return nothing -]] - ----[[ -Determines the overlay root used by opkg. - -@class function -@name overlay_root -@return String containing the directory path of the overlay root. -]] - ----[[ -lua version of opkg compare-versions - -@class function -@name compare_versions -@param ver1 string version 1 -@param ver2 string version 2 -@param comp string compare versions using - "<=" or "<" lower-equal - ">" or ">=" greater-equal - "=" equal - "<<" lower - ">>" greater - "~=" not equal -@return Boolean indicating the status of the compare -]] - diff --git a/luci-base/luasrc/model/network.lua b/luci-base/luasrc/model/network.lua index cce559aab..b8467b65e 100644 --- a/luci-base/luasrc/model/network.lua +++ b/luci-base/luasrc/model/network.lua @@ -20,7 +20,7 @@ module "luci.model.network" IFACE_PATTERNS_VIRTUAL = { } -IFACE_PATTERNS_IGNORE = { "^wmaster%d", "^wifi%d", "^hwsim%d", "^imq%d", "^ifb%d", "^mon%.wlan%d", "^sit%d", "^gre%d", "^gretap%d", "^ip6gre%d", "^ip6tnl%d", "^tunl%d", "^lo$" } +IFACE_PATTERNS_IGNORE = { "^wmaster%d", "^wifi%d", "^hwsim%d", "^imq%d", "^ifb%d", "^mon%.wlan%d", "^sit%d", "^gre%d", "^gretap%d", "^ip6gre%d", "^ip6tnl%d", "^tunl%d", "^lo$", "^teql%d" } IFACE_PATTERNS_WIRELESS = { "^wlan%d", "^wl%d", "^ath%d", "^%w+%.network%d" } IFACE_ERRORS = { @@ -622,6 +622,12 @@ function del_network(self, n) _uci:delete("wireless", s['.name'], "network") end end) + + local ok, fw = pcall(require, "luci.model.firewall") + if ok then + fw.init() + fw:del_network(n) + end end return r end @@ -813,6 +819,7 @@ function del_wifinet(self, net) end function get_status_by_route(self, addr, mask) + local route_statuses = { } local _, object for _, object in ipairs(utl.ubus()) do local net = object:match("^network%.interface%.(.+)") @@ -822,12 +829,14 @@ function get_status_by_route(self, addr, mask) local rt for _, rt in ipairs(s.route) do if not rt.table and rt.target == addr and rt.mask == mask then - return net, s + route_statuses[net] = s end end end end end + + return route_statuses end function get_status_by_address(self, addr) @@ -852,28 +861,40 @@ function get_status_by_address(self, addr) end end end + if s and s['ipv6-prefix-assignment'] then + local a + for _, a in ipairs(s['ipv6-prefix-assignment']) do + if a and a['local-address'] and a['local-address'].address == addr then + return net, s + end + end + end end end end -function get_wannet(self) - local net, stat = self:get_status_by_route("0.0.0.0", 0) - return net and network(net, stat.proto) +function get_wan_networks(self) + local k, v + local wan_nets = { } + local route_statuses = self:get_status_by_route("0.0.0.0", 0) + + for k, v in pairs(route_statuses) do + wan_nets[#wan_nets+1] = network(k, v.proto) + end + + return wan_nets end -function get_wandev(self) - local _, stat = self:get_status_by_route("0.0.0.0", 0) - return stat and interface(stat.l3_device or stat.device) -end +function get_wan6_networks(self) + local k, v + local wan6_nets = { } + local route_statuses = self:get_status_by_route("::", 0) -function get_wan6net(self) - local net, stat = self:get_status_by_route("::", 0) - return net and network(net, stat.proto) -end + for k, v in pairs(route_statuses) do + wan6_nets[#wan6_nets+1] = network(k, v.proto) + end -function get_wan6dev(self) - local _, stat = self:get_status_by_route("::", 0) - return stat and interface(stat.l3_device or stat.device) + return wan6_nets end function get_switch_topologies(self) @@ -1144,6 +1165,10 @@ function protocol.is_dynamic(self) return (self:_ubus("dynamic") == true) end +function protocol.is_auto(self) + return (self:_get("auto") ~= "0") +end + function protocol.is_alias(self) local ifn, parent = nil, nil diff --git a/luci-base/luasrc/model/uci.lua b/luci-base/luasrc/model/uci.lua index b2c1e463b..a50e28a87 100644 --- a/luci-base/luasrc/model/uci.lua +++ b/luci-base/luasrc/model/uci.lua @@ -15,7 +15,7 @@ local type, tostring, tonumber, unpack = type, tostring, tonumber, unpack -- cursor factory, modify data (via Cursor.add, Cursor.delete, etc.), -- save the changes to the staging area via Cursor.save and finally -- Cursor.commit the data to the actual config files. --- LuCI then needs to Cursor.apply the changes so deamons etc. are +-- LuCI then needs to Cursor.apply the changes so daemons etc. are -- reloaded. module "luci.model.uci" @@ -95,41 +95,15 @@ end function changes(self, config) - local rv = call("changes", { config = config }) - local res = {} + local rv, err = call("changes", { config = config }) if type(rv) == "table" and type(rv.changes) == "table" then - local package, changes - for package, changes in pairs(rv.changes) do - res[package] = {} - - local _, change - for _, change in ipairs(changes) do - local operation, section, option, value = unpack(change) - if option and operation ~= "add" then - res[package][section] = res[package][section] or { } - - if operation == "list-add" then - local v = res[package][section][option] - if type(v) == "table" then - v[#v+1] = value or "" - elseif v ~= nil then - res[package][section][option] = { v, value } - else - res[package][section][option] = { value } - end - else - res[package][section][option] = value or "" - end - else - res[package][section] = res[package][section] or {} - res[package][section][".type"] = option or "" - end - end - end + return rv.changes + elseif err then + return nil, ERRSTR[err] + else + return { } end - - return res end diff --git a/luci-base/luasrc/model/uci.luadoc b/luci-base/luasrc/model/uci.luadoc index d798b0033..0189d49aa 100644 --- a/luci-base/luasrc/model/uci.luadoc +++ b/luci-base/luasrc/model/uci.luadoc @@ -5,7 +5,7 @@ The typical workflow for UCI is: Get a cursor instance from the cursor factory, modify data (via Cursor.add, Cursor.delete, etc.), save the changes to the staging area via Cursor.save and finally Cursor.commit the data to the actual config files. -LuCI then needs to Cursor.apply the changes so deamons etc. are +LuCI then needs to Cursor.apply the changes so daemons etc. are reloaded. @cstyle instance ]] @@ -172,7 +172,7 @@ has the same effect as deleting the option. ---[[ Create a sub-state of this cursor. -The sub-state is tied to the parent curser, means it the parent unloads or +The sub-state is tied to the parent cursor, means it the parent unloads or loads configs, the sub state will do so as well. @class function @@ -339,7 +339,7 @@ Set the configuration directory. ]] ---[[ -Set the directory for uncommited changes. +Set the directory for uncommitted changes. @class function @name Cursor.set_savedir diff --git a/luci-base/luasrc/sys.lua b/luci-base/luasrc/sys.lua index dde7387d0..7e4a9d63c 100644 --- a/luci-base/luasrc/sys.lua +++ b/luci-base/luasrc/sys.lua @@ -13,8 +13,8 @@ local luci = {} luci.util = require "luci.util" luci.ip = require "luci.ip" -local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select = - tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select +local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack = + tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack module "luci.sys" @@ -70,6 +70,24 @@ function mounts() return data end +function mtds() + local data = {} + + if fs.access("/proc/mtd") then + for l in io.lines("/proc/mtd") do + local d, s, e, n = l:match('^([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+"([^%s]+)"') + if s and n then + local d = {} + d.size = tonumber(s, 16) + d.name = n + table.insert(data, d) + end + end + end + + return data +end + -- containing the whole environment is returned otherwise this function returns -- the corresponding string value for the given name or nil if no such variable -- exists. @@ -87,9 +105,9 @@ end function httpget(url, stream, target) if not target then local source = stream and io.popen or luci.util.exec - return source("wget -4 -T 20 -qO- %s" % luci.util.shellquote(url)) + return source("wget -qO- %s" % luci.util.shellquote(url)) else - return os.execute("wget -4 -T 20 -qO %s %s" % + return os.execute("wget -qO %s %s" % {luci.util.shellquote(target), luci.util.shellquote(url)}) end end @@ -386,8 +404,8 @@ function process.list() end for line in ps do - local pid, ppid, user, stat, vsz, mem, cpun, cpu, cmd = line:match( - "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][ 2 then + fd:close() + end +end + +function process.exec(command, stdout, stderr, nowait) + local out_r, out_w, err_r, err_w + if stdout then out_r, out_w = nixio.pipe() end + if stderr then err_r, err_w = nixio.pipe() end + + local pid = nixio.fork() + if pid == 0 then + nixio.chdir("/") + + local null = nixio.open("/dev/null", "w+") + if null then + nixio.dup(out_w or null, nixio.stdout) + nixio.dup(err_w or null, nixio.stderr) + nixio.dup(null, nixio.stdin) + xclose(out_w) + xclose(out_r) + xclose(err_w) + xclose(err_r) + xclose(null) + end + + nixio.exec(unpack(command)) + os.exit(-1) + end + + local _, pfds, rv = nil, {}, { code = -1, pid = pid } + + xclose(out_w) + xclose(err_w) + + if out_r then + pfds[#pfds+1] = { + fd = out_r, + cb = type(stdout) == "function" and stdout, + name = "stdout", + events = nixio.poll_flags("in", "err", "hup") + } + end + + if err_r then + pfds[#pfds+1] = { + fd = err_r, + cb = type(stderr) == "function" and stderr, + name = "stderr", + events = nixio.poll_flags("in", "err", "hup") + } + end + + while #pfds > 0 do + local nfds, err = nixio.poll(pfds, -1) + if not nfds and err ~= nixio.const.EINTR then + break + end + + local i + for i = #pfds, 1, -1 do + local rfd = pfds[i] + if rfd.revents > 0 then + local chunk, err = rfd.fd:read(4096) + if chunk and #chunk > 0 then + if rfd.cb then + rfd.cb(chunk) + else + rfd.buf = rfd.buf or {} + rfd.buf[#rfd.buf + 1] = chunk + end + else + table.remove(pfds, i) + if rfd.buf then + rv[rfd.name] = table.concat(rfd.buf, "") + end + rfd.fd:close() + end + end + end + end + + if not nowait then + _, _, rv.code = nixio.waitpid(pid) + end + + return rv +end + user = {} diff --git a/luci-base/luasrc/sys.luadoc b/luci-base/luasrc/sys.luadoc index 1c1fa9260..162650e7a 100644 --- a/luci-base/luasrc/sys.luadoc +++ b/luci-base/luasrc/sys.luadoc @@ -18,7 +18,7 @@ Execute a given shell command and capture its standard output @class function @name exec @param command Command to call -@return String containg the return the output of the command +@return String containing the return the output of the command ]] ---[[ @@ -38,7 +38,7 @@ exists. @class function @name getenv @param var Name of the environment variable to retrieve (optional) -@return String containg the value of the specified variable +@return String containing the value of the specified variable @return Table containing all variables if no variable name is given ]] @@ -271,6 +271,42 @@ Send a signal to a process identified by given pid. @return Number containing the error code if failed ]] +---[[ +Execute a process, optionally capturing stdio. + +Executes the process specified by the given argv vector, e.g. +`{ "/bin/sh", "-c", "echo 1" }` and waits for it to terminate unless a true +value has been passed for the "nowait" parameter. + +When a function value is passed for the stdout or stderr arguments, the passed +function is repeatedly called for each chunk read from the corresponding stdio +stream. The read data is passed as string containing at most 4096 bytes at a +time. + +When a true, non-function value is passed for the stdout or stderr arguments, +the data of the corresponding stdio stream is read into an internal string +buffer and returned as "stdout" or "stderr" field respectively in the result +table. + +When a true value is passed to the nowait parameter, the function does not +await process termination but returns as soon as all captured stdio streams +have been closed or - if no streams are captured - immediately after launching +the process. + +@class function +@name process.exec +@param commend Table containing the argv vector to execute +@param stdout Callback function or boolean to indicate capturing (optional) +@param stderr Callback function or boolean to indicate capturing (optional) +@param nowait Don't wait for process termination when true (optional) +@return Table containing at least the fields "code" which holds the exit + status of the invoked process or "-1" on error and "pid", which + contains the process id assigned to the spawned process. When + stdout and/or stderr capturing has been requested, it additionally + contains "stdout" and "stderr" fields respectively, holding the + captured stdio data as string. +]] + ---[[ LuCI system utilities / user related functions. @@ -279,7 +315,7 @@ LuCI system utilities / user related functions. ]] ---[[ -Retrieve user informations for given uid. +Retrieve user information for given uid. @class function @name getuser @@ -305,7 +341,7 @@ Test whether given string matches the password of a given system user. @name user.checkpasswd @param username String containing the Unix user name @param pass String containing the password to compare -@return Boolean indicating wheather the passwords are equal +@return Boolean indicating whether the passwords are equal ]] ---[[ diff --git a/luci-base/luasrc/sys/zoneinfo/tzdata.lua b/luci-base/luasrc/sys/zoneinfo/tzdata.lua index 47cb901a5..39fd4a3c8 100644 --- a/luci-base/luasrc/sys/zoneinfo/tzdata.lua +++ b/luci-base/luasrc/sys/zoneinfo/tzdata.lua @@ -16,14 +16,12 @@ TZ = { { 'Africa/Brazzaville', 'WAT-1' }, { 'Africa/Bujumbura', 'CAT-2' }, { 'Africa/Cairo', 'EET-2' }, - { 'Africa/Casablanca', 'WET0WEST,M3.5.0,M10.5.0/3' }, { 'Africa/Ceuta', 'CET-1CEST,M3.5.0,M10.5.0/3' }, { 'Africa/Conakry', 'GMT0' }, { 'Africa/Dakar', 'GMT0' }, { 'Africa/Dar es Salaam', 'EAT-3' }, { 'Africa/Djibouti', 'EAT-3' }, { 'Africa/Douala', 'WAT-1' }, - { 'Africa/El Aaiun', 'WET0WEST,M3.5.0,M10.5.0/3' }, { 'Africa/Freetown', 'GMT0' }, { 'Africa/Gaborone', 'CAT-2' }, { 'Africa/Harare', 'CAT-2' }, @@ -51,7 +49,7 @@ TZ = { { 'Africa/Nouakchott', 'GMT0' }, { 'Africa/Ouagadougou', 'GMT0' }, { 'Africa/Porto-Novo', 'WAT-1' }, - { 'Africa/Sao Tome', 'WAT-1' }, + { 'Africa/Sao Tome', 'GMT0' }, { 'Africa/Tripoli', 'EET-2' }, { 'Africa/Tunis', 'CET-1' }, { 'Africa/Windhoek', 'CAT-2' }, @@ -179,7 +177,7 @@ TZ = { { 'America/Resolute', 'CST6CDT,M3.2.0,M11.1.0' }, { 'America/Rio Branco', '<-05>5' }, { 'America/Santarem', '<-03>3' }, - { 'America/Santiago', '<-04>4<-03>,M8.2.6/24,M5.2.6/24' }, + { 'America/Santiago', '<-04>4<-03>,M9.1.6/24,M4.1.6/24' }, { 'America/Santo Domingo', 'AST4' }, { 'America/Sao Paulo', '<-03>3<-02>,M11.1.0/0,M2.3.0/0' }, { 'America/Scoresbysund', '<-01>1<+00>,M3.5.0/0,M10.5.0/1' }, @@ -261,7 +259,7 @@ TZ = { { 'Asia/Macau', 'CST-8' }, { 'Asia/Magadan', '<+11>-11' }, { 'Asia/Makassar', 'WITA-8' }, - { 'Asia/Manila', '<+08>-8' }, + { 'Asia/Manila', 'PST-8' }, { 'Asia/Muscat', '<+04>-4' }, { 'Asia/Nicosia', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, { 'Asia/Novokuznetsk', '<+07>-7' }, @@ -270,9 +268,10 @@ TZ = { { 'Asia/Oral', '<+05>-5' }, { 'Asia/Phnom Penh', '<+07>-7' }, { 'Asia/Pontianak', 'WIB-7' }, - { 'Asia/Pyongyang', 'KST-8:30' }, + { 'Asia/Pyongyang', 'KST-9' }, { 'Asia/Qatar', '<+03>-3' }, - { 'Asia/Qyzylorda', '<+06>-6' }, + { 'Asia/Qostanay', '<+06>-6' }, + { 'Asia/Qyzylorda', '<+05>-5' }, { 'Asia/Riyadh', '<+03>-3' }, { 'Asia/Sakhalin', '<+11>-11' }, { 'Asia/Samarkand', '<+05>-5' }, @@ -283,7 +282,7 @@ TZ = { { 'Asia/Taipei', 'CST-8' }, { 'Asia/Tashkent', '<+05>-5' }, { 'Asia/Tbilisi', '<+04>-4' }, - { 'Asia/Tehran', '<+0330>-3:30<+0430>,J80/0,J264/0' }, + { 'Asia/Tehran', '<+0330>-3:30<+0430>,J79/24,J263/24' }, { 'Asia/Thimphu', '<+06>-6' }, { 'Asia/Tokyo', 'JST-9' }, { 'Asia/Tomsk', '<+07>-7' }, @@ -358,7 +357,7 @@ TZ = { { 'Europe/Busingen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, { 'Europe/Chisinau', 'EET-2EEST,M3.5.0,M10.5.0/3' }, { 'Europe/Copenhagen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, - { 'Europe/Dublin', 'GMT0IST,M3.5.0/1,M10.5.0' }, + { 'Europe/Dublin', 'IST-1GMT0,M10.5.0,M3.5.0/1' }, { 'Europe/Gibraltar', 'CET-1CEST,M3.5.0,M10.5.0/3' }, { 'Europe/Guernsey', 'GMT0BST,M3.5.0/1,M10.5.0' }, { 'Europe/Helsinki', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, @@ -400,7 +399,7 @@ TZ = { { 'Europe/Vatican', 'CET-1CEST,M3.5.0,M10.5.0/3' }, { 'Europe/Vienna', 'CET-1CEST,M3.5.0,M10.5.0/3' }, { 'Europe/Vilnius', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, - { 'Europe/Volgograd', '<+03>-3' }, + { 'Europe/Volgograd', '<+04>-4' }, { 'Europe/Warsaw', 'CET-1CEST,M3.5.0,M10.5.0/3' }, { 'Europe/Zagreb', 'CET-1CEST,M3.5.0,M10.5.0/3' }, { 'Europe/Zaporozhye', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, @@ -421,11 +420,11 @@ TZ = { { 'Pacific/Bougainville', '<+11>-11' }, { 'Pacific/Chatham', '<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45' }, { 'Pacific/Chuuk', '<+10>-10' }, - { 'Pacific/Easter', '<-06>6<-05>,M8.2.6/22,M5.2.6/22' }, + { 'Pacific/Easter', '<-06>6<-05>,M9.1.6/22,M4.1.6/22' }, { 'Pacific/Efate', '<+11>-11' }, { 'Pacific/Enderbury', '<+13>-13' }, { 'Pacific/Fakaofo', '<+13>-13' }, - { 'Pacific/Fiji', '<+12>-12<+13>,M11.1.0,M1.2.1/147' }, + { 'Pacific/Fiji', '<+12>-12<+13>,M11.1.0,M1.2.2/123' }, { 'Pacific/Funafuti', '<+12>-12' }, { 'Pacific/Galapagos', '<-06>6' }, { 'Pacific/Gambier', '<-09>9' }, diff --git a/luci-base/luasrc/sys/zoneinfo/tzoffset.lua b/luci-base/luasrc/sys/zoneinfo/tzoffset.lua index cf5afeb9d..e63e2a695 100644 --- a/luci-base/luasrc/sys/zoneinfo/tzoffset.lua +++ b/luci-base/luasrc/sys/zoneinfo/tzoffset.lua @@ -9,7 +9,6 @@ OFFSET = { wat = 3600, -- WAT cat = 7200, -- CAT eet = 7200, -- EET - wet = 0, -- WET sast = 7200, -- SAST hst = -36000, -- HST hdt = -32400, -- HDT @@ -34,8 +33,9 @@ OFFSET = { idt = 10800, -- IDT pkt = 18000, -- PKT wita = 28800, -- WITA - kst = 30600, -- KST + kst = 32400, -- KST jst = 32400, -- JST + wet = 0, -- WET acst = 34200, -- ACST acdt = 37800, -- ACDT aest = 36000, -- AEST diff --git a/luci-base/luasrc/template.lua b/luci-base/luasrc/template.lua index 588028c2e..ed46f5075 100644 --- a/luci-base/luasrc/template.lua +++ b/luci-base/luasrc/template.lua @@ -95,6 +95,6 @@ function Template.render(self, scope) local stat, err = util.copcall(self.template) if not stat then error("Failed to execute template '" .. self.name .. "'.\n" .. - "A runtime error occured: " .. tostring(err or "(nil)")) + "A runtime error occurred: " .. tostring(err or "(nil)")) end end diff --git a/luci-base/luasrc/util.lua b/luci-base/luasrc/util.lua index f16b3afb2..1a329f3f2 100644 --- a/luci-base/luasrc/util.lua +++ b/luci-base/luasrc/util.lua @@ -262,7 +262,7 @@ end -- one token per invocation, the tokens are separated by whitespace. If the -- input value is a table, it is transformed into a string first. A nil value --- will result in a valid interator which aborts with the first invocation. +-- will result in a valid iterator which aborts with the first invocation. function imatch(v) if type(v) == "table" then local k = nil diff --git a/luci-base/luasrc/util.luadoc b/luci-base/luasrc/util.luadoc index c4f28d039..4ec68dd1e 100644 --- a/luci-base/luasrc/util.luadoc +++ b/luci-base/luasrc/util.luadoc @@ -158,7 +158,7 @@ Return a matching iterator for the given value. The iterator will return one token per invocation, the tokens are separated by whitespace. If the input value is a table, it is transformed into a string first. -A nil value will result in a valid interator which aborts with the first invocation. +A nil value will result in a valid iterator which aborts with the first invocation. @class function @name imatch @@ -289,7 +289,7 @@ will be stripped before it is returned. ]] ---[[ -Strips unnescessary lua bytecode from given string. +Strips unnecessary lua bytecode from given string. Information like line numbers and debugging numbers will be discarded. Original version by Peter Cawley (http://lua-users.org/lists/lua-l/2008-02/msg01158.html) diff --git a/luci-base/luasrc/view/admin_uci/changelog.htm b/luci-base/luasrc/view/admin_uci/changelog.htm new file mode 100644 index 000000000..9d6267cf3 --- /dev/null +++ b/luci-base/luasrc/view/admin_uci/changelog.htm @@ -0,0 +1,66 @@ +<%# + Copyright 2010 Jo-Philipp Wich + Licensed to the public under the Apache License 2.0. +-%> + +<% export("uci_changelog", function(changes) -%> +
    + <%:Legend:%> +
    +
      <%:Section added%>
    +
      <%:Section removed%>
    +
      <%:Option changed%>
    +
      <%:Option removed%>
    +
    +
    +
    + +
    <% + local util = luci.util + local tpl = { + ["add-3"] = "uci add %0 %3 # =%2", + ["set-3"] = "uci set %0.%2=%3", + ["set-4"] = "uci set %0.%2.%3=%4", + ["remove-2"] = "uci del %0.%2", + ["remove-3"] = "uci del %0.%2.%3", + ["order-3"] = "uci reorder %0.%2=%3", + ["list-add-4"] = "uci add_list %0.%2.%3=%4", + ["list-del-4"] = "uci del_list %0.%2.%3=%4", + ["rename-3"] = "uci rename %0.%2=%3", + ["rename-4"] = "uci rename %0.%2.%3=%4" + } + + local conf, deltas + for conf, deltas in util.kspairs(changes) do + write("

    # /etc/config/%s

    " % conf) + + local _, delta, added + for _, delta in pairs(deltas) do + local t = tpl["%s-%d" %{ delta[1], #delta }] + + write(t:gsub("%%(%d)", function(n) + if n == "0" then + return conf + elseif n == "2" then + if added and delta[2] == added[1] then + return "@%s[-1]" % added[2] + else + return delta[2] + end + elseif n == "4" then + return util.shellquote(delta[4]) + else + return delta[tonumber(n)] + end + end)) + + if delta[1] == "add" then + added = { delta[2], delta[3] } + end + end + + write("
    ") + end + %>
    +
    +<%- end) %> diff --git a/luci-mod-admin-full/luasrc/view/admin_uci/changes.htm b/luci-base/luasrc/view/admin_uci/changes.htm similarity index 100% rename from luci-mod-admin-full/luasrc/view/admin_uci/changes.htm rename to luci-base/luasrc/view/admin_uci/changes.htm diff --git a/luci-mod-admin-full/luasrc/view/admin_uci/revert.htm b/luci-base/luasrc/view/admin_uci/revert.htm similarity index 100% rename from luci-mod-admin-full/luasrc/view/admin_uci/revert.htm rename to luci-base/luasrc/view/admin_uci/revert.htm diff --git a/luci-base/luasrc/view/cbi/apply_widget.htm b/luci-base/luasrc/view/cbi/apply_widget.htm index 4d7e9c56e..0f9667390 100644 --- a/luci-base/luasrc/view/cbi/apply_widget.htm +++ b/luci-base/luasrc/view/cbi/apply_widget.htm @@ -1,49 +1,4 @@ <% export("cbi_apply_widget", function(redirect_ok, rollback_token) -%> - - - + + /> + <%+cbi/valuefooter%> diff --git a/luci-base/luasrc/view/cbi/cell_valueheader.htm b/luci-base/luasrc/view/cbi/cell_valueheader.htm index ea0568f40..cb11d8f61 100644 --- a/luci-base/luasrc/view/cbi/cell_valueheader.htm +++ b/luci-base/luasrc/view/cbi/cell_valueheader.htm @@ -6,7 +6,7 @@
    0, "data-type", ftype) .. - ifattr(title and #title > 0, "data-title", title) .. - ifattr(descr and #descr > 0, "data-description", descr) + ifattr(title and #title > 0, "data-title", title, true) .. + ifattr(descr and #descr > 0, "data-description", descr, true) %>>
    " data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>"> diff --git a/luci-base/luasrc/view/cbi/dropdown.htm b/luci-base/luasrc/view/cbi/dropdown.htm index cf8c03d22..6f4b89905 100644 --- a/luci-base/luasrc/view/cbi/dropdown.htm +++ b/luci-base/luasrc/view/cbi/dropdown.htm @@ -30,7 +30,7 @@ > <%=pcdata(self.vallist[i])%> diff --git a/luci-base/luasrc/view/cbi/dynlist.htm b/luci-base/luasrc/view/cbi/dynlist.htm index 4d0b50942..fa7dbdb41 100644 --- a/luci-base/luasrc/view/cbi/dynlist.htm +++ b/luci-base/luasrc/view/cbi/dynlist.htm @@ -6,22 +6,8 @@ self.keylist, self.vallist, self.datatype, self.optional or self.rmempty })) .. - + attr("data-values", luci.util.serialize_json(self:cfgvalue(section))) .. ifattr(self.size, "data-size", self.size) .. ifattr(self.placeholder, "data-placeholder", self.placeholder) -%>> -<% - local vals = self:cfgvalue(section) or {} - for i=1, #vals + 1 do - local val = vals[i] - if (val and #val > 0) or (i == 1) then -%> - />
    -<% end end %> -
    +%>>
    <%+cbi/valuefooter%> diff --git a/luci-base/luasrc/view/cbi/firewall_zoneforwards.htm b/luci-base/luasrc/view/cbi/firewall_zoneforwards.htm index b38e4b13d..dc251dbd9 100644 --- a/luci-base/luasrc/view/cbi/firewall_zoneforwards.htm +++ b/luci-base/luasrc/view/cbi/firewall_zoneforwards.htm @@ -63,7 +63,7 @@ if empty then %> <% end %> diff --git a/luci-base/luasrc/view/cbi/firewall_zonelist.htm b/luci-base/luasrc/view/cbi/firewall_zonelist.htm index 3a108020b..7ecec10a8 100644 --- a/luci-base/luasrc/view/cbi/firewall_zonelist.htm +++ b/luci-base/luasrc/view/cbi/firewall_zonelist.htm @@ -30,7 +30,7 @@ ifattr(self.rmempty or self.optional, "optional", "optional") %>>
      <% if self.allowlocal then %> -
    • > +
    • > <%:Device%> <% if self.allowany and self.allowlocal then -%> @@ -48,14 +48,14 @@
    • <% elseif self.widget ~= "checkbox" and (self.rmempty or self.optional) then %> -
    • > +
    • > <%:unspecified%>
    • <% end %> <% if self.allowany then %> -
    • > +
    • > <%:Any zone%> <% if self.allowany and self.allowlocal then %>(<%:forward%>)<% end %> @@ -67,7 +67,7 @@ if zone:name() ~= self.exclude then selected = selected or (value == zone:name()) %> - > + > <%=zone:name()%>: <%- @@ -94,11 +94,11 @@ <% end end %> <% if self.widget ~= "checkbox" and not self.nocreate then %> -
    • +
    • <%:create%>: - +
    • <% end %> diff --git a/luci-base/luasrc/view/cbi/ipaddr.htm b/luci-base/luasrc/view/cbi/ipaddr.htm new file mode 100644 index 000000000..1c924e154 --- /dev/null +++ b/luci-base/luasrc/view/cbi/ipaddr.htm @@ -0,0 +1,27 @@ +<%+cbi/valueheader%> + + 0, "data-choices", { self.keylist, self.vallist }) + %> /> +<%+cbi/valuefooter%> diff --git a/luci-base/luasrc/view/cbi/map.htm b/luci-base/luasrc/view/cbi/map.htm index d65a16167..cda4d3530 100644 --- a/luci-base/luasrc/view/cbi/map.htm +++ b/luci-base/luasrc/view/cbi/map.htm @@ -3,25 +3,25 @@ <%- end end -%>
      - <% if self.title and #self.title > 0 then %>

      <%=self.title%>

      <% end %> - <% if self.description and #self.description > 0 then %>
      <%=self.description%>
      <% end %> + <% if self.title and #self.title > 0 then %> +

      <%=self.title%>

      + <% end %> + <% if self.description and #self.description > 0 then %> +
      <%=self.description%>
      + <% end %> <% if self.tabbed then %> -
        - <%- self.selected_tab = luci.http.formvalue("tab.m-" .. self.config) %> - <% for i, section in ipairs(self.children) do %> - <%- if not self.selected_tab then self.selected_tab = section.sectiontype end %> -
      • - <%=section.title or section.section or section.sectiontype %> - <% if section.sectiontype == self.selected_tab then %><% end %> -
      • +
        + <% for i, section in ipairs(self.children) do + tab = section.section or section.sectiontype %> +
        > + <% section:render() %> +
        <% end %> -
      - <% for i, section in ipairs(self.children) do %> -
      style="display:none"<% end %>> - <% section:render() %> -
      - - <% end %> +
      <% if not self.save then -%>
      diff --git a/luci-base/luasrc/view/cbi/network_ifacelist.htm b/luci-base/luasrc/view/cbi/network_ifacelist.htm index a97e9ef6d..f23e51d18 100644 --- a/luci-base/luasrc/view/cbi/network_ifacelist.htm +++ b/luci-base/luasrc/view/cbi/network_ifacelist.htm @@ -41,13 +41,13 @@ -
      >
        <% if self.widget ~= "checkbox" then %> -
      • > +
      • > <%:unspecified%>
      • <% end %> @@ -44,7 +44,7 @@ (net:name() ~= self.exclude) and (not self.novirtual or not net:is_virtual()) then %> - > + > <%=net:name()%>: <% local empty = true @@ -63,7 +63,7 @@ <% end end %> <% if not self.nocreate then %> -
      • > +
      • > <%- if self.widget == "checkbox" then -%> <%:create:%> diff --git a/luci-base/luasrc/view/cbi/nsection.htm b/luci-base/luasrc/view/cbi/nsection.htm index 63abc5773..14232e3d9 100644 --- a/luci-base/luasrc/view/cbi/nsection.htm +++ b/luci-base/luasrc/view/cbi/nsection.htm @@ -11,7 +11,6 @@
      <%- end %> - <%+cbi/tabmenu%>
      <%+cbi/ucisection%>
      diff --git a/luci-base/luasrc/view/cbi/tabcontainer.htm b/luci-base/luasrc/view/cbi/tabcontainer.htm index 38c435d6a..7fcb83578 100644 --- a/luci-base/luasrc/view/cbi/tabcontainer.htm +++ b/luci-base/luasrc/view/cbi/tabcontainer.htm @@ -1,7 +1,14 @@ -<% for tab, data in pairs(self.tabs) do %> -
      style="display:none"<% end %>> - <% if data.description then %>
      <%=data.description%>
      <% end %> +<% for _, tab in ipairs(self.tab_names) do data = self.tabs[tab] %> +
      > + <% if data.description then %> +
      <%=data.description%>
      + <% end %> + <% self:render_tab(tab, section, scope or {}) %>
      - <% end %> diff --git a/luci-base/luasrc/view/cbi/tabmenu.htm b/luci-base/luasrc/view/cbi/tabmenu.htm deleted file mode 100644 index 06c1414bf..000000000 --- a/luci-base/luasrc/view/cbi/tabmenu.htm +++ /dev/null @@ -1,12 +0,0 @@ -<%- if self.tabs then %> -
        - <%- self.selected_tab = luci.http.formvalue("tab." .. self.config .. "." .. section) %> - <%- for _, tab in ipairs(self.tab_names) do if #self.tabs[tab].childs > 0 then %> - <%- if not self.selected_tab then self.selected_tab = tab end %> -
      • - <%=self.tabs[tab].title%> - <% if tab == self.selected_tab then %><% end %> -
      • - <% end end -%> -
      -<% end -%> diff --git a/luci-base/luasrc/view/cbi/tblsection.htm b/luci-base/luasrc/view/cbi/tblsection.htm index 408dfa7fe..11c2206d8 100644 --- a/luci-base/luasrc/view/cbi/tblsection.htm +++ b/luci-base/luasrc/view/cbi/tblsection.htm @@ -127,7 +127,7 @@ end section = k local sectionname = striptags((type(self.sectiontitle) == "function") and self:sectiontitle(section) or k) - local sectiontitle = ifattr(sectionname and (not self.anonymous or self.sectiontitle), "data-title", sectionname) + local sectiontitle = ifattr(sectionname and (not self.anonymous or self.sectiontitle), "data-title", sectionname, true) local colorclass = (self.extedit or self.rowcolors) and rowstyle() or "" local scope = { valueheader = "cbi/cell_valueheader", diff --git a/luci-base/luasrc/view/cbi/tsection.htm b/luci-base/luasrc/view/cbi/tsection.htm index 1a13df0c0..8f3b7f0ff 100644 --- a/luci-base/luasrc/view/cbi/tsection.htm +++ b/luci-base/luasrc/view/cbi/tsection.htm @@ -2,6 +2,11 @@ <% if self.title and #self.title > 0 then -%> <%=self.title%> <%- end %> + <% if self.error_msg and #self.error_msg > 0 then -%> +
      + <%=self.error_msg%> +
      + <%- end %> <% if self.description and #self.description > 0 then -%>
      <%=self.description%>
      <%- end %> @@ -18,8 +23,6 @@

      <%=section:upper()%>

      <%- end %> - <%+cbi/tabmenu%> -
      <%+cbi/ucisection%>
      diff --git a/luci-base/luasrc/view/cbi/value.htm b/luci-base/luasrc/view/cbi/value.htm index 74908ba7d..27f3cb2bd 100644 --- a/luci-base/luasrc/view/cbi/value.htm +++ b/luci-base/luasrc/view/cbi/value.htm @@ -1,6 +1,6 @@ <%+cbi/valueheader%> <%- if self.password then -%> - /> <%- end -%> @@ -21,6 +21,6 @@ ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist }) %> /> <%- if self.password then -%> -
      + <% end %> <%+cbi/valuefooter%> diff --git a/luci-mod-admin-full/luasrc/view/cbi/wireless_modefreq.htm b/luci-base/luasrc/view/cbi/wireless_modefreq.htm similarity index 100% rename from luci-mod-admin-full/luasrc/view/cbi/wireless_modefreq.htm rename to luci-base/luasrc/view/cbi/wireless_modefreq.htm diff --git a/luci-base/luasrc/view/empty_node_placeholder.htm b/luci-base/luasrc/view/empty_node_placeholder.htm new file mode 100644 index 000000000..b7e276b96 --- /dev/null +++ b/luci-base/luasrc/view/empty_node_placeholder.htm @@ -0,0 +1,11 @@ +<%# + Copyright 2010 Jo-Philipp Wich + Copyright 2018 Daniel F. Dickinson + Licensed to the public under the Apache License 2.0. +-%> + +<%+header%> + +

      Component not present.

      + +<%+footer%> diff --git a/luci-base/luasrc/view/header.htm b/luci-base/luasrc/view/header.htm index f6e20c9a4..d68e39f91 100644 --- a/luci-base/luasrc/view/header.htm +++ b/luci-base/luasrc/view/header.htm @@ -10,3 +10,15 @@ luci.dispatcher.context.template_header_sent = true end %> + + + diff --git a/luci-mod-admin-full/luasrc/view/admin_network/lease_status.htm b/luci-base/luasrc/view/lease_status.htm similarity index 97% rename from luci-mod-admin-full/luasrc/view/admin_network/lease_status.htm rename to luci-base/luasrc/view/lease_status.htm index 8fbbdc947..bf2a8968d 100644 --- a/luci-mod-admin-full/luasrc/view/admin_network/lease_status.htm +++ b/luci-base/luasrc/view/lease_status.htm @@ -1,5 +1,5 @@ diff --git a/luci-mod-admin-full/luasrc/view/admin_network/iface_status.htm b/luci-mod-admin-full/luasrc/view/admin_network/iface_status.htm deleted file mode 100644 index 34be35dd2..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_network/iface_status.htm +++ /dev/null @@ -1,66 +0,0 @@ -<%+cbi/valueheader%> - - - - - - - <%:Collecting data...%> - - - -<%+cbi/valuefooter%> diff --git a/luci-mod-admin-full/luasrc/view/admin_network/wifi_join.htm b/luci-mod-admin-full/luasrc/view/admin_network/wifi_join.htm deleted file mode 100644 index 987123642..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_network/wifi_join.htm +++ /dev/null @@ -1,224 +0,0 @@ -<%# - Copyright 2009-2015 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%- - - local sys = require "luci.sys" - local utl = require "luci.util" - - local dev = luci.http.formvalue("device") - local iw = luci.sys.wifi.getiwinfo(dev) - - if not iw then - luci.http.redirect(luci.dispatcher.build_url("admin/network/wireless")) - return - end --%> - -<%+header%> - - - -

      <%:Join Network: Wireless Scan%>

      - -
      -
      -
      -
      -
      <%:Signal%>
      -
      <%:SSID%>
      -
      <%:Channel%>
      -
      <%:Mode%>
      -
      <%:BSSID%>
      -
      <%:Encryption%>
      -
       
      -
      - -
      -
      - - <%:Collecting data...%> -
      -
      -
      -
      -
      -
      -
      " method="get"> - -
      -
      - - - -
      -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_network/wifi_overview_status.htm b/luci-mod-admin-full/luasrc/view/admin_network/wifi_overview_status.htm deleted file mode 100644 index 9730bc2c9..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_network/wifi_overview_status.htm +++ /dev/null @@ -1,127 +0,0 @@ -<%# - Copyright 2008-2009 Steven Barth - Copyright 2008-2018 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - - diff --git a/luci-mod-admin-full/luasrc/view/admin_network/wifi_status.htm b/luci-mod-admin-full/luasrc/view/admin_network/wifi_status.htm deleted file mode 100644 index bfad3d080..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_network/wifi_status.htm +++ /dev/null @@ -1,77 +0,0 @@ -<%+cbi/valueheader%> - - - - - -   - - - <%:Collecting data...%> - - - -<%+cbi/valuefooter%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/bandwidth.htm b/luci-mod-admin-full/luasrc/view/admin_status/bandwidth.htm deleted file mode 100644 index 3bb55f905..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/bandwidth.htm +++ /dev/null @@ -1,305 +0,0 @@ -<%# - Copyright 2010 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%- - local ntm = require "luci.model.network".init() - - local dev - local devices = { } - for _, dev in luci.util.vspairs(luci.sys.net.devices()) do - if dev ~= "lo" and not ntm:ignore_interface(dev) then - devices[#devices+1] = dev - end - end - - local curdev = luci.http.formvalue("dev") or devices[1] --%> - -<%+header%> - - - -

      <%:Realtime Traffic%>

      - - - - -
      -
      -
      - -
      -
      -
      <%:Inbound:%>
      -
      0 <%:kbit/s%>
      (0 <%:kB/s%>)
      - -
      <%:Average:%>
      -
      0 <%:kbit/s%>
      (0 <%:kB/s%>)
      - -
      <%:Peak:%>
      -
      0 <%:kbit/s%>
      (0 <%:kB/s%>)
      -
      -
      -
      <%:Outbound:%>
      -
      0 <%:kbit/s%>
      (0 <%:kB/s%>)
      - -
      <%:Average:%>
      -
      0 <%:kbit/s%>
      (0 <%:kB/s%>)
      - -
      <%:Peak:%>
      -
      0 <%:kbit/s%>
      (0 <%:kB/s%>)
      -
      -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/connections.htm b/luci-mod-admin-full/luasrc/view/admin_status/connections.htm deleted file mode 100644 index 0a0db3be7..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/connections.htm +++ /dev/null @@ -1,376 +0,0 @@ -<%# - Copyright 2010 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> - - - -

      <%:Realtime Connections%>

      - -
      <%:This page gives an overview over currently active network connections.%>
      - -
      - <%:Active Connections%> - - -
      -
      -
      - -
      -
      -
      <%:UDP:%>
      -
      0
      - -
      <%:Average:%>
      -
      0
      - -
      <%:Peak:%>
      -
      0
      -
      -
      -
      <%:TCP:%>
      -
      0
      - -
      <%:Average:%>
      -
      0
      - -
      <%:Peak:%>
      -
      0
      -
      -
      -
      <%:Other:%>
      -
      0
      - -
      <%:Average:%>
      -
      0
      - -
      <%:Peak:%>
      -
      0
      -
      -
      -
      - -
      -
      -
      -
      <%:Network%>
      -
      <%:Protocol%>
      -
      <%:Source%>
      -
      <%:Destination%>
      -
      <%:Transfer%>
      -
      - -
      -
      - <%:Collecting data...%> -
      -
      -
      -
      -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/dmesg.htm b/luci-mod-admin-full/luasrc/view/admin_status/dmesg.htm deleted file mode 100644 index 1a8770ef8..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/dmesg.htm +++ /dev/null @@ -1,12 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> -

      <%:Kernel Log%>

      -
      - -
      -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/index.htm b/luci-mod-admin-full/luasrc/view/admin_status/index.htm deleted file mode 100644 index 5abc6d61c..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/index.htm +++ /dev/null @@ -1,467 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008-2011 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<% - local fs = require "nixio.fs" - local ipc = require "luci.ip" - local util = require "luci.util" - local stat = require "luci.tools.status" - local ver = require "luci.version" - - local has_ipv6 = fs.access("/proc/net/ipv6_route") - local has_dhcp = fs.access("/etc/config/dhcp") - local has_mptcp = (luci.sys.exec("sysctl -n net.mptcp.mptcp_enabled | tr -d '\n'")) or 0 - local has_wifi = ((fs.stat("/etc/config/wireless", "size") or 0) > 0) - - local sysinfo = luci.util.ubus("system", "info") or { } - local boardinfo = luci.util.ubus("system", "board") or { } - local unameinfo = nixio.uname() or { } - - local meminfo = sysinfo.memory or { - total = 0, - free = 0, - buffered = 0, - shared = 0 - } - - local swapinfo = sysinfo.swap or { - total = 0, - free = 0 - } - - local has_dsl = fs.access("/etc/init.d/dsl_control") - - if luci.http.formvalue("status") == "1" then - local ntm = require "luci.model.network".init() - local wan = ntm:get_wannet() - local wan6 = ntm:get_wan6net() - - local conn_count = tonumber( - fs.readfile("/proc/sys/net/netfilter/nf_conntrack_count") or "") or 0 - - local conn_max = tonumber(luci.sys.exec( - "sysctl -n -e net.nf_conntrack_max net.ipv4.netfilter.ip_conntrack_max" - ):match("%d+")) or 4096 - - local rv = { - uptime = sysinfo.uptime or 0, - localtime = os.date(), - loadavg = sysinfo.load or { 0, 0, 0 }, - memory = meminfo, - swap = swapinfo, - connmax = conn_max, - conncount = conn_count, - wifinets = stat.wifi_networks() - } - - if wan then - local dev = wan:get_interface() - local link = dev and ipc.link(dev:name()) - rv.wan = { - ipaddr = wan:ipaddr(), - gwaddr = wan:gwaddr(), - netmask = wan:netmask(), - dns = wan:dnsaddrs(), - expires = wan:expires(), - uptime = wan:uptime(), - proto = wan:proto(), - i18n = wan:get_i18n(), - ifname = wan:ifname(), - link = wan:adminlink(), - mac = dev and dev:mac(), - type = dev and dev:type(), - name = dev and dev:get_i18n(), - ether = link and link.type == 1 - } - end - - if wan6 then - local dev = wan6:get_interface() - local link = dev and ipc.link(dev:name()) - rv.wan6 = { - ip6addr = wan6:ip6addr(), - gw6addr = wan6:gw6addr(), - dns = wan6:dns6addrs(), - ip6prefix = wan6:ip6prefix(), - uptime = wan6:uptime(), - proto = wan6:proto(), - i18n = wan6:get_i18n(), - ifname = wan6:ifname(), - link = wan6:adminlink(), - mac = dev and dev:mac(), - type = dev and dev:type(), - name = dev and dev:get_i18n(), - ether = link and link.type == 1 - } - end - - if has_dsl then - local dsl_stat = luci.sys.exec("/etc/init.d/dsl_control lucistat") - local dsl_func = loadstring(dsl_stat) - if dsl_func then - rv.dsl = dsl_func() - end - end - - luci.http.prepare_content("application/json") - luci.http.write_json(rv) - - return - end --%> - -<%+header%> - - - -

      <%:Status%>

      - -
      -

      <%:System%>

      - -
      -
      <%:Hostname%>
      <%=luci.sys.hostname() or "?"%>
      -
      <%:Model%>
      <%=pcdata(boardinfo.model or "?")%>
      -
      <%:Architecture%>
      <%=pcdata(boardinfo.system or "?")%>
      -
      <%:Firmware Version%>
      - <%=pcdata(ver.distname)%> <%=pcdata(ver.distversion)%> / - <%=pcdata(ver.luciname)%> (<%=pcdata(ver.luciversion)%>) -
      -
      <%:Kernel Version%>
      <%=unameinfo.release or "?"%>
      -
      <%:Local Time%>
      -
      -
      <%:Uptime%>
      -
      -
      <%:Load Average%>
      -
      -
      -
      - -
      -

      <%:Memory%>

      - -
      -
      <%:Total Available%>
      -
      -
      <%:Free%>
      -
      -
      <%:Buffered%>
      -
      -
      -
      - -<% if swapinfo.total > 0 then %> -
      -

      <%:Swap%>

      - -
      -
      <%:Total Available%>
      -
      -
      <%:Free%>
      -
      -
      -
      -<% end %> - -
      -

      <%:Network%>

      - - <% if has_mptcp == 0 then %> -
      -

      <%:Collecting data...%>

      -
      - <% end %> - -
      -
      <%:Active Connections%>
      -
      -
      -
      - -<% - if has_dhcp then - include("admin_network/lease_status") - end -%> - -<% if has_dsl then %> -
      -

      <%:DSL%>

      - -
      -

      <%:Collecting data...%>

      -
      -
      -<% end %> - -<% if has_wifi then %> -
      -

      <%:Wireless%>

      - -
      -

      <%:Collecting data...%>

      -
      -
      - -
      -

      <%:Associated Stations%>

      - - <%+admin_network/wifi_assoclist%> -
      -<% end %> - -<%- - local incdir = util.libpath() .. "/view/admin_status/index/" - if fs.access(incdir) then - local inc - for inc in fs.dir(incdir) do - if inc:match("%.htm$") then - include("admin_status/index/" .. inc:gsub("%.htm$", "")) - end - end - end --%> - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/iptables.htm b/luci-mod-admin-full/luasrc/view/admin_status/iptables.htm deleted file mode 100644 index 51e428e40..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/iptables.htm +++ /dev/null @@ -1,155 +0,0 @@ -<%# - Copyright 2008-2009 Steven Barth - Copyright 2008-2015 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%- - - require "luci.sys.iptparser" - local wba = require "luci.tools.webadmin" - local fs = require "nixio.fs" - local io = require "io" - - local has_ip6tables = fs.access("/usr/sbin/ip6tables") - local mode = 4 - - if has_ip6tables then - mode = luci.dispatcher.context.requestpath - mode = tonumber(mode[#mode] ~= "iptables" and mode[#mode]) or 4 - end - - local ipt = luci.sys.iptparser.IptParser(mode) - - local rowcnt = 1 - function rowstyle() - rowcnt = rowcnt + 1 - return (rowcnt % 2) + 1 - end - - function link_target(t,c) - if ipt:is_custom_target(c) then - return '%s' %{ t:lower(), c, c } - end - return c - end - - function link_iface(i) - local net = wba.iface_get_network(i) - if net and i ~= "lo" then - return '%s' %{ - url("admin/network/network", net), i - } - - end - return i - end - - local tables = { "Filter", "NAT", "Mangle", "Raw" } - if mode == 6 then - tables = { "Filter", "Mangle", "Raw" } - local ok, lines = pcall(io.lines, "/proc/net/ip6_tables_names") - if ok and lines then - local line - for line in lines do - if line == "nat" then - tables = { "Filter", "NAT", "Mangle", "Raw" } - end - end - end - end --%> - -<%+header%> - - - -

      <%:Firewall Status%>

      - -<% if has_ip6tables then %> - -<% end %> - -
      - -
      " style="position: absolute; right: 0"> - - - - -
      - -
      - - <% for _, tbl in ipairs(tables) do chaincnt = 0 %> -

      <%:Table%>: <%=tbl%>

      - - <% for _, chain in ipairs(ipt:chains(tbl)) do - rowcnt = 0 - chaincnt = chaincnt + 1 - chaininfo = ipt:chain(tbl, chain) - %> -

      - <%:Chain%> <%=chain%> - (<%- if chaininfo.policy then -%> - <%:Policy%>: <%=chaininfo.policy%>, <%:Packets%>: <%=chaininfo.packets%>, <%:Traffic%>: <%=wba.byte_format(chaininfo.bytes)-%> - <%- else -%> - <%:References%>: <%=chaininfo.references-%> - <%- end -%>) -

      - -
      -
      -
      -
      <%:Pkts.%>
      -
      <%:Traffic%>
      -
      <%:Target%>
      -
      <%:Prot.%>
      -
      <%:In%>
      -
      <%:Out%>
      -
      <%:Source%>
      -
      <%:Destination%>
      -
      <%:Options%>
      -
      - - <% for _, rule in ipairs(ipt:find({table=tbl, chain=chain})) do %> -
      -
      <%=rule.packets%>
      -
      <%=wba.byte_format(rule.bytes)%>
      -
      <%=rule.target and link_target(tbl, rule.target) or "-"%>
      -
      <%=rule.protocol%>
      -
      <%=link_iface(rule.inputif)%>
      -
      <%=link_iface(rule.outputif)%>
      -
      <%=rule.source%>
      -
      <%=rule.destination%>
      -
      <%=#rule.options > 0 and luci.util.pcdata(table.concat(rule.options, " ")) or "-"%>
      -
      - <% end %> - - <% if rowcnt == 1 then %> -
      -
      <%:No rules in this chain%>
      -
      - <% end %> -
      -
      - <% end %> - - <% if chaincnt == 0 then %> - <%:No chains in this table%> - <% end %> - -

      - <% end %> -
      -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/load.htm b/luci-mod-admin-full/luasrc/view/admin_status/load.htm deleted file mode 100644 index bced06fa2..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/load.htm +++ /dev/null @@ -1,285 +0,0 @@ -<%# - Copyright 2010 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> - - - -

      <%:Realtime Load%>

      - - -
      -
      -
      - -
      -
      -
      <%:1 Minute Load:%>
      -
      0
      - -
      <%:Average:%>
      -
      0
      - -
      <%:Peak:%>
      -
      0
      -
      -
      -
      <%:5 Minute Load:%>
      -
      0
      - -
      <%:Average:%>
      -
      0
      - -
      <%:Peak:%>
      -
      0
      -
      -
      -
      <%:15 Minute Load:%>
      -
      0
      - -
      <%:Average:%>
      -
      0
      - -
      <%:Peak:%>
      -
      0
      -
      -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/routes.htm b/luci-mod-admin-full/luasrc/view/admin_status/routes.htm deleted file mode 100644 index 74779f6ad..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/routes.htm +++ /dev/null @@ -1,156 +0,0 @@ -<%# - Copyright 2008-2009 Steven Barth - Copyright 2008-2015 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%- - require "luci.tools.webadmin" - require "nixio.fs" - - local ip = require "luci.ip" - local style = true - local _, v - - local rtn = { - [255] = "local", - [254] = "main", - [253] = "default", - [0] = "unspec" - } - - if nixio.fs.access("/etc/iproute2/rt_tables") then - local ln - for ln in io.lines("/etc/iproute2/rt_tables") do - local i, n = ln:match("^(%d+)%s+(%S+)") - if i and n then - rtn[tonumber(i)] = n - end - end - end --%> - -<%+header%> - - -
      -

      <%:Routes%>

      -
      <%:The following rules are currently active on this system.%>
      - -
      - ARP -
      -
      -
      -
      <%_IPv4-Address%>
      -
      <%_MAC-Address%>
      -
      <%:Interface%>
      -
      - - <% - for _, v in ipairs(ip.neighbors({ family = 4 })) do - if v.mac then - %> -
      -
      <%=v.dest%>
      -
      <%=v.mac%>
      -
      <%=luci.tools.webadmin.iface_get_network(v.dev) or '(' .. v.dev .. ')'%>
      -
      - <% - style = not style - end - end - %> -
      -
      -
      - -
      - <%_Active IPv4-Routes%> -
      -
      -
      -
      <%:Network%>
      -
      <%:Target%>
      -
      <%_IPv4-Gateway%>
      -
      <%:Metric%>
      -
      <%:Table%>
      -
      - <% for _, v in ipairs(ip.routes({ family = 4, type = 1 })) do %> -
      -
      <%=luci.tools.webadmin.iface_get_network(v.dev) or v.dev%>
      -
      <%=v.dest%>
      -
      <%=v.gw or "-"%>
      -
      <%=v.metric or 0%>
      -
      <%=rtn[v.table] or v.table%>
      -
      - <% style = not style end %> -
      -
      -
      - - <% - if nixio.fs.access("/proc/net/ipv6_route") then - style = true - %> -
      - <%_Active IPv6-Routes%> -
      -
      -
      -
      <%:Network%>
      -
      <%:Target%>
      -
      <%:Source%>
      -
      <%:Metric%>
      -
      <%:Table%>
      -
      - <% - for _, v in ipairs(ip.routes({ family = 6, type = 1 })) do - if v.dest and not v.dest:is6linklocal() then - %> -
      -
      <%=luci.tools.webadmin.iface_get_network(v.dev) or '(' .. v.dev .. ')'%>
      -
      <%=v.dest%>
      -
      <%=v.from%>
      -
      <%=v.metric or 0%>
      -
      <%=rtn[v.table] or v.table%>
      -
      - <% - style = not style - end - end - %> -
      -
      -
      - -
      - <%:IPv6 Neighbours%> -
      -
      -
      -
      <%:IPv6-Address%>
      -
      <%:MAC-Address%>
      -
      <%:Interface%>
      -
      - <% - for _, v in ipairs(ip.neighbors({ family = 6 })) do - if v.dest and not v.dest:is6linklocal() and v.mac then - %> -
      -
      <%=v.dest%>
      -
      <%=v.mac%>
      -
      <%=luci.tools.webadmin.iface_get_network(v.dev) or '(' .. v.dev .. ')'%>
      -
      - <% - style = not style - end - end - %> -
      -
      -
      - <% end %> -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/syslog.htm b/luci-mod-admin-full/luasrc/view/admin_status/syslog.htm deleted file mode 100644 index fb734a76d..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/syslog.htm +++ /dev/null @@ -1,12 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> -

      <%:System Log%>

      -
      - -
      -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_status/wireless.htm b/luci-mod-admin-full/luasrc/view/admin_status/wireless.htm deleted file mode 100644 index 8ec43cb0e..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_status/wireless.htm +++ /dev/null @@ -1,371 +0,0 @@ -<%# - Copyright 2011 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%- - local ntm = require "luci.model.network".init() - - local dev - local devices = { } - for _, dev in luci.util.vspairs(luci.sys.net.devices()) do - if dev:match("^wlan%d") or dev:match("^ath%d") or dev:match("^wl%d") then - devices[#devices+1] = dev - end - end - - local curdev = luci.http.formvalue("dev") or devices[1] --%> - -<%+header%> - - - -

      <%:Realtime Wireless%>

      - - - - -
      -
      -
      - -
      -
      -
      <%:Signal:%>
      -
      0 <%:dBm%>
      - -
      <%:Average:%>
      -
      0 <%:dBm%>
      - -
      <%:Peak:%>
      -
      0 <%:dBm%>
      -
      -
      -
      <%:Noise:%>
      -
      0 <%:dBm%>
      - -
      <%:Average:%>
      -
      0 <%:dBm%>
      - -
      <%:Peak:%>
      -
      0 <%:dBm%>
      -
      -
      - -
      - - -
      -
      -
      - -
      -
      -
      <%:Phy Rate:%>
      -
      0 MBit/s
      - -
      <%:Average:%>
      -
      0 MBit/s
      - -
      <%:Peak:%>
      -
      0 MBit/s
      -
      -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_system/applyreboot.htm b/luci-mod-admin-full/luasrc/view/admin_system/applyreboot.htm deleted file mode 100644 index e722a4809..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_system/applyreboot.htm +++ /dev/null @@ -1,41 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - - - - <%=luci.sys.hostname()%> - <% if title then %><%=title%><% else %><%:Rebooting...%><% end %> - - - - - -
      -
      -

      <%:System%> - <% if title then %><%=title%><% else %><%:Rebooting...%><% end %>

      -
      -

      - <% if msg then %><%=msg%><% else %><%:Changes applied.%><% end %> -

      -

      - <%:Loading%> - <%:Waiting for changes to be applied...%> -

      -
      -
      -
      - - diff --git a/luci-mod-admin-full/luasrc/view/admin_system/backupfiles.htm b/luci-mod-admin-full/luasrc/view/admin_system/backupfiles.htm deleted file mode 100644 index c1f3361ae..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_system/backupfiles.htm +++ /dev/null @@ -1,10 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - - diff --git a/luci-mod-admin-full/luasrc/view/admin_system/clock_status.htm b/luci-mod-admin-full/luasrc/view/admin_system/clock_status.htm deleted file mode 100644 index 37d8ae0e8..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_system/clock_status.htm +++ /dev/null @@ -1,36 +0,0 @@ -<%+cbi/valueheader%> - - - -<%:Collecting data...%> - - -<%+cbi/valuefooter%> diff --git a/luci-mod-admin-full/luasrc/view/admin_system/flashops.htm b/luci-mod-admin-full/luasrc/view/admin_system/flashops.htm deleted file mode 100644 index ee9c2f8fd..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_system/flashops.htm +++ /dev/null @@ -1,94 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008-2015 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> - -

      <%:Flash operations%>

      - - - -
      -

      <%:Backup%>

      -
      <%:Click "Generate archive" to download a tar archive of the current configuration files.%>
      -
      -
      - -
      - -
      - -
      -
      -
      -
      - -

      <%:Restore%>

      -
      <%:To restore configuration files, you can upload a previously generated backup archive here. To reset the firmware to its initial state, click "Perform reset" (only possible with squashfs images).%>
      -
      - <% if reset_avail then %> -
      - -
      - -
      - -
      -
      -
      - <% end %> -
      -
      - -
      - - - - <% if reset_avail then %> -
      <%:Custom files (certificates, scripts) may remain on the system. To prevent this, perform a factory-reset first.%>
      - <% end %> -
      -
      -
      - <% if backup_invalid then %> -
      <%:The backup archive does not appear to be a valid gzip file.%>
      - <% end %> -
      -
      - -
      -

      <%:Flash new firmware image%>

      - <% if upgrade_avail then %> -
      - -
      <%:Upload a sysupgrade-compatible image here to replace the running firmware. Check "Keep settings" to retain the current configuration (requires a compatible firmware image).%>
      -
      -
      - -
      - -
      -
      -
      - -
      - - -
      -
      -
      - <% if image_invalid then %> -
      <%:The uploaded image file does not contain a supported format. Make sure that you choose the generic image format for your platform. %>
      - <% end %> -
      - <% else %> -
      <%:Sorry, there is no sysupgrade support present; a new firmware image must be flashed manually. Please refer to the wiki for device specific install instructions.%>
      - <% end %> -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_system/ipkg.htm b/luci-mod-admin-full/luasrc/view/admin_system/ipkg.htm deleted file mode 100644 index a7ff4e50b..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_system/ipkg.htm +++ /dev/null @@ -1,10 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - - diff --git a/luci-mod-admin-full/luasrc/view/admin_system/packages.htm b/luci-mod-admin-full/luasrc/view/admin_system/packages.htm deleted file mode 100644 index 280eabb8e..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_system/packages.htm +++ /dev/null @@ -1,213 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008-2010 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%- -local opkg = require "luci.model.ipkg" -local fs = require "nixio.fs" -local wa = require "luci.tools.webadmin" -local rowcnt = 1 - -function rowstyle() - rowcnt = rowcnt + 1 - return (rowcnt % 2) + 1 -end - -local fstat = fs.statvfs(opkg.overlay_root()) -local space_total = fstat and fstat.blocks or 0 -local space_free = fstat and fstat.bfree or 0 -local space_used = space_total - space_free - -local used_perc = math.floor(0.5 + ((space_total > 0) and ((100 / space_total) * space_used) or 100)) -local free_byte = space_free * fstat.frsize - -local filter = { } - - -local opkg_list = luci.model.ipkg.list_all -local querypat -if query and #query > 0 then - querypat = '*%s*' % query - opkg_list = luci.model.ipkg.find -end - -local letterpat -if letter == 35 then - letterpat = "[^a-z]*" -else - letterpat = string.char(letter, 42) -- 'A' '*' -end - --%> - -<%+header%> - - -

      <%:Software%>

      - -
      - - - -
      - - - -
      -
      - <% if (install and next(install)) or (remove and next(remove)) or update or upgrade then %> -
      - <% if #stdout > 0 then %>
      <%=pcdata(stdout)%>
      <% end %> - <% if #stderr > 0 then %>
      <%=pcdata(stderr)%>
      <% end %> -
      - <% end %> - - <% if querypat then %> -
      - <%:Displaying only packages containing%> "<%=pcdata(query)%>" - -
      -
      - <% end %> - - <% if no_lists or old_lists then %> -
      - <% if old_lists then %> - <%:Package lists are older than 24 hours%> - <% else %> - <%:No package lists available%> - <% end %> - -
      - <% end %> - -
      - <%:Free space%>: <%=(100-used_perc)%>% (<%=wa.byte_format(free_byte)%>) -
      -
       
      -
      -
      -
      - -
      - -
      - - -
      - -
      - - -
      -
      - -
      - -
      - - -
      -
      -
      -
      -
      - - -

      <%:Status%>

      - - - - - <% if display ~= "available" then %> -
      -
      -
      -
      -
      <%:Package name%>
      -
      <%:Version%>
      -
       
      -
      - <% local empty = true; luci.model.ipkg.list_installed(querypat, function(n, v, s, d) empty = false; filter[n] = true %> -
      -
      <%=luci.util.pcdata(n)%>
      -
      <%=luci.util.pcdata(v)%>
      -
      -
      - - - - -
      -
      -
      - <% end) %> - <% if empty then %> -
      -
       
      -
      <%:none%>
      -
      <%:none%>
      -
      - <% end %> -
      -
      -
      - <% else %> -
      - <% if not querypat then %> - - <% end %> -
      -
      -
      -
      <%:Package name%>
      -
      <%:Version%>
      -
      <%:Size (.ipk)%>
      -
      <%:Description%>
      -
       
      -
      - <% local empty = true; opkg_list(querypat or letterpat, function(n, v, s, d) if filter[n] then return end; empty = false %> -
      -
      <%=luci.util.pcdata(n)%>
      -
      <%=luci.util.pcdata(v)%>
      -
      <%=luci.util.pcdata(s)%>
      -
      <%=luci.util.pcdata(d)%>
      -
      -
      - - - - -
      -
      -
      - <% end) %> - <% if empty then %> -
      -
       
      -
      <%:none%>
      -
      <%:none%>
      -
      <%:none%>
      -
      <%:none%>
      -
      - <% end %> -
      -
      -
      - <% end %> -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_system/reboot.htm b/luci-mod-admin-full/luasrc/view/admin_system/reboot.htm deleted file mode 100644 index d23664ada..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_system/reboot.htm +++ /dev/null @@ -1,62 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008-2015 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> - -

      <%:Reboot%>

      - -

      <%:Reboots the operating system of your device%>

      - -<%- local c = require("luci.model.uci").cursor():changes(); if c and next(c) then -%> -

      <%:Warning: There are unsaved changes that will get lost on reboot!%>

      -<%- end -%> - -
      - - - - - - - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_system/upgrade.htm b/luci-mod-admin-full/luasrc/view/admin_system/upgrade.htm deleted file mode 100644 index 7175248db..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_system/upgrade.htm +++ /dev/null @@ -1,59 +0,0 @@ -<%# - Copyright 2008 Steven Barth - Copyright 2008-2009 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> - -

      <%:Flash Firmware%> - <%:Verify%>

      -

      - <%_ The flash image was uploaded. - Below is the checksum and file size listed, - compare them with the original file to ensure data integrity.
      - Click "Proceed" below to start the flash procedure. %> - - <% if storage > 0 and size > storage then %> -

      -

      <%:It appears that you are trying to - flash an image that does not fit into the flash memory, please verify - the image file! %>
      - <% end %> - -

      - -
      -
        -
      • <%:Checksum%>
        - <%:MD5%>: <%=checksum%>
        - <%:SHA256%>: <%=sha256ch%>
      • -
      • <%:Size%>: <% - local w = require "luci.tools.webadmin" - write(w.byte_format(size)) - - if storage > 0 then - write(luci.i18n.translatef( - " (%s available)", - w.byte_format(storage) - )) - end - %>
      • -
      • <% if keep then %> - <%:Configuration files will be kept.%> - <% else %> - <%:Note: Configuration files will be erased.%> - <% end %>
      • -
      -
      - -
      -
      - - - " /> - - -
      -
      - -<%+footer%> diff --git a/luci-mod-admin-full/luasrc/view/admin_uci/changelog.htm b/luci-mod-admin-full/luasrc/view/admin_uci/changelog.htm deleted file mode 100644 index e05ccdece..000000000 --- a/luci-mod-admin-full/luasrc/view/admin_uci/changelog.htm +++ /dev/null @@ -1,81 +0,0 @@ -<%# - Copyright 2010 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<% export("uci_changelog", function(changes) -%> -
      - <%:Legend:%> -
      -
        <%:Section added%>
      -
        <%:Section removed%>
      -
        <%:Option changed%>
      -
        <%:Option removed%>
      -
      -
      -
      - -
      <% - local util = luci.util - local ret = { } - - for r, tbl in pairs(changes) do - for s, os in pairs(tbl) do - -- section add - if os['.type'] and os['.type'] ~= "" then - ret[#ret+1] = "%s.%s=%s" %{ r, s, os['.type'] } - for o, v in util.kspairs(os) do - if o:sub(1,1) ~= "." then - if type(v) == "table" then - local i - for i = 1, #v do - ret[#ret+1] = "
      %s.%s.%s+=%s" - %{ r, s, o, util.pcdata(v[i]) } - end - elseif v ~= "" then - ret[#ret+1] = "
      %s.%s.%s=%s" - %{ r, s, o, util.pcdata(v) } - else - ret[#ret+1] = "
      %s.%s.%s" %{ r, s, o } - end - end - end - ret[#ret+1] = "

      " - - -- section delete - elseif os['.type'] and os['.type'] == "" then - ret[#ret+1] = "%s.%s
      " %{ r, s } - - -- modifications - else - ret[#ret+1] = "%s.%s
      " %{ r, s } - for o, v in util.kspairs(os) do - if o:sub(1,1) ~= "." then - if v and #v > 0 then - ret[#ret+1] = "" - if type(v) == "table" then - local i - for i = 1, #v do - ret[#ret+1] = "%s.%s.%s+=%s
      " - %{ r, s, o, util.pcdata(v[i]) } - end - - else - ret[#ret+1] = "%s.%s.%s=%s
      " - %{ r, s, o, util.pcdata(v) } - end - ret[#ret+1] = "
      " - else - ret[#ret+1] = "%s.%s.%s
      " %{ r, s, o } - end - end - end - ret[#ret+1] = "

      " - end - end - end - - write(table.concat(ret)) - %>
      -
      -<%- end) %> diff --git a/luci-mod-admin-full/src/Makefile b/luci-mod-admin-full/src/Makefile deleted file mode 100644 index d6ed8c6e4..000000000 --- a/luci-mod-admin-full/src/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -%.o: %.c - $(CC) $(CPPFLAGS) $(CFLAGS) $(FPIC) -c -o $@ $< - -clean: - rm -f luci-bwc *.o - -luci-bwc: luci-bwc.o - $(CC) $(LDFLAGS) -o $@ $^ -ldl - -compile: luci-bwc - -install: compile - mkdir -p $(DESTDIR)/usr/bin - cp luci-bwc $(DESTDIR)/usr/bin/luci-bwc diff --git a/luci-mod-admin-full/src/luci-bwc.c b/luci-mod-admin-full/src/luci-bwc.c deleted file mode 100644 index 8ddd91727..000000000 --- a/luci-mod-admin-full/src/luci-bwc.c +++ /dev/null @@ -1,778 +0,0 @@ -/* - * luci-bwc - Very simple bandwidth collector cache for LuCI realtime graphs - * - * Copyright (C) 2010 Jo-Philipp Wich - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include - -#define STEP_COUNT 60 -#define STEP_TIME 1 -#define TIMEOUT 10 - -#define PID_PATH "/var/run/luci-bwc.pid" - -#define DB_PATH "/var/lib/luci-bwc" -#define DB_IF_FILE DB_PATH "/if/%s" -#define DB_RD_FILE DB_PATH "/radio/%s" -#define DB_CN_FILE DB_PATH "/connections" -#define DB_LD_FILE DB_PATH "/load" - -#define IF_SCAN_PATTERN \ - " %[^ :]:%u %u" \ - " %*d %*d %*d %*d %*d %*d" \ - " %u %u" - -#define LD_SCAN_PATTERN \ - "%f %f %f" - - -struct file_map { - int fd; - int size; - char *mmap; -}; - -struct traffic_entry { - uint32_t time; - uint32_t rxb; - uint32_t rxp; - uint32_t txb; - uint32_t txp; -}; - -struct conn_entry { - uint32_t time; - uint32_t udp; - uint32_t tcp; - uint32_t other; -}; - -struct load_entry { - uint32_t time; - uint16_t load1; - uint16_t load5; - uint16_t load15; -}; - -struct radio_entry { - uint32_t time; - uint16_t rate; - uint8_t rssi; - uint8_t noise; -}; - -static int readpid(void) -{ - int fd; - int pid = -1; - char buf[9] = { 0 }; - - if ((fd = open(PID_PATH, O_RDONLY)) > -1) - { - if (read(fd, buf, sizeof(buf))) - { - buf[8] = 0; - pid = atoi(buf); - } - - close(fd); - } - - return pid; -} - -static int writepid(void) -{ - int fd; - int wlen; - char buf[9] = { 0 }; - - if ((fd = open(PID_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0600)) > -1) - { - wlen = snprintf(buf, sizeof(buf), "%i", getpid()); - write(fd, buf, wlen); - close(fd); - - return 0; - } - - return -1; -} - -static int timeout = TIMEOUT; -static int countdown = -1; - -static void reset_countdown(int sig) -{ - countdown = timeout; - -} - - -static char *progname; -static int prognamelen; - -static struct iwinfo_ops *backend = NULL; - - -static int init_directory(char *path) -{ - char *p = path; - - for (p = &path[1]; *p; p++) - { - if (*p == '/') - { - *p = 0; - - if (mkdir(path, 0700) && (errno != EEXIST)) - return -1; - - *p = '/'; - } - } - - return 0; -} - -static int init_file(char *path, int esize) -{ - int i, file; - char buf[sizeof(struct traffic_entry)] = { 0 }; - - if (init_directory(path)) - return -1; - - if ((file = open(path, O_WRONLY | O_CREAT, 0600)) >= 0) - { - for (i = 0; i < STEP_COUNT; i++) - { - if (write(file, buf, esize) < 0) - break; - } - - close(file); - - return 0; - } - - return -1; -} - -static inline uint32_t timeof(void *entry) -{ - return ntohl(((struct traffic_entry *)entry)->time); -} - -static int update_file(const char *path, void *entry, int esize) -{ - int rv = -1; - int file; - char *map; - - if ((file = open(path, O_RDWR)) >= 0) - { - map = mmap(NULL, esize * STEP_COUNT, PROT_READ | PROT_WRITE, - MAP_SHARED | MAP_LOCKED, file, 0); - - if ((map != NULL) && (map != MAP_FAILED)) - { - if (timeof(entry) > timeof(map + esize * (STEP_COUNT-1))) - { - memmove(map, map + esize, esize * (STEP_COUNT-1)); - memcpy(map + esize * (STEP_COUNT-1), entry, esize); - } - - munmap(map, esize * STEP_COUNT); - - rv = 0; - } - - close(file); - } - - return rv; -} - -static int mmap_file(const char *path, int esize, struct file_map *m) -{ - m->fd = -1; - m->size = -1; - m->mmap = NULL; - - if ((m->fd = open(path, O_RDONLY)) >= 0) - { - m->size = STEP_COUNT * esize; - m->mmap = mmap(NULL, m->size, PROT_READ, - MAP_SHARED | MAP_LOCKED, m->fd, 0); - - if ((m->mmap != NULL) && (m->mmap != MAP_FAILED)) - return 0; - } - - return -1; -} - -static void umap_file(struct file_map *m) -{ - if ((m->mmap != NULL) && (m->mmap != MAP_FAILED)) - munmap(m->mmap, m->size); - - if (m->fd > -1) - close(m->fd); -} - -static void * iw_open(void) -{ - return dlopen("/usr/lib/libiwinfo.so", RTLD_LAZY); -} - -static int iw_update( - void *iw, const char *ifname, uint16_t *rate, uint8_t *rssi, uint8_t *noise -) { - struct iwinfo_ops *(*probe)(const char *); - int val; - - if (!backend) - { - probe = dlsym(iw, "iwinfo_backend"); - - if (!probe) - return 0; - - backend = probe(ifname); - - if (!backend) - return 0; - } - - *rate = (backend->bitrate && !backend->bitrate(ifname, &val)) ? val : 0; - *rssi = (backend->signal && !backend->signal(ifname, &val)) ? val : 0; - *noise = (backend->noise && !backend->noise(ifname, &val)) ? val : 0; - - return 1; -} - -static void iw_close(void *iw) -{ - void (*finish)(void); - - finish = dlsym(iw, "iwinfo_finish"); - - if (finish) - finish(); - - dlclose(iw); -} - - -static int update_ifstat( - const char *ifname, uint32_t rxb, uint32_t rxp, uint32_t txb, uint32_t txp -) { - char path[1024]; - - struct stat s; - struct traffic_entry e; - - snprintf(path, sizeof(path), DB_IF_FILE, ifname); - - if (stat(path, &s)) - { - if (init_file(path, sizeof(struct traffic_entry))) - { - fprintf(stderr, "Failed to init %s: %s\n", - path, strerror(errno)); - - return -1; - } - } - - e.time = htonl(time(NULL)); - e.rxb = htonl(rxb); - e.rxp = htonl(rxp); - e.txb = htonl(txb); - e.txp = htonl(txp); - - return update_file(path, &e, sizeof(struct traffic_entry)); -} - -static int update_radiostat( - const char *ifname, uint16_t rate, uint8_t rssi, uint8_t noise -) { - char path[1024]; - - struct stat s; - struct radio_entry e; - - snprintf(path, sizeof(path), DB_RD_FILE, ifname); - - if (stat(path, &s)) - { - if (init_file(path, sizeof(struct radio_entry))) - { - fprintf(stderr, "Failed to init %s: %s\n", - path, strerror(errno)); - - return -1; - } - } - - e.time = htonl(time(NULL)); - e.rate = htons(rate); - e.rssi = rssi; - e.noise = noise; - - return update_file(path, &e, sizeof(struct radio_entry)); -} - -static int update_cnstat(uint32_t udp, uint32_t tcp, uint32_t other) -{ - char path[1024]; - - struct stat s; - struct conn_entry e; - - snprintf(path, sizeof(path), DB_CN_FILE); - - if (stat(path, &s)) - { - if (init_file(path, sizeof(struct conn_entry))) - { - fprintf(stderr, "Failed to init %s: %s\n", - path, strerror(errno)); - - return -1; - } - } - - e.time = htonl(time(NULL)); - e.udp = htonl(udp); - e.tcp = htonl(tcp); - e.other = htonl(other); - - return update_file(path, &e, sizeof(struct conn_entry)); -} - -static int update_ldstat(uint16_t load1, uint16_t load5, uint16_t load15) -{ - char path[1024]; - - struct stat s; - struct load_entry e; - - snprintf(path, sizeof(path), DB_LD_FILE); - - if (stat(path, &s)) - { - if (init_file(path, sizeof(struct load_entry))) - { - fprintf(stderr, "Failed to init %s: %s\n", - path, strerror(errno)); - - return -1; - } - } - - e.time = htonl(time(NULL)); - e.load1 = htons(load1); - e.load5 = htons(load5); - e.load15 = htons(load15); - - return update_file(path, &e, sizeof(struct load_entry)); -} - -static int run_daemon(void) -{ - FILE *info; - uint32_t rxb, txb, rxp, txp; - uint32_t udp, tcp, other; - uint16_t rate; - uint8_t rssi, noise; - float lf1, lf5, lf15; - char line[1024]; - char ifname[16]; - int i; - void *iw; - struct sigaction sa; - - struct stat s; - const char *ipc = stat("/proc/net/nf_conntrack", &s) - ? "/proc/net/ip_conntrack" : "/proc/net/nf_conntrack"; - - switch (fork()) - { - case -1: - perror("fork()"); - return -1; - - case 0: - if (chdir("/") < 0) - { - perror("chdir()"); - exit(1); - } - - close(0); - close(1); - close(2); - break; - - default: - return 0; - } - - /* setup USR1 signal handler to reset timer */ - sa.sa_handler = reset_countdown; - sa.sa_flags = SA_RESTART; - sigemptyset(&sa.sa_mask); - sigaction(SIGUSR1, &sa, NULL); - - /* write pid */ - if (writepid()) - { - fprintf(stderr, "Failed to write pid file: %s\n", strerror(errno)); - return 1; - } - - /* initialize iwinfo */ - iw = iw_open(); - - /* go */ - for (reset_countdown(0); countdown >= 0; countdown--) - { - /* alter progname for ps, top */ - memset(progname, 0, prognamelen); - snprintf(progname, prognamelen, "luci-bwc %d", countdown); - - if ((info = fopen("/proc/net/dev", "r")) != NULL) - { - while (fgets(line, sizeof(line), info)) - { - if (strchr(line, '|')) - continue; - - if (sscanf(line, IF_SCAN_PATTERN, ifname, &rxb, &rxp, &txb, &txp)) - { - if (strncmp(ifname, "lo", sizeof(ifname))) - update_ifstat(ifname, rxb, rxp, txb, txp); - } - } - - fclose(info); - } - - if (iw) - { - for (i = 0; i < 5; i++) - { -#define iw_checkif(pattern) \ - do { \ - snprintf(ifname, sizeof(ifname), pattern, i); \ - if (iw_update(iw, ifname, &rate, &rssi, &noise)) \ - { \ - update_radiostat(ifname, rate, rssi, noise); \ - continue; \ - } \ - } while(0) - - iw_checkif("wlan%d"); - iw_checkif("ath%d"); - iw_checkif("wl%d"); - } - } - - if ((info = fopen(ipc, "r")) != NULL) - { - udp = 0; - tcp = 0; - other = 0; - - while (fgets(line, sizeof(line), info)) - { - if (strstr(line, "TIME_WAIT")) - continue; - - if ((strstr(line, "src=127.0.0.1 ") && strstr(line, "dst=127.0.0.1 ")) - || (strstr(line, "src=::1 ") && strstr(line, "dst=::1 "))) - continue; - - if (sscanf(line, "%*s %*d %s", ifname) || sscanf(line, "%s %*d", ifname)) - { - if (!strcmp(ifname, "tcp")) - tcp++; - else if (!strcmp(ifname, "udp")) - udp++; - else - other++; - } - } - - update_cnstat(udp, tcp, other); - - fclose(info); - } - - if ((info = fopen("/proc/loadavg", "r")) != NULL) - { - if (fscanf(info, LD_SCAN_PATTERN, &lf1, &lf5, &lf15)) - { - update_ldstat((uint16_t)(lf1 * 100), - (uint16_t)(lf5 * 100), - (uint16_t)(lf15 * 100)); - } - - fclose(info); - } - - sleep(STEP_TIME); - } - - unlink(PID_PATH); - - if (iw) - iw_close(iw); - - return 0; -} - -static void check_daemon(void) -{ - int pid; - - if ((pid = readpid()) < 0 || kill(pid, 0) < 0) - { - /* daemon ping failed, try to start it up */ - if (run_daemon()) - { - fprintf(stderr, - "Failed to ping daemon and unable to start it up: %s\n", - strerror(errno)); - - exit(1); - } - } - else if (kill(pid, SIGUSR1)) - { - fprintf(stderr, "Failed to send signal: %s\n", strerror(errno)); - exit(2); - } -} - -static int run_dump_ifname(const char *ifname) -{ - int i; - char path[1024]; - struct file_map m; - struct traffic_entry *e; - - check_daemon(); - snprintf(path, sizeof(path), DB_IF_FILE, ifname); - - if (mmap_file(path, sizeof(struct traffic_entry), &m)) - { - fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); - return 1; - } - - for (i = 0; i < m.size; i += sizeof(struct traffic_entry)) - { - e = (struct traffic_entry *) &m.mmap[i]; - - if (!e->time) - continue; - - printf("[ %u, %u, %" PRIu32 - ", %u, %u ]%s\n", - ntohl(e->time), - ntohl(e->rxb), ntohl(e->rxp), - ntohl(e->txb), ntohl(e->txp), - ((i + sizeof(struct traffic_entry)) < m.size) ? "," : ""); - } - - umap_file(&m); - - return 0; -} - -static int run_dump_radio(const char *ifname) -{ - int i; - char path[1024]; - struct file_map m; - struct radio_entry *e; - - check_daemon(); - snprintf(path, sizeof(path), DB_RD_FILE, ifname); - - if (mmap_file(path, sizeof(struct radio_entry), &m)) - { - fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); - return 1; - } - - for (i = 0; i < m.size; i += sizeof(struct radio_entry)) - { - e = (struct radio_entry *) &m.mmap[i]; - - if (!e->time) - continue; - - printf("[ %u, %d, %d, %d ]%s\n", - ntohl(e->time), - e->rate, e->rssi, e->noise, - ((i + sizeof(struct radio_entry)) < m.size) ? "," : ""); - } - - umap_file(&m); - - return 0; -} - -static int run_dump_conns(void) -{ - int i; - char path[1024]; - struct file_map m; - struct conn_entry *e; - - check_daemon(); - snprintf(path, sizeof(path), DB_CN_FILE); - - if (mmap_file(path, sizeof(struct conn_entry), &m)) - { - fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); - return 1; - } - - for (i = 0; i < m.size; i += sizeof(struct conn_entry)) - { - e = (struct conn_entry *) &m.mmap[i]; - - if (!e->time) - continue; - - printf("[ %u, %u, %u, %u ]%s\n", - ntohl(e->time), ntohl(e->udp), - ntohl(e->tcp), ntohl(e->other), - ((i + sizeof(struct conn_entry)) < m.size) ? "," : ""); - } - - umap_file(&m); - - return 0; -} - -static int run_dump_load(void) -{ - int i; - char path[1024]; - struct file_map m; - struct load_entry *e; - - check_daemon(); - snprintf(path, sizeof(path), DB_LD_FILE); - - if (mmap_file(path, sizeof(struct load_entry), &m)) - { - fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); - return 1; - } - - for (i = 0; i < m.size; i += sizeof(struct load_entry)) - { - e = (struct load_entry *) &m.mmap[i]; - - if (!e->time) - continue; - - printf("[ %u, %u, %u, %u ]%s\n", - ntohl(e->time), - ntohs(e->load1), ntohs(e->load5), ntohs(e->load15), - ((i + sizeof(struct load_entry)) < m.size) ? "," : ""); - } - - umap_file(&m); - - return 0; -} - - -int main(int argc, char *argv[]) -{ - int opt; - - progname = argv[0]; - prognamelen = -1; - - for (opt = 0; opt < argc; opt++) - prognamelen += 1 + strlen(argv[opt]); - - while ((opt = getopt(argc, argv, "t:i:r:cl")) > -1) - { - switch (opt) - { - case 't': - timeout = atoi(optarg); - break; - - case 'i': - if (optarg) - return run_dump_ifname(optarg); - break; - - case 'r': - if (optarg) - return run_dump_radio(optarg); - break; - - case 'c': - return run_dump_conns(); - - case 'l': - return run_dump_load(); - - default: - break; - } - } - - fprintf(stderr, - "Usage:\n" - " %s [-t timeout] -i ifname\n" - " %s [-t timeout] -r radiodev\n" - " %s [-t timeout] -c\n" - " %s [-t timeout] -l\n", - argv[0], argv[0], argv[0], argv[0] - ); - - return 1; -} diff --git a/luci-mod-network/Makefile b/luci-mod-network/Makefile new file mode 100644 index 000000000..a087218f0 --- /dev/null +++ b/luci-mod-network/Makefile @@ -0,0 +1,17 @@ +# +# Copyright (C) 2008-2014 The LuCI Team +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Network Administration +LUCI_DEPENDS:=+luci-base +libiwinfo-lua + +PKG_LICENSE:=Apache-2.0 + +include ../luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature + diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/iface_status.js b/luci-mod-network/htdocs/luci-static/resources/view/network/iface_status.js new file mode 100644 index 000000000..88f48d189 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/iface_status.js @@ -0,0 +1,42 @@ +requestAnimationFrame(function() { + document.querySelectorAll('[data-iface-status]').forEach(function(container) { + var network = container.getAttribute('data-iface-status'), + icon = container.querySelector('img'), + info = container.querySelector('span'); + + L.poll(5, L.url('admin/network/iface_status', network), null, function(xhr, ifaces) { + var ifc = Array.isArray(ifaces) ? ifaces[0] : null; + if (!ifc) + return; + + L.itemlist(info, [ + _('Device'), ifc.ifname, + _('Uptime'), ifc.is_up ? '%t'.format(ifc.uptime) : null, + _('MAC'), ifc.ifname ? ifc.macaddr : null, + _('RX'), ifc.ifname ? '%.2mB (%d %s)'.format(ifc.rx_bytes, ifc.rx_packets, _('Pkts.')) : null, + _('TX'), ifc.ifname ? '%.2mB (%d %s)'.format(ifc.tx_bytes, ifc.tx_packets, _('Pkts.')) : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[0] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[1] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[2] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[3] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[4] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[0] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[1] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[2] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[3] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[4] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[5] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[6] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[7] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[8] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[9] : null, + _('IPv6-PD'), ifc.ip6prefix, + null, ifc.ifname ? null : E('em', _('Interface not present or not connected yet.')) + ]); + + icon.src = L.resource('icons/%s%s.png').format(ifc.type, ifc.is_up ? '' : '_disabled'); + }); + + L.run(); + }); +}); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/network.js b/luci-mod-network/htdocs/luci-static/resources/view/network/network.js new file mode 100644 index 000000000..bab23cc3c --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/network.js @@ -0,0 +1,136 @@ +function iface_reconnect(id) { + L.halt(); + L.dom.content(document.getElementById(id + '-ifc-description'), E('em', _('Interface is reconnecting...'))); + L.post(L.url('admin/network/iface_reconnect', id), null, L.run); +} + +function iface_delete(ev) { + if (!confirm(_('Really delete this interface? The deletion cannot be undone! You might lose access to this device if you are connected via this interface'))) { + ev.preventDefault(); + return false; + } + + ev.target.previousElementSibling.value = '1'; + return true; +} + +var networks = []; + +document.querySelectorAll('[data-network]').forEach(function(n) { + networks.push(n.getAttribute('data-network')); +}); + +function render_iface(ifc) { + return E('span', { class: 'cbi-tooltip-container' }, [ + E('img', { 'class' : 'middle', 'src': L.resource('icons/%s%s.png').format( + ifc.is_alias ? 'alias' : ifc.type, + ifc.is_up ? '' : '_disabled') }), + E('span', { 'class': 'cbi-tooltip ifacebadge large' }, [ + E('img', { 'src': L.resource('icons/%s%s.png').format( + ifc.type, ifc.is_up ? '' : '_disabled') }), + L.itemlist(E('span', { 'class': 'left' }), [ + _('Type'), ifc.typename, + _('Device'), ifc.ifname, + _('Connected'), ifc.is_up ? _('yes') : _('no'), + _('MAC'), ifc.macaddr, + _('RX'), '%.2mB (%d %s)'.format(ifc.rx_bytes, ifc.rx_packets, _('Pkts.')), + _('TX'), '%.2mB (%d %s)'.format(ifc.tx_bytes, ifc.tx_packets, _('Pkts.')) + ]) + ]) + ]); +} + +L.poll(5, L.url('admin/network/iface_status', networks.join(',')), null, + function(x, ifcs) { + if (ifcs) { + for (var idx = 0; idx < ifcs.length; idx++) { + var ifc = ifcs[idx]; + + var s = document.getElementById(ifc.id + '-ifc-devices'); + if (s) { + var c = [ render_iface(ifc) ]; + + if (ifc.subdevices && ifc.subdevices.length) + { + var sifs = [ ' (' ]; + + for (var j = 0; j < ifc.subdevices.length; j++) + sifs.push(render_iface(ifc.subdevices[j])); + + sifs.push(')'); + + c.push(E('span', {}, sifs)); + } + + c.push(E('br')); + c.push(E('small', {}, ifc.is_alias ? _('Alias of "%s"').format(ifc.is_alias) : ifc.name)); + + L.dom.content(s, c); + } + + var d = document.getElementById(ifc.id + '-ifc-description'); + if (d && ifc.proto && ifc.ifname) { + var desc = null, c = []; + + if (ifc.is_dynamic) + desc = _('Virtual dynamic interface'); + else if (ifc.is_alias) + desc = _('Alias Interface'); + + if (ifc.desc) + desc = desc ? '%s (%s)'.format(desc, ifc.desc) : ifc.desc; + + L.itemlist(d, [ + _('Protocol'), desc || '?', + _('Uptime'), ifc.is_up ? '%t'.format(ifc.uptime) : null, + _('MAC'), (!ifc.is_dynamic && !ifc.is_alias && ifc.macaddr) ? ifc.macaddr : null, + _('RX'), (!ifc.is_dynamic && !ifc.is_alias) ? '%.2mB (%d %s)'.format(ifc.rx_bytes, ifc.rx_packets, _('Pkts.')) : null, + _('TX'), (!ifc.is_dynamic && !ifc.is_alias) ? '%.2mB (%d %s)'.format(ifc.tx_bytes, ifc.tx_packets, _('Pkts.')) : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[0] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[1] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[2] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[3] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[4] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[0] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[1] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[2] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[3] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[4] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[5] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[6] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[7] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[8] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[9] : null, + _('IPv6-PD'), ifc.ip6prefix, + _('Information'), ifc.is_auto ? null : _('Not started on boot'), + _('Error'), ifc.errors ? ifc.errors[0] : null, + _('Error'), ifc.errors ? ifc.errors[1] : null, + _('Error'), ifc.errors ? ifc.errors[2] : null, + _('Error'), ifc.errors ? ifc.errors[3] : null, + _('Error'), ifc.errors ? ifc.errors[4] : null, + ]); + } + else if (d && !ifc.proto) { + var e = document.getElementById(ifc.id + '-ifc-edit'); + if (e) e.disabled = true; + + var link = L.url('admin/system/opkg') + '?query=luci-proto'; + L.dom.content(d, [ + E('em', _('Unsupported protocol type.')), E('br'), + E('a', { href: link }, _('Install protocol extensions...')) + ]); + } + else if (d && !ifc.ifname) { + var link = L.url('admin/network/network', ifc.name) + '?tab.network.%s=physical'.format(ifc.name); + L.dom.content(d, [ + E('em', _('Network without interfaces.')), E('br'), + E('a', { href: link }, _('Assign interfaces...')) + ]); + } + else if (d) { + L.dom.content(d, E('em' ,_('Interface not present or not connected yet.'))); + } + } + } + } +); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_join.js b/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_join.js new file mode 100644 index 000000000..d5bd7b0a6 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_join.js @@ -0,0 +1,159 @@ +var poll = null; + +function format_signal(bss) { + var qval = bss.quality || 0, + qmax = bss.quality_max || 100, + scale = 100 / qmax * qval, + range = 'none'; + + if (!bss.bssid || bss.bssid == '00:00:00:00:00:00') + range = 'none'; + else if (scale < 15) + range = '0'; + else if (scale < 35) + range = '0-25'; + else if (scale < 55) + range = '25-50'; + else if (scale < 75) + range = '50-75'; + else + range = '75-100'; + + return E('span', { + class: 'ifacebadge', + title: '%s: %d%s / %s: %d/%d'.format(_('Signal'), bss.signal, _('dB'), _('Quality'), qval, qmax) + }, [ + E('img', { src: L.resource('icons/signal-%s.png').format(range) }), + ' %d%%'.format(scale) + ]); +} + +function format_encryption(bss) { + var enc = bss.encryption || { } + + if (enc.wep === true) + return 'WEP'; + else if (enc.wpa > 0) + return E('abbr', { + title: 'Pairwise: %h / Group: %h'.format( + enc.pair_ciphers.join(', '), + enc.group_ciphers.join(', ')) + }, + '%h - %h'.format( + (enc.wpa === 3) ? _('mixed WPA/WPA2') : (enc.wpa === 2 ? 'WPA2' : 'WPA'), + enc.auth_suites.join(', '))); + else + return E('em', enc.enabled ? _('unknown') : _('open')); +} + +function format_actions(dev, type, bss) { + var enc = bss.encryption || { }, + input = [ + E('input', { type: 'submit', class: 'cbi-button cbi-button-action important', value: _('Join Network') }), + E('input', { type: 'hidden', name: 'token', value: L.env.token }), + E('input', { type: 'hidden', name: 'device', value: dev }), + E('input', { type: 'hidden', name: 'join', value: bss.ssid }), + E('input', { type: 'hidden', name: 'mode', value: bss.mode }), + E('input', { type: 'hidden', name: 'bssid', value: bss.bssid }), + E('input', { type: 'hidden', name: 'channel', value: bss.channel }), + E('input', { type: 'hidden', name: 'clbridge', value: type === 'wl' ? 1 : 0 }), + E('input', { type: 'hidden', name: 'wep', value: enc.wep ? 1 : 0 }) + ]; + + if (enc.wpa) { + input.push(E('input', { type: 'hidden', name: 'wpa_version', value: enc.wpa })); + + enc.auth_suites.forEach(function(s) { + input.push(E('input', { type: 'hidden', name: 'wpa_suites', value: s })); + }); + + enc.group_ciphers.forEach(function(s) { + input.push(E('input', { type: 'hidden', name: 'wpa_group', value: s })); + }); + + enc.pair_ciphers.forEach(function(s) { + input.push(E('input', { type: 'hidden', name: 'wpa_pairwise', value: s })); + }); + } + + return E('form', { + class: 'inline', + method: 'post', + action: L.url('admin/network/wireless_join') + }, input); +} + +function fade(bss, content) { + if (bss.stale) + return E('span', { style: 'opacity:0.5' }, content); + else + return content; +} + +function flush() { + L.stop(poll); + L.halt(); + + scan(); +} + +function scan() { + var tbl = document.querySelector('[data-wifi-scan]'), + dev = tbl.getAttribute('data-wifi-scan'), + type = tbl.getAttribute('data-wifi-type'); + + cbi_update_table(tbl, [], E('em', { class: 'spinning' }, _('Starting wireless scan...'))); + + L.post(L.url('admin/network/wireless_scan_trigger', dev), null, function(s) { + if (s.status !== 204) { + cbi_update_table(tbl, [], E('em', _('Scan request failed'))); + return; + } + + var count = 0; + + poll = L.poll(3, L.url('admin/network/wireless_scan_results', dev), null, function(s, results) { + if (Array.isArray(results)) { + var bss = []; + + results.sort(function(a, b) { + var diff = (b.quality - a.quality) || (a.channel - b.channel); + + if (diff) + return diff; + + if (a.ssid < b.ssid) + return -1; + else if (a.ssid > b.ssid) + return 1; + + if (a.bssid < b.bssid) + return -1; + else if (a.bssid > b.bssid) + return 1; + }).forEach(function(res) { + bss.push([ + fade(res, format_signal(res)), + fade(res, res.ssid ? '%h'.format(res.ssid) : E('em', {}, _('hidden'))), + fade(res, res.channel), + fade(res, res.mode), + fade(res, res.bssid), + fade(res, format_encryption(res)), + format_actions(dev, type, res) + ]); + }); + + cbi_update_table(tbl, bss, E('em', { class: 'spinning' }, _('No scan results available yet...'))); + } + + if (count++ >= 3) { + count = 0; + L.post(L.url('admin/network/wireless_scan_trigger', dev, 1), null, function() {}); + } + }); + + L.run(); + }); +} + +document.addEventListener('DOMContentLoaded', scan); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_status.js b/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_status.js new file mode 100644 index 000000000..108a141f8 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_status.js @@ -0,0 +1,59 @@ +requestAnimationFrame(function() { + document.querySelectorAll('[data-wifi-status]').forEach(function(container) { + var ifname = container.getAttribute('data-wifi-status'), + small = container.querySelector('small'), + info = container.querySelector('span'); + + L.poll(5, L.url('admin/network/wireless_status', ifname), null, function(xhr, iws) { + var iw = Array.isArray(iws) ? iws[0] : null; + if (!iw) + return; + + var is_assoc = (iw.bssid && iw.bssid != '00:00:00:00:00:00' && iw.channel && !iw.disabled); + var p = iw.quality; + var q = iw.disabled ? -1 : p; + + var icon; + if (q < 0) + icon = L.resource('icons/signal-none.png'); + else if (q == 0) + icon = L.resource('icons/signal-0.png'); + else if (q < 25) + icon = L.resource('icons/signal-0-25.png'); + else if (q < 50) + icon = L.resource('icons/signal-25-50.png'); + else if (q < 75) + icon = L.resource('icons/signal-50-75.png'); + else + icon = L.resource('icons/signal-75-100.png'); + + L.dom.content(small, [ + E('img', { + src: icon, + title: '%s: %d %s / %s: %d %s'.format( + _('Signal'), iw.signal, _('dBm'), + _('Noise'), iw.noise, _('dBm')) + }), + '\u00a0', E('br'), '%d%%\u00a0'.format(p) + ]); + + L.itemlist(info, [ + _('Mode'), iw.mode, + _('SSID'), iw.ssid || '?', + _('BSSID'), is_assoc ? iw.bssid : null, + _('Encryption'), is_assoc ? iw.encryption || _('None') : null, + _('Channel'), is_assoc ? '%d (%.3f %s)'.format(iw.channel, iw.frequency || 0, _('GHz')) : null, + _('Tx-Power'), is_assoc ? '%d %s'.format(iw.txpower, _('dBm')) : null, + _('Signal'), is_assoc ? '%d %s'.format(iw.signal, _('dBm')) : null, + _('Noise'), is_assoc ? '%d %s'.format(iw.noise, _('dBm')) : null, + _('Bitrate'), is_assoc ? '%.1f %s'.format(iw.bitrate || 0, _('Mbit/s')) : null, + _('Country'), is_assoc ? iw.country : null + ], [ ' | ', E('br'), E('br'), E('br'), E('br'), E('br'), ' | ', E('br'), ' | ' ]); + + if (!is_assoc) + L.dom.append(info, E('em', iw.disabled ? _('Wireless is disabled') : _('Wireless is not associated'))); + }); + + L.run(); + }); +}); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js b/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js new file mode 100644 index 000000000..57e6bbb04 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js @@ -0,0 +1,93 @@ +function wifi_delete(ev) { + if (!confirm(_('Really delete this wireless network? The deletion cannot be undone! You might lose access to this device if you are connected via this network.'))) { + ev.preventDefault(); + return false; + } + + ev.target.previousElementSibling.value = '1'; + return true; +} + +function wifi_restart(ev) { + L.halt(); + + findParent(ev.target, '.table').querySelectorAll('[data-disabled="false"]').forEach(function(s) { + L.dom.content(s, E('em', _('Wireless is restarting...'))); + }); + + L.post(L.url('admin/network/wireless_reconnect', ev.target.getAttribute('data-radio')), L.run); +} + +var networks = [ ]; + +document.querySelectorAll('[data-network]').forEach(function(n) { + networks.push(n.getAttribute('data-network')); +}); + +L.poll(5, L.url('admin/network/wireless_status', networks.join(',')), null, + function(x, st) { + if (st) { + var rowstyle = 1; + var radiostate = { }; + + st.forEach(function(s) { + var r = radiostate[s.device.device] || (radiostate[s.device.device] = {}); + + s.is_assoc = (s.bssid && s.bssid != '00:00:00:00:00:00' && s.channel && s.mode != 'Unknown' && !s.disabled); + + r.up = r.up || s.is_assoc; + r.channel = r.channel || s.channel; + r.bitrate = r.bitrate || s.bitrate; + r.frequency = r.frequency || s.frequency; + }); + + for (var i = 0; i < st.length; i++) { + var iw = st[i], + sig = document.getElementById(iw.id + '-iw-signal'), + info = document.getElementById(iw.id + '-iw-status'), + disabled = (info && info.getAttribute('data-disabled') === 'true'); + + var p = iw.quality; + var q = disabled ? -1 : p; + + var icon; + if (q < 0) + icon = L.resource('icons/signal-none.png'); + else if (q == 0) + icon = L.resource('icons/signal-0.png'); + else if (q < 25) + icon = L.resource('icons/signal-0-25.png'); + else if (q < 50) + icon = L.resource('icons/signal-25-50.png'); + else if (q < 75) + icon = L.resource('icons/signal-50-75.png'); + else + icon = L.resource('icons/signal-75-100.png'); + + L.dom.content(sig, E('span', { + class: 'ifacebadge', + title: '%s %d %s / %s: %d %s'.format(_('Signal'), iw.signal, _('dBm'), _('Noise'), iw.noise, _('dBm')) + }, [ E('img', { src: icon }), ' %d%%'.format(p) ])); + + L.itemlist(info, [ + _('SSID'), iw.ssid || '?', + _('Mode'), iw.mode, + _('BSSID'), iw.is_assoc ? iw.bssid : null, + _('Encryption'), iw.is_assoc ? iw.encryption || _('None') : null, + null, iw.is_assoc ? null : E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated')) + ], [ ' | ', E('br') ]); + } + + for (var dev in radiostate) { + var img = document.getElementById(dev + '-iw-upstate'); + if (img) img.src = L.resource('icons/wifi' + (radiostate[dev].up ? '' : '_disabled') + '.png'); + + var stat = document.getElementById(dev + '-iw-devinfo'); + L.itemlist(stat, [ + _('Channel'), '%s (%s %s)'.format(radiostate[dev].channel || '?', radiostate[dev].frequency || '?', _('GHz')), + _('Bitrate'), '%s %s'.format(radiostate[dev].bitrate || '?', _('Mbit/s')) + ], ' | '); + } + } + } +); diff --git a/luci-mod-admin-full/luasrc/controller/admin/network.lua b/luci-mod-network/luasrc/controller/admin/network.lua similarity index 87% rename from luci-mod-admin-full/luasrc/controller/admin/network.lua rename to luci-mod-network/luasrc/controller/admin/network.lua index aef67a364..2312dcf6e 100644 --- a/luci-mod-admin-full/luasrc/controller/admin/network.lua +++ b/luci-mod-network/luasrc/controller/admin/network.lua @@ -8,12 +8,6 @@ function index() local uci = require("luci.model.uci").cursor() local page - page = node("admin", "network") - page.target = firstchild() - page.title = _("Network") - page.order = 50 - page.index = true - -- if page.inreq then local has_switch = false @@ -43,9 +37,6 @@ function index() end) if has_wifi then - page = entry({"admin", "network", "wireless_assoclist"}, call("wifi_assoclist"), nil) - page.leaf = true - page = entry({"admin", "network", "wireless_join"}, post("wifi_join"), nil) page.leaf = true @@ -116,9 +107,6 @@ function index() page.title = _("DHCP and DNS") page.order = 30 - page = entry({"admin", "network", "dhcplease_status"}, call("lease_status"), nil) - page.leaf = true - page = node("admin", "network", "hosts") page.target = cbi("admin_network/hosts") page.title = _("Hostnames") @@ -149,15 +137,6 @@ function index() page = entry({"admin", "network", "diag_traceroute6"}, post("diag_traceroute6"), nil) page.leaf = true - - page = entry({"admin", "network", "diag_speedtest"}, post("diag_speedtest"), nil) - page.leaf = true - - page = entry({"admin", "network", "diag_getip"}, post("diag_getip"), nil) - page.leaf = true - - page = entry({"admin", "network", "diag_netstat"}, post("diag_netstat"), nil) - page.leaf = true -- end end @@ -196,7 +175,8 @@ function wifi_add() local net = dev:add_wifinet({ mode = "ap", ssid = "OpenWrt", - encryption = "none" + encryption = "none", + disabled = 1 }) ntm:save("wireless") @@ -232,6 +212,7 @@ function iface_status(ifaces) is_up = net:is_up() and device:is_up(), is_alias = net:is_alias(), is_dynamic = net:is_dynamic(), + is_auto = net:is_auto(), rx_bytes = device:rx_bytes(), tx_bytes = device:tx_bytes(), rx_packets = device:rx_packets(), @@ -317,14 +298,6 @@ function wifi_reconnect(radio) end end -function wifi_assoclist() - local s = require "luci.tools.status" - - luci.http.prepare_content("application/json") - luci.http.write_json(s.wifi_assoclist()) -end - - local function _wifi_get_scan_results(cache_key) local results = luci.util.ubus("session", "get", { ubus_rpc_session = luci.model.uci:get_session_id(), @@ -349,7 +322,7 @@ function wifi_scan_trigger(radio, update) return end - luci.http.status(200, "Scan scheduled") + luci.http.status(204, "Scan scheduled") if nixio.fork() == 0 then io.stderr:close() @@ -396,17 +369,6 @@ function wifi_scan_results(radio) end end -function lease_status() - local s = require "luci.tools.status" - - luci.http.prepare_content("application/json") - luci.http.write('[') - luci.http.write_json(s.dhcp_leases()) - luci.http.write(',') - luci.http.write_json(s.dhcp6_leases()) - luci.http.write(']') -end - function switch_status(switches) local s = require "luci.tools.status" @@ -418,7 +380,7 @@ 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 % addr) + local util = io.popen(cmd % luci.util.shellquote(addr)) if util then while true do local ln = util:read("*l") @@ -455,19 +417,3 @@ end function diag_traceroute6(addr) diag_command("traceroute6 -q 1 -w 2 -n %s 2>&1", addr) end - -function diag_getip(addr) - diag_command("curl %q", addr) -end - -function diag_netstat(addr) - diag_command("netstat -lapute 2>&1", "openmptcprouter.com") -end - -function diag_speedtest(addr) - if addr then - diag_command("speedtestc --server %q 2>&1", addr) - else - diag_command("speedtestc 2>&1", "speedtest.net") - end -end diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/dhcp.lua b/luci-mod-network/luasrc/model/cbi/admin_network/dhcp.lua similarity index 97% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/dhcp.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/dhcp.lua index 934806ba0..0be1b3fb5 100644 --- a/luci-mod-admin-full/luasrc/model/cbi/admin_network/dhcp.lua +++ b/luci-mod-network/luasrc/model/cbi/admin_network/dhcp.lua @@ -250,23 +250,21 @@ o.rmempty = false o = s:taboption("general", Flag, "nonwildcard", translate("Non-wildcard"), - translate("Bind only to specific interfaces rather than wildcard address.")) + translate("Bind dynamically to interfaces rather than wildcard address (recommended as linux default)")) o.optional = false -o.rmempty = false +o.rmempty = true o = s:taboption("general", DynamicList, "interface", translate("Listen Interfaces"), translate("Limit listening to these interfaces, and loopback.")) o.optional = true -o:depends("nonwildcard", true) o = s:taboption("general", DynamicList, "notinterface", translate("Exclude interfaces"), translate("Prevent listening on these interfaces.")) o.optional = true -o:depends("nonwildcard", true) -m:section(SimpleSection).template = "admin_network/lease_status" +m:section(SimpleSection).template = "lease_status" s = m:section(TypedSection, "host", translate("Static Leases"), translate("Static leases are used to assign fixed IP addresses and symbolic hostnames to " .. @@ -297,7 +295,7 @@ function name.remove(self, section) end mac = s:option(Value, "mac", translate("MAC-Address")) -mac.datatype = "list(macaddr)" +mac.datatype = "list(unique(macaddr))" mac.rmempty = true function mac.cfgvalue(self, section) @@ -308,9 +306,6 @@ end ip = s:option(Value, "ip", translate("IPv4-Address")) ip.datatype = "or(ip4addr,'ignore')" -gw = s:option(Value, "gw", translate("Gateway")) -gw.datatype = "or(ip4addr,'ignore')" - time = s:option(Value, "leasetime", translate("Lease time")) time.rmempty = true diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/hosts.lua b/luci-mod-network/luasrc/model/cbi/admin_network/hosts.lua similarity index 100% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/hosts.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/hosts.lua diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/iface_add.lua b/luci-mod-network/luasrc/model/cbi/admin_network/iface_add.lua similarity index 99% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/iface_add.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/iface_add.lua index 8593b52c7..aac944c7a 100644 --- a/luci-mod-admin-full/luasrc/model/cbi/admin_network/iface_add.lua +++ b/luci-mod-network/luasrc/model/cbi/admin_network/iface_add.lua @@ -31,13 +31,11 @@ advice = m:field(DummyValue, "d1", translate("Note: interface name length"), newproto = m:field(ListValue, "_netproto", translate("Protocol of the new interface")) --netbridge = m:field(Flag, "_bridge", translate("Create a bridge over multiple interfaces")) - newtype = m:field(ListValue, "_type", translate("Type of the new interface")) newtype:value("", translate("Default")) newtype:value("macvlan", translate("Create a macvlan sub interface")) newtype:value("bridge", translate("Create a bridge over multiple interfaces")) - sifname = m:field(Value, "_ifname", translate("Cover the following interface")) sifname.widget = "radio" @@ -101,9 +99,9 @@ function newproto.write(self, section, value) local net = nw:add_network(name, { proto = value, type = newtype:formvalue(section), interface = isMacvlan and vifname:formvalue(section) or nil }) if net then local ifn - --br and mifname:formvalue(section) or sifname:formvalue(section) for ifn in utl.imatch( (isMacvlan and name) or (isBridge and mifname:formvalue(section)) or (sifname:formvalue(section)) + --br and mifname:formvalue(section) or sifname:formvalue(section) ) do net:add_interface(ifn) end diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/ifaces.lua b/luci-mod-network/luasrc/model/cbi/admin_network/ifaces.lua similarity index 97% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/ifaces.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/ifaces.lua index 86d3ec4e7..1e3d27ef6 100644 --- a/luci-mod-admin-full/luasrc/model/cbi/admin_network/ifaces.lua +++ b/luci-mod-network/luasrc/model/cbi/admin_network/ifaces.lua @@ -53,7 +53,7 @@ local function get_ifstate(name, option) m.uci:foreach("luci", "ifstate", function (s) if s.interface == name then - val = m.uci:get("luci", s[".name"], option) + val = s[option] return false end end) @@ -232,8 +232,8 @@ if not net:is_installed() then function p_install.write() return luci.http.redirect( - luci.dispatcher.build_url("admin/system/packages") .. - "?submit=1&install=%s" % net:opkg_package() + luci.dispatcher.build_url("admin/system/opkg") .. + "?query=%s" % net:opkg_package() ) end end @@ -284,6 +284,7 @@ if not net:is_virtual() then --br:depends("proto", "static") --br:depends("proto", "dhcp") --br:depends("proto", "none") + iftype = s:taboption("physical", ListValue, "type", translate("Type of the interface")) iftype:value("", translate("Default")) iftype:value("macvlan", translate("Create a macvlan sub interface")) @@ -301,9 +302,6 @@ if not net:is_virtual() then translate("Enables IGMP snooping on this bridge")) igmp:depends("type", "bridge") igmp.rmempty = true --- macsource = s:taboption("physical", DynamicList, "vlanmacs", translate("Add MACs address to enable source mode")) --- macsource:depends("type", "macvlan") --- macsource.rmempty = true end macvlanmaster = s:taboption("physical", Value, "masterintf", translate("Master interface")) @@ -314,7 +312,7 @@ if not net:is_floating() then ifname_single = s:taboption("physical", Value, "ifname_single", translate("Interface")) ifname_single.template = "cbi/network_ifacelist" ifname_single.widget = "radio" - ifname_single.nobridges = true + ifname_single.nobridges = net:is_bridge() ifname_single.noaliases = false ifname_single.rmempty = false ifname_single.network = arg[1] @@ -371,7 +369,7 @@ end if not net:is_virtual() then ifname_multi = s:taboption("physical", Value, "ifname_multi", translate("Interface")) ifname_multi.template = "cbi/network_ifacelist" - ifname_multi.nobridges = true + ifname_multi.nobridges = net:is_bridge() ifname_multi.noaliases = true ifname_multi.rmempty = false ifname_multi.network = arg[1] diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/network.lua b/luci-mod-network/luasrc/model/cbi/admin_network/network.lua similarity index 69% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/network.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/network.lua index 4fa5ba7b7..e2b3c6198 100644 --- a/luci-mod-admin-full/luasrc/model/cbi/admin_network/network.lua +++ b/luci-mod-network/luasrc/model/cbi/admin_network/network.lua @@ -16,59 +16,6 @@ m:chain("dhcp") m.pageaction = false -local tpl_networks = tpl.Template(nil, [[ -
      -
      - <% - for i, net in ipairs(netlist) do - local z = net[3] - local c = z and z:get_color() or "#EEEEEE" - local t = z and translate("Part of zone %q" % z:name()) or translate("No zone assigned") - local disabled = (net[4]:get("auto") == "0") - local dynamic = net[4]:is_dynamic() - %> -
      -
      -
      -
      - <%=net[1]:upper()%> -
      -
      -
      - ? -
      -
      -
      -
      - <%:Collecting data...%> -
      -
      -
      - /> - - <% if disabled then %> - - /> - <% else %> - - /> - <% end %> - - '" title="<%:Edit this interface%>" value="<%:Edit%>" id="<%=net[1]%>-ifc-edit"<%=ifattr(dynamic, "disabled", "disabled")%> /> - - - /> -
      -
      -
      - <% end %> -
      -
      -
      - '" /> -
      -]]) - local _, net local ifaces, netlist = { }, { } @@ -103,8 +50,10 @@ table.sort(netlist, end) s = m:section(TypedSection, "interface", translate("Interface Overview")) +s.template = "admin_network/iface_overview" +s.netlist = netlist -function s.sections(self) +function s.cfgsections(self) local _, net, sl = nil, nil, { } for _, net in ipairs(netlist) do @@ -114,18 +63,8 @@ function s.sections(self) return sl end -function s.render(self) - tpl_networks:render({ - netlist = netlist - }) -end - o = s:option(Value, "__disable__") -function o.cfgvalue(self, sid) - return (m:get(sid, "auto") == "0") and "1" or "0" -end - function o.write(self, sid, value) if value ~= "1" then m:set(sid, "auto", "") @@ -143,8 +82,6 @@ function o.write(self, sid, value) end -m:section(SimpleSection).template = "admin_network/iface_overview_status" - if fs.access("/etc/init.d/dsl_control") then local ok, boarddata = pcall(json.parse, fs.readfile("/etc/board.json")) local modemtype = (ok == true) @@ -254,12 +191,14 @@ if fs.access("/usr/sbin/br2684ctl") then end local network = require "luci.model.network" -local s = m:section(NamedSection, "globals", "globals", translate("Global network options")) if network:has_ipv6() then + local s = m:section(NamedSection, "globals", "globals", translate("Global network options")) local o = s:option(Value, "ula_prefix", translate("IPv6 ULA-Prefix")) o.datatype = "ip6addr" o.rmempty = true + m.pageaction = true end + if fs.access("/proc/sys/net/mptcp") then local uname = nixio.uname() local mtcp = s:option(ListValue, "multipath", translate("Multipath TCP")) @@ -291,8 +230,7 @@ if fs.access("/proc/sys/net/mptcp") then for cong in string.gmatch(availablecong, "[^%s]+") do congestion:value(cong, translate(cong)) end + m.pageaction = true end -m.pageaction = true - return m diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/routes.lua b/luci-mod-network/luasrc/model/cbi/admin_network/routes.lua similarity index 94% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/routes.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/routes.lua index dc75056c9..1970f36a2 100644 --- a/luci-mod-admin-full/luasrc/model/cbi/admin_network/routes.lua +++ b/luci-mod-network/luasrc/model/cbi/admin_network/routes.lua @@ -43,10 +43,6 @@ mtu.datatype = "range(64,9000)" mtu.size = 5 mtu.rmempty = true -table = s:option(Value, "table", translate("Table")) -table.rmempty = true -table.size = 5 - routetype = s:option(Value, "type", translate("Route type")) routetype:value("", "unicast") routetype:value("local", "local") @@ -89,10 +85,6 @@ if fs.access("/proc/net/ipv6_route") then mtu.size = 5 mtu.rmempty = true - table = s:option(Value, "table", translate("Table")) - table.rmempty = true - table.size = 5 - routetype = s:option(Value, "type", translate("Route type")) routetype:value("", "unicast") routetype:value("local", "local") diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/vlan.lua b/luci-mod-network/luasrc/model/cbi/admin_network/vlan.lua similarity index 93% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/vlan.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/vlan.lua index 3e46628d3..edeb193ef 100644 --- a/luci-mod-admin-full/luasrc/model/cbi/admin_network/vlan.lua +++ b/luci-mod-network/luasrc/model/cbi/admin_network/vlan.lua @@ -17,7 +17,7 @@ local update_interfaces = function(old_ifname, new_ifname) local info = { } m.uci:foreach("network", "interface", function(section) - local old_ifnames = m.uci:get("network", section[".name"], "ifname") + local old_ifnames = section.ifname local new_ifnames = { } local cur_ifname local changed = false @@ -42,6 +42,8 @@ local update_interfaces = function(old_ifname, new_ifname) end end +local vlan_already_created + m.uci:foreach("network", "switch", function(x) local sid = x['.name'] @@ -200,8 +202,29 @@ m.uci:foreach("network", "switch", -- When creating a new vlan, preset it with the highest found vid + 1. s.create = function(self, section, origin) - -- Filter by switch - if m:get(origin, "device") ~= switch_name then + -- VLAN has already been created for another switch + if vlan_already_created then + return + + -- VLAN add button was pressed in an empty VLAN section so only + -- accept the create event if our switch is without existing VLANs + elseif origin == "" then + local is_empty_switch = true + + m.uci:foreach("network", "switch_vlan", + function(s) + if s.device == switch_name then + is_empty_switch = false + return false + end + end) + + if not is_empty_switch then + return + end + + -- VLAN was created for another switch + elseif m:get(origin, "device") ~= switch_name then return end @@ -227,6 +250,8 @@ m.uci:foreach("network", "switch", m:set(sid, has_vlan4k, max_id + 1) end + vlan_already_created = true + return sid end diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/wifi.lua b/luci-mod-network/luasrc/model/cbi/admin_network/wifi.lua similarity index 87% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/wifi.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/wifi.lua index 758e0737c..7b7fc0ffc 100644 --- a/luci-mod-admin-full/luasrc/model/cbi/admin_network/wifi.lua +++ b/luci-mod-network/luasrc/model/cbi/admin_network/wifi.lua @@ -16,7 +16,10 @@ local acct_port, acct_secret, acct_server, anonymous_identity, ant1, ant2, mp, nasid, network, password, pmk_r1_push, privkey, privkey2, privkeypwd, privkeypwd2, r0_key_lifetime, r0kh, r1_key_holder, r1kh, reassociation_deadline, retry_timeout, ssid, st, tp, wepkey, wepslot, - wmm, wpakey, wps, disassoc_low_ack, short_preamble, beacon_int, dtim_period + wmm, wpakey, wps, disassoc_low_ack, short_preamble, beacon_int, dtim_period, + wparekey, inactivitypool, maxinactivity, listeninterval, + dae_client, dae_port, dae_port + arg[1] = arg[1] or "" @@ -35,8 +38,6 @@ nw.init(m.uci) local wnet = nw:get_wifinet(arg[1]) local wdev = wnet and wnet:get_device() -local wifname = m.uci:get("wireless",wnet.sid,"ifname") or "" - -- redirect to overview page if network does not exist anymore (e.g. after a revert) if not wnet or not wdev then luci.http.redirect(luci.dispatcher.build_url("admin/network/wireless")) @@ -363,13 +364,9 @@ s:tab("advanced", translate("Advanced Settings")) mode = s:taboption("general", ListValue, "mode", translate("Mode")) mode.override_values = true -if not wifname:match("^mtkwlan.*") then - mode:value("ap", translate("Access Point")) -end -if not wifname:match("^mtkap.*") then - mode:value("sta", translate("Client")) - mode:value("adhoc", translate("Ad-Hoc")) -end +mode:value("ap", translate("Access Point")) +mode:value("sta", translate("Client")) +mode:value("adhoc", translate("Ad-Hoc")) meshid = s:taboption("general", Value, "mesh_id", translate("Mesh Id")) meshid:depends({mode="mesh"}) @@ -379,6 +376,14 @@ meshfwd.rmempty = false meshfwd.default = "1" meshfwd:depends({mode="mesh"}) +mesh_rssi_th = s:taboption("advanced", Value, "mesh_rssi_threshold", + translate("RSSI threshold for joining"), + translate("0 = not using RSSI threshold, 1 = do not change driver default")) +mesh_rssi_th.rmempty = false +mesh_rssi_th.default = "0" +mesh_rssi_th.datatype = "range(-255,1)" +mesh_rssi_th:depends({mode="mesh"}) + ssid = s:taboption("general", Value, "ssid", translate("ESSID")) ssid.datatype = "maxlength(32)" ssid:depends({mode="ap"}) @@ -391,6 +396,7 @@ ssid:depends({mode="sta-wds"}) ssid:depends({mode="wds"}) bssid = s:taboption("general", Value, "bssid", translate("BSSID")) +bssid.datatype = "macaddr" network = s:taboption("general", Value, "network", translate("Network"), translate("Choose the network(s) you want to attach to this wireless interface or " .. @@ -437,13 +443,12 @@ end -------------------- MAC80211 Interface ---------------------- if hwtype == "mac80211" then - if not wifname:match("^mtkap.*") then - if fs.access("/usr/sbin/iw") then - mode:value("mesh", "802.11s") - end - mode:value("ahdemo", translate("Pseudo Ad-Hoc (ahdemo)")) - mode:value("monitor", translate("Monitor")) + if fs.access("/usr/sbin/iw") then + mode:value("mesh", "802.11s") end + + mode:value("ahdemo", translate("Pseudo Ad-Hoc (ahdemo)")) + mode:value("monitor", translate("Monitor")) bssid:depends({mode="adhoc"}) bssid:depends({mode="sta"}) bssid:depends({mode="sta-wds"}) @@ -461,12 +466,8 @@ if hwtype == "mac80211" then ml:depends({macfilter="deny"}) nt.mac_hints(function(mac, name) ml:value(mac, "%s (%s)" %{ mac, name }) end) - if not wifname:match("^mtkwlan.*") then - mode:value("ap-wds", "%s (%s)" % {translate("Access Point"), translate("WDS")}) - end - if not wifname:match("^mtkap.*") then - mode:value("sta-wds", "%s (%s)" % {translate("Client"), translate("WDS")}) - end + mode:value("ap-wds", "%s (%s)" % {translate("Access Point"), translate("WDS")}) + mode:value("sta-wds", "%s (%s)" % {translate("Client"), translate("WDS")}) function mode.write(self, section, value) if value == "ap-wds" then @@ -508,10 +509,8 @@ if hwtype == "mac80211" then isolate:depends({mode="ap"}) isolate:depends({mode="ap-wds"}) - if not wifname:match("^mtk.*") then - ifname = s:taboption("advanced", Value, "ifname", translate("Interface name"), translate("Override default interface name")) - ifname.optional = true - end + ifname = s:taboption("advanced", Value, "ifname", translate("Interface name"), translate("Override default interface name")) + ifname.optional = true short_preamble = s:taboption("advanced", Flag, "short_preamble", translate("Short Preamble")) short_preamble.default = short_preamble.enabled @@ -520,6 +519,26 @@ if hwtype == "mac80211" then dtim_period.optional = true dtim_period.placeholder = 2 dtim_period.datatype = "range(1,255)" + + + wparekey = s:taboption("advanced", Value, "wpa_group_rekey", translate("Time interval for rekeying GTK"), translate("sec")) + wparekey.optional = true + wparekey.placeholder = 600 + wparekey.datatype = "uinteger" + + inactivitypool = s:taboption("advanced", Flag , "skip_inactivity_poll", translate("Disable Inactivity Polling")) + inactivitypool.optional = true + inactivitypool.datatype = "uinteger" + + maxinactivity = s:taboption("advanced", Value, "max_inactivity", translate("Station inactivity limit"), translate("sec")) + maxinactivity.optional = true + maxinactivity.placeholder = 300 + maxinactivity.datatype = "uinteger" + + listeninterval = s:taboption("advanced", Value, "max_listen_interval", translate("Maximum allowed Listen Interval")) + listeninterval.optional = true + listeninterval.placeholder = 65535 + listeninterval.datatype = "uinteger" disassoc_low_ack = s:taboption("advanced", Flag, "disassoc_low_ack", translate("Disassociate On Low Acknowledgement"), translate("Allow AP mode to disconnect STAs based on low ACK condition")) @@ -655,22 +674,44 @@ if hwtype == "mac80211" or hwtype == "prism2" then local has_ap_eap = (os.execute("hostapd -veap >/dev/null 2>/dev/null") == 0) local has_sta_eap = (os.execute("wpa_supplicant -veap >/dev/null 2>/dev/null") == 0) + -- Probe SAE support + local has_ap_sae = (os.execute("hostapd -vsae >/dev/null 2>/dev/null") == 0) + local has_sta_sae = (os.execute("wpa_supplicant -vsae >/dev/null 2>/dev/null") == 0) + + -- Probe OWE support + local has_ap_owe = (os.execute("hostapd -vowe >/dev/null 2>/dev/null") == 0) + local has_sta_owe = (os.execute("wpa_supplicant -vowe >/dev/null 2>/dev/null") == 0) + if hostapd and supplicant then encr:value("psk", "WPA-PSK", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) encr:value("psk2", "WPA2-PSK", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) encr:value("psk-mixed", "WPA-PSK/WPA2-PSK Mixed Mode", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) + if has_ap_sae and has_sta_sae then + encr:value("sae", "WPA3-SAE", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}, {mode="mesh"}) + encr:value("sae-mixed", "WPA2-PSK/WPA3-SAE Mixed Mode", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) + end if has_ap_eap and has_sta_eap then encr:value("wpa", "WPA-EAP", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}) encr:value("wpa2", "WPA2-EAP", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}) end + if has_ap_owe and has_sta_owe then + encr:value("owe", "OWE", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) + end elseif hostapd and not supplicant then encr:value("psk", "WPA-PSK", {mode="ap"}, {mode="ap-wds"}) encr:value("psk2", "WPA2-PSK", {mode="ap"}, {mode="ap-wds"}) encr:value("psk-mixed", "WPA-PSK/WPA2-PSK Mixed Mode", {mode="ap"}, {mode="ap-wds"}) + if has_ap_sae then + encr:value("sae", "WPA3-SAE", {mode="ap"}, {mode="ap-wds"}) + encr:value("sae-mixed", "WPA2-PSK/WPA3-SAE Mixed Mode", {mode="ap"}, {mode="ap-wds"}) + end if has_ap_eap then encr:value("wpa", "WPA-EAP", {mode="ap"}, {mode="ap-wds"}) encr:value("wpa2", "WPA2-EAP", {mode="ap"}, {mode="ap-wds"}) end + if has_ap_owe then + encr:value("owe", "OWE", {mode="ap"}, {mode="ap-wds"}) + end encr.description = translate( "WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP " .. "and ad-hoc mode) to be installed." @@ -679,10 +720,17 @@ if hwtype == "mac80211" or hwtype == "prism2" then encr:value("psk", "WPA-PSK", {mode="sta"}, {mode="sta-wds"}, {mode="adhoc"}) encr:value("psk2", "WPA2-PSK", {mode="sta"}, {mode="sta-wds"}, {mode="adhoc"}) encr:value("psk-mixed", "WPA-PSK/WPA2-PSK Mixed Mode", {mode="sta"}, {mode="sta-wds"}, {mode="adhoc"}) + if has_sta_sae then + encr:value("sae", "WPA3-SAE", {mode="sta"}, {mode="sta-wds"}, {mode="mesh"}) + encr:value("sae-mixed", "WPA2-PSK/WPA3-SAE Mixed Mode", {mode="sta"}, {mode="sta-wds"}) + end if has_sta_eap then encr:value("wpa", "WPA-EAP", {mode="sta"}, {mode="sta-wds"}) encr:value("wpa2", "WPA2-EAP", {mode="sta"}, {mode="sta-wds"}) end + if has_sta_owe then + encr:value("owe", "OWE", {mode="sta"}, {mode="sta-wds"}) + end encr.description = translate( "WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP " .. "and ad-hoc mode) to be installed." @@ -747,11 +795,37 @@ acct_secret:depends({mode="ap-wds", encryption="wpa2"}) acct_secret.rmempty = true acct_secret.password = true +dae_client = s:taboption("encryption", Value, "dae_client", translate("DAE-Client")) +dae_client:depends({mode="ap", encryption="wpa"}) +dae_client:depends({mode="ap", encryption="wpa2"}) +dae_client:depends({mode="ap-wds", encryption="wpa"}) +dae_client:depends({mode="ap-wds", encryption="wpa2"}) +dae_client.rmempty = true +dae_client.datatype = "host(0)" + +dae_port = s:taboption("encryption", Value, "dae_port", translate("DAE-Port"), translatef("Default %d", 3799)) +dae_port:depends({mode="ap", encryption="wpa"}) +dae_port:depends({mode="ap", encryption="wpa2"}) +dae_port:depends({mode="ap-wds", encryption="wpa"}) +dae_port:depends({mode="ap-wds", encryption="wpa2"}) +dae_port.rmempty = true +dae_port.datatype = "port" + +dae_secret = s:taboption("encryption", Value, "dae_secret", translate("DAE-Secret")) +dae_secret:depends({mode="ap", encryption="wpa"}) +dae_secret:depends({mode="ap", encryption="wpa2"}) +dae_secret:depends({mode="ap-wds", encryption="wpa"}) +dae_secret:depends({mode="ap-wds", encryption="wpa2"}) +dae_secret.rmempty = true +dae_secret.password = true + wpakey = s:taboption("encryption", Value, "_wpa_key", translate("Key")) wpakey:depends("encryption", "psk") wpakey:depends("encryption", "psk2") wpakey:depends("encryption", "psk+psk2") wpakey:depends("encryption", "psk-mixed") +wpakey:depends("encryption", "sae") +wpakey:depends("encryption", "sae-mixed") wpakey.datatype = "wpakey" wpakey.rmempty = true wpakey.password = true @@ -807,7 +881,6 @@ for slot=1,4 do end end - if hwtype == "mac80211" or hwtype == "prism2" then -- Probe 802.11r support (and EAP support as a proxy for Openwrt) @@ -825,9 +898,13 @@ if hwtype == "mac80211" or hwtype == "prism2" then ieee80211r:depends({mode="ap", encryption="psk"}) ieee80211r:depends({mode="ap", encryption="psk2"}) ieee80211r:depends({mode="ap", encryption="psk-mixed"}) + ieee80211r:depends({mode="ap", encryption="sae"}) + ieee80211r:depends({mode="ap", encryption="sae-mixed"}) ieee80211r:depends({mode="ap-wds", encryption="psk"}) ieee80211r:depends({mode="ap-wds", encryption="psk2"}) ieee80211r:depends({mode="ap-wds", encryption="psk-mixed"}) + ieee80211r:depends({mode="ap-wds", encryption="sae"}) + ieee80211r:depends({mode="ap-wds", encryption="sae-mixed"}) end ieee80211r.rmempty = true @@ -865,12 +942,14 @@ if hwtype == "mac80211" or hwtype == "prism2" then ft_psk_generate_local = s:taboption("encryption", Flag, "ft_psk_generate_local", translate("Generate PMK locally"), - translate("When using a PSK, the PMK can be generated locally without inter AP communications")) + translate("When using a PSK, the PMK can be automatically generated. When enabled, the R0/R1 key options below are not applied. Disable this to use the R0 and R1 key options.")) ft_psk_generate_local:depends({ieee80211r="1"}) + ft_psk_generate_local.default = ft_psk_generate_local.enabled + ft_psk_generate_local.rmempty = false r0_key_lifetime = s:taboption("encryption", Value, "r0_key_lifetime", translate("R0 Key Lifetime"), translate("minutes")) - r0_key_lifetime:depends({ieee80211r="1", ft_psk_generate_local=""}) + r0_key_lifetime:depends({ieee80211r="1"}) r0_key_lifetime.placeholder = "10000" r0_key_lifetime.datatype = "uinteger" r0_key_lifetime.rmempty = true @@ -878,13 +957,13 @@ if hwtype == "mac80211" or hwtype == "prism2" then r1_key_holder = s:taboption("encryption", Value, "r1_key_holder", translate("R1 Key Holder"), translate("6-octet identifier as a hex string - no colons")) - r1_key_holder:depends({ieee80211r="1", ft_psk_generate_local=""}) + r1_key_holder:depends({ieee80211r="1"}) r1_key_holder.placeholder = "00004f577274" r1_key_holder.datatype = "and(hexstring,rangelength(12,12))" r1_key_holder.rmempty = true pmk_r1_push = s:taboption("encryption", Flag, "pmk_r1_push", translate("PMK R1 Push")) - pmk_r1_push:depends({ieee80211r="1", ft_psk_generate_local=""}) + pmk_r1_push:depends({ieee80211r="1"}) pmk_r1_push.placeholder = "0" pmk_r1_push.rmempty = true @@ -894,7 +973,7 @@ if hwtype == "mac80211" or hwtype == "prism2" then "
      This list is used to map R0KH-ID (NAS Identifier) to a destination " .. "MAC address when requesting PMK-R1 key from the R0KH that the STA " .. "used during the Initial Mobility Domain Association.")) - r0kh:depends({ieee80211r="1", ft_psk_generate_local=""}) + r0kh:depends({ieee80211r="1"}) r0kh.rmempty = true r1kh = s:taboption("encryption", DynamicList, "r1kh", translate("External R1 Key Holder List"), @@ -903,7 +982,7 @@ if hwtype == "mac80211" or hwtype == "prism2" then "
      This list is used to map R1KH-ID to a destination MAC address " .. "when sending PMK-R1 key from the R0KH. This is also the " .. "list of authorized R1KHs in the MD that can request PMK-R1 keys.")) - r1kh:depends({ieee80211r="1", ft_psk_generate_local=""}) + r1kh:depends({ieee80211r="1"}) r1kh.rmempty = true -- End of 802.11r options @@ -1052,8 +1131,8 @@ if hwtype == "mac80211" then ieee80211w = s:taboption("encryption", ListValue, "ieee80211w", translate("802.11w Management Frame Protection"), translate("Requires the 'full' version of wpad/hostapd " .. - "and support from the wifi driver
      (as of Feb 2017: " .. - "ath9k and ath10k, in LEDE also mwlwifi and mt76)")) + "and support from the wifi driver
      (as of Jan 2019: " .. + "ath9k, ath10k, mwlwifi and mt76)")) ieee80211w.default = "" ieee80211w.rmempty = true ieee80211w:value("", translate("Disabled (default)")) @@ -1063,8 +1142,14 @@ if hwtype == "mac80211" then ieee80211w:depends({mode="ap-wds", encryption="wpa2"}) ieee80211w:depends({mode="ap", encryption="psk2"}) ieee80211w:depends({mode="ap", encryption="psk-mixed"}) + ieee80211w:depends({mode="ap", encryption="sae"}) + ieee80211w:depends({mode="ap", encryption="sae-mixed"}) + ieee80211w:depends({mode="ap", encryption="owe"}) ieee80211w:depends({mode="ap-wds", encryption="psk2"}) ieee80211w:depends({mode="ap-wds", encryption="psk-mixed"}) + ieee80211w:depends({mode="ap-wds", encryption="sae"}) + ieee80211w:depends({mode="ap-wds", encryption="sae-mixed"}) + ieee80211w:depends({mode="ap-wds", encryption="owe"}) max_timeout = s:taboption("encryption", Value, "ieee80211w_max_timeout", translate("802.11w maximum timeout"), @@ -1092,9 +1177,13 @@ if hwtype == "mac80211" then key_retries:depends({mode="ap", encryption="wpa2"}) key_retries:depends({mode="ap", encryption="psk2"}) key_retries:depends({mode="ap", encryption="psk-mixed"}) + key_retries:depends({mode="ap", encryption="sae"}) + key_retries:depends({mode="ap", encryption="sae-mixed"}) key_retries:depends({mode="ap-wds", encryption="wpa2"}) key_retries:depends({mode="ap-wds", encryption="psk2"}) key_retries:depends({mode="ap-wds", encryption="psk-mixed"}) + key_retries:depends({mode="ap-wds", encryption="sae"}) + key_retries:depends({mode="ap-wds", encryption="sae-mixed"}) end if hwtype == "mac80211" or hwtype == "prism2" then diff --git a/luci-mod-admin-full/luasrc/model/cbi/admin_network/wifi_add.lua b/luci-mod-network/luasrc/model/cbi/admin_network/wifi_add.lua similarity index 100% rename from luci-mod-admin-full/luasrc/model/cbi/admin_network/wifi_add.lua rename to luci-mod-network/luasrc/model/cbi/admin_network/wifi_add.lua diff --git a/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua b/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua new file mode 100644 index 000000000..54720d688 --- /dev/null +++ b/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua @@ -0,0 +1,153 @@ +-- Copyright 2018 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local fs = require "nixio.fs" +local utl = require "luci.util" +local tpl = require "luci.template" +local ntm = require "luci.model.network" + +local has_iwinfo = pcall(require, "iwinfo") + +function guess_wifi_hw(dev) + local bands = "" + local ifname = dev:name() + local name, idx = ifname:match("^([a-z]+)(%d+)") + idx = tonumber(idx) + + if has_iwinfo then + local bl = dev.iwinfo.hwmodelist + if bl and next(bl) then + if bl.a then bands = bands .. "a" end + if bl.b then bands = bands .. "b" end + if bl.g then bands = bands .. "g" end + if bl.n then bands = bands .. "n" end + if bl.ac then bands = bands .. "ac" end + end + + local hw = dev.iwinfo.hardware_name + if hw then + return "%s 802.11%s" %{ hw, bands } + end + end + + -- wl.o + if name == "wl" then + local name = translatef("Broadcom 802.11%s Wireless Controller", bands) + local nm = 0 + + local fd = nixio.open("/proc/bus/pci/devices", "r") + if fd then + local ln + for ln in fd:linesource() do + if ln:match("wl$") then + if nm == idx then + local version = ln:match("^%S+%s+%S%S%S%S([0-9a-f]+)") + name = translatef( + "Broadcom BCM%04x 802.11 Wireless Controller", + tonumber(version, 16) + ) + + break + else + nm = nm + 1 + end + end + end + fd:close() + end + + return name + + -- dunno yet + else + return translatef("Generic 802.11%s Wireless Controller", bands) + end +end + + +m = Map("wireless", translate("Wireless Overview")) +m:chain("network") +m.pageaction = false + +if not has_iwinfo then + s = m:section(NamedSection, "__warning__") + + function s.render(self) + tpl.render_string([[ +
      +

      <%:Package libiwinfo required!%>

      +

      <%_The libiwinfo-lua package is not installed. You must install this component for working wireless configuration!%>

      +
      + ]]) + end +end + +local _, dev, net +for _, dev in ipairs(ntm:get_wifidevs()) do + s = m:section(TypedSection) + s.template = "admin_network/wifi_overview" + s.wnets = dev:get_wifinets() + s.dev = dev + s.hw = guess_wifi_hw(dev) + + function s.cfgsections(self) + local _, net, sl = nil, nil, { } + for _, net in ipairs(self.wnets) do + sl[#sl+1] = net:name() + self.wnets[net:name()] = net + end + return sl + end + + o = s:option(Value, "__disable__") + + function o.cfgvalue(self, sid) + local wnet = self.section.wnets[sid] + local wdev = wnet:get_device() + + return ((wnet and wnet:get("disabled") == "1") or + (wdev and wdev:get("disabled") == "1")) and "1" or "0" + end + + function o.write(self, sid, value) + local wnet = self.section.wnets[sid] + local wdev = wnet:get_device() + + if value ~= "1" then + wnet:set("disabled", nil) + wdev:set("disabled", nil) + else + wnet:set("disabled", "1") + end + end + + o.remove = o.write + + + o = s:option(Value, "__delete__") + + function o.write(self, sid, value) + local wnet = self.section.wnets[sid] + local nets = wnet:get_networks() + + ntm:del_wifinet(wnet:id()) + + local _, net + for _, net in ipairs(nets) do + if net:is_empty() then + ntm:del_network(net:name()) + end + end + end +end + +s = m:section(NamedSection, "__assoclist__") + +function s.render(self, sid) + tpl.render_string([[ +

      <%:Associated Stations%>

      + <%+wifi_assoclist%> + ]]) +end + +return m diff --git a/luci-mod-admin-full/luasrc/view/admin_network/diagnostics.htm b/luci-mod-network/luasrc/view/admin_network/diagnostics.htm similarity index 100% rename from luci-mod-admin-full/luasrc/view/admin_network/diagnostics.htm rename to luci-mod-network/luasrc/view/admin_network/diagnostics.htm diff --git a/luci-mod-network/luasrc/view/admin_network/iface_overview.htm b/luci-mod-network/luasrc/view/admin_network/iface_overview.htm new file mode 100644 index 000000000..9d4afd2b2 --- /dev/null +++ b/luci-mod-network/luasrc/view/admin_network/iface_overview.htm @@ -0,0 +1,53 @@ +
      +
      + <% + for i, net in ipairs(self.netlist) do + local z = net[3] + local c = z and z:get_color() or "#EEEEEE" + local t = z and translate("Part of zone %q") % z:name() or translate("No zone assigned") + local disabled = (net[4]:get("auto") == "0") + local dynamic = net[4]:is_dynamic() + %> +
      +
      +
      +
      + <%=net[1]:upper()%> +
      +
      +
      + ? +
      +
      +
      +
      + <%:Collecting data...%> +
      +
      +
      + /> + + <% if disabled then %> + + /> + <% else %> + + /> + <% end %> + + '" title="<%:Edit this interface%>" value="<%:Edit%>" id="<%=net[1]%>-ifc-edit"<%=ifattr(dynamic, "disabled", "disabled")%> /> + + + /> +
      +
      +
      + <% end %> +
      +
      + +
      + '" /> +
      + + diff --git a/luci-mod-network/luasrc/view/admin_network/iface_status.htm b/luci-mod-network/luasrc/view/admin_network/iface_status.htm new file mode 100644 index 000000000..a75b2755c --- /dev/null +++ b/luci-mod-network/luasrc/view/admin_network/iface_status.htm @@ -0,0 +1,12 @@ +<%+cbi/valueheader%> + +> + + + <%:Collecting data...%> + + + + + +<%+cbi/valuefooter%> diff --git a/luci-mod-admin-full/luasrc/view/admin_network/switch_status.htm b/luci-mod-network/luasrc/view/admin_network/switch_status.htm similarity index 94% rename from luci-mod-admin-full/luasrc/view/admin_network/switch_status.htm rename to luci-mod-network/luasrc/view/admin_network/switch_status.htm index 68f0bbc9d..6e741b419 100644 --- a/luci-mod-admin-full/luasrc/view/admin_network/switch_status.htm +++ b/luci-mod-network/luasrc/view/admin_network/switch_status.htm @@ -21,7 +21,7 @@ return status_row; } - XHR.poll(5, '<%=url('admin/network/switch_status')%>/' + switches.join(','), null, + XHR.poll(-1, '<%=url('admin/network/switch_status')%>/' + switches.join(','), null, function(x, st) { for (var i = 0; i < switches.length; i++) diff --git a/luci-mod-network/luasrc/view/admin_network/wifi_join.htm b/luci-mod-network/luasrc/view/admin_network/wifi_join.htm new file mode 100644 index 000000000..5a61ba099 --- /dev/null +++ b/luci-mod-network/luasrc/view/admin_network/wifi_join.htm @@ -0,0 +1,59 @@ +<%# + Copyright 2009-2015 Jo-Philipp Wich + Licensed to the public under the Apache License 2.0. +-%> + +<%- + + local sys = require "luci.sys" + local utl = require "luci.util" + + local dev = luci.http.formvalue("device") + local iw = luci.sys.wifi.getiwinfo(dev) + + if not iw then + luci.http.redirect(luci.dispatcher.build_url("admin/network/wireless")) + return + end +-%> + +<%+header%> + +

      <%:Join Network: Wireless Scan%>

      + +
      +
      +
      > +
      +
      <%:Signal%>
      +
      <%:SSID%>
      +
      <%:Channel%>
      +
      <%:Mode%>
      +
      <%:BSSID%>
      +
      <%:Encryption%>
      +
       
      +
      + +
      +
      + + <%:Collecting data...%> +
      +
      +
      +
      +
      +
      +
      " method="get"> + +
      +
      + + + +
      +
      + + + +<%+footer%> diff --git a/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm b/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm new file mode 100644 index 000000000..89bb404fd --- /dev/null +++ b/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm @@ -0,0 +1,61 @@ +
      +
      + +
      +
      + <%=self.dev:name()%> +
      +
      + <%=self.hw%>
      + +
      +
      +
      + + + +
      +
      +
      + + + + <% if #self.wnets > 0 then %> + <% for i, net in ipairs(self.wnets) do local disabled = (self.dev:get("disabled") == "1" or net:get("disabled") == "1") %> +
      +
      + .png" /> 0% +
      +
      "> + <%= disabled and translate("Wireless is disabled") or translate("Collecting data...") %> +
      +
      +
      + <% if disabled then %> + + + <% else %> + + + <% end %> + + + + + +
      +
      +
      + <% end %> + <% else %> +
      +
      + <%:No network configured on this device%> +
      +
      + <% end %> + +
      +
      + + diff --git a/luci-mod-network/luasrc/view/admin_network/wifi_status.htm b/luci-mod-network/luasrc/view/admin_network/wifi_status.htm new file mode 100644 index 000000000..93ae2f51f --- /dev/null +++ b/luci-mod-network/luasrc/view/admin_network/wifi_status.htm @@ -0,0 +1,14 @@ +<%+cbi/valueheader%> + +> + +   + + + <%:Collecting data...%> + + + + + +<%+cbi/valuefooter%> diff --git a/luci-mod-admin-full/root/etc/init.d/macvlan b/luci-mod-network/root/etc/init.d/macvlan similarity index 100% rename from luci-mod-admin-full/root/etc/init.d/macvlan rename to luci-mod-network/root/etc/init.d/macvlan diff --git a/luci-mod-admin-full/root/etc/uci-defaults/50_luci-mod-admin-full b/luci-mod-network/root/etc/uci-defaults/50_luci-mod-admin-full similarity index 100% rename from luci-mod-admin-full/root/etc/uci-defaults/50_luci-mod-admin-full rename to luci-mod-network/root/etc/uci-defaults/50_luci-mod-admin-full diff --git a/luci-mod-admin-full/root/etc/uci-defaults/51_macvlan b/luci-mod-network/root/etc/uci-defaults/51_macvlan similarity index 100% rename from luci-mod-admin-full/root/etc/uci-defaults/51_macvlan rename to luci-mod-network/root/etc/uci-defaults/51_macvlan diff --git a/luci-theme-openmptcprouter/htdocs/luci-static/openmptcprouter/cascade.css b/luci-theme-openmptcprouter/htdocs/luci-static/openmptcprouter/cascade.css index 9931a8a4d..76ee389da 100644 --- a/luci-theme-openmptcprouter/htdocs/luci-static/openmptcprouter/cascade.css +++ b/luci-theme-openmptcprouter/htdocs/luci-static/openmptcprouter/cascade.css @@ -1,5 +1,8 @@ /*! - * LuCI Bootstrap Theme + * OpenMPTCProuter LuCI Theme + * Copyright 2018-2019 Ycarus (Yannick Chabanois) + * + * Based on LuCI Bootstrap Theme * Copyright 2012 Nut & Bolt * By David Menting * Based on Bootstrap v1.4.0 @@ -13,28 +16,12 @@ /* Reset.less * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc). * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */ -html { - margin: 0; - padding: 0; -} -body { - margin: 0; - padding: 5px; -} - -h1, h2, h3, h4, h5, h6, p, pre, a, abbr, acronym, code, del, em, img, q, s, -small, strike, strong, sub, sup, tt, var, dd, dl, dt, li, ol, ul, fieldset, -form, label, legend, button, table, caption, tbody, tfoot, thead, tr, th, td, -.table, .tbody, .tfoot, .thead, .tr, .th, .td { +* { margin: 0; padding: 0; border: 0; - font-weight: normal; - font-style: normal; - font-size: 100%; - line-height: 1; - font-family: inherit; + box-sizing: border-box; } abbr[title], acronym[title] { @@ -52,15 +39,7 @@ ol, ul { list-style: none; } -q:before, -q:after, -blockquote:before, -blockquote:after { - content: ""; -} - html { - overflow-y: scroll; font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; @@ -74,14 +53,8 @@ a:hover, a:active { outline: 0; } -article, -aside, -details, -figcaption, -figure, footer, header, -hgroup, nav, section { display: block; @@ -103,7 +76,6 @@ sub { } img { - border: 0; -ms-interpolation-mode: bicubic; } @@ -116,12 +88,7 @@ textarea { margin: 0; box-sizing: border-box; vertical-align: baseline; - *vertical-align: middle; -} - -button, input { line-height: normal; - *overflow: visible; } button::-moz-focus-inner, input::-moz-focus-inner { @@ -164,18 +131,17 @@ textarea { * ------------------------------------------------------------------------------------------- */ body { background-color: #fff; - margin: 0; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 18px; color: #404040; - padding-top: 58px; + padding: 58px 5px 5px 5px; } .container { width: 100%; - max-width: 1050px; + max-width: 940px; margin-left: auto; margin-right: auto; zoom: 1; @@ -211,6 +177,10 @@ a:hover { float: left; } +.nowrap { + white-space: nowrap; +} + /* Typography.less * Headings, body text, lists, code, and more for a versatile and durable typography system * ---------------------------------------------------------------------------------------- */ @@ -416,11 +386,6 @@ fieldset legend { line-height: 1; color: #404040; padding-top: 20px; - *padding: 0 0 5px 0px; - /* IE6-7 */ - - *line-height: 1.5; - /* IE6-7 */ } form .cbi-tab-descr { @@ -429,20 +394,20 @@ form .cbi-tab-descr { } form .clearfix, -form .cbi-value { +.cbi-value { margin-bottom: 18px; zoom: 1; } form .clearfix:before, form .clearfix:after, -form .cbi-value:before, form .cbi-value:after { +.cbi-value:before, .cbi-value:after { display: table; content: ""; zoom: 1; } form .clearfix:after, -form .cbi-value:after { +.cbi-value:after { clear: both; } @@ -457,11 +422,11 @@ textarea { } form .input, -form .cbi-value-field { +.cbi-value-field { margin-left: 200px; } -form .cbi-value label.cbi-value-title { +.cbi-value label.cbi-value-title { padding-top: 6px; font-size: 13px; line-height: 18px; @@ -475,6 +440,12 @@ input[type=checkbox], input[type=radio] { cursor: pointer; } +label > input[type="checkbox"], +label > input[type="radio"] { + vertical-align: bottom; + margin: 0; +} + input, textarea, select, @@ -486,18 +457,67 @@ select, padding: 4px; font-size: 13px; line-height: 18px; - color: #808080; border: 1px solid #ccc; border-radius: 3px; - box-sizing: border-box; } -.cbi-dropdown { +.uneditable-input { + color: #808080; +} + +.cbi-dropdown, +.cbi-dynlist { min-width: 210px; max-width: 400px; width: auto; } +.cbi-dynlist { + height: auto; + min-height: 30px; + display: inline-flex; + flex-direction: column; +} + +.cbi-dynlist > .item { + margin-bottom: 4px; + box-shadow: 0 0 2px #ccc; + background: #fff; + padding: 2px 2em 2px 4px; + border: 1px solid #ccc; + border-radius: 3px; + position: relative; + pointer-events: none; +} + +.cbi-dynlist > .item::after { + content: "×"; + position: absolute; + display: inline-flex; + align-items: center; + top: -1px; + right: -1px; + bottom: -1px; + padding: 0 6px; + border: 1px solid #ccc; + border-radius: 0 3px 3px 0; + font-weight: bold; + color: #c44; + pointer-events: auto; +} + +.cbi-dynlist > .add-item { + display: flex; +} + +.cbi-dynlist > .add-item > input, +.cbi-dynlist > .add-item > button { + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + select { padding: initial; background: #fff; @@ -530,25 +550,20 @@ input[type=button], input[type=reset], input[type=submit] { height: auto; } -select, input[type=file] { - *height: auto; - *margin-top: 4px; - /* For IE7, add top margin to align select with labels */ -} - select[multiple] { height: inherit; background-color: #fff; } textarea { - height: auto; + height: auto !important; } .td > input[type=text], .td > input[type=password], .td > select, -.td > .cbi-dropdown { +.td > .cbi-dropdown, +.cbi-dynlist > .add-item > .cbi-dropdown { width: 100%; } @@ -568,11 +583,12 @@ textarea { color: #bfbfbf; } -.btn, .cbi-button, input, textarea { +.item::after, .btn, .cbi-button, input, textarea { transition: border linear 0.2s, box-shadow linear 0.2s; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); } +.item:hover::after, .btn:hover, .cbi-button:hover, input:focus, textarea:focus { outline: 0; @@ -724,39 +740,6 @@ textarea[readonly] { line-height: 3em; } -/* - * Table for compatibility with old packages - * ---------------------------------------- */ -table { - width: 100%; - margin-bottom: 18px; - padding: 0; - font-size: 13px; - border-collapse: collapse; -} - -table th, table td { - padding: 10px 10px 9px; - line-height: 18px; - text-align: left; -} - -table th { - padding-top: 9px; - font-weight: bold; - vertical-align: middle; -} - -table td { - vertical-align: top; - border-top: 1px solid #ddd; -} - -table tbody th { - border-top: 1px solid #ddd; - vertical-align: top; -} - /* Patterns.less * Repeatable UI elements outside the base styles provided from the scaffolding * ---------------------------------------------------------------------------- */ @@ -766,7 +749,7 @@ header { top: 0; left: 0; right: 0; - z-index: 10000; + z-index: 800; overflow: visible; color: #BFBFBF; } @@ -958,7 +941,7 @@ a.menu:after, .dropdown-toggle:after { background-color: #fff; float: left; position: absolute; - top: 45px; + top: 40px; left: -9999px; z-index: 900; min-width: 160px; @@ -1043,44 +1026,61 @@ header .dropdown-menu a.hover, } .tabs, .cbi-tabmenu { - margin: 0 0 18px; - padding: 0; + margin: 0 -5px 18px; + padding: 0 2px; list-style: none; - zoom: 1; -} - -.tabs:before, -.cbi-tabmenu:before, -.tabs:after, -.cbi-tabmenu:after { - display: table; - content: ""; - zoom: 1; -} - -.tabs:after, .cbi-tabmenu:after { - clear: both; + display: flex; + flex-wrap: wrap; + background: linear-gradient(#fff 28px, #ddd 28px); + background-size: 1px 29px; + background-position: left bottom; } .tabs > li, .cbi-tabmenu > li { - float: left; + flex: 0 1 auto; + display: flex; + align-items: center; + height: 25px; + max-width: 48%; + margin: 4px 2px 0 2px; + background: #fff; + border: 1px solid #ddd; + border-bottom: none; + border-radius: 4px 4px 0 0; + color: #0069d6; } .tabs > li > a, .cbi-tabmenu > li > a { - display: block; + padding: 4px 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: inherit; + text-decoration: none; + border-radius: 4px 4px 0 0; + line-height: 25px; + outline: none; } -.tabs, -.cbi-tabmenu { - border-color: #ddd; - border-style: solid; - border-width: 0 0 1px; +.tabs > li:not(.active):hover, .cbi-tabmenu > .cbi-tab-disabled:hover { + background: linear-gradient(#fff 90%, #ddd 100%); } -.tabs > li, -.cbi-tabmenu > li { - position: relative; - margin-bottom: -1px; +.tabs > li:not(.active), .cbi-tabmenu > .cbi-tab-disabled { + color: #999; + background: linear-gradient(#eee 90%, #ddd 100%); +} + +.cbi-tab-disabled[data-errors]::after { + content: attr(data-errors); + background: #c43c35; + color: #fff; + min-width: 12px; + line-height: 14px; + border-radius: 7px; + text-align: center; + margin: 0 5px 0 0; + padding: 1px 2px; } .cbi-tabmenu.map { @@ -1096,53 +1096,23 @@ header .dropdown-menu a.hover, display: none; } -.tabs > li > a, -.cbi-tabmenu > li > a { - padding: 0 15px; - margin-right: 2px; - line-height: 34px; - border: 1px solid transparent; - border-radius: 4px 4px 0 0; -} - -.tabs > li > a:hover, -.cbi-tabmenu > li > a:hover { - text-decoration: none; - background-color: #eee; - border-color: #eee #eee #ddd; -} - -.tabs .active > a, .tabs .active > a:hover, -.cbi-tabmenu .active > a, .cbi-tabmenu .active > a:hover, -.cbi-tab > a:link, .cbi-tab > a:hover { - color: #808080; - background-color: #fff; - border: 1px solid #ddd; - border-bottom-color: transparent; - cursor: default; -} - -.tabs .menu-dropdown, .tabs .dropdown-menu, -.cbi-tabmenu .menu-dropdown, .cbi-tabmenu .dropdown-menu { +.tabs .menu-dropdown, .tabs .dropdown-menu { top: 35px; border-width: 1px; border-radius: 0 6px 6px 6px; } -.tabs a.menu:after, .tabs .dropdown-toggle:after, -.cbi-tabmenu a.menu:after, .cbi-tabmenu .dropdown-toggle:after { +.tabs a.menu:after, .tabs .dropdown-toggle:after { border-top-color: #999; margin-top: 15px; margin-left: 5px; } -.tabs li.open.menu .menu, .tabs .open.dropdown .dropdown-toggle, -.cbi-tabmenu li.open.menu .menu, .cbi-tabmenu .open.dropdown .dropdown-toggle { +.tabs li.open.menu .menu, .tabs .open.dropdown .dropdown-toggle { border-color: #999; } -.tabs li.open a.menu:after, .tabs .dropdown.open .dropdown-toggle:after, -.cbi-tabmenu li.open a.menu:after, .cbi-tabmenu .dropdown.open .dropdown-toggle:after { +.tabs li.open a.menu:after, .tabs .dropdown.open .dropdown-toggle:after { border-top-color: #555; } @@ -1186,6 +1156,61 @@ footer { border-top: 1px solid #eee; } +#modal_overlay { + position: fixed; + top: 0; + bottom: 0; + left: -10000px; + right: 10000px; + background: rgba(0, 0, 0, 0.7); + z-index: 900; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; + transition: opacity .125s ease-in; + opacity: 0; + visibility: hidden; +} + +.modal { + width: 90%; + margin: 5em auto; + display: flex; + flex-wrap: wrap; + min-height: 32px; + max-width: 600px; + align-items: center; + border-radius: 3px; + background: #fff; + box-shadow: 0 0 3px #444; + padding: 1em 1em .5em 1em; + max-height: 2400px; + min-width: 270px; +} + +.modal > * { + flex-basis: 100%; + line-height: normal; + margin-bottom: .5em; +} + +.modal > pre, +.modal > textarea { + white-space: pre-wrap; + overflow: auto; +} + +body.modal-overlay-active { + overflow: hidden; + height: 100vh; +} + +body.modal-overlay-active #modal_overlay { + left: 0; + right: 0; + opacity: 1; + visibility: visible; +} + .btn.danger, .alert-message.danger, .btn.danger:hover, @@ -1201,7 +1226,8 @@ footer { .btn.info, .alert-message.info, .btn.info:hover, -.alert-message.info:hover { +.alert-message.info:hover, +.cbi-tooltip.error, .cbi-tooltip.success, .cbi-tooltip.info { color: #fff; } @@ -1213,30 +1239,32 @@ footer { .btn.danger, .alert-message.danger, .btn.error, -.alert-message.error { +.alert-message.error, +.cbi-tooltip.error { background: linear-gradient(to bottom, #ee5f5b, #c43c35) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } -.btn.success, .alert-message.success { +.btn.success, .alert-message.success, .cbi-tooltip.success { background: linear-gradient(to bottom, #62c462, #57a957) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } -.btn.info, .alert-message.info { +.btn.info, .alert-message.info, .cbi-tooltip.info { background: linear-gradient(to bottom, #5bc0de, #339bb9) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } -.alert-message.notice { +.alert-message.notice, .cbi-tooltip.notice { background: linear-gradient(to bottom, #efefef, #fefefe) repeat-x; text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } +.item::after, .btn, .cbi-button { cursor: pointer; @@ -1259,6 +1287,8 @@ footer { } .cbi-input-invalid, +.cbi-input-invalid.cbi-dropdown, +.cbi-input-invalid.cbi-dropdown:not([open]) > ul > li, .cbi-value-error input { color: #f00; border-color: #f00; @@ -1349,6 +1379,7 @@ footer { color: #404040; } +.cbi-dynlist > .item:focus, .cbi-dropdown:focus { outline: 2px solid #4b6e9b; } @@ -1385,6 +1416,7 @@ footer { font-weight: bold; text-shadow: 1px 1px 0px #fff; display: none; + justify-content: center; } .cbi-dropdown > ul > li { @@ -1442,10 +1474,11 @@ footer { border: 1px solid #918e8c; box-shadow: 0 0 4px #918e8c; position: absolute; - z-index: 1000; + z-index: 1100; max-width: none; min-width: 100%; width: auto; + transition: max-height .125s ease-in; } .cbi-dropdown > ul > li[display], @@ -1485,6 +1518,14 @@ footer { border-bottom: none; } +.cbi-dropdown[open] > ul.dropdown > li[unselectable] { + opacity: 0.7; +} + +.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { + width: 100%; +} + .cbi-dropdown[disabled] { pointer-events: none; opacity: .6; @@ -1495,12 +1536,11 @@ input[type="password"] + .cbi-button, select + .cbi-button { border-radius: 0 3px 3px 0; border-color: #ccc; - margin: 0 0 1px -2px; + margin-left: -2px; padding: 0 6px; vertical-align: top; - height: 28px; + height: 30px; font-size: 14px; - font-weight: bold; line-height: 28px; } @@ -1524,8 +1564,13 @@ select + .cbi-button { position: absolute; z-index: 1000; left: -1000px; + box-shadow: 0 0 2px #ccc; + border-radius: 3px; + background: #fff; + white-space: pre; + padding: 2px 5px; opacity: 0; - transition: opacity .25s ease-out; + transition: opacity .25s ease-in; } .cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { @@ -1534,6 +1579,37 @@ select + .cbi-button { transition: opacity .25s ease-in; } +.cbi-progressbar { + border: 1px solid #ccc; + border-radius: 3px; + position: relative; + min-width: 170px; + height: 20px; + margin: 4px 0; + background: #f9f9f9; +} + +.cbi-progressbar > div { + background: #90c0e0; + height: 100%; + transition: width .25s ease-in; + width: 0%; +} + +.cbi-progressbar::after { + position: absolute; + bottom: 0; + top: 0; + right: 0; + left: 0; + text-align: center; + text-shadow: 0 0 2px #fff; + content: attr(title); + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; +} + .zonebadge .cbi-tooltip { padding: 1px; background: inherit; @@ -1705,19 +1781,6 @@ a.label:hover { /* LuCI specific items */ .hidden { display: none } -#memtotal > div, -#memfree > div, -#memcache > div, -#membuff > div, -#conns > div { - border: 1px solid #ccc; - border-radius: 3px 3px 3px 3px; - color: #808080; - display: inline-block; - font-size: 13px; - line-height: 18px; -} - #xhr_poll_status { cursor: pointer; } @@ -1828,7 +1891,7 @@ table table td, background-position: .25em .2em; background-repeat: no-repeat; margin: .25em 0 0 0; - padding: 0 0 0.3em 1.7em; + padding: 0 0 0 1.7em; } .cbi-section-error { @@ -1933,7 +1996,7 @@ table table td, margin: -.125em; } -#dsl_status_table .ifacebox-body > span > strong { +#dsl_status_table .ifacebox-body span > strong { display: inline-block; min-width: 35%; } @@ -1989,47 +2052,51 @@ div.cbi-value var, } .uci-change-list { + line-height: 170%; + white-space: pre; +} + +.uci-change-list del, +.uci-change-list ins, +.uci-change-list var, +.uci-change-legend-label del, +.uci-change-legend-label ins, +.uci-change-legend-label var { + text-decoration: none; font-family: monospace; + font-style: normal; + border: 1px solid #ccc; + background: #eee; + padding: 2px; + display: block; + line-height: 15px; + margin-bottom: 1px; } .uci-change-list ins, .uci-change-legend-label ins { - text-decoration: none; - border: 1px solid #0f0; - background-color: #cfc; - display: block; - padding: 2px; + border-color: #0f0; + background: #cfc; } .uci-change-list del, .uci-change-legend-label del { - text-decoration: none; - border: 1px solid #f00; - background-color: #fcc; - display: block; - font-style: normal; - padding: 2px; + border-color: #f00; + background: #fcc; } .uci-change-list var, .uci-change-legend-label var { - text-decoration: none; - border: 1px solid #ccc; - background-color: #eee; - display: block; - font-style: normal; - padding: 2px; - line-height: 19px; - white-space: pre; + border-color: #ccc; + background: #eee; } .uci-change-list var ins, .uci-change-list var del { - display: inline; - /*border: none;*/ - white-space: pre; - font-style: normal; - padding: 0px; + display: inline-block; + border: none; + width: 100%; + padding: 0; } .uci-change-legend { @@ -2049,10 +2116,123 @@ div.cbi-value var, width: 10px; height: 10px; display: block; + position: relative; } .uci-change-legend-label var ins, .uci-change-legend-label var del { - line-height: 6px; border: none; + position: absolute; + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; } + +#modal_overlay { + position: fixed; + top: 0; + bottom: 0; + left: -10000px; + right: 10000px; + background: rgba(0, 0, 0, 0.7); + z-index: 900; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; + transition: opacity .125s ease-in; + opacity: 0; +} + +#modal_overlay > .modal { + width: 90%; + margin: 5em auto; + display: flex; + flex-wrap: wrap; + min-height: 32px; + max-width: 600px; + align-items: center; + border-radius: 3px; + background: #fff; + box-shadow: 0 0 3px #444; + padding: 1em 1em .5em 1em; + max-height: 2400px; + min-width: 270px; +} + +#modal_overlay .modal > * { + flex-basis: 100%; + line-height: normal; + margin-bottom: .5em; +} + +#modal_overlay .modal > pre, +#modal_overlay .modal > textarea { + white-space: pre-wrap; + overflow: auto; +} + +body.modal-overlay-active { + overflow: hidden; + height: 100vh; +} + +body.modal-overlay-active #modal_overlay { + left: 0; + right: 0; + opacity: 1; +} + +html body.apply-overlay-active { + height: calc(100vh - 63px); +} + +#applyreboot-section { + line-height: 300%; +} + +[data-page="admin-network-dhcp"] [data-name="ip"] { + width: 15%; +} + +@keyframes flash { + 0% { opacity: 1; } + 50% { opacity: .5; } + 100% { opacity: 1; } +} + +.flash { + animation: flash .35s; +} + +.spinning { + position: relative; + padding-left: 32px !important; +} + +.spinning::before { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 32px; + content: " "; + background: url(../resources/icons/loading.gif) no-repeat center; + background-size: 16px; +} + +[data-tab-title] { + height: 0; + opacity: 0; + overflow: hidden; +} + +[data-tab-active="true"] { + opacity: 1; + height: auto; + overflow: visible; + transition: opacity .25s ease-in; +} + +#cbi-dscp .cbi-dropdown { + min-width: 70px; +} \ No newline at end of file diff --git a/luci-theme-openmptcprouter/htdocs/luci-static/openmptcprouter/mobile.css b/luci-theme-openmptcprouter/htdocs/luci-static/openmptcprouter/mobile.css index b74f20904..062d274b7 100644 --- a/luci-theme-openmptcprouter/htdocs/luci-static/openmptcprouter/mobile.css +++ b/luci-theme-openmptcprouter/htdocs/luci-static/openmptcprouter/mobile.css @@ -6,11 +6,367 @@ header h3 a, header .brand { #maincontent.container { margin-top: 30px; } + + .tabs, .cbi-tabmenu { + background: linear-gradient(#fff 20%, #ddd 100%); + background-size: 1px 34px; + margin-bottom: 10px; + } + + .tabs > li, .cbi-tabmenu > li { + height: 30px; + } + + .tabs > li > a, .cbi-tabmenu > li > a { + padding: 0 8px; + line-height: 30px; + } + + .table { + display: flex; + flex-direction: column; + width: 100%; + } + + .tr { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-end; + border-top: 1px solid #ddd; + padding: 5px 0; + margin: 0 -3px; + } + + .table .th, + .table .td, + .table .tr::before { + flex: 2 2 33%; + align-self: flex-start; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + display: inline-block; + border-top: none; + padding: 3px; + box-sizing: border-box; + } + + .table .td.cbi-dropdown-open { + overflow: visible; + } + + .col-1 { flex: 1 1 30px !important; -webkit-flex: 1 1 30px !important; } + .col-2 { flex: 2 2 60px !important; -webkit-flex: 2 2 60px !important; } + .col-3 { flex: 3 3 90px !important; -webkit-flex: 3 3 90px !important; } + .col-4 { flex: 4 4 120px !important; -webkit-flex: 4 4 120px !important; } + .col-5 { flex: 5 5 150px !important; -webkit-flex: 5 5 150px !important; } + .col-6 { flex: 6 6 180px !important; -webkit-flex: 6 6 180px !important; } + .col-7 { flex: 7 7 210px !important; -webkit-flex: 7 7 210px !important; } + .col-8 { flex: 8 8 240px !important; -webkit-flex: 8 8 240px !important; } + .col-9 { flex: 9 9 270px !important; -webkit-flex: 9 9 270px !important; } + .col-10 { flex: 10 10 300px !important; -webkit-flex: 10 10 300px !important; } + + .td select { + word-wrap: normal; + } + + .td[data-type="button"], + .td[data-type="fvalue"] { + flex: 1 1 17%; + text-align: left; + } + + .td.cbi-value-field { + align-self: flex-start; + } + + .td.cbi-value-field .cbi-button { + width: 100%; + } + + .table.cbi-section-table { + border: none; + background: none; + margin: 0; + } + + .tr.table-titles, + .cbi-section-table-titles, + .cbi-section-table-descr { + display: none; + } + + .cbi-section-table-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin: 0 0 .5em 0; + } + + .cbi-section-table + .cbi-section-create { + padding-top: 0; + } + + .tr[data-title]::before { + display: block; + flex: 1 1 100%; + background: #f5f5f5 !important; + font-size: 16px; + border-bottom: 1px solid #ddd; + } + + .td[data-title]::before, + .td[data-description]::after { + display: block; + } + + .td[data-title] ~ .td.cbi-section-actions { + align-self: flex-start; + } + + .td[data-title] ~ .td.cbi-section-actions::before { + display: block; + content: "\a0"; + } + + .td.cbi-section-actions { + overflow: initial; + max-width: 100%; + padding: 3px 2px; + } + + .hide-sm, + .hide-xs { + display: none !important; + } + + .td.cbi-value-field { + flex-basis: 100%; + } + + .td.cbi-value-field[data-type="dvalue"] { + flex-basis: 50%; + } + + .td.cbi-value-field[data-type="button"], + .td.cbi-value-field[data-type="fvalue"] { + flex-basis: 25%; + text-align: left; + } + + .cbi-section-table .tr:hover .td, + .cbi-section-table .tr:hover .th, + .cbi-section-table .tr:hover::before { + background-color: transparent; + } + + .cbi-value { + padding-bottom: .5em; + border-bottom: 1px solid #ddd; + margin-bottom: .5em; + } + + .cbi-value label.cbi-value-title { + float: none; + font-weight: bold; + } + + .cbi-value-field, .cbi-dropdown { + width: 100%; + margin: 0; + } + + input, textarea, select { + font-size: 16px !important; + line-height: 28px; + } + + select, input[type="text"], input[type="password"] { + width: 100%; + height: 30px; + } + + input.cbi-input-password { + width: calc(100% - 25px); + } + + [data-dynlist] { + display: block; + } + + [data-dynlist] > .add-item > input { + width: calc(100% - 21px); + } + + [data-dynlist] > .add-item > .cbi-button { + margin-right: -1px; + } + + input[type="text"] + .cbi-button, + input[type="password"] + .cbi-button, + select + .cbi-button { + font-size: 14px !important; + line-height: 28px; + height: 30px; + box-sizing: border-box; + overflow: hidden; + text-overflow: ellipsis; + } + + .cbi-value-field input[type="checkbox"], + .cbi-value-field input[type="radio"] { + margin: 0; + } + + .btn, .cbi-button { + font-size: 14px !important; + padding: 4px 8px; + } + + .actions, + .cbi-page-actions { + border-top: none; + margin-top: -.5em; + padding: 8px; + } + + [data-page="admin-status-overview"] .cbi-section:nth-of-type(1) .td:first-child, + [data-page="admin-status-overview"] .cbi-section:nth-of-type(2) .td:first-child { + flex-grow: 1; + } + + header .pull-right .label { + white-space: normal; + display: inline-block; + text-align: center; + line-height: 12px; + margin: 1px 0; + } + + header > .fill { + padding: 1px; + } + + header > .fill > .container { + display: flex; + flex-direction: row; + } + + header .nav { + flex: 3 3 80%; + margin: 2px 5px 2px 0; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + } + + header .nav a { + padding: 2px 6px; + } + + header .pull-right { + flex: 1 1 20%; + display: flex; + flex-direction: column; + padding: 0; + justify-content: space-around; + } + + .menu-dropdown, .dropdown-menu { + top: 23px; + } + + body { + padding-top: 30px; + } + + .cbi-optionals, + .cbi-section-create { + padding: 0 0 14px 0; + } + + #cbi-network-switch_vlan .th, + #cbi-network-switch_vlan .td { + flex-basis: 12%; + } + + #cbi-network-switch_vlan .td.cbi-section-actions { + flex-basis: 100%; + } + + #cbi-network-switch_vlan .td.cbi-section-actions::before { + display: none; + } + + #cbi-network-switch_vlan .td.cbi-section-actions > * { + width: auto; + display: block; + } + + #wifi_assoclist_table .td, + [data-page="admin-status-processes"] .td { + flex-basis: 50% !important; + } + + [data-page="admin-status-processes"] .td[data-type="button"] { + flex-basis: 33% !important; + } + + [data-page="admin-status-processes"] .td[data-name="PID"], + [data-page="admin-status-processes"] .td[data-name="USER"] { + flex-basis: 25% !important; + } + + [data-page="admin-system-fstab"] .td[data-type="button"]::before, + [data-page="admin-system-startup"] .td[data-type="button"]::before, + [data-page="admin-status-processes"] .td[data-type="button"]::before { + display: none; + } } -@media screen and (max-device-width: 360px) { +@media screen and (max-device-width: 375px) { #maincontent.container { - margin-top: 60px; + margin-top: 55px; + } + + .cbi-page-actions { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin: 0 -1px; + padding: 0; + } + + .cbi-page-actions .cbi-button { + flex: 1 1 calc(50% - 2px); + margin: 1px !important; + overflow: hidden; + text-overflow: ellipsis; + } + + .cbi-page-actions .cbi-button-primary, + .cbi-page-actions .cbi-button-apply { + flex-basis: calc(100% - -2px); + } + + .cbi-section-actions .cbi-button { + overflow: hidden; + text-overflow: ellipsis; + } + + body[data-page="admin-network-wireless"] .td.col-2 { + max-width: 50px; + } + + body[data-page="admin-network-wireless"] .td.col-2 > .ifacebadge { + display: flex; + align-items: center; + flex-direction: column; + } + + body[data-page="admin-network-network"] .td.col-3 { + min-width: 250px; } } @@ -18,4 +374,29 @@ header h3 a, header .brand { #maincontent.container { margin-top: 230px; } -} \ No newline at end of file +} + +@media screen and (max-width: 375px) { + .td .ifacebox { + width: 100%; + margin: 0 !important; + flex-direction: row; + } + + .td .ifacebox .ifacebox-head { + min-width: 25%; + justify-content: space-around; + } + + .td .ifacebox .ifacebox-head, + .td .ifacebox .ifacebox-body { + display: flex; + border-bottom: none; + align-items: center; + } + + .td .ifacebox .ifacebox-head > *, + .ifacebox .ifacebox-body > * { + margin: .125em; + } +} diff --git a/luci-theme-openmptcprouter/luasrc/view/themes/openmptcprouter/footer.htm b/luci-theme-openmptcprouter/luasrc/view/themes/openmptcprouter/footer.htm index e0a41e1bc..773522fb1 100644 --- a/luci-theme-openmptcprouter/luasrc/view/themes/openmptcprouter/footer.htm +++ b/luci-theme-openmptcprouter/luasrc/view/themes/openmptcprouter/footer.htm @@ -14,7 +14,7 @@ local categories = disp.node_childs(tree) %>