LibWeb+LibWebView+WebContent: Replace DNT with GPC

Global Privacy Control aims to be a replacement for Do Not Track. DNT
ended up not being a great solution, as it wasn't enforced by law. This
actually resulted in the DNT header serving as an extra fingerprinting
data point.

GPC is becoming enforced by law in USA states such as California and
Colorado. CA is further working on a bill which requires that browsers
implement such an opt-out preference signal (OOPS):

https://cppa.ca.gov/announcements/2025/20250911.html

This patch replaces DNT with GPC and hooks up the associated settings.
This commit is contained in:
Timothy Flynn 2025-04-02 09:30:34 -04:00 committed by Jelle Raaijmakers
parent d9ebd44924
commit b4df857a57
Notes: github-actions[bot] 2025-09-16 08:39:19 +00:00
22 changed files with 105 additions and 60 deletions

View file

@ -377,9 +377,14 @@
<div class="card">
<div class="card-header">Privacy</div>
<div class="card-body">
<div class="card-group inline-container">
<label for="do-not-track-toggle">Send websites a "Do Not Track" request</label>
<input id="do-not-track-toggle" type="checkbox" switch />
<div class="card-group">
<div class="inline-container">
<label for="global-privacy-control-toggle">
Enable Global Privacy Control
</label>
<input id="global-privacy-control-toggle" type="checkbox" switch />
</div>
<p class="description">Tell websites not to sell or share your data.</p>
</div>
</div>
</div>

View file

