1
0
Fork 0
mirror of https://github.com/Ysurac/openmptcprouter-feeds.git synced 2025-02-14 11:31:51 +00:00

Update to latest LuCi changes

This commit is contained in:
Ycarus (Yannick Chabanois) 2019-07-12 18:27:38 +02:00
parent 976a467d5f
commit f139a9c784
75 changed files with 22413 additions and 14077 deletions

View file

@ -14,17 +14,13 @@ 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_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_SOURCE:=v1.0.0.tar.gz
PKG_SOURCE_URL:=https://github.com/jirutka/luasrcdiet/archive/
PKG_HASH:=48162e63e77d009f5848f18a5cabffbdfc867d0e5e73c6d407f6af5d6880151b
PKG_LICENSE:=MIT
HOST_BUILD_DIR:=$(BUILD_DIR_HOST)/$(PKG_SOURCE_SUBDIR)
HOST_BUILD_DIR:=$(BUILD_DIR_HOST)/luasrcdiet-1.0.0
include $(INCLUDE_DIR)/host-build.mk

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,560 @@
'use strict';
'require uci';
'require rpc';
'require tools.prng as random';
function initFirewallState() {
return uci.load('firewall');
}
function parseEnum(s, values) {
if (s == null)
return null;
s = String(s).toUpperCase();
if (s == '')
return null;
for (var i = 0; i < values.length; i++)
if (values[i].toUpperCase().indexOf(s) == 0)
return values[i];
return null;
}
function parsePolicy(s, defaultValue) {
return parseEnum(s, ['DROP', 'REJECT', 'ACCEPT']) || (arguments.length < 2 ? null : defaultValue);
}
var Firewall, AbstractFirewallItem, Defaults, Zone, Forwarding, Redirect, Rule;
function lookupZone(name) {
var z = uci.get('firewall', name);
if (z != null && z['.type'] == 'zone')
return new Zone(z['.name']);
var sections = uci.sections('firewall', 'zone');
for (var i = 0; i < sections.length; i++) {
if (sections[i].name != name)
continue;
return new Zone(sections[i]['.name']);
}
return null;
}
function getColorForName(forName) {
if (forName == null)
return '#eeeeee';
else if (forName == 'lan')
return '#90f090';
else if (forName == 'wan')
return '#f09090';
random.seed(parseInt(sfh(forName), 16));
var r = random.get(128),
g = random.get(128),
min = 0,
max = 128;
if ((r + g) < 128)
min = 128 - r - g;
else
max = 255 - r - g;
var b = min + Math.floor(random.get() * (max - min));
return '#%02x%02x%02x'.format(0xff - r, 0xff - g, 0xff - b);
}
Firewall = L.Class.extend({
getDefaults: function() {
return initFirewallState().then(function() {
return new Defaults();
});
},
newZone: function() {
return initFirewallState().then(L.bind(function() {
var name = 'newzone',
count = 1;
while (this.getZone(name) != null)
name = 'newzone%d'.format(++count);
return this.addZone(name);
}, this));
},
addZone: function(name) {
return initFirewallState().then(L.bind(function() {
if (name == null || !/^[a-zA-Z0-9_]+$/.test(name))
return null;
if (this.getZone(name) != null)
return null;
var d = new Defaults(),
z = uci.add('firewall', 'zone');
uci.set('firewall', z, 'name', name);
uci.set('firewall', z, 'network', ' ');
uci.set('firewall', z, 'input', d.getInput() || 'DROP');
uci.set('firewall', z, 'output', d.getOutput() || 'DROP');
uci.set('firewall', z, 'forward', d.getForward() || 'DROP');
return new Zone(z);
}, this));
},
getZone: function(name) {
return initFirewallState().then(function() {
return lookupZone(name);
});
},
getZones: function() {
return initFirewallState().then(function() {
var sections = uci.sections('firewall', 'zone'),
zones = [];
for (var i = 0; i < sections.length; i++)
zones.push(new Zone(sections[i]['.name']));
zones.sort(function(a, b) { return a.getName() > b.getName() });
return zones;
});
},
getZoneByNetwork: function(network) {
return initFirewallState().then(function() {
var sections = uci.sections('firewall', 'zone');
for (var i = 0; i < sections.length; i++)
if (L.toArray(sections[i].network || sections[i].name).indexOf(network) != -1)
return new Zone(sections[i]['.name']);
return null;
});
},
deleteZone: function(name) {
return initFirewallState().then(function() {
var section = uci.get('firewall', name),
found = false;
if (section != null && section['.type'] == 'zone') {
found = true;
name = zone.name;
uci.remove('firewall', zone['.name']);
}
else if (name != null) {
var sections = uci.sections('firewall', 'zone');
for (var i = 0; i < sections.length; i++) {
if (sections[i].name != name)
continue;
found = true;
uci.remove('firewall', sections[i]['.name']);
}
}
if (found == true) {
sections = uci.sections('firewall');
for (var i = 0; i < sections.length; i++) {
if (sections[i]['.type'] != 'rule' &&
sections[i]['.type'] != 'redirect' &&
sections[i]['.type'] != 'forwarding')
continue;
if (sections[i].src == name || sections[i].dest == name)
uci.remove('firewall', sections[i]['.name']);
}
}
return found;
});
},
renameZone: function(oldName, newName) {
return initFirewallState().then(L.bind(function() {
if (oldName == null || newName == null || !/^[a-zA-Z0-9_]+$/.test(newName))
return false;
if (lookupZone(newName) != null)
return false;
var sections = uci.sections('firewall', 'zone'),
found = false;
for (var i = 0; i < sections.length; i++) {
if (sections[i].name != oldName)
continue;
if (L.toArray(sections[i].network).length == 0)
uci.set('firewall', sections[i]['.name'], 'network', oldName);
uci.set('firewall', sections[i]['.name'], 'name', newName);
found = true;
}
if (found == true) {
sections = uci.sections('firewall');
for (var i = 0; i < sections.length; i++) {
if (sections[i]['.type'] != 'rule' &&
sections[i]['.type'] != 'redirect' &&
sections[i]['.type'] != 'forwarding')
continue;
if (sections[i].src == oldName)
uci.set('firewall', sections[i]['.name'], 'src', newName);
if (sections[i].dest == oldName)
uci.set('firewall', sections[i]['.name'], 'dest', newName);
}
}
return found;
}, this));
},
deleteNetwork: function(network) {
return this.getZones().then(L.bind(function(zones) {
var rv = false;
for (var i = 0; i < zones.length; i++)
if (zones[i].deleteNetwork(network))
rv = true;
return rv;
}, this));
},
getColorForName: getColorForName
});
AbstractFirewallItem = L.Class.extend({
get: function(option) {
return uci.get('firewall', this.sid, option);
},
set: function(option, value) {
return uci.set('firewall', this.sid, option, value);
}
});
Defaults = AbstractFirewallItem.extend({
__init__: function() {
var sections = uci.sections('firewall', 'defaults');
for (var i = 0; i < sections.length; i++) {
this.sid = sections[i]['.name'];
break;
}
if (this.sid == null)
this.sid = uci.add('firewall', 'defaults');
},
isSynFlood: function() {
return (this.get('syn_flood') == '1');
},
isDropInvalid: function() {
return (this.get('drop_invalid') == '1');
},
getInput: function() {
return parsePolicy(this.get('input'), 'DROP');
},
getOutput: function() {
return parsePolicy(this.get('output'), 'DROP');
},
getForward: function() {
return parsePolicy(this.get('forward'), 'DROP');
}
});
Zone = AbstractFirewallItem.extend({
__init__: function(name) {
var section = uci.get('firewall', name);
if (section != null && section['.type'] == 'zone') {
this.sid = name;
this.data = section;
}
else if (name != null) {
var sections = uci.get('firewall', 'zone');
for (var i = 0; i < sections.length; i++) {
if (sections[i].name != name)
continue;
this.sid = sections[i]['.name'];
this.data = sections[i];
break;
}
}
},
isMasquerade: function() {
return (this.get('masq') == '1');
},
getName: function() {
return this.get('name');
},
getNetwork: function() {
return this.get('network');
},
getInput: function() {
return parsePolicy(this.get('input'), (new Defaults()).getInput());
},
getOutput: function() {
return parsePolicy(this.get('output'), (new Defaults()).getOutput());
},
getForward: function() {
return parsePolicy(this.get('forward'), (new Defaults()).getForward());
},
addNetwork: function(network) {
var section = uci.get('network', network);
if (section == null || section['.type'] != 'interface')
return false;
var newNetworks = this.getNetworks();
if (newNetworks.filter(function(net) { return net == network }).length)
return false;
newNetworks.push(network);
this.set('network', newNetworks.join(' '));
return true;
},
deleteNetwork: function(network) {
var oldNetworks = this.getNetworks(),
newNetworks = oldNetworks.filter(function(net) { return net != network });
if (newNetworks.length > 0)
this.set('network', newNetworks.join(' '));
else
this.set('network', ' ');
return (newNetworks.length < oldNetworks.length);
},
getNetworks: function() {
return L.toArray(this.get('network') || this.get('name'));
},
clearNetworks: function() {
this.set('network', ' ');
},
getForwardingsBy: function(what) {
var sections = uci.sections('firewall', 'forwarding'),
forwards = [];
for (var i = 0; i < sections.length; i++) {
if (sections[i].src == null || sections[i].dest == null)
continue;
if (sections[i][what] != this.getName())
continue;
forwards.push(new Forwarding(sections[i]['.name']));
}
return forwards;
},
addForwardingTo: function(dest) {
var forwards = this.getForwardingsBy('src'),
zone = lookupZone(dest);
if (zone == null || zone.getName() == this.getName())
return null;
for (var i = 0; i < forwards.length; i++)
if (forwards[i].getDestination() == zone.getName())
return null;
var sid = uci.add('firewall', 'forwarding');
uci.set('firewall', sid, 'src', this.getName());
uci.set('firewall', sid, 'dest', zone.getName());
return new Forwarding(sid);
},
addForwardingFrom: function(src) {
var forwards = this.getForwardingsBy('dest'),
zone = lookupZone(src);
if (zone == null || zone.getName() == this.getName())
return null;
for (var i = 0; i < forwards.length; i++)
if (forwards[i].getSource() == zone.getName())
return null;
var sid = uci.add('firewall', 'forwarding');
uci.set('firewall', sid, 'src', zone.getName());
uci.set('firewall', sid, 'dest', this.getName());
return new Forwarding(sid);
},
deleteForwardingsBy: function(what) {
var sections = uci.sections('firewall', 'forwarding'),
found = false;
for (var i = 0; i < sections.length; i++) {
if (sections[i].src == null || sections[i].dest == null)
continue;
if (sections[i][what] != this.getName())
continue;
uci.remove('firewall', sections[i]['.name']);
found = true;
}
return found;
},
deleteForwarding: function(forwarding) {
if (!(forwarding instanceof Forwarding))
return false;
var section = uci.get('firewall', forwarding.sid);
if (!section || section['.type'] != 'forwarding')
return false;
uci.remove('firewall', section['.name']);
return true;
},
addRedirect: function(options) {
var sid = uci.add('firewall', 'redirect');
if (options != null && typeof(options) == 'object')
for (var key in options)
if (options.hasOwnProperty(key))
uci.set('firewall', sid, key, options[key]);
uci.set('firewall', sid, 'src', this.getName());
return new Redirect(sid);
},
addRule: function(options) {
var sid = uci.add('firewall', 'rule');
if (options != null && typeof(options) == 'object')
for (var key in options)
if (options.hasOwnProperty(key))
uci.set('firewall', sid, key, options[key]);
uci.set('firewall', sid, 'src', this.getName());
return new Redirect(sid);
},
getColor: function(forName) {
var name = (arguments.length > 0 ? forName : this.getName());
return getColorForName(name);
}
});
Forwarding = AbstractFirewallItem.extend({
__init__: function(sid) {
this.sid = sid;
},
getSource: function() {
return this.get('src');
},
getDestination: function() {
return this.get('dest');
},
getSourceZone: function() {
return lookupZone(this.getSource());
},
getDestinationZone: function() {
return lookupZone(this.getDestination());
}
});
Rule = AbstractFirewallItem.extend({
getSource: function() {
return this.get('src');
},
getDestination: function() {
return this.get('dest');
},
getSourceZone: function() {
return lookupZone(this.getSource());
},
getDestinationZone: function() {
return lookupZone(this.getDestination());
}
});
Redirect = AbstractFirewallItem.extend({
getSource: function() {
return this.get('src');
},
getDestination: function() {
return this.get('dest');
},
getSourceZone: function() {
return lookupZone(this.getSource());
},
getDestinationZone: function() {
return lookupZone(this.getDestination());
}
});
return Firewall;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
/* Licensed under the BSD license. Copyright 2014 - Bram Stein. All rights reserved.
* https://github.com/bramstein/promis */
(function(){'use strict';var f,g=[];function l(a){g.push(a);1==g.length&&f()}function m(){for(;g.length;)g[0](),g.shift()}f=function(){setTimeout(m)};function n(a){this.a=p;this.b=void 0;this.f=[];var b=this;try{a(function(a){q(b,a)},function(a){r(b,a)})}catch(c){r(b,c)}}var p=2;function t(a){return new n(function(b,c){c(a)})}function u(a){return new n(function(b){b(a)})}function q(a,b){if(a.a==p){if(b==a)throw new TypeError;var c=!1;try{var d=b&&b.then;if(null!=b&&"object"==typeof b&&"function"==typeof d){d.call(b,function(b){c||q(a,b);c=!0},function(b){c||r(a,b);c=!0});return}}catch(e){c||r(a,e);return}a.a=0;a.b=b;v(a)}}
function r(a,b){if(a.a==p){if(b==a)throw new TypeError;a.a=1;a.b=b;v(a)}}function v(a){l(function(){if(a.a!=p)for(;a.f.length;){var b=a.f.shift(),c=b[0],d=b[1],e=b[2],b=b[3];try{0==a.a?"function"==typeof c?e(c.call(void 0,a.b)):e(a.b):1==a.a&&("function"==typeof d?e(d.call(void 0,a.b)):b(a.b))}catch(h){b(h)}}})}n.prototype.g=function(a){return this.c(void 0,a)};n.prototype.c=function(a,b){var c=this;return new n(function(d,e){c.f.push([a,b,d,e]);v(c)})};
function w(a){return new n(function(b,c){function d(c){return function(d){h[c]=d;e+=1;e==a.length&&b(h)}}var e=0,h=[];0==a.length&&b(h);for(var k=0;k<a.length;k+=1)u(a[k]).c(d(k),c)})}function x(a){return new n(function(b,c){for(var d=0;d<a.length;d+=1)u(a[d]).c(b,c)})};window.Promise||(window.Promise=n,window.Promise.resolve=u,window.Promise.reject=t,window.Promise.race=x,window.Promise.all=w,window.Promise.prototype.then=n.prototype.c,window.Promise.prototype["catch"]=n.prototype.g,window.Promise.prototype.finally=function(a){return this.c(a,a)});}());

