Skip to content

Commit

Permalink
Bug 1900225: Part 3 - Implement system geolocation permission UX on W…
Browse files Browse the repository at this point in the history
…indows 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
  • Loading branch information
davidp3 committed Aug 27, 2024
1 parent 2a1cff7 commit befc675
Show file tree
Hide file tree
Showing 2 changed files with 302 additions and 2 deletions.
285 changes: 285 additions & 0 deletions dom/geolocation/GeolocationSystemWin.cpp
Original file line number Diff line number Diff line change
@@ -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
19 changes: 17 additions & 2 deletions dom/geolocation/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
with Files("**"):
BUG_COMPONENT = ("Core", "DOM: Geolocation")

with Files("GeolocationSystemWin.cpp"):
BUG_COMPONENT = ("Core", "Widget: Win32")

EXPORTS += [
"nsGeoPositionIPCSerialiser.h",
]
Expand All @@ -28,11 +31,19 @@ SOURCES += [
]

UNIFIED_SOURCES += [
"GeolocationSystem.cpp",
"MLSFallback.cpp",
]

if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["MOZ_BUILD_APP"] == "browser":
# MinGW is missing the headers needed to build the Windows Geolocation System
# Permission UI so it uses the platform-agnostic version.
if (
CONFIG["OS_TARGET"] == "WINNT"
and CONFIG["CC_TYPE"] == "clang-cl"
and CONFIG["MOZ_BUILD_APP"] == "browser"
):
UNIFIED_SOURCES += [
"GeolocationSystemWin.cpp",
]
EXTRA_JS_MODULES += [
"GeolocationUIUtilsWin.sys.mjs",
]
Expand All @@ -41,6 +52,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")

Expand Down

0 comments on commit befc675

Please sign in to comment.