Services.console.logStringMessage( "Attribution code cannot be written for MSIX builds, aborting." ); return; } let file = AttributionCode.attributionFile; await IOUtils.makeDirectory(file.parent.path); let bytes = new TextEncoder().encode(code); await AttributionIOUtils.write(file.path, bytes); }, /** * Returns an array of allowed attribution code keys. */ get allowedCodeKeys() { return [...ATTR_CODE_KEYS]; }, /** * Returns an object containing a key-value pair for each piece of attribution * data included in the passed-in attribution code string. * If the string isn't a valid attribution code, returns an empty object. */ parseAttributionCode(code) { if (code.length > ATTR_CODE_MAX_LENGTH) { return {}; } let isValid = true; let parsed = {}; for (let param of code.split(ATTR_CODE_FIELD_SEPARATOR)) { let [key, value] = param.split(ATTR_CODE_KEY_VALUE_SEPARATOR, 2); if (key && ATTR_CODE_KEYS.includes(key)) { if (value && ATTR_CODE_VALUE_REGEX.test(value)) { if (key === "msstoresignedin") { if (value === "true") { parsed[key] = true; } else if (value === "false") { parsed[key] = false; } else { throw new Error("Couldn't parse msstoresignedin"); } } else { parsed[key] = value; } } } else if (param.startsWith(MSCLKID_KEY_PREFIX)) { // Microsoft Store Ads uses `_` to separate the key and value, therefore // requires special handling. parsed.msclkid = param.substring(MSCLKID_KEY_PREFIX.length); } else { lazy.log.debug( `parseAttributionCode: "${code}" => isValid = false: "${key}", "${value}"` ); isValid = false; break; } } if (isValid) { return parsed; } Glean.browser.attributionErrors.decode_error.add(1); return {}; }, /** * Returns a string serializing the given attribution data. * * It is expected that the given values are already URL-encoded. */ serializeAttributionData(data) { // Iterating in this way makes the order deterministic. let s = ""; for (let key of ATTR_CODE_KEYS) { if (key in data) { let value = data[key]; if (s) { s += ATTR_CODE_FIELD_SEPARATOR; // URL-encoded & } s += `${key}${ATTR_CODE_KEY_VALUE_SEPARATOR}${value}`; // URL-encoded = } } return s; }, async _getMacAttrDataAsync() { // On macOS, we fish the attribution data from an extended attribute on // the .app bundle directory. try { let attrStr = await lazy.MacAttribution.getAttributionString(); lazy.log.debug( `_getMacAttrDataAsync: getAttributionString: "${attrStr}"` ); if (attrStr === null) { gCachedAttrData = {}; lazy.log.debug(`_getMacAttrDataAsync: null attribution string`); Glean.browser.attributionErrors.null_error.add(1); } else if (attrStr == "") { gCachedAttrData = {}; lazy.log.debug(`_getMacAttrDataAsync: empty attribution string`); Glean.browser.attributionErrors.empty_error.add(1); } else { gCachedAttrData = this.parseAttributionCode(attrStr); } } catch (ex) { // Avoid partial attribution data. gCachedAttrData = {}; // No attributions. Just `warn` 'cuz this isn't necessarily an error. lazy.log.warn("Caught exception fetching macOS attribution codes!", ex); if ( ex instanceof Ci.nsIException && ex.result == Cr.NS_ERROR_UNEXPECTED ) { // Bad quarantine data. Glean.browser.attributionErrors.quarantine_error.add(1); } } lazy.log.debug( `macOS attribution data is ${JSON.stringify(gCachedAttrData)}` ); return gCachedAttrData; }, async _getWindowsNSISAttrDataAsync() { return AttributionIOUtils.read(this.attributionFile.path); }, async _getWindowsMSIXAttrDataAsync() { // This comes out of windows-package-manager _not_ URL encoded or in an ArrayBuffer, // but the parsing code wants it that way. It's easier to just provide that // than have the parsing code support both. lazy.log.debug( `winPackageFamilyName is: ${Services.sysinfo.getProperty( "winPackageFamilyName" )}` ); let encoder = new TextEncoder(); return encoder.encode(encodeURIComponent(await this.msixCampaignId())); }, /** * Reads the attribution code, either from disk or a cached version. * Returns a promise that fulfills with an object containing the parsed * attribution data if the code could be read and is valid, * or an empty object otherwise. * * On windows the attribution service converts utm_* keys, removing "utm_". * On OSX the attributions are set directly on download and retain "utm_". We * strip "utm_" while retrieving the params. */ async getAttrDataAsync() { if (AppConstants.platform != "win" && AppConstants.platform != "macosx") { // This platform doesn't support attribution. return gCachedAttrData; } if (gCachedAttrData != null) { lazy.log.debug( `getAttrDataAsync: attribution is cached: ${JSON.stringify( gCachedAttrData )}` ); return gCachedAttrData; } gCachedAttrData = {}; if (AppConstants.platform == "macosx") { lazy.log.debug(`getAttrDataAsync: macOS`); return this._getMacAttrDataAsync(); } lazy.log.debug("getAttrDataAsync: !macOS"); let attributionFile = this.attributionFile; let bytes; try { if ( AppConstants.platform === "win" && Services.sysinfo.getProperty("hasWinPackageId") ) { lazy.log.debug("getAttrDataAsync: MSIX"); bytes = await this._getWindowsMSIXAttrDataAsync(); } else { lazy.log.debug("getAttrDataAsync: NSIS"); bytes = await this._getWindowsNSISAttrDataAsync(); } } catch (ex) { if (DOMException.isInstance(ex) && ex.name == "NotFoundError") { lazy.log.debug( `getAttrDataAsync: !exists("${ attributionFile.path }"), returning ${JSON.stringify(gCachedAttrData)}` ); return gCachedAttrData; } lazy.log.debug( `other error trying to read attribution data: attributionFile.path is: ${attributionFile.path}` ); lazy.log.debug("Full exception is:"); lazy.log.debug(ex); Glean.browser.attributionErrors.read_error.add(1); } if (bytes) { try { let decoder = new TextDecoder(); let code = decoder.decode(bytes); lazy.log.debug( `getAttrDataAsync: attribution bytes deserializes to ${code}` ); if (AppConstants.platform == "macosx" && !code) { // On macOS, an empty attribution code is fine. (On Windows, that // means the stub/full installer has been incorrectly attributed, // which is an error.) return gCachedAttrData; } gCachedAttrData = this.parseAttributionCode(code); lazy.log.debug( `getAttrDataAsync: ${code} parses to ${JSON.stringify( gCachedAttrData )}` ); } catch (ex) { // TextDecoder can throw an error Glean.browser.attributionErrors.decode_error.add(1); } } return gCachedAttrData; }, /** * Return the cached attribution data synchronously without hitting * the disk. * * @returns A dictionary with the attribution data if it's available, * null otherwise. */ getCachedAttributionData() { return gCachedAttrData; }, /** * Deletes the attribution data file. * Returns a promise that resolves when the file is deleted, * or if the file couldn't be deleted (the promise is never rejected). */ async deleteFileAsync() { // There is no cache file on macOS if (AppConstants.platform == "win") { try { await IOUtils.remove(this.attributionFile.path); } catch (ex) { // The attribution file may already have been deleted, // or it may have never been installed at all; // failure to delete it isn't an error. } } }, /** * Clears the cached attribution code value, if any. * Does nothing if called from outside of an xpcshell test. */ _clearCache() { if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { gCachedAttrData = null; } }, }; PK