Bug 1900225: Part 3 - Implement system geolocation permission UX on Windows r=win-reviewers,gstoll

This implements PresentSystemSettings, LocationIsPermittedHint and
SystemWillPromptForPermissionHint for Windows.  The Windows APIs are not always
available -- some are currently only available in Windows 11 Canary builds
(slated for September release).  In the event that APIs are not available, this
should do nothing.  At present, this is detailed here:

https://learn.microsoft.com/en-us/windows/win32/nativewifi/wi-fi-access-location-changes

There are two issues that this is intended to handle:

1. The system will display a one-time (or so) dialog to the user when Firefox
requests geolocation but doesn't have permission.  For that case, we inform the
user that they will be asked to grant location permission again.  This system
dialog is only presented in versions of Windows that support all of the relevant
APIs.
2. We open system settings to the right page and post a cancelable modal dialog
on the tab if the user grants geolocation to the page but geolocation permission
isn't currently granted in the OS.  This case will not happen if case #1 did.
Unfortunately, we can't get information about the permission status without a
location request on old versions of Windows, so this also does nothing unless
the recent APIs are supported (in this case, AppCapability::CheckAccess).

This work is necessitated not only by the new (occasional) system dialog but
also by Microsoft's plans to block wifi scanning if geolocation isn't available.
We have used wifi scanning as part of a fallback when system geolocation isn't
available -- that approach is no longer viable here.  A user would confusingly
get repeated errors or very poor results (e.g. IP lookup results) without
information as to why, if that happened.  This is what happens in the current
Windows Canary build if system geolocation is turned off.  The fallback remains
useful on other platforms, although Linux is in flux (but it is not in the
scope of this bug).

Differential Revision: https://phabricator.services.mozilla.com/D216474
This commit is contained in:
David P 2024-08-08 22:38:01 +00:00
parent a62e779ecc
commit 84edc8eeb2
2 changed files with 295 additions and 1 deletions

View File

