diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/repokeys.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/repokeys.js
index 3a6c22ab22..7785ee4320 100644
--- a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/repokeys.js
+++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/repokeys.js
@@ -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) {
- ui.addTimeLimitedNotification(_('Add key'), [
- E('div', _('The given software repository public key is already present.')),
- ], 7000, 'notice');
- return;
- }
+ 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;
+ }
- input.value = '';
- saveKeyFile(key, file, fileContent)
- .then(() => listKeyFiles())
- .then(keys => refreshKeyList(list, keys))
- .catch(e => ui.addNotification(null, E('p', e.message)));
+ // 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) {
@@ -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', {
- id: 'key-input',
- 'aria-label': _('Paste or drag repository public key'),
- class: 'cbi-input-text',
- type: 'text',
- style: 'width: 300px; 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); },
- disabled: isReadonlyView
- }),
+ 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.') + '
' +
+ _('Each key is stored as a file in %s.').format(`${KEYDIR}`)
+ ));
+ 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',
- click: ui.createHandlerFn(this, addKey),
- disabled: isReadonlyView
- }, _('Add key'))
- ])
- ]);
+ 'class': 'cbi-button cbi-button-negative remove',
+ 'click': ui.createHandlerFn(this, this.handleRemove, key),
+ 'disabled': isReservedKey ? true : null,
+ }, [_('Delete')]),
+ ];
- refreshKeyList(list, keys);
- window.addEventListener('dragover', handleWindowDragDropIgnore);
- window.addEventListener('drop', handleWindowDragDropIgnore);
+ return E('td', { 'class': 'td middle cbi-section-actions' }, E('div', btns));
+ };
- 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 %s.').format(KEYDIR)),
- E('div', { class: 'cbi-section-node' }, list)
- ]);
+ 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,