blob: ad88dc7a1d852025f71300152da3330e64f14f4c [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/devtools/protocol/emulation_handler.h"
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "build/build_config.h"
#include "components/download/public/common/download_url_parameters.h"
#include "content/browser/device_posture/device_posture_provider_impl.h"
#include "content/browser/devtools/devtools_agent_host_impl.h"
#include "content/browser/generic_sensor/web_contents_sensor_provider_proxy.h"
#include "content/browser/idle/idle_manager_impl.h"
#include "content/browser/renderer_host/input/touch_emulator.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_constants.h"
#include "net/http/http_util.h"
#include "services/device/public/cpp/generic_sensor/sensor_reading.h"
#include "services/device/public/cpp/geolocation/geoposition.h"
#include "services/device/public/mojom/geolocation_context.mojom.h"
#include "services/device/public/mojom/geoposition.mojom.h"
#include "services/device/public/mojom/sensor.mojom-shared.h"
#include "services/network/public/cpp/client_hints.h"
#include "third_party/blink/public/mojom/device_posture/device_posture_provider.mojom.h"
#include "ui/display/mojom/screen_orientation.mojom.h"
#include "ui/events/gesture_detection/gesture_provider_config_helper.h"
namespace content {
namespace protocol {
namespace {
constexpr char kCommandIsOnlyAvailableAtTopTarget[] =
"Command can only be executed on top-level targets";
constexpr char kSensorIsAlreadyOverridden[] =
"The specified sensor type is already overridden";
constexpr char kSensorIsNotOverridden[] =
"This sensor type is not being overridden with a virtual sensor";
display::mojom::ScreenOrientation WebScreenOrientationTypeFromString(
const std::string& type) {
if (type == Emulation::ScreenOrientation::TypeEnum::PortraitPrimary)
return display::mojom::ScreenOrientation::kPortraitPrimary;
if (type == Emulation::ScreenOrientation::TypeEnum::PortraitSecondary)
return display::mojom::ScreenOrientation::kPortraitSecondary;
if (type == Emulation::ScreenOrientation::TypeEnum::LandscapePrimary)
return display::mojom::ScreenOrientation::kLandscapePrimary;
if (type == Emulation::ScreenOrientation::TypeEnum::LandscapeSecondary)
return display::mojom::ScreenOrientation::kLandscapeSecondary;
return display::mojom::ScreenOrientation::kUndefined;
}
std::optional<content::DisplayFeature::Orientation>
DisplayFeatureOrientationTypeFromString(const std::string& type) {
if (type == Emulation::DisplayFeature::OrientationEnum::Vertical)
return content::DisplayFeature::Orientation::kVertical;
if (type == Emulation::DisplayFeature::OrientationEnum::Horizontal)
return content::DisplayFeature::Orientation::kHorizontal;
return std::nullopt;
}
base::expected<blink::mojom::DevicePostureType, protocol::Response>
DevicePostureTypeFromString(const std::string& type) {
if (type == Emulation::DevicePosture::TypeEnum::Continuous) {
return blink::mojom::DevicePostureType::kContinuous;
} else if (type == Emulation::DevicePosture::TypeEnum::Folded) {
return blink::mojom::DevicePostureType::kFolded;
} else {
return base::unexpected(
protocol::Response::InvalidParams("Invalid posture type"));
}
}
ui::GestureProviderConfigType TouchEmulationConfigurationToType(
const std::string& protocol_value) {
ui::GestureProviderConfigType result =
ui::GestureProviderConfigType::CURRENT_PLATFORM;
if (protocol_value ==
Emulation::SetEmitTouchEventsForMouse::ConfigurationEnum::Mobile) {
result = ui::GestureProviderConfigType::GENERIC_MOBILE;
}
if (protocol_value ==
Emulation::SetEmitTouchEventsForMouse::ConfigurationEnum::Desktop) {
result = ui::GestureProviderConfigType::GENERIC_DESKTOP;
}
return result;
}
bool ValidateClientHintString(const std::string& s) {
// Matches definition in structured headers:
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-17#section-3.3.3
for (char c : s) {
if (!base::IsAsciiPrintable(c))
return false;
}
return true;
}
} // namespace
EmulationHandler::EmulationHandler()
: DevToolsDomainHandler(Emulation::Metainfo::domainName),
touch_emulation_enabled_(false),
device_emulation_enabled_(false),
focus_emulation_enabled_(false),
host_(nullptr) {}
EmulationHandler::~EmulationHandler() = default;
// static
std::vector<EmulationHandler*> EmulationHandler::ForAgentHost(
DevToolsAgentHostImpl* host) {
return host->HandlersByName<EmulationHandler>(
Emulation::Metainfo::domainName);
}
void EmulationHandler::SetRenderer(int process_host_id,
RenderFrameHostImpl* frame_host) {
if (host_ == frame_host)
return;
if (!frame_host) {
sensor_overrides_.clear();
}
host_ = frame_host;
if (touch_emulation_enabled_)
UpdateTouchEventEmulationState();
if (device_emulation_enabled_)
UpdateDeviceEmulationState();
}
void EmulationHandler::Wire(UberDispatcher* dispatcher) {
Emulation::Dispatcher::wire(dispatcher, this);
}
Response EmulationHandler::Disable() {
if (touch_emulation_enabled_) {
touch_emulation_enabled_ = false;
UpdateTouchEventEmulationState();
}
user_agent_ = std::string();
if (device_emulation_enabled_) {
device_emulation_enabled_ = false;
UpdateDeviceEmulationState();
}
if (focus_emulation_enabled_)
SetFocusEmulationEnabled(false);
prefers_color_scheme_ = "";
prefers_reduced_motion_ = "";
prefers_reduced_transparency_ = "";
sensor_overrides_.clear();
ClearDevicePostureOverride();
return Response::Success();
}
namespace {
Response ConvertSensorType(const Emulation::SensorType& type,
device::mojom::SensorType* out_type) {
if (type == Emulation::SensorTypeEnum::AbsoluteOrientation) {
*out_type = device::mojom::SensorType::ABSOLUTE_ORIENTATION_QUATERNION;
} else if (type == Emulation::SensorTypeEnum::Accelerometer) {
*out_type = device::mojom::SensorType::ACCELEROMETER;
} else if (type == Emulation::SensorTypeEnum::AmbientLight) {
*out_type = device::mojom::SensorType::AMBIENT_LIGHT;
} else if (type == Emulation::SensorTypeEnum::Gravity) {
*out_type = device::mojom::SensorType::GRAVITY;
} else if (type == Emulation::SensorTypeEnum::Gyroscope) {
*out_type = device::mojom::SensorType::GYROSCOPE;
} else if (type == Emulation::SensorTypeEnum::LinearAcceleration) {
*out_type = device::mojom::SensorType::LINEAR_ACCELERATION;
} else if (type == Emulation::SensorTypeEnum::Magnetometer) {
*out_type = device::mojom::SensorType::MAGNETOMETER;
} else if (type == Emulation::SensorTypeEnum::RelativeOrientation) {
*out_type = device::mojom::SensorType::RELATIVE_ORIENTATION_QUATERNION;
} else {
return Response::InvalidParams("Invalid sensor type: " + type);
}
return Response::Success();
}
Response ConvertSensorReading(device::mojom::SensorType type,
Emulation::SensorReading* const reading,
device::SensorReading* out_reading) {
switch (type) {
case device::mojom::SensorType::AMBIENT_LIGHT: {
if (!reading->HasSingle()) {
return Response::InvalidParams(
"This sensor type requires a 'single' parameter");
}
auto* single_value = reading->GetSingle(nullptr);
out_reading->als.value = single_value->GetValue();
break;
}
case device::mojom::SensorType::ACCELEROMETER:
case device::mojom::SensorType::GRAVITY:
case device::mojom::SensorType::GYROSCOPE:
case device::mojom::SensorType::LINEAR_ACCELERATION:
case device::mojom::SensorType::MAGNETOMETER: {
if (!reading->HasXyz()) {
return Response::InvalidParams(
"This sensor type requires an 'xyz' parameter");
}
auto* xyz = reading->GetXyz(nullptr);
out_reading->accel.x = xyz->GetX();
out_reading->accel.y = xyz->GetY();
out_reading->accel.z = xyz->GetZ();
break;
}
case device::mojom::SensorType::ABSOLUTE_ORIENTATION_QUATERNION:
case device::mojom::SensorType::RELATIVE_ORIENTATION_QUATERNION: {
if (!reading->HasQuaternion()) {
return Response::InvalidParams(
"This sensor type requires a 'quaternion' parameter");
}
auto* quaternion = reading->GetQuaternion(nullptr);
out_reading->orientation_quat.x = quaternion->GetX();
out_reading->orientation_quat.y = quaternion->GetY();
out_reading->orientation_quat.z = quaternion->GetZ();
out_reading->orientation_quat.w = quaternion->GetW();
break;
}
case device::mojom::SensorType::ABSOLUTE_ORIENTATION_EULER_ANGLES:
case device::mojom::SensorType::PRESSURE:
case device::mojom::SensorType::PROXIMITY:
case device::mojom::SensorType::RELATIVE_ORIENTATION_EULER_ANGLES:
return Response::InvalidParams("Unsupported sensor type");
}
out_reading->raw.timestamp =
(base::TimeTicks::Now() - base::TimeTicks()).InSecondsF();
return Response::Success();
}
base::expected<device::mojom::VirtualSensorMetadataPtr, Response>
ParseSensorMetadata(Maybe<Emulation::SensorMetadata>& metadata) {
if (!metadata.has_value()) {
return device::mojom::VirtualSensorMetadata::New();
}
if (metadata->HasMinimumFrequency() && metadata->HasMaximumFrequency() &&
metadata->GetMinimumFrequency(0) > metadata->GetMaximumFrequency(0)) {
return base::unexpected(
Response::InvalidParams("The specified minimum frequency is higher "
"than the maximum frequency"));
}
auto virtual_sensor_metadata = device::mojom::VirtualSensorMetadata::New();
if (metadata->HasAvailable()) {
virtual_sensor_metadata->available = metadata->GetAvailable(true);
}
if (metadata->HasMinimumFrequency()) {
virtual_sensor_metadata->minimum_frequency =
metadata->GetMinimumFrequency(0);
}
if (metadata->HasMaximumFrequency()) {
virtual_sensor_metadata->maximum_frequency =
metadata->GetMaximumFrequency(0);
}
return virtual_sensor_metadata;
}
} // namespace
void EmulationHandler::GetOverriddenSensorInformation(
const Emulation::SensorType& type,
std::unique_ptr<GetOverriddenSensorInformationCallback> callback) {
if (!host_) {
callback->sendFailure(Response::InternalError());
return;
}
device::mojom::SensorType sensor_type;
if (auto response = ConvertSensorType(type, &sensor_type);
!response.IsSuccess()) {
callback->sendFailure(response);
return;
}
auto it = sensor_overrides_.find(sensor_type);
if (it == sensor_overrides_.end()) {
callback->sendFailure(Response::InvalidParams(kSensorIsNotOverridden));
return;
}
it->second->GetVirtualSensorInformation(base::BindOnce(
[](std::unique_ptr<GetOverriddenSensorInformationCallback> callback,
device::mojom::GetVirtualSensorInformationResultPtr result) {
if (result->is_error()) {
switch (result->get_error()) {
case device::mojom::GetVirtualSensorInformationError::
kSensorTypeNotOverridden:
callback->sendFailure(
Response::InvalidParams(kSensorIsNotOverridden));
return;
}
}
CHECK(result->is_info());
callback->sendSuccess(result->get_info()->sampling_frequency);
},
std::move(callback)));
}
void EmulationHandler::SetSensorOverrideEnabled(
bool enabled,
const Emulation::SensorType& type,
Maybe<Emulation::SensorMetadata> metadata,
std::unique_ptr<SetSensorOverrideEnabledCallback> callback) {
if (!host_) {
callback->sendFailure(Response::InternalError());
return;
}
device::mojom::SensorType sensor_type;
if (auto response = ConvertSensorType(type, &sensor_type);
!response.IsSuccess()) {
callback->sendFailure(response);
return;
}
if (enabled) {
auto virtual_sensor_metadata = ParseSensorMetadata(metadata);
if (!virtual_sensor_metadata.has_value()) {
callback->sendFailure(virtual_sensor_metadata.error());
return;
}
if (sensor_overrides_.contains(sensor_type)) {
callback->sendFailure(
Response::InvalidParams(kSensorIsAlreadyOverridden));
return;
}
auto virtual_sensor =
WebContentsSensorProviderProxy::GetOrCreate(GetWebContents())
->CreateVirtualSensorForDevTools(
sensor_type, std::move(virtual_sensor_metadata.value()));
if (!virtual_sensor) {
callback->sendFailure(
Response::InvalidParams(kSensorIsAlreadyOverridden));
return;
}
sensor_overrides_[sensor_type] = std::move(virtual_sensor);
} else {
sensor_overrides_.erase(sensor_type);
}
callback->sendSuccess();
}
void EmulationHandler::SetSensorOverrideReadings(
const Emulation::SensorType& type,
std::unique_ptr<Emulation::SensorReading> reading,
std::unique_ptr<SetSensorOverrideReadingsCallback> callback) {
if (!host_) {
callback->sendFailure(Response::InternalError());
return;
}
device::mojom::SensorType sensor_type;
if (auto response = ConvertSensorType(type, &sensor_type);
!response.IsSuccess()) {
callback->sendFailure(response);
return;
}
device::SensorReading device_reading;
if (auto response =
ConvertSensorReading(sensor_type, reading.get(), &device_reading);
!response.IsSuccess()) {
callback->sendFailure(response);
return;
}
auto it = sensor_overrides_.find(sensor_type);
if (it == sensor_overrides_.end()) {
callback->sendFailure(Response::InvalidParams(kSensorIsNotOverridden));
return;
}
it->second->UpdateVirtualSensor(
device_reading,
base::BindOnce(
[](std::unique_ptr<SetSensorOverrideReadingsCallback> callback,
device::mojom::UpdateVirtualSensorResult result) {
switch (result) {
case device::mojom::UpdateVirtualSensorResult::
kSensorTypeNotOverridden:
callback->sendFailure(
Response::InvalidParams(kSensorIsNotOverridden));
break;
case device::mojom::UpdateVirtualSensorResult::kSuccess:
callback->sendSuccess();
break;
}
},
std::move(callback)));
}
Response EmulationHandler::SetIdleOverride(bool is_user_active,
bool is_screen_unlocked) {
if (!host_)
return Response::InternalError();
host_->GetIdleManager()->SetIdleOverride(is_user_active, is_screen_unlocked);
return Response::Success();
}
Response EmulationHandler::ClearIdleOverride() {
if (!host_)
return Response::InternalError();
host_->GetIdleManager()->ClearIdleOverride();
return Response::Success();
}
Response EmulationHandler::SetGeolocationOverride(Maybe<double> latitude,
Maybe<double> longitude,
Maybe<double> accuracy) {
if (!host_)
return Response::InternalError();
auto* geolocation_context = GetWebContents()->GetGeolocationContext();
device::mojom::GeopositionResultPtr override_result;
if (latitude.has_value() && longitude.has_value() && accuracy.has_value()) {
auto position = device::mojom::Geoposition::New();
position->latitude = latitude.value();
position->longitude = longitude.value();
position->accuracy = accuracy.value();
position->timestamp = base::Time::Now();
if (!device::ValidateGeoposition(*position)) {
return Response::ServerError("Invalid geolocation");
}
override_result =
device::mojom::GeopositionResult::NewPosition(std::move(position));
} else {
override_result = device::mojom::GeopositionResult::NewError(
device::mojom::GeopositionError::New(
device::mojom::GeopositionErrorCode::kPositionUnavailable,
/*error_message=*/"", /*error_technical=*/""));
}
geolocation_context->SetOverride(std::move(override_result));
return Response::Success();
}
Response EmulationHandler::ClearGeolocationOverride() {
if (!host_)
return Response::InternalError();
auto* geolocation_context = GetWebContents()->GetGeolocationContext();
geolocation_context->ClearOverride();
return Response::Success();
}
Response EmulationHandler::SetEmitTouchEventsForMouse(
bool enabled,
Maybe<std::string> configuration) {
if (!host_)
return Response::InternalError();
if (host_->GetParentOrOuterDocument())
return Response::ServerError(kCommandIsOnlyAvailableAtTopTarget);
touch_emulation_enabled_ = enabled;
touch_emulation_configuration_ = configuration.value_or("");
UpdateTouchEventEmulationState();
return Response::Success();
}
Response EmulationHandler::CanEmulate(bool* result) {
#if BUILDFLAG(IS_ANDROID)
*result = false;
#else
*result = true;
if (host_) {
if (GetWebContents()->GetVisibleURL().SchemeIs(kChromeDevToolsScheme) ||
host_->GetRenderWidgetHost()->auto_resize_enabled())
*result = false;
}
#endif // BUILDFLAG(IS_ANDROID)
return Response::Success();
}
Response EmulationHandler::SetDeviceMetricsOverride(
int width,
int height,
double device_scale_factor,
bool mobile,
Maybe<double> scale,
Maybe<int> screen_width,
Maybe<int> screen_height,
Maybe<int> position_x,
Maybe<int> position_y,
Maybe<bool> dont_set_visible_size,
Maybe<Emulation::ScreenOrientation> screen_orientation,
Maybe<protocol::Page::Viewport> viewport,
Maybe<protocol::Emulation::DisplayFeature> display_feature,
Maybe<protocol::Emulation::DevicePosture> device_posture) {
const static int max_size = 10000000;
const static double max_scale = 10;
const static int max_orientation_angle = 360;
if (!host_ || host_->GetRenderWidgetHost()->auto_resize_enabled()) {
return Response::ServerError("Target does not support metrics override");
}
if (host_->GetParentOrOuterDocument())
return Response::ServerError(kCommandIsOnlyAvailableAtTopTarget);
if (screen_width.value_or(0) < 0 || screen_height.value_or(0) < 0 ||
screen_width.value_or(0) > max_size ||
screen_height.value_or(0) > max_size) {
return Response::InvalidParams(
"Screen width and height values must be positive, not greater than " +
base::NumberToString(max_size));
}
if (position_x.value_or(0) < 0 || position_y.value_or(0) < 0 ||
position_x.value_or(0) > screen_width.value_or(0) ||
position_y.value_or(0) > screen_height.value_or(0)) {
return Response::InvalidParams("View position should be on the screen");
}
if (width < 0 || height < 0 || width > max_size || height > max_size) {
return Response::InvalidParams(
"Width and height values must be positive, not greater than " +
base::NumberToString(max_size));
}
if (device_scale_factor < 0)
return Response::InvalidParams("deviceScaleFactor must be non-negative");
if (scale.value_or(1) <= 0 || scale.value_or(1) > max_scale) {
return Response::InvalidParams("scale must be positive, not greater than " +
base::NumberToString(max_scale));
}
display::mojom::ScreenOrientation orientationType =
display::mojom::ScreenOrientation::kUndefined;
int orientationAngle = 0;
if (screen_orientation.has_value()) {
Emulation::ScreenOrientation& orientation = screen_orientation.value();
orientationType = WebScreenOrientationTypeFromString(orientation.GetType());
if (orientationType == display::mojom::ScreenOrientation::kUndefined)
return Response::InvalidParams("Invalid screen orientation type value");
orientationAngle = orientation.GetAngle();
if (orientationAngle < 0 || orientationAngle >= max_orientation_angle) {
return Response::InvalidParams(
"Screen orientation angle must be non-negative, less than " +
base::NumberToString(max_orientation_angle));
}
}
std::optional<content::DisplayFeature> content_display_feature = std::nullopt;
if (display_feature.has_value()) {
protocol::Emulation::DisplayFeature& emu_display_feature =
display_feature.value();
std::optional<content::DisplayFeature::Orientation> disp_orientation =
DisplayFeatureOrientationTypeFromString(
emu_display_feature.GetOrientation());
if (!disp_orientation) {
return Response::InvalidParams(
"Invalid display feature orientation type");
}
content::DisplayFeature::ParamErrorEnum error;
content_display_feature = content::DisplayFeature::Create(
*disp_orientation, emu_display_feature.GetOffset(),
emu_display_feature.GetMaskLength(), width, height, &error);
if (!content_display_feature) {
switch (error) {
case content::DisplayFeature::ParamErrorEnum::
kDisplayFeatureWithZeroScreenSize:
return Response::InvalidParams(
"Cannot specify a display feature with zero width and height");
case content::DisplayFeature::ParamErrorEnum::
kNegativeDisplayFeatureParams:
return Response::InvalidParams("Negative display feature parameters");
case content::DisplayFeature::ParamErrorEnum::kOutsideScreenWidth:
return Response::InvalidParams(
"Display feature viewport segments outside screen width");
case content::DisplayFeature::ParamErrorEnum::kOutsideScreenHeight:
return Response::InvalidParams(
"Display feature viewport segments outside screen height");
}
}
}
blink::DeviceEmulationParams params;
params.screen_type = mobile ? blink::mojom::EmulatedScreenType::kMobile
: blink::mojom::EmulatedScreenType::kDesktop;
params.screen_size =
gfx::Size(screen_width.value_or(0), screen_height.value_or(0));
if (position_x.has_value() && position_y.has_value()) {
params.view_position =
gfx::Point(position_x.value_or(0), position_y.value_or(0));
}
params.device_scale_factor = device_scale_factor;
params.view_size = gfx::Size(width, height);
params.scale = scale.value_or(1);
params.screen_orientation_type = orientationType;
params.screen_orientation_angle = orientationAngle;
if (content_display_feature) {
params.viewport_segments =
content_display_feature->ComputeViewportSegments(params.view_size);
}
if (device_posture.has_value()) {
params.device_posture =
DevicePostureTypeFromString(device_posture.value().GetType()).value();
}
if (viewport.has_value()) {
params.viewport_offset.SetPoint(viewport->GetX(), viewport->GetY());
double dpfactor =
device_scale_factor
? device_scale_factor /
host_->GetRenderWidgetHost()->GetDeviceScaleFactor()
: 1;
params.viewport_scale = viewport->GetScale() * dpfactor;
// Resize the RenderWidgetHostView to the size of the overridden viewport.
width = base::ClampRound(viewport->GetWidth() * params.viewport_scale);
height = base::ClampRound(viewport->GetHeight() * params.viewport_scale);
}
bool size_changed = false;
if (!dont_set_visible_size.value_or(false) && width > 0 && height > 0) {
if (GetWebContents()) {
size_changed =
GetWebContents()->SetDeviceEmulationSize(gfx::Size(width, height));
} else {
return Response::ServerError("Can't find the associated web contents");
}
}
if (device_emulation_enabled_ && params == device_emulation_params_) {
// Renderer should answer after size was changed, so that the response is
// only sent to the client once updates were applied.
if (size_changed)
return Response::FallThrough();
return Response::Success();
}
device_emulation_enabled_ = true;
device_emulation_params_ = params;
UpdateDeviceEmulationState();
// Renderer should answer after emulation params were updated, so that the
// response is only sent to the client once updates were applied.
// Unless the renderer has crashed.
if (GetWebContents() && GetWebContents()->IsCrashed())
return Response::Success();
return Response::FallThrough();
}
Response EmulationHandler::ClearDeviceMetricsOverride() {
if (!host_)
return Response::ServerError("Can't find the associated web contents");
if (host_->GetParentOrOuterDocument())
return Response::ServerError(kCommandIsOnlyAvailableAtTopTarget);
if (!device_emulation_enabled_)
return Response::Success();
GetWebContents()->ClearDeviceEmulationSize();
device_emulation_enabled_ = false;
device_emulation_params_ = blink::DeviceEmulationParams();
UpdateDeviceEmulationState();
// Renderer should answer after emulation was disabled, so that the response
// is only sent to the client once updates were applied.
// Unless the renderer has crashed.
if (GetWebContents()->IsCrashed())
return Response::Success();
return Response::FallThrough();
}
Response EmulationHandler::SetVisibleSize(int width, int height) {
if (width < 0 || height < 0)
return Response::InvalidParams("Width and height must be non-negative");
if (!host_)
return Response::ServerError("Can't find the associated web contents");
GetWebContents()->SetDeviceEmulationSize(gfx::Size(width, height));
return Response::Success();
}
Response EmulationHandler::SetUserAgentOverride(
const std::string& user_agent,
Maybe<std::string> accept_language,
Maybe<std::string> platform,
Maybe<Emulation::UserAgentMetadata> ua_metadata_override) {
if (!user_agent.empty() && !net::HttpUtil::IsValidHeaderValue(user_agent))
return Response::InvalidParams("Invalid characters found in userAgent");
std::string accept_lang = accept_language.value_or(std::string());
if (!accept_lang.empty() && !net::HttpUtil::IsValidHeaderValue(accept_lang)) {
return Response::InvalidParams(
"Invalid characters found in acceptLanguage");
}
user_agent_ = user_agent;
accept_language_ = accept_lang;
user_agent_metadata_ = std::nullopt;
if (!ua_metadata_override.has_value()) {
return Response::FallThrough();
}
if (user_agent.empty()) {
return Response::InvalidParams(
"Empty userAgent invalid with userAgentMetadata provided");
}
Emulation::UserAgentMetadata& ua_metadata = ua_metadata_override.value();
blink::UserAgentMetadata new_ua_metadata;
blink::UserAgentMetadata default_ua_metadata =
GetContentClient()->browser()->GetUserAgentMetadata();
if (ua_metadata.HasBrands()) {
for (const auto& bv : *ua_metadata.GetBrands(nullptr)) {
blink::UserAgentBrandVersion out_bv;
if (!ValidateClientHintString(bv->GetBrand()))
return Response::InvalidParams("Invalid brand string");
out_bv.brand = bv->GetBrand();
if (!ValidateClientHintString(bv->GetVersion()))
return Response::InvalidParams("Invalid brand version string");
out_bv.version = bv->GetVersion();
new_ua_metadata.brand_version_list.push_back(std::move(out_bv));
}
} else {
new_ua_metadata.brand_version_list =
std::move(default_ua_metadata.brand_version_list);
}
if (ua_metadata.HasFullVersionList()) {
for (const auto& bv : *ua_metadata.GetFullVersionList(nullptr)) {
blink::UserAgentBrandVersion out_bv;
if (!ValidateClientHintString(bv->GetBrand()))
return Response::InvalidParams("Invalid brand string");
out_bv.brand = bv->GetBrand();
if (!ValidateClientHintString(bv->GetVersion()))
return Response::InvalidParams("Invalid brand version string");
out_bv.version = bv->GetVersion();
new_ua_metadata.brand_full_version_list.push_back(std::move(out_bv));
}
} else {
new_ua_metadata.brand_full_version_list =
std::move(default_ua_metadata.brand_full_version_list);
}
if (ua_metadata.HasFullVersion()) {
String full_version = ua_metadata.GetFullVersion("");
if (!ValidateClientHintString(full_version))
return Response::InvalidParams("Invalid full version string");
new_ua_metadata.full_version = full_version;
} else {
new_ua_metadata.full_version = std::move(default_ua_metadata.full_version);
}
if (!ValidateClientHintString(ua_metadata.GetPlatform())) {
return Response::InvalidParams("Invalid platform string");
}
new_ua_metadata.platform = ua_metadata.GetPlatform();
if (!ValidateClientHintString(ua_metadata.GetPlatformVersion())) {
return Response::InvalidParams("Invalid platform version string");
}
new_ua_metadata.platform_version = ua_metadata.GetPlatformVersion();
if (!ValidateClientHintString(ua_metadata.GetArchitecture())) {
return Response::InvalidParams("Invalid architecture string");
}
new_ua_metadata.architecture = ua_metadata.GetArchitecture();
if (!ValidateClientHintString(ua_metadata.GetModel())) {
return Response::InvalidParams("Invalid model string");
}
new_ua_metadata.model = ua_metadata.GetModel();
new_ua_metadata.mobile = ua_metadata.GetMobile();
if (ua_metadata.HasBitness()) {
String bitness = ua_metadata.GetBitness("");
if (!ValidateClientHintString(bitness))
return Response::InvalidParams("Invalid bitness string");
new_ua_metadata.bitness = std::move(bitness);
} else {
new_ua_metadata.bitness = std::move(default_ua_metadata.bitness);
}
if (ua_metadata.HasWow64()) {
new_ua_metadata.wow64 = ua_metadata.GetWow64(false);
} else {
new_ua_metadata.wow64 = default_ua_metadata.wow64;
}
// All checks OK, can update user_agent_metadata_.
user_agent_metadata_.emplace(std::move(new_ua_metadata));
return Response::FallThrough();
}
Response EmulationHandler::SetFocusEmulationEnabled(bool enabled) {
if (enabled == focus_emulation_enabled_)
return Response::FallThrough();
focus_emulation_enabled_ = enabled;
if (enabled) {
capture_handle_ =
GetWebContents()->IncrementCapturerCount(gfx::Size(),
/*stay_hidden=*/false,
/*stay_awake=*/false);
} else {
capture_handle_.RunAndReset();
}
return Response::FallThrough();
}
Response EmulationHandler::SetEmulatedMedia(
Maybe<std::string> media,
Maybe<protocol::Array<protocol::Emulation::MediaFeature>> features) {
if (!host_)
return Response::InternalError();
prefers_color_scheme_ = "";
prefers_reduced_motion_ = "";
prefers_reduced_transparency_ = "";
if (features.has_value()) {
for (auto const& mediaFeature : features.value()) {
auto const& name = mediaFeature->GetName();
auto const& value = mediaFeature->GetValue();
if (name == "prefers-color-scheme") {
prefers_color_scheme_ = (value == network::kPrefersColorSchemeLight ||
value == network::kPrefersColorSchemeDark)
? value
: "";
} else if (name == "prefers-reduced-motion") {
prefers_reduced_motion_ =
(value == network::kPrefersReducedMotionReduce) ? value : "";
} else if (name == "prefers-reduced-transparency") {
prefers_reduced_transparency_ =
(value == network::kPrefersReducedTransparencyReduce) ? value : "";
}
}
}
return Response::FallThrough();
}
blink::DeviceEmulationParams EmulationHandler::GetDeviceEmulationParams() {
return device_emulation_params_;
}
void EmulationHandler::SetDeviceEmulationParams(
const blink::DeviceEmulationParams& params) {
DCHECK(host_);
// Device emulation only happens on the outermost main frame.
DCHECK(!host_->GetParentOrOuterDocument());
bool enabled = params != blink::DeviceEmulationParams();
bool enable_changed = enabled != device_emulation_enabled_;
bool params_changed = params != device_emulation_params_;
if (!device_emulation_enabled_ && !enable_changed)
return; // Still disabled.
if (!enable_changed && !params_changed)
return; // Nothing changed.
device_emulation_enabled_ = enabled;
device_emulation_params_ = params;
UpdateDeviceEmulationState();
}
WebContentsImpl* EmulationHandler::GetWebContents() {
DCHECK(host_); // Only call if |host_| is set.
return static_cast<WebContentsImpl*>(WebContents::FromRenderFrameHost(host_));
}
void EmulationHandler::UpdateTouchEventEmulationState() {
if (!host_)
return;
// We only have a single TouchEmulator for all frames, so let the main frame's
// EmulationHandler enable/disable it.
DCHECK(!host_->GetParentOrOuterDocument());
if (touch_emulation_enabled_) {
if (auto* touch_emulator =
host_->GetRenderWidgetHost()->GetTouchEmulator()) {
touch_emulator->Enable(
TouchEmulator::Mode::kEmulatingTouchFromMouse,
TouchEmulationConfigurationToType(touch_emulation_configuration_));
}
} else {
if (auto* touch_emulator = host_->GetRenderWidgetHost()->GetTouchEmulator())
touch_emulator->Disable();
}
GetWebContents()->SetForceDisableOverscrollContent(touch_emulation_enabled_);
}
void EmulationHandler::UpdateDeviceEmulationState() {
if (!host_)
return;
// Device emulation only happens on the outermost main frame.
DCHECK(!host_->GetParentOrOuterDocument());
// TODO(eseckler): Once we change this to mojo, we should wait for an ack to
// these messages from the renderer. The renderer should send the ack once the
// emulation params were applied. That way, we can avoid having to handle
// Set/ClearDeviceMetricsOverride in the renderer. With the old IPC system,
// this is tricky since we'd have to track the DevTools message id with the
// WidgetMsg and acknowledgment, as well as plump the acknowledgment back to
// the EmulationHandler somehow. Mojo callbacks should make this much simpler.
host_->ForEachRenderFrameHostIncludingSpeculative(
[this](RenderFrameHostImpl* host) {
// The main frame of nested subpages (ex. fenced frames, portals) inside
// this page are updated as well.
if (host->is_main_frame())
UpdateDeviceEmulationStateForHost(host->GetRenderWidgetHost());
});
}
void EmulationHandler::UpdateDeviceEmulationStateForHost(
RenderWidgetHostImpl* render_widget_host) {
auto& frame_widget = render_widget_host->GetAssociatedFrameWidget();
if (!frame_widget)
return;
if (device_emulation_enabled_) {
frame_widget->EnableDeviceEmulation(device_emulation_params_);
} else {
frame_widget->DisableDeviceEmulation();
}
}
Response EmulationHandler::SetDevicePostureOverride(
std::unique_ptr<protocol::Emulation::DevicePosture> posture) {
ASSIGN_OR_RETURN(blink::mojom::DevicePostureType posture_type,
DevicePostureTypeFromString(posture->GetType()));
device_posture_emulation_enabled_ = true;
GetWebContents()
->GetDevicePostureProvider()
->OverrideDevicePostureForEmulation(posture_type);
return Response::Success();
}
Response EmulationHandler::ClearDevicePostureOverride() {
if (device_posture_emulation_enabled_) {
device_posture_emulation_enabled_ = false;
GetWebContents()
->GetDevicePostureProvider()
->DisableDevicePostureOverrideForEmulation();
}
return Response::Success();
}
void EmulationHandler::ApplyOverrides(net::HttpRequestHeaders* headers,
bool* user_agent_overridden,
bool* accept_language_overridden) {
if (!user_agent_.empty()) {
headers->SetHeader(net::HttpRequestHeaders::kUserAgent, user_agent_);
}
*user_agent_overridden = !user_agent_.empty();
if (!accept_language_.empty()) {
headers->SetHeader(
net::HttpRequestHeaders::kAcceptLanguage,
net::HttpUtil::GenerateAcceptLanguageHeader(accept_language_));
}
*accept_language_overridden = !accept_language_.empty();
if (!prefers_color_scheme_.empty()) {
const auto& prefers_color_scheme_client_hint_name =
network::GetClientHintToNameMap().at(
network::mojom::WebClientHintsType::kPrefersColorScheme);
if (headers->HasHeader(prefers_color_scheme_client_hint_name)) {
headers->SetHeader(prefers_color_scheme_client_hint_name,
prefers_color_scheme_);
}
}
if (!prefers_reduced_motion_.empty()) {
const auto& prefers_reduced_motion_client_hint_name =
network::GetClientHintToNameMap().at(
network::mojom::WebClientHintsType::kPrefersReducedMotion);
if (headers->HasHeader(prefers_reduced_motion_client_hint_name)) {
headers->SetHeader(prefers_reduced_motion_client_hint_name,
prefers_reduced_motion_);
}
}
if (!prefers_reduced_transparency_.empty()) {
const auto& prefers_reduced_transparency_client_hint_name =
network::GetClientHintToNameMap().at(
network::mojom::WebClientHintsType::kPrefersReducedTransparency);
if (headers->HasHeader(prefers_reduced_transparency_client_hint_name)) {
headers->SetHeader(prefers_reduced_transparency_client_hint_name,
prefers_reduced_transparency_);
}
}
}
bool EmulationHandler::ApplyUserAgentMetadataOverrides(
std::optional<blink::UserAgentMetadata>* override_out) {
// This is conditional on basic user agent override being on; this helps us
// emulate a device not sending any UA client hints.
if (user_agent_.empty())
return false;
*override_out = user_agent_metadata_;
return true;
}
void EmulationHandler::ApplyNetworkOverridesForDownload(
download::DownloadUrlParameters* parameters) {
net::HttpRequestHeaders headers;
bool user_agent_overridden;
bool accept_language_overridden;
ApplyOverrides(&headers, &user_agent_overridden, &accept_language_overridden);
for (net::HttpRequestHeaders::Iterator it(headers); it.GetNext();) {
parameters->add_request_header(it.name(), it.value());
}
}
} // namespace protocol
} // namespace content