Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(107)

Side by Side Diff: chrome/browser/sync/sync_setup_wizard.cc

Issue 3655004: Add UI for setting the encryption passphrase.... (Closed) Base URL: svn://chrome-svn/chrome/trunk/src/
Patch Set: fixes Created 10 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2010 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/sync/sync_setup_wizard.h" 5 #include "chrome/browser/sync/sync_setup_wizard.h"
6 6
7 #include "app/resource_bundle.h" 7 #include "app/resource_bundle.h"
8 #include "base/message_loop.h" 8 #include "base/message_loop.h"
9 #include "base/singleton.h" 9 #include "base/singleton.h"
10 #include "chrome/browser/chrome_thread.h" 10 #include "chrome/browser/chrome_thread.h"
11 #include "chrome/browser/dom_ui/chrome_url_data_manager.h" 11 #include "chrome/browser/dom_ui/chrome_url_data_manager.h"
12 #include "chrome/browser/google/google_util.h" 12 #include "chrome/browser/google/google_util.h"
13 #include "chrome/browser/prefs/pref_service.h" 13 #include "chrome/browser/prefs/pref_service.h"
14 #include "chrome/browser/profile.h" 14 #include "chrome/browser/profile.h"
15 #include "chrome/browser/sync/profile_sync_service.h" 15 #include "chrome/browser/sync/profile_sync_service.h"
16 #include "chrome/browser/sync/sync_setup_flow.h" 16 #include "chrome/browser/sync/sync_setup_flow.h"
17 #include "chrome/common/jstemplate_builder.h" 17 #include "chrome/common/jstemplate_builder.h"
18 #include "chrome/common/pref_names.h" 18 #include "chrome/common/pref_names.h"
19 #include "chrome/common/url_constants.h" 19 #include "chrome/common/url_constants.h"
20 #include "googleurl/src/gurl.h" 20 #include "googleurl/src/gurl.h"
21 #include "grit/app_resources.h" 21 #include "grit/app_resources.h"
22 #include "grit/browser_resources.h" 22 #include "grit/browser_resources.h"
23 #include "grit/chromium_strings.h" 23 #include "grit/chromium_strings.h"
24 #include "grit/locale_settings.h" 24 #include "grit/locale_settings.h"
25 25
26 namespace {
27
28 // Utility method to keep dictionary population code streamlined.
29 void AddString(DictionaryValue* dictionary,
30 const std::string& key,
31 int resource_id) {
32 dictionary->SetString(key, l10n_util::GetStringUTF16(resource_id));
33 }
34
35 }
36
26 class SyncResourcesSource : public ChromeURLDataManager::DataSource { 37 class SyncResourcesSource : public ChromeURLDataManager::DataSource {
27 public: 38 public:
28 SyncResourcesSource() 39 SyncResourcesSource()
29 : DataSource(chrome::kChromeUISyncResourcesHost, MessageLoop::current()) { 40 : DataSource(chrome::kChromeUISyncResourcesHost, MessageLoop::current()) {
30 } 41 }
31 42
32 virtual void StartDataRequest(const std::string& path, 43 virtual void StartDataRequest(const std::string& path,
33 bool is_off_the_record, 44 bool is_off_the_record,
34 int request_id); 45 int request_id);
35 46
(...skipping 18 matching lines...) Expand all
54 65
55 const char* SyncResourcesSource::kInvalidPasswordHelpUrl = 66 const char* SyncResourcesSource::kInvalidPasswordHelpUrl =
56 "http://www.google.com/support/accounts/bin/answer.py?ctx=ch&answer=27444"; 67 "http://www.google.com/support/accounts/bin/answer.py?ctx=ch&answer=27444";
57 const char* SyncResourcesSource::kCanNotAccessAccountUrl = 68 const char* SyncResourcesSource::kCanNotAccessAccountUrl =
58 "http://www.google.com/support/accounts/bin/answer.py?answer=48598"; 69 "http://www.google.com/support/accounts/bin/answer.py?answer=48598";
59 const char* SyncResourcesSource::kCreateNewAccountUrl = 70 const char* SyncResourcesSource::kCreateNewAccountUrl =
60 "https://www.google.com/accounts/NewAccount?service=chromiumsync"; 71 "https://www.google.com/accounts/NewAccount?service=chromiumsync";
61 72
62 void SyncResourcesSource::StartDataRequest(const std::string& path_raw, 73 void SyncResourcesSource::StartDataRequest(const std::string& path_raw,
63 bool is_off_the_record, int request_id) { 74 bool is_off_the_record, int request_id) {
75 using l10n_util::GetStringUTF16;
76 using l10n_util::GetStringFUTF16;
77
78 const char kSyncSetupFlowPath[] = "setup";
64 const char kSyncGaiaLoginPath[] = "gaialogin"; 79 const char kSyncGaiaLoginPath[] = "gaialogin";
65 const char kSyncChooseDataTypesPath[] = "choosedatatypes"; 80 const char kSyncConfigurePath[] = "configure";
66 const char kSyncSetupFlowPath[] = "setup"; 81 const char kSyncPassphrasePath[] = "passphrase";
82 const char kSyncSettingUpPath[] = "settingup";
67 const char kSyncSetupDonePath[] = "setupdone"; 83 const char kSyncSetupDonePath[] = "setupdone";
68 84
69 std::string response; 85 std::string response;
86 DictionaryValue strings;
87 DictionaryValue* dict = &strings;
88 int html_resource_id = 0;
70 if (path_raw == kSyncGaiaLoginPath) { 89 if (path_raw == kSyncGaiaLoginPath) {
71 DictionaryValue localized_strings; 90 html_resource_id = IDR_GAIA_LOGIN_HTML;
72 91
73 // Start by setting the per-locale URLs we show on the setup wizard. 92 // Start by setting the per-locale URLs we show on the setup wizard.
74 localized_strings.SetString("invalidpasswordhelpurl", 93 dict->SetString("invalidpasswordhelpurl",
75 GetLocalizedUrl(kInvalidPasswordHelpUrl)); 94 GetLocalizedUrl(kInvalidPasswordHelpUrl));
76 localized_strings.SetString("cannotaccessaccounturl", 95 dict->SetString("cannotaccessaccounturl",
77 GetLocalizedUrl(kCanNotAccessAccountUrl)); 96 GetLocalizedUrl(kCanNotAccessAccountUrl));
78 localized_strings.SetString("createnewaccounturl", 97 dict->SetString("createnewaccounturl",
79 GetLocalizedUrl(kCreateNewAccountUrl)); 98 GetLocalizedUrl(kCreateNewAccountUrl));
99 AddString(dict, "settingupsync", IDS_SYNC_LOGIN_SETTING_UP_SYNC);
100 dict->SetString("introduction",
101 GetStringFUTF16(IDS_SYNC_LOGIN_INTRODUCTION,
102 GetStringUTF16(IDS_PRODUCT_NAME)));
103 AddString(dict, "signinprefix", IDS_SYNC_LOGIN_SIGNIN_PREFIX);
104 AddString(dict, "signinsuffix", IDS_SYNC_LOGIN_SIGNIN_SUFFIX);
105 AddString(dict, "cannotbeblank",IDS_SYNC_CANNOT_BE_BLANK);
106 AddString(dict, "emaillabel", IDS_SYNC_LOGIN_EMAIL);
107 AddString(dict, "passwordlabel", IDS_SYNC_LOGIN_PASSWORD);
108 AddString(dict, "invalidcredentials", IDS_SYNC_INVALID_USER_CREDENTIALS);
109 AddString(dict, "signin", IDS_SYNC_SIGNIN);
110 AddString(dict, "couldnotconnect", IDS_SYNC_LOGIN_COULD_NOT_CONNECT);
111 AddString(dict, "cannotaccessaccount", IDS_SYNC_CANNOT_ACCESS_ACCOUNT);
112 AddString(dict, "createaccount", IDS_SYNC_CREATE_ACCOUNT);
113 AddString(dict, "cancel", IDS_CANCEL);
114 AddString(dict, "settingup", IDS_SYNC_LOGIN_SETTING_UP);
115 AddString(dict, "success", IDS_SYNC_SUCCESS);
116 AddString(dict, "errorsigningin", IDS_SYNC_ERROR_SIGNING_IN);
117 AddString(dict, "captchainstructions", IDS_SYNC_GAIA_CAPTCHA_INSTRUCTIONS);
80 118
81 localized_strings.SetString("settingupsync", 119 AddString(dict, "invalidaccesscode", IDS_SYNC_INVALID_ACCESS_CODE_LABEL);
82 l10n_util::GetStringUTF16(IDS_SYNC_LOGIN_SETTING_UP_SYNC)); 120 AddString(dict, "enteraccesscode", IDS_SYNC_ENTER_ACCESS_CODE_LABEL);
83 localized_strings.SetString("introduction", 121 AddString(dict, "getaccesscodehelp", IDS_SYNC_ACCESS_CODE_HELP_LABEL);
84 l10n_util::GetStringFUTF16(IDS_SYNC_LOGIN_INTRODUCTION, 122 AddString(dict, "getaccesscodeurl", IDS_SYNC_GET_ACCESS_CODE_URL);
85 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME))); 123 } else if (path_raw == kSyncConfigurePath) {
86 localized_strings.SetString("signinprefix", 124 html_resource_id = IDR_SYNC_CONFIGURE_HTML;
87 l10n_util::GetStringUTF16(IDS_SYNC_LOGIN_SIGNIN_PREFIX));
88 localized_strings.SetString("signinsuffix",
89 l10n_util::GetStringUTF16(IDS_SYNC_LOGIN_SIGNIN_SUFFIX));
90 localized_strings.SetString("cannotbeblank",
91 l10n_util::GetStringUTF16(IDS_SYNC_CANNOT_BE_BLANK));
92 localized_strings.SetString("emaillabel",
93 l10n_util::GetStringUTF16(IDS_SYNC_LOGIN_EMAIL));
94 localized_strings.SetString("passwordlabel",
95 l10n_util::GetStringUTF16(IDS_SYNC_LOGIN_PASSWORD));
96 localized_strings.SetString("invalidcredentials",
97 l10n_util::GetStringUTF16(IDS_SYNC_INVALID_USER_CREDENTIALS));
98 localized_strings.SetString("signin",
99 l10n_util::GetStringUTF16(IDS_SYNC_SIGNIN));
100 localized_strings.SetString("couldnotconnect",
101 l10n_util::GetStringUTF16(IDS_SYNC_LOGIN_COULD_NOT_CONNECT));
102 localized_strings.SetString("cannotaccessaccount",
103 l10n_util::GetStringUTF16(IDS_SYNC_CANNOT_ACCESS_ACCOUNT));
104 localized_strings.SetString("createaccount",
105 l10n_util::GetStringUTF16(IDS_SYNC_CREATE_ACCOUNT));
106 localized_strings.SetString("cancel",
107 l10n_util::GetStringUTF16(IDS_CANCEL));
108 localized_strings.SetString("settingup",
109 l10n_util::GetStringUTF16(IDS_SYNC_LOGIN_SETTING_UP));
110 localized_strings.SetString("success",
111 l10n_util::GetStringUTF16(IDS_SYNC_SUCCESS));
112 localized_strings.SetString("errorsigningin",
113 l10n_util::GetStringUTF16(IDS_SYNC_ERROR_SIGNING_IN));
114 localized_strings.SetString("captchainstructions",
115 l10n_util::GetStringUTF16(IDS_SYNC_GAIA_CAPTCHA_INSTRUCTIONS));
116 125
117 localized_strings.SetString("invalidaccesscode", 126 AddString(dict, "dataTypes", IDS_SYNC_DATA_TYPES_TAB_NAME);
118 l10n_util::GetStringUTF16(IDS_SYNC_INVALID_ACCESS_CODE_LABEL)); 127 AddString(dict, "encryption", IDS_SYNC_ENCRYPTION_TAB_NAME);
119 localized_strings.SetString("enteraccesscode",
120 l10n_util::GetStringUTF16(IDS_SYNC_ENTER_ACCESS_CODE_LABEL));
121 localized_strings.SetString("getaccesscodehelp",
122 l10n_util::GetStringUTF16(IDS_SYNC_ACCESS_CODE_HELP_LABEL));
123 localized_strings.SetString("getaccesscodeurl",
124 l10n_util::GetStringUTF16(IDS_SYNC_GET_ACCESS_CODE_URL));
125 128
126 static const base::StringPiece html(ResourceBundle::GetSharedInstance() 129 // Stuff for the choose data types localized.
127 .GetRawDataResource(IDR_GAIA_LOGIN_HTML)); 130 AddString(dict, "choosedatatypesheader", IDS_SYNC_CHOOSE_DATATYPES_HEADER);
128 SetFontAndTextDirection(&localized_strings); 131 dict->SetString("choosedatatypesinstructions",
129 response = jstemplate_builder::GetI18nTemplateHtml( 132 GetStringFUTF16(IDS_SYNC_CHOOSE_DATATYPES_INSTRUCTIONS,
130 html, &localized_strings); 133 GetStringUTF16(IDS_PRODUCT_NAME)));
131 } else if (path_raw == kSyncChooseDataTypesPath) { 134 AddString(dict, "keepeverythingsynced", IDS_SYNC_EVERYTHING);
132 DictionaryValue localized_strings; 135 AddString(dict, "choosedatatypes", IDS_SYNC_CHOOSE_DATATYPES);
133 localized_strings.SetString("choosedatatypesheader", 136 AddString(dict, "bookmarks", IDS_SYNC_DATATYPE_BOOKMARKS);
134 l10n_util::GetStringUTF16(IDS_SYNC_CHOOSE_DATATYPES_HEADER)); 137 AddString(dict, "preferences", IDS_SYNC_DATATYPE_PREFERENCES);
135 localized_strings.SetString("choosedatatypesinstructions", 138 AddString(dict, "autofill", IDS_SYNC_DATATYPE_AUTOFILL);
136 l10n_util::GetStringFUTF16(IDS_SYNC_CHOOSE_DATATYPES_INSTRUCTIONS, 139 AddString(dict, "themes", IDS_SYNC_DATATYPE_THEMES);
137 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME))); 140 AddString(dict, "passwords", IDS_SYNC_DATATYPE_PASSWORDS);
138 localized_strings.SetString("keepeverythingsynced", 141 AddString(dict, "extensions", IDS_SYNC_DATATYPE_EXTENSIONS);
139 l10n_util::GetStringUTF16(IDS_SYNC_EVERYTHING)); 142 AddString(dict, "typedurls", IDS_SYNC_DATATYPE_TYPED_URLS);
140 localized_strings.SetString("choosedatatypes", 143 AddString(dict, "apps", IDS_SYNC_DATATYPE_APPS);
141 l10n_util::GetStringUTF16(IDS_SYNC_CHOOSE_DATATYPES)); 144 AddString(dict, "synczerodatatypeserror", IDS_SYNC_ZERO_DATA_TYPES_ERROR);
142 localized_strings.SetString("bookmarks", 145 AddString(dict, "abortederror", IDS_SYNC_SETUP_ABORTED_BY_PENDING_CLEAR);
143 l10n_util::GetStringUTF16(IDS_SYNC_DATATYPE_BOOKMARKS)); 146
144 localized_strings.SetString("preferences", 147 // Stuff for the encryption tab.
145 l10n_util::GetStringUTF16(IDS_SYNC_DATATYPE_PREFERENCES)); 148 dict->SetString("encryptionInstructions",
146 localized_strings.SetString("autofill", 149 GetStringFUTF16(IDS_SYNC_ENCRYPTION_INSTRUCTIONS,
147 l10n_util::GetStringUTF16(IDS_SYNC_DATATYPE_AUTOFILL)); 150 GetStringUTF16(IDS_PRODUCT_NAME)));
148 localized_strings.SetString("themes", 151 AddString(dict, "encryptAllLabel", IDS_SYNC_ENCRYPT_ALL_LABEL);
149 l10n_util::GetStringUTF16(IDS_SYNC_DATATYPE_THEMES)); 152 AddString(dict, "usePassphraseLabel", IDS_SYNC_PASSPHRASE_CHECKBOX_LABEL);
150 localized_strings.SetString("passwords", 153 AddString(dict, "passphraseWarning", IDS_SYNC_PASSPHRASE_WARNING);
151 l10n_util::GetStringUTF16(IDS_SYNC_DATATYPE_PASSWORDS)); 154
152 localized_strings.SetString("extensions", 155 // Stuff for the footer.
153 l10n_util::GetStringUTF16(IDS_SYNC_DATATYPE_EXTENSIONS)); 156 AddString(dict, "ok", IDS_OK);
154 localized_strings.SetString("typedurls", 157 AddString(dict, "cancel", IDS_CANCEL);
155 l10n_util::GetStringUTF16(IDS_SYNC_DATATYPE_TYPED_URLS)); 158 } else if (path_raw == kSyncPassphrasePath) {
156 localized_strings.SetString("apps", 159 html_resource_id = IDR_SYNC_PASSPHRASE_HTML;
157 l10n_util::GetStringUTF16(IDS_SYNC_DATATYPE_APPS)); 160 AddString(dict, "newPassphraseTitle", IDS_SYNC_NEW_PASSPHRASE_TITLE);
158 localized_strings.SetString("synczerodatatypeserror", 161 AddString(dict, "newPassphraseBody", IDS_SYNC_NEW_PASSPHRASE_BODY);
159 l10n_util::GetStringUTF16(IDS_SYNC_ZERO_DATA_TYPES_ERROR)); 162 AddString(dict, "enterPassphraseTitle", IDS_SYNC_ENTER_PASSPHRASE_TITLE);
160 localized_strings.SetString("setupabortederror", 163 AddString(dict, "enterPassphraseBody", IDS_SYNC_ENTER_PASSPHRASE_BODY);
161 l10n_util::GetStringUTF16(IDS_SYNC_SETUP_ABORTED_BY_PENDING_CLEAR)); 164 AddString(dict, "gaiaPassphraseTitle", IDS_SYNC_GAIA_PASSPHRASE_TITLE);
162 localized_strings.SetString("ok", 165 AddString(dict, "gaiaPassphraseBody", IDS_SYNC_GAIA_PASSPHRASE_BODY);
163 l10n_util::GetStringUTF16(IDS_OK)); 166 AddString(dict, "passphraseLabel", IDS_SYNC_PASSPHRASE_LABEL);
164 localized_strings.SetString("cancel", 167 AddString(dict, "ok", IDS_OK);
165 l10n_util::GetStringUTF16(IDS_CANCEL)); 168 AddString(dict, "cancel", IDS_CANCEL);
166 localized_strings.SetString("settingup", 169 } else if (path_raw == kSyncSettingUpPath) {
167 l10n_util::GetStringUTF16(IDS_SYNC_LOGIN_SETTING_UP)); 170 html_resource_id = IDR_SYNC_SETTING_UP_HTML;
168 static const base::StringPiece html(ResourceBundle::GetSharedInstance() 171
169 .GetRawDataResource(IDR_SYNC_CHOOSE_DATATYPES_HTML)); 172 AddString(dict, "settingup", IDS_SYNC_LOGIN_SETTING_UP);
170 SetFontAndTextDirection(&localized_strings); 173 AddString(dict, "cancel", IDS_CANCEL);
171 response = jstemplate_builder::GetI18nTemplateHtml(
172 html, &localized_strings);
173 } else if (path_raw == kSyncSetupDonePath) { 174 } else if (path_raw == kSyncSetupDonePath) {
174 DictionaryValue localized_strings; 175 html_resource_id = IDR_SYNC_SETUP_DONE_HTML;
175 localized_strings.SetString("success", 176
176 l10n_util::GetStringUTF16(IDS_SYNC_SUCCESS)); 177 AddString(dict, "success", IDS_SYNC_SUCCESS);
177 localized_strings.SetString("setupsummary", 178 dict->SetString("setupsummary",
178 l10n_util::GetStringFUTF16(IDS_SYNC_SETUP_ALL_DONE, 179 GetStringFUTF16(IDS_SYNC_SETUP_ALL_DONE,
179 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME))); 180 GetStringUTF16(IDS_PRODUCT_NAME)));
180 localized_strings.SetString("firsttimesetupsummary", 181 AddString(dict, "firsttimesummary", IDS_SYNC_SETUP_FIRST_TIME_ALL_DONE);
181 l10n_util::GetStringUTF16(IDS_SYNC_SETUP_FIRST_TIME_ALL_DONE)); 182 AddString(dict, "okay", IDS_SYNC_SETUP_OK_BUTTON_LABEL);
182 localized_strings.SetString("okay",
183 l10n_util::GetStringUTF16(IDS_SYNC_SETUP_OK_BUTTON_LABEL));
184 static const base::StringPiece html(ResourceBundle::GetSharedInstance()
185 .GetRawDataResource(IDR_SYNC_SETUP_DONE_HTML));
186 SetFontAndTextDirection(&localized_strings);
187 response = jstemplate_builder::GetI18nTemplateHtml(
188 html, &localized_strings);
189 } else if (path_raw == kSyncSetupFlowPath) { 183 } else if (path_raw == kSyncSetupFlowPath) {
190 static const base::StringPiece html(ResourceBundle::GetSharedInstance() 184 html_resource_id = IDR_SYNC_SETUP_FLOW_HTML;
191 .GetRawDataResource(IDR_SYNC_SETUP_FLOW_HTML));
192 response = html.as_string();
193 } 185 }
186
187 if (html_resource_id > 0) {
188 const base::StringPiece html(
189 ResourceBundle::GetSharedInstance().GetRawDataResource(
190 html_resource_id));
191 SetFontAndTextDirection(dict);
192 response = jstemplate_builder::GetI18nTemplateHtml(html, dict);
193 }
194
194 // Send the response. 195 // Send the response.
195 scoped_refptr<RefCountedBytes> html_bytes(new RefCountedBytes); 196 scoped_refptr<RefCountedBytes> html_bytes(new RefCountedBytes);
196 html_bytes->data.resize(response.size()); 197 html_bytes->data.resize(response.size());
197 std::copy(response.begin(), response.end(), html_bytes->data.begin()); 198 std::copy(response.begin(), response.end(), html_bytes->data.begin());
198 SendResponse(request_id, html_bytes); 199 SendResponse(request_id, html_bytes);
199 } 200 }
200 201
201 std::string SyncResourcesSource::GetLocalizedUrl( 202 std::string SyncResourcesSource::GetLocalizedUrl(
202 const std::string& url) const { 203 const std::string& url) const {
203 GURL original_url(url); 204 GURL original_url(url);
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
271 void SyncSetupWizard::SetParent(gfx::NativeWindow parent_window) { 272 void SyncSetupWizard::SetParent(gfx::NativeWindow parent_window) {
272 parent_window_ = parent_window; 273 parent_window_ = parent_window;
273 } 274 }
274 275
275 // static 276 // static
276 SyncSetupWizard::State SyncSetupWizard::GetEndStateForDiscreteRun( 277 SyncSetupWizard::State SyncSetupWizard::GetEndStateForDiscreteRun(
277 State start_state) { 278 State start_state) {
278 State result = FATAL_ERROR; 279 State result = FATAL_ERROR;
279 if (start_state == GAIA_LOGIN) { 280 if (start_state == GAIA_LOGIN) {
280 result = GAIA_SUCCESS; 281 result = GAIA_SUCCESS;
281 } else if (start_state == CHOOSE_DATA_TYPES) { 282 } else if (start_state == CONFIGURE) {
282 result = DONE; 283 result = DONE;
283 } 284 }
284 DCHECK_NE(FATAL_ERROR, result) << 285 DCHECK_NE(FATAL_ERROR, result) <<
285 "Invalid start state for discrete run: " << start_state; 286 "Invalid start state for discrete run: " << start_state;
286 return result; 287 return result;
287 } 288 }
OLDNEW
« no previous file with comments | « chrome/browser/sync/sync_setup_wizard.h ('k') | chrome/browser/sync/sync_setup_wizard_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698