Commit b535aa4a authored by Eric Robinson's avatar Eric Robinson Committed by Commit Bot

Adding reporting for third party content that accesses storage.

This CL adds UseCounters for activation of third party frames,
as well as for general access to these frames, and third parties
that had frames that were both activated and accessed.

Change-Id: Ib366336a2553c5625341147e82875c4265b42225
Bug: 1115657
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2402196
Commit-Queue: Eric Robinson <ericrobinson@chromium.org>
Reviewed-by: default avatarCharlie Harrison <csharrison@chromium.org>
Reviewed-by: default avatarYao Xiao <yaoxia@chromium.org>
Cr-Commit-Position: refs/heads/master@{#813760}
parent 309b54c3
......@@ -37,35 +37,11 @@ bool IsSameSite(const GURL& url1, const GURL& url2) {
} // namespace
ThirdPartyMetricsObserver::AccessedTypes::AccessedTypes(
AccessType access_type) {
switch (access_type) {
case AccessType::kCookieRead:
cookie_read = true;
break;
case AccessType::kCookieWrite:
cookie_write = true;
break;
case AccessType::kLocalStorage:
local_storage = true;
break;
case AccessType::kSessionStorage:
session_storage = true;
break;
// No extra metadata required for the following types as they only record
// use counters.
case AccessType::kFileSystem:
case AccessType::kIndexedDb:
case AccessType::kCacheStorage:
break;
case AccessType::kUnknown:
NOTREACHED();
break;
}
}
ThirdPartyMetricsObserver::ThirdPartyMetricsObserver() = default;
ThirdPartyMetricsObserver::~ThirdPartyMetricsObserver() = default;
ThirdPartyMetricsObserver::ThirdPartyInfo::ThirdPartyInfo() = default;
ThirdPartyMetricsObserver::ThirdPartyInfo::ThirdPartyInfo(
const ThirdPartyInfo&) = default;
page_load_metrics::PageLoadMetricsObserver::ObservePolicy
ThirdPartyMetricsObserver::FlushMetricsOnAppEnterBackground(
......@@ -76,6 +52,23 @@ ThirdPartyMetricsObserver::FlushMetricsOnAppEnterBackground(
return STOP_OBSERVING;
}
void ThirdPartyMetricsObserver::FrameReceivedFirstUserActivation(
content::RenderFrameHost* render_frame_host) {
bool is_third_party = false;
auto* third_party_info = GetThirdPartyInfo(
render_frame_host->GetLastCommittedURL(),
content::WebContents::FromRenderFrameHost(render_frame_host)
->GetMainFrame()
->GetLastCommittedURL(),
is_third_party);
// Update the activation status and record use counters as necessary.
if (is_third_party && third_party_info != nullptr) {
third_party_info->activation = true;
RecordUseCounters(AccessType::kMaxValue, third_party_info);
}
}
void ThirdPartyMetricsObserver::OnComplete(
const page_load_metrics::mojom::PageLoadTiming& timing) {
RecordMetrics(timing);
......@@ -113,10 +106,35 @@ void ThirdPartyMetricsObserver::OnCookieChange(
AccessType::kCookieWrite);
}
void ThirdPartyMetricsObserver::RecordStorageAccessUseCounter(
AccessType access_type) {
// TODO(crbug.com/1115657): It would be simpler to just pass in ThirdPartyInfo
// and set the bits appropriately, but because this is called every time an
// access is made, that would mean re-calling old accesses. This could be fixed
// by calling this only when the page is removed or when backgrounded.
void ThirdPartyMetricsObserver::RecordUseCounters(
AccessType access_type,
const ThirdPartyInfo* third_party_info) {
page_load_metrics::mojom::PageLoadFeatures third_party_storage_features;
// We only record access/activation if the third_party_info didn't overflow.
if (third_party_info != nullptr) {
// Record any sort of access.
if (third_party_info->access_types.any()) {
third_party_storage_features.features.push_back(
blink::mojom::WebFeature::kThirdPartyAccess);
}
// Record any sort of activation.
if (third_party_info->activation) {
third_party_storage_features.features.push_back(
blink::mojom::WebFeature::kThirdPartyActivation);
}
// Record the combination of the above two
if (third_party_info->access_types.any() && third_party_info->activation) {
third_party_storage_features.features.push_back(
blink::mojom::WebFeature::kThirdPartyAccessAndActivation);
}
}
// Record the specific type of access, if appropriate.
switch (access_type) {
case AccessType::kCookieRead:
third_party_storage_features.features.push_back(
......@@ -146,14 +164,18 @@ void ThirdPartyMetricsObserver::RecordStorageAccessUseCounter(
third_party_storage_features.features.push_back(
blink::mojom::WebFeature::kThirdPartyCacheStorage);
break;
default
: // No feature usage recorded for storage types without a use counter.
return;
default:
// No feature usage recorded for storage types without a use counter.
// Also nothing reported for non storage access.
break;
}
page_load_metrics::MetricsWebContentsObserver::RecordFeatureUsage(
GetDelegate().GetWebContents()->GetMainFrame(),
third_party_storage_features);
// Report the feature usage if there's anything to report.
if (third_party_storage_features.features.size() > 0) {
page_load_metrics::MetricsWebContentsObserver::RecordFeatureUsage(
GetDelegate().GetWebContents()->GetMainFrame(),
third_party_storage_features);
}
}
void ThirdPartyMetricsObserver::OnStorageAccessed(
......@@ -220,26 +242,19 @@ void ThirdPartyMetricsObserver::OnTimingUpdate(
}
}
void ThirdPartyMetricsObserver::OnCookieOrStorageAccess(
const GURL& url,
const GURL& first_party_url,
bool blocked_by_policy,
AccessType access_type) {
if (blocked_by_policy) {
should_record_metrics_ = false;
return;
}
if (!url.is_valid())
return;
ThirdPartyMetricsObserver::ThirdPartyInfo*
ThirdPartyMetricsObserver::GetThirdPartyInfo(const GURL& url,
const GURL& first_party_url,
bool& is_third_party) {
is_third_party = false;
// TODO(csharrison): Optimize the domain lookup.
// Note: If either |url| or |first_party_url| is empty, SameDomainOrHost will
// return false, and function execution will continue because it is considered
// 3rd party. Since |first_party_url| is actually the |site_for_cookies|, this
// will happen e.g. for a 3rd party iframe on document.cookie access.
if (IsSameSite(url, first_party_url))
return;
if (!url.is_valid() || IsSameSite(url, first_party_url))
return nullptr;
std::string registrable_domain =
net::registry_controlled_domains::GetDomainAndRegistry(
......@@ -253,49 +268,48 @@ void ThirdPartyMetricsObserver::OnCookieOrStorageAccess(
if (url.has_host()) {
registrable_domain = url.host();
} else {
return;
return nullptr;
}
}
RecordStorageAccessUseCounter(access_type);
// If we haven't returned by this point, this is a third party access.
is_third_party = true;
GURL representative_url(
base::StrCat({url.scheme(), "://", registrable_domain, "/"}));
auto it = all_third_party_info_.find(representative_url);
if (it == all_third_party_info_.end() &&
all_third_party_info_.size() < 1000) { // Bound growth.
it = all_third_party_info_.emplace(url, ThirdPartyInfo()).first;
}
// If there's no valid iterator, we've gone over the size limit for the map.
// TODO(crbug.com/1115657): We probably want UMA to let us know how often we
// might be underreporting.
return (it == all_third_party_info_.end() ? nullptr : &it->second);
}
auto it = third_party_accessed_types_.find(representative_url);
if (it != third_party_accessed_types_.end()) {
switch (access_type) {
case AccessType::kCookieRead:
it->second.cookie_read = true;
break;
case AccessType::kCookieWrite:
it->second.cookie_write = true;
break;
case AccessType::kLocalStorage:
it->second.local_storage = true;
break;
case AccessType::kSessionStorage:
it->second.session_storage = true;
break;
// No metadata is tracked for the following types as they only record use
// counters.
case AccessType::kFileSystem:
case AccessType::kIndexedDb:
case AccessType::kCacheStorage:
break;
case AccessType::kUnknown:
NOTREACHED();
break;
}
void ThirdPartyMetricsObserver::OnCookieOrStorageAccess(
const GURL& url,
const GURL& first_party_url,
bool blocked_by_policy,
AccessType access_type) {
DCHECK(access_type != AccessType::kUnknown);
if (blocked_by_policy) {
should_record_metrics_ = false;
return;
}
// Don't let the map grow unbounded.
if (third_party_accessed_types_.size() >= 1000)
bool is_third_party = false;
auto* third_party_info =
GetThirdPartyInfo(url, first_party_url, is_third_party);
if (!is_third_party)
return;
if (third_party_info != nullptr) {
third_party_info->access_types[static_cast<size_t>(access_type)] = true;
}
third_party_accessed_types_.emplace(representative_url, access_type);
// Record the use counters as necessary.
RecordUseCounters(access_type, third_party_info);
}
void ThirdPartyMetricsObserver::RecordMetrics(
......@@ -308,11 +322,16 @@ void ThirdPartyMetricsObserver::RecordMetrics(
int local_storage_origin_access = 0;
int session_storage_origin_access = 0;
for (auto it : third_party_accessed_types_) {
cookie_origin_reads += it.second.cookie_read;
cookie_origin_writes += it.second.cookie_write;
local_storage_origin_access += it.second.local_storage;
session_storage_origin_access += it.second.session_storage;
for (auto it : all_third_party_info_) {
const ThirdPartyInfo& tpi = it.second;
if (tpi.access_types[static_cast<size_t>(AccessType::kCookieRead)])
++cookie_origin_reads;
if (tpi.access_types[static_cast<size_t>(AccessType::kCookieWrite)])
++cookie_origin_writes;
if (tpi.access_types[static_cast<size_t>(AccessType::kLocalStorage)])
++local_storage_origin_access;
if (tpi.access_types[static_cast<size_t>(AccessType::kSessionStorage)])
++session_storage_origin_access;
}
UMA_HISTOGRAM_COUNTS_1000("PageLoad.Clients.ThirdParty.Origins.CookieRead2",
......
......@@ -17,6 +17,9 @@
class ThirdPartyMetricsObserver
: public page_load_metrics::PageLoadMetricsObserver {
public:
// TODO(crbug.com/1115657): kUnknown is mostly unused except for passing it as
// a "dummy" type to RecordUseCounters. After we factor out AccessType from
// that method (see other TODOs), we should be able to remove it.
enum class AccessType {
kCookieRead,
kCookieWrite,
......@@ -26,6 +29,7 @@ class ThirdPartyMetricsObserver
kIndexedDb,
kCacheStorage,
kUnknown,
kMaxValue = kUnknown
};
ThirdPartyMetricsObserver();
......@@ -34,6 +38,8 @@ class ThirdPartyMetricsObserver
// page_load_metrics::PageLoadMetricsObserver:
ObservePolicy FlushMetricsOnAppEnterBackground(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
void FrameReceivedFirstUserActivation(
content::RenderFrameHost* render_frame_host) override;
void OnComplete(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
void OnLoadedResource(const page_load_metrics::ExtraRequestCompleteInfo&
......@@ -58,14 +64,22 @@ class ThirdPartyMetricsObserver
const page_load_metrics::mojom::PageLoadTiming& timing) override;
private:
struct AccessedTypes {
explicit AccessedTypes(AccessType access_type);
bool cookie_read = false;
bool cookie_write = false;
bool local_storage = false;
bool session_storage = false;
// The info about the types of activities for a third party.
struct ThirdPartyInfo {
ThirdPartyInfo();
ThirdPartyInfo(const ThirdPartyInfo&);
std::bitset<static_cast<size_t>(AccessType::kMaxValue)> access_types;
bool activation = false;
};
// Returns a pointer to the ThirdPartyInfo in all_third_party_info_ for |url|
// and |first_party_url|, adding an entry as necessary. The out parameter
// |is_third_party| indicates whether the two inputs are third party one
// another and may be true with a nullptr return if the map is full.
ThirdPartyInfo* GetThirdPartyInfo(const GURL& url,
const GURL& first_party_url,
bool& is_third_party);
void OnCookieOrStorageAccess(const GURL& url,
const GURL& first_party_url,
bool blocked_by_policy,
......@@ -73,14 +87,15 @@ class ThirdPartyMetricsObserver
void RecordMetrics(
const page_load_metrics::mojom::PageLoadTiming& main_frame_timing);
// Records feature usage for |access_type| with use counters.
void RecordStorageAccessUseCounter(AccessType access_type);
// Records feature usage for teh |access_type|, and also, when present, for
// generic access and activation for the |third_party_info|.
void RecordUseCounters(AccessType access_type,
const ThirdPartyInfo* third_party_info);
AccessType StorageTypeToAccessType(
page_load_metrics::StorageType storage_type);
// A map of third parties that have read or written cookies, or have
// accessed local storage or session storage on this page.
// A map of third parties and the types of activities they have performed.
//
// A third party document.cookie / window.localStorage /
// window.sessionStorage happens when the context's scheme://eTLD+1
......@@ -88,7 +103,7 @@ class ThirdPartyMetricsObserver
// when the URL request's scheme://eTLD+1 differs from the main frame's.
// For URLs which have no registrable domain, the hostname is used
// instead.
std::map<GURL, AccessedTypes> third_party_accessed_types_;
std::map<GURL, ThirdPartyInfo> all_third_party_info_;
// A set of RenderFrameHosts that we've recorded timing data for. The
// RenderFrameHosts are later removed when they navigate again or are deleted.
......
......@@ -111,6 +111,9 @@ class ThirdPartyMetricsObserverBrowserTest : public InProcessBrowserTest {
waiter->Wait();
}
// TODO(ericrobinson) The following functions all have an assumed frame.
// Prefer passing in a frame to make the tests clearer and extendable.
void NavigateFrameAndWaitForFCP(
const std::string& host,
const std::string& path,
......@@ -137,6 +140,14 @@ class ThirdPartyMetricsObserverBrowserTest : public InProcessBrowserTest {
EXPECT_TRUE(NavigateIframeToURL(web_contents(), "test", url));
}
void TriggerFrameActivation() {
// Activate one frame by executing a dummy script.
content::RenderFrameHost* ad_frame =
ChildFrameAt(web_contents()->GetMainFrame(), 0);
const std::string no_op_script = "// No-op script";
EXPECT_TRUE(ExecuteScript(ad_frame, no_op_script));
}
content::WebContents* web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
......@@ -227,6 +238,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest, NoStorageEvent) {
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 0);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
0);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
......@@ -244,6 +258,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 0);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
0);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
......@@ -264,6 +281,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
1);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
......@@ -289,6 +309,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
1);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
......@@ -313,6 +336,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
1);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
......@@ -338,6 +364,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 0);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
0);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
......@@ -364,6 +393,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
1);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
......@@ -387,6 +419,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 0);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
0);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
......@@ -410,6 +445,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyCookieWrite, 1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
1);
}
class ThirdPartyDomStorageAccessMetricsObserverBrowserTest
......@@ -509,6 +547,9 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount("Blink.UseCounter.Features", test_case,
0);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyAccess, 0);
}
}
......@@ -531,7 +572,85 @@ IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
histogram_tester.ExpectBucketCount("Blink.UseCounter.Features", test_case,
1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyAccess, 1);
}
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
ThirdPartyFrameWithActivationReported) {
base::HistogramTester histogram_tester;
NavigateToPageWithFrame("a.com");
NavigateFrameTo("b.com", "/");
TriggerFrameActivation();
NavigateToUntrackedUrl();
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyActivation, 1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
0);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyAccessAndActivation, 0);
}
IN_PROC_BROWSER_TEST_F(ThirdPartyMetricsObserverBrowserTest,
FirstPartyFrameWithActivationNotReported) {
base::HistogramTester histogram_tester;
NavigateToPageWithFrame("a.com");
NavigateFrameTo("a.com", "/");
TriggerFrameActivation();
NavigateToUntrackedUrl();
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyActivation, 0);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
0);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyAccessAndActivation, 0);
}
IN_PROC_BROWSER_TEST_F(
ThirdPartyMetricsObserverBrowserTest,
ThirdPartyFrameWithAccessAndActivationOnDifferentThirdParties) {
base::HistogramTester histogram_tester;
NavigateToPageWithFrame("a.com");
NavigateFrameTo("b.com", "/");
TriggerFrameActivation();
NavigateFrameTo("c.com", "/set-cookie?thirdparty=1;SameSite=None;Secure");
NavigateToUntrackedUrl();
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyActivation, 1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyAccessAndActivation, 0);
}
IN_PROC_BROWSER_TEST_F(
ThirdPartyMetricsObserverBrowserTest,
ThirdPartyFrameWithAccessAndActivationOnSameThirdParties) {
base::HistogramTester histogram_tester;
NavigateToPageWithFrame("a.com");
NavigateFrameTo("b.com", "/set-cookie?thirdparty=1;SameSite=None;Secure");
TriggerFrameActivation();
NavigateToUntrackedUrl();
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyActivation, 1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features", blink::mojom::WebFeature::kThirdPartyAccess,
1);
histogram_tester.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kThirdPartyAccessAndActivation, 1);
}
} // namespace
......@@ -3022,6 +3022,9 @@ enum WebFeature {
kAddressSpacePrivateEmbeddedInPublicNonSecureContext = 3695,
kAddressSpacePrivateEmbeddedInUnknownSecureContext = 3696,
kAddressSpacePrivateEmbeddedInUnknownNonSecureContext = 3697,
kThirdPartyAccess = 3698,
kThirdPartyActivation = 3699,
kThirdPartyAccessAndActivation = 3700,
// Add new features immediately above this line. Don't change assigned
// numbers of any item, and don't reuse removed slots.
......
......@@ -29583,6 +29583,9 @@ Called by update_use_counter_feature_enum.py.-->
<int value="3696" label="AddressSpacePrivateEmbeddedInUnknownSecureContext"/>
<int value="3697"
label="AddressSpacePrivateEmbeddedInUnknownNonSecureContext"/>
<int value="3698" label="ThirdPartyAccess"/>
<int value="3699" label="ThirdPartyActivation"/>
<int value="3700" label="ThirdPartyAccessAndActivation"/>
</enum>
<enum name="FeaturePolicyAllowlistType">
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