forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			177 lines
		
	
	
	
		
			5.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			177 lines
		
	
	
	
		
			5.5 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 { 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;
 | |
|   },
 | |
| };
 | 
