Commit 375564a1 authored by zysxqn@google.com's avatar zysxqn@google.com

Generate passwords only for the forms that autofill server classifies one of...

Generate passwords only for the forms that autofill server classifies one of its fields as ACCOUNT_CREATION_PASSWORD.

BUG=

Review URL: https://chromiumcodereview.appspot.com/23432002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@221773 0039d316-1c4b-4281-b951-d872f2087c98
parent a15f140a
......@@ -151,6 +151,9 @@ bool AwAutofillManagerDelegate::IsAutocompleteEnabled() {
return GetSaveFormData();
}
void AwAutofillManagerDelegate::DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) {}
void AwAutofillManagerDelegate::SuggestionSelected(JNIEnv* env,
jobject object,
jint position) {
......
......@@ -83,6 +83,8 @@ class AwAutofillManagerDelegate
const std::vector<base::string16>& labels) OVERRIDE;
virtual void HideAutofillPopup() OVERRIDE;
virtual bool IsAutocompleteEnabled() OVERRIDE;
virtual void DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) OVERRIDE;
void SuggestionSelected(JNIEnv* env,
jobject obj,
......
......@@ -13,8 +13,12 @@
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/pref_names.h"
#include "components/autofill/core/browser/autofill_field.h"
#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/password_generator.h"
#include "components/autofill/core/common/autofill_messages.h"
#include "components/autofill/core/common/form_data.h"
#include "components/user_prefs/pref_registry_syncable.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_view_host.h"
......@@ -55,6 +59,25 @@ void PasswordGenerationManager::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
}
void PasswordGenerationManager::DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) {
std::vector<autofill::FormData> account_creation_forms;
for (std::vector<autofill::FormStructure*>::const_iterator form_it =
forms.begin(); form_it != forms.end(); ++form_it) {
autofill::FormStructure* form = *form_it;
for (std::vector<autofill::AutofillField*>::const_iterator field_it =
form->begin(); field_it != form->end(); ++field_it) {
autofill::AutofillField* field = *field_it;
if (field->server_type() == autofill::ACCOUNT_CREATION_PASSWORD) {
account_creation_forms.push_back(form->ToFormData());
break;
}
}
}
SendAccountCreationFormsToRenderer(web_contents()->GetRenderViewHost(),
account_creation_forms);
}
void PasswordGenerationManager::RegisterWithSyncService() {
Profile* profile = Profile::FromBrowserContext(
web_contents()->GetBrowserContext());
......@@ -151,6 +174,13 @@ void PasswordGenerationManager::SendStateToRenderer(
enabled));
}
void PasswordGenerationManager::SendAccountCreationFormsToRenderer(
content::RenderViewHost* host,
const std::vector<autofill::FormData>& forms) {
host->Send(new AutofillMsg_AccountCreationFormsDetected(
host->GetRoutingID(), forms));
}
void PasswordGenerationManager::OnShowPasswordGenerationPopup(
const gfx::Rect& bounds,
int max_length,
......
......@@ -14,6 +14,8 @@
#include "content/public/browser/web_contents_user_data.h"
namespace autofill {
struct FormData;
class FormStructure;
class PasswordGenerator;
}
......@@ -49,6 +51,10 @@ class PasswordGenerationManager
static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
virtual ~PasswordGenerationManager();
// Detect account creation forms from forms with autofill type annotated.
void DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms);
protected:
explicit PasswordGenerationManager(content::WebContents* contents);
......@@ -81,6 +87,10 @@ class PasswordGenerationManager
// is a separate function to aid in testing.
virtual void SendStateToRenderer(content::RenderViewHost* host, bool enabled);
virtual void SendAccountCreationFormsToRenderer(
content::RenderViewHost* host,
const std::vector<autofill::FormData>& forms);
// Causes the password generation bubble UI to be shown for the specified
// form. The popup will be anchored at |icon_bounds|. The generated
// password will be no longer than |max_length|.
......
......@@ -5,6 +5,7 @@
#include <vector>
#include "base/prefs/pref_service.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/password_manager/password_generation_manager.h"
#include "chrome/browser/password_manager/password_manager.h"
#include "chrome/browser/password_manager/password_manager_delegate_impl.h"
......@@ -13,8 +14,27 @@
#include "chrome/common/pref_names.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "components/autofill/core/browser/autofill_field.h"
#include "components/autofill/core/browser/autofill_metrics.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/form_field_data.h"
#include "content/public/test/test_browser_thread.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace {
// Unlike the base AutofillMetrics, exposes copy and assignment constructors,
// which are handy for briefer test code. The AutofillMetrics class is
// stateless, so this is safe.
class TestAutofillMetrics : public autofill::AutofillMetrics {
public:
TestAutofillMetrics() {}
virtual ~TestAutofillMetrics() {}
};
} // anonymous namespace
class TestPasswordGenerationManager : public PasswordGenerationManager {
public:
......@@ -27,16 +47,32 @@ class TestPasswordGenerationManager : public PasswordGenerationManager {
sent_states_.push_back(enabled);
}
virtual void SendAccountCreationFormsToRenderer(
content::RenderViewHost* host,
const std::vector<autofill::FormData>& forms) OVERRIDE {
sent_account_creation_forms_.insert(
sent_account_creation_forms_.begin(), forms.begin(), forms.end());
}
const std::vector<bool>& GetSentStates() {
return sent_states_;
}
const std::vector<autofill::FormData>& GetSentAccountCreationForms() {
return sent_account_creation_forms_;
}
void ClearSentStates() {
sent_states_.clear();
}
void ClearSentAccountCreationForms() {
sent_account_creation_forms_.clear();
}
private:
std::vector<bool> sent_states_;
std::vector<autofill::FormData> sent_account_creation_forms_;
DISALLOW_COPY_AND_ASSIGN(TestPasswordGenerationManager);
};
......@@ -59,6 +95,11 @@ class PasswordGenerationManagerTest : public ChromeRenderViewHostTestHarness {
password_generation_manager_->UpdateState(NULL, new_renderer);
}
void DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) {
password_generation_manager_->DetectAccountCreationForms(forms);
}
scoped_ptr<TestPasswordGenerationManager> password_generation_manager_;
};
......@@ -180,6 +221,56 @@ TEST_F(PasswordGenerationManagerTest, UpdatePasswordSyncState) {
password_generation_manager_->ClearSentStates();
}
TEST_F(PasswordGenerationManagerTest, DetectAccountCreationForms) {
autofill::FormData login_form;
login_form.origin = GURL("http://www.yahoo.com/login/");
autofill::FormFieldData username;
username.label = ASCIIToUTF16("username");
username.name = ASCIIToUTF16("login");
username.form_control_type = "text";
login_form.fields.push_back(username);
autofill::FormFieldData password;
password.label = ASCIIToUTF16("password");
password.name = ASCIIToUTF16("password");
password.form_control_type = "password";
login_form.fields.push_back(password);
autofill::FormStructure form1(login_form);
std::vector<autofill::FormStructure*> forms;
forms.push_back(&form1);
autofill::FormData account_creation_form;
account_creation_form.origin = GURL("http://accounts.yahoo.com/");
account_creation_form.fields.push_back(username);
account_creation_form.fields.push_back(password);
autofill::FormFieldData confirm_password;
confirm_password.label = ASCIIToUTF16("confirm_password");
confirm_password.name = ASCIIToUTF16("password");
confirm_password.form_control_type = "password";
account_creation_form.fields.push_back(confirm_password);
autofill::FormStructure form2(account_creation_form);
forms.push_back(&form2);
// Simulate the server response to set the field types.
const char* const kServerResponse =
"<autofillqueryresponse>"
"<field autofilltype=\"9\" />"
"<field autofilltype=\"75\" />"
"<field autofilltype=\"9\" />"
"<field autofilltype=\"76\" />"
"<field autofilltype=\"75\" />"
"</autofillqueryresponse>";
autofill::FormStructure::ParseQueryResponse(
kServerResponse,
forms,
TestAutofillMetrics());
DetectAccountCreationForms(forms);
EXPECT_EQ(1u,
password_generation_manager_->GetSentAccountCreationForms().size());
EXPECT_EQ(
GURL("http://accounts.yahoo.com/"),
password_generation_manager_->GetSentAccountCreationForms()[0].origin);
}
TEST_F(IncognitoPasswordGenerationManagerTest,
UpdatePasswordSyncStateIncognito) {
// Disable password manager by going incognito, and enable syncing. The
......
......@@ -9,6 +9,7 @@
#include "chrome/browser/autofill/autofill_cc_infobar_delegate.h"
#include "chrome/browser/autofill/personal_data_manager_factory.h"
#include "chrome/browser/infobars/infobar_service.h"
#include "chrome/browser/password_manager/password_generation_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/autofill/autofill_dialog_controller.h"
#include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h"
......@@ -171,4 +172,12 @@ void TabAutofillManagerDelegate::WebContentsDestroyed(
HideAutofillPopup();
}
void TabAutofillManagerDelegate::DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) {
PasswordGenerationManager* manager =
PasswordGenerationManager::FromWebContents(web_contents_);
if (manager)
manager->DetectAccountCreationForms(forms);
}
} // namespace autofill
......@@ -64,6 +64,9 @@ class TabAutofillManagerDelegate
virtual void HideAutofillPopup() OVERRIDE;
virtual bool IsAutocompleteEnabled() OVERRIDE;
virtual void DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) OVERRIDE;
// content::WebContentsObserver implementation.
virtual void DidNavigateMainFrame(
const content::LoadCommittedDetails& details,
......
......@@ -5,8 +5,10 @@
#include "components/autofill/content/renderer/password_generation_manager.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "components/autofill/content/renderer/password_form_conversion_utils.h"
#include "components/autofill/core/common/autofill_messages.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/password_generation_util.h"
#include "content/public/renderer/render_view.h"
#include "google_apis/gaia/gaia_urls.h"
......@@ -64,6 +66,27 @@ bool GetAccountCreationPasswordFields(
return false;
}
bool ContainsURL(const std::vector<GURL>& urls, const GURL& url) {
return std::find(urls.begin(), urls.end(), url) != urls.end();
}
// Returns true if the |form1| is essentially equal to |form2|.
bool FormEquals(const autofill::FormData& form1,
const content::PasswordForm& form2) {
// TODO(zysxqn): use more signals than just origin to compare.
return form1.origin == form2.origin;
}
bool ContainsForm(const std::vector<autofill::FormData>& forms,
const content::PasswordForm& form) {
for (std::vector<autofill::FormData>::const_iterator it =
forms.begin(); it != forms.end(); ++it) {
if (FormEquals(*it, form))
return true;
}
return false;
}
} // namespace
PasswordGenerationManager::PasswordGenerationManager(
......@@ -83,10 +106,13 @@ void PasswordGenerationManager::DidFinishDocumentLoad(WebKit::WebFrame* frame) {
// as we don't want subframe loads to clear state that we have recieved from
// the main frame. Note that we assume there is only one account creation
// form, but there could be multiple password forms in each frame.
//
// TODO(zysxqn): Add stat when local heuristic fires but we don't show the
// password generation icon.
if (!frame->parent()) {
not_blacklisted_password_form_origins_.clear();
// Initialize to an empty and invalid GURL.
account_creation_form_origin_ = GURL();
account_creation_forms_.clear();
possible_account_creation_form_.reset(new content::PasswordForm());
passwords_.clear();
}
}
......@@ -127,7 +153,7 @@ void PasswordGenerationManager::DidFinishLoad(WebKit::WebFrame* frame) {
password_generation::LogPasswordGenerationEvent(
password_generation::SIGN_UP_DETECTED);
passwords_ = passwords;
account_creation_form_origin_ = password_form->origin;
possible_account_creation_form_.swap(password_form);
MaybeShowIcon();
// We assume that there is only one account creation field per URL.
return;
......@@ -176,6 +202,8 @@ bool PasswordGenerationManager::OnMessageReceived(const IPC::Message& message) {
OnPasswordAccepted)
IPC_MESSAGE_HANDLER(AutofillMsg_PasswordGenerationEnabled,
OnPasswordGenerationEnabled)
IPC_MESSAGE_HANDLER(AutofillMsg_AccountCreationFormsDetected,
OnAccountCreationFormsDetected)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
......@@ -203,27 +231,39 @@ void PasswordGenerationManager::OnPasswordGenerationEnabled(bool enabled) {
enabled_ = enabled;
}
void PasswordGenerationManager::OnAccountCreationFormsDetected(
const std::vector<autofill::FormData>& forms) {
account_creation_forms_.insert(
account_creation_forms_.end(), forms.begin(), forms.end());
MaybeShowIcon();
}
void PasswordGenerationManager::MaybeShowIcon() {
// We should show the password generation icon only when we have detected
// account creation form and we have confirmed from browser that this form
// is not blacklisted by the users.
if (!account_creation_form_origin_.is_valid() ||
// account creation form, we have confirmed from browser that this form
// is not blacklisted by the users, and the Autofill server has marked one
// of its field as ACCOUNT_CREATION_PASSWORD.
if (!possible_account_creation_form_.get() ||
passwords_.empty() ||
not_blacklisted_password_form_origins_.empty()) {
not_blacklisted_password_form_origins_.empty() ||
account_creation_forms_.empty()) {
return;
}
for (std::vector<GURL>::iterator it =
not_blacklisted_password_form_origins_.begin();
it != not_blacklisted_password_form_origins_.end(); ++it) {
if (*it == account_creation_form_origin_) {
passwords_[0].passwordGeneratorButtonElement().setAttribute("style",
"display:block");
password_generation::LogPasswordGenerationEvent(
password_generation::ICON_SHOWN);
return;
}
if (!ContainsURL(not_blacklisted_password_form_origins_,
possible_account_creation_form_->origin)) {
return;
}
if (!ContainsForm(account_creation_forms_,
*possible_account_creation_form_)) {
return;
}
passwords_[0].passwordGeneratorButtonElement().setAttribute("style",
"display:block");
password_generation::LogPasswordGenerationEvent(
password_generation::ICON_SHOWN);
}
} // namespace autofill
......@@ -9,6 +9,7 @@
#include <utility>
#include <vector>
#include "base/memory/scoped_ptr.h"
#include "content/public/renderer/render_view_observer.h"
#include "third_party/WebKit/public/web/WebInputElement.h"
#include "third_party/WebKit/public/web/WebPasswordGeneratorClient.h"
......@@ -19,6 +20,10 @@ class WebCString;
class WebDocument;
}
namespace autofill {
struct FormData;
}
namespace content {
struct PasswordForm;
}
......@@ -54,6 +59,8 @@ class PasswordGenerationManager : public content::RenderViewObserver,
void OnFormNotBlacklisted(const content::PasswordForm& form);
void OnPasswordAccepted(const base::string16& password);
void OnPasswordGenerationEnabled(bool enabled);
void OnAccountCreationFormsDetected(
const std::vector<autofill::FormData>& forms);
// Helper function to decide whether we should show password generation icon.
void MaybeShowIcon();
......@@ -65,13 +72,17 @@ class PasswordGenerationManager : public content::RenderViewObserver,
bool enabled_;
// Stores the origin of the account creation form we detected.
GURL account_creation_form_origin_;
scoped_ptr<content::PasswordForm> possible_account_creation_form_;
// Stores the origins of the password forms confirmed not to be blacklisted
// by the browser. A form can be blacklisted if a user chooses "never save
// passwords for this site".
std::vector<GURL> not_blacklisted_password_form_origins_;
// Stores each password form for which the Autofill server classifies one of
// the form's fields as an ACCOUNT_CREATION_PASSWORD.
std::vector<autofill::FormData> account_creation_forms_;
std::vector<WebKit::WebInputElement> passwords_;
DISALLOW_COPY_AND_ASSIGN(PasswordGenerationManager);
......
......@@ -707,6 +707,10 @@ void AutofillManager::OnLoadedServerPredictions(
form_structures_.get(),
*metric_logger_);
// Forward form structures to the password generation manager to detect
// account creation forms.
manager_delegate_->DetectAccountCreationForms(form_structures_.get());
// If the corresponding flag is set, annotate forms with the predicted types.
driver_->SendAutofillTypePredictionsToRenderer(form_structures_.get());
}
......
......@@ -95,6 +95,11 @@ class AutofillManagerDelegate {
// Whether the Autocomplete feature of Autofill should be enabled.
virtual bool IsAutocompleteEnabled() = 0;
// Pass the form structures to the password generation manager to detect
// account creation forms.
virtual void DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) = 0;
};
} // namespace autofill
......
......@@ -862,6 +862,7 @@ FormGroup* AutofillProfile::MutableFormGroupForType(const AutofillType& type) {
case NO_GROUP:
case CREDIT_CARD:
case PASSWORD_FIELD:
return NULL;
}
......
......@@ -108,6 +108,10 @@ FieldTypeGroup AutofillType::group() const {
case COMPANY_NAME:
return COMPANY;
case PASSWORD:
case ACCOUNT_CREATION_PASSWORD:
return PASSWORD_FIELD;
case NO_SERVER_DATA:
case EMPTY_TYPE:
case PHONE_FAX_NUMBER:
......@@ -116,6 +120,8 @@ FieldTypeGroup AutofillType::group() const {
case PHONE_FAX_CITY_AND_NUMBER:
case PHONE_FAX_WHOLE_NUMBER:
case FIELD_WITH_DEFAULT_VALUE:
case MERCHANT_EMAIL_SIGNUP:
case MERCHANT_PROMO_CODE:
return NO_GROUP;
case MAX_VALID_FIELD_TYPE:
......@@ -529,6 +535,14 @@ std::string AutofillType::ToString() const {
return "PHONE_BILLING_CITY_AND_NUMBER";
case PHONE_BILLING_WHOLE_NUMBER:
return "PHONE_BILLING_WHOLE_NUMBER";
case MERCHANT_EMAIL_SIGNUP:
return "MERCHANT_EMAIL_SIGNUP";
case MERCHANT_PROMO_CODE:
return "MERCHANT_PROMO_CODE";
case PASSWORD:
return "PASSWORD";
case ACCOUNT_CREATION_PASSWORD:
return "ACCOUNT_CREATION_PASSWORD";
case MAX_VALID_FIELD_TYPE:
return std::string();
}
......
......@@ -99,10 +99,22 @@ enum ServerFieldType {
NAME_BILLING_FULL = 71,
NAME_BILLING_SUFFIX = 72,
// Field types for options generally found in merchant buyflows. Given that
// these are likely to be filled out differently on a case by case basis,
// they are here primarly for use by Autocheckout.
MERCHANT_EMAIL_SIGNUP = 73,
MERCHANT_PROMO_CODE = 74,
// Field types for the password fields. PASSWORD is the default type for all
// password fields. ACCOUNT_CREATION_PASSWORD is the first password field in
// an account creation form and will trigger password generation.
PASSWORD = 75,
ACCOUNT_CREATION_PASSWORD = 76,
// No new types can be added without a corresponding change to the Autofill
// server.
MAX_VALID_FIELD_TYPE = 73,
MAX_VALID_FIELD_TYPE = 77,
};
// The list of all HTML autocomplete field type hints supported by Chrome.
......@@ -181,6 +193,7 @@ enum FieldTypeGroup {
PHONE_HOME,
PHONE_BILLING,
CREDIT_CARD,
PASSWORD_FIELD,
};
typedef std::set<ServerFieldType> ServerFieldTypeSet;
......
......@@ -660,7 +660,7 @@ std::string FormStructure::FormSignature() const {
}
bool FormStructure::ShouldSkipField(const FormFieldData& field) const {
return (field.is_checkable || field.form_control_type == "password");
return field.is_checkable;
}
bool FormStructure::IsAutofillable(bool require_method_post) const {
......
......@@ -2261,10 +2261,11 @@ TEST(FormStructureTest, CheckFormSignature) {
field.name = ASCIIToUTF16("first");
form.fields.push_back(field);
// Password fields shouldn't affect the signature.
field.label = ASCIIToUTF16("Password");
field.name = ASCIIToUTF16("password");
field.form_control_type = "password";
// Checkable fields shouldn't affect the signature.
field.label = ASCIIToUTF16("Select");
field.name = ASCIIToUTF16("Select");
field.form_control_type = "checkbox";
field.is_checkable = true;
form.fields.push_back(field);
form_structure.reset(new FormStructure(form));
......@@ -2291,6 +2292,7 @@ TEST(FormStructureTest, CheckFormSignature) {
std::string("https://login.facebook.com&login_form&email&first")),
form_structure->FormSignature());
field.is_checkable = false;
field.label = ASCIIToUTF16("Random Field label");
field.name = ASCIIToUTF16("random1234");
field.form_control_type = "text";
......@@ -2356,14 +2358,16 @@ TEST(FormStructureTest, SkipFieldTest) {
field.form_control_type = "text";
form.fields.push_back(field);
field.label = ASCIIToUTF16("password");
field.name = ASCIIToUTF16("password");
field.form_control_type = "password";
field.label = ASCIIToUTF16("select");
field.name = ASCIIToUTF16("select");
field.form_control_type = "checkbox";
field.is_checkable = true;
form.fields.push_back(field);
field.label = base::string16();
field.name = ASCIIToUTF16("email");
field.form_control_type = "text";
field.is_checkable = false;
form.fields.push_back(field);
ScopedVector<FormStructure> forms;
......
......@@ -51,4 +51,7 @@ bool TestAutofillManagerDelegate::IsAutocompleteEnabled() {
return true;
}
void TestAutofillManagerDelegate::DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) {}
} // namespace autofill
......@@ -46,6 +46,9 @@ class TestAutofillManagerDelegate : public AutofillManagerDelegate {
virtual void HideAutofillPopup() OVERRIDE;
virtual bool IsAutocompleteEnabled() OVERRIDE;
virtual void DetectAccountCreationForms(
const std::vector<autofill::FormStructure*>& forms) OVERRIDE;
private:
DISALLOW_COPY_AND_ASSIGN(TestAutofillManagerDelegate);
};
......
......@@ -21,7 +21,6 @@
#include "ipc/ipc_message_utils.h"
#include "third_party/WebKit/public/web/WebFormElement.h"
#include "ui/gfx/rect.h"
#include "url/gurl.h"
#define IPC_MESSAGE_START AutofillMsgStart
......@@ -169,6 +168,11 @@ IPC_MESSAGE_ROUTED2(AutofillMsg_RequestAutocompleteResult,
// after being preloaded.
IPC_MESSAGE_ROUTED0(AutofillMsg_PageShown)
// Sent when Autofill manager gets the query response from the Autofill server
// and there are fields classified as ACCOUNT_CREATION_PASSWORD in the response.
IPC_MESSAGE_ROUTED1(AutofillMsg_AccountCreationFormsDetected,
std::vector<autofill::FormData> /* forms */)
// Autofill messages sent from the renderer to the browser.
// TODO(creis): check in the browser that the renderer actually has permission
......
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