/* -*- 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 "nsGlobalWindow.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" # include "mozilla/WindowsVersion.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; // 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&& aOptions, nsIEventTarget* aMainThreadTarget, 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 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 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 mTimeoutTimer; GeoPositionCallback mCallback; GeoPositionErrorCallback mErrorCallback; UniquePtr mOptions; RefPtr mLocator; int32_t mWatchId; bool mShutdown; nsCOMPtr mMainThreadTarget; }; static UniquePtr CreatePositionOptionsCopy( const PositionOptions& aOptions) { UniquePtr geoOptions = MakeUnique(); 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 mPosition; RefPtr mRequest; RefPtr mLocator; }; //////////////////////////////////////////////////// // nsGeolocationRequest //////////////////////////////////////////////////// static nsPIDOMWindowInner* ConvertWeakReferenceToWindow( nsIWeakReference* aWeakPtr) { nsCOMPtr 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&& aOptions, nsIEventTarget* aMainThreadTarget, 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), mMainThreadTarget(aMainThreadTarget) { if (nsCOMPtr win = do_QueryReferent(mLocator->GetOwner())) { } } 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 aChoices) { MOZ_ASSERT(aChoices.isUndefined()); if (mLocator->ClearPendingRequest(this)) { return NS_OK; } RefPtr 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 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 wrapped; if (aPosition) { nsCOMPtr 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 obs = services::GetObserverService(); obs->NotifyObservers(wrapped, "geolocation-position-events", u"location-updated"); nsAutoMicroTask mt; if (mCallback.HasWebIDLCallback()) { RefPtr 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 ev = new RequestSendLocationEvent(aPosition, this); mMainThreadTarget->Dispatch(ev.forget()); return NS_OK; } NS_IMETHODIMP nsGeolocationRequest::NotifyError(uint16_t aErrorCode) { MOZ_ASSERT(NS_IsMainThread()); RefPtr 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 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 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 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 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) && IsWin8OrLater()) { 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 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 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> 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 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 obs = services::GetObserverService(); if (!obs) { return; } if (!mProvider) { return; } mHigherAccuracy = false; mProvider->Shutdown(); obs->NotifyObservers(mProvider, "geolocation-device-events", u"shutdown"); } StaticRefPtr nsGeolocationService::sService; already_AddRefed nsGeolocationService::GetGeolocationService() { RefPtr 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::sNonWindowSingleton; already_AddRefed Geolocation::NonWindowSingleton() { if (sNonWindowSingleton) { return do_AddRef(sNonWindowSingleton); } RefPtr result = new Geolocation(); DebugOnly 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 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 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 window = do_QueryReferent(this->GetOwner()); if (window) { nsCOMPtr document = window->GetDoc(); bool isHidden = document && document->Hidden(); if (isHidden || !window->IsFullyActive()) { return NS_OK; } } if (aSomewhere) { nsCOMPtr 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 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 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 win = do_QueryReferent(mOwner); if (!win) { return false; } nsCOMPtr 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); } } static nsIEventTarget* MainThreadTarget(Geolocation* geo) { nsCOMPtr window = do_QueryReferent(geo->GetOwner()); if (!window) { return GetMainThreadSerialEventTarget(); } return nsGlobalWindowInner::Cast(window)->EventTargetFor( mozilla::TaskCategory::Other); } nsresult Geolocation::GetCurrentPosition(GeoPositionCallback callback, GeoPositionErrorCallback errorCallback, UniquePtr&& options, CallerType aCallerType) { if (!IsFullyActiveOrChrome()) { RefPtr 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 = MainThreadTarget(this); RefPtr 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&& 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&& aOptions, CallerType aCallerType, ErrorResult& aRv) { if (!IsFullyActiveOrChrome()) { RefPtr 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 = MainThreadTarget(this); RefPtr 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 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 = MainThreadTarget(this); 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 aGivenProto) { return Geolocation_Binding::Wrap(aCtx, this, aGivenProto); }