diff --git a/eslint.config.mjs b/eslint.config.mjs index cf4bd4f..8ea9b47 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -51,6 +51,7 @@ export default defineConfig([globalIgnores(["**/*.min.js"]), { elementsOverlap: "readonly", EXTENSION_NAME: "readonly", getCurrentTab: "readonly", + getIconClass: "readonly", getLoginData: "readonly", getTopLevelDomainFromUrl: "readonly", GRAY_BUTTON_CLASS: "readonly", @@ -69,6 +70,7 @@ export default defineConfig([globalIgnores(["**/*.min.js"]), { isElementInside: "readonly", isFirefox: "readonly", isIframeAllowed: "readonly", + isSafari: "readonly", keepass: "readonly", keepassClient: "readonly", kpActions: "readonly", diff --git a/keepassxc-browser/background/browserAction.js b/keepassxc-browser/background/browserAction.js index 0b6095d..91d0e59 100755 --- a/keepassxc-browser/background/browserAction.js +++ b/keepassxc-browser/background/browserAction.js @@ -83,7 +83,7 @@ browserAction.generateIconName = async function(iconType) { style = page.settings.colorTheme; } } - const filetype = page.isFirefox ? 'svg' : 'png'; + const filetype = (page.isFirefox || page.isSafari) ? 'svg' : 'png'; return `/icons/toolbar/${style}/${name}.${filetype}`; }; diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 5a65300..6cacd24 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -241,11 +241,7 @@ kpxcEvent.sendBackToTabs = async function(tab, args = []) { } }; -kpxcEvent.isFirefox = async function(tab) { - return page.isFirefox; -}; - -kpxcEvent.getFeaturesList = async function (tab) { +kpxcEvent.getFeaturesList = async function() { return keepass.featuresList; }; @@ -279,7 +275,6 @@ kpxcEvent.messageHandlers = { 'iframe_detected': kpxcEvent.onIframeDetected, 'init_http_auth': kpxcEvent.initHttpAuth, 'is_connected': kpxcEvent.getIsKeePassXCAvailable, - 'is_firefox': kpxcEvent.isFirefox, 'is_iframe_allowed': page.isIframeAllowed, 'is_site_ignored': page.isSiteIgnored, 'load_keyring': kpxcEvent.onLoadKeyRing, diff --git a/keepassxc-browser/background/httpauth.js b/keepassxc-browser/background/httpauth.js index 64cc278..d993c5a 100755 --- a/keepassxc-browser/background/httpauth.js +++ b/keepassxc-browser/background/httpauth.js @@ -6,6 +6,11 @@ httpAuth.requests = []; httpAuth.pendingCallbacks = []; httpAuth.init = function() { + if (page.isSafari) { + debugLogMessage('HTTP Basic Auth implementation is not supported in Safari.'); + return; + } + let handleReq = httpAuth.handleRequestPromise; let reqType = 'blocking'; diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index 1476a8b..4560a90 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -50,6 +50,7 @@ page.clearCredentialsTimeout = null; page.currentRequest = {}; page.currentTabId = -1; page.isFirefox = false; +page.isSafari = false; page.manualFill = ManualFill.NONE; page.menuContexts = [ 'editable' ]; page.passwordFilled = false; @@ -64,10 +65,8 @@ page.popupData = { }; page.initBrowser = async function() { - page.isFirefox = - navigator.userAgent.indexOf('Firefox') !== -1 - || navigator.userAgent.indexOf('Gecko/') !== -1 - || typeof browser.runtime.getBrowserInfo === 'function'; + page.isFirefox = isFirefox(); + page.isSafari = isSafari(); }; page.initSettings = async function() { @@ -85,7 +84,7 @@ page.initSettings = async function() { } catch (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) => { if (managedSettings?.settings) { debugLogMessage('Managed settings found.'); diff --git a/keepassxc-browser/common/global.js b/keepassxc-browser/common/global.js index 10f58f6..e8e6ec6 100755 --- a/keepassxc-browser/common/global.js +++ b/keepassxc-browser/common/global.js @@ -28,23 +28,6 @@ const URL_WILDCARD = '1kpxcwc1'; const schemeSegment = '(\\*|http|https|ws|wss|ftp)'; 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 = { NOT_ASSOCIATED: 0, ASSOCIATED: 1, @@ -69,6 +52,36 @@ const SitePreferences = { 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 const Pixels = function(value) { return String(value) + 'px'; diff --git a/keepassxc-browser/content/banner.js b/keepassxc-browser/content/banner.js index 7e968b8..9e328dc 100644 --- a/keepassxc-browser/content/banner.js +++ b/keepassxc-browser/content/banner.js @@ -62,7 +62,7 @@ kpxcBanner.create = async function(credentials = {}) { const bannerInfo = kpxcUI.createElement('div', 'banner-info'); 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 infoText = kpxcUI.createElement('span', 'banner-info-text', {}, tr('rememberInfoText')); diff --git a/keepassxc-browser/content/custom-fields-banner.js b/keepassxc-browser/content/custom-fields-banner.js index 2b83d6a..fe72a5d 100644 --- a/keepassxc-browser/content/custom-fields-banner.js +++ b/keepassxc-browser/content/custom-fields-banner.js @@ -93,7 +93,7 @@ kpxcCustomLoginFieldsBanner.create = async function() { const bannerInfo = kpxcUI.createElement('div', 'banner-info'); 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 infoText = kpxcUI.createElement('span', 'banner-info-text', {}, tr('defineChooseCustomLoginFieldText')); const separator = kpxcUI.createElement('div', 'kpxc-separator'); diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index e935887..b6ac77d 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -21,7 +21,6 @@ kpxc.databaseState = DatabaseState.DISCONNECTED; kpxc.detectedFields = 0; kpxc.improvedFieldDetectionEnabledForPage = false; kpxc.inputs = []; -kpxc.isFirefox; kpxc.settings = {}; kpxc.singleInputEnabledForPage = false; kpxc.submitUrl = null; diff --git a/keepassxc-browser/content/passkeys-utils.js b/keepassxc-browser/content/passkeys-utils.js index ba8a9d1..b285412 100644 --- a/keepassxc-browser/content/passkeys-utils.js +++ b/keepassxc-browser/content/passkeys-utils.js @@ -48,7 +48,7 @@ kpxcPasskeysUtils.sendPasskeysResponse = function(publicKey, errorCode, errorMes const response = errorCode ? { errorCode: errorCode, errorMessage: errorMessage, 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 })); }; diff --git a/keepassxc-browser/content/pwgen.js b/keepassxc-browser/content/pwgen.js index b420fb1..8d04cf9 100644 --- a/keepassxc-browser/content/pwgen.js +++ b/keepassxc-browser/content/pwgen.js @@ -45,7 +45,7 @@ PasswordIcon.prototype.initField = function(field) { }; PasswordIcon.prototype.createIcon = function(field) { - const className = kpxc.isFirefox ? 'key-moz' : 'key'; + const className = getIconClass('key'); const size = this.calculateIconSize(field); const icon = kpxcUI.createElement('div', 'kpxc kpxc-pwgen-icon ' + className, diff --git a/keepassxc-browser/content/totp-field.js b/keepassxc-browser/content/totp-field.js index fd07e9d..65b1264 100644 --- a/keepassxc-browser/content/totp-field.js +++ b/keepassxc-browser/content/totp-field.js @@ -137,10 +137,10 @@ TOTPFieldIcon.prototype.initField = async function(field, segmented) { }; 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 icon = kpxcUI.createElement('div', 'kpxc kpxc-totp-icon ' + className, + const icon = kpxcUI.createElement('div', 'kpxc ' + className, { 'title': tr('totpFieldText'), 'size': size, diff --git a/keepassxc-browser/content/ui.js b/keepassxc-browser/content/ui.js index 652ca0d..46c07fd 100644 --- a/keepassxc-browser/content/ui.js +++ b/keepassxc-browser/content/ui.js @@ -195,7 +195,7 @@ kpxcUI.createNotification = async function(type, message) { const notification = kpxcUI.createElement('div', 'kpxc-notification kpxc-notification-' + type, {}); 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 label = kpxcUI.createElement('span', 'kpxc-label', {}, type); const msg = kpxcUI.createElement('span', '', {}, message); diff --git a/keepassxc-browser/content/username-field.js b/keepassxc-browser/content/username-field.js index 3442585..c7003f7 100644 --- a/keepassxc-browser/content/username-field.js +++ b/keepassxc-browser/content/username-field.js @@ -79,7 +79,7 @@ UsernameFieldIcon.prototype.createIcon = function(field) { 'kpxc-pwgen-field-id': field.getAttribute('data-kpxc-id'), 'popover': 'manual' }); - + if (kpxcFields.popoverSupported) { icon.style.margin = 0; } else { @@ -143,12 +143,12 @@ const iconClicked = async function(field, icon) { const getIconClassName = function(state = DatabaseState.UNLOCKED) { if (state === DatabaseState.LOCKED) { - return kpxc.isFirefox ? 'lock-moz' : 'lock'; + return getIconClass('lock'); } 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) { diff --git a/keepassxc-browser/css/banner.css b/keepassxc-browser/css/banner.css index 31ee590..b77ec08 100644 --- a/keepassxc-browser/css/banner.css +++ b/keepassxc-browser/css/banner.css @@ -81,6 +81,14 @@ div.kpxc-banner .kpxc-banner-icon-moz { 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 { width: 24px; height: 24px; @@ -97,6 +105,14 @@ div.kpxc-banner .kpxc-help-icon-moz { 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 { border-left: 1px solid #ccc; height: 100% !important; diff --git a/keepassxc-browser/css/notification.css b/keepassxc-browser/css/notification.css index 1bb815f..7cebdea 100644 --- a/keepassxc-browser/css/notification.css +++ b/keepassxc-browser/css/notification.css @@ -43,6 +43,16 @@ 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 { font-weight: bold; } diff --git a/keepassxc-browser/css/pwgen.css b/keepassxc-browser/css/pwgen.css index 4368d25..065b482 100644 --- a/keepassxc-browser/css/pwgen.css +++ b/keepassxc-browser/css/pwgen.css @@ -14,3 +14,8 @@ background: url('moz-extension://__MSG_@@extension_id__/icons/key.svg') right no-repeat; 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; +} diff --git a/keepassxc-browser/css/totp.css b/keepassxc-browser/css/totp.css index a34ed7c..24a7124 100644 --- a/keepassxc-browser/css/totp.css +++ b/keepassxc-browser/css/totp.css @@ -5,12 +5,17 @@ position: absolute; } -.kpxc-totp-icon.default { +.kpxc-totp-icon { background: url('chrome-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat; 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-size: contain; -} \ No newline at end of file +} + +.kpxc-totp-icon.safari { + background: url('safari-web-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat; + background-size: contain; +} diff --git a/keepassxc-browser/css/username.css b/keepassxc-browser/css/username.css index 42d438b..7b9686b 100644 --- a/keepassxc-browser/css/username.css +++ b/keepassxc-browser/css/username.css @@ -15,6 +15,11 @@ 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 { background: url('chrome-extension://__MSG_@@extension_id__/icons/locked.svg') right no-repeat; background-size: contain; @@ -25,6 +30,11 @@ 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 { background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; background-size: contain; @@ -34,3 +44,8 @@ background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; 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; +} diff --git a/keepassxc-browser/options/options.html b/keepassxc-browser/options/options.html index d5d1e27..ea03d19 100644 --- a/keepassxc-browser/options/options.html +++ b/keepassxc-browser/options/options.html @@ -160,7 +160,7 @@ -
+
@@ -284,7 +284,7 @@
-
+
diff --git a/keepassxc-browser/options/options.js b/keepassxc-browser/options/options.js index 6ed624a..0a8ad3c 100644 --- a/keepassxc-browser/options/options.js +++ b/keepassxc-browser/options/options.js @@ -858,12 +858,20 @@ options.createWarning = function(elem, text) { }, 5000); }; +options.hideUnsupportedFeatures = function() { + if (isSafari()) { + $('#tab-general-settings div#keyboardShortcuts').hide(); + $('#tab-general-settings div#autoFillHttpAuth').hide(); + } +}; + const getBrowserId = function(userAgent) { const browserQueries = [ { findStr: 'Firefox', name: 'Mozilla Firefox' }, { findStr: 'Edg', name: 'Microsoft Edge' }, { findStr: 'OPR', name: 'Opera' }, - { findStr: 'Chrome', name: 'Chrome/Chromium' } + { findStr: 'Chrome', name: 'Chrome/Chromium' }, + { findStr: 'Version/', name: 'Safari' } ]; const getVersion = (agent, findStr) => { @@ -968,7 +976,7 @@ window.addEventListener('scroll', function() { const keyRing = await browser.runtime.sendMessage({ action: 'load_keyring' }); options.keyRing = keyRing; - options.isFirefox = await browser.runtime.sendMessage({ action: 'is_firefox' }); + options.isFirefox = isFirefox(); options.initMenu(); await options.initGeneralSettings(); @@ -976,6 +984,7 @@ window.addEventListener('scroll', function() { options.initCustomLoginFields(); options.initSitePreferences(); options.initAbout(); + options.hideUnsupportedFeatures(); // The form-switch transitions should complete in 150 ms setTimeout(() => { diff --git a/keepassxc-browser/popups/popup.css b/keepassxc-browser/popups/popup.css index 3f0929f..8e38231 100644 --- a/keepassxc-browser/popups/popup.css +++ b/keepassxc-browser/popups/popup.css @@ -153,6 +153,15 @@ code { 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 { display: none; width: 2.5rem; diff --git a/keepassxc-browser/popups/popup_functions.js b/keepassxc-browser/popups/popup_functions.js index 30acbb3..7e9dd2c 100644 --- a/keepassxc-browser/popups/popup_functions.js +++ b/keepassxc-browser/popups/popup_functions.js @@ -16,10 +16,7 @@ async function initSettings() { }); const customLoginFieldsButton = document.body.querySelector('#settings #choose-custom-login-fields-button'); - const isFirefox = await browser.runtime.sendMessage({ action: 'is_firefox' }); - if (isFirefox) { - customLoginFieldsButton.id = 'choose-custom-login-fields-button-moz'; - } + customLoginFieldsButton.id = getIconClass('choose-custom-login-fields-button'); customLoginFieldsButton.addEventListener('click', async () => { const tab = await getCurrentTab();