diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/geolocation/Geolocation.cpp | |
parent | Initial commit. (diff) | |
download | firefox-upstream/124.0.1.tar.xz firefox-upstream/124.0.1.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/geolocation/Geolocation.cpp')
-rw-r--r-- | dom/geolocation/Geolocation.cpp | 1221 |
1 files changed, 1221 insertions, 0 deletions
diff --git a/dom/geolocation/Geolocation.cpp b/dom/geolocation/Geolocation.cpp new file mode 100644 index 0000000000..cb9107deb1 --- /dev/null +++ b/dom/geolocation/Geolocation.cpp @@ -0,0 +1,1221 @@ +/* -*- 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 "Geolocation.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/CycleCollectedJSContext.h" // for nsAutoMicroTask +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/dom/GeolocationPositionError.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_geo.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/EventStateManager.h" +#include "nsComponentManagerUtils.h" +#include "nsContentPermissionHelper.h" +#include "nsContentUtils.h" +#include "nsGlobalWindowInner.h" +#include "mozilla/dom/Document.h" +#include "nsINamed.h" +#include "nsIObserverService.h" +#include "nsIScriptError.h" +#include "nsPIDOMWindow.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +class nsIPrincipal; + +#ifdef MOZ_WIDGET_ANDROID +# include "AndroidLocationProvider.h" +#endif + +#ifdef MOZ_GPSD +# include "GpsdLocationProvider.h" +#endif + +#ifdef MOZ_ENABLE_DBUS +# include "mozilla/WidgetUtilsGtk.h" +# include "GeoclueLocationProvider.h" +# include "PortalLocationProvider.h" +#endif + +#ifdef MOZ_WIDGET_COCOA +# include "CoreLocationLocationProvider.h" +#endif + +#ifdef XP_WIN +# include "WindowsLocationProvider.h" +#endif + +// Some limit to the number of get or watch geolocation requests +// that a window can make. +#define MAX_GEO_REQUESTS_PER_WINDOW 1500 + +// This preference allows to override the "secure context" by +// default policy. +#define PREF_GEO_SECURITY_ALLOWINSECURE "geo.security.allowinsecure" + +using mozilla::Unused; // <snicker> +using namespace mozilla; +using namespace mozilla::dom; + +class nsGeolocationRequest final : public ContentPermissionRequestBase, + public nsIGeolocationUpdate, + public SupportsWeakPtr { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIGEOLOCATIONUPDATE + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsGeolocationRequest, + ContentPermissionRequestBase) + + nsGeolocationRequest(Geolocation* aLocator, GeoPositionCallback aCallback, + GeoPositionErrorCallback aErrorCallback, + UniquePtr<PositionOptions>&& aOptions, + nsIEventTarget* aMainThreadSerialEventTarget, + bool aWatchPositionRequest = false, + int32_t aWatchId = 0); + + // nsIContentPermissionRequest + MOZ_CAN_RUN_SCRIPT NS_IMETHOD Cancel(void) override; + MOZ_CAN_RUN_SCRIPT NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override; + + void Shutdown(); + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY is OK here because we're always called from a + // runnable. Ideally nsIRunnable::Run and its overloads would just be + // MOZ_CAN_RUN_SCRIPT and then we could be too... + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void SendLocation(nsIDOMGeoPosition* aLocation); + bool WantsHighAccuracy() { + return !mShutdown && mOptions && mOptions->mEnableHighAccuracy; + } + void SetTimeoutTimer(); + void StopTimeoutTimer(); + MOZ_CAN_RUN_SCRIPT + void NotifyErrorAndShutdown(uint16_t); + using ContentPermissionRequestBase::GetPrincipal; + nsIPrincipal* GetPrincipal(); + + bool IsWatch() { return mIsWatchPositionRequest; } + int32_t WatchId() { return mWatchId; } + + private: + virtual ~nsGeolocationRequest(); + + class TimerCallbackHolder final : public nsITimerCallback, public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + + explicit TimerCallbackHolder(nsGeolocationRequest* aRequest) + : mRequest(aRequest) {} + + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("nsGeolocationRequest::TimerCallbackHolder"); + return NS_OK; + } + + private: + ~TimerCallbackHolder() = default; + WeakPtr<nsGeolocationRequest> mRequest; + }; + + // Only called from a timer, so MOZ_CAN_RUN_SCRIPT_BOUNDARY ok for now. + MOZ_CAN_RUN_SCRIPT_BOUNDARY void Notify(); + + bool mIsWatchPositionRequest; + + nsCOMPtr<nsITimer> mTimeoutTimer; + GeoPositionCallback mCallback; + GeoPositionErrorCallback mErrorCallback; + UniquePtr<PositionOptions> mOptions; + + RefPtr<Geolocation> mLocator; + + int32_t mWatchId; + bool mShutdown; + nsCOMPtr<nsIEventTarget> mMainThreadSerialEventTarget; +}; + +static UniquePtr<PositionOptions> CreatePositionOptionsCopy( + const PositionOptions& aOptions) { + UniquePtr<PositionOptions> geoOptions = MakeUnique<PositionOptions>(); + + geoOptions->mEnableHighAccuracy = aOptions.mEnableHighAccuracy; + geoOptions->mMaximumAge = aOptions.mMaximumAge; + geoOptions->mTimeout = aOptions.mTimeout; + + return geoOptions; +} + +class RequestSendLocationEvent : public Runnable { + public: + RequestSendLocationEvent(nsIDOMGeoPosition* aPosition, + nsGeolocationRequest* aRequest) + : mozilla::Runnable("RequestSendLocationEvent"), + mPosition(aPosition), + mRequest(aRequest) {} + + NS_IMETHOD Run() override { + mRequest->SendLocation(mPosition); + return NS_OK; + } + + private: + nsCOMPtr<nsIDOMGeoPosition> mPosition; + RefPtr<nsGeolocationRequest> mRequest; + RefPtr<Geolocation> mLocator; +}; + +//////////////////////////////////////////////////// +// nsGeolocationRequest +//////////////////////////////////////////////////// + +static nsPIDOMWindowInner* ConvertWeakReferenceToWindow( + nsIWeakReference* aWeakPtr) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(aWeakPtr); + // This isn't usually safe, but here we're just extracting a raw pointer in + // order to pass it to a base class constructor which will in turn convert it + // into a strong pointer for us. + nsPIDOMWindowInner* raw = window.get(); + return raw; +} + +nsGeolocationRequest::nsGeolocationRequest( + Geolocation* aLocator, GeoPositionCallback aCallback, + GeoPositionErrorCallback aErrorCallback, + UniquePtr<PositionOptions>&& aOptions, + nsIEventTarget* aMainThreadSerialEventTarget, bool aWatchPositionRequest, + int32_t aWatchId) + : ContentPermissionRequestBase( + aLocator->GetPrincipal(), + ConvertWeakReferenceToWindow(aLocator->GetOwner()), "geo"_ns, + "geolocation"_ns), + mIsWatchPositionRequest(aWatchPositionRequest), + mCallback(std::move(aCallback)), + mErrorCallback(std::move(aErrorCallback)), + mOptions(std::move(aOptions)), + mLocator(aLocator), + mWatchId(aWatchId), + mShutdown(false), + mMainThreadSerialEventTarget(aMainThreadSerialEventTarget) {} + +nsGeolocationRequest::~nsGeolocationRequest() { StopTimeoutTimer(); } + +NS_IMPL_QUERY_INTERFACE_CYCLE_COLLECTION_INHERITED(nsGeolocationRequest, + ContentPermissionRequestBase, + nsIGeolocationUpdate) + +NS_IMPL_ADDREF_INHERITED(nsGeolocationRequest, ContentPermissionRequestBase) +NS_IMPL_RELEASE_INHERITED(nsGeolocationRequest, ContentPermissionRequestBase) +NS_IMPL_CYCLE_COLLECTION_WEAK_PTR_INHERITED(nsGeolocationRequest, + ContentPermissionRequestBase, + mCallback, mErrorCallback, mLocator) + +void nsGeolocationRequest::Notify() { + SetTimeoutTimer(); + NotifyErrorAndShutdown(GeolocationPositionError_Binding::TIMEOUT); +} + +void nsGeolocationRequest::NotifyErrorAndShutdown(uint16_t aErrorCode) { + MOZ_ASSERT(!mShutdown, "timeout after shutdown"); + if (!mIsWatchPositionRequest) { + Shutdown(); + mLocator->RemoveRequest(this); + } + + NotifyError(aErrorCode); +} + +NS_IMETHODIMP +nsGeolocationRequest::Cancel() { + if (mLocator->ClearPendingRequest(this)) { + return NS_OK; + } + + NotifyError(GeolocationPositionError_Binding::PERMISSION_DENIED); + return NS_OK; +} + +NS_IMETHODIMP +nsGeolocationRequest::Allow(JS::Handle<JS::Value> aChoices) { + MOZ_ASSERT(aChoices.isUndefined()); + + if (mLocator->ClearPendingRequest(this)) { + return NS_OK; + } + + RefPtr<nsGeolocationService> gs = + nsGeolocationService::GetGeolocationService(); + + bool canUseCache = false; + CachedPositionAndAccuracy lastPosition = gs->GetCachedPosition(); + if (lastPosition.position) { + EpochTimeStamp cachedPositionTime_ms; + lastPosition.position->GetTimestamp(&cachedPositionTime_ms); + // check to see if we can use a cached value + // if the user has specified a maximumAge, return a cached value. + if (mOptions && mOptions->mMaximumAge > 0) { + uint32_t maximumAge_ms = mOptions->mMaximumAge; + bool isCachedWithinRequestedAccuracy = + WantsHighAccuracy() <= lastPosition.isHighAccuracy; + bool isCachedWithinRequestedTime = + EpochTimeStamp(PR_Now() / PR_USEC_PER_MSEC - maximumAge_ms) <= + cachedPositionTime_ms; + canUseCache = + isCachedWithinRequestedAccuracy && isCachedWithinRequestedTime; + } + } + + gs->UpdateAccuracy(WantsHighAccuracy()); + if (canUseCache) { + // okay, we can return a cached position + // getCurrentPosition requests serviced by the cache + // will now be owned by the RequestSendLocationEvent + Update(lastPosition.position); + + // After Update is called, getCurrentPosition finishes it's job. + if (!mIsWatchPositionRequest) { + return NS_OK; + } + + } else { + // if it is not a watch request and timeout is 0, + // invoke the errorCallback (if present) with TIMEOUT code + if (mOptions && mOptions->mTimeout == 0 && !mIsWatchPositionRequest) { + NotifyError(GeolocationPositionError_Binding::TIMEOUT); + return NS_OK; + } + } + + // Non-cached location request + bool allowedRequest = mIsWatchPositionRequest || !canUseCache; + if (allowedRequest) { + // let the locator know we're pending + // we will now be owned by the locator + mLocator->NotifyAllowedRequest(this); + } + + // Kick off the geo device, if it isn't already running + nsresult rv = gs->StartDevice(); + + if (NS_FAILED(rv)) { + if (allowedRequest) { + mLocator->RemoveRequest(this); + } + // Location provider error + NotifyError(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return NS_OK; + } + + SetTimeoutTimer(); + + return NS_OK; +} + +void nsGeolocationRequest::SetTimeoutTimer() { + MOZ_ASSERT(!mShutdown, "set timeout after shutdown"); + + StopTimeoutTimer(); + + if (mOptions && mOptions->mTimeout != 0 && mOptions->mTimeout != 0x7fffffff) { + RefPtr<TimerCallbackHolder> holder = new TimerCallbackHolder(this); + NS_NewTimerWithCallback(getter_AddRefs(mTimeoutTimer), holder, + mOptions->mTimeout, nsITimer::TYPE_ONE_SHOT); + } +} + +void nsGeolocationRequest::StopTimeoutTimer() { + if (mTimeoutTimer) { + mTimeoutTimer->Cancel(); + mTimeoutTimer = nullptr; + } +} + +void nsGeolocationRequest::SendLocation(nsIDOMGeoPosition* aPosition) { + if (mShutdown) { + // Ignore SendLocationEvents issued before we were cleared. + return; + } + + if (mOptions && mOptions->mMaximumAge > 0) { + EpochTimeStamp positionTime_ms; + aPosition->GetTimestamp(&positionTime_ms); + const uint32_t maximumAge_ms = mOptions->mMaximumAge; + const bool isTooOld = EpochTimeStamp(PR_Now() / PR_USEC_PER_MSEC - + maximumAge_ms) > positionTime_ms; + if (isTooOld) { + return; + } + } + + RefPtr<mozilla::dom::GeolocationPosition> wrapped; + + if (aPosition) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (coords) { + wrapped = new mozilla::dom::GeolocationPosition(ToSupports(mLocator), + aPosition); + } + } + + if (!wrapped) { + NotifyError(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return; + } + + if (!mIsWatchPositionRequest) { + // Cancel timer and position updates in case the position + // callback spins the event loop + Shutdown(); + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + obs->NotifyObservers(wrapped, "geolocation-position-events", + u"location-updated"); + + nsAutoMicroTask mt; + if (mCallback.HasWebIDLCallback()) { + RefPtr<PositionCallback> callback = mCallback.GetWebIDLCallback(); + + MOZ_ASSERT(callback); + callback->Call(*wrapped); + } else { + nsIDOMGeoPositionCallback* callback = mCallback.GetXPCOMCallback(); + MOZ_ASSERT(callback); + callback->HandleEvent(aPosition); + } + + if (mIsWatchPositionRequest && !mShutdown) { + SetTimeoutTimer(); + } + MOZ_ASSERT(mShutdown || mIsWatchPositionRequest, + "non-shutdown getCurrentPosition request after callback!"); +} + +nsIPrincipal* nsGeolocationRequest::GetPrincipal() { + if (!mLocator) { + return nullptr; + } + return mLocator->GetPrincipal(); +} + +NS_IMETHODIMP +nsGeolocationRequest::Update(nsIDOMGeoPosition* aPosition) { + nsCOMPtr<nsIRunnable> ev = new RequestSendLocationEvent(aPosition, this); + mMainThreadSerialEventTarget->Dispatch(ev.forget()); + return NS_OK; +} + +NS_IMETHODIMP +nsGeolocationRequest::NotifyError(uint16_t aErrorCode) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<GeolocationPositionError> positionError = + new GeolocationPositionError(mLocator, aErrorCode); + positionError->NotifyCallback(mErrorCallback); + return NS_OK; +} + +void nsGeolocationRequest::Shutdown() { + MOZ_ASSERT(!mShutdown, "request shutdown twice"); + mShutdown = true; + + StopTimeoutTimer(); + + // If there are no other high accuracy requests, the geolocation service will + // notify the provider to switch to the default accuracy. + if (mOptions && mOptions->mEnableHighAccuracy) { + RefPtr<nsGeolocationService> gs = + nsGeolocationService::GetGeolocationService(); + if (gs) { + gs->UpdateAccuracy(); + } + } +} + +//////////////////////////////////////////////////// +// nsGeolocationRequest::TimerCallbackHolder +//////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsGeolocationRequest::TimerCallbackHolder, nsITimerCallback, + nsINamed) + +NS_IMETHODIMP +nsGeolocationRequest::TimerCallbackHolder::Notify(nsITimer*) { + if (mRequest && mRequest->mLocator) { + RefPtr<nsGeolocationRequest> request(mRequest); + request->Notify(); + } + + return NS_OK; +} + +//////////////////////////////////////////////////// +// nsGeolocationService +//////////////////////////////////////////////////// +NS_INTERFACE_MAP_BEGIN(nsGeolocationService) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIGeolocationUpdate) + NS_INTERFACE_MAP_ENTRY(nsIGeolocationUpdate) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(nsGeolocationService) +NS_IMPL_RELEASE(nsGeolocationService) + +nsresult nsGeolocationService::Init() { + if (!StaticPrefs::geo_enabled()) { + return NS_ERROR_FAILURE; + } + + if (XRE_IsContentProcess()) { + return NS_OK; + } + + // geolocation service can be enabled -> now register observer + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (!obs) { + return NS_ERROR_FAILURE; + } + + obs->AddObserver(this, "xpcom-shutdown", false); + +#ifdef MOZ_WIDGET_ANDROID + mProvider = new AndroidLocationProvider(); +#endif + +#ifdef MOZ_WIDGET_GTK +# ifdef MOZ_ENABLE_DBUS + if (!mProvider && widget::ShouldUsePortal(widget::PortalKind::Location)) { + mProvider = new PortalLocationProvider(); + } + // Geoclue includes GPS data so it has higher priority than raw GPSD + if (!mProvider && StaticPrefs::geo_provider_use_geoclue()) { + nsCOMPtr<nsIGeolocationProvider> gcProvider = new GeoclueLocationProvider(); + // The Startup() method will only succeed if Geoclue is available on D-Bus + if (NS_SUCCEEDED(gcProvider->Startup())) { + gcProvider->Shutdown(); + mProvider = std::move(gcProvider); + } + } +# ifdef MOZ_GPSD + if (!mProvider && Preferences::GetBool("geo.provider.use_gpsd", false)) { + mProvider = new GpsdLocationProvider(); + } +# endif +# endif +#endif + +#ifdef MOZ_WIDGET_COCOA + if (Preferences::GetBool("geo.provider.use_corelocation", true)) { + mProvider = new CoreLocationLocationProvider(); + } +#endif + +#ifdef XP_WIN + if (Preferences::GetBool("geo.provider.ms-windows-location", false)) { + mProvider = new WindowsLocationProvider(); + } +#endif + + if (Preferences::GetBool("geo.provider.use_mls", false)) { + mProvider = do_CreateInstance("@mozilla.org/geolocation/mls-provider;1"); + } + + // Override platform-specific providers with the default (network) + // provider while testing. Our tests are currently not meant to exercise + // the provider, and some tests rely on the network provider being used. + // "geo.provider.testing" is always set for all plain and browser chrome + // mochitests, and also for xpcshell tests. + if (!mProvider || Preferences::GetBool("geo.provider.testing", false)) { + nsCOMPtr<nsIGeolocationProvider> geoTestProvider = + do_GetService(NS_GEOLOCATION_PROVIDER_CONTRACTID); + + if (geoTestProvider) { + mProvider = geoTestProvider; + } + } + + return NS_OK; +} + +nsGeolocationService::~nsGeolocationService() = default; + +NS_IMETHODIMP +nsGeolocationService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp("xpcom-shutdown", aTopic)) { + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, "xpcom-shutdown"); + } + + for (uint32_t i = 0; i < mGeolocators.Length(); i++) { + mGeolocators[i]->Shutdown(); + } + StopDevice(); + + return NS_OK; + } + + if (!strcmp("timer-callback", aTopic)) { + // decide if we can close down the service. + for (uint32_t i = 0; i < mGeolocators.Length(); i++) + if (mGeolocators[i]->HasActiveCallbacks()) { + SetDisconnectTimer(); + return NS_OK; + } + + // okay to close up. + StopDevice(); + Update(nullptr); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsGeolocationService::Update(nsIDOMGeoPosition* aSomewhere) { + if (aSomewhere) { + SetCachedPosition(aSomewhere); + } + + for (uint32_t i = 0; i < mGeolocators.Length(); i++) { + mGeolocators[i]->Update(aSomewhere); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsGeolocationService::NotifyError(uint16_t aErrorCode) { + // nsTArray doesn't have a constructors that takes a different-type TArray. + nsTArray<RefPtr<Geolocation>> geolocators; + geolocators.AppendElements(mGeolocators); + for (uint32_t i = 0; i < geolocators.Length(); i++) { + // MOZ_KnownLive because the stack array above keeps it alive. + MOZ_KnownLive(geolocators[i])->NotifyError(aErrorCode); + } + return NS_OK; +} + +void nsGeolocationService::SetCachedPosition(nsIDOMGeoPosition* aPosition) { + mLastPosition.position = aPosition; + mLastPosition.isHighAccuracy = mHigherAccuracy; +} + +CachedPositionAndAccuracy nsGeolocationService::GetCachedPosition() { + return mLastPosition; +} + +nsresult nsGeolocationService::StartDevice() { + if (!StaticPrefs::geo_enabled()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // We do not want to keep the geolocation devices online + // indefinitely. + // Close them down after a reasonable period of inactivivity. + SetDisconnectTimer(); + + if (XRE_IsContentProcess()) { + ContentChild* cpc = ContentChild::GetSingleton(); + cpc->SendAddGeolocationListener(HighAccuracyRequested()); + return NS_OK; + } + + // Start them up! + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (!obs) { + return NS_ERROR_FAILURE; + } + + if (!mProvider) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + + if (NS_FAILED(rv = mProvider->Startup()) || + NS_FAILED(rv = mProvider->Watch(this))) { + NotifyError(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return rv; + } + + obs->NotifyObservers(mProvider, "geolocation-device-events", u"starting"); + + return NS_OK; +} + +void nsGeolocationService::SetDisconnectTimer() { + if (!mDisconnectTimer) { + mDisconnectTimer = NS_NewTimer(); + } else { + mDisconnectTimer->Cancel(); + } + + mDisconnectTimer->Init(this, StaticPrefs::geo_timeout(), + nsITimer::TYPE_ONE_SHOT); +} + +bool nsGeolocationService::HighAccuracyRequested() { + for (uint32_t i = 0; i < mGeolocators.Length(); i++) { + if (mGeolocators[i]->HighAccuracyRequested()) { + return true; + } + } + + return false; +} + +void nsGeolocationService::UpdateAccuracy(bool aForceHigh) { + bool highRequired = aForceHigh || HighAccuracyRequested(); + + if (XRE_IsContentProcess()) { + ContentChild* cpc = ContentChild::GetSingleton(); + if (cpc->IsAlive()) { + cpc->SendSetGeolocationHigherAccuracy(highRequired); + } + + return; + } + + mProvider->SetHighAccuracy(!mHigherAccuracy && highRequired); + mHigherAccuracy = highRequired; +} + +void nsGeolocationService::StopDevice() { + if (mDisconnectTimer) { + mDisconnectTimer->Cancel(); + mDisconnectTimer = nullptr; + } + + if (XRE_IsContentProcess()) { + ContentChild* cpc = ContentChild::GetSingleton(); + cpc->SendRemoveGeolocationListener(); + + return; // bail early + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (!obs) { + return; + } + + if (!mProvider) { + return; + } + + mHigherAccuracy = false; + + mProvider->Shutdown(); + obs->NotifyObservers(mProvider, "geolocation-device-events", u"shutdown"); +} + +StaticRefPtr<nsGeolocationService> nsGeolocationService::sService; + +already_AddRefed<nsGeolocationService> +nsGeolocationService::GetGeolocationService() { + RefPtr<nsGeolocationService> result; + if (nsGeolocationService::sService) { + result = nsGeolocationService::sService; + + return result.forget(); + } + + result = new nsGeolocationService(); + if (NS_FAILED(result->Init())) { + return nullptr; + } + + ClearOnShutdown(&nsGeolocationService::sService); + nsGeolocationService::sService = result; + return result.forget(); +} + +void nsGeolocationService::AddLocator(Geolocation* aLocator) { + mGeolocators.AppendElement(aLocator); +} + +void nsGeolocationService::RemoveLocator(Geolocation* aLocator) { + mGeolocators.RemoveElement(aLocator); +} + +//////////////////////////////////////////////////// +// Geolocation +//////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Geolocation) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIGeolocationUpdate) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Geolocation) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Geolocation) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Geolocation, mPendingCallbacks, + mWatchingCallbacks, mPendingRequests) + +Geolocation::Geolocation() + : mProtocolType(ProtocolType::OTHER), mLastWatchId(1) {} + +Geolocation::~Geolocation() { + if (mService) { + Shutdown(); + } +} + +StaticRefPtr<Geolocation> Geolocation::sNonWindowSingleton; + +already_AddRefed<Geolocation> Geolocation::NonWindowSingleton() { + if (sNonWindowSingleton) { + return do_AddRef(sNonWindowSingleton); + } + + RefPtr<Geolocation> result = new Geolocation(); + DebugOnly<nsresult> rv = result->Init(); + MOZ_ASSERT(NS_SUCCEEDED(rv), "How can this fail?"); + + ClearOnShutdown(&sNonWindowSingleton); + sNonWindowSingleton = result; + return result.forget(); +} + +nsresult Geolocation::Init(nsPIDOMWindowInner* aContentDom) { + // Remember the window + if (aContentDom) { + mOwner = do_GetWeakReference(aContentDom); + if (!mOwner) { + return NS_ERROR_FAILURE; + } + + // Grab the principal of the document + nsCOMPtr<Document> doc = aContentDom->GetDoc(); + if (!doc) { + return NS_ERROR_FAILURE; + } + + mPrincipal = doc->NodePrincipal(); + // Store the protocol to send via telemetry later. + if (mPrincipal->SchemeIs("http")) { + mProtocolType = ProtocolType::HTTP; + } else if (mPrincipal->SchemeIs("https")) { + mProtocolType = ProtocolType::HTTPS; + } + } + + // If no aContentDom was passed into us, we are being used + // by chrome/c++ and have no mOwner, no mPrincipal, and no need + // to prompt. + mService = nsGeolocationService::GetGeolocationService(); + if (mService) { + mService->AddLocator(this); + } + + return NS_OK; +} + +void Geolocation::Shutdown() { + // Release all callbacks + mPendingCallbacks.Clear(); + mWatchingCallbacks.Clear(); + + if (mService) { + mService->RemoveLocator(this); + mService->UpdateAccuracy(); + } + + mService = nullptr; + mPrincipal = nullptr; +} + +nsPIDOMWindowInner* Geolocation::GetParentObject() const { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mOwner); + return window.get(); +} + +bool Geolocation::HasActiveCallbacks() { + return mPendingCallbacks.Length() || mWatchingCallbacks.Length(); +} + +bool Geolocation::HighAccuracyRequested() { + for (uint32_t i = 0; i < mWatchingCallbacks.Length(); i++) { + if (mWatchingCallbacks[i]->WantsHighAccuracy()) { + return true; + } + } + + for (uint32_t i = 0; i < mPendingCallbacks.Length(); i++) { + if (mPendingCallbacks[i]->WantsHighAccuracy()) { + return true; + } + } + + return false; +} + +void Geolocation::RemoveRequest(nsGeolocationRequest* aRequest) { + bool requestWasKnown = (mPendingCallbacks.RemoveElement(aRequest) != + mWatchingCallbacks.RemoveElement(aRequest)); + + Unused << requestWasKnown; +} + +NS_IMETHODIMP +Geolocation::Update(nsIDOMGeoPosition* aSomewhere) { + if (!WindowOwnerStillExists()) { + Shutdown(); + return NS_OK; + } + + // Don't update position if window is not fully active or the document is + // hidden. We keep the pending callaback and watchers waiting for the next + // update. + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(this->GetOwner()); + if (window) { + nsCOMPtr<Document> document = window->GetDoc(); + bool isHidden = document && document->Hidden(); + if (isHidden || !window->IsFullyActive()) { + return NS_OK; + } + } + + if (aSomewhere) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aSomewhere->GetCoords(getter_AddRefs(coords)); + if (coords) { + double accuracy = -1; + coords->GetAccuracy(&accuracy); + mozilla::Telemetry::Accumulate( + mozilla::Telemetry::GEOLOCATION_ACCURACY_EXPONENTIAL, accuracy); + } + } + + for (uint32_t i = mPendingCallbacks.Length(); i > 0; i--) { + mPendingCallbacks[i - 1]->Update(aSomewhere); + RemoveRequest(mPendingCallbacks[i - 1]); + } + + // notify everyone that is watching + for (uint32_t i = 0; i < mWatchingCallbacks.Length(); i++) { + mWatchingCallbacks[i]->Update(aSomewhere); + } + + return NS_OK; +} + +NS_IMETHODIMP +Geolocation::NotifyError(uint16_t aErrorCode) { + if (!WindowOwnerStillExists()) { + Shutdown(); + return NS_OK; + } + + mozilla::Telemetry::Accumulate(mozilla::Telemetry::GEOLOCATION_ERROR, true); + + for (uint32_t i = mPendingCallbacks.Length(); i > 0; i--) { + RefPtr<nsGeolocationRequest> request = mPendingCallbacks[i - 1]; + request->NotifyErrorAndShutdown(aErrorCode); + // NotifyErrorAndShutdown() removes the request from the array + } + + // notify everyone that is watching + for (uint32_t i = 0; i < mWatchingCallbacks.Length(); i++) { + RefPtr<nsGeolocationRequest> request = mWatchingCallbacks[i]; + request->NotifyErrorAndShutdown(aErrorCode); + } + + return NS_OK; +} + +bool Geolocation::IsFullyActiveOrChrome() { + // For regular content window, only allow this proceed if the window is "fully + // active". + if (nsPIDOMWindowInner* window = this->GetParentObject()) { + return window->IsFullyActive(); + } + // Calls coming from chrome code don't have window, so we can proceed. + return true; +} + +bool Geolocation::IsAlreadyCleared(nsGeolocationRequest* aRequest) { + for (uint32_t i = 0, length = mClearedWatchIDs.Length(); i < length; ++i) { + if (mClearedWatchIDs[i] == aRequest->WatchId()) { + return true; + } + } + + return false; +} + +bool Geolocation::ShouldBlockInsecureRequests() const { + if (Preferences::GetBool(PREF_GEO_SECURITY_ALLOWINSECURE, false)) { + return false; + } + + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryReferent(mOwner); + if (!win) { + return false; + } + + nsCOMPtr<Document> doc = win->GetDoc(); + if (!doc) { + return false; + } + + if (!nsGlobalWindowInner::Cast(win)->IsSecureContext()) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, "DOM"_ns, doc, + nsContentUtils::eDOM_PROPERTIES, + "GeolocationInsecureRequestIsForbidden"); + return true; + } + + return false; +} + +bool Geolocation::ClearPendingRequest(nsGeolocationRequest* aRequest) { + if (aRequest->IsWatch() && this->IsAlreadyCleared(aRequest)) { + this->NotifyAllowedRequest(aRequest); + this->ClearWatch(aRequest->WatchId()); + return true; + } + + return false; +} + +void Geolocation::GetCurrentPosition(PositionCallback& aCallback, + PositionErrorCallback* aErrorCallback, + const PositionOptions& aOptions, + CallerType aCallerType, ErrorResult& aRv) { + nsresult rv = GetCurrentPosition( + GeoPositionCallback(&aCallback), GeoPositionErrorCallback(aErrorCallback), + CreatePositionOptionsCopy(aOptions), aCallerType); + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +nsresult Geolocation::GetCurrentPosition(GeoPositionCallback callback, + GeoPositionErrorCallback errorCallback, + UniquePtr<PositionOptions>&& options, + CallerType aCallerType) { + if (!IsFullyActiveOrChrome()) { + RefPtr<GeolocationPositionError> positionError = + new GeolocationPositionError( + this, GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + positionError->NotifyCallback(errorCallback); + return NS_OK; + } + + if (mPendingCallbacks.Length() > MAX_GEO_REQUESTS_PER_WINDOW) { + return NS_ERROR_NOT_AVAILABLE; + } + + // After this we hand over ownership of options to our nsGeolocationRequest. + + nsIEventTarget* target = GetMainThreadSerialEventTarget(); + RefPtr<nsGeolocationRequest> request = new nsGeolocationRequest( + this, std::move(callback), std::move(errorCallback), std::move(options), + target); + + if (!StaticPrefs::geo_enabled() || ShouldBlockInsecureRequests() || + !request->CheckPermissionDelegate()) { + request->RequestDelayedTask(target, + nsGeolocationRequest::DelayedTaskType::Deny); + return NS_OK; + } + + if (!mOwner && aCallerType != CallerType::System) { + return NS_ERROR_FAILURE; + } + + if (mOwner) { + if (!RegisterRequestWithPrompt(request)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; + } + + if (aCallerType != CallerType::System) { + return NS_ERROR_FAILURE; + } + + request->RequestDelayedTask(target, + nsGeolocationRequest::DelayedTaskType::Allow); + + return NS_OK; +} + +int32_t Geolocation::WatchPosition(PositionCallback& aCallback, + PositionErrorCallback* aErrorCallback, + const PositionOptions& aOptions, + CallerType aCallerType, ErrorResult& aRv) { + return WatchPosition(GeoPositionCallback(&aCallback), + GeoPositionErrorCallback(aErrorCallback), + CreatePositionOptionsCopy(aOptions), aCallerType, aRv); +} + +int32_t Geolocation::WatchPosition( + nsIDOMGeoPositionCallback* aCallback, + nsIDOMGeoPositionErrorCallback* aErrorCallback, + UniquePtr<PositionOptions>&& aOptions) { + MOZ_ASSERT(aCallback); + + return WatchPosition(GeoPositionCallback(aCallback), + GeoPositionErrorCallback(aErrorCallback), + std::move(aOptions), CallerType::System, IgnoreErrors()); +} + +// On errors we return 0 because that's not a valid watch id and will +// get ignored in clearWatch. +int32_t Geolocation::WatchPosition(GeoPositionCallback aCallback, + GeoPositionErrorCallback aErrorCallback, + UniquePtr<PositionOptions>&& aOptions, + CallerType aCallerType, ErrorResult& aRv) { + if (!IsFullyActiveOrChrome()) { + RefPtr<GeolocationPositionError> positionError = + new GeolocationPositionError( + this, GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + positionError->NotifyCallback(aErrorCallback); + return 0; + } + + if (mWatchingCallbacks.Length() > MAX_GEO_REQUESTS_PER_WINDOW) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return 0; + } + + // The watch ID: + int32_t watchId = mLastWatchId++; + + nsIEventTarget* target = GetMainThreadSerialEventTarget(); + RefPtr<nsGeolocationRequest> request = new nsGeolocationRequest( + this, std::move(aCallback), std::move(aErrorCallback), + std::move(aOptions), target, true, watchId); + + if (!StaticPrefs::geo_enabled() || ShouldBlockInsecureRequests() || + !request->CheckPermissionDelegate()) { + request->RequestDelayedTask(target, + nsGeolocationRequest::DelayedTaskType::Deny); + return watchId; + } + + if (!mOwner && aCallerType != CallerType::System) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + if (mOwner) { + if (!RegisterRequestWithPrompt(request)) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return 0; + } + + return watchId; + } + + if (aCallerType != CallerType::System) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + request->Allow(JS::UndefinedHandleValue); + return watchId; +} + +void Geolocation::ClearWatch(int32_t aWatchId) { + if (aWatchId < 1) { + return; + } + + if (!mClearedWatchIDs.Contains(aWatchId)) { + mClearedWatchIDs.AppendElement(aWatchId); + } + + for (uint32_t i = 0, length = mWatchingCallbacks.Length(); i < length; ++i) { + if (mWatchingCallbacks[i]->WatchId() == aWatchId) { + mWatchingCallbacks[i]->Shutdown(); + RemoveRequest(mWatchingCallbacks[i]); + mClearedWatchIDs.RemoveElement(aWatchId); + break; + } + } + + // make sure we also search through the pending requests lists for + // watches to clear... + for (uint32_t i = 0, length = mPendingRequests.Length(); i < length; ++i) { + if (mPendingRequests[i]->IsWatch() && + (mPendingRequests[i]->WatchId() == aWatchId)) { + mPendingRequests[i]->Shutdown(); + mPendingRequests.RemoveElementAt(i); + break; + } + } +} + +bool Geolocation::WindowOwnerStillExists() { + // an owner was never set when Geolocation + // was created, which means that this object + // is being used without a window. + if (mOwner == nullptr) { + return true; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mOwner); + + if (window) { + nsPIDOMWindowOuter* outer = window->GetOuterWindow(); + if (!outer || outer->GetCurrentInnerWindow() != window || outer->Closed()) { + return false; + } + } + + return true; +} + +void Geolocation::NotifyAllowedRequest(nsGeolocationRequest* aRequest) { + if (aRequest->IsWatch()) { + mWatchingCallbacks.AppendElement(aRequest); + } else { + mPendingCallbacks.AppendElement(aRequest); + } +} + +bool Geolocation::RegisterRequestWithPrompt(nsGeolocationRequest* request) { + nsIEventTarget* target = GetMainThreadSerialEventTarget(); + ContentPermissionRequestBase::PromptResult pr = request->CheckPromptPrefs(); + if (pr == ContentPermissionRequestBase::PromptResult::Granted) { + request->RequestDelayedTask(target, + nsGeolocationRequest::DelayedTaskType::Allow); + return true; + } + if (pr == ContentPermissionRequestBase::PromptResult::Denied) { + request->RequestDelayedTask(target, + nsGeolocationRequest::DelayedTaskType::Deny); + return true; + } + + request->RequestDelayedTask(target, + nsGeolocationRequest::DelayedTaskType::Request); + return true; +} + +JSObject* Geolocation::WrapObject(JSContext* aCtx, + JS::Handle<JSObject*> aGivenProto) { + return Geolocation_Binding::Wrap(aCtx, this, aGivenProto); +} |