From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../components/attribution/MacAttribution.sys.mjs | 177 +++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 browser/components/attribution/MacAttribution.sys.mjs (limited to 'browser/components/attribution/MacAttribution.sys.mjs') diff --git a/browser/components/attribution/MacAttribution.sys.mjs b/browser/components/attribution/MacAttribution.sys.mjs new file mode 100644 index 0000000000..cb4a8373c8 --- /dev/null +++ b/browser/components/attribution/MacAttribution.sys.mjs @@ -0,0 +1,177 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; +XPCOMUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + let consoleOptions = { + // tip: set maxLogLevel to "debug" and use lazy.log.debug() to create + // detailed messages during development. See LOG_LEVELS in Console.sys.mjs + // for details. + maxLogLevel: "error", + maxLogLevelPref: "browser.attribution.mac.loglevel", + prefix: "MacAttribution", + }; + return new ConsoleAPI(consoleOptions); +}); + +ChromeUtils.defineESModuleGetters(lazy, { + Subprocess: "resource://gre/modules/Subprocess.sys.mjs", +}); + +/** + * Get the location of the user's macOS quarantine database. + * @return {String} path. + */ +function getQuarantineDatabasePath() { + let file = Services.dirsvc.get("Home", Ci.nsIFile); + file.append("Library"); + file.append("Preferences"); + file.append("com.apple.LaunchServices.QuarantineEventsV2"); + return file.path; +} + +/** + * Query given path for quarantine extended attributes. + * @param {String} path of the file to query. + * @return {[String, String]} pair of the quarantine data GUID and remaining + * quarantine data (usually, Gatekeeper flags). + * @throws NS_ERROR_NOT_AVAILABLE if there is no quarantine GUID for the given path. + * @throws NS_ERROR_UNEXPECTED if there is a quarantine GUID, but it is malformed. + */ +async function getQuarantineAttributes(path) { + let bytes = await IOUtils.getMacXAttr(path, "com.apple.quarantine"); + if (!bytes) { + throw new Components.Exception( + `No macOS quarantine xattrs found for ${path}`, + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + + let string = new TextDecoder("utf-8").decode(bytes); + let parts = string.split(";"); + if (!parts.length) { + throw new Components.Exception( + `macOS quarantine data is not ; separated`, + Cr.NS_ERROR_UNEXPECTED + ); + } + let guid = parts[parts.length - 1]; + if (guid.length != 36) { + // Like "12345678-90AB-CDEF-1234-567890ABCDEF". + throw new Components.Exception( + `macOS quarantine data guid is not length 36: ${guid.length}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + + return { guid, parts }; +} + +/** + * Invoke system SQLite binary to extract the referrer URL corresponding to + * the given GUID from the given macOS quarantine database. + * @param {String} path of the user's macOS quarantine database. + * @param {String} guid to query. + * @return {String} referrer URL. + */ +async function queryQuarantineDatabase( + guid, + path = getQuarantineDatabasePath() +) { + let query = `SELECT COUNT(*), LSQuarantineOriginURLString + FROM LSQuarantineEvent + WHERE LSQuarantineEventIdentifier = '${guid}' + ORDER BY LSQuarantineTimeStamp DESC LIMIT 1`; + + let proc = await lazy.Subprocess.call({ + command: "/usr/bin/sqlite3", + arguments: [path, query], + environment: {}, + stderr: "stdout", + }); + + let stdout = await proc.stdout.readString(); + + let { exitCode } = await proc.wait(); + if (exitCode != 0) { + throw new Components.Exception( + "Failed to run sqlite3", + Cr.NS_ERROR_UNEXPECTED + ); + } + + // Output is like "integer|url". + let parts = stdout.split("|", 2); + if (parts.length != 2) { + throw new Components.Exception( + "Failed to parse sqlite3 output", + Cr.NS_ERROR_UNEXPECTED + ); + } + + if (parts[0].trim() == "0") { + throw new Components.Exception( + `Quarantine database does not contain URL for guid ${guid}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + + return parts[1].trim(); +} + +export var MacAttribution = { + /** + * The file path to the `.app` directory. + */ + get applicationPath() { + // On macOS, `GreD` is like "App.app/Contents/macOS". Return "App.app". + return Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path; + }, + + /** + * Used by the Attributions system to get the download referrer. + * + * @param {String} path to get the quarantine data from. + * Usually this is a `.app` directory but can be any + * (existing) file or directory. Default: `this.applicationPath`. + * @return {String} referrer URL. + * @throws NS_ERROR_NOT_AVAILABLE if there is no quarantine GUID for the given path. + * @throws NS_ERROR_UNEXPECTED if there is a quarantine GUID, but no corresponding referrer URL is known. + */ + async getReferrerUrl(path = this.applicationPath) { + lazy.log.debug(`getReferrerUrl(${JSON.stringify(path)})`); + + // First, determine the quarantine GUID assigned by macOS to the given path. + let guid; + try { + guid = (await getQuarantineAttributes(path)).guid; + } catch (ex) { + throw new Components.Exception( + `No macOS quarantine GUID found for ${path}`, + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + lazy.log.debug(`getReferrerUrl: guid: ${guid}`); + + // Second, fish the relevant record from the quarantine database. + let url = ""; + try { + url = await queryQuarantineDatabase(guid); + lazy.log.debug(`getReferrerUrl: url: ${url}`); + } catch (ex) { + // This path is known to macOS but we failed to extract a referrer -- be noisy. + throw new Components.Exception( + `No macOS quarantine referrer URL found for ${path} with GUID ${guid}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + + return url; + }, +}; -- cgit v1.2.3