LibWeb+LibWebView+WebContent: Allow muted media to autoplay by default

The autoplay setting was binary and its default blocked all media,
including muted video, leaving sites that rely on muted autoplay
visibly broken. Replace it with a tri-state user-agent autoplay
policy (allow audio and video, block audio, or block audio and video)
defaulting to allowing only inaudible media to autoplay.

This is enforced through the media element's "allowed to play" check,
so unmuting a muted autoplay or calling `play()` cannot slip audio
past the policy; audible playback is permitted once the document has
been activated by the user. The policy lives in a dedicated
AutoplaySettings consulted from HTMLMediaElement instead of the
Permissions Policy "allowed to use feature" check it was previously
conflated with.
This commit is contained in:
Luke Wilde 2026-06-09 17:55:09 +01:00 committed by Jelle Raaijmakers
parent ca87f977e6
commit 8dc8835b64
32 changed files with 369 additions and 221 deletions

View file

@ -642,8 +642,8 @@
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<div class="inline-container"> <div class="inline-container">
<label for="site-settings-global">Enable on all sites</label> <label for="site-settings-policy">Default for all sites</label>
<input id="site-settings-global" type="checkbox" switch /> <select id="site-settings-policy"></select>
</div> </div>
<hr /> <hr />
<label>Allowlist</label> <label>Allowlist</label>

View file

