diff --git a/keepassxc-browser/background/browserAction.js b/keepassxc-browser/background/browserAction.js index e1f1831..1a28354 100755 --- a/keepassxc-browser/background/browserAction.js +++ b/keepassxc-browser/background/browserAction.js @@ -11,7 +11,7 @@ browserAction.show = async function(tab, popupData) { path: await browserAction.generateIconName(popupData.iconType) }); - if (popupData.popup) { + if (popupData.popup && tab?.id) { browserActionWrapper.setPopup({ tabId: tab.id, popup: `popups/${popupData.popup}.html` @@ -19,9 +19,9 @@ browserAction.show = async function(tab, popupData) { let badgeText = ''; if (popupData.popup === 'popup_login') { - badgeText = String(page.tabs[tab.id]?.loginList?.length); + badgeText = page.tabs[tab.id]?.loginList?.length; } else if (popupData.popup === 'popup_httpauth') { - badgeText = String(page.tabs[tab.id]?.loginList?.logins?.length); + badgeText = page.tabs[tab.id]?.loginList?.logins?.length; } browserAction.setBadgeText(tab?.id, badgeText); @@ -52,33 +52,22 @@ browserAction.showDefault = async function(tab) { return; } - if (page.tabs[tab.id]?.loginList.length > 0) { + if (page?.tabs[tab.id]?.loginList.length > 0) { popupData.iconType = 'normal'; popupData.popup = 'popup_login'; - browserAction.setBadgeText(tab?.id, String(page.tabs[tab.id]?.loginList.length)); + browserAction.setBadgeText(tab?.id, page.tabs[tab.id]?.loginList.length); } await browserAction.show(tab, popupData); }; -browserAction.updateIcon = async function(tab, iconType) { - if (!tab) { - const tabs = await browser.tabs.query({ 'active': true, 'currentWindow': true }); - if (tabs.length === 0) { - return; - } - - tab = tabs[0]; +browserAction.setBadgeText = function(tabId, badgeText) { + if (!tabId) { + return; } - browserActionWrapper.setIcon({ - path: browserAction.generateIconName(iconType) - }); -}; - -browserAction.setBadgeText = function(tabId, badgeText) { browserActionWrapper.setBadgeBackgroundColor({ color: '#666666' }); - browserActionWrapper.setBadgeText({ text: badgeText, tabId: tabId }); + browserActionWrapper.setBadgeText({ text: String(badgeText), tabId: tabId }); }; browserAction.generateIconName = async function(iconType) { @@ -87,7 +76,7 @@ browserAction.generateIconName = async function(iconType) { name += (!iconType || iconType === 'normal') ? 'normal' : iconType; let style = 'colored'; - if (page.settings.useMonochromeToolbarIcon) { + if (page?.settings?.useMonochromeToolbarIcon) { if (page.settings.colorTheme === 'system') { style = await retrieveColorScheme(); } else { @@ -103,8 +92,10 @@ browserAction.ignoreSite = async function(url) { const tab = await getCurrentTab(); // Send the message to the current tab's content script - browser.tabs.sendMessage(tab.id, { - action: 'ignore_site', - args: [ url ] - }); + if (tab?.id) { + browser.tabs.sendMessage(tab.id, { + action: 'ignore_site', + args: [ url ] + }); + } }; diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index e5cb5dd..a6d3055 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -4,7 +4,7 @@ const kpxcEvent = {}; kpxcEvent.onMessage = async function(request, sender) { if (request.action in kpxcEvent.messageHandlers) { - if (!Object.hasOwn(sender, 'tab') || sender.tab.id < 1) { + if (!Object.hasOwn(sender, 'tab') || sender?.tab?.id < 1) { sender.tab = {}; sender.tab.id = page.currentTabId; } @@ -158,8 +158,10 @@ kpxcEvent.onLoginPopup = async function(tab, logins) { popup: 'popup_login' }; - page.tabs[tab.id].loginList = logins; - await browserAction.show(tab, popupData); + if (tab?.id) { + page.tabs[tab.id].loginList = logins; + await browserAction.show(tab, popupData); + } }; kpxcEvent.initHttpAuth = async function() { @@ -177,11 +179,15 @@ kpxcEvent.onHTTPAuthPopup = async function(tab, data) { }; kpxcEvent.onUsernameFieldDetected = async function(tab, detected) { - page.tabs[tab.id].usernameFieldDetected = detected; + if (tab?.id) { + page.tabs[tab.id].usernameFieldDetected = detected; + } }; kpxcEvent.onIframeDetected = async function(tab, detected) { - page.tabs[tab.id].iframeDetected = detected; + if (tab?.id) { + page.tabs[tab.id].iframeDetected = detected; + } }; kpxcEvent.passwordGetFilled = async function() { @@ -201,7 +207,7 @@ kpxcEvent.pageGetRedirectCount = async function() { }; kpxcEvent.pageClearLogins = async function(tab, alreadyCalled) { - if (!alreadyCalled) { + if (!alreadyCalled && tab?.id) { page.clearLogins(tab.id); } }; @@ -230,7 +236,9 @@ kpxcEvent.hideTroubleshootingGuideAlert = async function(tab) { // Bounce message back to all frames kpxcEvent.sendBackToTabs = async function(tab, args = []) { - await browser.tabs.sendMessage(tab.id, { action: 'frame_message', args: args }); + if (tab?.id) { + await browser.tabs.sendMessage(tab.id, { action: 'frame_message', args: args }); + } }; // All methods named in this object have to be declared BEFORE this! diff --git a/keepassxc-browser/background/init.js b/keepassxc-browser/background/init.js index add144c..73b2550 100644 --- a/keepassxc-browser/background/init.js +++ b/keepassxc-browser/background/init.js @@ -1,113 +1,5 @@ 'use strict'; -(async () => { - try { - await keepass.migrateKeyRing(); - await page.initSettings(); - await page.initSitePreferences(); - await page.initOpenedTabs(); - await httpAuth.init(); - await keepass.reconnect(null, 5000); // 5 second timeout for the first connect - await keepass.enableAutomaticReconnect(); - await keepass.updateDatabase(); - } catch (e) { - logError('init.js failed'); - } -})(); - -/** - * Generate information structure for created tab and invoke all needed - * functions if tab is created in foreground - * @param {object} tab - */ -browser.tabs.onCreated.addListener((tab) => { - if (tab?.id > 0 && tab?.selected) { - page.currentTabId = tab.id; - - if (!page.tabs[tab.id]) { - page.createTabEntry(tab.id); - } - - page.switchTab(tab); - } -}); - -/** - * Remove information structure of closed tab for freeing memory - * @param {integer} tabId - * @param {object} removeInfo - */ -browser.tabs.onRemoved.addListener(async function(tabId, removeInfo) { - if (page.currentTabId === tabId) { - const currentTab = await getCurrentTab(); - page.currentTabId = currentTab ? currentTab.id : -1; - } - delete page.tabs[tabId]; -}); - -/** - * Remove stored credentials on switching tabs. - * Invoke functions to retrieve credentials for focused tab - * @param {object} activeInfo - */ -browser.tabs.onActivated.addListener(async function(activeInfo) { - try { - const info = await browser.tabs.get(activeInfo.tabId); - if (info && info.id) { - page.currentTabId = info.id; - if (info.status === 'complete') { - if (!page.tabs[info.id]) { - page.createTabEntry(info.id); - } - page.switchTab(info); - } - } - } catch (err) { - logError(err.message); - } -}); - -/** - * Update browserAction on every update of the page - * @param {integer} tabId - * @param {object} changeInfo - * @param {object} tab - */ -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(tab); - if (!page.tabs[tab.id]) { - page.createTabEntry(tab.id); - } - } -}); - -/** - * Detects page redirects and increases the count. Count is reset after a normal navigation event. - * Form submit is counted as one. - * @param {object} details - */ -browser.webNavigation.onCommitted.addListener((details) => { - if (details.transitionQualifiers?.[0] === 'client_redirect' || details.transitionType === 'form_submit') { - page.redirectCount += 1; - return; - } - - // Clear credentials on reload so a new retrieval can be made - if (details.transitionType === 'reload') { - page.clearLogins(details.tabId); - } - - page.redirectCount = 0; -}); - -browser.runtime.onMessage.addListener(kpxcEvent.onMessage); - const contextMenuItems = [ { title: tr('contextMenuFillUsernameAndPassword'), action: 'fill_username_password' }, { title: tr('contextMenuFillPassword'), action: 'fill_password' }, @@ -119,53 +11,176 @@ const contextMenuItems = [ ]; const menuContexts = [ 'editable' ]; - + if (isFirefox()) { menuContexts.push('password'); } -// Create context menu items -for (const item of contextMenuItems) { - browser.contextMenus.create({ - title: item.title, - contexts: menuContexts, - visible: item.visible, - id: item.id || item.action - }); -} +const initListeners = async function() { + /** + * Generate information structure for created tab and invoke all needed + * functions if tab is created in foreground + * @param {object} tab + */ + browser.tabs.onCreated.addListener((tab) => { + if (tab?.id > 0 && tab?.selected) { + page.currentTabId = tab.id; -// Listen for keyboard shortcuts specified by user -browser.commands.onCommand.addListener(async (command) => { - if (contextMenuItems.some(e => e.action === command) - || command === 'redetect_fields' - || command === 'choose_credential_fields' - || command === 'retrive_credentials_forced' - || command === 'reload_extension') { - const tab = await getCurrentTab(); - if (tab) { - browser.tabs.sendMessage(tab.id, { action: command }); + if (!page.tabs[tab.id]) { + page.createTabEntry(tab.id); + } + + page.switchTab(tab); } - } -}); + }); -browser.contextMenus.onClicked.addListener(async (item, tab) => { - if (item?.menuItemId?.startsWith('fill_attribute')) { - const menuItem = page.attributeMenuItems.find(i => i?.action === item?.menuItemId); - if (menuItem) { - browser.tabs.sendMessage(tab.id, { - action: 'fill_attribute', - args: menuItem?.args - }).catch((err) => { - logError(err); + /** + * Remove information structure of closed tab for freeing memory + * @param {integer} tabId + * @param {object} removeInfo + */ + browser.tabs.onRemoved.addListener(async function(tabId, removeInfo) { + if (page.currentTabId === tabId) { + const currentTab = await getCurrentTab(); + page.currentTabId = currentTab ? currentTab.id : -1; + } + delete page.tabs[tabId]; + }); + + /** + * Remove stored credentials on switching tabs. + * Invoke functions to retrieve credentials for focused tab + * @param {object} activeInfo + */ + browser.tabs.onActivated.addListener(async function(activeInfo) { + try { + const info = await browser.tabs.get(activeInfo.tabId); + if (info && info.id) { + page.currentTabId = info.id; + if (info.status === 'complete') { + if (!page.tabs[info.id]) { + page.createTabEntry(info.id); + } + page.switchTab(info); + } + } + } catch (err) { + logError(err.message); + } + }); + + /** + * Update browserAction on every update of the page + * @param {integer} tabId + * @param {object} changeInfo + * @param {object} tab + */ + 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' && tab?.id) { + browserAction.showDefault(tab); + if (!page.tabs[tab.id]) { + page.createTabEntry(tab.id); + } + } + }); + + /** + * Detects page redirects and increases the count. Count is reset after a normal navigation event. + * Form submit is counted as one. + * @param {object} details + */ + browser.webNavigation.onCommitted.addListener((details) => { + if (details.transitionQualifiers?.[0] === 'client_redirect' || details.transitionType === 'form_submit') { + page.redirectCount += 1; + return; + } + + // Clear credentials on reload so a new retrieval can be made + if (details.transitionType === 'reload') { + page.clearLogins(details.tabId); + } + + page.redirectCount = 0; + }); + + browser.runtime.onMessage.addListener(kpxcEvent.onMessage); + + // Listen for keyboard shortcuts specified by user + browser.commands.onCommand.addListener(async (command) => { + if (contextMenuItems.some(e => e.action === command) + || command === 'redetect_fields' + || command === 'choose_credential_fields' + || command === 'retrive_credentials_forced' + || command === 'reload_extension') { + const tab = await getCurrentTab(); + if (tab?.id) { + browser.tabs.sendMessage(tab.id, { action: command }); + } + } + }); + + browser.contextMenus.onClicked.addListener(async (item, tab) => { + if (!tab?.id) { + return; + } + + if (item?.menuItemId?.startsWith('fill_attribute')) { + const menuItem = page.attributeMenuItems.find(i => i?.action === item?.menuItemId); + if (menuItem) { + browser.tabs.sendMessage(tab.id, { + action: 'fill_attribute', + args: menuItem?.args + }).catch((err) => { + logError(err); + }); + } + + return; + } + + browser.tabs.sendMessage(tab.id, { + action: item.menuItemId + }).catch((err) => { + logError(err); + }); + }); +}; + +const initContextMenuItems = async function() { + // Create context menu items + await browser.contextMenus.removeAll(); + for (const item of contextMenuItems) { + try { + await browser.contextMenus.create({ + title: item.title, + contexts: menuContexts, + visible: item.visible, + id: item.id || item.action }); - } - - return; + } catch (e) { + logError(e); + } } +}; - browser.tabs.sendMessage(tab.id, { - action: item.menuItemId - }).catch((err) => { - logError(err); - }); -}); +(async () => { + try { + await keepass.migrateKeyRing(); + await page.initSettings(); + await page.initSitePreferences(); + await page.initOpenedTabs(); + await initListeners(); + await initContextMenuItems(); + await httpAuth.init(); + await keepass.reconnect(null, 5000); // 5 second timeout for the first connect + await keepass.enableAutomaticReconnect(); + await keepass.updateDatabase(); + } catch (e) { + logError('init.js failed'); + } +})(); diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index e4bf979..ae6ec86 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -929,7 +929,7 @@ keepass.updateDatabase = async function() { keepass.updateDatabaseHashToContent = async function() { try { const tab = await getCurrentTab(); - if (tab) { + if (tab?.id) { // Send message to content script browser.tabs.sendMessage(tab.id, { action: 'check_database_hash', diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index 4d16bcb..5a9f8a0 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -65,14 +65,25 @@ page.initSettings = async function() { const item = await browser.storage.local.get({ 'settings': {} }); // Load managed settings if found - try { - const managedSettings = await browser.storage.managed.get('settings'); - if (managedSettings?.settings) { - console.log('Managed settings found.'); - item.settings = managedSettings.settings; + if (isFirefox()) { + try { + const managedSettings = await browser.storage.managed.get('settings'); + if (managedSettings?.settings) { + debugLogMessage('Managed settings found.'); + item.settings = managedSettings.settings; + } + } catch (err) { + logError('page.initSettings error: ' + err); } - } catch (err) { - logError('page.initSettings error: ' + err); + } else { + chrome.storage.managed.get('settings').then((managedSettings) => { + if (managedSettings?.settings) { + debugLogMessage('Managed settings found.'); + item.settings = managedSettings.settings; + } + }).catch((err) => { + logError('page.initSettings error: ' + err); + }); } page.settings = item.settings; @@ -106,7 +117,7 @@ page.initOpenedTabs = async function() { return; } - page.currentTabId = currentTab.id; + page.currentTabId = currentTab?.id; browserAction.showDefault(currentTab); } catch (err) { logError('page.initOpenedTabs error: ' + err); @@ -128,25 +139,27 @@ page.initSitePreferences = async function() { page.switchTab = async function(tab) { // Clears Fill Attribute selection from context menu - browser.contextMenus.update('fill_attribute', { visible: false }); + page.setFillAttributeContextMenuItemVisible(false); // Clears all logins from other tabs after a timeout - if (page.clearCredentialsTimeout) { + if (page?.clearCredentialsTimeout) { clearTimeout(page.clearCredentialsTimeout); } page.clearCredentialsTimeout = setTimeout(() => { for (const pageTabId of Object.keys(page.tabs)) { - if (tab.id !== Number(pageTabId)) { + if (tab?.id !== Number(pageTabId)) { page.clearCredentials(Number(pageTabId), true); } } }, page.settings.clearCredentialsTimeout * 1000); browserAction.showDefault(tab); - browser.tabs.sendMessage(tab.id, { action: 'activated_tab' }).catch((e) => { - logError('Cannot send activated_tab message: ' + e.message); - }); + if (tab?.id) { + browser.tabs.sendMessage(tab.id, { action: 'activated_tab' }).catch((e) => { + logError('Cannot send activated_tab message: ' + e.message); + }); + } }; page.clearCredentials = async function(tabId, complete) { @@ -166,7 +179,7 @@ page.clearCredentials = async function(tabId, complete) { } }; -page.clearLogins = function(tabId) { +page.clearLogins = async function(tabId) { if (!page.tabs[tabId]) { return; } @@ -176,8 +189,7 @@ page.clearLogins = function(tabId) { page.tabs[tabId].loginList = []; page.currentRequest = {}; page.passwordFilled = false; - - browser.contextMenus.update('fill_attribute', { visible: false }); + page.setFillAttributeContextMenuItemVisible(false); }; // Clear all logins from all pages and update the content scripts @@ -201,7 +213,7 @@ page.clearSubmittedCredentials = async function() { page.submittedCredentials = {}; }; -page.createTabEntry = function(tabId) { +page.createTabEntry = async function(tabId) { page.tabs[tabId] = { allowIframes: false, credentials: [], @@ -211,14 +223,14 @@ page.createTabEntry = function(tabId) { }; page.clearSubmittedCredentials(); - browser.contextMenus.update('fill_attribute', { visible: false }); + page.setFillAttributeContextMenuItemVisible(false); }; // Retrieves the credentials. Returns cached values when found. // Page reload or tab switch clears the cache. // If the retrieval is forced (from Credential Banner), get new credentials normally. page.retrieveCredentials = async function(tab, args = []) { - if (!tab?.active) { + if (!tab?.active || !tab?.id) { return []; } @@ -256,7 +268,9 @@ page.getLoginId = async function(tab) { }; page.setLoginId = async function(tab, loginId) { - page.tabs[tab.id].loginId = loginId; + if (tab?.id) { + page.tabs[tab.id].loginId = loginId; + } }; page.getManualFill = async function(tab) { @@ -278,7 +292,7 @@ page.setBannerPosition = async function(tab, position) { page.getSubmitted = async function(tab) { // Do not return any credentials if the tab ID does not match. - if (tab.id !== page.submittedCredentials.tabId) { + if (tab?.id !== page.submittedCredentials.tabId) { return {}; } @@ -336,6 +350,15 @@ page.isSiteIgnored = async function(tab, currentLocation) { return false; }; +// Shows or hides the Fill Attribute context menu item +page.setFillAttributeContextMenuItemVisible = async function(visible) { + try { + await browser.contextMenus.update('fill_attribute', { visible: visible }); + } catch (e) { + logError(e); + } +}; + // Update context menu for attribute filling page.updateContextMenu = async function(tab, credentials) { // Remove any old attribute items @@ -345,7 +368,7 @@ page.updateContextMenu = async function(tab, credentials) { page.attributeMenuItems = []; // Set parent item visibility - browser.contextMenus.update('fill_attribute', { visible: true }); + page.setFillAttributeContextMenuItemVisible(true); // Add any new attribute items for (const cred of credentials) { @@ -381,7 +404,7 @@ page.setAllowIframes = async function(tab, args = []) { const [ allowIframes, site ] = args; // Only set when main windows' URL is used - if (trimURL(tab?.url) === trimURL(site)) { + if (trimURL(tab?.url) === trimURL(site) && tab?.id) { page.tabs[tab.id].allowIframes = allowIframes; } }; diff --git a/keepassxc-browser/common/global.js b/keepassxc-browser/common/global.js index d97a8a7..55de510 100755 --- a/keepassxc-browser/common/global.js +++ b/keepassxc-browser/common/global.js @@ -179,7 +179,7 @@ const getFileAndLine = function() { const getCurrentTab = async function() { const tabs = await browser.tabs.query({ active: true, currentWindow: true }); - return tabs.length > 0 ? tabs[0] : undefined; + return tabs?.length > 0 ? tabs[0] : undefined; }; // Exports for tests