summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/Helpers.ios.mjs
blob: 83137331f16044446db7a7fd976d5da520e55806 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/* 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";