diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index 7a9e07835..046cd5f20 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -350,6 +350,14 @@ "message": "Choose a Custom Login Field", "description": "Help text for Custom Login Field banner." }, + "url": { + "message": "URL", + "description": "General text for URL." + }, + "group": { + "message": "Group", + "description": "General text for group." + }, "username": { "message": "Username", "description": "General text for username." @@ -414,6 +422,22 @@ "message": "Settings", "description": "Popup Settings button text." }, + "popupAddCredentialsText": { + "message": "Add a new credential", + "description": "Popup add a new credential text." + }, + "popupAddCredentialsPasswordText": { + "message": "If empty, KeePassXC generates the password automatically.", + "description": "Popup add a new credential password help text." + }, + "popupAddCredentialGroupText": { + "message": "Separate the group with slashes. For example: Group/ChildGroup. If empty, the default group is used.", + "description": "Popup add a new credential group help text." + }, + "popupAddCredentialsTitlePlaceholder": { + "message": "Title", + "description": "Title placeholder." + }, "popupChooseCredentialsText": { "message": "Choose Custom Login Fields", "description": "Popup credential choosing button text." diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 253ec71c2..89141898a 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -27,11 +27,12 @@ kpxcEvent.showStatus = async function(tab, configured, internalPoll) { const errorMessage = page.tabs[tab.id]?.errorMessage ?? undefined; const usernameFieldDetected = page.tabs[tab.id]?.usernameFieldDetected ?? false; const iframeDetected = page.tabs[tab.id]?.iframeDetected ?? false; + const compareResult = keepass.compareMultipleVersions([ '2.7.11' ], keepass.currentKeePassXC); return { associated: keepass.isAssociated(), - configured: configured, + compareResult: compareResult, databaseClosed: keepass.isDatabaseClosed, encryptionKeyUnrecognized: keepass.isEncryptionKeyUnrecognized, error: errorMessage, @@ -267,6 +268,7 @@ kpxcEvent.messageHandlers = { 'load_keyring': kpxcEvent.onLoadKeyRing, 'load_settings': kpxcEvent.onLoadSettings, 'lock_database': kpxcEvent.lockDatabase, + 'page_add_new_credential': page.addNewCredential, 'page_clear_logins': kpxcEvent.pageClearLogins, 'page_clear_submitted': page.clearSubmittedCredentials, 'page_get_autosubmit_performed': page.getAutoSubmitPerformed, diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index e4bf97938..efd69b1ff 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -46,13 +46,13 @@ browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': n //-------------------------------------------------------------------------- keepass.addCredentials = async function(tab, args = []) { - const [ username, password, url, group, groupUuid ] = args; - return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]); + const [ username, password, url, group, groupUuid, generatePassword ] = args; + return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid, generatePassword ]); }; keepass.updateCredentials = async function(tab, args = []) { try { - const [ entryId, username, password, url, group, groupUuid ] = args; + const [ entryId, username, password, url, group, groupUuid, generatePassword ] = args; const taResponse = await keepass.testAssociation(tab); if (!taResponse) { browserAction.showDefault(tab); @@ -69,7 +69,8 @@ keepass.updateCredentials = async function(tab, args = []) { login: username, password: password, url: url, - submitUrl: url + submitUrl: url, + generatePassword: generatePassword }; if (entryId) { diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index a3bfe5eb2..3c91af2fe 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -40,6 +40,7 @@ const defaultSettings = { }; const AUTO_SUBMIT_TIMEOUT = 5000; +const DEFAULT_BROWSER_GROUP = 'KeePassXC-Browser Passwords'; const page = {}; page.autoSubmitPerformed = false; @@ -149,6 +150,87 @@ page.switchTab = async function(tab) { }); }; +// Adds a new credential and handles setting/creating the group +page.addNewCredential = async function(tab, args) { + if (!tab || !page.tabs[tab.id]) { + return; + } + + // Traverse the groups and ensure all paths are found + const getDefaultGroup = function(groups, defaultGroup) { + const getGroup = function(group, splitted, depth = -1) { + ++depth; + for (const g of group) { + if (g.name === splitted[depth]) { + if (splitted.length === (depth + 1)) { + return [ g.name, g.uuid ]; + } + return getGroup(g.children, splitted, depth); + } + } + return [ '', '' ]; + }; + + const splitted = defaultGroup.split('/'); + return getGroup(groups, splitted); + }; + + const saveToDefaultGroup = async function(creds) { + const res = await keepass.addCredentials(tab, [ + creds.username, + creds.password, + creds.url, + creds.group, + undefined, + creds?.generatePassword + ]); + return res; + }; + + const result = await keepass.getDatabaseGroups(tab); + if (!result || !result.groups) { + const res = await saveToDefaultGroup(args); + return res; + } + + // Group has not been set + if (args?.group === '' + || (!args?.group && (result.defaultGroup === '' || result.defaultGroup === DEFAULT_BROWSER_GROUP))) { + const res = await saveToDefaultGroup(args); + return res; + } + + // A specified group is used + const [ groupName, groupUUID ] = getDefaultGroup(result.groups[0].children, args.group || result.defaultGroup); + if (groupName === '' && groupUUID === '') { + // Create a new group + const newGroup = await keepass.createNewGroup(tab, [ args.group || result.defaultGroup ]); + if (newGroup.name && newGroup.uuid) { + const res = await keepass.addCredentials(tab, [ + args.username, + args.password, + args.url, + newGroup.name, + newGroup.uuid, + args?.generatePassword + ]); + return res; + } + + return 'canceled'; + } + + const res = await await keepass.addCredentials(tab, [ + args.username, + args.password, + args.url, + groupName, + groupUUID, + args?.generatePassword + ]); + return res; +}; + page.clearCredentials = async function(tabId, complete) { if (!page.tabs[tabId]) { return; diff --git a/keepassxc-browser/content/banner.js b/keepassxc-browser/content/banner.js index eb732a322..244df8b75 100644 --- a/keepassxc-browser/content/banner.js +++ b/keepassxc-browser/content/banner.js @@ -1,7 +1,5 @@ 'use strict'; -const DEFAULT_BROWSER_GROUP = 'KeePassXC-Browser Passwords'; - const kpxcBanner = {}; kpxcBanner.banner = undefined; kpxcBanner.created = false; @@ -170,64 +168,16 @@ kpxcBanner.create = async function(credentials = {}) { }; kpxcBanner.saveNewCredentials = async function(credentials = {}) { - const saveToDefaultGroup = async function(creds) { - const args = [ creds.username, creds.password, creds.url ]; - const res = await sendMessage('add_credentials', args); - kpxcBanner.verifyResult(res); - }; - const result = await sendMessage('get_database_groups'); if (!result || !result.groups) { logError('Empty result from get_database_groups'); - await saveToDefaultGroup(credentials); return; } if (!result.defaultGroupAlwaysAsk) { - if (result.defaultGroup === '' || result.defaultGroup === DEFAULT_BROWSER_GROUP) { - await saveToDefaultGroup(credentials); - return; - } else { - // A specified group is used - let gname = ''; - let guuid = ''; - - if (result.defaultGroup.toLowerCase() === 'root') { - result.defaultGroup = '/'; - gname = result.groups[0].name; - guuid = result.groups[0].uuid; - } else { - [ gname, guuid ] = kpxcBanner.getDefaultGroup(result.groups[0].children, result.defaultGroup); - if (gname === '' && guuid === '') { - // Create a new group - const newGroup = await sendMessage('create_new_group', [ result.defaultGroup ]); - if (newGroup.name && newGroup.uuid) { - const res = await sendMessage('add_credentials', [ - credentials.username, - credentials.password, - credentials.url, - newGroup.name, - newGroup.uuid, - ]); - kpxcBanner.verifyResult(res); - } else { - kpxcUI.createNotification('error', tr('rememberErrorCreatingNewGroup')); - } - - return; - } - } - - const res = await sendMessage('add_credentials', [ - credentials.username, - credentials.password, - credentials.url, - gname, - guuid, - ]); - kpxcBanner.verifyResult(res); - return; - } + const res = await sendMessage('page_add_new_credential', credentials); + kpxcBanner.verifyResult(res, credentials.username); + return; } const addChildren = function(group, parentElement, depth = 0) { @@ -366,19 +316,23 @@ kpxcBanner.updateCredentials = async function(credentials = {}) { } }; -kpxcBanner.verifyResult = async function(code) { +kpxcBanner.verifyResult = async function(code, givenUsername) { + const username = givenUsername || kpxcBanner.credentials.username; + if (code === 'error') { kpxcUI.createNotification('error', tr('rememberErrorCannotSaveCredentials')); + } else if (code === 'error_new_group') { + kpxcUI.createNotification('error', tr('rememberErrorCreatingNewGroup')); } else if (code === 'created') { kpxcUI.createNotification( 'success', - tr('rememberCredentialsSaved', kpxcBanner.credentials.username || tr('rememberEmptyUsername')), + tr('rememberCredentialsSaved', username || tr('rememberEmptyUsername')), ); await kpxc.retrieveCredentials(true); // Forced reload } else if (code === 'updated') { kpxcUI.createNotification( 'success', - tr('rememberCredentialsUpdated', kpxcBanner.credentials.username || tr('rememberEmptyUsername')), + tr('rememberCredentialsUpdated', username || tr('rememberEmptyUsername')), ); await kpxc.retrieveCredentials(true); // Forced reload } else if (code === 'canceled') { @@ -386,26 +340,8 @@ kpxcBanner.verifyResult = async function(code) { } else { kpxcUI.createNotification('error', tr('rememberErrorDatabaseClosed')); } - kpxcBanner.destroy(); -}; -// Traverse the groups and ensure all paths are found -kpxcBanner.getDefaultGroup = function(groups, defaultGroup) { - const getGroup = function(group, splitted, depth = -1) { - ++depth; - for (const g of group) { - if (g.name === splitted[depth]) { - if (splitted.length === (depth + 1)) { - return [ g.name, g.uuid ]; - } - return getGroup(g.children, splitted, depth); - } - } - return [ '', '' ]; - }; - - const splitted = defaultGroup.split('/'); - return getGroup(groups, splitted); + kpxcBanner.destroy(); }; kpxcBanner.createCredentialDialog = async function() { diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 3ab81b9fb..fb8953383 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -945,6 +945,9 @@ browser.runtime.onMessage.addListener(async function(req, sender) { kpxc.triggerActivatedTab(); } else if (req.action === 'add_allow_iframes_option') { kpxc.addToSitePreferences('allowIframes'); + } else if (req.action === 'add_new_credential') { + const res = await sendMessage('page_add_new_credential', req.args); + kpxcBanner.verifyResult(res, req.args.username); } else if (req.action === 'add_username_only_option') { kpxc.addToSitePreferences('usernameOnly', true); } else if (req.action === 'check_database_hash' && 'hash' in req) { diff --git a/keepassxc-browser/popups/popup.css b/keepassxc-browser/popups/popup.css index 3f0929fea..7653551a6 100644 --- a/keepassxc-browser/popups/popup.css +++ b/keepassxc-browser/popups/popup.css @@ -130,7 +130,7 @@ code { display: none; } -#options-button { +#options-button, #add-credentials-button { height: 31px; width: 2.5rem; } @@ -205,3 +205,8 @@ code { color: var(--kpxc-text-color) !important; } } + +/* Add new credentials */ +.help-text { + margin-inline-start: 1.725em; +} diff --git a/keepassxc-browser/popups/popup.html b/keepassxc-browser/popups/popup.html index 7f842d0c4..2f6771ccb 100644 --- a/keepassxc-browser/popups/popup.html +++ b/keepassxc-browser/popups/popup.html @@ -23,6 +23,9 @@ +
+ + + diff --git a/keepassxc-browser/popups/popup.js b/keepassxc-browser/popups/popup.js index 28232843d..c32913431 100644 --- a/keepassxc-browser/popups/popup.js +++ b/keepassxc-browser/popups/popup.js @@ -10,7 +10,7 @@ HTMLElement.prototype.hide = function() { this.style.display = 'none'; }; -function statusResponse(r) { +async function statusResponse(response) { $('#initial-state').hide(); $('#error-encountered').hide(); $('#need-reconfigure').hide(); @@ -20,44 +20,49 @@ function statusResponse(r) { $('#lock-database-button').hide(); $('#getting-started-guide').hide(); $('#database-not-opened').hide(); + $('#add-credentials-button').hide(); - if (!r.keePassXCAvailable) { - $('#error-message').textContent = r.error; + if (!response?.keePassXCAvailable) { + $('#error-message').textContent = response?.error; $('#error-encountered').show(); - if (r.showGettingStartedGuideAlert) { + if (response?.showGettingStartedGuideAlert) { $('#getting-started-guide').show(); } - if (r.showTroubleshootingGuideAlert && reloadCount >= 2) { + if (response?.showTroubleshootingGuideAlert && reloadCount >= 2) { $('#troubleshooting-guide').show(); } else { $('#troubleshooting-guide').hide(); } - } else if (r.keePassXCAvailable && r.databaseClosed) { - $('#database-error-message').textContent = r.error; + } else if (response?.keePassXCAvailable && response?.databaseClosed) { + $('#database-error-message').textContent = response?.error; $('#database-not-opened').show(); - } else if (!r.configured) { + } else if (!response?.configured) { $('#not-configured').show(); - } else if (r.encryptionKeyUnrecognized) { + } else if (response?.encryptionKeyUnrecognized) { $('#need-reconfigure').show(); - $('#need-reconfigure-message').textContent = r.error; - } else if (!r.associated) { + $('#need-reconfigure-message').textContent = response?.error; + } else if (!response?.associated) { $('#need-reconfigure').show(); - $('#need-reconfigure-message').textContent = r.error; - } else if (r.error) { + $('#need-reconfigure-message').textContent = response?.error; + } else if (response?.error) { $('#error-encountered').show(); - $('#error-message').textContent = r.error; + $('#error-message').textContent = response?.error; } else { $('#configured-and-associated').show(); - $('#associated-identifier').textContent = r.identifier; + $('#associated-identifier').textContent = response?.identifier; $('#lock-database-button').show(); - if (r.usernameFieldDetected) { + if (response?.compareResult['2.7.11']) { + $('#add-credentials-button').show(); + } + + if (response?.usernameFieldDetected) { $('#username-field-detected').show(); } - if (r.iframeDetected) { + if (response?.iframeDetected) { $('#iframe-detected').show(); } diff --git a/keepassxc-browser/popups/popup_functions.js b/keepassxc-browser/popups/popup_functions.js index 82ad4543a..902feff60 100644 --- a/keepassxc-browser/popups/popup_functions.js +++ b/keepassxc-browser/popups/popup_functions.js @@ -11,11 +11,13 @@ function updateAvailableResponse(available) { } async function initSettings() { + const tab = await getCurrentTab(); + $('#settings #options-button').addEventListener('click', () => { browser.runtime.openOptionsPage().then(close()); }); - const customLoginFieldsButton = document.body.querySelector('#settings #choose-custom-login-fields-button'); + const customLoginFieldsButton = $('#settings #choose-custom-login-fields-button'); if (isFirefox()) { customLoginFieldsButton.id = 'choose-custom-login-fields-button-moz'; } @@ -27,6 +29,37 @@ async function initSettings() { }); close(); }); + + $('#settings #add-credentials-button').addEventListener('click', () => { + $('#add-credentials-title').value = tab?.title; + $('#add-credentials-url').value = tab?.url; + if ($('#add-credentials')?.style?.display === 'none') { + $('#add-credentials').show(); + $('input#add-credentials-username')?.focus(); + } else { + $('#add-credentials').hide(); + } + + if ($('#credentialsList')?.style?.display !== 'none') { + $('#credentialsList')?.hide(); + } else { + $('#credentialsList')?.show(); + } + }); + + $('#add-credentials-save-button').addEventListener('click', async (e) => { + if (e?.currentTarget?.form?.checkValidity()) { + await addNewCredential(); + close(); + } + }); + + $('#add-credentials-cancel-button').addEventListener('click', async () => { + $('#add-credentials').hide(); + if ($('#credentialsList')?.style?.display === 'none') { + $('#credentialsList').show(); + } + }); } async function initColorTheme() { @@ -53,6 +86,25 @@ async function getLoginData() { return logins; } +async function addNewCredential() { + const tab = await getCurrentTab(); + if (!tab) { + return []; + } + + const password = $('input#add-credentials-password')?.value; + browser.tabs.sendMessage(tab?.id, { + action: 'add_new_credential', args: { + username: $('input#add-credentials-username')?.value, + password: password, + group: $('input#add-credentials-group')?.value, + title: $('input#add-credentials-title')?.value, + url: $('input#add-credentials-url')?.value, + generatePassword: !password || password?.length === 0 + } + }); +} + (async () => { if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) { await initSettings(); diff --git a/keepassxc-browser/popups/popup_login.html b/keepassxc-browser/popups/popup_login.html index 57dad2f2b..f01b625b9 100644 --- a/keepassxc-browser/popups/popup_login.html +++ b/keepassxc-browser/popups/popup_login.html @@ -23,6 +23,9 @@ +
-
+

@@ -58,6 +61,55 @@

+ + + diff --git a/keepassxc-browser/popups/popup_login.js b/keepassxc-browser/popups/popup_login.js index 4703b9a1b..618d6e1fe 100644 --- a/keepassxc-browser/popups/popup_login.js +++ b/keepassxc-browser/popups/popup_login.js @@ -1,5 +1,18 @@ 'use strict'; +async function hideAddCredentialsButton() { + const keePassVersions = await browser.runtime.sendMessage({ + action: 'get_keepassxc_versions' + }); + const versionResults = await browser.runtime.sendMessage({ + action: 'compare_versions', + args: [ [ '2.7.11' ], keePassVersions.current ], + }); + if (!versionResults['2.7.11']) { + $('#add-credentials-button').hide(); + } +} + (async () => { await initColorTheme(); @@ -10,6 +23,8 @@ return []; } + await hideAddCredentialsButton(); + const logins = await getLoginData(); const ll = document.getElementById('login-list'); @@ -68,6 +83,8 @@ $('#credentialsList').hide(); $('#database-not-opened').show(); $('#lock-database-button').hide(); + $('#add-credentials').hide(); + $('#add-credentials-button').hide(); $('#database-error-message').textContent = tr('errorMessageDatabaseNotOpened'); }); diff --git a/keepassxc-protocol.md b/keepassxc-protocol.md index 74def3439..07a37265a 100644 --- a/keepassxc-protocol.md +++ b/keepassxc-protocol.md @@ -366,7 +366,7 @@ Response message data (success, decrypted): ``` ### set-login -Unencrypted message (downloadFavicon supported in KeePassXC 2.7.0 and later, but not when updating credentials): +Unencrypted message: ```json { "action": "set-login", @@ -379,7 +379,8 @@ Unencrypted message (downloadFavicon supported in KeePassXC 2.7.0 and later, but "group": "", "groupUuid": "", "uuid": "", - "downloadFavicon": "true" + "downloadFavicon": "true" (KeePassXC 2.7.0 and later, but not when updating credentials) + "generatePassword": "true" (KeePassXC 2.7.11 and later) } ```