diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 7051c97d7..830416894 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -6578,7 +6578,9 @@ class AndroidInterface extends _InterfacePrototype.default { createUIController() { - return new _NativeUIController.NativeUIController(); + return new _NativeUIController.NativeUIController({ + onPointerDown: event => this._onPointerDown(event) + }); } /** * @deprecated use `this.settings.availableInputTypes.email` in the future @@ -6747,7 +6749,9 @@ class AppleDeviceInterface extends _InterfacePrototype.default { var _this$globalConfig$us, _this$globalConfig$us2; if (((_this$globalConfig$us = this.globalConfig.userPreferences) === null || _this$globalConfig$us === void 0 ? void 0 : (_this$globalConfig$us2 = _this$globalConfig$us.platform) === null || _this$globalConfig$us2 === void 0 ? void 0 : _this$globalConfig$us2.name) === 'ios') { - return new _NativeUIController.NativeUIController(); + return new _NativeUIController.NativeUIController({ + onPointerDown: event => this._onPointerDown(event) + }); } if (!this.globalConfig.supportsTopFrame) { @@ -7090,32 +7094,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { poll(); }); } - /** - * on macOS we try to detect if a click occurred within a form - * @param {PointerEvent} event - */ - - - _onPointerDown(event) { - if (this.settings.featureToggles.credentials_saving) { - this._detectFormSubmission(event); - } - } - /** - * @param {PointerEvent} event - */ - - - _detectFormSubmission(event) { - const matchingForm = [...this.scanner.forms.values()].find(form => { - const btns = [...form.submitButtons]; // @ts-ignore - - if (btns.includes(event.target)) return true; // @ts-ignore - - if (btns.find(btn => btn.contains(event.target))) return true; - }); - matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler(); - } } @@ -7502,6 +7480,8 @@ var _deviceApi = require("../../packages/device-api"); var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls"); +var _selectorsCss = require("../Form/selectors-css"); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); } @@ -8230,6 +8210,55 @@ class InterfacePrototype { this.storeFormData(withAutoGeneratedFlag); } } + /** + * on macOS we try to detect if a click occurred within a form + * @param {PointerEvent} event + */ + + + _onPointerDown(event) { + if (this.settings.featureToggles.credentials_saving) { + this._detectFormSubmission(event); + } + } + /** + * @param {PointerEvent} event + */ + + + _detectFormSubmission(event) { + const matchingForm = [...this.scanner.forms.values()].find(form => { + const btns = [...form.submitButtons]; // @ts-ignore + + if (btns.includes(event.target)) return true; // @ts-ignore + + if (btns.find(btn => btn.contains(event.target))) return true; + }); + matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler(); + + if (!matchingForm) { + var _event$target; + + // check if the click happened on a button + const button = + /** @type HTMLElement */ + (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(_selectorsCss.SUBMIT_BUTTON_SELECTOR); + if (!button) return; + const text = (0, _matching.removeExcessWhitespace)(button === null || button === void 0 ? void 0 : button.textContent); + const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text); + + if (hasRelevantText && text.length < 25) { + // check if there's a form with values + const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues()); + + if (filledForm && (0, _autofillUtils.buttonMatchesFormType)( + /** @type HTMLElement */ + button, filledForm)) { + filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler(); + } + } + } + } /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios @@ -8252,7 +8281,7 @@ class InterfacePrototype { var _default = InterfacePrototype; exports.default = _default; -},{"../../packages/device-api":10,"../Form/formatters":27,"../Form/listenForFormSubmission":31,"../Form/matching":34,"../InputTypes/Credentials":37,"../PasswordGenerator":40,"../Scanner":41,"../Settings":42,"../UI/controllers/NativeUIController":47,"../autofill-utils":54,"../config":56,"../deviceApiCalls/__generated__/deviceApiCalls":58,"../deviceApiCalls/transports/transports":64}],24:[function(require,module,exports){ +},{"../../packages/device-api":10,"../Form/formatters":27,"../Form/listenForFormSubmission":31,"../Form/matching":34,"../Form/selectors-css":35,"../InputTypes/Credentials":37,"../PasswordGenerator":40,"../Scanner":41,"../Settings":42,"../UI/controllers/NativeUIController":47,"../autofill-utils":54,"../config":56,"../deviceApiCalls/__generated__/deviceApiCalls":58,"../deviceApiCalls/transports/transports":64}],24:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8347,13 +8376,16 @@ class Form { this.categorizeInputs(); } /** - * Checks if the form element contains the activeElement + * Checks if the form element contains the activeElement or the event target * @return {boolean} + * @param {KeyboardEvent | null} [e] */ - hasFocus() { - return this.form.contains(document.activeElement); + hasFocus(e) { + return this.form.contains(document.activeElement) || this.form.contains( + /** @type HTMLElement */ + e === null || e === void 0 ? void 0 : e.target); } /** * Checks that the form element doesn't contain an invalid field @@ -8513,16 +8545,7 @@ class Form { const allButtons = /** @type {HTMLElement[]} */ [...this.form.querySelectorAll(selector)]; - return allButtons.filter(_autofillUtils.isLikelyASubmitButton) // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form - .filter(button => { - if (this.isLogin) { - return !/sign.?up/i.test(button.textContent || ''); - } else if (this.isSignup) { - return !/(log|sign).?([io])n/i.test(button.textContent || ''); - } else { - return true; - } - }); + return allButtons.filter(btn => (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); } /** * Executes a function on input elements. Can be limited to certain element types @@ -10260,15 +10283,15 @@ const listenForGlobalFormSubmission = forms => { (_forms$get = forms.get(e.target)) === null || _forms$get === void 0 ? void 0 : _forms$get.submitHandler() ); }, true); - window.addEventListener('keypress', e => { + window.addEventListener('keydown', e => { if (e.key === 'Enter') { - const focusedForm = [...forms.values()].find(form => form.hasFocus()); + const focusedForm = [...forms.values()].find(form => form.hasFocus(e)); focusedForm === null || focusedForm === void 0 ? void 0 : focusedForm.submitHandler(); } }); const observer = new PerformanceObserver(list => { const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`? - ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && entry.name.match(/login|sign-in|signin/)); + ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && /login|sign-in|signin/.test(entry.name)); if (!entries.length) return; const filledForm = [...forms.values()].find(form => form.hasValues()); filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler(); @@ -10545,14 +10568,14 @@ const matchingConfiguration = { email: { match: '.mail\\b', skip: 'phone|name|reservation number', - forceUnknown: 'search|filter|subject|title|tab' + forceUnknown: 'search|filter|subject|title|\btab\b' }, password: { match: 'password', forceUnknown: 'captcha|mfa|2fa|two factor' }, username: { - match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$', + match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername', forceUnknown: 'search' }, // CC @@ -11968,7 +11991,7 @@ const birthdayMonth = "\n[name=bday-month],\n[name=birthday_month], [name=birthd const birthdayYear = "\n[name=bday-year],\n[name=birthday_year], [name=birthday-year],\n[name=date_of_birth_year], [name=date-of-birth-year],\n[name^=birthdate_y], [name^=birthdate-y],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]"; const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user]"), "input[name=username i]", // fix for `aa.com` "input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login -"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]"]; // todo: these are still used directly right now, mostly in scanForInputs +"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]", "input[autocomplete=username]"]; // todo: these are still used directly right now, mostly in scanForInputs // todo: ensure these can be set via configuration module.exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR; @@ -13713,15 +13736,13 @@ class OverlayUIController extends _UIController.UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */ - /** - * @type {OverlayControllerOptions} - */ - /** * @param {OverlayControllerOptions} options */ constructor(options) { - super(); + super(options); // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. _classPrivateFieldInitSpec(this, _state, { writable: true, @@ -13730,12 +13751,6 @@ class OverlayUIController extends _UIController.UIController { _defineProperty(this, "_activeTooltip", null); - _defineProperty(this, "_options", void 0); - - this._options = options; // We always register this 'pointerdown' event, regardless of - // whether we have a tooltip currently open or not. This is to ensure - // we can clear out any existing state before opening a new one. - window.addEventListener('pointerdown', this, true); } /** @@ -13901,6 +13916,8 @@ Object.defineProperty(exports, "__esModule", { }); exports.UIController = void 0; +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + /** * @typedef AttachArgs The argument required to 'attach' a tooltip * @property {import("../../Form/Form").Form} form the Form that triggered this 'attach' call @@ -13915,6 +13932,34 @@ exports.UIController = void 0; * This is the base interface that `UIControllers` should extend/implement */ class UIController { + /** + * @type {any} + */ + + /** + * @param {any} [options] + */ + constructor(options) { + _defineProperty(this, "_options", void 0); + + this._options = options; // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. + + window.addEventListener('pointerdown', this, true); + } + + handleEvent(event) { + switch (event.type) { + case 'pointerdown': + { + var _this$_options$onPoin, _this$_options; + + (_this$_options$onPoin = (_this$_options = this._options).onPointerDown) === null || _this$_options$onPoin === void 0 ? void 0 : _this$_options$onPoin.call(_this$_options, event); + break; + } + } + } /** * Implement this method to control what happen when Autofill * has enough information to 'attach' a tooltip. @@ -13922,6 +13967,8 @@ class UIController { * @param {AttachArgs} _args * @returns {void} */ + + attach(_args) { throw new Error('must implement attach'); } @@ -14198,7 +14245,7 @@ exports.default = _default; Object.defineProperty(exports, "__esModule", { value: true }); -exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; +exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; exports.escapeXML = escapeXML; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = exports.isVisible = exports.isLikelyASubmitButton = exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0; @@ -14564,9 +14611,27 @@ const isLikelyASubmitButton = el => { el.offsetHeight * el.offsetWidth >= 10000) && // it's a large element, at least 250x40px !SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel); }; +/** + * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form + * @param {HTMLElement} el + * @param {import('./Form/Form').Form} formObj + */ + exports.isLikelyASubmitButton = isLikelyASubmitButton; +const buttonMatchesFormType = (el, formObj) => { + if (formObj.isLogin) { + return !/sign.?up/i.test(el.textContent || ''); + } else if (formObj.isSignup) { + return !/(log|sign).?([io])n/i.test(el.textContent || ''); + } else { + return true; + } +}; + +exports.buttonMatchesFormType = buttonMatchesFormType; + },{"./Form/matching":34}],55:[function(require,module,exports){ "use strict"; @@ -14579,8 +14644,18 @@ var _DeviceInterface = require("./DeviceInterface"); if (!window.isSecureContext) return false; try { - const deviceInterface = (0, _DeviceInterface.createDevice)(); - deviceInterface.init(); + const startupAutofill = () => { + if (document.visibilityState === 'visible') { + const deviceInterface = (0, _DeviceInterface.createDevice)(); + deviceInterface.init(); + } else { + document.addEventListener('visibilitychange', startupAutofill, { + once: true + }); + } + }; + + startupAutofill(); } catch (e) { console.error(e); // Noop, we errored } @@ -14620,9 +14695,16 @@ function createGlobalConfig() { // The native layer will inject a randomised secret here and use it to verify the origin let secret = 'PLACEHOLDER_SECRET'; - let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || isApp || isTopFrame; - const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent); - const isMobileApp = isDDGApp && !isApp; + /** + * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name` + */ + // @ts-ignore + + const isAndroid = (userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent); // @ts-ignore + + const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; // @ts-ignore + + const isMobileApp = ['ios', 'android'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; const isFirefox = navigator.userAgent.includes('Firefox'); const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX)); return { @@ -14979,11 +15061,11 @@ class AndroidTransport extends _deviceApi.DeviceApiTransport { var _window$BrowserAutofi, _window$BrowserAutofi2; if (typeof ((_window$BrowserAutofi = window.BrowserAutofill) === null || _window$BrowserAutofi === void 0 ? void 0 : _window$BrowserAutofi.getAutofillData) !== 'function') { - throw new Error('window.BrowserAutofill.getAutofillData missing'); + console.warn('window.BrowserAutofill.getAutofillData missing'); } if (typeof ((_window$BrowserAutofi2 = window.BrowserAutofill) === null || _window$BrowserAutofi2 === void 0 ? void 0 : _window$BrowserAutofi2.storeFormData) !== 'function') { - throw new Error('window.BrowserAutofill.storeFormData missing'); + console.warn('window.BrowserAutofill.storeFormData missing'); } } } diff --git a/dist/autofill.js b/dist/autofill.js index f7e1d1780..436136508 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -2902,7 +2902,9 @@ class AndroidInterface extends _InterfacePrototype.default { createUIController() { - return new _NativeUIController.NativeUIController(); + return new _NativeUIController.NativeUIController({ + onPointerDown: event => this._onPointerDown(event) + }); } /** * @deprecated use `this.settings.availableInputTypes.email` in the future @@ -3071,7 +3073,9 @@ class AppleDeviceInterface extends _InterfacePrototype.default { var _this$globalConfig$us, _this$globalConfig$us2; if (((_this$globalConfig$us = this.globalConfig.userPreferences) === null || _this$globalConfig$us === void 0 ? void 0 : (_this$globalConfig$us2 = _this$globalConfig$us.platform) === null || _this$globalConfig$us2 === void 0 ? void 0 : _this$globalConfig$us2.name) === 'ios') { - return new _NativeUIController.NativeUIController(); + return new _NativeUIController.NativeUIController({ + onPointerDown: event => this._onPointerDown(event) + }); } if (!this.globalConfig.supportsTopFrame) { @@ -3414,32 +3418,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { poll(); }); } - /** - * on macOS we try to detect if a click occurred within a form - * @param {PointerEvent} event - */ - - - _onPointerDown(event) { - if (this.settings.featureToggles.credentials_saving) { - this._detectFormSubmission(event); - } - } - /** - * @param {PointerEvent} event - */ - - - _detectFormSubmission(event) { - const matchingForm = [...this.scanner.forms.values()].find(form => { - const btns = [...form.submitButtons]; // @ts-ignore - - if (btns.includes(event.target)) return true; // @ts-ignore - - if (btns.find(btn => btn.contains(event.target))) return true; - }); - matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler(); - } } @@ -3826,6 +3804,8 @@ var _deviceApi = require("../../packages/device-api"); var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls"); +var _selectorsCss = require("../Form/selectors-css"); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); } @@ -4554,6 +4534,55 @@ class InterfacePrototype { this.storeFormData(withAutoGeneratedFlag); } } + /** + * on macOS we try to detect if a click occurred within a form + * @param {PointerEvent} event + */ + + + _onPointerDown(event) { + if (this.settings.featureToggles.credentials_saving) { + this._detectFormSubmission(event); + } + } + /** + * @param {PointerEvent} event + */ + + + _detectFormSubmission(event) { + const matchingForm = [...this.scanner.forms.values()].find(form => { + const btns = [...form.submitButtons]; // @ts-ignore + + if (btns.includes(event.target)) return true; // @ts-ignore + + if (btns.find(btn => btn.contains(event.target))) return true; + }); + matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler(); + + if (!matchingForm) { + var _event$target; + + // check if the click happened on a button + const button = + /** @type HTMLElement */ + (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(_selectorsCss.SUBMIT_BUTTON_SELECTOR); + if (!button) return; + const text = (0, _matching.removeExcessWhitespace)(button === null || button === void 0 ? void 0 : button.textContent); + const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text); + + if (hasRelevantText && text.length < 25) { + // check if there's a form with values + const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues()); + + if (filledForm && (0, _autofillUtils.buttonMatchesFormType)( + /** @type HTMLElement */ + button, filledForm)) { + filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler(); + } + } + } + } /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios @@ -4576,7 +4605,7 @@ class InterfacePrototype { var _default = InterfacePrototype; exports.default = _default; -},{"../../packages/device-api":2,"../Form/formatters":19,"../Form/listenForFormSubmission":23,"../Form/matching":26,"../InputTypes/Credentials":29,"../PasswordGenerator":32,"../Scanner":33,"../Settings":34,"../UI/controllers/NativeUIController":39,"../autofill-utils":46,"../config":48,"../deviceApiCalls/__generated__/deviceApiCalls":50,"../deviceApiCalls/transports/transports":56}],16:[function(require,module,exports){ +},{"../../packages/device-api":2,"../Form/formatters":19,"../Form/listenForFormSubmission":23,"../Form/matching":26,"../Form/selectors-css":27,"../InputTypes/Credentials":29,"../PasswordGenerator":32,"../Scanner":33,"../Settings":34,"../UI/controllers/NativeUIController":39,"../autofill-utils":46,"../config":48,"../deviceApiCalls/__generated__/deviceApiCalls":50,"../deviceApiCalls/transports/transports":56}],16:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4671,13 +4700,16 @@ class Form { this.categorizeInputs(); } /** - * Checks if the form element contains the activeElement + * Checks if the form element contains the activeElement or the event target * @return {boolean} + * @param {KeyboardEvent | null} [e] */ - hasFocus() { - return this.form.contains(document.activeElement); + hasFocus(e) { + return this.form.contains(document.activeElement) || this.form.contains( + /** @type HTMLElement */ + e === null || e === void 0 ? void 0 : e.target); } /** * Checks that the form element doesn't contain an invalid field @@ -4837,16 +4869,7 @@ class Form { const allButtons = /** @type {HTMLElement[]} */ [...this.form.querySelectorAll(selector)]; - return allButtons.filter(_autofillUtils.isLikelyASubmitButton) // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form - .filter(button => { - if (this.isLogin) { - return !/sign.?up/i.test(button.textContent || ''); - } else if (this.isSignup) { - return !/(log|sign).?([io])n/i.test(button.textContent || ''); - } else { - return true; - } - }); + return allButtons.filter(btn => (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); } /** * Executes a function on input elements. Can be limited to certain element types @@ -6584,15 +6607,15 @@ const listenForGlobalFormSubmission = forms => { (_forms$get = forms.get(e.target)) === null || _forms$get === void 0 ? void 0 : _forms$get.submitHandler() ); }, true); - window.addEventListener('keypress', e => { + window.addEventListener('keydown', e => { if (e.key === 'Enter') { - const focusedForm = [...forms.values()].find(form => form.hasFocus()); + const focusedForm = [...forms.values()].find(form => form.hasFocus(e)); focusedForm === null || focusedForm === void 0 ? void 0 : focusedForm.submitHandler(); } }); const observer = new PerformanceObserver(list => { const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`? - ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && entry.name.match(/login|sign-in|signin/)); + ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && /login|sign-in|signin/.test(entry.name)); if (!entries.length) return; const filledForm = [...forms.values()].find(form => form.hasValues()); filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler(); @@ -6869,14 +6892,14 @@ const matchingConfiguration = { email: { match: '.mail\\b', skip: 'phone|name|reservation number', - forceUnknown: 'search|filter|subject|title|tab' + forceUnknown: 'search|filter|subject|title|\btab\b' }, password: { match: 'password', forceUnknown: 'captcha|mfa|2fa|two factor' }, username: { - match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$', + match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername', forceUnknown: 'search' }, // CC @@ -8292,7 +8315,7 @@ const birthdayMonth = "\n[name=bday-month],\n[name=birthday_month], [name=birthd const birthdayYear = "\n[name=bday-year],\n[name=birthday_year], [name=birthday-year],\n[name=date_of_birth_year], [name=date-of-birth-year],\n[name^=birthdate_y], [name^=birthdate-y],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]"; const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user]"), "input[name=username i]", // fix for `aa.com` "input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login -"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]"]; // todo: these are still used directly right now, mostly in scanForInputs +"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]", "input[autocomplete=username]"]; // todo: these are still used directly right now, mostly in scanForInputs // todo: ensure these can be set via configuration module.exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR; @@ -10037,15 +10060,13 @@ class OverlayUIController extends _UIController.UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */ - /** - * @type {OverlayControllerOptions} - */ - /** * @param {OverlayControllerOptions} options */ constructor(options) { - super(); + super(options); // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. _classPrivateFieldInitSpec(this, _state, { writable: true, @@ -10054,12 +10075,6 @@ class OverlayUIController extends _UIController.UIController { _defineProperty(this, "_activeTooltip", null); - _defineProperty(this, "_options", void 0); - - this._options = options; // We always register this 'pointerdown' event, regardless of - // whether we have a tooltip currently open or not. This is to ensure - // we can clear out any existing state before opening a new one. - window.addEventListener('pointerdown', this, true); } /** @@ -10225,6 +10240,8 @@ Object.defineProperty(exports, "__esModule", { }); exports.UIController = void 0; +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + /** * @typedef AttachArgs The argument required to 'attach' a tooltip * @property {import("../../Form/Form").Form} form the Form that triggered this 'attach' call @@ -10239,6 +10256,34 @@ exports.UIController = void 0; * This is the base interface that `UIControllers` should extend/implement */ class UIController { + /** + * @type {any} + */ + + /** + * @param {any} [options] + */ + constructor(options) { + _defineProperty(this, "_options", void 0); + + this._options = options; // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. + + window.addEventListener('pointerdown', this, true); + } + + handleEvent(event) { + switch (event.type) { + case 'pointerdown': + { + var _this$_options$onPoin, _this$_options; + + (_this$_options$onPoin = (_this$_options = this._options).onPointerDown) === null || _this$_options$onPoin === void 0 ? void 0 : _this$_options$onPoin.call(_this$_options, event); + break; + } + } + } /** * Implement this method to control what happen when Autofill * has enough information to 'attach' a tooltip. @@ -10246,6 +10291,8 @@ class UIController { * @param {AttachArgs} _args * @returns {void} */ + + attach(_args) { throw new Error('must implement attach'); } @@ -10522,7 +10569,7 @@ exports.default = _default; Object.defineProperty(exports, "__esModule", { value: true }); -exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; +exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; exports.escapeXML = escapeXML; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = exports.isVisible = exports.isLikelyASubmitButton = exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0; @@ -10888,9 +10935,27 @@ const isLikelyASubmitButton = el => { el.offsetHeight * el.offsetWidth >= 10000) && // it's a large element, at least 250x40px !SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel); }; +/** + * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form + * @param {HTMLElement} el + * @param {import('./Form/Form').Form} formObj + */ + exports.isLikelyASubmitButton = isLikelyASubmitButton; +const buttonMatchesFormType = (el, formObj) => { + if (formObj.isLogin) { + return !/sign.?up/i.test(el.textContent || ''); + } else if (formObj.isSignup) { + return !/(log|sign).?([io])n/i.test(el.textContent || ''); + } else { + return true; + } +}; + +exports.buttonMatchesFormType = buttonMatchesFormType; + },{"./Form/matching":26}],47:[function(require,module,exports){ "use strict"; @@ -10903,8 +10968,18 @@ var _DeviceInterface = require("./DeviceInterface"); if (!window.isSecureContext) return false; try { - const deviceInterface = (0, _DeviceInterface.createDevice)(); - deviceInterface.init(); + const startupAutofill = () => { + if (document.visibilityState === 'visible') { + const deviceInterface = (0, _DeviceInterface.createDevice)(); + deviceInterface.init(); + } else { + document.addEventListener('visibilitychange', startupAutofill, { + once: true + }); + } + }; + + startupAutofill(); } catch (e) { console.error(e); // Noop, we errored } @@ -10944,9 +11019,16 @@ function createGlobalConfig() { // The native layer will inject a randomised secret here and use it to verify the origin let secret = 'PLACEHOLDER_SECRET'; - let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || isApp || isTopFrame; - const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent); - const isMobileApp = isDDGApp && !isApp; + /** + * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name` + */ + // @ts-ignore + + const isAndroid = (userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent); // @ts-ignore + + const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; // @ts-ignore + + const isMobileApp = ['ios', 'android'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; const isFirefox = navigator.userAgent.includes('Firefox'); const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX)); return { @@ -11188,11 +11270,11 @@ class AndroidTransport extends _deviceApi.DeviceApiTransport { var _window$BrowserAutofi, _window$BrowserAutofi2; if (typeof ((_window$BrowserAutofi = window.BrowserAutofill) === null || _window$BrowserAutofi === void 0 ? void 0 : _window$BrowserAutofi.getAutofillData) !== 'function') { - throw new Error('window.BrowserAutofill.getAutofillData missing'); + console.warn('window.BrowserAutofill.getAutofillData missing'); } if (typeof ((_window$BrowserAutofi2 = window.BrowserAutofill) === null || _window$BrowserAutofi2 === void 0 ? void 0 : _window$BrowserAutofi2.storeFormData) !== 'function') { - throw new Error('window.BrowserAutofill.storeFormData missing'); + console.warn('window.BrowserAutofill.storeFormData missing'); } } } diff --git a/integration-test/helpers/mocks.js b/integration-test/helpers/mocks.js index 24219f4cf..ab3249fd6 100644 --- a/integration-test/helpers/mocks.js +++ b/integration-test/helpers/mocks.js @@ -6,7 +6,8 @@ export const constants = { 'overlay': 'overlay.html', 'email-autofill': 'email-autofill.html', 'signup': 'signup.html', - 'login': 'login.html' + 'login': 'login.html', + 'loginWithPoorForm': 'login-poor-form.html' }, fields: { email: { diff --git a/integration-test/helpers/pages.js b/integration-test/helpers/pages.js index aa6d0b6a8..e14f21e73 100644 --- a/integration-test/helpers/pages.js +++ b/integration-test/helpers/pages.js @@ -233,10 +233,16 @@ export function loginPage (page, server, opts = {}) { await page.type('#email', data.username) await page.click('#login button[type="submit"]') }, - async shouldNotPromptToSave () { - const calls = await page.evaluate('window.__playwright.mocks.calls') - // todo(Shane): is it too apple specific? - const mockCalls = calls.filter(([name]) => name === 'pmHandlerStoreData') + /** @param {Platform} platform */ + async shouldNotPromptToSave (platform = 'ios') { + let mockCalls = [] + if (['ios', 'macos'].includes(platform)) { + mockCalls = await mockedCalls(page, ['pmHandlerStoreData']) + } + if (platform === 'android') { + mockCalls = await mockedCalls(page, ['storeFormData']) + } + expect(mockCalls.length).toBe(0) }, /** @param {string} mockCallName */ @@ -301,6 +307,23 @@ export function loginPage (page, server, opts = {}) { } } +/** + * A wrapper around interactions for `integration-test/pages/login-poor-form.html` + * + * @param {import("playwright").Page} page + * @param {ServerWrapper} server + * @param {{overlay?: boolean, clickLabel?: boolean}} [opts] + */ +export function loginPageWithPoorForm (page, server, opts) { + const originalLoginPage = loginPage(page, server, opts) + return { + ...originalLoginPage, + async navigate () { + await page.goto(server.urlForPath(constants.pages['loginWithPoorForm'])) + } + } +} + /** * A wrapper around interactions for `integration-test/pages/email-autofill.html` * diff --git a/integration-test/pages/login-poor-form.html b/integration-test/pages/login-poor-form.html new file mode 100644 index 000000000..32b8fa609 --- /dev/null +++ b/integration-test/pages/login-poor-form.html @@ -0,0 +1,30 @@ + + + + + + + Login form + + + + +

[Home]

+ +

+ +
+
+ + + + +
+
+
+
Log in
+ Sign up +
+ + + diff --git a/integration-test/tests/email-autofill.android.spec.js b/integration-test/tests/email-autofill.android.spec.js index 17bccb6fa..972202028 100644 --- a/integration-test/tests/email-autofill.android.spec.js +++ b/integration-test/tests/email-autofill.android.spec.js @@ -38,6 +38,7 @@ test.describe('android', () => { // create + inject the script await createAutofillScript() + .replaceAll(androidStringReplacements()) .platform('android') .applyTo(page) diff --git a/integration-test/tests/save-prompts.android.spec.js b/integration-test/tests/save-prompts.android.spec.js index 6e191a2b2..cb7589061 100644 --- a/integration-test/tests/save-prompts.android.spec.js +++ b/integration-test/tests/save-prompts.android.spec.js @@ -4,7 +4,7 @@ import { setupServer, withAndroidContext } from '../helpers/harness.js' import {test as base} from '@playwright/test' -import {loginPage, signupPage} from '../helpers/pages.js' +import {loginPage, loginPageWithPoorForm, signupPage} from '../helpers/pages.js' import {androidStringReplacements, createAndroidMocks} from '../helpers/mocks.android.js' import {constants} from '../helpers/mocks.js' @@ -124,4 +124,60 @@ test.describe('Android Save prompts', () => { await login.promptWasNotShown() }) }) + + test.describe('Prompting to save from a poor login form (using Enter and click on a button outside the form)', () => { + const credentials = { + username: 'dax@wearejh.com', + password: '123456' + } + /** + * @param {import("playwright").Page} page + */ + async function setup (page) { + await forwardConsoleMessages(page) + const login = loginPageWithPoorForm(page, server) + await login.navigate() + + await createAndroidMocks() + .applyTo(page) + await createAutofillScript() + .replaceAll(androidStringReplacements({ + featureToggles: { + credentials_saving: true + } + })) + .platform('android') + .applyTo(page) + + await page.type('#password', credentials.password) + await page.type('#email', credentials.username) + + // Check that we haven't detected any submission at this point + await login.shouldNotPromptToSave() + + return login + } + + test('submit by clicking on the out-of-form button', async ({page}) => { + const login = await setup(page) + + await page.click('"Log in"') + await login.assertWasPromptedToSave(credentials, 'android') + }) + test('should not prompt if the out-of-form button does not match the form type', async ({page}) => { + const login = await setup(page) + + await page.click('"Sign up"') + await login.shouldNotPromptToSave() + }) + test('should prompt when hitting enter while an input is focused', async ({page}) => { + const login = await setup(page) + + await page.press('#email', 'Tab') + await login.shouldNotPromptToSave() + + await page.press('#password', 'Enter') + await login.assertWasPromptedToSave(credentials, 'android') + }) + }) }) diff --git a/integration-test/tests/save-prompts.ios.spec.js b/integration-test/tests/save-prompts.ios.spec.js index ff92e58f4..482398a10 100644 --- a/integration-test/tests/save-prompts.ios.spec.js +++ b/integration-test/tests/save-prompts.ios.spec.js @@ -4,7 +4,7 @@ import { withIOSContext, withIOSFeatureToggles } from '../helpers/harness.js' import {test as base} from '@playwright/test' -import {loginPage, signupPage} from '../helpers/pages.js' +import {loginPage, loginPageWithPoorForm, signupPage} from '../helpers/pages.js' import {createWebkitMocks} from '../helpers/mocks.webkit.js' import {constants} from '../helpers/mocks.js' @@ -123,5 +123,54 @@ test.describe('iOS Save prompts', () => { await login.shouldNotPromptToSave() }) }) + + test.describe('Prompting to save from a poor login form (using Enter and click on a button outside the form)', () => { + const credentials = { + username: 'dax@wearejh.com', + password: '123456' + } + /** + * @param {import("playwright").Page} page + */ + async function setup (page) { + await forwardConsoleMessages(page) + await createWebkitMocks().applyTo(page) + await withIOSFeatureToggles(page, { + credentials_saving: true + }) + const login = loginPageWithPoorForm(page, server) + await login.navigate() + + await page.type('#password', credentials.password) + await page.type('#email', credentials.username) + + // Check that we haven't detected any submission at this point + await login.shouldNotPromptToSave() + + return login + } + + test('submit by clicking on the out-of-form button', async ({page}) => { + const login = await setup(page) + + await page.click('"Log in"') + await login.assertWasPromptedToSave(credentials) + }) + test('should not prompt if the out-of-form button does not match the form type', async ({page}) => { + const login = await setup(page) + + await page.click('"Sign up"') + await login.shouldNotPromptToSave() + }) + test('should prompt when hitting enter while an input is focused', async ({page}) => { + const login = await setup(page) + + await page.press('#email', 'Tab') + await login.shouldNotPromptToSave() + + await page.press('#password', 'Enter') + await login.assertWasPromptedToSave(credentials) + }) + }) }) }) diff --git a/src/DeviceInterface/AndroidInterface.js b/src/DeviceInterface/AndroidInterface.js index 8713a387b..d6640dd65 100644 --- a/src/DeviceInterface/AndroidInterface.js +++ b/src/DeviceInterface/AndroidInterface.js @@ -19,7 +19,9 @@ class AndroidInterface extends InterfacePrototype { * @override */ createUIController () { - return new NativeUIController() + return new NativeUIController({ + onPointerDown: (event) => this._onPointerDown(event) + }) } /** diff --git a/src/DeviceInterface/AppleDeviceInterface.js b/src/DeviceInterface/AppleDeviceInterface.js index adaa3c365..e4cd825c3 100644 --- a/src/DeviceInterface/AppleDeviceInterface.js +++ b/src/DeviceInterface/AppleDeviceInterface.js @@ -29,7 +29,9 @@ class AppleDeviceInterface extends InterfacePrototype { */ createUIController () { if (this.globalConfig.userPreferences?.platform?.name === 'ios') { - return new NativeUIController() + return new NativeUIController({ + onPointerDown: (event) => this._onPointerDown(event) + }) } if (!this.globalConfig.supportsTopFrame) { @@ -314,32 +316,6 @@ class AppleDeviceInterface extends InterfacePrototype { poll() }) } - /** - * on macOS we try to detect if a click occurred within a form - * @param {PointerEvent} event - */ - _onPointerDown (event) { - if (this.settings.featureToggles.credentials_saving) { - this._detectFormSubmission(event) - } - } - /** - * @param {PointerEvent} event - */ - _detectFormSubmission (event) { - const matchingForm = [...this.scanner.forms.values()].find( - (form) => { - const btns = [...form.submitButtons] - // @ts-ignore - if (btns.includes(event.target)) return true - - // @ts-ignore - if (btns.find((btn) => btn.contains(event.target))) return true - } - ) - - matchingForm?.submitHandler() - } } export {AppleDeviceInterface} diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index c75f8ddd9..6997f3fa8 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -4,10 +4,10 @@ import { sendAndWaitForAnswer, formatDuckAddress, autofillEnabled, - notifyWebApp, getDaxBoundingBox + notifyWebApp, getDaxBoundingBox, buttonMatchesFormType } from '../autofill-utils' -import { getInputType, getSubtypeFromType } from '../Form/matching' +import {getInputType, getSubtypeFromType, removeExcessWhitespace} from '../Form/matching' import { formatFullName } from '../Form/formatters' import listenForGlobalFormSubmission from '../Form/listenForFormSubmission' import { fromPassword, appendGeneratedId, AUTOGENERATED_KEY } from '../InputTypes/Credentials' @@ -19,6 +19,7 @@ import {createTransport} from '../deviceApiCalls/transports/transports' import {Settings} from '../Settings' import {DeviceApi} from '../../packages/device-api' import {StoreFormDataCall} from '../deviceApiCalls/__generated__/deviceApiCalls' +import {SUBMIT_BUTTON_SELECTOR} from '../Form/selectors-css' /** * @typedef {import('../deviceApiCalls/__generated__/validators-ts').StoreFormData} StoreFormData @@ -610,6 +611,48 @@ class InterfacePrototype { this.storeFormData(withAutoGeneratedFlag) } } + /** + * on macOS we try to detect if a click occurred within a form + * @param {PointerEvent} event + */ + _onPointerDown (event) { + if (this.settings.featureToggles.credentials_saving) { + this._detectFormSubmission(event) + } + } + /** + * @param {PointerEvent} event + */ + _detectFormSubmission (event) { + const matchingForm = [...this.scanner.forms.values()].find( + (form) => { + const btns = [...form.submitButtons] + // @ts-ignore + if (btns.includes(event.target)) return true + + // @ts-ignore + if (btns.find((btn) => btn.contains(event.target))) return true + } + ) + + matchingForm?.submitHandler() + + if (!matchingForm) { + // check if the click happened on a button + const button = /** @type HTMLElement */(event.target)?.closest(SUBMIT_BUTTON_SELECTOR) + if (!button) return + + const text = removeExcessWhitespace(button?.textContent) + const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text) + if (hasRelevantText && text.length < 25) { + // check if there's a form with values + const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues()) + if (filledForm && buttonMatchesFormType(/** @type HTMLElement */(button), filledForm)) { + filledForm?.submitHandler() + } + } + } + } /** * This serves as a single place to create a default instance diff --git a/src/Form/Form.js b/src/Form/Form.js index 836137d57..cff9d4520 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -6,7 +6,7 @@ import { setValue, isEventWithinDax, isLikelyASubmitButton, - isVisible + isVisible, buttonMatchesFormType } from '../autofill-utils' import { getInputSubtype, getInputMainType, createMatching } from './matching' @@ -85,11 +85,12 @@ class Form { } /** - * Checks if the form element contains the activeElement + * Checks if the form element contains the activeElement or the event target * @return {boolean} + * @param {KeyboardEvent | null} [e] */ - hasFocus () { - return this.form.contains(document.activeElement) + hasFocus (e) { + return this.form.contains(document.activeElement) || this.form.contains(/** @type HTMLElement */(e?.target)) } /** @@ -225,17 +226,9 @@ class Form { const allButtons = /** @type {HTMLElement[]} */([...this.form.querySelectorAll(selector)]) return allButtons - .filter(isLikelyASubmitButton) - // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form - .filter((button) => { - if (this.isLogin) { - return !/sign.?up/i.test(button.textContent || '') - } else if (this.isSignup) { - return !/(log|sign).?([io])n/i.test(button.textContent || '') - } else { - return true - } - }) + .filter((btn) => + isLikelyASubmitButton(btn) && buttonMatchesFormType(btn, this) + ) } /** diff --git a/src/Form/listenForFormSubmission.js b/src/Form/listenForFormSubmission.js index 14d5af181..aaaba51ac 100644 --- a/src/Form/listenForFormSubmission.js +++ b/src/Form/listenForFormSubmission.js @@ -8,9 +8,9 @@ const listenForGlobalFormSubmission = (forms) => { forms.get(e.target)?.submitHandler(), true) - window.addEventListener('keypress', (e) => { + window.addEventListener('keydown', (e) => { if (e.key === 'Enter') { - const focusedForm = [...forms.values()].find((form) => form.hasFocus()) + const focusedForm = [...forms.values()].find((form) => form.hasFocus(e)) focusedForm?.submitHandler() } }) @@ -19,7 +19,7 @@ const listenForGlobalFormSubmission = (forms) => { const entries = list.getEntries().filter((entry) => // @ts-ignore why does TS not know about `entry.initiatorType`? ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && - entry.name.match(/login|sign-in|signin/) + /login|sign-in|signin/.test(entry.name) ) if (!entries.length) return diff --git a/src/Form/matching-configuration.js b/src/Form/matching-configuration.js index 40e392cd2..7fc6b55fe 100644 --- a/src/Form/matching-configuration.js +++ b/src/Form/matching-configuration.js @@ -255,9 +255,9 @@ const matchingConfiguration = { /** @type {DDGMatcherConfiguration} */ ddgMatcher: { matchers: { - email: {match: '.mail\\b', skip: 'phone|name|reservation number', forceUnknown: 'search|filter|subject|title|tab'}, + email: {match: '.mail\\b', skip: 'phone|name|reservation number', forceUnknown: 'search|filter|subject|title|\btab\b'}, password: {match: 'password', forceUnknown: 'captcha|mfa|2fa|two factor'}, - username: {match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$', forceUnknown: 'search'}, + username: {match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername', forceUnknown: 'search'}, // CC cardName: {match: '(card.*name|name.*card)|(card.*holder|holder.*card)|(card.*owner|owner.*card)'}, diff --git a/src/Form/selectors-css.js b/src/Form/selectors-css.js index 2916bcf9c..4d630c493 100644 --- a/src/Form/selectors-css.js +++ b/src/Form/selectors-css.js @@ -182,7 +182,8 @@ const username = [ // fix for https://online.mbank.pl/pl/Login `input[name="userID" i]`, `input[id="login-id" i]`, - `input[name=accountname i]` + `input[name=accountname i]`, + `input[autocomplete=username]` ] // todo: these are still used directly right now, mostly in scanForInputs diff --git a/src/UI/controllers/OverlayUIController.js b/src/UI/controllers/OverlayUIController.js index 120a0a949..d08ed6468 100644 --- a/src/UI/controllers/OverlayUIController.js +++ b/src/UI/controllers/OverlayUIController.js @@ -44,17 +44,11 @@ export class OverlayUIController extends UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */ _activeTooltip = null - /** - * @type {OverlayControllerOptions} - */ - _options; - /** * @param {OverlayControllerOptions} options */ constructor (options) { - super() - this._options = options + super(options) // We always register this 'pointerdown' event, regardless of // whether we have a tooltip currently open or not. This is to ensure diff --git a/src/UI/controllers/UIController.js b/src/UI/controllers/UIController.js index c16e97ac4..2333b249d 100644 --- a/src/UI/controllers/UIController.js +++ b/src/UI/controllers/UIController.js @@ -12,6 +12,32 @@ * This is the base interface that `UIControllers` should extend/implement */ export class UIController { + /** + * @type {any} + */ + _options; + + /** + * @param {any} [options] + */ + constructor (options) { + this._options = options + + // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. + window.addEventListener('pointerdown', this, true) + } + + handleEvent (event) { + switch (event.type) { + case 'pointerdown': { + this._options.onPointerDown?.(event) + break + } + } + } + /** * Implement this method to control what happen when Autofill * has enough information to 'attach' a tooltip. diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 53d4478c2..d83276dc7 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -287,6 +287,21 @@ const isLikelyASubmitButton = (el) => { !SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel) } +/** + * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form + * @param {HTMLElement} el + * @param {import('./Form/Form').Form} formObj + */ +const buttonMatchesFormType = (el, formObj) => { + if (formObj.isLogin) { + return !/sign.?up/i.test(el.textContent || '') + } else if (formObj.isSignup) { + return !/(log|sign).?([io])n/i.test(el.textContent || '') + } else { + return true + } +} + export { notifyWebApp, sendAndWaitForAnswer, @@ -303,5 +318,6 @@ export { ADDRESS_DOMAIN, formatDuckAddress, escapeXML, - isLikelyASubmitButton + isLikelyASubmitButton, + buttonMatchesFormType } diff --git a/src/autofill.js b/src/autofill.js index 9ad819c46..76db638bf 100644 --- a/src/autofill.js +++ b/src/autofill.js @@ -5,8 +5,15 @@ import {createDevice} from './DeviceInterface' (() => { if (!window.isSecureContext) return false try { - const deviceInterface = createDevice() - deviceInterface.init() + const startupAutofill = () => { + if (document.visibilityState === 'visible') { + const deviceInterface = createDevice() + deviceInterface.init() + } else { + document.addEventListener('visibilitychange', startupAutofill, {once: true}) + } + } + startupAutofill() } catch (e) { console.error(e) // Noop, we errored diff --git a/src/config.js b/src/config.js index b3c903a4e..277702e56 100644 --- a/src/config.js +++ b/src/config.js @@ -33,11 +33,15 @@ function createGlobalConfig () { // The native layer will inject a randomised secret here and use it to verify the origin let secret = 'PLACEHOLDER_SECRET' - let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || - isApp || - isTopFrame - const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent) - const isMobileApp = isDDGApp && !isApp + /** + * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name` + */ + // @ts-ignore + const isAndroid = userPreferences?.platform.name === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent) + // @ts-ignore + const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences?.platform.name) || isAndroid + // @ts-ignore + const isMobileApp = ['ios', 'android'].includes(userPreferences?.platform.name) || isAndroid const isFirefox = navigator.userAgent.includes('Firefox') const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX)) diff --git a/src/deviceApiCalls/transports/android.transport.js b/src/deviceApiCalls/transports/android.transport.js index 562a79dbe..031548827 100644 --- a/src/deviceApiCalls/transports/android.transport.js +++ b/src/deviceApiCalls/transports/android.transport.js @@ -17,10 +17,10 @@ export class AndroidTransport extends DeviceApiTransport { if (this.config.isDDGTestMode) { if (typeof window.BrowserAutofill?.getAutofillData !== 'function') { - throw new Error('window.BrowserAutofill.getAutofillData missing') + console.warn('window.BrowserAutofill.getAutofillData missing') } if (typeof window.BrowserAutofill?.storeFormData !== 'function') { - throw new Error('window.BrowserAutofill.storeFormData missing') + console.warn('window.BrowserAutofill.storeFormData missing') } } } diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 7051c97d7..830416894 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -6578,7 +6578,9 @@ class AndroidInterface extends _InterfacePrototype.default { createUIController() { - return new _NativeUIController.NativeUIController(); + return new _NativeUIController.NativeUIController({ + onPointerDown: event => this._onPointerDown(event) + }); } /** * @deprecated use `this.settings.availableInputTypes.email` in the future @@ -6747,7 +6749,9 @@ class AppleDeviceInterface extends _InterfacePrototype.default { var _this$globalConfig$us, _this$globalConfig$us2; if (((_this$globalConfig$us = this.globalConfig.userPreferences) === null || _this$globalConfig$us === void 0 ? void 0 : (_this$globalConfig$us2 = _this$globalConfig$us.platform) === null || _this$globalConfig$us2 === void 0 ? void 0 : _this$globalConfig$us2.name) === 'ios') { - return new _NativeUIController.NativeUIController(); + return new _NativeUIController.NativeUIController({ + onPointerDown: event => this._onPointerDown(event) + }); } if (!this.globalConfig.supportsTopFrame) { @@ -7090,32 +7094,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { poll(); }); } - /** - * on macOS we try to detect if a click occurred within a form - * @param {PointerEvent} event - */ - - - _onPointerDown(event) { - if (this.settings.featureToggles.credentials_saving) { - this._detectFormSubmission(event); - } - } - /** - * @param {PointerEvent} event - */ - - - _detectFormSubmission(event) { - const matchingForm = [...this.scanner.forms.values()].find(form => { - const btns = [...form.submitButtons]; // @ts-ignore - - if (btns.includes(event.target)) return true; // @ts-ignore - - if (btns.find(btn => btn.contains(event.target))) return true; - }); - matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler(); - } } @@ -7502,6 +7480,8 @@ var _deviceApi = require("../../packages/device-api"); var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls"); +var _selectorsCss = require("../Form/selectors-css"); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); } @@ -8230,6 +8210,55 @@ class InterfacePrototype { this.storeFormData(withAutoGeneratedFlag); } } + /** + * on macOS we try to detect if a click occurred within a form + * @param {PointerEvent} event + */ + + + _onPointerDown(event) { + if (this.settings.featureToggles.credentials_saving) { + this._detectFormSubmission(event); + } + } + /** + * @param {PointerEvent} event + */ + + + _detectFormSubmission(event) { + const matchingForm = [...this.scanner.forms.values()].find(form => { + const btns = [...form.submitButtons]; // @ts-ignore + + if (btns.includes(event.target)) return true; // @ts-ignore + + if (btns.find(btn => btn.contains(event.target))) return true; + }); + matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler(); + + if (!matchingForm) { + var _event$target; + + // check if the click happened on a button + const button = + /** @type HTMLElement */ + (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(_selectorsCss.SUBMIT_BUTTON_SELECTOR); + if (!button) return; + const text = (0, _matching.removeExcessWhitespace)(button === null || button === void 0 ? void 0 : button.textContent); + const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text); + + if (hasRelevantText && text.length < 25) { + // check if there's a form with values + const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues()); + + if (filledForm && (0, _autofillUtils.buttonMatchesFormType)( + /** @type HTMLElement */ + button, filledForm)) { + filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler(); + } + } + } + } /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios @@ -8252,7 +8281,7 @@ class InterfacePrototype { var _default = InterfacePrototype; exports.default = _default; -},{"../../packages/device-api":10,"../Form/formatters":27,"../Form/listenForFormSubmission":31,"../Form/matching":34,"../InputTypes/Credentials":37,"../PasswordGenerator":40,"../Scanner":41,"../Settings":42,"../UI/controllers/NativeUIController":47,"../autofill-utils":54,"../config":56,"../deviceApiCalls/__generated__/deviceApiCalls":58,"../deviceApiCalls/transports/transports":64}],24:[function(require,module,exports){ +},{"../../packages/device-api":10,"../Form/formatters":27,"../Form/listenForFormSubmission":31,"../Form/matching":34,"../Form/selectors-css":35,"../InputTypes/Credentials":37,"../PasswordGenerator":40,"../Scanner":41,"../Settings":42,"../UI/controllers/NativeUIController":47,"../autofill-utils":54,"../config":56,"../deviceApiCalls/__generated__/deviceApiCalls":58,"../deviceApiCalls/transports/transports":64}],24:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8347,13 +8376,16 @@ class Form { this.categorizeInputs(); } /** - * Checks if the form element contains the activeElement + * Checks if the form element contains the activeElement or the event target * @return {boolean} + * @param {KeyboardEvent | null} [e] */ - hasFocus() { - return this.form.contains(document.activeElement); + hasFocus(e) { + return this.form.contains(document.activeElement) || this.form.contains( + /** @type HTMLElement */ + e === null || e === void 0 ? void 0 : e.target); } /** * Checks that the form element doesn't contain an invalid field @@ -8513,16 +8545,7 @@ class Form { const allButtons = /** @type {HTMLElement[]} */ [...this.form.querySelectorAll(selector)]; - return allButtons.filter(_autofillUtils.isLikelyASubmitButton) // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form - .filter(button => { - if (this.isLogin) { - return !/sign.?up/i.test(button.textContent || ''); - } else if (this.isSignup) { - return !/(log|sign).?([io])n/i.test(button.textContent || ''); - } else { - return true; - } - }); + return allButtons.filter(btn => (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); } /** * Executes a function on input elements. Can be limited to certain element types @@ -10260,15 +10283,15 @@ const listenForGlobalFormSubmission = forms => { (_forms$get = forms.get(e.target)) === null || _forms$get === void 0 ? void 0 : _forms$get.submitHandler() ); }, true); - window.addEventListener('keypress', e => { + window.addEventListener('keydown', e => { if (e.key === 'Enter') { - const focusedForm = [...forms.values()].find(form => form.hasFocus()); + const focusedForm = [...forms.values()].find(form => form.hasFocus(e)); focusedForm === null || focusedForm === void 0 ? void 0 : focusedForm.submitHandler(); } }); const observer = new PerformanceObserver(list => { const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`? - ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && entry.name.match(/login|sign-in|signin/)); + ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && /login|sign-in|signin/.test(entry.name)); if (!entries.length) return; const filledForm = [...forms.values()].find(form => form.hasValues()); filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler(); @@ -10545,14 +10568,14 @@ const matchingConfiguration = { email: { match: '.mail\\b', skip: 'phone|name|reservation number', - forceUnknown: 'search|filter|subject|title|tab' + forceUnknown: 'search|filter|subject|title|\btab\b' }, password: { match: 'password', forceUnknown: 'captcha|mfa|2fa|two factor' }, username: { - match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$', + match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername', forceUnknown: 'search' }, // CC @@ -11968,7 +11991,7 @@ const birthdayMonth = "\n[name=bday-month],\n[name=birthday_month], [name=birthd const birthdayYear = "\n[name=bday-year],\n[name=birthday_year], [name=birthday-year],\n[name=date_of_birth_year], [name=date-of-birth-year],\n[name^=birthdate_y], [name^=birthdate-y],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]"; const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user]"), "input[name=username i]", // fix for `aa.com` "input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login -"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]"]; // todo: these are still used directly right now, mostly in scanForInputs +"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]", "input[autocomplete=username]"]; // todo: these are still used directly right now, mostly in scanForInputs // todo: ensure these can be set via configuration module.exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR; @@ -13713,15 +13736,13 @@ class OverlayUIController extends _UIController.UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */ - /** - * @type {OverlayControllerOptions} - */ - /** * @param {OverlayControllerOptions} options */ constructor(options) { - super(); + super(options); // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. _classPrivateFieldInitSpec(this, _state, { writable: true, @@ -13730,12 +13751,6 @@ class OverlayUIController extends _UIController.UIController { _defineProperty(this, "_activeTooltip", null); - _defineProperty(this, "_options", void 0); - - this._options = options; // We always register this 'pointerdown' event, regardless of - // whether we have a tooltip currently open or not. This is to ensure - // we can clear out any existing state before opening a new one. - window.addEventListener('pointerdown', this, true); } /** @@ -13901,6 +13916,8 @@ Object.defineProperty(exports, "__esModule", { }); exports.UIController = void 0; +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + /** * @typedef AttachArgs The argument required to 'attach' a tooltip * @property {import("../../Form/Form").Form} form the Form that triggered this 'attach' call @@ -13915,6 +13932,34 @@ exports.UIController = void 0; * This is the base interface that `UIControllers` should extend/implement */ class UIController { + /** + * @type {any} + */ + + /** + * @param {any} [options] + */ + constructor(options) { + _defineProperty(this, "_options", void 0); + + this._options = options; // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. + + window.addEventListener('pointerdown', this, true); + } + + handleEvent(event) { + switch (event.type) { + case 'pointerdown': + { + var _this$_options$onPoin, _this$_options; + + (_this$_options$onPoin = (_this$_options = this._options).onPointerDown) === null || _this$_options$onPoin === void 0 ? void 0 : _this$_options$onPoin.call(_this$_options, event); + break; + } + } + } /** * Implement this method to control what happen when Autofill * has enough information to 'attach' a tooltip. @@ -13922,6 +13967,8 @@ class UIController { * @param {AttachArgs} _args * @returns {void} */ + + attach(_args) { throw new Error('must implement attach'); } @@ -14198,7 +14245,7 @@ exports.default = _default; Object.defineProperty(exports, "__esModule", { value: true }); -exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; +exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; exports.escapeXML = escapeXML; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = exports.isVisible = exports.isLikelyASubmitButton = exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0; @@ -14564,9 +14611,27 @@ const isLikelyASubmitButton = el => { el.offsetHeight * el.offsetWidth >= 10000) && // it's a large element, at least 250x40px !SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel); }; +/** + * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form + * @param {HTMLElement} el + * @param {import('./Form/Form').Form} formObj + */ + exports.isLikelyASubmitButton = isLikelyASubmitButton; +const buttonMatchesFormType = (el, formObj) => { + if (formObj.isLogin) { + return !/sign.?up/i.test(el.textContent || ''); + } else if (formObj.isSignup) { + return !/(log|sign).?([io])n/i.test(el.textContent || ''); + } else { + return true; + } +}; + +exports.buttonMatchesFormType = buttonMatchesFormType; + },{"./Form/matching":34}],55:[function(require,module,exports){ "use strict"; @@ -14579,8 +14644,18 @@ var _DeviceInterface = require("./DeviceInterface"); if (!window.isSecureContext) return false; try { - const deviceInterface = (0, _DeviceInterface.createDevice)(); - deviceInterface.init(); + const startupAutofill = () => { + if (document.visibilityState === 'visible') { + const deviceInterface = (0, _DeviceInterface.createDevice)(); + deviceInterface.init(); + } else { + document.addEventListener('visibilitychange', startupAutofill, { + once: true + }); + } + }; + + startupAutofill(); } catch (e) { console.error(e); // Noop, we errored } @@ -14620,9 +14695,16 @@ function createGlobalConfig() { // The native layer will inject a randomised secret here and use it to verify the origin let secret = 'PLACEHOLDER_SECRET'; - let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || isApp || isTopFrame; - const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent); - const isMobileApp = isDDGApp && !isApp; + /** + * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name` + */ + // @ts-ignore + + const isAndroid = (userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent); // @ts-ignore + + const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; // @ts-ignore + + const isMobileApp = ['ios', 'android'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; const isFirefox = navigator.userAgent.includes('Firefox'); const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX)); return { @@ -14979,11 +15061,11 @@ class AndroidTransport extends _deviceApi.DeviceApiTransport { var _window$BrowserAutofi, _window$BrowserAutofi2; if (typeof ((_window$BrowserAutofi = window.BrowserAutofill) === null || _window$BrowserAutofi === void 0 ? void 0 : _window$BrowserAutofi.getAutofillData) !== 'function') { - throw new Error('window.BrowserAutofill.getAutofillData missing'); + console.warn('window.BrowserAutofill.getAutofillData missing'); } if (typeof ((_window$BrowserAutofi2 = window.BrowserAutofill) === null || _window$BrowserAutofi2 === void 0 ? void 0 : _window$BrowserAutofi2.storeFormData) !== 'function') { - throw new Error('window.BrowserAutofill.storeFormData missing'); + console.warn('window.BrowserAutofill.storeFormData missing'); } } } diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index f7e1d1780..436136508 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -2902,7 +2902,9 @@ class AndroidInterface extends _InterfacePrototype.default { createUIController() { - return new _NativeUIController.NativeUIController(); + return new _NativeUIController.NativeUIController({ + onPointerDown: event => this._onPointerDown(event) + }); } /** * @deprecated use `this.settings.availableInputTypes.email` in the future @@ -3071,7 +3073,9 @@ class AppleDeviceInterface extends _InterfacePrototype.default { var _this$globalConfig$us, _this$globalConfig$us2; if (((_this$globalConfig$us = this.globalConfig.userPreferences) === null || _this$globalConfig$us === void 0 ? void 0 : (_this$globalConfig$us2 = _this$globalConfig$us.platform) === null || _this$globalConfig$us2 === void 0 ? void 0 : _this$globalConfig$us2.name) === 'ios') { - return new _NativeUIController.NativeUIController(); + return new _NativeUIController.NativeUIController({ + onPointerDown: event => this._onPointerDown(event) + }); } if (!this.globalConfig.supportsTopFrame) { @@ -3414,32 +3418,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { poll(); }); } - /** - * on macOS we try to detect if a click occurred within a form - * @param {PointerEvent} event - */ - - - _onPointerDown(event) { - if (this.settings.featureToggles.credentials_saving) { - this._detectFormSubmission(event); - } - } - /** - * @param {PointerEvent} event - */ - - - _detectFormSubmission(event) { - const matchingForm = [...this.scanner.forms.values()].find(form => { - const btns = [...form.submitButtons]; // @ts-ignore - - if (btns.includes(event.target)) return true; // @ts-ignore - - if (btns.find(btn => btn.contains(event.target))) return true; - }); - matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler(); - } } @@ -3826,6 +3804,8 @@ var _deviceApi = require("../../packages/device-api"); var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls"); +var _selectorsCss = require("../Form/selectors-css"); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); } @@ -4554,6 +4534,55 @@ class InterfacePrototype { this.storeFormData(withAutoGeneratedFlag); } } + /** + * on macOS we try to detect if a click occurred within a form + * @param {PointerEvent} event + */ + + + _onPointerDown(event) { + if (this.settings.featureToggles.credentials_saving) { + this._detectFormSubmission(event); + } + } + /** + * @param {PointerEvent} event + */ + + + _detectFormSubmission(event) { + const matchingForm = [...this.scanner.forms.values()].find(form => { + const btns = [...form.submitButtons]; // @ts-ignore + + if (btns.includes(event.target)) return true; // @ts-ignore + + if (btns.find(btn => btn.contains(event.target))) return true; + }); + matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler(); + + if (!matchingForm) { + var _event$target; + + // check if the click happened on a button + const button = + /** @type HTMLElement */ + (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(_selectorsCss.SUBMIT_BUTTON_SELECTOR); + if (!button) return; + const text = (0, _matching.removeExcessWhitespace)(button === null || button === void 0 ? void 0 : button.textContent); + const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text); + + if (hasRelevantText && text.length < 25) { + // check if there's a form with values + const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues()); + + if (filledForm && (0, _autofillUtils.buttonMatchesFormType)( + /** @type HTMLElement */ + button, filledForm)) { + filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler(); + } + } + } + } /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios @@ -4576,7 +4605,7 @@ class InterfacePrototype { var _default = InterfacePrototype; exports.default = _default; -},{"../../packages/device-api":2,"../Form/formatters":19,"../Form/listenForFormSubmission":23,"../Form/matching":26,"../InputTypes/Credentials":29,"../PasswordGenerator":32,"../Scanner":33,"../Settings":34,"../UI/controllers/NativeUIController":39,"../autofill-utils":46,"../config":48,"../deviceApiCalls/__generated__/deviceApiCalls":50,"../deviceApiCalls/transports/transports":56}],16:[function(require,module,exports){ +},{"../../packages/device-api":2,"../Form/formatters":19,"../Form/listenForFormSubmission":23,"../Form/matching":26,"../Form/selectors-css":27,"../InputTypes/Credentials":29,"../PasswordGenerator":32,"../Scanner":33,"../Settings":34,"../UI/controllers/NativeUIController":39,"../autofill-utils":46,"../config":48,"../deviceApiCalls/__generated__/deviceApiCalls":50,"../deviceApiCalls/transports/transports":56}],16:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4671,13 +4700,16 @@ class Form { this.categorizeInputs(); } /** - * Checks if the form element contains the activeElement + * Checks if the form element contains the activeElement or the event target * @return {boolean} + * @param {KeyboardEvent | null} [e] */ - hasFocus() { - return this.form.contains(document.activeElement); + hasFocus(e) { + return this.form.contains(document.activeElement) || this.form.contains( + /** @type HTMLElement */ + e === null || e === void 0 ? void 0 : e.target); } /** * Checks that the form element doesn't contain an invalid field @@ -4837,16 +4869,7 @@ class Form { const allButtons = /** @type {HTMLElement[]} */ [...this.form.querySelectorAll(selector)]; - return allButtons.filter(_autofillUtils.isLikelyASubmitButton) // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form - .filter(button => { - if (this.isLogin) { - return !/sign.?up/i.test(button.textContent || ''); - } else if (this.isSignup) { - return !/(log|sign).?([io])n/i.test(button.textContent || ''); - } else { - return true; - } - }); + return allButtons.filter(btn => (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); } /** * Executes a function on input elements. Can be limited to certain element types @@ -6584,15 +6607,15 @@ const listenForGlobalFormSubmission = forms => { (_forms$get = forms.get(e.target)) === null || _forms$get === void 0 ? void 0 : _forms$get.submitHandler() ); }, true); - window.addEventListener('keypress', e => { + window.addEventListener('keydown', e => { if (e.key === 'Enter') { - const focusedForm = [...forms.values()].find(form => form.hasFocus()); + const focusedForm = [...forms.values()].find(form => form.hasFocus(e)); focusedForm === null || focusedForm === void 0 ? void 0 : focusedForm.submitHandler(); } }); const observer = new PerformanceObserver(list => { const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`? - ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && entry.name.match(/login|sign-in|signin/)); + ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && /login|sign-in|signin/.test(entry.name)); if (!entries.length) return; const filledForm = [...forms.values()].find(form => form.hasValues()); filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler(); @@ -6869,14 +6892,14 @@ const matchingConfiguration = { email: { match: '.mail\\b', skip: 'phone|name|reservation number', - forceUnknown: 'search|filter|subject|title|tab' + forceUnknown: 'search|filter|subject|title|\btab\b' }, password: { match: 'password', forceUnknown: 'captcha|mfa|2fa|two factor' }, username: { - match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$', + match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername', forceUnknown: 'search' }, // CC @@ -8292,7 +8315,7 @@ const birthdayMonth = "\n[name=bday-month],\n[name=birthday_month], [name=birthd const birthdayYear = "\n[name=bday-year],\n[name=birthday_year], [name=birthday-year],\n[name=date_of_birth_year], [name=date-of-birth-year],\n[name^=birthdate_y], [name^=birthdate-y],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]"; const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user]"), "input[name=username i]", // fix for `aa.com` "input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login -"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]"]; // todo: these are still used directly right now, mostly in scanForInputs +"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]", "input[autocomplete=username]"]; // todo: these are still used directly right now, mostly in scanForInputs // todo: ensure these can be set via configuration module.exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR; @@ -10037,15 +10060,13 @@ class OverlayUIController extends _UIController.UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */ - /** - * @type {OverlayControllerOptions} - */ - /** * @param {OverlayControllerOptions} options */ constructor(options) { - super(); + super(options); // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. _classPrivateFieldInitSpec(this, _state, { writable: true, @@ -10054,12 +10075,6 @@ class OverlayUIController extends _UIController.UIController { _defineProperty(this, "_activeTooltip", null); - _defineProperty(this, "_options", void 0); - - this._options = options; // We always register this 'pointerdown' event, regardless of - // whether we have a tooltip currently open or not. This is to ensure - // we can clear out any existing state before opening a new one. - window.addEventListener('pointerdown', this, true); } /** @@ -10225,6 +10240,8 @@ Object.defineProperty(exports, "__esModule", { }); exports.UIController = void 0; +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + /** * @typedef AttachArgs The argument required to 'attach' a tooltip * @property {import("../../Form/Form").Form} form the Form that triggered this 'attach' call @@ -10239,6 +10256,34 @@ exports.UIController = void 0; * This is the base interface that `UIControllers` should extend/implement */ class UIController { + /** + * @type {any} + */ + + /** + * @param {any} [options] + */ + constructor(options) { + _defineProperty(this, "_options", void 0); + + this._options = options; // We always register this 'pointerdown' event, regardless of + // whether we have a tooltip currently open or not. This is to ensure + // we can clear out any existing state before opening a new one. + + window.addEventListener('pointerdown', this, true); + } + + handleEvent(event) { + switch (event.type) { + case 'pointerdown': + { + var _this$_options$onPoin, _this$_options; + + (_this$_options$onPoin = (_this$_options = this._options).onPointerDown) === null || _this$_options$onPoin === void 0 ? void 0 : _this$_options$onPoin.call(_this$_options, event); + break; + } + } + } /** * Implement this method to control what happen when Autofill * has enough information to 'attach' a tooltip. @@ -10246,6 +10291,8 @@ class UIController { * @param {AttachArgs} _args * @returns {void} */ + + attach(_args) { throw new Error('must implement attach'); } @@ -10522,7 +10569,7 @@ exports.default = _default; Object.defineProperty(exports, "__esModule", { value: true }); -exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; +exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; exports.escapeXML = escapeXML; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = exports.isVisible = exports.isLikelyASubmitButton = exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0; @@ -10888,9 +10935,27 @@ const isLikelyASubmitButton = el => { el.offsetHeight * el.offsetWidth >= 10000) && // it's a large element, at least 250x40px !SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel); }; +/** + * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form + * @param {HTMLElement} el + * @param {import('./Form/Form').Form} formObj + */ + exports.isLikelyASubmitButton = isLikelyASubmitButton; +const buttonMatchesFormType = (el, formObj) => { + if (formObj.isLogin) { + return !/sign.?up/i.test(el.textContent || ''); + } else if (formObj.isSignup) { + return !/(log|sign).?([io])n/i.test(el.textContent || ''); + } else { + return true; + } +}; + +exports.buttonMatchesFormType = buttonMatchesFormType; + },{"./Form/matching":26}],47:[function(require,module,exports){ "use strict"; @@ -10903,8 +10968,18 @@ var _DeviceInterface = require("./DeviceInterface"); if (!window.isSecureContext) return false; try { - const deviceInterface = (0, _DeviceInterface.createDevice)(); - deviceInterface.init(); + const startupAutofill = () => { + if (document.visibilityState === 'visible') { + const deviceInterface = (0, _DeviceInterface.createDevice)(); + deviceInterface.init(); + } else { + document.addEventListener('visibilitychange', startupAutofill, { + once: true + }); + } + }; + + startupAutofill(); } catch (e) { console.error(e); // Noop, we errored } @@ -10944,9 +11019,16 @@ function createGlobalConfig() { // The native layer will inject a randomised secret here and use it to verify the origin let secret = 'PLACEHOLDER_SECRET'; - let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || isApp || isTopFrame; - const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent); - const isMobileApp = isDDGApp && !isApp; + /** + * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name` + */ + // @ts-ignore + + const isAndroid = (userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent); // @ts-ignore + + const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; // @ts-ignore + + const isMobileApp = ['ios', 'android'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; const isFirefox = navigator.userAgent.includes('Firefox'); const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX)); return { @@ -11188,11 +11270,11 @@ class AndroidTransport extends _deviceApi.DeviceApiTransport { var _window$BrowserAutofi, _window$BrowserAutofi2; if (typeof ((_window$BrowserAutofi = window.BrowserAutofill) === null || _window$BrowserAutofi === void 0 ? void 0 : _window$BrowserAutofi.getAutofillData) !== 'function') { - throw new Error('window.BrowserAutofill.getAutofillData missing'); + console.warn('window.BrowserAutofill.getAutofillData missing'); } if (typeof ((_window$BrowserAutofi2 = window.BrowserAutofill) === null || _window$BrowserAutofi2 === void 0 ? void 0 : _window$BrowserAutofi2.storeFormData) !== 'function') { - throw new Error('window.BrowserAutofill.storeFormData missing'); + console.warn('window.BrowserAutofill.storeFormData missing'); } } }