blob: 96e78e882e5779158a5b824f3db114215db7b487 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/data_sharing/internal/preview_server_proxy.h"
#include <optional>
#include <utility>
#include "base/base64.h"
#include "base/json/json_reader.h"
#include "base/json/values_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "components/data_sharing/public/features.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
namespace data_sharing {
namespace {
// RPC timeout duration.
constexpr base::TimeDelta kTimeout = base::Milliseconds(5000);
// Content type for network request.
constexpr char kContentType[] = "application/json; charset=UTF-8";
// OAuth name.
constexpr char kOAuthName[] = "shared_data_preview";
// OAuth scope of the server.
constexpr char kOAuthScope[] = "https://www.googleapis.com/auth/chromesync";
// Server address to get preview data.
constexpr char kDefaultServiceBaseUrl[] =
"https://autopush-chromesyncsharedentities-pa.sandbox.googleapis.com/v1";
constexpr base::FeatureParam<std::string> kServiceBaseUrl{
&features::kDataSharingFeature, "preview_service_base_url",
kDefaultServiceBaseUrl};
// How many share entities to retrieve for preview.
constexpr int kDefaultPreviewDataSize = 100;
constexpr base::FeatureParam<int> kPreviewDataSize{
&features::kDataSharingFeature, "preview_data_size",
kDefaultPreviewDataSize};
// lowerCamelCase JSON proto message keys.
constexpr char kSharedEntitiesKey[] = "sharedEntities";
constexpr char kClientTagHashKey[] = "clientTagHash";
constexpr char kDeletedKey[] = "deleted";
constexpr char kNameKey[] = "name";
constexpr char kVersionKey[] = "version";
constexpr char kCollaborationKey[] = "collaboration";
constexpr char kCollaborationIdKey[] = "collaborationId";
constexpr char kCreateTimeKey[] = "createTime";
constexpr char kUpdateTimeKey[] = "updateTime";
constexpr char kNanosKey[] = "nanos";
constexpr char kSecondsKey[] = "seconds";
constexpr char kSpecificsKey[] = "specifics";
constexpr char kSharedGroupDataKey[] = "sharedTabGroupData";
constexpr char kGuidKey[] = "guid";
constexpr char kUpdateTimeWindowsEpochMicrosKey[] =
"updateTimeWindowsEpochMicros";
constexpr char kTabKey[] = "tab";
constexpr char kTabGroupKey[] = "tabGroup";
constexpr char kUrlKey[] = "url";
constexpr char kTitleKey[] = "title";
constexpr char kFaviconUrlKey[] = "faviconUrl";
constexpr char kSharedTabGroupGuidKey[] = "sharedTabGroupGuid";
constexpr char kUniquePositionKey[] = "uniquePosition";
constexpr char kCustomCompressedV1Key[] = "customCompressedV1";
constexpr char kColorKey[] = "color";
// Network annotation for getting preview data from server.
constexpr net::NetworkTrafficAnnotationTag
kGetSharedDataPreviewTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("chrome_data_sharing_preview",
R"(
semantics {
sender: "Chrome Data Sharing"
description:
"Ask server for a preview of the data shared to a group."
trigger:
"A Chrome-initiated request that requires user enabling the "
"data sharing feature. The request is sent after user receives "
"an invitation link to join a group, and click on a button to "
"get a preview of the data shared to that group."
user_data {
type: OTHER
type: ACCESS_TOKEN
}
data:
"Group ID and access token obtained from the invitation that "
"the user has received."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts { email: "chrome-data-sharing-eng@google.com" }
}
last_reviewed: "2024-08-20"
}
policy {
cookies_allowed: NO
setting:
"This fetch is enabled for any non-enterprise user that has "
"the data sharing feature enabled and is signed in."
chrome_policy {}
}
)");
// Find a a value for a field from a child dictionary in json.
std::optional<std::string> GetFieldValueFromChildDict(
const base::Value::Dict& parent_dict,
const std::string& child_dict_name,
const std::string& field_name) {
auto* child_dict = parent_dict.FindDict(child_dict_name);
if (!child_dict) {
return std::nullopt;
}
auto* field_value = child_dict->FindString(field_name);
if (!field_value) {
return std::nullopt;
}
return *field_value;
}
// Parse the shared tab from the dict.
std::optional<sync_pb::SharedTab> ParseSharedTab(
const base::Value::Dict& dict) {
auto* url = dict.FindString(kUrlKey);
if (!url) {
return std::nullopt;
}
auto* title = dict.FindString(kTitleKey);
if (!title) {
return std::nullopt;
}
auto* shared_tab_group_guid = dict.FindString(kSharedTabGroupGuidKey);
if (!shared_tab_group_guid) {
return std::nullopt;
}
auto custom_compressed = GetFieldValueFromChildDict(dict, kUniquePositionKey,
kCustomCompressedV1Key);
std::optional<sync_pb::SharedTab> shared_tab =
std::make_optional<sync_pb::SharedTab>();
shared_tab->set_url(*url);
shared_tab->set_title(*title);
shared_tab->set_shared_tab_group_guid(*shared_tab_group_guid);
auto* favicon_url = dict.FindString(kFaviconUrlKey);
if (favicon_url) {
shared_tab->set_favicon_url(*favicon_url);
}
if (custom_compressed) {
shared_tab->mutable_unique_position()->set_custom_compressed_v1(
custom_compressed.value());
}
return shared_tab;
}
// Parse the entity specifics from the dict.
std::optional<sync_pb::EntitySpecifics> ParseEntitySpecifics(
const base::Value::Dict& dict) {
auto* shared_tab_group_dict = dict.FindDict(kSharedGroupDataKey);
if (!shared_tab_group_dict) {
return std::nullopt;
}
std::optional<sync_pb::EntitySpecifics> specifics =
std::make_optional<sync_pb::EntitySpecifics>();
sync_pb::SharedTabGroupDataSpecifics* tab_group_data =
specifics->mutable_shared_tab_group_data();
auto* guid = shared_tab_group_dict->FindString(kGuidKey);
if (!guid) {
return std::nullopt;
}
tab_group_data->set_guid(*guid);
auto* update_time_str =
shared_tab_group_dict->FindString(kUpdateTimeWindowsEpochMicrosKey);
uint64_t update_time;
if (update_time_str && base::StringToUint64(*update_time_str, &update_time)) {
tab_group_data->set_update_time_windows_epoch_micros(update_time);
}
auto* tab_dict = shared_tab_group_dict->FindDict(kTabKey);
if (tab_dict) {
auto shared_tab = ParseSharedTab(*tab_dict);
if (shared_tab) {
*(tab_group_data->mutable_tab()) = std::move(shared_tab.value());
} else {
return std::nullopt;
}
} else {
auto* tab_group_dict = shared_tab_group_dict->FindDict(kTabGroupKey);
if (tab_group_dict) {
auto* title = tab_group_dict->FindString(kTitleKey);
if (!title) {
return std::nullopt;
}
sync_pb::SharedTabGroup* shared_tab_group =
tab_group_data->mutable_tab_group();
shared_tab_group->set_title(*title);
auto* color = tab_group_dict->FindString(kColorKey);
if (color) {
sync_pb::SharedTabGroup::Color group_color;
SharedTabGroup_Color_Parse(*color, &group_color);
shared_tab_group->set_color(group_color);
}
} else {
return std::nullopt;
}
}
return specifics;
}
// Returns a time for a timestamp object in a child dict.
std::optional<base::Time> GetTimeFromDict(const base::Value::Dict& dict,
const std::string& timestamp_name) {
auto* time_stamp_dict = dict.FindDict(timestamp_name);
if (!time_stamp_dict) {
return std::nullopt;
}
auto* seconds_str = time_stamp_dict->FindString(kSecondsKey);
uint64_t seconds;
if (!seconds_str || !base::StringToUint64(*seconds_str, &seconds)) {
return std::nullopt;
}
auto nanos = time_stamp_dict->FindInt(kNanosKey);
return base::Time::FromSecondsSinceUnixEpoch(
seconds + (nanos ? (nanos.value() /
static_cast<double>(base::Seconds(1).InNanoseconds()))
: 0.0));
}
// Deserializes a shared entity ID from JSON.
std::optional<SharedEntity> Deserialize(const base::Value& value) {
if (!value.is_dict()) {
return std::nullopt;
}
const base::Value::Dict& value_dict = value.GetDict();
// Check if entry is deleted.
auto deleted = value_dict.FindBool(kDeletedKey);
if (deleted.has_value() && deleted.value()) {
return std::nullopt;
}
std::optional<SharedEntity> entity = std::make_optional<SharedEntity>();
// Get group id.
auto collaboration_id = GetFieldValueFromChildDict(
value_dict, kCollaborationKey, kCollaborationIdKey);
if (!collaboration_id) {
return std::nullopt;
}
entity->group_id = GroupId(*collaboration_id);
// Get entity specifics.
auto* specifics_dict = value_dict.FindDict(kSpecificsKey);
if (!specifics_dict) {
return std::nullopt;
}
auto specifics = ParseEntitySpecifics(*specifics_dict);
if (specifics) {
entity->specifics = std::move(specifics.value());
} else {
return std::nullopt;
}
// Get client tag hash.
auto* type = value_dict.FindString(kClientTagHashKey);
if (type) {
entity->client_tag_hash = *type;
}
// Get name.
auto* name = value_dict.FindString(kNameKey);
if (name) {
entity->name = *name;
}
// Get version.
auto* version_string = value_dict.FindString(kVersionKey);
uint64_t version;
if (version_string && base::StringToUint64(*version_string, &version)) {
entity->version = version;
}
// Get time.
auto create_time = GetTimeFromDict(value_dict, kCreateTimeKey);
if (create_time) {
entity->create_time = *create_time;
}
auto update_time = GetTimeFromDict(value_dict, kUpdateTimeKey);
if (update_time) {
entity->update_time = *update_time;
}
return entity;
}
} // namespace
PreviewServerProxy::PreviewServerProxy(
signin::IdentityManager* identity_manager,
const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory)
: identity_manager_(identity_manager),
url_loader_factory_(url_loader_factory) {}
PreviewServerProxy::~PreviewServerProxy() = default;
void PreviewServerProxy::GetSharedDataPreview(
const GroupToken& group_token,
base::OnceCallback<
void(const DataSharingService::SharedDataPreviewOrFailureOutcome&)>
callback) {
if (!group_token.IsValid()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback),
base::unexpected(DataSharingService::PeopleGroupActionFailure::
kPersistentFailure)));
return;
}
// Path in the URL to get shared entnties preview, {collaborationId} needs to
// be replaced by the caller.
const char kSharedEntitiesPreviewPath[] =
"collaborations/{collaborationId}/dataTypes/-/sharedEntities:preview";
std::string shared_entities_preview_path = kSharedEntitiesPreviewPath;
base::ReplaceFirstSubstringAfterOffset(
&shared_entities_preview_path, 0, "{collaborationId}",
base::Base64Encode(group_token.group_id.value()));
GURL url = GURL(
kServiceBaseUrl.Get().append("/").append(shared_entities_preview_path));
// Query string in the URL to get shared entnties preview. {token} needs to
// be replaced by the caller. {pageSize} can be configured through finch.
const std::string kQueryString =
"accessToken={token}&pageToken=&pageSize={pageSize}";
std::string query_str = kQueryString;
base::ReplaceFirstSubstringAfterOffset(&query_str, 0, "{token}",
group_token.access_token);
base::ReplaceFirstSubstringAfterOffset(
&query_str, 0, "{pageSize}",
base::NumberToString(kPreviewDataSize.Get()));
GURL::Replacements replacements;
replacements.SetQueryStr(query_str);
url = url.ReplaceComponents(replacements);
auto fetcher = CreateEndpointFetcher(url);
auto* const fetcher_ptr = fetcher.get();
fetcher_ptr->Fetch(base::BindOnce(&PreviewServerProxy::HandleServerResponse,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback), std::move(fetcher)));
}
std::unique_ptr<EndpointFetcher> PreviewServerProxy::CreateEndpointFetcher(
const GURL& url) {
return std::make_unique<EndpointFetcher>(
url_loader_factory_, kOAuthName, url, net::HttpRequestHeaders::kGetMethod,
kContentType, std::vector<std::string>{kOAuthScope}, kTimeout,
/* post_data= */ std::string(), kGetSharedDataPreviewTrafficAnnotation,
identity_manager_, signin::ConsentLevel::kSignin);
}
void PreviewServerProxy::HandleServerResponse(
base::OnceCallback<void(
const DataSharingService::SharedDataPreviewOrFailureOutcome&)> callback,
std::unique_ptr<EndpointFetcher> endpoint_fetcher,
std::unique_ptr<EndpointResponse> response) {
if (response->http_status_code != net::HTTP_OK || response->error_type) {
DLOG(ERROR) << "Got bad response (" << response->http_status_code
<< ") for shared data preview!";
std::move(callback).Run(base::unexpected(
DataSharingService::PeopleGroupActionFailure::kTransientFailure));
return;
}
data_decoder::DataDecoder::ParseJsonIsolated(
response->response,
base::BindOnce(&PreviewServerProxy::OnResponseJsonParsed,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void PreviewServerProxy::OnResponseJsonParsed(
base::OnceCallback<void(
const DataSharingService::SharedDataPreviewOrFailureOutcome&)> callback,
data_decoder::DataDecoder::ValueOrError result) {
SharedDataPreview preview;
if (result.has_value() && result->is_dict()) {
if (auto* response_json = result->GetDict().FindList(kSharedEntitiesKey)) {
for (const auto& shared_entity_json : *response_json) {
if (auto shared_entity = Deserialize(shared_entity_json)) {
if (shared_entity) {
preview.shared_entities.push_back(std::move(*shared_entity));
}
}
}
}
}
if (preview.shared_entities.empty()) {
std::move(callback).Run(base::unexpected(
DataSharingService::PeopleGroupActionFailure::kPersistentFailure));
} else {
std::move(callback).Run(std::move(preview));
}
}
} // namespace data_sharing