mirror of
https://github.com/openwrt/luci.git
synced 2026-02-04 12:06:01 +08:00
luci-mod-system: refresh repokeys
Remove manual UI setup and implement JSONMap. Signed-off-by: Paul Donald <newtwen+github@gmail.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
const APK_DIR = '/etc/apk/keys/';
|
||||
const OPKG_DIR = '/etc/opkg/keys/';
|
||||
@@ -61,34 +62,6 @@ function safeText(str) {
|
||||
}[s]));
|
||||
}
|
||||
|
||||
function renderKeyItem(pubkey) {
|
||||
const safeFile = isFileInSafeList(pubkey?.filename);
|
||||
const lines = pubkey?.key?.trim()?.split('\n').map(line =>
|
||||
[ E('br'), E('code', {}, [ safeText(line) ]) ]
|
||||
).flat();
|
||||
return E('div', {
|
||||
class: 'item',
|
||||
click: (isReadonlyView || safeFile) ? null : removeKey,
|
||||
'data-file': pubkey?.filename,
|
||||
'data-key': normalizeKey(pubkey?.key)
|
||||
}, [
|
||||
E('strong', {}, [ pubkey?.filename || _('Unnamed key') ]),
|
||||
...lines
|
||||
]);
|
||||
}
|
||||
|
||||
function refreshKeyList(list, keys) {
|
||||
while (!matchesElem(list.firstElementChild, '.add-item'))
|
||||
list.removeChild(list.firstElementChild);
|
||||
|
||||
keys.forEach(function(pubkey) {
|
||||
list.insertBefore(renderKeyItem(pubkey), list.lastElementChild);
|
||||
});
|
||||
|
||||
if (list.firstElementChild === list.lastElementChild)
|
||||
list.insertBefore(E('p', _('No software repository public keys present yet.')), list.lastElementChild);
|
||||
}
|
||||
|
||||
function saveKeyFile(keyContent, file, fileContent) {
|
||||
const ts = Date.now();
|
||||
// Note: opkg can only verify against a key with filename that matches its key hash
|
||||
@@ -98,23 +71,20 @@ function saveKeyFile(keyContent, file, fileContent) {
|
||||
return fs.write(KEYDIR + (filename ?? noname), fileContent ?? keyContent, 384 /* 0600 */);
|
||||
}
|
||||
|
||||
function removeKey(ev) {
|
||||
const file = ev.currentTarget.getAttribute('data-file');
|
||||
const list = findParent(ev.target, '.cbi-dynlist');
|
||||
|
||||
function removeKey(ev, key) {
|
||||
L.showModal(_('Delete key'), [
|
||||
E('div', _('Really delete the following software repository public key?')),
|
||||
E('pre', [ file ]),
|
||||
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 + file).then(() => {
|
||||
return listKeyFiles().then(keys => refreshKeyList(list, keys));
|
||||
});
|
||||
ui.hideModal();
|
||||
fs.remove(KEYDIR + key.filename)
|
||||
.then(() => window.location.reload())
|
||||
.catch(e => ui.addNotification(null, E('p', e.message)))
|
||||
.finally(() => ui.hideModal());
|
||||
}
|
||||
}, _('Delete key'))
|
||||
])
|
||||
@@ -138,11 +108,10 @@ function keyEnvironmentCheck(key) {
|
||||
}
|
||||
|
||||
function addKey(ev, file, fileContent) {
|
||||
const list = findParent(ev.target, '.cbi-dynlist');
|
||||
const input = list.querySelector('textarea[type="text"]');
|
||||
let key = (fileContent ?? input.value.trim());
|
||||
const input = document.getElementById('key-input');
|
||||
const key = (fileContent ?? input?.value?.trim());
|
||||
|
||||
if (!key.length)
|
||||
if (!key || !key.length)
|
||||
return;
|
||||
|
||||
// Handle remote URL paste
|
||||
@@ -197,21 +166,20 @@ function addKey(ev, file, fileContent) {
|
||||
}
|
||||
|
||||
// Prevent duplicates
|
||||
const exists = Array.from(list.querySelectorAll('.item')).some(
|
||||
item => item.getAttribute('data-key') === normalizeKey(key)
|
||||
);
|
||||
if (exists) {
|
||||
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(() => listKeyFiles())
|
||||
.then(keys => refreshKeyList(list, keys))
|
||||
.then(() => window.location.reload())
|
||||
.catch(e => ui.addNotification(null, E('p', e.message)));
|
||||
});
|
||||
}
|
||||
|
||||
function dragKey(ev) {
|
||||
@@ -224,7 +192,10 @@ function dropKey(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const input = ev.currentTarget.querySelector('textarea[type="text"]');
|
||||
const input = document.getElementById('key-input');
|
||||
|
||||
if (!input)
|
||||
return;
|
||||
|
||||
for (const file of ev.dataTransfer.files) {
|
||||
const reader = new FileReader();
|
||||
@@ -243,47 +214,103 @@ function handleWindowDragDropIgnore(ev) {
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return determineKeyEnv().then(listKeyFiles);
|
||||
return Promise.all([
|
||||
determineKeyEnv().then(listKeyFiles),
|
||||
]);
|
||||
},
|
||||
|
||||
render(keys) {
|
||||
const list = E('div', {
|
||||
class: 'cbi-dynlist',
|
||||
style: 'max-width: 800px',
|
||||
dragover: isReadonlyView ? null : dragKey,
|
||||
drop: isReadonlyView ? null : dropKey
|
||||
}, [
|
||||
E('div', { class: 'add-item' }, [
|
||||
E('textarea', {
|
||||
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: 300px; min-height: 120px; ',
|
||||
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) addKey(ev); },
|
||||
keydown: function(ev) { if (ev.keyCode === 13 && (ev.ctrlKey || ev.metaKey)) addKey(ev); },
|
||||
disabled: isReadonlyView
|
||||
}),
|
||||
E('button', {
|
||||
});
|
||||
|
||||
addInput.addEventListener('dragover', handleWindowDragDropIgnore);
|
||||
addInput.addEventListener('drop', handleWindowDragDropIgnore);
|
||||
|
||||
const addBtn = E('button', {
|
||||
class: 'cbi-button',
|
||||
click: ui.createHandlerFn(this, addKey),
|
||||
disabled: isReadonlyView
|
||||
}, _('Add key'))
|
||||
])
|
||||
]);
|
||||
}, _('Add key'));
|
||||
|
||||
refreshKeyList(list, keys);
|
||||
window.addEventListener('dragover', handleWindowDragDropIgnore);
|
||||
window.addEventListener('drop', handleWindowDragDropIgnore);
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Repository Public Keys')),
|
||||
E('div', { class: 'cbi-section-descr' },
|
||||
_('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.')),
|
||||
E('div', { class: 'cbi-section-descr' },
|
||||
_('Each key is stored as a file in <code>%s</code>.').format(KEYDIR)),
|
||||
E('div', { class: 'cbi-section-node' }, list)
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user