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
--- 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);