Bug 1384689: Add a helper for adding dynamic chrome registry entries. f=Mossop r=froydnj
authorKris Maglione <maglione.k@gmail.com>
Thu, 03 Aug 2017 20:32:25 -0700
changeset 645661 be93c09fd3e37112fda7693205b7c1f70675091f
parent 645660 4be1e24368459311e7e1cc2bddf895f5f205e829
child 645662 d869a081b44f2a7af39ddc0ba0b16fa41b75f928
child 645665 2b1ff264612d9b06350dee43571f36292b53ecf9
child 645767 7d1659a7c5828478eaa33846775064724dca6ef1
push id73820
push userbmo:gl@mozilla.com
push dateSun, 13 Aug 2017 22:09:20 +0000
reviewersfroydnj
bugs1384689
milestone57.0a1
Bug 1384689: Add a helper for adding dynamic chrome registry entries. f=Mossop r=froydnj I went with the simplest possible approach here, and only added support for "locale" and "override" entries, since we don't expect this to stick around very long. MozReview-Commit-ID: IDQ86s3jgnu
toolkit/mozapps/extensions/AddonManagerStartup.cpp
toolkit/mozapps/extensions/AddonManagerStartup.h
toolkit/mozapps/extensions/amIAddonManagerStartup.idl
toolkit/mozapps/extensions/moz.build
toolkit/mozapps/extensions/test/xpcshell/test_registerchrome.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
xpcom/components/nsComponentManager.cpp
--- a/toolkit/mozapps/extensions/AddonManagerStartup.cpp
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.cpp
@@ -9,31 +9,37 @@
 #include "jsapi.h"
 #include "jsfriendapi.h"
 #include "js/TracingAPI.h"
 #include "xpcpublic.h"
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/EndianUtils.h"
 #include "mozilla/Compression.h"
+#include "mozilla/LinkedList.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/ScopeExit.h"
 #include "mozilla/Services.h"
 #include "mozilla/Unused.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/ipc/StructuredCloneData.h"
 
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsAppRunner.h"
 #include "nsContentUtils.h"
+#include "nsChromeRegistry.h"
 #include "nsIAddonInterposition.h"
+#include "nsIDOMWindowUtils.h" // for nsIJSRAIIHelper
+#include "nsIFileURL.h"
 #include "nsIIOService.h"
 #include "nsIJARProtocolHandler.h"
+#include "nsIJARURI.h"
 #include "nsIStringEnumerator.h"
 #include "nsIZipReader.h"
+#include "nsJSUtils.h"
 #include "nsReadableUtils.h"
 #include "nsXULAppAPI.h"
 
 #include <stdlib.h>
 
 namespace mozilla {
 
 template <>
@@ -103,17 +109,17 @@ AddonManagerStartup::ProfileDir()
 
     rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mProfileDir));
     MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
   }
 
   return mProfileDir;
 }
 
-NS_IMPL_ISUPPORTS(AddonManagerStartup, amIAddonManagerStartup)
+NS_IMPL_ISUPPORTS(AddonManagerStartup, amIAddonManagerStartup, nsIObserver)
 
 
 /*****************************************************************************
  * File utils
  *****************************************************************************/
 
 static already_AddRefed<nsIFile>
 CloneAndAppend(nsIFile* aFile, const char* name)
@@ -265,16 +271,47 @@ GetJarCache()
   MOZ_ASSERT(jar);
 
   nsCOMPtr<nsIZipReaderCache> zipCache;
   NS_TRY(jar->GetJARCache(getter_AddRefs(zipCache)));
 
   return Move(zipCache);
 }
 
+static Result<FileLocation, nsresult>
+GetFileLocation(nsIURI* uri)
+{
+  FileLocation location;
+
+  nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri);
+  nsCOMPtr<nsIFile> file;
+  if (fileURL) {
+    NS_TRY(fileURL->GetFile(getter_AddRefs(file)));
+    location.Init(file);
+  } else {
+    nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri);
+    NS_ENSURE_TRUE(jarURI, Err(NS_ERROR_INVALID_ARG));
+
+    nsCOMPtr<nsIURI> fileURI;
+    NS_TRY(jarURI->GetJARFile(getter_AddRefs(fileURI)));
+
+    fileURL = do_QueryInterface(fileURI);
+    NS_ENSURE_TRUE(fileURL, Err(NS_ERROR_INVALID_ARG));
+
+    NS_TRY(fileURL->GetFile(getter_AddRefs(file)));
+
+    nsCString entry;
+    NS_TRY(jarURI->GetJAREntry(entry));
+
+    location.Init(file, entry.get());
+  }
+
+  return Move(location);
+}
+
 
 /*****************************************************************************
  * JSON data handling
  *****************************************************************************/
 
 class MOZ_STACK_CLASS WrapperBase {
 protected:
   WrapperBase(JSContext* cx, JSObject* object)
@@ -766,9 +803,182 @@ AddonManagerStartup::Reset()
   mInitialized = false;
 
   mExtensionPaths.Clear();
   mThemePaths.Clear();
 
   return NS_OK;
 }
 
