Bug 1344771 - Implement attribution on OSX using quarantine data, r?spohl,Mossop draft
authorShane Caraveo <scaraveo@mozilla.com>
Mon, 23 Jul 2018 11:01:26 -0300
changeset 821509 2f01c4c54d1886177c32e581a418f5f852b14dd1
parent 821479 143984185dcece46031c970179ddea4837a6c01d
push id117117
push usermixedpuppy@gmail.com
push dateMon, 23 Jul 2018 14:02:30 +0000
reviewersspohl, Mossop
bugs1344771
milestone63.0a1
Bug 1344771 - Implement attribution on OSX using quarantine data, r?spohl,Mossop MozReview-Commit-ID: NgjE1HZS7M
browser/components/attribution/AttributionCode.jsm
browser/components/attribution/moz.build
browser/components/attribution/nsIMacAttribution.idl
browser/components/attribution/nsMacAttribution.cpp
browser/components/attribution/nsMacAttribution.h
browser/components/attribution/test/.eslintrc.js
browser/components/attribution/test/xpcshell/test_AttributionCode.js
browser/components/attribution/test/xpcshell/test_attribution.js
browser/components/attribution/test/xpcshell/xpcshell.ini
browser/components/build/nsBrowserCompsCID.h
browser/components/build/nsModule.cpp
browser/components/moz.build
browser/modules/AttributionCode.jsm
browser/modules/moz.build
browser/modules/test/unit/test_AttributionCode.js
browser/modules/test/unit/xpcshell.ini
xpcom/io/CocoaFileUtils.h
xpcom/io/CocoaFileUtils.mm
rename from browser/modules/AttributionCode.jsm
rename to browser/components/attribution/AttributionCode.jsm
--- a/browser/modules/AttributionCode.jsm
+++ b/browser/components/attribution/AttributionCode.jsm
@@ -7,22 +7,24 @@ var EXPORTED_SYMBOLS = ["AttributionCode
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "AppConstants",
   "resource://gre/modules/AppConstants.jsm");
 ChromeUtils.defineModuleGetter(this, "OS",
   "resource://gre/modules/osfile.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
 
 const ATTR_CODE_MAX_LENGTH = 200;
 const ATTR_CODE_KEYS_REGEX = /^source|medium|campaign|content$/;
 const ATTR_CODE_VALUE_REGEX = /[a-zA-Z0-9_%\\-\\.\\(\\)]*/;
 const ATTR_CODE_FIELD_SEPARATOR = "%26"; // URL-encoded &
 const ATTR_CODE_KEY_VALUE_SEPARATOR = "%3D"; // URL-encoded =
+const ATTR_CODE_KEYS = ["source", "medium", "campaign", "content"];
 
 let gCachedAttrData = null;
 
 /**
  * Returns an nsIFile for the file containing the attribution data.
  */
 function getAttributionFile() {
   let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
@@ -60,35 +62,59 @@ function parseAttributionCode(code) {
 }
 
 var AttributionCode = {
   /**
    * 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.
    */
   getAttrDataAsync() {
     return (async function() {
       if (gCachedAttrData != null) {
         return gCachedAttrData;
       }
 
-      let code = "";
-      try {
-        let bytes = await OS.File.read(getAttributionFile().path);
-        let decoder = new TextDecoder();
-        code = decoder.decode(bytes);
-      } catch (ex) {
-        // The attribution file may already have been deleted,
-        // or it may have never been installed at all;
-        // failure to open or read it isn't an error.
+      gCachedAttrData = {};
+      if (AppConstants.platform == "win") {
+        try {
+          let bytes = await OS.File.read(getAttributionFile().path);
+          let decoder = new TextDecoder();
+          let code = decoder.decode(bytes);
+          gCachedAttrData = parseAttributionCode(code);
+        } catch (ex) {
+          // The attribution file may already have been deleted,
+          // or it may have never been installed at all;
+          // failure to open or read it isn't an error.
+        }
+      } else if (AppConstants.platform == "macosx") {
+        try {
+          let appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path;
+          let attributionSvc = Cc["@mozilla.org/mac-attribution;1"]
+                                  .getService(Ci.nsIMacAttributionService);
+          let referrer = attributionSvc.getReferrerUrl(appPath);
+          let params = new URL(referrer).searchParams;
+          for (let key of ATTR_CODE_KEYS) {
+            let utm_key = `utm_${key}`;
+            if (params.has(utm_key)) {
+              let value = params.get(utm_key);
+              if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
+                gCachedAttrData[key] = value;
+              }
+            }
+          }
+        } catch (ex) {
+          // No attributions
+        }
       }
-
-      gCachedAttrData = parseAttributionCode(code);
       return gCachedAttrData;
     })();
   },
 
   /**
    * Return the cached attribution data synchronously without hitting
    * the disk.
    * @returns A dictionary with the attribution data if it's available,
new file mode 100644
--- /dev/null
+++ b/browser/components/attribution/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+    BUG_COMPONENT = ("Toolkit", "Telemetry")
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
+
+EXTRA_JS_MODULES += [
+    'AttributionCode.jsm',
+]
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+    XPIDL_SOURCES += [
+        'nsIMacAttribution.idl',
+    ]
+
+    XPIDL_MODULE = 'attribution'
+
+    EXPORTS += [
+        'nsMacAttribution.h',
+    ]
+
+    SOURCES += [
+        'nsMacAttribution.cpp',
+    ]
+
+    FINAL_LIBRARY = 'browsercomps'
new file mode 100644
--- /dev/null
+++ b/browser/components/attribution/nsIMacAttribution.idl
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(6FC66A78-6CBC-4B3F-B7BA-379289B29276)]
+interface nsIMacAttributionService : nsISupports
+{
+  /**
+   * Used by the Attributions system to get the download referrer.
+   *
+   * @param aFilePath A path to the file to get the quarantine data from.
+   * @returns referrerUrl
+   */
+  AString getReferrerUrl(in ACString aFilePath);
+
+  /**
+   * Used by the tests.
+   *
+   * @param aFilePath A path to the file to set the quarantine data on.
+   * @param aReferrer A url to set as the referrer for the download.
+   * @param aCreate   If true, creates new quarantine properties, overwriting
+   *                  any existing properties.  If false, the referrer is only
+   *                  set if quarantine properties already exist on the file.
+   */
+  void setReferrerUrl(in ACString aFilePath,
+                      in ACString aReferrer,
+                      in boolean aCreate);
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/attribution/nsMacAttribution.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMacAttribution.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <ApplicationServices/ApplicationServices.h>
+
+#include "../../../xpcom/io/CocoaFileUtils.h"
+#include "nsCocoaFeatures.h"
+#include "nsString.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsMacAttributionService, nsIMacAttributionService)
+
+NS_IMETHODIMP
+nsMacAttributionService::GetReferrerUrl(const nsACString& aFilePath,
+                                              nsAString& aReferrer)
+{
+  const nsCString& flat = PromiseFlatCString(aFilePath);
+  CFStringRef filePath = ::CFStringCreateWithCString(kCFAllocatorDefault,
+                                                     flat.get(),
+                                                     kCFStringEncodingUTF8);
+
+  CocoaFileUtils::CopyQuarantineReferrerUrl(filePath, aReferrer);
+
+  ::CFRelease(filePath);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacAttributionService::SetReferrerUrl(const nsACString& aFilePath,
+                                        const nsACString& aReferrerUrl,
+                                        const bool aCreate)
+{
+  const nsCString& flat = PromiseFlatCString(aFilePath);
+  CFStringRef filePath = ::CFStringCreateWithCString(kCFAllocatorDefault,
+                                                     flat.get(),
+                                                     kCFStringEncodingUTF8);
+
+  if (!filePath) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  const nsCString& flatReferrer = PromiseFlatCString(aReferrerUrl);
+  CFStringRef referrer = ::CFStringCreateWithCString(kCFAllocatorDefault,
+                                                     flatReferrer.get(),
+                                                     kCFStringEncodingUTF8);
+  if (!referrer) {
+    ::CFRelease(filePath);
+    return NS_ERROR_UNEXPECTED;
+  }
+  CFURLRef referrerURL = ::CFURLCreateWithString(kCFAllocatorDefault,
+                                                 referrer, nullptr);
+
+  CocoaFileUtils::AddQuarantineMetadataToFile(filePath,
+                                              NULL,
+                                              referrerURL,
+                                              true,
+                                              aCreate);
+
+  ::CFRelease(filePath);
+  ::CFRelease(referrer);
+  ::CFRelease(referrerURL);
+
+  return NS_OK;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/attribution/nsMacAttribution.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsmacattribution_h____
+#define nsmacattribution_h____
+
+#include "nsIMacAttribution.h"
+
+class nsMacAttributionService : public nsIMacAttributionService
+{
+public:
+  nsMacAttributionService() {};
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIMACATTRIBUTIONSERVICE
+
+protected:
+  virtual ~nsMacAttributionService() {};
+};
+
+#endif // nsmacattribution_h____
new file mode 100644
--- /dev/null
+++ b/browser/components/attribution/test/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+  "extends": "plugin:mozilla/xpcshell-test",
+};
rename from browser/modules/test/unit/test_AttributionCode.js
rename to browser/components/attribution/test/xpcshell/test_AttributionCode.js
new file mode 100644
--- /dev/null
+++ b/browser/components/attribution/test/xpcshell/test_attribution.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+add_task(async function test_attribution() {
+  let appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path;
+  let attributionSvc = Cc["@mozilla.org/mac-attribution;1"]
+                         .getService(Ci.nsIMacAttributionService);
+
+  attributionSvc.setReferrerUrl(appPath, "", true);
+  let referrer = attributionSvc.getReferrerUrl(appPath);
+  equal(referrer, "", "force an empty referrer url");
+
+  // Set a url referrer
+  let url = "http://example.com";
+  attributionSvc.setReferrerUrl(appPath, url, true);
+  referrer = attributionSvc.getReferrerUrl(appPath);
+  equal(referrer, url, "overwrite referrer url");
+
+  // Does not overwrite existing properties.
+  attributionSvc.setReferrerUrl(appPath, "http://test.com", false);
+  referrer = attributionSvc.getReferrerUrl(appPath);
+  equal(referrer, url, "referrer url is not changed");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/attribution/test/xpcshell/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_AttributionCode.js]
+skip-if = os != 'win' # windows specific tests
+[test_attribution.js]
+skip-if = toolkit != "cocoa" # osx specific tests
--- a/browser/components/build/nsBrowserCompsCID.h
+++ b/browser/components/build/nsBrowserCompsCID.h
@@ -36,8 +36,16 @@
 
 // 7e4bb6ad-2fc4-4dc6-89ef-23e8e5ccf980
 #define NS_BROWSER_ABOUT_REDIRECTOR_CID \
 { 0x7e4bb6ad, 0x2fc4, 0x4dc6, { 0x89, 0xef, 0x23, 0xe8, 0xe5, 0xcc, 0xf9, 0x80 } }
 
 // {6DEB193C-F87D-4078-BC78-5E64655B4D62}
 #define NS_BROWSERDIRECTORYPROVIDER_CID \
 { 0x6deb193c, 0xf87d, 0x4078, { 0xbc, 0x78, 0x5e, 0x64, 0x65, 0x5b, 0x4d, 0x62 } }
+
+#if defined(MOZ_WIDGET_COCOA)
+#define NS_MACATTRIBUTIONSERVICE_CONTRACTID \
+  "@mozilla.org/mac-attribution;1"
+
+#define NS_MACATTRIBUTIONSERVICE_CID \
+{ 0x6FC66A78, 0x6CBC, 0x4B3F, { 0xB7, 0xBA, 0x37, 0x92, 0x89, 0xB2, 0x92, 0x76 } }
+#endif
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -11,16 +11,20 @@
 #if defined(XP_WIN)
 #include "nsWindowsShellService.h"
 #elif defined(XP_MACOSX)
 #include "nsMacShellService.h"
 #elif defined(MOZ_WIDGET_GTK)
 #include "nsGNOMEShellService.h"
 #endif
 
+#if defined(MOZ_WIDGET_COCOA)
+#include "nsMacAttribution.h"
+#endif
+
 #if defined(XP_WIN)
 #include "nsIEHistoryEnumerator.h"
 #endif
 
 #include "nsFeedSniffer.h"
 #include "AboutRedirector.h"
 #include "nsIAboutModule.h"
 
@@ -34,16 +38,20 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(Directory
 #if defined(XP_WIN)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsWindowsShellService)
 #elif defined(XP_MACOSX)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsMacShellService)
 #elif defined(MOZ_WIDGET_GTK)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsGNOMEShellService, Init)
 #endif
 
+#if defined(MOZ_WIDGET_COCOA)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsMacAttributionService)
+#endif
+
 #if defined(XP_WIN)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsIEHistoryEnumerator)
 #endif
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsFeedSniffer)
 
 NS_DEFINE_NAMED_CID(NS_BROWSERDIRECTORYPROVIDER_CID);
 #if defined(XP_WIN)
@@ -53,31 +61,37 @@ NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID)
 #endif
 NS_DEFINE_NAMED_CID(NS_FEEDSNIFFER_CID);
 NS_DEFINE_NAMED_CID(NS_BROWSER_ABOUT_REDIRECTOR_CID);
 #if defined(XP_WIN)
 NS_DEFINE_NAMED_CID(NS_WINIEHISTORYENUMERATOR_CID);
 #elif defined(XP_MACOSX)
 NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
 #endif
+#if defined(MOZ_WIDGET_COCOA)
+NS_DEFINE_NAMED_CID(NS_MACATTRIBUTIONSERVICE_CID);
+#endif
 
 static const mozilla::Module::CIDEntry kBrowserCIDs[] = {
     { &kNS_BROWSERDIRECTORYPROVIDER_CID, false, nullptr, DirectoryProviderConstructor },
 #if defined(XP_WIN)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsWindowsShellServiceConstructor },
 #elif defined(MOZ_WIDGET_GTK)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsGNOMEShellServiceConstructor },
 #endif
     { &kNS_FEEDSNIFFER_CID, false, nullptr, nsFeedSnifferConstructor },
     { &kNS_BROWSER_ABOUT_REDIRECTOR_CID, false, nullptr, AboutRedirector::Create },
 #if defined(XP_WIN)
     { &kNS_WINIEHISTORYENUMERATOR_CID, false, nullptr, nsIEHistoryEnumeratorConstructor },
 #elif defined(XP_MACOSX)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsMacShellServiceConstructor },
 #endif
+#if defined(MOZ_WIDGET_COCOA)
+    { &kNS_MACATTRIBUTIONSERVICE_CID, false, nullptr, nsMacAttributionServiceConstructor },
+#endif
     { nullptr }
 };
 
 static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
     { NS_BROWSERDIRECTORYPROVIDER_CONTRACTID, &kNS_BROWSERDIRECTORYPROVIDER_CID },
 #if defined(XP_WIN)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #elif defined(MOZ_WIDGET_GTK)
@@ -103,16 +117,19 @@ static const mozilla::Module::ContractID
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "restartrequired", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "welcome", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "policies", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #if defined(XP_WIN)
     { NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
 #elif defined(XP_MACOSX)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #endif
+#if defined(MOZ_WIDGET_COCOA)
+    { NS_MACATTRIBUTIONSERVICE_CONTRACTID, &kNS_MACATTRIBUTIONSERVICE_CID },
+#endif
     { nullptr }
 };
 
 static const mozilla::Module::CategoryEntry kBrowserCategories[] = {
     { XPCOM_DIRECTORY_PROVIDER_CATEGORY, "browser-directory-provider", NS_BROWSERDIRECTORYPROVIDER_CONTRACTID },
     { NS_CONTENT_SNIFFER_CATEGORY, "Feed Sniffer", NS_FEEDSNIFFER_CONTRACTID },
     { nullptr }
 };
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -29,16 +29,17 @@ with Files("safebrowsing/**"):
     BUG_COMPONENT = ("Toolkit", "Safe Browsing")
 
 with Files('controlcenter/**'):
     BUG_COMPONENT = ('Firefox', 'General')
 
 
 DIRS += [
     'about',
+    'attribution',
     'contextualidentity',
     'customizableui',
     'dirprovider',
     'downloads',
     'enterprisepolicies',
     'extensions',
     'feeds',
     'library',
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -23,37 +23,31 @@ with Files("test/browser/browser_Unsubmi
     BUG_COMPONENT = ("Toolkit", "Crash Reporting")
 
 with Files("test/browser/browser_taskbar_preview.js"):
     BUG_COMPONENT = ("Firefox", "Shell Integration")
 
 with Files("test/browser/browser_urlBar_zoom.js"):
     BUG_COMPONENT = ("Firefox", "General")
 
-with Files("test/unit/test_AttributionCode.js"):
-    BUG_COMPONENT = ("Toolkit", "Telemetry")
-
 with Files("test/unit/test_E10SUtils_nested_URIs.js"):
     BUG_COMPONENT = ("Core", "Security: Process Sandboxing")
 
 with Files("test/unit/test_LaterRun.js"):
     BUG_COMPONENT = ("Firefox", "Tours")
 
 with Files("test/unit/test_SitePermissions.js"):
     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
 
 with Files("AboutNewTab.jsm"):
     BUG_COMPONENT = ("Firefox", "New Tab Page")
 
 with Files('AsyncTabSwitcher.jsm'):
     BUG_COMPONENT = ('Firefox', 'Tabbed Browser')
 
-with Files("AttributionCode.jsm"):
-    BUG_COMPONENT = ("Toolkit", "Telemetry")
-
 with Files("BrowserWindowTracker.jsm"):
     BUG_COMPONENT = ("Core", "Networking")
 
 with Files("*Telemetry.jsm"):
     BUG_COMPONENT = ("Toolkit", "Telemetry")
 
 with Files("ContentCrashHandlers.jsm"):
     BUG_COMPONENT = ("Toolkit", "Crash Reporting")
@@ -135,17 +129,16 @@ BROWSER_CHROME_MANIFESTS += [
     'test/browser/browser.ini',
     'test/browser/formValidation/browser.ini',
 ]
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 EXTRA_JS_MODULES += [
     'AboutNewTab.jsm',
     'AsyncTabSwitcher.jsm',
-    'AttributionCode.jsm',
     'BlockedSiteContent.jsm',
     'BrowserErrorReporter.jsm',
     'BrowserUsageTelemetry.jsm',
     'BrowserWindowTracker.jsm',
     'ClickEventHandler.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.jsm',
--- a/browser/modules/test/unit/xpcshell.ini
+++ b/browser/modules/test/unit/xpcshell.ini
@@ -1,12 +1,10 @@
 [DEFAULT]
 head =
 firefox-appdir = browser
 skip-if = toolkit == 'android'
 
-[test_AttributionCode.js]
-skip-if = os != 'win'
 [test_E10SUtils_nested_URIs.js]
 [test_HomePage.js]
 [test_Sanitizer_interrupted.js]
 [test_SitePermissions.js]
 [test_LaterRun.js]
--- a/xpcom/io/CocoaFileUtils.h
+++ b/xpcom/io/CocoaFileUtils.h
@@ -6,30 +6,34 @@
 
 // This namespace contains methods with Obj-C/Cocoa implementations. The header
 // is C/C++ for inclusion in C/C++-only files.
 
 #ifndef CocoaFileUtils_h_
 #define CocoaFileUtils_h_
 
 #include "nscore.h"
+#include "nsString.h"
 #include <CoreFoundation/CoreFoundation.h>
 
 namespace CocoaFileUtils {
 
 nsresult RevealFileInFinder(CFURLRef aUrl);
 nsresult OpenURL(CFURLRef aUrl);
 nsresult GetFileCreatorCode(CFURLRef aUrl, OSType* aCreatorCode);
 nsresult SetFileCreatorCode(CFURLRef aUrl, OSType aCreatorCode);
 nsresult GetFileTypeCode(CFURLRef aUrl, OSType* aTypeCode);
 nsresult SetFileTypeCode(CFURLRef aUrl, OSType aTypeCode);
 void     AddOriginMetadataToFile(const CFStringRef filePath,
                                  const CFURLRef sourceURL,
                                  const CFURLRef referrerURL);
 void     AddQuarantineMetadataToFile(const CFStringRef filePath,
                                      const CFURLRef sourceURL,
                                      const CFURLRef referrerURL,
-                                     const bool isFromWeb);
+                                     const bool isFromWeb,
+                                     const bool createProps=false);
+void CopyQuarantineReferrerUrl(const CFStringRef aFilePath,
+                               nsAString& aReferrer);
 CFURLRef GetTemporaryFolderCFURLRef();
 
 } // namespace CocoaFileUtils
 
 #endif
--- a/xpcom/io/CocoaFileUtils.mm
+++ b/xpcom/io/CocoaFileUtils.mm
@@ -5,16 +5,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "CocoaFileUtils.h"
 #include "nsCocoaFeatures.h"
 #include "nsCocoaUtils.h"
 #include <Cocoa/Cocoa.h>
 #include "nsObjCExceptions.h"
 #include "nsDebug.h"
+#include "nsString.h"
+#include "mozilla/MacStringHelpers.h"
 
 // Need to cope with us using old versions of the SDK and needing this on 10.10+
 #if !defined(MAC_OS_X_VERSION_10_10) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_10)
 const CFStringRef kCFURLQuarantinePropertiesKey = CFSTR("NSURLQuarantinePropertiesKey");
 #endif
 
 namespace CocoaFileUtils {
 
@@ -186,82 +188,134 @@ void AddOriginMetadataToFile(const CFStr
   }
 
   mdItemSetAttributeFunc(mdItem, kMDItemWhereFroms, list);
 
   ::CFRelease(list);
   ::CFRelease(mdItem);
 }
 
+CFStringRef GetQuarantinePropKey() {
+  if (nsCocoaFeatures::OnYosemiteOrLater()) {
+    return kCFURLQuarantinePropertiesKey;
+  }
+  return kLSItemQuarantineProperties;
+}
+
+CFMutableDictionaryRef CreateQuarantineDictionary(const CFURLRef aFileURL,
+                                                  const bool aCreateProps) {
+  // The properties key changed in 10.10:
+  CFDictionaryRef quarantineProps = NULL;
+  if (aCreateProps) {
+    quarantineProps = ::CFDictionaryCreate(NULL, NULL, NULL, 0,
+                                           &kCFTypeDictionaryKeyCallBacks,
+                                           &kCFTypeDictionaryValueCallBacks);
+  } else {
+    Boolean success = ::CFURLCopyResourcePropertyForKey(aFileURL,
+                                                        GetQuarantinePropKey(),
+                                                        &quarantineProps,
+                                                        NULL);
+    // If there aren't any quarantine properties then the user probably
+    // set up an exclusion and we don't need to add metadata.
+    if (!success || !quarantineProps) {
+      return NULL;
+    }
+  }
+
+  // We don't know what to do if the props aren't a dictionary.
+  if (::CFGetTypeID(quarantineProps) != ::CFDictionaryGetTypeID()) {
+    ::CFRelease(quarantineProps);
+    return NULL;
+  }
+
+  // Make a mutable copy of the properties.
+  CFMutableDictionaryRef mutQuarantineProps =
+    ::CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0,
+                                    (CFDictionaryRef)quarantineProps);
+  ::CFRelease(quarantineProps);
+
+  return mutQuarantineProps;
+}
+
 void AddQuarantineMetadataToFile(const CFStringRef filePath,
                                  const CFURLRef sourceURL,
                                  const CFURLRef referrerURL,
-                                 const bool isFromWeb) {
+                                 const bool isFromWeb,
+                                 const bool createProps /* = false */) {
   CFURLRef fileURL = ::CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
                                                      filePath,
                                                      kCFURLPOSIXPathStyle,
                                                      false);
 
-  // The properties key changed in 10.10:
-  CFStringRef quarantinePropKey;
-  if (nsCocoaFeatures::OnYosemiteOrLater()) {
-    quarantinePropKey = kCFURLQuarantinePropertiesKey;
-  } else {
-    quarantinePropKey = kLSItemQuarantineProperties;
-  }
-  CFDictionaryRef quarantineProps = NULL;
-  Boolean success = ::CFURLCopyResourcePropertyForKey(fileURL,
-                                                      quarantinePropKey,
-                                                      &quarantineProps,
-                                                      NULL);
-
-  // If there aren't any quarantine properties then the user probably
-  // set up an exclusion and we don't need to add metadata.
-  if (!success || !quarantineProps) {
+  CFMutableDictionaryRef mutQuarantineProps =
+    CreateQuarantineDictionary(fileURL, createProps);
+  if (!mutQuarantineProps) {
     ::CFRelease(fileURL);
     return;
   }
 
-  // We don't know what to do if the props aren't a dictionary.
-  if (::CFGetTypeID(quarantineProps) != ::CFDictionaryGetTypeID()) {
-    ::CFRelease(fileURL);
-    ::CFRelease(quarantineProps);
-    return;
-  }
-
-  // Make a mutable copy of the properties.
-  CFMutableDictionaryRef mutQuarantineProps =
-    ::CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, (CFDictionaryRef)quarantineProps);
-  ::CFRelease(quarantineProps);
-
   // Add metadata that the OS couldn't infer.
 
   if (!::CFDictionaryGetValue(mutQuarantineProps, kLSQuarantineTypeKey)) {
-    CFStringRef type = isFromWeb ? kLSQuarantineTypeWebDownload : kLSQuarantineTypeOtherDownload;
+    CFStringRef type = isFromWeb ? kLSQuarantineTypeWebDownload :
+                                   kLSQuarantineTypeOtherDownload;
     ::CFDictionarySetValue(mutQuarantineProps, kLSQuarantineTypeKey, type);
   }
 
-  if (!::CFDictionaryGetValue(mutQuarantineProps, kLSQuarantineOriginURLKey) && referrerURL) {
-    ::CFDictionarySetValue(mutQuarantineProps, kLSQuarantineOriginURLKey, referrerURL);
+  if (!::CFDictionaryGetValue(mutQuarantineProps, kLSQuarantineOriginURLKey) &&
+      referrerURL) {
+    ::CFDictionarySetValue(mutQuarantineProps,
+                           kLSQuarantineOriginURLKey,
+                           referrerURL);
   }
 
-  if (!::CFDictionaryGetValue(mutQuarantineProps, kLSQuarantineDataURLKey) && sourceURL) {
-    ::CFDictionarySetValue(mutQuarantineProps, kLSQuarantineDataURLKey, sourceURL);
+  if (!::CFDictionaryGetValue(mutQuarantineProps, kLSQuarantineDataURLKey) &&
+      sourceURL) {
+    ::CFDictionarySetValue(mutQuarantineProps,
+                           kLSQuarantineDataURLKey,
+                           sourceURL);
   }
 
   // Set quarantine properties on file.
   ::CFURLSetResourcePropertyForKey(fileURL,
-                                   quarantinePropKey,
+                                   GetQuarantinePropKey(),
                                    mutQuarantineProps,
                                    NULL);
 
   ::CFRelease(fileURL);
   ::CFRelease(mutQuarantineProps);
 }
 
+void CopyQuarantineReferrerUrl(const CFStringRef aFilePath,
+                               nsAString& aReferrer)
+{
+  CFURLRef fileURL = ::CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
+                                                     aFilePath,
+                                                     kCFURLPOSIXPathStyle,
+                                                     false);
+
+  CFMutableDictionaryRef mutQuarantineProps =
+    CreateQuarantineDictionary(fileURL, false);
+  ::CFRelease(fileURL);
+  if (!mutQuarantineProps) {
+    return;
+  }
+
+  CFTypeRef referrerRef = ::CFDictionaryGetValue(mutQuarantineProps,
+                                                 kLSQuarantineOriginURLKey);
+  if (referrerRef && ::CFGetTypeID(referrerRef) == ::CFURLGetTypeID()) {
+    // URL string must be copied prior to releasing the dictionary.
+    mozilla::CopyCocoaStringToXPCOMString(
+      (NSString*)::CFURLGetString(static_cast<CFURLRef>(referrerRef)),
+                                  aReferrer);
+  }
+
+  ::CFRelease(mutQuarantineProps);
+}
+
 CFURLRef GetTemporaryFolderCFURLRef()
 {
   NSString* tempDir = ::NSTemporaryDirectory();
   return tempDir == nil ? NULL : (CFURLRef)[NSURL fileURLWithPath:tempDir
                                                       isDirectory:YES];
 }
 
 } // namespace CocoaFileUtils