From 718f8615db069c1e2f94076d30ceae2dbc44b5b8 Mon Sep 17 00:00:00 2001 From: Toshihito Kikuchi Date: Fri, 28 May 2021 22:35:59 +0000 Subject: [PATCH] Bug 1695817 - Part 6: Show name and publisher of an application owning a module. r=Gijs,fluent-reviewers,mhowell This patch adds application info (Name and Publisher for now) in the about:third-party page if a module is a part of an installed application, which is registered in the registry and shown in Windows Control Panel. To achieve this, we parse the registry to collect installed applications in the background task. Differential Revision: https://phabricator.services.mozilla.com/D109306 --- .../aboutthirdparty/AboutThirdParty.cpp | 343 ++++++++++++++++++ .../aboutthirdparty/AboutThirdParty.h | 35 ++ .../aboutthirdparty/AboutThirdPartyUtils.cpp | 69 ++++ .../aboutthirdparty/AboutThirdPartyUtils.h | 36 ++ .../aboutthirdparty/MsiDatabase.cpp | 88 +++++ .../components/aboutthirdparty/MsiDatabase.h | 94 +++++ .../content/aboutThirdParty.js | 19 + toolkit/components/aboutthirdparty/moz.build | 8 + .../aboutthirdparty/nsIAboutThirdParty.idl | 13 + .../tests/gtest/TestAboutThirdParty.cpp | 125 +++++++ .../aboutthirdparty/tests/gtest/moz.build | 13 + .../tests/xpcshell/test_aboutthirdparty.js | 12 + .../en-US/toolkit/about/aboutThirdParty.ftl | 2 + xpcom/base/nsWindowsHelpers.h | 15 + 14 files changed, 872 insertions(+) create mode 100644 toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp create mode 100644 toolkit/components/aboutthirdparty/AboutThirdPartyUtils.h create mode 100644 toolkit/components/aboutthirdparty/MsiDatabase.cpp create mode 100644 toolkit/components/aboutthirdparty/MsiDatabase.h create mode 100644 toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp create mode 100644 toolkit/components/aboutthirdparty/tests/gtest/moz.build diff --git a/toolkit/components/aboutthirdparty/AboutThirdParty.cpp b/toolkit/components/aboutthirdparty/AboutThirdParty.cpp index 0549cc268086..b5a7dfb2cbf1 100644 --- a/toolkit/components/aboutthirdparty/AboutThirdParty.cpp +++ b/toolkit/components/aboutthirdparty/AboutThirdParty.cpp @@ -6,10 +6,12 @@ #include "AboutThirdParty.h" +#include "AboutThirdPartyUtils.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/dom/Promise.h" #include "mozilla/NativeNt.h" #include "mozilla/StaticPtr.h" +#include "MsiDatabase.h" #include "nsComponentManagerUtils.h" #include "nsIWindowsRegKey.h" #include "nsThreadUtils.h" @@ -56,6 +58,292 @@ void EnumSubkeys(nsIWindowsRegKey* aRegBase, const CallbackT& aCallback) { } // 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 mInstallerData; + nsCOMPtr mCurrentApp; + ComponentPathMapT mComponentPaths; + nsTArray 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 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 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& 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 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(KnownModuleType::Last)]; @@ -317,8 +605,25 @@ namespace mozilla { static StaticRefPtr 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::GetSingleton() { if (!sSingleton) { @@ -355,6 +660,9 @@ void AboutThirdParty::BackgroundThread() { self->AddKnownModule(aDllPath, aType); }); + InstalledApplications apps; + apps.Collect(mComponentPaths, mLocations); + mWorkerState = WorkerState::Done; } @@ -395,6 +703,41 @@ NS_IMETHODIMP AboutThirdParty::LookupModuleType(const nsAString& aLeafName, 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 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; +} RefPtr AboutThirdParty::CollectSystemInfoAsync() { MOZ_ASSERT(NS_IsMainThread()); diff --git a/toolkit/components/aboutthirdparty/AboutThirdParty.h b/toolkit/components/aboutthirdparty/AboutThirdParty.h index b55ea4e75c5f..124f3d6e2dda 100644 --- a/toolkit/components/aboutthirdparty/AboutThirdParty.h +++ b/toolkit/components/aboutthirdparty/AboutThirdParty.h @@ -9,10 +9,17 @@ #include "mozilla/MozPromise.h" #include "nsIAboutThirdParty.h" +#include "nsInterfaceHashtable.h" +#include "nsTArray.h" #include "nsTHashMap.h" namespace mozilla { +using InstallLocationT = + CompactPair>; +using ComponentPathMapT = nsInterfaceHashtable; + enum class KnownModuleType : uint32_t { Ime = 0, IconOverlay, @@ -29,6 +36,32 @@ enum class KnownModuleType : uint32_t { 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; @@ -42,6 +75,8 @@ class AboutThirdParty final : public nsIAboutThirdParty { Atomic mWorkerState; RefPtr mPromise; nsTHashMap mKnownModules; + ComponentPathMapT mComponentPaths; + nsTArray mLocations; ~AboutThirdParty() = default; void BackgroundThread(); diff --git a/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp new file mode 100644 index 000000000000..050975c9da13 --- /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 000000000000..c65e5f7c5704 --- /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/MsiDatabase.cpp b/toolkit/components/aboutthirdparty/MsiDatabase.cpp new file mode 100644 index 000000000000..6c667072b7fb --- /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(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 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(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::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 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 000000000000..91c1370b8d61 --- /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 +#include +#include + +namespace mozilla { + +class MsiDatabase final { + static UniquePtr 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 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 + 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 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/content/aboutThirdParty.js b/toolkit/components/aboutthirdparty/content/aboutThirdParty.js index 3a6289a7ccc9..783b8c10573a 100644 --- a/toolkit/components/aboutthirdparty/content/aboutThirdParty.js +++ b/toolkit/components/aboutthirdparty/content/aboutThirdParty.js @@ -58,6 +58,9 @@ async function fetchData() { module.typeFlags = AboutThirdParty.lookupModuleType( module.dllFile?.leafName ); + module.application = AboutThirdParty.lookupApplication( + module.dllFile?.path + ); } for (const [proc, perProc] of Object.entries(data.processes)) { @@ -166,6 +169,10 @@ function copyDataToClipboard(aData) { 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 => { @@ -225,6 +232,18 @@ function visualizeData(aData) { 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) diff --git a/toolkit/components/aboutthirdparty/moz.build b/toolkit/components/aboutthirdparty/moz.build index 208c98cd5ab7..dc7b1018418c 100644 --- a/toolkit/components/aboutthirdparty/moz.build +++ b/toolkit/components/aboutthirdparty/moz.build @@ -21,4 +21,12 @@ EXPORTS.mozilla += [ SOURCES += [ "AboutThirdParty.cpp", + "AboutThirdPartyUtils.cpp", + "MsiDatabase.cpp", ] + +OS_LIBS += ["msi"] +DELAYLOAD_DLLS += ["msi.dll"] + +if CONFIG["ENABLE_TESTS"]: + DIRS += ["tests/gtest"] diff --git a/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl b/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl index b5cd55443d36..ca67ddf5ac00 100644 --- a/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl +++ b/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl @@ -5,6 +5,13 @@ #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 { @@ -21,6 +28,12 @@ interface nsIAboutThirdParty : nsISupports */ 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); + /** * Posts a background task to collect system information and resolves * the returned promise when the task is finished. diff --git a/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp b/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp new file mode 100644 index 000000000000..7defc2c7acf6 --- /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 +#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 locations(ArrayLength(kDirectoriesUnsorted)); + for (int 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 000000000000..659c0836db47 --- /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/test_aboutthirdparty.js b/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js index 109122b80472..281fe42188ef 100644 --- a/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js +++ b/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js @@ -50,4 +50,16 @@ add_task(async () => { 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/locales/en-US/toolkit/about/aboutThirdParty.ftl b/toolkit/locales/en-US/toolkit/about/aboutThirdParty.ftl index c7080d90c1a9..6c95e0485ba6 100644 --- a/toolkit/locales/en-US/toolkit/about/aboutThirdParty.ftl +++ b/toolkit/locales/en-US/toolkit/about/aboutThirdParty.ftl @@ -22,6 +22,8 @@ third-party-detail-occurrences = Occurrences .title = How many times this module was loaded. third-party-detail-duration = Avg. Blocking time (ms) .title = How long this module blocked the application. +third-party-detail-app = Application +third-party-detail-publisher = Publisher third-party-th-process = Process third-party-th-duration = Loading Duration (ms) diff --git a/xpcom/base/nsWindowsHelpers.h b/xpcom/base/nsWindowsHelpers.h index a336e085f149..2299fb2d090b 100644 --- a/xpcom/base/nsWindowsHelpers.h +++ b/xpcom/base/nsWindowsHelpers.h @@ -11,6 +11,7 @@ #define nsWindowsHelpers_h #include +#include #include "nsAutoRef.h" #include "mozilla/Assertions.h" #include "mozilla/UniquePtr.h" @@ -172,6 +173,19 @@ class nsAutoRefTraits { } }; +template <> +class nsAutoRefTraits { + public: + typedef MSIHANDLE RawRef; + static RawRef Void() { return 0; } + + static void Release(RawRef aHandle) { + if (aHandle != Void()) { + ::MsiCloseHandle(aHandle); + } + } +}; + // HGLOBAL is just a typedef of HANDLE which nsSimpleRef has a specialization // of, that means having a nsAutoRefTraits specialization for HGLOBAL is // useless. Therefore we create a wrapper class for HGLOBAL to make @@ -237,6 +251,7 @@ typedef nsAutoRef nsModuleHandle; typedef nsAutoRef nsAutoDevMode; typedef nsAutoRef nsAutoGlobalMem; typedef nsAutoRef nsAutoPrinter; +typedef nsAutoRef nsAutoMsiHandle; namespace {