ladybird/Libraries/LibWeb/Geolocation/Geolocation.cpp

402 lines
19 KiB
C++
Raw Normal View History

2025-06-19 15:28:02 +02:00
/*
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Time.h>
2025-06-19 15:28:02 +02:00
#include <LibWeb/Bindings/GeolocationPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/DocumentObserver.h>
2025-06-19 15:28:02 +02:00
#include <LibWeb/Geolocation/Geolocation.h>
#include <LibWeb/Geolocation/GeolocationPosition.h>
#include <LibWeb/HTML/EventLoop/Task.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Platform/Timer.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
2025-06-19 15:28:02 +02:00
namespace Web::Geolocation {
static constexpr u32 VISIBILITY_STATE_TIMEOUT_MS = 5'000;
static WebIDL::UnsignedLong s_next_watch_id = 0;
2025-06-19 15:28:02 +02:00
GC_DEFINE_ALLOCATOR(Geolocation);
Geolocation::Geolocation(JS::Realm& realm)
: PlatformObject(realm)
{
}
void Geolocation::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(Geolocation);
Base::initialize(realm);
}
void Geolocation::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_cached_position);
visitor.visit(m_timeout_timers);
}
2025-06-19 15:28:02 +02:00
// https://w3c.github.io/geolocation/#dom-geolocation-getcurrentposition
void Geolocation::get_current_position(GC::Ref<WebIDL::CallbackType> success_callback,
GC::Ptr<WebIDL::CallbackType> error_callback, PositionOptions options)
2025-06-19 15:28:02 +02:00
{
// 1. If this's relevant global object's associated Document is not fully active:
auto& window = as<HTML::Window>(HTML::relevant_global_object(*this));
if (!window.associated_document().is_fully_active()) {
// 1. Call back with error errorCallback and POSITION_UNAVAILABLE.
call_back_with_error(error_callback, GeolocationPositionError::ErrorCode::PositionUnavailable);
2025-06-19 15:28:02 +02:00
// 2. Terminate this algorithm.
return;
2025-06-19 15:28:02 +02:00
}
// 2. Request a position passing this, successCallback, errorCallback, and options.
request_a_position(success_callback, error_callback, options);
2025-06-19 15:28:02 +02:00
}
// https://w3c.github.io/geolocation/#watchposition-method
WebIDL::Long Geolocation::watch_position(GC::Ref<WebIDL::CallbackType> success_callback,
GC::Ptr<WebIDL::CallbackType> error_callback, PositionOptions options)
2025-06-19 15:28:02 +02:00
{
// 1. If this's relevant global object's associated Document is not fully active:
auto& window = as<HTML::Window>(HTML::relevant_global_object(*this));
if (!window.associated_document().is_fully_active()) {
// 1. Call back with error passing errorCallback and POSITION_UNAVAILABLE.
call_back_with_error(error_callback, GeolocationPositionError::ErrorCode::PositionUnavailable);
2025-06-19 15:28:02 +02:00
// 2. Return 0.
return 0;
2025-06-19 15:28:02 +02:00
}
// 2. Let watchId be an implementation-defined unsigned long that is greater than zero.
auto watch_id = ++s_next_watch_id;
2025-06-19 15:28:02 +02:00
// 3. Append watchId to this's [[watchIDs]].
m_watch_ids.set(watch_id);
2025-06-19 15:28:02 +02:00
// 4. Request a position passing this, successCallback, errorCallback, options, and watchId.
request_a_position(success_callback, error_callback, options, watch_id);
2025-06-19 15:28:02 +02:00
// 5. Return watchId.
return watch_id;
2025-06-19 15:28:02 +02:00
}
// https://w3c.github.io/geolocation/#clearwatch-method
void Geolocation::clear_watch(WebIDL::Long watch_id)
{
// 1. Remove watchId from this's [[watchIDs]].
m_watch_ids.remove(watch_id);
}
// https://w3c.github.io/geolocation/#dfn-acquire-a-position
void Geolocation::acquire_a_position(GC::Ref<WebIDL::CallbackType> success_callback,
GC::Ptr<WebIDL::CallbackType> error_callback, PositionOptions options, Optional<WebIDL::UnsignedLong> watch_id)
{
// 1. If watchId was passed and this's [[watchIDs]] does not contain watchId, terminate this algorithm.
if (watch_id.has_value() && !m_watch_ids.contains(watch_id.value()))
return;
// 2. Let acquisitionTime be a new EpochTimeStamp that represents now.
HighResolutionTime::EpochTimeStamp const acquisition_time = AK::UnixDateTime::now().milliseconds_since_epoch();
// 3. Let timeoutTime be the sum of acquisitionTime and options.timeout.
[[maybe_unused]] HighResolutionTime::EpochTimeStamp const timeout_time = acquisition_time + options.timeout;
// 4. Let cachedPosition be this's [[cachedPosition]].
auto cached_position = m_cached_position;
// FIXME: 5. Create an implementation-specific timeout task that elapses at timeoutTime, during which it tries to acquire
// the device's position by running the following steps:
{
// FIXME: 1. Let permission be get the current permission state of "geolocation".
// FIXME: 2. If permission is "denied":
if (false) {
// FIXME: 1. Stop timeout.
// FIXME: 2. Do the user or system denied permission failure case step.
}
// FIXME: 3. If permission is "granted":
if (true) {
// 1. Check if an emulated position should be used by running the following steps:
{
// 1. Let emulatedPositionData be get emulated position data passing this.
auto emulated_position_data = get_emulated_position_data();
// 2. If emulatedPositionData is not null:
if (!emulated_position_data.has<Empty>()) {
// 1. If emulatedPositionData is a GeolocationPositionError:
if (emulated_position_data.has<GeolocationPositionError::ErrorCode>()) {
// 1. Call back with error passing errorCallback and emulatedPositionData.
// FIXME: We pass along the code instead of the entire error object. Spec issue:
// https://github.com/w3c/geolocation/issues/186
call_back_with_error(error_callback, emulated_position_data.get<GeolocationPositionError::ErrorCode>());
// 2. Terminate this algorithm.
return;
}
// 2. Let position be a new GeolocationPosition passing emulatedPositionData, acquisitionTime and
// options.enableHighAccuracy.
auto position = realm().create<GeolocationPosition>(realm(),
emulated_position_data.get<GC::Ref<GeolocationCoordinates>>(),
acquisition_time,
options.enable_high_accuracy);
// 3. Queue a task on the geolocation task source with a step that invokes successCallback with
// « position » and "report".
HTML::queue_a_task(HTML::Task::Source::Geolocation, nullptr, nullptr, GC::create_function(heap(), [success_callback, position] {
(void)WebIDL::invoke_callback(success_callback, {}, WebIDL::ExceptionBehavior::Report, { { position } });
}));
// 4. Terminate this algorithm.
return;
}
}
// 2. Let position be null.
GC::Ptr<GeolocationPosition> position;
// 3. If cachedPosition is not null, and options.maximumAge is greater than 0:
if (cached_position && options.maximum_age > 0) {
// 1. Let cacheTime be acquisitionTime minus the value of the options.maximumAge member.
HighResolutionTime::EpochTimeStamp const cache_time = acquisition_time - options.maximum_age;
// 2. If cachedPosition's timestamp's value is greater than cacheTime, and
// cachedPosition.[[isHighAccuracy]] equals options.enableHighAccuracy:
if (cached_position->timestamp() > cache_time
&& cached_position->is_high_accuracy() == options.enable_high_accuracy) {
// 1. Queue a task on the geolocation task source with a step that invokes successCallback with
// « cachedPosition » and "report".
HTML::queue_a_task(HTML::Task::Source::Geolocation, nullptr, nullptr, GC::create_function(heap(), [success_callback, cached_position] {
(void)WebIDL::invoke_callback(success_callback, {}, WebIDL::ExceptionBehavior::Report, { { cached_position } });
}));
// 2. Terminate this algorithm.
return;
}
}
// FIXME: 4. Otherwise, if position is not cachedPosition, try to acquire position data from the underlying system,
// optionally taking into consideration the value of options.enableHighAccuracy during acquisition.
// FIXME: 5. If the timeout elapses during acquisition, or acquiring the device's position results in failure:
if (false) {
// FIXME: 1. Stop the timeout.
// FIXME: 2. Go to dealing with failures.
// 3. Terminate this algorithm.
return;
}
// FIXME: 6. If acquiring the position data from the system succeeds:
if (true) {
// FIXME: 1. Let positionData be a map with the following name/value pairs based on the acquired position data:
// * longitude:
// A double that represents the longitude coordinates on the Earth's surface in degrees, using
// the [WGS84] coordinate system. Longitude measures how far east or west a point is from the
// Prime Meridian.
// * altitude:
// A double? that represents the altitude in meters above the [WGS84] ellipsoid, or null if not
// available. Altitude measures the height above sea level.
// * accuracy:
// A non-negative double that represents the accuracy value indicating the 95% confidence level
// in meters. Accuracy measures how close the measured coordinates are to the true position.
// * altitudeAccuracy:
// A non-negative double? that represents the altitude accuracy, or null if not available,
// indicating the 95% confidence level in meters. Altitude accuracy measures how close the
// measured altitude is to the true altitude.
// * speed:
// A non-negative double? that represents the speed in meters per second, or null if not
// available. Speed measures how fast the device is moving.
// * heading:
// A double? that represents the heading in degrees, or null if not available or the device is
// stationary. Heading measures the direction in which the device is moving relative to true
// north.
GC::Ref<GeolocationCoordinates> position_data = realm().create<GeolocationCoordinates>(realm());
// 2. Set position to a new GeolocationPosition passing positionData, acquisitionTime and
// options.enableHighAccuracy.
position = realm().create<GeolocationPosition>(realm(), position_data, acquisition_time, options.enable_high_accuracy);
// 3. Set this's [[cachedPosition]] to position.
m_cached_position = *position;
}
// FIXME: 7. Stop the timeout.
// 8. Queue a task on the geolocation task source with a step that invokes successCallback with « position »
// and "report".
HTML::queue_a_task(HTML::Task::Source::Geolocation, nullptr, nullptr, GC::create_function(heap(), [success_callback, position] {
(void)WebIDL::invoke_callback(success_callback, {}, WebIDL::ExceptionBehavior::Report, { { position } });
}));
}
}
}
// https://w3c.github.io/geolocation/#dfn-call-back-with-error
void Geolocation::call_back_with_error(GC::Ptr<WebIDL::CallbackType> callback, GeolocationPositionError::ErrorCode code) const
{
// 1. If callback is null, return.
if (!callback)
return;
// 2. Let error be a newly created GeolocationPositionError instance whose code attribute is initialized to code.
auto error = realm().create<GeolocationPositionError>(realm(), code);
// 3. Queue a task on the geolocation task source with a step that invokes callback with « error » and "report".
HTML::queue_a_task(HTML::Task::Source::Geolocation, nullptr, nullptr, GC::create_function(heap(), [callback, error] {
(void)WebIDL::invoke_callback(*callback, {}, WebIDL::ExceptionBehavior::Report,
{ { error } });
}));
}
// https://w3c.github.io/geolocation/#dfn-get-emulated-position-data
EmulatedPositionData Geolocation::get_emulated_position_data() const
{
// 1. Let navigable be geolocation's relevant global object's associated Document's node navigable.
auto navigable = as<HTML::Window>(HTML::relevant_global_object(*this)).navigable();
// 2. If navigable is null, return null.
if (!navigable)
return Empty {};
// 3. Let traversable be navigable’s top-level traversable.
auto traversable = navigable->top_level_traversable();
// 4. If traversable is null, return null.
if (!traversable)
return Empty {};
// 5. Return traversable's associated emulated position data.
return traversable->emulated_position_data();
}
// https://w3c.github.io/geolocation/#dfn-request-a-position
void Geolocation::request_a_position(GC::Ref<WebIDL::CallbackType> success_callback,
GC::Ptr<WebIDL::CallbackType> error_callback, PositionOptions options, Optional<WebIDL::UnsignedLong> watch_id)
2025-06-19 15:28:02 +02:00
{
// 1. Let watchIDs be geolocation's [[watchIDs]].
// 2. Let document be the geolocation's relevant global object's associated Document.
[[maybe_unused]] auto& document = as<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
// FIXME: 3. If document is not allowed to use the "geolocation" feature:
if (false) {
// 1. If watchId was passed, remove watchId from watchIDs.
if (watch_id.has_value())
m_watch_ids.remove(watch_id.value());
// 2. Call back with error passing errorCallback and PERMISSION_DENIED.
call_back_with_error(error_callback, GeolocationPositionError::ErrorCode::PermissionDenied);
// 3. Terminate this algorithm.
return;
}
// 4. If geolocation's environment settings object is a non-secure context:
if (is_non_secure_context(HTML::relevant_settings_object(*this))) {
// 1. If watchId was passed, remove watchId from watchIDs.
if (watch_id.has_value())
m_watch_ids.remove(watch_id.value());
// 2. Call back with error passing errorCallback and PERMISSION_DENIED.
call_back_with_error(error_callback, GeolocationPositionError::ErrorCode::PermissionDenied);
// 3. Terminate this algorithm.
return;
}
// 5. If document's visibility state is "hidden", wait for the following page visibility change steps to run:
run_in_parallel_when_document_is_visible(document, GC::create_function(heap(), [this, watch_id, success_callback, error_callback, options] {
// 1. Assert: document's visibility state is "visible".
// 2. Continue to the next steps below.
// AD-HOC: This is implemented by run_in_parallel_when_document_is_visible().
// FIXME: 6. Let descriptor be a new PermissionDescriptor whose name is "geolocation".
// 7. In parallel:
// AD-HOC: run_in_parallel_when_document_is_visible() already runs this in parallel.
{
// FIXME: 1. Set permission to request permission to use descriptor.
// FIXME: 2. If permission is "denied", then:
if (false) {
// 1. If watchId was passed, remove watchId from watchIDs.
if (watch_id.has_value())
m_watch_ids.remove(watch_id.value());
// 2. Call back with error passing errorCallback and PERMISSION_DENIED.
call_back_with_error(error_callback, GeolocationPositionError::ErrorCode::PermissionDenied);
// 3. Terminate this algorithm.
return;
}
// FIXME: 3. Wait to acquire a position passing successCallback, errorCallback, options, and watchId.
acquire_a_position(success_callback, error_callback, options, watch_id);
// 4. If watchId was not passed, terminate this algorithm.
if (!watch_id.has_value())
return;
// FIXME: 5. While watchIDs contains watchId:
{
// FIXME: 1. Wait for a significant change of geographic position. What constitutes a significant change of
// geographic position is left to the implementation. User agents MAY impose a rate limit on how
// frequently position changes are reported. User agents MUST consider invoking set emulated position
// data as a significant change.
// FIXME: 2. If document is not fully active or visibility state is not "visible", go back to the previous step
// and again wait for a significant change of geographic position.
// FIXME: 3. Wait to acquire a position passing successCallback, errorCallback, options, and watchId.
}
}
}));
}
void Geolocation::run_in_parallel_when_document_is_visible(DOM::Document& document, GC::Ref<GC::Function<void()>> callback)
{
// Run callback in parallel if the document is already visible.
if (document.visibility_state_value() == HTML::VisibilityState::Visible) {
Platform::EventLoopPlugin::the().deferred_invoke(callback);
return;
}
// Run the callback as soon as the document becomes visible. If we time out, do not run the callback at all.
auto document_observer = realm().create<DOM::DocumentObserver>(realm(), document);
auto timeout_timer = Platform::Timer::create_single_shot(heap(), VISIBILITY_STATE_TIMEOUT_MS, {});
m_timeout_timers.append(timeout_timer);
auto clear_observer_and_timer = [this, document_observer, timeout_timer] {
document_observer->set_document_visibility_state_observer({});
timeout_timer->stop();
m_timeout_timers.remove_first_matching([&](auto timer) { return timer == timeout_timer; });
};
timeout_timer->on_timeout = GC::create_function(heap(), [clear_observer_and_timer] {
dbgln("Geolocation: Waiting for visibility state update timed out");
clear_observer_and_timer();
});
document_observer->set_document_visibility_state_observer([clear_observer_and_timer, callback](HTML::VisibilityState state) {
if (state == HTML::VisibilityState::Visible) {
clear_observer_and_timer();
callback->function()();
}
});
timeout_timer->start();
2025-06-19 15:28:02 +02:00
}
}