1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This is a policy object used to override behavior for testing.
8 export const AttributionIOUtils = {
9 write: async (path, bytes) => IOUtils.write(path, bytes),
10 read: async path => IOUtils.read(path),
11 exists: async path => IOUtils.exists(path),
14 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
17 ChromeUtils.defineESModuleGetters(lazy, {
18 MacAttribution: "resource:///modules/MacAttribution.sys.mjs",
20 ChromeUtils.defineLazyGetter(lazy, "log", () => {
21 let { ConsoleAPI } = ChromeUtils.importESModule(
22 "resource://gre/modules/Console.sys.mjs"
24 let consoleOptions = {
25 // tip: set maxLogLevel to "debug" and use lazy.log.debug() to create
26 // detailed messages during development. See LOG_LEVELS in Console.sys.mjs
29 maxLogLevelPref: "browser.attribution.loglevel",
30 prefix: "AttributionCode",
32 return new ConsoleAPI(consoleOptions);
35 // This maximum length was originally based on how much space we have in the PE
36 // file header that we store attribution codes in for full and stub installers.
37 // Windows Store builds instead use a "Campaign ID" passed through URLs to send
38 // attribution information, which Microsoft's documentation claims must be no
39 // longer than 100 characters. In our own testing, we've been able to retrieve
40 // the first 208 characters of the Campaign ID. Either way, the "max" length
41 // for Microsoft Store builds is much lower than this limit implies.
42 const ATTR_CODE_MAX_LENGTH = 1010;
43 const ATTR_CODE_VALUE_REGEX = /[a-zA-Z0-9_%\\-\\.\\(\\)]*/;
44 const ATTR_CODE_FIELD_SEPARATOR = "%26"; // URL-encoded &
45 const ATTR_CODE_KEY_VALUE_SEPARATOR = "%3D"; // URL-encoded =
46 const ATTR_CODE_KEYS = [
59 let gCachedAttrData = null;
61 export var AttributionCode = {
63 * Wrapper to pull campaign IDs from MSIX builds.
64 * This function solely exists to make it easy to mock out for tests.
66 async msixCampaignId() {
67 const windowsPackageManager = Cc[
68 "@mozilla.org/windows-package-manager;1"
69 ].createInstance(Ci.nsIWindowsPackageManager);
71 return windowsPackageManager.campaignId();
75 * Returns a platform-specific nsIFile for the file containing the attribution
76 * data, or null if the current platform does not support (caching)
79 get attributionFile() {
80 if (AppConstants.platform == "win") {
81 let file = Services.dirsvc.get("GreD", Ci.nsIFile);
82 file.append("postSigningData");
90 * Write the given attribution code to the attribution file.
91 * @param {String} code to write.
93 async writeAttributionFile(code) {
94 // Writing attribution files is only used as part of test code
95 // so bailing here for MSIX builds is no big deal.
97 AppConstants.platform === "win" &&
98 Services.sysinfo.getProperty("hasWinPackageId")
100 Services.console.logStringMessage(
101 "Attribution code cannot be written for MSIX builds, aborting."
105 let file = AttributionCode.attributionFile;
106 await IOUtils.makeDirectory(file.parent.path);
107 let bytes = new TextEncoder().encode(code);
108 await AttributionIOUtils.write(file.path, bytes);
112 * Returns an array of allowed attribution code keys.
114 get allowedCodeKeys() {
115 return [...ATTR_CODE_KEYS];
119 * Returns an object containing a key-value pair for each piece of attribution
120 * data included in the passed-in attribution code string.
121 * If the string isn't a valid attribution code, returns an empty object.
123 parseAttributionCode(code) {
124 if (code.length > ATTR_CODE_MAX_LENGTH) {
130 for (let param of code.split(ATTR_CODE_FIELD_SEPARATOR)) {
131 let [key, value] = param.split(ATTR_CODE_KEY_VALUE_SEPARATOR, 2);
132 if (key && ATTR_CODE_KEYS.includes(key)) {
133 if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
134 if (key === "msstoresignedin") {
135 if (value === "true") {
137 } else if (value === "false") {
140 throw new Error("Couldn't parse msstoresignedin");
148 `parseAttributionCode: "${code}" => isValid = false: "${key}", "${value}"`
160 .getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
161 .add("decode_error");
167 * Returns a string serializing the given attribution data.
169 * It is expected that the given values are already URL-encoded.
171 serializeAttributionData(data) {
172 // Iterating in this way makes the order deterministic.
174 for (let key of ATTR_CODE_KEYS) {
176 let value = data[key];
178 s += ATTR_CODE_FIELD_SEPARATOR; // URL-encoded &
180 s += `${key}${ATTR_CODE_KEY_VALUE_SEPARATOR}${value}`; // URL-encoded =
186 async _getMacAttrDataAsync() {
187 // On macOS, we fish the attribution data from an extended attribute on
188 // the .app bundle directory.
190 let attrStr = await lazy.MacAttribution.getAttributionString();
192 `_getMacAttrDataAsync: getAttributionString: "${attrStr}"`
195 if (attrStr === null) {
196 gCachedAttrData = {};
198 lazy.log.debug(`_getMacAttrDataAsync: null attribution string`);
200 .getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
202 } else if (attrStr == "") {
203 gCachedAttrData = {};
205 lazy.log.debug(`_getMacAttrDataAsync: empty attribution string`);
207 .getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
210 gCachedAttrData = this.parseAttributionCode(attrStr);
213 // Avoid partial attribution data.
214 gCachedAttrData = {};
216 // No attributions. Just `warn` 'cuz this isn't necessarily an error.
217 lazy.log.warn("Caught exception fetching macOS attribution codes!", ex);
220 ex instanceof Ci.nsIException &&
221 ex.result == Cr.NS_ERROR_UNEXPECTED
223 // Bad quarantine data.
225 .getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
226 .add("quarantine_error");
231 `macOS attribution data is ${JSON.stringify(gCachedAttrData)}`
234 return gCachedAttrData;
237 async _getWindowsNSISAttrDataAsync() {
238 return AttributionIOUtils.read(this.attributionFile.path);
241 async _getWindowsMSIXAttrDataAsync() {
242 // This comes out of windows-package-manager _not_ URL encoded or in an ArrayBuffer,
243 // but the parsing code wants it that way. It's easier to just provide that
244 // than have the parsing code support both.
246 `winPackageFamilyName is: ${Services.sysinfo.getProperty(
247 "winPackageFamilyName"
250 let encoder = new TextEncoder();
251 return encoder.encode(encodeURIComponent(await this.msixCampaignId()));
255 * Reads the attribution code, either from disk or a cached version.
256 * Returns a promise that fulfills with an object containing the parsed
257 * attribution data if the code could be read and is valid,
258 * or an empty object otherwise.
260 * On windows the attribution service converts utm_* keys, removing "utm_".
261 * On OSX the attributions are set directly on download and retain "utm_". We
262 * strip "utm_" while retrieving the params.
264 async getAttrDataAsync() {
265 if (AppConstants.platform != "win" && AppConstants.platform != "macosx") {
266 // This platform doesn't support attribution.
267 return gCachedAttrData;
269 if (gCachedAttrData != null) {
271 `getAttrDataAsync: attribution is cached: ${JSON.stringify(
275 return gCachedAttrData;
278 gCachedAttrData = {};
280 if (AppConstants.platform == "macosx") {
281 lazy.log.debug(`getAttrDataAsync: macOS`);
282 return this._getMacAttrDataAsync();
285 lazy.log.debug("getAttrDataAsync: !macOS");
287 let attributionFile = this.attributionFile;
291 AppConstants.platform === "win" &&
292 Services.sysinfo.getProperty("hasWinPackageId")
294 lazy.log.debug("getAttrDataAsync: MSIX");
295 bytes = await this._getWindowsMSIXAttrDataAsync();
297 lazy.log.debug("getAttrDataAsync: NSIS");
298 bytes = await this._getWindowsNSISAttrDataAsync();
301 if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
303 `getAttrDataAsync: !exists("${
305 }"), returning ${JSON.stringify(gCachedAttrData)}`
307 return gCachedAttrData;
310 `other error trying to read attribution data:
311 attributionFile.path is: ${attributionFile.path}`
313 lazy.log.debug("Full exception is:");
317 .getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
322 let decoder = new TextDecoder();
323 let code = decoder.decode(bytes);
325 `getAttrDataAsync: attribution bytes deserializes to ${code}`
327 if (AppConstants.platform == "macosx" && !code) {
328 // On macOS, an empty attribution code is fine. (On Windows, that
329 // means the stub/full installer has been incorrectly attributed,
330 // which is an error.)
331 return gCachedAttrData;
334 gCachedAttrData = this.parseAttributionCode(code);
336 `getAttrDataAsync: ${code} parses to ${JSON.stringify(
341 // TextDecoder can throw an error
343 .getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
344 .add("decode_error");
348 return gCachedAttrData;
352 * Return the cached attribution data synchronously without hitting
354 * @returns A dictionary with the attribution data if it's available,
357 getCachedAttributionData() {
358 return gCachedAttrData;
362 * Deletes the attribution data file.
363 * Returns a promise that resolves when the file is deleted,
364 * or if the file couldn't be deleted (the promise is never rejected).
366 async deleteFileAsync() {
367 // There is no cache file on macOS
368 if (AppConstants.platform == "win") {
370 await IOUtils.remove(this.attributionFile.path);
372 // The attribution file may already have been deleted,
373 // or it may have never been installed at all;
374 // failure to delete it isn't an error.
380 * Clears the cached attribution code value, if any.
381 * Does nothing if called from outside of an xpcshell test.
384 if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
385 gCachedAttrData = null;