GeolocationSystemWin.cpp (13814B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include <windows.security.authorization.appcapabilityaccess.h> 8 #include <windows.system.h> 9 #include <wrl.h> 10 11 #include "GeolocationSystem.h" 12 #include "mozilla/Components.h" 13 #include "mozilla/ScopeExit.h" 14 #include "mozilla/dom/BrowsingContext.h" 15 #include "nsIGeolocationUIUtilsWin.h" 16 #include "nsIWifiListener.h" 17 #include "nsIWifiMonitor.h" 18 19 namespace mozilla::dom::geolocation { 20 21 using namespace ABI::Windows::Foundation; 22 using namespace ABI::Windows::Security::Authorization::AppCapabilityAccess; 23 using namespace ABI::Windows::System; 24 using namespace Microsoft::WRL; 25 using Wrappers::HStringReference; 26 27 namespace { 28 29 const auto& kAppCapabilityGuid = 30 RuntimeClass_Windows_Security_Authorization_AppCapabilityAccess_AppCapability; 31 const auto& kLauncherGuid = RuntimeClass_Windows_System_Launcher; 32 const auto& kUriGuid = RuntimeClass_Windows_Foundation_Uri; 33 const wchar_t kLocationSettingsPage[] = L"ms-settings:privacy-location"; 34 35 template <typename TypeToCreate> 36 ComPtr<TypeToCreate> CreateFromActivationFactory(const wchar_t* aNamespace) { 37 ComPtr<TypeToCreate> newObject; 38 GetActivationFactory(HStringReference(aNamespace).Get(), &newObject); 39 return newObject; 40 } 41 42 RefPtr<IAppCapability> GetWifiControlAppCapability() { 43 ComPtr<IAppCapabilityStatics> appCapabilityStatics = 44 CreateFromActivationFactory<IAppCapabilityStatics>(kAppCapabilityGuid); 45 NS_ENSURE_TRUE(appCapabilityStatics, nullptr); 46 47 RefPtr<IAppCapability> appCapability; 48 HRESULT hr = appCapabilityStatics->Create( 49 HStringReference(L"wifiControl").Get(), getter_AddRefs(appCapability)); 50 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); 51 NS_ENSURE_TRUE(appCapability, nullptr); 52 return appCapability; 53 } 54 55 Maybe<AppCapabilityAccessStatus> GetWifiControlAccess( 56 IAppCapability* aWifiControlAppCapability) { 57 NS_ENSURE_TRUE(aWifiControlAppCapability, Nothing()); 58 AppCapabilityAccessStatus status; 59 HRESULT hr = aWifiControlAppCapability->CheckAccess(&status); 60 NS_ENSURE_TRUE(SUCCEEDED(hr), Nothing()); 61 return Some(status); 62 } 63 64 Maybe<AppCapabilityAccessStatus> GetWifiControlAccess() { 65 RefPtr wifiControlAppCapability = GetWifiControlAppCapability(); 66 // Hold on to the RefPtr through the lifetime of the callee 67 return GetWifiControlAccess(wifiControlAppCapability.get()); 68 } 69 70 // Takes in wifiAccess instead of calculating here so we can avoid calling 71 // GetWifiControlAccess() multiple times, which can be slow (bug 1972405). 72 bool SystemWillPromptForPermissionHint( 73 Maybe<AppCapabilityAccessStatus> aWifiAccess) { 74 if (aWifiAccess != 75 mozilla::Some(AppCapabilityAccessStatus:: 76 AppCapabilityAccessStatus_UserPromptRequired)) { 77 return false; 78 } 79 80 // If wifi is not available (e.g. because there is no wifi device present) 81 // then the API may report that Windows will request geolocation permission 82 // but it can't without the wifi scanner. Check for that case. 83 nsCOMPtr<nsIWifiMonitor> wifiMonitor = components::WifiMonitor::Service(); 84 NS_ENSURE_TRUE(wifiMonitor, false); 85 return wifiMonitor->GetHasWifiAdapter(); 86 } 87 88 // Takes in wifiAccess instead of calculating here so we can avoid calling 89 // GetWifiControlAccess() multiple times, which can be slow (bug 1972405). 90 bool LocationIsPermittedHint(Maybe<AppCapabilityAccessStatus> aWifiAccess) { 91 // This API wasn't available on earlier versions of Windows, so a failure to 92 // get the result means that we will assume that location access is permitted. 93 return aWifiAccess.isNothing() || 94 *aWifiAccess == 95 AppCapabilityAccessStatus::AppCapabilityAccessStatus_Allowed; 96 } 97 98 class WindowsGeolocationPermissionRequest final 99 : public SystemGeolocationPermissionRequest, 100 public SupportsThreadSafeWeakPtr<WindowsGeolocationPermissionRequest> { 101 public: 102 MOZ_DECLARE_REFCOUNTED_TYPENAME(WindowsGeolocationPermissionRequest); 103 // Define SystemGeolocationPermissionRequest's ref-counting by forwarding the 104 // calls to SupportsThreadSafeWeakPtr<WindowsGeolocationPermissionRequest> 105 NS_INLINE_DECL_REFCOUNTING_INHERITED( 106 WindowsGeolocationPermissionRequest, 107 SupportsThreadSafeWeakPtr<WindowsGeolocationPermissionRequest>); 108 109 WindowsGeolocationPermissionRequest(BrowsingContext* aBrowsingContext, 110 ParentRequestResolver&& aResolver) 111 : mResolver(std::move(aResolver)), mBrowsingContext(aBrowsingContext) {} 112 113 void Initialize() { 114 MOZ_ASSERT(!mIsRunning); 115 auto failedToWatch = MakeScopeExit([&]() { 116 if (!mIsRunning) { 117 mAppCapability = nullptr; 118 mToken = EventRegistrationToken{}; 119 mResolver(GeolocationPermissionStatus::Error); 120 } 121 }); 122 123 mAppCapability = GetWifiControlAppCapability(); 124 if (!mAppCapability) { 125 return; 126 } 127 128 using AccessChangedHandler = 129 ITypedEventHandler<AppCapability*, 130 AppCapabilityAccessChangedEventArgs*>; 131 132 // Note: This creates the callback that listens for location permission 133 // changes as free threaded, which we need to do to overcome a (Microsoft) 134 // issue with the callback proxy's exports with (at least) the pre-24H2 135 // versions of Windows. 136 ComPtr<AccessChangedHandler> acHandlerRef = Callback<Implements< 137 RuntimeClassFlags<ClassicCom>, AccessChangedHandler, FtmBase>>( 138 [weakSelf = ThreadSafeWeakPtr<WindowsGeolocationPermissionRequest>( 139 this)](IAppCapability*, IAppCapabilityAccessChangedEventArgs*) { 140 // Because of the free threaded access mentioned above, our 141 // callback can run on a background thread, so dispatch it to 142 // main. 143 if (!NS_IsMainThread()) { 144 NS_DispatchToMainThread( 145 NS_NewRunnableFunction(__func__, [weakSelf]() { 146 RefPtr<WindowsGeolocationPermissionRequest> self(weakSelf); 147 if (self) { 148 self->StopIfLocationIsPermitted(); 149 } 150 })); 151 return S_OK; 152 } 153 154 RefPtr<WindowsGeolocationPermissionRequest> self(weakSelf); 155 if (self) { 156 self->StopIfLocationIsPermitted(); 157 } 158 return S_OK; 159 }); 160 161 if (!acHandlerRef) { 162 return; 163 } 164 165 HRESULT hr = mAppCapability->add_AccessChanged(acHandlerRef.Get(), &mToken); 166 NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); 167 MOZ_ASSERT(mToken.value); 168 169 mIsRunning = true; 170 failedToWatch.release(); 171 172 // Avoid a race for accessChanged by checking it now and stopping if 173 // permission is already granted. 174 StopIfLocationIsPermitted(); 175 } 176 177 protected: 178 void Stop(Maybe<AppCapabilityAccessStatus> aWifiAccess) { 179 MOZ_ASSERT(NS_IsMainThread()); 180 if (!mIsRunning) { 181 return; 182 } 183 mIsRunning = false; 184 185 if (LocationIsPermittedHint(aWifiAccess)) { 186 mResolver(GeolocationPermissionStatus::Granted); 187 } else { 188 mResolver(GeolocationPermissionStatus::Canceled); 189 } 190 191 // Remove the modal with the cancel button. 192 nsresult rv = DismissPrompt(); 193 NS_ENSURE_SUCCESS_VOID(rv); 194 195 // Stop watching location settings. 196 MOZ_ASSERT(mAppCapability); 197 mAppCapability->remove_AccessChanged(mToken); 198 mAppCapability = nullptr; 199 mToken = EventRegistrationToken{}; 200 } 201 202 public: 203 void Stop() override { 204 MOZ_ASSERT(NS_IsMainThread()); 205 if (!mIsRunning) { 206 return; 207 } 208 Stop(GetWifiControlAccess(mAppCapability)); 209 } 210 211 bool IsStopped() { return !mIsRunning; } 212 213 protected: 214 // Ref-counting demands that the destructor be non-public but 215 // SupportsThreadSafeWeakPtr needs to be able to call it, because we use 216 // NS_INLINE_DECL_REFCOUNTING_INHERITED. 217 friend SupportsThreadSafeWeakPtr<WindowsGeolocationPermissionRequest>; 218 219 virtual ~WindowsGeolocationPermissionRequest() { 220 Stop(); 221 MOZ_ASSERT(mToken.value == 0); 222 } 223 224 void StopIfLocationIsPermitted() { 225 auto wifiAccess = GetWifiControlAccess(mAppCapability); 226 if (LocationIsPermittedHint(wifiAccess)) { 227 Stop(wifiAccess); 228 } 229 } 230 231 nsresult DismissPrompt() { 232 nsresult rv; 233 nsCOMPtr<nsIGeolocationUIUtilsWin> utils = 234 do_GetService("@mozilla.org/geolocation/ui-utils-win;1", &rv); 235 NS_ENSURE_SUCCESS(rv, rv); 236 return utils->DismissPrompts(mBrowsingContext); 237 } 238 239 // This IAppCapability object must be held for the duration of the period 240 // where we listen for location permission changes or else the callback 241 // will not be called. 242 RefPtr<IAppCapability> mAppCapability; 243 ParentRequestResolver mResolver; 244 RefPtr<BrowsingContext> mBrowsingContext; 245 EventRegistrationToken mToken; 246 bool mIsRunning = false; 247 }; 248 249 // Opens Windows geolocation settings and cancels the geolocation request on 250 // error. 251 void OpenWindowsLocationSettings( 252 SystemGeolocationPermissionRequest* aPermissionRequest) { 253 auto cancelRequest = MakeScopeExit([&]() { aPermissionRequest->Stop(); }); 254 255 ComPtr<IUriRuntimeClassFactory> uriFactory = 256 CreateFromActivationFactory<IUriRuntimeClassFactory>(kUriGuid); 257 NS_ENSURE_TRUE_VOID(uriFactory); 258 259 RefPtr<IUriRuntimeClass> uri; 260 HRESULT hr = uriFactory->CreateUri( 261 HStringReference(kLocationSettingsPage).Get(), getter_AddRefs(uri)); 262 NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); 263 264 ComPtr<ILauncherStatics> launcherStatics = 265 CreateFromActivationFactory<ILauncherStatics>(kLauncherGuid); 266 NS_ENSURE_TRUE_VOID(launcherStatics); 267 268 RefPtr<IAsyncOperation<bool>> handler; 269 hr = launcherStatics->LaunchUriAsync(uri, getter_AddRefs(handler)); 270 NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); 271 272 // The IAsyncOperation is similar to a promise so there is no race here, 273 // despite us adding this callback after requesting launch instead of before. 274 handler->put_Completed( 275 Callback<IAsyncOperationCompletedHandler<bool>>( 276 [permissionRequest = RefPtr{aPermissionRequest}]( 277 IAsyncOperation<bool>* asyncInfo, AsyncStatus status) { 278 unsigned char verdict = 0; 279 asyncInfo->GetResults(&verdict); 280 if (!verdict) { 281 permissionRequest->Stop(); 282 } 283 return S_OK; 284 }) 285 .Get()); 286 287 cancelRequest.release(); 288 } 289 290 class LocationPermissionWifiScanListener final : public nsIWifiListener { 291 public: 292 NS_DECL_ISUPPORTS 293 294 explicit LocationPermissionWifiScanListener( 295 SystemGeolocationPermissionRequest* aRequest) 296 : mRequest(aRequest) {} 297 298 NS_IMETHOD OnChange(const nsTArray<RefPtr<nsIWifiAccessPoint>>&) override { 299 // We will remove ourselves from the nsIWifiMonitor, which is our owner. 300 // Hold a strong reference to ourselves until we complete the callback. 301 RefPtr<LocationPermissionWifiScanListener> self = this; 302 return PermissionWasDecided(); 303 } 304 305 NS_IMETHOD OnError(nsresult) override { 306 // We will remove ourselves from the nsIWifiMonitor, which is our owner. 307 // Hold a strong reference to ourselves until we complete the callback. 308 RefPtr<LocationPermissionWifiScanListener> self = this; 309 return PermissionWasDecided(); 310 } 311 312 private: 313 virtual ~LocationPermissionWifiScanListener() = default; 314 RefPtr<SystemGeolocationPermissionRequest> mRequest; 315 316 // Any response to our wifi scan request means that the user has selected 317 // either to grant or deny permission in the Windows dialog. Either way, we 318 // are done asking for permission, so Stop the permission request. 319 nsresult PermissionWasDecided() { 320 nsCOMPtr<nsIWifiMonitor> wifiMonitor = components::WifiMonitor::Service(); 321 NS_ENSURE_TRUE(wifiMonitor, NS_ERROR_FAILURE); 322 wifiMonitor->StopWatching(this); 323 mRequest->Stop(); 324 return NS_OK; 325 } 326 }; 327 328 NS_IMPL_ISUPPORTS(LocationPermissionWifiScanListener, nsIWifiListener) 329 330 } // namespace 331 332 //----------------------------------------------------------------------------- 333 334 SystemGeolocationPermissionBehavior GetGeolocationPermissionBehavior() { 335 auto wifiAccess = GetWifiControlAccess(); 336 if (SystemWillPromptForPermissionHint(wifiAccess)) { 337 return SystemGeolocationPermissionBehavior::SystemWillPromptUser; 338 } 339 if (!LocationIsPermittedHint(wifiAccess)) { 340 return SystemGeolocationPermissionBehavior::GeckoWillPromptUser; 341 } 342 return SystemGeolocationPermissionBehavior::NoPrompt; 343 } 344 345 already_AddRefed<SystemGeolocationPermissionRequest> 346 RequestLocationPermissionFromUser(BrowsingContext* aBrowsingContext, 347 ParentRequestResolver&& aResolver) { 348 RefPtr<WindowsGeolocationPermissionRequest> permissionRequest = 349 new WindowsGeolocationPermissionRequest(aBrowsingContext, 350 std::move(aResolver)); 351 permissionRequest->Initialize(); 352 if (permissionRequest->IsStopped()) { 353 return nullptr; 354 } 355 if (SystemWillPromptForPermissionHint(GetWifiControlAccess())) { 356 // To tell the system to prompt for permission, run one wifi scan (no need 357 // to poll). We won't use the result -- either the user will grant 358 // geolocation permission, meaning we will not need wifi scanning, or the 359 // user will deny permission, in which case no scan can be done. We just 360 // want the prompt. 361 nsCOMPtr<nsIWifiMonitor> wifiMonitor = components::WifiMonitor::Service(); 362 NS_ENSURE_TRUE(wifiMonitor, nullptr); 363 auto listener = 364 MakeRefPtr<LocationPermissionWifiScanListener>(permissionRequest); 365 wifiMonitor->StartWatching(listener, false); 366 } else { 367 OpenWindowsLocationSettings(permissionRequest); 368 } 369 return permissionRequest.forget(); 370 } 371 372 } // namespace mozilla::dom::geolocation