#include "content/services/auction_worklet/private_aggregation_bindings.h"
#include <cmath>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "content/services/auction_worklet/auction_v8_helper.h"
#include "content/services/auction_worklet/public/mojom/private_aggregation_request.mojom.h"
#include "content/services/worklet_utils/private_aggregation_utils.h"
#include "gin/arguments.h"
#include "gin/dictionary.h"
#include "third_party/abseil-cpp/absl/numeric/int128.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/private_aggregation/aggregatable_report.mojom.h"
#include "v8/include/v8-exception.h"
#include "v8/include/v8-external.h"
#include "v8/include/v8-function-callback.h"
#include "v8/include/v8-function.h"
#include "v8/include/v8-local-handle.h"
#include "v8/include/v8-object.h"
namespace auction_worklet {
namespace {
absl::optional<auction_worklet::mojom::BaseValue> BaseValueStringToEnum(
const std::string& base_value) {
if (base_value == "winning-bid") {
return auction_worklet::mojom::BaseValue::kWinningBid;
} else if (base_value == "highest-scoring-other-bid") {
return auction_worklet::mojom::BaseValue::kHighestScoringOtherBid;
} else if (base_value == "script-run-time") {
return auction_worklet::mojom::BaseValue::kScriptRunTime;
} else if (base_value == "signals-fetch-time") {
return auction_worklet::mojom::BaseValue::kSignalsFetchTime;
} else if (base_value == "bid-reject-reason") {
return auction_worklet::mojom::BaseValue::kBidRejectReason;
}
return absl::nullopt;
}
absl::optional<auction_worklet::mojom::BucketOffsetPtr>
ConvertBigIntToBucketOffset(v8::Local<v8::BigInt> bigint,
std::string* error_out) {
if (bigint.IsEmpty()) {
*error_out = "Failed to interpret as BigInt";
return absl::nullopt;
}
if (bigint->WordCount() > 2) {
*error_out = "BigInt is too large";
return absl::nullopt;
}
int word_count = 2;
int sign_bit = 0;
uint64_t words[2] = {0, 0};
bigint->ToWordsArray(&sign_bit, &word_count, words);
return auction_worklet::mojom::BucketOffset::New(
absl::MakeUint128(words[1], words[0]),
sign_bit);
}
absl::optional<auction_worklet::mojom::BaseValue> GetBaseValue(
gin::Dictionary& dictionary) {
std::string base_value_string;
if (!dictionary.Get("baseValue", &base_value_string)) {
return absl::nullopt;
}
return BaseValueStringToEnum(base_value_string);
}
absl::optional<double> GetScale(gin::Dictionary& dictionary) {
double scale = 1.0;
v8::Local<v8::Value> js_scale;
if (dictionary.Get("scale", &js_scale) && !js_scale.IsEmpty() &&
!js_scale->IsNullOrUndefined()) {
if (!js_scale->IsNumber()) {
return absl::nullopt;
}
scale = js_scale.As<v8::Number>()->Value();
if (!std::isfinite(scale)) {
return absl::nullopt;
}
}
return scale;
}
absl::optional<auction_worklet::mojom::SignalBucketPtr> GetSignalBucket(
v8::Isolate* isolate,
v8::Local<v8::Value> input) {
CHECK(input->IsObject());
gin::Dictionary result_dict(isolate, input.As<v8::Object>());
absl::optional<auction_worklet::mojom::BaseValue> base_value_opt =
GetBaseValue(result_dict);
if (!base_value_opt.has_value()) {
return absl::nullopt;
}
absl::optional<double> scale_opt = GetScale(result_dict);
if (!scale_opt.has_value()) {
return absl::nullopt;
}
v8::Local<v8::Value> js_offset;
if (!result_dict.Get("offset", &js_offset) || js_offset.IsEmpty() ||
js_offset->IsNullOrUndefined()) {
return auction_worklet::mojom::SignalBucket::New(*base_value_opt,
*scale_opt,
nullptr);
}
if (!js_offset->IsBigInt()) {
return absl::nullopt;
}
std::string error;
absl::optional<auction_worklet::mojom::BucketOffsetPtr> offset_opt =
ConvertBigIntToBucketOffset(js_offset.As<v8::BigInt>(), &error);
if (!offset_opt.has_value()) {
return nullptr;
}
return auction_worklet::mojom::SignalBucket::New(*base_value_opt, *scale_opt,
std::move(*offset_opt));
}
absl::optional<auction_worklet::mojom::SignalValuePtr> GetSignalValue(
v8::Isolate* isolate,
v8::Local<v8::Value> input) {
CHECK(input->IsObject());
gin::Dictionary result_dict(isolate, input.As<v8::Object>());
absl::optional<auction_worklet::mojom::BaseValue> base_value_opt =
GetBaseValue(result_dict);
if (!base_value_opt.has_value()) {
return absl::nullopt;
}
absl::optional<double> scale_opt = GetScale(result_dict);
if (!scale_opt.has_value()) {
return absl::nullopt;
}
v8::Local<v8::Value> js_offset;
if (!result_dict.Get("offset", &js_offset) || js_offset.IsEmpty() ||
js_offset->IsNullOrUndefined()) {
return auction_worklet::mojom::SignalValue::New(*base_value_opt, *scale_opt,
0);
}
if (!js_offset->IsInt32()) {
return absl::nullopt;
}
int32_t offset = js_offset.As<v8::Int32>()->Value();
return auction_worklet::mojom::SignalValue::New(*base_value_opt, *scale_opt,
offset);
}
auction_worklet::mojom::ForEventSignalBucketPtr GetBucket(
v8::Isolate* isolate,
v8::Local<v8::Value> js_bucket,
std::string* error) {
auction_worklet::mojom::ForEventSignalBucketPtr bucket;
if (js_bucket->IsBigInt()) {
std::string bucket_error;
absl::optional<absl::uint128> maybe_bucket =
worklet_utils::ConvertBigIntToUint128(js_bucket.As<v8::BigInt>(),
&bucket_error);
if (!maybe_bucket.has_value()) {
CHECK(base::IsStringUTF8(bucket_error));
*error = bucket_error;
return nullptr;
}
bucket = auction_worklet::mojom::ForEventSignalBucket::NewIdBucket(
*maybe_bucket);
} else if (js_bucket->IsObject()) {
absl::optional<auction_worklet::mojom::SignalBucketPtr>
maybe_signal_bucket_ptr = GetSignalBucket(isolate, js_bucket);
if (!maybe_signal_bucket_ptr.has_value()) {
*error = "Invalid bucket dictionary";
return nullptr;
}
bucket = auction_worklet::mojom::ForEventSignalBucket::NewSignalBucket(
std::move(*maybe_signal_bucket_ptr));
} else {
*error = "Bucket must be a BigInt or a dictionary";
return nullptr;
}
return bucket;
}
auction_worklet::mojom::ForEventSignalValuePtr GetValue(
v8::Isolate* isolate,
v8::Local<v8::Value> js_value,
std::string* error) {
auction_worklet::mojom::ForEventSignalValuePtr value;
if (js_value->IsInt32()) {
int int_value = js_value.As<v8::Int32>()->Value();
value = auction_worklet::mojom::ForEventSignalValue::NewIntValue(int_value);
if (int_value < 0) {
*error = "Value must be non-negative";
return nullptr;
}
} else if (js_value->IsObject()) {
absl::optional<auction_worklet::mojom::SignalValuePtr>
maybe_signal_value_ptr = GetSignalValue(isolate, js_value);
if (!maybe_signal_value_ptr.has_value()) {
*error = "Invalid value dictionary";
return nullptr;
}
value = auction_worklet::mojom::ForEventSignalValue::NewSignalValue(
std::move(*maybe_signal_value_ptr));
} else if (js_value->IsBigInt()) {
*error = "Value cannot be a BigInt";
return nullptr;
} else {
*error = "Value must be an integer or a dictionary";
return nullptr;
}
return value;
}
auction_worklet::mojom::AggregatableReportForEventContributionPtr
ParseForEventContribution(v8::Isolate* isolate,
const std::string& event_type,
v8::Local<v8::Value> arg_contribution,
std::string* error) {
gin::Dictionary dict(isolate);
bool success = gin::ConvertFromV8(isolate, arg_contribution, &dict);
CHECK(success);
v8::Local<v8::Value> js_bucket;
if (!dict.Get("bucket", &js_bucket) || js_bucket.IsEmpty() ||
js_bucket->IsNullOrUndefined()) {
*error =
"Invalid or missing bucket in reportContributionForEvent's argument";
return nullptr;
}
v8::Local<v8::Value> js_value;
if (!dict.Get("value", &js_value) || js_value.IsEmpty() ||
js_value->IsNullOrUndefined()) {
*error =
"Invalid or missing value in reportContributionForEvent's argument";
return nullptr;
}
auction_worklet::mojom::ForEventSignalBucketPtr bucket =
GetBucket(isolate, std::move(js_bucket), error);
if (!bucket) {
return nullptr;
}
auction_worklet::mojom::ForEventSignalValuePtr value =
GetValue(isolate, std::move(js_value), error);
if (!value) {
return nullptr;
}
return auction_worklet::mojom::AggregatableReportForEventContribution::New(
std::move(bucket), std::move(value), std::move(event_type));
}
}
const char kReservedAlways[] = "reserved.always";
const char kReservedWin[] = "reserved.win";
const char kReservedLoss[] = "reserved.loss";
PrivateAggregationBindings::PrivateAggregationBindings(
AuctionV8Helper* v8_helper,
bool private_aggregation_permissions_policy_allowed)
: v8_helper_(v8_helper),
private_aggregation_permissions_policy_allowed_(
private_aggregation_permissions_policy_allowed) {}
PrivateAggregationBindings::~PrivateAggregationBindings() = default;
void PrivateAggregationBindings::AttachToContext(
v8::Local<v8::Context> context) {
if (!base::FeatureList::IsEnabled(blink::features::kPrivateAggregationApi) ||
!blink::features::kPrivateAggregationApiEnabledInFledge.Get()) {
return;
}
v8::Local<v8::External> v8_this =
v8::External::New(v8_helper_->isolate(), this);
v8::Local<v8::Object> private_aggregation =
v8::Object::New(v8_helper_->isolate());
v8::Local<v8::Function> send_histogram_report_function =
v8::Function::New(
context, &PrivateAggregationBindings::SendHistogramReport, v8_this)
.ToLocalChecked();
private_aggregation
->Set(context, v8_helper_->CreateStringFromLiteral("sendHistogramReport"),
send_histogram_report_function)
.Check();
if (blink::features::kPrivateAggregationApiFledgeExtensionsEnabled.Get() ||
base::FeatureList::IsEnabled(
blink::features::
kPrivateAggregationApiFledgeExtensionsLocalTestingOverride)) {
v8::Local<v8::Function> report_contribution_for_event_function =
v8::Function::New(
context, &PrivateAggregationBindings::ReportContributionForEvent,
v8_this)
.ToLocalChecked();
private_aggregation
->Set(context,
v8_helper_->CreateStringFromLiteral("reportContributionForEvent"),
report_contribution_for_event_function)
.Check();
}
v8::Local<v8::Function> enable_debug_mode_function =
v8::Function::New(context, &PrivateAggregationBindings::EnableDebugMode,
v8_this)
.ToLocalChecked();
private_aggregation
->Set(context, v8_helper_->CreateStringFromLiteral("enableDebugMode"),
enable_debug_mode_function)
.Check();
context->Global()
->Set(context, v8_helper_->CreateStringFromLiteral("privateAggregation"),
private_aggregation)
.Check();
}
void PrivateAggregationBindings::Reset() {
private_aggregation_contributions_.clear();
debug_mode_details_.is_enabled = false;
debug_mode_details_.debug_key = nullptr;
}
std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>
PrivateAggregationBindings::TakePrivateAggregationRequests() {
std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr> requests;
requests.reserve(private_aggregation_contributions_.size());
base::ranges::transform(
private_aggregation_contributions_, std::back_inserter(requests),
[this](auction_worklet::mojom::AggregatableReportContributionPtr&
contribution) {
return auction_worklet::mojom::PrivateAggregationRequest::New(
std::move(contribution),
blink::mojom::AggregationServiceMode::kDefault,
debug_mode_details_.Clone());
});
private_aggregation_contributions_.clear();
return requests;
}
void PrivateAggregationBindings::SendHistogramReport(
const v8::FunctionCallbackInfo<v8::Value>& args) {
PrivateAggregationBindings* bindings =
static_cast<PrivateAggregationBindings*>(
v8::External::Cast(*args.Data())->Value());
blink::mojom::AggregatableReportHistogramContributionPtr contribution =
worklet_utils::ParseSendHistogramReportArguments(
gin::Arguments(args),
bindings->private_aggregation_permissions_policy_allowed_);
if (contribution.is_null()) {
return;
}
bindings->private_aggregation_contributions_.push_back(
auction_worklet::mojom::AggregatableReportContribution::
NewHistogramContribution(std::move(contribution)));
}
void PrivateAggregationBindings::ReportContributionForEvent(
const v8::FunctionCallbackInfo<v8::Value>& args) {
PrivateAggregationBindings* bindings =
static_cast<PrivateAggregationBindings*>(
v8::External::Cast(*args.Data())->Value());
AuctionV8Helper* v8_helper = bindings->v8_helper_;
v8::Isolate* isolate = v8_helper->isolate();
std::string event_type;
if (args.Length() < 2 || args[0].IsEmpty() || args[1].IsEmpty() ||
!gin::ConvertFromV8(isolate, args[0], &event_type) ||
!args[1]->IsObject()) {
isolate->ThrowException(
v8::Exception::TypeError(v8_helper->CreateStringFromLiteral(
"reportContributionForEvent requires 2 parameters, with first "
"parameter being a string and second parameter being an object")));
return;
}
if (base::StartsWith(event_type, "reserved.") && event_type != kReservedWin &&
event_type != kReservedLoss && event_type != kReservedAlways) {
return;
}
std::string error;
auction_worklet::mojom::AggregatableReportForEventContributionPtr
contribution =
ParseForEventContribution(isolate, event_type, args[1], &error);
if (contribution.is_null()) {
CHECK(base::IsStringUTF8(error));
isolate->ThrowException(v8::Exception::TypeError(
v8_helper->CreateUtf8String(error).ToLocalChecked()));
return;
}
bindings->private_aggregation_contributions_.push_back(
auction_worklet::mojom::AggregatableReportContribution::
NewForEventContribution(std::move(contribution)));
}
void PrivateAggregationBindings::EnableDebugMode(
const v8::FunctionCallbackInfo<v8::Value>& args) {
PrivateAggregationBindings* bindings =
static_cast<PrivateAggregationBindings*>(
v8::External::Cast(*args.Data())->Value());
worklet_utils::ParseAndApplyEnableDebugModeArguments(
gin::Arguments(args),
bindings->private_aggregation_permissions_policy_allowed_,
bindings->debug_mode_details_);
}
}