diff --git a/luci-base/htdocs/luci-static/resources/form.js b/luci-base/htdocs/luci-static/resources/form.js index 508e2c485..58d8f7100 100644 --- a/luci-base/htdocs/luci-static/resources/form.js +++ b/luci-base/htdocs/luci-static/resources/form.js @@ -1461,6 +1461,7 @@ var CBIListValue = CBIValue.extend({ size: this.size, sort: this.keylist, optional: this.rmempty || this.optional, + placeholder: this.placeholder, validate: L.bind(this.validate, this, section_id) }); diff --git a/luci-base/htdocs/luci-static/resources/tools/widgets.js b/luci-base/htdocs/luci-static/resources/tools/widgets.js index 7f2997f17..39e5aa165 100644 --- a/luci-base/htdocs/luci-static/resources/tools/widgets.js +++ b/luci-base/htdocs/luci-static/resources/tools/widgets.js @@ -30,8 +30,22 @@ var CBIZoneSelect = form.ListValue.extend({ renderWidget: function(section_id, option_index, cfgvalue) { var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default), + isOutputOnly = false, choices = {}; + if (this.option == 'dest') { + for (var i = 0; i < this.section.children.length; i++) { + var opt = this.section.children[i]; + if (opt.option == 'src') { + var val = opt.cfgvalue(section_id) || opt.default; + isOutputOnly = (val == null || val == ''); + break; + } + } + + this.title = isOutputOnly ? _('Output zone') : _('Destination zone'); + } + if (this.allowlocal) { choices[''] = E('span', { 'class': 'zonebadge', @@ -39,7 +53,7 @@ var CBIZoneSelect = form.ListValue.extend({ }, [ E('strong', _('Device')), (this.allowany || this.allowlocal) - ? ' (%s)'.format(this.alias != 'dest' ? _('output') : _('input')) : '' + ? ' (%s)'.format(this.option != 'dest' ? _('output') : _('input')) : '' ]); } else if (!this.multiple && (this.rmempty || this.optional)) { @@ -55,7 +69,7 @@ var CBIZoneSelect = form.ListValue.extend({ 'style': 'background-color:' + firewall.getColorForName(null) }, [ E('strong', _('Any zone')), - (this.allowany && this.allowlocal) ? ' (%s)'.format(_('forward')) : '' + (this.allowany && this.allowlocal && !isOutputOnly) ? ' (%s)'.format(_('forward')) : '' ]); } @@ -120,7 +134,64 @@ var CBIZoneSelect = form.ListValue.extend({ '' }); - return widget.render(); + var elem = widget.render(); + + if (this.option == 'src') { + elem.addEventListener('cbi-dropdown-change', L.bind(function(ev) { + var opt = this.map.lookupOption('dest', section_id), + val = ev.detail.instance.getValue(); + + if (opt == null) + return; + + var cbid = opt[0].cbid(section_id), + label = document.querySelector('label[for="widget.%s"]'.format(cbid)), + node = document.getElementById(cbid); + + L.dom.content(label, val == '' ? _('Output zone') : _('Destination zone')); + + if (val == '') { + if (L.dom.callClassMethod(node, 'getValue') == '') + L.dom.callClassMethod(node, 'setValue', '*'); + + var emptyval = node.querySelector('[data-value=""]'), + anyval = node.querySelector('[data-value="*"]'); + + L.dom.content(anyval.querySelector('span'), E('strong', _('Any zone'))); + + if (emptyval != null) + emptyval.parentNode.removeChild(emptyval); + } + else { + var anyval = node.querySelector('[data-value="*"]'), + emptyval = node.querySelector('[data-value=""]'); + + if (emptyval == null) { + emptyval = anyval.cloneNode(true); + emptyval.removeAttribute('display'); + emptyval.removeAttribute('selected'); + emptyval.setAttribute('data-value', ''); + } + + L.dom.content(emptyval.querySelector('span'), [ + E('strong', _('Device')), ' (%s)'.format(_('input')) + ]); + + L.dom.content(anyval.querySelector('span'), [ + E('strong', _('Any zone')), ' (%s)'.format(_('forward')) + ]); + + anyval.parentNode.insertBefore(emptyval, anyval); + } + + }, this)); + } + else if (isOutputOnly) { + var emptyval = elem.querySelector('[data-value=""]'); + emptyval.parentNode.removeChild(emptyval); + } + + return elem; }, }); @@ -128,10 +199,16 @@ 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 Promise.all([ + firewall.getDefaults(), + firewall.getZones(), + network.getNetworks(), + network.getDevices() + ]).then(L.bind(function(dznd) { + this.defaults = dznd[0]; + this.zones = dznd[1]; + this.networks = dznd[2]; + this.devices = dznd[3]; return this.super('load', section_id); }, this)); @@ -140,6 +217,8 @@ var CBIZoneForwards = form.DummyValue.extend({ renderZone: function(zone) { var name = zone.getName(), networks = zone.getNetworks(), + devices = zone.getDevices(), + subnets = zone.getSubnets(), ifaces = []; for (var j = 0; j < networks.length; j++) { @@ -152,21 +231,39 @@ var CBIZoneForwards = form.DummyValue.extend({ 'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '') }, network.getName() + ': '); - var devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice()); + var subdevs = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice()); - for (var k = 0; k < devices.length && devices[k]; k++) { + for (var k = 0; k < subdevs.length && subdevs[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')) + 'title': subdevs[k].getI18n(), + 'src': L.resource('icons/%s%s.png'.format(subdevs[k].getType(), subdevs[k].isUp() ? '' : '_disabled')) })); } - if (!devices.length) + if (!subdevs.length) span.appendChild(E('em', _('(empty)'))); ifaces.push(span); } + for (var i = 0; i < devices.length; i++) { + var device = this.devices.filter(function(dev) { return dev.getName() == devices[i] })[0], + title = device ? device.getI18n() : _('Absent Interface'), + type = device ? device.getType() : 'ethernet', + up = device ? device.isUp() : false; + + ifaces.push(E('span', { 'class': 'ifacebadge' }, [ + E('img', { + 'title': title, + 'src': L.resource('icons/%s%s.png'.format(type, up ? '' : '_disabled')) + }), + device ? device.getName() : devices[i] + ])); + } + + if (subnets.length > 0) + ifaces.push(E('span', { 'class': 'ifacebadge' }, [ '{ %s }'.format(subnets.join('; ')) ])); + if (!ifaces.length) ifaces.push(E('span', { 'class': 'ifacebadge' }, E('em', _('(empty)')))); @@ -319,9 +416,120 @@ var CBINetworkSelect = form.ListValue.extend({ }, }); +var CBIDeviceSelect = form.ListValue.extend({ + __name__: 'CBI.DeviceSelect', + + load: function(section_id) { + return network.getDevices().then(L.bind(function(devices) { + this.devices = devices; + + return this.super('load', section_id); + }, this)); + }, + + filter: function(section_id, value) { + return true; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default), + choices = {}, + checked = {}, + order = []; + + 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.devices.length; i++) { + var device = this.devices[i], + name = device.getName(), + type = device.getType(); + + if (name == 'lo' || name == this.exclude || !this.filter(section_id, name)) + continue; + + if (this.noaliases && type == 'alias') + continue; + + if (this.nobridges && type == 'bridge') + continue; + + if (this.noinactive && device.isUp() == false) + continue; + + var item = E([ + E('img', { + 'title': device.getI18n(), + 'src': L.resource('icons/%s%s.png'.format(type, device.isUp() ? '' : '_disabled')) + }), + E('span', { 'class': 'hide-open' }, [ name ]), + E('span', { 'class': 'hide-close'}, [ device.getI18n() ]) + ]); + + var networks = device.getNetworks(); + + if (networks.length > 0) + L.dom.append(item.lastChild, [ ' (', networks.join(', '), ')' ]); + + if (checked[name]) + values.push(name); + + choices[name] = item; + order.push(name); + } + + if (!this.nocreate) { + var keys = Object.keys(checked).sort(); + + for (var i = 0; i < keys.length; i++) { + if (choices.hasOwnProperty(keys[i])) + continue; + + choices[keys[i]] = E([ + E('img', { + 'title': _('Absent Interface'), + 'src': L.resource('icons/ethernet_disabled.png') + }), + E('span', { 'class': 'hide-open' }, [ keys[i] ]), + E('span', { 'class': 'hide-close'}, [ '%s: "%h"'.format(_('Absent Interface'), keys[i]) ]) + ]); + + values.push(keys[i]); + order.push(keys[i]); + } + } + + var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, { + id: this.cbid(section_id), + sort: order, + 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: '' + + '
  • ' + + '' + + '{{value}}' + + ''+_('Custom Interface')+': "{{value}}"' + + '
  • ' + }); + + return widget.render(); + }, +}); + return L.Class.extend({ ZoneSelect: CBIZoneSelect, ZoneForwards: CBIZoneForwards, - NetworkSelect: CBINetworkSelect + NetworkSelect: CBINetworkSelect, + DeviceSelect: CBIDeviceSelect, }); diff --git a/luci-base/htdocs/luci-static/resources/ui.js b/luci-base/htdocs/luci-static/resources/ui.js index 4aac8bcfc..db76f4530 100644 --- a/luci-base/htdocs/luci-static/resources/ui.js +++ b/luci-base/htdocs/luci-static/resources/ui.js @@ -287,8 +287,8 @@ var UISelect = UIElement.extend({ this.node = frameEl; if (this.options.widget == 'select') { - this.setUpdateEvents(frameEl, 'change', 'click', 'blur'); - this.setChangeEvents(frameEl, 'change'); + this.setUpdateEvents(frameEl.firstChild, 'change', 'click', 'blur'); + this.setChangeEvents(frameEl.firstChild, 'change'); } else { var radioEls = frameEl.querySelectorAll('input[type="radio"]'); @@ -879,7 +879,7 @@ var UIDropdown = UIElement.extend({ else markup = '
  • {{value}}
  • '; - new_item = E(markup.replace(/{{value}}/g, item)); + new_item = E(markup.replace(/{{value}}/g, '%h'.format(item))); if (sbox.options.multi) { sbox.transformItem(sb, new_item); @@ -1797,7 +1797,7 @@ return L.Class.extend({ return chg[1]; case 4: - return "'" + chg[3].replace(/'/g, "'\"'\"'") + "'"; + return "'%ḧ'".format(chg[3].replace(/'/g, "'\"'\"'")); default: return chg[m1-1]; @@ -1929,7 +1929,7 @@ return L.Class.extend({ method: 'post', timeout: L.env.apply_timeout * 1000, query: L.ui.changes.confirm_auth - }).then(call); + }).then(call, call); }, delay); }; diff --git a/luci-base/root/usr/libexec/rpcd/luci b/luci-base/root/usr/libexec/rpcd/luci index 55233d6d0..a00fb5f9d 100755 --- a/luci-base/root/usr/libexec/rpcd/luci +++ b/luci-base/root/usr/libexec/rpcd/luci @@ -285,6 +285,41 @@ local methods = { local fs = require "nixio.fs" return { offload_support = not not fs.access("/sys/module/xt_FLOWOFFLOAD/refcnt") } end + }, + + + conntrack_helpers = { + call = function() + local fd = io.open("/usr/share/fw3/helpers.conf", "r") + local rv = {} + + local line, entry + while true do + line = fd:read("*l") + if not line then + break + end + + if line:match("^%s*config%s") then + if entry then + rv[#rv+1] = entry + end + entry = {} + else + local opt, val = line:match("^%s*option%s+(%S+)%s+(%S.*)$") + if opt and val then + opt = opt:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1") + val = val:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1") + entry[opt] = val + end + end + end + if entry then + rv[#rv+1] = entry + end + + return { helpers = rv } + end } } diff --git a/luci-base/root/usr/share/rpcd/acl.d/luci-base.json b/luci-base/root/usr/share/rpcd/acl.d/luci-base.json index a9baef8f9..de145ce78 100644 --- a/luci-base/root/usr/share/rpcd/acl.d/luci-base.json +++ b/luci-base/root/usr/share/rpcd/acl.d/luci-base.json @@ -13,7 +13,7 @@ "read": { "ubus": { "iwinfo": [ "info" ], - "luci": [ "boardjson", "duid_hints", "host_hints", "ifaddrs", "initList", "getLocaltime", "leases", "leds", "netdevs", "offload_support", "usb" ], + "luci": [ "boardjson", "duid_hints", "host_hints", "ifaddrs", "initList", "getLocaltime", "leases", "leds", "netdevs", "usb" ], "network.device": [ "status" ], "network.interface": [ "dump" ], "network.wireless": [ "status" ], @@ -28,5 +28,17 @@ }, "uci": [ "*" ] } + }, + "luci-app-firewall": { + "description": "Grant access to firewall procedures", + "read": { + "ubus": { + "luci": [ "conntrack_helpers", "offload_support" ] + }, + "uci": [ "firewall" ] + }, + "write": { + "uci": [ "firewall" ] + } } }