View file

@ -0,0 +1,160 @@
'use strict';
var rpcRequestID = 1,
rpcSessionID = L.env.sessionid || '00000000000000000000000000000000',
rpcBaseURL = L.url('admin/ubus');
return L.Class.extend({
call: function(req, cb) {
var q = '';
if (Array.isArray(req)) {
if (req.length == 0)
return Promise.resolve([]);
for (var i = 0; i < req.length; i++)
q += '%s%s.%s'.format(
q ? ';' : '/',
req[i].params[1],
req[i].params[2]
);
}
else {
q += '/%s.%s'.format(req.params[1], req.params[2]);
}
return L.Request.post(rpcBaseURL + q, req, {
timeout: (L.env.rpctimeout || 5) * 1000,
credentials: true
}).then(cb);
},
handleListReply: function(req, msg) {
var list = msg.result;
/* verify message frame */
if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !Array.isArray(list))
list = [ ];
req.resolve(list);
},
handleCallReply: function(req, res) {
var type = Object.prototype.toString,
msg = null;
if (!res.ok)
L.error('RPCError', 'RPC call failed with HTTP error %d: %s',
res.status, res.statusText || '?');
msg = res.json();
/* fetch response attribute and verify returned type */
var ret = undefined;
/* verify message frame */
if (typeof(msg) == 'object' && msg.jsonrpc == '2.0') {
if (typeof(msg.error) == 'object' && msg.error.code && msg.error.message)
req.reject(new Error('RPC call failed with error %d: %s'
.format(msg.error.code, msg.error.message || '?')));
else if (Array.isArray(msg.result) && msg.result[0] == 0)
ret = (msg.result.length > 1) ? msg.result[1] : msg.result[0];
}
else {
req.reject(new Error('Invalid message frame received'));
}
if (req.expect) {
for (var key in req.expect) {
if (ret != null && key != '')
ret = ret[key];
if (ret == null || type.call(ret) != type.call(req.expect[key]))
ret = req.expect[key];
break;
}
}
/* apply filter */
if (typeof(req.filter) == 'function') {
req.priv[0] = ret;
req.priv[1] = req.params;
ret = req.filter.apply(this, req.priv);
}
req.resolve(ret);
},
list: function() {
var msg = {
jsonrpc: '2.0',
id: rpcRequestID++,
method: 'list',
params: arguments.length ? this.varargs(arguments) : undefined
};
return this.call(msg, this.handleListReply);
},
declare: function(options) {
return Function.prototype.bind.call(function(rpc, options) {
var args = this.varargs(arguments, 2);
return new Promise(function(resolveFn, rejectFn) {
/* build parameter object */
var p_off = 0;
var params = { };
if (Array.isArray(options.params))
for (p_off = 0; p_off < options.params.length; p_off++)
params[options.params[p_off]] = args[p_off];
/* all remaining arguments are private args */
var priv = [ undefined, undefined ];
for (; p_off < args.length; p_off++)
priv.push(args[p_off]);
/* store request info */
var req = {
expect: options.expect,
filter: options.filter,
resolve: resolveFn,
reject: rejectFn,
params: params,
priv: priv
};
/* build message object */
var msg = {
jsonrpc: '2.0',
id: rpcRequestID++,
method: 'call',
params: [
rpcSessionID,
options.object,
options.method,
params
]
};
/* call rpc */
rpc.call(msg, rpc.handleCallReply.bind(rpc, req));
});
}, this, this, options);
},
getSessionID: function() {
return rpcSessionID;
},
setSessionID: function(sid) {
rpcSessionID = sid;
},
getBaseURL: function() {
return rpcBaseURL;
},
setBaseURL: function(url) {
rpcBaseURL = url;
}
});

View file

@ -0,0 +1,93 @@
'use strict';
var s = [0x0000, 0x0000, 0x0000, 0x0000];
function mul(a, b) {
var r = [0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000];
for (var j = 0; j < 4; j++) {
var k = 0;
for (var i = 0; i < 4; i++) {
var t = a[i] * b[j] + r[i+j] + k;
r[i+j] = t & 0xffff;
k = t >>> 16;
}
r[j+4] = k;
}
r.length = 4;
return r;
}
function add(a, n) {
var r = [0x0000, 0x0000, 0x0000, 0x0000],
k = n;
for (var i = 0; i < 4; i++) {
var t = a[i] + k;
r[i] = t & 0xffff;
k = t >>> 16;
}
return r;
}
function shr(a, n) {
var r = [a[0], a[1], a[2], a[3], 0x0000],
i = 4,
k = 0;
for (; n > 16; n -= 16, i--)
for (var j = 0; j < 4; j++)
r[j] = r[j+1];
for (; i > 0; i--) {
var s = r[i-1];
r[i-1] = (s >>> n) | k;
k = ((s & ((1 << n) - 1)) << (16 - n));
}
r.length = 4;
return r;
}
return L.Class.extend({
seed: function(n) {
n = (n - 1)|0;
s[0] = n & 0xffff;
s[1] = n >>> 16;
s[2] = 0;
s[3] = 0;
},
int: function() {
s = mul(s, [0x7f2d, 0x4c95, 0xf42d, 0x5851]);
s = add(s, 1);
var r = shr(s, 33);
return (r[1] << 16) | r[0];
},
get: function() {
var r = (this.int() % 0x7fffffff) / 0x7fffffff, l, u;
switch (arguments.length) {
case 0:
return r;
case 1:
l = 1;
u = arguments[0]|0;
break;
case 2:
l = arguments[0]|0;
u = arguments[1]|0;
break;
}
return Math.floor(r * (u - l + 1)) + l;
}
});

View file

@ -0,0 +1,327 @@
'use strict';
'require ui';
'require form';
'require network';
'require firewall';
var CBIZoneSelect = form.ListValue.extend({
__name__: 'CBI.ZoneSelect',
load: function(section_id) {
return Promise.all([ firewall.getZones(), network.getNetworks() ]).then(L.bind(function(zn) {
this.zones = zn[0];
this.networks = zn[1];
return this.super('load', section_id);
}, this));
},
filter: function(section_id, value) {
return true;
},
lookupZone: function(name) {
return this.zones.filter(function(zone) { return zone.getName() == name })[0];
},
lookupNetwork: function(name) {
return this.networks.filter(function(network) { return network.getName() == name })[0];
},
renderWidget: function(section_id, option_index, cfgvalue) {
var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
choices = {};
if (this.allowlocal) {
choices[''] = E('span', {
'class': 'zonebadge',
'style': 'background-color:' + firewall.getColorForName(null)
}, [
E('strong', _('Device')),
(this.allowany || this.allowlocal)
? ' (%s)'.format(this.alias != 'dest' ? _('output') : _('input')) : ''
]);
}
else if (!this.multiple && (this.rmempty || this.optional)) {
choices[''] = E('span', {
'class': 'zonebadge',
'style': 'background-color:' + firewall.getColorForName(null)
}, E('em', _('unspecified')));
}
if (this.allowany) {
choices['*'] = E('span', {
'class': 'zonebadge',
'style': 'background-color:' + firewall.getColorForName(null)
}, [
E('strong', _('Any zone')),
(this.allowany && this.allowlocal) ? ' (%s)'.format(_('forward')) : ''
]);
}
for (var i = 0; i < this.zones.length; i++) {
var zone = this.zones[i],
name = zone.getName(),
networks = zone.getNetworks(),
ifaces = [];
if (!this.filter(section_id, name))
continue;
for (var j = 0; j < networks.length; j++) {
var network = this.lookupNetwork(networks[j]);
if (!network)
continue;
var span = E('span', {
'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
}, network.getName() + ': ');
var devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
for (var k = 0; k < devices.length; k++) {
span.appendChild(E('img', {
'title': devices[k].getI18n(),
'src': L.resource('icons/%s%s.png'.format(devices[k].getType(), devices[k].isUp() ? '' : '_disabled'))
}));
}
if (!devices.length)
span.appendChild(E('em', _('(empty)')));
ifaces.push(span);
}
if (!ifaces.length)
ifaces.push(E('em', _('(empty)')));
choices[name] = E('span', {
'class': 'zonebadge',
'style': 'background-color:' + zone.getColor()
}, [ E('strong', name) ].concat(ifaces));
}
var widget = new ui.Dropdown(values, choices, {
id: this.cbid(section_id),
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<span class="zonebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
'<strong>{{value}}:</strong> <em>('+_('create')+')</em>' +
'</span>' +
'</li>'
});
return widget.render();
},
});
var CBIZoneForwards = form.DummyValue.extend({
__name__: 'CBI.ZoneForwards',
load: function(section_id) {
return Promise.all([ firewall.getDefaults(), firewall.getZones(), network.getNetworks() ]).then(L.bind(function(dzn) {
this.defaults = dzn[0];
this.zones = dzn[1];
this.networks = dzn[2];
return this.super('load', section_id);
}, this));
},
renderZone: function(zone) {
var name = zone.getName(),
networks = zone.getNetworks(),
ifaces = [];
for (var j = 0; j < networks.length; j++) {
var network = this.networks.filter(function(net) { return net.getName() == networks[j] })[0];
if (!network)
continue;
var span = E('span', {
'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
}, network.getName() + ': ');
var devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
for (var k = 0; k < devices.length && devices[k]; k++) {
span.appendChild(E('img', {
'title': devices[k].getI18n(),
'src': L.resource('icons/%s%s.png'.format(devices[k].getType(), devices[k].isUp() ? '' : '_disabled'))
}));
}
if (!devices.length)
span.appendChild(E('em', _('(empty)')));
ifaces.push(span);
}
if (!ifaces.length)
ifaces.push(E('span', { 'class': 'ifacebadge' }, E('em', _('(empty)'))));
return E('label', {
'class': 'zonebadge cbi-tooltip-container',
'style': 'background-color:' + zone.getColor()
}, [
E('strong', name),
E('div', { 'class': 'cbi-tooltip' }, ifaces)
]);
},
renderWidget: function(section_id, option_index, cfgvalue) {
var value = (cfgvalue != null) ? cfgvalue : this.default,
zone = this.zones.filter(function(z) { return z.getName() == value })[0];
if (!zone)
return E([]);
var forwards = zone.getForwardingsBy('src'),
dzones = [];
for (var i = 0; i < forwards.length; i++) {
var dzone = forwards[i].getDestinationZone();
if (!dzone)
continue;
dzones.push(this.renderZone(dzone));
}
if (!dzones.length)
dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' },
E('strong', this.defaults.getForward())));
return E('div', { 'class': 'zone-forwards' }, [
E('div', { 'class': 'zone-src' }, this.renderZone(zone)),
E('span', '⇒'),
E('div', { 'class': 'zone-dest' }, dzones)
]);
},
});
var CBINetworkSelect = form.ListValue.extend({
__name__: 'CBI.NetworkSelect',
load: function(section_id) {
return network.getNetworks().then(L.bind(function(networks) {
this.networks = networks;
return this.super('load', section_id);
}, this));
},
filter: function(section_id, value) {
return true;
},
renderIfaceBadge: function(network) {
var span = E('span', { 'class': 'ifacebadge' }, network.getName() + ': '),
devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
for (var j = 0; j < devices.length && devices[j]; j++) {
span.appendChild(E('img', {
'title': devices[j].getI18n(),
'src': L.resource('icons/%s%s.png'.format(devices[j].getType(), devices[j].isUp() ? '' : '_disabled'))
}));
}
if (!devices.length) {
span.appendChild(E('em', { 'class': 'hide-close' }, _('(no interfaces attached)')));
span.appendChild(E('em', { 'class': 'hide-open' }, '-'));
}
return span;
},
renderWidget: function(section_id, option_index, cfgvalue) {
var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
choices = {},
checked = {};
for (var i = 0; i < values.length; i++)
checked[values[i]] = true;
values = [];
if (!this.multiple && (this.rmempty || this.optional))
choices[''] = E('em', _('unspecified'));
for (var i = 0; i < this.networks.length; i++) {
var network = this.networks[i],
name = network.getName();
if (name == 'loopback' || !this.filter(section_id, name))
continue;
if (this.novirtual && network.isVirtual())
continue;
if (checked[name])
values.push(name);
choices[name] = this.renderIfaceBadge(network);
}
var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
id: this.cbid(section_id),
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<span class="ifacebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
'{{value}}: <em>('+_('create')+')</em>' +
'</span>' +
'</li>'
});
return widget.render();
},
textvalue: function(section_id) {
var cfgvalue = this.cfgvalue(section_id),
values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
rv = E([]);
for (var i = 0; i < (this.networks || []).length; i++) {
var network = this.networks[i],
name = network.getName();
if (values.indexOf(name) == -1)
continue;
if (rv.length)
L.dom.append(rv, ' ');
L.dom.append(rv, this.renderIfaceBadge(network));
}
if (!rv.firstChild)
rv.appendChild(E('em', _('unspecified')));
return rv;
},
});
return L.Class.extend({
ZoneSelect: CBIZoneSelect,
ZoneForwards: CBIZoneForwards,
NetworkSelect: CBINetworkSelect
});

View file

