Bug 1197451 - Add clipboardWrite permission r=billm
authorRob Wu <rob@robwu.nl>
Fri, 08 Jul 2016 17:19:17 -0700
changeset 334820 2263d29df71686a2bea8255859867910fc66b220
parent 334819 5d16433ff39ebc0ac79ec07083e4a8a30973fe47
child 334821 f90a98f65eba0f954cfc98796238a25082d5217a
push id10033
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:50:26 +0000
treeherdermozilla-aurora@5dddbefdf759 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1197451
milestone51.0a1
Bug 1197451 - Add clipboardWrite permission r=billm MozReview-Commit-ID: 6d1mQSVWRPe
caps/BasePrincipal.cpp
caps/BasePrincipal.h
caps/nsIAddonPolicyService.idl
caps/nsPrincipal.cpp
caps/nsPrincipal.h
dom/base/nsContentUtils.cpp
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/schemas/manifest.json
toolkit/components/extensions/test/mochitest/mochitest.ini
toolkit/components/extensions/test/mochitest/test_clipboard.html
toolkit/components/utils/simpleServices.js
--- a/caps/BasePrincipal.cpp
+++ b/caps/BasePrincipal.cpp
@@ -581,16 +581,32 @@ BasePrincipal::GetIsInIsolatedMozBrowser
 
 NS_IMETHODIMP
 BasePrincipal::GetUnknownAppId(bool* aUnknownAppId)
 {
   *aUnknownAppId = AppId() == nsIScriptSecurityManager::UNKNOWN_APP_ID;
   return NS_OK;
 }
 
