mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 23:45:13 +08:00
⛄ Sync 2026-04-23 20:32:35
This commit is contained in:
1 parent
17738c6f79
commit
fe51d2b304
206 files changed
+725671
No files matched your search
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
var isReadonlyView = !L.hasViewPermission() || null;
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return L.resolveDefault(fs.read('/etc/crontabs/root'), '');
|
||||
},
|
||||
|
||||
handleSave: function(ev) {
|
||||
var value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n';
|
||||
|
||||
return fs.write('/etc/crontabs/root', value).then(function(rc) {
|
||||
document.querySelector('textarea').value = value;
|
||||
ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
|
||||
|
||||
return fs.exec('/etc/init.d/cron', [ 'reload' ]);
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message)));
|
||||
});
|
||||
},
|
||||
|
||||
render: function(crontab) {
|
||||
return E([
|
||||
E('h2', _('Scheduled Tasks')),
|
||||
E('p', { 'class': 'cbi-section-descr' }, _('This is the system crontab in which scheduled tasks can be defined.')),
|
||||
E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 25, 'disabled': isReadonlyView }, [ crontab != null ? crontab : '' ]))
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('dropbear', _('SSH Access'), _('Dropbear offers <abbr title="Secure Shell">SSH</abbr> network shell access and an integrated <abbr title="Secure Copy">SCP</abbr> server'));
|
||||
|
||||
s = m.section(form.TypedSection, 'dropbear', _('Dropbear Instance'));
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.addbtntitle = _('Add instance');
|
||||
|
||||
o = s.option(form.Flag, 'enable', _('Enable Instance'), _('Enable <abbr title="Secure Shell">SSH</abbr> service instance'));
|
||||
o.default = o.enabled;
|
||||
|
||||
o = s.option(form.Flag, '_direct', _('Bind to Interface'));
|
||||
o.default = o.disabled;
|
||||
|
||||
o = s.option(widgets.NetworkSelect, 'DirectInterface', _('Interface'), _('Listen only on the given interface or, if unspecified, on all'));
|
||||
o.nocreate = true;
|
||||
o.depends('_direct', '1');
|
||||
|
||||
o = s.option(widgets.NetworkSelect, 'Interface', _('Interface'), _('Listen on up to 10 IPs on the given interface or, if unspecified, on all interfaces'));
|
||||
o.nocreate = true;
|
||||
o.depends('_direct', '0');
|
||||
|
||||
o = s.option(form.Value, 'Port', _('Port'));
|
||||
o.datatype = 'port';
|
||||
o.placeholder = 22;
|
||||
|
||||
o = s.option(form.Flag, 'PasswordAuth', _('Password authentication'), _('Allow <abbr title="Secure Shell">SSH</abbr> password authentication'));
|
||||
o.enabled = 'on';
|
||||
o.disabled = 'off';
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'RootPasswordAuth', _('Allow root logins with password'), _('Allow the <em>root</em> user to log in with password'));
|
||||
o.enabled = 'on';
|
||||
o.disabled = 'off';
|
||||
o.default = o.enabled;
|
||||
|
||||
o = s.option(form.Flag, 'GatewayPorts', _('Gateway Ports'), _('Allow remote hosts to connect to local SSH forwarded ports'));
|
||||
o.enabled = 'on';
|
||||
o.disabled = 'off';
|
||||
o.default = o.disabled;
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,492 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
const isReadonlyView = !L.hasViewPermission();
|
||||
|
||||
const callSystemValidateFirmwareImage = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'validate_firmware_image',
|
||||
params: [ 'path' ],
|
||||
expect: { '': { valid: false, forcable: true } }
|
||||
});
|
||||
|
||||
function findStorageSize(procmtd, procpart) {
|
||||
let kernsize = 0, rootsize = 0, wholesize = 0;
|
||||
|
||||
procmtd.split(/\n/).forEach(function(ln) {
|
||||
const match = ln.match(/^mtd\d+: ([0-9a-f]+) [0-9a-f]+ "(.+)"$/);
|
||||
const size = match ? parseInt(match[1], 16) : 0;
|
||||
|
||||
switch (match ? match[2] : '') {
|
||||
case 'linux':
|
||||
case 'firmware':
|
||||
if (size > wholesize)
|
||||
wholesize = size;
|
||||
break;
|
||||
|
||||
case 'kernel':
|
||||
case 'kernel0':
|
||||
kernsize = size;
|
||||
break;
|
||||
|
||||
case 'rootfs':
|
||||
case 'rootfs0':
|
||||
case 'ubi':
|
||||
case 'ubi0':
|
||||
rootsize = size;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (wholesize > 0)
|
||||
return wholesize;
|
||||
else if (kernsize > 0 && rootsize > kernsize)
|
||||
return kernsize + rootsize;
|
||||
|
||||
procpart.split(/\n/).forEach(function(ln) {
|
||||
const match = ln.match(/^\s*\d+\s+\d+\s+(\d+)\s+(\S+)$/);
|
||||
if (match) {
|
||||
const size = parseInt(match[1], 10);
|
||||
|
||||
if (!match[2].match(/\d/) && size > 2048 && wholesize == 0)
|
||||
wholesize = size * 1024;
|
||||
}
|
||||
});
|
||||
|
||||
return wholesize;
|
||||
}
|
||||
|
||||
|
||||
const mapdata = { actions: {}, config: {} };
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
const tasks = [
|
||||
L.resolveDefault(fs.stat('/lib/upgrade/platform.sh'), {}),
|
||||
fs.trimmed('/proc/sys/kernel/hostname'),
|
||||
fs.trimmed('/proc/mtd'),
|
||||
fs.trimmed('/proc/partitions'),
|
||||
fs.trimmed('/proc/mounts')
|
||||
];
|
||||
|
||||
return Promise.all(tasks);
|
||||
},
|
||||
|
||||
handleBackup(ev) {
|
||||
const form = E('form', {
|
||||
method: 'post',
|
||||
action: L.env.cgi_base + '/cgi-backup',
|
||||
enctype: 'application/x-www-form-urlencoded'
|
||||
}, E('input', { type: 'hidden', name: 'sessionid', value: rpc.getSessionID() }));
|
||||
|
||||
ev.currentTarget.parentNode.appendChild(form);
|
||||
|
||||
form.submit();
|
||||
form.parentNode.removeChild(form);
|
||||
},
|
||||
|
||||
handleFirstboot(ev) {
|
||||
if (!confirm(_('Do you really want to erase all settings?')))
|
||||
return;
|
||||
|
||||
ui.showModal(_('Erasing...'), [
|
||||
E('p', { 'class': 'spinning' }, _('The system is erasing the configuration partition now and will reboot itself when finished.'))
|
||||
]);
|
||||
|
||||
/* Currently the sysupgrade rpc call will not return, hence no promise handling */
|
||||
fs.exec('/sbin/firstboot', [ '-r', '-y' ]);
|
||||
|
||||
ui.awaitReconnect('10.0.0.1', 'kwrt.lan');
|
||||
},
|
||||
|
||||
handleRestore(ev) {
|
||||
return ui.uploadFile('/tmp/backup.tar.gz', ev.target)
|
||||
.then(L.bind(function(btn, res) {
|
||||
btn.firstChild.data = _('Checking archive…');
|
||||
return fs.exec('/bin/tar', [ '-tzf', '/tmp/backup.tar.gz' ]);
|
||||
}, this, ev.target))
|
||||
.then(L.bind(function(btn, res) {
|
||||
if (res.code != 0) {
|
||||
ui.addNotification(null, E('p', _('The uploaded backup archive is not readable')));
|
||||
return fs.remove('/tmp/backup.tar.gz');
|
||||
}
|
||||
|
||||
ui.showModal(_('Apply backup?'), [
|
||||
E('p', _('The uploaded backup archive appears to be valid and contains the files listed below. Press "Continue" to restore the backup and reboot, or "Cancel" to abort the operation.')),
|
||||
E('pre', {}, [ res.stdout ]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.createHandlerFn(this, function(ev) {
|
||||
return fs.remove('/tmp/backup.tar.gz').finally(ui.hideModal);
|
||||
})
|
||||
}, [ _('Cancel') ]), ' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-action important',
|
||||
'click': ui.createHandlerFn(this, 'handleRestoreConfirm', btn)
|
||||
}, [ _('Continue') ])
|
||||
])
|
||||
]);
|
||||
}, this, ev.target))
|
||||
.catch(function(e) { ui.addNotification(null, E('p', e.message)) })
|
||||
.finally(L.bind(function(btn, input) {
|
||||
btn.firstChild.data = _('Upload archive...');
|
||||
}, this, ev.target));
|
||||
},
|
||||
|
||||
handleRestoreConfirm(btn, ev) {
|
||||
return fs.exec('/sbin/sysupgrade', [ '--restore-backup', '/tmp/backup.tar.gz' ])
|
||||
.then(L.bind(function(btn, res) {
|
||||
if (res.code != 0) {
|
||||
ui.addNotification(null, [
|
||||
E('p', _('The restore command failed with code %d').format(res.code)),
|
||||
res.stderr ? E('pre', {}, [ res.stderr ]) : ''
|
||||
]);
|
||||
L.raise('Error', 'Unpack failed');
|
||||
}
|
||||
|
||||
btn.firstChild.data = _('Rebooting…');
|
||||
return fs.exec('/sbin/reboot');
|
||||
}, this, ev.target))
|
||||
.then(L.bind(function(res) {
|
||||
if (res.code != 0) {
|
||||
ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res.code)));
|
||||
L.raise('Error', 'Reboot failed');
|
||||
}
|
||||
|
||||
ui.showModal(_('Rebooting…'), [
|
||||
E('p', { 'class': 'spinning' }, _('The system is rebooting now. If the restored configuration changed the current LAN IP address, you might need to reconnect manually.'))
|
||||
]);
|
||||
|
||||
ui.awaitReconnect(window.location.host, '10.0.0.1', 'kwrt.lan');
|
||||
}, this))
|
||||
.catch(function(e) { ui.addNotification(null, E('p', e.message)) })
|
||||
.finally(function() { btn.firstChild.data = _('Upload archive...') });
|
||||
},
|
||||
|
||||
handleBlock(hostname, ev) {
|
||||
const mtdblock = dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select');
|
||||
const mtdnumber = mtdblock.value;
|
||||
const mtdname = mtdblock.selectedOptions[0].text.replace(/([^a-zA-Z0-9]+)/g, '-');
|
||||
const form = E('form', {
|
||||
'method': 'post',
|
||||
'action': L.env.cgi_base + '/cgi-download',
|
||||
'enctype': 'application/x-www-form-urlencoded'
|
||||
}, [
|
||||
E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }),
|
||||
E('input', { 'type': 'hidden', 'name': 'path', 'value': '/dev/mtdblock%d'.format(mtdnumber) }),
|
||||
E('input', { 'type': 'hidden', 'name': 'filename', 'value': '%s.mtd%d.%s.bin'.format(hostname, mtdnumber, mtdname) })
|
||||
]);
|
||||
|
||||
ev.currentTarget.parentNode.appendChild(form);
|
||||
|
||||
form.submit();
|
||||
form.parentNode.removeChild(form);
|
||||
},
|
||||
|
||||
handleSysupgrade(storage_size, has_rootfs_data, ev) {
|
||||
return ui.uploadFile('/tmp/firmware.bin', ev.target.firstChild)
|
||||
.then(L.bind(function(btn, reply) {
|
||||
btn.firstChild.data = _('Checking image…');
|
||||
|
||||
ui.showModal(_('Checking image…'), [
|
||||
E('span', { 'class': 'spinning' }, _('Verifying the uploaded image file.'))
|
||||
]);
|
||||
|
||||
return callSystemValidateFirmwareImage('/tmp/firmware.bin')
|
||||
.then(function(res) { return [ reply, res ]; });
|
||||
}, this, ev.target))
|
||||
.then(L.bind(function(btn, reply) {
|
||||
return fs.exec('/sbin/sysupgrade', [ '--test', '/tmp/firmware.bin' ])
|
||||
.then(function(res) { reply.push(res); return reply; });
|
||||
}, this, ev.target))
|
||||
.then(L.bind(function(btn, res) {
|
||||
/* sysupgrade opts table [0]:checkbox element [1]:check condition [2]:args to pass */
|
||||
const opts = {
|
||||
keep : [ E('input', { type: 'checkbox' }), false, '-n' ],
|
||||
force : [ E('input', { type: 'checkbox' }), true, '--force' ],
|
||||
skip_orig : [ E('input', { type: 'checkbox' }), true, '-u' ],
|
||||
backup_pkgs : [ E('input', { type: 'checkbox' }), true, '-k' ],
|
||||
},
|
||||
is_valid = res[1].valid,
|
||||
is_forceable = res[1].forceable,
|
||||
allow_backup = res[1].allow_backup,
|
||||
is_too_big = (storage_size > 0 && res[0].size > storage_size),
|
||||
body = [];
|
||||
|
||||
body.push(E('p', _("The flash image was uploaded. Below is the checksum and file size listed, compare them with the original file to ensure data integrity. <br /> Click 'Continue' below to start the flash procedure.")));
|
||||
body.push(E('ul', {}, [
|
||||
res[0].size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res[0].size)) : '',
|
||||
res[0].checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res[0].checksum)) : '',
|
||||
res[0].sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res[0].sha256sum)) : ''
|
||||
]));
|
||||
|
||||
body.push(E('p', {}, E('label', { 'class': 'btn' }, [
|
||||
opts.keep[0], ' ', _('Keep settings and retain the current configuration')
|
||||
])));
|
||||
|
||||
if (!is_valid || is_too_big)
|
||||
body.push(E('hr'));
|
||||
|
||||
if (is_too_big)
|
||||
body.push(E('p', { 'class': 'alert-message' }, [
|
||||
_('It appears that you are trying to flash an image that does not fit into the flash memory, please verify the image file!')
|
||||
]));
|
||||
|
||||
if (!is_valid)
|
||||
body.push(E('p', { 'class': 'alert-message' }, [
|
||||
res[2].stderr ? res[2].stderr : '',
|
||||
res[2].stderr ? E('br') : '',
|
||||
res[2].stderr ? E('br') : '',
|
||||
_('The uploaded image file does not contain a supported format. Make sure that you choose the generic image format for your platform.')
|
||||
]));
|
||||
|
||||
if (!allow_backup) {
|
||||
body.push(E('p', { 'class': 'alert-message' }, [
|
||||
_('The uploaded firmware does not allow keeping current configuration.')
|
||||
]));
|
||||
opts.keep[0].disabled = true;
|
||||
} else {
|
||||
opts.keep[0].checked = true;
|
||||
|
||||
if (has_rootfs_data) {
|
||||
body.push(E('p', {}, E('label', { 'class': 'btn' }, [
|
||||
opts.skip_orig[0], ' ', _('Skip from backup files that are equal to those in /rom')
|
||||
])));
|
||||
}
|
||||
|
||||
body.push(E('p', {}, E('label', { 'class': 'btn' }, [
|
||||
opts.backup_pkgs[0], ' ', _('Include in backup a list of current installed packages at /etc/backup/installed_packages.txt')
|
||||
])));
|
||||
opts.backup_pkgs[0].checked = true;
|
||||
};
|
||||
|
||||
const cntbtn = E('button', {
|
||||
'class': 'btn cbi-button-action important',
|
||||
'click': ui.createHandlerFn(this, 'handleSysupgradeConfirm', btn, opts),
|
||||
}, [ _('Continue') ]);
|
||||
|
||||
if (res[2].code != 0) {
|
||||
body.push(E('p', { 'class': 'alert-message danger' }, E('label', {}, [
|
||||
_('Image check failed:'),
|
||||
E('br'), E('br'),
|
||||
res[2].stderr
|
||||
])));
|
||||
};
|
||||
|
||||
if ((!is_valid || is_too_big || res[2].code != 0) && is_forceable) {
|
||||
body.push(E('p', {}, E('label', { 'class': 'btn alert-message danger' }, [
|
||||
opts.force[0], ' ', _('Force upgrade'),
|
||||
E('br'), E('br'),
|
||||
_('Select \'Force upgrade\' to flash the image even if the image format check fails. Use only if you are sure that the firmware is correct and meant for your device!')
|
||||
])));
|
||||
cntbtn.disabled = true;
|
||||
};
|
||||
|
||||
|
||||
body.push(E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.createHandlerFn(this, function(ev) {
|
||||
return fs.remove('/tmp/firmware.bin').finally(ui.hideModal);
|
||||
})
|
||||
}, [ _('Cancel') ]), ' ', cntbtn
|
||||
]));
|
||||
|
||||
opts.force[0].addEventListener('change', function(ev) {
|
||||
cntbtn.disabled = !ev.target.checked;
|
||||
});
|
||||
|
||||
opts.keep[0].addEventListener('change', function(ev) {
|
||||
opts.skip_orig[0].disabled = !ev.target.checked;
|
||||
opts.backup_pkgs[0].disabled = !ev.target.checked;
|
||||
if (ev.target.checked == false){
|
||||
opts.skip_orig[0].checked =false
|
||||
opts.backup_pkgs[0].checked =false
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ui.showModal(_('Flash image?'), body);
|
||||
}, this, ev.target))
|
||||
.catch(function(e) { ui.addNotification(null, E('p', e.message)) })
|
||||
.finally(L.bind(function(btn) {
|
||||
btn.firstChild.data = _('Flash image...');
|
||||
}, this, ev.target));
|
||||
},
|
||||
|
||||
handleSysupgradeConfirm(btn, opts, ev) {
|
||||
btn.firstChild.data = _('Flashing…');
|
||||
|
||||
ui.showModal(_('Flashing…'), [
|
||||
E('p', { 'class': 'spinning' }, _('The system is flashing now.<br /> DO NOT POWER OFF THE DEVICE!<br /> Wait a few minutes before you try to reconnect. It might be necessary to renew the address of your computer to reach the device again, depending on your settings.'))
|
||||
]);
|
||||
|
||||
const args = [];
|
||||
|
||||
for (let key in opts)
|
||||
/* if checkbox == condition add args to sysupgrade */
|
||||
if (opts[key][0].checked == opts[key][1])
|
||||
args.push(opts[key][2]);
|
||||
|
||||
args.push('/tmp/firmware.bin');
|
||||
|
||||
/* Currently the sysupgrade rpc call will not return, hence no promise handling */
|
||||
fs.exec('/sbin/sysupgrade', args);
|
||||
|
||||
if (opts['keep'][0].checked)
|
||||
ui.awaitReconnect(window.location.host);
|
||||
else
|
||||
ui.awaitReconnect('10.0.0.1', 'kwrt.lan');
|
||||
},
|
||||
|
||||
handleBackupList(ev) {
|
||||
return fs.exec('/sbin/sysupgrade', [ '--list-backup' ]).then(function(res) {
|
||||
if (res.code != 0) {
|
||||
ui.addNotification(null, [
|
||||
E('p', _('The sysupgrade command failed with code %d').format(res.code)),
|
||||
res.stderr ? E('pre', {}, [ res.stderr ]) : ''
|
||||
]);
|
||||
L.raise('Error', 'Sysupgrade failed');
|
||||
}
|
||||
|
||||
ui.showModal(_('Backup file list'), [
|
||||
E('p', _('Below is the determined list of files to backup. It consists of changed configuration files marked by opkg, essential base files and the user defined backup patterns.')),
|
||||
E('ul', {}, (res.stdout || '').trim().split(/\n/).map(function(ln) { return E('li', {}, ln) })),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, [ _('Dismiss') ])
|
||||
])
|
||||
], 'cbi-modal');
|
||||
});
|
||||
},
|
||||
|
||||
handleBackupSave(m, ev) {
|
||||
return m.save(function() {
|
||||
return fs.write('/etc/sysupgrade.conf', mapdata.config.editlist.trim().replace(/\r\n/g, '\n') + '\n');
|
||||
}).then(function() {
|
||||
ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e)));
|
||||
});
|
||||
},
|
||||
|
||||
render([p_fstat, hostname, procmtd, procpart, procmounts]) {
|
||||
const has_sysupgrade = (p_fstat.type == 'file');
|
||||
const has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:/overlay / ") != null);
|
||||
const storage_size = findStorageSize(procmtd, procpart);
|
||||
let m, s, o, ss;
|
||||
|
||||
m = new form.JSONMap(mapdata, _('Flash operations'));
|
||||
m.tabbed = true;
|
||||
m.readonly = isReadonlyView;
|
||||
|
||||
s = m.section(form.NamedSection, 'actions', _('Actions'));
|
||||
|
||||
|
||||
o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Backup'), _('Click "Generate archive" to download a tar archive of the current configuration files.'));
|
||||
ss = o.subsection;
|
||||
|
||||
o = ss.option(form.Button, 'dl_backup', _('Download backup'));
|
||||
o.inputstyle = 'action important';
|
||||
o.inputtitle = _('Generate archive');
|
||||
o.onclick = this.handleBackup;
|
||||
|
||||
|
||||
o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Restore'), _('To restore configuration files, you can upload a previously generated backup archive here. To reset the firmware to its initial state, click "Perform reset" (only possible with squashfs images).'));
|
||||
ss = o.subsection;
|
||||
|
||||
if (has_rootfs_data) {
|
||||
o = ss.option(form.Button, 'reset', _('Reset to defaults'));
|
||||
o.inputstyle = 'negative important';
|
||||
o.inputtitle = _('Perform reset');
|
||||
o.onclick = this.handleFirstboot;
|
||||
}
|
||||
|
||||
o = ss.option(form.Button, 'restore', _('Restore backup'), _('Custom files (certificates, scripts) may remain on the system. To prevent this, perform a factory-reset first.'));
|
||||
o.inputstyle = 'action important';
|
||||
o.inputtitle = _('Upload archive...');
|
||||
o.onclick = L.bind(this.handleRestore, this);
|
||||
|
||||
|
||||
const mtdblocks = [];
|
||||
procmtd.split(/\n/).forEach(function(ln) {
|
||||
const match = ln.match(/^mtd(\d+): .+ "(.+?)"$/);
|
||||
if (match)
|
||||
mtdblocks.push(match[1], match[2]);
|
||||
});
|
||||
|
||||
if (mtdblocks.length) {
|
||||
o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Save mtdblock contents'), _('Click "Save mtdblock" to download specified mtdblock file. (NOTE: THIS FEATURE IS FOR PROFESSIONALS! )'));
|
||||
ss = o.subsection;
|
||||
|
||||
o = ss.option(form.ListValue, 'mtdselect', _('Choose mtdblock'));
|
||||
|
||||
for (var i = 0; i < mtdblocks.length; i += 2)
|
||||
o.value(mtdblocks[i], mtdblocks[i+1]);
|
||||
|
||||
o = ss.option(form.Button, 'mtddownload', _('Download mtdblock'));
|
||||
o.inputstyle = 'action important';
|
||||
o.inputtitle = _('Save mtdblock');
|
||||
o.onclick = L.bind(this.handleBlock, this, hostname);
|
||||
}
|
||||
|
||||
|
||||
o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'),
|
||||
has_sysupgrade
|
||||
? _('Upload a sysupgrade-compatible image here to replace the running firmware.')
|
||||
: _('Sorry, there is no sysupgrade support present; a new firmware image must be flashed manually. Please refer to the wiki for device specific install instructions.'));
|
||||
|
||||
ss = o.subsection;
|
||||
|
||||
if (has_sysupgrade) {
|
||||
o = ss.option(form.Button, 'sysupgrade', _('Image'));
|
||||
o.inputstyle = 'action important';
|
||||
o.inputtitle = _('Flash image...');
|
||||
o.onclick = L.bind(this.handleSysupgrade, this, storage_size, has_rootfs_data);
|
||||
}
|
||||
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'config', _('Configuration'), _('This is a list of shell glob patterns for matching files and directories to include during sysupgrade. Modified files in /etc/config/ and certain other configurations are automatically preserved.'));
|
||||
s.render = L.bind(function(view /*, ... */) {
|
||||
return form.NamedSection.prototype.render.apply(this, this.varargs(arguments, 1))
|
||||
.then(L.bind(function(node) {
|
||||
node.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-save',
|
||||
'click': ui.createHandlerFn(view, 'handleBackupSave', this.map),
|
||||
'disabled': isReadonlyView || null
|
||||
}, [ _('Save') ])
|
||||
]));
|
||||
|
||||
return node;
|
||||
}, this));
|
||||
}, s, this);
|
||||
|
||||
o = s.option(form.Button, 'showlist', _('Show current backup file list'));
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = _('Open list...');
|
||||
o.onclick = L.bind(this.handleBackupList, this);
|
||||
|
||||
o = s.option(form.TextValue, 'editlist');
|
||||
o.forcewrite = true;
|
||||
o.rows = 30;
|
||||
o.load = function(section_id) {
|
||||
return L.resolveDefault(fs.read('/etc/sysupgrade.conf'), '');
|
||||
};
|
||||
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
return baseclass.extend({
|
||||
trigger: _('Always on (kernel: default-on)'),
|
||||
description: _('The LED is always in default state on.'),
|
||||
kernel: true,
|
||||
addFormOptions: function(s) {}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
|
||||
return baseclass.extend({
|
||||
trigger: _('Heartbeat interval (kernel: heartbeat)'),
|
||||
description: _('The LED flashes to simulate actual heart beat.') +
|
||||
_('The frequency is in direct proportion to 1-minute average CPU load.'),
|
||||
kernel: true,
|
||||
addFormOptions: function(s) {
|
||||
var o;
|
||||
|
||||
o = s.option(form.Flag, 'inverted', _('Invert blinking'),
|
||||
_('When inverted, the LED is continuously lit and flickers instead of it being off by default and blinking on system activity.'));
|
||||
o.rmempty = true;
|
||||
o.modalonly = true;
|
||||
o.depends('trigger', 'heartbeat');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
return baseclass.extend({
|
||||
trigger: _("Network device activity (kernel: netdev)"),
|
||||
description: _('The LED flashes with link status and activity on the configured interface.'),
|
||||
kernel: true,
|
||||
addFormOptions: function(s) {
|
||||
var o;
|
||||
|
||||
o = s.option(widgets.DeviceSelect, '_net_dev', _('Device'));
|
||||
o.rmempty = true;
|
||||
o.ucioption = 'dev';
|
||||
o.modalonly = true;
|
||||
o.noaliases = true;
|
||||
o.depends('trigger', 'netdev');
|
||||
|
||||
o = s.option(form.MultiValue, 'mode', _('Trigger Mode'));
|
||||
o.rmempty = true;
|
||||
o.modalonly = true;
|
||||
o.depends('trigger', 'netdev');
|
||||
o.value('link', _('Link On'));
|
||||
o.value('link_10', _('Link 10M On'));
|
||||
o.value('link_100', _('Link 100M On'));
|
||||
o.value('link_1000', _('Link 1G On'));
|
||||
o.value('link_2500', _('Link 2.5G On'));
|
||||
o.value('link_5000', _('Link 5G On'));
|
||||
o.value('link_10000', _('Link 10G On'));
|
||||
o.value('half_duplex', _('Half Duplex'));
|
||||
o.value('full_duplex', _('Full Duplex'));
|
||||
o.value('tx', _('Transmit'));
|
||||
o.value('rx', _('Receive'));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require baseclass';
|
||||
|
||||
return baseclass.extend({
|
||||
trigger: _('Always off (kernel: none)'),
|
||||
description: _('The LED is always in default state off.'),
|
||||
kernel: true,
|
||||
addFormOptions: function(s) {
|
||||
var o;
|
||||
|
||||
o = s.option(form.Flag, 'default', _('Default state'));
|
||||
o.rmempty = false;
|
||||
o.depends('trigger', 'none');
|
||||
o.textvalue = function(section_id) {
|
||||
var cval = this.cfgvalue(section_id);
|
||||
if (cval == null)
|
||||
cval = this.default;
|
||||
return (cval == this.enabled) ? _('On') : _('Off');
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
|
||||
return baseclass.extend({
|
||||
trigger: _('Custom flash interval (kernel: timer)'),
|
||||
description: _('The LED blinks with the configured on/off frequency'),
|
||||
kernel: true,
|
||||
addFormOptions: function(s) {
|
||||
var o;
|
||||
|
||||
o = s.option(form.Value, 'delayon', _('On-State Delay'),
|
||||
_('How long (in milliseconds) the LED should be on')
|
||||
);
|
||||
o.modalonly = true;
|
||||
o.depends('trigger', 'timer');
|
||||
|
||||
o = s.option(form.Value, 'delayoff', _('Off-State Delay'),
|
||||
_('How long (in milliseconds) the LED should be off')
|
||||
);
|
||||
o.modalonly = true;
|
||||
o.depends('trigger', 'timer');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require form';
|
||||
'require fs';
|
||||
|
||||
const callLeds = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getLEDs',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
callLeds(),
|
||||
L.resolveDefault(fs.list('/www' + L.resource('view/system/led-trigger')), [])
|
||||
]).then(function([leds, plugins]) {
|
||||
const tasks = [];
|
||||
|
||||
for (let p of plugins) {
|
||||
const m = p.name.match(/^(.+)\.js$/);
|
||||
|
||||
if (p.type != 'file' || m == null)
|
||||
continue;
|
||||
|
||||
tasks.push(L.require('view.system.led-trigger.' + m[1]).then(L.bind(function(name){
|
||||
return L.resolveDefault(L.require('view.system.led-trigger.' + name)).then(function(form) {
|
||||
return {
|
||||
name: name,
|
||||
form: form,
|
||||
};
|
||||
});
|
||||
}, this, m[1])));
|
||||
}
|
||||
|
||||
return Promise.all(tasks).then(function(plugins) {
|
||||
return [leds, plugins];
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render([leds, plugins]) {
|
||||
let m, s, o;
|
||||
const triggers = [];
|
||||
|
||||
for (let k in leds)
|
||||
for (let t of leds[k].triggers)
|
||||
triggers.push(t);
|
||||
|
||||
m = new form.Map('system',
|
||||
_('<abbr title="Light Emitting Diode">LED</abbr> Configuration'),
|
||||
_('Customizes the behaviour of the device <abbr title="Light Emitting Diode">LED</abbr>s if possible.'));
|
||||
|
||||
s = m.section(form.GridSection, 'led', '');
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.addbtntitle = _('Add LED action');
|
||||
s.nodescriptions = true;
|
||||
|
||||
s.option(form.Value, 'name', _('Name'));
|
||||
|
||||
o = s.option(form.ListValue, 'sysfs', _('<abbr title="Light Emitting Diode">LED</abbr> Name'));
|
||||
Object.keys(leds).sort().forEach(function(name) {
|
||||
o.value(name)
|
||||
});
|
||||
|
||||
o = s.option(form.ListValue, 'trigger', _('Trigger'));
|
||||
for (let plugin of plugins) {
|
||||
if ( plugin.form.kernel == false ) {
|
||||
o.value(plugin.name, plugin.form.trigger);
|
||||
}
|
||||
else {
|
||||
if (triggers.indexOf(plugin.name) >= 0)
|
||||
o.value(plugin.name, plugin.form.trigger);
|
||||
}
|
||||
}
|
||||
o.onchange = function(ev, section, value) {
|
||||
const nes = this.map.findElement('id', 'cbid.system.%s.trigger'.format(section)).nextElementSibling;
|
||||
for (let plugin of plugins) {
|
||||
if ( plugin.name === value && nes )
|
||||
nes.innerText = plugin.form.description || '';
|
||||
}
|
||||
}
|
||||
o.load = function(section_id) {
|
||||
const trigger = uci.get('system', section_id, 'trigger');
|
||||
for (let plugin of plugins) {
|
||||
if ( plugin.name === trigger)
|
||||
this.description = plugin.form.description || ' ';
|
||||
}
|
||||
return trigger;
|
||||
};
|
||||
|
||||
s.addModalOptions = function(s) {
|
||||
for (let plugin of plugins) {
|
||||
plugin.form.addFormOptions(s);
|
||||
}
|
||||
|
||||
const opts = s.getOption();
|
||||
|
||||
const removeIfNoneActive = function(original_remove_fn, section_id) {
|
||||
let isAnyActive = false;
|
||||
|
||||
for (let optname in opts) {
|
||||
if (opts[optname].ucioption != this.ucioption)
|
||||
continue;
|
||||
|
||||
if (!opts[optname].isActive(section_id))
|
||||
continue;
|
||||
|
||||
isAnyActive = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isAnyActive)
|
||||
original_remove_fn.call(this, section_id);
|
||||
};
|
||||
|
||||
for (let optname in opts) {
|
||||
if (!opts[optname].ucioption || optname == opts[optname].ucioption)
|
||||
continue;
|
||||
opts[optname].remove = removeIfNoneActive.bind(opts[optname], opts[optname].remove);
|
||||
}
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'interval', _('Interval'), _('milliseconds'));
|
||||
o.placeholder = '50';
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,386 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require form';
|
||||
|
||||
const callBlockDevices = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getBlockDevices',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callMountPoints = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getMountPoints',
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const callBlockDetect = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setBlockDetect',
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
function device_textvalue(devices, section_id) {
|
||||
let v = (uci.get('fstab', section_id, 'uuid') || '').toLowerCase();
|
||||
let e = Object.keys(devices).filter(function(dev) { return (devices[dev].uuid || '-').toLowerCase() == v })[0];
|
||||
|
||||
if (v) {
|
||||
this.section.devices[section_id] = devices[e];
|
||||
|
||||
if (e && devices[e].size)
|
||||
return E('span', 'UUID: %h (%s, %1024.2mB)'.format(v, devices[e].dev, devices[e].size));
|
||||
else if (e)
|
||||
return E('span', 'UUID: %h (%s)'.format(v, devices[e].dev));
|
||||
else
|
||||
return E('span', 'UUID: %h (<em>%s</em>)'.format(v, _('not present')));
|
||||
}
|
||||
|
||||
v = uci.get('fstab', section_id, 'label');
|
||||
e = Object.keys(devices).filter(function(dev) { return devices[dev].label == v })[0];
|
||||
|
||||
if (v) {
|
||||
this.section.devices[section_id] = this.section.devices[section_id] || devices[e];
|
||||
|
||||
if (e && devices[e].size)
|
||||
return E('span', 'Label: %h (%s, %1024.2mB)'.format(v, devices[e].dev, devices[e].size));
|
||||
else if (e)
|
||||
return E('span', 'Label: %h (%s)'.format(v, devices[e].dev));
|
||||
else
|
||||
return E('span', 'Label: %h (<em>%s</em>)'.format(v, _('not present')));
|
||||
}
|
||||
|
||||
v = uci.get('fstab', section_id, 'device');
|
||||
e = Object.keys(devices).filter(function(dev) { return devices[dev].dev == v })[0];
|
||||
|
||||
if (v) {
|
||||
this.section.devices[section_id] = this.section.devices[section_id] || devices[e];
|
||||
|
||||
if (e && devices[e].size)
|
||||
return E('span', '%h (%1024.2mB)'.format(v, devices[e].size));
|
||||
else if (e)
|
||||
return E('span', '%h'.format(v));
|
||||
else
|
||||
return E('span', '%h (<em>%s</em>)'.format(v, _('not present')));
|
||||
}
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleDetect(m, ev) {
|
||||
return callBlockDetect()
|
||||
.then(L.bind(uci.unload, uci, 'fstab'))
|
||||
.then(L.bind(m.render, m));
|
||||
},
|
||||
|
||||
handleMountAll(m, ev) {
|
||||
return fs.exec('/sbin/block', ['mount'])
|
||||
.then(function(res) {
|
||||
if (res.code != 0)
|
||||
ui.addNotification(null, E('p', _('The <em>block mount</em> command failed with code %d').format(res.code)));
|
||||
})
|
||||
.then(L.bind(uci.unload, uci, 'fstab'))
|
||||
.then(L.bind(m.render, m));
|
||||
},
|
||||
|
||||
handleUmount(m, path, ev) {
|
||||
return fs.exec('/bin/umount', [path])
|
||||
.then(L.bind(uci.unload, uci, 'fstab'))
|
||||
.then(L.bind(m.render, m))
|
||||
.catch(function(e) { ui.addNotification(null, E('p', e.message)) });
|
||||
},
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
callBlockDevices(),
|
||||
fs.lines('/proc/filesystems'),
|
||||
fs.lines('/etc/filesystems'),
|
||||
uci.load('fstab'),
|
||||
]);
|
||||
},
|
||||
|
||||
render([devices, procfs, etcfs]) {
|
||||
let m, s, o;
|
||||
|
||||
let filesystems = {};
|
||||
|
||||
for (let p of procfs)
|
||||
if (p.match(/\S/) && !p.match(/^nodev\t/))
|
||||
filesystems[p.trim()] = true;
|
||||
|
||||
for (let e of etcfs)
|
||||
if (e.match(/\S/))
|
||||
filesystems[e.trim()] = true;
|
||||
|
||||
filesystems = Object.keys(filesystems).sort();
|
||||
|
||||
|
||||
if (!uci.sections('fstab', 'global').length)
|
||||
uci.add('fstab', 'global');
|
||||
|
||||
m = new form.Map('fstab', _('Mount Points'));
|
||||
|
||||
s = m.section(form.TypedSection, 'global', _('Global Settings'));
|
||||
s.addremove = false;
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Button, '_detect', _('Generate Config'), _('Find all currently attached filesystems and swap and replace configuration with defaults based on what was detected'));
|
||||
o.onclick = this.handleDetect.bind(this, m);
|
||||
o.inputstyle = 'reload';
|
||||
|
||||
o = s.option(form.Button, '_mountall', _('Mount attached devices'), _('Attempt to enable configured mount points for attached devices'));
|
||||
o.onclick = this.handleMountAll.bind(this, m);
|
||||
o.inputstyle = 'reload';
|
||||
|
||||
o = s.option(form.Flag, 'anon_swap', _('Anonymous Swap'), _('Mount swap not specifically configured'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'anon_mount', _('Anonymous Mount'), _('Mount filesystems not specifically configured'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'auto_swap', _('Automount Swap'), _('Automatically mount swap on hotplug'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'auto_mount', _('Automount Filesystem'), _('Automatically mount filesystems on hotplug'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'check_fs', _('Check filesystems before mount'), _('Automatically check filesystem for errors before mounting'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
|
||||
// Mount status table
|
||||
o = s.option(form.DummyValue, '_mtab');
|
||||
|
||||
o.load = function(section_id) {
|
||||
return callMountPoints().then(L.bind(function(mounts) {
|
||||
this.mounts = mounts;
|
||||
}, this));
|
||||
};
|
||||
|
||||
o.render = L.bind(function(view, section_id) {
|
||||
const table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Filesystem')),
|
||||
E('th', { 'class': 'th' }, _('Mount Point')),
|
||||
E('th', { 'class': 'th center' }, _('Available')),
|
||||
E('th', { 'class': 'th center' }, _('Used')),
|
||||
E('th', { 'class': 'th' }, _('Unmount'))
|
||||
])
|
||||
]);
|
||||
|
||||
const rows = [];
|
||||
|
||||
for (let tm of this.mounts) {
|
||||
var used = tm.size - tm.free,
|
||||
umount = true;
|
||||
|
||||
if (/^\/(overlay|rom|tmp(?:\/.+)?|dev(?:\/.+)?|)$/.test(tm.mount))
|
||||
umount = false;
|
||||
|
||||
rows.push([
|
||||
tm.device,
|
||||
tm.mount,
|
||||
'%1024.2mB / %1024.2mB'.format(tm.avail, tm.size),
|
||||
'%.2f%% (%1024.2mB)'.format(100 / tm.size * used, used),
|
||||
umount ? E('button', {
|
||||
'class': 'btn cbi-button-remove',
|
||||
'click': ui.createHandlerFn(view, 'handleUmount', m, tm.mount),
|
||||
'disabled': this.map.readonly || null
|
||||
}, [ _('Unmount') ]) : '-'
|
||||
]);
|
||||
}
|
||||
|
||||
cbi_update_table(table, rows, E('em', _('Unable to obtain mount information')));
|
||||
|
||||
return E([], [ E('h3', _('Mounted file systems')), table ]);
|
||||
}, o, this);
|
||||
|
||||
|
||||
// Mounts
|
||||
s = m.section(form.GridSection, 'mount', _('Mount Points'), _('Mount Points define at which point a memory device will be attached to the filesystem'));
|
||||
s.modaltitle = _('Mount Points - Mount Entry');
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.devices = {};
|
||||
|
||||
s.renderHeaderRows = function(/* ... */) {
|
||||
var trEls = form.GridSection.prototype.renderHeaderRows.apply(this, arguments);
|
||||
return trEls.childNodes[0];
|
||||
}
|
||||
|
||||
s.tab('general', _('General Settings'));
|
||||
s.tab('advanced', _('Advanced Settings'));
|
||||
|
||||
o = s.taboption('general', form.Flag, 'enabled', _('Enabled'));
|
||||
o.rmempty = false;
|
||||
o.default = true;
|
||||
o.editable = true;
|
||||
|
||||
o = s.taboption('general', form.DummyValue, '_device', _('Device'));
|
||||
o.rawhtml = true;
|
||||
o.modalonly = false;
|
||||
o.write = function() {};
|
||||
o.remove = function() {};
|
||||
o.textvalue = device_textvalue.bind(o, devices);
|
||||
|
||||
o = s.taboption('general', form.Value, 'uuid', _('UUID'), _('If specified, mount the device by its UUID instead of a fixed device node'));
|
||||
o.modalonly = true;
|
||||
o.value('', _('-- match by uuid --'));
|
||||
|
||||
const devs = Object.keys(devices).sort();
|
||||
for (let d of devs) {
|
||||
const dev = devices[d];
|
||||
if (dev.uuid && dev.size)
|
||||
o.value(dev.uuid, '%s (%s, %1024.2mB)'.format(dev.uuid, dev.dev, dev.size));
|
||||
else if (dev.uuid)
|
||||
o.value(dev.uuid, '%s (%s)'.format(dev.uuid, dev.dev));
|
||||
}
|
||||
|
||||
o = s.taboption('general', form.Value, 'label', _('Label'), _('If specified, mount the device by the partition label instead of a fixed device node'));
|
||||
o.modalonly = true;
|
||||
o.depends('uuid', '');
|
||||
o.value('', _('-- match by label --'));
|
||||
|
||||
for (let d of devs) {
|
||||
const dev = devices[d];
|
||||
if (dev.label && dev.size)
|
||||
o.value(dev.label, '%s (%s, %1024.2mB)'.format(dev.label, dev.dev, dev.size));
|
||||
else if (dev.label)
|
||||
o.value(dev.label, '%s (%s)'.format(dev.label, dev.dev));
|
||||
}
|
||||
|
||||
o = s.taboption('general', form.Value, 'device', _('Device'), _('The device file of the memory or partition (<abbr title="for example">e.g.</abbr> <code>/dev/sda1</code>)'));
|
||||
o.modalonly = true;
|
||||
o.depends({ uuid: '', label: '' });
|
||||
|
||||
for (let d of devs) {
|
||||
const dev = devices[d];
|
||||
if (dev.size)
|
||||
o.value(dev.dev, '%s (%1024.2mB)'.format(dev.dev, dev.size));
|
||||
else
|
||||
o.value(dev.dev);
|
||||
}
|
||||
|
||||
o = s.taboption('general', form.Value, 'target', _('Mount point'), _('Specifies the directory the device is attached to'));
|
||||
o.value('/', _('Use as root filesystem (/)'));
|
||||
o.value('/overlay', _('Use as external overlay (/overlay)'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('general', form.DummyValue, '__notice', _('Root preparation'));
|
||||
o.depends('target', '/');
|
||||
o.modalonly = true;
|
||||
o.rawhtml = true;
|
||||
o.default = '' +
|
||||
'<p>%s</p>'.format(_('Make sure to clone the root filesystem using something like the commands below:')) +
|
||||
'<pre>' +
|
||||
'mkdir -p /tmp/introot\n' +
|
||||
'mkdir -p /tmp/extroot\n' +
|
||||
'mount --bind / /tmp/introot\n' +
|
||||
'mount /dev/sda1 /tmp/extroot\n' +
|
||||
'tar -C /tmp/introot -cvf - . | tar -C /tmp/extroot -xf -\n' +
|
||||
'umount /tmp/introot\n' +
|
||||
'umount /tmp/extroot\n' +
|
||||
'</pre>'
|
||||
;
|
||||
|
||||
o = s.taboption('advanced', form.ListValue, 'fstype', _('Filesystem'));
|
||||
|
||||
o.textvalue = function(section_id) {
|
||||
const dev = this.section.devices[section_id];
|
||||
let text = this.cfgvalue(section_id) || 'auto';
|
||||
|
||||
if (dev && dev.type && dev.type != text)
|
||||
text += ' (%s)'.format(dev.type);
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
o.value('', 'auto');
|
||||
|
||||
for (let fs of filesystems)
|
||||
o.value(fs);
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'options', _('Mount options'), _('See "mount" manpage for details'));
|
||||
o.textvalue = function(section_id) { return this.cfgvalue(section_id) || 'defaults' };
|
||||
o.placeholder = 'defaults';
|
||||
|
||||
|
||||
// Swaps
|
||||
s = m.section(form.GridSection, 'swap', _('SWAP'), _('If your physical memory is insufficient unused data can be temporarily swapped to a swap-device resulting in a higher amount of usable <abbr title="Random Access Memory">RAM</abbr>. Be aware that swapping data is a very slow process as the swap-device cannot be accessed with the high datarates of the <abbr title="Random Access Memory">RAM</abbr>.'));
|
||||
s.modaltitle = _('Mount Points - Swap Entry');
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.devices = {};
|
||||
|
||||
s.renderHeaderRows = function(/* ... */) {
|
||||
var trEls = form.GridSection.prototype.renderHeaderRows.apply(this, arguments);
|
||||
trEls.childNodes[0].childNodes[1].style.width = '90%';
|
||||
return trEls.childNodes[0];
|
||||
}
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_device', _('Device'));
|
||||
o.modalonly = false;
|
||||
o.textvalue = device_textvalue.bind(o, devices);
|
||||
|
||||
o = s.option(form.Value, 'uuid', _('UUID'), _('If specified, mount the device by its UUID instead of a fixed device node'));
|
||||
o.modalonly = true;
|
||||
o.value('', _('-- match by uuid --'));
|
||||
|
||||
for (let d of devs) {
|
||||
const dev = devices[d];
|
||||
if (dev.dev.match(/^\/dev\/(mtdblock|ubi|ubiblock)\d/))
|
||||
continue;
|
||||
|
||||
if (dev.uuid && dev.size)
|
||||
o.value(dev.uuid, '%s (%s, %1024.2mB)'.format(dev.uuid, dev.dev, dev.size));
|
||||
else if (dev.uuid)
|
||||
o.value(dev.uuid, '%s (%s)'.format(dev.uuid, dev.dev));
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'label', _('Label'), _('If specified, mount the device by the partition label instead of a fixed device node'));
|
||||
o.modalonly = true;
|
||||
o.depends('uuid', '');
|
||||
o.value('', _('-- match by label --'));
|
||||
|
||||
for (let d of devs) {
|
||||
const dev = devices[d];
|
||||
if (dev.dev.match(/^\/dev\/(mtdblock|ubi|ubiblock)\d/))
|
||||
continue;
|
||||
|
||||
if (dev.label && dev.size)
|
||||
o.value(dev.label, '%s (%s, %1024.2mB)'.format(dev.label, dev.dev, dev.size));
|
||||
else if (dev.label)
|
||||
o.value(dev.label, '%s (%s)'.format(dev.label, dev.dev));
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'device', _('Device'), _('The device file of the memory or partition (<abbr title="for example">e.g.</abbr> <code>/dev/sda1</code>)'));
|
||||
o.modalonly = true;
|
||||
o.depends({ uuid: '', label: '' });
|
||||
|
||||
for (let d of devs) {
|
||||
const dev = devices[d];
|
||||
if (dev.dev.match(/^\/dev\/(mtdblock|ubi|ubiblock)\d/))
|
||||
continue;
|
||||
|
||||
if (dev.size)
|
||||
o.value(dev.dev, '%s (%1024.2mB)'.format(dev.dev, dev.size));
|
||||
else
|
||||
o.value(dev.dev);
|
||||
}
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require form';
|
||||
'require rpc';
|
||||
|
||||
var formData = {
|
||||
password: {
|
||||
pw1: null,
|
||||
pw2: null
|
||||
}
|
||||
};
|
||||
|
||||
var callSetPassword = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setPassword',
|
||||
params: [ 'username', 'password' ],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
checkPassword: function(section_id, value) {
|
||||
var strength = document.querySelector('.cbi-value-description'),
|
||||
strongRegex = new RegExp("^(?=.{8,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*\\W).*$", "g"),
|
||||
mediumRegex = new RegExp("^(?=.{7,})(((?=.*[A-Z])(?=.*[a-z]))|((?=.*[A-Z])(?=.*[0-9]))|((?=.*[a-z])(?=.*[0-9]))).*$", "g"),
|
||||
enoughRegex = new RegExp("(?=.{6,}).*", "g");
|
||||
|
||||
if (strength && value.length) {
|
||||
if (false == enoughRegex.test(value))
|
||||
strength.innerHTML = '%s: <span style="color:red">%s</span>'.format(_('Password strength'), _('More Characters'));
|
||||
else if (strongRegex.test(value))
|
||||
strength.innerHTML = '%s: <span style="color:green">%s</span>'.format(_('Password strength'), _('Strong'));
|
||||
else if (mediumRegex.test(value))
|
||||
strength.innerHTML = '%s: <span style="color:orange">%s</span>'.format(_('Password strength'), _('Medium'));
|
||||
else
|
||||
strength.innerHTML = '%s: <span style="color:red">%s</span>'.format(_('Password strength'), _('Weak'));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.JSONMap(formData, _('Router Password'), _('Changes the administrator password for accessing the device'));
|
||||
m.readonly = !L.hasViewPermission();
|
||||
|
||||
s = m.section(form.NamedSection, 'password', 'password');
|
||||
|
||||
o = s.option(form.Value, 'pw1', _('Password'));
|
||||
o.password = true;
|
||||
o.validate = this.checkPassword;
|
||||
|
||||
o = s.option(form.Value, 'pw2', _('Confirmation'), ' ');
|
||||
o.password = true;
|
||||
o.renderWidget = function(/* ... */) {
|
||||
var node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
node.querySelector('input').addEventListener('keydown', function(ev) {
|
||||
if (ev.keyCode == 13 && !ev.currentTarget.classList.contains('cbi-input-invalid'))
|
||||
document.querySelector('.cbi-button-save').click();
|
||||
});
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSave: function() {
|
||||
var map = document.querySelector('.cbi-map');
|
||||
|
||||
return dom.callClassMethod(map, 'save').then(function() {
|
||||
if (formData.password.pw1 == null || formData.password.pw1.length == 0)
|
||||
return;
|
||||
|
||||
if (formData.password.pw1 != formData.password.pw2) {
|
||||
ui.addNotification(null, E('p', _('Given password confirmation did not match, password not changed!')), 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
return callSetPassword('root', formData.password.pw1).then(function(success) {
|
||||
if (success)
|
||||
ui.addNotification(null, E('p', _('The system password has been successfully changed.')), 'info');
|
||||
else
|
||||
ui.addNotification(null, E('p', _('Failed to change the system password.')), 'danger');
|
||||
|
||||
formData.password.pw1 = null;
|
||||
formData.password.pw2 = null;
|
||||
|
||||
dom.callClassMethod(map, 'render');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require uci';
|
||||
|
||||
var callReboot = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'reboot',
|
||||
expect: { result: 0 }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return uci.changes();
|
||||
},
|
||||
|
||||
render: function(changes) {
|
||||
var body = E([
|
||||
E('h2', _('Reboot')),
|
||||
E('p', {}, _('Reboots the operating system of your device'))
|
||||
]);
|
||||
|
||||
for (var config in (changes || {})) {
|
||||
body.appendChild(E('p', { 'class': 'alert-message warning' },
|
||||
_('Warning: There are unsaved changes that will get lost on reboot!')));
|
||||
break;
|
||||
}
|
||||
|
||||
body.appendChild(E('hr'));
|
||||
body.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-action important',
|
||||
'click': ui.createHandlerFn(this, 'handleReboot')
|
||||
}, _('Perform reboot')));
|
||||
|
||||
return body;
|
||||
},
|
||||
|
||||
handleReboot: function(ev) {
|
||||
return callReboot().then(function(res) {
|
||||
if (res != 0) {
|
||||
L.ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res)));
|
||||
L.raise('Error', 'Reboot failed');
|
||||
}
|
||||
|
||||
L.ui.showModal(_('Rebooting…'), [
|
||||
E('p', { 'class': 'spinning' }, _('Waiting for device...'))
|
||||
]);
|
||||
|
||||
window.setTimeout(function() {
|
||||
L.ui.showModal(_('Rebooting…'), [
|
||||
E('p', { 'class': 'spinning alert-message warning' },
|
||||
_('Device unreachable! Still waiting for device...'))
|
||||
]);
|
||||
}, 150000);
|
||||
|
||||
L.ui.awaitReconnect();
|
||||
})
|
||||
.catch(function(e) { L.ui.addNotification(null, E('p', e.message)) });
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,313 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
const APK_DIR = '/etc/apk/keys/';
|
||||
const OPKG_DIR = '/etc/opkg/keys/';
|
||||
const isReadonlyView = !L.hasViewPermission() || null;
|
||||
|
||||
let KEYDIR = null;
|
||||
let KEYEXT = null;
|
||||
|
||||
/* This safeList is not bullet-proof, but should prevent users
|
||||
accidentally deleting official repo keys */
|
||||
const safeList = [
|
||||
'd310c6f2833e97f7', // 24.10 release usign key
|
||||
'openwrt-snapshots.pem', // main snapshots EC pub key
|
||||
];
|
||||
|
||||
function isFileInSafeList(file){
|
||||
for (let name of safeList) {
|
||||
if (file === name)
|
||||
return true;
|
||||
if (file.toLocaleLowerCase().replace(/^openwrt-[0-9]+\.[0-9]+/i, '') !== file)
|
||||
return true;
|
||||
if (file.toLocaleLowerCase().replace(/^openwrt-snapshots/i, '') !== file)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalizeKey(s) {
|
||||
return s?.replace(/\s+/g, ' ')?.trim();
|
||||
}
|
||||
|
||||
function determineKeyEnv() {
|
||||
return fs.stat(APK_DIR).then(() => {
|
||||
KEYDIR = APK_DIR;
|
||||
KEYEXT = '.pem'; // not strictly necessary - apk allows any extension
|
||||
}).catch(() => {
|
||||
KEYDIR = OPKG_DIR;
|
||||
KEYEXT = null; // opkg requires key filenames without an extension
|
||||
});
|
||||
}
|
||||
|
||||
function listKeyFiles() {
|
||||
return fs.list(KEYDIR).then(entries =>
|
||||
Promise.all(entries.map(entry =>
|
||||
fs.read(KEYDIR + entry.name).then(content => ({
|
||||
filename: entry.name,
|
||||
key: content
|
||||
}))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
function saveKeyFile(keyContent, file, fileContent) {
|
||||
const ts = Date.now();
|
||||
// Note: opkg can only verify against a key with filename that matches its key hash
|
||||
// generate a file name in case key content was pasted
|
||||
const filename = file ? file?.name?.split('.')?.[0] + (KEYEXT || '') : null;
|
||||
const noname = 'key_' + ts + (KEYEXT || '');
|
||||
return fs.write(KEYDIR + (filename ?? noname), fileContent ?? keyContent, 384 /* 0600 */);
|
||||
}
|
||||
|
||||
function removeKey(ev, key) {
|
||||
L.showModal(_('Delete key'), [
|
||||
E('div', _('Really delete the following software repository public key?')),
|
||||
E('pre', [ key.filename ]),
|
||||
E('div', { class: 'right' }, [
|
||||
E('div', { class: 'btn', click: L.hideModal }, _('Cancel')),
|
||||
' ',
|
||||
E('div', {
|
||||
class: 'btn danger',
|
||||
click: function() {
|
||||
fs.remove(KEYDIR + key.filename)
|
||||
.then(() => window.location.reload())
|
||||
.catch(e => ui.addNotification(null, E('p', e.message)))
|
||||
.finally(() => ui.hideModal());
|
||||
}
|
||||
}, _('Delete key'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
function isPemFormat(content) {
|
||||
return (/-BEGIN ([A-Z ]+)?PUBLIC KEY-/.test(content));
|
||||
}
|
||||
|
||||
function keyEnvironmentCheck(key) {
|
||||
const isPem = isPemFormat(key);
|
||||
|
||||
// Reject PEM in OPKG; reject non-PEM in APK
|
||||
if (KEYDIR === OPKG_DIR && isPem)
|
||||
return _('This key appears to be in PEM format, which is not supported in an opkg environment.');
|
||||
if (KEYDIR === APK_DIR && !isPem)
|
||||
return _('This key does not appear to be in PEM format, which is required in an apk environment.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function addKey(ev, file, fileContent) {
|
||||
const input = document.getElementById('key-input');
|
||||
const key = (fileContent ?? input?.value?.trim());
|
||||
|
||||
if (!key || !key.length)
|
||||
return;
|
||||
|
||||
// Handle remote URL paste
|
||||
if (/^https?:\/\/\S+$/i.test(key) && !fileContent) {
|
||||
ui.addTimeLimitedNotification(_('Fetching key from URL…'), [], 5000, 'info');
|
||||
|
||||
L.Request.request(key, { method: 'GET' }).then(res => {
|
||||
if (res.status !== 200) {
|
||||
ui.addTimeLimitedNotification(_('Failed to fetch key'), [
|
||||
E('p', _('HTTP error %d').format(res.status)),
|
||||
], 7000, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const fetched = res.responseText?.trim();
|
||||
if (!fetched || fetched.length > 8192) {
|
||||
ui.addTimeLimitedNotification(_('Key file too large'), [
|
||||
E('p', _('Fetched content seems too long. Maximum 8192 bytes.')),
|
||||
], 7000, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fetched || fetched.length < 32) {
|
||||
ui.addTimeLimitedNotification(_('Invalid or empty key file'), [
|
||||
E('p', _('Fetched content seems empty or too short.')),
|
||||
], 7000, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = res?.url?.split('/').pop().split('?')[0].split('#')[0];
|
||||
|
||||
// Remove extension if any (we'll re-add based on environment)
|
||||
const file = {name: filename.replace(/\.[^.]+$/, '') };
|
||||
|
||||
addKey(ev, file, fetched);
|
||||
}).catch(err => {
|
||||
ui.addTimeLimitedNotification(_('Failed to fetch key'), [
|
||||
E('p', err.message),
|
||||
], 7000, 'warning');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// From here on, key content (either pasted, fetched, or dropped)
|
||||
const formatError = keyEnvironmentCheck(key);
|
||||
if (formatError) {
|
||||
ui.addTimeLimitedNotification(_('Invalid key format'), [
|
||||
E('p', formatError)
|
||||
], 7000, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicates
|
||||
listKeyFiles().then(existingKeys => {
|
||||
if (existingKeys.some(k => normalizeKey(k.key) === normalizeKey(key))) {
|
||||
ui.addTimeLimitedNotification(_('Add key'), [
|
||||
E('div', _('The given software repository public key is already present.')),
|
||||
], 7000, 'notice');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save and refresh the UI
|
||||
input.value = '';
|
||||
saveKeyFile(key, file, fileContent)
|
||||
.then(() => window.location.reload())
|
||||
.catch(e => ui.addNotification(null, E('p', e.message)));
|
||||
});
|
||||
}
|
||||
|
||||
function dragKey(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
function dropKey(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const input = document.getElementById('key-input');
|
||||
|
||||
if (!input)
|
||||
return;
|
||||
|
||||
for (const file of ev.dataTransfer.files) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = rev => {
|
||||
input.value = rev.target.result;
|
||||
addKey(ev, file, rev.target.result);
|
||||
input.value = '';
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowDragDropIgnore(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
determineKeyEnv().then(listKeyFiles),
|
||||
]);
|
||||
},
|
||||
|
||||
render([keys]) {
|
||||
|
||||
const m = new form.JSONMap({
|
||||
keys: keys,
|
||||
fup: {},
|
||||
},
|
||||
_('Repository Public Keys'), _(
|
||||
_('Each software repository public key (from official or third party repositories) allows packages in lists signed by it to be installed by the package manager.') + '<br/>' +
|
||||
_('Each key is stored as a file in %s.').format(`<code>${KEYDIR}</code>`)
|
||||
));
|
||||
m.submit = false;
|
||||
m.reset = false;
|
||||
m.readonly = isReadonlyView;
|
||||
|
||||
let s, o;
|
||||
|
||||
s = m.section(form.TableSection, 'keys');
|
||||
s.anonymous = true;
|
||||
s.nodescriptions = true;
|
||||
|
||||
o = s.option(form.DummyValue, 'filename', _('Name'));
|
||||
o.width = '20%';
|
||||
o = s.option(form.TextValue, 'key', _('Key'));
|
||||
o.readonly = true;
|
||||
o.monospace = true;
|
||||
o.cols = 85;
|
||||
o.rows = 5;
|
||||
|
||||
s.renderRowActions = function (section_id) {
|
||||
const key = this.map.data.get(this.map.config, section_id);
|
||||
const isReservedKey = isFileInSafeList(key.filename);
|
||||
|
||||
const btns = [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-negative remove',
|
||||
'click': ui.createHandlerFn(this, this.handleRemove, key),
|
||||
'disabled': isReservedKey ? true : null,
|
||||
}, [_('Delete')]),
|
||||
];
|
||||
|
||||
return E('td', { 'class': 'td middle cbi-section-actions' }, E('div', btns));
|
||||
};
|
||||
|
||||
s.handleRemove = function(key, ev) {
|
||||
if (isFileInSafeList(key.filename)) {
|
||||
ui.addTimeLimitedNotification(null, E('p', _('This key is protected and cannot be deleted.')), 3000, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
return removeKey(ev, key)
|
||||
};
|
||||
|
||||
s = m.section(form.NamedSection, 'fup');
|
||||
|
||||
o = s.option(form.DummyValue, '_newkey');
|
||||
o.cfgvalue = function(/* section_id*/) {
|
||||
|
||||
const addInput = E('textarea', {
|
||||
id: 'key-input',
|
||||
'aria-label': _('Paste or drag repository public key'),
|
||||
class: 'cbi-input-text',
|
||||
type: 'text',
|
||||
style: 'width: 100%; min-height: 120px;',
|
||||
placeholder: _('Paste content of a file, or a URL to a key file, or drag and drop here to upload a software repository public key…'),
|
||||
keydown: function(ev) { if (ev.keyCode === 13 && (ev.ctrlKey || ev.metaKey)) addKey(ev); },
|
||||
disabled: isReadonlyView
|
||||
});
|
||||
|
||||
addInput.addEventListener('dragover', handleWindowDragDropIgnore);
|
||||
addInput.addEventListener('drop', handleWindowDragDropIgnore);
|
||||
|
||||
const addBtn = E('button', {
|
||||
class: 'cbi-button',
|
||||
click: ui.createHandlerFn(this, addKey),
|
||||
disabled: isReadonlyView
|
||||
}, _('Add key'));
|
||||
|
||||
return E('div', {
|
||||
class: 'cbi-section-node',
|
||||
dragover: isReadonlyView ? null : dragKey,
|
||||
drop: isReadonlyView ? null : dropKey
|
||||
}, [
|
||||
E('div', { class: 'cbi-section-descr' }, _('Add new repository public key by pasting its content, a file, or a URL.')),
|
||||
E('div', {
|
||||
'style': 'height: 20px',
|
||||
}, [' ']),
|
||||
addInput,
|
||||
E('div', { class: 'right' }, [ addBtn ])
|
||||
]);
|
||||
};
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,298 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
var isReadonlyView = !L.hasViewPermission() || null;
|
||||
|
||||
var SSHPubkeyDecoder = baseclass.singleton({
|
||||
lengthDecode: function(s, off)
|
||||
{
|
||||
var l = (s.charCodeAt(off++) << 24) |
|
||||
(s.charCodeAt(off++) << 16) |
|
||||
(s.charCodeAt(off++) << 8) |
|
||||
s.charCodeAt(off++);
|
||||
|
||||
if (l < 0 || (off + l) > s.length)
|
||||
return -1;
|
||||
|
||||
return l;
|
||||
},
|
||||
|
||||
decode: function(s)
|
||||
{
|
||||
var parts = s.trim().match(/^((?:(?:^|,)[^ =,]+(?:=(?:[^ ",]+|"(?:[^"\\]|\\.)*"))?)+ +)?(ssh-dss|ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+|sk-ecdsa-sha2-nistp256@openssh\.com|sk-ssh-ed25519@openssh\.com) +([^ ]+)( +.*)?$/);
|
||||
|
||||
if (!parts)
|
||||
return null;
|
||||
|
||||
var key = null;
|
||||
try { key = atob(parts[3]); } catch(e) {}
|
||||
if (!key)
|
||||
return null;
|
||||
|
||||
var off, len;
|
||||
|
||||
off = 0;
|
||||
len = this.lengthDecode(key, off);
|
||||
|
||||
if (len <= 0)
|
||||
return null;
|
||||
|
||||
var type = key.substr(off + 4, len);
|
||||
if (type !== parts[2])
|
||||
return null;
|
||||
|
||||
off += 4 + len;
|
||||
|
||||
var len1 = off < key.length ? this.lengthDecode(key, off) : 0;
|
||||
if (len1 <= 0)
|
||||
return null;
|
||||
|
||||
var curve = null;
|
||||
if (type.indexOf('ecdsa-sha2-') === 0) {
|
||||
curve = key.substr(off + 4, len1);
|
||||
|
||||
if (!len1 || type.substr(11) !== curve)
|
||||
return null;
|
||||
|
||||
type = 'ecdsa-sha2';
|
||||
curve = curve.replace(/^nistp(\d+)$/, 'NIST P-$1');
|
||||
}
|
||||
|
||||
off += 4 + len1;
|
||||
|
||||
var len2 = off < key.length ? this.lengthDecode(key, off) : 0;
|
||||
if (len2 < 0)
|
||||
return null;
|
||||
|
||||
if (len1 & 1)
|
||||
len1--;
|
||||
|
||||
if (len2 & 1)
|
||||
len2--;
|
||||
|
||||
var comment = (parts[4] || '').trim(),
|
||||
fprint = parts[3].length > 68 ? parts[3].substr(0, 33) + '…' + parts[3].substr(-34) : parts[3];
|
||||
|
||||
var options = null;
|
||||
(parts[1] || '').trim().replace(/(?:^|,)([^ =,]+)(?:=(?:([^ ",]+)|"((?:[^"\\]|\\.)*)"))?/g, function(m, k, p, q) {
|
||||
options = options || {};
|
||||
|
||||
if (options.hasOwnProperty(k))
|
||||
options[k] += ',' + (q || p || true);
|
||||
else
|
||||
options[k] = (q || p || true);
|
||||
});
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case 'ssh-rsa':
|
||||
return { type: 'RSA', bits: len2 * 8, comment: comment, options: options, fprint: fprint, src: s };
|
||||
|
||||
case 'ssh-dss':
|
||||
return { type: 'DSA', bits: len1 * 8, comment: comment, options: options, fprint: fprint, src: s };
|
||||
|
||||
case 'ssh-ed25519':
|
||||
return { type: 'EdDSA', curve: 'Curve25519', comment: comment, options: options, fprint: fprint, src: s };
|
||||
|
||||
case 'ecdsa-sha2':
|
||||
return { type: 'ECDSA', curve: curve, comment: comment, options: options, fprint: fprint, src: s };
|
||||
|
||||
case 'sk-ecdsa-sha2-nistp256@openssh.com':
|
||||
return { type: 'ECDSA-SK', curve: 'NIST P-256', comment: comment, options: options, fprint: fprint, src: s };
|
||||
|
||||
case 'sk-ssh-ed25519@openssh.com':
|
||||
return { type: 'EdDSA-SK', curve: 'Curve25519', comment: comment, options: options, fprint: fprint, src: s };
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function renderKeyItem(pubkey) {
|
||||
return E('div', {
|
||||
class: 'item',
|
||||
click: isReadonlyView ? null : removeKey,
|
||||
'data-key': pubkey.src
|
||||
}, [
|
||||
E('strong', {}, [ pubkey.comment || _('Unnamed key') ]), E('br'),
|
||||
E('small', {}, [
|
||||
'%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
|
||||
pubkey.options ? E([], {}, [
|
||||
' / ', _('Options:'), ' ',
|
||||
E('code', {}, [Object.keys(pubkey.options).sort().join(', ')])
|
||||
]) : '',
|
||||
E('br'), E('code', {}, [pubkey.fprint])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
function renderKeys(keys) {
|
||||
var list = document.querySelector('.cbi-dynlist');
|
||||
|
||||
while (!matchesElem(list.firstElementChild, '.add-item'))
|
||||
list.removeChild(list.firstElementChild);
|
||||
|
||||
keys.forEach(function(key) {
|
||||
var pubkey = SSHPubkeyDecoder.decode(key);
|
||||
if (pubkey)
|
||||
list.insertBefore(renderKeyItem(pubkey), list.lastElementChild);
|
||||
});
|
||||
|
||||
if (list.firstElementChild === list.lastElementChild)
|
||||
list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
|
||||
}
|
||||
|
||||
function saveKeys(keys) {
|
||||
return fs.write('/etc/dropbear/authorized_keys', keys.join('\n') + '\n', 384 /* 0600 */)
|
||||
.then(renderKeys.bind(this, keys))
|
||||
.catch(function(e) { ui.addNotification(null, E('p', e.message)) })
|
||||
.finally(ui.hideModal);
|
||||
}
|
||||
|
||||
function addKey(ev) {
|
||||
var list = findParent(ev.target, '.cbi-dynlist'),
|
||||
input = list.querySelector('input[type="text"]'),
|
||||
key = input.value.trim(),
|
||||
pubkey = SSHPubkeyDecoder.decode(key),
|
||||
keys = [];
|
||||
|
||||
if (!key.length)
|
||||
return;
|
||||
|
||||
list.querySelectorAll('.item').forEach(function(item) {
|
||||
keys.push(item.getAttribute('data-key'));
|
||||
});
|
||||
|
||||
if (keys.indexOf(key) !== -1) {
|
||||
ui.showModal(_('Add key'), [
|
||||
E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')),
|
||||
E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
|
||||
]);
|
||||
}
|
||||
else if (!pubkey) {
|
||||
ui.showModal(_('Add key'), [
|
||||
E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA, ED25519 or ECDSA keys.')),
|
||||
E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
|
||||
]);
|
||||
}
|
||||
else {
|
||||
keys.push(key);
|
||||
input.value = '';
|
||||
|
||||
return saveKeys(keys).then(function() {
|
||||
var added = list.querySelector('[data-key="%s"]'.format(key.replace(/["\\]/g, '\\$&')));
|
||||
if (added)
|
||||
added.classList.add('flash');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeKey(ev) {
|
||||
var list = findParent(ev.target, '.cbi-dynlist'),
|
||||
delkey = ev.target.getAttribute('data-key'),
|
||||
keys = [];
|
||||
|
||||
list.querySelectorAll('.item').forEach(function(item) {
|
||||
var key = item.getAttribute('data-key');
|
||||
if (key !== delkey)
|
||||
keys.push(key);
|
||||
});
|
||||
|
||||
L.showModal(_('Delete key'), [
|
||||
E('div', _('Do you really want to delete the following SSH key?')),
|
||||
E('pre', [ delkey ]),
|
||||
E('div', { class: 'right' }, [
|
||||
E('div', { class: 'btn', click: L.hideModal }, _('Cancel')),
|
||||
' ',
|
||||
E('div', { class: 'btn danger', click: ui.createHandlerFn(this, saveKeys, keys) }, _('Delete key')),
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
function dragKey(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
function dropKey(ev) {
|
||||
var file = ev.dataTransfer.files[0],
|
||||
input = ev.currentTarget.querySelector('input[type="text"]'),
|
||||
reader = new FileReader();
|
||||
|
||||
if (file) {
|
||||
reader.onload = function(rev) {
|
||||
input.value = rev.target.result.trim();
|
||||
addKey(ev);
|
||||
input.value = '';
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function handleWindowDragDropIgnore(ev) {
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return fs.lines('/etc/dropbear/authorized_keys').then(function(lines) {
|
||||
return lines.map(function(line) {
|
||||
return SSHPubkeyDecoder.decode(line);
|
||||
}).filter(function(line) {
|
||||
return line != null;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function(keys) {
|
||||
var list = E('div', {
|
||||
'class': 'cbi-dynlist',
|
||||
'dragover': isReadonlyView ? null : dragKey,
|
||||
'drop': isReadonlyView ? null : dropKey
|
||||
}, [
|
||||
E('div', { 'class': 'add-item' }, [
|
||||
E('input', {
|
||||
'class': 'cbi-input-text',
|
||||
'type': 'text',
|
||||
'placeholder': _('Paste or drag SSH key file…') ,
|
||||
'keydown': function(ev) { if (ev.keyCode === 13) addKey(ev) },
|
||||
'disabled': isReadonlyView
|
||||
}),
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'click': ui.createHandlerFn(this, addKey),
|
||||
'disabled': isReadonlyView
|
||||
}, _('Add key'))
|
||||
])
|
||||
]);
|
||||
|
||||
keys.forEach(L.bind(function(pubkey) {
|
||||
list.insertBefore(renderKeyItem(pubkey), list.lastElementChild);
|
||||
}, this));
|
||||
|
||||
if (list.firstElementChild === list.lastElementChild)
|
||||
list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
|
||||
|
||||
window.addEventListener('dragover', handleWindowDragDropIgnore);
|
||||
window.addEventListener('drop', handleWindowDragDropIgnore);
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('SSH-Keys')),
|
||||
E('div', { 'class': 'cbi-section-descr' }, _('Public keys allow for passwordless SSH logins with higher security than plain passwords. In order to upload a new key to the device, paste an OpenSSH-compatible public key or drag a <code>.pub</code> file into the input field.')),
|
||||
E('div', { 'class': 'cbi-section-node' }, list)
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
var isReadonlyView = !L.hasViewPermission() || null;
|
||||
|
||||
return view.extend({
|
||||
callRcList: rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'list',
|
||||
params: [ 'skip_running_check' ],
|
||||
expect: { '': {} }
|
||||
}),
|
||||
|
||||
callRcInit: rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: [ 'name', 'action' ],
|
||||
}),
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.read('/etc/rc.local'), ''),
|
||||
this.callRcList(true)
|
||||
]);
|
||||
},
|
||||
|
||||
handleAction: function(name, action, ev) {
|
||||
return this.callRcInit(name, action).then(function(ret) {
|
||||
if (ret)
|
||||
throw _('Command failed');
|
||||
|
||||
return true;
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', _('Failed to execute "/etc/init.d/%s %s" action: %s').format(name, action, e)));
|
||||
});
|
||||
},
|
||||
|
||||
handleEnableDisable: function(name, isEnabled, ev) {
|
||||
return this.handleAction(name, isEnabled ? 'disable' : 'enable', ev).then(L.bind(function(name, isEnabled, btn) {
|
||||
btn.parentNode.replaceChild(this.renderEnableDisable({
|
||||
name: name,
|
||||
enabled: isEnabled
|
||||
}), btn);
|
||||
}, this, name, !isEnabled, ev.currentTarget));
|
||||
},
|
||||
|
||||
handleRcLocalSave: function(ev) {
|
||||
var value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n';
|
||||
|
||||
return fs.write('/etc/rc.local', value).then(function() {
|
||||
document.querySelector('textarea').value = value;
|
||||
ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message)));
|
||||
});
|
||||
},
|
||||
|
||||
renderEnableDisable: function(init) {
|
||||
return E('button', {
|
||||
class: 'btn cbi-button-%s'.format(init.enabled ? 'positive' : 'negative'),
|
||||
click: ui.createHandlerFn(this, 'handleEnableDisable', init.name, init.enabled),
|
||||
disabled: isReadonlyView
|
||||
}, init.enabled ? _('Enabled') : _('Disabled'));
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var rcLocal = data[0],
|
||||
initList = data[1],
|
||||
rows = [], list = [];
|
||||
|
||||
var table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Start priority')),
|
||||
E('th', { 'class': 'th' }, _('Initscript')),
|
||||
E('th', { 'class': 'th nowrap cbi-section-actions' })
|
||||
])
|
||||
]);
|
||||
|
||||
for (var init in initList)
|
||||
if (initList[init].start < 100)
|
||||
list.push(Object.assign({ name: init }, initList[init]));
|
||||
|
||||
list.sort(function(a, b) {
|
||||
if (a.start != b.start)
|
||||
return a.start - b.start
|
||||
|
||||
return a.name > b.name;
|
||||
});
|
||||
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
rows.push([
|
||||
'%02d'.format(list[i].start),
|
||||
list[i].name,
|
||||
E('div', [
|
||||
this.renderEnableDisable(list[i]),
|
||||
E('button', { 'class': 'btn cbi-button-action', 'click': ui.createHandlerFn(this, 'handleAction', list[i].name, 'start'), 'disabled': isReadonlyView }, _('Start', 'daemon start action')),
|
||||
E('button', { 'class': 'btn cbi-button-action', 'click': ui.createHandlerFn(this, 'handleAction', list[i].name, 'restart'), 'disabled': isReadonlyView }, _('Restart', 'daemon restart action')),
|
||||
E('button', { 'class': 'btn cbi-button-action', 'click': ui.createHandlerFn(this, 'handleAction', list[i].name, 'reload'), 'disabled': isReadonlyView }, _('Reload', 'daemon reload action')),
|
||||
E('button', { 'class': 'btn cbi-button-action', 'click': ui.createHandlerFn(this, 'handleAction', list[i].name, 'stop'), 'disabled': isReadonlyView }, _('Stop', 'daemon stop action'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
cbi_update_table(table, rows);
|
||||
|
||||
var view = E('div', {}, [
|
||||
E('h2', _('Startup')),
|
||||
E('div', {}, [
|
||||
E('div', { 'data-tab': 'init', 'data-tab-title': _('Initscripts') }, [
|
||||
E('p', {}, _('You can enable or disable installed init scripts here. Changes will be applied after a device reboot.<br /><strong>Warning: If you disable essential init scripts like "network", your device might become inaccessible!</strong>')),
|
||||
table
|
||||
]),
|
||||
E('div', { 'data-tab': 'rc', 'data-tab-title': _('Local Startup') }, [
|
||||
E('p', {}, _('This is the content of /etc/rc.local. Insert your own commands here (in front of \'exit 0\') to execute them at the end of the boot process.')),
|
||||
E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 20, 'disabled': isReadonlyView }, [ (rcLocal != null ? rcLocal : '') ])),
|
||||
E('div', { 'class': 'cbi-page-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-save',
|
||||
'click': ui.createHandlerFn(this, 'handleRcLocalSave'),
|
||||
'disabled': isReadonlyView
|
||||
}, _('Save'))
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
ui.tabs.initTabGroup(view.lastElementChild.childNodes);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,390 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require form';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
const callRcList = rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'list',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} },
|
||||
filter(res) {
|
||||
for (var k in res)
|
||||
return +res[k].enabled;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const callRcInit = rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: [ 'name', 'action' ],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
const callGetUnixtime = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getUnixtime',
|
||||
expect: { result: 0 }
|
||||
});
|
||||
|
||||
const callSetLocaltime = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setLocaltime',
|
||||
params: [ 'localtime' ],
|
||||
expect: { result: 0 }
|
||||
});
|
||||
|
||||
const callTimezone = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getTimezones',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function formatTime(epoch) {
|
||||
const date = new Date(epoch * 1000);
|
||||
const zn = uci.get('system', '@system[0]', 'zonename')?.replaceAll(' ', '_') || 'UTC';
|
||||
const ts = uci.get('system', '@system[0]', 'clock_timestyle') || 0;
|
||||
const hc = uci.get('system', '@system[0]', 'clock_hourcycle') || 0;
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: (ts == 0) ? 'long' : 'full',
|
||||
hourCycle: (hc == 0) ? undefined : hc,
|
||||
timeZone: zn
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
const CBILocalTime = form.DummyValue.extend({
|
||||
renderWidget(section_id, option_id, cfgvalue) {
|
||||
return E([], [
|
||||
E('input', {
|
||||
'id': 'localtime',
|
||||
'type': 'text',
|
||||
'readonly': true,
|
||||
'value': formatTime(cfgvalue)
|
||||
}),
|
||||
E('br'),
|
||||
E('span', { 'class': 'control-group' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return callSetLocaltime(Math.floor(Date.now() / 1000));
|
||||
}),
|
||||
'disabled': (this.readonly != null) ? this.readonly : this.map.readonly
|
||||
}, _('Sync with browser')),
|
||||
' ',
|
||||
this.ntpd_support ? E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return callRcInit('sysntpd', 'restart');
|
||||
}),
|
||||
'disabled': (this.readonly != null) ? this.readonly : this.map.readonly
|
||||
}, _('Sync with NTP-Server')) : ''
|
||||
])
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
callRcList('sysntpd'),
|
||||
callTimezone(),
|
||||
callGetUnixtime(),
|
||||
uci.load('luci'),
|
||||
uci.load('system'),
|
||||
L.hasSystemFeature('zram')
|
||||
? L.resolveDefault(fs.read('/sys/block/zram0/comp_algorithm'), '')
|
||||
: ''
|
||||
]);
|
||||
},
|
||||
|
||||
render([ntpd_enabled, timezones, unixtime, _luci, _system, zram_algorithms]) {
|
||||
let m, s, o;
|
||||
const system_section = uci.sections('system', 'system')[0];
|
||||
|
||||
m = new form.Map('system',
|
||||
_('System'),
|
||||
_('Here you can configure the basic aspects of your device like its hostname or the timezone.'));
|
||||
this.map = m;
|
||||
this.systemSid = system_section?.['.name'];
|
||||
|
||||
m.chain('luci');
|
||||
|
||||
s = m.section(form.TypedSection, 'system', _('System Properties'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
s.tab('general', _('General Settings'));
|
||||
s.tab('logging', _('Logging'));
|
||||
s.tab('timesync', _('Time Synchronization'));
|
||||
s.tab('language', _('Language and Style'));
|
||||
|
||||
/*
|
||||
* System Properties
|
||||
*/
|
||||
|
||||
o = s.taboption('general', CBILocalTime, '_systime', _('Local Time'));
|
||||
o.cfgvalue = function() { return unixtime };
|
||||
o.ntpd_support = ntpd_enabled;
|
||||
|
||||
o = s.taboption('general', form.Value, 'hostname', _('Hostname'));
|
||||
o.datatype = 'hostname';
|
||||
|
||||
/* could be used also as a default for LLDP, SNMP "system description" in the future */
|
||||
o = s.taboption('general', form.Value, 'description', _('Description'), _('An optional, short description for this device'));
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('general', form.TextValue, 'notes', _('Notes'), _('Optional, free-form notes about this device'));
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('general', form.ListValue, 'zonename', _('Timezone'));
|
||||
o.value('UTC');
|
||||
|
||||
const zones = Object.keys(timezones || {}).sort();
|
||||
for (let zone of zones)
|
||||
o.value(zone);
|
||||
|
||||
o.write = function(section_id, formvalue) {
|
||||
const tz = timezones[formvalue] ? timezones[formvalue].tzstring : null;
|
||||
uci.set('system', section_id, 'zonename', formvalue);
|
||||
uci.set('system', section_id, 'timezone', tz);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Flag, 'clock_timestyle', _('Full TimeZone Name'), _('Unchecked means the timezone offset (E.g. GMT+1) is displayed'));
|
||||
|
||||
o = s.taboption('general', form.ListValue, 'clock_hourcycle', _('Time Format'));
|
||||
o.value('', _('Default'));
|
||||
o.value('h12', _('12-Hour Clock'));
|
||||
o.value('h23', _('24-Hour Clock'));
|
||||
|
||||
/*
|
||||
* Logging
|
||||
*/
|
||||
|
||||
o = s.taboption('logging', form.Value, 'log_size', _('System log buffer size'), "kiB");
|
||||
o.optional = true;
|
||||
o.placeholder = 128;
|
||||
o.datatype = 'uinteger';
|
||||
|
||||
o = s.taboption('logging', form.Value, 'log_ip', _('External system log server'));
|
||||
o.optional = true;
|
||||
o.placeholder = '0.0.0.0';
|
||||
o.datatype = 'host';
|
||||
|
||||
o = s.taboption('logging', form.Value, 'log_port', _('External system log server port'));
|
||||
o.optional = true;
|
||||
o.placeholder = 514;
|
||||
o.datatype = 'port';
|
||||
|
||||
o = s.taboption('logging', form.ListValue, 'log_proto', _('External system log server protocol'));
|
||||
o.value('udp', 'UDP');
|
||||
o.value('tcp', 'TCP');
|
||||
|
||||
o = s.taboption('logging', form.Value, 'log_file', _('Write system log to file'));
|
||||
o.optional = true;
|
||||
o.placeholder = '/tmp/system.log';
|
||||
|
||||
o = s.taboption('logging', form.ListValue, 'conloglevel', _('Log output level'), _('Only affects dmesg kernel log'));
|
||||
o.value(8, _('Debug'));
|
||||
o.value(7, _('Info'));
|
||||
o.value(6, _('Notice'));
|
||||
o.value(5, _('Warning'));
|
||||
o.value(4, _('Error'));
|
||||
o.value(3, _('Critical'));
|
||||
o.value(2, _('Alert'));
|
||||
o.value(1, _('Emergency'));
|
||||
|
||||
o = s.taboption('logging', form.ListValue, 'cronloglevel', _('Cron Log Level'));
|
||||
o.default = 7;
|
||||
o.value(7, _('Normal'));
|
||||
o.value(9, _('Disabled'));
|
||||
o.value(5, _('Debug'));
|
||||
|
||||
/*
|
||||
* Zram Properties
|
||||
*/
|
||||
|
||||
if (L.hasSystemFeature('zram')) {
|
||||
s.tab('zram', _('ZRam Settings'));
|
||||
|
||||
o = s.taboption('zram', form.Flag, 'zram_enabled', _('Enable'));
|
||||
o.enabled = '1';
|
||||
o.disabled = '0';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.cfgvalue = function(section_id) {
|
||||
return String(uci.get('system', section_id, 'zram_enabled') ?? '1');
|
||||
};
|
||||
|
||||
o = s.taboption('zram', form.Value, 'zram_size_mb', _('ZRam Size'), _('Size of the ZRam device in megabytes'));
|
||||
o.optional = true;
|
||||
o.placeholder = 16;
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('zram_enabled', '1');
|
||||
|
||||
const supported_algorithms = String(zram_algorithms || '')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map(algo => algo.replace(/[\[\]]/g, ''))
|
||||
.filter((algo, index, list) => algo && list.indexOf(algo) === index);
|
||||
const current_algorithm = uci.get('system', '@system[0]', 'zram_comp_algo');
|
||||
|
||||
o = s.taboption('zram', form.ListValue, 'zram_comp_algo', _('ZRam Compression Algorithm'));
|
||||
o.optional = true;
|
||||
o.default = current_algorithm || supported_algorithms[0] || 'lzo';
|
||||
o.depends('zram_enabled', '1');
|
||||
|
||||
if (!supported_algorithms.length) {
|
||||
[ 'lzo', 'lz4', 'zstd' ].forEach(algo => o.value(algo, algo));
|
||||
}
|
||||
else {
|
||||
supported_algorithms.forEach(algo => o.value(algo, algo));
|
||||
|
||||
if (current_algorithm && supported_algorithms.indexOf(current_algorithm) < 0)
|
||||
o.value(current_algorithm, '%s (%s)'.format(current_algorithm, _('unsupported')));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Language & Style
|
||||
*/
|
||||
|
||||
o = s.taboption('language', form.ListValue, '_lang', _('Language'))
|
||||
o.uciconfig = 'luci';
|
||||
o.ucisection = 'main';
|
||||
o.ucioption = 'lang';
|
||||
o.value('auto', _('auto'));
|
||||
|
||||
const l = Object.assign({ en: 'English' }, uci.get('luci', 'languages'));
|
||||
const keys = Object.keys(l).sort();
|
||||
for (let k of keys)
|
||||
if (k.charAt(0) != '.')
|
||||
o.value(k, l[k]);
|
||||
|
||||
o = s.taboption('language', form.ListValue, '_mediaurlbase', _('Design'))
|
||||
o.uciconfig = 'luci';
|
||||
o.ucisection = 'main';
|
||||
o.ucioption = 'mediaurlbase';
|
||||
|
||||
const th = Object.keys(uci.get('luci', 'themes') || {}).sort();
|
||||
for (let t of th)
|
||||
if (t.charAt(0) != '.')
|
||||
o.value(uci.get('luci', 'themes', t), t);
|
||||
|
||||
o = s.taboption('language', form.Flag, '_tablefilters', _('Table Filters'));
|
||||
o.default = o.disabled;
|
||||
o.uciconfig = 'luci';
|
||||
o.ucisection = 'main';
|
||||
o.ucioption = 'tablefilters';
|
||||
|
||||
/*
|
||||
* NTP
|
||||
*/
|
||||
|
||||
if (L.hasSystemFeature('sysntpd')) {
|
||||
var default_servers = [
|
||||
'0.openwrt.pool.ntp.org', '1.openwrt.pool.ntp.org',
|
||||
'2.openwrt.pool.ntp.org', '3.openwrt.pool.ntp.org'
|
||||
];
|
||||
|
||||
o = s.taboption('timesync', form.Flag, 'enabled', _('Enable NTP client'));
|
||||
o.rmempty = false;
|
||||
o.ucisection = 'ntp';
|
||||
o.default = o.disabled;
|
||||
o.write = function(section_id, value) {
|
||||
ntpd_enabled = +value;
|
||||
|
||||
if (ntpd_enabled && !uci.get('system', 'ntp')) {
|
||||
uci.add('system', 'timeserver', 'ntp');
|
||||
uci.set('system', 'ntp', 'server', default_servers);
|
||||
}
|
||||
|
||||
if (!ntpd_enabled)
|
||||
uci.set('system', 'ntp', 'enabled', 0);
|
||||
else
|
||||
uci.unset('system', 'ntp', 'enabled');
|
||||
|
||||
return callRcInit('sysntpd', 'enable');
|
||||
};
|
||||
o.load = function(section_id) {
|
||||
return (ntpd_enabled == 1 &&
|
||||
uci.get('system', 'ntp') != null &&
|
||||
uci.get('system', 'ntp', 'enabled') != 0) ? '1' : '0';
|
||||
};
|
||||
|
||||
o = s.taboption('timesync', form.Flag, 'enable_server', _('Provide NTP server'));
|
||||
o.ucisection = 'ntp';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.taboption('timesync', widgets.NetworkSelect, 'interface',
|
||||
_('Bind NTP server'),
|
||||
_('Provide the NTP server to the selected interface or, if unspecified, to all interfaces'));
|
||||
o.ucisection = 'ntp';
|
||||
o.depends('enable_server', '1');
|
||||
o.multiple = false;
|
||||
o.nocreate = true;
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('timesync', form.Flag, 'use_dhcp', _('Use DHCP advertised servers'));
|
||||
o.ucisection = 'ntp';
|
||||
o.default = o.enabled;
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.taboption('timesync', form.DynamicList, 'server', _('NTP server candidates'),
|
||||
_('List of upstream NTP server candidates with which to synchronize.'));
|
||||
o.datatype = 'host(0)';
|
||||
o.ucisection = 'ntp';
|
||||
o.depends('enabled', '1');
|
||||
o.load = function(section_id) {
|
||||
return uci.get('system', 'ntp', 'server');
|
||||
};
|
||||
}
|
||||
|
||||
return m.render().then(function(mapEl) {
|
||||
poll.add(function() {
|
||||
return callGetUnixtime().then(function(t) {
|
||||
mapEl.querySelector('#localtime').value = formatTime(t);
|
||||
});
|
||||
});
|
||||
|
||||
return mapEl;
|
||||
});
|
||||
},
|
||||
|
||||
isZramChanged() {
|
||||
if (!L.hasSystemFeature('zram') || !this.map || !this.systemSid)
|
||||
return false;
|
||||
|
||||
const enabled = this.map.lookupOption('zram_enabled', this.systemSid);
|
||||
const size = this.map.lookupOption('zram_size_mb', this.systemSid);
|
||||
const algo = this.map.lookupOption('zram_comp_algo', this.systemSid);
|
||||
const current_enabled = String(uci.get('system', '@system[0]', 'zram_enabled') ?? '1');
|
||||
const current_size = String(uci.get('system', '@system[0]', 'zram_size_mb') ?? '');
|
||||
const current_algo = String(uci.get('system', '@system[0]', 'zram_comp_algo') ?? '');
|
||||
const form_enabled = String(enabled ? (enabled[0].formvalue(enabled[1]) ?? '') : current_enabled);
|
||||
const form_size = String(size ? (size[0].formvalue(size[1]) ?? '') : current_size);
|
||||
const form_algo = String(algo ? (algo[0].formvalue(algo[1]) ?? '') : current_algo);
|
||||
|
||||
return (form_enabled !== current_enabled || form_size !== current_size || form_algo !== current_algo);
|
||||
},
|
||||
|
||||
handleSaveApply(ev, mode) {
|
||||
if (this.isZramChanged()) {
|
||||
const fn = L.bind(() => {
|
||||
callRcInit('zram', 'restart');
|
||||
document.removeEventListener('uci-applied', fn);
|
||||
});
|
||||
|
||||
document.addEventListener('uci-applied', fn);
|
||||
}
|
||||
|
||||
return this.super('handleSaveApply', [ev, mode]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('uhttpd', _('HTTP(S) Access'), _('uHTTPd offers <abbr title="Hypertext Transfer Protocol">HTTP</abbr> or <abbr title="Hypertext Transfer Protocol Secure">HTTPS</abbr> network access.'));
|
||||
|
||||
s = m.section(form.NamedSection, 'main', 'uhttpd', _('Settings'));
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'redirect_https', _('Redirect to HTTPS'), _('Enable automatic redirection of <abbr title="Hypertext Transfer Protocol">HTTP</abbr> requests to <abbr title="Hypertext Transfer Protocol Secure">HTTPS</abbr> port.'));
|
||||
o.rmempty = false;
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user