@ -0,0 +1,540 @@
'use strict';
'require rpc';
return L.Class.extend({
__init__: function() {
this.state = {
newidx: 0,
values: { },
creates: { },
changes: { },
deletes: { },
reorder: { }
};
this.loaded = {};
},
callLoad: rpc.declare({
object: 'uci',
method: 'get',
params: [ 'config' ],
expect: { values: { } }
}),
callOrder: rpc.declare({
object: 'uci',
method: 'order',
params: [ 'config', 'sections' ]
}),
callAdd: rpc.declare({
object: 'uci',
method: 'add',
params: [ 'config', 'type', 'name', 'values' ],
expect: { section: '' }
}),
callSet: rpc.declare({
object: 'uci',
method: 'set',
params: [ 'config', 'section', 'values' ]
}),
callDelete: rpc.declare({
object: 'uci',
method: 'delete',
params: [ 'config', 'section', 'options' ]
}),
callApply: rpc.declare({
object: 'uci',
method: 'apply',
params: [ 'timeout', 'rollback' ]
}),
callConfirm: rpc.declare({
object: 'uci',
method: 'confirm'
}),
createSID: function(conf) {
var v = this.state.values,
n = this.state.creates,
sid;
do {
sid = "new%06x".format(Math.random() * 0xFFFFFF);
} while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
return sid;
},
resolveSID: function(conf, sid) {
if (typeof(sid) != 'string')
return sid;
var m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);
if (m) {
var type = m[1],
pos = +m[2],
sections = this.sections(conf, type),
section = sections[pos >= 0 ? pos : sections.length + pos];
return section ? section['.name'] : null;
}
return sid;
},
reorderSections: function() {
var v = this.state.values,
n = this.state.creates,
r = this.state.reorder,
tasks = [];
if (Object.keys(r).length === 0)
return Promise.resolve();
/*
gather all created and existing sections, sort them according
to their index value and issue an uci order call
*/
for (var c in r) {
var o = [ ];
if (n[c])
for (var s in n[c])
o.push(n[c][s]);
for (var s in v[c])
o.push(v[c][s]);
if (o.length > 0) {
o.sort(function(a, b) {
return (a['.index'] - b['.index']);
});
var sids = [ ];
for (var i = 0; i < o.length; i++)
sids.push(o[i]['.name']);
tasks.push(this.callOrder(c, sids));
}
}
this.state.reorder = { };
return Promise.all(tasks);
},
loadPackage: function(packageName) {
if (this.loaded[packageName] == null)
return (this.loaded[packageName] = this.callLoad(packageName));
return Promise.resolve(this.loaded[packageName]);
},
load: function(packages) {
var self = this,
pkgs = [ ],
tasks = [];
if (!Array.isArray(packages))
packages = [ packages ];
for (var i = 0; i < packages.length; i++)
if (!self.state.values[packages[i]]) {
pkgs.push(packages[i]);
tasks.push(self.loadPackage(packages[i]));
}
return Promise.all(tasks).then(function(responses) {
for (var i = 0; i < responses.length; i++)
self.state.values[pkgs[i]] = responses[i];
if (responses.length)
document.dispatchEvent(new CustomEvent('uci-loaded'));
return pkgs;
});
},
unload: function(packages) {
if (!Array.isArray(packages))
packages = [ packages ];
for (var i = 0; i < packages.length; i++) {
delete this.state.values[packages[i]];
delete this.state.creates[packages[i]];
delete this.state.changes[packages[i]];
delete this.state.deletes[packages[i]];
delete this.loaded[packages[i]];
}
},
add: function(conf, type, name) {
var n = this.state.creates,
sid = name || this.createSID(conf);
if (!n[conf])
n[conf] = { };
n[conf][sid] = {
'.type': type,
'.name': sid,
'.create': name,
'.anonymous': !name,
'.index': 1000 + this.state.newidx++
};
return sid;
},
remove: function(conf, sid) {
var n = this.state.creates,
c = this.state.changes,
d = this.state.deletes;
/* requested deletion of a just created section */
if (n[conf] && n[conf][sid]) {
delete n[conf][sid];
}
else {
if (c[conf])
delete c[conf][sid];
if (!d[conf])
d[conf] = { };
d[conf][sid] = true;
}
},
sections: function(conf, type, cb) {
var sa = [ ],
v = this.state.values[conf],
n = this.state.creates[conf],
c = this.state.changes[conf],
d = this.state.deletes[conf];
if (!v)
return sa;
for (var s in v)
if (!d || d[s] !== true)
if (!type || v[s]['.type'] == type)
sa.push(Object.assign({ }, v[s], c ? c[s] : undefined));
if (n)
for (var s in n)
if (!type || n[s]['.type'] == type)
sa.push(Object.assign({ }, n[s]));
sa.sort(function(a, b) {
return a['.index'] - b['.index'];
});
for (var i = 0; i < sa.length; i++)
sa[i]['.index'] = i;
if (typeof(cb) == 'function')
for (var i = 0; i < sa.length; i++)
cb.call(this, sa[i], sa[i]['.name']);
return sa;
},
get: function(conf, sid, opt) {
var v = this.state.values,
n = this.state.creates,
c = this.state.changes,
d = this.state.deletes;
sid = this.resolveSID(conf, sid);
if (sid == null)
return null;
/* requested option in a just created section */
if (n[conf] && n[conf][sid]) {
if (!n[conf])
return undefined;
if (opt == null)
return n[conf][sid];
return n[conf][sid][opt];
}
/* requested an option value */
if (opt != null) {
/* check whether option was deleted */
if (d[conf] && d[conf][sid]) {
if (d[conf][sid] === true)
return undefined;
for (var i = 0; i < d[conf][sid].length; i++)
if (d[conf][sid][i] == opt)
return undefined;
}
/* check whether option was changed */
if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
return c[conf][sid][opt];
/* return base value */
if (v[conf] && v[conf][sid])
return v[conf][sid][opt];
return undefined;
}
/* requested an entire section */
if (v[conf])
return v[conf][sid];
return undefined;
},
set: function(conf, sid, opt, val) {
var v = this.state.values,
n = this.state.creates,
c = this.state.changes,
d = this.state.deletes;
sid = this.resolveSID(conf, sid);
if (sid == null || opt == null || opt.charAt(0) == '.')
return;
if (n[conf] && n[conf][sid]) {
if (val != null)
n[conf][sid][opt] = val;
else
delete n[conf][sid][opt];
}
else if (val != null && val !== '') {
/* do not set within deleted section */
if (d[conf] && d[conf][sid] === true)
return;
/* only set in existing sections */
if (!v[conf] || !v[conf][sid])
return;
if (!c[conf])
c[conf] = {};
if (!c[conf][sid])
c[conf][sid] = {};
/* undelete option */
if (d[conf] && d[conf][sid])
d[conf][sid] = d[conf][sid].filter(function(o) { return o !== opt });
c[conf][sid][opt] = val;
}
else {
/* only delete in existing sections */
if (!(v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) &&
!(c[conf] && c[conf][sid] && c[conf][sid].hasOwnProperty(opt)))
return;
if (!d[conf])
d[conf] = { };
if (!d[conf][sid])
d[conf][sid] = [ ];
if (d[conf][sid] !== true)
d[conf][sid].push(opt);
}
},
unset: function(conf, sid, opt) {
return this.set(conf, sid, opt, null);
},
get_first: function(conf, type, opt) {
var sid = null;
this.sections(conf, type, function(s) {
if (sid == null)
sid = s['.name'];
});
return this.get(conf, sid, opt);
},
set_first: function(conf, type, opt, val) {
var sid = null;
this.sections(conf, type, function(s) {
if (sid == null)
sid = s['.name'];
});
return this.set(conf, sid, opt, val);
},
unset_first: function(conf, type, opt) {
return this.set_first(conf, type, opt, null);
},
move: function(conf, sid1, sid2, after) {
var sa = this.sections(conf),
s1 = null, s2 = null;
sid1 = this.resolveSID(conf, sid1);
sid2 = this.resolveSID(conf, sid2);
for (var i = 0; i < sa.length; i++) {
if (sa[i]['.name'] != sid1)
continue;
s1 = sa[i];
sa.splice(i, 1);
break;
}
if (s1 == null)
return false;
if (sid2 == null) {
sa.push(s1);
}
else {
for (var i = 0; i < sa.length; i++) {
if (sa[i]['.name'] != sid2)
continue;
s2 = sa[i];
sa.splice(i + !!after, 0, s1);
break;
}
if (s2 == null)
return false;
}
for (var i = 0; i < sa.length; i++)
this.get(conf, sa[i]['.name'])['.index'] = i;
this.state.reorder[conf] = true;
return true;
},
save: function() {
var v = this.state.values,
n = this.state.creates,
c = this.state.changes,
d = this.state.deletes,
r = this.state.reorder,
self = this,
snew = [ ],
pkgs = { },
tasks = [];
if (n)
for (var conf in n) {
for (var sid in n[conf]) {
var r = {
config: conf,
values: { }
};
for (var k in n[conf][sid]) {
if (k == '.type')
r.type = n[conf][sid][k];
else if (k == '.create')
r.name = n[conf][sid][k];
else if (k.charAt(0) != '.')
r.values[k] = n[conf][sid][k];
}
snew.push(n[conf][sid]);
tasks.push(self.callAdd(r.config, r.type, r.name, r.values));
}
pkgs[conf] = true;
}
if (c)
for (var conf in c) {
for (var sid in c[conf])
tasks.push(self.callSet(conf, sid, c[conf][sid]));
pkgs[conf] = true;
}
if (d)
for (var conf in d) {
for (var sid in d[conf]) {
var o = d[conf][sid];
tasks.push(self.callDelete(conf, sid, (o === true) ? null : o));
}
pkgs[conf] = true;
}
if (r)
for (var conf in r)
pkgs[conf] = true;
return Promise.all(tasks).then(function(responses) {
/*
array "snew" holds references to the created uci sections,
use it to assign the returned names of the new sections
*/
for (var i = 0; i < snew.length; i++)
snew[i]['.name'] = responses[i];
return self.reorderSections();
}).then(function() {
pkgs = Object.keys(pkgs);
self.unload(pkgs);
return self.load(pkgs);
});
},
apply: function(timeout) {
var self = this,
date = new Date();
if (typeof(timeout) != 'number' || timeout < 1)
timeout = 10;
return self.callApply(timeout, true).then(function(rv) {
if (rv != 0)
return Promise.reject(rv);
var try_deadline = date.getTime() + 1000 * timeout;
var try_confirm = function() {
return self.callConfirm().then(function(rv) {
if (rv != 0) {
if (date.getTime() < try_deadline)
window.setTimeout(try_confirm, 250);
else
return Promise.reject(rv);
}
return rv;
});
};
window.setTimeout(try_confirm, 1000);
});
},
changes: rpc.declare({
object: 'uci',
method: 'changes',
expect: { changes: { } }
})
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,568 @@
'use strict';
var Validator = L.Class.extend({
__name__: 'Validation',
__init__: function(field, type, optional, vfunc, validatorFactory) {
this.field = field;
this.optional = optional;
this.vfunc = vfunc;
this.vstack = validatorFactory.compile(type);
this.factory = validatorFactory;
},
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;
},
apply: function(name, value, args) {
var func;
if (typeof(name) === 'function')
func = name;
else if (typeof(this.factory.types[name]) === 'function')
func = this.factory.types[name];
else
return false;
if (value != null)
this.value = value;
return func.apply(this, args);
},
validate: function() {
/* element is detached */
if (!findParent(this.field, 'body') && !findParent(this.field, '[data-field]'))
return true;
this.field.classList.remove('cbi-input-invalid');
this.value = (this.field.value != null) ? this.field.value : '';
this.error = null;
var valid;
if (this.value.length === 0)
valid = this.assert(this.optional, _('non-empty value'));
else
valid = this.vstack[0].apply(this, this.vstack[1]);
if (valid !== true) {
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 }));
return false;
}
if (typeof(this.vfunc) == 'function')
valid = this.vfunc(this.value);
if (valid !== true) {
this.assert(false, valid);
this.field.setAttribute('data-tooltip', valid);
this.field.setAttribute('data-tooltip-style', 'error');
this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true }));
return false;
}
this.field.removeAttribute('data-tooltip');
this.field.removeAttribute('data-tooltip-style');
this.field.dispatchEvent(new CustomEvent('validation-success', { bubbles: true }));
return true;
},
});
var ValidatorFactory = L.Class.extend({
__name__: 'ValidatorFactory',
create: function(field, type, optional, vfunc) {
return new Validator(field, type, optional, vfunc, 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 {
L.raise('SyntaxError', 'Unhandled token "%s"', label);
}
}
pos = i+1;
}
depth += (code.charCodeAt(i) == 40);
break;
case 41:
if (--depth <= 0) {
if (typeof stack[stack.length-2] != 'function')
L.raise('SyntaxError', 'Argument list follows non-function');
stack[stack.length-1] = this.compile(code.substring(pos, i));
pos = i+1;
}
break;
}
}
return stack;
},
parseInteger: function(x) {
return (/^-?\d+$/.test(x) ? +x : NaN);
},
parseDecimal: function(x) {
return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN);
},
parseIPv4: function(x) {
if (!x.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))
return null;
if (RegExp.$1 > 255 || RegExp.$2 > 255 || RegExp.$3 > 255 || RegExp.$4 > 255)
return null;
return [ +RegExp.$1, +RegExp.$2, +RegExp.$3, +RegExp.$4 ];
},
parseIPv6: function(x) {
if (x.match(/^([a-fA-F0-9:]+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)) {
var v6 = RegExp.$1, v4 = this.parseIPv4(RegExp.$2);
if (!v4)
return null;
x = v6 + ':' + (v4[0] * 256 + v4[1]).toString(16)
+ ':' + (v4[2] * 256 + v4[3]).toString(16);
}
if (!x.match(/^[a-fA-F0-9:]+$/))
return null;
var prefix_suffix = x.split(/::/);
if (prefix_suffix.length > 2)
return null;
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_suffix.length < 2 && prefix.length < 8) || prefix.length > 8))
return null;
var i, word;
var words = [];
for (i = 0, word = parseInt(prefix[0], 16); i < prefix.length; word = parseInt(prefix[++i], 16))
if (prefix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
words.push(word);
else
return null;
for (i = 0; i < (8 - prefix.length - suffix.length); i++)
words.push(0);
for (i = 0, word = parseInt(suffix[0], 16); i < suffix.length; word = parseInt(suffix[++i], 16))
if (suffix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
words.push(word);
else
return null;
return words;
},
types: {
integer: function() {
return this.assert(this.factory.parseInteger(this.value) !== NaN, _('valid integer value'));
},
uinteger: function() {
return this.assert(this.factory.parseInteger(this.value) >= 0, _('positive integer value'));
},
float: function() {
return this.assert(this.factory.parseDecimal(this.value) !== NaN, _('valid decimal value'));
},
ufloat: function() {
return this.assert(this.factory.parseDecimal(this.value) >= 0, _('positive decimal value'));
},
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'));
},
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);
return this.assert(m && this.factory.parseIPv4(m[1]) && (m[2] ? this.factory.parseIPv4(m[2]) : (m[3] ? this.apply('ip4prefix', m[3]) : true)),
nomask ? _('valid IPv4 address') : _('valid IPv4 address or network'));
},
ip6addr: function(nomask) {
var re = nomask ? /^([0-9a-fA-F:.]+)$/ : /^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/,
m = this.value.match(re);
return this.assert(m && this.factory.parseIPv6(m[1]) && (m[2] ? this.apply('ip6prefix', m[2]) : true),
nomask ? _('valid IPv6 address') : _('valid IPv6 address or prefix'));
},
ip4prefix: function() {
return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 32,
_('valid IPv4 prefix value (0-32)'));
},
ip6prefix: function() {
return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 128,
_('valid IPv6 prefix value (0-128)'));
},
cidr: function() {
return this.assert(this.apply('cidr4') || this.apply('cidr6'), _('valid IPv4 or IPv6 CIDR'));
},
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 && this.factory.parseIPv4(m[1]) && this.apply('ip4prefix', m[2]), _('valid IPv4 CIDR'));
},
cidr6: function() {
var m = this.value.match(/^([0-9a-fA-F:.]+)\/(\d{1,3})$/);
return this.assert(m && this.factory.parseIPv6(m[1]) && this.apply('ip6prefix', m[2]), _('valid IPv6 CIDR'));
},
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 && this.factory.parseIPv4(m[1]) && this.factory.parseIPv4(m[2]), _('IPv4 network in address/netmask notation'));
},
ipnet6: function() {
var m = this.value.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/);
return this.assert(m && this.factory.parseIPv6(m[1]) && this.factory.parseIPv6(m[2]), _('IPv6 network in address/netmask notation'));
},
ip6hostid: function() {
if (this.value == "eui64" || this.value == "random")
return true;
var v6 = this.factory.parseIPv6(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 = this.factory.parseInteger(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 this.assert(this.apply('port'), _('valid port or port range (port1-port2)'));
},
macaddr: function() {
return this.assert(this.value.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null,
_('valid MAC address'));
},
host: function(ipv4only) {
return this.assert(this.apply('hostname') || this.apply(ipv4only == 1 ? 'ip4addr' : 'ipaddr'),
_('valid hostname or IP address'));
},
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 this.assert(false, _('valid hostname'));
},
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 = this.factory.parseDecimal(this.value);
return this.assert(val >= +min && val <= +max, _('value between %f and %f').format(min, max));
},
min: function(min) {
return this.assert(this.factory.parseDecimal(this.value) >= +min, _('value greater or equal to %f').format(min));
},
max: function(max) {
return this.assert(this.factory.parseDecimal(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);
}
}
var t = _('One of the following: %s');
return this.assert(false, t.format('\n - ' + 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() {
this.value = this.value.replace(/^[ \t]*![ \t]*/, '');
if (arguments[0].apply(this, arguments[1]))
return this.assert(true);
return this.assert(false, _('Potential negation of: %s').format(this.error));
},
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 ];
var is_leap_year = function(year) {
return ((!(year % 4) && (year % 100)) || !(year % 400));
}
var get_days_in_month = function(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, null, 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'));
},
string: function() {
return true;
}
}
});
return ValidatorFactory;

