Merge tag '1.2.0'

Version 1.2.0
This commit is contained in:
Janek Bevendorff 2018-07-29 13:27:31 +02:00
commit 191cf22a33
20 changed files with 727 additions and 221 deletions

View file

@ -1,3 +1,38 @@
1.2.0 (29-07-2018)
=========================
- Replace "Ignored Sites" with new "Site Preferences" settings page [#208]
- Fix checks throwing errors [#207, #222]
- Ignore more non-input elements [#212, 210]
- Fix URL matching when there is a trailing slash [#231]
- Allow infiting waiting on "Remember Credentials" popup [#232]
- Minor user interface adjustments [#233, #230, #213]
- Improve search field detection [#195]
1.1.7 (13-06-2018)
=========================
- Fix credential field detection regression [#199]
1.1.6 (12-06-2018)
=========================
- Disable single username field detection [#194]
- Fix ignored sites [#196]
- Detect credential fields without type [#198]
1.1.5 (11-06-2018)
=========================
- Fix search fields being detected as username fields [#189]
1.1.4 (10-06-2018)
=========================
- Improve performance of field detection and limit it to 100 fields [#166,#157,185]
- Improve option to ignore fields [#170]
- Automatically retrieve credentials after unlocking the database [#153]
- Fix option to only save the domain name not working as intended [#151]
- Fix credentials popup not being shown on some websites [#154]
- Improve detection of username fields [#164]
- Allow filling of TOTP fields when they are on a separate page [#162]
- Ignore invisible input fields more strictly [#176]
1.1.3 (11-05-2018)
=========================
- Remove autoreconnect to prevent proxy process leakage on Windows [#147]

View file

@ -8,7 +8,7 @@ const BLINK_TIMEOUT_REDIRECT_COUNT_DEFAULT = 1;
browserAction.show = function(callback, tab) {
let data = {};
if (!page.tabs[tab.id] || page.tabs[tab.id].stack.length == 0) {
if (!page.tabs[tab.id] || page.tabs[tab.id].stack.length === 0) {
browserAction.showDefault(callback, tab);
return;
}
@ -36,7 +36,7 @@ browserAction.update = function(interval) {
let data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1];
if (typeof data.visibleForMilliSeconds !== 'undefined') {
if (data.visibleForMilliSeconds !== undefined && data.visibleForMilliSeconds !== -1) {
if (data.visibleForMilliSeconds <= 0) {
browserAction.stackPop(page.currentTabId);
browserAction.show(null, {'id': page.currentTabId});
@ -87,7 +87,7 @@ browserAction.showDefault = function(callback, tab) {
});
};
browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visibleForMilliSeconds, visibleForPageUpdates, redirectOffset, dontShow) {
browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visibleForMilliSeconds, visibleForPageUpdates, redirectOffset, dontShow) {
const id = tab.id || page.currentTabId;
if (!level) {
@ -103,15 +103,15 @@ browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visib
stackData.popup = popup;
}
if (visibleForMilliSeconds) {
if (visibleForMilliSeconds !== undefined) {
stackData.visibleForMilliSeconds = visibleForMilliSeconds;
}
if (visibleForPageUpdates) {
if (visibleForPageUpdates !== undefined) {
stackData.visibleForPageUpdates = visibleForPageUpdates;
}
if (redirectOffset) {
if (redirectOffset !== undefined) {
stackData.redirectOffset = redirectOffset;
}
@ -181,7 +181,7 @@ browserAction.removeRememberPopup = function(callback, tab, removeImmediately) {
return;
}
if( page.tabs[tab.id].stack.length == 0) {
if (page.tabs[tab.id].stack.length === 0) {
page.clearCredentials(tab.id);
return;
}
@ -205,21 +205,23 @@ browserAction.setRememberPopup = function(tabId, username, password, url, userna
browser.storage.local.get({'settings': {}}).then(function(item) {
const settings = item.settings;
// Don't show anything if the site is in the ignore list
for (const site in settings.ignoredSites) {
if (site === url) {
return;
// Don't show anything if the site is in the ignore
if (settings.sitePreferences !== undefined) {
for (const site of settings.sitePreferences) {
if (site.ignore === IGNORE_NORMAL && (site.url === url || siteMatch(site.url, url))) {
return;
}
}
}
const id = tabId || page.currentTabId;
let timeoutMinMillis = Number(getValueOrDefault(settings, 'blinkMinTimeout', BLINK_TIMEOUT_REDIRECT_THRESHOLD_TIME_DEFAULT, 0));
let timeoutMinMillis = Number(getValueOrDefault(settings, 'blinkMinTimeout', BLINK_TIMEOUT_REDIRECT_THRESHOLD_TIME_DEFAULT, -1));
if (timeoutMinMillis > 0) {
timeoutMinMillis += Date.now();
}
const blinkTimeout = getValueOrDefault(settings, 'blinkTimeout', BLINK_TIMEOUT_DEFAULT, 0);
const blinkTimeout = getValueOrDefault(settings, 'blinkTimeout', BLINK_TIMEOUT_DEFAULT, -1);
const pageUpdateAllowance = getValueOrDefault(settings, 'allowedRedirect', BLINK_TIMEOUT_REDIRECT_COUNT_DEFAULT, 0);
const stackData = {

View file

@ -159,6 +159,9 @@ kpxcEvent.onReconnect = function(callback, tab) {
// Add a small timeout after reconnecting. Just to make sure. It's not pretty, I know :(
setTimeout(() => {
keepass.reconnect(callback, tab).then((configured) => {
browser.tabs.sendMessage(tab.id, {
action: 'redetect_fields'
});
kpxcEvent.showStatus(configured, tab, callback);
});
}, 500);
@ -269,6 +272,14 @@ kpxcEvent.pageClearLogins = function(callback, tab, alreadyCalled) {
callback();
};
kpxcEvent.pageGetLoginId = function(callback, tab) {
callback(page.loginId);
};
kpxcEvent.pageSetLoginId = function(callback, tab, loginId) {
page.loginId = loginId;
};
// all methods named in this object have to be declared BEFORE this!
kpxcEvent.messageHandlers = {
'add_credentials': keepass.addCredentials,
@ -284,6 +295,8 @@ kpxcEvent.messageHandlers = {
'load_settings': kpxcEvent.onLoadSettings,
'lock-database': kpxcEvent.lockDatabase,
'page_clear_logins': kpxcEvent.pageClearLogins,
'page_get_login_id': kpxcEvent.pageGetLoginId,
'page_set_login_id': kpxcEvent.pageSetLoginId,
'pop_stack': kpxcEvent.onPopStack,
'popup_login': kpxcEvent.onLoginPopup,
'popup_multiple-fields': kpxcEvent.onMultipleFieldsPopup,

View file

@ -16,7 +16,6 @@ keepass.migrateKeyRing().then(() => {
// Milliseconds for intervall (e.g. to update browserAction)
const _interval = 250;
/**
* Generate information structure for created tab and invoke all needed
* functions if tab is created in foreground
@ -71,7 +70,13 @@ browser.tabs.onActivated.addListener((activeInfo) => {
* @param {object} changeInfo
*/
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// If the tab URL has changed (e.g. logged in) clear credentials
if (changeInfo.url) {
page.clearLogins(tabId);
}
if (changeInfo.status === 'complete') {
browserAction.showDefault(null, tab);
kpxcEvent.invoke(browserAction.removeRememberPopup, null, tabId, []);
}
});

View file

@ -185,6 +185,7 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password,
}
else if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode, response.error);
callback('error');
}
else {
browserAction.showDefault(null, tab);
@ -839,6 +840,17 @@ keepass.onNativeMessage = function(response) {
keepass.testAssociation((associationResponse) => {
keepass.isConfigured().then((configured) => {
keepass.updatePopup(configured ? 'normal' : 'cross');
// Send message to content script
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
if (tabs.length) {
browser.tabs.sendMessage(tabs[0].id, {
action: 'check_database_hash',
hash: {old: keepass.previousDatabaseHash, new: keepass.databaseHash}
});
keepass.previousDatabaseHash = keepass.databaseHash;
}
});
});
}, null);
}
@ -1041,7 +1053,9 @@ keepass.reconnect = function(callback, tab) {
};
keepass.updatePopup = function(iconType) {
const data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1];
data.iconType = iconType;
browserAction.show(null, {'id': page.currentTabId});
if (page && page.tabs.length > 0) {
const data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1];
data.iconType = iconType;
browserAction.show(null, {'id': page.currentTabId});
}
};

View file

@ -16,6 +16,7 @@ var page = {};
page.tabs = [];
page.currentTabId = -1;
page.blockedTabs = [];
page.loginId = -1;
page.initSettings = function() {
return new Promise((resolve, reject) => {
@ -104,6 +105,10 @@ page.clearCredentials = function(tabId, complete) {
};
page.clearLogins = function(tabId) {
if (!page.tabs[tabId]) {
return;
}
page.tabs[tabId].loginList = [];
};

View file

@ -1,5 +1,13 @@
'use strict';
const IGNORE_NOTHING = 'ignoreNothing';
const IGNORE_NORMAL = 'ignoreNormal';
const IGNORE_FULL = 'ignoreFull';
var schemeSegment = '(\\*|http|https|ws|wss|file|ftp)';
var hostSegment = '(\\*|(?:\\*\\.)?(?:[^/*]+))?';
var pathSegment = '(.*)';
var isFirefox = function() {
if (!(/Chrome/.test(navigator.userAgent) && /Google/.test(navigator.vendor))) {
return true;
@ -15,3 +23,71 @@ var showNotification = function(message) {
'message': message
});
};
/**
* Transforms a valid match pattern into a regular expression
* which matches all URLs included by that pattern.
*
* @param {string} pattern The pattern to transform.
* @return {RegExp} The pattern's equivalent as a RegExp.
* @throws {TypeError} If the pattern is not a valid MatchPattern
*
* https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns
*/
var matchPatternToRegExp = function(pattern) {
if (pattern === '') {
return (/^(?:http|https|file|ftp|app):\/\//);
}
const matchPatternRegExp = new RegExp(
`^${schemeSegment}://${hostSegment}/${pathSegment}$`
);
let match = matchPatternRegExp.exec(pattern);
if (!match) {
throw new TypeError(pattern + ' is not a valid MatchPattern');
}
let [, scheme, host, path] = match;
if (!host) {
throw new TypeError(pattern + ' does not have a valid host');
}
let regex = '^';
if (scheme === '*') {
regex += '(http|https)';
} else {
regex += scheme;
}
regex += '://';
if (host && host === '*') {
regex += '[^/]+?';
} else if (host) {
if (host.match(/^\*\./)) {
regex += '[^/]*?';
host = host.substring(2);
}
regex += host.replace(/\./g, '\\.');
}
if (path) {
if (path === '*') {
regex += '(/.*)?';
} else if (path.charAt(0) !== '/') {
regex += '/';
regex += path.replace(/\./g, '\\.').replace(/\*/g, '.*?');
regex += '/?';
}
}
regex += '$';
return new RegExp(regex);
};
var siteMatch = function(site, url) {
const rx = matchPatternToRegExp(site);
return url.match(rx);
};

View file

@ -6,6 +6,8 @@ _called.retrieveCredentials = false;
_called.clearLogins = false;
_called.manualFillRequested = 'none';
let _loginId = -1;
let _singleInputEnabledForPage = false;
const _maximumInputs = 100;
// Count of detected form fields on the page
var _detectedFields = 0;
@ -21,12 +23,16 @@ browser.runtime.onMessage.addListener(function(req, sender, callback) {
if (cip.u) {
cip.setValueWithChange(cip.u, cip.credentials[req.id].login);
combination = cipFields.getCombination('username', cip.u);
_loginId = req.id;
browser.runtime.sendMessage({
action: 'page_set_login_id', args: [req.id]
});
cip.u.focus();
}
if (cip.p) {
cip.setValueWithChange(cip.p, cip.credentials[req.id].password);
_loginId = req.id;
browser.runtime.sendMessage({
action: 'page_set_login_id', args: [req.id]
});
combination = cipFields.getCombination('password', cip.p);
}
@ -497,7 +503,7 @@ cipForm.init = function(form, credentialFields) {
// TODO: could be called multiple times --> update credentialFields
// not already initialized && password-field is not null
if (!form.data('cipForm-initialized') && (credentialFields.password || credentialFields.username)) {
if (!form.data('cipForm-initialized') && (credentialFields.password || (_singleInputEnabledForPage && credentialFields.username))) {
form.data('cipForm-initialized', true);
cipForm.setInputFields(form, credentialFields);
form.submit(cipForm.onSubmit);
@ -614,8 +620,8 @@ cipDefine.initDescription = function() {
.addClass('btn').addClass('btn-primary')
.css('margin-right', '15px')
.click(function(e) {
if (!cip.settings['defined-credential-fields']) {
cip.settings['defined-credential-fields'] = {};
if (!cip.settings['defined-custom-fields']) {
cip.settings['defined-custom-fields'] = {};
}
if (cipDefine.selection.username) {
@ -632,7 +638,8 @@ cipDefine.initDescription = function() {
fieldIds.push(cipFields.prepareId(i));
}
cip.settings['defined-credential-fields'][document.location.href] = {
const location = cip.getDocumentLocation();
cip.settings['defined-custom-fields'][location] = {
username: cipDefine.selection.username,
password: cipDefine.selection.password,
fields: fieldIds
@ -652,7 +659,8 @@ cipDefine.initDescription = function() {
$description.append($btnAgain);
$description.append($btnDismiss);
if (cip.settings['defined-credential-fields'] && cip.settings['defined-credential-fields'][document.location.href]) {
const location = cip.getDocumentLocation();
if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) {
const $p = jQuery('<p>').html('For this page credential fields are already selected and will be overwritten.<br />');
const $btnDiscard = jQuery('<button>')
.attr('id', 'btn-warning')
@ -662,7 +670,7 @@ cipDefine.initDescription = function() {
.addClass('btn-sm')
.addClass('btn-danger')
.click(function(e) {
delete cip.settings['defined-credential-fields'][document.location.href];
delete cip.settings['defined-custom-fields'][location];
browser.runtime.sendMessage({
action: 'save_settings',
@ -734,7 +742,7 @@ cipDefine.markFields = function ($chooser, $pattern) {
return true;
}
if (jQuery(this).is(':visible') && jQuery(this).css('visibility') !== 'hidden' && jQuery(this).css('visibility') !== 'collapsed') {
if (cipFields.isVisible(this)) {
const $field = jQuery('<div>').addClass('b2c-fixed-field')
.css('top', jQuery(this).offset().top)
.css('left', jQuery(this).offset().left)
@ -812,61 +820,117 @@ cipFields.prepareId = function(id) {
return id.replace(/[:#.,\[\]\(\)' "]/g, function(m) { return '\\'+m; });
};
// Check aria-hidden attribute by looping the parent elements of input field
cipFields.getAriaHidden = function(field) {
let $par = jQuery(field).parents();
for (const p of $par) {
const val = $(p).attr('aria-hidden');
if (val) {
return val;
/**
* Returns the first parent element satifying the {@code predicate} mapped by {@code resultFn} or else {@code defaultVal}.
* @param {HTMLElement} element The start element (excluded, starting with the parents)
* @param {function} predicate Matcher for the element to find, type (HTMLElement) => boolean
* @param {function} resultFn Callback function of type (HTMLElement) => {*} called for the first matching element
* @param {fun} defaultValFn Fallback return value supplier, if no element matching the predicate can be found
*/
cipFields.traverseParents = function(element, predicate, resultFn = () => true, defaultValFn = () => false) {
for (let f = element.parentElement; f !== null; f = f.parentElement) {
if (predicate(f)) {
return resultFn(f);
}
}
return 'false';
return defaultValFn();
};
cipFields.getAriaHidden = function(field) {
// Check the main element
const val = field.getAttribute('aria-hidden');
if (val) {
return val;
}
const ariaFunc = f => f.getAttribute('aria-hidden');
return cipFields.traverseParents(field, ariaFunc, ariaFunc, () => 'false');
};
cipFields.getOverflowHidden = function(field) {
let $par = jQuery(field).parents();
for (const p of $par) {
const val = $(p).css('overflow');
if (val === 'hidden') {
return cipFields.traverseParents(field, f => f.style.overflow === 'hidden');
};
// Checks if input field is a search field. Attributes or form action containing 'search', or parent element holding
// role="search" will be identified as a search field.
cipFields.isSearchField = function(target) {
const attributes = target.attributes;
// Check element attributes
for (const attr of attributes) {
if ((attr.value && (attr.value.toLowerCase().includes('search')) || attr.value === 'q')) {
return true;
}
}
// Check closest form
const closestForm = target.closest('form');
if (closestForm) {
// Check form action
const formAction = closestForm.getAttribute('action');
if (formAction && formAction.includes('search')) {
return true;
}
// Check form class and id
const closestFormId = closestForm.getAttribute('id');
const closestFormClass = closestForm.className;
if (closestFormClass && (closestForm.className.toLowerCase().includes('search') ||
(closestFormId && closestFormId.toLowerCase().includes('search')))) {
return true;
}
}
// Check parent elements for role="search"
const roleFunc = f => f.getAttribute('role');
const roleValue = cipFields.traverseParents(target, roleFunc, roleFunc, () => null);
if (roleValue && roleValue === 'search') {
return true;
}
return false;
};
cipFields.isVisible = function(field) {
const rect = field.getBoundingClientRect();
// Check CSS visibility
const fieldStyle = getComputedStyle(field);
if (fieldStyle.visibility && (fieldStyle.visibility === 'hidden' || fieldStyle.visibility === 'collapse')) {
return false;
}
// Check element position and size
if (rect.x < 0 || rect.y < 0 || rect.width < 16 || rect.height < 16) {
return false;
}
// Check aria-hidden property
if (cipFields.getAriaHidden(field) !== 'false') {
return false;
}
return true;
};
cipFields.getAllFields = function() {
let fields = [];
// get all input fields which are text, email or password and visible
jQuery(cipFields.inputQueryPattern).each(function() {
let ariaHidden = cipFields.getAriaHidden(this);
let overflowHidden = cipFields.getOverflowHidden(this);
if (jQuery(this).is(':visible') && jQuery(this).css('visibility') !== 'hidden' && jQuery(this).css('visibility') !== 'collapsed' && ariaHidden === 'false') {
cipFields.setUniqueId(jQuery(this));
fields.push(jQuery(this));
const inputs = cipObserverHelper.getInputs(document);
for (const i of inputs) {
if (cipFields.isVisible(i) && !cipFields.isSearchField(i)) {
cipFields.setUniqueId(jQuery(i));
fields.push(jQuery(i));
}
});
};
_detectedFields = fields.length;
return fields;
};
cipFields.getHiddenFieldCount = function() {
let count = 0;
jQuery(cipFields.inputQueryPattern).each(function() {
if (jQuery(this).is(':hidden')) {
count++;
}
});
return count;
};
cipFields.prepareVisibleFieldsWithID = function($pattern) {
jQuery($pattern).each(function() {
if (jQuery(this).is(':visible') && jQuery(this).css('visibility') !== 'hidden' && jQuery(this).css('visibility') !== 'collapsed') {
if (cipFields.isVisible(this) && !cipFields.isSearchField(this)) {
cipFields.setUniqueId(jQuery(this));
}
});
@ -897,6 +961,14 @@ cipFields.getAllCombinations = function(inputs) {
}
}
if (_singleInputEnabledForPage && fields.length === 0 && uField) {
const combination = {
username: uField[0].getAttribute('data-cip-id'),
password: null
};
fields.push(combination);
}
return fields;
};
@ -907,7 +979,8 @@ cipFields.getCombination = function(givenType, fieldId) {
}
}
// use defined credential fields (already loaded into combinations)
if (cip.settings['defined-credential-fields'] && cip.settings['defined-credential-fields'][document.location.href]) {
const location = cip.getDocumentLocation();
if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) {
return cipFields.combinations[0];
}
@ -1107,8 +1180,9 @@ cipFields.prepareCombinations = function(combinations) {
};
cipFields.useDefinedCredentialFields = function() {
if (cip.settings['defined-credential-fields'] && cip.settings['defined-credential-fields'][document.location.href]) {
const creds = cip.settings['defined-credential-fields'][document.location.href];
const location = cip.getDocumentLocation();
if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) {
const creds = cip.settings['defined-custom-fields'][location];
let $found = _f(creds.username) || _f(creds.password);
for (const i of creds.fields) {
@ -1134,6 +1208,116 @@ cipFields.useDefinedCredentialFields = function() {
return false;
};
var cipObserverHelper = {};
cipObserverHelper.inputTypes = [
'text',
'email',
'password',
'tel',
'number',
null // Input field can be without any type. Include these to the list.
];
// Ignores all nodes that doesn't contain elements
cipObserverHelper.ignoredNode = function(target) {
if (target.nodeType === Node.ATTRIBUTE_NODE ||
target.nodeType === Node.TEXT_NODE ||
target.nodeType === Node.CDATA_SECTION_NODE ||
target.nodeType === Node.PROCESSING_INSTRUCTION_NODE ||
target.nodeType === Node.COMMENT_NODE ||
target.nodeType === Node.DOCUMENT_TYPE_NODE ||
target.nodeType === Node.NOTATION_NODE) {
return true;
}
return false;
};
cipObserverHelper.getInputs = function(target) {
if (cipObserverHelper.ignoredNode(target)) {
return [];
}
const input = target.getElementsByTagName('input');
if (input.length === 0 || input.length > _maximumInputs) {
return [];
}
let inputs = [];
for (const i of input) {
if (cipObserverHelper.inputTypes.includes(i.getAttribute('type'))) {
inputs.push(i);
}
}
return inputs;
};
cipObserverHelper.getId = function(target) {
return target.classList.length === 0 ? target.id : target.classList;
};
cipObserverHelper.ignoredElement = function(target) {
// Ignore SVG elements
if (target.nodeName === 'svg' ||
target.nodeName === 'g' ||
(target.parentNode &&
(target.parentNode.nodeName === 'svg' || target.parentNode.nodeName === 'g'))) {
return true;
}
// Ignore KeePassXC-Browser classes
if (target.className && (target.className.includes('kpxc') || target.className.includes('ui-helper'))) {
return true;
}
return false;
};
cipObserverHelper.handleObserverAdd = function(target) {
if (cipObserverHelper.ignoredElement(target)) {
return;
}
const inputs = cipObserverHelper.getInputs(target);
if (inputs.length === 0) {
return;
}
const neededLength = _detectedFields === 1 ? 0 : 1;
const id = cipObserverHelper.getId(target);
if (inputs.length > neededLength && !_observerIds.includes(id)) {
// Save target element id for preventing multiple calls to initCredentialsFields()
_observerIds.push(id);
// Sometimes the settings haven't been loaded before new input fields are detected
if (Object.keys(cip.settings).length === 0) {
cip.init();
} else {
cip.initCredentialFields(true);
}
}
};
cipObserverHelper.handleObserverRemove = function(target) {
if (cipObserverHelper.ignoredElement(target)) {
return;
}
const inputs = cipObserverHelper.getInputs(target);
if (inputs.length === 0) {
return;
}
// Remove target element id from the list
const id = cipObserverHelper.getId(target);
if (_observerIds.includes(id)) {
const index = _observerIds.indexOf(id);
if (index >= 0) {
_observerIds.splice(index, 1);
}
}
};
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
// Detects DOM changes in the document
@ -1143,15 +1327,24 @@ let observer = new MutationObserver(function(mutations, observer) {
}
for (const mut of mutations) {
// Check if the added element has any inputs
const inputs = mut.target.querySelectorAll(cipFields.inputQueryPattern);
// Skip text nodes
if (mut.target.nodeType === Node.TEXT_NODE) {
continue;
}
// If only password field is shown it's enough to have one field visible for initCredentialFields
const neededLength = _detectedFields === 1 ? 0 : 1;
if (inputs.length > neededLength && !_observerIds.includes(mut.target.id)) {
// Save target element id for preventing multiple calls to initCredentialsFields()
_observerIds.push(mut.target.id);
cip.initCredentialFields(true);
// Handle attributes only if CSS display is modified
if (mut.type === 'attributes') {
const newValue = mut.target.getAttribute(mut.attributeName);
if (newValue && (newValue.includes('display') || newValue.includes('z-index'))) {
if (mut.target.style.display !== 'none') {
cipObserverHelper.handleObserverAdd(mut.target);
} else {
cipObserverHelper.handleObserverRemove(mut.target);
}
}
} else if (mut.type === 'childList') {
cipObserverHelper.handleObserverAdd((mut.addedNodes.length > 0) ? mut.addedNodes[0] : mut.target);
cipObserverHelper.handleObserverRemove((mut.removedNodes.length > 0) ? mut.removedNodes[0] : mut.target);
}
}
});
@ -1162,9 +1355,11 @@ observer.observe(document, {
subtree: true,
attributes: true,
childList: true,
characterData: true
characterData: true,
attributeFilter: ['style']
});
var cip = {};
cip.settings = {};
cip.u = null;
@ -1202,6 +1397,7 @@ cip.detectDatabaseChange = function(response) {
args: [ true ] // Set polling to true, this is an internal function call
});
} else if (response.new !== 'no-hash' && response.new !== response.old) {
_called.retrieveCredentials = false;
browser.runtime.sendMessage({
action: 'load_settings',
}).then((response) => {
@ -1227,6 +1423,21 @@ cip.initCredentialFields = function(forceCall) {
browser.runtime.sendMessage({ 'action': 'page_clear_logins', args: [_called.clearLogins] }).then(() => {
_called.clearLogins = true;
// Check site preferences
cip.initializeSitePreferences();
if (cip.settings.sitePreferences) {
for (const site of cip.settings.sitePreferences) {
if (site.url === document.location.href || siteMatch(site.url, document.location.href)) {
if (site.ignore === IGNORE_FULL) {
return;
}
_singleInputEnabledForPage = site.usernameOnly;
}
}
}
const inputs = cipFields.getAllFields();
if (inputs.length === 0) {
return;
@ -1252,18 +1463,19 @@ cip.initCredentialFields = function(forceCall) {
cip.submitUrl = cip.getFormActionUrl(cipFields.combinations[0]);
// Get submitUrl for a single input
if (!cip.submitUrl && cipFields.combinations.length === 1 && inputs.length === 1) {
if (_singleInputEnabledForPage && !cip.submitUrl && cipFields.combinations.length === 1 && inputs.length === 1) {
cip.submitUrl = cip.getFormActionUrlFromSingleInput(inputs[0]);
}
}
if (cip.settings.autoRetrieveCredentials && _called.retrieveCredentials === false && (cip.url && cip.submitUrl)) {
_called.retrieveCredentials = true;
browser.runtime.sendMessage({
action: 'retrieve_credentials',
args: [ cip.url, cip.submitUrl ]
}).then(cip.retrieveCredentialsCallback).catch((e) => {
console.log(e);
});
} else if (_singleInputEnabledForPage) {
cip.preparePageForMultipleCredentials(cip.credentials);
}
});
};
@ -1510,23 +1722,26 @@ cip.fillInFromActiveElementTOTPOnly = function(suppressWarnings) {
const el = document.activeElement;
cipFields.setUniqueId(jQuery(el));
const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id'));
const pos = _loginId;
if (pos >= 0 && cip.credentials[pos]) {
// Check the value from stringFields (to be removed)
const $sf = _fs(fieldId);
if (cip.credentials[pos].stringFields && cip.credentials[pos].stringFields.length > 0) {
const sFields = cip.credentials[pos].stringFields;
for (const s of sFields) {
const val = s["KPH: {TOTP}"];
if (val) {
cip.setValue($sf, val);
browser.runtime.sendMessage({
action: 'page_get_login_id'
}).then((pos) => {
if (pos >= 0 && cip.credentials[pos]) {
// Check the value from stringFields (to be removed)
const currentField = _fs(fieldId);
if (cip.credentials[pos].stringFields && cip.credentials[pos].stringFields.length > 0) {
const stringFields = cip.credentials[pos].stringFields;
for (const s of stringFields) {
const val = s["KPH: {TOTP}"];
if (val) {
cip.setValue(currentField, val);
}
}
} else if (cip.credentials[pos].totp && cip.credentials[pos].totp.length > 0) {
cip.setValue(currentField, cip.credentials[pos].totp);
}
} else if (cip.credentials[pos].totp && cip.credentials[pos].totp.length > 0) {
cip.setValue($sf, cip.credentials[pos].totp);
}
}
});
};
cip.setValue = function(field, value) {
@ -1546,22 +1761,22 @@ cip.setValue = function(field, value) {
};
cip.fillInStringFields = function(fields, stringFields, filledInFields) {
let $filledIn = false;
let filledIn = false;
filledInFields.list = [];
if (fields && stringFields && fields.length > 0 && stringFields.length > 0) {
for (let i = 0; i < fields.length; i++) {
const $sf = _fs(fields[i]);
const currentField = _fs(fields[i]);
const stringFieldValue = Object.values(stringFields[i]);
if ($sf && stringFieldValue[0]) {
cip.setValue($sf, stringFieldValue[0]);
if (currentField && stringFieldValue[0]) {
cip.setValue(currentField, stringFieldValue[0]);
filledInFields.list.push(fields[i]);
$filledIn = true;
filledIn = true;
}
}
}
return $filledIn;
return filledIn;
};
cip.setValueWithChange = function(field, value) {
@ -1596,14 +1811,18 @@ cip.fillIn = function(combination, onlyPassword, suppressWarnings) {
let filledIn = false;
if (uField && !onlyPassword) {
cip.setValueWithChange(uField, cip.credentials[0].login);
_loginId = 0;
browser.runtime.sendMessage({
action: 'page_set_login_id', args: [0]
});
filledIn = true;
}
if (pField) {
pField.attr('type', 'password');
cip.setValueWithChange(pField, cip.credentials[0].password);
pField.data('unchanged', true);
_loginId = 0;
browser.runtime.sendMessage({
action: 'page_set_login_id', args: [0]
});
filledIn = true;
}
@ -1628,14 +1847,18 @@ cip.fillIn = function(combination, onlyPassword, suppressWarnings) {
let filledIn = false;
if (uField) {
cip.setValueWithChange(uField, cip.credentials[combination.loginId].login);
_loginId = combination.loginId;
browser.runtime.sendMessage({
action: 'page_set_login_id', args: [combination.loginId]
});
filledIn = true;
}
if (pField) {
cip.setValueWithChange(pField, cip.credentials[combination.loginId].password);
pField.data('unchanged', true);
_loginId = combination.loginId;
browser.runtime.sendMessage({
action: 'page_set_login_id', args: [combination.loginId]
});
filledIn = true;
}
@ -1807,7 +2030,7 @@ cip.rememberCredentials = function(usernameValue, passwordValue) {
let url = jQuery(this)[0].action;
if (!url) {
url = cip.settings.saveDomainOnly ? document.location.origin : document.location.href;
url = cip.getDocumentLocation();
if (url.indexOf('?') > 0) {
url = url.substring(0, url.indexOf('?'));
if (url.length < document.location.origin.length) {
@ -1833,13 +2056,24 @@ cip.ignoreSite = function(sites) {
}
const site = sites[0];
if (!cip.settings['ignoredSites']) {
cip.settings['ignoredSites'] = {};
cip.initializeSitePreferences();
// Check if the site already exists
let siteExists = false;
for (const existingSite of cip.settings['sitePreferences']) {
if (existingSite.url === site) {
existingSite.ignore = IGNORE_NORMAL;
siteExists = true;
}
}
cip.settings['ignoredSites'][site] = {
url: site
};
if (!siteExists) {
cip.settings['sitePreferences'].push({
url: site,
ignore: IGNORE_NORMAL,
usernameOnly: false
});
}
browser.runtime.sendMessage({
action: 'save_settings',
@ -1847,6 +2081,20 @@ cip.ignoreSite = function(sites) {
});
};
// Delete previously created Object if it exists. It will be replaced by an Array
cip.initializeSitePreferences = function() {
if (cip.settings['sitePreferences'] !== undefined && cip.settings['sitePreferences'].constructor === Object) {
delete cip.settings['sitePreferences'];
}
if (!cip.settings['sitePreferences']) {
cip.settings['sitePreferences'] = [];
}
};
cip.getDocumentLocation = function() {
return cip.settings.saveDomainOnly ? document.location.origin : document.location.href;
};
var cipEvents = {};

View file

@ -1,8 +1,7 @@
{
"manifest_version": 2,
"name": "KeePassXC-Browser",
"version": "1.1.3",
"version_name": "1.1.3",
"version": "1.2.0",
"description": "KeePassXC integration for modern web browsers",
"author": "KeePassXC Team",
"icons": {

View file

@ -3,6 +3,10 @@ body {
padding-bottom: 60px;
}
table td {
width: 100%;
}
/* Custom container */
.container {
margin: 0 auto;
@ -111,6 +115,10 @@ h2+hr {
margin-right: 5px;
}
#manualUrl {
width: 75%;
}
tr.clone {
display: none;
}

View file

@ -19,15 +19,15 @@
<ul class="nav navbar-nav">
<li class="active"><a href="#general-settings">General</a></li>
<li><a href="#connected-databases">Connected Databases</a></li>
<li><a href="#specified-fields">Specified credential fields</a></li>
<li><a href="#ignored-sites">Ignored sites</a></li>
<li><a href="#custom-fields">Custom credential fields</a></li>
<li><a href="#site-preferences">Site preferences</a></li>
<li><a href="#about">About</a></li>
</ul>
</nav>
<!-- General Settings -->
<div class="tab" id="tab-general-settings">
<h2>General Settings</h2>
<h2>General settings</h2>
<hr />
<p>
If you just want to insert username + password into the fields where your focus is, press <code><span id="mac-user-shortcut">Ctrl + Shift + U</span><span id="default-user-shortcut">Alt + Shift + U</span></code>.
@ -40,14 +40,14 @@
<label for="blinkTimeout">Blink Time:</label>
<div class="control-group">
<div class="input-append">
<input type="number" id="blinkTimeout" placeholder="7500" value="7500" min="0"/>
<input type="number" id="blinkTimeout" placeholder="7500" value="7500" min="-1"/>
<button class="btn btn-sm btn-primary" id="blinkTimeoutButton" type="button"><span class="glyphicon glyphicon-floppy-disk"></span> Save</button>
</div>
</div>
<span class="help-inline">
What is the maximum time (ms) the icon should blink after detecting new credentials
<br />
Default: 7500
Default: 7500, Infinite: -1
</span>
</div>
</p>
@ -189,7 +189,7 @@
<!-- Connected Databases -->
<div class="tab" id="tab-connected-databases">
<h2>Connected Databases</h2>
<h2>Connected databases</h2>
<hr />
<p>
The following KeePassXC databases are connected to KeePassXC-Browser.
@ -241,16 +241,16 @@
</div>
</div>
<!-- Specified credential fields -->
<div class="tab" id="tab-specified-fields">
<h2>Specified credential fields</h2>
<!-- Custom credential fields -->
<div class="tab" id="tab-custom-fields">
<h2>Custom credential fields</h2>
<hr />
<p>
If KeePassXC-Browser detects the wrong credential fields, you are able to specify the correct fields by yourself.
<br />
Just go to the page and click on the KeePassXC-Browser-Icon, now select <em>Choose own credential fields for this page</em>.
Go to the page and click on the KeePassXC-Browser-Icon, now select <em>Choose custom credential fields for this page</em>.
<br />
On this page you can manage theses specified credential fields.
On this page you can manage saved custom credential fields.
</p>
<table class="table table-striped table-bordered table-hover">
<thead>
@ -261,7 +261,7 @@
</thead>
<tbody>
<tr class="empty">
<td colspan="2">No specified credential fields found.</td>
<td colspan="2">No saved custom credential fields found.</td>
</tr>
<tr class="clone">
<td></td>
@ -269,16 +269,16 @@
</tr>
</tbody>
</table>
<div id="dialogDeleteSpecifiedCredentialFields" class="modal fade" tabindex="-1" role="dialog">
<div id="dialogDeleteCustomCredentialFields" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Remove specified credential fields?</h3>
<h3 id="myModalLabel">Remove saved custom credential fields?</h3>
</div>
<div class="modal-body">
<p>Do you really want to remove the specified credential fields on the page <strong></strong>?</p>
<p class="help-block">KeePassXC-Browser will automatically detect the credential fields the next time you visit this page.</p>
<p>DDo you really want to remove the saved custom credential fields on the page: <strong></strong>?</p>
<p class="help-block">KeePassXC-Browser will automatically detect the credential fields next time you visit this page.</p>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
@ -289,33 +289,58 @@
</div>
</div>
<!-- Ignored sites -->
<div class="tab" id="tab-ignored-sites">
<h2>Ignored sites</h2>
<!-- Site preferences -->
<div class="tab" id="tab-site-preferences">
<h2>Site preferences</h2>
<hr />
<p>
Sites in this list are ignored when new credentials are detected.
Sites on this page have special handling methods associated with them.
<br />
Go to the page with new credentials, click the blinking KeePassXC-Browser icon or the notification and select <em>Never ask for this page</em>.
To ignore new/modified credentials on a specific site, add it below or click the blinking KeePassXC-Browser icon and select <em>Never ask for this page</em>.
<br />
If a site is fully ignored (<em>Disable all features</em> is selected), then the plugin will do nothing when visiting that site.
<br />
Enabling the <em>Username-Only Detection</em> feature allows KeePassXC-Browser to fill-in pages on sites that do present a separated username and password input.
</p>
<hr />
<div class="form-group">
<label for="sitePreferencesManualAdd">Add URL manually:</label>
<div class="control-group">
<div class="input-append">
<input type="url" id="manualUrl"/>
<button class="btn btn-sm btn-primary" id="sitePreferencesManualAdd" type="button"><span class="glyphicon glyphicon-plus-sign"></span> Add</button>
</div>
</div>
</div>
<hr />
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>Page URL</th>
<th>Ignore</th>
<th>Username-Only Detection</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr class="empty">
<td colspan="2">No ignored sites found.</td>
<td colspan="4">No sites found.</td>
</tr>
<tr class="clone">
<td></td>
<td>
<select name="ignore">
<option value="ignoreNothing">Enable all features</option>
<option value="ignoreNormal">Disable new/modified credentials</option>
<option value="ignoreFull">Disable all features</option>
</select>
</td>
<td><input type="checkbox" name="usernameOnly" value="false" /></td>
<td><button class="btn delete btn-danger btn"><span class="glyphicon glyphicon-remove-sign"></span> Remove</button></td>
</tr>
</tbody>
</table>
<div id="dialogDeleteIgnoredSite" class="modal fade" tabindex="-1" role="dialog">
<div id="dialogDeleteSite" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -323,8 +348,8 @@
<h3 id="myModalLabel">Remove site?</h3>
</div>
<div class="modal-body">
<p>Do you really want to remove the specified site from the ignore list?</p>
<p class="help-block">KeePassXC-Browser will detect new credentials the next time you visit this page.</p>
<p>Do you really want to remove the specified site from the list?</p>
<p class="help-block">KeePassXC-Browser will enable all features for this site and remove username-only detection.</p>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>

View file

@ -18,8 +18,8 @@ $(function() {
options.initMenu();
options.initGeneralSettings();
options.initConnectedDatabases();
options.initSpecifiedCredentialFields();
options.initIgnoredSites();
options.initCustomCredentialFields();
options.initSitePreferences();
options.initAbout();
});
});
@ -52,9 +52,9 @@ options.saveSettingsPromise = function() {
}
options.saveSetting = function(name) {
const $id = '#' + name;
$($id).closest('.control-group').removeClass('error').addClass('success');
setTimeout(() => { $($id).closest('.control-group').removeClass('success'); }, 2500);
const id = '#' + name;
$(id).closest('.control-group').removeClass('error').addClass('success');
setTimeout(() => { $(id).closest('.control-group').removeClass('success'); }, 2500);
browser.storage.local.set({'settings': options.settings});
browser.runtime.sendMessage({
@ -122,7 +122,7 @@ options.initGeneralSettings = function() {
const blinkTimeout = $.trim($('#blinkTimeout').val());
const blinkTimeoutval = blinkTimeout !== '' ? Number(blinkTimeout) : defaultSettings.blinkTimeout;
options.settings['blinkTimeout'] = String(blinkTimeoutval);
options.settings['blinkTimeout'] = blinkTimeoutval;
options.saveSetting('blinkTimeout');
});
@ -130,7 +130,7 @@ options.initGeneralSettings = function() {
const blinkMinTimeout = $.trim($('#blinkMinTimeout').val());
const blinkMinTimeoutval = blinkMinTimeout !== '' ? Number(blinkMinTimeout) : defaultSettings.redirectOffset;
options.settings['blinkMinTimeout'] = String(blinkMinTimeoutval);
options.settings['blinkMinTimeout'] = blinkMinTimeoutval;
options.saveSetting('blinkMinTimeout');
});
@ -138,7 +138,7 @@ options.initGeneralSettings = function() {
const allowedRedirect = $.trim($('#allowedRedirect').val());
const allowedRedirectval = allowedRedirect !== '' ? Number(allowedRedirect) : defaultSettings.redirectAllowance;
options.settings['allowedRedirect'] = String(allowedRedirectval);
options.settings['allowedRedirect'] = allowedRedirectval;
options.saveSetting('allowedRedirect');
});
};
@ -168,10 +168,10 @@ options.initConnectedDatabases = function() {
$('#dialogDeleteConnectedDatabase .modal-footer:first button.yes:first').click(function(e) {
$('#dialogDeleteConnectedDatabase').modal('hide');
const $hash = $('#dialogDeleteConnectedDatabase').data('hash');
$('#tab-connected-databases #tr-cd-' + $hash).remove();
const hash = $('#dialogDeleteConnectedDatabase').data('hash');
$('#tab-connected-databases #tr-cd-' + hash).remove();
delete options.keyRing[$hash];
delete options.keyRing[hash];
options.saveKeyRing();
if ($('#tab-connected-databases table tbody:first tr').length > 2) {
@ -183,22 +183,22 @@ options.initConnectedDatabases = function() {
$('#tab-connected-databases tr.clone:first .dropdown-menu:first').width('230px');
const $trClone = $('#tab-connected-databases table tr.clone:first').clone(true);
$trClone.removeClass('clone');
const trClone = $('#tab-connected-databases table tr.clone:first').clone(true);
trClone.removeClass('clone');
for (let hash in options.keyRing) {
const $tr = $trClone.clone(true);
$tr.data('hash', hash);
$tr.attr('id', 'tr-cd-' + hash);
const tr = trClone.clone(true);
tr.data('hash', hash);
tr.attr('id', 'tr-cd-' + hash);
$('a.dropdown-toggle:first img:first', $tr).attr('src', '/icons/19x19/icon_normal_19x19.png');
$('a.dropdown-toggle:first img:first', tr).attr('src', '/icons/19x19/icon_normal_19x19.png');
$tr.children('td:first').text(options.keyRing[hash].id);
$tr.children('td:eq(1)').text(options.keyRing[hash].key);
tr.children('td:first').text(options.keyRing[hash].id);
tr.children('td:eq(1)').text(options.keyRing[hash].key);
const lastUsed = (options.keyRing[hash].lastUsed) ? new Date(options.keyRing[hash].lastUsed).toLocaleString() : 'unknown';
$tr.children('td:eq(2)').text(lastUsed);
tr.children('td:eq(2)').text(lastUsed);
const date = (options.keyRing[hash].created) ? new Date(options.keyRing[hash].created).toLocaleDateString() : 'unknown';
$tr.children('td:eq(3)').text(date);
$('#tab-connected-databases table tbody:first').append($tr);
tr.children('td:eq(3)').text(date);
$('#tab-connected-databases table tbody:first').append(tr);
}
if ($('#tab-connected-databases table tbody:first tr').length > 2) {
@ -214,97 +214,163 @@ options.initConnectedDatabases = function() {
});
};
options.initSpecifiedCredentialFields = function() {
$('#dialogDeleteSpecifiedCredentialFields').modal({keyboard: true, show: false, backdrop: true});
$('#tab-specified-fields tr.clone:first button.delete:first').click(function(e) {
options.initCustomCredentialFields = function() {
$('#dialogDeleteCustomCredentialFields').modal({keyboard: true, show: false, backdrop: true});
$('#tab-custom-fields tr.clone:first button.delete:first').click(function(e) {
e.preventDefault();
$('#dialogDeleteSpecifiedCredentialFields').data('url', $(this).closest('tr').data('url'));
$('#dialogDeleteSpecifiedCredentialFields').data('tr-id', $(this).closest('tr').attr('id'));
$('#dialogDeleteSpecifiedCredentialFields .modal-body:first strong:first').text($(this).closest('tr').children('td:first').text());
$('#dialogDeleteSpecifiedCredentialFields').modal('show');
$('#dialogDeleteCustomCredentialFields').data('url', $(this).closest('tr').data('url'));
$('#dialogDeleteCustomCredentialFields').data('tr-id', $(this).closest('tr').attr('id'));
$('#dialogDeleteCustomCredentialFields .modal-body:first strong:first').text($(this).closest('tr').children('td:first').text());
$('#dialogDeleteCustomCredentialFields').modal('show');
});
$('#dialogDeleteSpecifiedCredentialFields .modal-footer:first button.yes:first').click(function(e) {
$('#dialogDeleteSpecifiedCredentialFields').modal('hide');
$('#dialogDeleteCustomCredentialFields .modal-footer:first button.yes:first').click(function(e) {
$('#dialogDeleteCustomCredentialFields').modal('hide');
const $url = $('#dialogDeleteSpecifiedCredentialFields').data('url');
const $trId = $('#dialogDeleteSpecifiedCredentialFields').data('tr-id');
$('#tab-specified-fields #' + $trId).remove();
const url = $('#dialogDeleteCustomCredentialFields').data('url');
const trId = $('#dialogDeleteCustomCredentialFields').data('tr-id');
$('#tab-custom-fields #' + trId).remove();
delete options.settings['defined-credential-fields'][$url];
delete options.settings['defined-custom-fields'][url];
options.saveSettings();
if ($('#tab-specified-fields table tbody:first tr').length > 2) {
$('#tab-specified-fields table tbody:first tr.empty:first').hide();
if ($('#tab-custom-fields table tbody:first tr').length > 2) {
$('#tab-custom-fields table tbody:first tr.empty:first').hide();
} else {
$('#tab-specified-fields table tbody:first tr.empty:first').show();
$('#tab-custom-fields table tbody:first tr.empty:first').show();
}
});
const $trClone = $('#tab-specified-fields table tr.clone:first').clone(true);
$trClone.removeClass('clone');
const trClone = $('#tab-custom-fields table tr.clone:first').clone(true);
trClone.removeClass('clone');
let counter = 1;
for (let url in options.settings['defined-credential-fields']) {
const $tr = $trClone.clone(true);
$tr.data('url', url);
$tr.attr('id', 'tr-scf' + counter);
for (let url in options.settings['defined-custom-fields']) {
const tr = trClone.clone(true);
tr.data('url', url);
tr.attr('id', 'tr-scf' + counter);
++counter;
$tr.children('td:first').text(url);
$('#tab-specified-fields table tbody:first').append($tr);
tr.children('td:first').text(url);
$('#tab-custom-fields table tbody:first').append(tr);
}
if ($('#tab-specified-fields table tbody:first tr').length > 2) {
$('#tab-specified-fields table tbody:first tr.empty:first').hide();
if ($('#tab-custom-fields table tbody:first tr').length > 2) {
$('#tab-custom-fields table tbody:first tr.empty:first').hide();
} else {
$('#tab-specified-fields table tbody:first tr.empty:first').show();
$('#tab-custom-fields table tbody:first tr.empty:first').show();
}
};
options.initIgnoredSites = function() {
$('#dialogDeleteIgnoredSite').modal({keyboard: true, show: false, backdrop: true});
$('#tab-ignored-sites tr.clone:first button.delete:first').click(function(e) {
options.initSitePreferences = function() {
$('#dialogDeleteSite').modal({keyboard: true, show: false, backdrop: true});
$('#tab-site-preferences tr.clone:first button.delete:first').click(function(e) {
e.preventDefault();
$('#dialogDeleteIgnoredSite').data('url', $(this).closest('tr').data('url'));
$('#dialogDeleteIgnoredSite').data('tr-id', $(this).closest('tr').attr('id'));
$('#dialogDeleteIgnoredSite .modal-body:first strong:first').text($(this).closest('tr').children('td:first').text());
$('#dialogDeleteIgnoredSite').modal('show');
$('#dialogDeleteSite').data('url', $(this).closest('tr').data('url'));
$('#dialogDeleteSite').data('tr-id', $(this).closest('tr').attr('id'));
$('#dialogDeleteSite .modal-body:first strong:first').text($(this).closest('tr').children('td:first').text());
$('#dialogDeleteSite').modal('show');
});
$('#dialogDeleteIgnoredSite .modal-footer:first button.yes:first').click(function(e) {
$('#dialogDeleteIgnoredSite').modal('hide');
const $url = $('#dialogDeleteIgnoredSite').data('url');
const $trId = $('#dialogDeleteIgnoredSite').data('tr-id');
$('#tab-ignored-sites #' + $trId).remove();
delete options.settings['ignoredSites'][$url];
$('#tab-site-preferences tr.clone:first input[type=checkbox]:first').change(function() {
const url = $(this).closest('tr').data('url');
for (let site of options.settings['sitePreferences']) {
if (site.url === url) {
site.usernameOnly = $(this).is(':checked');
}
}
options.saveSettings();
});
if ($('#tab-ignored-sites table tbody:first tr').length > 2) {
$('#tab-ignored-sites table tbody:first tr.empty:first').hide();
} else {
$('#tab-ignored-sites table tbody:first tr.empty:first').show();
$('#tab-site-preferences tr.clone:first select:first').change(function() {
const url = $(this).closest('tr').data('url');
for (let site of options.settings['sitePreferences']) {
if (site.url === url) {
site.ignore = $(this).val();
}
}
options.saveSettings();
});
$("#manualUrl").keyup(function(event) {
if (event.keyCode === 13) {
$("#sitePreferencesManualAdd").click();
}
});
const $trClone = $('#tab-ignored-sites table tr.clone:first').clone(true);
$trClone.removeClass('clone');
$('#sitePreferencesManualAdd').click(function(e) {
e.preventDefault();
let value = $('#manualUrl').val();
if (value.length > 10 && value.length <= 2000) {
if (options.settings['sitePreferences'] === undefined) {
options.settings['sitePreferences'] = [];
}
const newValue = options.settings['sitePreferences'].length + 1;
const trClone = $('#tab-site-preferences table tr.clone:first').clone(true);
trClone.removeClass('clone');
// Fills the last / char if needed. This ensures the compatibility with Match Patterns
if (options.slashNeededForUrl(value)) {
value += '/';
}
const tr = trClone.clone(true);
tr.data('url', value);
tr.attr('id', 'tr-scf' + newValue);
tr.children('td:first').text(value);
tr.children('td:nth-child(2)').children('select').val(IGNORE_NORMAL);
$('#tab-site-preferences table tbody:first').append(tr);
$('#tab-site-preferences table tbody:first tr.empty:first').hide();
options.settings['sitePreferences'].push({url: value, ignore: IGNORE_NORMAL, usernameOnly: false});
options.saveSettings();
$('#manualUrl').val('');
}
});
$('#dialogDeleteSite .modal-footer:first button.yes:first').click(function(e) {
$('#dialogDeleteSite').modal('hide');
const url = $('#dialogDeleteSite').data('url');
const trId = $('#dialogDeleteSite').data('tr-id');
$('#tab-site-preferences #' + trId).remove();
for (let i = 0; i < options.settings['sitePreferences'].length; ++i) {
if (options.settings['sitePreferences'][i].url === url) {
options.settings['sitePreferences'].splice(i, 1);
}
}
options.saveSettings();
if ($('#tab-site-preferences table tbody:first tr').length > 2) {
$('#tab-site-preferences table tbody:first tr.empty:first').hide();
} else {
$('#tab-site-preferences table tbody:first tr.empty:first').show();
}
});
const trClone = $('#tab-site-preferences table tr.clone:first').clone(true);
trClone.removeClass('clone');
let counter = 1;
for (let url in options.settings['ignoredSites']) {
const $tr = $trClone.clone(true);
$tr.data('url', url);
$tr.attr('id', 'tr-scf' + counter);
++counter;
if (options.settings['sitePreferences']){
for (let site of options.settings['sitePreferences']) {
const tr = trClone.clone(true);
tr.data('url', site.url);
tr.attr('id', 'tr-scf' + counter);
++counter;
$tr.children('td:first').text(url);
$('#tab-ignored-sites table tbody:first').append($tr);
tr.children('td:first').text(site.url);
tr.children('td:nth-child(2)').children('select').val(site.ignore);
tr.children('td:nth-child(3)').children('input[type=checkbox]').attr('checked', site.usernameOnly);
$('#tab-site-preferences table tbody:first').append(tr);
}
}
if ($('#tab-ignored-sites table tbody:first tr').length > 2) {
$('#tab-ignored-sites table tbody:first tr.empty:first').hide();
if ($('#tab-site-preferences table tbody:first tr').length > 2) {
$('#tab-site-preferences table tbody:first tr.empty:first').hide();
} else {
$('#tab-ignored-sites table tbody:first tr.empty:first').show();
$('#tab-site-preferences table tbody:first tr.empty:first').show();
}
};
@ -326,3 +392,9 @@ options.initAbout = function() {
$('#default-pass-shortcut').show();
}
};
// Checks if URL has only scheme and host without the last / char.
options.slashNeededForUrl = function(pattern) {
const matchPattern = new RegExp(`^${schemeSegment}://${hostSegment}$`);
return matchPattern.exec(pattern);
};

View file

@ -1,12 +1,15 @@
body {
font-family: sans-serif;
min-width: 440px;
max-width: 440px;
overflow-x: hidden;
background-color: #eee;
font-size: 15px;
padding: 8px;
}
.container {
min-width: 440px;
max-width: 460px;
width: auto;
}
.list-group {
font-size: .9em !important;
}

View file

@ -15,7 +15,7 @@
<div class="container">
<div id="settings" class="settings">
<button id="btn-options" class="btn btn-sm btn-success"><span class="glyphicon glyphicon-cog"></span> Settings</button>
<button id="btn-choose-credential-fields" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-list-alt"></span> Choose own credential fields for this page</button>
<button id="btn-choose-credential-fields" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-list-alt"></span> Choose custom credential fields for this page</button>
<button id="lock-database-button" class="btn btn-danger" title="Lock database"><span class="glyphicon glyphicon-lock"></span></button>
<div id="update-available" class="alert alert-danger">

View file

@ -15,7 +15,7 @@
<div class="container">
<div id="settings" class="settings">
<button id="btn-options" class="btn btn-sm btn-success"><span class="glyphicon glyphicon-cog"></span> Settings</button>
<button id="btn-choose-credential-fields" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-list-alt"></span> Choose own credential fields for this page</button>
<button id="btn-choose-credential-fields" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-list-alt"></span> Choose custom credential fields for this page</button>
<button id="lock-database-button" class="btn btn-danger" title="Lock database"><span class="glyphicon glyphicon-lock"></span></button>
<div id="update-available" class="alert alert-danger">

View file

@ -15,7 +15,7 @@
<div class="container">
<div id="settings" class="settings">
<button id="btn-options" class="btn btn-sm btn-success"><span class="glyphicon glyphicon-cog"></span> Settings</button>
<button id="btn-choose-credential-fields" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-list-alt"></span> Choose own credential fields for this page</button>
<button id="btn-choose-credential-fields" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-list-alt"></span> Choose custom credential fields for this page</button>
<button id="lock-database-button" class="btn btn-danger" title="Lock database"><span class="glyphicon glyphicon-lock"></span></button>
<div id="update-available" class="alert alert-danger">

View file

@ -19,7 +19,7 @@ $(function() {
const id = e.target.id;
browser.tabs.sendMessage(tab.id, {
action: 'fill_user_pass_with_specific_login',
id: id
id: Number(id)
});
close();
});

View file

@ -14,7 +14,7 @@
<div class="container">
<div id="settings" class="settings">
<button id="btn-options" class="btn btn-sm btn-success"><span class="glyphicon glyphicon-cog"></span> Settings</button>
<button id="btn-choose-credential-fields" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-list-alt"></span> Choose own credential fields for this page</button>
<button id="btn-choose-credential-fields" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-list-alt"></span> Choose custom credential fields for this page</button>
<button id="lock-database-button" class="btn btn-danger" title="Lock database"><span class="glyphicon glyphicon-lock"></span></button>
<div id="update-available" class="alert alert-danger">

View file

@ -108,9 +108,10 @@ function _connected_database(db) {
}
function _verifyResult(code) {
if (code === 'success') {
_close();
if (code === 'error') {
showNotification('Error: Credentials cannot be saved or updated.');
}
_close();
}
function _close() {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 583 B