+
+/******************************************************************************
+ * RegisterChrome
+ ******************************************************************************/
+
+namespace {
+static bool sObserverRegistered;
+
+class RegistryEntries final : public nsIJSRAIIHelper
+                            , public LinkedListElement<RegistryEntries>
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIJSRAIIHELPER
+
+  using Override = AutoTArray<nsCString, 2>;
+  using Locale = AutoTArray<nsCString, 3>;
+
+  RegistryEntries(FileLocation& location, nsTArray<Override>&& overrides, nsTArray<Locale>&& locales)
+    : mLocation(location)
+    , mOverrides(Move(overrides))
+    , mLocales(Move(locales))
+  {}
+
+  void Register();
+
+protected:
+  virtual ~RegistryEntries()
+  {
+    Unused << Destruct();
+  }
+
+private:
+  FileLocation mLocation;
+  const nsTArray<Override> mOverrides;
+  const nsTArray<Locale> mLocales;
+};
+
+NS_IMPL_ISUPPORTS(RegistryEntries, nsIJSRAIIHelper)
+
+void
+RegistryEntries::Register()
+{
+  RefPtr<nsChromeRegistry> cr = nsChromeRegistry::GetSingleton();
+
+  nsChromeRegistry::ManifestProcessingContext context(NS_EXTENSION_LOCATION, mLocation);
+
+  for (auto& override : mOverrides) {
+    const char* args[] = {override[0].get(), override[1].get()};
+    cr->ManifestOverride(context, 0, const_cast<char**>(args), 0);
+  }
+
+  for (auto& locale : mLocales) {
+    const char* args[] = {locale[0].get(), locale[1].get(), locale[2].get()};
+    cr->ManifestLocale(context, 0, const_cast<char**>(args), 0);
+  }
+}
+
+NS_IMETHODIMP
+RegistryEntries::Destruct()
+{
+  if (isInList()) {
+    remove();
+
+    // When we remove dynamic entries from the registry, we need to rebuild it
+    // in order to ensure a consistent state. See comments in Observe().
+    RefPtr<nsChromeRegistry> cr = nsChromeRegistry::GetSingleton();
+    return cr->CheckForNewChrome();
+  }
+  return NS_OK;
+}
+
+static LinkedList<RegistryEntries>&
+GetRegistryEntries()
+{
+  static LinkedList<RegistryEntries> sEntries;
+  return sEntries;
+}
+}; // anonymous namespace
+
+NS_IMETHODIMP
+AddonManagerStartup::RegisterChrome(nsIURI* manifestURI, JS::HandleValue locations,
+                                    JSContext* cx, nsIJSRAIIHelper** result)
+{
+  auto IsArray = [cx] (JS::HandleValue val) -> bool {
+    bool isArray;
+    return JS_IsArrayObject(cx, val, &isArray) && isArray;
+  };
+
+  NS_ENSURE_ARG_POINTER(manifestURI);
+  NS_ENSURE_TRUE(IsArray(locations), NS_ERROR_INVALID_ARG);
+
+  FileLocation location;
+  MOZ_TRY_VAR(location, GetFileLocation(manifestURI));
+
+
+  nsTArray<RegistryEntries::Locale> locales;
+  nsTArray<RegistryEntries::Override> overrides;
+
+  JS::RootedObject locs(cx, &locations.toObject());
+  JS::RootedValue arrayVal(cx);
+  JS::RootedObject array(cx);
+
+  for (auto elem : ArrayIter(cx, locs)) {
+    arrayVal = elem.Value();
+    NS_ENSURE_TRUE(IsArray(arrayVal), NS_ERROR_INVALID_ARG);
+
+    array = &arrayVal.toObject();
+
+    AutoTArray<nsCString, 4> vals;
+    for (auto val : ArrayIter(cx, array)) {
+      nsAutoJSString str;
+      NS_ENSURE_TRUE(str.init(cx, val.Value()), NS_ERROR_OUT_OF_MEMORY);
+
+      vals.AppendElement(NS_ConvertUTF16toUTF8(str));
+    }
+    NS_ENSURE_TRUE(vals.Length() > 0, NS_ERROR_INVALID_ARG);
+
+    nsCString type = vals[0];
+    vals.RemoveElementAt(0);
+
+    if (type.EqualsLiteral("override")) {
+      NS_ENSURE_TRUE(vals.Length() == 2, NS_ERROR_INVALID_ARG);
+      overrides.AppendElement(vals);
+    } else if (type.EqualsLiteral("locale")) {
+      NS_ENSURE_TRUE(vals.Length() == 3, NS_ERROR_INVALID_ARG);
+      locales.AppendElement(vals);
+    } else {
+      return NS_ERROR_INVALID_ARG;
+    }
+  }
+
+  if (!sObserverRegistered) {
+    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+    NS_ENSURE_TRUE(obs, NS_ERROR_UNEXPECTED);
+    obs->AddObserver(this, "chrome-manifests-loaded", false);
+
+    sObserverRegistered = true;
+  }
+
+  auto entry = MakeRefPtr<RegistryEntries>(location,
+                                           Move(overrides),
+                                           Move(locales));
+
+  entry->Register();
+  GetRegistryEntries().insertBack(entry);
+
+  entry.forget(result);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AddonManagerStartup::Observe(nsISupports* subject, const char* topic, const char16_t* data)
+{
+  // The chrome registry is maintained as a set of global resource mappings
+  // generated mainly from manifest files, on-the-fly, as they're parsed.
+  // Entries added later override entries added earlier, and no record is kept
+  // of the former state.
+  //
+  // As a result, if we remove a dynamically-added manifest file, or a set of
+  // dynamic entries, the registry needs to be rebuilt from scratch, from the
+  // manifests and dynamic entries that remain. The chrome registry itself
+  // takes care of re-parsing manifes files. This observer notification lets
+  // us know when we need to re-register our dynamic entries.
+  if (!strcmp(topic, "chrome-manifests-loaded")) {
+    for (auto entry : GetRegistryEntries()) {
+      entry->Register();
+    }
+  }
+
+  return NS_OK;
+}
+
 } // namespace mozilla