View file

@ -1,250 +1 @@
/*
* xhr.js - XMLHttpRequest helper class
* (c) 2008-2018 Jo-Philipp Wich <jo@mein.io>
*/
XHR.prototype = {
_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;
},
_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;
switch (this._xmlHttp.readyState)
{
case 1:
case 2:
case 3:
return true;
default:
return false;
}
},
abort: function() {
if (this.busy())
this._xmlHttp.abort();
},
get: function(url, data, callback, timeout) {
this._xmlHttp = new XMLHttpRequest();
var xhr = this._xmlHttp,
code = this._encode(data);
url = location.protocol + '//' + location.host + url;
if (code)
if (url.substr(url.length-1,1) == '&')
url += code;
else
url += '?' + code;
xhr.open('GET', url, true);
if (!isNaN(timeout))
xhr.timeout = timeout;
xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
xhr.send(null);
},
post: function(url, data, callback, timeout) {
this._xmlHttp = new XMLHttpRequest();
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);
},
cancel: function() {
this._xmlHttp.onreadystatechange = function() {};
this._xmlHttp.abort();
},
send_form: function(form, callback, extra_values) {
var code = '';
for (var i = 0; i < form.elements.length; i++) {
var e = form.elements[i];
if (e.options) {
code += (code ? '&' : '') +
form.elements[i].name + '=' + encodeURIComponent(
e.options[e.selectedIndex].value
);
}
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 {
code += (code ? '&' : '') +
e.name + '=' + encodeURIComponent(e.value);
}
}
if (typeof extra_values == 'object')
for (var key in extra_values)
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));
}
}
XHR.get = function(url, data, callback) {
(new XHR()).get(url, data, callback);
}
XHR.post = function(url, data, callback) {
(new XHR()).post(url, data, callback);
}
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() {
for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i])
{
if (!(XHR._t % e.interval) && !e.xhr.busy())
e.xhr[post ? 'post' : 'get'](e.url, e.data, e.callback, e.interval * 1000 * 5 - 5);
}
XHR._t++;
};
}
var e = {
interval: interval,
callback: callback,
url: url,
data: data,
xhr: new XHR()
};
XHR._q.push(e);
return e;
}
XHR.stop = function(e) {
for (var i = 0; XHR._q && XHR._q[i]; i++) {
if (XHR._q[i] === e) {
e.xhr.cancel();
XHR._q.splice(i, 1);
return true;
}
}
return false;
}
XHR.halt = function() {
if (XHR._i) {
/* show & set poll indicator */
try {
document.getElementById('xhr_poll_status').style.display = '';
document.getElementById('xhr_poll_status_on').style.display = 'none';
document.getElementById('xhr_poll_status_off').style.display = '';
} catch(e) { }
window.clearInterval(XHR._i);
XHR._i = null;
}
}
XHR.run = function() {
if (XHR._r && !XHR._i) {
/* show & set poll indicator */
try {
document.getElementById('xhr_poll_status').style.display = '';
document.getElementById('xhr_poll_status_on').style.display = '';
document.getElementById('xhr_poll_status_off').style.display = 'none';
} catch(e) { }
/* kick first round manually to prevent one second lag when setting up
* the poll interval */
XHR._r();
XHR._i = window.setInterval(XHR._r, 1000);
}
}
XHR.running = function() {
return !!(XHR._r && XHR._i);
}
function XHR() {}
document.addEventListener('DOMContentLoaded', XHR.run);
/* replaced by luci.js */

View file

@ -1347,6 +1347,18 @@ function AbstractValue.deplist2json(self, section, deplist)
return util.serialize_json(deps)
end
-- Serialize choices
function AbstractValue.choices(self)
if type(self.keylist) == "table" and #self.keylist > 0 then
local i, k, v = nil, nil, {}
for i, k in ipairs(self.keylist) do
v[k] = self.vallist[i] or k
end
return v
end
return nil
end
-- Generates the unique CBID
function AbstractValue.cbid(self, section)
return "cbid."..self.map.config.."."..section.."."..self.option

View file

@ -418,7 +418,7 @@ function maxlength(val, max)
end
function phonedigit(val)
return (val:match("^[0-9\*#!%.]+$") ~= nil)
return (val:match("^[0-9%*#!%.]+$") ~= nil)
end
function timehhmmss(val)

View file

@ -88,6 +88,9 @@ function index()
page = entry({"admin", "translations"}, call("action_translations"), nil)
page.leaf = true
page = entry({"admin", "ubus"}, call("action_ubus"), nil)
page.leaf = true
-- Logout is last
entry({"admin", "logout"}, call("action_logout"), _("Logout"), 999)
end
@ -129,6 +132,124 @@ function action_translations(lang)
http.write_json(i18n.dump())
end
local function ubus_reply(id, data, code, errmsg)
local reply = { jsonrpc = "2.0", id = id }
if errmsg then
reply.error = {
code = code,
message = errmsg
}
else
reply.result = { code, data }
end
return reply
end
local ubus_types = {
nil,
"array",
"object",
"string",
nil, -- INT64
"number",
nil, -- INT16,
"boolean",
"double"
}
local function ubus_request(req)
if type(req) ~= "table" or type(req.method) ~= "string" or type(req.params) ~= "table" or
#req.params < 2 or req.jsonrpc ~= "2.0" or req.id == nil then
return ubus_reply(nil, nil, -32600, "Invalid request")
elseif req.method == "call" then
local sid, obj, fun, arg =
req.params[1], req.params[2], req.params[3], req.params[4] or {}
if type(arg) ~= "table" or arg.ubus_rpc_session ~= nil then
return ubus_reply(req.id, nil, -32602, "Invalid parameters")
end
if sid == "00000000000000000000000000000000" then
sid = luci.dispatcher.context.authsession
end
arg.ubus_rpc_session = sid
local res, code = luci.util.ubus(obj, fun, arg)
return ubus_reply(req.id, res, code or 0)
elseif req.method == "list" then
if type(params) ~= "table" or #params == 0 then
local objs = { luci.util.ubus() }
return ubus_reply(req.id, objs, 0)
else
local n, rv = nil, {}
for n = 1, #params do
if type(params[n]) ~= "string" then
return ubus_reply(req.id, nil, -32602, "Invalid parameters")
end
local sig = luci.util.ubus(params[n])
if sig and type(sig) == "table" then
rv[params[n]] = {}
local m, p
for m, p in pairs(sig) do
if type(p) == "table" then
rv[params[n]][m] = {}
local pn, pt
for pn, pt in pairs(p) do
rv[params[n]][m][pn] = ubus_types[pt] or "unknown"
end
end
end
end
end
return ubus_reply(req.id, rv, 0)
end
end
return ubus_reply(req.id, nil, -32601, "Method not found")
end
function action_ubus()
local parser = require "luci.jsonc".new()
luci.http.context.request:setfilehandler(function(_, s)
if not s then
return nil
end
local ok, err = parser:parse(s)
return (not err or nil)
end)
luci.http.context.request:content()
local json = parser:get()
if json == nil or type(json) ~= "table" then
luci.http.prepare_content("application/json")
luci.http.write_json(ubus_reply(nil, nil, -32700, "Parse error"))
return
end
local response
if #json == 0 then
response = ubus_request(json)
else
response = {}
local _, request
for _, request in ipairs(json) do
response[_] = ubus_request(request)
end
end
luci.http.prepare_content("application/json")
luci.http.write_json(response)
end
function lease_status()
local s = require "luci.tools.status"

View file

