blob: a248edf324eb3663d5cdfb2a5c4bfb3945eaf416 [file] [log] [blame]
// Copyright 2023 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/plus_addresses/plus_address_parsing_utils.h"
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/strings/pattern.h"
#include "base/strings/string_number_conversions.h"
#include "components/affiliations/core/browser/affiliation_utils.h"
#include "components/plus_addresses/core/browser/plus_address_types.h"
#include "components/plus_addresses/core/common/features.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
namespace plus_addresses {
namespace {
// Creates a PlusProfile for `dict` if it fits this schema (in TS notation):
// {
// "ProfileId": string,
// "facet": string,
// "plusEmail": {
// "plusAddress": string,
// "plusMode": string,
// }
// }
// Returns nullopt if none of the values are parsed.
std::optional<PlusProfile> ParsePlusProfileFromV1Dict(base::Value::Dict dict) {
std::string profile_id;
std::string facet_str;
PlusAddress plus_address;
std::optional<bool> is_confirmed;
for (std::pair<const std::string&, base::Value&> entry : dict) {
auto [key, val] = entry;
if (base::MatchPattern(key, "*ProfileId") && val.is_string()) {
profile_id = std::move(val.GetString());
continue;
}
if (base::MatchPattern(key, "facet") && val.is_string()) {
facet_str = std::move(val.GetString());
continue;
}
if (base::MatchPattern(key, "*Email") && val.is_dict()) {
for (std::pair<const std::string&, base::Value&> email_entry :
val.GetDict()) {
auto [email_key, email_val] = email_entry;
if (!email_val.is_string()) {
continue;
}
if (base::MatchPattern(email_key, "*Address")) {
plus_address = PlusAddress(std::move(email_val.GetString()));
}
if (base::MatchPattern(email_key, "*Mode")) {
is_confirmed =
!base::MatchPattern(email_val.GetString(), "*UNSPECIFIED");
}
}
}
}
if (profile_id.empty() || facet_str.empty() || plus_address->empty() ||
!is_confirmed.has_value()) {
return std::nullopt;
}
affiliations::FacetURI facet =
affiliations::FacetURI::FromPotentiallyInvalidSpec(facet_str);
if (!facet.is_valid()) {
return std::nullopt;
}
return PlusProfile(std::move(profile_id), std::move(facet),
std::move(plus_address), *is_confirmed);
}
// Attempts to parse a string of format "[0-9]+s" into a `base::TimeDelta`.
std::optional<base::TimeDelta> ParseLifetime(std::string* str) {
if (!str) {
return std::nullopt;
}
if (str->empty() || !str->ends_with('s')) {
return std::nullopt;
}
int64_t seconds;
if (!base::StringToInt64(std::string_view(*str).substr(0, str->size() - 1),
&seconds)) {
return std::nullopt;
}
return base::Seconds(seconds);
}
// Attempts to parse a `base::Value::Dict` into to a `PreallocatedPlusAddress`.
std::optional<PreallocatedPlusAddress> ParsePreallocatedPlusAddress(
base::Value::Dict dict) {
static constexpr std::string_view kAddressKey = "emailAddress";
static constexpr std::string_view kLifetimeKey = "reservationLifetime";
PlusAddress address;
if (std::string* address_str = dict.FindString(kAddressKey)) {
address = PlusAddress(std::move(*address_str));
} else {
return std::nullopt;
}
base::TimeDelta lifetime;
if (std::optional<base::TimeDelta> time =
ParseLifetime(dict.FindString(kLifetimeKey))) {
lifetime = *time;
} else {
return std::nullopt;
}
return std::make_optional<PreallocatedPlusAddress>(std::move(address),
lifetime);
}
} // namespace
std::optional<PlusProfile> ParsePlusProfileFromV1Create(
data_decoder::DataDecoder::ValueOrError response) {
if (!response.has_value() || !response->is_dict()) {
return std::nullopt;
}
// Use iterators to avoid looking up by JSON keys.
base::Value::List* existing_profiles = nullptr;
for (std::pair<const std::string&, base::Value&> first_level_entry :
response->GetDict()) {
auto [first_key, first_val] = first_level_entry;
if (base::MatchPattern(first_key, "*Profile") && first_val.is_dict()) {
return ParsePlusProfileFromV1Dict(std::move(first_val.GetDict()));
}
if (base::MatchPattern(first_key, "existing*Profiles") &&
first_val.is_list()) {
existing_profiles = first_val.GetIfList();
}
}
if (!existing_profiles || existing_profiles->empty() ||
!base::FeatureList::IsEnabled(
features::kPlusAddressParseExistingProfilesFromCreateResponse)) {
return std::nullopt;
}
// If kPlusAddressParseExistingProfilesFromCreateResponse is enabled, there is
// no entry for a newly created profile, but there is an entry for existing
// profiles, then we return the first existing profile. This scenario can
// occur if the client tries to create a plus profile on a domain for which
// the server already has affiliated profiles. In theory, there could even be
// multiple such profiles (due to changing affiliations over time), but to
// save complexity, we currently just pick the first profile and return it.
// At a later point, we may choose to add logic that picks one out of multiple
// profiles further downstream - in that case, we would need to change the
// signature of this function.
base::Value::Dict* first_existing_profile =
(*existing_profiles)[0].GetIfDict();
return first_existing_profile
? ParsePlusProfileFromV1Dict(std::move(*first_existing_profile))
: std::nullopt;
}
std::optional<std::vector<PreallocatedPlusAddress>>
ParsePreallocatedPlusAddresses(
data_decoder::DataDecoder::ValueOrError response) {
static constexpr std::string_view kAddressesKey = "emailAddresses";
if (!response.has_value() || !response->is_dict()) {
return std::nullopt;
}
base::Value::List* addresses = response->GetDict().FindList(kAddressesKey);
if (!addresses) {
return std::nullopt;
}
std::vector<PreallocatedPlusAddress> result;
result.reserve(addresses->size());
for (base::Value& entry : *addresses) {
if (!entry.is_dict()) {
continue;
}
if (std::optional<PreallocatedPlusAddress> address =
ParsePreallocatedPlusAddress(std::move(entry.GetDict()))) {
result.push_back(*std::move(address));
}
}
return std::move(result);
}
} // namespace plus_addresses