Files
tubestation/toolkit/components/formautofill/Helpers.ios.mjs
Stanca Serban fdfaad305b Backed out 7 changesets (bug 1802809, bug 1877717, bug 1403081, bug 1890883, bug 1877720) for causing localization checks failures in aboutLogins.ftl. CLOSED TREE
Backed out changeset 62eeb65b23bd (bug 1877717)
Backed out changeset 4fde845a1182 (bug 1877717)
Backed out changeset d3124d383693 (bug 1877717)
Backed out changeset 344ac200b360 (bug 1802809)
Backed out changeset 754a1bf3b55e (bug 1877720)
Backed out changeset 961a1121017e (bug 1403081)
Backed out changeset 19b2252a4ccf (bug 1890883)
2024-05-06 18:27:03 +03:00

234 lines
8.5 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { IOSAppConstants } from "resource://gre/modules/shared/Constants.ios.mjs";
import Overrides from "resource://gre/modules/Overrides.ios.js";
/* eslint mozilla/use-isInstance: 0 */
HTMLSelectElement.isInstance = element => element instanceof HTMLSelectElement;
HTMLInputElement.isInstance = element => element instanceof HTMLInputElement;
HTMLFormElement.isInstance = element => element instanceof HTMLFormElement;
ShadowRoot.isInstance = element => element instanceof ShadowRoot;
HTMLElement.prototype.ownerGlobal = window;
HTMLInputElement.prototype.setUserInput = function (value) {
this.value = value;
// In React apps, setting .value may not always work reliably.
// We dispatch change, input as a workaround.
// There are other more "robust" solutions:
// - Dispatching keyboard events and comparing the value after setting it
// (https://github.com/fmeum/browserpass-extension/blob/5efb1f9de6078b509904a83847d370c8e92fc097/src/inject.js#L412-L440)
// - Using the native setter
// (https://github.com/facebook/react/issues/10135#issuecomment-401496776)
// These are a bit more bloated. We can consider using these later if we encounter any further issues.
["input", "change"].forEach(eventName => {
this.dispatchEvent(new Event(eventName, { bubbles: true }));
});
this.dispatchEvent(new Event("blur", { bubbles: true }));
};
// Mimic the behavior of .getAutocompleteInfo()
// It should return an object with a fieldName property matching the autocomplete attribute
// only if it's a valid value from this list https://searchfox.org/mozilla-central/source/dom/base/AutocompleteFieldList.h#89-149
// Also found here: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
HTMLElement.prototype.getAutocompleteInfo = function () {
const autocomplete = this.getAttribute("autocomplete");
return {
fieldName: IOSAppConstants.validAutocompleteFields.includes(autocomplete)
? autocomplete
: "",
};
};
// This function helps us debug better when an error occurs because a certain mock is missing
const withNotImplementedError = obj =>
new Proxy(obj, {
get(target, prop) {
if (!Object.keys(target).includes(prop)) {
throw new Error(
`Not implemented: ${prop} doesn't exist in mocked object `
);
}
return Reflect.get(...arguments);
},
});
// This function will create a proxy for each undefined property
// This is useful when the accessed property name is unkonwn beforehand
const undefinedProxy = () =>
new Proxy(() => {}, {
get() {
return undefinedProxy();
},
});
// Webpack needs to be able to statically analyze require statements in order to build the dependency graph
// In order to require modules dynamically at runtime, we use require.context() to create a dynamic require
// that is still able to be parsed by Webpack at compile time. The "./" and ".mjs" tells webpack that files
// in the current directory ending with .mjs might be needed and should be added to the dependency graph.
// NOTE: This can't handle circular dependencies. A static import can be used in this case.
// https://webpack.js.org/guides/dependency-management/
const internalModuleResolvers = {
resolveModule(moduleURI) {
// eslint-disable-next-line no-undef
const moduleResolver = require.context("./", false, /.mjs$/);
// Desktop code uses uris for importing modules of the form resource://gre/modules/<module_path>
// We only need the filename here
const moduleName = moduleURI.split("/").pop();
const modulePath =
"./" + (Overrides.ModuleOverrides[moduleName] ?? moduleName);
return moduleResolver(modulePath);
},
resolveModules(obj, modules) {
for (const [exportName, moduleURI] of Object.entries(modules)) {
const resolvedModule = this.resolveModule(moduleURI);
obj[exportName] = resolvedModule?.[exportName];
}
},
};
// Define mock for XPCOMUtils
export const XPCOMUtils = withNotImplementedError({
defineLazyPreferenceGetter: (
obj,
prop,
pref,
defaultValue = null,
onUpdate,
transform = val => val
) => {
if (!Object.keys(IOSAppConstants.prefs).includes(pref)) {
throw Error(`Pref ${pref} is not defined.`);
}
obj[prop] = transform(IOSAppConstants.prefs[pref] ?? defaultValue);
},
defineLazyModuleGetters(obj, modules) {
internalModuleResolvers.resolveModules(obj, modules);
},
});
// eslint-disable-next-line no-shadow
export const ChromeUtils = withNotImplementedError({
defineLazyGetter: (obj, prop, getFn) => {
obj[prop] = getFn?.call(obj);
},
defineESModuleGetters(obj, modules) {
internalModuleResolvers.resolveModules(obj, modules);
},
importESModule(moduleURI) {
return internalModuleResolvers.resolveModule(moduleURI);
},
});
window.ChromeUtils = ChromeUtils;
// Define mock for Region.sys.mjs
export const Region = withNotImplementedError({
home: "US",
});
// Define mock for OSKeyStore.sys.mjs
export const OSKeyStore = withNotImplementedError({
ensureLoggedIn: () => true,
});
// Define mock for Services
// NOTE: Services is a global so we need to attach it to the window
// eslint-disable-next-line no-shadow
export const Services = withNotImplementedError({
locale: withNotImplementedError({ isAppLocaleRTL: false }),
prefs: withNotImplementedError({ prefIsLocked: () => false }),
strings: withNotImplementedError({
createBundle: () =>
withNotImplementedError({
GetStringFromName: () => "",
formatStringFromName: () => "",
}),
}),
telemetry: withNotImplementedError({
scalarAdd: (scalarName, scalarValue) => {
// For now, we only care about the address form telemetry
// TODO(FXCM-935): move address telemetry to Glean so we can remove this
// Data format of the sent message is:
// {
// type: "scalar",
// name: "formautofill.addresses.detected_sections_count",
// value: Number,
// }
if (scalarName !== "formautofill.addresses.detected_sections_count") {
return;
}
// eslint-disable-next-line no-undef
webkit.messageHandlers.addressFormTelemetryMessageHandler.postMessage(
JSON.stringify({
type: "scalar",
object: scalarName,
value: scalarValue,
})
);
},
recordEvent: (category, method, object, value, extra) => {
// For now, we only care about the address form telemetry
// TODO(FXCM-935): move address telemetry to Glean so we can remove this
// Data format of the sent message is:
// {
// type: "event",
// category: "address",
// method: "detected" | "filled" | "filled_modified",
// object: "address_form" | "address_form_ext",
// value: String,
// extra: Any,
// }
if (category !== "address") {
return;
}
// eslint-disable-next-line no-undef
webkit.messageHandlers.addressFormTelemetryMessageHandler.postMessage(
JSON.stringify({
type: "event",
category,
method,
object,
value,
extra,
})
);
},
}),
// TODO(FXCM-936): we should use crypto.randomUUID() instead of Services.uuid.generateUUID() in our codebase
// Underneath crypto.randomUUID() uses the same implementation as generateUUID()
// https://searchfox.org/mozilla-central/rev/d405168c4d3c0fb900a7354ae17bb34e939af996/dom/base/Crypto.cpp#96
// The only limitation is that it's not available in insecure contexts, which should be fine for both iOS and Desktop
// since we only autofill in secure contexts
uuid: withNotImplementedError({ generateUUID: () => crypto.randomUUID() }),
});
window.Services = Services;
// Define mock for Localization
window.Localization = function () {
return { formatValueSync: () => "" };
};
// For now, we ignore all calls to glean.
// TODO(FXCM-935): move address telemetry to Glean so we can create a universal mock for glean that
// dispatches telemetry messages to the iOS.
window.Glean = {
formautofillCreditcards: undefinedProxy(),
formautofill: undefinedProxy(),
};
export const windowUtils = withNotImplementedError({
removeManuallyManagedState: () => {},
addManuallyManagedState: () => {},
});
window.windowUtils = windowUtils;
export { IOSAppConstants as AppConstants } from "resource://gre/modules/shared/Constants.ios.mjs";