summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutthirdparty
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutthirdparty')
-rw-r--r--toolkit/components/aboutthirdparty/AboutThirdParty.cpp923
-rw-r--r--toolkit/components/aboutthirdparty/AboutThirdParty.h113
-rw-r--r--toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp69
-rw-r--r--toolkit/components/aboutthirdparty/AboutThirdPartyUtils.h36
-rw-r--r--toolkit/components/aboutthirdparty/AboutThirdParty_TestMethods.cpp138
-rw-r--r--toolkit/components/aboutthirdparty/MsiDatabase.cpp88
-rw-r--r--toolkit/components/aboutthirdparty/MsiDatabase.h94
-rw-r--r--toolkit/components/aboutthirdparty/components.conf17
-rw-r--r--toolkit/components/aboutthirdparty/content/aboutThirdParty.css114
-rw-r--r--toolkit/components/aboutthirdparty/content/aboutThirdParty.html152
-rw-r--r--toolkit/components/aboutthirdparty/content/aboutThirdParty.js690
-rw-r--r--toolkit/components/aboutthirdparty/jar.mn8
-rw-r--r--toolkit/components/aboutthirdparty/moz.build37
-rw-r--r--toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl68
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/Factory.cpp74
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/Icon.cpp109
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.cpp148
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.h52
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/Resource.h12
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.cpp68
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.def10
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.rc39
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.icobin0 -> 1406 bytes
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/moz.build33
-rw-r--r--toolkit/components/aboutthirdparty/tests/browser/browser.ini7
-rw-r--r--toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js317
-rw-r--r--toolkit/components/aboutthirdparty/tests/browser/head.js144
-rw-r--r--toolkit/components/aboutthirdparty/tests/browser/hello.zzz1
-rw-r--r--toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp125
-rw-r--r--toolkit/components/aboutthirdparty/tests/gtest/moz.build13
-rw-r--r--toolkit/components/aboutthirdparty/tests/xpcshell/head.js10
-rw-r--r--toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js65
-rw-r--r--toolkit/components/aboutthirdparty/tests/xpcshell/xpcshell.ini4
33 files changed, 3778 insertions, 0 deletions
diff --git a/toolkit/components/aboutthirdparty/AboutThirdParty.cpp b/toolkit/components/aboutthirdparty/AboutThirdParty.cpp
new file mode 100644
index 0000000000..bf716e3bfb
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/AboutThirdParty.cpp
@@ -0,0 +1,923 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "AboutThirdParty.h"
+
+#include "AboutThirdPartyUtils.h"
+#include "base/command_line.h"
+#include "base/string_util.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/DynamicBlocklist.h"
+#include "mozilla/GeckoArgs.h"
+#include "mozilla/NativeNt.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/WinDllServices.h"
+#include "MsiDatabase.h"
+#include "nsAppRunner.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIWindowsRegKey.h"
+#include "nsThreadUtils.h"
+
+#include <objbase.h>
+
+using namespace mozilla;
+
+template <>
+bool DllBlockInfo::IsValidDynamicBlocklistEntry() const {
+ if (!mName.Buffer || !mName.Length || mName.Length > mName.MaximumLength) {
+ return false;
+ }
+ MOZ_ASSERT(mMaxVersion == DllBlockInfo::ALL_VERSIONS,
+ "dynamic blocklist does not allow custom version");
+ MOZ_ASSERT(mFlags == DllBlockInfoFlags::FLAGS_DEFAULT,
+ "dynamic blocklist does not allow custom flags");
+ return true;
+}
+
+namespace {
+
+// A callback function passed to EnumSubkeys uses this type
+// to control the enumeration loop.
+enum class CallbackResult { Continue, Stop };
+
+template <typename CallbackT>
+void EnumSubkeys(nsIWindowsRegKey* aRegBase, const CallbackT& aCallback) {
+ uint32_t count = 0;
+ if (NS_FAILED(aRegBase->GetChildCount(&count))) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < count; ++i) {
+ nsAutoString subkeyName;
+ if (NS_FAILED(aRegBase->GetChildName(i, subkeyName))) {
+ continue;
+ }
+
+ nsCOMPtr<nsIWindowsRegKey> subkey;
+ if (NS_FAILED(aRegBase->OpenChild(subkeyName, nsIWindowsRegKey::ACCESS_READ,
+ getter_AddRefs(subkey)))) {
+ continue;
+ }
+
+ CallbackResult result = aCallback(subkeyName, subkey);
+ if (result == CallbackResult::Continue) {
+ continue;
+ } else if (result == CallbackResult::Stop) {
+ break;
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Unexpected CallbackResult.");
+ }
+ }
+}
+
+Span<const DllBlockInfo> GetDynamicBlocklistSpan(
+ RefPtr<DllServices>&& aDllSvc) {
+ if (!aDllSvc) {
+ return nullptr;
+ }
+
+ nt::SharedSection* sharedSection = aDllSvc->GetSharedSection();
+ if (!sharedSection) {
+ return nullptr;
+ }
+
+ return sharedSection->GetDynamicBlocklist();
+}
+
+} // anonymous namespace
+
+InstallLocationComparator::InstallLocationComparator(const nsAString& aFilePath)
+ : mFilePath(aFilePath) {}
+
+int InstallLocationComparator::operator()(
+ const InstallLocationT& aLocation) const {
+ // Firstly we check whether mFilePath begins with aLocation.
+ // If yes, mFilePath is a part of the target installation,
+ // so we return 0 showing match.
+ const nsAString& location = aLocation.first();
+ size_t locationLen = location.Length();
+ if (locationLen <= mFilePath.Length() &&
+ nsCaseInsensitiveStringComparator(mFilePath.BeginReading(),
+ location.BeginReading(), locationLen,
+ locationLen) == 0) {
+ return 0;
+ }
+
+ return CompareIgnoreCase(mFilePath, location);
+}
+
+// The InstalledApplications class behaves like Chrome's InstalledApplications,
+// which collects installed applications from two resources below.
+//
+// 1) Path strings in MSI package components
+// An MSI package is consisting of multiple components. This class collects
+// MSI components representing a file and stores them as a hash table.
+//
+// 2) Install location paths in the InstallLocation registry value
+// If an application's installer is not MSI but sets the InstallLocation
+// registry value, we can use it to search for an application by comparing
+// a target module is located under that location path. This class stores
+// location path strings as a sorted array so that we can binary-search it.
+class InstalledApplications final {
+ // Limit the number of entries to avoid consuming too much memory
+ constexpr static uint32_t kMaxComponents = 1000000;
+ constexpr static uint32_t kMaxInstallLocations = 1000;
+
+ nsCOMPtr<nsIWindowsRegKey> mInstallerData;
+ nsCOMPtr<nsIInstalledApplication> mCurrentApp;
+ ComponentPathMapT mComponentPaths;
+ nsTArray<InstallLocationT> mLocations;
+
+ void AddInstallLocation(nsIWindowsRegKey* aProductSubKey) {
+ nsString location;
+ if (NS_FAILED(
+ aProductSubKey->ReadStringValue(u"InstallLocation"_ns, location)) ||
+ location.IsEmpty()) {
+ return;
+ }
+
+ if (location.Last() != u'\\') {
+ location.Append(u'\\');
+ }
+
+ mLocations.EmplaceBack(location, this->mCurrentApp);
+ }
+
+ void AddComponentGuid(const nsString& aPackedProductGuid,
+ const nsString& aPackedComponentGuid) {
+ nsAutoString componentSubkey(L"Components\\");
+ componentSubkey += aPackedComponentGuid;
+
+ // Pick a first value in the subkeys under |componentSubkey|.
+ nsString componentPath;
+
+ EnumSubkeys(mInstallerData, [&aPackedProductGuid, &componentSubkey,
+ &componentPath](const nsString& aSid,
+ nsIWindowsRegKey* aSidSubkey) {
+ // If we have a value in |componentPath|, the loop should
+ // have been stopped.
+ MOZ_ASSERT(componentPath.IsEmpty());
+
+ nsCOMPtr<nsIWindowsRegKey> compKey;
+ nsresult rv =
+ aSidSubkey->OpenChild(componentSubkey, nsIWindowsRegKey::ACCESS_READ,
+ getter_AddRefs(compKey));
+ if (NS_FAILED(rv)) {
+ return CallbackResult::Continue;
+ }
+
+ nsString compData;
+ if (NS_FAILED(compKey->ReadStringValue(aPackedProductGuid, compData))) {
+ return CallbackResult::Continue;
+ }
+
+ if (!CorrectMsiComponentPath(compData)) {
+ return CallbackResult::Continue;
+ }
+
+ componentPath = std::move(compData);
+ return CallbackResult::Stop;
+ });
+
+ if (componentPath.IsEmpty()) {
+ return;
+ }
+
+ // Use a full path as a key rather than a leaf name because
+ // the same name's module can be installed under system32
+ // and syswow64.
+ mComponentPaths.WithEntryHandle(componentPath, [this](auto&& addPtr) {
+ if (addPtr) {
+ // If the same file appeared in multiple installations, we set null
+ // for its value because there is no way to know which installation is
+ // the real owner.
+ addPtr.Data() = nullptr;
+ } else {
+ addPtr.Insert(this->mCurrentApp);
+ }
+ });
+ }
+
+ void AddProduct(const nsString& aProductId,
+ nsIWindowsRegKey* aProductSubKey) {
+ nsString displayName;
+ if (NS_FAILED(
+ aProductSubKey->ReadStringValue(u"DisplayName"_ns, displayName)) ||
+ displayName.IsEmpty()) {
+ // Skip if no name is found.
+ return;
+ }
+
+ nsString publisher;
+ if (NS_SUCCEEDED(
+ aProductSubKey->ReadStringValue(u"Publisher"_ns, publisher)) &&
+ publisher.EqualsIgnoreCase("Microsoft") &&
+ publisher.EqualsIgnoreCase("Microsoft Corporation")) {
+ // Skip if the publisher is Microsoft because it's not a third-party.
+ // We don't skip an application without the publisher name.
+ return;
+ }
+
+ mCurrentApp =
+ new InstalledApplication(std::move(displayName), std::move(publisher));
+ // Try an MSI database first because it's more accurate,
+ // then fall back to the InstallLocation key.
+ do {
+ if (!mInstallerData) {
+ break;
+ }
+
+ nsAutoString packedProdGuid;
+ if (!MsiPackGuid(aProductId, packedProdGuid)) {
+ break;
+ }
+
+ auto db = MsiDatabase::FromProductId(aProductId.get());
+ if (db.isNothing()) {
+ break;
+ }
+
+ db->ExecuteSingleColumnQuery(
+ L"SELECT DISTINCT ComponentId FROM Component",
+ [this, &packedProdGuid](const wchar_t* aComponentGuid) {
+ if (this->mComponentPaths.Count() >= kMaxComponents) {
+ return MsiDatabase::CallbackResult::Stop;
+ }
+
+ nsAutoString packedComponentGuid;
+ if (MsiPackGuid(nsDependentString(aComponentGuid),
+ packedComponentGuid)) {
+ this->AddComponentGuid(packedProdGuid, packedComponentGuid);
+ }
+
+ return MsiDatabase::CallbackResult::Continue;
+ });
+
+ // We've decided to collect data from an MSI database.
+ // Exiting the function.
+ return;
+ } while (false);
+
+ if (mLocations.Length() >= kMaxInstallLocations) {
+ return;
+ }
+
+ // If we cannot use an MSI database for any reason,
+ // try the InstallLocation key.
+ AddInstallLocation(aProductSubKey);
+ }
+
+ public:
+ InstalledApplications() {
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ if (NS_SUCCEEDED(rv) &&
+ NS_SUCCEEDED(regKey->Open(
+ nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE,
+ u"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
+ u"Installer\\UserData"_ns,
+ nsIWindowsRegKey::ACCESS_READ | nsIWindowsRegKey::WOW64_64))) {
+ mInstallerData.swap(regKey);
+ }
+ }
+ ~InstalledApplications() = default;
+
+ InstalledApplications(InstalledApplications&&) = delete;
+ InstalledApplications& operator=(InstalledApplications&&) = delete;
+ InstalledApplications(const InstalledApplications&) = delete;
+ InstalledApplications& operator=(const InstalledApplications&) = delete;
+
+ void Collect(ComponentPathMapT& aOutComponentPaths,
+ nsTArray<InstallLocationT>& aOutLocations) {
+ const nsLiteralString kUninstallKey(
+ u"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall");
+
+ static const uint16_t sProcessor = []() -> uint16_t {
+ SYSTEM_INFO si;
+ ::GetSystemInfo(&si);
+ return si.wProcessorArchitecture;
+ }();
+
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ switch (sProcessor) {
+ case PROCESSOR_ARCHITECTURE_INTEL:
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE,
+ kUninstallKey, nsIWindowsRegKey::ACCESS_READ);
+ if (NS_SUCCEEDED(rv)) {
+ EnumSubkeys(regKey, [this](const nsString& aProductId,
+ nsIWindowsRegKey* aProductSubKey) {
+ this->AddProduct(aProductId, aProductSubKey);
+ return CallbackResult::Continue;
+ });
+ }
+ break;
+
+ case PROCESSOR_ARCHITECTURE_AMD64:
+ // A 64-bit application may be installed by a 32-bit installer,
+ // or vice versa. So we enumerate both views regardless of
+ // the process's (not processor's) bitness.
+ rv = regKey->Open(
+ nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, kUninstallKey,
+ nsIWindowsRegKey::ACCESS_READ | nsIWindowsRegKey::WOW64_64);
+ if (NS_SUCCEEDED(rv)) {
+ EnumSubkeys(regKey, [this](const nsString& aProductId,
+ nsIWindowsRegKey* aProductSubKey) {
+ this->AddProduct(aProductId, aProductSubKey);
+ return CallbackResult::Continue;
+ });
+ }
+ rv = regKey->Open(
+ nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, kUninstallKey,
+ nsIWindowsRegKey::ACCESS_READ | nsIWindowsRegKey::WOW64_32);
+ if (NS_SUCCEEDED(rv)) {
+ EnumSubkeys(regKey, [this](const nsString& aProductId,
+ nsIWindowsRegKey* aProductSubKey) {
+ this->AddProduct(aProductId, aProductSubKey);
+ return CallbackResult::Continue;
+ });
+ }
+ break;
+
+ default:
+ MOZ_ASSERT(false, "Unsupported CPU architecture");
+ return;
+ }
+
+ // The "HKCU\SOFTWARE\" subtree is shared between the 32-bits and 64 bits
+ // views. No need to enumerate wow6432node for HKCU.
+ // https://docs.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, kUninstallKey,
+ nsIWindowsRegKey::ACCESS_READ);
+ if (NS_SUCCEEDED(rv)) {
+ EnumSubkeys(regKey, [this](const nsString& aProductId,
+ nsIWindowsRegKey* aProductSubKey) {
+ this->AddProduct(aProductId, aProductSubKey);
+ return CallbackResult::Continue;
+ });
+ }
+
+ aOutComponentPaths.SwapElements(mComponentPaths);
+
+ mLocations.Sort([](const InstallLocationT& aA, const InstallLocationT& aB) {
+ return CompareIgnoreCase(aA.first(), aB.first());
+ });
+ aOutLocations.SwapElements(mLocations);
+ }
+};
+
+class KnownModule final {
+ static KnownModule sKnownExtensions[static_cast<int>(KnownModuleType::Last)];
+
+ static bool GetInprocServerDllPathFromGuid(const GUID& aGuid,
+ nsAString& aResult) {
+ nsAutoStringN<60> subkey;
+ subkey.AppendPrintf(
+ "CLSID\\{%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}\\"
+ "InProcServer32",
+ aGuid.Data1, aGuid.Data2, aGuid.Data3, aGuid.Data4[0], aGuid.Data4[1],
+ aGuid.Data4[2], aGuid.Data4[3], aGuid.Data4[4], aGuid.Data4[5],
+ aGuid.Data4[6], aGuid.Data4[7]);
+
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, subkey,
+ nsIWindowsRegKey::ACCESS_READ);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ rv = regKey->ReadStringValue(u""_ns, aResult);
+ return NS_SUCCEEDED(rv);
+ }
+
+ enum class HandlerType {
+ // For this type of handler, multiple extensions can be registered as
+ // subkeys under the handler subkey.
+ Multi,
+ // For this type of handler, a single extension can be registered as
+ // the default value of the handler subkey.
+ Single,
+ };
+
+ HandlerType mHandlerType;
+ nsLiteralString mSubkeyName;
+
+ using CallbackT = std::function<void(const nsString&, KnownModuleType)>;
+
+ void EnumInternal(nsIWindowsRegKey* aRegBase, KnownModuleType aType,
+ const CallbackT& aCallback) const {
+ nsCOMPtr<nsIWindowsRegKey> shexType;
+ if (NS_FAILED(aRegBase->OpenChild(mSubkeyName,
+ nsIWindowsRegKey::ACCESS_READ,
+ getter_AddRefs(shexType)))) {
+ return;
+ }
+
+ switch (mHandlerType) {
+ case HandlerType::Single: {
+ nsAutoString valData;
+ GUID guid;
+ if (NS_FAILED(shexType->ReadStringValue(u""_ns, valData)) ||
+ FAILED(::CLSIDFromString(valData.get(), &guid))) {
+ return;
+ }
+
+ nsAutoString dllPath;
+ if (!GetInprocServerDllPathFromGuid(guid, dllPath)) {
+ return;
+ }
+
+ aCallback(dllPath, aType);
+ break;
+ }
+
+ case HandlerType::Multi:
+ EnumSubkeys(shexType, [aType, &aCallback](const nsString& aSubKeyName,
+ nsIWindowsRegKey* aSubKey) {
+ GUID guid;
+ HRESULT hr = ::CLSIDFromString(aSubKeyName.get(), &guid);
+ if (hr == CO_E_CLASSSTRING) {
+ // If the key's name is not a GUID, the default value of the key
+ // may be a GUID.
+ nsAutoString valData;
+ if (NS_SUCCEEDED(aSubKey->ReadStringValue(u""_ns, valData))) {
+ hr = ::CLSIDFromString(valData.get(), &guid);
+ }
+ }
+
+ if (FAILED(hr)) {
+ return CallbackResult::Continue;
+ }
+
+ nsAutoString dllPath;
+ if (!GetInprocServerDllPathFromGuid(guid, dllPath)) {
+ return CallbackResult::Continue;
+ }
+
+ aCallback(dllPath, aType);
+ return CallbackResult::Continue;
+ });
+ break;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected KnownModule::Type.");
+ break;
+ }
+ }
+
+ static void Enum(nsIWindowsRegKey* aRegBase, KnownModuleType aType,
+ const CallbackT& aCallback) {
+ sKnownExtensions[static_cast<int>(aType)].EnumInternal(aRegBase, aType,
+ aCallback);
+ }
+
+ KnownModule(HandlerType aHandlerType, nsLiteralString aSubkeyName)
+ : mHandlerType(aHandlerType), mSubkeyName(aSubkeyName) {}
+
+ public:
+ static void EnumAll(const CallbackT& aCallback) {
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ // Icon Overlay Handlers are registered under HKLM only.
+ // No need to look at HKCU.
+ rv = regKey->Open(
+ nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE,
+ u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer"_ns,
+ nsIWindowsRegKey::ACCESS_READ);
+ if (NS_SUCCEEDED(rv)) {
+ Enum(regKey, KnownModuleType::IconOverlay, aCallback);
+ }
+
+ // IMEs can be enumerated by
+ // ITfInputProcessorProfiles::EnumInputProcessorInfo, but enumerating
+ // the registry key is easier.
+ // The "HKLM\Software\Microsoft\CTF\TIP" subtree is shared between
+ // the 32-bits and 64 bits views.
+ // https://docs.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys
+ // This logic cannot detect legacy (TSF-unaware) IMEs.
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE,
+ u"Software\\Microsoft\\CTF"_ns,
+ nsIWindowsRegKey::ACCESS_READ);
+ if (NS_SUCCEEDED(rv)) {
+ Enum(regKey, KnownModuleType::Ime, aCallback);
+ }
+
+ // Because HKCR is a merged view of HKLM\Software\Classes and
+ // HKCU\Software\Classes, looking at HKCR covers both per-machine
+ // and per-user extensions.
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, u""_ns,
+ nsIWindowsRegKey::ACCESS_READ);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ EnumSubkeys(regKey, [&aCallback](const nsString& aSubKeyName,
+ nsIWindowsRegKey* aSubKey) {
+ if (aSubKeyName.EqualsIgnoreCase("DesktopBackground") ||
+ aSubKeyName.EqualsIgnoreCase("AudioCD")) {
+ return CallbackResult::Continue;
+ }
+
+ if (aSubKeyName.EqualsIgnoreCase("Directory")) {
+ nsCOMPtr<nsIWindowsRegKey> regBackground;
+ if (NS_SUCCEEDED(aSubKey->OpenChild(u"Background\\shellex"_ns,
+ nsIWindowsRegKey::ACCESS_READ,
+ getter_AddRefs(regBackground)))) {
+ Enum(regBackground, KnownModuleType::ContextMenuHandler, aCallback);
+ }
+ } else if (aSubKeyName.EqualsIgnoreCase("Network")) {
+ nsCOMPtr<nsIWindowsRegKey> regNetworkTypes;
+ if (NS_SUCCEEDED(aSubKey->OpenChild(u"Type"_ns,
+ nsIWindowsRegKey::ACCESS_READ,
+ getter_AddRefs(regNetworkTypes)))) {
+ EnumSubkeys(
+ regNetworkTypes,
+ [&aCallback](const nsString&, nsIWindowsRegKey* aRegNetworkType) {
+ nsCOMPtr<nsIWindowsRegKey> regNetworkTypeShex;
+ if (NS_FAILED(aRegNetworkType->OpenChild(
+ u"shellex"_ns, nsIWindowsRegKey::ACCESS_READ,
+ getter_AddRefs(regNetworkTypeShex)))) {
+ return CallbackResult::Continue;
+ }
+
+ Enum(regNetworkTypeShex, KnownModuleType::ContextMenuHandler,
+ aCallback);
+ Enum(regNetworkTypeShex, KnownModuleType::PropertySheetHandler,
+ aCallback);
+ return CallbackResult::Continue;
+ });
+ }
+ }
+
+ nsCOMPtr<nsIWindowsRegKey> regShex;
+ if (NS_FAILED(aSubKey->OpenChild(u"shellex"_ns,
+ nsIWindowsRegKey::ACCESS_READ,
+ getter_AddRefs(regShex)))) {
+ return CallbackResult::Continue;
+ }
+
+ Enum(regShex, KnownModuleType::ContextMenuHandler, aCallback);
+ Enum(regShex, KnownModuleType::PropertySheetHandler, aCallback);
+
+ if (aSubKeyName.EqualsIgnoreCase("AllFileSystemObjects") ||
+ aSubKeyName.EqualsIgnoreCase("Network") ||
+ aSubKeyName.EqualsIgnoreCase("NetShare") ||
+ aSubKeyName.EqualsIgnoreCase("NetServer") ||
+ aSubKeyName.EqualsIgnoreCase("DVD")) {
+ return CallbackResult::Continue;
+ }
+
+ if (aSubKeyName.EqualsIgnoreCase("Directory")) {
+ Enum(regShex, KnownModuleType::CopyHookHandler, aCallback);
+ Enum(regShex, KnownModuleType::DragDropHandler, aCallback);
+ return CallbackResult::Continue;
+ } else if (aSubKeyName.EqualsIgnoreCase("Drive")) {
+ Enum(regShex, KnownModuleType::DragDropHandler, aCallback);
+ return CallbackResult::Continue;
+ } else if (aSubKeyName.EqualsIgnoreCase("Folder")) {
+ Enum(regShex, KnownModuleType::DragDropHandler, aCallback);
+ return CallbackResult::Continue;
+ } else if (aSubKeyName.EqualsIgnoreCase("Printers")) {
+ Enum(regShex, KnownModuleType::CopyHookHandler, aCallback);
+ return CallbackResult::Continue;
+ }
+
+ Enum(regShex, KnownModuleType::DataHandler, aCallback);
+ Enum(regShex, KnownModuleType::DropHandler, aCallback);
+ Enum(regShex, KnownModuleType::IconHandler, aCallback);
+ Enum(regShex, KnownModuleType::PropertyHandler, aCallback);
+ Enum(regShex, KnownModuleType::InfotipHandler, aCallback);
+ return CallbackResult::Continue;
+ });
+ }
+
+ KnownModule() = delete;
+ KnownModule(KnownModule&&) = delete;
+ KnownModule& operator=(KnownModule&&) = delete;
+ KnownModule(const KnownModule&) = delete;
+ KnownModule& operator=(const KnownModule&) = delete;
+};
+
+KnownModule KnownModule::sKnownExtensions[] = {
+ {HandlerType::Multi, u"TIP"_ns},
+ {HandlerType::Multi, u"ShellIconOverlayIdentifiers"_ns},
+ {HandlerType::Multi, u"ContextMenuHandlers"_ns},
+ {HandlerType::Multi, u"CopyHookHandlers"_ns},
+ {HandlerType::Multi, u"DragDropHandlers"_ns},
+ {HandlerType::Multi, u"PropertySheetHandlers"_ns},
+ {HandlerType::Single, u"DataHandler"_ns},
+ {HandlerType::Single, u"DropHandler"_ns},
+ {HandlerType::Single, u"IconHandler"_ns},
+ {HandlerType::Single, u"{00021500-0000-0000-C000-000000000046}"_ns},
+ {HandlerType::Single, u"PropertyHandler"_ns},
+};
+
+namespace mozilla {
+
+static StaticRefPtr<AboutThirdParty> sSingleton;
+
+NS_IMPL_ISUPPORTS(InstalledApplication, nsIInstalledApplication);
+NS_IMPL_ISUPPORTS(AboutThirdParty, nsIAboutThirdParty);
+
+InstalledApplication::InstalledApplication(nsString&& aAppName,
+ nsString&& aPublisher)
+ : mName(std::move(aAppName)), mPublisher(std::move(aPublisher)) {}
+
+NS_IMETHODIMP
+InstalledApplication::GetName(nsAString& aResult) {
+ aResult = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+InstalledApplication::GetPublisher(nsAString& aResult) {
+ aResult = mPublisher;
+ return NS_OK;
+}
+
+/*static*/
+already_AddRefed<AboutThirdParty> AboutThirdParty::GetSingleton() {
+ if (!sSingleton) {
+ sSingleton = new AboutThirdParty;
+ ClearOnShutdown(&sSingleton);
+ }
+
+ return do_AddRef(sSingleton);
+}
+
+AboutThirdParty::AboutThirdParty()
+ : mPromise(new BackgroundThreadPromise::Private(__func__)) {}
+
+void AboutThirdParty::AddKnownModule(const nsString& aPath,
+ KnownModuleType aType) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ const uint32_t flag = 1u << static_cast<uint32_t>(aType);
+ mKnownModules.WithEntryHandle(nt::GetLeafName(aPath), [flag](auto&& addPtr) {
+ if (addPtr) {
+ addPtr.Data() |= flag;
+ } else {
+ addPtr.Insert(flag);
+ }
+ });
+}
+
+void AboutThirdParty::BackgroundThread() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWorkerState == WorkerState::Running);
+
+ auto cleanup = MakeScopeExit(
+ [self = RefPtr{this}] { self->mWorkerState = WorkerState::Done; });
+
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+ if (!dllSvc) {
+ // Probably we're shutting down. Bail out before expensive tasks.
+ return;
+ }
+
+ KnownModule::EnumAll(
+ [self = RefPtr{this}](const nsString& aDllPath, KnownModuleType aType) {
+ self->AddKnownModule(aDllPath, aType);
+ });
+
+ InstalledApplications apps;
+ apps.Collect(mComponentPaths, mLocations);
+
+#if defined(MOZ_LAUNCHER_PROCESS)
+ Span<const DllBlockInfo> blocklist =
+ GetDynamicBlocklistSpan(std::move(dllSvc));
+ for (const auto& info : blocklist) {
+ if (!info.IsValidDynamicBlocklistEntry()) {
+ break;
+ }
+
+ nsString name(info.mName.Buffer, info.mName.Length / sizeof(wchar_t));
+ mDynamicBlocklist.Insert(name);
+ mDynamicBlocklistAtLaunch.Insert(std::move(name));
+ }
+#endif // defined(MOZ_LAUNCHER_PROCESS)
+}
+
+NS_IMETHODIMP AboutThirdParty::LookupModuleType(const nsAString& aLeafName,
+ uint32_t* aResult) {
+ static_assert(static_cast<uint32_t>(KnownModuleType::Last) <= 32,
+ "Too many flags in KnownModuleType");
+ constexpr uint32_t kShellExtensions =
+ 1u << static_cast<uint32_t>(KnownModuleType::IconOverlay) |
+ 1u << static_cast<uint32_t>(KnownModuleType::ContextMenuHandler) |
+ 1u << static_cast<uint32_t>(KnownModuleType::CopyHookHandler) |
+ 1u << static_cast<uint32_t>(KnownModuleType::DragDropHandler) |
+ 1u << static_cast<uint32_t>(KnownModuleType::PropertySheetHandler) |
+ 1u << static_cast<uint32_t>(KnownModuleType::DataHandler) |
+ 1u << static_cast<uint32_t>(KnownModuleType::DropHandler) |
+ 1u << static_cast<uint32_t>(KnownModuleType::IconHandler) |
+ 1u << static_cast<uint32_t>(KnownModuleType::InfotipHandler) |
+ 1u << static_cast<uint32_t>(KnownModuleType::PropertyHandler);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ *aResult = 0;
+ if (mWorkerState != WorkerState::Done) {
+ return NS_OK;
+ }
+
+#if defined(MOZ_LAUNCHER_PROCESS)
+ if (mDynamicBlocklist.Contains(aLeafName)) {
+ *aResult |= nsIAboutThirdParty::ModuleType_BlockedByUser;
+ }
+ if (mDynamicBlocklistAtLaunch.Contains(aLeafName)) {
+ *aResult |= nsIAboutThirdParty::ModuleType_BlockedByUserAtLaunch;
+ }
+#endif
+
+ uint32_t flags;
+ if (!mKnownModules.Get(aLeafName, &flags)) {
+ *aResult |= nsIAboutThirdParty::ModuleType_Unknown;
+ return NS_OK;
+ }
+
+ if (flags & (1u << static_cast<uint32_t>(KnownModuleType::Ime))) {
+ *aResult |= nsIAboutThirdParty::ModuleType_IME;
+ }
+
+ if (flags & kShellExtensions) {
+ *aResult |= nsIAboutThirdParty::ModuleType_ShellExtension;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP AboutThirdParty::LookupApplication(
+ const nsAString& aModulePath, nsIInstalledApplication** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ *aResult = nullptr;
+ if (mWorkerState != WorkerState::Done) {
+ return NS_OK;
+ }
+
+ const nsDependentSubstring leaf = nt::GetLeafName(aModulePath);
+ if (leaf.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // Look up the component path's map first because it's more accurate
+ // than the location's array.
+ nsCOMPtr<nsIInstalledApplication> app = mComponentPaths.Get(aModulePath);
+ if (app) {
+ app.forget(aResult);
+ return NS_OK;
+ }
+
+ auto bounds = EqualRange(mLocations, 0, mLocations.Length(),
+ InstallLocationComparator(aModulePath));
+
+ // If more than one application includes the module, we return null
+ // because there is no way to know which is the real owner.
+ if (bounds.second() - bounds.first() != 1) {
+ return NS_OK;
+ }
+
+ app = mLocations[bounds.first()].second();
+ app.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP AboutThirdParty::GetIsDynamicBlocklistAvailable(
+ bool* aIsDynamicBlocklistAvailable) {
+ *aIsDynamicBlocklistAvailable =
+ !GetDynamicBlocklistSpan(DllServices::Get()).IsEmpty();
+ return NS_OK;
+}
+
+NS_IMETHODIMP AboutThirdParty::GetIsDynamicBlocklistDisabled(
+ bool* aIsDynamicBlocklistDisabled) {
+ *aIsDynamicBlocklistDisabled = IsDynamicBlocklistDisabled(
+ gSafeMode, CommandLine::ForCurrentProcess()->HasSwitch(UTF8ToWide(
+ mozilla::geckoargs::sDisableDynamicDllBlocklist.sMatch)));
+ return NS_OK;
+}
+
+NS_IMETHODIMP AboutThirdParty::UpdateBlocklist(const nsAString& aLeafName,
+ bool aNewBlockStatus,
+ JSContext* aCx,
+ dom::Promise** aResult) {
+#if defined(MOZ_LAUNCHER_PROCESS)
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ MOZ_ASSERT(global);
+
+ ErrorResult result;
+ RefPtr<dom::Promise> promise(dom::Promise::Create(global, result));
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ auto returnPromise = MakeScopeExit([&] { promise.forget(aResult); });
+
+ if (aNewBlockStatus) {
+ mDynamicBlocklist.Insert(aLeafName);
+ } else {
+ mDynamicBlocklist.Remove(aLeafName);
+ }
+
+ auto newTask = MakeUnique<DynamicBlocklistWriter>(promise, mDynamicBlocklist);
+ if (!newTask->IsReady()) {
+ promise->MaybeReject(NS_ERROR_CANNOT_CONVERT_DATA);
+ return NS_OK;
+ }
+
+ UniquePtr<DynamicBlocklistWriter> oldTask(
+ mPendingWriter.exchange(newTask.release()));
+ if (oldTask) {
+ oldTask->Cancel();
+ }
+
+ nsresult rv = NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(__func__,
+ [self = RefPtr{this}]() {
+ UniquePtr<DynamicBlocklistWriter> task(
+ self->mPendingWriter.exchange(nullptr));
+ if (task) {
+ task->Run();
+ }
+ }),
+ NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ promise->MaybeReject(rv);
+ }
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif // defined(MOZ_LAUNCHER_PROCESS)
+}
+
+RefPtr<BackgroundThreadPromise> AboutThirdParty::CollectSystemInfoAsync() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Allow only the first call to start a background task.
+ if (mWorkerState.compareExchange(WorkerState::Init, WorkerState::Running)) {
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "AboutThirdParty::BackgroundThread", [self = RefPtr{this}]() mutable {
+ self->BackgroundThread();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "AboutThirdParty::BackgroundThread Done",
+ [self]() { self->mPromise->Resolve(true, __func__); }));
+ });
+
+ nsresult rv =
+ NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ mPromise->Reject(rv, __func__);
+ }
+ }
+
+ return mPromise;
+}
+
+NS_IMETHODIMP
+AboutThirdParty::CollectSystemInfo(JSContext* aCx, dom::Promise** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ MOZ_ASSERT(global);
+
+ ErrorResult result;
+ RefPtr<dom::Promise> promise(dom::Promise::Create(global, result));
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ CollectSystemInfoAsync()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](bool) { promise->MaybeResolve(JS::NullHandleValue); },
+ [promise](nsresult aRv) { promise->MaybeReject(aRv); });
+
+ promise.forget(aResult);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/aboutthirdparty/AboutThirdParty.h b/toolkit/components/aboutthirdparty/AboutThirdParty.h
new file mode 100644
index 0000000000..9aa8310a1f
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/AboutThirdParty.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef __AboutThirdParty_h__
+#define __AboutThirdParty_h__
+
+#include "mozilla/MozPromise.h"
+#include "nsIAboutThirdParty.h"
+#include "nsInterfaceHashtable.h"
+#include "nsTArray.h"
+#include "nsTHashMap.h"
+#include "nsTHashSet.h"
+
+namespace mozilla {
+
+class DynamicBlocklistWriter;
+
+using InstallLocationT =
+ CompactPair<nsString, nsCOMPtr<nsIInstalledApplication>>;
+using ComponentPathMapT = nsInterfaceHashtable<nsStringCaseInsensitiveHashKey,
+ nsIInstalledApplication>;
+
+enum class KnownModuleType : uint32_t {
+ Ime = 0,
+ IconOverlay,
+ ContextMenuHandler,
+ CopyHookHandler,
+ DragDropHandler,
+ PropertySheetHandler,
+ DataHandler,
+ DropHandler,
+ IconHandler,
+ InfotipHandler,
+ PropertyHandler,
+
+ Last,
+};
+
+struct InstallLocationComparator {
+ const nsAString& mFilePath;
+
+ explicit InstallLocationComparator(const nsAString& aFilePath);
+ int operator()(const InstallLocationT& aLocation) const;
+};
+
+class InstalledApplication final : public nsIInstalledApplication {
+ nsString mName;
+ nsString mPublisher;
+
+ ~InstalledApplication() = default;
+
+ public:
+ InstalledApplication() = default;
+ InstalledApplication(nsString&& aAppName, nsString&& aPublisher);
+
+ InstalledApplication(InstalledApplication&&) = delete;
+ InstalledApplication& operator=(InstalledApplication&&) = delete;
+ InstalledApplication(const InstalledApplication&) = delete;
+ InstalledApplication& operator=(const InstalledApplication&) = delete;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIINSTALLEDAPPLICATION
+};
+
+using BackgroundThreadPromise =
+ MozPromise<bool /* aIgnored */, nsresult, /* IsExclusive */ false>;
+
+class AboutThirdParty final : public nsIAboutThirdParty {
+ // Atomic only supports 32-bit or 64-bit types.
+ enum class WorkerState : uint32_t {
+ Init,
+ Running,
+ Done,
+ };
+ Atomic<WorkerState, SequentiallyConsistent> mWorkerState;
+ RefPtr<BackgroundThreadPromise::Private> mPromise;
+ nsTHashMap<nsStringCaseInsensitiveHashKey, uint32_t> mKnownModules;
+ ComponentPathMapT mComponentPaths;
+ nsTArray<InstallLocationT> mLocations;
+
+#if defined(MOZ_LAUNCHER_PROCESS)
+ Atomic<DynamicBlocklistWriter*> mPendingWriter;
+ // The current blocklist. May differ from mDynamicBlocklistAtLaunch
+ // if the user has blocked/unblocked modules. Note that this does not
+ // take effect until restart.
+ nsTHashSet<nsStringCaseInsensitiveHashKey> mDynamicBlocklist;
+ // The blocklist that was used at launch, which is currently in effect.
+ nsTHashSet<nsStringCaseInsensitiveHashKey> mDynamicBlocklistAtLaunch;
+#endif
+
+ ~AboutThirdParty() = default;
+ void BackgroundThread();
+ void AddKnownModule(const nsString& aPath, KnownModuleType aType);
+
+ public:
+ static already_AddRefed<AboutThirdParty> GetSingleton();
+
+ AboutThirdParty();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIABOUTTHIRDPARTY
+
+ // Have a function separated from dom::Promise so that
+ // both JS method and GTest can use.
+ RefPtr<BackgroundThreadPromise> CollectSystemInfoAsync();
+};
+
+} // namespace mozilla
+
+#endif // __AboutThirdParty_h__
diff --git a/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp
new file mode 100644
index 0000000000..050975c9da
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "AboutThirdPartyUtils.h"
+
+#include "nsUnicharUtils.h"
+
+namespace mozilla {
+
+int32_t CompareIgnoreCase(const nsAString& aStr1, const nsAString& aStr2) {
+ uint32_t len1 = aStr1.Length();
+ uint32_t len2 = aStr2.Length();
+ uint32_t lenMin = XPCOM_MIN(len1, len2);
+
+ int32_t result = nsCaseInsensitiveStringComparator(
+ aStr1.BeginReading(), aStr2.BeginReading(), lenMin, lenMin);
+ return result ? result : len1 - len2;
+}
+
+bool MsiPackGuid(const nsAString& aGuid, nsAString& aPacked) {
+ if (aGuid.Length() != 38 || aGuid.First() != u'{' || aGuid.Last() != u'}') {
+ return false;
+ }
+
+ constexpr int kPackedLength = 32;
+ const uint8_t kIndexMapping[kPackedLength] = {
+ // clang-format off
+ 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
+ 0x0d, 0x0c, 0x0b, 0x0a, 0x12, 0x11, 0x10, 0x0f,
+ 0x15, 0x14, 0x17, 0x16, 0x1a, 0x19, 0x1c, 0x1b,
+ 0x1e, 0x1d, 0x20, 0x1f, 0x22, 0x21, 0x24, 0x23,
+ // clang-format on
+ };
+
+ int index = 0;
+ aPacked.SetLength(kPackedLength);
+ for (auto iter = aPacked.BeginWriting(), strEnd = aPacked.EndWriting();
+ iter != strEnd; ++iter, ++index) {
+ *iter = aGuid[kIndexMapping[index]];
+ }
+
+ return true;
+}
+
+bool CorrectMsiComponentPath(nsAString& aPath) {
+ if (aPath.Length() < 3 || !aPath.BeginReading()[0]) {
+ return false;
+ }
+
+ char16_t* strBegin = aPath.BeginWriting();
+
+ if (strBegin[1] == u'?') {
+ strBegin[1] = strBegin[0] == u'\\' ? u'\\' : u':';
+ }
+
+ if (strBegin[1] != u':' || strBegin[2] != u'\\') {
+ return false;
+ }
+
+ if (aPath.Length() > 3 && aPath.BeginReading()[3] == u'?') {
+ aPath.ReplaceLiteral(3, 1, u"");
+ }
+ return true;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.h b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.h
new file mode 100644
index 0000000000..c65e5f7c57
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef __AboutThirdPartyUtils_h__
+#define __AboutThirdPartyUtils_h__
+
+#include "nsString.h"
+
+namespace mozilla {
+
+// Define a custom case-insensitive string comparator wrapping
+// nsCaseInsensitiveStringComparator to sort items alphabetically because
+// nsCaseInsensitiveStringComparator sorts items by the length first.
+int32_t CompareIgnoreCase(const nsAString& aStr1, const nsAString& aStr2);
+
+// Mimicking the logic in msi!PackGUID to convert a GUID string to
+// a packed GUID used as registry keys.
+bool MsiPackGuid(const nsAString& aGuid, nsAString& aPacked);
+
+// Mimicking the validation logic for a path in msi!_GetComponentPath
+//
+// Accecpted patterns and conversions:
+// C:\path --> C:\path
+// C?\path --> C:\path
+// C:\?path --> C:\path
+//
+// msi!_GetComponentPath also checks the existence by calling
+// RegOpenKeyExW or GetFileAttributesExW, but we don't need it.
+bool CorrectMsiComponentPath(nsAString& aPath);
+
+} // namespace mozilla
+
+#endif // __AboutThirdPartyUtils_h__
diff --git a/toolkit/components/aboutthirdparty/AboutThirdParty_TestMethods.cpp b/toolkit/components/aboutthirdparty/AboutThirdParty_TestMethods.cpp
new file mode 100644
index 0000000000..b52ba49eac
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/AboutThirdParty_TestMethods.cpp
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "AboutThirdParty.h"
+
+#include <windows.h>
+#include <shlobj.h>
+#include <shobjidl.h>
+
+namespace {
+
+class FileDialogEventsForTesting final : public IFileDialogEvents {
+ mozilla::Atomic<uint32_t> mRefCnt;
+ const nsString mTargetName;
+ RefPtr<IShellItem> mTargetDir;
+
+ ~FileDialogEventsForTesting() = default;
+
+ public:
+ FileDialogEventsForTesting(const nsAString& aTargetName,
+ IShellItem* aTargetDir)
+ : mRefCnt(0),
+ mTargetName(PromiseFlatString(aTargetName)),
+ mTargetDir(aTargetDir) {}
+
+ // IUnknown
+
+ STDMETHODIMP QueryInterface(REFIID aRefIID, void** aResult) {
+ if (!aResult) {
+ return E_INVALIDARG;
+ }
+
+ if (aRefIID == IID_IFileDialogEvents) {
+ RefPtr ref(static_cast<IFileDialogEvents*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+
+ STDMETHODIMP_(ULONG) AddRef() { return ++mRefCnt; }
+
+ STDMETHODIMP_(ULONG) Release() {
+ ULONG result = --mRefCnt;
+ if (!result) {
+ delete this;
+ }
+ return result;
+ }
+
+ // IFileDialogEvents
+
+ STDMETHODIMP OnFileOk(IFileDialog*) { return E_NOTIMPL; }
+ STDMETHODIMP OnFolderChanging(IFileDialog*, IShellItem*) { return E_NOTIMPL; }
+ STDMETHODIMP OnShareViolation(IFileDialog*, IShellItem*,
+ FDE_SHAREVIOLATION_RESPONSE*) {
+ return E_NOTIMPL;
+ }
+ STDMETHODIMP OnTypeChange(IFileDialog*) { return E_NOTIMPL; }
+ STDMETHODIMP OnOverwrite(IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE*) {
+ return E_NOTIMPL;
+ }
+ STDMETHODIMP OnFolderChange(IFileDialog*) { return E_NOTIMPL; }
+ STDMETHODIMP OnSelectionChange(IFileDialog* aDialog) {
+ if (::GetModuleHandleW(mTargetName.get())) {
+ aDialog->Close(S_OK);
+ } else {
+ // This sends a notification which is processed asynchronously. Calling
+ // SetFolder from OnSelectionChange gives the main thread some cycles to
+ // process other window messages, while calling SetFolder from
+ // OnFolderChange causes freeze. Thus we can safely wait until a common
+ // dialog loads a shell extension without blocking UI.
+ aDialog->SetFolder(mTargetDir);
+ }
+
+ return E_NOTIMPL;
+ }
+};
+
+} // anonymous namespace
+
+namespace mozilla {
+
+NS_IMETHODIMP AboutThirdParty::OpenAndCloseFileDialogForTesting(
+ const nsAString& aModuleName, const nsAString& aInitialDir,
+ const nsAString& aFilter) {
+ // Notify the shell of a new icon handler which should have been registered
+ // by the test script.
+ ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
+
+ RefPtr<IFileOpenDialog> dialog;
+ if (FAILED(::CoCreateInstance(CLSID_FileOpenDialog, nullptr,
+ CLSCTX_INPROC_SERVER, IID_IFileOpenDialog,
+ getter_AddRefs(dialog)))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ const nsString& filter = PromiseFlatString(aFilter);
+ COMDLG_FILTERSPEC fileFilter = {L"Test Target", filter.get()};
+ if (FAILED(dialog->SetFileTypes(1, &fileFilter))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<IShellItem> folder;
+ if (FAILED(::SHCreateItemFromParsingName(PromiseFlatString(aInitialDir).get(),
+ nullptr, IID_IShellItem,
+ getter_AddRefs(folder)))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Need to send a first notification outside FileDialogEventsForTesting.
+ if (FAILED(dialog->SetFolder(folder))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr events(new FileDialogEventsForTesting(aModuleName, folder));
+
+ DWORD cookie;
+ if (FAILED(dialog->Advise(events, &cookie))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (FAILED(dialog->Show(nullptr))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (FAILED(dialog->Unadvise(cookie))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/aboutthirdparty/MsiDatabase.cpp b/toolkit/components/aboutthirdparty/MsiDatabase.cpp
new file mode 100644
index 0000000000..6c667072b7
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/MsiDatabase.cpp
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "MsiDatabase.h"
+
+#ifdef UNICODE
+# define MSIDBOPEN_READONLY_W MSIDBOPEN_READONLY
+# define INSTALLPROPERTY_LOCALPACKAGE_W INSTALLPROPERTY_LOCALPACKAGE
+#else
+// MSIDBOPEN_READONLY is defined as `(LPCTSTR)0` in msiquery.h, so we need to
+// cast it to LPCWSTR.
+# define MSIDBOPEN_READONLY_W reinterpret_cast<LPCWSTR>(MSIDBOPEN_READONLY)
+// INSTALLPROPERTY_LOCALPACKAGE is defined as `__TEXT("LocalPackage")` in msi.h,
+// so we need to define a wchar_t version.
+# define INSTALLPROPERTY_LOCALPACKAGE_W L"LocalPackage"
+#endif // UNICODE
+
+namespace mozilla {
+
+/*static*/
+UniquePtr<wchar_t[]> MsiDatabase::GetRecordString(MSIHANDLE aRecord,
+ UINT aFieldIndex) {
+ // The 3rd parameter of MsiRecordGetStringW must not be nullptr.
+ wchar_t kEmptyString[] = L"";
+ DWORD len = 0;
+ UINT ret = ::MsiRecordGetStringW(aRecord, aFieldIndex, kEmptyString, &len);
+ if (ret != ERROR_MORE_DATA) {
+ return nullptr;
+ }
+
+ // |len| returned from MsiRecordGetStringW does not include
+ // a null-character, but a length to pass to MsiRecordGetStringW
+ // needs to include a null-character.
+ ++len;
+
+ auto buf = MakeUnique<wchar_t[]>(len);
+ ret = ::MsiRecordGetStringW(aRecord, aFieldIndex, buf.get(), &len);
+ if (ret != ERROR_SUCCESS) {
+ return nullptr;
+ }
+
+ return buf;
+}
+
+MsiDatabase::MsiDatabase(const wchar_t* aDatabasePath) {
+ MSIHANDLE handle = 0;
+ UINT ret = ::MsiOpenDatabaseW(aDatabasePath, MSIDBOPEN_READONLY_W, &handle);
+ if (ret != ERROR_SUCCESS) {
+ return;
+ }
+
+ mDatabase.own(handle);
+}
+
+Maybe<MsiDatabase> MsiDatabase::FromProductId(const wchar_t* aProductId) {
+ DWORD len = MAX_PATH;
+ wchar_t bufStack[MAX_PATH];
+ UINT ret = ::MsiGetProductInfoW(aProductId, INSTALLPROPERTY_LOCALPACKAGE_W,
+ bufStack, &len);
+ if (ret == ERROR_SUCCESS) {
+ return Some(MsiDatabase(bufStack));
+ }
+
+ if (ret != ERROR_MORE_DATA) {
+ return Nothing();
+ }
+
+ // |len| returned from MsiGetProductInfoW does not include
+ // a null-character, but a length to pass to MsiGetProductInfoW
+ // needs to include a null-character.
+ ++len;
+
+ std::unique_ptr<wchar_t[]> bufHeap(new wchar_t[len]);
+ ret = ::MsiGetProductInfoW(aProductId, INSTALLPROPERTY_LOCALPACKAGE_W,
+ bufHeap.get(), &len);
+ if (ret == ERROR_SUCCESS) {
+ return Some(MsiDatabase(bufHeap.get()));
+ }
+
+ return Nothing();
+}
+
+MsiDatabase::operator bool() const { return !!mDatabase; }
+
+} // namespace mozilla
diff --git a/toolkit/components/aboutthirdparty/MsiDatabase.h b/toolkit/components/aboutthirdparty/MsiDatabase.h
new file mode 100644
index 0000000000..91c1370b8d
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/MsiDatabase.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef __MsiDatabase_h__
+#define __MsiDatabase_h__
+
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+#include "nsWindowsHelpers.h"
+
+#include <windows.h>
+#include <msi.h>
+#include <msiquery.h>
+
+namespace mozilla {
+
+class MsiDatabase final {
+ static UniquePtr<wchar_t[]> GetRecordString(MSIHANDLE aRecord,
+ UINT aFieldIndex);
+
+ nsAutoMsiHandle mDatabase;
+
+ MsiDatabase() = default;
+ explicit MsiDatabase(const wchar_t* aDatabasePath);
+
+ public:
+ // A callback function passed to ExecuteSingleColumnQuery uses this type
+ // to control the enumeration loop.
+ enum class CallbackResult { Continue, Stop };
+
+ static Maybe<MsiDatabase> FromProductId(const wchar_t* aProductId);
+
+ MsiDatabase(const MsiDatabase&) = delete;
+ MsiDatabase& operator=(const MsiDatabase&) = delete;
+ MsiDatabase(MsiDatabase&& aOther) : mDatabase(aOther.mDatabase.disown()) {}
+ MsiDatabase& operator=(MsiDatabase&& aOther) {
+ if (this != &aOther) {
+ mDatabase.own(aOther.mDatabase.disown());
+ }
+ return *this;
+ }
+
+ explicit operator bool() const;
+
+ template <typename CallbackT>
+ bool ExecuteSingleColumnQuery(const wchar_t* aQuery,
+ const CallbackT& aCallback) const {
+ MSIHANDLE handle;
+ UINT ret = ::MsiDatabaseOpenViewW(mDatabase, aQuery, &handle);
+ if (ret != ERROR_SUCCESS) {
+ return false;
+ }
+
+ nsAutoMsiHandle view(handle);
+
+ ret = ::MsiViewExecute(view, 0);
+ if (ret != ERROR_SUCCESS) {
+ return false;
+ }
+
+ for (;;) {
+ ret = ::MsiViewFetch(view, &handle);
+ if (ret == ERROR_NO_MORE_ITEMS) {
+ break;
+ } else if (ret != ERROR_SUCCESS) {
+ return false;
+ }
+
+ nsAutoMsiHandle record(handle);
+ UniquePtr<wchar_t[]> guidStr = GetRecordString(record, 1);
+ if (!guidStr) {
+ continue;
+ }
+
+ CallbackResult result = aCallback(guidStr.get());
+ if (result == CallbackResult::Continue) {
+ continue;
+ } else if (result == CallbackResult::Stop) {
+ break;
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Unexpected CallbackResult.");
+ }
+ }
+
+ return true;
+ }
+};
+
+} // namespace mozilla
+
+#endif // __MsiDatabase_h__
diff --git a/toolkit/components/aboutthirdparty/components.conf b/toolkit/components/aboutthirdparty/components.conf
new file mode 100644
index 0000000000..5278392c66
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/components.conf
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{bb6afd78-2e02-4e96-b6b9-eef8cbcdc29c}',
+ 'contract_ids': ['@mozilla.org/about-thirdparty;1'],
+ 'type': 'AboutThirdParty',
+ 'singleton': True,
+ 'constructor': 'mozilla::AboutThirdParty::GetSingleton',
+ 'headers': ['mozilla/AboutThirdParty.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
diff --git a/toolkit/components/aboutthirdparty/content/aboutThirdParty.css b/toolkit/components/aboutthirdparty/content/aboutThirdParty.css
new file mode 100644
index 0000000000..6b20f86d3a
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/content/aboutThirdParty.css
@@ -0,0 +1,114 @@
+/* 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/. */
+
+.svg-common {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 16px;
+}
+
+.svg-button {
+ height: 32px;
+ width: 32px;
+ min-width: 0;
+ padding: 0;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.button-open-dir {
+ background-image: url(chrome://global/skin/icons/folder.svg);
+}
+
+.button-block {
+ background-image: url(chrome://global/skin/icons/blocked.svg);
+}
+
+.button-expand {
+ background-image: url(chrome://global/skin/icons/arrow-down.svg);
+}
+
+.button-collapse {
+ background-image: url(chrome://global/skin/icons/arrow-up.svg);
+}
+
+.card-head > img {
+ margin-inline-start: .5em;
+}
+
+.image-warning {
+ fill: #fcd100;
+}
+
+.blocked-by-builtin {
+ fill: red;
+}
+.button-block.module-blocked {
+ fill: red;
+ background-image: url(chrome://global/skin/icons/close-fill.svg);
+}
+.button-block.module-blocked.blocklist-disabled {
+ fill: goldenrod;
+}
+
+
+.card {
+ margin-bottom: 16px;
+}
+
+.card-head {
+ display: flex;
+ align-items: center;
+}
+
+.module-name {
+ margin: 0;
+}
+
+.module-tags {
+ margin-inline: .5em;
+}
+
+.module-tag {
+ font-size: 12px;
+ white-space: nowrap;
+ border-radius: 4px;
+ border: 1px solid transparent;
+ background-color: var(--in-content-button-background);
+ padding: 0 2px;
+ margin-inline-end: 2px;
+}
+
+.module-details {
+ font-size: 14px;
+ margin-top: var(--card-padding);
+}
+
+.module-details > div {
+ display: flex;
+}
+
+.module-detail-label {
+ width: 12em;
+}
+
+.spacer {
+ flex-grow: 1;
+}
+
+.event-table {
+ width: max-content;
+ margin-top: var(--card-padding);
+}
+
+.event-table > thead,
+.event-table > tbody > tr {
+ height: 28px;
+ vertical-align: middle;
+}
+
+.event-table > thead > tr > th,
+.event-table > tbody > tr > td {
+ padding-inline: 10px;
+}
diff --git a/toolkit/components/aboutthirdparty/content/aboutThirdParty.html b/toolkit/components/aboutthirdparty/content/aboutThirdParty.html
new file mode 100644
index 0000000000..5cba5847e3
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/content/aboutThirdParty.html
@@ -0,0 +1,152 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <title data-l10n-id="third-party-page-title"></title>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:; object-src 'none'"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <link
+ rel="stylesheet"
+ href="chrome://global/skin/in-content/info-pages.css"
+ />
+ <link rel="stylesheet" href="chrome://global/content/aboutThirdParty.css" />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="toolkit/global/processTypes.ftl" />
+ <link rel="localization" href="toolkit/about/aboutThirdParty.ftl" />
+ <script src="chrome://global/content/aboutThirdParty.js"></script>
+ </head>
+ <body class="wide-container">
+ <h1 data-l10n-id="third-party-page-title"></h1>
+ <p data-l10n-id="third-party-intro"></p>
+
+ <div class="button-container">
+ <button
+ id="button-copy-to-clipboard"
+ data-l10n-id="third-party-button-copy-to-clipboard"
+ ></button>
+ <img
+ src="chrome://global/skin/icons/loading.png"
+ class="svg-common"
+ id="background-data-loading"
+ data-l10n-id="third-party-loading-data"
+ />
+ <button
+ id="button-reload"
+ hidden
+ data-l10n-id="third-party-button-reload"
+ ></button>
+ </div>
+
+ <h2 data-l10n-id="third-party-section-title"></h2>
+ <p id="no-data" data-l10n-id="third-party-message-empty" hidden></p>
+
+ <div id="main"></div>
+
+ <template name="module-detail-row">
+ <div><label class="module-detail-label"></label><span></span></div>
+ </template>
+
+ <template name="event-table-row">
+ <tr>
+ <td>
+ <span class="process-type"></span>
+ (<span class="process-id"></span>)
+ </td>
+ <td>
+ <span class="event-duration"></span>
+ <span
+ class="module-tag tag-background"
+ hidden
+ data-l10n-id="third-party-tag-background"
+ ></span>
+ </td>
+ <td></td>
+ </tr>
+ </template>
+
+ <template name="card-blocked">
+ <div class="card card-no-hover">
+ <div class="card-head">
+ <h3 class="module-name"></h3>
+ <button
+ class="svg-common svg-button button-block"
+ hidden
+ data-l10n-id="third-party-button-to-block"
+ ></button>
+ </div>
+ </div>
+ </template>
+
+ <template name="card">
+ <div class="card card-no-hover">
+ <div class="card-head">
+ <h3 class="module-name"></h3>
+ <img
+ src="chrome://global/skin/icons/warning.svg"
+ class="svg-common image-warning"
+ hidden
+ data-l10n-id="third-party-icon-warning"
+ />
+ <img
+ src="chrome://global/skin/icons/security-broken.svg"
+ class="svg-common image-unsigned"
+ hidden
+ data-l10n-id="third-party-icon-unsigned"
+ />
+ <div class="module-tags">
+ <span
+ class="module-tag tag-ime"
+ hidden
+ data-l10n-id="third-party-tag-ime"
+ ></span>
+ <span
+ class="module-tag tag-shellex"
+ hidden
+ data-l10n-id="third-party-tag-shellex"
+ ></span>
+ </div>
+ <button
+ class="svg-common svg-button button-open-dir"
+ data-l10n-id="third-party-button-open"
+ ></button>
+ <button
+ class="svg-common svg-button button-block"
+ hidden
+ data-l10n-id="third-party-button-to-block"
+ ></button>
+ <img
+ src="chrome://global/skin/icons/warning.svg"
+ class="svg-common blocked-by-builtin"
+ hidden
+ data-l10n-id="third-party-blocked-by-builtin"
+ />
+ <div class="spacer"></div>
+ <button
+ class="svg-common svg-button button-expand"
+ data-l10n-id="third-party-button-expand"
+ ></button>
+ </div>
+
+ <div class="module-details"></div>
+
+ <table class="event-table" hidden>
+ <thead>
+ <tr>
+ <th data-l10n-id="third-party-th-process" />
+ <th data-l10n-id="third-party-th-duration" />
+ <th data-l10n-id="third-party-th-status" />
+ </tr>
+ </thead>
+ <tbody></tbody>
+ </table>
+ </div>
+ </template>
+ </body>
+</html>
diff --git a/toolkit/components/aboutthirdparty/content/aboutThirdParty.js b/toolkit/components/aboutthirdparty/content/aboutThirdParty.js
new file mode 100644
index 0000000000..9029064baa
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/content/aboutThirdParty.js
@@ -0,0 +1,690 @@
+/* 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/. */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { ProcessType } = ChromeUtils.importESModule(
+ "resource://gre/modules/ProcessType.sys.mjs"
+);
+
+let AboutThirdParty = null;
+let CrashModuleSet = null;
+let gBackgroundTasksDone = false;
+
+function moduleCompareForDisplay(a, b) {
+ // First, show blocked modules that were blocked at launch - this will keep the ordering
+ // consistent when the user blocks/unblocks things.
+ const bBlocked =
+ b.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
+ ? 1
+ : 0;
+ const aBlocked =
+ a.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
+ ? 1
+ : 0;
+
+ let diff = bBlocked - aBlocked;
+ if (diff) {
+ return diff;
+ }
+
+ // Next, show crasher modules
+ diff = b.isCrasher - a.isCrasher;
+ if (diff) {
+ return diff;
+ }
+
+ // Then unknown-type modules
+ diff = a.typeFlags - b.typeFlags;
+ if (diff) {
+ return diff;
+ }
+
+ // Lastly sort the remaining modules in descending order
+ // of duration to move up slower modules.
+ return b.loadingOnMain - a.loadingOnMain;
+}
+
+async function fetchData() {
+ let data = null;
+ try {
+ data = await Services.telemetry.getUntrustedModuleLoadEvents(
+ Services.telemetry.INCLUDE_OLD_LOADEVENTS |
+ Services.telemetry.KEEP_LOADEVENTS_NEW |
+ Services.telemetry.INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS |
+ Services.telemetry.EXCLUDE_STACKINFO_FROM_LOADEVENTS
+ );
+ } catch (e) {
+ // No error report in case of NS_ERROR_NOT_AVAILABLE
+ // because the method throws it when data is empty.
+ if (
+ !(e instanceof Components.Exception) ||
+ e.result != Cr.NS_ERROR_NOT_AVAILABLE
+ ) {
+ console.error(e);
+ }
+ }
+
+ if (!data || !data.modules || !data.processes) {
+ return null;
+ }
+
+ // The original telemetry data structure has an array of modules
+ // and an array of loading events referring to the module array's
+ // item via its index.
+ // To easily display data per module, we put loading events into
+ // a corresponding module object and return the module array.
+
+ for (const module of data.modules) {
+ module.events = [];
+ module.loadingOnMain = { count: 0, sum: 0 };
+
+ const moduleName = module.dllFile?.leafName;
+ module.typeFlags = AboutThirdParty.lookupModuleType(moduleName);
+ module.isCrasher = CrashModuleSet?.has(moduleName);
+
+ module.application = AboutThirdParty.lookupApplication(
+ module.dllFile?.path
+ );
+ module.moduleName = module.dllFile?.leafName;
+ module.hasLoadInformation = true;
+ }
+
+ let blockedModules = data.blockedModules.map(blockedModuleName => {
+ return {
+ moduleName: blockedModuleName,
+ typeFlags: AboutThirdParty.lookupModuleType(blockedModuleName),
+ isCrasher: CrashModuleSet?.has(blockedModuleName),
+ hasLoadInformation: false,
+ };
+ });
+
+ for (const [proc, perProc] of Object.entries(data.processes)) {
+ for (const event of perProc.events) {
+ // The expected format of |proc| is <type>.<pid> like "browser.0x1234"
+ const [ptype, pidHex] = proc.split(".");
+ event.processType = ptype;
+ event.processID = parseInt(pidHex, 16);
+
+ event.mainThread =
+ event.threadName == "MainThread" || event.threadName == "Main Thread";
+
+ const module = data.modules[event.moduleIndex];
+ if (event.mainThread) {
+ ++module.loadingOnMain.count;
+ module.loadingOnMain.sum += event.loadDurationMS;
+ }
+
+ module.events.push(event);
+ }
+ }
+
+ for (const module of data.modules) {
+ const avg = module.loadingOnMain.count
+ ? module.loadingOnMain.sum / module.loadingOnMain.count
+ : 0;
+ module.loadingOnMain = avg;
+ module.events.sort((a, b) => {
+ const diff = a.processType.localeCompare(b.processType);
+ return diff ? diff : a.processID - b.processID;
+ });
+ // If this module was blocked but not by the user, it must have been blocked
+ // by the static blocklist.
+ // But we don't know this for sure unless the background tasks were done
+ // by the time we gathered data about the module above.
+ if (gBackgroundTasksDone) {
+ module.isBlockedByBuiltin =
+ !(
+ module.typeFlags &
+ Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
+ ) &&
+ !!module.events.length &&
+ module.events.every(e => e.loadStatus !== 0);
+ } else {
+ module.isBlockedByBuiltin = false;
+ }
+ }
+
+ data.modules.sort(moduleCompareForDisplay);
+
+ return { modules: data.modules, blocked: blockedModules };
+}
+
+function setContent(element, text, l10n) {
+ if (text) {
+ element.textContent = text;
+ } else if (l10n) {
+ element.setAttribute("data-l10n-id", l10n);
+ }
+}
+
+function onClickOpenDir(event) {
+ const module = event.target.closest(".card").module;
+ if (!module?.dllFile) {
+ return;
+ }
+ module.dllFile.reveal();
+}
+
+// Returns whether we should restart.
+async function confirmRestartPrompt() {
+ let [msg, title, restartButtonText, restartLaterButtonText] =
+ await document.l10n.formatValues([
+ { id: "third-party-blocking-requires-restart" },
+ { id: "third-party-should-restart-title" },
+ { id: "third-party-restart-now" },
+ { id: "third-party-restart-later" },
+ ]);
+ let buttonFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1_DEFAULT;
+ let buttonIndex = Services.prompt.confirmEx(
+ window.browsingContext.topChromeWindow,
+ title,
+ msg,
+ buttonFlags,
+ restartButtonText,
+ restartLaterButtonText,
+ null,
+ null,
+ {}
+ );
+ return buttonIndex === 0;
+}
+
+let processingBlockRequest = false;
+async function onBlock(event) {
+ const module = event.target.closest(".card").module;
+ if (!module?.moduleName) {
+ return;
+ }
+ // To avoid race conditions, don't allow any modules to be blocked/unblocked
+ // until we've updated and written the blocklist.
+ if (processingBlockRequest) {
+ return;
+ }
+ processingBlockRequest = true;
+
+ let updatedBlocklist = false;
+ try {
+ const wasBlocked = event.target.classList.contains("module-blocked");
+ await AboutThirdParty.updateBlocklist(module.moduleName, !wasBlocked);
+
+ event.target.classList.toggle("module-blocked");
+ let blockButtonL10nId;
+ if (wasBlocked) {
+ blockButtonL10nId = "third-party-button-to-block";
+ } else {
+ blockButtonL10nId = AboutThirdParty.isDynamicBlocklistDisabled
+ ? "third-party-button-to-unblock-disabled"
+ : "third-party-button-to-unblock";
+ }
+ event.target.setAttribute("data-l10n-id", blockButtonL10nId);
+ updatedBlocklist = true;
+ } catch (ex) {
+ console.error("Failed to update the blocklist file - ", ex.result);
+ } finally {
+ processingBlockRequest = false;
+ }
+ if (updatedBlocklist && (await confirmRestartPrompt())) {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+ if (!cancelQuit.data) {
+ // restart was not cancelled.
+ // Note that even if we're in safe mode, we don't restart
+ // into safe mode, because it's likely the user is trying to
+ // fix a crash or something, and they'd probably like to
+ // see if it works.
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
+ );
+ }
+ }
+}
+
+function onClickExpand(event) {
+ const card = event.target.closest(".card");
+ const button = event.target.closest("button");
+
+ const table = card.querySelector(".event-table");
+ if (!table) {
+ return;
+ }
+
+ if (table.hidden) {
+ table.hidden = false;
+ button.classList.add("button-collapse");
+ button.classList.remove("button-expand");
+ setContent(button, null, "third-party-button-collapse");
+ } else {
+ table.hidden = true;
+ button.classList.add("button-expand");
+ button.classList.remove("button-collapse");
+ setContent(button, null, "third-party-button-expand");
+ }
+}
+
+function createDetailRow(label, value) {
+ if (!document.templateDetailRow) {
+ document.templateDetailRow = document.querySelector(
+ "template[name=module-detail-row]"
+ );
+ }
+
+ const fragment = document.templateDetailRow.content.cloneNode(true);
+ setContent(fragment.querySelector("div > label"), null, label);
+ setContent(fragment.querySelector("div > span"), value);
+ return fragment;
+}
+
+function copyDataToClipboard(aData) {
+ const modulesData = aData.modules.map(module => {
+ const copied = {
+ name: module.moduleName,
+ fileVersion: module.fileVersion,
+ };
+
+ // We include the typeFlags field only when it's not 0 because
+ // typeFlags == 0 means system info is not yet collected.
+ if (module.typeFlags) {
+ copied.typeFlags = module.typeFlags;
+ }
+ if (module.signedBy) {
+ copied.signedBy = module.signedBy;
+ }
+ if (module.isCrasher) {
+ copied.isCrasher = module.isCrasher;
+ }
+ if (module.companyName) {
+ copied.companyName = module.companyName;
+ }
+ if (module.application) {
+ copied.applicationName = module.application.name;
+ copied.applicationPublisher = module.application.publisher;
+ }
+
+ if (Array.isArray(module.events)) {
+ copied.events = module.events.map(event => {
+ return {
+ processType: event.processType,
+ processID: event.processID,
+ threadID: event.threadID,
+ loadStatus: event.loadStatus,
+ loadDurationMS: event.loadDurationMS,
+ };
+ });
+ }
+
+ return copied;
+ });
+ const blockedData = aData.blocked.map(blockedModule => {
+ const copied = {
+ name: blockedModule.moduleName,
+ };
+ // We include the typeFlags field only when it's not 0 because
+ // typeFlags == 0 means system info is not yet collected.
+ if (blockedModule.typeFlags) {
+ copied.typeFlags = blockedModule.typeFlags;
+ }
+ if (blockedModule.isCrasher) {
+ copied.isCrasher = blockedModule.isCrasher;
+ }
+ return copied;
+ });
+ let clipboardData = { modules: modulesData, blocked: blockedData };
+
+ return navigator.clipboard.writeText(JSON.stringify(clipboardData, null, 2));
+}
+
+function correctProcessTypeForFluent(type) {
+ // GetProcessTypeString() in UntrustedModulesDataSerializer.cpp converted
+ // the "default" process type to "browser" to send as telemetry. We revert
+ // it to pass to ProcessType API.
+ const geckoType = type == "browser" ? "default" : type;
+ return ProcessType.fluentNameFromProcessTypeString(geckoType);
+}
+
+function setUpBlockButton(aCard, isBlocklistDisabled, aModule) {
+ const blockButton = aCard.querySelector(".button-block");
+ if (aModule.hasLoadInformation) {
+ if (!aModule.isBlockedByBuiltin) {
+ blockButton.hidden = aModule.typeFlags == 0;
+ }
+ } else {
+ // This means that this is an entry in the dynamic blocklist that
+ // has not attempted to load, thus we have very little information
+ // about it (just its name). So this should always show up.
+ blockButton.hidden = false;
+ // Bug 1808904 - don't allow unblocking this module before we've loaded
+ // the list of blocked modules in the background task.
+ blockButton.disabled = !gBackgroundTasksDone;
+ }
+ // If we haven't loaded the typeFlags yet and we don't have any load information for this
+ // module, default to showing that the module is blocked (because we must have gotten this
+ // module's info from the dynamic blocklist)
+ if (
+ aModule.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUser ||
+ (aModule.typeFlags == 0 && !aModule.hasLoadInformation)
+ ) {
+ blockButton.classList.add("module-blocked");
+ }
+
+ if (isBlocklistDisabled) {
+ blockButton.classList.add("blocklist-disabled");
+ }
+ if (blockButton.classList.contains("module-blocked")) {
+ blockButton.setAttribute(
+ "data-l10n-id",
+ isBlocklistDisabled
+ ? "third-party-button-to-unblock-disabled"
+ : "third-party-button-to-unblock"
+ );
+ }
+}
+
+function visualizeData(aData) {
+ const templateCard = document.querySelector("template[name=card]");
+ const templateBlockedCard = document.querySelector(
+ "template[name=card-blocked]"
+ );
+ const templateTableRow = document.querySelector(
+ "template[name=event-table-row]"
+ );
+
+ // These correspond to the enum ModuleLoadInfo::Status
+ const labelLoadStatus = [
+ "third-party-status-loaded",
+ "third-party-status-blocked",
+ "third-party-status-redirected",
+ "third-party-status-blocked",
+ ];
+
+ const isBlocklistAvailable =
+ AboutThirdParty.isDynamicBlocklistAvailable &&
+ Services.policies.isAllowed("thirdPartyModuleBlocking");
+ const isBlocklistDisabled = AboutThirdParty.isDynamicBlocklistDisabled;
+
+ const mainContentFragment = new DocumentFragment();
+
+ // Blocklist entries are case-insensitive
+ let lowercaseModuleNames = new Set(
+ aData.modules.map(module => module.moduleName.toLowerCase())
+ );
+ for (const module of aData.blocked) {
+ if (lowercaseModuleNames.has(module.moduleName.toLowerCase())) {
+ // Only show entries that we haven't already tried to load,
+ // because those will already show up in the page
+ continue;
+ }
+ const newCard = templateBlockedCard.content.cloneNode(true);
+ setContent(newCard.querySelector(".module-name"), module.moduleName);
+ // Referred by the button click handlers
+ newCard.querySelector(".card").module = {
+ moduleName: module.moduleName,
+ };
+
+ if (isBlocklistAvailable) {
+ setUpBlockButton(newCard, isBlocklistDisabled, module);
+ }
+ if (module.isCrasher) {
+ newCard.querySelector(".image-warning").hidden = false;
+ }
+ mainContentFragment.appendChild(newCard);
+ }
+
+ for (const module of aData.modules) {
+ const newCard = templateCard.content.cloneNode(true);
+ const moduleName = module.moduleName;
+
+ // Referred by the button click handlers
+ newCard.querySelector(".card").module = {
+ dllFile: module.dllFile,
+ moduleName: module.moduleName,
+ fileVersion: module.fileVersion,
+ };
+
+ setContent(newCard.querySelector(".module-name"), moduleName);
+
+ const modTagsContainer = newCard.querySelector(".module-tags");
+ if (module.typeFlags & Ci.nsIAboutThirdParty.ModuleType_IME) {
+ modTagsContainer.querySelector(".tag-ime").hidden = false;
+ }
+ if (module.typeFlags & Ci.nsIAboutThirdParty.ModuleType_ShellExtension) {
+ modTagsContainer.querySelector(".tag-shellex").hidden = false;
+ }
+
+ newCard.querySelector(".blocked-by-builtin").hidden =
+ !module.isBlockedByBuiltin;
+ if (isBlocklistAvailable) {
+ setUpBlockButton(newCard, isBlocklistDisabled, module);
+ }
+
+ if (module.isCrasher) {
+ newCard.querySelector(".image-warning").hidden = false;
+ }
+
+ if (!module.signedBy) {
+ newCard.querySelector(".image-unsigned").hidden = false;
+ }
+
+ const modDetailContainer = newCard.querySelector(".module-details");
+
+ if (module.application) {
+ modDetailContainer.appendChild(
+ createDetailRow("third-party-detail-app", module.application.name)
+ );
+ modDetailContainer.appendChild(
+ createDetailRow(
+ "third-party-detail-publisher",
+ module.application.publisher
+ )
+ );
+ }
+
+ if (module.fileVersion) {
+ modDetailContainer.appendChild(
+ createDetailRow("third-party-detail-version", module.fileVersion)
+ );
+ }
+
+ const vendorInfo = module.signedBy || module.companyName;
+ if (vendorInfo) {
+ modDetailContainer.appendChild(
+ createDetailRow("third-party-detail-vendor", vendorInfo)
+ );
+ }
+
+ modDetailContainer.appendChild(
+ createDetailRow("third-party-detail-occurrences", module.events.length)
+ );
+ modDetailContainer.appendChild(
+ createDetailRow(
+ "third-party-detail-duration",
+ module.loadingOnMain || "-"
+ )
+ );
+
+ const eventTable = newCard.querySelector(".event-table > tbody");
+ for (const event of module.events) {
+ const fragment = templateTableRow.content.cloneNode(true);
+
+ const row = fragment.querySelector("tr");
+
+ setContent(
+ row.children[0].querySelector(".process-type"),
+ null,
+ correctProcessTypeForFluent(event.processType)
+ );
+ setContent(row.children[0].querySelector(".process-id"), event.processID);
+
+ // Use setContent() instead of simple assignment because
+ // loadDurationMS can be empty (not zero) when a module is
+ // loaded very early in the process and we need to show
+ // a text in that case.
+ setContent(
+ row.children[1].querySelector(".event-duration"),
+ event.loadDurationMS,
+ "third-party-message-no-duration"
+ );
+ row.querySelector(".tag-background").hidden = event.mainThread;
+
+ setContent(row.children[2], null, labelLoadStatus[event.loadStatus]);
+ eventTable.appendChild(fragment);
+ }
+
+ mainContentFragment.appendChild(newCard);
+ }
+
+ const main = document.getElementById("main");
+ main.appendChild(mainContentFragment);
+ main.addEventListener("click", onClickInMain);
+}
+
+function onClickInMain(event) {
+ const classList = event.target.classList;
+ if (classList.contains("button-open-dir")) {
+ onClickOpenDir(event);
+ } else if (classList.contains("button-block")) {
+ onBlock(event);
+ } else if (
+ classList.contains("button-expand") ||
+ classList.contains("button-collapse")
+ ) {
+ onClickExpand(event);
+ }
+}
+
+function clearVisualizedData() {
+ const mainDiv = document.getElementById("main");
+ while (mainDiv.firstChild) {
+ mainDiv.firstChild.remove();
+ }
+}
+
+async function collectCrashInfo() {
+ const parseBigInt = maybeBigInt => {
+ try {
+ return BigInt(maybeBigInt);
+ } catch (e) {
+ console.error(e);
+ }
+ return NaN;
+ };
+
+ if (CrashModuleSet || !AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+
+ const { getCrashManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/CrashManager.sys.mjs"
+ );
+ const crashes = await getCrashManager().getCrashes();
+ CrashModuleSet = new Set(
+ crashes.map(crash => {
+ const stackInfo = crash.metadata?.StackTraces;
+ if (!stackInfo) {
+ return null;
+ }
+
+ const crashAddr = parseBigInt(stackInfo.crash_info?.address);
+ if (typeof crashAddr !== "bigint") {
+ return null;
+ }
+
+ // Find modules whose address range includes the crashing address.
+ // No need to check the type of the return value from parseBigInt
+ // because comparing BigInt with NaN returns false.
+ return stackInfo.modules?.find(
+ module =>
+ crashAddr >= parseBigInt(module.base_addr) &&
+ crashAddr < parseBigInt(module.end_addr)
+ )?.filename;
+ })
+ );
+}
+
+async function onLoad() {
+ document
+ .getElementById("button-copy-to-clipboard")
+ .addEventListener("click", async e => {
+ e.target.disabled = true;
+
+ const data = await fetchData();
+ await copyDataToClipboard(data || []).catch(console.error);
+
+ e.target.disabled = false;
+ });
+
+ const backgroundTasks = [
+ AboutThirdParty.collectSystemInfo(),
+ collectCrashInfo(),
+ ];
+
+ let hasData = false;
+ Promise.all(backgroundTasks)
+ .then(() => {
+ gBackgroundTasksDone = true;
+ // Reload button will either show or is not needed, so we can hide the
+ // loading indicator.
+ document.getElementById("background-data-loading").hidden = true;
+ if (!hasData) {
+ // If all async tasks were completed before fetchData,
+ // or there was no data available, visualizeData shows
+ // full info and the reload button is not needed.
+ return;
+ }
+
+ // Add {once: true} to prevent multiple listeners from being scheduled
+ const button = document.getElementById("button-reload");
+ button.addEventListener(
+ "click",
+ async event => {
+ // Update the content with data we've already collected.
+ clearVisualizedData();
+ visualizeData(await fetchData());
+ event.target.hidden = true;
+ },
+ { once: true }
+ );
+
+ // Coming here means visualizeData is completed before the background
+ // tasks are completed. Because the page does not show full information,
+ // we show the reload button to call visualizeData again.
+ button.hidden = false;
+ })
+ .catch(console.error);
+
+ const data = await fetchData();
+ // Used for testing purposes
+ window.fetchDataDone = true;
+
+ hasData = !!data?.modules.length || !!data?.blocked.length;
+ if (!hasData) {
+ document.getElementById("no-data").hidden = false;
+ return;
+ }
+
+ visualizeData(data);
+}
+
+try {
+ AboutThirdParty = Cc["@mozilla.org/about-thirdparty;1"].getService(
+ Ci.nsIAboutThirdParty
+ );
+ document.addEventListener("DOMContentLoaded", onLoad, { once: true });
+} catch (ex) {
+ // Do nothing if we fail to create a singleton instance,
+ // showing the default no-module message.
+ console.error(ex);
+}
diff --git a/toolkit/components/aboutthirdparty/jar.mn b/toolkit/components/aboutthirdparty/jar.mn
new file mode 100644
index 0000000000..c71a282645
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/jar.mn
@@ -0,0 +1,8 @@
+# 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/.
+
+toolkit.jar:
+ content/global/aboutThirdParty.css (content/aboutThirdParty.css)
+ content/global/aboutThirdParty.html (content/aboutThirdParty.html)
+ content/global/aboutThirdParty.js (content/aboutThirdParty.js)
diff --git a/toolkit/components/aboutthirdparty/moz.build b/toolkit/components/aboutthirdparty/moz.build
new file mode 100644
index 0000000000..ad7782b9b5
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/moz.build
@@ -0,0 +1,37 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Launcher Process")
+
+FINAL_LIBRARY = "xul"
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+JAR_MANIFESTS += ["jar.mn"]
+XPCOM_MANIFESTS += ["components.conf"]
+XPIDL_MODULE = "AboutThirdParty"
+XPIDL_SOURCES += ["nsIAboutThirdParty.idl"]
+
+EXPORTS.mozilla += [
+ "AboutThirdParty.h",
+]
+
+SOURCES += [
+ "AboutThirdParty.cpp",
+ "AboutThirdParty_TestMethods.cpp",
+ "AboutThirdPartyUtils.cpp",
+ "MsiDatabase.cpp",
+]
+
+OS_LIBS += ["msi"]
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["tests/gtest"]
+
+TEST_DIRS += ["tests/TestShellEx"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl b/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl
new file mode 100644
index 0000000000..ef5590465a
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* 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"
+
+[scriptable, uuid(063813a0-85d8-4e77-80ea-b61292c0493d)]
+interface nsIInstalledApplication : nsISupports
+{
+ readonly attribute AString name;
+ readonly attribute AString publisher;
+};
+
+[scriptable, uuid(d33ff086-b328-4ae6-aaf5-52d41aa5df38)]
+interface nsIAboutThirdParty : nsISupports
+{
+ /**
+ * ModuleType flags used by lookupModuleType.
+ */
+ const unsigned long ModuleType_Unknown = 1 << 0;
+ const unsigned long ModuleType_IME = 1 << 1;
+ const unsigned long ModuleType_ShellExtension = 1 << 2;
+ const unsigned long ModuleType_BlockedByUser = 1 << 3;
+ const unsigned long ModuleType_BlockedByUserAtLaunch = 1 << 4;
+
+ /**
+ * Returns a bitwise combination of the ModuleType_* flags
+ * for the given leaf name of a module.
+ */
+ unsigned long lookupModuleType(in AString aLeafName);
+
+ /**
+ * Returns an object representing an application which includes
+ * the given path of a module in its installation.
+ */
+ nsIInstalledApplication lookupApplication(in AString aModulePath);
+
+ /**
+ * Returns true if DynamicBlocklist is available.
+ */
+ readonly attribute bool isDynamicBlocklistAvailable;
+
+ /**
+ * Returns true if DynamicBlocklist is available but disabled.
+ */
+ readonly attribute bool isDynamicBlocklistDisabled;
+
+ /**
+ * Add or remove an entry from the dynamic blocklist and save
+ * the resulting file.
+ */
+ [implicit_jscontext] Promise updateBlocklist(in AString aLeafName,
+ in boolean aNewBlockStatus);
+
+ /**
+ * Posts a background task to collect system information and resolves
+ * the returned promise when the task is finished.
+ */
+ [implicit_jscontext] Promise collectSystemInfo();
+
+ /**
+ * Open an OpenFile dialog with given parameters and immediately close it.
+ * This method should only be used for testing.
+ */
+ void openAndCloseFileDialogForTesting(
+ in AString aModuleName, in AString aInitialDir, in AString aFilter);
+};
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/Factory.cpp b/toolkit/components/aboutthirdparty/tests/TestShellEx/Factory.cpp
new file mode 100644
index 0000000000..a341c27b73
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/Factory.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 "mozilla/Atomics.h"
+#include "mozilla/RefPtr.h"
+
+#include <windows.h>
+#include <shlobj.h>
+
+already_AddRefed<IExtractIconW> CreateIconExtension();
+
+class ClassFactory final : public IClassFactory {
+ mozilla::Atomic<uint32_t> mRefCnt;
+
+ ~ClassFactory() = default;
+
+ public:
+ ClassFactory() : mRefCnt(0) {}
+
+ // IUnknown
+
+ STDMETHODIMP QueryInterface(REFIID aRefIID, void** aResult) {
+ if (!aResult) {
+ return E_INVALIDARG;
+ }
+
+ if (aRefIID == IID_IClassFactory) {
+ RefPtr ref(static_cast<IClassFactory*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+
+ STDMETHODIMP_(ULONG) AddRef() { return ++mRefCnt; }
+
+ STDMETHODIMP_(ULONG) Release() {
+ ULONG result = --mRefCnt;
+ if (!result) {
+ delete this;
+ }
+ return result;
+ }
+
+ // IClassFactory
+
+ STDMETHODIMP CreateInstance(IUnknown* aOuter, REFIID aRefIID,
+ void** aResult) {
+ if (aOuter) {
+ return CLASS_E_NOAGGREGATION;
+ }
+
+ RefPtr<IUnknown> instance;
+ if (IsEqualCLSID(aRefIID, IID_IExtractIconA) ||
+ IsEqualCLSID(aRefIID, IID_IExtractIconW)) {
+ instance = CreateIconExtension();
+ } else {
+ return E_NOINTERFACE;
+ }
+
+ return instance ? instance->QueryInterface(aRefIID, aResult)
+ : E_OUTOFMEMORY;
+ }
+
+ STDMETHODIMP LockServer(BOOL) { return S_OK; }
+};
+
+already_AddRefed<IClassFactory> CreateFactory() {
+ return mozilla::MakeAndAddRef<ClassFactory>();
+}
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/Icon.cpp b/toolkit/components/aboutthirdparty/tests/TestShellEx/Icon.cpp
new file mode 100644
index 0000000000..116c0e03a1
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/Icon.cpp
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 "mozilla/Atomics.h"
+#include "mozilla/RefPtr.h"
+#include "Resource.h"
+
+#include <windows.h>
+#include <shlobj.h>
+
+#include <string>
+
+extern std::wstring gDllPath;
+extern GUID CLSID_TestShellEx;
+
+class IconExtension final : public IPersistFile,
+ public IExtractIconA,
+ public IExtractIconW {
+ mozilla::Atomic<uint32_t> mRefCnt;
+
+ ~IconExtension() = default;
+
+ public:
+ IconExtension() : mRefCnt(0) {}
+
+ // IUnknown
+
+ STDMETHODIMP QueryInterface(REFIID aRefIID, void** aResult) {
+ if (!aResult) {
+ return E_INVALIDARG;
+ }
+
+ if (aRefIID == IID_IPersist) {
+ RefPtr ref(static_cast<IPersist*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ } else if (aRefIID == IID_IPersistFile) {
+ RefPtr ref(static_cast<IPersistFile*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ } else if (aRefIID == IID_IExtractIconA) {
+ RefPtr ref(static_cast<IExtractIconA*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ } else if (aRefIID == IID_IExtractIconW) {
+ RefPtr ref(static_cast<IExtractIconW*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+
+ STDMETHODIMP_(ULONG) AddRef() { return ++mRefCnt; }
+
+ STDMETHODIMP_(ULONG) Release() {
+ ULONG result = --mRefCnt;
+ if (!result) {
+ delete this;
+ }
+ return result;
+ }
+
+ // IPersist
+
+ STDMETHODIMP GetClassID(CLSID* aClassID) {
+ *aClassID = CLSID_TestShellEx;
+ return S_OK;
+ }
+
+ // IPersistFile
+
+ STDMETHODIMP GetCurFile(LPOLESTR*) { return E_NOTIMPL; }
+ STDMETHODIMP IsDirty() { return S_FALSE; }
+ STDMETHODIMP Load(LPCOLESTR, DWORD) { return S_OK; }
+ STDMETHODIMP Save(LPCOLESTR, BOOL) { return E_NOTIMPL; }
+ STDMETHODIMP SaveCompleted(LPCOLESTR) { return E_NOTIMPL; }
+
+ // IExtractIconA
+
+ STDMETHODIMP Extract(PCSTR, UINT, HICON*, HICON*, UINT) { return E_NOTIMPL; }
+ STDMETHODIMP GetIconLocation(UINT, PSTR, UINT, int*, UINT*) {
+ return E_NOTIMPL;
+ }
+
+ // IExtractIconW
+
+ STDMETHODIMP Extract(PCWSTR, UINT, HICON*, HICON*, UINT) { return S_FALSE; }
+
+ STDMETHODIMP GetIconLocation(UINT, PWSTR aIconFile, UINT aCchMax, int* aIndex,
+ UINT* aOutFlags) {
+ if (aCchMax <= gDllPath.size()) {
+ return E_NOT_SUFFICIENT_BUFFER;
+ }
+
+ gDllPath.copy(aIconFile, gDllPath.size());
+ aIconFile[gDllPath.size()] = 0;
+ *aOutFlags = GIL_DONTCACHE;
+ *aIndex = -IDI_ICON1;
+ return S_OK;
+ }
+};
+
+already_AddRefed<IExtractIconW> CreateIconExtension() {
+ return mozilla::MakeAndAddRef<IconExtension>();
+}
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.cpp b/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.cpp
new file mode 100644
index 0000000000..33b2feb12a
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.cpp
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 "mozilla/UniquePtr.h"
+#include "RegUtils.h"
+
+#include <windows.h>
+#include <strsafe.h>
+
+extern std::wstring gDllPath;
+
+const wchar_t kClsIdPrefix[] = L"CLSID\\";
+const wchar_t* kExtensionSubkeys[] = {
+ L".zzz\\shellex\\IconHandler",
+};
+
+bool RegKey::SetStringInternal(const wchar_t* aValueName,
+ const wchar_t* aValueData,
+ DWORD aValueDataLength) {
+ if (!mKey) {
+ return false;
+ }
+
+ return ::RegSetValueExW(mKey, aValueName, 0, REG_SZ,
+ reinterpret_cast<const BYTE*>(aValueData),
+ aValueDataLength) == ERROR_SUCCESS;
+}
+
+RegKey::RegKey(HKEY root, const wchar_t* aSubkey) : mKey(nullptr) {
+ ::RegCreateKeyExW(root, aSubkey, 0, nullptr, 0, KEY_ALL_ACCESS, nullptr,
+ &mKey, nullptr);
+}
+
+RegKey::~RegKey() {
+ if (mKey) {
+ ::RegCloseKey(mKey);
+ }
+}
+
+bool RegKey::SetString(const wchar_t* aValueName, const wchar_t* aValueData) {
+ return SetStringInternal(
+ aValueName, aValueData,
+ aValueData
+ ? static_cast<DWORD>((wcslen(aValueData) + 1) * sizeof(wchar_t))
+ : 0);
+}
+
+bool RegKey::SetString(const wchar_t* aValueName,
+ const std::wstring& aValueData) {
+ return SetStringInternal(
+ aValueName, aValueData.c_str(),
+ static_cast<DWORD>((aValueData.size() + 1) * sizeof(wchar_t)));
+}
+
+std::wstring RegKey::GetString(const wchar_t* aValueName) {
+ DWORD len = 0;
+ LSTATUS status = ::RegGetValueW(mKey, aValueName, nullptr, RRF_RT_REG_SZ,
+ nullptr, nullptr, &len);
+
+ mozilla::UniquePtr<uint8_t[]> buf = mozilla::MakeUnique<uint8_t[]>(len);
+ status = ::RegGetValueW(mKey, aValueName, nullptr, RRF_RT_REG_SZ, nullptr,
+ buf.get(), &len);
+ if (status != ERROR_SUCCESS) {
+ return L"";
+ }
+
+ return reinterpret_cast<wchar_t*>(buf.get());
+}
+
+ComRegisterer::ComRegisterer(const GUID& aClsId, const wchar_t* aFriendlyName)
+ : mClassRoot(HKEY_CURRENT_USER, L"Software\\Classes"),
+ mFriendlyName(aFriendlyName) {
+ wchar_t guidStr[64];
+ HRESULT hr = ::StringCbPrintfW(
+ guidStr, sizeof(guidStr),
+ L"{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", aClsId.Data1,
+ aClsId.Data2, aClsId.Data3, aClsId.Data4[0], aClsId.Data4[1],
+ aClsId.Data4[2], aClsId.Data4[3], aClsId.Data4[4], aClsId.Data4[5],
+ aClsId.Data4[6], aClsId.Data4[7]);
+ if (FAILED(hr)) {
+ return;
+ }
+
+ mClsId = guidStr;
+}
+
+bool ComRegisterer::UnregisterAll() {
+ bool isOk = true;
+ LSTATUS ls;
+
+ for (const wchar_t* subkey : kExtensionSubkeys) {
+ RegKey root(mClassRoot, subkey);
+
+ std::wstring currentHandler = root.GetString(nullptr);
+ if (currentHandler != mClsId) {
+ // If another extension is registered, don't overwrite it.
+ continue;
+ }
+
+ // Set an empty string instead of deleting the key.
+ if (!root.SetString(nullptr)) {
+ isOk = false;
+ }
+ }
+
+ std::wstring subkey(kClsIdPrefix);
+ subkey += mClsId;
+ ls = ::RegDeleteTreeW(mClassRoot, subkey.c_str());
+ if (ls != ERROR_SUCCESS && ls != ERROR_FILE_NOT_FOUND) {
+ isOk = false;
+ }
+
+ return isOk;
+}
+
+bool ComRegisterer::RegisterObject(const wchar_t* aThreadModel) {
+ std::wstring subkey(kClsIdPrefix);
+ subkey += mClsId;
+
+ RegKey root(mClassRoot, subkey.c_str());
+ if (!root || !root.SetString(nullptr, mFriendlyName)) {
+ return false;
+ }
+
+ RegKey inproc(root, L"InprocServer32");
+ return inproc && inproc.SetString(nullptr, gDllPath) &&
+ inproc.SetString(L"ThreadingModel", aThreadModel);
+}
+
+bool ComRegisterer::RegisterExtensions() {
+ for (const wchar_t* subkey : kExtensionSubkeys) {
+ RegKey root(mClassRoot, subkey);
+ std::wstring currentHandler = root.GetString(nullptr);
+ if (!currentHandler.empty()) {
+ // If another extension is registered, don't overwrite it.
+ continue;
+ }
+
+ if (!root.SetString(nullptr, mClsId)) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.h b/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.h
new file mode 100644
index 0000000000..21a23fdf16
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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/. */
+
+#ifndef mozilla_TestShellEx_RegUtils_h
+#define mozilla_TestShellEx_RegUtils_h
+
+#include <windows.h>
+#include <string>
+
+class RegKey final {
+ HKEY mKey;
+
+ bool SetStringInternal(const wchar_t* aValueName, const wchar_t* aValueData,
+ DWORD aValueDataLength);
+
+ public:
+ RegKey() : mKey(nullptr) {}
+ RegKey(HKEY root, const wchar_t* aSubkey);
+ ~RegKey();
+
+ RegKey(RegKey&& aOther) = delete;
+ RegKey& operator=(RegKey&& aOther) = delete;
+ RegKey(const RegKey&) = delete;
+ RegKey& operator=(const RegKey&) = delete;
+
+ explicit operator bool() const { return !!mKey; }
+ operator HKEY() const { return mKey; }
+
+ bool SetString(const wchar_t* aValueName,
+ const wchar_t* aValueData = nullptr);
+ bool SetString(const wchar_t* aValueName, const std::wstring& aValueData);
+ std::wstring GetString(const wchar_t* aValueName);
+};
+
+class ComRegisterer final {
+ RegKey mClassRoot;
+ std::wstring mClsId;
+ std::wstring mFriendlyName;
+
+ public:
+ ComRegisterer(const GUID& aClsId, const wchar_t* aFriendlyName);
+ ~ComRegisterer() = default;
+
+ bool UnregisterAll();
+ bool RegisterObject(const wchar_t* aThreadModel);
+ bool RegisterExtensions();
+};
+
+#endif // mozilla_TestShellEx_RegUtils_h
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/Resource.h b/toolkit/components/aboutthirdparty/tests/TestShellEx/Resource.h
new file mode 100644
index 0000000000..fa037194d7
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/Resource.h
@@ -0,0 +1,12 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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/. */
+
+#ifndef mozilla_TestShellEx_Resource_h
+#define mozilla_TestShellEx_Resource_h
+
+#define IDI_ICON1 100
+
+#endif // mozilla_TestShellEx_Resource_h
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.cpp b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.cpp
new file mode 100644
index 0000000000..dc0588483f
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.cpp
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 "mozilla/RefPtr.h"
+#include "RegUtils.h"
+
+#include <windows.h>
+#include <objbase.h>
+
+already_AddRefed<IClassFactory> CreateFactory();
+
+// {10A9521E-0205-4CC7-93A1-62F30A9A54B3}
+GUID CLSID_TestShellEx = {
+ 0x10a9521e, 0x205, 0x4cc7, {0x93, 0xa1, 0x62, 0xf3, 0xa, 0x9a, 0x54, 0xb3}};
+wchar_t kFriendlyName[] = L"Minimum Shell Extension for Firefox testing";
+
+std::wstring gDllPath;
+
+BOOL APIENTRY DllMain(HMODULE aModule, DWORD aReason, LPVOID) {
+ wchar_t buf[MAX_PATH];
+ switch (aReason) {
+ case DLL_PROCESS_ATTACH:
+ if (!::GetModuleFileNameW(aModule, buf, ARRAYSIZE(buf))) {
+ return FALSE;
+ }
+ gDllPath = buf;
+ break;
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ case DLL_PROCESS_DETACH:
+ break;
+ }
+ return TRUE;
+}
+
+STDAPI DllGetClassObject(REFCLSID aClsid, REFIID aIid, void** aResult) {
+ if (!IsEqualCLSID(aClsid, CLSID_TestShellEx) ||
+ !IsEqualCLSID(aIid, IID_IClassFactory)) {
+ return CLASS_E_CLASSNOTAVAILABLE;
+ }
+
+ RefPtr<IClassFactory> factory = CreateFactory();
+ return factory ? factory->QueryInterface(aIid, aResult) : E_OUTOFMEMORY;
+}
+
+STDAPI DllCanUnloadNow() { return S_OK; }
+
+// These functions enable us to run "regsvr32 [/u] TestShellEx.dll" manually.
+// (No admin privilege is needed because all access is under HKCU.)
+// We don't use these functions in the mochitest, but having these makes easier
+// to test the module manually.
+STDAPI DllRegisterServer() {
+ ComRegisterer reg(CLSID_TestShellEx, kFriendlyName);
+ if (!reg.RegisterObject(L"Apartment") || !reg.RegisterExtensions()) {
+ return E_ACCESSDENIED;
+ }
+ return S_OK;
+}
+STDAPI DllUnregisterServer() {
+ ComRegisterer reg(CLSID_TestShellEx, kFriendlyName);
+ if (!reg.UnregisterAll()) {
+ return E_ACCESSDENIED;
+ }
+ return S_OK;
+}
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.def b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.def
new file mode 100644
index 0000000000..e83041771d
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.def
@@ -0,0 +1,10 @@
+;+# 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/.
+
+LIBRARY TestShellEx
+EXPORTS
+ DllGetClassObject PRIVATE
+ DllCanUnloadNow PRIVATE
+ DllRegisterServer PRIVATE
+ DllUnregisterServer PRIVATE
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.rc b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.rc
new file mode 100644
index 0000000000..7500551f02
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.rc
@@ -0,0 +1,39 @@
+/* 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 <winres.h>
+
+#include "Resource.h"
+
+IDI_ICON1 ICON DISCARDABLE "dinosaur.ico"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,2,3,4 // This field will be collected
+ PRODUCTVERSION 5,6,7,8
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_DLL
+ FILESUBTYPE 0x0L
+
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "Mozilla Corporation"
+ VALUE "OriginalFilename", "TestShellEx.dll"
+ VALUE "ProductName", "Sample Shell Extension"
+ END
+ END
+
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409, 1252
+ END
+END
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.ico b/toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.ico
Binary files differ
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/moz.build b/toolkit/components/aboutthirdparty/tests/TestShellEx/moz.build
new file mode 100644
index 0000000000..44344a0e2e
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+DIST_INSTALL = False
+
+SharedLibrary("TestShellEx")
+
+UNIFIED_SOURCES = [
+ "Factory.cpp",
+ "Icon.cpp",
+ "RegUtils.cpp",
+ "TestShellEx.cpp",
+]
+
+RCFILE = "TestShellEx.rc"
+DEFFILE = "TestShellEx.def"
+USE_LIBS += [
+ "mozglue",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ OS_LIBS += [
+ "advapi32",
+ "uuid",
+ ]
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ shared_library = "!%sTestShellEx%s" % (CONFIG["DLL_PREFIX"], CONFIG["DLL_SUFFIX"])
+ TEST_HARNESS_FILES.testing.mochitest.browser.toolkit.components.aboutthirdparty.tests.browser += [
+ shared_library
+ ]
diff --git a/toolkit/components/aboutthirdparty/tests/browser/browser.ini b/toolkit/components/aboutthirdparty/tests/browser/browser.ini
new file mode 100644
index 0000000000..ccb9c69fff
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/browser/browser.ini
@@ -0,0 +1,7 @@
+[default]
+head = head.js
+
+[browser_aboutthirdparty.js]
+support-files = hello.zzz
+skip-if =
+ os == "win" # Bug 1776048
diff --git a/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js b/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js
new file mode 100644
index 0000000000..e4c8ca35ca
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js
@@ -0,0 +1,317 @@
+/* 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/. */
+
+// Return card containers matching a given name
+function getCardsByName(aContainer, aLeafName) {
+ const matchedCards = [];
+ const allCards = aContainer.querySelectorAll(".card");
+ for (const card of allCards) {
+ const nameLabel = card.querySelector(".module-name");
+ if (nameLabel.textContent == aLeafName) {
+ matchedCards.push(card);
+ }
+ }
+ return matchedCards;
+}
+
+function getDetailRow(aContainer, aLabel) {
+ return aContainer.querySelector(`[data-l10n-id=${aLabel}]`).parentElement;
+}
+
+function verifyClipboardData(aModuleJson) {
+ Assert.ok(
+ aModuleJson.hasOwnProperty("blocked"),
+ "Clipboard data should have blocked property."
+ );
+ const blocked = aModuleJson.blocked.filter(
+ x => x.name == kUserBlockedModuleName
+ );
+ Assert.equal(
+ blocked.length,
+ 1,
+ "Blocked array should contain the blocked module one time."
+ );
+ Assert.ok(
+ aModuleJson.hasOwnProperty("modules"),
+ "Clipboard data should have modules property"
+ );
+ let aModuleArray = aModuleJson.modules;
+ const filtered = aModuleArray.filter(x => x.name == kExtensionModuleName);
+ Assert.equal(filtered.length, 1, "No duplicate data for the module.");
+
+ const kDeletedPropertiesOfModule = [
+ "application",
+ "dllFile",
+ "loadingOnMain",
+ "trustFlags",
+ ];
+ const kDeletedPropertiesOfLoadingEvent = [
+ "baseAddress",
+ "isDependent",
+ "mainThread",
+ "moduleIndex",
+ "processUptimeMS",
+ ];
+
+ for (const module of aModuleArray) {
+ for (const deletedProperty of kDeletedPropertiesOfModule) {
+ Assert.ok(
+ !module.hasOwnProperty(deletedProperty),
+ `The property \`${deletedProperty}\` is deleted.`
+ );
+ }
+
+ Assert.ok(
+ !module.hasOwnProperty("typeFlags") || module.typeFlags != 0,
+ "typeFlags does not exist or is non-zero."
+ );
+
+ for (const event of module.events) {
+ for (const deletedProperty of kDeletedPropertiesOfLoadingEvent) {
+ Assert.ok(
+ !event.hasOwnProperty(deletedProperty),
+ `The property \`${deletedProperty}\` is deleted.`
+ );
+ }
+ }
+ }
+}
+
+function verifyModuleSorting(compareFunc) {
+ const uninteresting = {
+ typeFlags: 0,
+ isCrasher: false,
+ loadingOnMain: 0,
+ };
+ const crasherNotBlocked = { ...uninteresting, isCrasher: true };
+ const crasherBlocked = {
+ ...uninteresting,
+ isCrasher: true,
+ typeFlags: Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch,
+ };
+ const justBlocked = {
+ ...uninteresting,
+ typeFlags: Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch,
+ };
+ const uninterestingButSlow = {
+ ...uninteresting,
+ loadingOnMain: 10,
+ };
+ let modules = [
+ uninteresting,
+ uninterestingButSlow,
+ crasherNotBlocked,
+ justBlocked,
+ crasherBlocked,
+ ];
+ modules.sort(compareFunc);
+ Assert.equal(
+ JSON.stringify([
+ crasherBlocked,
+ justBlocked,
+ crasherNotBlocked,
+ uninterestingButSlow,
+ uninteresting,
+ ]),
+ JSON.stringify(modules),
+ "Modules sort in expected order"
+ );
+}
+
+add_task(async () => {
+ registerCleanupFunction(() => {
+ unregisterAll();
+ });
+ await registerObject();
+ registerExtensions();
+ loadShellExtension();
+
+ await kATP.collectSystemInfo();
+ Assert.equal(
+ kATP.lookupModuleType(kExtensionModuleName),
+ Ci.nsIAboutThirdParty.ModuleType_ShellExtension,
+ "lookupModuleType() returns a correct type " +
+ "after system info was collected."
+ );
+
+ await BrowserTestUtils.withNewTab("about:third-party", async browser => {
+ if (!content.fetchDataDone) {
+ const mainDiv = content.document.getElementById("main");
+ await BrowserTestUtils.waitForMutationCondition(
+ mainDiv,
+ { childList: true },
+ () => mainDiv.childElementCount > 0
+ );
+ Assert.ok(content.fetchDataDone, "onLoad() is completed.");
+ }
+
+ const reload = content.document.getElementById("button-reload");
+ if (!reload.hidden) {
+ reload.click();
+ await BrowserTestUtils.waitForMutationCondition(
+ reload,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => reload.hidden
+ );
+ }
+
+ Assert.ok(
+ content.document.getElementById("no-data").hidden,
+ "The no-data message is hidden."
+ );
+ const blockedCards = getCardsByName(
+ content.document,
+ kUserBlockedModuleName
+ );
+ Assert.equal(
+ blockedCards.length,
+ 1,
+ "Only one card matching the blocked module exists."
+ );
+ const blockedCard = blockedCards[0];
+ Assert.equal(
+ blockedCard.querySelectorAll(".button-block.module-blocked").length,
+ 1,
+ "The blocked module has a button indicating it is blocked"
+ );
+ let blockedBlockButton = blockedCard.querySelector(
+ ".button-block.module-blocked"
+ );
+ Assert.equal(
+ blockedBlockButton.getAttribute("data-l10n-id"),
+ "third-party-button-to-unblock",
+ "Button to block the module has correct title"
+ );
+ blockedBlockButton.click();
+ await BrowserTestUtils.promiseAlertDialogOpen("cancel");
+ Assert.ok(
+ !blockedBlockButton.classList.contains("module-blocked"),
+ "After clicking to unblock a module, button should not have module-blocked class."
+ );
+ Assert.equal(
+ blockedBlockButton.getAttribute("data-l10n-id"),
+ "third-party-button-to-block",
+ "After clicking to unblock a module, button should have correct title."
+ );
+ // Restore this to blocked for later tests
+ blockedBlockButton.click();
+ await BrowserTestUtils.promiseAlertDialogOpen("cancel");
+ Assert.ok(
+ blockedBlockButton.classList.contains("module-blocked"),
+ "After clicking to block a module, button should have module-blocked class."
+ );
+ Assert.equal(
+ blockedBlockButton.getAttribute("data-l10n-id"),
+ "third-party-button-to-unblock",
+ "After clicking to block a module, button should have correct title."
+ );
+
+ const cards = getCardsByName(content.document, kExtensionModuleName);
+ Assert.equal(cards.length, 1, "Only one card matching the module exists.");
+ const card = cards[0];
+
+ const blockButton = card.querySelector(".button-block");
+ Assert.ok(
+ !blockButton.classList.contains("blocklist-disabled"),
+ "Button to block the module does not indicate the blocklist is disabled."
+ );
+ Assert.ok(
+ !blockButton.classList.contains("module-blocked"),
+ "Button to block the module does not indicate the module is blocked."
+ );
+
+ Assert.ok(
+ card.querySelector(".image-warning").hidden,
+ "No warning sign for the module."
+ );
+ Assert.equal(
+ card.querySelector(".image-unsigned").hidden,
+ false,
+ "The module is labeled as unsigned."
+ );
+ Assert.equal(
+ card.querySelector(".tag-shellex").hidden,
+ false,
+ "The module is labeled as a shell extension."
+ );
+ Assert.equal(
+ card.querySelector(".tag-ime").hidden,
+ true,
+ "The module is not labeled as an IME."
+ );
+
+ const versionRow = getDetailRow(card, "third-party-detail-version");
+ Assert.equal(
+ versionRow.childNodes[1].textContent,
+ "1.2.3.4",
+ "The version matches a value in TestShellEx.rc."
+ );
+ const vendorRow = getDetailRow(card, "third-party-detail-vendor");
+ Assert.equal(
+ vendorRow.childNodes[1].textContent,
+ "Mozilla Corporation",
+ "The vendor name matches a value in TestShellEx.rc."
+ );
+ const occurrencesRow = getDetailRow(card, "third-party-detail-occurrences");
+ Assert.equal(
+ Number(occurrencesRow.childNodes[1].textContent),
+ 1,
+ "The module was loaded once."
+ );
+ const durationRow = getDetailRow(card, "third-party-detail-duration");
+ Assert.ok(
+ Number(durationRow.childNodes[1].textContent),
+ "The duration row shows a valid number."
+ );
+
+ const eventTable = card.querySelector(".event-table");
+ const tableCells = eventTable.querySelectorAll("td");
+ Assert.equal(
+ tableCells.length,
+ 3,
+ "The table has three cells as there is only one event."
+ );
+ Assert.equal(
+ tableCells[0].querySelector(".process-type").getAttribute("data-l10n-id"),
+ "process-type-default",
+ "The module was loaded into the main process."
+ );
+ Assert.ok(
+ Number(tableCells[0].querySelector(".process-id").textContent),
+ "A valid process ID is displayed."
+ );
+ Assert.equal(
+ tableCells[1].querySelector(".event-duration").textContent,
+ durationRow.childNodes[1].textContent,
+ "The event's duration is the same as the average " +
+ "as there is only one event."
+ );
+ Assert.equal(
+ tableCells[1].querySelector(".tag-background").hidden,
+ true,
+ "The icon handler is loaded in the main thread."
+ );
+ Assert.equal(
+ tableCells[2].getAttribute("data-l10n-id"),
+ "third-party-status-loaded",
+ "The module was really loaded without being blocked."
+ );
+
+ const button = content.document.getElementById("button-copy-to-clipboard");
+ button.click();
+
+ // Wait until copying is done and the button becomes clickable.
+ await BrowserTestUtils.waitForMutationCondition(
+ button,
+ { attributes: true },
+ () => !button.disabled
+ );
+
+ const copiedJSON = JSON.parse(await navigator.clipboard.readText());
+ Assert.ok(copiedJSON instanceof Object, "Data is an object.");
+ verifyClipboardData(copiedJSON);
+
+ verifyModuleSorting(content.moduleCompareForDisplay);
+ });
+});
diff --git a/toolkit/components/aboutthirdparty/tests/browser/head.js b/toolkit/components/aboutthirdparty/tests/browser/head.js
new file mode 100644
index 0000000000..eba3948046
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/browser/head.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+"use strict";
+
+const kClsidTestShellEx = "{10a9521e-0205-4cc7-93a1-62f30a9a54b3}";
+const kFriendlyName = "Minimum Shell Extension for Firefox testing";
+const kExtensionSubkeys = [".zzz\\shellex\\IconHandler"];
+const kExtensionModuleName = "TestShellEx.dll";
+const kUserBlockedModuleName = "TestDllBlocklist_UserBlocked.dll";
+const kFileFilterInDialog = "*.zzz";
+const kATP = Cc["@mozilla.org/about-thirdparty;1"].getService(
+ Ci.nsIAboutThirdParty
+);
+
+function loadShellExtension() {
+ // This method call opens the file dialog and shows the support file
+ // "hello.zzz" in it, which loads TestShellEx.dll to show an icon
+ // for files with the .zzz extension.
+ kATP.openAndCloseFileDialogForTesting(
+ kExtensionModuleName,
+ getTestFilePath(""),
+ kFileFilterInDialog
+ );
+}
+
+async function registerObject() {
+ const reg = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+
+ reg.create(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Classes\\CLSID\\" + kClsidTestShellEx,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ reg.writeStringValue("", kFriendlyName);
+
+ const inprocServer = reg.createChild(
+ "InprocServer32",
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ const moduleFullPath = getTestFilePath(kExtensionModuleName);
+ Assert.ok(await IOUtils.exists(moduleFullPath), "The module file exists.");
+
+ inprocServer.writeStringValue("", moduleFullPath);
+ inprocServer.writeStringValue("ThreadingModel", "Apartment");
+ reg.close();
+
+ info("registerObject() done - " + moduleFullPath);
+}
+
+function registerExtensions() {
+ for (const subkey of kExtensionSubkeys) {
+ const reg = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ reg.create(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Classes\\" + subkey,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ let currentExtension = "";
+ try {
+ // If the key was just created above, the default value does not exist,
+ // so readStringValue will throw NS_ERROR_FAILURE.
+ currentExtension = reg.readStringValue("");
+ } catch (e) {}
+
+ try {
+ if (!currentExtension) {
+ reg.writeStringValue("", kClsidTestShellEx);
+ } else if (currentExtension != kClsidTestShellEx) {
+ throw new Error(
+ `Another extension \`${currentExtension}\` has been registered.`
+ );
+ }
+ } catch (e) {
+ throw new Error("Failed to register TestShellEx.dll: " + e);
+ } finally {
+ reg.close();
+ }
+ }
+}
+
+function unregisterAll() {
+ for (const subkey of kExtensionSubkeys) {
+ const reg = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+
+ try {
+ reg.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Classes\\" + subkey,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ if (reg.readStringValue("") != kClsidTestShellEx) {
+ // If another extension is registered, don't overwrite it.
+ continue;
+ }
+
+ // Set an empty string instead of deleting the key
+ // not to touch non-default values.
+ reg.writeStringValue("", "");
+ } catch (e) {
+ info(`Failed to unregister \`${subkey}\`: ` + e);
+ } finally {
+ reg.close();
+ }
+ }
+
+ const reg = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ reg.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Classes\\CLSID",
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ try {
+ const child = reg.openChild(
+ kClsidTestShellEx,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+ try {
+ child.removeChild("InprocServer32");
+ } catch (e) {
+ } finally {
+ child.close();
+ }
+
+ reg.removeChild(kClsidTestShellEx);
+ } catch (e) {
+ } finally {
+ reg.close();
+ }
+}
diff --git a/toolkit/components/aboutthirdparty/tests/browser/hello.zzz b/toolkit/components/aboutthirdparty/tests/browser/hello.zzz
new file mode 100644
index 0000000000..e69ab604b3
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/browser/hello.zzz
@@ -0,0 +1 @@
+Showing this file on Windows Shell loads TestShellEx.dll.
diff --git a/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp b/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp
new file mode 100644
index 0000000000..c8fb9c016f
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 8; 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 https://mozilla.org/MPL/2.0/. */
+
+#include <windows.h>
+#include "gtest/gtest.h"
+
+#include "../../AboutThirdPartyUtils.h"
+#include "mozilla/AboutThirdParty.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+
+#define WEATHER_RU u"\x041F\x043E\x0433\x043E\x0434\x0430"_ns
+#define WEATHER_JA u"\x5929\x6C17"_ns
+
+TEST(AboutThirdParty, CompareIgnoreCase)
+{
+ EXPECT_EQ(CompareIgnoreCase(u""_ns, u""_ns), 0);
+ EXPECT_EQ(CompareIgnoreCase(u"abc"_ns, u"aBc"_ns), 0);
+ EXPECT_LT(CompareIgnoreCase(u"a"_ns, u"ab"_ns), 0);
+ EXPECT_GT(CompareIgnoreCase(u"ab"_ns, u"A"_ns), 0);
+ EXPECT_LT(CompareIgnoreCase(u""_ns, u"aB"_ns), 0);
+ EXPECT_GT(CompareIgnoreCase(u"ab"_ns, u""_ns), 0);
+
+ // non-ascii testcases
+ EXPECT_EQ(CompareIgnoreCase(WEATHER_JA, WEATHER_JA), 0);
+ EXPECT_EQ(CompareIgnoreCase(WEATHER_RU, WEATHER_RU), 0);
+ EXPECT_LT(CompareIgnoreCase(WEATHER_RU, WEATHER_JA), 0);
+ EXPECT_GT(CompareIgnoreCase(WEATHER_JA, WEATHER_RU), 0);
+ EXPECT_EQ(CompareIgnoreCase(WEATHER_RU u"x"_ns WEATHER_JA,
+ WEATHER_RU u"X"_ns WEATHER_JA),
+ 0);
+ EXPECT_GT(
+ CompareIgnoreCase(WEATHER_RU u"a"_ns WEATHER_JA, WEATHER_RU u"A"_ns), 0);
+ EXPECT_LT(CompareIgnoreCase(WEATHER_RU u"a"_ns WEATHER_RU,
+ WEATHER_RU u"A"_ns WEATHER_JA),
+ 0);
+}
+
+TEST(AboutThirdParty, MsiPackGuid)
+{
+ nsAutoString packedGuid;
+ EXPECT_FALSE(
+ MsiPackGuid(u"EDA620E3-AA98-3846-B81E-3493CB2E0E02"_ns, packedGuid));
+ EXPECT_FALSE(
+ MsiPackGuid(u"*EDA620E3-AA98-3846-B81E-3493CB2E0E02*"_ns, packedGuid));
+ EXPECT_TRUE(
+ MsiPackGuid(u"{EDA620E3-AA98-3846-B81E-3493CB2E0E02}"_ns, packedGuid));
+ EXPECT_STREQ(packedGuid.get(), L"3E026ADE89AA64838BE14339BCE2E020");
+}
+
+TEST(AboutThirdParty, CorrectMsiComponentPath)
+{
+ nsAutoString testPath;
+
+ testPath = u""_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+
+ testPath = u"\\\\server\\share"_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+
+ testPath = u"hello"_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+
+ testPath = u"02:\\Software"_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+
+ testPath = u"C:\\path\\"_ns;
+ EXPECT_TRUE(CorrectMsiComponentPath(testPath));
+ EXPECT_STREQ(testPath.get(), L"C:\\path\\");
+
+ testPath = u"C?\\path\\"_ns;
+ EXPECT_TRUE(CorrectMsiComponentPath(testPath));
+ EXPECT_STREQ(testPath.get(), L"C:\\path\\");
+
+ testPath = u"C:\\?path\\"_ns;
+ EXPECT_TRUE(CorrectMsiComponentPath(testPath));
+ EXPECT_STREQ(testPath.get(), L"C:\\path\\");
+
+ testPath = u"\\?path\\"_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+}
+
+TEST(AboutThirdParty, InstallLocations)
+{
+ const nsLiteralString kDirectoriesUnsorted[] = {
+ u"C:\\duplicate\\"_ns, u"C:\\duplicate\\"_ns, u"C:\\app1\\"_ns,
+ u"C:\\app2\\"_ns, u"C:\\app11\\"_ns, u"C:\\app12\\"_ns,
+ };
+
+ struct TestCase {
+ nsLiteralString mFile;
+ nsLiteralString mInstallPath;
+ } const kTestCases[] = {
+ {u"C:\\app\\sub\\file.dll"_ns, u""_ns},
+ {u"C:\\app1\\sub\\file.dll"_ns, u"C:\\app1\\"_ns},
+ {u"C:\\app11\\sub\\file.dll"_ns, u"C:\\app11\\"_ns},
+ {u"C:\\app12\\sub\\file.dll"_ns, u"C:\\app12\\"_ns},
+ {u"C:\\app13\\sub\\file.dll"_ns, u""_ns},
+ {u"C:\\duplicate\\sub\\file.dll"_ns, u""_ns},
+ };
+
+ nsTArray<InstallLocationT> locations(ArrayLength(kDirectoriesUnsorted));
+ for (size_t i = 0; i < ArrayLength(kDirectoriesUnsorted); ++i) {
+ locations.EmplaceBack(kDirectoriesUnsorted[i], new InstalledApplication());
+ }
+
+ locations.Sort([](const InstallLocationT& aA, const InstallLocationT& aB) {
+ return CompareIgnoreCase(aA.first(), aB.first());
+ });
+
+ for (const auto& testCase : kTestCases) {
+ auto bounds = EqualRange(locations, 0, locations.Length(),
+ InstallLocationComparator(testCase.mFile));
+ if (bounds.second() - bounds.first() != 1) {
+ EXPECT_TRUE(testCase.mInstallPath.IsEmpty());
+ continue;
+ }
+
+ EXPECT_EQ(locations[bounds.first()].first(), testCase.mInstallPath);
+ }
+}
diff --git a/toolkit/components/aboutthirdparty/tests/gtest/moz.build b/toolkit/components/aboutthirdparty/tests/gtest/moz.build
new file mode 100644
index 0000000000..659c0836db
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/gtest/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+LOCAL_INCLUDES += [
+ "../..",
+]
+
+UNIFIED_SOURCES += ["TestAboutThirdParty.cpp"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/aboutthirdparty/tests/xpcshell/head.js b/toolkit/components/aboutthirdparty/tests/xpcshell/head.js
new file mode 100644
index 0000000000..ede1d3cc73
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/xpcshell/head.js
@@ -0,0 +1,10 @@
+/* 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/. */
+
+"use strict";
+
+const kExtensionModuleName = "TestShellEx.dll";
+const kATP = Cc["@mozilla.org/about-thirdparty;1"].getService(
+ Ci.nsIAboutThirdParty
+);
diff --git a/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js b/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js
new file mode 100644
index 0000000000..281fe42188
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+add_task(async () => {
+ Assert.equal(
+ kATP.lookupModuleType(kExtensionModuleName),
+ 0,
+ "lookupModuleType() returns 0 before system info is collected."
+ );
+
+ // Make sure successive calls of collectSystemInfo() do not
+ // cause anything bad.
+ const kLoopCount = 100;
+ const promises = [];
+ for (let i = 0; i < kLoopCount; ++i) {
+ promises.push(kATP.collectSystemInfo());
+ }
+
+ const collectSystemInfoResults = await Promise.allSettled(promises);
+ Assert.equal(collectSystemInfoResults.length, kLoopCount);
+
+ for (const result of collectSystemInfoResults) {
+ Assert.ok(
+ result.status == "fulfilled",
+ "All results from collectSystemInfo() are resolved."
+ );
+ }
+
+ Assert.equal(
+ kATP.lookupModuleType("SHELL32.dll"),
+ Ci.nsIAboutThirdParty.ModuleType_ShellExtension,
+ "Shell32.dll is always registered as a shell extension."
+ );
+
+ Assert.equal(
+ kATP.lookupModuleType(""),
+ Ci.nsIAboutThirdParty.ModuleType_Unknown,
+ "Looking up an empty string succeeds and returns ModuleType_Unknown."
+ );
+
+ Assert.equal(
+ kATP.lookupModuleType(null),
+ Ci.nsIAboutThirdParty.ModuleType_Unknown,
+ "Looking up null succeeds and returns ModuleType_Unknown."
+ );
+
+ Assert.equal(
+ kATP.lookupModuleType("invalid name"),
+ Ci.nsIAboutThirdParty.ModuleType_Unknown,
+ "Looking up an invalid name succeeds and returns ModuleType_Unknown."
+ );
+
+ Assert.equal(
+ kATP.lookupApplication(""),
+ null,
+ "Looking up an empty string returns null."
+ );
+
+ Assert.equal(
+ kATP.lookupApplication("invalid path"),
+ null,
+ "Looking up an invalid path returns null."
+ );
+});
diff --git a/toolkit/components/aboutthirdparty/tests/xpcshell/xpcshell.ini b/toolkit/components/aboutthirdparty/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..7fdb13e444
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+head = head.js
+
+[test_aboutthirdparty.js]