+bool
+BasePrincipal::AddonHasPermission(const nsAString& aPerm)
+{
+  if (mOriginAttributes.mAddonId.IsEmpty()) {
+    return false;
+  }
+  nsCOMPtr<nsIAddonPolicyService> aps =
+    do_GetService("@mozilla.org/addons/policy-service;1");
+  NS_ENSURE_TRUE(aps, false);
+
+  bool retval = false;
+  nsresult rv = aps->AddonHasPermission(mOriginAttributes.mAddonId, aPerm, &retval);
+  NS_ENSURE_SUCCESS(rv, false);
+  return retval;
+}
+
 already_AddRefed<BasePrincipal>
 BasePrincipal::CreateCodebasePrincipal(nsIURI* aURI, const PrincipalOriginAttributes& aAttrs)
 {
   // If the URI is supposed to inherit the security context of whoever loads it,
   // we shouldn't make a codebase principal for it.
   bool inheritsPrincipal;
   nsresult rv = NS_URIChainHasFlags(aURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT,
                                     &inheritsPrincipal);
--- a/caps/BasePrincipal.h
+++ b/caps/BasePrincipal.h
@@ -261,16 +261,18 @@ public:
   NS_IMETHOD GetAppStatus(uint16_t* aAppStatus) final;
   NS_IMETHOD GetAppId(uint32_t* aAppStatus) final;
   NS_IMETHOD GetAddonId(nsAString& aAddonId) final;
   NS_IMETHOD GetIsInIsolatedMozBrowserElement(bool* aIsInIsolatedMozBrowserElement) final;
   NS_IMETHOD GetUnknownAppId(bool* aUnknownAppId) final;
   NS_IMETHOD GetUserContextId(uint32_t* aUserContextId) final;
   NS_IMETHOD GetPrivateBrowsingId(uint32_t* aPrivateBrowsingId) final;
 
+  virtual bool AddonHasPermission(const nsAString& aPerm);
+
   virtual bool IsOnCSSUnprefixingWhitelist() override { return false; }
 
   virtual bool IsCodebasePrincipal() const { return false; };
 
   static BasePrincipal* Cast(nsIPrincipal* aPrin) { return static_cast<BasePrincipal*>(aPrin); }
   static already_AddRefed<BasePrincipal>
   CreateCodebasePrincipal(nsIURI* aURI, const PrincipalOriginAttributes& aAttrs);
   static already_AddRefed<BasePrincipal> CreateCodebasePrincipal(const nsACString& aOrigin);
--- a/caps/nsIAddonPolicyService.idl
+++ b/caps/nsIAddonPolicyService.idl
@@ -36,16 +36,21 @@ interface nsIAddonPolicyService : nsISup
   /**
    * Returns the generated background page as a data-URI, if any. If the addon
    * does not have an auto-generated background page, an empty string is
    * returned.
    */
   ACString getGeneratedBackgroundPageUrl(in ACString aAddonId);
 
   /**
+   * Returns true if the addon was granted the |aPerm| API permission.
+   */
+  boolean addonHasPermission(in AString aAddonId, in AString aPerm);
+
+  /**
    * Returns true if unprivileged code associated with the given addon may load
    * data from |aURI|.
    */
   boolean addonMayLoadURI(in AString aAddonId, in nsIURI aURI);
 
   /**
    * Returns true if a given extension:// URI is web-accessible.
    */
--- a/caps/nsPrincipal.cpp
+++ b/caps/nsPrincipal.cpp
@@ -782,16 +782,27 @@ nsExpandedPrincipal::GetWhiteList(nsTArr
 
 NS_IMETHODIMP
 nsExpandedPrincipal::GetBaseDomain(nsACString& aBaseDomain)
 {
   return NS_ERROR_NOT_AVAILABLE;
 }
 
 bool
+nsExpandedPrincipal::AddonHasPermission(const nsAString& aPerm)
+{
+  for (size_t i = 0; i < mPrincipals.Length(); ++i) {
+    if (BasePrincipal::Cast(mPrincipals[i])->AddonHasPermission(aPerm)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool
 nsExpandedPrincipal::IsOnCSSUnprefixingWhitelist()
 {
   // CSS Unprefixing Whitelist is a per-origin thing; doesn't really make sense
   // for an expanded principal. (And probably shouldn't be needed.)
   return false;
 }
 
 
--- a/caps/nsPrincipal.h
+++ b/caps/nsPrincipal.h
@@ -74,16 +74,17 @@ public:
   NS_IMETHODIMP_(MozExternalRefCountType) AddRef() override { return nsJSPrincipals::AddRef(); };
   NS_IMETHODIMP_(MozExternalRefCountType) Release() override { return nsJSPrincipals::Release(); };
   NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
   NS_IMETHOD GetHashValue(uint32_t* aHashValue) override;
   NS_IMETHOD GetURI(nsIURI** aURI) override;
   NS_IMETHOD GetDomain(nsIURI** aDomain) override;
   NS_IMETHOD SetDomain(nsIURI* aDomain) override;
   NS_IMETHOD GetBaseDomain(nsACString& aBaseDomain) override;
+  virtual bool AddonHasPermission(const nsAString& aPerm) override;
   virtual bool IsOnCSSUnprefixingWhitelist() override;
   virtual void GetScriptLocation(nsACString &aStr) override;
   nsresult GetOriginInternal(nsACString& aOrigin) override;
 
   PrincipalKind Kind() override { return eExpandedPrincipal; }
 
 protected:
   virtual ~nsExpandedPrincipal();
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -93,16 +93,17 @@
 #include "nsError.h"
 #include "nsFocusManager.h"
 #include "nsGenericHTMLElement.h"
 #include "nsGenericHTMLFrameElement.h"
 #include "nsGkAtoms.h"
 #include "nsHostObjectProtocolHandler.h"
 #include "nsHtml5Module.h"
 #include "nsHtml5StringParser.h"
+#include "nsIAddonPolicyService.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsICategoryManager.h"
 #include "nsIChannelEventSink.h"
 #include "nsICharsetDetectionObserver.h"
 #include "nsIChromeRegistry.h"
 #include "nsIConsoleService.h"
 #include "nsIContent.h"
 #include "nsIContentSecurityPolicy.h"
@@ -6754,19 +6755,22 @@ nsContentUtils::IsRequestFullScreenAllow
          EventStateManager::IsHandlingUserInput() ||
          IsCallerChrome();
 }
 
 /* static */
 bool
 nsContentUtils::IsCutCopyAllowed()
 {
-  return (!IsCutCopyRestricted() &&
-          EventStateManager::IsHandlingUserInput()) ||
-         IsCallerChrome();
+  if ((!IsCutCopyRestricted() && EventStateManager::IsHandlingUserInput()) ||
+      IsCallerChrome()) {
+    return true;
+  }
+
+  return BasePrincipal::Cast(SubjectPrincipal())->AddonHasPermission(NS_LITERAL_STRING("clipboardWrite"));
 }
 
 /* static */
 bool
 nsContentUtils::IsFrameTimingEnabled()
 {
   return sIsFrameTimingPrefEnabled;
 }
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -799,16 +799,20 @@ BrowserExtensionContent.prototype = {
 
   localizeMessage(...args) {
     return this.localeData.localizeMessage(...args);
   },
 
   localize(...args) {
     return this.localeData.localize(...args);
   },
+
+  hasPermission(perm) {
+    return this.permissions.has(perm);
+  },
 };
 
 ExtensionManager = {
   // Map[extensionId, BrowserExtensionContent]
   extensions: new Map(),
 
   init() {
     Schemas.init();
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -161,26 +161,28 @@ var Service = {
       uri = Services.io.newURI("jar:remoteopen" + uri.spec.substr("jar:".length), null, null);
     }
 
     let handler = Services.io.getProtocolHandler("moz-extension");
     handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
     handler.setSubstitution(uuid, uri);
 
     this.uuidMap.set(uuid, extension);
+    this.aps.setAddonHasPermissionCallback(extension.id, extension.hasPermission.bind(extension));
     this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
     this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension));
     this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy);
     this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.bind(this, extension));
   },
 
   // Called when an extension is unloaded.
   shutdownExtension(uuid) {
     let extension = this.uuidMap.get(uuid);
     this.uuidMap.delete(uuid);
+    this.aps.setAddonHasPermissionCallback(extension.id, null);
     this.aps.setAddonLoadURICallback(extension.id, null);
     this.aps.setAddonLocalizeCallback(extension.id, null);
     this.aps.setAddonCSP(extension.id, null);
     this.aps.setBackgroundPageUrlCallback(uuid, null);
 
     let handler = Services.io.getProtocolHandler("moz-extension");
     handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
     handler.setSubstitution(uuid, null);
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -182,16 +182,17 @@
       },
       {
         "id": "Permission",
         "choices": [
           {
             "type": "string",
             "enum": [
               "alarms",
+              "clipboardWrite",
               "idle",
               "notifications",
               "storage"
             ]
           },
           { "$ref": "MatchPattern" }
         ]
       },
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -30,16 +30,18 @@ support-files =
   file_script_redirect.js
   file_script_xhr.js
   file_sample.html
   redirection.sjs
   file_privilege_escalation.html
   file_ext_test_api_injection.js
   file_permission_xhr.html
 
+[test_clipboard.html]
+# skip-if = # disabled test case with_permission_allow_copy, see inline comment.
 [test_ext_inIncognitoContext_window.html]
 skip-if = os == 'android' # Android does not currently support windows.
 [test_ext_geturl.html]
 [test_ext_background_canvas.html]
 [test_ext_content_security_policy.html]
 [test_ext_contentscript.html]
 skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
 [test_ext_contentscript_api_injection.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_clipboard.html
@@ -0,0 +1,140 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>clipboard permission test</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script src="head.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+function doCopy(txt) {
+  let field = document.createElement("textarea");
+  document.body.appendChild(field);
+  field.value = txt;
+  field.select();
+  return document.execCommand("copy");
+}
+
+add_task(function* no_permission_deny_copy() {
+  function backgroundScript() {
+    browser.test.assertEq(false, doCopy("whatever"),
+      "copy should be denied without permission");
+    browser.test.sendMessage("ready");
+  }
+  let extensionData = {
+    background: `${doCopy};(${backgroundScript})();`,
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  yield extension.startup();
+
+  yield extension.awaitMessage("ready");
+
+  yield extension.unload();
+});
+
+/** Selecting text in a bg page is not possible, skip test until it's fixed.
+add_task(function* with_permission_allow_copy() {
+  function backgroundScript() {
+    browser.test.onMessage.addListener(txt => {
+      browser.test.assertEq(true, doCopy(txt),
+        "copy should be allowed with permission");
+    });
+    browser.test.sendMessage("ready");
+  }
+  let extensionData = {
+    background: `${doCopy};(${backgroundScript})();`,
+    manifest: {
+      permissions: [
+        "clipboardWrite",
+      ],
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+
+  const DUMMY_STR = "dummy string to copy";
+  yield new Promise(resolve => {
+    SimpleTest.waitForClipboard(DUMMY_STR, () => {
+      extension.sendMessage(DUMMY_STR);
+    }, resolve, resolve);
+  });
+
+  yield extension.unload();
+}); */
+
+add_task(function* content_script_no_permission_deny_copy() {
+  function contentScript() {
+    browser.test.assertEq(false, doCopy("whatever"),
+      "copy should be denied without permission");
+    browser.test.sendMessage("ready");
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["contentscript.js"],
+        matches: ["http://mochi.test/*/file_sample.html"],
+      }],
+    },
+    files: {
+      "contentscript.js": `${doCopy};(${contentScript})();`,
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  yield extension.startup();
+
+  let win = window.open("file_sample.html");
+  yield extension.awaitMessage("ready");
+  win.close();
+
+  yield extension.unload();
+});
+
+add_task(function* content_script_with_permission_allow_copy() {
+  function contentScript() {
+    browser.test.onMessage.addListener(txt => {
+      browser.test.assertEq(true, doCopy(txt),
+        "copy should be allowed with permission");
+    });
+    browser.test.sendMessage("ready");
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["contentscript.js"],
+        matches: ["http://mochi.test/*/file_sample.html"],
+      }],
+      permissions: [
+        "clipboardWrite",
+      ],
+    },
+    files: {
+      "contentscript.js": `${doCopy};(${contentScript})();`,
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  yield extension.startup();
+
+  let win = window.open("file_sample.html");
+  yield extension.awaitMessage("ready");
+
+  const DUMMY_STR = "dummy string to copy in content script";
+  yield new Promise(resolve => {
+    SimpleTest.waitForClipboard(DUMMY_STR, () => {
+      extension.sendMessage(DUMMY_STR);
+    }, resolve, resolve);
+  });
+
+  win.close();
+
+  yield extension.unload();
+});
+</script>
+</body>
+</html>
--- a/toolkit/components/utils/simpleServices.js
+++ b/toolkit/components/utils/simpleServices.js
@@ -24,16 +24,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 function AddonPolicyService()
 {
   this.wrappedJSObject = this;
   this.cspStrings = new Map();
   this.backgroundPageUrlCallbacks = new Map();
+  this.checkHasPermissionCallbacks = new Map();
   this.mayLoadURICallbacks = new Map();
   this.localizeCallbacks = new Map();
 
   XPCOMUtils.defineLazyPreferenceGetter(
     this, "baseCSP", "extensions.webextensions.base-content-security-policy",
     "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " +
     "object-src 'self' https://* moz-extension: blob: filesystem:;");
 
@@ -63,16 +64,27 @@ AddonPolicyService.prototype = {
    */
   getGeneratedBackgroundPageUrl(aAddonId) {
     let cb = this.backgroundPageUrlCallbacks.get(aAddonId);
     return cb && cb(aAddonId) || '';
   },
 
   /*
    * Invokes a callback (if any) associated with the addon to determine whether
+   * the addon is granted the |aPerm| API permission.
+   *
+   * @see nsIAddonPolicyService.addonHasPermission
+   */
+  addonHasPermission(aAddonId, aPerm) {
+    let cb = this.checkHasPermissionCallbacks.get(aAddonId);
+    return cb ? cb(aPerm) : false;
+  },
+
+  /*
+   * Invokes a callback (if any) associated with the addon to determine whether
    * unprivileged code running within the addon is allowed to perform loads from
    * the given URI.
    *
    * @see nsIAddonPolicyService.addonMayLoadURI
    */
   addonMayLoadURI(aAddonId, aURI) {
     let cb = this.mayLoadURICallbacks.get(aAddonId);
     return cb ? cb(aURI) : false;
@@ -115,16 +127,29 @@ AddonPolicyService.prototype = {
     let cb = this.extensionURIToAddonIdCallback;
     if (!cb) {
       throw new Error("no callback set to map extension URIs to addon Ids");
     }
     return cb(aURI);
   },
 
   /*
+   * Sets the callbacks used in addonHasPermission above. Not accessible over
+   * XPCOM - callers should use .wrappedJSObject on the service to call it
+   * directly.
+   */
+  setAddonHasPermissionCallback(aAddonId, aCallback) {
+    if (aCallback) {
+      this.checkHasPermissionCallbacks.set(aAddonId, aCallback);
+    } else {
+      this.checkHasPermissionCallbacks.delete(aAddonId);
+    }
+  },
+
+  /*
    * Sets the callbacks used in addonMayLoadURI above. Not accessible over
    * XPCOM - callers should use .wrappedJSObject on the service to call it
    * directly.
    */
   setAddonLoadURICallback(aAddonId, aCallback) {
     if (aCallback) {
       this.mayLoadURICallbacks.set(aAddonId, aCallback);
     } else {