diff options
Diffstat (limited to 'toolkit/system/windowsPackageManager')
7 files changed, 503 insertions, 0 deletions
diff --git a/toolkit/system/windowsPackageManager/components.conf b/toolkit/system/windowsPackageManager/components.conf new file mode 100644 index 0000000000..a1bab60e03 --- /dev/null +++ b/toolkit/system/windowsPackageManager/components.conf @@ -0,0 +1,14 @@ +# -*- 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': '{c75da378-521f-11ec-84cc-336cd3921c24}', + 'contract_ids': ['@mozilla.org/windows-package-manager;1'], + 'type': 'mozilla::toolkit::system::nsWindowsPackageManager', + 'headers': ['/toolkit/system/windowsPackageManager/nsWindowsPackageManager.h'] + }, +] diff --git a/toolkit/system/windowsPackageManager/moz.build b/toolkit/system/windowsPackageManager/moz.build new file mode 100644 index 0000000000..13f940edbe --- /dev/null +++ b/toolkit/system/windowsPackageManager/moz.build @@ -0,0 +1,26 @@ +# -*- 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", "Installer") + +TEST_DIRS += ["tests/gtest"] + +SOURCES += ["nsWindowsPackageManager.cpp"] + +LOCAL_INCLUDES += ["/toolkit/components/jsoncpp/include"] + +USE_LIBS += ["jsoncpp"] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +XPIDL_MODULE = "windows_package_manager" + +XPIDL_SOURCES += ["nsIWindowsPackageManager.idl"] + +FINAL_LIBRARY = "xul" diff --git a/toolkit/system/windowsPackageManager/nsIWindowsPackageManager.idl b/toolkit/system/windowsPackageManager/nsIWindowsPackageManager.idl new file mode 100644 index 0000000000..c78fc841e7 --- /dev/null +++ b/toolkit/system/windowsPackageManager/nsIWindowsPackageManager.idl @@ -0,0 +1,41 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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(ad57ac40-52f0-11ec-ada8-4f671255c4aa)] +interface nsIWindowsPackageManager : nsISupports +{ +/* Searches for any user installed MSIX packages whose + * packageFamilyName matches any of the provided `aNamePrefixes` + * and returns them. The Windows APIs only allow querying user + * installed packages without elevation, so this will not see any + * packages installed by another user. + */ + Array<AString> findUserInstalledPackages(in Array<AString> prefix); + +/* When running within a Packaged App environment, returns the + * InstalledDate of the Package. If called when not running within + * a Packaged App environment, throws NS_ERROR_NOT_IMPLEMENTED. + * Any other others will cause NS_ERROR_FAILURE to be thrown. + */ + unsigned long long getInstalledDate(); + +/* Retrieves the campaignId, if any, a user's Microsoft Store install is + * associated with. These are present if the user clicked a "ms-window-store://" + * or "https://" link that included a "cid" query argument the very first time + * they installed the app. (This value appears to be cached forever, so + * subsequent installs will not refresh it.) If a non-empty campaign ID is + * found it will be assumed to be a properly formatted attribution code and + * have an additional "msstoresignedin" key appended to it indicate whether or + * not the user was signed in when they installed the application. This key + * will either be set to "true" or "false". + * + * @throw NS_ERROR_NOT_IMPLEMENTED if called on Windows 8 or earlier, or from + * a non-packaged build. + * @throw NS_ERROR_FAILURE for any other errors + */ + AString getCampaignId(); +}; diff --git a/toolkit/system/windowsPackageManager/nsWindowsPackageManager.cpp b/toolkit/system/windowsPackageManager/nsWindowsPackageManager.cpp new file mode 100644 index 0000000000..98cff7db40 --- /dev/null +++ b/toolkit/system/windowsPackageManager/nsWindowsPackageManager.cpp @@ -0,0 +1,341 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWindowsPackageManager.h" +#include "mozilla/Logging.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/mscom/EnsureMTA.h" +#ifndef __MINGW32__ +# include <comutil.h> +# include <wrl.h> +# include <windows.applicationmodel.store.h> +# include <windows.management.deployment.h> +# include <windows.services.store.h> +#endif // __MINGW32__ +#include "nsError.h" +#include "nsString.h" +#include "nsISupportsPrimitives.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsXPCOMCID.h" +#include "json/json.h" + +#ifndef __MINGW32__ // WinRT headers not yet supported by MinGW +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace ABI::Windows; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Management; +using namespace ABI::Windows::Services::Store; +#endif + +// Campaign IDs are stored in a JSON data structure under this key +// for installs done without the user being signed in to the Microsoft +// store. +#define CAMPAIGN_ID_JSON_FIELD_NAME "customPolicyField1" + +namespace mozilla { +namespace toolkit { +namespace system { + +NS_IMPL_ISUPPORTS(nsWindowsPackageManager, nsIWindowsPackageManager) + +NS_IMETHODIMP +nsWindowsPackageManager::FindUserInstalledPackages( + const nsTArray<nsString>& aNamePrefixes, nsTArray<nsString>& aPackages) { +#ifdef __MINGW32__ + return NS_ERROR_NOT_IMPLEMENTED; +#else + // The classes we're using are only available beginning with Windows 10 + if (!mozilla::IsWin10OrLater()) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + ComPtr<IInspectable> pmInspectable; + ComPtr<Deployment::IPackageManager> pm; + HRESULT hr = RoActivateInstance( + HStringReference( + RuntimeClass_Windows_Management_Deployment_PackageManager) + .Get(), + &pmInspectable); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + hr = pmInspectable.As(&pm); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + + ComPtr<Collections::IIterable<ApplicationModel::Package*> > pkgs; + hr = pm->FindPackagesByUserSecurityId(NULL, &pkgs); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + ComPtr<Collections::IIterator<ApplicationModel::Package*> > iterator; + hr = pkgs->First(&iterator); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + boolean hasCurrent; + hr = iterator->get_HasCurrent(&hasCurrent); + while (SUCCEEDED(hr) && hasCurrent) { + ComPtr<ApplicationModel::IPackage> package; + hr = iterator->get_Current(&package); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + ComPtr<ApplicationModel::IPackageId> packageId; + hr = package->get_Id(&packageId); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + HString rawName; + hr = packageId->get_FamilyName(rawName.GetAddressOf()); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + unsigned int tmp; + nsString name(rawName.GetRawBuffer(&tmp)); + for (uint32_t i = 0; i < aNamePrefixes.Length(); i++) { + if (name.Find(aNamePrefixes.ElementAt(i)) != kNotFound) { + aPackages.AppendElement(name); + break; + } + } + hr = iterator->MoveNext(&hasCurrent); + } + return NS_OK; +#endif // __MINGW32__ +} + +NS_IMETHODIMP +nsWindowsPackageManager::GetInstalledDate(uint64_t* ts) { +#ifdef __MINGW32__ + return NS_ERROR_NOT_IMPLEMENTED; +#else + // The classes we're using are only available beginning with Windows 10 + if (!mozilla::IsWin10OrLater()) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + ComPtr<ApplicationModel::IPackageStatics> pkgStatics; + HRESULT hr = RoGetActivationFactory( + HStringReference(RuntimeClass_Windows_ApplicationModel_Package).Get(), + IID_PPV_ARGS(&pkgStatics)); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + + ComPtr<ApplicationModel::IPackage> package; + hr = pkgStatics->get_Current(&package); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + + ComPtr<ApplicationModel::IPackage3> package3; + hr = package.As(&package3); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + + DateTime installedDate; + hr = package3->get_InstalledDate(&installedDate); + if (!SUCCEEDED(hr)) { + return NS_ERROR_FAILURE; + } + + *ts = installedDate.UniversalTime; + return NS_OK; +#endif // __MINGW32__ +} + +NS_IMETHODIMP +nsWindowsPackageManager::GetCampaignId(nsAString& aCampaignId) { +#ifdef __MINGW32__ + return NS_ERROR_NOT_IMPLEMENTED; +#else + // The classes we're using are only available beginning with Windows 10, + // and this is only relevant for MSIX packaged builds. + if (!mozilla::IsWin10OrLater() || !mozilla::HasPackageIdentity()) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + ComPtr<IStoreContextStatics> scStatics = nullptr; + HRESULT hr = RoGetActivationFactory( + HStringReference(RuntimeClass_Windows_Services_Store_StoreContext).Get(), + IID_PPV_ARGS(&scStatics)); + /* Per + * https://docs.microsoft.com/en-us/windows/uwp/publish/create-a-custom-app-promotion-campaign#programmatically-retrieve-the-custom-campaign-id-for-an-app + * there are three ways to find a campaign ID. + * 1) If the user was logged into the Microsoft Store when they installed + * it will be available in a SKU. + * 2) If they were not logged in, it will be available through the + * StoreAppLicense + * + * There's also a third way, in theory, to retrieve it on very old versions of + * Windows 10 (1511 or earlier). However, these versions don't appear to be + * able to use the Windows Store at all anymore - so there's no point in + * supporting that scenario. + * + */ + if (!SUCCEEDED(hr) || scStatics == nullptr) return NS_ERROR_FAILURE; + ComPtr<IStoreContext> storeContext = nullptr; + hr = scStatics->GetDefault(&storeContext); + if (!SUCCEEDED(hr) || storeContext == nullptr) return NS_ERROR_FAILURE; + + ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr = nullptr; + + { + nsAutoHandle event(CreateEventW(nullptr, true, false, nullptr)); + bool asyncOpSucceeded = false; + + // Despite the documentation indicating otherwise, the async operations + // and callbacks used here don't seem to work outside of a COM MTA. + mozilla::mscom::EnsureMTA( + [&event, &asyncOpSucceeded, &hr, &storeContext, &asyncSpr]() -> void { + auto callback = + Callback<IAsyncOperationCompletedHandler<StoreProductResult*> >( + [&asyncOpSucceeded, &event]( + IAsyncOperation<StoreProductResult*>* asyncInfo, + AsyncStatus status) -> HRESULT { + asyncOpSucceeded = status == AsyncStatus::Completed; + return SetEvent(event.get()); + }); + + hr = storeContext->GetStoreProductForCurrentAppAsync(&asyncSpr); + if (!SUCCEEDED(hr) || asyncSpr == nullptr) { + asyncOpSucceeded = false; + return; + } + hr = asyncSpr->put_Completed(callback.Get()); + if (!SUCCEEDED(hr)) { + asyncOpSucceeded = false; + return; + } + + DWORD ret = WaitForSingleObject(event.get(), 30000); + if (ret != WAIT_OBJECT_0) { + asyncOpSucceeded = false; + } + }); + if (!asyncOpSucceeded) return NS_ERROR_FAILURE; + } + + ComPtr<IStoreProductResult> productResult = nullptr; + hr = asyncSpr->GetResults(&productResult); + if (!SUCCEEDED(hr) || productResult == nullptr) return NS_ERROR_FAILURE; + + ComPtr<IStoreProduct> product = nullptr; + hr = productResult->get_Product(&product); + if (!SUCCEEDED(hr) || product == nullptr) return NS_ERROR_FAILURE; + ComPtr<Collections::IVectorView<StoreSku*> > skus = nullptr; + hr = product->get_Skus(&skus); + if (!SUCCEEDED(hr) || skus == nullptr) return NS_ERROR_FAILURE; + + unsigned int size; + hr = skus->get_Size(&size); + if (!SUCCEEDED(hr)) return NS_ERROR_FAILURE; + + for (unsigned int i = 0; i < size; i++) { + ComPtr<IStoreSku> sku = nullptr; + hr = skus->GetAt(i, &sku); + if (!SUCCEEDED(hr) || sku == nullptr) return NS_ERROR_FAILURE; + + boolean isInUserCollection = false; + hr = sku->get_IsInUserCollection(&isInUserCollection); + if (!SUCCEEDED(hr) || !isInUserCollection) continue; + + ComPtr<IStoreCollectionData> scd = nullptr; + hr = sku->get_CollectionData(&scd); + if (!SUCCEEDED(hr) || scd == nullptr) continue; + + HString campaignId; + hr = scd->get_CampaignId(campaignId.GetAddressOf()); + if (!SUCCEEDED(hr)) continue; + + unsigned int tmp; + aCampaignId.Assign(campaignId.GetRawBuffer(&tmp)); + if (aCampaignId.Length() > 0) { + aCampaignId.AppendLiteral("&msstoresignedin=true"); + } + } + + // There's various points above that could exit without a failure. + // If we get here without a campaignId we may as well just check + // the AppStoreLicense. + if (aCampaignId.IsEmpty()) { + ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal = nullptr; + bool asyncOpSucceeded = false; + nsAutoHandle event(CreateEventW(nullptr, true, false, nullptr)); + mozilla::mscom::EnsureMTA( + [&event, &asyncOpSucceeded, &hr, &storeContext, &asyncSal]() -> void { + auto callback = + Callback<IAsyncOperationCompletedHandler<StoreAppLicense*> >( + [&asyncOpSucceeded, &event]( + IAsyncOperation<StoreAppLicense*>* asyncInfo, + AsyncStatus status) -> HRESULT { + asyncOpSucceeded = status == AsyncStatus::Completed; + return SetEvent(event.get()); + }); + + hr = storeContext->GetAppLicenseAsync(&asyncSal); + if (!SUCCEEDED(hr) || asyncSal == nullptr) { + asyncOpSucceeded = false; + return; + } + hr = asyncSal->put_Completed(callback.Get()); + if (!SUCCEEDED(hr)) { + asyncOpSucceeded = false; + return; + } + + DWORD ret = WaitForSingleObject(event.get(), 30000); + if (ret != WAIT_OBJECT_0) { + asyncOpSucceeded = false; + } + }); + if (!asyncOpSucceeded) return NS_ERROR_FAILURE; + + ComPtr<IStoreAppLicense> license = nullptr; + hr = asyncSal->GetResults(&license); + if (!SUCCEEDED(hr) || license == nullptr) return NS_ERROR_FAILURE; + + HString extendedData; + hr = license->get_ExtendedJsonData(extendedData.GetAddressOf()); + if (!SUCCEEDED(hr)) return NS_ERROR_FAILURE; + + Json::Value jsonData; + Json::Reader jsonReader; + + unsigned int tmp; + nsAutoString key(extendedData.GetRawBuffer(&tmp)); + if (!jsonReader.parse(NS_ConvertUTF16toUTF8(key).get(), jsonData, false)) { + return NS_ERROR_FAILURE; + } + + if (jsonData.isMember(CAMPAIGN_ID_JSON_FIELD_NAME) && + jsonData[CAMPAIGN_ID_JSON_FIELD_NAME].isString()) { + aCampaignId.Assign( + NS_ConvertUTF8toUTF16( + jsonData[CAMPAIGN_ID_JSON_FIELD_NAME].asString().c_str()) + .get()); + if (aCampaignId.Length() > 0) { + aCampaignId.AppendLiteral("&msstoresignedin=false"); + } + } + } + + // No matter what happens in either block above, if they don't exit with a + // failure we managed to successfully pull the campaignId from somewhere + // (even if its empty). + return NS_OK; + +#endif // __MINGW32__ +} + +} // namespace system +} // namespace toolkit +} // namespace mozilla diff --git a/toolkit/system/windowsPackageManager/nsWindowsPackageManager.h b/toolkit/system/windowsPackageManager/nsWindowsPackageManager.h new file mode 100644 index 0000000000..30cc8a288e --- /dev/null +++ b/toolkit/system/windowsPackageManager/nsWindowsPackageManager.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_toolkit_system_nsWindowsPackageManager_h +#define mozilla_toolkit_system_nsWindowsPackageManager_h + +#include "nsIWindowsPackageManager.h" + +namespace mozilla { +namespace toolkit { +namespace system { + +class nsWindowsPackageManager final : public nsIWindowsPackageManager { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWINDOWSPACKAGEMANAGER + + private: + ~nsWindowsPackageManager(){}; +}; + +} // namespace system +} // namespace toolkit +} // namespace mozilla + +#endif // mozilla_toolkit_system_nsWindowsPackageManager_h diff --git a/toolkit/system/windowsPackageManager/tests/gtest/TestWindowsPackageManager.cpp b/toolkit/system/windowsPackageManager/tests/gtest/TestWindowsPackageManager.cpp new file mode 100644 index 0000000000..235f44a2f7 --- /dev/null +++ b/toolkit/system/windowsPackageManager/tests/gtest/TestWindowsPackageManager.cpp @@ -0,0 +1,35 @@ +/* -*- 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 "gtest/gtest.h" +#include "nsWindowsPackageManager.h" +#include "nsServiceManagerUtils.h" + +using namespace mozilla::toolkit::system; + +TEST(WindowsPackageManager, TestWithMatches) +{ + nsCOMPtr<nsIWindowsPackageManager> wpm( + do_GetService("@mozilla.org/windows-package-manager;1")); + nsTArray<nsString> prefixes, packages; + // We're assuming that there will always be at least _one_ Microsoft package + // installed when we run tests. This will _probably_ hold true. + prefixes.AppendElement(u"Microsoft"_ns); + nsresult retVal = wpm->FindUserInstalledPackages(prefixes, packages); + ASSERT_GT(packages.Length(), 0U); + ASSERT_EQ(NS_OK, retVal); +} + +TEST(WindowsPackageManager, TestWithoutMatches) +{ + nsCOMPtr<nsIWindowsPackageManager> wpm( + do_GetService("@mozilla.org/windows-package-manager;1")); + nsTArray<nsString> prefixes, packages; + prefixes.AppendElement(u"DoesntExist"_ns); + nsresult retVal = wpm->FindUserInstalledPackages(prefixes, packages); + ASSERT_EQ(packages.Length(), 0U); + ASSERT_EQ(NS_OK, retVal); +} diff --git a/toolkit/system/windowsPackageManager/tests/gtest/moz.build b/toolkit/system/windowsPackageManager/tests/gtest/moz.build new file mode 100644 index 0000000000..244ae2dfaa --- /dev/null +++ b/toolkit/system/windowsPackageManager/tests/gtest/moz.build @@ -0,0 +1,18 @@ +# -*- 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/. + +UNIFIED_SOURCES += [ + "TestWindowsPackageManager.cpp", +] + +LOCAL_INCLUDES += [ + "/toolkit/system/windowsPackageManager", +] + +FINAL_LIBRARY = "xul-gtest" + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wshadow"] |