diff options
Diffstat (limited to 'toolkit/components/aboutthirdparty')
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 Binary files differnew file mode 100644 index 0000000000..d44438903b --- /dev/null +++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.ico 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] |