mirror of
https://github.com/openwrt/luci.git
synced 2026-10-12 05:07:26 +04:00
luci-app-banip: sync with base package 1.8.13-1
- rework the custom feed editor Signed-off-by: Dirk Brenken dev@brenken.org Signed-off-by: Dirk Brenken <dev@brenken.org>
This commit is contained in:
4 files changed
+265
-268
No files matched your search
@@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=LuCI support for banIP
|
||||
LUCI_DEPENDS:=+luci-base +banip
|
||||
|
||||
PKG_VERSION:=1.8.12
|
||||
PKG_RELEASE:=4
|
||||
PKG_VERSION:=1.8.13
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Dirk Brenken <dev@brenken.org>
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.cbi-input-text {
|
||||
width: 90% !important;
|
||||
margin-bottom: -5px;
|
||||
padding-top: 0rem;
|
||||
}
|
||||
.cbi-input-select {
|
||||
margin-bottom: -5px;
|
||||
padding-top: 0rem;
|
||||
}
|
||||
@@ -4,325 +4,331 @@
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
/*
|
||||
include custom CSS
|
||||
*/
|
||||
document.querySelector('head').appendChild(E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'type': 'text/css',
|
||||
'href': L.resource('view/banip/custom.css')
|
||||
}));
|
||||
const feedFile = '/etc/banip/banip.custom.feeds';
|
||||
const uploadFile = '/tmp/banip.custom.feeds.upload';
|
||||
const feedKeys = ['url_4', 'url_6', 'rule', 'chain', 'descr', 'flag'];
|
||||
|
||||
/*
|
||||
module-level file size set during render, read by observer
|
||||
convert the feed object into named sections of a single section type,
|
||||
the feed name is kept as a regular option, the section id stays internal
|
||||
*/
|
||||
let fileSize = 0;
|
||||
function toSections(feeds) {
|
||||
const sections = [];
|
||||
let index = 0;
|
||||
|
||||
/*
|
||||
button state helper
|
||||
*/
|
||||
function updateButtons() {
|
||||
const enable = function (id) {
|
||||
const btn = document.getElementById(id);
|
||||
if (btn) {
|
||||
btn.removeAttribute('disabled');
|
||||
for (const name of Object.keys(feeds ?? {})) {
|
||||
const feed = feeds[name];
|
||||
if (!L.isObject(feed)) {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if (fileSize === 0) {
|
||||
enable('btnCreate');
|
||||
enable('btnUpload');
|
||||
} else {
|
||||
enable('btnDownload');
|
||||
enable('btnClear');
|
||||
enable('btnSave');
|
||||
const section = { '.name': 'feed%d'.format(index++), 'name': name };
|
||||
for (const key of feedKeys) {
|
||||
if (feed[key] != null) {
|
||||
section[key] = String(feed[key]);
|
||||
}
|
||||
}
|
||||
sections.push(section);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
/*
|
||||
observe DOM changes
|
||||
a feed entry needs a name, a description, a parsing rule and at least one url
|
||||
*/
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
if (mutations) {
|
||||
const inputs = document.querySelectorAll('input');
|
||||
inputs.forEach(function (input) {
|
||||
input.setAttribute('autocomplete', 'off')
|
||||
input.setAttribute('autocorrect', 'off')
|
||||
input.setAttribute('autocapitalize', 'off')
|
||||
input.setAttribute('spellcheck', false)
|
||||
})
|
||||
const labels = document.querySelectorAll('label[for^="widget.cbid.json"][for$="name"]');
|
||||
labels.forEach(function (label) {
|
||||
label.setAttribute("style", "font-weight: bold !important; color: #595 !important;");
|
||||
})
|
||||
updateButtons();
|
||||
function checkFeed(name, feed) {
|
||||
if (!name || !name.match(/^[a-z0-9]{3,15}$/)) {
|
||||
return _('Invalid feed name: %s').format(name || '?');
|
||||
}
|
||||
});
|
||||
|
||||
const targetNode = document.getElementById('view');
|
||||
const observerConfig = {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: false,
|
||||
characterData: false
|
||||
};
|
||||
observer.observe(targetNode, observerConfig);
|
||||
if (!L.isObject(feed)) {
|
||||
return _('Invalid feed entry: %s').format(name);
|
||||
}
|
||||
if (!feed.descr || !feed.rule) {
|
||||
return _('Missing description or rule: %s').format(name);
|
||||
}
|
||||
if (!feed.url_4 && !feed.url_6) {
|
||||
return _('Missing URL: %s').format(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
button handling
|
||||
rebuild the feed object from the form model
|
||||
*/
|
||||
function handleEdit(ev) {
|
||||
if (ev === 'upload') {
|
||||
return ui.uploadFile('/etc/banip/banip.custom.feeds').then(function () {
|
||||
return L.resolveDefault(fs.read_direct('/etc/banip/banip.custom.feeds', 'json'), "").then(function (data) {
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
for (let i = 0; i < Object.keys(data).length; i++) {
|
||||
let feed = Object.keys(data)[i];
|
||||
let descr = data[feed]?.descr;
|
||||
if (feed && descr) {
|
||||
continue;
|
||||
}
|
||||
return fs.write('/etc/banip/banip.custom.feeds', null).then(function () {
|
||||
ui.addNotification(null, E('p', _('Upload of the custom feed file failed.')), 'error');
|
||||
});
|
||||
}
|
||||
location.reload();
|
||||
} else {
|
||||
return fs.write('/etc/banip/banip.custom.feeds', null).then(function () {
|
||||
ui.addNotification(null, E('p', _('Upload of the custom feed file failed.')), 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(function () { });
|
||||
}
|
||||
if (ev === 'download') {
|
||||
return fs.read_direct('/etc/banip/banip.custom.feeds', 'blob').then(function (blob) {
|
||||
let url = window.URL.createObjectURL(blob),
|
||||
date = new Date(),
|
||||
name = 'banip.custom.feeds_%04d-%02d-%02d.json'.format(date.getFullYear(), date.getMonth() + 1, date.getDate()),
|
||||
link = E('a', { 'style': 'display:none', 'href': url, 'download': name });
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}).catch(function () { });
|
||||
}
|
||||
if (ev === 'create') {
|
||||
return fs.read_direct('/etc/banip/banip.feeds', 'json').then(function (content) {
|
||||
fs.write('/etc/banip/banip.custom.feeds', JSON.stringify(content)).then(function () {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
if (ev === 'clear') {
|
||||
return fs.write('/etc/banip/banip.custom.feeds', null).then(function () {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
if (ev === 'save') {
|
||||
const invalid = document.querySelectorAll('.cbi-input-invalid');
|
||||
if (invalid.length > 0) {
|
||||
document.body.scrollTop = document.documentElement.scrollTop = 0;
|
||||
return ui.addNotification(null, E('p', _('Invalid input values, unable to save modifications.')), 'error');
|
||||
}
|
||||
}
|
||||
/*
|
||||
gather all input data and fall through from 'save'
|
||||
*/
|
||||
const exportObj = {};
|
||||
const nodeKeys = document.querySelectorAll('[id^="widget.cbid.json"][id$="name"]');
|
||||
for (const keyNode of nodeKeys) {
|
||||
const keyValue = keyNode.value?.trim();
|
||||
if (!keyValue) continue;
|
||||
const idParts = keyNode.id.split(".");
|
||||
const ruleId = idParts[3];
|
||||
if (!ruleId) continue;
|
||||
const selector =
|
||||
`[id^="widget.cbid.json.${ruleId}."], ` +
|
||||
`[id^="cbid.json.${ruleId}.rule"]`;
|
||||
const elements = document.querySelectorAll(selector);
|
||||
const sub = {};
|
||||
for (const el of elements) {
|
||||
const parts = el.id.split(".");
|
||||
const key = parts[parts.length - 1];
|
||||
const value = el.value?.trim();
|
||||
if (!value) continue;
|
||||
if (["url_4", "url_6", "rule", "chain", "descr", "flag"].includes(key)) {
|
||||
sub[key] = value;
|
||||
function fromSections(map) {
|
||||
const feeds = {};
|
||||
const names = [];
|
||||
|
||||
for (const section of map.data.sections('json', 'feed')) {
|
||||
const name = (section.name ?? '').trim();
|
||||
const feed = {};
|
||||
for (const key of feedKeys) {
|
||||
const value = (section[key] ?? '').trim?.() ?? section[key];
|
||||
if (value) {
|
||||
feed[key] = value;
|
||||
}
|
||||
}
|
||||
/* require at least descr and rule to produce a valid feed entry */
|
||||
if (sub.descr && sub.rule && (sub.url_4 || sub.url_6)) {
|
||||
exportObj[keyValue] = sub;
|
||||
const error = checkFeed(name, feed);
|
||||
if (error) {
|
||||
return { error: error };
|
||||
}
|
||||
if (names.includes(name)) {
|
||||
return { error: _('Duplicate feed name: %s').format(name) };
|
||||
}
|
||||
names.push(name);
|
||||
feeds[name] = feed;
|
||||
}
|
||||
return { feeds: feeds };
|
||||
}
|
||||
|
||||
/*
|
||||
validate an uploaded feed file
|
||||
*/
|
||||
function checkUpload(data) {
|
||||
if (!L.isObject(data) || Object.keys(data).length === 0) {
|
||||
return _('The uploaded file is no valid custom feed file.');
|
||||
}
|
||||
for (const name of Object.keys(data)) {
|
||||
const error = checkFeed(name, data[name]);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
/*
|
||||
save to file and reload
|
||||
*/
|
||||
const exportJson = JSON.stringify(exportObj, null, 4);
|
||||
return fs.write('/etc/banip/banip.custom.feeds', exportJson)
|
||||
.then(() => location.reload());
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
notification handler
|
||||
*/
|
||||
function notify(message, type) {
|
||||
document.querySelectorAll('.banip-notification').forEach(function (node) {
|
||||
node.parentNode?.removeChild(node);
|
||||
});
|
||||
return ui.addNotification(null, E('p', message), type, 'banip-notification');
|
||||
}
|
||||
|
||||
function urlValidator(section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^https?:\/\/[A-Za-z0-9[\]/.?&+_@%=:~#-]+$/)) {
|
||||
return _('Protocol/URL format not supported');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
keep browser autofill out of the modal input fields
|
||||
*/
|
||||
function plainInput(option) {
|
||||
const renderWidget = option.renderWidget;
|
||||
|
||||
option.renderWidget = function (...args) {
|
||||
const node = renderWidget.apply(this, args);
|
||||
const input = node?.querySelector?.('input');
|
||||
if (input) {
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
input.setAttribute('autocorrect', 'off');
|
||||
input.setAttribute('autocapitalize', 'off');
|
||||
input.setAttribute('spellcheck', 'false');
|
||||
}
|
||||
return node;
|
||||
};
|
||||
return option;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return L.resolveDefault(fs.stat('/etc/banip/banip.custom.feeds'), null)
|
||||
.then(function (stat) {
|
||||
if (!stat) {
|
||||
return fs.write('/etc/banip/banip.custom.feeds', "").then(function () {
|
||||
return { size: 0, data: null };
|
||||
});
|
||||
}
|
||||
return L.resolveDefault(fs.read_direct('/etc/banip/banip.custom.feeds', 'json'), "")
|
||||
.then(function (data) {
|
||||
return { size: stat.size, data: data };
|
||||
});
|
||||
});
|
||||
return L.resolveDefault(fs.read_direct(feedFile, 'json'), null);
|
||||
},
|
||||
|
||||
render: function (result) {
|
||||
let m, s, o, feed, url_4, url_6, rule, chain, descr, flag;
|
||||
const data = result.data;
|
||||
fileSize = result.size;
|
||||
handleEdit: function (map, ev) {
|
||||
if (ev === 'upload') {
|
||||
return ui.uploadFile(uploadFile).then(function () {
|
||||
return L.resolveDefault(fs.read_direct(uploadFile, 'json'), null).then(function (data) {
|
||||
const error = checkUpload(data);
|
||||
if (error) {
|
||||
return fs.remove(uploadFile).finally(function () {
|
||||
notify(_('Upload of the custom feed file failed: %s').format(error), 'error');
|
||||
});
|
||||
}
|
||||
return fs.write(feedFile, JSON.stringify(data, null, 4)).then(function () {
|
||||
return fs.remove(uploadFile).finally(function () {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
}).catch(function () { });
|
||||
}
|
||||
|
||||
m = new form.JSONMap(data, null, _('With this editor you can upload your local custom feed file or fill up an initial one (a 1:1 copy of the version shipped with the package). \
|
||||
if (ev === 'download') {
|
||||
return fs.read_direct(feedFile, 'blob').then(function (blob) {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const date = new Date();
|
||||
const name = 'banip.custom.feeds_%04d-%02d-%02d.json'.format(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||
const link = E('a', { 'style': 'display:none', 'href': url, 'download': name });
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}).catch(function () { });
|
||||
}
|
||||
|
||||
if (ev === 'fill') {
|
||||
return fs.read_direct('/etc/banip/banip.feeds', 'json').then(function (content) {
|
||||
return fs.write(feedFile, JSON.stringify(content, null, 4)).then(function () {
|
||||
location.reload();
|
||||
});
|
||||
}).catch(function () {
|
||||
notify(_('Unable to read the maintainers feed file.'), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
if (ev === 'clear') {
|
||||
return fs.write(feedFile, null).then(function () {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
save, the modal dialogs have already written their values to the model
|
||||
*/
|
||||
const result = fromSections(map);
|
||||
if (result.error) {
|
||||
notify(_('Unable to save modifications: %s').format(result.error), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
return fs.write(feedFile, JSON.stringify(result.feeds, null, 4)).then(function () {
|
||||
notify(_('Custom feed file saved.'), 'info');
|
||||
});
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
const feeds = L.isObject(data) ? data : {};
|
||||
const empty = Object.keys(feeds).length === 0;
|
||||
let m, s, o;
|
||||
|
||||
m = new form.JSONMap({ 'feed': toSections(feeds) }, null, _('With this editor you can upload your local custom feed file or fill up an initial one (a 1:1 copy of the version shipped with the package). \
|
||||
The file is located at \'/etc/banip/banip.custom.feeds\'. \
|
||||
Then you can edit this file, delete entries, add new ones or make a local backup. To go back to the maintainers version just clear the custom feed file.'));
|
||||
for (let i = 0; i < Object.keys(m.data.data).length; i++) {
|
||||
feed = Object.keys(m.data.data)[i];
|
||||
url_4 = m.data.data[feed].url_4;
|
||||
url_6 = m.data.data[feed].url_6;
|
||||
rule = m.data.data[feed].rule;
|
||||
chain = m.data.data[feed].chain;
|
||||
descr = m.data.data[feed].descr;
|
||||
flag = m.data.data[feed].flag;
|
||||
|
||||
s = m.section(form.TypedSection, feed, null);
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
s = m.section(form.GridSection, 'feed');
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
s.sortable = false;
|
||||
s.nodescriptions = true;
|
||||
s.addbtntitle = _('Add feed');
|
||||
s.modaltitle = function (section_id) {
|
||||
return _('Custom Feed') + ' » ' + (this.map.data.get('json', section_id, 'name') || _('new'));
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'name', _('Feed Name'));
|
||||
o.ucioption = '.name';
|
||||
o.datatype = 'and(minlength(3),maxlength(15))';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _('Empty field not allowed');
|
||||
}
|
||||
if (!value.match(/^[a-z0-9]+$/)) {
|
||||
return _('Invalid characters');
|
||||
o = plainInput(s.option(form.Value, 'name', _('Feed Name')));
|
||||
o.datatype = 'and(minlength(3),maxlength(15))';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _('Empty field not allowed');
|
||||
}
|
||||
if (!value.match(/^[a-z0-9]+$/)) {
|
||||
return _('Invalid characters');
|
||||
}
|
||||
for (const section of this.map.data.sections('json', 'feed')) {
|
||||
if (section['.name'] !== section_id && section.name === value) {
|
||||
return _('Feed name already used');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o = plainInput(s.option(form.Value, 'descr', _('Description')));
|
||||
o.datatype = 'and(minlength(5),maxlength(30))';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _('Empty field not allowed');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.option(form.ListValue, 'chain', _('Chain'));
|
||||
o.value('in', _('Inbound'));
|
||||
o.value('out', _('Outbound'));
|
||||
o.value('inout', _('Inbound & Outbound'));
|
||||
o.default = 'in';
|
||||
|
||||
o = plainInput(s.option(form.Value, 'url_4', _('URLv4')));
|
||||
o.modalonly = true;
|
||||
o.validate = urlValidator;
|
||||
|
||||
o = plainInput(s.option(form.Value, 'url_6', _('URLv6')));
|
||||
o.modalonly = true;
|
||||
o.validate = urlValidator;
|
||||
|
||||
o = plainInput(s.option(form.Value, 'rule', _('Rule')));
|
||||
o.modalonly = true;
|
||||
o.value('feed 1', _('<IP-Address>'));
|
||||
o.value('feed 1 ,', _('<IP-Address><CSV-Separator>'));
|
||||
o.value('feed 13', _('<IP-Address><Space><Netmask>'));
|
||||
o.value('feed 4 "', _('<JSON Lines><IP-Address><JSON Lines>'));
|
||||
o.value('suricata 1', _('<Suricata Syntax>'));
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _('Empty field not allowed');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o = plainInput(s.option(form.Value, 'flag', _('Flag')));
|
||||
o.modalonly = true;
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'url_4', _('URLv4'));
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^https?:\/\/[A-Za-z0-9[\]/.?&+_@%=:~#-]+$/)) {
|
||||
return _('Protocol/URL format not supported');
|
||||
}
|
||||
return true;
|
||||
if (!value.match(/^(\bgz\b|\btcp\b|\budp\b|\b[0-9-]+\b| )*$/)) {
|
||||
return _('Flag not supported');
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'url_6', _('URLv6'));
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^https?:\/\/[A-Za-z0-9[\]/.?&+_@%=:~#-]+$/)) {
|
||||
return _('Protocol/URL format not supported');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'rule', _('Rule'));
|
||||
o.value('feed 1', _('<IP-Address>'));
|
||||
o.value('feed 1 ,', _('<IP-Address><CSV-Separator>'));
|
||||
o.value('feed 13', _('<IP-Address><Space><Netmask>'));
|
||||
o.value('feed 4 "', _('<JSON Lines><IP-Address><JSON Lines>'));
|
||||
o.value('suricata 1', _('<Suricata Syntax>'));
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.ListValue, 'chain', _('Chain'));
|
||||
o.value('in', _('Inbound'));
|
||||
o.value('out', _('Outbound'));
|
||||
o.value('inout', _('Inbound & Outbound'));
|
||||
o.default = 'in';
|
||||
|
||||
o = s.option(form.Value, 'descr', _('Description'));
|
||||
o.datatype = 'and(minlength(5),maxlength(30))';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _('Empty field not allowed');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'flag', _('Flag'));
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^(\bgz\b|\btcp\b|\budp\b|\b[0-9-]+\b| )*$/)) {
|
||||
return _('Flag not supported');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
s = m.section(form.NamedSection, 'global');
|
||||
s.render = L.bind(function () {
|
||||
return E('div', { 'class': 'cbi-page-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action important',
|
||||
'style': 'float:none;margin-right:.4em;',
|
||||
'id': 'btnDownload',
|
||||
'disabled': 'disabled',
|
||||
'disabled': empty ? 'disabled' : null,
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('download');
|
||||
return this.handleEdit(m, 'download');
|
||||
})
|
||||
}, [_('Download')]),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action important',
|
||||
'style': 'float:none;margin-right:.4em;',
|
||||
'id': 'btnUpload',
|
||||
'disabled': 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('upload');
|
||||
return this.handleEdit(m, 'upload');
|
||||
})
|
||||
}, [_('Upload')]),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action important',
|
||||
'style': 'float:none;margin-right:.4em;',
|
||||
'id': 'btnCreate',
|
||||
'disabled': 'disabled',
|
||||
'disabled': empty ? null : 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('create');
|
||||
return this.handleEdit(m, 'fill');
|
||||
})
|
||||
}, [_('Fill')]),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-negative important',
|
||||
'style': 'float:none;margin-right:.4em;',
|
||||
'id': 'btnClear',
|
||||
'disabled': 'disabled',
|
||||
'disabled': empty ? 'disabled' : null,
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('clear');
|
||||
return this.handleEdit(m, 'clear');
|
||||
})
|
||||
}, [_('Clear')]),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-positive important',
|
||||
'style': 'float:none',
|
||||
'id': 'btnSave',
|
||||
'disabled': 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('save');
|
||||
return this.handleEdit(m, 'save');
|
||||
})
|
||||
}, [_('Save')]),
|
||||
])
|
||||
});
|
||||
}, [_('Save')])
|
||||
]);
|
||||
}, this);
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
|
||||
@@ -184,7 +184,7 @@ return view.extend({
|
||||
|
||||
let m, s, o;
|
||||
m = new form.Map('banip', 'banIP', _('Configuration of the banIP package to ban incoming and outgoing IPs via named nftables Sets. \
|
||||
For further information please check the %s.'.format(`<a style="color:#37c;font-weight:bold;" href="https://github.com/openwrt/packages/blob/master/net/banip/files/README.md" target="_blank" rel="noreferrer noopener" >${_('online documentation')}</a>`)));
|
||||
For further information please check the %s.'.format(`<a style="color:#37c;font-weight:bold;" href="https://github.com/openwrt/packages/blob/master/net/banip/README.md" target="_blank" rel="noreferrer noopener" >${_('online documentation')}</a>`)));
|
||||
|
||||
/*
|
||||
set text content helper function
|
||||
|
||||
Reference in new issue
Block a user