253 lines
9.6 KiB
JavaScript
253 lines
9.6 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;
|
|
HTMLTextAreaElement.isInstance = element =>
|
|
element instanceof HTMLTextAreaElement;
|
|
HTMLIFrameElement.isInstance = element => element instanceof HTMLIFrameElement;
|
|
HTMLFormElement.isInstance = element => element instanceof HTMLFormElement;
|
|
ShadowRoot.isInstance = element => element instanceof ShadowRoot;
|
|
|
|
HTMLElement.prototype.ownerGlobal = window;
|
|
|
|
// We cannot mock this in WebKit because we lack access to low-level APIs.
|
|
// For completeness, we simply return true when the input type is "password".
|
|
// NOTE: Since now we also include this file for password generator, it might be included multiple times
|
|
// which causes the defineProperty to throw. Allowing it to be overwritten for now is fine, since
|
|
// our code runs in a sandbox and only firefox code can overwrite it.
|
|
Object.defineProperty(HTMLInputElement.prototype, "hasBeenTypePassword", {
|
|
get() {
|
|
return this.type === "password";
|
|
},
|
|
configurable: true,
|
|
});
|
|
|
|
function setUserInput(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 }));
|
|
}
|
|
|
|
HTMLInputElement.prototype.setUserInput = setUserInput;
|
|
HTMLTextAreaElement.prototype.setUserInput = setUserInput;
|
|
|
|
// 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
|
|
) => {
|
|
const value = IOSAppConstants.prefs[pref] ?? defaultValue;
|
|
// Explicitly check for undefined since null, false, "" and 0 are valid values
|
|
if (value === undefined) {
|
|
throw Error(
|
|
`Pref ${pref} is not defined and no valid default value was provided.`
|
|
);
|
|
}
|
|
obj[prop] = transform(value);
|
|
},
|
|
defineLazyServiceGetter() {
|
|
// Don't do anything
|
|
// We need this for OS Auth fixes for formautofill.
|
|
// TODO(issam, Bug 1894967): Move os auth to separate module and remove this.
|
|
},
|
|
});
|
|
|
|
// eslint-disable-next-line no-shadow
|
|
export const ChromeUtils = withNotImplementedError({
|
|
defineLazyGetter: (obj, prop, getFn) => {
|
|
const callback = prop === "log" ? genericLogger : getFn;
|
|
obj[prop] = callback?.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: () => "",
|
|
}),
|
|
}),
|
|
// 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: () => "" };
|
|
};
|
|
|
|
// TODO(issam, FXCM-935): In order to create create a universal mock for glean that
|
|
// dispatches telemetry messages to the iOS, we need to modify typedefs in swift. For now, we map the telemetry events
|
|
// to the expected shape. FXCM-935 will tackle cleaning this up.
|
|
window.Glean = {
|
|
// While moving away from Legacy Telemetry to Glean, the automated script generated the additional categories
|
|
// `creditcard` and `address`. After bug 1933961 all probes will have moved to category formautofillCreditcards and formautofillAddresses.
|
|
formautofillCreditcards: undefinedProxy(),
|
|
formautofill: undefinedProxy(),
|
|
creditcard: undefinedProxy(),
|
|
_mapGleanToLegacy: (eventName, { value, ...extra }) => {
|
|
const eventMapping = {
|
|
filledModifiedAddressForm: {
|
|
method: "filled_modified",
|
|
object: "address_form",
|
|
},
|
|
filledAddressForm: { method: "filled", object: "address_form" },
|
|
detectedAddressForm: { method: "detected", object: "address_form" },
|
|
filledModifiedAddressFormExt: {
|
|
method: "filled_modified",
|
|
object: "address_form_ext",
|
|
},
|
|
filledAddressFormExt: { method: "filled", object: "address_form_ext" },
|
|
detectedAddressFormExt: {
|
|
method: "detected",
|
|
object: "address_form_ext",
|
|
},
|
|
};
|
|
// eslint-disable-next-line no-undef
|
|
webkit.messageHandlers.addressFormTelemetryMessageHandler.postMessage(
|
|
JSON.stringify({
|
|
type: "event",
|
|
category: "address",
|
|
...eventMapping[eventName],
|
|
value,
|
|
extra,
|
|
})
|
|
);
|
|
},
|
|
address: new Proxy(
|
|
{},
|
|
{
|
|
get(_target, prop) {
|
|
return {
|
|
record: extras => Glean._mapGleanToLegacy(prop, extras),
|
|
};
|
|
},
|
|
}
|
|
),
|
|
// Keeping unused category formautofillAddresses here, because Bug 1933961
|
|
// will move probes from the glean category address to formautofillAddresses
|
|
formautofillAddresses: undefinedProxy(),
|
|
};
|
|
|
|
const genericLogger = () =>
|
|
withNotImplementedError({
|
|
info: () => {},
|
|
error: () => {},
|
|
warn: () => {},
|
|
debug: () => {},
|
|
});
|
|
|
|
export { IOSAppConstants as AppConstants } from "resource://gre/modules/shared/Constants.ios.mjs";
|