/* -*- 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 "GeolocationSystem.h" #include "mozilla/Components.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/ScopeExit.h" #include "nsIGeolocationUIUtilsWin.h" #include "nsIWifiListener.h" #include "nsIWifiMonitor.h" #include #include #include namespace mozilla::dom::geolocation { using namespace ABI::Windows::Foundation; using namespace ABI::Windows::Security::Authorization::AppCapabilityAccess; using namespace ABI::Windows::System; using namespace Microsoft::WRL; using Wrappers::HStringReference; namespace { const auto& kAppCapabilityGuid = RuntimeClass_Windows_Security_Authorization_AppCapabilityAccess_AppCapability; const auto& kLauncherGuid = RuntimeClass_Windows_System_Launcher; const auto& kUriGuid = RuntimeClass_Windows_Foundation_Uri; const wchar_t kLocationSettingsPage[] = L"ms-settings:privacy-location"; template ComPtr CreateFromActivationFactory(const wchar_t* aNamespace) { ComPtr newObject; GetActivationFactory(HStringReference(aNamespace).Get(), &newObject); return newObject; } RefPtr GetWifiControlAppCapability() { ComPtr appCapabilityStatics = CreateFromActivationFactory(kAppCapabilityGuid); NS_ENSURE_TRUE(appCapabilityStatics, nullptr); RefPtr appCapability; HRESULT hr = appCapabilityStatics->Create( HStringReference(L"wifiControl").Get(), getter_AddRefs(appCapability)); NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); NS_ENSURE_TRUE(appCapability, nullptr); return appCapability; } Maybe GetWifiControlAccess() { auto appCapability = GetWifiControlAppCapability(); NS_ENSURE_TRUE(appCapability, Nothing()); AppCapabilityAccessStatus status; HRESULT hr = appCapability->CheckAccess(&status); NS_ENSURE_TRUE(SUCCEEDED(hr), Nothing()); return Some(status); } bool SystemWillPromptForPermissionHint() { auto wifiAccess = GetWifiControlAccess(); if (wifiAccess != mozilla::Some(AppCapabilityAccessStatus:: AppCapabilityAccessStatus_UserPromptRequired)) { return false; } // If wifi is not available (e.g. because there is no wifi device present) // then the API may report that Windows will request geolocation permission // but it can't without the wifi scanner. Check for that case. nsCOMPtr wifiMonitor = components::WifiMonitor::Service(); NS_ENSURE_TRUE(wifiMonitor, false); return wifiMonitor->GetHasWifiAdapter(); } bool LocationIsPermittedHint() { auto wifiAccess = GetWifiControlAccess(); // This API wasn't available on earlier versions of Windows, so a failure to // get the result means that we will assume that location access is permitted. return wifiAccess.isNothing() || *wifiAccess == AppCapabilityAccessStatus::AppCapabilityAccessStatus_Allowed; } class WindowsGeolocationPermissionRequest final : public SystemGeolocationPermissionRequest, public SupportsThreadSafeWeakPtr { public: MOZ_DECLARE_REFCOUNTED_TYPENAME(WindowsGeolocationPermissionRequest); // Define SystemGeolocationPermissionRequest's ref-counting by forwarding the // calls to SupportsThreadSafeWeakPtr NS_INLINE_DECL_REFCOUNTING_INHERITED( WindowsGeolocationPermissionRequest, SupportsThreadSafeWeakPtr); WindowsGeolocationPermissionRequest(BrowsingContext* aBrowsingContext, ParentRequestResolver&& aResolver) : mResolver(std::move(aResolver)), mBrowsingContext(aBrowsingContext) {} void Initialize() { MOZ_ASSERT(!mIsRunning); auto failedToWatch = MakeScopeExit([&]() { if (!mIsRunning) { mAppCapability = nullptr; mToken = EventRegistrationToken{}; mResolver(GeolocationPermissionStatus::Error); } }); mAppCapability = GetWifiControlAppCapability(); if (!mAppCapability) { return; } using AccessChangedHandler = ITypedEventHandler; // Note: This creates the callback that listens for location permission // changes as free threaded, which we need to do to overcome a (Microsoft) // issue with the callback proxy's exports with (at least) the pre-24H2 // versions of Windows. ComPtr acHandlerRef = Callback, AccessChangedHandler, FtmBase>>( [weakSelf = ThreadSafeWeakPtr( this)](IAppCapability*, IAppCapabilityAccessChangedEventArgs*) { // Because of the free threaded access mentioned above, our // callback can run on a background thread, so dispatch it to // main. if (!NS_IsMainThread()) { NS_DispatchToMainThread( NS_NewRunnableFunction(__func__, [weakSelf]() { RefPtr self(weakSelf); if (self) { self->StopIfLocationIsPermitted(); } })); return S_OK; } RefPtr self(weakSelf); if (self) { self->StopIfLocationIsPermitted(); } return S_OK; }); if (!acHandlerRef) { return; } HRESULT hr = mAppCapability->add_AccessChanged(acHandlerRef.Get(), &mToken); NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); MOZ_ASSERT(mToken.value); mIsRunning = true; failedToWatch.release(); // Avoid a race for accessChanged by checking it now and stopping if // permission is already granted. StopIfLocationIsPermitted(); } void Stop() override { MOZ_ASSERT(NS_IsMainThread()); if (!mIsRunning) { return; } mIsRunning = false; if (LocationIsPermittedHint()) { mResolver(GeolocationPermissionStatus::Granted); } else { mResolver(GeolocationPermissionStatus::Canceled); } // Remove the modal with the cancel button. nsresult rv = DismissPrompt(); NS_ENSURE_SUCCESS_VOID(rv); // Stop watching location settings. MOZ_ASSERT(mAppCapability); mAppCapability->remove_AccessChanged(mToken); mAppCapability = nullptr; mToken = EventRegistrationToken{}; } bool IsStopped() { return !mIsRunning; } protected: // Ref-counting demands that the destructor be non-public but // SupportsThreadSafeWeakPtr needs to be able to call it, because we use // NS_INLINE_DECL_REFCOUNTING_INHERITED. friend SupportsThreadSafeWeakPtr; virtual ~WindowsGeolocationPermissionRequest() { Stop(); MOZ_ASSERT(mToken.value == 0); } void StopIfLocationIsPermitted() { if (LocationIsPermittedHint()) { Stop(); } } nsresult DismissPrompt() { nsresult rv; nsCOMPtr utils = do_GetService("@mozilla.org/geolocation/ui-utils-win;1", &rv); NS_ENSURE_SUCCESS(rv, rv); return utils->DismissPrompts(mBrowsingContext); } // This IAppCapability object must be held for the duration of the period // where we listen for location permission changes or else the callback // will not be called. RefPtr mAppCapability; ParentRequestResolver mResolver; RefPtr mBrowsingContext; EventRegistrationToken mToken; bool mIsRunning = false; }; // Opens Windows geolocation settings and cancels the geolocation request on // error. void OpenWindowsLocationSettings( SystemGeolocationPermissionRequest* aPermissionRequest) { auto cancelRequest = MakeScopeExit([&]() { aPermissionRequest->Stop(); }); ComPtr uriFactory = CreateFromActivationFactory(kUriGuid); NS_ENSURE_TRUE_VOID(uriFactory); RefPtr uri; HRESULT hr = uriFactory->CreateUri( HStringReference(kLocationSettingsPage).Get(), getter_AddRefs(uri)); NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); ComPtr launcherStatics = CreateFromActivationFactory(kLauncherGuid); NS_ENSURE_TRUE_VOID(launcherStatics); RefPtr> handler; hr = launcherStatics->LaunchUriAsync(uri, getter_AddRefs(handler)); NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); // The IAsyncOperation is similar to a promise so there is no race here, // despite us adding this callback after requesting launch instead of before. handler->put_Completed( Callback>( [permissionRequest = RefPtr{aPermissionRequest}]( IAsyncOperation* asyncInfo, AsyncStatus status) { unsigned char verdict = 0; asyncInfo->GetResults(&verdict); if (!verdict) { permissionRequest->Stop(); } return S_OK; }) .Get()); cancelRequest.release(); } class LocationPermissionWifiScanListener final : public nsIWifiListener { public: NS_DECL_ISUPPORTS explicit LocationPermissionWifiScanListener( SystemGeolocationPermissionRequest* aRequest) : mRequest(aRequest) {} NS_IMETHOD OnChange(const nsTArray>&) override { // We will remove ourselves from the nsIWifiMonitor, which is our owner. // Hold a strong reference to ourselves until we complete the callback. RefPtr self = this; return PermissionWasDecided(); } NS_IMETHOD OnError(nsresult) override { // We will remove ourselves from the nsIWifiMonitor, which is our owner. // Hold a strong reference to ourselves until we complete the callback. RefPtr self = this; return PermissionWasDecided(); } private: virtual ~LocationPermissionWifiScanListener() = default; RefPtr mRequest; // Any response to our wifi scan request means that the user has selected // either to grant or deny permission in the Windows dialog. Either way, we // are done asking for permission, so Stop the permission request. nsresult PermissionWasDecided() { nsCOMPtr wifiMonitor = components::WifiMonitor::Service(); NS_ENSURE_TRUE(wifiMonitor, NS_ERROR_FAILURE); wifiMonitor->StopWatching(this); mRequest->Stop(); return NS_OK; } }; NS_IMPL_ISUPPORTS(LocationPermissionWifiScanListener, nsIWifiListener) } // namespace //----------------------------------------------------------------------------- SystemGeolocationPermissionBehavior GetGeolocationPermissionBehavior() { if (SystemWillPromptForPermissionHint()) { return SystemGeolocationPermissionBehavior::SystemWillPromptUser; } if (!LocationIsPermittedHint()) { return SystemGeolocationPermissionBehavior::GeckoWillPromptUser; } return SystemGeolocationPermissionBehavior::NoPrompt; } already_AddRefed RequestLocationPermissionFromUser(BrowsingContext* aBrowsingContext, ParentRequestResolver&& aResolver) { RefPtr permissionRequest = new WindowsGeolocationPermissionRequest(aBrowsingContext, std::move(aResolver)); permissionRequest->Initialize(); if (permissionRequest->IsStopped()) { return nullptr; } if (SystemWillPromptForPermissionHint()) { // To tell the system to prompt for permission, run one wifi scan (no need // to poll). We won't use the result -- either the user will grant // geolocation permission, meaning we will not need wifi scanning, or the // user will deny permission, in which case no scan can be done. We just // want the prompt. nsCOMPtr wifiMonitor = components::WifiMonitor::Service(); NS_ENSURE_TRUE(wifiMonitor, nullptr); auto listener = MakeRefPtr(permissionRequest); wifiMonitor->StartWatching(listener, false); } else { OpenWindowsLocationSettings(permissionRequest); } return permissionRequest.forget(); } } // namespace mozilla::dom::geolocation