blob: bffa09d2a6534c726fb9f976b235265b893f3f78 [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/services/auction_worklet/set_bid_bindings.h"
#include <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "content/services/auction_worklet/auction_v8_helper.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "gin/converter.h"
#include "gin/dictionary.h"
#include "third_party/blink/public/common/interest_group/ad_auction_constants.h"
#include "url/gurl.h"
#include "url/url_constants.h"
#include "v8/include/v8-exception.h"
#include "v8/include/v8-external.h"
#include "v8/include/v8-function-callback.h"
#include "v8/include/v8-template.h"
namespace auction_worklet {
namespace {
// Checks that `url` is a valid URL and is in `ads`. Appends an error to
// `out_errors` if not. `error_prefix` is used in output error messages
// only.
bool IsAllowedAdUrl(const GURL& url,
std::string& error_prefix,
const char* argument_name,
const std::vector<blink::InterestGroup::Ad>& ads,
std::vector<std::string>& out_errors) {
if (!url.is_valid() || !url.SchemeIs(url::kHttpsScheme)) {
out_errors.push_back(base::StrCat({error_prefix, "bid ", argument_name,
" URL '", url.possibly_invalid_spec(),
"' isn't a valid https:// URL."}));
return false;
}
for (const auto& ad : ads) {
if (url == ad.render_url)
return true;
}
out_errors.push_back(
base::StrCat({error_prefix, "bid ", argument_name, " URL '",
url.possibly_invalid_spec(),
"' isn't one of the registered creative URLs."}));
return false;
}
} // namespace
mojom::BidderWorkletBidPtr SetBidBindings::TakeBid() {
DCHECK(has_bid());
return std::move(bid_);
}
SetBidBindings::SetBidBindings(
AuctionV8Helper* v8_helper,
v8::Local<v8::ObjectTemplate> global_template,
base::TimeTicks start,
bool has_top_level_seller_origin,
const absl::optional<std::vector<blink::InterestGroup::Ad>>& ads,
const absl::optional<std::vector<blink::InterestGroup::Ad>>& ad_components)
: v8_helper_(v8_helper),
start_(start),
has_top_level_seller_origin_(has_top_level_seller_origin),
ads_(ads),
ad_components_(ad_components) {
v8::Local<v8::External> v8_this =
v8::External::New(v8_helper_->isolate(), this);
v8::Local<v8::FunctionTemplate> v8_template = v8::FunctionTemplate::New(
v8_helper_->isolate(), &SetBidBindings::SetBid, v8_this);
v8_template->RemovePrototype();
global_template->Set(v8_helper_->CreateStringFromLiteral("setBid"),
v8_template);
}
SetBidBindings::~SetBidBindings() = default;
void SetBidBindings::SetBid(const v8::FunctionCallbackInfo<v8::Value>& args) {
SetBidBindings* bindings =
static_cast<SetBidBindings*>(v8::External::Cast(*args.Data())->Value());
AuctionV8Helper* v8_helper = bindings->v8_helper_;
if (args.Length() < 1 || args[0].IsEmpty()) {
args.GetIsolate()->ThrowException(
v8::Exception::TypeError(v8_helper->CreateStringFromLiteral(
"setBid requires 1 object parameter")));
return;
}
std::vector<std::string> errors;
if (!bindings->SetBid(args[0], /*error_prefix=*/"", errors)) {
DCHECK_EQ(1u, errors.size());
// Remove the trailing period from the error message.
std::string error_msg = errors[0].substr(0, errors[0].length() - 1);
args.GetIsolate()->ThrowException(v8::Exception::TypeError(
v8_helper->CreateUtf8String(error_msg).ToLocalChecked()));
return;
}
}
bool SetBidBindings::SetBid(v8::Local<v8::Value> generate_bid_result,
std::string error_prefix,
std::vector<std::string>& errors_out) {
v8::Isolate* isolate = v8_helper_->isolate();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
bid_.reset();
if (!generate_bid_result->IsObject()) {
errors_out.push_back(base::StrCat({error_prefix, "bid not an object."}));
return false;
}
gin::Dictionary result_dict(isolate, generate_bid_result.As<v8::Object>());
v8::Local<v8::Value> ad_object;
double bid;
std::string render_url_string;
// Parse and validate values.
if (!result_dict.Get("ad", &ad_object) || !result_dict.Get("bid", &bid) ||
!result_dict.Get("render", &render_url_string)) {
errors_out.push_back(
base::StrCat({error_prefix, "bid has incorrect structure."}));
return false;
}
// "ad" field is optional, but if present, must be possible to convert to
// JSON. Note that if "ad" field isn't present, Get("ad", ...) succeeds, but
// `ad_object` is undefined.
std::string ad_json;
if (ad_object->IsUndefined()) {
ad_json = "null";
} else {
if (!v8_helper_->ExtractJson(context, ad_object, &ad_json)) {
errors_out.push_back(
base::StrCat({error_prefix, "bid has invalid ad value."}));
return false;
}
}
if (has_top_level_seller_origin_) {
bool allow_component_auction;
if (!result_dict.Get("allowComponentAuction", &allow_component_auction) ||
!allow_component_auction) {
errors_out.push_back(
base::StrCat({error_prefix,
"bid does not have allowComponentAuction "
"set to true. Bid dropped from component auction."}));
return false;
}
}
if (!std::isfinite(bid) || bid < 0.0) {
// Bids should not be infinite or NaN.
errors_out.push_back(base::StringPrintf("%sbid of %lf is not a valid bid.",
error_prefix.c_str(), bid));
return false;
}
if (bid <= 0.0) {
// Not an error, just no bid.
return false;
}
GURL render_url(render_url_string);
if (!IsAllowedAdUrl(render_url, error_prefix, "render", *ads_, errors_out)) {
return false;
}
absl::optional<std::vector<GURL>> ad_component_urls;
v8::Local<v8::Value> ad_components;
if (result_dict.Get("adComponents", &ad_components) &&
!ad_components->IsNullOrUndefined()) {
if (!ad_components_) {
errors_out.push_back(
base::StrCat({error_prefix,
"bid contains adComponents but InterestGroup has no "
"adComponents."}));
return false;
}
if (!ad_components->IsArray()) {
errors_out.push_back(base::StrCat(
{error_prefix, "bid adComponents value must be an array."}));
return false;
}
v8::Local<v8::Array> ad_components_array = ad_components.As<v8::Array>();
if (ad_components_array->Length() > blink::kMaxAdAuctionAdComponents) {
errors_out.push_back(base::StringPrintf(
"%sbid adComponents with over %zu items.", error_prefix.c_str(),
blink::kMaxAdAuctionAdComponents));
return false;
}
ad_component_urls.emplace();
for (size_t i = 0; i < ad_components_array->Length(); ++i) {
std::string url_string;
if (!gin::ConvertFromV8(
isolate, ad_components_array->Get(context, i).ToLocalChecked(),
&url_string)) {
errors_out.push_back(base::StrCat(
{error_prefix,
"bid adComponents value must be an array of strings."}));
return false;
}
GURL ad_component_url(url_string);
if (!IsAllowedAdUrl(ad_component_url, error_prefix, "adComponents",
*ad_components_, errors_out)) {
return false;
}
ad_component_urls->emplace_back(std::move(ad_component_url));
}
}
bid_ = mojom::BidderWorkletBid::New(
std::move(ad_json), bid, std::move(render_url),
std::move(ad_component_urls),
/*bid_duration=*/base::TimeTicks::Now() - start_);
return true;
}
} // namespace auction_worklet