diff options
Diffstat (limited to 'toolkit/mozapps/extensions/AddonManagerStartup.cpp')
-rw-r--r-- | toolkit/mozapps/extensions/AddonManagerStartup.cpp | 882 |
1 files changed, 882 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/AddonManagerStartup.cpp b/toolkit/mozapps/extensions/AddonManagerStartup.cpp new file mode 100644 index 0000000000..d39796ad69 --- /dev/null +++ b/toolkit/mozapps/extensions/AddonManagerStartup.cpp @@ -0,0 +1,882 @@ +/* -*- 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 "AddonManagerStartup.h" +#include "AddonManagerStartup-inlines.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/Array.h" // JS::IsArrayObject +#include "js/ArrayBuffer.h" +#include "js/Exception.h" +#include "js/JSON.h" +#include "js/PropertyAndElement.h" // JS_GetProperty, JS_SetProperty +#include "js/TracingAPI.h" +#include "xpcpublic.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/Components.h" +#include "mozilla/Compression.h" +#include "mozilla/LinkedList.h" +#include "mozilla/Preferences.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/URLPreloader.h" +#include "mozilla/Unused.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Services.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsAppRunner.h" +#include "nsContentUtils.h" +#include "nsChromeRegistry.h" +#include "nsIDOMWindowUtils.h" // for nsIJSRAIIHelper +#include "nsIFileURL.h" +#include "nsIIOService.h" +#include "nsIJARURI.h" +#include "nsIStringEnumerator.h" +#include "nsIZipReader.h" +#include "nsJARProtocolHandler.h" +#include "nsJSUtils.h" +#include "nsIObserverService.h" +#include "nsReadableUtils.h" +#include "nsXULAppAPI.h" + +#include <stdlib.h> + +namespace mozilla { + +using Compression::LZ4; +using dom::ipc::StructuredCloneData; + +AddonManagerStartup& AddonManagerStartup::GetSingleton() { + static RefPtr<AddonManagerStartup> singleton; + if (!singleton) { + singleton = new AddonManagerStartup(); + ClearOnShutdown(&singleton); + } + return *singleton; +} + +AddonManagerStartup::AddonManagerStartup() = default; + +nsIFile* AddonManagerStartup::ProfileDir() { + if (!mProfileDir) { + nsresult rv; + + 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, nsIObserver) + +/***************************************************************************** + * URI utils + *****************************************************************************/ + +static nsresult ParseJARURI(nsIJARURI* uri, nsIURI** jarFile, + nsCString& entry) { + MOZ_TRY(uri->GetJARFile(jarFile)); + MOZ_TRY(uri->GetJAREntry(entry)); + + // The entry portion of a jar: URI is required to begin with a '/', but for + // nested JAR URIs, the leading / of the outer entry is currently stripped. + // This is a bug which should be fixed in the JAR URI code, but... + if (entry.IsEmpty() || entry[0] != '/') { + entry.Insert('/', 0); + } + return NS_OK; +} + +static nsresult ParseJARURI(nsIURI* uri, nsIURI** jarFile, nsCString& entry) { + nsresult rv; + nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri, &rv); + MOZ_TRY(rv); + + return ParseJARURI(jarURI, jarFile, entry); +} + +static Result<nsCOMPtr<nsIFile>, nsresult> GetFile(nsIURI* uri) { + nsresult rv; + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIFile> file; + MOZ_TRY(fileURL->GetFile(getter_AddRefs(file))); + MOZ_ASSERT(file); + + return std::move(file); +} + +/***************************************************************************** + * File utils + *****************************************************************************/ + +static already_AddRefed<nsIFile> CloneAndAppend(nsIFile* aFile, + const char* name) { + nsCOMPtr<nsIFile> file; + aFile->Clone(getter_AddRefs(file)); + file->AppendNative(nsDependentCString(name)); + return file.forget(); +} + +static bool IsNormalFile(nsIFile* file) { + bool result; + return NS_SUCCEEDED(file->IsFile(&result)) && result; +} + +static const char STRUCTURED_CLONE_MAGIC[] = "mozJSSCLz40v001"; + +template <typename T> +static Result<nsCString, nsresult> DecodeLZ4(const nsACString& lz4, + const T& magicNumber) { + constexpr auto HEADER_SIZE = sizeof(magicNumber) + 4; + + // Note: We want to include the null terminator here. + nsDependentCSubstring magic(magicNumber, sizeof(magicNumber)); + + if (lz4.Length() < HEADER_SIZE || StringHead(lz4, magic.Length()) != magic) { + return Err(NS_ERROR_UNEXPECTED); + } + + auto data = lz4.BeginReading() + magic.Length(); + auto size = LittleEndian::readUint32(data); + data += 4; + + size_t dataLen = lz4.EndReading() - data; + size_t outputSize; + + nsCString result; + if (!result.SetLength(size, fallible) || + !LZ4::decompress(data, dataLen, result.BeginWriting(), size, + &outputSize)) { + return Err(NS_ERROR_UNEXPECTED); + } + + MOZ_DIAGNOSTIC_ASSERT(size == outputSize); + + return std::move(result); +} + +// Our zlib headers redefine this to MOZ_Z_compress, which breaks LZ4::compress +#undef compress + +template <typename T> +static Result<nsCString, nsresult> EncodeLZ4(const nsACString& data, + const T& magicNumber) { + // Note: We want to include the null terminator here. + nsDependentCSubstring magic(magicNumber, sizeof(magicNumber)); + + nsAutoCString result; + result.Append(magic); + + auto off = result.Length(); + if (!result.SetLength(off + 4, fallible)) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + LittleEndian::writeUint32(result.BeginWriting() + off, data.Length()); + off += 4; + + auto size = LZ4::maxCompressedSize(data.Length()); + if (!result.SetLength(off + size, fallible)) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + size = LZ4::compress(data.BeginReading(), data.Length(), + result.BeginWriting() + off); + + if (!result.SetLength(off + size, fallible)) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + return std::move(result); +} + +static_assert(sizeof STRUCTURED_CLONE_MAGIC % 8 == 0, + "Magic number should be an array of uint64_t"); + +/** + * Reads the contents of a LZ4-compressed file, as stored by the IOUtils + * module, and returns the decompressed contents on success. + */ +static Result<nsCString, nsresult> ReadFileLZ4(nsIFile* file) { + static const char MAGIC_NUMBER[] = "mozLz40"; + + nsCString lz4; + MOZ_TRY_VAR(lz4, URLPreloader::ReadFile(file)); + + if (lz4.IsEmpty()) { + return lz4; + } + + return DecodeLZ4(lz4, MAGIC_NUMBER); +} + +static bool ParseJSON(JSContext* cx, nsACString& jsonData, + JS::MutableHandle<JS::Value> result) { + NS_ConvertUTF8toUTF16 str(jsonData); + jsonData.Truncate(); + + return JS_ParseJSON(cx, str.Data(), str.Length(), result); +} + +static Result<nsCOMPtr<nsIZipReaderCache>, nsresult> GetJarCache() { + nsCOMPtr<nsIIOService> ios = components::IO::Service(); + NS_ENSURE_TRUE(ios, Err(NS_ERROR_FAILURE)); + + nsCOMPtr<nsIProtocolHandler> jarProto; + MOZ_TRY(ios->GetProtocolHandler("jar", getter_AddRefs(jarProto))); + + auto jar = static_cast<nsJARProtocolHandler*>(jarProto.get()); + MOZ_ASSERT(jar); + + nsCOMPtr<nsIZipReaderCache> zipCache = jar->JarCache(); + return std::move(zipCache); +} + +static Result<FileLocation, nsresult> GetFileLocation(nsIURI* uri) { + FileLocation location; + + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri); + nsCOMPtr<nsIFile> file; + if (fileURL) { + MOZ_TRY(fileURL->GetFile(getter_AddRefs(file))); + location.Init(file); + } else { + nsCOMPtr<nsIURI> fileURI; + nsCString entry; + MOZ_TRY(ParseJARURI(uri, getter_AddRefs(fileURI), entry)); + + MOZ_TRY_VAR(file, GetFile(fileURI)); + + location.Init(file, entry.get()); + } + + return std::move(location); +} + +/***************************************************************************** + * JSON data handling + *****************************************************************************/ + +class MOZ_STACK_CLASS WrapperBase { + protected: + WrapperBase(JSContext* cx, JSObject* object) : mCx(cx), mObject(cx, object) {} + + WrapperBase(JSContext* cx, const JS::Value& value) : mCx(cx), mObject(cx) { + if (value.isObject()) { + mObject = &value.toObject(); + } else { + mObject = JS_NewPlainObject(cx); + } + } + + protected: + JSContext* mCx; + JS::Rooted<JSObject*> mObject; + + bool GetBool(const char* name, bool defVal = false); + + double GetNumber(const char* name, double defVal = 0); + + nsString GetString(const char* name, const char* defVal = ""); + + JSObject* GetObject(const char* name); +}; + +bool WrapperBase::GetBool(const char* name, bool defVal) { + JS::Rooted<JSObject*> obj(mCx, mObject); + + JS::Rooted<JS::Value> val(mCx, JS::UndefinedValue()); + if (!JS_GetProperty(mCx, obj, name, &val)) { + JS_ClearPendingException(mCx); + } + + if (val.isBoolean()) { + return val.toBoolean(); + } + return defVal; +} + +double WrapperBase::GetNumber(const char* name, double defVal) { + JS::Rooted<JSObject*> obj(mCx, mObject); + + JS::Rooted<JS::Value> val(mCx, JS::UndefinedValue()); + if (!JS_GetProperty(mCx, obj, name, &val)) { + JS_ClearPendingException(mCx); + } + + if (val.isNumber()) { + return val.toNumber(); + } + return defVal; +} + +nsString WrapperBase::GetString(const char* name, const char* defVal) { + JS::Rooted<JSObject*> obj(mCx, mObject); + + JS::Rooted<JS::Value> val(mCx, JS::UndefinedValue()); + if (!JS_GetProperty(mCx, obj, name, &val)) { + JS_ClearPendingException(mCx); + } + + nsString res; + if (val.isString()) { + AssignJSString(mCx, res, val.toString()); + } else { + res.AppendASCII(defVal); + } + return res; +} + +JSObject* WrapperBase::GetObject(const char* name) { + JS::Rooted<JSObject*> obj(mCx, mObject); + + JS::Rooted<JS::Value> val(mCx, JS::UndefinedValue()); + if (!JS_GetProperty(mCx, obj, name, &val)) { + JS_ClearPendingException(mCx); + } + + if (val.isObject()) { + return &val.toObject(); + } + return nullptr; +} + +class MOZ_STACK_CLASS InstallLocation : public WrapperBase { + public: + InstallLocation(JSContext* cx, const JS::Value& value); + + MOZ_IMPLICIT InstallLocation(PropertyIterElem& iter) + : InstallLocation(iter.Cx(), iter.Value()) {} + + InstallLocation(const InstallLocation& other) + : InstallLocation(other.mCx, JS::ObjectValue(*other.mObject)) {} + + void SetChanged(bool changed) { + JS::Rooted<JSObject*> obj(mCx, mObject); + + JS::Rooted<JS::Value> val(mCx, JS::BooleanValue(changed)); + if (!JS_SetProperty(mCx, obj, "changed", val)) { + JS_ClearPendingException(mCx); + } + } + + PropertyIter& Addons() { return mAddonsIter.ref(); } + + nsString Path() { return GetString("path"); } + + bool ShouldCheckStartupModifications() { + return GetBool("checkStartupModifications"); + } + + private: + JS::Rooted<JSObject*> mAddonsObj; + Maybe<PropertyIter> mAddonsIter; +}; + +class MOZ_STACK_CLASS Addon : public WrapperBase { + public: + Addon(JSContext* cx, InstallLocation& location, const nsAString& id, + JSObject* object) + : WrapperBase(cx, object), mId(id), mLocation(location) {} + + MOZ_IMPLICIT Addon(PropertyIterElem& iter) + : WrapperBase(iter.Cx(), iter.Value()), + mId(iter.Name()), + mLocation(*static_cast<InstallLocation*>(iter.Context())) {} + + Addon(const Addon& other) + : WrapperBase(other.mCx, other.mObject), + mId(other.mId), + mLocation(other.mLocation) {} + + const nsString& Id() { return mId; } + + nsString Path() { return GetString("path"); } + + nsString Type() { return GetString("type", "extension"); } + + bool Enabled() { return GetBool("enabled"); } + + double LastModifiedTime() { return GetNumber("lastModifiedTime"); } + + bool ShouldCheckStartupModifications() { + return Type().EqualsLiteral("locale"); + } + + Result<nsCOMPtr<nsIFile>, nsresult> FullPath(); + + Result<bool, nsresult> UpdateLastModifiedTime(); + + private: + nsString mId; + InstallLocation& mLocation; +}; + +Result<nsCOMPtr<nsIFile>, nsresult> Addon::FullPath() { + nsString path = Path(); + + // First check for an absolute path, in case we have a proxy file. + nsCOMPtr<nsIFile> file; + if (NS_SUCCEEDED(NS_NewLocalFile(path, false, getter_AddRefs(file)))) { + return std::move(file); + } + + // If not an absolute path, fall back to a relative path from the location. + MOZ_TRY(NS_NewLocalFile(mLocation.Path(), false, getter_AddRefs(file))); + + MOZ_TRY(file->AppendRelativePath(path)); + return std::move(file); +} + +Result<bool, nsresult> Addon::UpdateLastModifiedTime() { + nsCOMPtr<nsIFile> file; + MOZ_TRY_VAR(file, FullPath()); + + JS::Rooted<JSObject*> obj(mCx, mObject); + + bool result; + if (NS_FAILED(file->Exists(&result)) || !result) { + JS::Rooted<JS::Value> value(mCx, JS::NullValue()); + if (!JS_SetProperty(mCx, obj, "currentModifiedTime", value)) { + JS_ClearPendingException(mCx); + } + + return true; + } + + PRTime time; + + nsCOMPtr<nsIFile> manifest = file; + if (!IsNormalFile(manifest)) { + manifest = CloneAndAppend(file, "manifest.json"); + if (!IsNormalFile(manifest)) { + return true; + } + } + + if (NS_FAILED(manifest->GetLastModifiedTime(&time))) { + return true; + } + + double lastModified = time; + JS::Rooted<JS::Value> value(mCx, JS::NumberValue(lastModified)); + if (!JS_SetProperty(mCx, obj, "currentModifiedTime", value)) { + JS_ClearPendingException(mCx); + } + + return lastModified != LastModifiedTime(); +} + +InstallLocation::InstallLocation(JSContext* cx, const JS::Value& value) + : WrapperBase(cx, value), mAddonsObj(cx), mAddonsIter() { + mAddonsObj = GetObject("addons"); + if (!mAddonsObj) { + mAddonsObj = JS_NewPlainObject(cx); + } + mAddonsIter.emplace(cx, mAddonsObj, this); +} + +/***************************************************************************** + * XPC interfacing + *****************************************************************************/ + +nsresult AddonManagerStartup::ReadStartupData( + JSContext* cx, JS::MutableHandle<JS::Value> locations) { + locations.set(JS::UndefinedValue()); + + nsCOMPtr<nsIFile> file = + CloneAndAppend(ProfileDir(), "addonStartup.json.lz4"); + + nsCString data; + auto res = ReadFileLZ4(file); + if (res.isOk()) { + data = res.unwrap(); + } else if (res.inspectErr() != NS_ERROR_FILE_NOT_FOUND) { + return res.unwrapErr(); + } + + if (data.IsEmpty() || !ParseJSON(cx, data, locations)) { + return NS_OK; + } + + if (!locations.isObject()) { + return NS_ERROR_UNEXPECTED; + } + + JS::Rooted<JSObject*> locs(cx, &locations.toObject()); + for (auto e1 : PropertyIter(cx, locs)) { + InstallLocation loc(e1); + + bool shouldCheck = loc.ShouldCheckStartupModifications(); + + for (auto e2 : loc.Addons()) { + Addon addon(e2); + + if (addon.Enabled() && + (shouldCheck || addon.ShouldCheckStartupModifications())) { + bool changed; + MOZ_TRY_VAR(changed, addon.UpdateLastModifiedTime()); + if (changed) { + loc.SetChanged(true); + } + } + } + } + + return NS_OK; +} + +nsresult AddonManagerStartup::EncodeBlob(JS::Handle<JS::Value> value, + JSContext* cx, + JS::MutableHandle<JS::Value> result) { + StructuredCloneData holder; + + ErrorResult rv; + holder.Write(cx, value, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + nsAutoCString scData; + + holder.Data().ForEachDataChunk([&](const char* aData, size_t aSize) { + scData.Append(nsDependentCSubstring(aData, aSize)); + return true; + }); + + nsCString lz4; + MOZ_TRY_VAR(lz4, EncodeLZ4(scData, STRUCTURED_CLONE_MAGIC)); + + JS::Rooted<JSObject*> obj(cx); + MOZ_TRY(nsContentUtils::CreateArrayBuffer(cx, lz4, &obj.get())); + + result.set(JS::ObjectValue(*obj)); + return NS_OK; +} + +nsresult AddonManagerStartup::DecodeBlob(JS::Handle<JS::Value> value, + JSContext* cx, + JS::MutableHandle<JS::Value> result) { + NS_ENSURE_TRUE(value.isObject() && + JS::IsArrayBufferObject(&value.toObject()) && + JS::ArrayBufferHasData(&value.toObject()), + NS_ERROR_INVALID_ARG); + + StructuredCloneData holder; + + nsCString data; + { + JS::AutoCheckCannotGC nogc; + + auto obj = &value.toObject(); + bool isShared; + + size_t len = JS::GetArrayBufferByteLength(obj); + NS_ENSURE_TRUE(len <= INT32_MAX, NS_ERROR_INVALID_ARG); + nsDependentCSubstring lz4( + reinterpret_cast<char*>(JS::GetArrayBufferData(obj, &isShared, nogc)), + uint32_t(len)); + + MOZ_TRY_VAR(data, DecodeLZ4(lz4, STRUCTURED_CLONE_MAGIC)); + } + + bool ok = holder.CopyExternalData(data.get(), data.Length()); + NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY); + + ErrorResult rv; + holder.Read(cx, result, rv); + return rv.StealNSResult(); + ; +} + +static nsresult EnumerateZip(nsIZipReader* zip, const nsACString& pattern, + nsTArray<nsString>& results) { + nsCOMPtr<nsIUTF8StringEnumerator> entries; + MOZ_TRY(zip->FindEntries(pattern, getter_AddRefs(entries))); + + bool hasMore; + while (NS_SUCCEEDED(entries->HasMore(&hasMore)) && hasMore) { + nsAutoCString name; + MOZ_TRY(entries->GetNext(name)); + + results.AppendElement(NS_ConvertUTF8toUTF16(name)); + } + + return NS_OK; +} + +nsresult AddonManagerStartup::EnumerateJAR(nsIURI* uri, + const nsACString& pattern, + nsTArray<nsString>& results) { + nsCOMPtr<nsIZipReaderCache> zipCache; + MOZ_TRY_VAR(zipCache, GetJarCache()); + + nsCOMPtr<nsIZipReader> zip; + nsCOMPtr<nsIFile> file; + if (nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri)) { + nsCOMPtr<nsIURI> fileURI; + nsCString entry; + MOZ_TRY(ParseJARURI(jarURI, getter_AddRefs(fileURI), entry)); + + MOZ_TRY_VAR(file, GetFile(fileURI)); + MOZ_TRY( + zipCache->GetInnerZip(file, Substring(entry, 1), getter_AddRefs(zip))); + } else { + MOZ_TRY_VAR(file, GetFile(uri)); + MOZ_TRY(zipCache->GetZip(file, getter_AddRefs(zip))); + } + MOZ_ASSERT(zip); + + return EnumerateZip(zip, pattern, results); +} + +nsresult AddonManagerStartup::EnumerateJARSubtree(nsIURI* uri, + nsTArray<nsString>& results) { + nsCOMPtr<nsIURI> fileURI; + nsCString entry; + MOZ_TRY(ParseJARURI(uri, getter_AddRefs(fileURI), entry)); + + // Mangle the path into a pattern to match all child entries by escaping any + // existing pattern matching metacharacters it contains and appending "/*". + constexpr auto metaChars = "[]()?*~|$\\"_ns; + + nsCString pattern; + pattern.SetCapacity(entry.Length()); + + // The first character of the entry name is "/", which we want to skip. + for (auto chr : Span(Substring(entry, 1))) { + if (metaChars.FindChar(chr) >= 0) { + pattern.Append('\\'); + } + pattern.Append(chr); + } + if (!pattern.IsEmpty() && !StringEndsWith(pattern, "/"_ns)) { + pattern.Append('/'); + } + pattern.Append('*'); + + return EnumerateJAR(fileURI, pattern, results); +} + +nsresult AddonManagerStartup::InitializeURLPreloader() { + MOZ_RELEASE_ASSERT(xpc::IsInAutomation()); + + URLPreloader::ReInitialize(); + + return NS_OK; +} + +/****************************************************************************** + * RegisterChrome + ******************************************************************************/ + +namespace { +static bool sObserverRegistered; + +struct ContentEntry final { + explicit ContentEntry(nsTArray<nsCString>&& aArgs, uint8_t aFlags = 0) + : mArgs(std::move(aArgs)), mFlags(aFlags) {} + + AutoTArray<nsCString, 2> mArgs; + uint8_t mFlags; +}; + +}; // anonymous namespace +}; // namespace mozilla + +MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::ContentEntry); + +namespace mozilla { +namespace { + +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<ContentEntry>&& content, nsTArray<Locale>&& locales) + : mLocation(location), + mOverrides(std::move(overrides)), + mContent(std::move(content)), + mLocales(std::move(locales)) {} + + void Register(); + + protected: + virtual ~RegistryEntries() { Unused << Destruct(); } + + private: + FileLocation mLocation; + const nsTArray<Override> mOverrides; + const nsTArray<ContentEntry> mContent; + 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& content : mContent) { + const char* args[] = {content.mArgs[0].get(), content.mArgs[1].get()}; + cr->ManifestContent(context, 0, const_cast<char**>(args), content.mFlags); + } + + 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(); + + // No point in doing I/O to check for new chrome during shutdown, return + // early in that case. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return NS_OK; + } + + // 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::Handle<JS::Value> locations, + JSContext* cx, nsIJSRAIIHelper** result) { + auto IsArray = [cx](JS::Handle<JS::Value> 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<ContentEntry> content; + nsTArray<RegistryEntries::Override> overrides; + + JS::Rooted<JSObject*> locs(cx, &locations.toObject()); + JS::Rooted<JS::Value> arrayVal(cx); + JS::Rooted<JSObject*> 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(std::move(vals)); + } else if (type.EqualsLiteral("content")) { + if (vals.Length() == 3 && + vals[2].EqualsLiteral("contentaccessible=yes")) { + NS_ENSURE_TRUE(xpc::IsInAutomation(), NS_ERROR_INVALID_ARG); + vals.RemoveElementAt(2); + content.AppendElement(ContentEntry( + std::move(vals), nsChromeRegistry::CONTENT_ACCESSIBLE)); + } else { + NS_ENSURE_TRUE(vals.Length() == 2, NS_ERROR_INVALID_ARG); + content.AppendElement(ContentEntry(std::move(vals))); + } + } else if (type.EqualsLiteral("locale")) { + NS_ENSURE_TRUE(vals.Length() == 3, NS_ERROR_INVALID_ARG); + locales.AppendElement(std::move(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, std::move(overrides), std::move(content), std::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 |