Bug 1197451 - Add clipboardWrite permission r=billm
authorRob Wu <rob@robwu.nl>
Fri, 08 Jul 2016 17:19:17 -0700
changeset 350356 2263d29df71686a2bea8255859867910fc66b220
parent 350355 5d16433ff39ebc0ac79ec07083e4a8a30973fe47
child 350357 f90a98f65eba0f954cfc98796238a25082d5217a
push id6570
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:26:13 +0000
treeherdermozilla-beta@f455459b2ae5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1197451
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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 {