@ -0,0 +1,285 @@
/* -*- 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/dom/BrowsingContext.h"
#include "mozilla/ScopeExit.h"
#include "nsIGeolocationUIUtilsWin.h"
#include <windows.system.h>
#include <windows.security.authorization.appcapabilityaccess.h>
#include <wrl.h>
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 <typename TypeToCreate>
ComPtr<TypeToCreate> CreateFromActivationFactory(const wchar_t* aNamespace) {
ComPtr<TypeToCreate> newObject;
GetActivationFactory(HStringReference(aNamespace).Get(), &newObject);
return newObject;
}
RefPtr<IAppCapability> GetWifiControlAppCapability() {
ComPtr<IAppCapabilityStatics> appCapabilityStatics =
CreateFromActivationFactory<IAppCapabilityStatics>(kAppCapabilityGuid);
NS_ENSURE_TRUE(appCapabilityStatics, nullptr);
RefPtr<IAppCapability> 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<AppCapabilityAccessStatus> 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();
return wifiAccess ==
mozilla::Some(AppCapabilityAccessStatus::
AppCapabilityAccessStatus_UserPromptRequired);
}
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<WindowsGeolocationPermissionRequest> {
public:
MOZ_DECLARE_REFCOUNTED_TYPENAME(WindowsGeolocationPermissionRequest);
// Define SystemGeolocationPermissionRequest's ref-counting by forwarding the
// calls to SupportsThreadSafeWeakPtr<WindowsGeolocationPermissionRequest>
NS_INLINE_DECL_REFCOUNTING_INHERITED(
WindowsGeolocationPermissionRequest,
SupportsThreadSafeWeakPtr<WindowsGeolocationPermissionRequest>);
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<AppCapability*,
AppCapabilityAccessChangedEventArgs*>;
// 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<AccessChangedHandler> acHandlerRef = Callback<Implements<
RuntimeClassFlags<ClassicCom>, AccessChangedHandler, FtmBase>>(
[weakSelf = ThreadSafeWeakPtr<WindowsGeolocationPermissionRequest>(
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<WindowsGeolocationPermissionRequest> self(weakSelf);
if (self) {
self->StopIfLocationIsPermitted();
}
}));
return S_OK;
}
RefPtr<WindowsGeolocationPermissionRequest> 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<WindowsGeolocationPermissionRequest>;
virtual ~WindowsGeolocationPermissionRequest() {
Stop();
MOZ_ASSERT(mToken.value == 0);
}
void StopIfLocationIsPermitted() {
if (LocationIsPermittedHint()) {
Stop();
}
}
nsresult DismissPrompt() {
nsresult rv;
nsCOMPtr<nsIGeolocationUIUtilsWin> 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<IAppCapability> mAppCapability;
ParentRequestResolver mResolver;
RefPtr<BrowsingContext> 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<IUriRuntimeClassFactory> uriFactory =
CreateFromActivationFactory<IUriRuntimeClassFactory>(kUriGuid);
NS_ENSURE_TRUE_VOID(uriFactory);
RefPtr<IUriRuntimeClass> uri;
HRESULT hr = uriFactory->CreateUri(
HStringReference(kLocationSettingsPage).Get(), getter_AddRefs(uri));
NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
ComPtr<ILauncherStatics> launcherStatics =
CreateFromActivationFactory<ILauncherStatics>(kLauncherGuid);
NS_ENSURE_TRUE_VOID(launcherStatics);
RefPtr<IAsyncOperation<bool>> 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<IAsyncOperationCompletedHandler<bool>>(
[permissionRequest = RefPtr{aPermissionRequest}](
IAsyncOperation<bool>* asyncInfo, AsyncStatus status) {
unsigned char verdict = 0;
asyncInfo->GetResults(&verdict);
if (!verdict) {
permissionRequest->Stop();
}
return S_OK;
})
.Get());
cancelRequest.release();
}
} // namespace
//-----------------------------------------------------------------------------
SystemGeolocationPermissionBehavior GetGeolocationPermissionBehavior() {
if (SystemWillPromptForPermissionHint()) {
return SystemGeolocationPermissionBehavior::SystemWillPromptUser;
}
if (!LocationIsPermittedHint()) {
return SystemGeolocationPermissionBehavior::GeckoWillPromptUser;
}
return SystemGeolocationPermissionBehavior::NoPrompt;
}
already_AddRefed<SystemGeolocationPermissionRequest> PresentSystemSettings(
BrowsingContext* aBrowsingContext, ParentRequestResolver&& aResolver) {
RefPtr<WindowsGeolocationPermissionRequest> permissionRequest =
new WindowsGeolocationPermissionRequest(aBrowsingContext,
std::move(aResolver));
permissionRequest->Initialize();
if (permissionRequest->IsStopped()) {
return nullptr;
}
OpenWindowsLocationSettings(permissionRequest);
return permissionRequest.forget();
}
} // namespace mozilla::dom::geolocation

View File

@ -7,6 +7,9 @@
with Files("**"):
BUG_COMPONENT = ("Core", "DOM: Geolocation")
with Files("GeolocationSystemWin.cpp"):
BUG_COMPONENT = ("Core", "Widget: Win32")
EXPORTS += [
"nsGeoPositionIPCSerialiser.h",
]
@ -28,11 +31,13 @@ SOURCES += [
]
UNIFIED_SOURCES += [
"GeolocationSystem.cpp",
"MLSFallback.cpp",
]
if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["MOZ_BUILD_APP"] == "browser":
UNIFIED_SOURCES += [
"GeolocationSystemWin.cpp",
]
EXTRA_JS_MODULES += [
"GeolocationUIUtilsWin.sys.mjs",
]
@ -41,6 +46,10 @@ if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["MOZ_BUILD_APP"] == "browser":
]
XPIDL_SOURCES += ["nsIGeolocationUIUtilsWin.idl"]
XPIDL_MODULE = "dom_geolocation"
else:
UNIFIED_SOURCES += [
"GeolocationSystem.cpp",
]
include("/ipc/chromium/chromium-config.mozbuild")