Commit c657d45e authored by Emily Stark's avatar Emily Stark Committed by Commit Bot

Implement a pref mode for the recurrent error message

When a certificate error recurs multiple times within a browsing session, we add
a special message to the interstitial. This CL adds a Finch-controlled mode
where the count of certificate errors is stored in a pref instead of in memory,
so that we can trigger the recurrent error message when an error recurs within a
certain period of time (even spanning browsing sessions). We might want to roll
out this pref mode on certain platforms (like Android) where browsing sessions
might be significantly shorter than desktop.

Bug: 839969
Change-Id: I04f1af1aa30043821859f4459602627b9ba7af77
Reviewed-on: https://chromium-review.googlesource.com/1053563
Commit-Queue: Emily Stark <estark@chromium.org>
Reviewed-by: default avatarChristopher Thompson <cthomp@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#557966}
parent 87d1292f
......@@ -54,6 +54,7 @@
#include "chrome/browser/rlz/chrome_rlz_tracker_delegate.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/signin/signin_manager_factory.h"
#include "chrome/browser/ssl/chrome_ssl_host_state_delegate.h"
#include "chrome/browser/ssl/ssl_config_service_manager.h"
#include "chrome/browser/task_manager/task_manager_interface.h"
#include "chrome/browser/tracing/chrome_tracing_delegate.h"
......@@ -481,6 +482,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
browsing_data::prefs::RegisterBrowserUserPrefs(registry);
certificate_transparency::prefs::RegisterPrefs(registry);
ChromeContentBrowserClient::RegisterProfilePrefs(registry);
ChromeSSLHostStateDelegate::RegisterProfilePrefs(registry);
ChromeVersionService::RegisterProfilePrefs(registry);
chrome_browser_net::Predictor::RegisterProfilePrefs(registry);
chrome_browser_net::RegisterPredictionOptionsProfilePrefs(registry);
......
......@@ -222,6 +222,10 @@ void Profile::RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kWebShareVisitedTargets);
registry->RegisterDictionaryPref(prefs::kExcludedSchemes);
// Instead of registering new prefs here, please create a static method and
// invoke it from RegisterProfilePrefs() in
// chrome/browser/prefs/browser_prefs.cc.
}
std::string Profile::GetDebugName() {
......
......@@ -25,8 +25,12 @@
#include "base/values.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/common/content_switches.h"
#include "net/base/hash_value.h"
......@@ -39,11 +43,31 @@
namespace {
const char kRecurrentInterstitialThresholdParam[] = "threshold";
const int kRecurrentInterstitialDefaultThreshold = 3;
// The default expiration is one week, unless overidden by a field trial group.
// See https://crbug.com/487270.
// Parameters and defaults for the |kRecurrentInterstitialFeature| field trial.
// This parameter controls whether the count of recurrent errors is
// per-browsing-session or persisted to a pref, accumulating across browsing
// sessions. Default is "in-memory".
constexpr char kRecurrentInterstitialModeParam[] = "mode";
constexpr char kRecurrentInterstitialModeInMemory[] = "in-memory";
constexpr char kRecurrentInterstitialModePref[] = "pref";
// The number of times an error must recur before the recurrent error message is
// shown.
constexpr char kRecurrentInterstitialThresholdParam[] = "threshold";
constexpr int kRecurrentInterstitialDefaultThreshold = 3;
// If "mode" is "pref", a pref stores the time at which each error most recently
// occurred, and the recurrent error message is shown if the error has recurred
// more than the threshold number of times with the most recent instance being
// less than |kRecurrentInterstitialResetTimeParam| seconds in the past. The
// default is 3 days.
constexpr char kRecurrentInterstitialResetTimeParam[] = "reset-time";
constexpr int kRecurrentInterstitialDefaultResetTime =
259200; // 3 days in seconds
// The default expiration for certificate error bypasses is one week, unless
// overidden by a field trial group. See https://crbug.com/487270.
const uint64_t kDeltaDefaultExpirationInSeconds = UINT64_C(604800);
// Field trial information
......@@ -60,6 +84,85 @@ const char kSSLCertDecisionGUIDKey[] = "guid";
const int kDefaultSSLCertDecisionVersion = 1;
// Records a new occurrence of |error|. The occurrence is stored in the
// recurrent interstitial pref, which keeps track of the most recent timestamps
// at which each error type occurred (up to the |threshold| most recent
// instances per error). The list is reset if the clock has gone backwards at
// any point.
void UpdateRecurrentInterstitialPref(Profile* profile,
base::Clock* clock,
int error,
int threshold) {
double now = clock->Now().ToJsTime();
DictionaryPrefUpdate pref_update(profile->GetPrefs(),
prefs::kRecurrentSSLInterstitial);
base::Value* list_value =
pref_update->FindKey(net::ErrorToShortString(error));
if (list_value) {
// Check that the values are in increasing order and wipe out the list if
// not (presumably because the clock changed).
base::ListValue::ListStorage& error_list = list_value->GetList();
double previous = 0;
for (const auto& error_instance : error_list) {
double error_time = error_instance.GetDouble();
if (error_time < previous) {
list_value = nullptr;
break;
}
previous = error_time;
}
if (now < previous)
list_value = nullptr;
}
if (!list_value) {
// Either there was no list of occurrences of this error, or it was corrupt
// (i.e. out of order). Save a new list composed of just this one error
// instance.
base::ListValue error_list;
error_list.GetList().push_back(base::Value(now));
pref_update->SetKey(net::ErrorToShortString(error), std::move(error_list));
} else {
// Only up to |threshold| values need to be stored. If the list already
// contains |threshold| values, pop one off the front and append the new one
// at the end; otherwise just append the new one.
base::ListValue::ListStorage& error_list = list_value->GetList();
while (base::MakeStrictNum(error_list.size()) >= threshold) {
error_list.erase(error_list.begin());
}
error_list.push_back(base::Value(now));
pref_update->SetKey(net::ErrorToShortString(error),
base::ListValue(error_list));
}
}
bool DoesRecurrentInterstitialPrefMeetThreshold(Profile* profile,
base::Clock* clock,
int error,
int threshold) {
const base::DictionaryValue* pref =
profile->GetPrefs()->GetDictionary(prefs::kRecurrentSSLInterstitial);
const base::Value* list_value = pref->FindKey(net::ErrorToShortString(error));
if (!list_value)
return false;
base::Time cutoff_time =
clock->Now() -
base::TimeDelta::FromSeconds(base::GetFieldTrialParamByFeatureAsInt(
kRecurrentInterstitialFeature, kRecurrentInterstitialResetTimeParam,
kRecurrentInterstitialDefaultResetTime));
// Assume that the values in the list are in increasing order;
// UpdateRecurrentInterstitialPref() maintains this ordering. Check if there
// are more than |threshold| values after the cutoff time.
const base::ListValue::ListStorage& error_list = list_value->GetList();
for (size_t i = 0; i < error_list.size(); i++) {
if (base::Time::FromJsTime(error_list[i].GetDouble()) >= cutoff_time)
return base::MakeStrictNum(error_list.size() - i) >= threshold;
}
return false;
}
void CloseIdleConnections(
scoped_refptr<net::URLRequestContextGetter> url_request_context_getter) {
url_request_context_getter->
......@@ -297,6 +400,11 @@ ChromeSSLHostStateDelegate::ChromeSSLHostStateDelegate(Profile* profile)
ChromeSSLHostStateDelegate::~ChromeSSLHostStateDelegate() {
}
void ChromeSSLHostStateDelegate::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kRecurrentSSLInterstitial);
}
void ChromeSSLHostStateDelegate::AllowCert(const std::string& host,
const net::X509Certificate& cert,
net::CertStatus error) {
......@@ -496,7 +604,8 @@ bool ChromeSSLHostStateDelegate::DidHostRunInsecureContent(
return false;
}
void ChromeSSLHostStateDelegate::SetClock(std::unique_ptr<base::Clock> clock) {
void ChromeSSLHostStateDelegate::SetClockForTesting(
std::unique_ptr<base::Clock> clock) {
clock_ = std::move(clock);
}
......@@ -505,29 +614,52 @@ void ChromeSSLHostStateDelegate::DidDisplayErrorPage(int error) {
error != net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED) {
return;
}
const auto count_it = recurrent_errors_.find(error);
if (count_it == recurrent_errors_.end()) {
recurrent_errors_[error] = 1;
if (!base::FeatureList::IsEnabled(kRecurrentInterstitialFeature)) {
return;
}
if (count_it->second >= base::GetFieldTrialParamByFeatureAsInt(
kRecurrentInterstitialFeature,
kRecurrentInterstitialThresholdParam,
kRecurrentInterstitialDefaultThreshold)) {
return;
const std::string mode_param = base::GetFieldTrialParamValueByFeature(
kRecurrentInterstitialFeature, kRecurrentInterstitialModeParam);
const int threshold = base::GetFieldTrialParamByFeatureAsInt(
kRecurrentInterstitialFeature, kRecurrentInterstitialThresholdParam,
kRecurrentInterstitialDefaultThreshold);
if (mode_param.empty() || mode_param == kRecurrentInterstitialModeInMemory) {
const auto count_it = recurrent_errors_.find(error);
if (count_it == recurrent_errors_.end()) {
recurrent_errors_[error] = 1;
return;
}
if (count_it->second >= threshold) {
return;
}
recurrent_errors_[error] = count_it->second + 1;
} else if (mode_param == kRecurrentInterstitialModePref) {
UpdateRecurrentInterstitialPref(profile_, clock_.get(), error, threshold);
}
recurrent_errors_[error] = count_it->second + 1;
}
bool ChromeSSLHostStateDelegate::HasSeenRecurrentErrors(int error) const {
if (!base::FeatureList::IsEnabled(kRecurrentInterstitialFeature)) {
return false;
}
const auto count = recurrent_errors_.find(error);
if (count == recurrent_errors_.end())
return false;
return count->second >= base::GetFieldTrialParamByFeatureAsInt(
kRecurrentInterstitialFeature,
kRecurrentInterstitialThresholdParam,
kRecurrentInterstitialDefaultThreshold);
const std::string mode_param = base::GetFieldTrialParamValueByFeature(
kRecurrentInterstitialFeature, kRecurrentInterstitialModeParam);
const int threshold = base::GetFieldTrialParamByFeatureAsInt(
kRecurrentInterstitialFeature, kRecurrentInterstitialThresholdParam,
kRecurrentInterstitialDefaultThreshold);
if (mode_param.empty() || mode_param == kRecurrentInterstitialModeInMemory) {
const auto count_it = recurrent_errors_.find(error);
if (count_it == recurrent_errors_.end())
return false;
return count_it->second >= threshold;
} else if (mode_param == kRecurrentInterstitialModePref) {
return DoesRecurrentInterstitialPrefMeetThreshold(profile_, clock_.get(),
error, threshold);
}
return false;
}
......@@ -9,7 +9,6 @@
#include <set>
#include "base/feature_list.h"
#include "base/gtest_prod_util.h"
#include "base/macros.h"
#include "base/time/time.h"
#include "content/public/browser/ssl_host_state_delegate.h"
......@@ -21,6 +20,12 @@ class Clock;
class DictionaryValue;
} // namespace base
namespace user_prefs {
class PrefRegistrySyncable;
} // namespace user_prefs
// The Finch feature that controls whether a message is shown when users
// encounter the same error multiiple times.
extern const base::Feature kRecurrentInterstitialFeature;
// Tracks state related to certificate and SSL errors. This state includes:
......@@ -33,6 +38,8 @@ class ChromeSSLHostStateDelegate : public content::SSLHostStateDelegate {
explicit ChromeSSLHostStateDelegate(Profile* profile);
~ChromeSSLHostStateDelegate() override;
static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
// SSLHostStateDelegate:
void AllowCert(const std::string& host,
const net::X509Certificate& cert,
......@@ -73,21 +80,17 @@ class ChromeSSLHostStateDelegate : public content::SSLHostStateDelegate {
void DidDisplayErrorPage(int error);
// Returns true if DidDisplayErrorPage() has been called over a threshold
// number of times for a particular error. Always returns false if
// |kRecurrentInterstitialFeature| is not enabled. Only certain error codes of
// interest are tracked, so this may return false for an error code that has
// recurred.
// number of times for a particular error in a particular time period. Always
// returns false if |kRecurrentInterstitialFeature| is not enabled. The number
// of times and time period are controlled by the feature parameters. Only
// certain error codes of interest are tracked, so this may return false for
// an error code that has recurred.
bool HasSeenRecurrentErrors(int error) const;
protected:
// SetClock takes ownership of the passed in clock.
void SetClock(std::unique_ptr<base::Clock> clock);
// SetClockForTesting takes ownership of the passed in clock.
void SetClockForTesting(std::unique_ptr<base::Clock> clock);
private:
FRIEND_TEST_ALL_PREFIXES(DefaultMemorySSLHostStateDelegateTest, AfterRestart);
FRIEND_TEST_ALL_PREFIXES(DefaultMemorySSLHostStateDelegateTest,
QueryPolicyExpired);
// Used to specify whether new content setting entries should be created if
// they don't already exist when querying the user's settings.
enum CreateDictionaryEntriesDisposition {
......
......@@ -346,7 +346,8 @@ IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDelegateTest, Migrate) {
}
// Tests that ChromeSSLHostStateDelegate::HasSeenRecurrentErrors returns true
// after seeing an error of interest multiple times.
// after seeing an error of interest multiple times, in the default mode in
// which error occurrences are stored in-memory.
IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDelegateTest, HasSeenRecurrentErrors) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(kRecurrentInterstitialFeature,
......@@ -369,6 +370,127 @@ IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDelegateTest, HasSeenRecurrentErrors) {
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
}
// Tests that ChromeSSLHostStateDelegate::HasSeenRecurrentErrors returns true
// after seeing an error of interest multiple times in pref mode (where the
// count of each error is persisted across browsing sessions).
IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDelegateTest,
HasSeenRecurrentErrorsPref) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
kRecurrentInterstitialFeature, {{"threshold", "2"}, {"mode", "pref"}});
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate();
ChromeSSLHostStateDelegate* chrome_state =
static_cast<ChromeSSLHostStateDelegate*>(state);
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_FALSE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
chrome_state->DidDisplayErrorPage(net::ERR_CERT_SYMANTEC_LEGACY);
EXPECT_FALSE(
chrome_state->HasSeenRecurrentErrors(net::ERR_CERT_SYMANTEC_LEGACY));
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_TRUE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
chrome_state->DidDisplayErrorPage(net::ERR_CERT_SYMANTEC_LEGACY);
EXPECT_TRUE(
chrome_state->HasSeenRecurrentErrors(net::ERR_CERT_SYMANTEC_LEGACY));
// Create a new ChromeSSLHostStateDelegate to check that the state has been
// saved to the pref and that the new ChromeSSLHostStateDelegate reads it.
ChromeSSLHostStateDelegate new_state(profile);
EXPECT_TRUE(new_state.HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
EXPECT_TRUE(new_state.HasSeenRecurrentErrors(net::ERR_CERT_SYMANTEC_LEGACY));
// Also test the logic for when the number of displayed errors exceeds the
// threshold.
new_state.DidDisplayErrorPage(net::ERR_CERT_SYMANTEC_LEGACY);
EXPECT_TRUE(new_state.HasSeenRecurrentErrors(net::ERR_CERT_SYMANTEC_LEGACY));
}
// Tests that ChromeSSLHostStateDelegate::HasSeenRecurrentErrors handles clocks
// going backwards in pref mode.
IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDelegateTest,
HasSeenRecurrentErrorsPrefClockGoesBackwards) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
kRecurrentInterstitialFeature, {{"threshold", "2"}, {"mode", "pref"}});
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate();
ChromeSSLHostStateDelegate* chrome_state =
static_cast<ChromeSSLHostStateDelegate*>(state);
base::SimpleTestClock* clock = new base::SimpleTestClock();
clock->SetNow(base::Time::Now());
chrome_state->SetClockForTesting(
std::unique_ptr<base::SimpleTestClock>(clock));
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_FALSE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
// Move the clock backwards and test that the recurrent error state is reset.
clock->Advance(-base::TimeDelta::FromSeconds(10));
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_FALSE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
// If the clock continues to move forwards, a subsequent error page should
// trigger the recurrent error message.
clock->Advance(base::TimeDelta::FromSeconds(10));
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_TRUE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
}
// Tests that ChromeSSLHostStateDelegate::HasSeenRecurrentErrors in pref mode
// ignores errors that occurred too far in the past. Note that this test uses a
// threshold of 3 errors, unlike previous tests which use a threshold of 2.
IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDelegateTest,
HasSeenRecurrentErrorsPrefErrorsInPast) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
kRecurrentInterstitialFeature,
{{"threshold", "3"}, {"mode", "pref"}, {"reset-time", "10"}});
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate();
ChromeSSLHostStateDelegate* chrome_state =
static_cast<ChromeSSLHostStateDelegate*>(state);
base::SimpleTestClock* clock = new base::SimpleTestClock();
clock->SetNow(base::Time::Now());
chrome_state->SetClockForTesting(
std::unique_ptr<base::SimpleTestClock>(clock));
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_FALSE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
// Subsequent errors more than 10 seconds later shouldn't trigger the
// recurrent error message.
clock->Advance(base::TimeDelta::FromSeconds(12));
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_FALSE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
clock->Advance(base::TimeDelta::FromSeconds(3));
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_FALSE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
// But a third subsequent error within 10 seconds should.
clock->Advance(base::TimeDelta::FromSeconds(3));
chrome_state->DidDisplayErrorPage(net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED);
EXPECT_TRUE(chrome_state->HasSeenRecurrentErrors(
net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED));
}
class ForgetAtSessionEndSSLHostStateDelegateTest
: public ChromeSSLHostStateDelegateTest {
protected:
......@@ -542,7 +664,7 @@ IN_PROC_BROWSER_TEST_F(DefaultMemorySSLHostStateDelegateTest, AfterRestart) {
base::SimpleTestClock* clock = new base::SimpleTestClock();
ChromeSSLHostStateDelegate* chrome_state =
static_cast<ChromeSSLHostStateDelegate*>(state);
chrome_state->SetClock(std::unique_ptr<base::Clock>(clock));
chrome_state->SetClockForTesting(std::unique_ptr<base::Clock>(clock));
// Start the clock at standard system time.
clock->SetNow(base::Time::NowFromSystemTime());
......@@ -591,7 +713,7 @@ IN_PROC_BROWSER_TEST_F(DefaultMemorySSLHostStateDelegateTest,
base::SimpleTestClock* clock = new base::SimpleTestClock();
ChromeSSLHostStateDelegate* chrome_state =
static_cast<ChromeSSLHostStateDelegate*>(state);
chrome_state->SetClock(std::unique_ptr<base::Clock>(clock));
chrome_state->SetClockForTesting(std::unique_ptr<base::Clock>(clock));
// Start the clock at standard system time but do not advance at all to
// emphasize that instant forget works.
......
......@@ -100,6 +100,11 @@ const char kSessionExitType[] = "profile.exit_type";
// in-product help is active. Observed time is active session time in seconds.
const char kObservedSessionTime[] = "profile.observed_session_time";
// Stores counts and timestamps of SSL certificate errors that have occurred.
// When the same error recurs within some period of time, a message is added to
// the SSL interstitial.
const char kRecurrentSSLInterstitial[] = "profile.ssl_recurrent_interstitial";
// The last time that the site engagement service recorded an engagement event
// for this profile for any URL. Recorded only during shutdown. Used to prevent
// the service from decaying engagement when a user does not use Chrome at all
......
......@@ -40,6 +40,7 @@ extern const char kRestoreOnStartup[];
extern const char kSessionExitedCleanly[];
extern const char kSessionExitType[];
extern const char kObservedSessionTime[];
extern const char kRecurrentSSLInterstitial[];
extern const char kSiteEngagementLastUpdateTime[];
extern const char kSupervisedUserApprovedExtensions[];
extern const char kSupervisedUserCustodianEmail[];
......
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