Preliminary support for Safari (#2800)

This commit is contained in:
Sami Vänttinen 2026-02-16 18:07:49 +02:00 committed by GitHub
parent 9d254d1ffe
commit 8d4e46882d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 131 additions and 52 deletions

View file

@ -51,6 +51,7 @@ export default defineConfig([globalIgnores(["**/*.min.js"]), {
elementsOverlap: "readonly", elementsOverlap: "readonly",
EXTENSION_NAME: "readonly", EXTENSION_NAME: "readonly",
getCurrentTab: "readonly", getCurrentTab: "readonly",
getIconClass: "readonly",
getLoginData: "readonly", getLoginData: "readonly",
getTopLevelDomainFromUrl: "readonly", getTopLevelDomainFromUrl: "readonly",
GRAY_BUTTON_CLASS: "readonly", GRAY_BUTTON_CLASS: "readonly",
@ -69,6 +70,7 @@ export default defineConfig([globalIgnores(["**/*.min.js"]), {
isElementInside: "readonly", isElementInside: "readonly",
isFirefox: "readonly", isFirefox: "readonly",
isIframeAllowed: "readonly", isIframeAllowed: "readonly",
isSafari: "readonly",
keepass: "readonly", keepass: "readonly",
keepassClient: "readonly", keepassClient: "readonly",
kpActions: "readonly", kpActions: "readonly",

View file

@ -83,7 +83,7 @@ browserAction.generateIconName = async function(iconType) {
style = page.settings.colorTheme; style = page.settings.colorTheme;
} }
} }
const filetype = page.isFirefox ? 'svg' : 'png'; const filetype = (page.isFirefox || page.isSafari) ? 'svg' : 'png';
return `/icons/toolbar/${style}/${name}.${filetype}`; return `/icons/toolbar/${style}/${name}.${filetype}`;
}; };

View file

@ -241,11 +241,7 @@ kpxcEvent.sendBackToTabs = async function(tab, args = []) {
} }
}; };
kpxcEvent.isFirefox = async function(tab) { kpxcEvent.getFeaturesList = async function() {
return page.isFirefox;
};
kpxcEvent.getFeaturesList = async function (tab) {
return keepass.featuresList; return keepass.featuresList;
}; };
@ -279,7 +275,6 @@ kpxcEvent.messageHandlers = {
'iframe_detected': kpxcEvent.onIframeDetected, 'iframe_detected': kpxcEvent.onIframeDetected,
'init_http_auth': kpxcEvent.initHttpAuth, 'init_http_auth': kpxcEvent.initHttpAuth,
'is_connected': kpxcEvent.getIsKeePassXCAvailable, 'is_connected': kpxcEvent.getIsKeePassXCAvailable,
'is_firefox': kpxcEvent.isFirefox,
'is_iframe_allowed': page.isIframeAllowed, 'is_iframe_allowed': page.isIframeAllowed,
'is_site_ignored': page.isSiteIgnored, 'is_site_ignored': page.isSiteIgnored,
'load_keyring': kpxcEvent.onLoadKeyRing, 'load_keyring': kpxcEvent.onLoadKeyRing,

View file

@ -6,6 +6,11 @@ httpAuth.requests = [];
httpAuth.pendingCallbacks = []; httpAuth.pendingCallbacks = [];
httpAuth.init = function() { httpAuth.init = function() {
if (page.isSafari) {
debugLogMessage('HTTP Basic Auth implementation is not supported in Safari.');
return;
}
let handleReq = httpAuth.handleRequestPromise; let handleReq = httpAuth.handleRequestPromise;
let reqType = 'blocking'; let reqType = 'blocking';

View file

@ -50,6 +50,7 @@ page.clearCredentialsTimeout = null;
page.currentRequest = {}; page.currentRequest = {};
page.currentTabId = -1; page.currentTabId = -1;
page.isFirefox = false; page.isFirefox = false;
page.isSafari = false;
page.manualFill = ManualFill.NONE; page.manualFill = ManualFill.NONE;
page.menuContexts = [ 'editable' ]; page.menuContexts = [ 'editable' ];
page.passwordFilled = false; page.passwordFilled = false;
@ -64,10 +65,8 @@ page.popupData = {
}; };
page.initBrowser = async function() { page.initBrowser = async function() {
page.isFirefox = page.isFirefox = isFirefox();
navigator.userAgent.indexOf('Firefox') !== -1 page.isSafari = isSafari();
|| navigator.userAgent.indexOf('Gecko/') !== -1
|| typeof browser.runtime.getBrowserInfo === 'function';
}; };
page.initSettings = async function() { page.initSettings = async function() {
@ -85,7 +84,7 @@ page.initSettings = async function() {
} catch (err) { } catch (err) {
debugLogMessage('page.initSettings: ' + err); debugLogMessage('page.initSettings: ' + err);
} }
} else if (typeof chrome.storage.managed === 'object') { } else if (!page.isSafari && typeof chrome.storage.managed === 'object') {
chrome.storage.managed.get('settings').then((managedSettings) => { chrome.storage.managed.get('settings').then((managedSettings) => {
if (managedSettings?.settings) { if (managedSettings?.settings) {
debugLogMessage('Managed settings found.'); debugLogMessage('Managed settings found.');

View file

@ -28,23 +28,6 @@ const URL_WILDCARD = '1kpxcwc1';
const schemeSegment = '(\\*|http|https|ws|wss|ftp)'; const schemeSegment = '(\\*|http|https|ws|wss|ftp)';
const hostSegment = '(\\*|(?:\\*\\.)?(?:[^/*]+))?'; const hostSegment = '(\\*|(?:\\*\\.)?(?:[^/*]+))?';
const isFirefox = function() {
return navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Gecko/') !== -1;
};
const isEdge = function() {
return navigator.userAgent.indexOf('Edg') !== -1;
};
const showNotification = function(message) {
browser.notifications.create({
'type': 'basic',
'iconUrl': browser.runtime.getURL('icons/keepassxc_64x64.png'),
'title': 'KeePassXC-Browser',
'message': message
});
};
const AssociatedAction = { const AssociatedAction = {
NOT_ASSOCIATED: 0, NOT_ASSOCIATED: 0,
ASSOCIATED: 1, ASSOCIATED: 1,
@ -69,6 +52,36 @@ const SitePreferences = {
USERNAME_ONLY: 'usernameOnly', USERNAME_ONLY: 'usernameOnly',
}; };
const isFirefox = function() {
return browser.runtime.getURL('')?.startsWith('moz-extension');
};
const isSafari = function() {
return browser.runtime.getURL('')?.startsWith('safari-web-extension');
};
const isEdge = function() {
return navigator.userAgent.indexOf('Edg') !== -1;
};
const getIconClass = function(className) {
if (isFirefox()) {
return className + '-moz';
} else if (isSafari()) {
return className + '-safari';
}
return className;
};
const showNotification = function(message) {
browser.notifications.create({
'type': 'basic',
'iconUrl': browser.runtime.getURL('icons/keepassxc_64x64.png'),
'title': 'KeePassXC-Browser',
'message': message
});
};
// Returns a string with 'px' for CSS styles // Returns a string with 'px' for CSS styles
const Pixels = function(value) { const Pixels = function(value) {
return String(value) + 'px'; return String(value) + 'px';

View file

@ -62,7 +62,7 @@ kpxcBanner.create = async function(credentials = {}) {
const bannerInfo = kpxcUI.createElement('div', 'banner-info'); const bannerInfo = kpxcUI.createElement('div', 'banner-info');
const bannerButtons = kpxcUI.createElement('div', 'banner-buttons'); const bannerButtons = kpxcUI.createElement('div', 'banner-buttons');
const className = kpxc.isFirefox ? 'kpxc-banner-icon-moz' : 'kpxc-banner-icon'; const className = getIconClass('kpxc-banner-icon');
const icon = kpxcUI.createElement('span', className, { 'alt': 'logo' }); const icon = kpxcUI.createElement('span', className, { 'alt': 'logo' });
const infoText = kpxcUI.createElement('span', 'banner-info-text', {}, tr('rememberInfoText')); const infoText = kpxcUI.createElement('span', 'banner-info-text', {}, tr('rememberInfoText'));

View file

@ -93,7 +93,7 @@ kpxcCustomLoginFieldsBanner.create = async function() {
const bannerInfo = kpxcUI.createElement('div', 'banner-info'); const bannerInfo = kpxcUI.createElement('div', 'banner-info');
const bannerButtons = kpxcUI.createElement('div', 'banner-buttons'); const bannerButtons = kpxcUI.createElement('div', 'banner-buttons');
const iconClassName = kpxc.isFirefox ? 'kpxc-banner-icon-moz' : 'kpxc-banner-icon'; const iconClassName = getIconClass('kpxc-banner-icon');
const icon = kpxcUI.createElement('span', iconClassName); const icon = kpxcUI.createElement('span', iconClassName);
const infoText = kpxcUI.createElement('span', 'banner-info-text', {}, tr('defineChooseCustomLoginFieldText')); const infoText = kpxcUI.createElement('span', 'banner-info-text', {}, tr('defineChooseCustomLoginFieldText'));
const separator = kpxcUI.createElement('div', 'kpxc-separator'); const separator = kpxcUI.createElement('div', 'kpxc-separator');

View file

@ -21,7 +21,6 @@ kpxc.databaseState = DatabaseState.DISCONNECTED;
kpxc.detectedFields = 0; kpxc.detectedFields = 0;
kpxc.improvedFieldDetectionEnabledForPage = false; kpxc.improvedFieldDetectionEnabledForPage = false;
kpxc.inputs = []; kpxc.inputs = [];
kpxc.isFirefox;
kpxc.settings = {}; kpxc.settings = {};
kpxc.singleInputEnabledForPage = false; kpxc.singleInputEnabledForPage = false;
kpxc.submitUrl = null; kpxc.submitUrl = null;

View file

@ -48,7 +48,7 @@ kpxcPasskeysUtils.sendPasskeysResponse = function(publicKey, errorCode, errorMes
const response = errorCode const response = errorCode
? { errorCode: errorCode, errorMessage: errorMessage, fallback: kpxcPasskeysUtils?.passkeysFallback } ? { errorCode: errorCode, errorMessage: errorMessage, fallback: kpxcPasskeysUtils?.passkeysFallback }
: { publicKey: publicKey, fallback: kpxcPasskeysUtils?.passkeysFallback }; : { publicKey: publicKey, fallback: kpxcPasskeysUtils?.passkeysFallback };
const details = kpxc.isFirefox ? cloneInto(response, document.defaultView) : response; const details = isFirefox() ? cloneInto(response, document.defaultView) : response;
document.dispatchEvent(new CustomEvent('kpxc-passkeys-response', { detail: details })); document.dispatchEvent(new CustomEvent('kpxc-passkeys-response', { detail: details }));
}; };

View file

@ -45,7 +45,7 @@ PasswordIcon.prototype.initField = function(field) {
}; };
PasswordIcon.prototype.createIcon = function(field) { PasswordIcon.prototype.createIcon = function(field) {
const className = kpxc.isFirefox ? 'key-moz' : 'key'; const className = getIconClass('key');
const size = this.calculateIconSize(field); const size = this.calculateIconSize(field);
const icon = kpxcUI.createElement('div', 'kpxc kpxc-pwgen-icon ' + className, const icon = kpxcUI.createElement('div', 'kpxc kpxc-pwgen-icon ' + className,

View file

@ -137,10 +137,10 @@ TOTPFieldIcon.prototype.initField = async function(field, segmented) {
}; };
TOTPFieldIcon.prototype.createIcon = function(field, segmented = false) { TOTPFieldIcon.prototype.createIcon = function(field, segmented = false) {
const className = kpxc.isFirefox ? 'moz' : 'default'; const className = getIconClass('kpxc-totp-icon');
const size = this.calculateIconSize(field); const size = this.calculateIconSize(field);
const icon = kpxcUI.createElement('div', 'kpxc kpxc-totp-icon ' + className, const icon = kpxcUI.createElement('div', 'kpxc ' + className,
{ {
'title': tr('totpFieldText'), 'title': tr('totpFieldText'),
'size': size, 'size': size,

View file

@ -195,7 +195,7 @@ kpxcUI.createNotification = async function(type, message) {
const notification = kpxcUI.createElement('div', 'kpxc-notification kpxc-notification-' + type, {}); const notification = kpxcUI.createElement('div', 'kpxc-notification kpxc-notification-' + type, {});
type = type.charAt(0).toUpperCase() + type.slice(1) + '!'; type = type.charAt(0).toUpperCase() + type.slice(1) + '!';
const className = kpxc.isFirefox ? 'kpxc-banner-icon-moz' : 'kpxc-banner-icon'; const className = getIconClass('kpxc-banner-icon');
const icon = kpxcUI.createElement('span', className, { 'alt': 'logo' }); const icon = kpxcUI.createElement('span', className, { 'alt': 'logo' });
const label = kpxcUI.createElement('span', 'kpxc-label', {}, type); const label = kpxcUI.createElement('span', 'kpxc-label', {}, type);
const msg = kpxcUI.createElement('span', '', {}, message); const msg = kpxcUI.createElement('span', '', {}, message);

View file

@ -79,7 +79,7 @@ UsernameFieldIcon.prototype.createIcon = function(field) {
'kpxc-pwgen-field-id': field.getAttribute('data-kpxc-id'), 'kpxc-pwgen-field-id': field.getAttribute('data-kpxc-id'),
'popover': 'manual' 'popover': 'manual'
}); });
if (kpxcFields.popoverSupported) { if (kpxcFields.popoverSupported) {
icon.style.margin = 0; icon.style.margin = 0;
} else { } else {
@ -143,12 +143,12 @@ const iconClicked = async function(field, icon) {
const getIconClassName = function(state = DatabaseState.UNLOCKED) { const getIconClassName = function(state = DatabaseState.UNLOCKED) {
if (state === DatabaseState.LOCKED) { if (state === DatabaseState.LOCKED) {
return kpxc.isFirefox ? 'lock-moz' : 'lock'; return getIconClass('lock');
} else if (state === DatabaseState.DISCONNECTED) { } else if (state === DatabaseState.DISCONNECTED) {
return kpxc.isFirefox ? 'disconnected-moz' : 'disconnected'; return getIconClass('disconnected');
} }
return kpxc.isFirefox ? 'unlock-moz' : 'unlock'; return getIconClass('unlock');
}; };
const getIconText = function(state) { const getIconText = function(state) {

View file

@ -81,6 +81,14 @@ div.kpxc-banner .kpxc-banner-icon-moz {
background-size: contain; background-size: contain;
} }
div.kpxc-banner .kpxc-banner-icon-safari {
width: 24px;
height: 24px;
overflow: hidden;
background: url('safari-web-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat;
background-size: contain;
}
div.kpxc-banner .kpxc-help-icon { div.kpxc-banner .kpxc-help-icon {
width: 24px; width: 24px;
height: 24px; height: 24px;
@ -97,6 +105,14 @@ div.kpxc-banner .kpxc-help-icon-moz {
background-size: contain; background-size: contain;
} }
div.kpxc-banner .kpxc-help-icon-safari {
width: 24px;
height: 24px;
overflow: hidden;
background: url('safari-web-extension://__MSG_@@extension_id__/icons/help.svg') right no-repeat;
background-size: contain;
}
.kpxc-separator { .kpxc-separator {
border-left: 1px solid #ccc; border-left: 1px solid #ccc;
height: 100% !important; height: 100% !important;

View file

@ -43,6 +43,16 @@
background-size: contain; background-size: contain;
} }
.kpxc-notification .kpxc-banner-icon-safari {
width: 24px;
height: 24px;
padding: 10px;
margin-right: 4px;
overflow: hidden;
background: url('safari-web-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat;
background-size: contain;
}
.kpxc-notification .kpxc-label { .kpxc-notification .kpxc-label {
font-weight: bold; font-weight: bold;
} }

View file

@ -14,3 +14,8 @@
background: url('moz-extension://__MSG_@@extension_id__/icons/key.svg') right no-repeat; background: url('moz-extension://__MSG_@@extension_id__/icons/key.svg') right no-repeat;
background-size: contain; background-size: contain;
} }
.kpxc-pwgen-icon.key-safari {
background: url('safari-web-extension://__MSG_@@extension_id__/icons/key.svg') right no-repeat;
background-size: contain;
}

View file

@ -5,12 +5,17 @@
position: absolute; position: absolute;
} }
.kpxc-totp-icon.default { .kpxc-totp-icon {
background: url('chrome-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat; background: url('chrome-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat;
background-size: contain; background-size: contain;
} }
.kpxc-totp-icon.moz { .kpxc-totp-icon-moz {
background: url('moz-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat; background: url('moz-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat;
background-size: contain; background-size: contain;
} }
.kpxc-totp-icon.safari {
background: url('safari-web-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat;
background-size: contain;
}

View file

@ -15,6 +15,11 @@
background-size: contain; background-size: contain;
} }
.kpxc-username-icon.disconnected-safari {
background: url('safari-web-extension://__MSG_@@extension_id__/icons/disconnected.svg') right no-repeat;
background-size: contain;
}
.kpxc-username-icon.lock { .kpxc-username-icon.lock {
background: url('chrome-extension://__MSG_@@extension_id__/icons/locked.svg') right no-repeat; background: url('chrome-extension://__MSG_@@extension_id__/icons/locked.svg') right no-repeat;
background-size: contain; background-size: contain;
@ -25,6 +30,11 @@
background-size: contain; background-size: contain;
} }
.kpxc-username-icon.lock-safari {
background: url('safari-web-extension://__MSG_@@extension_id__/icons/locked.svg') right no-repeat;
background-size: contain;
}
.kpxc-username-icon.unlock { .kpxc-username-icon.unlock {
background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat;
background-size: contain; background-size: contain;
@ -34,3 +44,8 @@
background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat;
background-size: contain; background-size: contain;
} }
.kpxc-username-icon.unlock-safari {
background: url('safari-web-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat;
background-size: contain;
}

View file

@ -160,7 +160,7 @@
</div> </div>
<!-- Keyboard shortcuts --> <!-- Keyboard shortcuts -->
<div class="card my-4 shadow"> <div class="card my-4 shadow" id="keyboardShortcuts">
<div class="card-header h6 rounded-0"> <div class="card-header h6 rounded-0">
<i class="fa fa-keyboard-o" aria-hidden="true"></i> <i class="fa fa-keyboard-o" aria-hidden="true"></i>
<span data-i18n="optionsKeyboardShortcutsHeader"></span> <span data-i18n="optionsKeyboardShortcutsHeader"></span>
@ -284,7 +284,7 @@
</div> </div>
<!-- Autofill HTTP Auth dialogs --> <!-- Autofill HTTP Auth dialogs -->
<div class="form-group pb-1"> <div class="form-group pb-1" id="autoFillHttpAuth">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="autoFillAndSend" id="autoFillAndSend" value="true"> <input class="form-check-input" type="checkbox" name="autoFillAndSend" id="autoFillAndSend" value="true">
<label class="form-check-label" for="autoFillAndSend" data-i18n="optionsCheckboxAutoFillAndSend"></label> <label class="form-check-label" for="autoFillAndSend" data-i18n="optionsCheckboxAutoFillAndSend"></label>

View file

@ -858,12 +858,20 @@ options.createWarning = function(elem, text) {
}, 5000); }, 5000);
}; };
options.hideUnsupportedFeatures = function() {
if (isSafari()) {
$('#tab-general-settings div#keyboardShortcuts').hide();
$('#tab-general-settings div#autoFillHttpAuth').hide();
}
};
const getBrowserId = function(userAgent) { const getBrowserId = function(userAgent) {
const browserQueries = [ const browserQueries = [
{ findStr: 'Firefox', name: 'Mozilla Firefox' }, { findStr: 'Firefox', name: 'Mozilla Firefox' },
{ findStr: 'Edg', name: 'Microsoft Edge' }, { findStr: 'Edg', name: 'Microsoft Edge' },
{ findStr: 'OPR', name: 'Opera' }, { findStr: 'OPR', name: 'Opera' },
{ findStr: 'Chrome', name: 'Chrome/Chromium' } { findStr: 'Chrome', name: 'Chrome/Chromium' },
{ findStr: 'Version/', name: 'Safari' }
]; ];
const getVersion = (agent, findStr) => { const getVersion = (agent, findStr) => {
@ -968,7 +976,7 @@ window.addEventListener('scroll', function() {
const keyRing = await browser.runtime.sendMessage({ action: 'load_keyring' }); const keyRing = await browser.runtime.sendMessage({ action: 'load_keyring' });
options.keyRing = keyRing; options.keyRing = keyRing;
options.isFirefox = await browser.runtime.sendMessage({ action: 'is_firefox' }); options.isFirefox = isFirefox();
options.initMenu(); options.initMenu();
await options.initGeneralSettings(); await options.initGeneralSettings();
@ -976,6 +984,7 @@ window.addEventListener('scroll', function() {
options.initCustomLoginFields(); options.initCustomLoginFields();
options.initSitePreferences(); options.initSitePreferences();
options.initAbout(); options.initAbout();
options.hideUnsupportedFeatures();
// The form-switch transitions should complete in 150 ms // The form-switch transitions should complete in 150 ms
setTimeout(() => { setTimeout(() => {

View file

@ -153,6 +153,15 @@ code {
width: 2.5rem; width: 2.5rem;
} }
#choose-custom-login-fields-button-safari {
background-image: url('safari-web-extension://__MSG_@@extension_id__/icons/custom_login_fields.svg');
background-position: center;
background-repeat: no-repeat;
background-size: 70%;
height: 31px;
width: 2.5rem;
}
#lock-database-button { #lock-database-button {
display: none; display: none;
width: 2.5rem; width: 2.5rem;

View file

@ -16,10 +16,7 @@ async function initSettings() {
}); });
const customLoginFieldsButton = document.body.querySelector('#settings #choose-custom-login-fields-button'); const customLoginFieldsButton = document.body.querySelector('#settings #choose-custom-login-fields-button');
const isFirefox = await browser.runtime.sendMessage({ action: 'is_firefox' }); customLoginFieldsButton.id = getIconClass('choose-custom-login-fields-button');
if (isFirefox) {
customLoginFieldsButton.id = 'choose-custom-login-fields-button-moz';
}
customLoginFieldsButton.addEventListener('click', async () => { customLoginFieldsButton.addEventListener('click', async () => {
const tab = await getCurrentTab(); const tab = await getCurrentTab();