blob: 3cf20567b8f1cc9a6ad72c648e4ed244064fdadd [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/renderer/cart/commerce_hint_agent.h"
#include "base/features.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros_local.h"
#include "base/metrics/user_metrics.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/common/chrome_isolated_world_ids.h"
#include "chrome/grit/renderer_resources.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/commerce/core/commerce_heuristics_data.h"
#include "components/commerce/core/commerce_heuristics_data_metrics_helper.h"
#include "components/commerce/core/heuristics/commerce_heuristics_provider.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "services/metrics/public/cpp/mojo_ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/common/loader/http_body_element_type.h"
#include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h"
#include "third_party/blink/public/platform/web_http_body.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_element_collection.h"
#include "third_party/blink/public/web/web_form_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_script_source.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/base/resource/resource_bundle.h"
#include "v8/include/v8-isolate.h"
#if !BUILDFLAG(IS_ANDROID)
#include "components/search/ntp_features.h"
#endif
using base::UserMetricsAction;
using blink::WebElement;
using blink::WebElementCollection;
using blink::WebString;
namespace cart {
namespace {
constexpr unsigned kLengthLimit = 4096;
constexpr char kAmazonDomain[] = "amazon.com";
constexpr char kEbayDomain[] = "ebay.com";
constexpr char kElectronicExpressDomain[] = "electronicexpress.com";
constexpr char kGStoreHost[] = "store.google.com";
constexpr char kInputType[] = "INPUT";
constexpr char kValueAttributeName[] = "value";
constexpr base::FeatureParam<std::string> kSkipPattern{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "product-skip-pattern",
#else
&commerce::kCommerceHintAndroid, "product-skip-pattern",
#endif
// This regex does not match anything.
"\\b\\B"
};
// This is based on top 30 US shopping sites.
// TODO(crbug/1164236): cover more shopping sites.
constexpr base::FeatureParam<std::string> kAddToCartPattern{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "add-to-cart-pattern",
#else
&commerce::kCommerceHintAndroid, "add-to-cart-pattern",
#endif
"(\\b|[^a-z])"
"((add(ed)?(-|_|(%20)|\\s)?(item)?(-|_|(%20)|\\s)?to(-|_|(%20)|\\s)?("
"cart|"
"basket|bag)"
")|(cart\\/add)|(checkout\\/basket)|(cart_type)|(isquickaddtocartbutton))"
"(\\b|[^a-z])"
};
constexpr base::FeatureParam<std::string> kSkipAddToCartMapping{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "skip-add-to-cart-mapping",
#else
&commerce::kCommerceHintAndroid, "skip-add-to-cart-mapping",
#endif
// Empty JSON string.
""
};
constexpr base::FeatureParam<std::string> kPurchaseURLPatternMapping{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "purchase-url-pattern-mapping",
#else
&commerce::kCommerceHintAndroid, "purchase-url-pattern-mapping",
#endif
// Empty JSON string.
""
};
constexpr base::FeatureParam<std::string> kPurchaseButtonPattern{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "purchase-button-pattern",
#else
&commerce::kCommerceHintAndroid, "purchase-button-pattern",
#endif
// clang-format off
"^("
"("
"(place|submit|complete|confirm|finalize|make)(\\s(an|your|my|this))?"
"(\\ssecure)?\\s(order|purchase|checkout|payment)"
")"
"|"
"((pay|buy)(\\ssecurely)?(\\sUSD)?\\s(it|now|((\\$)?\\d+(\\.\\d+)?)))"
"|"
"((make|authorise|authorize|secure)\\spayment)"
"|"
"(confirm\\s(and|&)\\s(buy|purchase|order|pay|checkout))"
"|"
"((\\W)*(buy|purchase|order|pay|checkout)(\\W)*)"
")$"
// clang-format on
};
constexpr base::FeatureParam<std::string> kPurchaseButtonPatternMapping{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "purchase-button-pattern-mapping",
#else
&commerce::kCommerceHintAndroid, "purchase-button-pattern-mapping",
#endif
// Empty JSON map.
"{}"
};
constexpr base::FeatureParam<base::TimeDelta> kCartExtractionGapTime{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "cart-extraction-gap-time",
#else
&commerce::kCommerceHintAndroid, "cart-extraction-gap-time",
#endif
base::Seconds(2)
};
constexpr base::FeatureParam<int> kCartExtractionMaxCount{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "cart-extraction-max-count",
#else
&commerce::kCommerceHintAndroid, "cart-extraction-max-count",
#endif
20
};
constexpr base::FeatureParam<base::TimeDelta> kCartExtractionMinTaskTime{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "cart-extraction-min-task-time",
#else
&commerce::kCommerceHintAndroid, "cart-extraction-min-task-time",
#endif
base::Seconds(0.01)
};
constexpr base::FeatureParam<double> kCartExtractionDutyCycle{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "cart-extraction-duty-cycle",
#else
&commerce::kCommerceHintAndroid, "cart-extraction-duty-cycle",
#endif
0.05
};
constexpr base::FeatureParam<base::TimeDelta> kCartExtractionTimeout{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "cart-extraction-timeout",
#else
&commerce::kCommerceHintAndroid, "cart-extraction-timeout",
#endif
base::Seconds(0.25)
};
constexpr base::FeatureParam<std::string> kProductIdPatternMapping{
#if !BUILDFLAG(IS_ANDROID)
&ntp_features::kNtpChromeCartModule, "product-id-pattern-mapping",
#else
&commerce::kCommerceHintAndroid, "product-id-pattern-mapping",
#endif
// Empty JSON string.
""
};
constexpr base::FeatureParam<std::string> kCouponProductIdPatternMapping{
&commerce::kRetailCoupons, "coupon-product-id-pattern-mapping",
// Empty JSON string.
""};
constexpr base::FeatureParam<std::string> kDOMBasedAddToCartRequestPattern{
&commerce::kChromeCartDomBasedHeuristics, "add-to-cart-request-pattern",
"(cart|product)?[\"|_|-]quantity\":[\"]?[\\d]+"};
std::string eTLDPlusOne(const GURL& url) {
return net::registry_controlled_domains::GetDomainAndRegistry(
url, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
}
bool IsCartHeuristicsImprovementEnabled() {
#if !BUILDFLAG(IS_ANDROID)
return base::GetFieldTrialParamByFeatureAsBool(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleHeuristicsImprovementParam, true);
#else
return base::GetFieldTrialParamByFeatureAsBool(
commerce::kCommerceHintAndroid,
commerce::kCommerceHintAndroidHeuristicsImprovementParam, true);
#endif
}
enum class CommerceEvent {
kAddToCartByForm,
kAddToCartByURL,
kVisitCart,
kVisitCheckout,
kPurchaseByForm,
kPurchaseByURL,
};
void RecordCommerceEvent(CommerceEvent event) {
switch (event) {
case CommerceEvent::kAddToCartByForm:
LOCAL_HISTOGRAM_BOOLEAN("Commerce.Carts.AddToCartByPOST", true);
DVLOG(1) << "Commerce.AddToCart by POST form";
base::RecordAction(base::UserMetricsAction("Commerce.AddToCart"));
break;
case CommerceEvent::kAddToCartByURL:
LOCAL_HISTOGRAM_BOOLEAN("Commerce.Carts.AddToCartByURL", true);
DVLOG(1) << "Commerce.AddToCart by URL";
base::RecordAction(base::UserMetricsAction("Commerce.AddToCart"));
break;
case CommerceEvent::kVisitCart:
LOCAL_HISTOGRAM_BOOLEAN("Commerce.Carts.VisitCart", true);
DVLOG(1) << "Commerce.VisitCart";
base::RecordAction(base::UserMetricsAction("Commerce.VisitCart"));
break;
case CommerceEvent::kVisitCheckout:
LOCAL_HISTOGRAM_BOOLEAN("Commerce.Carts.VisitCheckout", true);
DVLOG(1) << "Commerce.VisitCheckout";
base::RecordAction(base::UserMetricsAction("Commerce.VisitCheckout"));
break;
case CommerceEvent::kPurchaseByForm:
LOCAL_HISTOGRAM_BOOLEAN("Commerce.Carts.PurchaseByPOST", true);
DVLOG(1) << "Commerce.Purchase by POST form";
base::RecordAction(base::UserMetricsAction("Commerce.Purchase"));
break;
case CommerceEvent::kPurchaseByURL:
LOCAL_HISTOGRAM_BOOLEAN("Commerce.Carts.PurchaseByURL", true);
DVLOG(1) << "Commerce.Purchase by URL";
base::RecordAction(base::UserMetricsAction("Commerce.Purchase"));
break;
default:
NOTREACHED();
}
}
mojo::Remote<mojom::CommerceHintObserver> GetObserver(
content::RenderFrame* render_frame) {
// Connect to Mojo service on browser to notify commerce signals.
mojo::Remote<mojom::CommerceHintObserver> observer;
// Subframes including fenced frames shouldn't be reached here.
DCHECK(render_frame->IsMainFrame() && !render_frame->IsInFencedFrameTree());
render_frame->GetBrowserInterfaceBroker()->GetInterface(
observer.BindNewPipeAndPassReceiver());
return observer;
}
std::optional<GURL> ScanCartURL(content::RenderFrame* render_frame) {
blink::WebDocument doc = render_frame->GetWebFrame()->GetDocument();
std::optional<GURL> best;
blink::WebVector<WebElement> elements =
doc.QuerySelectorAll(WebString("a[href]"));
for (WebElement element : elements) {
GURL link = doc.CompleteURL(element.GetAttribute("href"));
if (!link.is_valid())
continue;
link = link.GetAsReferrer();
// Only keep the shortest match. First match or most frequent match might
// work better, but we need larger validating corpus.
if (best && link.spec().size() >= best->spec().size())
continue;
if (!CommerceHintAgent::IsVisitCart(link))
continue;
DVLOG(2) << "Cart link: " << link;
best = link;
}
if (best)
DVLOG(1) << "Best cart link: " << *best;
return best;
}
void OnAddToCart(content::RenderFrame* render_frame,
const std::string& product_id = std::string()) {
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame);
observer->OnAddToCart(ScanCartURL(render_frame), product_id);
}
void OnVisitCart(content::RenderFrame* render_frame) {
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame);
observer->OnVisitCart();
}
void OnCartProductUpdated(content::RenderFrame* render_frame,
std::vector<mojom::ProductPtr> products) {
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame);
observer->OnCartProductUpdated(std::move(products));
}
void OnVisitCheckout(content::RenderFrame* render_frame) {
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame);
observer->OnVisitCheckout();
}
void OnPurchase(content::RenderFrame* render_frame) {
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame);
observer->OnPurchase();
}
void OnFormSubmit(content::RenderFrame* render_frame, bool is_purchase) {
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame);
observer->OnFormSubmit(is_purchase);
}
void OnWillSendRequest(content::RenderFrame* render_frame, bool is_addtocart) {
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame);
observer->OnWillSendRequest(is_addtocart);
}
const re2::RE2& GetAddToCartPattern() {
auto* pattern_from_component =
commerce_heuristics::CommerceHeuristicsData::GetInstance()
.GetAddToCartRequestPattern();
if (pattern_from_component &&
kAddToCartPattern.Get() == kAddToCartPattern.default_value) {
return *pattern_from_component;
}
re2::RE2::Options options;
options.set_case_sensitive(false);
static base::NoDestructor<re2::RE2> instance(kAddToCartPattern.Get(),
options);
return *instance;
}
const re2::RE2& GetDOMBasedAddToCartPattern() {
re2::RE2::Options options;
options.set_case_sensitive(false);
static base::NoDestructor<re2::RE2> instance(
kDOMBasedAddToCartRequestPattern.Get(), options);
return *instance;
}
const std::map<std::string, std::string>& GetPurchaseURLPatternMapping() {
static base::NoDestructor<std::map<std::string, std::string>> pattern_map([] {
base::Value json(
base::JSONReader::Read(
kPurchaseURLPatternMapping.Get().empty()
? ui::ResourceBundle::GetSharedInstance()
.LoadDataResourceString(
IDR_PURCHASE_URL_REGEX_DOMAIN_MAPPING_JSON)
: kPurchaseURLPatternMapping.Get())
.value());
DCHECK(json.is_dict());
std::map<std::string, std::string> map;
for (auto&& item : json.GetDict()) {
map.insert({std::move(item.first), std::move(item.second).TakeString()});
}
return map;
}());
return *pattern_map;
}
const std::map<std::string, std::string>& GetPurchaseButtonPatternMapping() {
static base::NoDestructor<std::map<std::string, std::string>> pattern_map([] {
base::Value json(
base::JSONReader::Read(kPurchaseButtonPatternMapping.Get()).value());
DCHECK(json.is_dict());
std::map<std::string, std::string> map;
for (auto&& item : json.GetDict()) {
map.insert({std::move(item.first), std::move(item.second).TakeString()});
}
return map;
}());
return *pattern_map;
}
const re2::RE2* GetVisitPurchasePattern(const GURL& url) {
std::string domain = eTLDPlusOne(url);
auto* pattern_from_component =
commerce_heuristics::CommerceHeuristicsData::GetInstance()
.GetPurchasePageURLPatternForDomain(domain);
if (pattern_from_component && kPurchaseURLPatternMapping.Get() ==
kPurchaseURLPatternMapping.default_value) {
return pattern_from_component;
}
const std::map<std::string, std::string>& purchase_string_map =
GetPurchaseURLPatternMapping();
if (purchase_string_map.find(domain) == purchase_string_map.end()) {
return nullptr;
}
static base::NoDestructor<std::map<std::string, std::unique_ptr<re2::RE2>>>
purchase_regex_map;
static re2::RE2::Options options;
options.set_case_sensitive(false);
if (purchase_regex_map->find(domain) == purchase_regex_map->end()) {
purchase_regex_map->insert(
{domain,
std::make_unique<re2::RE2>(purchase_string_map.at(domain), options)});
}
return purchase_regex_map->at(domain).get();
}
const re2::RE2& GetSkipPattern() {
auto* pattern_from_component =
commerce_heuristics::CommerceHeuristicsData::GetInstance()
.GetProductSkipPattern();
if (pattern_from_component &&
kSkipPattern.Get() == kSkipPattern.default_value) {
DVLOG(1) << "SkipPattern = " << pattern_from_component->pattern();
CommerceHeuristicsDataMetricsHelper::RecordSkipProductPatternSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::FROM_COMPONENT);
return *pattern_from_component;
}
static base::NoDestructor<re2::RE2> instance([] {
const std::string& pattern = kSkipPattern.Get();
DVLOG(1) << "SkipPattern = " << pattern;
return pattern;
}());
CommerceHeuristicsDataMetricsHelper::RecordSkipProductPatternSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::
FROM_FEATURE_PARAMETER);
return *instance;
}
// TODO(crbug/1164236): need i18n.
const re2::RE2& GetPurchaseTextPattern() {
auto* pattern_from_component =
commerce_heuristics::CommerceHeuristicsData::GetInstance()
.GetPurchaseButtonTextPattern();
if (pattern_from_component &&
kPurchaseButtonPattern.Get() == kPurchaseButtonPattern.default_value) {
return *pattern_from_component;
}
re2::RE2::Options options;
options.set_case_sensitive(false);
static base::NoDestructor<re2::RE2> instance(kPurchaseButtonPattern.Get(),
options);
return *instance;
}
bool GetProductIdFromRequest(base::StringPiece request,
std::string* product_id) {
re2::RE2::Options options;
options.set_case_sensitive(false);
static base::NoDestructor<re2::RE2> re("(product_id|pr1id)=(\\w+)", options);
return RE2::PartialMatch(request, *re, nullptr, product_id);
}
bool IsSameDomainXHR(const std::string& host,
const blink::WebURLRequest& request) {
// Only handle XHR POST requests here.
// Other matches like navigation is handled in DidStartNavigation().
if (!request.HttpMethod().Equals("POST"))
return false;
const GURL url = request.Url();
return url.DomainIs(host);
}
const std::map<std::string, std::string>& GetSkipAddToCartMapping() {
static base::NoDestructor<std::map<std::string, std::string>> skip_map([] {
base::Value json(
base::JSONReader::Read(
kSkipAddToCartMapping.Get().empty()
? ui::ResourceBundle::GetSharedInstance()
.LoadDataResourceString(
IDR_SKIP_ADD_TO_CART_REQUEST_DOMAIN_MAPPING_JSON)
: kSkipAddToCartMapping.Get())
.value());
DCHECK(json.is_dict());
std::map<std::string, std::string> map;
for (auto&& item : json.GetDict()) {
map.insert({std::move(item.first), std::move(item.second).TakeString()});
}
return map;
}());
return *skip_map;
}
bool DetectAddToCart(content::RenderFrame* render_frame,
const blink::WebURLRequest& request,
bool should_use_dom_based_heuristics) {
blink::WebLocalFrame* frame = render_frame->GetWebFrame();
const GURL& navigation_url(frame->GetDocument().Url());
const GURL& url = request.Url();
if (CommerceHintAgent::ShouldSkipAddToCartRequest(navigation_url, url)) {
return false;
}
bool is_add_to_cart = false;
if (navigation_url.DomainIs("dickssportinggoods.com")) {
is_add_to_cart = CommerceHintAgent::IsAddToCart(url.spec());
} else if (url.DomainIs("rei.com")) {
// TODO(crbug.com/1188143): There are other true positives like
// 'neo-product/rs/cart/item' that are missed here. Figure out a more
// comprehensive solution.
is_add_to_cart = url.path_piece() == "/rest/cart/item";
} else if (navigation_url.DomainIs(kElectronicExpressDomain)) {
is_add_to_cart =
CommerceHintAgent::IsAddToCart(url.spec()) &&
GetProductIdFromRequest(url.spec().substr(0, kLengthLimit), nullptr);
} else if (navigation_url.host() == kGStoreHost) {
is_add_to_cart = url.spec().find("O2JPA") != std::string::npos;
} else if (url.DomainIs("zappos.com")) {
is_add_to_cart = url.spec().find("mobileapi/v1/cart?displayRewards=true") !=
std::string::npos;
} else {
is_add_to_cart = CommerceHintAgent::IsAddToCart(url.path_piece());
}
if (is_add_to_cart) {
std::string url_product_id;
if (commerce::IsPartnerMerchant(navigation_url)) {
GetProductIdFromRequest(url.spec().substr(0, kLengthLimit),
&url_product_id);
}
RecordCommerceEvent(CommerceEvent::kAddToCartByURL);
OnAddToCart(render_frame, std::move(url_product_id));
return true;
}
if (IsCartHeuristicsImprovementEnabled()) {
if (navigation_url.DomainIs("abebooks.com"))
return false;
if (navigation_url.DomainIs("abercrombie.com"))
return false;
if (navigation_url.DomainIs(kAmazonDomain) &&
url.host() != "fls-na.amazon.com")
return false;
if (navigation_url.DomainIs("bestbuy.com"))
return false;
if (navigation_url.DomainIs("containerstore.com"))
return false;
if (navigation_url.DomainIs("gap.com") && url.DomainIs("granify.com"))
return false;
if (navigation_url.DomainIs("kohls.com"))
return false;
if (navigation_url.DomainIs("officedepot.com") &&
url.DomainIs("chatid.com"))
return false;
if (navigation_url.DomainIs("pier1.com"))
return false;
}
blink::WebHTTPBody body = request.HttpBody();
if (body.IsNull())
return false;
unsigned i = 0;
blink::WebHTTPBody::Element element;
while (body.ElementAt(i++, element)) {
if (element.type != blink::HTTPBodyElementType::kTypeData)
continue;
// TODO(crbug/1168704): this copy is avoidable if element is guaranteed to
// have contiguous buffer.
std::vector<uint8_t> buf = element.data.Copy().ReleaseVector();
base::StringPiece str(reinterpret_cast<char*>(buf.data()), buf.size());
// Per-site hard-coded exclusion rules:
if (navigation_url.DomainIs("groupon.com") && buf.size() > 10000)
return false;
// Per-site skipping length limit when checking request text.
bool skip_length_limit = navigation_url.DomainIs("otterbox.com");
bool is_add_to_cart_request =
CommerceHintAgent::IsAddToCart(str, skip_length_limit);
if (should_use_dom_based_heuristics && !is_add_to_cart_request) {
is_add_to_cart_request =
CommerceHintAgent::IsAddToCartForDomBasedHeuristics(str);
}
if (is_add_to_cart_request) {
std::string product_id;
if (commerce::IsPartnerMerchant(url)) {
GetProductIdFromRequest(str.substr(0, kLengthLimit), &product_id);
}
RecordCommerceEvent(CommerceEvent::kAddToCartByForm);
DVLOG(2) << "Matched add-to-cart. Request from \"" << navigation_url
<< "\" to \"" << url << "\" with payload (size = " << str.size()
<< ") \"" << str << "\"";
OnAddToCart(render_frame, std::move(product_id));
return true;
}
}
return false;
}
std::string CanonicalURL(const GURL& url) {
return base::JoinString({url.scheme_piece(), "://", url.host_piece(),
url.path_piece().substr(0, kLengthLimit)},
"");
}
const WebString GetProductExtractionScript(
const std::string& product_id_json_component,
const std::string& cart_extraction_script_component) {
std::string script_string;
if (cart_extraction_script_component.empty()) {
script_string =
ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
IDR_CART_PRODUCT_EXTRACTION_JS);
if (IsCartHeuristicsImprovementEnabled()) {
script_string = "var isImprovementEnabled = true;\n" + script_string;
}
const std::string config =
"var kSleeperMinTaskTimeMs = " +
base::NumberToString(
kCartExtractionMinTaskTime.Get().InMillisecondsF()) +
";\n" + "var kSleeperDutyCycle = " +
base::NumberToString(kCartExtractionDutyCycle.Get()) + ";\n" +
"var kTimeoutMs = " +
base::NumberToString(kCartExtractionTimeout.Get().InMillisecondsF()) +
";\n";
DVLOG(2) << config;
script_string = config + script_string;
CommerceHeuristicsDataMetricsHelper::RecordCartExtractionScriptSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::FROM_RESOURCE);
} else {
script_string = cart_extraction_script_component;
CommerceHeuristicsDataMetricsHelper::RecordCartExtractionScriptSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::FROM_COMPONENT);
}
if (!product_id_json_component.empty()) {
script_string = "var idExtractionMap = " + product_id_json_component +
";\n" + script_string;
CommerceHeuristicsDataMetricsHelper::RecordProductIDExtractionPatternSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::FROM_COMPONENT);
return WebString::FromUTF8(std::move(script_string));
}
CommerceHeuristicsDataMetricsHelper::RecordProductIDExtractionPatternSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::
FROM_FEATURE_PARAMETER);
const std::string id_extraction_map =
kProductIdPatternMapping.Get().empty()
? ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
IDR_CART_DOMAIN_PRODUCT_ID_REGEX_JSON)
: kProductIdPatternMapping.Get();
script_string =
"var idExtractionMap = " + id_extraction_map + ";\n" + script_string;
const std::string coupon_id_extraction_map =
kCouponProductIdPatternMapping.Get();
if (!coupon_id_extraction_map.empty()) {
script_string = "var couponIdExtractionMap = " + coupon_id_extraction_map +
";\n" + script_string;
}
return WebString::FromUTF8(std::move(script_string));
}
} // namespace
using commerce_heuristics::CommerceHeuristicsData;
CommerceHintAgent::CommerceHintAgent(content::RenderFrame* render_frame)
: content::RenderFrameObserver(render_frame),
content::RenderFrameObserverTracker<CommerceHintAgent>(render_frame) {
DCHECK(render_frame);
// Subframes including fenced frames shouldn't be reached here.
DCHECK(render_frame->IsMainFrame() && !render_frame->IsInFencedFrameTree());
mojo::Remote<ukm::mojom::UkmRecorderFactory> factory;
content::RenderThread::Get()->BindHostReceiver(
factory.BindNewPipeAndPassReceiver());
ukm_recorder_ = ukm::MojoUkmRecorder::Create(*factory);
}
CommerceHintAgent::~CommerceHintAgent() = default;
bool CommerceHintAgent::IsAddToCart(base::StringPiece str,
bool skip_length_limit) {
return RE2::PartialMatch(
skip_length_limit ? str : str.substr(0, kLengthLimit),
GetAddToCartPattern());
}
bool CommerceHintAgent::IsAddToCartForDomBasedHeuristics(
base::StringPiece str) {
return RE2::PartialMatch(str.substr(0, kLengthLimit),
GetDOMBasedAddToCartPattern());
}
// TODO(crbug.com/1310422): Remove below two APIs and move all related unit
// tests to component.
bool CommerceHintAgent::IsVisitCart(const GURL& url) {
return commerce_heuristics::IsVisitCart(url);
}
bool CommerceHintAgent::IsVisitCheckout(const GURL& url) {
return commerce_heuristics::IsVisitCheckout(url);
}
bool CommerceHintAgent::IsPurchase(const GURL& url) {
auto* pattern = GetVisitPurchasePattern(url);
if (!pattern)
return false;
return RE2::PartialMatch(CanonicalURL(url).substr(0, kLengthLimit), *pattern);
}
bool CommerceHintAgent::IsPurchase(const GURL& url,
base::StringPiece button_text) {
const std::map<std::string, std::string>& purchase_string_map =
GetPurchaseButtonPatternMapping();
static base::NoDestructor<std::map<std::string, std::unique_ptr<re2::RE2>>>
purchase_regex_map;
std::string domain = eTLDPlusOne(url);
if (purchase_string_map.find(domain) == purchase_string_map.end()) {
return RE2::PartialMatch(button_text, GetPurchaseTextPattern());
}
static re2::RE2::Options options;
options.set_case_sensitive(false);
if (purchase_regex_map->find(domain) == purchase_regex_map->end()) {
purchase_regex_map->insert(
{domain,
std::make_unique<re2::RE2>(purchase_string_map.at(domain), options)});
}
return RE2::PartialMatch(button_text, *purchase_regex_map->at(domain));
}
bool CommerceHintAgent::ShouldSkip(base::StringPiece product_name) {
return RE2::PartialMatch(product_name.substr(0, kLengthLimit),
GetSkipPattern());
}
const std::vector<std::string> CommerceHintAgent::ExtractButtonTexts(
const blink::WebFormElement& form) {
static base::NoDestructor<WebString> kButton("button");
const WebElementCollection& buttons = form.GetElementsByHTMLTagName(*kButton);
std::vector<std::string> button_texts;
for (WebElement button = buttons.FirstItem(); !button.IsNull();
button = buttons.NextItem()) {
// TODO(crbug/1164236): emulate innerText to be more robust.
button_texts.push_back(base::UTF16ToUTF8(base::CollapseWhitespace(
base::TrimWhitespace(button.TextContent().Utf16(),
base::TrimPositions::TRIM_ALL),
true)));
}
return button_texts;
}
bool CommerceHintAgent::IsAddToCartButton(blink::WebElement& element) {
// Find the first non-null, non-empty element and terminates anytime an
// element with wrong size is found.
std::string button_text;
std::u16string button_text_utf16;
while (!element.IsNull()) {
gfx::Size client_size = element.GetClientSize();
if (!commerce_heuristics::IsAddToCartButtonSpec(client_size.height(),
client_size.width())) {
return false;
}
base::TrimWhitespace(element.TextContent().Utf16(), base::TRIM_ALL,
&button_text_utf16);
button_text = base::UTF16ToUTF8(button_text_utf16);
if (button_text.empty() && element.TagName().Ascii() == kInputType &&
!element.GetAttribute(kValueAttributeName).IsEmpty()) {
base::TrimWhitespace(element.GetAttribute(kValueAttributeName).Utf16(),
base::TRIM_ALL, &button_text_utf16);
button_text = base::UTF16ToUTF8(button_text_utf16);
}
if (!button_text.empty())
break;
if (!element.ParentNode().IsElementNode()) {
return false;
}
element = element.ParentNode().To<blink::WebElement>();
}
if (element.IsNull() ||
!commerce_heuristics::IsAddToCartButtonTag(element.TagName().Ascii()) ||
!commerce_heuristics::IsAddToCartButtonText(button_text)) {
return false;
}
return true;
}
void CommerceHintAgent::MaybeExtractProducts() {
// TODO(crbug/1241582): Add a test for rate control based on whether the
// histogram is recorded.
if (is_extraction_pending_) {
DVLOG(1) << "Extraction is scheduled. Skip this request.";
return;
}
if (extraction_count_ >= kCartExtractionMaxCount.Get()) {
DVLOG(1) << "Extraction exceeds quota. Skip until navigation.";
return;
}
is_extraction_pending_ = true;
DVLOG(1) << "Scheduled extraction";
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CommerceHintAgent::ExtractProducts,
weak_factory_.GetWeakPtr()),
kCartExtractionGapTime.Get());
}
void CommerceHintAgent::ExtractProducts() {
if (!IsVisitCart(GURL(render_frame()->GetWebFrame()->GetDocument().Url()))) {
return;
}
is_extraction_pending_ = false;
if (is_extraction_running_) {
DVLOG(1) << "Extraction is running. Try again later.";
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CommerceHintAgent::MaybeExtractProducts,
weak_factory_.GetWeakPtr()),
kCartExtractionGapTime.Get());
return;
}
// Use current script if it has already been initialized; otherwise fetch
// script from browser side.
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame());
auto* observer_ptr = observer.get();
observer_ptr->OnCartExtraction(
base::BindOnce(&CommerceHintAgent::ExtractCartWithUpdatedScript,
weak_factory_.GetWeakPtr(), std::move(observer)));
}
void CommerceHintAgent::ExtractCartWithUpdatedScript(
mojo::Remote<mojom::CommerceHintObserver> observer,
const std::string& product_id_json,
const std::string& cart_extraction_script) {
is_extraction_running_ = true;
DVLOG(2) << "is_extraction_running_ = " << is_extraction_running_;
blink::WebLocalFrame* main_frame = render_frame()->GetWebFrame();
v8::HandleScope handle_scope(main_frame->GetAgentGroupScheduler()->Isolate());
blink::WebScriptSource source = blink::WebScriptSource(
GetProductExtractionScript(product_id_json, cart_extraction_script));
main_frame->RequestExecuteScript(
ISOLATED_WORLD_ID_CHROME_INTERNAL, base::make_span(&source, 1u),
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::EvaluationTiming::kAsynchronous,
blink::mojom::LoadEventBlockingOption::kDoNotBlock,
base::BindOnce(&CommerceHintAgent::OnProductsExtracted,
weak_factory_.GetWeakPtr()),
blink::BackForwardCacheAware::kAllow,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::PromiseResultOption::kAwait);
}
void CommerceHintAgent::OnProductsExtracted(std::optional<base::Value> results,
base::TimeTicks start_time) {
// Only record when the start time is correctly captured.
if (!results || !results->is_dict())
return;
base::Value::Dict& results_dict = results->GetDict();
if (!start_time.is_null()) {
results_dict.Set("execution_ms",
(base::TimeTicks::Now() - start_time).InMillisecondsF());
}
DVLOG(2) << "OnProductsExtracted: " << *results;
auto builder = ukm::builders::Shopping_CartExtraction(
render_frame()->GetWebFrame()->GetDocument().GetUkmSourceId());
auto record_time = [&](const std::string& key,
const std::string& metric_name) {
std::optional<double> optional_time = results_dict.FindDouble(key);
if (!optional_time) {
return;
}
double time_value = optional_time.value();
base::TimeDelta time = base::Milliseconds(time_value);
base::UmaHistogramTimes("Commerce.Carts." + metric_name, time);
if (metric_name == "ExtractionLongestTaskTime") {
builder.SetExtractionLongestTaskTime(time_value);
} else if (metric_name == "ExtractionTotalTasksTime") {
builder.SetExtractionTotalTasksTime(time_value);
} else if (metric_name == "ExtractionElapsedTime") {
builder.SetExtractionElapsedTime(time_value);
} else if (metric_name == "ExtractionExecutionTime") {
builder.SetExtractionExecutionTime(time_value);
}
};
record_time("longest_task_ms", "ExtractionLongestTaskTime");
record_time("total_tasks_ms", "ExtractionTotalTasksTime");
record_time("elapsed_ms", "ExtractionElapsedTime");
record_time("execution_ms", "ExtractionExecutionTime");
std::optional<bool> timedout = results_dict.FindBool("timedout");
if (timedout) {
base::UmaHistogramBoolean("Commerce.Carts.ExtractionTimedOut",
timedout.value());
builder.SetExtractionTimedOut(timedout.value());
}
builder.Record(ukm_recorder_.get());
const base::Value::List* extracted_products =
results_dict.FindList("products");
// Don't update cart when the return value does not exist or is not a list.
// This could be due to that the cart is not loaded.
if (!extracted_products) {
return;
}
std::vector<mojom::ProductPtr> products;
for (const auto& product_val : *extracted_products) {
if (!product_val.is_dict()) {
continue;
}
const base::Value::Dict& product = product_val.GetDict();
const std::string* image_url = product.FindString("imageUrl");
const std::string* product_name = product.FindString("title");
mojom::ProductPtr product_ptr(mojom::Product::New());
product_ptr->image_url = GURL(*image_url);
product_ptr->name = *product_name;
DVLOG(1) << "image_url = " << product_ptr->image_url;
DVLOG(1) << "name = " << product_ptr->name;
if (ShouldSkip(product_ptr->name)) {
DVLOG(1) << "skipped";
continue;
}
const std::string* product_id = product.FindString("productId");
if (product_id) {
DVLOG(1) << "product_id = " << *product_id;
DCHECK(!product_id->empty());
product_ptr->product_id = *product_id;
}
products.push_back(std::move(product_ptr));
}
OnCartProductUpdated(render_frame(), std::move(products));
is_extraction_running_ = false;
extraction_count_++;
DVLOG(2) << "is_extraction_running_ = " << is_extraction_running_;
}
bool CommerceHintAgent::ShouldUseDOMBasedHeuristics() {
if (!should_use_dom_heuristics_.has_value()) {
const GURL& url(render_frame()->GetWebFrame()->GetDocument().Url());
should_use_dom_heuristics_ =
commerce_heuristics::ShouldUseDOMBasedHeuristics(url);
}
return should_use_dom_heuristics_.value();
}
void CommerceHintAgent::OnDestruct() {
delete this;
}
void CommerceHintAgent::WillSendRequest(const blink::WebURLRequest& request) {
if (!should_skip_.has_value() || should_skip_.value()) {
return;
}
blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
const GURL& url(frame->GetDocument().Url());
if (!url.SchemeIsHTTPOrHTTPS())
return;
// The rest of this method is not concerned with data URLs but makes a copy of
// the URL which can be expensive for large data URLs.
// TODO(crbug.com/1321924): Clean up this method to avoid copies once this
// optimization has been measured in the field and launches.
if (base::FeatureList::IsEnabled(base::features::kOptimizeDataUrls) &&
request.Url().ProtocolIs(url::kDataScheme)) {
return;
}
// Only check XHR POST requests for add-to-cart.
// Other add-to-cart matches like navigation is handled in
// DidStartNavigation(). Some sites use GET requests though, so special-case
// them here.
GURL request_url = request.Url();
bool should_use_dom_based_heuristics = ShouldUseDOMBasedHeuristics();
bool add_to_cart_active = true;
if (should_use_dom_based_heuristics) {
add_to_cart_active = base::Time::Now() - add_to_cart_focus_time_ <
commerce::kAddToCartButtonActiveTime.Get();
}
if ((request.HttpMethod().Equals("POST") ||
request_url.DomainIs(kEbayDomain) ||
url.DomainIs(kElectronicExpressDomain)) &&
add_to_cart_active) {
bool is_add_to_cart = DetectAddToCart(render_frame(), request,
should_use_dom_based_heuristics);
OnWillSendRequest(render_frame(), is_add_to_cart);
}
// TODO(crbug/1164236): use MutationObserver on cart instead.
// Detect XHR in cart page.
// Don't do anything for subframes.
if (frame->Parent())
return;
if (!url.SchemeIs(url::kHttpsScheme))
return;
if (IsVisitCart(url) && IsSameDomainXHR(url.host(), request)) {
DVLOG(1) << "In-cart XHR: " << request.Url();
MaybeExtractProducts();
}
}
void CommerceHintAgent::DidStartNavigation(
const GURL& url,
std::optional<blink::WebNavigationType> navigation_type) {
if (!url.SchemeIsHTTPOrHTTPS())
return;
should_use_dom_heuristics_.reset();
has_finished_loading_ = false;
starting_url_ = url;
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame());
auto* observer_ptr = observer.get();
observer_ptr->OnNavigation(
url, CommerceHeuristicsData::GetInstance().GetVersion(),
base::BindOnce(&CommerceHintAgent::DidStartNavigationCallback,
weak_factory_.GetWeakPtr(), url, std::move(observer)));
}
void CommerceHintAgent::DidStartNavigationCallback(
const GURL& url,
mojo::Remote<mojom::CommerceHintObserver> observer,
bool should_skip,
mojom::HeuristicsPtr heuristics) {
should_skip_ = should_skip;
if (should_skip)
return;
if (!heuristics->version_number.empty() &&
heuristics->version_number !=
CommerceHeuristicsData::GetInstance().GetVersion()) {
bool is_populated =
CommerceHeuristicsData::GetInstance().PopulateDataFromComponent(
heuristics->hint_json_data, heuristics->global_json_data,
/*product_id_json_data*/ "", /*cart_extraction_script*/ "");
DCHECK(is_populated);
CommerceHeuristicsData::GetInstance().UpdateVersion(
base::Version(heuristics->version_number));
}
}
void CommerceHintAgent::DidCommitProvisionalLoad(
ui::PageTransition transition) {
if (!starting_url_.is_valid())
return;
DCHECK(starting_url_.SchemeIsHTTPOrHTTPS());
should_use_dom_heuristics_.reset();
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame());
auto* observer_ptr = observer.get();
observer_ptr->OnNavigation(
starting_url_, CommerceHeuristicsData::GetInstance().GetVersion(),
base::BindOnce(&CommerceHintAgent::DidCommitProvisionalLoadCallback,
weak_factory_.GetWeakPtr(), starting_url_,
std::move(observer)));
}
void CommerceHintAgent::DidCommitProvisionalLoadCallback(
const GURL& url,
mojo::Remote<mojom::CommerceHintObserver> observer,
bool should_skip,
mojom::HeuristicsPtr heuristics) {
should_skip_ = should_skip;
if (should_skip)
return;
if (!heuristics->version_number.empty() &&
heuristics->version_number !=
CommerceHeuristicsData::GetInstance().GetVersion()) {
bool is_populated =
CommerceHeuristicsData::GetInstance().PopulateDataFromComponent(
heuristics->hint_json_data, heuristics->global_json_data,
/*product_id_json_data*/ "", /*cart_extraction_script*/ "");
DCHECK(is_populated);
CommerceHeuristicsData::GetInstance().UpdateVersion(
base::Version(heuristics->version_number));
}
// TODO(crbug.com/1383422): Add a test for when starting_url_ is invalid
// because of multiple continuous DidCommitProvisionalLoad calls.
if (!starting_url_.is_valid())
return;
if (IsAddToCart(starting_url_.PathForRequestPiece())) {
RecordCommerceEvent(CommerceEvent::kAddToCartByURL);
OnAddToCart(render_frame());
}
if (!IsVisitCart(starting_url_) && IsVisitCheckout(starting_url_)) {
RecordCommerceEvent(CommerceEvent::kVisitCheckout);
OnVisitCheckout(render_frame());
}
if (IsPurchase(starting_url_)) {
RecordCommerceEvent(CommerceEvent::kPurchaseByURL);
OnPurchase(render_frame());
}
starting_url_ = GURL();
}
void CommerceHintAgent::DidFinishLoad() {
blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
// Don't do anything for subframes.
if (frame->Parent())
return;
should_use_dom_heuristics_.reset();
const GURL& url(frame->GetDocument().Url());
if (!url.SchemeIs(url::kHttpsScheme))
return;
has_finished_loading_ = true;
extraction_count_ = 0;
mojo::Remote<mojom::CommerceHintObserver> observer =
GetObserver(render_frame());
auto* observer_ptr = observer.get();
observer_ptr->OnNavigation(
url, CommerceHeuristicsData::GetInstance().GetVersion(),
base::BindOnce(&CommerceHintAgent::DidFinishLoadCallback,
weak_factory_.GetWeakPtr(), url, std::move(observer)));
}
void CommerceHintAgent::DidFinishLoadCallback(
const GURL& url,
mojo::Remote<mojom::CommerceHintObserver> observer,
bool should_skip,
mojom::HeuristicsPtr heuristics) {
should_skip_ = should_skip;
if (should_skip)
return;
if (!heuristics->version_number.empty() &&
heuristics->version_number !=
CommerceHeuristicsData::GetInstance().GetVersion()) {
bool is_populated =
CommerceHeuristicsData::GetInstance().PopulateDataFromComponent(
heuristics->hint_json_data, heuristics->global_json_data,
/*product_id_json_data*/ "", /*cart_extraction_script*/ "");
DCHECK(is_populated);
CommerceHeuristicsData::GetInstance().UpdateVersion(
base::Version(heuristics->version_number));
}
// Some URLs might satisfy the patterns for both cart and checkout (e.g.
// https://www.foo.com/cart/checkout). In those cases, cart has higher
// priority.
if (IsVisitCart(url)) {
RecordCommerceEvent(CommerceEvent::kVisitCart);
OnVisitCart(render_frame());
DVLOG(1) << "Extract products after loading";
MaybeExtractProducts();
} else if (IsVisitCheckout(url)) {
RecordCommerceEvent(CommerceEvent::kVisitCheckout);
OnVisitCheckout(render_frame());
}
}
void CommerceHintAgent::WillSubmitForm(const blink::WebFormElement& form) {
if (!should_skip_.has_value() || should_skip_.value())
return;
blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
const GURL url(frame->GetDocument().Url());
if (!url.SchemeIsHTTPOrHTTPS())
return;
bool is_purchase = false;
for (const std::string& button_text : ExtractButtonTexts(form)) {
if (IsPurchase(url, button_text)) {
RecordCommerceEvent(CommerceEvent::kPurchaseByForm);
OnPurchase(render_frame());
is_purchase = true;
break;
}
}
OnFormSubmit(render_frame(), is_purchase);
}
// TODO(crbug/1164236): use MutationObserver on cart instead.
void CommerceHintAgent::ExtractCartFromCurrentFrame() {
if (!should_skip_.has_value() || should_skip_.value())
return;
if (!has_finished_loading_)
return;
blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
// Don't do anything for subframes.
if (frame->Parent())
return;
const GURL url(frame->GetDocument().Url());
if (!url.SchemeIs(url::kHttpsScheme))
return;
if (IsVisitCart(url)) {
DVLOG(1) << "Extract products due to layout shift or intersection change";
MaybeExtractProducts();
}
}
void CommerceHintAgent::DidObserveLayoutShift(double score,
bool after_input_or_scroll) {
DVLOG(1) << "Layout shift " << score << " " << after_input_or_scroll;
ExtractCartFromCurrentFrame();
}
void CommerceHintAgent::OnMainFrameIntersectionChanged(
const gfx::Rect& intersect_rect) {
DVLOG(1) << "Intersection changed " << intersect_rect.x() << " "
<< intersect_rect.y() << " " << intersect_rect.width() << " "
<< intersect_rect.height();
ExtractCartFromCurrentFrame();
}
void CommerceHintAgent::FocusedElementChanged(
const blink::WebElement& focused_element) {
// Don't observe focused element change when the navigation hasn't finished
// to avoid being triggered by auto focus due to page rendering.
if (!starting_url_.is_empty()) {
return;
}
base::Time before_check = base::Time::Now();
if ((before_check - add_to_cart_heuristics_execution_time_) <
commerce::kHeuristicsExecutionGapTime.Get()) {
return;
}
if (!should_skip_.has_value() || should_skip_.value()) {
return;
}
if (!ShouldUseDOMBasedHeuristics()) {
return;
}
auto builder = ukm::builders::Shopping_AddToCartDetection(
render_frame()->GetWebFrame()->GetDocument().GetUkmSourceId());
blink::WebElement element = focused_element;
// Record the last time that the heuristics is run.
add_to_cart_heuristics_execution_time_ = base::Time::Now();
if (IsAddToCartButton(element)) {
add_to_cart_focus_time_ = base::Time::Now();
}
base::TimeDelta execution_time = base::Time::Now() - before_check;
base::UmaHistogramMicrosecondsTimes("Commerce.Carts.AddToCartButtonDetection",
execution_time);
builder.SetHeuristicsExecutionTime(execution_time.InMicroseconds())
.Record(ukm_recorder_.get());
}
bool CommerceHintAgent::ShouldSkipAddToCartRequest(const GURL& navigation_url,
const GURL& request_url) {
const std::string& navigation_domain = eTLDPlusOne(navigation_url);
const re2::RE2* pattern =
commerce_heuristics::CommerceHeuristicsData::GetInstance()
.GetSkipAddToCartPatternForDomain(navigation_domain);
if (pattern) {
return RE2::PartialMatch(request_url.spec().substr(0, kLengthLimit),
*pattern);
}
const std::map<std::string, std::string>& skip_string_map =
GetSkipAddToCartMapping();
static base::NoDestructor<std::map<std::string, std::unique_ptr<re2::RE2>>>
skip_regex_map;
if (skip_string_map.find(navigation_domain) == skip_string_map.end()) {
return false;
}
static re2::RE2::Options options;
options.set_case_sensitive(false);
if (skip_regex_map->find(navigation_domain) == skip_regex_map->end()) {
skip_regex_map->insert(
{navigation_domain,
std::make_unique<re2::RE2>(skip_string_map.at(navigation_domain),
options)});
}
return RE2::PartialMatch(request_url.spec().substr(0, kLengthLimit),
*skip_regex_map->at(navigation_domain));
}
} // namespace cart