Add support for Creating a new credential manually

This commit is contained in:
varjolintu 2024-09-21 15:10:46 +03:00
parent 44e8b45cc6
commit 12563189fa
11 changed files with 280 additions and 79 deletions

View file

@ -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."

View file

@ -267,6 +267,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,

View file

@ -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,79 @@ 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 args = [ creds.username, creds.password, creds.url, creds.group ];
const res = await keepass.addCredentials(tab, args);
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,
]);
return res;
}
return 'canceled';
}
const res = await await keepass.addCredentials(tab, [
args.username,
args.password,
args.url,
groupName,
groupUUID,
]);
return res;
};
page.clearCredentials = async function(tabId, complete) {
if (!page.tabs[tabId]) {
return;

View file

@ -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,28 +340,10 @@ 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.createCredentialDialog = async function() {
kpxcBanner.shadowSelector('#kpxc-banner-btn-new').hidden = true;
kpxcBanner.shadowSelector('#kpxc-banner-btn-update').hidden = true;

View file

@ -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) {

View file

@ -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;
}

View file

@ -23,6 +23,9 @@
<i class="fa fa-cog" aria-hidden="true"></i>
</button>
<button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button>
<button id="add-credentials-button" class="btn btn-sm btn-warning" data-i18n="[title]popupAddCredentialsText" style="display: none">
<i class="fa fa-user-plus" aria-hidden="true"></i>
</button>
</div>
<div class="lock-button-area">
<button id="lock-database-button" class="btn btn-sm btn-danger" data-i18n="[title]lockDatabase">
@ -142,6 +145,56 @@
</button>
</div>
</div>
<div id="add-credentials" style="display: none">
<hr>
<span data-i18n="popupAddCredentialsText"></span>
<form>
<div class="content pt-2 pb-2">
<div class="pb-2">
<input class="form-control form-control-sm" type="text" id="add-credentials-title" data-i18n="[placeholder]popupAddCredentialsTitlePlaceholder" required>
</div>
<div class="pb-2">
<input type="text" class="form-control form-control-sm" id="add-credentials-username" data-i18n="[placeholder]username" required>
</div>
<div class="pb-2">
<input type="password" class="form-control form-control-sm" id="add-credentials-password"data-i18n="[placeholder]password" required>
<div class="form-text help-text" id="add-credentials-password-help-text">
<span data-i18n="popupAddCredentialsPasswordText"></span>
</div>
</div>
<div class="pb-2">
<input type="url" class="form-control form-control-sm" id="add-credentials-url" data-i18n="[placeholder]url" required>
</div>
<div class="pb-2">
<input type="text" class="form-control form-control-sm" id="add-credentials-group" data-i18n="[placeholder]group">
<div class="form-text help-text" id="add-credentials-group-help-text">
<span data-i18n="popupAddCredentialGroupText"></span>
</div>
</div>
<!-- TODO: Protocol V2
<div class="input-group input-group-sm pb-2">
<label class="input-group-text" for="add-credentials-database">Database</label>
<select class="form-select form-select-sm" id="add-credentials-database">
<option value="1">Database 1</option>
<option value="2">...</option>
</select>
</div>
-->
</div>
<div class="content right-align pt-2 pb-2">
<button class="btn btn-sm btn-primary me-1" type="submit" id="add-credentials-save-button" data-i18n="[title]optionsButtonSave">
<i class="fa fa-save" aria-hidden="true"></i>
<span data-i18n="optionsButtonSave"></span>
</button>
<button class="btn btn-sm btn-danger" type="button" id="add-credentials-cancel-button" data-i18n="[title]optionsButtonCancel">
<i class="fa fa-remove" aria-hidden="true"></i>
<span data-i18n="optionsButtonCancel"></span>
</button>
</div>
</form>
</div>
</div>
</body>
</html>

View file

@ -10,7 +10,7 @@ HTMLElement.prototype.hide = function() {
this.style.display = 'none';
};
function statusResponse(r) {
async function statusResponse(r) {
$('#initial-state').hide();
$('#error-encountered').hide();
$('#need-reconfigure').hide();
@ -20,6 +20,7 @@ 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;
@ -52,6 +53,7 @@ function statusResponse(r) {
$('#configured-and-associated').show();
$('#associated-identifier').textContent = r.identifier;
$('#lock-database-button').show();
$('#add-credentials-button').show();
if (r.usernameFieldDetected) {
$('#username-field-detected').show();

View file

@ -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,36 @@ 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();
} 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 +85,23 @@ async function getLoginData() {
return logins;
}
async function addNewCredential() {
const tab = await getCurrentTab();
if (!tab) {
return [];
}
browser.tabs.sendMessage(tab?.id, {
action: 'add_new_credential', args: {
username: $('input#add-credentials-username')?.value,
password: $('input#add-credentials-password')?.value,
group: $('input#add-credentials-group')?.value,
title: $('input#add-credentials-title')?.value,
url: $('input#add-credentials-url')?.value,
}
});
}
(async () => {
if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) {
await initSettings();

View file

@ -23,6 +23,9 @@
<i class="fa fa-cog" aria-hidden="true"></i>
</button>
<button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button>
<button id="add-credentials-button" class="btn btn-sm btn-warning" data-i18n="[title]popupAddCredentialsText">
<i class="fa fa-user-plus" aria-hidden="true"></i>
</button>
</div>
<div class="lock-button-area">
<button id="lock-database-button" class="btn btn-sm btn-danger" data-i18n="[title]lockDatabase">
@ -46,7 +49,7 @@
<div id="login-list" class="list-group"></div>
</div>
<div id="database-not-opened">
<div id="database-not-opened">
<p data-i18n="popupErrorEncountered"></p>
<p style="margin-left: 1em">
<code id="database-error-message"></code>
@ -58,6 +61,55 @@
</button>
</div>
</div>
<div id="add-credentials" style="display: none">
<span data-i18n="popupAddCredentialsText"></span>
<form>
<div class="content pt-2 pb-2">
<div class="pb-2">
<input class="form-control form-control-sm" type="text" id="add-credentials-title" data-i18n="[placeholder]popupAddCredentialsTitlePlaceholder" required>
</div>
<div class="pb-2">
<input type="text" class="form-control form-control-sm" id="add-credentials-username" data-i18n="[placeholder]username" required>
</div>
<div class="pb-2">
<input type="password" class="form-control form-control-sm" id="add-credentials-password"data-i18n="[placeholder]password" required>
<div class="form-text help-text" id="add-credentials-password-help-text">
<span data-i18n="popupAddCredentialsPasswordText"></span>
</div>
</div>
<div class="pb-2">
<input type="url" class="form-control form-control-sm" id="add-credentials-url" data-i18n="[placeholder]url" required>
</div>
<div class="pb-2">
<input type="text" class="form-control form-control-sm" id="add-credentials-group" data-i18n="[placeholder]group">
<div class="form-text help-text" id="add-credentials-group-help-text">
<span data-i18n="popupAddCredentialGroupText"></span>
</div>
</div>
<!-- TODO: Protocol V2
<div class="input-group input-group-sm pb-2">
<label class="input-group-text" for="add-credentials-database">Database</label>
<select class="form-select form-select-sm" id="add-credentials-database">
<option value="1">Database 1</option>
<option value="2">...</option>
</select>
</div>
-->
</div>
<div class="content right-align pt-2 pb-2">
<button class="btn btn-sm btn-primary me-1" type="submit" id="add-credentials-save-button" data-i18n="[title]optionsButtonSave">
<i class="fa fa-save" aria-hidden="true"></i>
<span data-i18n="optionsButtonSave"></span>
</button>
<button class="btn btn-sm btn-danger" type="button" id="add-credentials-cancel-button" data-i18n="[title]optionsButtonCancel">
<i class="fa fa-remove" aria-hidden="true"></i>
<span data-i18n="optionsButtonCancel"></span>
</button>
</div>
</form>
</div>
</div>
</body>
</html>

View file

@ -68,6 +68,8 @@
$('#credentialsList').hide();
$('#database-not-opened').show();
$('#lock-database-button').hide();
$('#add-credentials').hide();
$('#add-credentials-button').hide();
$('#database-error-message').textContent = tr('errorMessageDatabaseNotOpened');
});