Fixes for page object checks and context menu item creation

This commit is contained in:
varjolintu 2025-02-08 10:00:09 +02:00
parent a8297aeb5d
commit d5063a6cfd
6 changed files with 243 additions and 206 deletions

View file

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

View file

@ -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!

View file

@ -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');
}
})();

View file

@ -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',

View file

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

View file

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