summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/htdocs
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base/htdocs')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/cbi.js48
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/form.js141
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js162
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/network.js434
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js62
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/protocol/none.js8
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/protocol/static.js231
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/rpc.js135
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/tools/widgets.js2
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js472
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/validation.js6
11 files changed, 1351 insertions, 350 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js
index d4d61eb381..4c3128bfd1 100644
--- a/modules/luci-base/htdocs/luci-static/resources/cbi.js
+++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js
@@ -298,8 +298,6 @@ function cbi_init() {
node.getAttribute('data-type'));
}
- document.querySelectorAll('[data-browser]').forEach(cbi_browser_init);
-
document.querySelectorAll('.cbi-tooltip:not(:empty)').forEach(function(s) {
s.parentNode.classList.add('cbi-tooltip-container');
});
@@ -316,39 +314,20 @@ function cbi_init() {
i.addEventListener('mouseout', handler);
});
+ var tasks = [];
+
document.querySelectorAll('[data-ui-widget]').forEach(function(node) {
var args = JSON.parse(node.getAttribute('data-ui-widget') || '[]'),
widget = new (Function.prototype.bind.apply(L.ui[args[0]], args)),
markup = widget.render();
- markup.addEventListener('widget-change', cbi_d_update);
- node.parentNode.replaceChild(markup, node);
+ tasks.push(Promise.resolve(markup).then(function(markup) {
+ markup.addEventListener('widget-change', cbi_d_update);
+ node.parentNode.replaceChild(markup, node);
+ }));
});
- cbi_d_update();
-}
-
-function cbi_filebrowser(id, defpath) {
- var field = L.dom.elem(id) ? id : document.getElementById(id);
- var browser = window.open(
- cbi_strings.path.browser + (field.value || defpath || '') + '?field=' + field.id,
- "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes"
- );
-
- browser.focus();
-}
-
-function cbi_browser_init(field)
-{
- field.parentNode.insertBefore(
- E('img', {
- 'src': L.resource('cbi/folder.gif'),
- 'class': 'cbi-image-button',
- 'click': function(ev) {
- cbi_filebrowser(field, field.getAttribute('data-browser'));
- ev.preventDefault();
- }
- }), field.nextSibling);
+ Promise.all(tasks).then(cbi_d_update);
}
function cbi_validate_form(form, errmsg)
@@ -566,7 +545,7 @@ String.prototype.format = function()
switch(pType) {
case 'b':
- subst = (~~param || 0).toString(2);
+ subst = Math.floor(+param || 0).toString(2);
break;
case 'c':
@@ -574,11 +553,12 @@ String.prototype.format = function()
break;
case 'd':
- subst = (~~param || 0);
+ subst = Math.floor(+param || 0).toFixed(0);
break;
case 'u':
- subst = ~~Math.abs(+param || 0);
+ var n = +param || 0;
+ subst = Math.floor((n < 0) ? 0x100000000 + n : n).toFixed(0);
break;
case 'f':
@@ -588,7 +568,7 @@ String.prototype.format = function()
break;
case 'o':
- subst = (~~param || 0).toString(8);
+ subst = Math.floor(+param || 0).toString(8);
break;
case 's':
@@ -596,11 +576,11 @@ String.prototype.format = function()
break;
case 'x':
- subst = ('' + (~~param || 0).toString(16)).toLowerCase();
+ subst = Math.floor(+param || 0).toString(16).toLowerCase();
break;
case 'X':
- subst = ('' + (~~param || 0).toString(16)).toUpperCase();
+ subst = Math.floor(+param || 0).toString(16).toUpperCase();
break;
case 'h':
diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js
index cc72d6e487..f0629d0ca7 100644
--- a/modules/luci-base/htdocs/luci-static/resources/form.js
+++ b/modules/luci-base/htdocs/luci-static/resources/form.js
@@ -52,7 +52,7 @@ var CBINode = Class.extend({
},
stripTags: function(s) {
- if (!s.match(/[<>]/))
+ if (typeof(s) == 'string' && !s.match(/[<>]/))
return s;
var x = E('div', {}, s);
@@ -132,18 +132,19 @@ var CBIMap = CBINode.extend({
return Promise.all(tasks);
},
- save: function(cb) {
+ save: function(cb, silent) {
this.checkDepends();
return this.parse()
.then(cb)
.then(uci.save.bind(uci))
.then(this.load.bind(this))
- .then(this.renderContents.bind(this))
.catch(function(e) {
- alert('Cannot save due to invalid values')
+ if (!silent)
+ alert('Cannot save due to invalid values');
+
return Promise.reject();
- });
+ }).finally(this.renderContents.bind(this));
},
reset: function() {
@@ -516,7 +517,7 @@ var CBIAbstractValue = CBINode.extend({
else {
var conf = this.uciconfig || this.section.uciconfig || this.map.config,
res = this.map.lookupOption(dep, section_id, conf),
- val = res ? res[0].formvalue(res[1]) : null;
+ val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
istat = (istat && isEqual(val, this.deps[i][dep]));
}
@@ -658,14 +659,14 @@ var CBITypedSection = CBIAbstractSection.extend({
var config_name = this.uciconfig || this.map.config;
uci.add(config_name, this.sectiontype, name);
- this.map.save();
+ return this.map.save(null, true);
},
handleRemove: function(section_id, ev) {
var config_name = this.uciconfig || this.map.config;
uci.remove(config_name, section_id);
- this.map.save();
+ return this.map.save(null, true);
},
renderSectionAdd: function(extra_class) {
@@ -680,13 +681,11 @@ var CBITypedSection = CBIAbstractSection.extend({
createEl.classList.add(extra_class);
if (this.anonymous) {
- createEl.appendChild(E('input', {
- 'type': 'submit',
+ createEl.appendChild(E('button', {
'class': 'cbi-button cbi-button-add',
- 'value': btn_title || _('Add'),
'title': btn_title || _('Add'),
- 'click': L.bind(this.handleAdd, this)
- }));
+ 'click': L.ui.createHandlerFn(this, 'handleAdd')
+ }, btn_title || _('Add')));
}
else {
var nameEl = E('input', {
@@ -701,12 +700,12 @@ var CBITypedSection = CBIAbstractSection.extend({
'type': 'submit',
'value': btn_title || _('Add'),
'title': btn_title || _('Add'),
- 'click': L.bind(function(ev) {
+ 'click': L.ui.createHandlerFn(this, function(ev) {
if (nameEl.classList.contains('cbi-input-invalid'))
return;
- this.handleAdd(ev, nameEl.value);
- }, this)
+ return this.handleAdd(ev, nameEl.value);
+ })
})
]);
@@ -743,14 +742,12 @@ var CBITypedSection = CBIAbstractSection.extend({
if (this.addremove) {
sectionEl.appendChild(
E('div', { 'class': 'cbi-section-remove right' },
- E('input', {
- 'type': 'submit',
+ E('button', {
'class': 'cbi-button',
'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
- 'value': _('Delete'),
'data-section-id': cfgsections[i],
- 'click': L.bind(this.handleRemove, this, cfgsections[i])
- })));
+ 'click': L.ui.createHandlerFn(this, 'handleRemove', cfgsections[i])
+ }, _('Delete'))));
}
if (!this.anonymous)
@@ -988,7 +985,7 @@ var CBITableSection = CBITypedSection.extend({
'value': more_label,
'title': more_label,
'class': 'cbi-button cbi-button-edit',
- 'click': L.bind(this.renderMoreOptionsModal, this, section_id)
+ 'click': L.ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
})
);
}
@@ -997,16 +994,14 @@ var CBITableSection = CBITypedSection.extend({
var btn_title = this.titleFn('removebtntitle', section_id);
L.dom.append(tdEl.lastElementChild,
- E('input', {
- 'type': 'submit',
- 'value': btn_title || _('Delete'),
+ E('button', {
'title': btn_title || _('Delete'),
'class': 'cbi-button cbi-button-remove',
- 'click': L.bind(function(sid, ev) {
+ 'click': L.ui.createHandlerFn(this, function(sid, ev) {
uci.remove(config_name, sid);
- this.map.save();
- }, this, section_id)
- })
+ return this.map.save(null, true);
+ }, section_id)
+ }, [ btn_title || _('Delete') ])
);
}
@@ -1120,6 +1115,8 @@ var CBITableSection = CBITypedSection.extend({
m = new CBIMap(this.map.config, null, null),
s = m.section(CBINamedSection, section_id, this.sectiontype);
+ m.parent = parent;
+
s.tabs = this.tabs;
s.tab_names = this.tab_names;
@@ -1156,24 +1153,18 @@ var CBITableSection = CBITypedSection.extend({
}
}
- //ev.target.classList.add('spinning');
- Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
- //ev.target.classList.remove('spinning');
+ return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
L.ui.showModal(title, [
nodes,
E('div', { 'class': 'right' }, [
- E('input', {
- 'type': 'button',
+ E('button', {
'class': 'btn',
- 'click': L.bind(this.handleModalCancel, this, m),
- 'value': _('Dismiss')
- }), ' ',
- E('input', {
- 'type': 'button',
+ 'click': L.ui.createHandlerFn(this, 'handleModalCancel', m)
+ }, _('Dismiss')), ' ',
+ E('button', {
'class': 'cbi-button cbi-button-positive important',
- 'click': L.bind(this.handleModalSave, this, m),
- 'value': _('Save')
- })
+ 'click': L.ui.createHandlerFn(this, 'handleModalSave', m)
+ }, _('Save'))
])
], 'cbi-modal');
}, this)).catch(L.error);
@@ -1189,8 +1180,8 @@ var CBIGridSection = CBITableSection.extend({
var config_name = this.uciconfig || this.map.config,
section_id = uci.add(config_name, this.sectiontype);
- this.addedSection = section_id;
- this.renderMoreOptionsModal(section_id);
+ this.addedSection = section_id;
+ return this.renderMoreOptionsModal(section_id);
},
handleModalSave: function(/* ... */) {
@@ -1283,7 +1274,7 @@ var CBINamedSection = CBIAbstractSection.extend({
config_name = this.uciconfig || this.map.config;
uci.add(config_name, this.sectiontype, section_id);
- this.map.save();
+ return this.map.save(null, true);
},
handleRemove: function(ev) {
@@ -1291,7 +1282,7 @@ var CBINamedSection = CBIAbstractSection.extend({
config_name = this.uciconfig || this.map.config;
uci.remove(config_name, section_id);
- this.map.save();
+ return this.map.save(null, true);
},
renderContents: function(data) {
@@ -1315,12 +1306,10 @@ var CBINamedSection = CBIAbstractSection.extend({
if (this.addremove) {
sectionEl.appendChild(
E('div', { 'class': 'cbi-section-remove right' },
- E('input', {
- 'type': 'submit',
+ E('button', {
'class': 'cbi-button',
- 'value': _('Delete'),
- 'click': L.bind(this.handleRemove, this)
- })));
+ 'click': L.ui.createHandlerFn(this, 'handleRemove')
+ }, _('Delete'))));
}
sectionEl.appendChild(E('div', {
@@ -1332,12 +1321,10 @@ var CBINamedSection = CBIAbstractSection.extend({
}
else if (this.addremove) {
sectionEl.appendChild(
- E('input', {
- 'type': 'submit',
+ E('button', {
'class': 'cbi-button cbi-button-add',
- 'value': _('Add'),
- 'click': L.bind(this.handleAdd, this)
- }));
+ 'click': L.ui.createHandlerFn(this, 'handleAdd')
+ }, _('Add')));
}
L.dom.bindClassInstance(sectionEl, this);
@@ -1642,6 +1629,9 @@ var CBIDummyValue = CBIValue.extend({
hiddenEl.render()
]);
},
+
+ remove: function() {},
+ write: function() {}
});
var CBIButtonValue = CBIValue.extend({
@@ -1655,15 +1645,13 @@ var CBIButtonValue = CBIValue.extend({
if (value !== false)
L.dom.content(outputEl, [
- E('input', {
+ E('button', {
'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
- 'type': 'button',
- 'value': btn_title,
- 'click': L.bind(this.onclick || function(ev) {
+ 'click': L.ui.createHandlerFn(this, this.onclick || function(ev) {
ev.target.previousElementSibling.value = ev.target.value;
- this.map.save();
- }, this)
- })
+ return this.map.save();
+ })
+ }, [ btn_title ])
]);
else
L.dom.content(outputEl, ' - ');
@@ -1687,6 +1675,32 @@ var CBIHiddenValue = CBIValue.extend({
}
});
+var CBIFileUpload = CBIValue.extend({
+ __name__: 'CBI.FileSelect',
+
+ __init__: function(/* ... */) {
+ this.super('__init__', arguments);
+
+ this.show_hidden = false;
+ this.enable_upload = true;
+ this.enable_remove = true;
+ this.root_directory = '/etc/luci-uploads';
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
+ id: this.cbid(section_id),
+ name: this.cbid(section_id),
+ show_hidden: this.show_hidden,
+ enable_upload: this.enable_upload,
+ enable_remove: this.enable_remove,
+ root_directory: this.root_directory
+ });
+
+ return browserEl.render();
+ }
+});
+
var CBISectionValue = CBIValue.extend({
__name__: 'CBI.ContainerValue',
__init__: function(map, section, option, cbiClass /*, ... */) {
@@ -1740,5 +1754,6 @@ return L.Class.extend({
DummyValue: CBIDummyValue,
Button: CBIButtonValue,
HiddenValue: CBIHiddenValue,
+ FileUpload: CBIFileUpload,
SectionValue: CBISectionValue
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
index d72764b114..bcc6870bd2 100644
--- a/modules/luci-base/htdocs/luci-static/resources/luci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -360,8 +360,13 @@
break;
case 'object':
- content = JSON.stringify(opt.content);
- contenttype = 'application/json';
+ if (!(opt.content instanceof FormData)) {
+ content = JSON.stringify(opt.content);
+ contenttype = 'application/json';
+ }
+ else {
+ content = opt.content;
+ }
break;
default:
@@ -378,6 +383,9 @@
contenttype = opt.headers[header];
}
+ if ('progress' in opt && 'upload' in opt.xhr)
+ opt.xhr.upload.addEventListener('progress', opt.progress);
+
if (contenttype != null)
opt.xhr.setRequestHeader('Content-Type', contenttype);
@@ -491,7 +499,7 @@
return true;
},
- remove: function(entry) {
+ remove: function(fn) {
if (typeof(fn) != 'function')
throw new TypeError('Invalid argument to LuCI.Poll.remove()');
@@ -567,8 +575,13 @@
__init__: function(env) {
document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
- if (env.base_url == null || env.base_url == '')
- env.base_url = s.getAttribute('src').replace(/\/luci\.js(?:\?v=[^?]+)?$/, '');
+ if (env.base_url == null || env.base_url == '') {
+ var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
+ if (m) {
+ env.base_url = m[1];
+ env.resource_version = m[2];
+ }
+ }
});
if (env.base_url == null)
@@ -611,16 +624,35 @@
if (type instanceof Error) {
e = type;
- stack = (e.stack || '').split(/\n/);
if (msg)
e.message = msg + ': ' + e.message;
}
else {
+ try { throw new Error('stacktrace') }
+ catch (e2) { stack = (e2.stack || '').split(/\n/) }
+
e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
e.name = type || 'Error';
}
+ stack = (stack || []).map(function(frame) {
+ frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
+ return frame ? ' ' + frame : '';
+ });
+
+ if (!/^ at /.test(stack[0]))
+ stack.shift();
+
+ if (/\braise /.test(stack[0]))
+ stack.shift();
+
+ if (/\berror /.test(stack[0]))
+ stack.shift();
+
+ if (stack.length)
+ e.message += '\n' + stack.join('\n');
+
if (window.console && console.debug)
console.debug(e);
@@ -632,28 +664,16 @@
L.raise.apply(L, Array.prototype.slice.call(arguments));
}
catch (e) {
- var stack = (e.stack || '').split(/\n/).map(function(frame) {
- frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
- return frame ? ' ' + frame : '';
- });
-
- if (!/^ at /.test(stack[0]))
- stack.shift();
-
- if (/\braise /.test(stack[0]))
- stack.shift();
-
- if (/\berror /.test(stack[0]))
- stack.shift();
-
- stack = stack.length ? '\n' + stack.join('\n') : '';
+ if (!e.reported) {
+ if (L.ui)
+ L.ui.addNotification(e.name || _('Runtime error'),
+ E('pre', {}, e.message), 'danger');
+ else
+ L.dom.content(document.querySelector('#maincontent'),
+ E('pre', { 'class': 'alert-message error' }, e.message));
- if (L.ui)
- L.ui.showModal(e.name || _('Runtime error'),
- E('pre', { 'class': 'alert-message error' }, e.message + stack));
- else
- L.dom.content(document.querySelector('#maincontent'),
- E('pre', { 'class': 'alert-message error' }, e + stack));
+ e.reported = true;
+ }
throw e;
}
@@ -678,7 +698,7 @@
return classes[name];
}
- url = '%s/%s.js'.format(L.env.base_url, name.replace(/\./g, '/'));
+ url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
from = [ name ].concat(from);
var compileClass = function(res) {
@@ -835,6 +855,25 @@
return (ft != null && ft != false);
},
+ notifySessionExpiry: function() {
+ Poll.stop();
+
+ L.ui.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…')))
+ ]);
+
+ L.raise('SessionError', 'Login session is expired');
+ },
+
setupDOM: function(res) {
var domEv = res[0],
uiClass = res[1],
@@ -844,26 +883,31 @@
rpcClass.setBaseURL(rpcBaseURL);
- Request.addInterceptor(function(res) {
- if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
+ rpcClass.addInterceptor(function(msg, req) {
+ if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
return;
- Poll.stop();
-
- L.ui.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…')))
- ]);
+ if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
+ return;
+
+ return rpcClass.declare({
+ 'object': 'session',
+ 'method': 'access',
+ 'params': [ 'scope', 'object', 'function' ],
+ 'expect': { access: true }
+ })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
+ });
+
+ Request.addInterceptor(function(res) {
+ var isDenied = false;
+
+ if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
+ isDenied = true;
+
+ if (!isDenied)
+ return;
- throw 'Session expired';
+ L.notifySessionExpiry();
});
return this.probeSystemFeatures().finally(this.initDOM);
@@ -1230,9 +1274,9 @@
return inst[method].apply(inst, inst.varargs(arguments, 2));
},
- isEmpty: function(node) {
+ isEmpty: function(node, ignoreFn) {
for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
- if (!child.classList.contains('hidden'))
+ if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
return false;
return true;
@@ -1298,24 +1342,18 @@
if (mc.querySelector('.cbi-map')) {
footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
- E('input', {
+ E('button', {
'class': 'cbi-button cbi-button-apply',
- 'type': 'button',
- 'value': _('Save & Apply'),
- 'click': L.bind(this.handleSaveApply, this)
- }), ' ',
- E('input', {
+ 'click': L.ui.createHandlerFn(this, 'handleSaveApply')
+ }, _('Save & Apply')), ' ',
+ E('button', {
'class': 'cbi-button cbi-button-save',
- 'type': 'submit',
- 'value': _('Save'),
- 'click': L.bind(this.handleSave, this)
- }), ' ',
- E('input', {
+ 'click': L.ui.createHandlerFn(this, 'handleSave')
+ }, _('Save')), ' ',
+ E('button', {
'class': 'cbi-button cbi-button-reset',
- 'type': 'button',
- 'value': _('Reset'),
- 'click': L.bind(this.handleReset, this)
- })
+ 'click': L.ui.createHandlerFn(this, 'handleReset')
+ }, _('Reset'))
]));
}
diff --git a/modules/luci-base/htdocs/luci-static/resources/network.js b/modules/luci-base/htdocs/luci-static/resources/network.js
index fb23fba857..525b7c9f19 100644
--- a/modules/luci-base/htdocs/luci-static/resources/network.js
+++ b/modules/luci-base/htdocs/luci-static/resources/network.js
@@ -44,17 +44,18 @@ var iface_patterns_wireless = [
var iface_patterns_virtual = [ ];
-var callNetworkWirelessStatus = rpc.declare({
- object: 'network.wireless',
- method: 'status'
-});
-
var callLuciNetdevs = rpc.declare({
object: 'luci',
method: 'getNetworkDevices',
expect: { '': {} }
});
+var callLuciWifidevs = rpc.declare({
+ object: 'luci',
+ method: 'getWirelessDevices',
+ expect: { '': {} }
+});
+
var callLuciIfaddrs = rpc.declare({
object: 'luci',
method: 'getIfaddrs',
@@ -66,10 +67,18 @@ var callLuciBoardjson = rpc.declare({
method: 'getBoardJSON'
});
-var callIwinfoInfo = rpc.declare({
+var callIwinfoAssoclist = rpc.declare({
object: 'iwinfo',
- method: 'info',
- params: [ 'device' ]
+ method: 'assoclist',
+ params: [ 'device', 'mac' ],
+ expect: { results: [] }
+});
+
+var callIwinfoScan = rpc.declare({
+ object: 'iwinfo',
+ method: 'scan',
+ params: [ 'device' ],
+ expect: { results: [] }
});
var callNetworkInterfaceStatus = rpc.declare({
@@ -90,21 +99,17 @@ var callGetProtoHandlers = rpc.declare({
expect: { '': {} }
});
+var callGetHostHints = rpc.declare({
+ object: 'luci',
+ method: 'getHostHints',
+ expect: { '': {} }
+});
+
var _init = null,
_state = null,
_protocols = {},
_protospecs = {};
-function getWifiState(cache) {
- return callNetworkWirelessStatus().then(function(state) {
- if (!L.isObject(state))
- throw !1;
- return state;
- }).catch(function() {
- return {};
- });
-}
-
function getInterfaceState(cache) {
return callNetworkInterfaceStatus().then(function(state) {
if (!Array.isArray(state))
@@ -145,6 +150,16 @@ function getNetdevState(cache) {
});
}
+function getWifidevState(cache) {
+ return callLuciWifidevs().then(function(state) {
+ if (!L.isObject(state))
+ throw !1;
+ return state;
+ }).catch(function() {
+ return {};
+ });
+}
+
function getBoardState(cache) {
return callLuciBoardjson().then(function(state) {
if (!L.isObject(state))
@@ -160,6 +175,14 @@ function getProtocolHandlers(cache) {
if (!L.isObject(protos))
throw !1;
+ /* Register "none" protocol */
+ if (!protos.hasOwnProperty('none'))
+ Object.assign(protos, { none: { no_device: false } });
+
+ /* Hack: emulate relayd protocol */
+ if (!protos.hasOwnProperty('relay'))
+ Object.assign(protos, { relay: { no_device: true } });
+
Object.assign(_protospecs, protos);
return Promise.all(Object.keys(protos).map(function(p) {
@@ -175,6 +198,16 @@ function getProtocolHandlers(cache) {
});
}
+function getHostHints(cache) {
+ return callGetHostHints().then(function(hosts) {
+ if (!L.isObject(hosts))
+ throw !1;
+ return hosts;
+ }).catch(function() {
+ return {};
+ });
+}
+
function getWifiStateBySid(sid) {
var s = uci.get('wireless', sid);
@@ -188,8 +221,12 @@ function getWifiStateBySid(sid) {
var s2 = uci.get('wireless', netstate.section);
- if (s2 != null && s['.type'] == s2['.type'] && s['.name'] == s2['.name'])
+ if (s2 != null && s['.type'] == s2['.type'] && s['.name'] == s2['.name']) {
+ if (s2['.anonymous'] == false && netstate.section.charAt(0) == '@')
+ return null;
+
return [ radioname, _state.radios[radioname], netstate ];
+ }
}
}
}
@@ -221,37 +258,6 @@ function isWifiIfname(ifname) {
return false;
}
-function getWifiIwinfoByIfname(ifname, forcePhyOnly) {
- var tasks = [ callIwinfoInfo(ifname) ];
-
- if (!forcePhyOnly)
- tasks.push(getNetdevState());
-
- return Promise.all(tasks).then(function(info) {
- var iwinfo = info[0],
- devstate = info[1],
- phyonly = forcePhyOnly || !devstate[ifname] || (devstate[ifname].type != 1);
-
- if (L.isObject(iwinfo)) {
- if (phyonly) {
- delete iwinfo.bitrate;
- delete iwinfo.quality;
- delete iwinfo.quality_max;
- delete iwinfo.mode;
- delete iwinfo.ssid;
- delete iwinfo.bssid;
- delete iwinfo.encryption;
- }
-
- iwinfo.ifname = ifname;
- }
-
- return iwinfo;
- }).catch(function() {
- return null;
- });
-}
-
function getWifiSidByNetid(netid) {
var m = /^(\w+)\.network(\d+)$/.exec(netid);
if (m) {
@@ -427,14 +433,15 @@ function initNetworkState(refresh) {
if (_state == null || refresh) {
_init = _init || Promise.all([
getInterfaceState(), getDeviceState(), getBoardState(),
- getWifiState(), getIfaddrState(), getNetdevState(), getProtocolHandlers(),
+ getIfaddrState(), getNetdevState(), getWifidevState(),
+ getHostHints(), getProtocolHandlers(),
uci.load('network'), uci.load('wireless'), uci.load('luci')
]).then(function(data) {
- var board = data[2], ifaddrs = data[4], devices = data[5];
+ var board = data[2], ifaddrs = data[3], devices = data[4];
var s = {
isTunnel: {}, isBridge: {}, isSwitch: {}, isWifi: {},
- ifaces: data[0], devices: data[1], radios: data[3],
- netdevs: {}, bridges: {}, switches: {}
+ ifaces: data[0], devices: data[1], radios: data[5],
+ hosts: data[6], netdevs: {}, bridges: {}, switches: {}
};
for (var i = 0, a; (a = ifaddrs[i]) != null; i++) {
@@ -609,12 +616,91 @@ function deviceSort(a, b) {
return a.getName() > b.getName();
}
+function formatWifiEncryption(enc) {
+ if (!L.isObject(enc))
+ return null;
+
+ if (!enc.enabled)
+ return 'None';
+
+ var ciphers = Array.isArray(enc.ciphers)
+ ? enc.ciphers.map(function(c) { return c.toUpperCase() }) : [ 'NONE' ];
+
+ if (Array.isArray(enc.wep)) {
+ var has_open = false,
+ has_shared = false;
+
+ for (var i = 0; i < enc.wep.length; i++)
+ if (enc.wep[i] == 'open')
+ has_open = true;
+ else if (enc.wep[i] == 'shared')
+ has_shared = true;
+
+ if (has_open && has_shared)
+ return 'WEP Open/Shared (%s)'.format(ciphers.join(', '));
+ else if (has_open)
+ return 'WEP Open System (%s)'.format(ciphers.join(', '));
+ else if (has_shared)
+ return 'WEP Shared Auth (%s)'.format(ciphers.join(', '));
+
+ return 'WEP';
+ }
+
+ if (Array.isArray(enc.wpa)) {
+ var versions = [],
+ suites = Array.isArray(enc.authentication)
+ ? enc.authentication.map(function(a) { return a.toUpperCase() }) : [ 'NONE' ];
+
+ for (var i = 0; i < enc.wpa.length; i++)
+ switch (enc.wpa[i]) {
+ case 1:
+ versions.push('WPA');
+ break;
+
+ default:
+ versions.push('WPA%d'.format(enc.wpa[i]));
+ break;
+ }
+
+ if (versions.length > 1)
+ return 'mixed %s %s (%s)'.format(versions.join('/'), suites.join(', '), ciphers.join(', '));
+
+ return '%s %s (%s)'.format(versions[0], suites.join(', '), ciphers.join(', '));
+ }
+
+ return 'Unknown';
+}
+
+function enumerateNetworks() {
+ var uciInterfaces = uci.sections('network', 'interface'),
+ networks = {};
+
+ for (var i = 0; i < uciInterfaces.length; i++)
+ networks[uciInterfaces[i]['.name']] = this.instantiateNetwork(uciInterfaces[i]['.name']);
+
+ for (var i = 0; i < _state.ifaces.length; i++)
+ if (networks[_state.ifaces[i].interface] == null)
+ networks[_state.ifaces[i].interface] =
+ this.instantiateNetwork(_state.ifaces[i].interface, _state.ifaces[i].proto);
+
+ var rv = [];
+
+ for (var network in networks)
+ if (networks.hasOwnProperty(network))
+ rv.push(networks[network]);
+
+ rv.sort(networkSort);
+
+ return rv;
+}
+
-var Network, Protocol, Device, WifiDevice, WifiNetwork;
+var Hosts, Network, Protocol, Device, WifiDevice, WifiNetwork;
Network = L.Class.extend({
prefixToMask: prefixToMask,
maskToPrefix: maskToPrefix,
+ formatWifiEncryption: formatWifiEncryption,
flushCache: function() {
initNetworkState(true);
@@ -729,28 +815,7 @@ Network = L.Class.extend({
},
getNetworks: function() {
- return initNetworkState().then(L.bind(function() {
- var uciInterfaces = uci.sections('network', 'interface'),
- networks = {};
-
- for (var i = 0; i < uciInterfaces.length; i++)
- networks[uciInterfaces[i]['.name']] = this.instantiateNetwork(uciInterfaces[i]['.name']);
-
- for (var i = 0; i < _state.ifaces.length; i++)
- if (networks[_state.ifaces[i].interface] == null)
- networks[_state.ifaces[i].interface] =
- this.instantiateNetwork(_state.ifaces[i].interface, _state.ifaces[i].proto);
-
- var rv = [];
-
- for (var network in networks)
- if (networks.hasOwnProperty(network))
- rv.push(networks[network]);
-
- rv.sort(networkSort);
-
- return rv;
- }, this));
+ return initNetworkState().then(L.bind(enumerateNetworks, this));
},
deleteNetwork: function(name) {
@@ -967,38 +1032,26 @@ Network = L.Class.extend({
},
getWifiDevice: function(devname) {
- return Promise.all([ getWifiIwinfoByIfname(devname, true), initNetworkState() ]).then(L.bind(function(res) {
+ return initNetworkState().then(L.bind(function() {
var existingDevice = uci.get('wireless', devname);
if (existingDevice == null || existingDevice['.type'] != 'wifi-device')
return null;
- return this.instantiateWifiDevice(devname, res[0]);
+ return this.instantiateWifiDevice(devname, _state.radios[devname] || {});
}, this));
},
getWifiDevices: function() {
- var deviceNames = [];
-
return initNetworkState().then(L.bind(function() {
var uciWifiDevices = uci.sections('wireless', 'wifi-device'),
- tasks = [];
+ rv = [];
for (var i = 0; i < uciWifiDevices.length; i++) {
- tasks.push(callIwinfoInfo(uciWifiDevices['.name'], true));
- deviceNames.push(uciWifiDevices['.name']);
+ var devname = uciWifiDevices[i]['.name'];
+ rv.push(this.instantiateWifiDevice(devname, _state.radios[devname] || {}));
}
- return Promise.all(tasks);
- }, this)).then(L.bind(function(iwinfos) {
- var rv = [];
-
- for (var i = 0; i < deviceNames.length; i++)
- if (L.isObject(iwinfos[i]))
- rv.push(this.instantiateWifiDevice(deviceNames[i], iwinfos[i]));
-
- rv.sort(function(a, b) { return a.getName() < b.getName() });
-
return rv;
}, this));
},
@@ -1048,11 +1101,7 @@ Network = L.Class.extend({
}
}
- return (netstate ? getWifiIwinfoByIfname(netstate.ifname) : Promise.reject())
- .catch(function() { return radioname ? getWifiIwinfoByIfname(radioname) : Promise.reject() })
- .catch(function() { return Promise.resolve({ ifname: netid || sid || netname }) });
- }, this)).then(L.bind(function(iwinfo) {
- return this.instantiateWifiNetwork(sid || netname, radioname, radiostate, netid, netstate, iwinfo);
+ return this.instantiateWifiNetwork(sid || netname, radioname, radiostate, netid, netstate);
}, this));
},
@@ -1075,7 +1124,7 @@ Network = L.Class.extend({
var radioname = existingDevice['.name'],
netid = getWifiNetidBySid(sid) || [];
- return this.instantiateWifiNetwork(sid, radioname, _state.radios[radioname], netid[0], null, { ifname: netid });
+ return this.instantiateWifiNetwork(sid, radioname, _state.radios[radioname], netid[0], null);
}, this));
},
@@ -1187,16 +1236,19 @@ Network = L.Class.extend({
return new protoClass(name);
},
- instantiateDevice: function(name, network) {
+ instantiateDevice: function(name, network, extend) {
+ if (extend != null)
+ return new (Device.extend(extend))(name, network);
+
return new Device(name, network);
},
- instantiateWifiDevice: function(radioname, iwinfo) {
- return new WifiDevice(radioname, iwinfo);
+ instantiateWifiDevice: function(radioname, radiostate) {
+ return new WifiDevice(radioname, radiostate);
},
- instantiateWifiNetwork: function(sid, radioname, radiostate, netid, netstate, iwinfo) {
- return new WifiNetwork(sid, radioname, radiostate, netid, netstate, iwinfo);
+ instantiateWifiNetwork: function(sid, radioname, radiostate, netid, netstate) {
+ return new WifiNetwork(sid, radioname, radiostate, netid, netstate);
},
getIfnameOf: function(obj) {
@@ -1207,6 +1259,70 @@ Network = L.Class.extend({
return initNetworkState().then(function() {
return _state.hasDSLModem ? _state.hasDSLModem.type : null;
});
+ },
+
+ getHostHints: function() {
+ return initNetworkState().then(function() {
+ return new Hosts(_state.hosts);
+ });
+ }
+});
+
+Hosts = L.Class.extend({
+ __init__: function(hosts) {
+ this.hosts = hosts;
+ },
+
+ getHostnameByMACAddr: function(mac) {
+ return this.hosts[mac] ? this.hosts[mac].name : null;
+ },
+
+ getIPAddrByMACAddr: function(mac) {
+ return this.hosts[mac] ? this.hosts[mac].ipv4 : null;
+ },
+
+ getIP6AddrByMACAddr: function(mac) {
+ return this.hosts[mac] ? this.hosts[mac].ipv6 : null;
+ },
+
+ getHostnameByIPAddr: function(ipaddr) {
+ for (var mac in this.hosts)
+ if (this.hosts[mac].ipv4 == ipaddr && this.hosts[mac].name != null)
+ return this.hosts[mac].name;
+ return null;
+ },
+
+ getMACAddrByIPAddr: function(ipaddr) {
+ for (var mac in this.hosts)
+ if (this.hosts[mac].ipv4 == ipaddr)
+ return mac;
+ return null;
+ },
+
+ getHostnameByIP6Addr: function(ip6addr) {
+ for (var mac in this.hosts)
+ if (this.hosts[mac].ipv6 == ip6addr && this.hosts[mac].name != null)
+ return this.hosts[mac].name;
+ return null;
+ },
+
+ getMACAddrByIP6Addr: function(ip6addr) {
+ for (var mac in this.hosts)
+ if (this.hosts[mac].ipv6 == ip6addr)
+ return mac;
+ return null;
+ },
+
+ getMACHints: function(preferIp6) {
+ var rv = [];
+ for (var mac in this.hosts) {
+ var hint = this.hosts[mac].name ||
+ this.hosts[mac][preferIp6 ? 'ipv6' : 'ipv4'] ||
+ this.hosts[mac][preferIp6 ? 'ipv4' : 'ipv6'];
+
+ rv.push([mac, hint]);
+ }
+ return rv.sort(function(a, b) { return a[0] > b[0] });
}
});
@@ -1802,7 +1918,7 @@ Device = L.Class.extend({
if (this.networks == null) {
this.networks = [];
- var networks = L.network.getNetworks();
+ var networks = enumerateNetworks.apply(L.network);
for (var i = 0; i < networks.length; i++)
if (networks[i].containsDevice(this.ifname) || networks[i].getIfname() == this.ifname)
@@ -1820,18 +1936,32 @@ Device = L.Class.extend({
});
WifiDevice = L.Class.extend({
- __init__: function(name, iwinfo) {
+ __init__: function(name, radiostate) {
var uciWifiDevice = uci.get('wireless', name);
if (uciWifiDevice != null &&
uciWifiDevice['.type'] == 'wifi-device' &&
uciWifiDevice['.name'] != null) {
this.sid = uciWifiDevice['.name'];
- this.iwinfo = iwinfo;
}
- this.sid = this.sid || name;
- this.iwinfo = this.iwinfo || { ifname: this.sid };
+ this.sid = this.sid || name;
+ this._ubusdata = {
+ radio: name,
+ dev: radiostate
+ };
+ },
+
+ ubus: function(/* ... */) {
+ var v = this._ubusdata;
+
+ for (var i = 0; i < arguments.length; i++)
+ if (L.isObject(v))
+ v = v[arguments[i]];
+ else
+ return null;
+
+ return v;
},
get: function(opt) {
@@ -1842,34 +1972,45 @@ WifiDevice = L.Class.extend({
return uci.set('wireless', this.sid, opt, value);
},
+ isDisabled: function() {
+ return this.ubus('dev', 'disabled') || this.get('disabled') == '1';
+ },
+
getName: function() {
return this.sid;
},
getHWModes: function() {
- if (L.isObject(this.iwinfo.hwmodelist))
- for (var k in this.iwinfo.hwmodelist)
- return this.iwinfo.hwmodelist;
+ var hwmodes = this.ubus('dev', 'iwinfo', 'hwmodes');
+ return Array.isArray(hwmodes) ? hwmodes : [ 'b', 'g' ];
+ },
- return { b: true, g: true };
+ getHTModes: function() {
+ var htmodes = this.ubus('dev', 'iwinfo', 'htmodes');
+ return (Array.isArray(htmodes) && htmodes.length) ? htmodes : null;
},
getI18n: function() {
- var type = this.iwinfo.hardware_name || 'Generic';
+ var hw = this.ubus('dev', 'iwinfo', 'hardware'),
+ type = L.isObject(hw) ? hw.name : null;
- if (this.iwinfo.type == 'wl')
+ if (this.ubus('dev', 'iwinfo', 'type') == 'wl')
type = 'Broadcom';
var hwmodes = this.getHWModes(),
modestr = '';
- if (hwmodes.a) modestr += 'a';
- if (hwmodes.b) modestr += 'b';
- if (hwmodes.g) modestr += 'g';
- if (hwmodes.n) modestr += 'n';
- if (hwmodes.ad) modestr += 'ac';
+ hwmodes.sort(function(a, b) {
+ return (a.length != b.length ? a.length > b.length : a > b);
+ });
+
+ modestr = hwmodes.join('');
- return '%s 802.11%s Wireless Controller (%s)'.format(type, modestr, this.getName());
+ return '%s 802.11%s Wireless Controller (%s)'.format(type || 'Generic', modestr, this.getName());
+ },
+
+ getScanList: function() {
+ return callIwinfoScan(this.sid);
},
isUp: function() {
@@ -1933,10 +2074,8 @@ WifiDevice = L.Class.extend({
});
WifiNetwork = L.Class.extend({
- __init__: function(sid, radioname, radiostate, netid, netstate, iwinfo) {
+ __init__: function(sid, radioname, radiostate, netid, netstate) {
this.sid = sid;
- this.wdev = iwinfo.ifname;
- this.iwinfo = iwinfo;
this.netid = netid;
this._ubusdata = {
radio: radioname,
@@ -1965,6 +2104,10 @@ WifiNetwork = L.Class.extend({
return uci.set('wireless', this.sid, opt, value);
},
+ isDisabled: function() {
+ return this.ubus('dev', 'disabled') || this.get('disabled') == '1';
+ },
+
getMode: function() {
return this.ubus('net', 'config', 'mode') || this.get('mode') || 'ap';
},
@@ -1990,7 +2133,7 @@ WifiNetwork = L.Class.extend({
},
getIfname: function() {
- var ifname = this.ubus('net', 'ifname') || this.iwinfo.ifname;
+ var ifname = this.ubus('net', 'ifname') || this.ubus('net', 'iwinfo', 'ifname');
if (ifname == null || ifname.match(/^(wifi|radio)\d/))
ifname = this.netid;
@@ -1998,8 +2141,12 @@ WifiNetwork = L.Class.extend({
return ifname;
},
+ getWifiDeviceName: function() {
+ return this.ubus('radio') || this.get('device');
+ },
+
getWifiDevice: function() {
- var radioname = this.ubus('radio') || this.get('device');
+ var radioname = this.getWifiDeviceName();
if (radioname == null)
return Promise.reject();
@@ -2017,7 +2164,7 @@ WifiNetwork = L.Class.extend({
},
getActiveMode: function() {
- var mode = this.iwinfo.mode || this.ubus('net', 'config', 'mode') || this.get('mode') || 'ap';
+ var mode = this.ubus('net', 'iwinfo', 'mode') || this.ubus('net', 'config', 'mode') || this.get('mode') || 'ap';
switch (mode) {
case 'ap': return 'Master';
@@ -2043,25 +2190,23 @@ WifiNetwork = L.Class.extend({
},
getActiveSSID: function() {
- return this.iwinfo.ssid || this.ubus('net', 'config', 'ssid') || this.get('ssid');
+ return this.ubus('net', 'iwinfo', 'ssid') || this.ubus('net', 'config', 'ssid') || this.get('ssid');
},
getActiveBSSID: function() {
- return this.iwinfo.bssid || this.ubus('net', 'config', 'bssid') || this.get('bssid');
+ return this.ubus('net', 'iwinfo', 'bssid') || this.ubus('net', 'config', 'bssid') || this.get('bssid');
},
getActiveEncryption: function() {
- var encryption = this.iwinfo.encryption;
-
- return (L.isObject(encryption) ? encryption.description || '-' : '-');
+ return formatWifiEncryption(this.ubus('net', 'iwinfo', 'encryption')) || '-';
},
getAssocList: function() {
- // XXX tbd
+ return callIwinfoAssoclist(this.getIfname());
},
getFrequency: function() {
- var freq = this.iwinfo.frequency;
+ var freq = this.ubus('net', 'iwinfo', 'frequency');
if (freq != null && freq > 0)
return '%.03f'.format(freq / 1000);
@@ -2070,7 +2215,7 @@ WifiNetwork = L.Class.extend({
},
getBitRate: function() {
- var rate = this.iwinfo.bitrate;
+ var rate = this.ubus('net', 'iwinfo', 'bitrate');
if (rate != null && rate > 0)
return (rate / 1000);
@@ -2079,28 +2224,27 @@ WifiNetwork = L.Class.extend({
},
getChannel: function() {
- return this.iwinfo.channel || this.ubus('dev', 'config', 'channel') || this.get('channel');
+ return this.ubus('net', 'iwinfo', 'channel') || this.ubus('dev', 'config', 'channel') || this.get('channel');
},
getSignal: function() {
- return this.iwinfo.signal || 0;
+ return this.ubus('net', 'iwinfo', 'signal') || 0;
},
getNoise: function() {
- return this.iwinfo.noise || 0;
+ return this.ubus('net', 'iwinfo', 'noise') || 0;
},
getCountryCode: function() {
- return this.iwinfo.country || this.ubus('dev', 'config', 'country') || '00';
+ return this.ubus('net', 'iwinfo', 'country') || this.ubus('dev', 'config', 'country') || '00';
},
getTXPower: function() {
- var pwr = this.iwinfo.txpower || 0;
- return (pwr + this.getTXPowerOffset());
+ return this.ubus('net', 'iwinfo', 'txpower');
},
getTXPowerOffset: function() {
- return this.iwinfo.txpower_offset || 0;
+ return this.ubus('net', 'iwinfo', 'txpower_offset') || 0;
},
getSignalLevel: function(signal, noise) {
@@ -2119,8 +2263,8 @@ WifiNetwork = L.Class.extend({
},
getSignalPercent: function() {
- var qc = this.iwinfo.quality || 0,
- qm = this.iwinfo.quality_max || 0;
+ var qc = this.ubus('net', 'iwinfo', 'quality') || 0,
+ qm = this.ubus('net', 'iwinfo', 'quality_max') || 0;
if (qc > 0 && qm > 0)
return Math.floor((100 / qm) * qc);
diff --git a/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js b/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js
new file mode 100644
index 0000000000..f0a3ec579c
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js
@@ -0,0 +1,62 @@
+'use strict';
+'require rpc';
+'require form';
+'require network';
+
+var callFileRead = rpc.declare({
+ object: 'file',
+ method: 'read',
+ params: [ 'path' ],
+ expect: { data: '' },
+ filter: function(value) { return value.trim() }
+});
+
+return network.registerProtocol('dhcp', {
+ getI18n: function() {
+ return _('DHCP client');
+ },
+
+ renderFormOptions: function(s) {
+ var dev = this.getL2Device() || this.getDevice(), o;
+
+ o = s.taboption('general', form.Value, 'hostname', _('Hostname to send when requesting DHCP'));
+ o.datatype = 'hostname';
+ o.load = function(section_id) {
+ return callFileRead('/proc/sys/kernel/hostname').then(L.bind(function(hostname) {
+ this.placeholder = hostname;
+ return form.Value.prototype.load.apply(this, [section_id]);
+ }, this));
+ };
+
+ o = s.taboption('advanced', form.Flag, 'broadcast', _('Use broadcast flag'), _('Required for certain ISPs, e.g. Charter with DOCSIS 3'));
+ o.default = o.disabled;
+
+ o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
+ o.default = o.enabled;
+
+ o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
+ o.default = o.enabled;
+
+ o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
+ o.depends('peerdns', '0');
+ o.datatype = 'ipaddr';
+ o.cast = 'string';
+
+ o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
+ o.placeholder = '0';
+ o.datatype = 'uinteger';
+
+ o = s.taboption('advanced', form.Value, 'clientid', _('Client ID to send when requesting DHCP'));
+ o.datatype = 'hexstring';
+
+ s.taboption('advanced', form.Value, 'vendorid', _('Vendor Class to send when requesting DHCP'));
+
+ o = s.taboption('advanced', form.Value, 'macaddr', _('Override MAC address'));
+ o.datatype = 'macaddr';
+ o.placeholder = dev ? (dev.getMAC() || '') : '';
+
+ o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
+ o.placeholder = dev ? (dev.getMTU() || '1500') : '1500';
+ o.datatype = 'max(9200)';
+ }
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/protocol/none.js b/modules/luci-base/htdocs/luci-static/resources/protocol/none.js
new file mode 100644
index 0000000000..37674c0ea4
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/protocol/none.js
@@ -0,0 +1,8 @@
+'use strict';
+'require network';
+
+return network.registerProtocol('none', {
+ getI18n: function() {
+ return _('Unmanaged');
+ }
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/protocol/static.js b/modules/luci-base/htdocs/luci-static/resources/protocol/static.js
new file mode 100644
index 0000000000..8470e0a20e
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/protocol/static.js
@@ -0,0 +1,231 @@
+'use strict';
+'require form';
+'require network';
+'require validation';
+
+function isCIDR(value) {
+ return Array.isArray(value) || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/(\d{1,2}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.test(value);
+}
+
+function calculateBroadcast(s, use_cfgvalue) {
+ var readfn = use_cfgvalue ? 'cfgvalue' : 'formvalue',
+ addropt = s.children.filter(function(o) { return o.option == 'ipaddr'})[0],
+ addrvals = addropt ? L.toArray(addropt[readfn](s.section)) : [],
+ maskopt = s.children.filter(function(o) { return o.option == 'netmask'})[0],
+ maskval = maskopt ? maskopt[readfn](s.section) : null,
+ firstsubnet = maskval ? addrvals[0] + '/' + maskval : addrvals.filter(function(a) { return a.indexOf('/') > 0 })[0];
+
+ if (firstsubnet == null)
+ return null;
+
+ var addr_mask = firstsubnet.split('/'),
+ addr = validation.parseIPv4(addr_mask[0]),
+ mask = addr_mask[1];
+
+ if (!isNaN(mask))
+ mask = validation.parseIPv4(network.prefixToMask(+mask));
+ else
+ mask = validation.parseIPv4(mask);
+
+ var bc = [
+ addr[0] | (~mask[0] >>> 0 & 255),
+ addr[1] | (~mask[1] >>> 0 & 255),
+ addr[2] | (~mask[2] >>> 0 & 255),
+ addr[3] | (~mask[3] >>> 0 & 255)
+ ];
+
+ return bc.join('.');
+}
+
+function validateBroadcast(section_id, value) {
+ var opt = this.map.lookupOption('broadcast', section_id),
+ node = opt ? this.map.findElement('id', opt[0].cbid(section_id)) : null,
+ addr = node ? calculateBroadcast(this.section, false) : null;
+
+ if (node != null) {
+ if (addr != null)
+ node.querySelector('input').setAttribute('placeholder', addr);
+ else
+ node.querySelector('input').removeAttribute('placeholder');
+ }
+
+ return true;
+}
+
+return network.registerProtocol('static', {
+ CBIIPValue: form.Value.extend({
+ handleSwitch: function(section_id, option_index, ev) {
+ var maskopt = this.map.lookupOption('netmask', section_id);
+
+ if (maskopt == null || !this.isValid(section_id))
+ return;
+
+ var maskval = maskopt[0].formvalue(section_id),
+ addrval = this.formvalue(section_id),
+ prefix = maskval ? network.maskToPrefix(maskval) : 32;
+
+ if (prefix == null)
+ return;
+
+ this.datatype = 'or(cidr4,ipmask4)';
+
+ var parent = L.dom.parent(ev.target, '.cbi-value-field');
+ L.dom.content(parent, form.DynamicList.prototype.renderWidget.apply(this, [
+ section_id,
+ option_index,
+ addrval ? '%s/%d'.format(addrval, prefix) : ''
+ ]));
+
+ var masknode = this.map.findElement('id', maskopt[0].cbid(section_id));
+ if (masknode) {
+ parent = L.dom.parent(masknode, '.cbi-value');
+ parent.parentNode.removeChild(parent);
+ }
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var maskopt = this.map.lookupOption('netmask', section_id),
+ widget = isCIDR(cfgvalue) ? 'DynamicList' : 'Value';
+
+ if (widget == 'DynamicList') {
+ this.datatype = 'or(cidr4,ipmask4)';
+ this.placeholder = _('Add IPv4 address…');
+ }
+ else {
+ this.datatype = 'ip4addr("nomask")';
+ }
+
+ var node = form[widget].prototype.renderWidget.apply(this, [ section_id, option_index, cfgvalue ]);
+
+ if (widget == 'Value')
+ L.dom.append(node, E('button', {
+ 'class': 'cbi-button cbi-button-neutral',
+ 'title': _('Switch to CIDR list notation'),
+ 'aria-label': _('Switch to CIDR list notation'),
+ 'click': L.bind(this.handleSwitch, this, section_id, option_index)
+ }, '…'));
+
+ return node;
+ },
+
+ validate: validateBroadcast
+ }),
+
+ CBINetmaskValue: form.Value.extend({
+ render: function(option_index, section_id, in_table) {
+ var addropt = this.section.children.filter(function(o) { return o.option == 'ipaddr' })[0],
+ addrval = addropt ? addropt.cfgvalue(section_id) : null;
+
+ if (addrval != null && isCIDR(addrval))
+ return E([]);
+
+ this.value('255.255.255.0');
+ this.value('255.255.0.0');
+ this.value('255.0.0.0');
+
+ return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
+ },
+
+ validate: validateBroadcast
+ }),
+
+ CBIGatewayValue: form.Value.extend({
+ datatype: 'ip4addr("nomask")',
+
+ render: function(option_index, section_id, in_table) {
+ return network.getWANNetworks().then(L.bind(function(wans) {
+ if (wans.length == 1) {
+ var gwaddr = wans[0].getGatewayAddr();
+ this.placeholder = gwaddr ? '%s (%s)'.format(gwaddr, wans[0].getName()) : '';
+ }
+
+ return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
+ }, this));
+ },
+
+ validate: function(section_id, value) {
+ var addropt = this.section.children.filter(function(o) { return o.option == 'ipaddr' })[0],
+ addrval = addropt ? L.toArray(addropt.cfgvalue(section_id)) : null;
+
+ if (addrval != null) {
+ for (var i = 0; i < addrval.length; i++) {
+ var addr = addrval[i].split('/')[0];
+ if (value == addr)
+ return _('The gateway address must not be a local IP address');
+ }
+ }
+
+ return true;
+ }
+ }),
+
+ CBIBroadcastValue: form.Value.extend({
+ datatype: 'ip4addr("nomask")',
+
+ render: function(option_index, section_id, in_table) {
+ this.placeholder = calculateBroadcast(this.section, true);
+ return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
+ }
+ }),
+
+ getI18n: function() {
+ return _('Static address');
+ },
+
+ renderFormOptions: function(s) {
+ var dev = this.getL2Device() || this.getDevice(), o;
+
+ s.taboption('general', this.CBIIPValue, 'ipaddr', _('IPv4 address'));
+ s.taboption('general', this.CBINetmaskValue, 'netmask', _('IPv4 netmask'));
+ s.taboption('general', this.CBIGatewayValue, 'gateway', _('IPv4 gateway'));
+ s.taboption('general', this.CBIBroadcastValue, 'broadcast', _('IPv4 broadcast'));
+ s.taboption('general', form.DynamicList, 'dns', _('Use custom DNS servers'));
+
+ o = s.taboption('general', form.Value, 'ip6assign', _('IPv6 assignment length'), _('Assign a part of given length of every public IPv6-prefix to this interface'));
+ o.value('', _('disabled'));
+ o.value('64');
+ o.datatype = 'max(64)';
+
+ o = s.taboption('general', form.Value, 'ip6hint', _('IPv6 assignment hint'), _('Assign prefix parts using this hexadecimal subprefix ID for this interface.'));
+ o.placeholder = '0';
+ o.validate = function(section_id, value) {
+ var n = parseInt(value, 16);
+
+ if (!/^(0x)?[0-9a-fA-F]+$/.test(value) || isNaN(n) || n >= 0xffffffff)
+ return _('Expecting an hexadecimal assignment hint');
+
+ return true;
+ };
+ for (var i = 33; i <= 64; i++)
+ o.depends('ip6assign', String(i));
+
+ o = s.taboption('general', form.DynamicList, 'ip6addr', _('IPv6 address'));
+ o.datatype = 'ip6addr';
+ o.placeholder = _('Add IPv6 address…');
+ o.depends('ip6assign', '');
+
+ o = s.taboption('general', form.Value, 'ip6gw', _('IPv6 gateway'));
+ o.datatype = 'ip6addr("nomask")';
+ o.depends('ip6assign', '');
+
+ o = s.taboption('general', form.Value, 'ip6prefix', _('IPv6 routed prefix'), _('Public prefix routed to this device for distribution to clients.'));
+ o.datatype = 'ip6addr';
+ o.depends('ip6assign', '');
+
+ o = s.taboption('general', form.Value, 'ip6ifaceid', _('IPv6 suffix'), _("Optional. Allowed values: 'eui64', 'random', fixed value like '::1' or '::1:2'. When IPv6 prefix (like 'a:b:c:d::') is received from a delegating server, use the suffix (like '::1') to form the IPv6 address ('a:b:c:d::1') for the interface."));
+ o.datatype = 'ip6hostid';
+ o.placeholder = '::1';
+
+ o = s.taboption('advanced', form.Value, 'macaddr', _('Override MAC address'));
+ o.datatype = 'macaddr';
+ o.placeholder = dev ? (dev.getMAC() || '') : '';
+
+ o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
+ o.datatype = 'max(9200)';
+ o.placeholder = dev ? (dev.getMTU() || '1500') : '1500';
+
+ o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
+ o.placeholder = this.getMetric() || '0';
+ o.datatype = 'uinteger';
+ }
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js
index e12c2f77ee..87850b8564 100644
--- a/modules/luci-base/htdocs/luci-static/resources/rpc.js
+++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js
@@ -2,7 +2,8 @@
var rpcRequestID = 1,
rpcSessionID = L.env.sessionid || '00000000000000000000000000000000',
- rpcBaseURL = L.url('admin/ubus');
+ rpcBaseURL = L.url('admin/ubus'),
+ rpcInterceptorFns = [];
return L.Class.extend({
call: function(req, cb) {
@@ -13,55 +14,73 @@ return L.Class.extend({
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]
- );
+ if (req[i].params)
+ q += '%s%s.%s'.format(
+ q ? ';' : '/',
+ req[i].params[1],
+ req[i].params[2]
+ );
}
- else {
+ else if (req.params) {
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);
+ }).then(cb, cb);
},
- handleListReply: function(req, msg) {
- var list = msg.result;
+ parseCallReply: function(req, res) {
+ var msg = null;
- /* verify message frame */
- if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !Array.isArray(list))
- list = [ ];
+ if (res instanceof Error)
+ return req.reject(res);
- req.resolve(list);
- },
-
- handleCallReply: function(req, res) {
- var type = Object.prototype.toString,
- msg = null;
+ try {
+ if (!res.ok)
+ L.raise('RPCError', 'RPC call to %s/%s failed with HTTP error %d: %s',
+ req.object, req.method, res.status, res.statusText || '?');
- if (!res.ok)
- L.error('RPCError', 'RPC call failed with HTTP error %d: %s',
- res.status, res.statusText || '?');
+ msg = res.json();
+ }
+ catch (e) {
+ return req.reject(e);
+ }
- msg = res.json();
+ /*
+ * The interceptor args are intentionally swapped.
+ * Response is passed as first arg to align with Request class interceptors
+ */
+ Promise.all(rpcInterceptorFns.map(function(fn) { return fn(msg, req) }))
+ .then(this.handleCallReply.bind(this, req, msg))
+ .catch(req.reject);
+ },
- /* fetch response attribute and verify returned type */
- var ret = undefined;
+ handleCallReply: function(req, msg) {
+ var type = Object.prototype.toString,
+ ret = null;
+
+ try {
+ /* verify message frame */
+ if (!L.isObject(msg) || msg.jsonrpc != '2.0')
+ L.raise('RPCError', 'RPC call to %s/%s returned invalid message frame',
+ req.object, req.method);
+
+ /* check error condition */
+ if (L.isObject(msg.error) && msg.error.code && msg.error.message)
+ L.raise('RPCError', 'RPC call to %s/%s failed with error %d: %s',
+ req.object, req.method, msg.error.code, msg.error.message || '?');
+ }
+ catch (e) {
+ return req.reject(e);
+ }
- /* 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];
+ if (!req.object && !req.method) {
+ ret = msg.result;
}
- else {
- req.reject(new Error('Invalid message frame received'));
+ else if (Array.isArray(msg.result)) {
+ ret = (msg.result.length > 1) ? msg.result[1] : msg.result[0];
}
if (req.expect) {
@@ -94,7 +113,16 @@ return L.Class.extend({
params: arguments.length ? this.varargs(arguments) : undefined
};
- return this.call(msg, this.handleListReply);
+ return new Promise(L.bind(function(resolveFn, rejectFn) {
+ /* store request info */
+ var req = {
+ resolve: resolveFn,
+ reject: rejectFn
+ };
+
+ /* call rpc */
+ this.call(msg, this.parseCallReply.bind(this, req));
+ }, this));
},
declare: function(options) {
@@ -120,7 +148,9 @@ return L.Class.extend({
resolve: resolveFn,
reject: rejectFn,
params: params,
- priv: priv
+ priv: priv,
+ object: options.object,
+ method: options.method
};
/* build message object */
@@ -137,7 +167,7 @@ return L.Class.extend({
};
/* call rpc */
- rpc.call(msg, rpc.handleCallReply.bind(rpc, req));
+ rpc.call(msg, rpc.parseCallReply.bind(rpc, req));
});
}, this, this, options);
},
@@ -156,5 +186,36 @@ return L.Class.extend({
setBaseURL: function(url) {
rpcBaseURL = url;
+ },
+
+ getStatusText: function(statusCode) {
+ switch (statusCode) {
+ case 0: return _('Command OK');
+ case 1: return _('Invalid command');
+ case 2: return _('Invalid argument');
+ case 3: return _('Method not found');
+ case 4: return _('Resource not found');
+ case 5: return _('No data received');
+ case 6: return _('Permission denied');
+ case 7: return _('Request timeout');
+ case 8: return _('Not supported');
+ case 9: return _('Unspecified error');
+ case 10: return _('Connection lost');
+ default: return _('Unknown error code');
+ }
+ },
+
+ addInterceptor: function(interceptorFn) {
+ if (typeof(interceptorFn) == 'function')
+ rpcInterceptorFns.push(interceptorFn);
+ return interceptorFn;
+ },
+
+ removeInterceptor: function(interceptorFn) {
+ var oldlen = rpcInterceptorFns.length, i = oldlen;
+ while (i--)
+ if (rpcInterceptorFns[i] === interceptorFn)
+ rpcInterceptorFns.splice(i, 1);
+ return (rpcInterceptorFns.length < oldlen);
}
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
index 861d8c8cea..1667fa6707 100644
--- a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
+++ b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
@@ -478,7 +478,7 @@ var CBIDeviceSelect = form.ListValue.extend({
var networks = device.getNetworks();
if (networks.length > 0)
- L.dom.append(item.lastChild, [ ' (', networks.join(', '), ')' ]);
+ L.dom.append(item.lastChild, [ ' (', networks.map(function(n) { return n.getName() }).join(', '), ')' ]);
if (checked[name])
values.push(name);
diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
index 77cef53964..fed5dafa33 100644
--- a/modules/luci-base/htdocs/luci-static/resources/ui.js
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -1,4 +1,5 @@
'use strict';
+'require rpc';
'require uci';
'require validation';
@@ -1453,6 +1454,417 @@ var UIHiddenfield = UIElement.extend({
}
});
+var UIFileUpload = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ show_hidden: false,
+ enable_upload: true,
+ enable_remove: true,
+ root_directory: '/etc/luci-uploads'
+ }, options);
+ },
+
+ callFileStat: rpc.declare({
+ 'object': 'file',
+ 'method': 'stat',
+ 'params': [ 'path' ],
+ 'expect': { '': {} }
+ }),
+
+ callFileList: rpc.declare({
+ 'object': 'file',
+ 'method': 'list',
+ 'params': [ 'path' ],
+ 'expect': { 'entries': [] }
+ }),
+
+ callFileRemove: rpc.declare({
+ 'object': 'file',
+ 'method': 'remove',
+ 'params': [ 'path' ]
+ }),
+
+ bind: function(browserEl) {
+ this.node = browserEl;
+
+ this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
+ this.setChangeEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
+
+ L.dom.bindClassInstance(browserEl, this);
+
+ return browserEl;
+ },
+
+ render: function() {
+ return Promise.resolve(this.value != null ? this.callFileStat(this.value) : null).then(L.bind(function(stat) {
+ var label;
+
+ if (L.isObject(stat) && stat.type != 'directory')
+ this.stat = stat;
+
+ if (this.stat != null)
+ label = [ this.iconForType(this.stat.type), ' %s (%1000mB)'.format(this.truncatePath(this.stat.path), this.stat.size) ];
+ else if (this.value != null)
+ label = [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value), _('File not accessible')) ];
+ else
+ label = _('Select file…');
+
+ return this.bind(E('div', { 'id': this.options.id }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleFileBrowser')
+ }, label),
+ E('div', {
+ 'class': 'cbi-filebrowser'
+ }),
+ E('input', {
+ 'type': 'hidden',
+ 'name': this.options.name,
+ 'value': this.value
+ })
+ ]));
+ }, this));
+ },
+
+ truncatePath: function(path) {
+ if (path.length > 50)
+ path = path.substring(0, 25) + '…' + path.substring(path.length - 25);
+
+ return path;
+ },
+
+ iconForType: function(type) {
+ switch (type) {
+ case 'symlink':
+ return E('img', {
+ 'src': L.resource('cbi/link.gif'),
+ 'title': _('Symbolic link'),
+ 'class': 'middle'
+ });
+
+ case 'directory':
+ return E('img', {
+ 'src': L.resource('cbi/folder.gif'),
+ 'title': _('Directory'),
+ 'class': 'middle'
+ });
+
+ default:
+ return E('img', {
+ 'src': L.resource('cbi/file.gif'),
+ 'title': _('File'),
+ 'class': 'middle'
+ });
+ }
+ },
+
+ canonicalizePath: function(path) {
+ return path.replace(/\/{2,}/, '/')
+ .replace(/\/\.(\/|$)/g, '/')
+ .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
+ .replace(/\/$/, '');
+ },
+
+ splitPath: function(path) {
+ var croot = this.canonicalizePath(this.options.root_directory || '/'),
+ cpath = this.canonicalizePath(path || '/');
+
+ if (cpath.length <= croot.length)
+ return [ croot ];
+
+ if (cpath.charAt(croot.length) != '/')
+ return [ croot ];
+
+ var parts = cpath.substring(croot.length + 1).split(/\//);
+
+ parts.unshift(croot);
+
+ return parts;
+ },
+
+ handleUpload: function(path, list, ev) {
+ var form = ev.target.parentNode,
+ fileinput = form.querySelector('input[type="file"]'),
+ nameinput = form.querySelector('input[type="text"]'),
+ filename = (nameinput.value != null ? nameinput.value : '').trim();
+
+ ev.preventDefault();
+
+ if (filename == '' || filename.match(/\//) || fileinput.files[0] == null)
+ return;
+
+ var existing = list.filter(function(e) { return e.name == filename })[0];
+
+ if (existing != null && existing.type == 'directory')
+ return alert(_('A directory with the same name already exists.'));
+ else if (existing != null && !confirm(_('Overwrite existing file "%s" ?').format(filename)))
+ return;
+
+ var data = new FormData();
+
+ data.append('sessionid', L.env.sessionid);
+ data.append('filename', path + '/' + filename);
+ data.append('filedata', fileinput.files[0]);
+
+ return L.Request.post('/cgi-bin/cgi-upload', data, {
+ progress: L.bind(function(btn, ev) {
+ btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
+ }, this, ev.target)
+ }).then(L.bind(function(path, ev, res) {
+ var reply = res.json();
+
+ if (L.isObject(reply) && reply.failure)
+ alert(_('Upload request failed: %s').format(reply.message));
+
+ return this.handleSelect(path, null, ev);
+ }, this, path, ev));
+ },
+
+ handleDelete: function(path, fileStat, ev) {
+ var parent = path.replace(/\/[^\/]+$/, '') || '/',
+ name = path.replace(/^.+\//, ''),
+ msg;
+
+ ev.preventDefault();
+
+ if (fileStat.type == 'directory')
+ msg = _('Do you really want to recursively delete the directory "%s" ?').format(name);
+ else
+ msg = _('Do you really want to delete "%s" ?').format(name);
+
+ if (confirm(msg)) {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ if (path == hidden.value) {
+ L.dom.content(button, _('Select file…'));
+ hidden.value = '';
+ }
+
+ return this.callFileRemove(path).then(L.bind(function(parent, ev, rc) {
+ if (rc == 0)
+ return this.handleSelect(parent, null, ev);
+ else if (rc == 6)
+ alert(_('Delete permission denied'));
+ else
+ alert(_('Delete request failed: %d %s').format(rc, rpc.getStatusText(rc)));
+
+ }, this, parent, ev));
+ }
+ },
+
+ renderUpload: function(path, list) {
+ if (!this.options.enable_upload)
+ return E([]);
+
+ return E([
+ E('a', {
+ 'href': '#',
+ 'class': 'btn cbi-button-positive',
+ 'click': function(ev) {
+ var uploadForm = ev.target.nextElementSibling,
+ fileInput = uploadForm.querySelector('input[type="file"]');
+
+ ev.target.style.display = 'none';
+ uploadForm.style.display = '';
+ fileInput.click();
+ }
+ }, _('Upload file…')),
+ E('div', { 'class': 'upload', 'style': 'display:none' }, [
+ E('input', {
+ 'type': 'file',
+ 'style': 'display:none',
+ 'change': function(ev) {
+ var nameinput = ev.target.parentNode.querySelector('input[type="text"]'),
+ uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
+
+ nameinput.value = ev.target.value.replace(/^.+[\/\\]/, '');
+ uploadbtn.disabled = false;
+ }
+ }),
+ E('button', {
+ 'class': 'btn',
+ 'click': function(ev) {
+ ev.preventDefault();
+ ev.target.previousElementSibling.click();
+ }
+ }, _('Browse…')),
+ E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
+ E('button', {
+ 'class': 'btn cbi-button-save',
+ 'click': L.ui.createHandlerFn(this, 'handleUpload', path, list),
+ 'disabled': true
+ }, _('Upload file'))
+ ])
+ ]);
+ },
+
+ renderListing: function(container, path, list) {
+ var breadcrumb = E('p'),
+ rows = E('ul');
+
+ list.sort(function(a, b) {
+ var isDirA = (a.type == 'directory'),
+ isDirB = (b.type == 'directory');
+
+ if (isDirA != isDirB)
+ return isDirA < isDirB;
+
+ return a.name > b.name;
+ });
+
+ for (var i = 0; i < list.length; i++) {
+ if (!this.options.show_hidden && list[i].name.charAt(0) == '.')
+ continue;
+
+ var entrypath = this.canonicalizePath(path + '/' + list[i].name),
+ selected = (entrypath == this.node.lastElementChild.value),
+ mtime = new Date(list[i].mtime * 1000);
+
+ rows.appendChild(E('li', [
+ E('div', { 'class': 'name' }, [
+ this.iconForType(list[i].type),
+ ' ',
+ E('a', {
+ 'href': '#',
+ 'style': selected ? 'font-weight:bold' : null,
+ 'click': L.ui.createHandlerFn(this, 'handleSelect',
+ entrypath, list[i].type != 'directory' ? list[i] : null)
+ }, '%h'.format(list[i].name))
+ ]),
+ E('div', { 'class': 'mtime hide-xs' }, [
+ ' %04d-%02d-%02d %02d:%02d:%02d '.format(
+ mtime.getFullYear(),
+ mtime.getMonth() + 1,
+ mtime.getDate(),
+ mtime.getHours(),
+ mtime.getMinutes(),
+ mtime.getSeconds())
+ ]),
+ E('div', [
+ selected ? E('button', {
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleReset')
+ }, _('Deselect')) : '',
+ this.options.enable_remove ? E('button', {
+ 'class': 'btn cbi-button-negative',
+ 'click': L.ui.createHandlerFn(this, 'handleDelete', entrypath, list[i])
+ }, _('Delete')) : ''
+ ])
+ ]));
+ }
+
+ if (!rows.firstElementChild)
+ rows.appendChild(E('em', _('No entries in this directory')));
+
+ var dirs = this.splitPath(path),
+ cur = '';
+
+ for (var i = 0; i < dirs.length; i++) {
+ cur = cur ? cur + '/' + dirs[i] : dirs[i];
+ L.dom.append(breadcrumb, [
+ i ? ' » ' : '',
+ E('a', {
+ 'href': '#',
+ 'click': L.ui.createHandlerFn(this, 'handleSelect', cur || '/', null)
+ }, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
+ ]);
+ }
+
+ L.dom.content(container, [
+ breadcrumb,
+ rows,
+ E('div', { 'class': 'right' }, [
+ this.renderUpload(path, list),
+ E('a', {
+ 'href': '#',
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleCancel')
+ }, _('Cancel'))
+ ]),
+ ]);
+ },
+
+ handleCancel: function(ev) {
+ var button = this.node.firstElementChild,
+ browser = button.nextElementSibling;
+
+ browser.classList.remove('open');
+ button.style.display = '';
+
+ this.node.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
+ },
+
+ handleReset: function(ev) {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ hidden.value = '';
+ L.dom.content(button, _('Select file…'));
+
+ this.handleCancel(ev);
+ },
+
+ handleSelect: function(path, fileStat, ev) {
+ var browser = L.dom.parent(ev.target, '.cbi-filebrowser'),
+ ul = browser.querySelector('ul');
+
+ if (fileStat == null) {
+ L.dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
+ this.callFileList(path).then(L.bind(this.renderListing, this, browser, path));
+ }
+ else {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ path = this.canonicalizePath(path);
+
+ L.dom.content(button, [
+ this.iconForType(fileStat.type),
+ ' %s (%1000mB)'.format(this.truncatePath(path), fileStat.size)
+ ]);
+
+ browser.classList.remove('open');
+ button.style.display = '';
+ hidden.value = path;
+
+ this.stat = Object.assign({ path: path }, fileStat);
+ this.node.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail: this.stat }));
+ }
+ },
+
+ handleFileBrowser: function(ev) {
+ var button = ev.target,
+ browser = button.nextElementSibling,
+ path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : this.options.root_directory;
+
+ if (this.options.root_directory.indexOf(path) != 0)
+ path = this.options.root_directory;
+
+ ev.preventDefault();
+
+ return this.callFileList(path).then(L.bind(function(button, browser, path, list) {
+ document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
+ L.dom.findClassInstance(browserEl).handleCancel(ev);
+ });
+
+ button.style.display = 'none';
+ browser.classList.add('open');
+
+ return this.renderListing(browser, path, list);
+ }, this, button, browser, path));
+ },
+
+ getValue: function() {
+ return this.node.lastElementChild.value;
+ },
+
+ setValue: function(value) {
+ this.node.lastElementChild.value = value;
+ }
+});
+
return L.Class.extend({
__init__: function() {
@@ -1555,6 +1967,43 @@ return L.Class.extend({
tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
},
+ addNotification: function(title, children /*, ... */) {
+ var mc = document.querySelector('#maincontent') || document.body;
+ var msg = E('div', {
+ 'class': 'alert-message fade-in',
+ 'style': 'display:flex',
+ 'transitionend': function(ev) {
+ var node = ev.currentTarget;
+ if (node.parentNode && node.classList.contains('fade-out'))
+ node.parentNode.removeChild(node);
+ }
+ }, [
+ E('div', { 'style': 'flex:10' }),
+ E('div', { 'style': 'flex:1; display:flex' }, [
+ E('button', {
+ 'class': 'btn',
+ 'style': 'margin-left:auto; margin-top:auto',
+ 'click': function(ev) {
+ L.dom.parent(ev.target, '.alert-message').classList.add('fade-out');
+ },
+
+ }, _('Dismiss'))
+ ])
+ ]);
+
+ if (title != null)
+ L.dom.append(msg.firstElementChild, E('h4', {}, title));
+
+ L.dom.append(msg.firstElementChild, children);
+
+ for (var i = 2; i < arguments.length; i++)
+ msg.classList.add(arguments[i]);
+
+ mc.insertBefore(msg, mc.firstElementChild);
+
+ return msg;
+ },
+
/* Widget helper */
itemlist: function(node, items, separators) {
var children = [];
@@ -1628,7 +2077,7 @@ return L.Class.extend({
active = pane.getAttribute('data-tab-active') === 'true';
menu.appendChild(E('li', {
- 'style': L.dom.isEmpty(pane) ? 'display:none' : null,
+ 'style': this.isEmptyPane(pane) ? 'display:none' : null,
'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
'data-tab': name
}, E('a', {
@@ -1645,9 +2094,9 @@ return L.Class.extend({
if (selected === null) {
selected = this.getActiveTabId(panes[0]);
- if (selected < 0 || selected >= panes.length || L.dom.isEmpty(panes[selected])) {
+ if (selected < 0 || selected >= panes.length || this.isEmptyPane(panes[selected])) {
for (var i = 0; i < panes.length; i++) {
- if (!L.dom.isEmpty(panes[i])) {
+ if (!this.isEmptyPane(panes[i])) {
selected = i;
break;
}
@@ -1660,6 +2109,12 @@ return L.Class.extend({
this.setActiveTabId(panes[selected], selected);
}
+
+ this.updateTabs(group);
+ },
+
+ isEmptyPane: function(pane) {
+ return L.dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
},
getPathForPane: function(pane) {
@@ -1712,7 +2167,7 @@ return L.Class.extend({
},
updateTabs: function(ev, root) {
- (root || document).querySelectorAll('[data-tab-title]').forEach(function(pane) {
+ (root || document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
var menu = pane.parentNode.previousElementSibling,
tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null,
n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
@@ -1720,7 +2175,7 @@ return L.Class.extend({
if (!menu || !tab)
return;
- if (L.dom.isEmpty(pane)) {
+ if (this.isEmptyPane(pane)) {
tab.style.display = 'none';
tab.classList.remove('flash');
}
@@ -1738,7 +2193,7 @@ return L.Class.extend({
tab.removeAttribute('data-errors');
tab.removeAttribute('data-tooltip');
}
- });
+ }, this));
},
switchTab: function(ev) {
@@ -2152,7 +2607,7 @@ return L.Class.extend({
if (t.blur)
t.blur();
- Promise.resolve(fn.apply(ctx, arguments)).then(function() {
+ Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
t.classList.remove('spinning');
t.disabled = false;
});
@@ -2167,5 +2622,6 @@ return L.Class.extend({
Dropdown: UIDropdown,
DynamicList: UIDynamicList,
Combobox: UICombobox,
- Hiddenfield: UIHiddenfield
+ Hiddenfield: UIHiddenfield,
+ FileUpload: UIFileUpload
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/validation.js b/modules/luci-base/htdocs/luci-static/resources/validation.js
index ca544cb15d..79ae1d6707 100644
--- a/modules/luci-base/htdocs/luci-static/resources/validation.js
+++ b/modules/luci-base/htdocs/luci-static/resources/validation.js
@@ -419,6 +419,12 @@ var ValidatorFactory = L.Class.extend({
return this.assert(this.factory.parseDecimal(this.value) <= +max, _('value smaller or equal to %f').format(max));
},
+ length: function(len) {
+ var val = '' + this.value;
+ return this.assert(val.length == +len,
+ _('value with %d characters').format(len));
+ },
+
rangelength: function(min, max) {
var val = '' + this.value;
return this.assert((val.length >= +min) && (val.length <= +max),