--- a/toolkit/mozapps/extensions/AddonManagerStartup.h
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.h
@@ -6,29 +6,32 @@
 #ifndef AddonManagerStartup_h
 #define AddonManagerStartup_h
 
 #include "amIAddonManagerStartup.h"
 #include "mozilla/Result.h"
 #include "nsCOMArray.h"
 #include "nsCOMPtr.h"
 #include "nsIFile.h"
+#include "nsIObserver.h"
 #include "nsISupports.h"
 
 #include "jsapi.h"
 
 namespace mozilla {
 
 class Addon;
 
 class AddonManagerStartup final : public amIAddonManagerStartup
+                                , public nsIObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_AMIADDONMANAGERSTARTUP
+  NS_DECL_NSIOBSERVER
 
   AddonManagerStartup();
 
   static AddonManagerStartup& GetSingleton();
 
   static already_AddRefed<AddonManagerStartup> GetInstance()
   {
     RefPtr<AddonManagerStartup> inst = &GetSingleton();
--- a/toolkit/mozapps/extensions/amIAddonManagerStartup.idl
+++ b/toolkit/mozapps/extensions/amIAddonManagerStartup.idl
@@ -1,15 +1,17 @@
 /* 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"
 
 interface nsIFile;
+interface nsIJSRAIIHelper;
+interface nsIURI;
 
 [scriptable, builtinclass, uuid(01dfa47b-87e4-4135-877b-586d033e1b5d)]
 interface amIAddonManagerStartup : nsISupports
 {
   /**
    * Reads and parses startup data from the addonState.json.lz4 file, checks
    * for modifications, and returns the result.
    *
@@ -21,16 +23,33 @@ interface amIAddonManagerStartup : nsISu
 
   /**
    * Initializes the chrome registry for the enabled, non-restartless add-on
    * in the given state data.
    */
   [implicit_jscontext]
   void initializeExtensions(in jsval locations);
 
+  /**
+   * Registers a set of dynamic chrome registry entries, and returns an object
+   * with a `destruct()` method which must be called in order to unregister
+   * the entries.
+   *
+   * @param manifestURI The base manifest URI for the entries. URL values are
+   *        resolved relative to this URI.
+   * @param entries An array of arrays, each containing a registry entry as it
+   *        would appar in a chrome.manifest file. Only the following entry
+   *        types are currently accepted:
+   *
+   *         - "locale" A locale package entry. Must be a 4-element array.
+   *         - "override" A URL override entry. Must be a 3-element array.
+   */
+  [implicit_jscontext]
+  nsIJSRAIIHelper registerChrome(in nsIURI manifestURI, in jsval entries);
+
   [implicit_jscontext]
   jsval encodeBlob(in jsval value);
 
   [implicit_jscontext]
   jsval decodeBlob(in jsval value);
 
   /**
    * Enumerates over all entries in the given zip file matching the given
--- a/toolkit/mozapps/extensions/moz.build
+++ b/toolkit/mozapps/extensions/moz.build
@@ -53,15 +53,16 @@ EXPORTS.mozilla += [
 UNIFIED_SOURCES += [
     'AddonContentPolicy.cpp',
     'AddonManagerStartup.cpp',
     'AddonManagerWebAPI.cpp',
     'AddonPathService.cpp',
 ]
 
 LOCAL_INCLUDES += [
+    '/chrome',
     '/dom/base',
 ]
 
 FINAL_LIBRARY = 'xul'
 
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'Add-ons Manager')
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_registerchrome.js
@@ -0,0 +1,76 @@
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+function getFileURI(path) {
+  let file = do_get_file(".");
+  file.append(path);
+  return Services.io.newFileURI(file);
+}
+
+add_task(async function() {
+  const registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry);
+
+  let file1 = getFileURI("file1");
+  let file2 = getFileURI("file2");
+
+  let uri1 = getFileURI("chrome.manifest");
+  let uri2 = getFileURI("manifest.json");
+
+  let overrideURL = Services.io.newURI("chrome://global/content/foo");
+  let localeURL = Services.io.newURI("chrome://global/locale/foo");
+
+  let origOverrideURL = registry.convertChromeURL(overrideURL);
+  let origLocaleURL = registry.convertChromeURL(localeURL);
+
+  // eslint-disable-next-line no-unused-vars
+  let entry1 = aomStartup.registerChrome(uri1, [
+    ["override", "chrome://global/content/foo", file1.spec],
+    ["locale", "global", "en-US", file2.spec + "/"],
+  ]);
+
+  let entry2 = aomStartup.registerChrome(uri2, [
+    ["override", "chrome://global/content/foo", file2.spec],
+    ["locale", "global", "en-US", file1.spec + "/"],
+  ]);
+
+  // Initially, the second entry should override the first.
+  equal(registry.convertChromeURL(overrideURL).spec, file2.spec);
+  equal(registry.convertChromeURL(localeURL).spec, file1.spec + "/foo");
+
+  // After destroying the second entry, the first entry should not take
+  // precedence.
+  entry2.destruct();
+  equal(registry.convertChromeURL(overrideURL).spec, file1.spec);
+  equal(registry.convertChromeURL(localeURL).spec, file2.spec + "/foo");
+
+  // After dropping the reference to the first entry and allowing it to
+  // be GCed, we should be back to the original entries.
+  entry1 = null;
+  Cu.forceGC();
+  Cu.forceCC();
+  equal(registry.convertChromeURL(overrideURL).spec, origOverrideURL.spec);
+  equal(registry.convertChromeURL(localeURL).spec, origLocaleURL.spec);
+});
+
+add_task(async function() {
+  const INVALID_VALUES = [
+    {},
+    "foo",
+    ["foo"],
+    [{}],
+    [[]],
+    [["content", "foo", "bar"]],
+    [["locale", "global"]],
+    [["locale", "global", "en", "foo", "foo"]],
+    [["override", "en"]],
+    [["override", "en", "US", "OR"]],
+  ];
+
+  let uri = getFileURI("chrome.manifest");
+  for (let arg of INVALID_VALUES) {
+    Assert.throws(() => aomStartup.registerChrome(uri, arg),
+                  e => e.result == Cr.NS_ERROR_INVALID_ARG,
+                  `Arg ${uneval(arg)} should throw`);
+  }
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -46,10 +46,11 @@ tags = webextensions
 [test_delay_update.js]
 [test_nodisable_hidden.js]
 [test_delay_update_webextension.js]
 skip-if = appname == "thunderbird"
 tags = webextensions
 [test_dependencies.js]
 [test_system_delay_update.js]
 [test_schema_change.js]
+[test_registerchrome.js]
 
 [include:xpcshell-shared.ini]
--- a/xpcom/components/nsComponentManager.cpp
+++ b/xpcom/components/nsComponentManager.cpp
@@ -735,16 +735,21 @@ nsComponentManagerImpl::ManifestCategory
 
 void
 nsComponentManagerImpl::RereadChromeManifests(bool aChromeOnly)
 {
   for (uint32_t i = 0; i < sModuleLocations->Length(); ++i) {
     ComponentLocation& l = sModuleLocations->ElementAt(i);
     RegisterManifest(l.type, l.location, aChromeOnly);
   }
+
+  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+  if (obs) {
+    obs->NotifyObservers(nullptr, "chrome-manifests-loaded", nullptr);
+  }
 }
 
 bool
 nsComponentManagerImpl::KnownModule::EnsureLoader()
 {
   if (!mLoader) {
     nsCString extension;
     mFile.GetURIString(extension);