From eb4c153155337aab8ea7cecb990927287d5d6576 Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Tue, 3 Feb 2026 06:48:37 +0100 Subject: [PATCH] luci-mod-system: refresh repokeys Remove manual UI setup and implement JSONMap. Signed-off-by: Paul Donald --- .../resources/view/system/repokeys.js | 213 ++++++++++-------- 1 file changed, 120 insertions(+), 93 deletions(-) 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,