@ -1,5 +1,5 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Copyright 2010-2015 Jo-Philipp Wich <jow@openwrt.org>
-- Copyright 2010-2019 Jo-Philipp Wich <jo@mein.io>
-- Licensed to the public under the Apache License 2.0.
module("luci.controller.admin.uci", package.seeall)
@ -9,8 +9,7 @@ function index()
or table.concat(luci.dispatcher.context.request, "/")
entry({"admin", "uci"}, nil, _("Configuration"))
entry({"admin", "uci", "changes"}, post_on({ trigger_apply = true }, "action_changes"), _("Changes"), 40).query = {redir=redir}
entry({"admin", "uci", "revert"}, post("action_revert"), _("Revert"), 30).query = {redir=redir}
entry({"admin", "uci", "revert"}, post("action_revert"), nil)
local node
local authen = function(checkpass, allowed_users)
@ -31,34 +30,6 @@ function index()
end
function action_changes()
local uci = require "luci.model.uci"
local changes = uci:changes()
luci.template.render("admin_uci/changes", {
changes = next(changes) and changes,
timeout = timeout,
trigger_apply = luci.http.formvalue("trigger_apply") and true or false
})
end
function action_revert()
local uci = require "luci.model.uci"
local changes = uci:changes()
-- Collect files to be reverted
local r, tbl
for r, tbl in pairs(changes) do
uci:revert(r)
end
luci.template.render("admin_uci/revert", {
changes = next(changes) and changes,
trigger_revert = true
})
end
local function ubus_state_to_http(errstr)
local map = {
["Invalid command"] = 400,
@ -107,3 +78,19 @@ function action_confirm()
local _, errstr = uci:confirm(token)
ubus_state_to_http(errstr)
end
function action_revert()
local uci = require "luci.model.uci"
local changes = uci:changes()
-- Collect files to be reverted
local _, errstr, r, tbl
for r, tbl in pairs(changes) do
_, errstr = uci:revert(r)
if errstr then
break
end
end
ubus_state_to_http(errstr or "OK")
end

View file

@ -857,6 +857,15 @@ function template(name)
end
local _view = function(self, ...)
require "luci.template".render("view", { view = self.view })
end
function view(name)
return {type = "view", view = name, target = _view}
end
local function _cbi(self, ...)
local cbi = require "luci.cbi"
local tpl = require "luci.template"

View file

@ -137,7 +137,7 @@ end
net = {}
local function _nethints(what, callback)
local _, k, e, mac, ip, name
local _, k, e, mac, ip, name, duid, iaid
local cur = uci.cursor()
local ifn = { }
local hosts = { }
@ -189,6 +189,24 @@ local function _nethints(what, callback)
end
end
)
cur:foreach("dhcp", "odhcpd",
function(s)
if type(s.leasefile) == "string" and fs.access(s.leasefile) then
for e in io.lines(s.leasefile) do
duid, iaid, name, _, ip = e:match("^# %S+ (%S+) (%S+) (%S+) (-?%d+) %S+ %S+ ([0-9a-f:.]+)/[0-9]+")
mac = net.duid_to_mac(duid)
if mac then
if ip and iaid == "ipv4" then
_add(what, mac, ip, nil, name ~= "*" and name)
elseif ip then
_add(what, mac, nil, ip, name ~= "*" and name)
end
end
end
end
end
)
cur:foreach("dhcp", "host",
function(s)
@ -386,6 +404,26 @@ function net.devices()
return devs
end
function net.duid_to_mac(duid)
local b1, b2, b3, b4, b5, b6
if type(duid) == "string" then
-- DUID-LLT / Ethernet
if #duid == 28 then
b1, b2, b3, b4, b5, b6 = duid:match("^00010001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)%x%x%x%x%x%x%x%x$")
-- DUID-LL / Ethernet
elseif #duid == 20 then
b1, b2, b3, b4, b5, b6 = duid:match("^00030001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
-- DUID-LL / Ethernet (Without Header)
elseif #duid == 12 then
b1, b2, b3, b4, b5, b6 = duid:match("^(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
end
end
return b1 and luci.ip.checkmac(table.concat({ b1, b2, b3, b4, b5, b6 }, ":"))
end
process = {}
@ -405,11 +443,11 @@ function process.list()
for line in ps do
local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
"^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
"^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][<NW ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
)
local idx = tonumber(pid)
if idx then
if idx and not cmd:match("top %-bn1") then
data[idx] = {
['PID'] = pid,
['PPID'] = ppid,
@ -621,3 +659,11 @@ end
function init.stop(name)
return (init_action("stop", name) == 0)
end
function init.restart(name)
return (init_action("restart", name) == 0)
end
function init.reload(name)
return (init_action("reload", name) == 0)
end

View file

@ -16,12 +16,14 @@ TZ = {
{ 'Africa/Brazzaville', 'WAT-1' },
{ 'Africa/Bujumbura', 'CAT-2' },
{ 'Africa/Cairo', 'EET-2' },
{ 'Africa/Casablanca', '<+01>-1' },
{ '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', '<+01>-1' },
{ 'Africa/Freetown', 'GMT0' },
{ 'Africa/Gaborone', 'CAT-2' },
{ 'Africa/Harare', 'CAT-2' },
@ -83,7 +85,7 @@ TZ = {
{ 'America/Bogota', '<-05>5' },
{ 'America/Boise', 'MST7MDT,M3.2.0,M11.1.0' },
{ 'America/Cambridge Bay', 'MST7MDT,M3.2.0,M11.1.0' },
{ 'America/Campo Grande', '<-04>4<-03>,M11.1.0/0,M2.3.0/0' },
{ 'America/Campo Grande', '<-04>4' },
{ 'America/Cancun', 'EST5' },
{ 'America/Caracas', '<-04>4' },
{ 'America/Cayenne', '<-03>3' },
@ -92,7 +94,7 @@ TZ = {
{ 'America/Chihuahua', 'MST7MDT,M4.1.0,M10.5.0' },
{ 'America/Costa Rica', 'CST6' },
{ 'America/Creston', 'MST7' },
{ 'America/Cuiaba', '<-04>4<-03>,M11.1.0/0,M2.3.0/0' },
{ 'America/Cuiaba', '<-04>4' },
{ 'America/Curacao', 'AST4' },
{ 'America/Danmarkshavn', 'GMT0' },
{ 'America/Dawson', 'PST8PDT,M3.2.0,M11.1.0' },
@ -179,7 +181,7 @@ TZ = {
{ 'America/Santarem', '<-03>3' },
{ '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/Sao Paulo', '<-03>3' },
{ 'America/Scoresbysund', '<-01>1<+00>,M3.5.0/0,M10.5.0/1' },
{ 'America/Sitka', 'AKST9AKDT,M3.2.0,M11.1.0' },
{ 'America/St Barthelemy', 'AST4' },
@ -237,8 +239,8 @@ TZ = {
{ 'Asia/Dubai', '<+04>-4' },
{ 'Asia/Dushanbe', '<+05>-5' },
{ 'Asia/Famagusta', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Asia/Gaza', 'EET-2EEST,M3.4.6/1,M10.5.6/1' },
{ 'Asia/Hebron', 'EET-2EEST,M3.4.6/1,M10.5.6/1' },
{ 'Asia/Gaza', 'EET-2EEST,M3.5.5/0,M10.5.6/1' },
{ 'Asia/Hebron', 'EET-2EEST,M3.5.5/0,M10.5.6/1' },
{ 'Asia/Ho Chi Minh', '<+07>-7' },
{ 'Asia/Hong Kong', 'HKT-8' },
{ 'Asia/Hovd', '<+07>-7' },

View file

@ -6,21 +6,6 @@ module("luci.tools.status", package.seeall)
local uci = require "luci.model.uci".cursor()
local ipc = require "luci.ip"
local function duid_to_mac(duid)
local b1, b2, b3, b4, b5, b6
-- DUID-LLT / Ethernet
if type(duid) == "string" and #duid == 28 then
b1, b2, b3, b4, b5, b6 = duid:match("^00010001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)%x%x%x%x%x%x%x%x$")
-- DUID-LL / Ethernet
elseif type(duid) == "string" and #duid == 20 then
b1, b2, b3, b4, b5, b6 = duid:match("^00030001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
end
return b1 and ipc.checkmac(table.concat({ b1, b2, b3, b4, b5, b6 }, ":"))
end
local function dhcp_leases_common(family)
local rv = { }
local nfs = require "nixio.fs"
@ -93,7 +78,7 @@ local function dhcp_leases_common(family)
elseif ip and iaid == "ipv4" and family == 4 then
rv[#rv+1] = {
expires = (expire >= 0) and os.difftime(expire, os.time()),
macaddr = ipc.checkmac(duid:gsub("^(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$", "%1:%2:%3:%4:%5:%6")) or "00:00:00:00:00:00",
macaddr = sys.net.duid_to_mac(duid) or "00:00:00:00:00:00",
ipaddr = ip,
hostname = (name ~= "-") and name
}
@ -107,7 +92,7 @@ local function dhcp_leases_common(family)
local _, lease
local hosts = sys.net.host_hints()
for _, lease in ipairs(rv) do
local mac = duid_to_mac(lease.duid)
local mac = sys.net.duid_to_mac(lease.duid)
local host = mac and hosts[mac]
if host then
if not lease.name then

View file

@ -207,9 +207,8 @@ end
-- handling. It may actually be a property of the getopt function
-- rather than the shell proper.
function shellstartsqescape(value)
res, _ = string.gsub(value, "^\-", "\\-")
res, _ = string.gsub(res, "^-", "\-")
return shellsqescape(value)
res, _ = string.gsub(value, "^%-", "\\-")
return shellsqescape(res)
end
-- containing the resulting substrings. The optional max parameter specifies

View file

@ -1,66 +0,0 @@
<%#
Copyright 2010 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-%>
<% export("uci_changelog", function(changes) -%>
<div class="cbi-section">
<strong><%:Legend:%></strong>
<div class="uci-change-legend">
<div class="uci-change-legend-label"><ins>&#160;</ins> <%:Section added%></div>
<div class="uci-change-legend-label"><del>&#160;</del> <%:Section removed%></div>
<div class="uci-change-legend-label"><var><ins>&#160;</ins></var> <%:Option changed%></div>
<div class="uci-change-legend-label"><var><del>&#160;</del></var> <%:Option removed%></div>
<br style="clear:both" />
</div>
<br />
<div class="uci-change-list"><%
local util = luci.util
local tpl = {
["add-3"] = "<ins>uci add %0 <strong>%3</strong> # =%2</ins>",
["set-3"] = "<ins>uci set %0.<strong>%2</strong>=%3</ins>",
["set-4"] = "<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>",
["remove-2"] = "<del>uci del %0.<strong>%2</strong></del>",
["remove-3"] = "<var><del>uci del %0.%2.<strong>%3</strong></del></var>",
["order-3"] = "<var>uci reorder %0.%2=<strong>%3</strong></var>",
["list-add-4"] = "<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>",
["list-del-4"] = "<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>",
["rename-3"] = "<var>uci rename %0.%2=<strong>%3</strong></var>",
["rename-4"] = "<var>uci rename %0.%2.%3=<strong>%4</strong></var>"
}
local conf, deltas
for conf, deltas in util.kspairs(changes) do
write("<h3># /etc/config/%s</h3>" % 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("<br />")
end
%></div>
</div>
<%- end) %>

View file

@ -1,45 +0,0 @@
<%#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008-2018 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-%>
<%+header%>
<%-
local node, redir_url = luci.dispatcher.lookup(luci.http.formvalue("redir"))
export("redirect", redir_url or url("admin/uci/changes"))
include("admin_uci/changelog")
-%>
<h2 name="content"><%:Configuration%> / <%:Changes%></h2>
<% if changes then %>
<%- uci_changelog(changes) -%>
<% else %>
<p><strong><%:There are no pending changes!%></strong></p>
<% end %>
<div class="alert-message" id="cbi_apply_status" style="display:none"></div>
<div class="cbi-page-actions">
<% if redir_url then %>
<form method="get" action="<%=luci.util.pcdata(redir_url)%>">
<input class="cbi-button cbi-button-link" type="submit" value="<%:Back%>" />
</form>
<% end %>
<form method="post" action="<%=url("admin/uci/changes")%>">
<input type="hidden" name="token" value="<%=token%>" />
<input type="hidden" name="redir" value="<%=pcdata(luci.http.formvalue("redir"))%>" />
<input class="cbi-button cbi-button-save" type="submit" name="trigger_apply" value="<%:Save & Apply%>" />
</form>
<form method="post" action="<%=url("admin/uci/revert")%>">
<input type="hidden" name="token" value="<%=token%>" />
<input type="hidden" name="redir" value="<%=pcdata(luci.http.formvalue("redir"))%>" />
<input class="cbi-button cbi-button-reset" type="submit" value="<%:Revert%>" />
</form>
</div>
<%+footer%>

View file

@ -1,33 +0,0 @@
<%#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008-2018 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-%>
<%+header%>
<%-
local node, redir_url = luci.dispatcher.lookup(luci.http.formvalue("redir"))
export("redirect", redir_url or url("admin/uci/changes"))
include("admin_uci/changelog")
-%>
<h2 name="content"><%:Configuration%> / <%:Revert%></h2>
<% if changes then %>
<p><strong><%:The following changes have been reverted%>:</strong></p>
<%- uci_changelog(changes) -%>
<% else %>
<p><strong><%:There are no pending changes to revert!%></strong></p>
<% end %>
<% if redir_url then %>
<div class="cbi-page-actions">
<form class="inline" method="get" action="<%=luci.util.pcdata(redir_url)%>">
<input class="cbi-button cbi-button-link" style="margin:0" type="submit" value="<%:Back%>" />
</form>
</div>
<% end %>
<%+footer%>

View file

@ -1,172 +0,0 @@
<% export("cbi_apply_widget", function(redirect_ok, rollback_token) -%>
<script type="text/javascript">//<![CDATA[
var xhr = new XHR(),
uci_apply_auth = { sid: '<%=luci.dispatcher.context.authsession%>', token: '<%=token%>' },
uci_apply_rollback = <%=math.max(luci.config and luci.config.apply and luci.config.apply.rollback or 30, 30)%>,
uci_apply_holdoff = <%=math.max(luci.config and luci.config.apply and luci.config.apply.holdoff or 4, 1)%>,
uci_apply_timeout = <%=math.max(luci.config and luci.config.apply and luci.config.apply.timeout or 5, 1)%>,
uci_apply_display = <%=math.max(luci.config and luci.config.apply and luci.config.apply.display or 1.5, 1)%>,
uci_confirm_auth = <% if rollback_token then %>{ token: '<%=rollback_token%>' }<% else %>null<% end %>,
was_xhr_poll_running = false;
function uci_status_message(type, content) {
if (type) {
var message = showModal('', '');
message.classList.add('alert-message');
DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
if (content)
message.innerHTML = content;
if (!was_xhr_poll_running) {
was_xhr_poll_running = XHR.running();
XHR.halt();
}
}
else {
hideModal();
if (was_xhr_poll_running)
XHR.run();
}
}
function uci_rollback(checked) {
if (checked) {
uci_status_message('warning spinning',
'<p><%:Failed to confirm apply within %ds, waiting for rollback…%></p>'.format(uci_apply_rollback));
var call = function(r, data, duration) {
if (r.status === 204) {
uci_status_message('warning',
'<h4><%:Configuration has been rolled back!%></h4>' +
'<p><%:The device could not be reached within %d seconds after applying the pending changes, which caused the configuration to be rolled back for safety reasons. If you believe that the configuration changes are correct nonetheless, proceed by applying anyway. Alternatively, you can dismiss this warning and edit changes before attempting to apply again, or revert all pending changes to keep the currently working configuration state.%></p>'.format(uci_apply_rollback) +
'<div class="right">' +
'<input type="button" class="btn" onclick="uci_status_message(false)" value="<%:Dismiss%>" /> ' +
'<input type="button" class="btn cbi-button-action important" onclick="uci_revert()" value="<%:Revert changes%>" /> ' +
'<input type="button" class="btn cbi-button-negative important" onclick="uci_apply(false)" value="<%:Apply anyway%>" />' +
'</div>');
return;
}
var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
window.setTimeout(function() {
xhr.post('<%=url("admin/uci/confirm")%>', uci_apply_auth, call, uci_apply_timeout * 1000);
}, delay);
};
call({ status: 0 });
}
else {
uci_status_message('warning',
'<h4><%:Device unreachable!%></h4>' +
'<p><%:Could not regain access to the device after applying the configuration changes. You might need to reconnect if you modified network related settings such as the IP address or wireless security credentials.%></p>');
}
}
function uci_confirm(checked, deadline) {
var tt;
var ts = Date.now();
uci_status_message('notice');
var call = function(r, data, duration) {
if (Date.now() >= deadline) {
window.clearTimeout(tt);
uci_rollback(checked);
return;
}
else if (r && (r.status === 200 || r.status === 204)) {
var indicator = document.querySelector('.uci_change_indicator');
if (indicator) indicator.style.display = 'none';
uci_status_message('notice', '<p><%:Configuration has been applied.%></p>');
window.clearTimeout(tt);
window.setTimeout(function() {
<% if redirect_ok then -%>
location.href = decodeURIComponent('<%=luci.util.urlencode(redirect_ok)%>');
<%- else -%>
window.location = window.location.href.split('#')[0];
<% end %>
}, uci_apply_display * 1000);
return;
}
var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
window.setTimeout(function() {
xhr.post('<%=url("admin/uci/confirm")%>', uci_confirm_auth, call, uci_apply_timeout * 1000);
}, delay);
};
var tick = function() {
var now = Date.now();
uci_status_message('notice spinning',
'<p><%:Waiting for configuration to be applied… %ds%></p>'.format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0)));
if (now >= deadline)
return;
tt = window.setTimeout(tick, 1000 - (now - ts));
ts = now;
};
tick();
/* wait a few seconds for the settings to become effective */
window.setTimeout(call, Math.max(uci_apply_holdoff * 1000 - ((ts + uci_apply_rollback * 1000) - deadline), 1));
}
function uci_apply(checked) {
uci_status_message('notice spinning', '<p><%:Starting configuration apply…%></p>');
xhr.post('<%=url("admin/uci")%>/' + (checked ? 'apply_rollback' : 'apply_unchecked'), uci_apply_auth, function(r, tok) {
if (r.status === (checked ? 200 : 204)) {
if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
uci_confirm_auth = tok;
uci_confirm(checked, Date.now() + uci_apply_rollback * 1000);
}
else if (checked && r.status === 204) {
uci_status_message('notice', '<p><%:There are no changes to apply.%></p>');
window.setTimeout(function() {
<% if redirect_ok then -%>
location.href = decodeURIComponent('<%=luci.util.urlencode(redirect_ok)%>');
<%- else -%>
uci_status_message(false);
<%- end %>
}, uci_apply_display * 1000);
}
else {
uci_status_message('warning', '<p><%_Apply request failed with status <code>%h</code>%></p>'.format(r.responseText || r.statusText || r.status));
window.setTimeout(function() { uci_status_message(false); }, uci_apply_display * 1000);
}
});
}
function uci_revert() {
uci_status_message('notice spinning', '<p><%:Reverting configuration…%></p>');
xhr.post('<%=url("admin/uci/revert")%>', uci_apply_auth, function(r) {
if (r.status === 200) {
uci_status_message('notice', '<p><%:Changes have been reverted.%></p>');
window.setTimeout(function() {
<% if redirect_ok then -%>
location.href = decodeURIComponent('<%=luci.util.urlencode(redirect_ok)%>');
<%- else -%>
window.location = window.location.href.split('#')[0];
<%- end %>
}, uci_apply_display * 1000);
}
else {
uci_status_message('warning', '<p><%_Revert request failed with status <code>%h</code>%></p>'.format(r.statusText || r.status));
window.setTimeout(function() { uci_status_message(false); }, uci_apply_display * 1000);
}
});
}
//]]></script>
<%- end) %>

View file

@ -3,7 +3,7 @@
local descr = luci.util.trim(striptags(self.description))
local ftype = self.typename or (self.template and self.template:gsub("^.+/", ""))
-%>
<div class="td cbi-value-field<% if self.error and self.error[section] then %> cbi-value-error<% end %>"<%=
<div class="td cbi-value-field<% if self.error and self.error[section] then %> cbi-value-error<% end %><% if self.password then %> nowrap<% end %>"<%=
attr("data-name", self.option) ..
ifattr(ftype and #ftype > 0, "data-type", ftype) ..
ifattr(title and #title > 0, "data-title", title, true) ..

View file

@ -1,54 +1,19 @@
<%+cbi/valueheader%>
<%-
local selected = { }
if self.multiple then
local val
for val in luci.util.imatch(self:cfgvalue(section)) do
selected[val] = true
end
else
selected[self:cfgvalue(section)] = true
end
if not next(selected) and self.default then
selected[self.default] = true
end
-%>
<div class="cbi-dropdown"<%=
attr("name", cbid) ..
attr("display-items", self.display or self.size or 3) ..
attr("dropdown-items", self.dropdown or self.display or self.size or 5) ..
attr("placeholder", self.placeholder or translate("-- please select --")) ..
ifattr(self.multiple, "multiple", "multiple") ..
ifattr(self.optional or self.rmempty, "optional", "optional")
%>>
<ul>
<% local i, key; for i, key in pairs(self.keylist) do %>
<li<%=
attr("data-index", i) ..
attr("data-depends", self:deplist2json(section, self.deplist[i])) ..
attr("data-value", key) ..
ifattr(selected[key], "selected", "selected")
%>>
<%=pcdata(self.vallist[i])%>
</li>
<% end %>
<% if self.custom then %>
<li>
<input type="password" style="display:none" />
<input class="create-item-input" type="text"<%=
attr("placeholder", self.custom ~= true and
self.custom or
(self.multiple and
translate("Enter custom values") or
translate("Enter custom value")))
%> />
</li>
<% end %>
</ul>
</div>
<div<%=attr("data-ui-widget", luci.util.serialize_json({
"Dropdown", self:cfgvalue(section), self:choices(), {
id = cbid,
name = cbid,
sort = self.keylist,
multi = self.multiple,
datatype = self.datatype,
optional = self.optional or self.rmempty,
readonly = self.readonly,
maxlength = self.maxlength,
placeholder = self.placeholder,
display_items = self.display or self.size or 3,
dropdown_items = self.dropdown or self.display or self.size or 5,
custom_placeholder = self.custom or
(self.multiple and translate("Enter custom values") or translate("Enter custom value"))
}
}))%>></div>
<%+cbi/valuefooter%>

View file

@ -1,13 +1,12 @@
<%+cbi/valueheader%>
<div<%=
attr("data-prefix", cbid) ..
attr("data-browser-path", self.default_path) ..
attr("data-dynlist", luci.util.serialize_json({
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)
%>></div>
<div<%=attr("data-ui-widget", luci.util.serialize_json({
"DynamicList", self:cfgvalue(section), self:choices(), {
name = cbid,
size = self.size,
sort = self.keylist,
datatype = self.datatype,
optional = self.optional or self.rmempty,
placeholder = self.placeholder
}
}))%>></div>
<%+cbi/valuefooter%>

View file

@ -1,4 +1,4 @@
<div class="cbi-value<% if self.error and self.error[section] then %> cbi-value-error<% end %><% if self.last_child then %> cbi-value-last<% end %>" id="cbi-<%=self.config.."-"..section.."-"..self.option%>" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>">
<div class="cbi-value<% if self.error and self.error[section] then %> cbi-value-error<% end %><% if self.last_child then %> cbi-value-last<% end %><% if self.password then %> nowrap<% end %>" id="cbi-<%=self.config.."-"..section.."-"..self.option%>" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>">
<%- if self.title and #self.title > 0 then -%>
<label class="cbi-value-title"<%= attr("for", cbid) %>>
<%- if self.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=self.titleref%>"><%- end -%>

View file

@ -1,10 +1,12 @@
<%+cbi/valueheader%>
<input type="hidden" value="1"<%=
attr("name", "cbi.cbe." .. self.config .. "." .. section .. "." .. self.option)
%> />
<input class="cbi-input-checkbox" data-update="click change" type="checkbox"<%=
attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) ..
ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked")
%> />
<label<%= attr("for", cbid)%>></label>
<div<%=attr("data-ui-widget", luci.util.serialize_json({
"Checkbox", self:cfgvalue(section) or self.default, {
id = cbid,
name = cbid,
readonly = self.readonly,
hiddenname = "cbi.cbe." .. self.config .. "." .. section .. "." .. self.option,
value_enabled = self.enabled or 1,
value_disabled = self.disabled or 0
}
}))%>></div>
<%+cbi/valuefooter%>

View file

@ -1,43 +1,14 @@
<%
local i, key
local br = self.orientation == "horizontal" and '&#160;' or '<br />'
%>
<%+cbi/valueheader%>
<% if self.widget == "select" then %>
<select class="cbi-input-select" data-update="change"<%=
attr("id", cbid) ..
attr("name", cbid) ..
ifattr(self.size, "size")
%>>
<% for i, key in pairs(self.keylist) do -%>
<option<%=
attr("id", cbid.."-"..key) ..
attr("value", key) ..
attr("data-index", i) ..
attr("data-depends", self:deplist2json(section, self.deplist[i])) ..
ifattr(tostring(self:cfgvalue(section) or self.default) == key, "selected", "selected")
%>><%=pcdata(self.vallist[i])%></option>
<%- end %>
</select>
<% elseif self.widget == "radio" then %>
<div>
<% for i, key in pairs(self.keylist) do %>
<label<%=
attr("data-index", i) ..
attr("data-depends", self:deplist2json(section, self.deplist[i]))
%>>
<input class="cbi-input-radio" data-update="click change" type="radio"<%=
attr("id", cbid.."-"..key) ..
attr("name", cbid) ..
attr("value", key) ..
ifattr((self:cfgvalue(section) or self.default) == key, "checked", "checked")
%> />
<label<%= attr("for", cbid.."-"..key)%>></label>
<%=pcdata(self.vallist[i])%>
</label>
<% if i == self.size then write(br) end %>
<% end %>
</div>
<% end %>
<div<%=attr("data-ui-widget", luci.util.serialize_json({
"Select", self:cfgvalue(section), self:choices(), {
id = cbid,
name = cbid,
size = self.size,
sort = self.keylist,
widget = self.widget,
datatype = self.datatype,
optional = self.optional or self.rmempty,
placeholder = self.placeholder
}
}))%>></div>
<%+cbi/valuefooter%>

View file

@ -1,43 +1,24 @@
<%
local i, key
local v = self:valuelist(section) or {}
-%>
<%+cbi/valueheader%>
<% if self.widget == "select" then %>
<select class="cbi-input-select" multiple="multiple" data-update="click change"<%=
attr("id", cbid) ..
attr("name", cbid) ..
ifattr(self.size, "size")
%>>
<% for i, key in pairs(self.keylist) do -%>
<option<%=
attr("id", cbid.."-"..key) ..
attr("value", key) ..
attr("data-index", i) ..
attr("data-depends", self:deplist2json(section, self.deplist[i])) ..
ifattr(luci.util.contains(v, key), "selected", "selected")
%>><%=pcdata(self.vallist[i])%></option>
<%- end %>
</select>
<% elseif self.widget == "checkbox" then %>
<div>
<% for i, key in pairs(self.keylist) do %>
<label<%=
attr("data-index", i) ..
attr("data-depends", self:deplist2json(section, self.deplist[i]))
%>>
<input class="cbi-input-checkbox" type="checkbox" data-update="click change"<%=
attr("id", cbid.."-"..key) ..
attr("name", cbid) ..
attr("value", key) ..
ifattr(luci.util.contains(v, key), "checked", "checked")
%> />
<label<%= attr("for", cbid.."-"..key)%>></label>
<%=pcdata(self.vallist[i])%>
</label>
<% if self.size and (i % self.size) == 0 then write('<br />') end %>
<% end %>
</div>
<% end %>
<%
local util = require "luci.util"
local values = {}
local value
for value in util.imatch(self:cfgvalue(section) or self.default) do
values[#values+1] = value
end
%>
<div<%=attr("data-ui-widget", luci.util.serialize_json({
"Select", values, self:choices(), {
id = cbid,
name = cbid,
size = self.size,
sort = self.keylist,
multi = true,
widget = self.widget,
datatype = self.datatype,
optional = self.optional or self.rmempty,
readonly = self.readonly,
placeholder = self.placeholder
}
}))%>></div>
<%+cbi/valuefooter%>

View file

@ -1,26 +1,35 @@
<%+cbi/valueheader%>
<%- if self.password then -%>
<input type="password" style="position:absolute; left:-2000px" aria-hidden="true" tabindex="-1"<%=
attr("name", "password." .. cbid)
%> />
<%- end -%>
<input data-update="change"<%=
attr("id", cbid) ..
attr("name", cbid) ..
attr("type", self.password and "password" or "text") ..
attr("class", self.password and "cbi-input-password" or "cbi-input-text") ..
attr("value", self:cfgvalue(section) or self.default) ..
ifattr(self.password, "autocomplete", "new-password") ..
ifattr(self.size, "size") ..
ifattr(self.placeholder, "placeholder") ..
ifattr(self.readonly, "readonly") ..
ifattr(self.maxlength, "maxlength") ..
ifattr(self.datatype, "data-type", self.datatype) ..
ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist })
%> />
<%- if self.password then -%>
<button class="cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" aria-label="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'; event.preventDefault()"></button>
<% end %>
<% local choices = self:choices()
if choices then %>
<div<%=attr("data-ui-widget", luci.util.serialize_json({
"Combobox", self:cfgvalue(section) or self.default, choices, {
id = cbid,
name = cbid,
size = self.size,
sort = self.keylist,
datatype = self.datatype,
optional = self.optional or self.rmempty,
readonly = self.readonly,
maxlength = self.maxlength,
placeholder = self.placeholder,
custom_placeholder = self.combobox_manual
}
}))%>></div>
<% else %>
<div<%=attr("data-ui-widget", luci.util.serialize_json({
"Textfield", self:cfgvalue(section) or self.default, {
id = cbid,
name = cbid,
size = self.size,
datatype = self.datatype,
optional = self.optional or self.rmempty,
password = self.password,
readonly = self.readonly,
maxlength = self.maxlength,
placeholder = self.placeholder
}
}))%>></div>
<% end %>
<%+cbi/valuefooter%>

View file

@ -4,6 +4,7 @@
var freqlist = <%= luci.http.write_json(self.iwinfo.freqlist) %>;
var hwmodes = <%= luci.http.write_json(self.iwinfo.hwmodelist or {}) %>;
var htmodes = <%= luci.http.write_json(self.iwinfo.htmodelist) %>;
var acs = <%= luci.http.write_json(self.hostapd_acs or 0) %>;
var channels = {
'11g': [
@ -14,6 +15,10 @@
]
};
if (acs < 1) {
channels[(freqlist[freqlist.length - 1].mhz > 2484) ? '11a' : '11g'].length = 0;
}
for (var i = 0; i < freqlist.length; i++)
channels[(freqlist[i].mhz > 2484) ? '11a' : '11g'].push(
freqlist[i].channel,

View file

@ -1,6 +1,6 @@
<%#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
Copyright 2008-2019 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-%>
@ -8,18 +8,15 @@
local is_rollback_pending, rollback_time_remaining, rollback_session, rollback_token = luci.model.uci:rollback_pending()
if is_rollback_pending or trigger_apply or trigger_revert then
include("cbi/apply_widget")
cbi_apply_widget(redirect, rollback_token)
%>
<div class="alert-message" id="cbi_apply_status" style="display:none"></div>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("luci-loaded", function() {
<% if trigger_apply then -%>
uci_apply(true);
L.ui.changes.apply(true);
<%- elseif trigger_revert then -%>
uci_revert();
L.ui.changes.revert();
<%- else -%>
uci_confirm(true, Date.now() + <%=rollback_time_remaining%> * 1000);
L.ui.changes.confirm(true, Date.now() + <%=rollback_time_remaining%> * 1000, <%=luci.http.write_json(rollback_token)%>);
<%- end %>
});
</script>

View file

@ -1,6 +1,6 @@
<%#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
Copyright 2008-2019 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-%>
@ -9,16 +9,25 @@
include("themes/" .. theme .. "/header")
luci.dispatcher.context.template_header_sent = true
end
local applyconf = luci.config and luci.config.apply
%>
<script type="text/javascript" src="<%=resource%>/promis.min.js"></script>
<script type="text/javascript" src="<%=resource%>/luci.js"></script>
<script type="text/javascript">
L = new LuCI(<%= luci.http.write_json({
token = token,
resource = resource,
scriptname = luci.http.getenv("SCRIPT_NAME"),
pathinfo = luci.http.getenv("PATH_INFO"),
requestpath = luci.dispatcher.context.requestpath,
pollinterval = luci.config.main.pollinterval or 5
token = token,
resource = resource,
scriptname = luci.http.getenv("SCRIPT_NAME"),
pathinfo = luci.http.getenv("PATH_INFO"),
requestpath = luci.dispatcher.context.requestpath,
pollinterval = luci.config.main.pollinterval or 5,
sessionid = luci.dispatcher.context.authsession,
apply_rollback = math.max(applyconf and applyconf.rollback or 30, 30),
apply_holdoff = math.max(applyconf and applyconf.holdoff or 4, 1),
apply_timeout = math.max(applyconf and applyconf.timeout or 5, 1),
apply_display = math.max(applyconf and applyconf.display or 1.5, 1),
rollback_token = rollback_token
}) %>);
</script>

View file

@ -79,17 +79,24 @@
</div>
</div>
<div class="cbi-section" style="display:none">
<h3><%:Active DHCPv6 Leases%></h3>
<div class="table" id="lease6_status_table">
<div class="tr table-titles">
<div class="th"><%:Host%></div>
<div class="th"><%:IPv6-Address%></div>
<div class="th"><%:DUID%></div>
<div class="th"><%:Leasetime remaining%></div>
</div>
<div class="tr placeholder">
<div class="td"><em><%:Collecting data...%></em></div>
<%
local fs = require "nixio.fs"
local has_ipv6 = fs.access("/proc/net/ipv6_route")
if has_ipv6 then
-%>
<div class="cbi-section" style="display:none">
<h3><%:Active DHCPv6 Leases%></h3>
<div class="table" id="lease6_status_table">
<div class="tr table-titles">
<div class="th"><%:Host%></div>
<div class="th"><%:IPv6-Address%></div>
<div class="th"><%:DUID%></div>
<div class="th"><%:Leasetime remaining%></div>
</div>
<div class="tr placeholder">
<div class="td"><em><%:Collecting data...%></em></div>
</div>
</div>
</div>
</div>
<% end -%>

View file

@ -0,0 +1,8 @@
<%+header%>
<div id="view">
<div class="spinning"><%:Loading view…%></div>
<script type="text/javascript">L.require('view.<%=view%>');</script>
</div>
<%+footer%>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,350 @@
#!/usr/bin/env lua
local json = require "luci.jsonc"
local fs = require "nixio.fs"
local function readfile(path)
local s = fs.readfile(path)
return s and (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
local methods = {
initList = {
args = { name = "name" },
call = function(args)
local sys = require "luci.sys"
local _, name, scripts = nil, nil, {}
for _, name in ipairs(args.name and { args.name } or sys.init.names()) do
local index = sys.init.index(name)
if index then
scripts[name] = { index = index, enabled = sys.init.enabled(name) }
else
return { error = "No such init script" }
end
end
return { result = scripts }
end
},
initCall = {
args = { name = "name", action = "action" },
call = function(args)
local sys = require "luci.sys"
if type(sys.init[args.action]) ~= "function" then
return { error = "Invalid action" }
end
return { result = sys.init[args.action](args.name) }
end
},
getLocaltime = {
call = function(args)
return { localtime = os.time() }
end
},
setLocaltime = {
args = { localtime = 0 },
call = function(args)
local sys = require "luci.sys"
local date = os.date("*t", args.localtime)
if date then
sys.call("date -s '%04d-%02d-%02d %02d:%02d:%02d' >/dev/null" %{ date.year, date.month, date.day, date.hour, date.min, date.sec })
sys.call("/etc/init.d/sysfixtime restart >/dev/null")
end
return { localtime = args.localtime }
end
},
timezone = {
call = function(args)
local util = require "luci.util"
local zones = require "luci.sys.zoneinfo"
local tz = readfile("/etc/TZ")
local res = util.ubus("uci", "get", {
config = "system",
section = "@system[0]",
option = "zonename"
})
local result = {}
local _, zone
for _, zone in ipairs(zones.TZ) do
result[zone[1]] = {
tzstring = zone[2],
active = (res and res.value == zone[1]) and true or nil
}
end
return { result = result }
end
},
leds = {
call = function()
local iter = fs.dir("/sys/class/leds")
local result = { }
if iter then
local led
for led in iter do
local m, s
result[led] = { triggers = {} }
s = readfile("/sys/class/leds/"..led.."/trigger")
for s in (s or ""):gmatch("%S+") do
m = s:match("^%[(.+)%]$")
result[led].triggers[#result[led].triggers+1] = m or s
result[led].active_trigger = m or result[led].active_trigger
end
s = readfile("/sys/class/leds/"..led.."/brightness")
if s then
result[led].brightness = tonumber(s)
end
s = readfile("/sys/class/leds/"..led.."/max_brightness")
if s then
result[led].max_brightness = tonumber(s)
end
end
end
return result
end
},
usb = {
call = function()
local fs = require "nixio.fs"
local iter = fs.glob("/sys/bus/usb/devices/[0-9]*/manufacturer")
local result = { }
if iter then
result.devices = {}
local p
for p in iter do
local id = p:match("%d+-%d+")
result.devices[#result.devices+1] = {
id = id,
vid = readfile("/sys/bus/usb/devices/"..id.."/idVendor"),
pid = readfile("/sys/bus/usb/devices/"..id.."/idProduct"),
vendor = readfile("/sys/bus/usb/devices/"..id.."/manufacturer"),
product = readfile("/sys/bus/usb/devices/"..id.."/product"),
speed = tonumber((readfile("/sys/bus/usb/devices/"..id.."/product")))
}
end
end
iter = fs.glob("/sys/bus/usb/devices/*/usb[0-9]*-port[0-9]*")
if iter then
result.ports = {}
local p
for p in iter do
local bus, port = p:match("usb(%d+)-port(%d+)")
result.ports[#result.ports+1] = {
hub = tonumber(bus),
port = tonumber(port)
}
end
end
return result
end
},
ifaddrs = {
call = function()
return { result = nixio.getifaddrs() }
end
},
host_hints = {
call = function()
local sys = require "luci.sys"
return sys.net.host_hints()
end
},
duid_hints = {
call = function()
local fp = io.open('/var/hosts/odhcpd')
local result = { }
if fp then
for line in fp:lines() do
local dev, duid, name = string.match(line, '# (%S+)%s+(%S+)%s+%d+%s+(%S+)')
if dev and duid and name then
result[duid] = {
name = (name ~= "-") and name or nil,
device = dev
}
end
end
fp:close()
end
return result
end
},
leases = {
args = { family = 0 },
call = function(args)
local s = require "luci.tools.status"
if args.family == 4 then
return { dhcp_leases = s.dhcp_leases() }
elseif args.family == 6 then
return { dhcp6_leases = s.dhcp6_leases() }
else
return {
dhcp_leases = s.dhcp_leases(),
dhcp6_leases = s.dhcp6_leases()
}
end
end
},
netdevs = {
call = function(args)
local dir = fs.dir("/sys/class/net")
local result = { }
if dir then
local dev
for dev in dir do
if not result[dev] then
result[dev] = { name = dev }
end
if fs.access("/sys/class/net/"..dev.."/master") then
local brname = fs.basename(fs.readlink("/sys/class/net/"..dev.."/master"))
if not result[brname] then
result[brname] = { name = brname }
end
if not result[brname].ports then
result[brname].ports = { }
end
result[brname].ports[#result[brname].ports+1] = dev
elseif fs.access("/sys/class/net/"..dev.."/bridge") then
if not result[dev].ports then
result[dev].ports = { }
end
result[dev].id = readfile("/sys/class/net/"..dev.."/bridge/bridge_id")
result[dev].stp = (readfile("/sys/class/net/"..dev.."/bridge/stp_state") ~= "0")
result[dev].bridge = true
end
local opr = readfile("/sys/class/net/"..dev.."/operstate")
result[dev].up = (opr == "up" or opr == "unknown")
result[dev].type = tonumber(readfile("/sys/class/net/"..dev.."/type"))
result[dev].name = dev
local mtu = tonumber(readfile("/sys/class/net/"..dev.."/mtu"))
if mtu and mtu > 0 then
result[dev].mtu = mtu
end
local qlen = tonumber(readfile("/sys/class/net/"..dev.."/tx_queue_len"))
if qlen and qlen > 0 then
result[dev].qlen = qlen
end
local master = fs.readlink("/sys/class/net/"..dev.."/master")
if master then
result[dev].master = fs.basename(master)
end
local mac = readfile("/sys/class/net/"..dev.."/address")
if mac and #mac == 17 then
result[dev].mac = mac
end
end
end
return result
end
},
boardjson = {
call = function(args)
local jsc = require "luci.jsonc"
return jsc.parse(fs.readfile("/etc/board.json") or "")
end
},
offload_support = {
call = function()
local fs = require "nixio.fs"
return { offload_support = not not fs.access("/sys/module/xt_FLOWOFFLOAD/refcnt") }
end
}
}
local function parseInput()
local parse = json.new()
local done, err
while true do
local chunk = io.read(4096)
if not chunk then
break
elseif not done and not err then
done, err = parse:parse(chunk)
end
end
if not done then
print(json.stringify({ error = err or "Incomplete input" }))
os.exit(1)
end
return parse:get()
end
local function validateArgs(func, uargs)
local method = methods[func]
if not method then
print(json.stringify({ error = "Method not found" }))
os.exit(1)
end
if type(uargs) ~= "table" then
print(json.stringify({ error = "Invalid arguments" }))
os.exit(1)
end
uargs.ubus_rpc_session = nil
local k, v
local margs = method.args or {}
for k, v in pairs(uargs) do
if margs[k] == nil or
(v ~= nil and type(v) ~= type(margs[k]))
then
print(json.stringify({ error = "Invalid arguments" }))
os.exit(1)
end
end
return method
end
if arg[1] == "list" then
local _, method, rv = nil, nil, {}
for _, method in pairs(methods) do rv[_] = method.args or {} end
print((json.stringify(rv):gsub(":%[%]", ":{}")))
elseif arg[1] == "call" then
local args = parseInput()
local method = validateArgs(arg[2], args)
local result, code = method.call(args)
print((json.stringify(result):gsub("^%[%]$", "{}")))
os.exit(code or 0)
end

View file

@ -7,5 +7,26 @@
"write": {
"uci": [ "*" ]
}
},
"luci-access": {
"description": "Grant access to basic LuCI procedures",
"read": {
"ubus": {
"iwinfo": [ "info" ],
"luci": [ "boardjson", "duid_hints", "host_hints", "ifaddrs", "initList", "getLocaltime", "leases", "leds", "netdevs", "offload_support", "usb" ],
"network.device": [ "status" ],
"network.interface": [ "dump" ],
"network.wireless": [ "status" ],
"uci": [ "changes", "get" ]
},
"uci": [ "*" ]
},
"write": {
"ubus": {
"luci": [ "initCall", "setLocaltime", "timezone" ],
"uci": [ "add", "apply", "confirm", "delete", "order", "set" ]
},
"uci": [ "*" ]
}
}
}

View file

@ -0,0 +1,410 @@
'use strict';
'require rpc';
'require uci';
'require form';
var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus;
callHostHints = rpc.declare({
object: 'luci',
method: 'host_hints'
});
callDUIDHints = rpc.declare({
object: 'luci',
method: 'duid_hints'
});
callDHCPLeases = rpc.declare({
object: 'luci',
method: 'leases',
params: [ 'family' ],
expect: { dhcp_leases: [] }
});
CBILeaseStatus = form.DummyValue.extend({
renderWidget: function(section_id, option_id, cfgvalue) {
return E([
E('h4', _('Active DHCP Leases')),
E('div', { 'id': 'lease_status_table', 'class': 'table' }, [
E('div', { 'class': 'tr table-titles' }, [
E('div', { 'class': 'th' }, _('Hostname')),
E('div', { 'class': 'th' }, _('IPv4-Address')),
E('div', { 'class': 'th' }, _('MAC-Address')),
E('div', { 'class': 'th' }, _('Leasetime remaining'))
]),
E('div', { 'class': 'tr placeholder' }, [
E('div', { 'class': 'td' }, E('em', _('Collecting data...')))
])
])
]);
}
});
return L.view.extend({
load: function() {
return Promise.all([
callHostHints(),
callDUIDHints()
]);
},
render: function(hosts_duids) {
var hosts = hosts_duids[0],
duids = hosts_duids[1],
m, s, o, ss, so;
m = new form.Map('dhcp', _('DHCP and DNS'), _('Dnsmasq is a combined <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server and <abbr title="Domain Name System">DNS</abbr>-Forwarder for <abbr title="Network Address Translation">NAT</abbr> firewalls'));
m.tabbed = true;
s = m.section(form.TypedSection, 'dnsmasq', _('Server Settings'));
s.anonymous = true;
s.addremove = false;
s.tab('general', _('General Settings'));
s.tab('files', _('Resolv and Hosts Files'));
s.tab('tftp', _('TFTP Settings'));
s.tab('advanced', _('Advanced Settings'));
s.tab('leases', _('Static Leases'));
s.taboption('general', form.Flag, 'domainneeded',
_('Domain required'),
_('Don\'t forward <abbr title="Domain Name System">DNS</abbr>-Requests without <abbr title="Domain Name System">DNS</abbr>-Name'));
s.taboption('general', form.Flag, 'authoritative',
_('Authoritative'),
_('This is the only <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> in the local network'));
s.taboption('files', form.Flag, 'readethers',
_('Use <code>/etc/ethers</code>'),
_('Read <code>/etc/ethers</code> to configure the <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server'));
s.taboption('files', form.Value, 'leasefile',
_('Leasefile'),
_('file where given <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-leases will be stored'));
s.taboption('files', form.Flag, 'noresolv',
_('Ignore resolve file')).optional = true;
o = s.taboption('files', form.Value, 'resolvfile',
_('Resolve file'),
_('local <abbr title="Domain Name System">DNS</abbr> file'));
o.depends('noresolv', '');
o.optional = true;
s.taboption('files', form.Flag, 'nohosts',
_('Ignore <code>/etc/hosts</code>')).optional = true;
s.taboption('files', form.DynamicList, 'addnhosts',
_('Additional Hosts files')).optional = true;
o = s.taboption('advanced', form.Flag, 'quietdhcp',
_('Suppress logging'),
_('Suppress logging of the routine operation of these protocols'));
o.optional = true;
o = s.taboption('advanced', form.Flag, 'sequential_ip',
_('Allocate IP sequentially'),
_('Allocate IP addresses sequentially, starting from the lowest available address'));
o.optional = true;
o = s.taboption('advanced', form.Flag, 'boguspriv',
_('Filter private'),
_('Do not forward reverse lookups for local networks'));
o.default = o.enabled;
s.taboption('advanced', form.Flag, 'filterwin2k',
_('Filter useless'),
_('Do not forward requests that cannot be answered by public name servers'));
s.taboption('advanced', form.Flag, 'localise_queries',
_('Localise queries'),
_('Localise hostname depending on the requesting subnet if multiple IPs are available'));
//local have_dnssec_support = luci.util.checklib('/usr/sbin/dnsmasq', 'libhogweed.so');
var have_dnssec_support = true;
if (have_dnssec_support) {
o = s.taboption('advanced', form.Flag, 'dnssec',
_('DNSSEC'));
o.optional = true;
o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
_('DNSSEC check unsigned'),
_('Requires upstream supports DNSSEC; verify unsigned domain responses really come from unsigned domains'));
o.optional = true;
}
s.taboption('general', form.Value, 'local',
_('Local server'),
_('Local domain specification. Names matching this domain are never forwarded and are resolved from DHCP or hosts files only'));
s.taboption('general', form.Value, 'domain',
_('Local domain'),
_('Local domain suffix appended to DHCP names and hosts file entries'));
s.taboption('advanced', form.Flag, 'expandhosts',
_('Expand hosts'),
_('Add local domain suffix to names served from hosts files'));
s.taboption('advanced', form.Flag, 'nonegcache',
_('No negative cache'),
_('Do not cache negative replies, e.g. for not existing domains'));
s.taboption('advanced', form.Value, 'serversfile',
_('Additional servers file'),
_('This file may contain lines like \'server=/domain/1.2.3.4\' or \'server=1.2.3.4\' for domain-specific or full upstream <abbr title="Domain Name System">DNS</abbr> servers.'));
s.taboption('advanced', form.Flag, 'strictorder',
_('Strict order'),
_('<abbr title="Domain Name System">DNS</abbr> servers will be queried in the order of the resolvfile')).optional = true;
s.taboption('advanced', form.Flag, 'allservers',
_('All Servers'),
_('Query all available upstream <abbr title="Domain Name System">DNS</abbr> servers')).optional = true;
o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain', _('Bogus NX Domain Override'),
_('List of hosts that supply bogus NX domain results'));
o.optional = true;
o.placeholder = '67.215.65.132';
s.taboption('general', form.Flag, 'logqueries',
_('Log queries'),
_('Write received DNS requests to syslog')).optional = true;
o = s.taboption('general', form.DynamicList, 'server', _('DNS forwardings'),
_('List of <abbr title="Domain Name System">DNS</abbr> servers to forward requests to'));
o.optional = true;
o.placeholder = '/example.org/10.1.2.3';
o = s.taboption('general', form.Flag, 'rebind_protection',
_('Rebind protection'),
_('Discard upstream RFC1918 responses'));
o.rmempty = false;
o = s.taboption('general', form.Flag, 'rebind_localhost',
_('Allow localhost'),
_('Allow upstream responses in the 127.0.0.0/8 range, e.g. for RBL services'));
o.depends('rebind_protection', '1');
o = s.taboption('general', form.DynamicList, 'rebind_domain',
_('Domain whitelist'),
_('List of domains to allow RFC1918 responses for'));
o.optional = true;
o.depends('rebind_protection', '1');
o.datatype = 'host(1)';
o.placeholder = 'ihost.netflix.com';
o = s.taboption('advanced', form.Value, 'port',
_('<abbr title="Domain Name System">DNS</abbr> server port'),
_('Listening port for inbound DNS queries'));
o.optional = true;
o.datatype = 'port';
o.placeholder = 53;
o = s.taboption('advanced', form.Value, 'queryport',
_('<abbr title="Domain Name System">DNS</abbr> query port'),
_('Fixed source port for outbound DNS queries'));
o.optional = true;
o.datatype = 'port';
o.placeholder = _('any');
o = s.taboption('advanced', form.Value, 'dhcpleasemax',
_('<abbr title="maximal">Max.</abbr> <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> leases'),
_('Maximum allowed number of active DHCP leases'));
o.optional = true;
o.datatype = 'uinteger';
o.placeholder = _('unlimited');
o = s.taboption('advanced', form.Value, 'ednspacket_max',
_('<abbr title="maximal">Max.</abbr> <abbr title="Extension Mechanisms for Domain Name System">EDNS0</abbr> packet size'),
_('Maximum allowed size of EDNS.0 UDP packets'));
o.optional = true;
o.datatype = 'uinteger';
o.placeholder = 1280;
o = s.taboption('advanced', form.Value, 'dnsforwardmax',
_('<abbr title="maximal">Max.</abbr> concurrent queries'),
_('Maximum allowed number of concurrent DNS queries'));
o.optional = true;
o.datatype = 'uinteger';
o.placeholder = 150;
o = s.taboption('advanced', form.Value, 'cachesize',
_('Size of DNS query cache'),
_('Number of cached DNS entries (max is 10000, 0 is no caching)'));
o.optional = true;
o.datatype = 'range(0,10000)';
o.placeholder = 150;
s.taboption('tftp', form.Flag, 'enable_tftp',
_('Enable TFTP server')).optional = true;
o = s.taboption('tftp', form.Value, 'tftp_root',
_('TFTP server root'),
_('Root directory for files served via TFTP'));
o.optional = true;
o.depends('enable_tftp', '1');
o.placeholder = '/';
o = s.taboption('tftp', form.Value, 'dhcp_boot',
_('Network boot image'),
_('Filename of the boot image advertised to clients'));
o.optional = true;
o.depends('enable_tftp', '1');
o.placeholder = 'pxelinux.0';
o = s.taboption('general', form.Flag, 'localservice',
_('Local Service Only'),
_('Limit DNS service to subnets interfaces on which we are serving DNS.'));
o.optional = false;
o.rmempty = false;
o = s.taboption('general', form.Flag, 'nonwildcard',
_('Non-wildcard'),
_('Bind dynamically to interfaces rather than wildcard address (recommended as linux default)'));
o.optional = false;
o.rmempty = true;
o = s.taboption('general', form.DynamicList, 'interface',
_('Listen Interfaces'),
_('Limit listening to these interfaces, and loopback.'));
o.optional = true;
o = s.taboption('general', form.DynamicList, 'notinterface',
_('Exclude interfaces'),
_('Prevent listening on these interfaces.'));
o.optional = true;
o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
_('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '<br />' +
_('Use the <em>Add</em> Button to add a new lease entry. The <em>MAC-Address</em> identifies the host, the <em>IPv4-Address</em> specifies the fixed address to use, and the <em>Hostname</em> is assigned as a symbolic name to the requesting host. The optional <em>Lease time</em> can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.'));
ss = o.subsection;
ss.addremove = true;
ss.anonymous = true;
so = ss.option(form.Value, 'name', _('Hostname'));
so.datatype = 'hostname("strict")';
so.rmempty = true;
so.write = function(section, value) {
uci.set('dhcp', section, 'name', value);
uci.set('dhcp', section, 'dns', '1');
};
so.remove = function(section) {
uci.unset('dhcp', section, 'name');
uci.unset('dhcp', section, 'dns');
};
so = ss.option(form.Value, 'mac', _('<abbr title="Media Access Control">MAC</abbr>-Address'));
so.datatype = 'list(unique(macaddr))';
so.rmempty = true;
so.cfgvalue = function(section) {
var macs = uci.get('dhcp', section, 'mac'),
result = [];
if (!Array.isArray(macs))
macs = (macs != null && macs != '') ? macs.split(/\ss+/) : [];
for (var i = 0, mac; (mac = macs[i]) != null; i++)
if (/^([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2})$/.test(mac))
result.push('%02X:%02X:%02X:%02X:%02X:%02X'.format(
parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)));
return result.length ? result.join(' ') : null;
};
Object.keys(hosts).forEach(function(mac) {
so.value(mac);
});
so = ss.option(form.Value, 'ip', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address'));
so.datatype = 'or(ip4addr,"ignore")';
so.validate = function(section, value) {
var mac = this.map.lookupOption('mac', section),
name = this.map.lookupOption('name', section),
m = mac ? mac[0].formvalue(section) : null,
n = name ? name[0].formvalue(section) : null;
if ((m == null || m == '') && (n == null || n == ''))
return _('One of hostname or mac address must be specified!');
return true;
};
Object.keys(hosts).forEach(function(mac) {
if (hosts[mac].ipv4)
so.value(hosts[mac].ipv4);
});
so = ss.option(form.Value, 'leasetime', _('Lease time'));
so.rmempty = true;
so = ss.option(form.Value, 'duid', _('<abbr title="The DHCP Unique Identifier">DUID</abbr>'));
so.datatype = 'and(rangelength(20,36),hexstring)';
Object.keys(duids).forEach(function(duid) {
so.value(duid, '%s (%s)'.format(duid, duids[duid].name || '?'));
});
so = ss.option(form.Value, 'hostid', _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Suffix (hex)'));
o = s.taboption('leases', CBILeaseStatus, '__status__');
return m.render().then(function(mapEl) {
L.Poll.add(function() {
return callDHCPLeases(4).then(function(leases) {
cbi_update_table(mapEl.querySelector('#lease_status_table'),
leases.map(function(lease) {
var exp;
if (lease.expires === false)
exp = E('em', _('unlimited'));
else if (lease.expires <= 0)
exp = E('em', _('expired'));
else
exp = '%t'.format(lease.expires);
return [
lease.hostname || '?',
lease.ipaddr,
lease.macaddr,
exp
];
}),
E('em', _('There are no active leases')));
});
});
return mapEl;
});
}
});

View file

@ -0,0 +1,41 @@
'use strict';
'require rpc';
'require form';
return L.view.extend({
callHostHints: rpc.declare({
object: 'luci',
method: 'host_hints'
}),
load: function() {
return this.callHostHints();
},
render: function(hosts) {
var m, s, o;
m = new form.Map('dhcp', _('Hostnames'));
s = m.section(form.GridSection, 'domain', _('Host entries'));
s.addremove = true;
s.anonymous = true;
s.sortable = true;
o = s.option(form.Value, 'name', _('Hostname'));
o.datatype = 'hostname';
o.rmempty = true;
o = s.option(form.Value, 'ip', _('IP address'));
o.datatype = 'ipaddr';
o.rmempty = true;
L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) {
o.value(hosts[mac].ipv4, '%s (%s)'.format(
hosts[mac].ipv4,
hosts[mac].name || mac
));
});
return m.render();
}
});

View file

@ -0,0 +1,102 @@
'use strict';
'require form';
'require network';
'require tools.widgets as widgets';
return L.view.extend({
load: function() {
return network.getDevices();
},
render: function(netdevs) {
var m, s, o;
m = new form.Map('network', _('Routes'), _('Routes specify over which interface and gateway a certain host or network can be reached.'));
m.tabbed = true;
for (var i = 4; i <= 6; i += 2) {
s = m.section(form.GridSection, (i == 4) ? 'route' : 'route6', (i == 4) ? _('Static IPv4 Routes') : _('Static IPv6 Routes'));
s.anonymous = true;
s.addremove = true;
s.sortable = true;
s.tab('general', _('General Settings'));
s.tab('advanced', _('Advanced Settings'));
o = s.taboption('general', widgets.NetworkSelect, 'interface', _('Interface'));
o.rmempty = false;
o.nocreate = true;
o = s.taboption('general', form.Value, 'target', _('Target'), (i == 4) ? _('Host-<abbr title="Internet Protocol Address">IP</abbr> or Network') : _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Address or Network (CIDR)'));
o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr';
o.rmempty = false;
if (i == 4) {
o = s.taboption('general', form.Value, 'netmask', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Netmask'), _('if target is a network'));
o.placeholder = '255.255.255.255';
o.datatype = 'ip4addr';
o.rmempty = true;
}
o = s.taboption('general', form.Value, 'gateway', (i == 4) ? _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Gateway') : _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Gateway'));
o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr';
o.rmempty = true;
o = s.taboption('advanced', form.Value, 'metric', _('Metric'));
o.placeholder = 0;
o.datatype = (i == 4) ? 'range(0,255)' : 'range(0,65535)';
o.rmempty = true;
o.textvalue = function(section_id) {
return this.cfgvalue(section_id) || 0;
};
o = s.taboption('advanced', form.Value, 'mtu', _('MTU'));
o.placeholder = 1500;
o.datatype = 'range(64,9000)';
o.rmempty = true;
o.modalonly = true;
o = s.taboption('advanced', form.ListValue, 'type', _('Route type'));
o.value('', 'unicast');
o.value('local');
o.value('broadcast');
o.value('multicast');
o.value('unreachable');
o.value('prohibit');
o.value('blackhole');
o.value('anycast');
o.default = '';
o.rmempty = true;
o.modalonly = true;
o = s.taboption('advanced', form.Value, 'table', _('Route table'));
o.value('local', 'local (255)');
o.value('main', 'main (254)');
o.value('default', 'default (253)');
o.rmempty = true;
o.modalonly = true;
o.cfgvalue = function(section_id) {
var cfgvalue = this.super('cfgvalue', [section_id]);
return cfgvalue || 'main';
};
o = s.taboption('advanced', form.Value, 'source', _('Source Address'));
o.placeholder = E('em', _('automatic'));
for (var j = 0; j < netdevs.length; j++) {
var addrs = netdevs[j].getIPAddrs();
for (var k = 0; k < addrs.length; k++)
o.value(addrs[k].split('/')[0]);
}
o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr';
o.default = '';
o.rmempty = true;
o.modalonly = true;
o = s.taboption('advanced', form.Flag, 'onlink', _('On-Link route'));
o.default = o.disabled;
o.rmempty = true;
}
return m.render();
}
});

View file

@ -103,18 +103,21 @@ function index()
if nixio.fs.access("/etc/config/dhcp") then
page = node("admin", "network", "dhcp")
page.target = cbi("admin_network/dhcp")
--page.target = cbi("admin_network/dhcp")
page.target = view("network/dhcp")
page.title = _("DHCP and DNS")
page.order = 30
page = node("admin", "network", "hosts")
page.target = cbi("admin_network/hosts")
--page.target = cbi("admin_network/hosts")
page.target = view("network/hosts")
page.title = _("Hostnames")
page.order = 40
end
page = node("admin", "network", "routes")
page.target = cbi("admin_network/routes")
--page.target = cbi("admin_network/routes")
page.target = view("network/routes")
page.title = _("Static Routes")
page.order = 50

View file

@ -413,6 +413,7 @@ form .clearfix:after,
label,
input,
button,
select,
textarea {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
@ -583,13 +584,13 @@ textarea {
color: #bfbfbf;
}
.item::after, .btn, .cbi-button, input, textarea {
.item::after, .btn, .cbi-button, input, button, 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,
.btn:hover, .cbi-button:hover, button:hover,
input:focus, textarea:focus {
outline: 0;
border-color: rgba(82, 168, 236, 0.8) !important;
@ -603,9 +604,11 @@ input[type=file]:focus, input[type=checkbox]:focus, select:focus {
}
input[disabled],
button[disabled],
select[disabled],
textarea[disabled],
input[readonly],
button[readonly],
select[readonly],
textarea[readonly] {
background-color: #f5f5f5;
@ -740,6 +743,16 @@ textarea[readonly] {
line-height: 3em;
}
.tr.drag-over-above,
.tr.drag-over-below {
border: 2px solid #0069d6;
border-width: 2px 0 0 0;
}
.tr.drag-over-below {
border-width: 0 0 2px 0;
}
/* Patterns.less
* Repeatable UI elements outside the base styles provided from the scaffolding
* ---------------------------------------------------------------------------- */
@ -1988,6 +2001,7 @@ table table td,
.network-status-table .ifacebox-body > span {
flex: 10 10 auto;
height: 100%;
}
.network-status-table .ifacebox-body > div {
@ -2051,6 +2065,11 @@ div.cbi-value var,
color: #0069d6;
}
#modal_overlay > .modal.uci-dialog,
#modal_overlay > .modal.cbi-modal {
max-width: 900px;
}
.uci-change-list {
line-height: 170%;
white-space: pre;
@ -2113,8 +2132,8 @@ div.cbi-value var,
.uci-change-legend-label > var {
float: left;
margin-right: 4px;
width: 10px;
height: 10px;
width: 16px;
height: 16px;
display: block;
position: relative;
}

View file

@ -131,30 +131,6 @@
end
end
local function render_changes()
-- calculate the number of unsaved changes
if tree.nodes[category] and tree.nodes[category].ucidata then
local ucichanges = 0
for i, j in pairs(require("luci.model.uci").cursor():changes()) do
for k, l in pairs(j) do
for m, n in pairs(l) do
ucichanges = ucichanges + 1;
end
end
end
if ucichanges > 0 then
write('<a class="uci_change_indicator label notice" href="%s?redir=%s">%s: %d</a>' %{
url(category, 'uci/changes'),
http.urlencode(http.formvalue('redir') or table.concat(disp.context.request, "/")),
translate('Unsaved Changes'),
ucichanges
})
end
end
end
-- Get current and latest OMR version
local current_omr_version = luci.model.uci.cursor():get("openmptcprouter","settings","version") or ""
local latest_omr_version = luci.model.uci.cursor():get("openmptcprouter","latest_versions","omr") or ""
@ -186,7 +162,6 @@
<a class="brand" href="#" alt="OpenMPTCProuter"><img src="<%=resource%>/openmptcprouter/images/omr-logo.png" height="30" width="30" alt="OMR" /> OpenMPTCProuter</a>
<% render_topmenu() %>
<div class="pull-right">
<% render_changes() %>
<span id="xhr_poll_status" style="display:none" onclick="XHR.running() ? XHR.halt() : XHR.run()">
<span class="label success" id="xhr_poll_status_on"><%:Auto Refresh%> <%:on%></span>
<span class="label" id="xhr_poll_status_off" style="display:none"><%:Auto Refresh%> <%:off%></span>