From 16da52acd2097d7c123479ddf6d7f23df38c7f9c Mon Sep 17 00:00:00 2001 From: Alexander Hendrich Date: Fri, 17 Mar 2023 14:26:29 +0000 Subject: [PATCH] [2/3] Add ScreenCaptureWithoutGestureAllowedForOrigins policy The policy is a list of URL templates (applied on a per-origin basis) that allows these origins to use the getDisplayMedia() web API without the user gesture ("transient activation") requirement (pending feature launch). This is needed for enhanced browser content redirection (BCR++) to enable video conferencing offloading in VDI. Google-internal design doc: https://docs.google.com/document/d/1CIybpXPScskAp7MBovXTMjeCvdrgBjAxJlNgkunutaw/edit?usp=sharing Bug: b:271258742 Change-Id: I4a41dff5412b469ad39adee874cad45b52a87ece Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4294945 Reviewed-by: Elad Alon Reviewed-by: Evan Liu Reviewed-by: Fr Reviewed-by: Maksim Ivanov Reviewed-by: Kinuko Yasuda Reviewed-by: Jesse Doherty Commit-Queue: Alexander Hendrich Cr-Commit-Position: refs/heads/main@{#1118647} --- .../browser/chrome_content_browser_client.cc | 51 +++--- .../media/webrtc/capture_policy_utils.cc | 31 ++++ .../media/webrtc/capture_policy_utils.h | 5 + .../webrtc/display_media_access_handler.cc | 8 +- .../webrtc_getdisplaymedia_browsertest.cc | 148 ++++++++++++++++-- ...nfiguration_policy_handler_list_factory.cc | 3 + chrome/common/pref_names.cc | 5 + chrome/common/pref_names.h | 1 + .../observers/use_counter/ukm_features.cc | 1 - .../policy/resources/templates/policies.yaml | 2 + ...aptureWithoutGestureAllowedForOrigins.yaml | 36 +++++ .../policy/test/data/policy_test_cases.json | 37 +++++ .../common/web_preferences/web_preferences.cc | 1 + .../web_preferences_mojom_traits.cc | 2 + .../common/web_preferences/web_preferences.h | 4 + .../web_preferences_mojom_traits.h | 5 + .../use_counter/metrics/web_feature.mojom | 2 +- .../webpreferences/web_preferences.mojom | 4 + third_party/blink/public/web/web_settings.h | 1 + .../core/exported/web_settings_impl.cc | 5 + .../core/exported/web_settings_impl.h | 1 + .../renderer/core/exported/web_view_impl.cc | 2 + .../blink/renderer/core/frame/settings.json5 | 7 + .../modules/mediastream/media_devices.cc | 82 +++++++--- tools/metrics/histograms/enums.xml | 10 +- .../histograms/metadata/media/histograms.xml | 14 ++ 26 files changed, 415 insertions(+), 53 deletions(-) create mode 100644 components/policy/resources/templates/policy_definitions/Miscellaneous/ScreenCaptureWithoutGestureAllowedForOrigins.yaml diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc index 9d2a1bc1236b7..bb5bce45cf48c 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc @@ -1572,6 +1572,8 @@ void ChromeContentBrowserClient::RegisterProfilePrefs( #if !BUILDFLAG(IS_ANDROID) registry->RegisterBooleanPref(prefs::kAutoplayAllowed, false); registry->RegisterListPref(prefs::kAutoplayAllowlist); + registry->RegisterListPref( + prefs::kScreenCaptureWithoutGestureAllowedForOrigins); registry->RegisterIntegerPref(prefs::kFetchKeepaliveDurationOnShutdown, 0); registry->RegisterBooleanPref( prefs::kSharedArrayBufferUnrestrictedAccessAllowed, false); @@ -4156,6 +4158,11 @@ void ChromeContentBrowserClient::OverrideWebkitPrefs( } web_prefs->autoplay_policy = GetAutoplayPolicyForWebContents(web_contents); +#if !BUILDFLAG(IS_ANDROID) + web_prefs->require_transient_activation_for_get_display_media = + capture_policy::IsTransientActivationRequiredForGetDisplayMedia( + web_contents); +#endif // !BUILDFLAG(IS_ANDROID) switch (GetWebTheme()->GetPreferredContrast()) { case ui::NativeTheme::PreferredContrast::kNoPreference: @@ -4221,41 +4228,47 @@ bool ChromeContentBrowserClientParts::OverrideWebPreferencesAfterNavigation( bool ChromeContentBrowserClient::OverrideWebPreferencesAfterNavigation( WebContents* web_contents, - WebPreferences* prefs) { + WebPreferences* web_prefs) { + bool prefs_changed = false; + const auto autoplay_policy = GetAutoplayPolicyForWebContents(web_contents); - const bool new_autoplay_policy_needed = - prefs->autoplay_policy != autoplay_policy; - if (new_autoplay_policy_needed) - prefs->autoplay_policy = autoplay_policy; + prefs_changed |= (web_prefs->autoplay_policy != autoplay_policy); + web_prefs->autoplay_policy = autoplay_policy; + +#if !BUILDFLAG(IS_ANDROID) + const bool require_transient_activation_for_get_display_media = + capture_policy::IsTransientActivationRequiredForGetDisplayMedia( + web_contents); + prefs_changed |= + (web_prefs->require_transient_activation_for_get_display_media != + require_transient_activation_for_get_display_media); + web_prefs->require_transient_activation_for_get_display_media = + require_transient_activation_for_get_display_media; +#endif // !BUILDFLAG(IS_ANDROID) - bool extra_parts_need_update = false; for (ChromeContentBrowserClientParts* parts : extra_parts_) { - extra_parts_need_update |= - parts->OverrideWebPreferencesAfterNavigation(web_contents, prefs); + prefs_changed |= + parts->OverrideWebPreferencesAfterNavigation(web_contents, web_prefs); } - bool preferred_color_scheme_updated = UpdatePreferredColorScheme( - prefs, web_contents->GetLastCommittedURL(), web_contents, GetWebTheme()); + prefs_changed |= + UpdatePreferredColorScheme(web_prefs, web_contents->GetLastCommittedURL(), + web_contents, GetWebTheme()); #if BUILDFLAG(IS_ANDROID) - bool force_dark_mode_changed = false; auto* delegate = TabAndroid::FromWebContents(web_contents) ? static_cast( web_contents->GetDelegate()) : nullptr; if (delegate) { bool force_dark_mode_new_state = delegate->IsForceDarkWebContentEnabled(); - force_dark_mode_changed = - prefs->force_dark_mode_enabled != force_dark_mode_new_state; - prefs->force_dark_mode_enabled = force_dark_mode_new_state; + prefs_changed |= + (web_prefs->force_dark_mode_enabled != force_dark_mode_new_state); + web_prefs->force_dark_mode_enabled = force_dark_mode_new_state; } #endif - return new_autoplay_policy_needed || extra_parts_need_update || -#if BUILDFLAG(IS_ANDROID) - force_dark_mode_changed || -#endif - preferred_color_scheme_updated; + return prefs_changed; } void ChromeContentBrowserClient::BrowserURLHandlerCreated( diff --git a/chrome/browser/media/webrtc/capture_policy_utils.cc b/chrome/browser/media/webrtc/capture_policy_utils.cc index 671edfd6f5f22..5556863117b78 100644 --- a/chrome/browser/media/webrtc/capture_policy_utils.cc +++ b/chrome/browser/media/webrtc/capture_policy_utils.cc @@ -5,11 +5,13 @@ #include "chrome/browser/media/webrtc/capture_policy_utils.h" #include "base/containers/cxx20_erase_vector.h" +#include "base/feature_list.h" #include "base/ranges/algorithm.h" #include "build/build_config.h" #include "build/chromeos_buildflags.h" #include "chrome/browser/content_settings/host_content_settings_map_factory.h" #include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h" +#include "chrome/browser/policy/policy_util.h" #include "chrome/browser/profiles/profile.h" #include "chrome/common/pref_names.h" #include "chrome/grit/generated_resources.h" @@ -19,6 +21,7 @@ #include "components/prefs/pref_service.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/web_contents.h" +#include "third_party/blink/public/common/features_generated.h" #include "url/gurl.h" #include "url/origin.h" @@ -164,6 +167,34 @@ bool IsGetDisplayMediaSetSelectAllScreensAllowed( #endif } +#if !BUILDFLAG(IS_ANDROID) +bool IsTransientActivationRequiredForGetDisplayMedia( + content::WebContents* contents) { + if (!base::FeatureList::IsEnabled( + blink::features::kGetDisplayMediaRequiresUserActivation)) { + return false; + } + + if (!contents) { + return true; + } + + Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); + if (!profile) { + return true; + } + + PrefService* prefs = profile->GetPrefs(); + if (!prefs) { + return true; + } + + return !policy::IsOriginInAllowlist( + contents->GetURL(), prefs, + prefs::kScreenCaptureWithoutGestureAllowedForOrigins); +} +#endif // !BUILDFLAG(IS_ANDROID) + DesktopMediaList::WebContentsFilter GetIncludableWebContentsFilter( const GURL& request_origin, AllowedScreenCaptureLevel capture_level) { diff --git a/chrome/browser/media/webrtc/capture_policy_utils.h b/chrome/browser/media/webrtc/capture_policy_utils.h index 0aeccd44f7546..4719e9c2a510b 100644 --- a/chrome/browser/media/webrtc/capture_policy_utils.h +++ b/chrome/browser/media/webrtc/capture_policy_utils.h @@ -68,6 +68,11 @@ bool IsGetDisplayMediaSetSelectAllScreensAllowed( bool IsGetDisplayMediaSetSelectAllScreensAllowedForAnySite( content::BrowserContext* context); +#if !BUILDFLAG(IS_ANDROID) +bool IsTransientActivationRequiredForGetDisplayMedia( + content::WebContents* contents); +#endif // !BUILDFLAG(IS_ANDROID) + } // namespace capture_policy #endif // CHROME_BROWSER_MEDIA_WEBRTC_CAPTURE_POLICY_UTILS_H_ diff --git a/chrome/browser/media/webrtc/display_media_access_handler.cc b/chrome/browser/media/webrtc/display_media_access_handler.cc index 65091295cf206..f392b030764f0 100644 --- a/chrome/browser/media/webrtc/display_media_access_handler.cc +++ b/chrome/browser/media/webrtc/display_media_access_handler.cc @@ -10,12 +10,12 @@ #include "base/containers/contains.h" #include "base/containers/cxx20_erase.h" -#include "base/feature_list.h" #include "base/functional/bind.h" #include "base/functional/callback.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "chrome/browser/bad_message.h" +#include "chrome/browser/media/webrtc/capture_policy_utils.h" #include "chrome/browser/media/webrtc/desktop_capture_devices_util.h" #include "chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h" #include "chrome/browser/media/webrtc/native_desktop_media_list.h" @@ -197,9 +197,9 @@ void DisplayMediaAccessHandler::HandleRequest( // before sending IPC, but just to be sure double check here as well. This // is not treated as a BadMessage because it is possible for the transient // user activation to expire between the renderer side check and this check. - if (base::FeatureList::IsEnabled( - blink::features::kGetDisplayMediaRequiresUserActivation) && - !rfh->HasTransientUserActivation()) { + if (!rfh->HasTransientUserActivation() && + capture_policy::IsTransientActivationRequiredForGetDisplayMedia( + web_contents)) { std::move(callback).Run( blink::mojom::StreamDevicesSet(), blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, diff --git a/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc b/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc index c5ed97e168a32..f3e8c048f320d 100644 --- a/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc +++ b/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include +#include #include #include "base/base_switches.h" @@ -27,6 +28,11 @@ #include "components/infobars/content/content_infobar_manager.h" #include "components/infobars/core/confirm_infobar_delegate.h" #include "components/infobars/core/infobar.h" +#include "components/policy/core/browser/browser_policy_connector.h" +#include "components/policy/core/common/mock_configuration_policy_provider.h" +#include "components/policy/core/common/policy_map.h" +#include "components/policy/core/common/policy_types.h" +#include "components/policy/policy_constants.h" #include "components/prefs/pref_service.h" #include "components/url_formatter/elide_url.h" #include "content/public/browser/web_contents.h" @@ -35,6 +41,7 @@ #include "content/public/test/browser_test_utils.h" #include "media/base/media_switches.h" #include "net/base/filename_util.h" +#include "third_party/abseil-cpp/absl/types/optional.h" #include "third_party/blink/public/common/features.h" #include "ui/base/l10n/l10n_util.h" @@ -108,6 +115,9 @@ struct TestConfigForHiDpi { constexpr char kAppWindowTitle[] = "AppWindow Display Capture Test"; +constexpr char kEmbeddedTestServerOrigin[] = "http://127.0.0.1"; +constexpr char kOtherOrigin[] = "https://other-origin.com"; + std::string DisplaySurfaceTypeAsString( DisplaySurfaceType display_surface_type) { switch (display_surface_type) { @@ -127,16 +137,23 @@ void RunGetDisplayMedia(content::WebContents* tab, bool is_fake_ui, bool expect_success, bool is_tab_capture, - const std::string& expected_error = "") { + const std::string& expected_error = "", + bool with_user_gesture = true) { DCHECK(!expect_success || expected_error.empty()); + const content::ToRenderFrameHost& adapter = tab->GetPrimaryMainFrame(); + const std::string script = base::StringPrintf( + "runGetDisplayMedia(%s, \"top-level-document\", \"%s\");", + constraints.c_str(), expected_error.c_str()); std::string result; - EXPECT_TRUE(content::ExecuteScriptAndExtractString( - tab->GetPrimaryMainFrame(), - base::StringPrintf( - "runGetDisplayMedia(%s, \"top-level-document\", \"%s\");", - constraints.c_str(), expected_error.c_str()), - &result)); + + if (with_user_gesture) { + EXPECT_TRUE( + content::ExecuteScriptAndExtractString(adapter, script, &result)); + } else { + EXPECT_TRUE(content::ExecuteScriptWithoutUserGestureAndExtractString( + adapter, script, &result)); + } #if BUILDFLAG(IS_MAC) if (!is_fake_ui && !is_tab_capture && @@ -1411,8 +1428,9 @@ class WebRtcScreenCaptureSelectAllScreensTest // Enables GetDisplayMedia and GetDisplayMediaSetAutoSelectAllScreens // features for multi surface capture. // TODO(simonha): remove when feature becomes stable. - if (test_config_.enable_select_all_screens) + if (test_config_.enable_select_all_screens) { command_line->AppendSwitch(switches::kEnableBlinkTestFeatures); + } command_line->AppendSwitch( switches::kEnableExperimentalWebPlatformFeatures); command_line->AppendSwitch(switches::kUseFakeUIForMediaStream); @@ -1473,4 +1491,116 @@ INSTANTIATE_TEST_SUITE_P( TestConfigForSelectAllScreens{/*display_surface=*/"monitor", /*enable_select_all_screens=*/false})); -#endif +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) || BUILDFLAG(IS_CHROMEOS_ASH) + +class GetDisplayMediaTransientActivationRequiredTest + : public WebRtcScreenCaptureBrowserTest, + public testing::WithParamInterface< + std::tuple>> { + public: + GetDisplayMediaTransientActivationRequiredTest() + : with_user_gesture_(std::get<0>(GetParam())), + require_gesture_feature_enabled_(std::get<1>(GetParam())), + prefer_current_tab_(std::get<2>(GetParam())), + policy_allowlist_value_(std::get<3>(GetParam())) {} + ~GetDisplayMediaTransientActivationRequiredTest() override = default; + + static std::string GetDescription( + const testing::TestParamInfo< + GetDisplayMediaTransientActivationRequiredTest::ParamType>& info) { + std::string name = base::StrCat( + {std::get<0>(info.param) ? "WithUserGesture_" : "WithoutUserGesture_", + std::get<1>(info.param) ? "RequireGestureFeatureEnabled_" + : "_RequireGestureFeatureDisabled_", + std::get<2>(info.param) ? "PreferCurrentTab_" + : "DontPreferCurrentTab_", + std::get<3>(info.param).has_value() + ? (*std::get<3>(info.param) == kEmbeddedTestServerOrigin) + ? "Allowlisted" + : "OtherAllowlisted" + : "NoPolicySet"}); + return name; + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + command_line->AppendSwitch(switches::kUseFakeUIForMediaStream); + } + + void SetUpInProcessBrowserTestFixture() override { + WebRtcScreenCaptureBrowserTest::SetUpInProcessBrowserTestFixture(); + + if (require_gesture_feature_enabled_) { + feature_list_.InitAndEnableFeature( + blink::features::kGetDisplayMediaRequiresUserActivation); + } else { + feature_list_.InitAndDisableFeature( + blink::features::kGetDisplayMediaRequiresUserActivation); + } + + policy_provider_.SetDefaultReturns( + /*is_initialization_complete_return=*/true, + /*is_first_policy_load_complete_return=*/true); + policy::BrowserPolicyConnector::SetPolicyProviderForTesting( + &policy_provider_); + + DetectErrorsInJavaScript(); + } + + bool PreferCurrentTab() const override { return prefer_current_tab_; } + + protected: + const bool with_user_gesture_; + const bool require_gesture_feature_enabled_; + const bool prefer_current_tab_; + const absl::optional policy_allowlist_value_; + base::test::ScopedFeatureList feature_list_; + testing::NiceMock policy_provider_; +}; + +IN_PROC_BROWSER_TEST_P(GetDisplayMediaTransientActivationRequiredTest, Check) { + ASSERT_TRUE(embedded_test_server()->Start()); + + if (policy_allowlist_value_.has_value()) { + policy::PolicyMap policy_map; + base::Value::List allowed_origins; + allowed_origins.Append(base::Value(*policy_allowlist_value_)); + policy_map.Set(policy::key::kScreenCaptureWithoutGestureAllowedForOrigins, + policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER, + policy::POLICY_SOURCE_PLATFORM, + base::Value(std::move(allowed_origins)), nullptr); + policy_provider_.UpdateChromePolicy(policy_map); + } + + content::WebContents* tab = OpenTestPageInNewTab(kMainHtmlPage); + + const bool expect_success = + with_user_gesture_ || !require_gesture_feature_enabled_ || + (policy_allowlist_value_ && + *policy_allowlist_value_ == kEmbeddedTestServerOrigin); + const std::string expected_error = + expect_success + ? "" + : "InvalidStateError: Failed to execute 'getDisplayMedia' on " + "'MediaDevices': getDisplayMedia() requires transient activation " + "(user gesture)."; + + RunGetDisplayMedia(tab, + GetConstraints(/*video=*/true, /*audio=*/true, + SelectAllScreens::kUndefined), + /*is_fake_ui=*/true, expect_success, + /*is_tab_capture=*/false, expected_error, + with_user_gesture_); +} + +INSTANTIATE_TEST_SUITE_P( + /* no prefix */, + GetDisplayMediaTransientActivationRequiredTest, + testing::Combine( + /*with_user_gesture=*/testing::Bool(), + /*require_gesture_feature_enabled=*/testing::Bool(), + /*prefer_current_tab=*/testing::Bool(), + /*policy_allowlist_value=*/ + testing::Values(absl::nullopt, + kEmbeddedTestServerOrigin, + kOtherOrigin)), + &GetDisplayMediaTransientActivationRequiredTest::GetDescription); diff --git a/chrome/browser/policy/configuration_policy_handler_list_factory.cc b/chrome/browser/policy/configuration_policy_handler_list_factory.cc index d6688b93992b3..2ed679961991d 100644 --- a/chrome/browser/policy/configuration_policy_handler_list_factory.cc +++ b/chrome/browser/policy/configuration_policy_handler_list_factory.cc @@ -378,6 +378,9 @@ const PolicyToPreferenceMapEntry kSimplePolicyMap[] = { { key::kAutoplayAllowlist, prefs::kAutoplayAllowlist, base::Value::Type::LIST }, + { key::kScreenCaptureWithoutGestureAllowedForOrigins, + prefs::kScreenCaptureWithoutGestureAllowedForOrigins, + base::Value::Type::LIST }, { key::kBasicAuthOverHttpEnabled, prefs::kBasicAuthOverHttpEnabled, base::Value::Type::BOOLEAN }, diff --git a/chrome/common/pref_names.cc b/chrome/common/pref_names.cc index e7ebdddb326bb..69b2a9af1c2d1 100644 --- a/chrome/common/pref_names.cc +++ b/chrome/common/pref_names.cc @@ -3211,6 +3211,11 @@ const char kAutoplayAllowlist[] = "media.autoplay_whitelist"; // Boolean that specifies whether autoplay blocking is enabled. const char kBlockAutoplayEnabled[] = "media.block_autoplay"; + +// Holds URL patterns that specify origins that will be allowed to call +// `getDisplayMedia()` without prior user gesture. +const char kScreenCaptureWithoutGestureAllowedForOrigins[] = + "media.screen_capture_without_gesture_allowed_for_origins"; #endif // !BUILDFLAG(IS_ANDROID) // Boolean allowing Chrome to block external protocol navigation in sandboxed diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h index c6807efcad18c..7d23d73c97a82 100644 --- a/chrome/common/pref_names.h +++ b/chrome/common/pref_names.h @@ -1107,6 +1107,7 @@ extern const char kSharedArrayBufferUnrestrictedAccessAllowed[]; extern const char kAutoplayAllowed[]; extern const char kAutoplayAllowlist[]; extern const char kBlockAutoplayEnabled[]; +extern const char kScreenCaptureWithoutGestureAllowedForOrigins[]; #endif extern const char kSandboxExternalProtocolBlocked[]; diff --git a/components/page_load_metrics/browser/observers/use_counter/ukm_features.cc b/components/page_load_metrics/browser/observers/use_counter/ukm_features.cc index 6434aac6ebafe..a4573bd1eef96 100644 --- a/components/page_load_metrics/browser/observers/use_counter/ukm_features.cc +++ b/components/page_load_metrics/browser/observers/use_counter/ukm_features.cc @@ -263,7 +263,6 @@ UseCounterMetricsRecorder::GetAllowedUkmFeatures() { WebFeature::kGamepadButtons, WebFeature::kWebNfcNdefReaderScan, WebFeature::kWakeLockAcquireScreenLockWithoutActivation, - WebFeature::kGetDisplayMediaWithoutUserActivation, WebFeature::kDataUrlInSvgUse, WebFeature::kExecutedNonTrivialJavaScriptURL, WebFeature::kV8DeprecatedStorageQuota_QueryUsageAndQuota_Method, diff --git a/components/policy/resources/templates/policies.yaml b/components/policy/resources/templates/policies.yaml index e59c4bc2de84b..127b7ab5d8af1 100644 --- a/components/policy/resources/templates/policies.yaml +++ b/components/policy/resources/templates/policies.yaml @@ -1090,6 +1090,8 @@ policies: 1089: UserAvatarCustomizationSelectorsEnabled 1090: DefaultThirdPartyStoragePartitioningSetting 1091: ThirdPartyStoragePartitioningBlockedForOrigins + 1092: ScreenCaptureWithoutGestureAllowedForOrigins + atomic_groups: 1: Homepage 2: RemoteAccess diff --git a/components/policy/resources/templates/policy_definitions/Miscellaneous/ScreenCaptureWithoutGestureAllowedForOrigins.yaml b/components/policy/resources/templates/policy_definitions/Miscellaneous/ScreenCaptureWithoutGestureAllowedForOrigins.yaml new file mode 100644 index 0000000000000..5136d0ed9181d --- /dev/null +++ b/components/policy/resources/templates/policy_definitions/Miscellaneous/ScreenCaptureWithoutGestureAllowedForOrigins.yaml @@ -0,0 +1,36 @@ +caption: Allow screen capture without prior user gesture +desc: |- + For security reasons, the + getDisplayMedia() web API requires + a prior user gesture ("transient activation") to be called or will otherwise + fail. + + With this policy set, admins can specify origins on which this API can be + called without prior user gesture. + + For detailed information on valid url patterns, please see + https://cloud.google.com/docs/chrome-enterprise/policies/url-patterns. * is + not an accepted value for this policy. + + If this policy is unset, all origins will require a prior user gesture to call + this API. +example_value: +- https://www.example.com +- '[*.]example.edu' +features: + dynamic_refresh: true + per_profile: true +owners: +- file://third_party/blink/renderer/modules/mediastream/OWNERS +- hendrich@chromium.org +schema: + items: + type: string + type: array +supported_on: +- chrome.*:113- +- chrome_os:113- +future_on: +- fuchsia +tags: [] +type: list diff --git a/components/policy/test/data/policy_test_cases.json b/components/policy/test/data/policy_test_cases.json index 0cf16380d1522..bd94ffb9e20f4 100644 --- a/components/policy/test/data/policy_test_cases.json +++ b/components/policy/test/data/policy_test_cases.json @@ -15108,6 +15108,43 @@ } ] }, + "ScreenCaptureWithoutGestureAllowedForOrigins": { + "os": [ + "win", + "linux", + "mac", + "chromeos_ash", + "chromeos_lacros", + "fuchsia" + ], + "policy_pref_mapping_tests": [ + { + "note": "Check default values (no policies set).", + "prefs": { + "media.screen_capture_without_gesture_allowed_for_origins": { + "default_value": [] + } + } + }, + { + "note": "Simple value.", + "policies": { + "ScreenCaptureWithoutGestureAllowedForOrigins": [ + "https://mydomain.com", + "https://test.mydomain.com" + ] + }, + "prefs": { + "media.screen_capture_without_gesture_allowed_for_origins": { + "value": [ + "https://mydomain.com", + "https://test.mydomain.com" + ] + } + } + } + ] + }, "TabUnderAllowed": { "reason_for_missing_test": "Policy was removed" }, diff --git a/third_party/blink/common/web_preferences/web_preferences.cc b/third_party/blink/common/web_preferences/web_preferences.cc index 589b70693c006..825fe182a24ae 100644 --- a/third_party/blink/common/web_preferences/web_preferences.cc +++ b/third_party/blink/common/web_preferences/web_preferences.cc @@ -201,6 +201,7 @@ WebPreferences::WebPreferences() do_not_update_selection_on_mutating_selection_range(false), autoplay_policy( blink::mojom::AutoplayPolicy::kDocumentUserActivationRequired), + require_transient_activation_for_get_display_media(true), low_priority_iframes_threshold( EffectiveConnectionType::kEffectiveConnectionUnknownType), picture_in_picture_enabled(true), diff --git a/third_party/blink/common/web_preferences/web_preferences_mojom_traits.cc b/third_party/blink/common/web_preferences/web_preferences_mojom_traits.cc index 797286d9dfafc..5c9427d0d43a6 100644 --- a/third_party/blink/common/web_preferences/web_preferences_mojom_traits.cc +++ b/third_party/blink/common/web_preferences/web_preferences_mojom_traits.cc @@ -215,6 +215,8 @@ bool StructTraitsdo_not_update_selection_on_mutating_selection_range = data.do_not_update_selection_on_mutating_selection_range(); out->autoplay_policy = data.autoplay_policy(); + out->require_transient_activation_for_get_display_media = + data.require_transient_activation_for_get_display_media(); out->preferred_color_scheme = data.preferred_color_scheme(); out->preferred_contrast = data.preferred_contrast(); out->picture_in_picture_enabled = data.picture_in_picture_enabled(); diff --git a/third_party/blink/public/common/web_preferences/web_preferences.h b/third_party/blink/public/common/web_preferences/web_preferences.h index 4d80bf0895028..642644dbf46a4 100644 --- a/third_party/blink/public/common/web_preferences/web_preferences.h +++ b/third_party/blink/public/common/web_preferences/web_preferences.h @@ -293,6 +293,10 @@ struct BLINK_COMMON_EXPORT WebPreferences { blink::mojom::AutoplayPolicy autoplay_policy = blink::mojom::AutoplayPolicy::kNoUserGestureRequired; + // `getDisplayMedia()`'s transient activation requirement can be bypassed via + // `ScreenCaptureWithoutGestureAllowedForOrigins` policy. + bool require_transient_activation_for_get_display_media; + // The preferred color scheme for the web content. The scheme is used to // evaluate the prefers-color-scheme media query and resolve UA color scheme // to be used based on the supported-color-schemes META tag and CSS property. diff --git a/third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h b/third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h index 3f36eb927cb48..dc2eac781dd65 100644 --- a/third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h +++ b/third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h @@ -690,6 +690,11 @@ struct BLINK_COMMON_EXPORT StructTraits(policy)); } +void WebSettingsImpl::SetRequireTransientActivationForGetDisplayMedia( + bool required) { + settings_->SetRequireTransientActivationForGetDisplayMedia(required); +} + void WebSettingsImpl::SetAutoZoomFocusedEditableToLegibleScale( bool auto_zoom_focused_editable_to_legible_scale) { auto_zoom_focused_editable_to_legible_scale_ = diff --git a/third_party/blink/renderer/core/exported/web_settings_impl.h b/third_party/blink/renderer/core/exported/web_settings_impl.h index 555074d331ef0..3ef9b0a8d8cda 100644 --- a/third_party/blink/renderer/core/exported/web_settings_impl.h +++ b/third_party/blink/renderer/core/exported/web_settings_impl.h @@ -53,6 +53,7 @@ class CORE_EXPORT WebSettingsImpl final : public WebSettings { bool ViewportEnabled() const override; void SetAccelerated2dCanvasMSAASampleCount(int) override; void SetAutoplayPolicy(mojom::blink::AutoplayPolicy) override; + void SetRequireTransientActivationForGetDisplayMedia(bool) override; void SetLCDTextPreference(LCDTextPreference) override; void SetAccessibilityPasswordValuesEnabled(bool) override; void SetAllowFileAccessFromFileURLs(bool) override; diff --git a/third_party/blink/renderer/core/exported/web_view_impl.cc b/third_party/blink/renderer/core/exported/web_view_impl.cc index f40d632e9cacb..c734e277f15c5 100644 --- a/third_party/blink/renderer/core/exported/web_view_impl.cc +++ b/third_party/blink/renderer/core/exported/web_view_impl.cc @@ -1712,6 +1712,8 @@ void WebView::ApplyWebPreferences(const web_pref::WebPreferences& prefs, settings->SetAccessibilityAlwaysShowFocus(prefs.always_show_focus); settings->SetAutoplayPolicy(prefs.autoplay_policy); + settings->SetRequireTransientActivationForGetDisplayMedia( + prefs.require_transient_activation_for_get_display_media); settings->SetViewportEnabled(prefs.viewport_enabled); settings->SetViewportMetaEnabled(prefs.viewport_meta_enabled); settings->SetViewportStyle(prefs.viewport_style); diff --git a/third_party/blink/renderer/core/frame/settings.json5 b/third_party/blink/renderer/core/frame/settings.json5 index de21b8d183c95..e11d13ab38049 100644 --- a/third_party/blink/renderer/core/frame/settings.json5 +++ b/third_party/blink/renderer/core/frame/settings.json5 @@ -884,6 +884,13 @@ initial: "AutoplayPolicy::Type::kNoUserGestureRequired", }, + // `getDisplayMedia()`'s transient activation requirement can be bypassed via + // `ScreenCaptureWithoutGestureAllowedForOrigins` policy. + { + name: "requireTransientActivationForGetDisplayMedia", + initial: true, + }, + // // Dark mode // diff --git a/third_party/blink/renderer/modules/mediastream/media_devices.cc b/third_party/blink/renderer/modules/mediastream/media_devices.cc index 1b88bdb92609c..b0550d58f9a39 100644 --- a/third_party/blink/renderer/modules/mediastream/media_devices.cc +++ b/third_party/blink/renderer/modules/mediastream/media_devices.cc @@ -37,6 +37,7 @@ #include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/navigator.h" +#include "third_party/blink/renderer/core/frame/settings.h" #include "third_party/blink/renderer/core/frame/web_feature.h" #include "third_party/blink/renderer/core/html/html_element.h" #include "third_party/blink/renderer/modules/mediastream/crop_target.h" @@ -52,6 +53,7 @@ #include "third_party/blink/renderer/platform/mediastream/webrtc_uma_histograms.h" #include "third_party/blink/renderer/platform/privacy_budget/identifiability_digest_helpers.h" #include "third_party/blink/renderer/platform/region_capture_crop_id.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "third_party/blink/renderer/platform/scheduler/public/event_loop.h" #include "third_party/blink/renderer/platform/weborigin/security_origin.h" #include "third_party/blink/renderer/platform/wtf/functional.h" @@ -176,7 +178,61 @@ void RecordUma(ProduceCropTargetPromiseResult result) { base::UmaHistogramEnumeration( "Media.RegionCapture.ProduceCropTarget.Promise.Result", result); } -#endif +#endif // !BUILDFLAG(IS_ANDROID) + +// When `blink::features::kGetDisplayMediaRequiresUserActivation` is enabled, +// calls to `getDisplayMedia()` will require a transient user activation. This +// can be bypassed with the `ScreenCaptureWithoutGestureAllowedForOrigins` +// policy though. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class GetDisplayMediaTransientActivation { + kPresent = 0, + kMissing = 1, + kMissingButFeatureDisabled = 2, + kMissingButPolicyOverrides = 3, + kMaxValue = kMissingButPolicyOverrides +}; + +void RecordUma(GetDisplayMediaTransientActivation activation) { + base::UmaHistogramEnumeration( + "Media.GetDisplayMedia.RequiresUserActivationResult", activation); +} + +bool TransientActivationRequirementSatisfied(LocalDOMWindow* window) { + DCHECK(window); + + LocalFrame* const frame = window->GetFrame(); + if (!frame) { + return false; // Err on the side of caution. Intentionally neglect UMA. + } + + const Settings* const settings = frame->GetSettings(); + if (!settings) { + return false; // Err on the side of caution. Intentionally neglect UMA. + } + + if (LocalFrame::HasTransientUserActivation(frame) || + (RuntimeEnabledFeatures:: + CapabilityDelegationDisplayCaptureRequestEnabled() && + window->IsDisplayCaptureRequestTokenActive())) { + RecordUma(GetDisplayMediaTransientActivation::kPresent); + return true; + } + + if (!RuntimeEnabledFeatures::GetDisplayMediaRequiresUserActivationEnabled()) { + RecordUma(GetDisplayMediaTransientActivation::kMissingButFeatureDisabled); + return true; + } + + if (!settings->GetRequireTransientActivationForGetDisplayMedia()) { + RecordUma(GetDisplayMediaTransientActivation::kMissingButPolicyOverrides); + return true; + } + + RecordUma(GetDisplayMediaTransientActivation::kMissing); + return false; +} void RecordEnumerateDevicesLatency(base::TimeTicks start_time) { const base::TimeDelta elapsed = base::TimeTicks::Now() - start_time; @@ -267,8 +323,9 @@ MediaStreamConstraints* ToMediaStreamConstraints( MediaStreamConstraints* ToMediaStreamConstraints( const DisplayMediaStreamOptions* source) { MediaStreamConstraints* const constraints = MediaStreamConstraints::Create(); - if (source->hasAudio()) + if (source->hasAudio()) { constraints->setAudio(source->audio()); + } if (source->hasVideo()) { constraints->setVideo(source->video()); } @@ -520,22 +577,11 @@ ScriptPromise MediaDevices::getDisplayMedia( return ScriptPromise(); } - const bool has_transient_user_activation = - LocalFrame::HasTransientUserActivation(window->GetFrame()) || - (RuntimeEnabledFeatures:: - CapabilityDelegationDisplayCaptureRequestEnabled() && - window->IsDisplayCaptureRequestTokenActive()); - - if (!has_transient_user_activation) { - UseCounter::Count(window, - WebFeature::kGetDisplayMediaWithoutUserActivation); - if (RuntimeEnabledFeatures:: - GetDisplayMediaRequiresUserActivationEnabled()) { - exception_state.ThrowDOMException( - DOMExceptionCode::kInvalidStateError, - "getDisplayMedia() requires transient activation (user gesture)."); - return ScriptPromise(); - } + if (!TransientActivationRequirementSatisfied(window)) { + exception_state.ThrowDOMException( + DOMExceptionCode::kInvalidStateError, + "getDisplayMedia() requires transient activation (user gesture)."); + return ScriptPromise(); } if (options->hasAutoSelectAllScreens() && options->autoSelectAllScreens()) { diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml index fbb6568be4ba9..054f3b8f48214 100644 --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -33037,6 +33037,7 @@ Called by update_document_policy_enum.py.--> + @@ -42347,7 +42348,7 @@ Called by update_use_counter_feature_enum.py.--> - + @@ -47900,6 +47901,13 @@ Called by update_permissions_policy_enum.py.--> + + + + + + + diff --git a/tools/metrics/histograms/metadata/media/histograms.xml b/tools/metrics/histograms/metadata/media/histograms.xml index 38346bb463296..f1f8caae6025b 100644 --- a/tools/metrics/histograms/metadata/media/histograms.xml +++ b/tools/metrics/histograms/metadata/media/histograms.xml @@ -2811,6 +2811,20 @@ chromium-metrics-reviews@google.com. + + eladalon@chromium.org + fbeaufort@chromium.org + hendrich@chromium.org + + Records whether getDisplayMedia was called with a user gesture (transient + activation) or whether it wasn't required. The requirement is only enforced + once `blink::features::kGetDisplayMediaRequiresUserActivation` is enabled + and can be bypassed with the `ScreenCaptureWithoutGestureAllowedForOrigins` + policy. + + + steimel@chromium.org