Commit 542dd7a8 authored by Caitlin Fischer's avatar Caitlin Fischer Committed by Commit Bot

Generate four types of X-Client-Data headers and store them in a map.

The status quo implementation generates two types,
cached_variation_ids_header_signed_in_ and cached_variation_ids_header_,
which are stored in strings.

In order to limit the number of VariationIDs included in the header in
third-party contexts, four headers are needed. The headers are stored in
a map.

FYI: While GetClientDataHeader()'s |web_visibility| argument is only
Study_GoogleWebVisibility_ANY for the time being, this will not always
be the case.

This change paves the way for limiting the set of experiment IDs
included in X-Client-Data headers sent in third-party contexts.

Bug: 1094303
Change-Id: Ifdc502fbc25fc3b09911414198022e059132a83c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2388763Reviewed-by: default avatarJohn Abd-El-Malek <jam@chromium.org>
Reviewed-by: default avatarAlexei Svitkine <asvitkine@chromium.org>
Commit-Queue: Caitlin Fischer <caitlinfischer@google.com>
Cr-Commit-Position: refs/heads/master@{#807875}
parent bf61ec62
......@@ -2151,6 +2151,7 @@ static_library("browser") {
"//components/variations",
"//components/variations/field_trial_config",
"//components/variations/net",
"//components/variations/proto",
"//components/variations/service",
"//components/vector_icons",
"//components/version_info",
......
......@@ -35,6 +35,7 @@
#include "components/optimization_guide/proto/hints.pb.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/variations/net/variations_http_headers.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/variations_ids_provider.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
......@@ -381,10 +382,19 @@ void CreateGoogleSignedInFieldTrial() {
scoped_refptr<base::FieldTrial> trial_1(CreateTrialAndAssociateId(
"t1", default_name, variations::GOOGLE_WEB_PROPERTIES_SIGNED_IN, 123));
auto* variations_ids_provider =
variations::VariationsIdsProvider::GetInstance();
EXPECT_NE(variations_ids_provider->GetClientDataHeader(true),
variations_ids_provider->GetClientDataHeader(false));
auto* provider = variations::VariationsIdsProvider::GetInstance();
EXPECT_NE(
provider->GetClientDataHeader(
/*is_signed_in=*/true, variations::Study_GoogleWebVisibility_ANY),
provider->GetClientDataHeader(
/*is_signed_in=*/false, variations::Study_GoogleWebVisibility_ANY));
EXPECT_NE(provider->GetClientDataHeader(
/*is_signed_in=*/true,
variations::Study_GoogleWebVisibility_FIRST_PARTY),
provider->GetClientDataHeader(
/*is_signed_in=*/false,
variations::Study_GoogleWebVisibility_FIRST_PARTY));
}
} // namespace
......@@ -439,8 +449,10 @@ IN_PROC_BROWSER_TEST_F(VariationsHttpHeadersBrowserTest, UserSignedIn) {
base::Optional<std::string> header =
GetReceivedHeader(GetGoogleUrl(), "X-Client-Data");
ASSERT_TRUE(header);
EXPECT_EQ(*header, variations::VariationsIdsProvider::GetInstance()
->GetClientDataHeader(true));
EXPECT_EQ(
*header,
variations::VariationsIdsProvider::GetInstance()->GetClientDataHeader(
/*is_signed_in=*/true, variations::Study_GoogleWebVisibility_ANY));
}
IN_PROC_BROWSER_TEST_F(VariationsHttpHeadersBrowserTest, UserNotSignedIn) {
......@@ -454,8 +466,10 @@ IN_PROC_BROWSER_TEST_F(VariationsHttpHeadersBrowserTest, UserNotSignedIn) {
base::Optional<std::string> header =
GetReceivedHeader(GetGoogleUrl(), "X-Client-Data");
ASSERT_TRUE(header);
EXPECT_EQ(*header, variations::VariationsIdsProvider::GetInstance()
->GetClientDataHeader(false));
EXPECT_EQ(
*header,
variations::VariationsIdsProvider::GetInstance()->GetClientDataHeader(
/*is_signed_in=*/false, variations::Study_GoogleWebVisibility_ANY));
}
IN_PROC_BROWSER_TEST_F(
......
......@@ -29,6 +29,7 @@
#include "components/sync/base/sync_prefs.h"
#include "components/sync/driver/sync_driver_switches.h"
#include "components/sync/driver/sync_service.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/variations_client.h"
#include "components/variations/variations_ids_provider.h"
#include "content/public/browser/notification_service.h"
......@@ -92,9 +93,13 @@ class ChromeVariationsClient : public variations::VariationsClient {
return browser_context_->IsOffTheRecord();
}
// TODO(crbug/1094303): Update the signature to accept a
// variations::Study_GoogleWebVisibility and pass the given value to
// GetClientDataHeader().
std::string GetVariationsHeader() const override {
return variations::VariationsIdsProvider::GetInstance()
->GetClientDataHeader(IsSignedIn());
->GetClientDataHeader(IsSignedIn(),
variations::Study_GoogleWebVisibility_ANY);
}
private:
......
......@@ -16,6 +16,7 @@
#include "base/strings/string_util.h"
#include "components/google/core/common/google_util.h"
#include "components/variations/net/omnibox_http_headers.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/variations_ids_provider.h"
#include "net/base/isolation_info.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
......@@ -234,9 +235,12 @@ class VariationsHeaderHelper {
}
private:
// TODO(crbug/1094303): Update the signature to accept a
// Study_GoogleWebVisibility and pass the given value to
// GetClientDataHeader().
static std::string CreateVariationsHeader(SignedIn signed_in) {
return VariationsIdsProvider::GetInstance()->GetClientDataHeader(
signed_in == SignedIn::kYes);
signed_in == SignedIn::kYes, Study_GoogleWebVisibility_ANY);
}
network::ResourceRequest* resource_request_;
......
......@@ -47,6 +47,8 @@ void VariationsURLLoaderThrottle::AppendThrottleIfNeeded(
if (!variations_client || variations_client->IsOffTheRecord())
return;
// TODO(crbug/1094303): Consider both variations::Study_GoogleWebVisibility
// values.
throttles->push_back(std::make_unique<VariationsURLLoaderThrottle>(
variations_client->GetVariationsHeader()));
}
......
......@@ -20,6 +20,8 @@ class VariationsClient {
virtual bool IsOffTheRecord() const = 0;
// Returns the variations header that should be appended for google requests.
// TODO(crbug/1094303): Update the signature to take a
// variations::Study_GoogleWebVisibility.
virtual std::string GetVariationsHeader() const = 0;
};
......
......@@ -4,8 +4,6 @@
#include "components/variations/variations_ids_provider.h"
#include <stddef.h>
#include <algorithm>
#include "base/base64.h"
......@@ -19,6 +17,12 @@
namespace variations {
bool VariationsHeaderKey::operator<(const VariationsHeaderKey& other) const {
if (is_signed_in != other.is_signed_in)
return is_signed_in < other.is_signed_in;
return web_visibility < other.web_visibility;
}
// Adding/removing headers is implemented by request consumers, and how it is
// implemented depends on the request type.
// There are three cases:
......@@ -37,17 +41,20 @@ VariationsIdsProvider* VariationsIdsProvider::GetInstance() {
return base::Singleton<VariationsIdsProvider>::get();
}
std::string VariationsIdsProvider::GetClientDataHeader(bool is_signed_in) {
std::string VariationsIdsProvider::GetClientDataHeader(
bool is_signed_in,
Study_GoogleWebVisibility web_visibility) {
// Lazily initialize the header, if not already done, before attempting to
// transmit it.
InitVariationIDsCacheIfNeeded();
std::string variation_ids_header_copy;
{
base::AutoLock scoped_lock(lock_);
variation_ids_header_copy = is_signed_in
? cached_variation_ids_header_signed_in_
: cached_variation_ids_header_;
auto it = variations_headers_map_.find(
VariationsHeaderKey{is_signed_in, web_visibility});
if (it == variations_headers_map_.end())
return "";
variation_ids_header_copy = it->second;
}
return variation_ids_header_copy;
}
......@@ -149,8 +156,7 @@ void VariationsIdsProvider::ResetForTesting() {
default_variation_ids_set_.clear();
synthetic_variation_ids_set_.clear();
force_disabled_ids_set_.clear();
cached_variation_ids_header_.clear();
cached_variation_ids_header_signed_in_.clear();
variations_headers_map_.clear();
}
VariationsIdsProvider::VariationsIdsProvider()
......@@ -228,17 +234,30 @@ void VariationsIdsProvider::CacheVariationsId(const std::string& trial_name,
void VariationsIdsProvider::UpdateVariationIDsHeaderValue() {
lock_.AssertAcquired();
// The header value is a serialized protobuffer of Variation IDs which is
// base64 encoded before transmitting as a string.
cached_variation_ids_header_.clear();
cached_variation_ids_header_signed_in_.clear();
variations_headers_map_.clear();
// If successful, swap the header value with the new one.
// Note that the list of IDs and the header could be temporarily out of sync
// if IDs are added as the header is recreated. The receiving servers are OK
// with such discrepancies.
cached_variation_ids_header_ = GenerateBase64EncodedProto(false);
cached_variation_ids_header_signed_in_ = GenerateBase64EncodedProto(true);
variations_headers_map_[VariationsHeaderKey{/*is_signed_in=*/false,
Study_GoogleWebVisibility_ANY}] =
GenerateBase64EncodedProto(/*is_signed_in=*/false,
/*is_first_party_context=*/false);
variations_headers_map_[VariationsHeaderKey{
/*is_signed_in=*/false, Study_GoogleWebVisibility_FIRST_PARTY}] =
GenerateBase64EncodedProto(/*is_signed_in=*/false,
/*is_first_party_context=*/true);
variations_headers_map_[VariationsHeaderKey{/*is_signed_in=*/true,
Study_GoogleWebVisibility_ANY}] =
GenerateBase64EncodedProto(/*is_signed_in=*/true,
/*is_first_party_context=*/false);
variations_headers_map_[VariationsHeaderKey{
/*is_signed_in=*/true, Study_GoogleWebVisibility_FIRST_PARTY}] =
GenerateBase64EncodedProto(/*is_signed_in=*/true,
/*is_first_party_context=*/true);
for (auto& observer : observer_list_) {
observer.VariationIdsHeaderUpdated();
......@@ -246,7 +265,8 @@ void VariationsIdsProvider::UpdateVariationIDsHeaderValue() {
}
std::string VariationsIdsProvider::GenerateBase64EncodedProto(
bool is_signed_in) {
bool is_signed_in,
bool is_first_party_context) {
std::set<VariationIDEntry> all_variation_ids_set = GetAllVariationIds();
ClientVariations proto;
......@@ -262,15 +282,14 @@ std::string VariationsIdsProvider::GenerateBase64EncodedProto(
case GOOGLE_WEB_PROPERTIES_FIRST_PARTY:
if (base::FeatureList::IsEnabled(
internal::kRestrictGoogleWebVisibility)) {
// TODO(crbug/1094303): Send fewer VariationIDs in third-party
// contexts by excluding IDs associated with
// GOOGLE_WEB_PROPERTIES_FIRST_PARTY.
break;
if (is_first_party_context)
proto.add_variation_id(entry.first);
} else {
// When the feature is not enabled, treat VariationIDs associated with
// GOOGLE_WEB_PROPERTIES_FIRST_PARTY in the same way as those
// associated with GOOGLE_WEB_PROPERTIES_ANY_CONTEXT.
proto.add_variation_id(entry.first);
}
// When the feature is not enabled, treat VariationIDs associated with
// GOOGLE_WEB_PROPERTIES_FIRST_PARTY in the same way as those
// associated with GOOGLE_WEB_PROPERTIES_ANY_CONTEXT.
proto.add_variation_id(entry.first);
break;
case GOOGLE_WEB_PROPERTIES_TRIGGER_ANY_CONTEXT:
proto.add_trigger_variation_id(entry.first);
......@@ -278,15 +297,14 @@ std::string VariationsIdsProvider::GenerateBase64EncodedProto(
case GOOGLE_WEB_PROPERTIES_TRIGGER_FIRST_PARTY:
if (base::FeatureList::IsEnabled(
internal::kRestrictGoogleWebVisibility)) {
// TODO(crbug/1094303): Send fewer VariationIDs in third-party
// contexts by excluding IDs associated with
// GOOGLE_WEB_PROPERTIES_FIRST_PARTY.
break;
if (is_first_party_context)
proto.add_trigger_variation_id(entry.first);
} else {
// When the feature is not enabled, treat VariationIDs associated with
// GOOGLE_WEB_PROPERTIES_TRIGGER_FIRST_PARTY in the same way as those
// associated with GOOGLE_WEB_PROPERTIES_TRIGGER_ANY_CONTEXT.
proto.add_trigger_variation_id(entry.first);
}
// When the feature is not enabled, treat VariationIDs associated with
// GOOGLE_WEB_PROPERTIES_TRIGGER_FIRST_PARTY in the same way as those
// associated with GOOGLE_WEB_PROPERTIES_TRIGGER_ANY_CONTEXT.
proto.add_trigger_variation_id(entry.first);
break;
case GOOGLE_APP:
// These IDs should not be added into Google Web headers.
......
......@@ -5,6 +5,7 @@
#ifndef COMPONENTS_VARIATIONS_VARIATIONS_IDS_PROVIDER_H_
#define COMPONENTS_VARIATIONS_VARIATIONS_IDS_PROVIDER_H_
#include <map>
#include <set>
#include <string>
#include <utility>
......@@ -15,6 +16,7 @@
#include "base/metrics/field_trial.h"
#include "base/observer_list.h"
#include "base/synchronization/lock.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/synthetic_trials.h"
#include "components/variations/variations_associated_data.h"
......@@ -26,6 +28,21 @@ struct DefaultSingletonTraits;
namespace variations {
class VariationsClient;
// The key for a VariationsIdsProvider's |variations_headers_map_|. A
// VariationsHeaderKey provides more details about the VariationsIDs included in
// a particular header. For example, the header associated with a key with true
// for |is_signed_in| and Study_GoogleWebVisibility_ANY for |web_visibility| has
// (i) VariationsIDs associated with external experiments, which can be sent
// only for signed-in users and (ii) VariationsIDs that can be sent in first-
// and third-party contexts.
struct VariationsHeaderKey {
bool is_signed_in;
Study_GoogleWebVisibility web_visibility;
// This allows the struct to be used as a key in a map.
bool operator<(const VariationsHeaderKey& other) const;
};
// A helper class for maintaining client experiments and metrics state
// transmitted in custom HTTP request headers.
// This class is a thread-safe singleton.
......@@ -43,11 +60,15 @@ class VariationsIdsProvider : public base::FieldTrialList::Observer,
static VariationsIdsProvider* GetInstance();
// Returns the value of the client data header, computing and caching it if
// necessary. If |is_signed_in| is false, variation ids that should only be
// sent for signed in users (i.e. GOOGLE_WEB_PROPERTIES_SIGNED_IN entries)
// will not be included.
std::string GetClientDataHeader(bool is_signed_in);
// Returns the value of the X-Client-Data header corresponding to
// |is_signed_in| and |web_visibility|, computing and caching the header if
// necessary. If |is_signed_in| is false, VariationIDs that should be sent for
// only signed in users (i.e. GOOGLE_WEB_PROPERTIES_SIGNED_IN entries) are not
// included. Considering |web_visibility| allows fewer VariationIDs to be sent
// in third-party contexts. See IsFirstPartyContext() in
// variations_http_headers.cc for more details.
std::string GetClientDataHeader(bool is_signed_in,
Study_GoogleWebVisibility web_visibility);
// Returns a space-separated string containing the list of current active
// variations (as would be reported in the |variation_id| repeated field of
......@@ -160,9 +181,10 @@ class VariationsIdsProvider : public base::FieldTrialList::Observer,
// held.
void UpdateVariationIDsHeaderValue();
// Generates a base64-encoded proto to be used as a header value for the given
// |is_signed_in| state.
std::string GenerateBase64EncodedProto(bool is_signed_in);
// Generates a base64-encoded ClientVariations proto to be used as a header
// value for the given |is_signed_in| and |is_first_party_context| states.
std::string GenerateBase64EncodedProto(bool is_signed_in,
bool is_first_party_context);
// Adds variation ids and trigger variation ids to |target_set|.
static bool AddVariationIdsToSet(
......@@ -203,8 +225,14 @@ class VariationsIdsProvider : public base::FieldTrialList::Observer,
// Provides the google experiment ids that are force-disabled by command line.
std::set<VariationIDEntry> force_disabled_ids_set_;
std::string cached_variation_ids_header_;
std::string cached_variation_ids_header_signed_in_;
// A collection of variations headers. Each header is a base64-encoded
// ClientVariations proto containing VariationIDs that may be sent to Google
// web properties. For more details about when this may be sent, see
// AppendHeaderIfNeeded() in variations_http_headers.cc.
//
// The key for each header describes the VariationIDs included in its
// associated header. See VariationsHeaderKey's comments for more information.
std::map<VariationsHeaderKey, std::string> variations_headers_map_;
// List of observers to notify on variation ids header update.
// NOTE this should really check observers are unregistered but due to
......
......@@ -12,6 +12,7 @@
#include "base/test/task_environment.h"
#include "components/variations/entropy_provider.h"
#include "components/variations/proto/client_variations.pb.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/variations_associated_data.h"
#include "testing/gtest/include/gtest/gtest.h"
......@@ -75,7 +76,8 @@ TEST_F(VariationsIdsProviderTest, ForceVariationIds_Valid) {
EXPECT_EQ(VariationsIdsProvider::ForceIdsResult::SUCCESS,
provider.ForceVariationIds({"12", "456", "t789"}, ""));
provider.InitVariationIDsCacheIfNeeded();
std::string variations = provider.GetClientDataHeader(false);
std::string variations = provider.GetClientDataHeader(
/*is_signed_in=*/false, Study_GoogleWebVisibility_ANY);
EXPECT_FALSE(variations.empty());
std::set<VariationID> variation_ids;
std::set<VariationID> trigger_ids;
......@@ -93,7 +95,8 @@ TEST_F(VariationsIdsProviderTest, ForceVariationIds_ValidCommandLine) {
EXPECT_EQ(VariationsIdsProvider::ForceIdsResult::SUCCESS,
provider.ForceVariationIds({"12"}, "456,t789"));
provider.InitVariationIDsCacheIfNeeded();
std::string variations = provider.GetClientDataHeader(false);
std::string variations = provider.GetClientDataHeader(
/*is_signed_in=*/false, Study_GoogleWebVisibility_ANY);
EXPECT_FALSE(variations.empty());
std::set<VariationID> variation_ids;
std::set<VariationID> trigger_ids;
......@@ -111,19 +114,28 @@ TEST_F(VariationsIdsProviderTest, ForceVariationIds_Invalid) {
EXPECT_EQ(VariationsIdsProvider::ForceIdsResult::INVALID_VECTOR_ENTRY,
provider.ForceVariationIds({"abcd12", "456"}, ""));
provider.InitVariationIDsCacheIfNeeded();
EXPECT_TRUE(provider.GetClientDataHeader(false).empty());
EXPECT_TRUE(provider
.GetClientDataHeader(/*is_signed_in=*/false,
Study_GoogleWebVisibility_ANY)
.empty());
// Invalid trigger experiment id
EXPECT_EQ(VariationsIdsProvider::ForceIdsResult::INVALID_VECTOR_ENTRY,
provider.ForceVariationIds({"12", "tabc456"}, ""));
provider.InitVariationIDsCacheIfNeeded();
EXPECT_TRUE(provider.GetClientDataHeader(false).empty());
EXPECT_TRUE(provider
.GetClientDataHeader(/*is_signed_in=*/false,
Study_GoogleWebVisibility_ANY)
.empty());
// Invalid command-line ids.
EXPECT_EQ(VariationsIdsProvider::ForceIdsResult::INVALID_SWITCH_ENTRY,
provider.ForceVariationIds({"12", "50"}, "tabc456"));
provider.InitVariationIDsCacheIfNeeded();
EXPECT_TRUE(provider.GetClientDataHeader(false).empty());
EXPECT_TRUE(provider
.GetClientDataHeader(/*is_signed_in=*/false,
Study_GoogleWebVisibility_ANY)
.empty());
}
TEST_F(VariationsIdsProviderTest, ForceDisableVariationIds_ValidCommandLine) {
......@@ -134,7 +146,8 @@ TEST_F(VariationsIdsProviderTest, ForceDisableVariationIds_ValidCommandLine) {
provider.ForceVariationIds({"1", "2", "t3", "t4"}, "5,6,t7,t8"));
EXPECT_TRUE(provider.ForceDisableVariationIds("2,t4,6,t8"));
provider.InitVariationIDsCacheIfNeeded();
std::string variations = provider.GetClientDataHeader(false);
std::string variations = provider.GetClientDataHeader(
/*is_signed_in=*/false, Study_GoogleWebVisibility_ANY);
EXPECT_FALSE(variations.empty());
std::set<VariationID> variation_ids;
std::set<VariationID> trigger_ids;
......@@ -156,7 +169,10 @@ TEST_F(VariationsIdsProviderTest, ForceDisableVariationIds_Invalid) {
EXPECT_FALSE(provider.ForceDisableVariationIds("abc"));
EXPECT_FALSE(provider.ForceDisableVariationIds("tabc456"));
provider.InitVariationIDsCacheIfNeeded();
EXPECT_TRUE(provider.GetClientDataHeader(false).empty());
EXPECT_TRUE(provider
.GetClientDataHeader(/*is_signed_in=*/false,
Study_GoogleWebVisibility_ANY)
.empty());
}
TEST_F(VariationsIdsProviderTest, OnFieldTrialGroupFinalized) {
......@@ -190,7 +206,8 @@ TEST_F(VariationsIdsProviderTest, OnFieldTrialGroupFinalized) {
// Get non-signed in ids.
{
std::string variations = provider.GetClientDataHeader(false);
std::string variations = provider.GetClientDataHeader(
/*is_signed_in=*/false, Study_GoogleWebVisibility_ANY);
std::set<VariationID> variation_ids;
std::set<VariationID> trigger_ids;
ASSERT_TRUE(ExtractVariationIds(variations, &variation_ids, &trigger_ids));
......@@ -204,7 +221,8 @@ TEST_F(VariationsIdsProviderTest, OnFieldTrialGroupFinalized) {
// Now, get signed-in ids.
{
std::string variations = provider.GetClientDataHeader(true);
std::string variations = provider.GetClientDataHeader(
/*is_signed_in=*/true, Study_GoogleWebVisibility_ANY);
std::set<VariationID> variation_ids;
std::set<VariationID> trigger_ids;
ASSERT_TRUE(ExtractVariationIds(variations, &variation_ids, &trigger_ids));
......
......@@ -117,6 +117,8 @@ void FieldTrialSynchronizer::UpdateRendererVariationsHeader(
renderer_variations_configuration;
channel->GetRemoteAssociatedInterface(&renderer_variations_configuration);
// TODO(crbug/1094303): Consider both variations::Study_GoogleWebVisibility
// values.
renderer_variations_configuration->SetVariationsHeader(
client->GetVariationsHeader());
}
......
......@@ -429,6 +429,7 @@ source_set("weblayer_lib_base") {
"//components/user_prefs",
"//components/variations",
"//components/variations/net",
"//components/variations/proto",
"//components/variations/service",
"//components/version_info",
"//components/viz/common",
......
......@@ -25,6 +25,7 @@
#include "components/translate/core/browser/translate_pref_names.h"
#include "components/translate/core/browser/translate_prefs.h"
#include "components/user_prefs/user_prefs.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/variations_client.h"
#include "components/variations/variations_ids_provider.h"
#include "content/public/browser/device_service.h"
......@@ -295,9 +296,13 @@ class BrowserContextImpl::WebLayerVariationsClient
return browser_context_->IsOffTheRecord();
}
// TODO(crbug/1094303): Update the signature to accept a
// variations::Study_GoogleWebVisibility and pass the given value to
// GetClientDataHeader().
std::string GetVariationsHeader() const override {
return variations::VariationsIdsProvider::GetInstance()
->GetClientDataHeader(IsSignedIn());
->GetClientDataHeader(IsSignedIn(),
variations::Study_GoogleWebVisibility_ANY);
}
private:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment