OLD | NEW |
---|---|
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/ui/webui/options/password_manager_handler.h" | 5 #include "chrome/browser/ui/webui/options/password_manager_handler.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/command_line.h" | 8 #include "base/command_line.h" |
9 #include "base/metrics/histogram.h" | |
9 #include "base/prefs/pref_service.h" | 10 #include "base/prefs/pref_service.h" |
10 #include "base/strings/string_number_conversions.h" | 11 #include "base/strings/string_number_conversions.h" |
11 #include "base/strings/string_split.h" | 12 #include "base/strings/string_split.h" |
12 #include "base/strings/utf_string_conversions.h" | 13 #include "base/strings/utf_string_conversions.h" |
13 #include "base/values.h" | 14 #include "base/values.h" |
14 #include "chrome/browser/chrome_notification_types.h" | 15 #include "chrome/browser/chrome_notification_types.h" |
16 #include "chrome/browser/password_manager/sync_metrics.h" | |
15 #include "chrome/browser/profiles/profile.h" | 17 #include "chrome/browser/profiles/profile.h" |
16 #include "chrome/browser/sync/profile_sync_service.h" | 18 #include "chrome/browser/sync/profile_sync_service.h" |
17 #include "chrome/browser/sync/profile_sync_service_factory.h" | 19 #include "chrome/browser/sync/profile_sync_service_factory.h" |
18 #if defined(OS_WIN) && defined(USE_ASH) | 20 #if defined(OS_WIN) && defined(USE_ASH) |
19 #include "chrome/browser/ui/ash/ash_util.h" | 21 #include "chrome/browser/ui/ash/ash_util.h" |
20 #endif | 22 #endif |
23 #include "chrome/browser/ui/chrome_select_file_policy.h" | |
21 #include "chrome/common/pref_names.h" | 24 #include "chrome/common/pref_names.h" |
22 #include "chrome/common/url_constants.h" | 25 #include "chrome/common/url_constants.h" |
23 #include "chrome/grit/generated_resources.h" | 26 #include "chrome/grit/generated_resources.h" |
24 #include "components/autofill/core/common/password_form.h" | 27 #include "components/autofill/core/common/password_form.h" |
25 #include "components/password_manager/core/browser/affiliation_utils.h" | 28 #include "components/password_manager/core/browser/affiliation_utils.h" |
29 #include "components/password_manager/core/browser/export/password_exporter.h" | |
26 #include "components/password_manager/core/browser/password_bubble_experiment.h" | 30 #include "components/password_manager/core/browser/password_bubble_experiment.h" |
27 #include "components/password_manager/core/common/experiments.h" | 31 #include "components/password_manager/core/common/experiments.h" |
32 #include "content/public/browser/browser_thread.h" | |
28 #include "content/public/browser/notification_details.h" | 33 #include "content/public/browser/notification_details.h" |
29 #include "content/public/browser/notification_source.h" | 34 #include "content/public/browser/notification_source.h" |
30 #include "content/public/browser/user_metrics.h" | 35 #include "content/public/browser/user_metrics.h" |
31 #include "content/public/browser/web_contents.h" | 36 #include "content/public/browser/web_contents.h" |
32 #include "content/public/browser/web_ui.h" | 37 #include "content/public/browser/web_ui.h" |
33 #include "content/public/common/content_switches.h" | 38 #include "content/public/common/content_switches.h" |
34 #include "net/base/net_util.h" | 39 #include "net/base/net_util.h" |
35 #include "ui/base/l10n/l10n_util.h" | 40 #include "ui/base/l10n/l10n_util.h" |
36 | 41 |
37 namespace options { | 42 namespace options { |
38 | 43 |
39 PasswordManagerHandler::PasswordManagerHandler() | 44 namespace { |
40 : password_manager_presenter_(this) {} | 45 |
46 // Enumeration of different callers of SelectFile. | |
47 enum { | |
48 IMPORT_FILE_SELECTED = 1, | |
Garrett Casto
2015/06/23 23:42:36
I'm assuming you are starting this at "1" so that
xunlu
2015/06/25 17:12:15
Done.
| |
49 EXPORT_FILE_SELECTED, | |
50 }; | |
51 | |
52 } // namespace | |
53 | |
54 PasswordManagerHandler::PasswordManagerHandler() { | |
55 password_manager_presenter_.reset(new PasswordManagerPresenter(this)); | |
56 } | |
57 | |
58 PasswordManagerHandler::PasswordManagerHandler( | |
59 PasswordManagerPresenter* presenter) { | |
60 password_manager_presenter_.reset(presenter); | |
61 } | |
41 | 62 |
42 PasswordManagerHandler::~PasswordManagerHandler() {} | 63 PasswordManagerHandler::~PasswordManagerHandler() {} |
43 | 64 |
44 Profile* PasswordManagerHandler::GetProfile() { | 65 Profile* PasswordManagerHandler::GetProfile() { |
45 return Profile::FromWebUI(web_ui()); | 66 return Profile::FromWebUI(web_ui()); |
46 } | 67 } |
47 | 68 |
48 #if !defined(OS_ANDROID) | 69 #if !defined(OS_ANDROID) |
49 gfx::NativeWindow PasswordManagerHandler::GetNativeWindow() const { | 70 gfx::NativeWindow PasswordManagerHandler::GetNativeWindow() const { |
50 return web_ui()->GetWebContents()->GetTopLevelNativeWindow(); | 71 return web_ui()->GetWebContents()->GetTopLevelNativeWindow(); |
51 } | 72 } |
52 #endif | 73 #endif |
53 | 74 |
54 void PasswordManagerHandler::GetLocalizedValues( | 75 void PasswordManagerHandler::GetLocalizedValues( |
55 base::DictionaryValue* localized_strings) { | 76 base::DictionaryValue* localized_strings) { |
56 DCHECK(localized_strings); | 77 DCHECK(localized_strings); |
57 | 78 |
58 static const OptionsStringResource resources[] = { | 79 static const OptionsStringResource resources[] = { |
59 { "autoSigninTitle", | 80 {"autoSigninTitle", IDS_PASSWORDS_AUTO_SIGNIN_TITLE}, |
60 IDS_PASSWORDS_AUTO_SIGNIN_TITLE }, | 81 {"autoSigninDescription", IDS_PASSWORDS_AUTO_SIGNIN_DESCRIPTION}, |
61 { "autoSigninDescription", | 82 {"savedPasswordsTitle", IDS_PASSWORDS_SHOW_PASSWORDS_TAB_TITLE}, |
62 IDS_PASSWORDS_AUTO_SIGNIN_DESCRIPTION }, | 83 {"passwordExceptionsTitle", IDS_PASSWORDS_EXCEPTIONS_TAB_TITLE}, |
63 { "savedPasswordsTitle", | 84 {"passwordSearchPlaceholder", IDS_PASSWORDS_PAGE_SEARCH_PASSWORDS}, |
64 IDS_PASSWORDS_SHOW_PASSWORDS_TAB_TITLE }, | 85 {"passwordShowButton", IDS_PASSWORDS_PAGE_VIEW_SHOW_BUTTON}, |
65 { "passwordExceptionsTitle", | 86 {"passwordHideButton", IDS_PASSWORDS_PAGE_VIEW_HIDE_BUTTON}, |
66 IDS_PASSWORDS_EXCEPTIONS_TAB_TITLE }, | 87 {"passwordsNoPasswordsDescription", |
67 { "passwordSearchPlaceholder", | 88 IDS_PASSWORDS_PAGE_VIEW_NO_PASSWORDS_DESCRIPTION}, |
68 IDS_PASSWORDS_PAGE_SEARCH_PASSWORDS }, | 89 {"passwordsNoExceptionsDescription", |
69 { "passwordShowButton", | 90 IDS_PASSWORDS_PAGE_VIEW_NO_EXCEPTIONS_DESCRIPTION}, |
70 IDS_PASSWORDS_PAGE_VIEW_SHOW_BUTTON }, | 91 {"passwordManagerImportPasswordButtonText", |
71 { "passwordHideButton", | 92 IDS_PASSWORD_MANAGER_IMPORT_BUTTON}, |
72 IDS_PASSWORDS_PAGE_VIEW_HIDE_BUTTON }, | 93 {"passwordManagerExportPasswordButtonText", |
73 { "passwordsNoPasswordsDescription", | 94 IDS_PASSWORD_MANAGER_EXPORT_BUTTON}, |
74 IDS_PASSWORDS_PAGE_VIEW_NO_PASSWORDS_DESCRIPTION }, | 95 {"importPasswordCompletedMessage", |
75 { "passwordsNoExceptionsDescription", | 96 IDS_PASSWORD_MANAGER_IMPORT_COMPLETED_MESSAGE}, |
76 IDS_PASSWORDS_PAGE_VIEW_NO_EXCEPTIONS_DESCRIPTION }, | |
77 }; | 97 }; |
78 | 98 |
79 RegisterStrings(localized_strings, resources, arraysize(resources)); | 99 RegisterStrings(localized_strings, resources, arraysize(resources)); |
80 | 100 |
81 const ProfileSyncService* sync_service = | 101 const ProfileSyncService* sync_service = |
82 ProfileSyncServiceFactory::GetForProfile(GetProfile()); | 102 ProfileSyncServiceFactory::GetForProfile(GetProfile()); |
83 int title_id = | 103 int title_id = |
84 password_bubble_experiment::IsSmartLockBrandingEnabled(sync_service) ? | 104 password_bubble_experiment::IsSmartLockBrandingEnabled(sync_service) ? |
85 IDS_PASSWORDS_EXCEPTIONS_SMART_LOCK_WINDOW_TITLE : | 105 IDS_PASSWORDS_EXCEPTIONS_SMART_LOCK_WINDOW_TITLE : |
86 IDS_PASSWORDS_EXCEPTIONS_WINDOW_TITLE; | 106 IDS_PASSWORDS_EXCEPTIONS_WINDOW_TITLE; |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
134 base::Bind(&PasswordManagerHandler::HandleRemoveSavedPassword, | 154 base::Bind(&PasswordManagerHandler::HandleRemoveSavedPassword, |
135 base::Unretained(this))); | 155 base::Unretained(this))); |
136 web_ui()->RegisterMessageCallback( | 156 web_ui()->RegisterMessageCallback( |
137 "removePasswordException", | 157 "removePasswordException", |
138 base::Bind(&PasswordManagerHandler::HandleRemovePasswordException, | 158 base::Bind(&PasswordManagerHandler::HandleRemovePasswordException, |
139 base::Unretained(this))); | 159 base::Unretained(this))); |
140 web_ui()->RegisterMessageCallback( | 160 web_ui()->RegisterMessageCallback( |
141 "requestShowPassword", | 161 "requestShowPassword", |
142 base::Bind(&PasswordManagerHandler::HandleRequestShowPassword, | 162 base::Bind(&PasswordManagerHandler::HandleRequestShowPassword, |
143 base::Unretained(this))); | 163 base::Unretained(this))); |
164 web_ui()->RegisterMessageCallback( | |
165 "importPassword", | |
166 base::Bind(&PasswordManagerHandler::HandlePasswordImport, | |
167 base::Unretained(this))); | |
168 web_ui()->RegisterMessageCallback( | |
169 "exportPassword", | |
170 base::Bind(&PasswordManagerHandler::HandlePasswordExport, | |
171 base::Unretained(this))); | |
144 } | 172 } |
145 | 173 |
146 void PasswordManagerHandler::InitializeHandler() { | 174 void PasswordManagerHandler::InitializeHandler() { |
147 password_manager_presenter_.Initialize(); | 175 password_manager_presenter_->Initialize(); |
176 std::vector<std::string> tmp_supported_extensions = | |
Garrett Casto
2015/06/23 23:42:36
The fact that you need to specify the restrictions
xunlu
2015/06/25 17:12:15
Done.
| |
177 password_manager::PasswordImporter::GetSupportedFileExtensions(); | |
178 for (size_t i = 0; i < tmp_supported_extensions.size(); ++i) { | |
179 supported_extentions_.push_back( | |
180 new std::string(tmp_supported_extensions[i])); | |
181 } | |
148 } | 182 } |
149 | 183 |
150 void PasswordManagerHandler::InitializePage() { | 184 void PasswordManagerHandler::InitializePage() { |
151 base::FundamentalValue visible( | 185 base::FundamentalValue visible( |
152 password_manager::ManageAccountLinkExperimentEnabled()); | 186 password_manager::ManageAccountLinkExperimentEnabled()); |
153 web_ui()->CallJavascriptFunction( | 187 web_ui()->CallJavascriptFunction( |
154 "PasswordManager.setManageAccountLinkVisibility", visible); | 188 "PasswordManager.setManageAccountLinkVisibility", visible); |
155 } | 189 } |
156 | 190 |
157 void PasswordManagerHandler::HandleRemoveSavedPassword( | 191 void PasswordManagerHandler::HandleRemoveSavedPassword( |
158 const base::ListValue* args) { | 192 const base::ListValue* args) { |
159 std::string string_value = base::UTF16ToUTF8(ExtractStringValue(args)); | 193 std::string string_value = base::UTF16ToUTF8(ExtractStringValue(args)); |
160 int index; | 194 int index; |
161 if (base::StringToInt(string_value, &index) && index >= 0) { | 195 if (base::StringToInt(string_value, &index) && index >= 0) { |
162 password_manager_presenter_.RemoveSavedPassword(static_cast<size_t>(index)); | 196 password_manager_presenter_->RemoveSavedPassword( |
197 static_cast<size_t>(index)); | |
163 } | 198 } |
164 } | 199 } |
165 | 200 |
166 void PasswordManagerHandler::HandleRemovePasswordException( | 201 void PasswordManagerHandler::HandleRemovePasswordException( |
167 const base::ListValue* args) { | 202 const base::ListValue* args) { |
168 std::string string_value = base::UTF16ToUTF8(ExtractStringValue(args)); | 203 std::string string_value = base::UTF16ToUTF8(ExtractStringValue(args)); |
169 int index; | 204 int index; |
170 if (base::StringToInt(string_value, &index) && index >= 0) { | 205 if (base::StringToInt(string_value, &index) && index >= 0) { |
171 password_manager_presenter_.RemovePasswordException( | 206 password_manager_presenter_->RemovePasswordException( |
172 static_cast<size_t>(index)); | 207 static_cast<size_t>(index)); |
173 } | 208 } |
174 } | 209 } |
175 | 210 |
176 void PasswordManagerHandler::HandleRequestShowPassword( | 211 void PasswordManagerHandler::HandleRequestShowPassword( |
177 const base::ListValue* args) { | 212 const base::ListValue* args) { |
178 int index; | 213 int index; |
179 if (!ExtractIntegerValue(args, &index)) | 214 if (!ExtractIntegerValue(args, &index)) |
180 NOTREACHED(); | 215 NOTREACHED(); |
181 | 216 |
182 password_manager_presenter_.RequestShowPassword(static_cast<size_t>(index)); | 217 password_manager_presenter_->RequestShowPassword(static_cast<size_t>(index)); |
183 } | 218 } |
184 | 219 |
185 void PasswordManagerHandler::ShowPassword( | 220 void PasswordManagerHandler::ShowPassword( |
186 size_t index, | 221 size_t index, |
187 const std::string& origin_url, | 222 const std::string& origin_url, |
188 const std::string& username, | 223 const std::string& username, |
189 const base::string16& password_value) { | 224 const base::string16& password_value) { |
190 // Call back the front end to reveal the password. | 225 // Call back the front end to reveal the password. |
191 web_ui()->CallJavascriptFunction( | 226 web_ui()->CallJavascriptFunction( |
192 "PasswordManager.showPassword", | 227 "PasswordManager.showPassword", |
193 base::FundamentalValue(static_cast<int>(index)), | 228 base::FundamentalValue(static_cast<int>(index)), |
194 base::StringValue(password_value)); | 229 base::StringValue(password_value)); |
195 } | 230 } |
196 | 231 |
197 void PasswordManagerHandler::HandleUpdatePasswordLists( | 232 void PasswordManagerHandler::HandleUpdatePasswordLists( |
198 const base::ListValue* args) { | 233 const base::ListValue* args) { |
199 password_manager_presenter_.UpdatePasswordLists(); | 234 password_manager_presenter_->UpdatePasswordLists(); |
200 } | 235 } |
201 | 236 |
202 void PasswordManagerHandler::SetPasswordList( | 237 void PasswordManagerHandler::SetPasswordList( |
203 const ScopedVector<autofill::PasswordForm>& password_list, | 238 const ScopedVector<autofill::PasswordForm>& password_list, |
204 bool show_passwords) { | 239 bool show_passwords) { |
205 base::ListValue entries; | 240 base::ListValue entries; |
206 languages_ = GetProfile()->GetPrefs()->GetString(prefs::kAcceptLanguages); | 241 languages_ = GetProfile()->GetPrefs()->GetString(prefs::kAcceptLanguages); |
207 base::string16 placeholder(base::ASCIIToUTF16(" ")); | 242 base::string16 placeholder(base::ASCIIToUTF16(" ")); |
208 for (size_t i = 0; i < password_list.size(); ++i) { | 243 for (size_t i = 0; i < password_list.size(); ++i) { |
209 base::ListValue* entry = new base::ListValue(); | 244 base::ListValue* entry = new base::ListValue(); |
(...skipping 25 matching lines...) Expand all Loading... | |
235 base::ListValue entries; | 270 base::ListValue entries; |
236 for (size_t i = 0; i < password_exception_list.size(); ++i) { | 271 for (size_t i = 0; i < password_exception_list.size(); ++i) { |
237 entries.AppendString(password_manager::GetHumanReadableOrigin( | 272 entries.AppendString(password_manager::GetHumanReadableOrigin( |
238 *password_exception_list[i], languages_)); | 273 *password_exception_list[i], languages_)); |
239 } | 274 } |
240 | 275 |
241 web_ui()->CallJavascriptFunction("PasswordManager.setPasswordExceptionsList", | 276 web_ui()->CallJavascriptFunction("PasswordManager.setPasswordExceptionsList", |
242 entries); | 277 entries); |
243 } | 278 } |
244 | 279 |
280 void PasswordManagerHandler::FileSelected(const base::FilePath& path, | |
281 int index, | |
282 void* params) { | |
283 switch (reinterpret_cast<intptr_t>(params)) { | |
284 case IMPORT_FILE_SELECTED: | |
285 ImportPasswordFileSelected(path); | |
286 break; | |
287 case EXPORT_FILE_SELECTED: | |
288 ExportPasswordFileSelected(path); | |
289 break; | |
290 default: | |
291 NOTREACHED(); | |
292 } | |
293 } | |
294 | |
295 void PasswordManagerHandler::HandlePasswordImport(const base::ListValue* args) { | |
296 #if !defined(OS_ANDROID) // This is never called on Android. | |
297 ui::SelectFileDialog::FileTypeInfo file_type_info; | |
298 file_type_info.extensions.resize(supported_extentions_.size()); | |
299 for (size_t i = 0; i < supported_extentions_.size(); ++i) { | |
300 file_type_info.extensions[i].push_back( | |
301 (*supported_extentions_[i]).substr(1)); | |
302 } | |
303 file_type_info.include_all_files = true; | |
304 selected_file_dialog_ = ui::SelectFileDialog::Create( | |
305 this, new ChromeSelectFilePolicy(web_ui()->GetWebContents())); | |
306 selected_file_dialog_->SelectFile( | |
307 ui::SelectFileDialog::SELECT_OPEN_FILE, base::string16(), | |
Garrett Casto
2015/06/23 23:42:36
We may want to see if we should specify a title fo
xunlu
2015/06/25 17:12:16
Done.
| |
308 base::FilePath(), &file_type_info, 1, "csv", | |
Garrett Casto
2015/06/23 23:42:36
I would DCHECK that supported extensions is not em
xunlu
2015/06/25 17:12:15
Done.
| |
309 web_ui()->GetWebContents()->GetTopLevelNativeWindow(), | |
310 reinterpret_cast<void*>(IMPORT_FILE_SELECTED)); | |
311 #endif | |
312 } | |
313 | |
314 void PasswordManagerHandler::ImportPasswordFileSelected( | |
315 const base::FilePath& path) { | |
316 password_manager::PasswordImporter::Import( | |
317 path, content::BrowserThread::GetMessageLoopProxyForThread( | |
318 content::BrowserThread::FILE).get(), | |
319 base::Bind(&PasswordManagerHandler::ImportPasswordFileRead, | |
320 base::Unretained(this))); | |
321 } | |
322 | |
323 void PasswordManagerHandler::ImportPasswordFileRead( | |
324 password_manager::PasswordImporter::Result result, | |
325 const std::vector<autofill::PasswordForm>& forms) { | |
326 if (result != password_manager::PasswordImporter::SUCCESS) { | |
327 // TODO(xunlu) forms.size() will always be 0 in our current implementation, | |
328 // we may want to change this in the future | |
329 UMA_HISTOGRAM_COUNTS("PasswordManager.ImportedPasswordsPerUserInCSV.Fail", | |
330 forms.size()); | |
Garrett Casto
2015/06/23 23:42:35
Especially since forms will always be empty here,
Garrett Casto
2015/06/23 23:42:35
Do we want to return here? I guess we haven't comp
xunlu
2015/06/25 17:12:15
Done.
xunlu
2015/06/25 17:12:15
Done.
| |
331 } | |
332 UMA_HISTOGRAM_COUNTS("PasswordManager.ImportedPasswordsPerUserInCSV.Success", | |
333 forms.size()); | |
334 | |
335 if (password_manager_presenter_->AddPasswordsToStore(forms)) { | |
336 // Display Feedback UI | |
337 base::FundamentalValue visible(true); | |
338 web_ui()->CallJavascriptFunction( | |
339 "PasswordManager.setImportCompleteUIVisibility", visible); | |
340 } | |
Garrett Casto
2015/06/23 23:42:36
Also logging failures due to this function returni
xunlu
2015/06/25 17:12:15
Done.
| |
341 } | |
342 | |
343 void PasswordManagerHandler::HandlePasswordExport(const base::ListValue* args) { | |
344 #if !defined(OS_ANDROID) // This is never called on Android. | |
345 if (password_manager_presenter_->RequestToExportPassword()) { | |
346 ui::SelectFileDialog::FileTypeInfo file_type_info; | |
347 file_type_info.extensions.resize(supported_extentions_.size()); | |
Garrett Casto
2015/06/23 23:42:36
Not that this currently makes the assumption that
xunlu
2015/06/25 17:12:15
Done.
| |
348 for (size_t i = 0; i < supported_extentions_.size(); ++i) { | |
349 file_type_info.extensions[i].push_back( | |
350 (*supported_extentions_[i]).substr(1)); | |
351 } | |
352 file_type_info.include_all_files = true; | |
353 selected_file_dialog_ = ui::SelectFileDialog::Create( | |
354 this, new ChromeSelectFilePolicy(web_ui()->GetWebContents())); | |
355 selected_file_dialog_->SelectFile( | |
356 ui::SelectFileDialog::SELECT_SAVEAS_FILE, base::string16(), | |
357 base::FilePath(), &file_type_info, 1, "csv", GetNativeWindow(), | |
358 reinterpret_cast<void*>(EXPORT_FILE_SELECTED)); | |
359 } | |
360 #endif | |
361 } | |
362 | |
363 void PasswordManagerHandler::ExportPasswordFileSelected( | |
364 const base::FilePath& path) { | |
365 ScopedVector<autofill::PasswordForm> password_list = | |
366 password_manager_presenter_->GetAllPasswords().Pass(); | |
367 UMA_HISTOGRAM_COUNTS("PasswordManager.ExportedPasswordsPerUserInCSV", | |
368 password_list.size()); | |
369 password_manager::PasswordExporter::Export( | |
370 path.ReplaceExtension("csv"), password_list.Pass(), | |
371 content::BrowserThread::GetMessageLoopProxyForThread( | |
372 content::BrowserThread::FILE).get(), | |
373 base::Bind(&PasswordManagerHandler::ExportPasswordFileWritten, | |
374 base::Unretained(this))); | |
375 } | |
376 | |
377 void PasswordManagerHandler::ExportPasswordFileWritten() { | |
378 // TODO(xunlu): We do not plan to give any UI feedback for export | |
Garrett Casto
2015/06/23 23:42:36
I probably wouldn't label this as a TODO, since it
xunlu
2015/06/25 17:12:15
Done.
| |
379 // at this moment as it will almost always succeed. If the plan changes, this | |
380 // is the place to put the feedback logic. | |
381 } | |
382 | |
245 } // namespace options | 383 } // namespace options |
OLD | NEW |