@ -1,7 +1,7 @@
const siteSettings = document.querySelector("#site-settings"); const siteSettings = document.querySelector("#site-settings");
const siteSettingsAdd = document.querySelector("#site-settings-add"); const siteSettingsAdd = document.querySelector("#site-settings-add");
const siteSettingsClose = document.querySelector("#site-settings-close"); const siteSettingsClose = document.querySelector("#site-settings-close");
const siteSettingsGlobal = document.querySelector("#site-settings-global"); const siteSettingsPolicy = document.querySelector("#site-settings-policy");
const siteSettingsList = document.querySelector("#site-settings-list"); const siteSettingsList = document.querySelector("#site-settings-list");
const siteSettingsInput = document.querySelector("#site-settings-input"); const siteSettingsInput = document.querySelector("#site-settings-input");
const siteSettingsRemoveAll = document.querySelector("#site-settings-remove-all"); const siteSettingsRemoveAll = document.querySelector("#site-settings-remove-all");
@ -9,6 +9,14 @@ const siteSettingsTitle = document.querySelector("#site-settings-title");
const autoplaySettings = document.querySelector("#autoplay-settings"); const autoplaySettings = document.querySelector("#autoplay-settings");
const SITE_SETTING_POLICY_OPTIONS = {
autoplay: [
{ value: "allow-audio-and-video", label: "Allow Audio and Video" },
{ value: "block-audio", label: "Block Audio" },
{ value: "block-audio-and-video", label: "Block Audio and Video" },
],
};
let AUTOPLAY_SETTINGS = {}; let AUTOPLAY_SETTINGS = {};
function loadSiteSettings(settings) { function loadSiteSettings(settings) {
@ -40,14 +48,29 @@ function currentSiteSetting() {
} }
function showSiteSettings(title, settings) { function showSiteSettings(title, settings) {
const setting = title.toLowerCase();
siteSettingsTitle.innerText = title; siteSettingsTitle.innerText = title;
siteSettingsGlobal.checked = settings.enabledGlobally;
siteSettingsPolicy.innerHTML = "";
const policyOptions = SITE_SETTING_POLICY_OPTIONS[setting] ?? [];
policyOptions.forEach(({ value, label }) => {
const option = document.createElement("option");
option.value = value;
option.textContent = label;
siteSettingsPolicy.appendChild(option);
});
siteSettingsPolicy.value = settings.policy;
siteSettingsList.innerHTML = ""; siteSettingsList.innerHTML = "";
siteSettingsGlobal.onchange = () => { siteSettingsPolicy.onchange = () => {
ladybird.sendMessage("setSiteSettingEnabledGlobally", { ladybird.sendMessage("setSiteSettingPolicy", {
setting: currentSiteSetting(), setting: currentSiteSetting(),
enabled: siteSettingsGlobal.checked, policy: siteSettingsPolicy.value,
}); });
}; };

View file

@ -474,6 +474,7 @@ set(SOURCES
HTML/AudioTrack.cpp HTML/AudioTrack.cpp
HTML/AudioTrackList.cpp HTML/AudioTrackList.cpp
HTML/AutocompleteElement.cpp HTML/AutocompleteElement.cpp
HTML/AutoplaySettings.cpp
HTML/BarProp.cpp HTML/BarProp.cpp
HTML/BeforeUnloadEvent.cpp HTML/BeforeUnloadEvent.cpp
HTML/BroadcastChannel.cpp HTML/BroadcastChannel.cpp
@ -912,7 +913,6 @@ set(SOURCES
PerformanceTimeline/PerformanceEntry.cpp PerformanceTimeline/PerformanceEntry.cpp
PerformanceTimeline/PerformanceObserver.cpp PerformanceTimeline/PerformanceObserver.cpp
PerformanceTimeline/PerformanceObserverEntryList.cpp PerformanceTimeline/PerformanceObserverEntryList.cpp
PermissionsPolicy/AutoplayAllowlist.cpp
PermissionsAPI/Permissions.cpp PermissionsAPI/Permissions.cpp
PermissionsAPI/PermissionStore.cpp PermissionsAPI/PermissionStore.cpp
PermissionsAPI/PermissionStatus.cpp PermissionsAPI/PermissionStatus.cpp

View file

@ -197,7 +197,6 @@
#include <LibWeb/Painting/PaintableBox.h> #include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/StackingContext.h> #include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/ViewportPaintable.h> #include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
#include <LibWeb/Platform/EventLoopPlugin.h> #include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/ResizeObserver/ResizeObserver.h> #include <LibWeb/ResizeObserver/ResizeObserver.h>
#include <LibWeb/ResizeObserver/ResizeObserverEntry.h> #include <LibWeb/ResizeObserver/ResizeObserverEntry.h>
@ -5808,9 +5807,8 @@ bool Document::is_allowed_to_use_feature(PolicyControlledFeature feature) const
// FIXME: This is ad-hoc. Implement the Permissions Policy specification. // FIXME: This is ad-hoc. Implement the Permissions Policy specification.
switch (feature) { switch (feature) {
case PolicyControlledFeature::Autoplay: case PolicyControlledFeature::Autoplay:
if (PermissionsPolicy::AutoplayAllowlist::the().is_allowed_for_origin(*this, origin()) == PermissionsPolicy::Decision::Enabled) // FIXME: Implement allowlist for this.
return true; return true;
break;
case PolicyControlledFeature::Camera: case PolicyControlledFeature::Camera:
// FIXME: Implement allowlist for this. // FIXME: Implement allowlist for this.
return true; return true;

View file

@ -693,6 +693,7 @@ namespace Web::HTML {
class AnimationFrameCallbackDriver; class AnimationFrameCallbackDriver;
class AudioTrack; class AudioTrack;
class AudioTrackList; class AudioTrackList;
class AutoplaySettings;
class BarProp; class BarProp;
class BeforeUnloadEvent; class BeforeUnloadEvent;
class BroadcastChannel; class BroadcastChannel;
@ -1056,12 +1057,6 @@ class PerformanceObserverEntryList;
} }
namespace Web::PermissionsPolicy {
class AutoplayAllowlist;
}
namespace Web::PermissionsAPI { namespace Web::PermissionsAPI {
class Permissions; class Permissions;

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include <LibWeb/Export.h>
namespace Web::HTML {
enum class AutoplayPolicy : u8 {
AllowAudioAndVideo,
BlockAudio,
BlockAudioAndVideo,
};
WEB_API Optional<AutoplayPolicy> autoplay_policy_from_string(StringView);
WEB_API StringView autoplay_policy_to_string(AutoplayPolicy);
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2023-2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/String.h>
#include <LibURL/Origin.h>
#include <LibURL/Parser.h>
#include <LibURL/URL.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/AutoplaySettings.h>
namespace Web::HTML {
AutoplaySettings& AutoplaySettings::the()
{
static auto& settings = *new AutoplaySettings;
return settings;
}
AutoplaySettings::AutoplaySettings() = default;
AutoplaySettings::~AutoplaySettings() = default;
AutoplayDecision AutoplaySettings::decision_for_origin(DOM::Document const& document, URL::Origin const& origin) const
{
// An origin in the allowlist may always autoplay, with or without audio.
for (auto const& allowed : m_allowlist) {
if (allowed.is_same_origin_domain(origin))
return AutoplayDecision::Allowed;
}
// AD-HOC: Allow autoplay for file:// URLs if the document is also from a file:// URL.
if (origin.is_opaque_file_origin() && document.origin().is_opaque_file_origin())
return AutoplayDecision::Allowed;
switch (m_policy) {
case AutoplayPolicy::AllowAudioAndVideo:
return AutoplayDecision::Allowed;
case AutoplayPolicy::BlockAudio:
return AutoplayDecision::AllowedIfInaudible;
case AutoplayPolicy::BlockAudioAndVideo:
return AutoplayDecision::Blocked;
}
VERIFY_NOT_REACHED();
}
void AutoplaySettings::set_policy(AutoplayPolicy policy, ReadonlySpan<String> allowlist)
{
m_policy = policy;
m_allowlist.clear_with_capacity();
m_allowlist.ensure_capacity(allowlist.size());
for (auto const& origin : allowlist) {
auto url = URL::Parser::basic_parse(origin);
if (!url.has_value())
url = URL::Parser::basic_parse(MUST(String::formatted("https://{}", origin)));
if (!url.has_value()) {
dbgln("Invalid origin for autoplay allowlist: {}", origin);
continue;
}
m_allowlist.append(url->origin());
}
}
Optional<AutoplayPolicy> autoplay_policy_from_string(StringView string)
{
if (string == "allow-audio-and-video"sv)
return AutoplayPolicy::AllowAudioAndVideo;
if (string == "block-audio"sv)
return AutoplayPolicy::BlockAudio;
if (string == "block-audio-and-video"sv)
return AutoplayPolicy::BlockAudioAndVideo;
return {};
}
StringView autoplay_policy_to_string(AutoplayPolicy policy)
{
switch (policy) {
case AutoplayPolicy::AllowAudioAndVideo:
return "allow-audio-and-video"sv;
case AutoplayPolicy::BlockAudio:
return "block-audio"sv;
case AutoplayPolicy::BlockAudioAndVideo:
return "block-audio-and-video"sv;
}
VERIFY_NOT_REACHED();
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2023-2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Vector.h>
#include <LibURL/Forward.h>
#include <LibWeb/Export.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/AutoplayPolicy.h>
namespace Web::HTML {
enum class AutoplayDecision : u8 {
Allowed,
AllowedIfInaudible,
Blocked,
};
// Holds the user agent's autoplay configuration for this process: the global policy plus the set of
// origins that may always autoplay regardless of that policy. Populated from the browser process.
class WEB_API AutoplaySettings {
public:
static AutoplaySettings& the();
AutoplayDecision decision_for_origin(DOM::Document const&, URL::Origin const&) const;
void set_policy(AutoplayPolicy, ReadonlySpan<String> allowlist);
private:
AutoplaySettings();
~AutoplaySettings();
AutoplayPolicy m_policy { AutoplayPolicy::BlockAudio };
Vector<URL::Origin> m_allowlist;
};
}

View file

@ -30,6 +30,7 @@
#include <LibWeb/HTML/AudioPlayState.h> #include <LibWeb/HTML/AudioPlayState.h>
#include <LibWeb/HTML/AudioTrack.h> #include <LibWeb/HTML/AudioTrack.h>
#include <LibWeb/HTML/AudioTrackList.h> #include <LibWeb/HTML/AudioTrackList.h>
#include <LibWeb/HTML/AutoplaySettings.h>
#include <LibWeb/HTML/CORSSettingAttribute.h> #include <LibWeb/HTML/CORSSettingAttribute.h>
#include <LibWeb/HTML/HTMLAudioElement.h> #include <LibWeb/HTML/HTMLAudioElement.h>
#include <LibWeb/HTML/HTMLMediaElement.h> #include <LibWeb/HTML/HTMLMediaElement.h>
@ -531,7 +532,11 @@ GC::Ref<WebIDL::Promise> HTMLMediaElement::play()
{ {
auto& realm = this->realm(); auto& realm = this->realm();
// FIXME: 1. If the media element is not allowed to play, then return a promise rejected with a "NotAllowedError" DOMException. // 1. If the media element is not allowed to play, then return a promise rejected with a "NotAllowedError" DOMException.
if (!is_allowed_to_play()) {
auto exception = WebIDL::NotAllowedError::create(realm, "Media playback is not allowed without user interaction"_utf16);
return WebIDL::create_rejected_promise_from_exception(realm, exception);
}
// 2. If the media element's error attribute is not null and its code is MEDIA_ERR_SRC_NOT_SUPPORTED, then return a promise // 2. If the media element's error attribute is not null and its code is MEDIA_ERR_SRC_NOT_SUPPORTED, then return a promise
// rejected with a "NotSupportedError" DOMException. // rejected with a "NotSupportedError" DOMException.
@ -612,7 +617,10 @@ void HTMLMediaElement::volume_or_muted_attribute_changed()
self.dispatch_event(DOM::Event::create(self.realm(), HTML::EventNames::volumechange)); self.dispatch_event(DOM::Event::create(self.realm(), HTML::EventNames::volumechange));
}); });
// FIXME: Then, if the media element is not allowed to play, the user agent must run the internal pause steps for the media element. // Then, if the media element is not allowed to play, the user agent must run the internal pause steps for the
// media element.
if (!is_allowed_to_play())
pause_element();
update_volume(); update_volume();
} }
@ -2148,7 +2156,7 @@ void HTMLMediaElement::set_ready_state(ReadyState ready_state)
return; return;
// The user agent may run the following substeps: // The user agent may run the following substeps:
{ if (is_allowed_to_play()) {
// Set the paused attribute to false. // Set the paused attribute to false.
set_paused(false); set_paused(false);
@ -2686,6 +2694,29 @@ bool HTMLMediaElement::is_eligible_for_autoplay() const
document().is_allowed_to_use_feature(DOM::PolicyControlledFeature::Autoplay)); document().is_allowed_to_use_feature(DOM::PolicyControlledFeature::Autoplay));
} }
// https://html.spec.whatwg.org/multipage/media.html#allowed-to-play
bool HTMLMediaElement::is_allowed_to_play() const
{
// A media element is said to be allowed to play if the user agent and the system allow media playback in the
// current context.
// NB: We allow playback once the document has been activated by the user, with an exception for inaudible media.
// Gating on transient activation instead pauses audible media once the activation expires, e.g. between ads,
// or media in a playlist.
if (auto window = document().window(); window && window->has_sticky_activation())
return true;
switch (AutoplaySettings::the().decision_for_origin(document(), document().origin())) {
case AutoplayDecision::Allowed:
return true;
case AutoplayDecision::AllowedIfInaudible:
return effective_media_volume() == 0.0;
case AutoplayDecision::Blocked:
return false;
}
VERIFY_NOT_REACHED();
}
HTMLMediaElement::PlaybackDirection HTMLMediaElement::direction_of_playback() const HTMLMediaElement::PlaybackDirection HTMLMediaElement::direction_of_playback() const
{ {
return m_playback_rate >= 0 ? PlaybackDirection::Forwards : PlaybackDirection::Backwards; return m_playback_rate >= 0 ? PlaybackDirection::Forwards : PlaybackDirection::Backwards;

View file

@ -255,6 +255,7 @@ private:
void update_current_video_frame(); void update_current_video_frame();
bool is_eligible_for_autoplay() const; bool is_eligible_for_autoplay() const;
bool is_allowed_to_play() const;
enum class PlaybackDirection : u8 { enum class PlaybackDirection : u8 {
Forwards, Forwards,

View file

@ -35,6 +35,7 @@
#include <LibWeb/Fetch/Fetching/Fetching.h> #include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/Geometry/DOMRect.h> #include <LibWeb/Geometry/DOMRect.h>
#include <LibWeb/HTML/AnimatedBitmapDecodedImageData.h> #include <LibWeb/HTML/AnimatedBitmapDecodedImageData.h>
#include <LibWeb/HTML/AutoplaySettings.h>
#include <LibWeb/HTML/BrowsingContext.h> #include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h> #include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/EventLoop/TaskQueue.h> #include <LibWeb/HTML/EventLoop/TaskQueue.h>
@ -548,6 +549,12 @@ void Internals::set_content_blocking_enabled(bool enabled)
page().set_content_blocking_enabled(enabled); page().set_content_blocking_enabled(enabled);
} }
void Internals::set_autoplay_policy(String const& policy)
{
if (auto parsed = HTML::autoplay_policy_from_string(policy); parsed.has_value())
HTML::AutoplaySettings::the().set_policy(*parsed, {});
}
// NOLINTNEXTLINE(readability-convert-member-functions-to-static // NOLINTNEXTLINE(readability-convert-member-functions-to-static
String Internals::get_computed_role(DOM::Element& element) String Internals::get_computed_role(DOM::Element& element)
{ {

View file

@ -88,6 +88,7 @@ public:
bool set_http_memory_cache_enabled(bool enabled); bool set_http_memory_cache_enabled(bool enabled);
WebIDL::ExceptionOr<void> set_content_blockers(String const& patterns); WebIDL::ExceptionOr<void> set_content_blockers(String const& patterns);
void set_content_blocking_enabled(bool enabled); void set_content_blocking_enabled(bool enabled);
void set_autoplay_policy(String const& policy);
String get_computed_role(DOM::Element& element); String get_computed_role(DOM::Element& element);
String get_computed_label(DOM::Element& element); String get_computed_label(DOM::Element& element);

View file

@ -79,6 +79,7 @@ interface Internals {
boolean setHttpMemoryCacheEnabled(boolean enabled); boolean setHttpMemoryCacheEnabled(boolean enabled);
undefined setContentBlockers(DOMString patterns); undefined setContentBlockers(DOMString patterns);
undefined setContentBlockingEnabled(boolean enabled); undefined setContentBlockingEnabled(boolean enabled);
undefined setAutoplayPolicy(DOMString policy);
DOMString getComputedRole(Element element); DOMString getComputedRole(Element element);
DOMString getComputedLabel(Element element); DOMString getComputedLabel(Element element);

View file

@ -1,98 +0,0 @@
/*
* Copyright (c) 2023-2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/String.h>
#include <LibURL/Origin.h>
#include <LibURL/Parser.h>
#include <LibURL/URL.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
// FIXME: This is an ad-hoc implementation of the "autoplay" policy-controlled feature:
// https://w3c.github.io/webappsec-permissions-policy/#policy-controlled-feature
namespace Web::PermissionsPolicy {
AutoplayAllowlist& AutoplayAllowlist::the()
{
static auto& filter = *new AutoplayAllowlist;
return filter;
}
AutoplayAllowlist::AutoplayAllowlist() = default;
AutoplayAllowlist::~AutoplayAllowlist() = default;
// https://w3c.github.io/webappsec-permissions-policy/#is-feature-enabled
Decision AutoplayAllowlist::is_allowed_for_origin(DOM::Document const& document, URL::Origin const& origin) const
{
// FIXME: 1. Let policy be documents Permissions Policy
// FIXME: 2. If policys inherited policy for feature is Disabled, return "Disabled".
// 3. If feature is present in policys declared policy:
if (m_allowlist.has_value()) {
// 1. If the allowlist for feature in policys declared policy matches origin, then return "Enabled".
// 2. Otherwise return "Disabled".
return m_allowlist->visit(
[](Global) {
return Decision::Enabled;
},
[&](auto const& patterns) {
for (auto const& pattern : patterns) {
if (pattern.is_same_origin_domain(origin))
return Decision::Enabled;
// AD-HOC: Allow autoplay for file:// URLs if the document is also from a file:// URL.
if (origin.is_opaque_file_origin() && document.origin().is_opaque_file_origin())
return Decision::Enabled;
}
return Decision::Disabled;
});
}
// 4. If features default allowlist is *, return "Enabled".
// 5. If features default allowlist is 'self', and origin is same origin with documents origin, return "Enabled".
// NOTE: The "autoplay" feature's default allowlist is 'self'.
// https://html.spec.whatwg.org/multipage/infrastructure.html#autoplay-feature
if (origin.is_same_origin(document.origin()))
return Decision::Enabled;
// AD-HOC: Allow autoplay for file:// URLs if the document is also from a file:// URL.
if (origin.is_opaque_file_origin() && document.origin().is_opaque_file_origin())
return Decision::Enabled;
// 6. Return "Disabled".
return Decision::Disabled;
}
void AutoplayAllowlist::enable_globally()
{
m_allowlist = Global {};
}
void AutoplayAllowlist::enable_for_origins(ReadonlySpan<String> origins)
{
m_allowlist = Patterns {};
auto& allowlist = m_allowlist->get<Patterns>();
allowlist.ensure_capacity(origins.size());
for (auto const& origin : origins) {
auto url = URL::Parser::basic_parse(origin);
if (!url.has_value())
url = URL::Parser::basic_parse(MUST(String::formatted("https://{}", origin)));
if (!url.has_value()) {
dbgln("Invalid origin for autoplay allowlist: {}", origin);
continue;
}
allowlist.append(url->origin());
}
}
}

View file

@ -1,38 +0,0 @@
/*
* Copyright (c) 2023-2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibURL/Forward.h>
#include <LibWeb/Export.h>
#include <LibWeb/Forward.h>
#include <LibWeb/PermissionsPolicy/Decision.h>
namespace Web::PermissionsPolicy {
class WEB_API AutoplayAllowlist {
public:
static AutoplayAllowlist& the();
Decision is_allowed_for_origin(DOM::Document const&, URL::Origin const&) const;
void enable_globally();
void enable_for_origins(ReadonlySpan<String>);
private:
AutoplayAllowlist();
~AutoplayAllowlist();
using Patterns = Vector<URL::Origin>;
struct Global { };
Optional<Variant<Patterns, Global>> m_allowlist;
};
}

View file

@ -1,16 +0,0 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace Web::PermissionsPolicy {
enum class Decision {
Enabled,
Disabled,
};
}

View file

@ -55,7 +55,7 @@ static constexpr auto SEARCH_ENGINE_URL_KEY = "url"sv;
static constexpr auto AUTOCOMPLETE_ENGINE_KEY = "autocompleteEngine"sv; static constexpr auto AUTOCOMPLETE_ENGINE_KEY = "autocompleteEngine"sv;
static constexpr auto AUTOCOMPLETE_ENGINE_NAME_KEY = "name"sv; static constexpr auto AUTOCOMPLETE_ENGINE_NAME_KEY = "name"sv;
static constexpr auto SITE_SETTING_ENABLED_GLOBALLY_KEY = "enabledGlobally"sv; static constexpr auto SITE_SETTING_POLICY_KEY = "policy"sv;
static constexpr auto SITE_SETTING_SITE_FILTERS_KEY = "siteFilters"sv; static constexpr auto SITE_SETTING_SITE_FILTERS_KEY = "siteFilters"sv;
static constexpr auto AUTOPLAY_KEY = "autoplay"sv; static constexpr auto AUTOPLAY_KEY = "autoplay"sv;
@ -242,13 +242,15 @@ Settings Settings::create(Badge<Application>)
} }
} }
auto load_site_setting = [&](SiteSetting& site_setting, StringView key) { auto load_site_setting = [&](AutoplaySiteSetting& site_setting, StringView key) {
auto saved_settings = settings_json.value().get_object(key); auto saved_settings = settings_json.value().get_object(key);
if (!saved_settings.has_value()) if (!saved_settings.has_value())
return; return;
if (auto enabled_globally = saved_settings->get_bool(SITE_SETTING_ENABLED_GLOBALLY_KEY); enabled_globally.has_value()) if (auto policy = saved_settings->get_string(SITE_SETTING_POLICY_KEY); policy.has_value()) {
site_setting.enabled_globally = *enabled_globally; if (auto parsed = Web::HTML::autoplay_policy_from_string(*policy); parsed.has_value())
site_setting.policy = *parsed;
}
if (auto site_filters = saved_settings->get_array(SITE_SETTING_SITE_FILTERS_KEY); site_filters.has_value()) { if (auto site_filters = saved_settings->get_array(SITE_SETTING_SITE_FILTERS_KEY); site_filters.has_value()) {
site_setting.site_filters.clear(); site_setting.site_filters.clear();
@ -360,7 +362,7 @@ JsonValue Settings::serialize_json() const
settings.set(AUTOCOMPLETE_ENGINE_KEY, move(autocomplete_engine)); settings.set(AUTOCOMPLETE_ENGINE_KEY, move(autocomplete_engine));
} }
auto save_site_setting = [&](SiteSetting const& site_setting, StringView key) { auto save_site_setting = [&](AutoplaySiteSetting const& site_setting, StringView key) {
JsonArray site_filters; JsonArray site_filters;
site_filters.ensure_capacity(site_setting.site_filters.size()); site_filters.ensure_capacity(site_setting.site_filters.size());
@ -368,8 +370,8 @@ JsonValue Settings::serialize_json() const
site_filters.must_append(site_filter); site_filters.must_append(site_filter);
JsonObject setting; JsonObject setting;
setting.set("enabledGlobally"sv, site_setting.enabled_globally); setting.set(SITE_SETTING_POLICY_KEY, Web::HTML::autoplay_policy_to_string(site_setting.policy));
setting.set("siteFilters"sv, move(site_filters)); setting.set(SITE_SETTING_SITE_FILTERS_KEY, move(site_filters));
settings.set(key, move(setting)); settings.set(key, move(setting));
}; };
@ -654,9 +656,9 @@ void Settings::set_autocomplete_engine(Optional<StringView> autocomplete_engine_
observer.autocomplete_engine_changed(); observer.autocomplete_engine_changed();
} }
void Settings::set_autoplay_enabled_globally(bool enabled_globally) void Settings::set_autoplay_policy(Web::HTML::AutoplayPolicy policy)
{ {
m_autoplay.enabled_globally = enabled_globally; m_autoplay.policy = policy;
persist_settings(); persist_settings();
for (auto& observer : m_observers) for (auto& observer : m_observers)
@ -859,11 +861,6 @@ SettingsObserver::~SettingsObserver()
Settings::remove_observer({}, *this); Settings::remove_observer({}, *this);
} }
SiteSetting::SiteSetting()
{
site_filters.set("file://"_string);
}
} }
namespace IPC { namespace IPC {

View file

@ -15,6 +15,7 @@
#include <LibHTTP/Cache/DiskCacheSettings.h> #include <LibHTTP/Cache/DiskCacheSettings.h>
#include <LibIPC/Forward.h> #include <LibIPC/Forward.h>
#include <LibURL/URL.h> #include <LibURL/URL.h>
#include <LibWeb/HTML/AutoplayPolicy.h>
#include <LibWebView/Autocomplete.h> #include <LibWebView/Autocomplete.h>
#include <LibWebView/Forward.h> #include <LibWebView/Forward.h>
#include <LibWebView/Options.h> #include <LibWebView/Options.h>
@ -35,10 +36,14 @@ struct BrowsingBehavior {
}; };
struct SiteSetting { struct SiteSetting {
SiteSetting();
bool enabled_globally { false };
OrderedHashTable<String> site_filters; OrderedHashTable<String> site_filters;
protected:
SiteSetting() = default;
};
struct AutoplaySiteSetting : public SiteSetting {
Web::HTML::AutoplayPolicy policy { Web::HTML::AutoplayPolicy::BlockAudio };
}; };
struct BrowsingDataSettings { struct BrowsingDataSettings {
@ -137,8 +142,8 @@ public:
Optional<AutocompleteEngine> const& autocomplete_engine() const { return m_autocomplete_engine; } Optional<AutocompleteEngine> const& autocomplete_engine() const { return m_autocomplete_engine; }
void set_autocomplete_engine(Optional<StringView> autocomplete_engine_name); void set_autocomplete_engine(Optional<StringView> autocomplete_engine_name);
SiteSetting const& autoplay_settings() const { return m_autoplay; } AutoplaySiteSetting const& autoplay_settings() const { return m_autoplay; }
void set_autoplay_enabled_globally(bool); void set_autoplay_policy(Web::HTML::AutoplayPolicy);
void add_autoplay_site_filter(String const&); void add_autoplay_site_filter(String const&);
void remove_autoplay_site_filter(String const&); void remove_autoplay_site_filter(String const&);
void remove_all_autoplay_site_filters(); void remove_all_autoplay_site_filters();
@ -183,7 +188,7 @@ private:
Optional<SearchEngine> m_search_engine; Optional<SearchEngine> m_search_engine;
Vector<SearchEngine> m_custom_search_engines; Vector<SearchEngine> m_custom_search_engines;
Optional<AutocompleteEngine> m_autocomplete_engine; Optional<AutocompleteEngine> m_autocomplete_engine;
SiteSetting m_autoplay; AutoplaySiteSetting m_autoplay;
BrowsingDataSettings m_browsing_data_settings; BrowsingDataSettings m_browsing_data_settings;
GlobalPrivacyControl m_global_privacy_control { GlobalPrivacyControl::No }; GlobalPrivacyControl m_global_privacy_control { GlobalPrivacyControl::No };
DNSSettings m_dns_settings { SystemDNS() }; DNSSettings m_dns_settings { SystemDNS() };

View file

@ -2373,10 +2373,11 @@ void ViewImplementation::autoplay_settings_changed()
auto const& autoplay_settings = Application::settings().autoplay_settings(); auto const& autoplay_settings = Application::settings().autoplay_settings();
auto const& web_content_options = Application::web_content_options(); auto const& web_content_options = Application::web_content_options();
if (autoplay_settings.enabled_globally || web_content_options.enable_autoplay == EnableAutoplay::Yes) auto policy = autoplay_settings.policy;
client().async_set_autoplay_allowed_on_all_websites(page_id()); if (web_content_options.enable_autoplay == EnableAutoplay::Yes)
else policy = Web::HTML::AutoplayPolicy::AllowAudioAndVideo;
client().async_set_autoplay_allowlist(page_id(), autoplay_settings.site_filters.values());
client().async_set_autoplay_settings(page_id(), policy, autoplay_settings.site_filters.values());
} }
void ViewImplementation::global_privacy_control_changed() void ViewImplementation::global_privacy_control_changed()

View file

@ -7,6 +7,7 @@
#include <AK/JsonArray.h> #include <AK/JsonArray.h>
#include <AK/Platform.h> #include <AK/Platform.h>
#include <LibURL/Parser.h> #include <LibURL/Parser.h>
#include <LibWeb/HTML/AutoplayPolicy.h>
#include <LibWebView/Application.h> #include <LibWebView/Application.h>
#include <LibWebView/SearchEngine.h> #include <LibWebView/SearchEngine.h>
#include <LibWebView/WebUI/SettingsUI.h> #include <LibWebView/WebUI/SettingsUI.h>
@ -92,8 +93,8 @@ void SettingsUI::register_interfaces()
register_interface("loadForciblyEnabledSiteSettings"sv, [this](auto const&) { register_interface("loadForciblyEnabledSiteSettings"sv, [this](auto const&) {
load_forcibly_enabled_site_settings(); load_forcibly_enabled_site_settings();
}); });
register_interface("setSiteSettingEnabledGlobally"sv, [this](auto const& data) { register_interface("setSiteSettingPolicy"sv, [this](auto const& data) {
set_site_setting_enabled_globally(data); set_site_setting_policy(data);
}); });
register_interface("addSiteSettingFilter"sv, [this](auto const& data) { register_interface("addSiteSettingFilter"sv, [this](auto const& data) {
add_site_setting_filter(data); add_site_setting_filter(data);
@ -316,19 +317,20 @@ void SettingsUI::load_forcibly_enabled_site_settings()
async_send_message("forciblyEnableSiteSettings"sv, move(site_settings)); async_send_message("forciblyEnableSiteSettings"sv, move(site_settings));
} }
void SettingsUI::set_site_setting_enabled_globally(JsonValue const& site_setting) void SettingsUI::set_site_setting_policy(JsonValue const& site_setting)
{ {
auto setting = site_setting_type(site_setting); auto setting = site_setting_type(site_setting);
if (!setting.has_value()) if (!setting.has_value())
return; return;
auto enabled = site_setting.as_object().get_bool("enabled"sv); auto policy = site_setting.as_object().get_string("policy"sv);
if (!enabled.has_value()) if (!policy.has_value())
return; return;
switch (*setting) { switch (*setting) {
case SiteSettingType::Autoplay: case SiteSettingType::Autoplay:
WebView::Application::settings().set_autoplay_enabled_globally(*enabled); if (auto parsed = Web::HTML::autoplay_policy_from_string(*policy); parsed.has_value())
WebView::Application::settings().set_autoplay_policy(*parsed);
break; break;
} }

View file

@ -34,7 +34,7 @@ private:
void set_autocomplete_engine(JsonValue const&); void set_autocomplete_engine(JsonValue const&);
void load_forcibly_enabled_site_settings(); void load_forcibly_enabled_site_settings();
void set_site_setting_enabled_globally(JsonValue const&); void set_site_setting_policy(JsonValue const&);
void add_site_setting_filter(JsonValue const&); void add_site_setting_filter(JsonValue const&);
void remove_site_setting_filter(JsonValue const&); void remove_site_setting_filter(JsonValue const&);
void remove_all_site_setting_filters(JsonValue const&); void remove_all_site_setting_filters(JsonValue const&);

View file

@ -44,6 +44,7 @@
#include <LibWeb/DOM/Text.h> #include <LibWeb/DOM/Text.h>
#include <LibWeb/Dump.h> #include <LibWeb/Dump.h>
#include <LibWeb/Fetch/Fetching/Fetching.h> #include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/HTML/AutoplaySettings.h>
#include <LibWeb/HTML/BroadcastChannel.h> #include <LibWeb/HTML/BroadcastChannel.h>
#include <LibWeb/HTML/BrowsingContext.h> #include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h> #include <LibWeb/HTML/EventLoop/EventLoop.h>
@ -68,7 +69,6 @@
#include <LibWeb/Painting/FlexboxInspectorOverlay.h> #include <LibWeb/Painting/FlexboxInspectorOverlay.h>
#include <LibWeb/Painting/StackingContext.h> #include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/ViewportPaintable.h> #include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
#include <LibWeb/Platform/EventLoopPlugin.h> #include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWebView/Attribute.h> #include <LibWebView/Attribute.h>
#include <LibWebView/ViewImplementation.h> #include <LibWebView/ViewImplementation.h>
@ -1972,16 +1972,9 @@ void ConnectionFromClient::set_content_blockers(u64 page_id, Core::AnonymousBuff
} }
} }
void ConnectionFromClient::set_autoplay_allowed_on_all_websites(u64) void ConnectionFromClient::set_autoplay_settings(u64, Web::HTML::AutoplayPolicy policy, Vector<String> allowlist)
{ {
auto& autoplay_allowlist = Web::PermissionsPolicy::AutoplayAllowlist::the(); Web::HTML::AutoplaySettings::the().set_policy(policy, allowlist);
autoplay_allowlist.enable_globally();
}
void ConnectionFromClient::set_autoplay_allowlist(u64, Vector<String> allowlist)
{
auto& autoplay_allowlist = Web::PermissionsPolicy::AutoplayAllowlist::the();
autoplay_allowlist.enable_for_origins(allowlist);
} }
void ConnectionFromClient::set_proxy_mappings(u64, Vector<ByteString> proxies, HashMap<ByteString, size_t> mappings) void ConnectionFromClient::set_proxy_mappings(u64, Vector<ByteString> proxies, HashMap<ByteString, size_t> mappings)

View file

@ -24,6 +24,7 @@
#include <LibWeb/CSS/PreferredMotion.h> #include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/Compositor/Types.h> #include <LibWeb/Compositor/Types.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWeb/HTML/AutoplayPolicy.h>
#include <LibWeb/HTML/SessionHistoryEntry.h> #include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/WorkerAgentTypes.h> #include <LibWeb/HTML/WorkerAgentTypes.h>
#include <LibWeb/Loader/FileRequest.h> #include <LibWeb/Loader/FileRequest.h>
@ -140,8 +141,7 @@ private:
virtual void remove_dom_node(u64 page_id, Web::UniqueNodeID node_id) override; virtual void remove_dom_node(u64 page_id, Web::UniqueNodeID node_id) override;
virtual void set_content_blockers(u64 page_id, Core::AnonymousBuffer patterns) override; virtual void set_content_blockers(u64 page_id, Core::AnonymousBuffer patterns) override;
virtual void set_autoplay_allowed_on_all_websites(u64 page_id) override; virtual void set_autoplay_settings(u64 page_id, Web::HTML::AutoplayPolicy policy, Vector<String> allowlist) override;
virtual void set_autoplay_allowlist(u64 page_id, Vector<String> allowlist) override;
virtual void set_proxy_mappings(u64 page_id, Vector<ByteString>, HashMap<ByteString, size_t>) override; virtual void set_proxy_mappings(u64 page_id, Vector<ByteString>, HashMap<ByteString, size_t>) override;
virtual void set_preferred_color_scheme(u64 page_id, Web::CSS::PreferredColorScheme) override; virtual void set_preferred_color_scheme(u64 page_id, Web::CSS::PreferredColorScheme) override;
virtual void set_preferred_contrast(u64 page_id, Web::CSS::PreferredContrast) override; virtual void set_preferred_contrast(u64 page_id, Web::CSS::PreferredContrast) override;

View file

@ -11,6 +11,7 @@
#include <LibWeb/CSS/PreferredMotion.h> #include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/CSS/Selector.h> #include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h> #include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/HTML/AutoplayPolicy.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h> #include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/BroadcastChannelMessage.h> #include <LibWeb/HTML/BroadcastChannelMessage.h>
#include <LibWeb/HTML/SelectedFile.h> #include <LibWeb/HTML/SelectedFile.h>
@ -131,8 +132,7 @@ endpoint WebContentServer
find_in_page_previous_match(u64 page_id) =| find_in_page_previous_match(u64 page_id) =|
set_content_blockers(u64 page_id, Core::AnonymousBuffer patterns) =| set_content_blockers(u64 page_id, Core::AnonymousBuffer patterns) =|
set_autoplay_allowed_on_all_websites(u64 page_id) =| set_autoplay_settings(u64 page_id, Web::HTML::AutoplayPolicy policy, Vector<String> allowlist) =|
set_autoplay_allowlist(u64 page_id, Vector<String> allowlist) =|
set_proxy_mappings(u64 page_id, Vector<ByteString> proxies, HashMap<ByteString, size_t> mappings) =| set_proxy_mappings(u64 page_id, Vector<ByteString> proxies, HashMap<ByteString, size_t> mappings) =|
set_preferred_color_scheme(u64 page_id, Web::CSS::PreferredColorScheme color_scheme) =| set_preferred_color_scheme(u64 page_id, Web::CSS::PreferredColorScheme color_scheme) =|
set_preferred_contrast(u64 page_id, Web::CSS::PreferredContrast contrast) =| set_preferred_contrast(u64 page_id, Web::CSS::PreferredContrast contrast) =|

View file

@ -0,0 +1,3 @@
muted autoplay started: true
paused after unmuting without a gesture: true
scripted audible play() rejected with: NotAllowedError

View file

@ -0,0 +1,2 @@
muted video autoplays: true
audible video autoplays: false

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<!-- Pin the policy here (not via the browser setting test-web inherits) so the result is deterministic
regardless of test order on a reused WebContent process. Served over HTTP (see the .headers file)
so the file:// carve-out does not apply. With no user gesture, "Block Audio" must keep audible
playback from sneaking in: a muted autoplay that is later unmuted pauses, and a scripted play() of
audible media is rejected. -->
<script>internals.setAutoplayPolicy("block-audio");</script>
<video autoplay muted id="muted" src="../../../Assets/test-webm.webm"></video>
<video id="scripted" src="../../../Assets/test-webm.webm"></video>
<script src="../include.js"></script>
<script>
asyncTest(async done => {
const muted = document.getElementById("muted");
const scripted = document.getElementById("scripted");
setTimeout(() => {
println("FAIL: timeout waiting for muted autoplay");
done();
}, 15_000);
if (muted.paused)
await new Promise(resolve => muted.addEventListener("playing", resolve, { once: true }));
println(`muted autoplay started: ${!muted.paused}`);
muted.muted = false;
println(`paused after unmuting without a gesture: ${muted.paused}`);
let rejection = "none";
try {
await scripted.play();
} catch (error) {
rejection = error.name;
}
println(`scripted audible play() rejected with: ${rejection}`);
done();
});
</script>

View file

@ -0,0 +1 @@
Content-Type: text/html

View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<!-- Pin the policy here (not via the browser setting test-web inherits) so the result is deterministic
regardless of test order on a reused WebContent process. Served over HTTP (see the .headers file)
so the file:// autoplay carve-out does not apply: under "Block Audio", muted media autoplays while
audible media does not. -->
<script>internals.setAutoplayPolicy("block-audio");</script>
<video autoplay muted id="muted" src="../../../Assets/test-webm.webm"></video>
<video autoplay id="audible" src="../../../Assets/test-webm.webm"></video>
<script src="../include.js"></script>
<script>
asyncTest(done => {
const results = {};
let remaining = 2;
setTimeout(() => {
println("FAIL: timeout waiting for media to load");
done();
}, 15_000);
function watch(key, video) {
const finish = value => {
results[key] = value;
if (--remaining === 0) {
println(`muted video autoplays: ${results.muted}`);
println(`audible video autoplays: ${results.audible}`);
done();
}
};
video.addEventListener("canplaythrough", () => finish(!video.paused));
video.addEventListener("error", () => finish("error"));
}
watch("muted", document.getElementById("muted"));
watch("audible", document.getElementById("audible"));
});
</script>

View file

@ -0,0 +1 @@
Content-Type: text/html

View file

@ -1,4 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- Allow autoplay so the scripted play() works without a user gesture, independent of the browser's
autoplay policy that test-web would otherwise inherit. -->
<script>internals.setAutoplayPolicy("allow-audio-and-video");</script>
<video id="video"></video> <video id="video"></video>
<script src="../include.js"></script> <script src="../include.js"></script>
<script> <script>

View file

@ -16,11 +16,11 @@
#include <LibMedia/Audio/Loader.h> #include <LibMedia/Audio/Loader.h>
#include <LibRequests/RequestClient.h> #include <LibRequests/RequestClient.h>
#include <LibWeb/Bindings/MainThreadVM.h> #include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/HTML/AutoplaySettings.h>
#include <LibWeb/HTML/Window.h> #include <LibWeb/HTML/Window.h>
#include <LibWeb/Loader/ContentBlocker.h> #include <LibWeb/Loader/ContentBlocker.h>
#include <LibWeb/Loader/GeneratedPagesLoader.h> #include <LibWeb/Loader/GeneratedPagesLoader.h>
#include <LibWeb/Loader/ResourceLoader.h> #include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
#include <LibWeb/Platform/EventLoopPlugin.h> #include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Platform/FontPlugin.h> #include <LibWeb/Platform/FontPlugin.h>
#include <LibWebView/HelperProcess.h> #include <LibWebView/HelperProcess.h>
@ -150,8 +150,7 @@ static ErrorOr<void> load_autoplay_allowlist()
TRY(origins.try_append(move(domain))); TRY(origins.try_append(move(domain)));
} }
auto& autoplay_allowlist = Web::PermissionsPolicy::AutoplayAllowlist::the(); Web::HTML::AutoplaySettings::the().set_policy(Web::HTML::AutoplayPolicy::BlockAudio, origins);
autoplay_allowlist.enable_for_origins(origins);
return {}; return {};
} }