@ -1,11 +1,11 @@
const doNotTrackToggle = document.querySelector("#do-not-track-toggle");
const globalPrivacyControlToggle = document.querySelector("#global-privacy-control-toggle");
function loadSettings(settings) {
doNotTrackToggle.checked = settings.doNotTrack;
globalPrivacyControlToggle.checked = settings.globalPrivacyControl;
}
doNotTrackToggle.addEventListener("change", () => {
ladybird.sendMessage("setDoNotTrack", doNotTrackToggle.checked);
globalPrivacyControlToggle.addEventListener("change", () => {
ladybird.sendMessage("setGlobalPrivacyControl", globalPrivacyControlToggle.checked);
});
document.addEventListener("WebUIMessage", event => {

View file

@ -405,6 +405,7 @@ set(SOURCES
Geometry/DOMRect.cpp
Geometry/DOMRectList.cpp
Geometry/DOMRectReadOnly.cpp
GPC/GlobalPrivacyControl.cpp
HighResolutionTime/Performance.cpp
HighResolutionTime/TimeOrigin.cpp
HTML/AbstractWorker.cpp

View file

@ -1931,8 +1931,10 @@ WebIDL::ExceptionOr<GC::Ref<PendingResponse>> http_network_or_cache_fetch(JS::Re
// NOTE: `Accept` and `Accept-Language` are already included (unless fetch() is used, which does not include
// the latter by default), and `Accept-Charset` is a waste of bytes. See HTTP header layer division for
// more details.
if (ResourceLoader::the().enable_do_not_track() && !http_request->header_list()->contains("DNT"sv.bytes())) {
auto header = Infrastructure::Header::from_string_pair("DNT"sv, "1"sv);
//
// https://w3c.github.io/gpc/#the-sec-gpc-header-field-for-http-requests
if (ResourceLoader::the().enable_global_privacy_control() && !http_request->header_list()->contains("Sec-GPC"sv.bytes())) {
auto header = Infrastructure::Header::from_string_pair("Sec-GPC"sv, "1"sv);
http_request->header_list()->append(move(header));
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/GPC/GlobalPrivacyControl.h>
#include <LibWeb/Loader/ResourceLoader.h>
namespace Web::GlobalPrivacyControl {
GlobalPrivacyControlMixin::~GlobalPrivacyControlMixin() = default;
// https://w3c.github.io/gpc/#dom-globalprivacycontrol-globalprivacycontrol
bool GlobalPrivacyControlMixin::global_privacy_control() const
{
// The value is false if no Sec-GPC header field would be sent; otherwise, the value is true.
return ResourceLoader::the().enable_global_privacy_control();
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace Web::GlobalPrivacyControl {
// https://w3c.github.io/gpc/#dom-globalprivacycontrol
class GlobalPrivacyControlMixin {
public:
virtual ~GlobalPrivacyControlMixin();
bool global_privacy_control() const;
};
}

View file

@ -0,0 +1,4 @@
// https://w3c.github.io/gpc/#dom-globalprivacycontrol
interface mixin GlobalPrivacyControl {
readonly attribute boolean globalPrivacyControl;
};

View file

@ -134,18 +134,6 @@ WebIDL::Long Navigator::max_touch_points()
return 0;
}
// https://www.w3.org/TR/tracking-dnt/#dom-navigator-donottrack
Optional<FlyString> Navigator::do_not_track() const
{
// The value is null if no DNT header field would be sent (e.g., because a tracking preference is not
// enabled and no user-granted exception is applicable); otherwise, the value is a string beginning with
// "0" or "1", possibly followed by DNT-extension characters.
if (ResourceLoader::the().enable_do_not_track())
return "1"_fly_string;
return {};
}
GC::Ref<ServiceWorker::ServiceWorkerContainer> Navigator::service_worker()
{
if (!m_service_worker_container)

View file

@ -8,6 +8,7 @@
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/EncryptedMediaExtensions/NavigatorEncryptedMediaExtensionsPartial.h>
#include <LibWeb/GPC/GlobalPrivacyControl.h>
#include <LibWeb/Gamepad/NavigatorGamepad.h>
#include <LibWeb/HTML/MimeTypeArray.h>
#include <LibWeb/HTML/NavigatorBeacon.h>
@ -24,11 +25,13 @@
namespace Web::HTML {
class Navigator : public Bindings::PlatformObject
class Navigator
: public Bindings::PlatformObject
, public NavigatorBeaconPartial
, public NavigatorConcurrentHardwareMixin
, public NavigatorDeviceMemoryMixin
, public Gamepad::NavigatorGamepadPartial
, public GlobalPrivacyControl::GlobalPrivacyControlMixin
, public EncryptedMediaExtensions::NavigatorEncryptedMediaExtensionsPartial
, public NavigatorIDMixin
, public NavigatorLanguageMixin
@ -63,8 +66,6 @@ public:
[[nodiscard]] GC::Ref<UserActivation> user_activation();
[[nodiscard]] GC::Ref<CredentialManagement::CredentialsContainer> credentials();
Optional<FlyString> do_not_track() const;
GC::Ref<ServiceWorker::ServiceWorkerContainer> service_worker();
GC::Ref<MediaCapabilitiesAPI::MediaCapabilities> media_capabilities();

View file

@ -3,6 +3,7 @@
#import <EncryptedMediaExtensions/MediaKeySystemAccess.idl>
#import <Gamepad/Gamepad.idl>
#import <Geolocation/Geolocation.idl>
#import <GPC/GlobalPrivacyControl.idl>
#import <HTML/MimeTypeArray.idl>
#import <HTML/NavigatorBeacon.idl>
#import <HTML/NavigatorConcurrentHardware.idl>
@ -37,9 +38,6 @@ interface Navigator {
// https://html.spec.whatwg.org/multipage/interaction.html#useractivation
[SameObject] readonly attribute UserActivation userActivation;
// https://www.w3.org/TR/tracking-dnt/#dom-navigator-donottrack
readonly attribute DOMString? doNotTrack;
// https://w3c.github.io/ServiceWorker/#navigator-serviceworker
[SecureContext, SameObject] readonly attribute ServiceWorkerContainer serviceWorker;
@ -77,6 +75,7 @@ interface mixin NavigatorAutomationInformation {
readonly attribute boolean webdriver;
};
Navigator includes GlobalPrivacyControl;
Navigator includes NavigatorID;
Navigator includes NavigatorLanguage;
Navigator includes NavigatorOnLine;

View file

@ -8,6 +8,7 @@
#pragma once
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/GPC/GlobalPrivacyControl.h>
#include <LibWeb/HTML/NavigatorConcurrentHardware.h>
#include <LibWeb/HTML/NavigatorDeviceMemory.h>
#include <LibWeb/HTML/NavigatorID.h>
@ -20,7 +21,9 @@
namespace Web::HTML {
class WorkerNavigator : public Bindings::PlatformObject
class WorkerNavigator
: public Bindings::PlatformObject
, public GlobalPrivacyControl::GlobalPrivacyControlMixin
, public NavigatorConcurrentHardwareMixin
, public NavigatorDeviceMemoryMixin
, public NavigatorIDMixin

View file

@ -1,3 +1,4 @@
#import <GPC/GlobalPrivacyControl.idl>
#import <HTML/NavigatorConcurrentHardware.idl>
#import <HTML/NavigatorDeviceMemory.idl>
#import <HTML/NavigatorID.idl>
@ -20,6 +21,7 @@ interface WorkerNavigator {
[SecureContext, SameObject] readonly attribute ServiceWorkerContainer serviceWorker;
};
Navigator includes GlobalPrivacyControl;
WorkerNavigator includes NavigatorID;
WorkerNavigator includes NavigatorLanguage;
WorkerNavigator includes NavigatorOnLine;

View file

@ -66,8 +66,8 @@ public:
NavigatorCompatibilityMode navigator_compatibility_mode() { return m_navigator_compatibility_mode; }
void set_navigator_compatibility_mode(NavigatorCompatibilityMode mode) { m_navigator_compatibility_mode = mode; }
bool enable_do_not_track() const { return m_enable_do_not_track; }
void set_enable_do_not_track(bool enable) { m_enable_do_not_track = enable; }
bool enable_global_privacy_control() const { return m_enable_global_privacy_control; }
void set_enable_global_privacy_control(bool enable) { m_enable_global_privacy_control = enable; }
void clear_cache();
void evict_from_cache(LoadRequest const&);
@ -91,7 +91,7 @@ private:
String m_platform;
Vector<String> m_preferred_languages = { "en"_string };
NavigatorCompatibilityMode m_navigator_compatibility_mode;
bool m_enable_do_not_track { false };
bool m_enable_global_privacy_control { false };
};
}

View file

@ -42,7 +42,7 @@ static constexpr auto site_setting_site_filters_key = "siteFilters"sv;
static constexpr auto autoplay_key = "autoplay"sv;
static constexpr auto do_not_track_key = "doNotTrack"sv;
static constexpr auto global_privacy_control_key = "globalPrivacyControl"sv;
static constexpr auto dns_settings_key = "dnsSettings"sv;
@ -141,8 +141,8 @@ Settings Settings::create(Badge<Application>)
load_site_setting(settings.m_autoplay, autoplay_key);
if (auto do_not_track = settings_json.value().get_bool(do_not_track_key); do_not_track.has_value())
settings.m_do_not_track = *do_not_track ? DoNotTrack::Yes : DoNotTrack::No;
if (auto global_privacy_control = settings_json.value().get_bool(global_privacy_control_key); global_privacy_control.has_value())
settings.m_global_privacy_control = *global_privacy_control ? GlobalPrivacyControl::Yes : GlobalPrivacyControl::No;
if (auto dns_settings = settings_json.value().get(dns_settings_key); dns_settings.has_value())
settings.m_dns_settings = parse_dns_settings(*dns_settings);
@ -215,7 +215,7 @@ JsonValue Settings::serialize_json() const
save_site_setting(m_autoplay, autoplay_key);
settings.set(do_not_track_key, m_do_not_track == DoNotTrack::Yes);
settings.set(global_privacy_control_key, m_global_privacy_control == GlobalPrivacyControl::Yes);
// dnsSettings :: { mode: "system" } | { mode: "custom", server: string, port: u16, type: "udp" | "tls", forciblyEnabled: bool, dnssec: bool }
JsonObject dns_settings;
@ -253,7 +253,7 @@ void Settings::restore_defaults()
m_custom_search_engines.clear();
m_autocomplete_engine.clear();
m_autoplay = SiteSetting {};
m_do_not_track = DoNotTrack::No;
m_global_privacy_control = GlobalPrivacyControl::No;
m_dns_settings = SystemDNS {};
persist_settings();
@ -265,7 +265,7 @@ void Settings::restore_defaults()
observer.search_engine_changed();
observer.autocomplete_engine_changed();
observer.autoplay_settings_changed();
observer.do_not_track_changed();
observer.global_privacy_control_changed();
observer.dns_settings_changed();
}
}
@ -438,13 +438,13 @@ void Settings::remove_all_autoplay_site_filters()
observer.autoplay_settings_changed();
}
void Settings::set_do_not_track(DoNotTrack do_not_track)
void Settings::set_global_privacy_control(GlobalPrivacyControl global_privacy_control)
{
m_do_not_track = do_not_track;
m_global_privacy_control = global_privacy_control;
persist_settings();
for (auto& observer : m_observers)
observer.do_not_track_changed();
observer.global_privacy_control_changed();
}
DNSSettings Settings::parse_dns_settings(JsonValue const& dns_settings)

View file

@ -25,7 +25,7 @@ struct WEBVIEW_API SiteSetting {
OrderedHashTable<String> site_filters;
};
enum class DoNotTrack {
enum class GlobalPrivacyControl {
No,
Yes,
};
@ -41,7 +41,7 @@ public:
virtual void search_engine_changed() { }
virtual void autocomplete_engine_changed() { }
virtual void autoplay_settings_changed() { }
virtual void do_not_track_changed() { }
virtual void global_privacy_control_changed() { }
virtual void dns_settings_changed() { }
};
@ -79,8 +79,8 @@ public:
void remove_autoplay_site_filter(String const&);
void remove_all_autoplay_site_filters();
DoNotTrack do_not_track() const { return m_do_not_track; }
void set_do_not_track(DoNotTrack);
GlobalPrivacyControl global_privacy_control() const { return m_global_privacy_control; }
void set_global_privacy_control(GlobalPrivacyControl);
static DNSSettings parse_dns_settings(JsonValue const&);
DNSSettings const& dns_settings() const { return m_dns_settings; }
@ -105,7 +105,7 @@ private:
Vector<SearchEngine> m_custom_search_engines;
Optional<AutocompleteEngine> m_autocomplete_engine;
SiteSetting m_autoplay;
DoNotTrack m_do_not_track { DoNotTrack::No };
GlobalPrivacyControl m_global_privacy_control { GlobalPrivacyControl::No };
DNSSettings m_dns_settings { SystemDNS() };
bool m_dns_override_by_command_line { false };

View file

@ -605,7 +605,7 @@ void ViewImplementation::initialize_client(CreateNewClient create_new_client)
default_zoom_level_factor_changed();
languages_changed();
autoplay_settings_changed();
do_not_track_changed();
global_privacy_control_changed();
}
void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_error_page)
@ -676,10 +676,10 @@ void ViewImplementation::autoplay_settings_changed()
client().async_set_autoplay_allowlist(page_id(), autoplay_settings.site_filters.values());
}
void ViewImplementation::do_not_track_changed()
void ViewImplementation::global_privacy_control_changed()
{
auto do_not_track = Application::settings().do_not_track();
client().async_set_enable_do_not_track(page_id(), do_not_track == DoNotTrack::Yes);
auto global_privacy_control = Application::settings().global_privacy_control();
client().async_set_enable_global_privacy_control(page_id(), global_privacy_control == GlobalPrivacyControl::Yes);
}
static ErrorOr<LexicalPath> save_screenshot(Gfx::Bitmap const* bitmap)

View file

@ -275,7 +275,7 @@ protected:
virtual void default_zoom_level_factor_changed() override;
virtual void languages_changed() override;
virtual void autoplay_settings_changed() override;
virtual void do_not_track_changed() override;
virtual void global_privacy_control_changed() override;
void initialize_context_menus();

View file

@ -63,8 +63,8 @@ void SettingsUI::register_interfaces()
remove_all_site_setting_filters(data);
});
register_interface("setDoNotTrack"sv, [this](auto const& data) {
set_do_not_track(data);
register_interface("setGlobalPrivacyControl"sv, [this](auto const& data) {
set_global_privacy_control(data);
});
register_interface("setDNSSettings"sv, [this](auto const& data) {
@ -275,12 +275,12 @@ void SettingsUI::remove_all_site_setting_filters(JsonValue const& site_setting)
load_current_settings();
}
void SettingsUI::set_do_not_track(JsonValue const& do_not_track)
void SettingsUI::set_global_privacy_control(JsonValue const& global_privacy_control)
{
if (!do_not_track.is_bool())
if (!global_privacy_control.is_bool())
return;
WebView::Application::settings().set_do_not_track(do_not_track.as_bool() ? DoNotTrack::Yes : DoNotTrack::No);
WebView::Application::settings().set_global_privacy_control(global_privacy_control.as_bool() ? GlobalPrivacyControl::Yes : GlobalPrivacyControl::No);
}
void SettingsUI::set_dns_settings(JsonValue const& dns_settings)

View file

@ -36,7 +36,7 @@ private:
void remove_site_setting_filter(JsonValue const&);
void remove_all_site_setting_filters(JsonValue const&);
void set_do_not_track(JsonValue const&);
void set_global_privacy_control(JsonValue const&);
void set_dns_settings(JsonValue const&);
};

View file

@ -1142,9 +1142,9 @@ void ConnectionFromClient::set_preferred_languages(u64, Vector<String> preferred
Web::ResourceLoader::the().set_preferred_languages(move(preferred_languages));
}
void ConnectionFromClient::set_enable_do_not_track(u64, bool enable)
void ConnectionFromClient::set_enable_global_privacy_control(u64, bool enable)
{
Web::ResourceLoader::the().set_enable_do_not_track(enable);
Web::ResourceLoader::the().set_enable_global_privacy_control(enable);
}
void ConnectionFromClient::set_has_focus(u64 page_id, bool has_focus)

View file

@ -110,7 +110,7 @@ private:
virtual void set_preferred_contrast(u64 page_id, Web::CSS::PreferredContrast) override;
virtual void set_preferred_motion(u64 page_id, Web::CSS::PreferredMotion) override;
virtual void set_preferred_languages(u64 page_id, Vector<String>) override;
virtual void set_enable_do_not_track(u64 page_id, bool) override;
virtual void set_enable_global_privacy_control(u64 page_id, bool) override;
virtual void set_has_focus(u64 page_id, bool) override;
virtual void set_is_scripting_enabled(u64 page_id, bool) override;
virtual void set_device_pixels_per_css_pixel(u64 page_id, float) override;

View file

@ -96,7 +96,7 @@ endpoint WebContentServer
set_preferred_contrast(u64 page_id, Web::CSS::PreferredContrast contrast) =|
set_preferred_motion(u64 page_id, Web::CSS::PreferredMotion motion) =|
set_preferred_languages(u64 page_id, Vector<String> preferred_languages) =|
set_enable_do_not_track(u64 page_id, bool enable) =|
set_enable_global_privacy_control(u64 page_id, bool enable) =|
set_has_focus(u64 page_id, bool has_focus) =|
set_is_scripting_enabled(u64 page_id, bool is_scripting_enabled) =|
set_device_pixels_per_css_pixel(u64 page_id, float device_pixels_per_css_pixel) =|