fune/dom/geolocation/Geolocation.cpp
Olivia Hall fd773b706c Bug 1765835 - Adjustments to Location Provider r=geckoview-reviewers,owlish,m_kato,smaug
In Geolocation.cpp, the call to start the geolocation device always
had HighAccuracyRequested() set to false because no callbacks were
listening. Changed to set the callbacks first, before listening.

In Android, this patch stops the use of checking for last known location
on high accuracy location requests, adjusts comparing locations,
and also streamlines the criteria for picking the best location provider.

Differential Revision: https://phabricator.services.mozilla.com/D153226
2022-08-31 18:50:16 +00:00

1235 lines
36 KiB
C++

/* -*- 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; // <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* 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<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> mMainThreadTarget;
};
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* 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<nsPIDOMWindowInner> 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<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);
mMainThreadTarget->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) &&
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<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);
}
}
static nsIEventTarget* MainThreadTarget(Geolocation* geo) {
nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(geo->GetOwner());
if (!window) {
return GetMainThreadEventTarget();
}
return nsGlobalWindowInner::Cast(window)->EventTargetFor(
mozilla::TaskCategory::Other);
}
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 = MainThreadTarget(this);
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 = MainThreadTarget(this);
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 = 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<JSObject*> aGivenProto) {
return Geolocation_Binding::Wrap(aCtx, this, aGivenProto);
}