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

Side by Side Diff: chrome/browser/extensions/apps_promo.cc

Issue 7820003: Add support to download web store promo logos over https. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: cleanup Created 9 years, 3 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) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 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/extensions/apps_promo.h" 5 #include "chrome/browser/extensions/apps_promo.h"
6 6
7 #include "base/base64.h"
7 #include "base/command_line.h" 8 #include "base/command_line.h"
8 #include "base/metrics/histogram.h" 9 #include "base/metrics/histogram.h"
9 #include "chrome/browser/browser_process.h" 10 #include "chrome/browser/browser_process.h"
10 #include "chrome/browser/prefs/pref_service.h" 11 #include "chrome/browser/prefs/pref_service.h"
12 #include "chrome/browser/profiles/profile.h"
11 #include "chrome/browser/ui/webui/ntp/shown_sections_handler.h" 13 #include "chrome/browser/ui/webui/ntp/shown_sections_handler.h"
12 #include "chrome/common/chrome_switches.h" 14 #include "chrome/common/chrome_switches.h"
13 #include "chrome/common/extensions/extension.h" 15 #include "chrome/common/extensions/extension.h"
16 #include "chrome/common/chrome_notification_types.h"
14 #include "chrome/common/pref_names.h" 17 #include "chrome/common/pref_names.h"
18 #include "content/common/url_constants.h"
19 #include "content/common/notification_service.h"
20 #include "net/base/load_flags.h"
21 #include "net/url_request/url_request_status.h"
15 22
16 const int AppsPromo::kDefaultAppsCounterMax = 10; 23 const int AppsPromo::kDefaultAppsCounterMax = 10;
17 24
18 namespace { 25 namespace {
19 26
20 // The default logo for the promo. 27 // The default logo for the promo.
21 const char kDefaultPromoLogo[] = "chrome://theme/IDR_WEBSTORE_ICON"; 28 const char kDefaultLogo[] = "chrome://theme/IDR_WEBSTORE_ICON";
29
30 // The default promo data (this is only used with --force-apps-promo-visible).
Miranda Callahan 2011/09/01 07:43:38 nit: could you just make it "this is only used for
jstritar 2011/09/01 16:10:13 Done.
31 const char kDefaultHeader[] = "Browse thousands of apps and games for Chrome";
32 const char kDefaultButton[] = "Visit the Chrome Web Store";
33 const char kDefaultExpire[] = "No thanks";
34 const char kDefaultLink[] = "https://chrome.google.com/webstore";
35
36 // Http success status code.
37 const int kHttpSuccess = 200;
38
39 // The match pattern for valid logo URLs.
40 const char kValidLogoPattern[] = "https://*.google.com/*.png";
41
42 // The prefix for 'data' URL images.
43 const char kPNGDataURLPrefix[] = "data:image/png;base64,";
22 44
23 // Returns the string pref at |path|, using |fallback| as the default (if there 45 // Returns the string pref at |path|, using |fallback| as the default (if there
24 // is no pref value present). |fallback| is used for debugging in concert with 46 // is no pref value present). |fallback| is used for debugging in concert with
25 // --force-apps-promo-visible. 47 // --force-apps-promo-visible.
26 std::string GetStringPref(const char* path, const std::string& fallback) { 48 std::string GetStringPref(const char* path, const std::string& fallback) {
27 PrefService* local_state = g_browser_process->local_state(); 49 PrefService* local_state = g_browser_process->local_state();
28 std::string retval(local_state->GetString(path)); 50 std::string retval(local_state->GetString(path));
29 if (retval.empty() && CommandLine::ForCurrentProcess()->HasSwitch( 51 if (retval.empty() && CommandLine::ForCurrentProcess()->HasSwitch(
30 switches::kForceAppsPromoVisible)) { 52 switches::kForceAppsPromoVisible)) {
31 retval = fallback; 53 retval = fallback;
32 } 54 }
33 return retval; 55 return retval;
34 } 56 }
35 57
36 } // namespace 58 } // namespace
37 59
60 AppsPromo::PromoData::PromoData() {}
61 AppsPromo::PromoData::PromoData(const std::string& id,
62 const std::string& header,
63 const std::string& button,
64 const GURL& link,
65 const std::string& expire,
66 const GURL& logo,
67 const int user_group)
68 : id(id),
69 header(header),
70 button(button),
71 link(link),
72 expire(expire),
73 logo(logo),
74 user_group(user_group) {}
75
76 AppsPromo::PromoData::~PromoData() {}
77
38 // static 78 // static
39 void AppsPromo::RegisterPrefs(PrefService* local_state) { 79 void AppsPromo::RegisterPrefs(PrefService* local_state) {
40 std::string empty; 80 std::string empty;
41 local_state->RegisterBooleanPref(prefs::kNTPWebStoreEnabled, false); 81 local_state->RegisterBooleanPref(prefs::kNTPWebStoreEnabled, false);
42 local_state->RegisterStringPref(prefs::kNTPWebStorePromoId, empty); 82 local_state->RegisterStringPref(prefs::kNTPWebStorePromoId, empty);
43 local_state->RegisterStringPref(prefs::kNTPWebStorePromoHeader, empty); 83 local_state->RegisterStringPref(prefs::kNTPWebStorePromoHeader, empty);
44 local_state->RegisterStringPref(prefs::kNTPWebStorePromoButton, empty); 84 local_state->RegisterStringPref(prefs::kNTPWebStorePromoButton, empty);
45 local_state->RegisterStringPref(prefs::kNTPWebStorePromoLink, empty); 85 local_state->RegisterStringPref(prefs::kNTPWebStorePromoLink, empty);
46 local_state->RegisterStringPref(prefs::kNTPWebStorePromoLogo, empty); 86 local_state->RegisterStringPref(prefs::kNTPWebStorePromoLogo, empty);
87 local_state->RegisterStringPref(prefs::kNTPWebStorePromoLogoOriginal, empty);
47 local_state->RegisterStringPref(prefs::kNTPWebStorePromoExpire, empty); 88 local_state->RegisterStringPref(prefs::kNTPWebStorePromoExpire, empty);
48 local_state->RegisterIntegerPref(prefs::kNTPWebStorePromoUserGroup, 0); 89 local_state->RegisterIntegerPref(prefs::kNTPWebStorePromoUserGroup, 0);
49 } 90 }
50 91
51 // static 92 // static
52 void AppsPromo::RegisterUserPrefs(PrefService* prefs) { 93 void AppsPromo::RegisterUserPrefs(PrefService* prefs) {
53 // Set the default value for the counter to max+1 since we don't install 94 // Set the default value for the counter to max+1 since we don't install
54 // default apps for new users. 95 // default apps for new users.
55 prefs->RegisterIntegerPref(prefs::kAppsPromoCounter, 96 prefs->RegisterIntegerPref(prefs::kAppsPromoCounter,
56 kDefaultAppsCounterMax + 1, 97 kDefaultAppsCounterMax + 1,
57 PrefService::UNSYNCABLE_PREF); 98 PrefService::UNSYNCABLE_PREF);
58 prefs->RegisterBooleanPref(prefs::kDefaultAppsInstalled, 99 prefs->RegisterBooleanPref(prefs::kDefaultAppsInstalled,
59 false, 100 false,
60 PrefService::UNSYNCABLE_PREF); 101 PrefService::UNSYNCABLE_PREF);
61 prefs->RegisterStringPref(prefs::kNTPWebStorePromoLastId, 102 prefs->RegisterStringPref(prefs::kNTPWebStorePromoLastId,
62 std::string(), 103 std::string(),
63 PrefService::UNSYNCABLE_PREF); 104 PrefService::UNSYNCABLE_PREF);
64 prefs->RegisterBooleanPref(prefs::kNTPHideWebStorePromo, 105 prefs->RegisterBooleanPref(prefs::kNTPHideWebStorePromo,
65 false, 106 false,
66 PrefService::UNSYNCABLE_PREF); 107 PrefService::UNSYNCABLE_PREF);
67 } 108 }
68 109
69 // static 110 // static
70 void AppsPromo::ClearPromo() {
71 PrefService* local_state = g_browser_process->local_state();
72 local_state->ClearPref(prefs::kNTPWebStoreEnabled);
73 local_state->ClearPref(prefs::kNTPWebStorePromoId);
74 local_state->ClearPref(prefs::kNTPWebStorePromoHeader);
75 local_state->ClearPref(prefs::kNTPWebStorePromoButton);
76 local_state->ClearPref(prefs::kNTPWebStorePromoLink);
77 local_state->ClearPref(prefs::kNTPWebStorePromoLogo);
78 local_state->ClearPref(prefs::kNTPWebStorePromoExpire);
79 local_state->ClearPref(prefs::kNTPWebStorePromoUserGroup);
80 }
81
82 // static
83 bool AppsPromo::IsPromoSupportedForLocale() { 111 bool AppsPromo::IsPromoSupportedForLocale() {
84 PrefService* local_state = g_browser_process->local_state(); 112 PrefService* local_state = g_browser_process->local_state();
85 // PromoResourceService will clear the promo data if the current locale is 113 // PromoResourceService will clear the promo data if the current locale is
86 // not supported. 114 // not supported.
87 return local_state->HasPrefPath(prefs::kNTPWebStorePromoId) && 115 return local_state->HasPrefPath(prefs::kNTPWebStorePromoId) &&
88 local_state->HasPrefPath(prefs::kNTPWebStorePromoHeader) && 116 local_state->HasPrefPath(prefs::kNTPWebStorePromoHeader) &&
89 local_state->HasPrefPath(prefs::kNTPWebStorePromoButton) && 117 local_state->HasPrefPath(prefs::kNTPWebStorePromoButton) &&
90 local_state->HasPrefPath(prefs::kNTPWebStorePromoLink) && 118 local_state->HasPrefPath(prefs::kNTPWebStorePromoLink) &&
91 local_state->HasPrefPath(prefs::kNTPWebStorePromoLogo) && 119 local_state->HasPrefPath(prefs::kNTPWebStorePromoLogo) &&
92 local_state->HasPrefPath(prefs::kNTPWebStorePromoExpire) && 120 local_state->HasPrefPath(prefs::kNTPWebStorePromoExpire) &&
93 local_state->HasPrefPath(prefs::kNTPWebStorePromoUserGroup); 121 local_state->HasPrefPath(prefs::kNTPWebStorePromoUserGroup);
94 } 122 }
95 123
96 // static 124 // static
97 bool AppsPromo::IsWebStoreSupportedForLocale() { 125 bool AppsPromo::IsWebStoreSupportedForLocale() {
98 PrefService* local_state = g_browser_process->local_state(); 126 PrefService* local_state = g_browser_process->local_state();
99 return local_state->GetBoolean(prefs::kNTPWebStoreEnabled); 127 return local_state->GetBoolean(prefs::kNTPWebStoreEnabled);
100 } 128 }
101 129
102 // static 130 // static
103 std::string AppsPromo::GetPromoButtonText() {
104 return GetStringPref(prefs::kNTPWebStorePromoButton, "Click here now");
105 }
106
107 // static
108 std::string AppsPromo::GetPromoId() {
109 return GetStringPref(prefs::kNTPWebStorePromoId, "");
110 }
111
112 // static
113 std::string AppsPromo::GetPromoHeaderText() {
114 return GetStringPref(prefs::kNTPWebStorePromoHeader, "Get great apps!");
115 }
116
117 // static
118 GURL AppsPromo::GetPromoLink() {
119 return GURL(GetStringPref(prefs::kNTPWebStorePromoLink,
120 "https://chrome.google.com/webstore"));
121 }
122
123 // static
124 GURL AppsPromo::GetPromoLogo() {
125 PrefService* local_state = g_browser_process->local_state();
126 GURL logo_url(local_state->GetString(prefs::kNTPWebStorePromoLogo));
127 if (logo_url.is_valid() && logo_url.SchemeIs("data"))
128 return logo_url;
129 return GURL(kDefaultPromoLogo);
130 }
131
132 // static
133 std::string AppsPromo::GetPromoExpireText() {
134 return GetStringPref(prefs::kNTPWebStorePromoExpire, "No thanks.");
135 }
136
137 // static
138 int AppsPromo::GetPromoUserGroup() {
139 PrefService* local_state = g_browser_process->local_state();
140 return local_state->GetInteger(prefs::kNTPWebStorePromoUserGroup);
141 }
142
143 // static
144 void AppsPromo::SetPromo(const std::string& id,
145 const std::string& header_text,
146 const std::string& button_text,
147 const GURL& link,
148 const std::string& expire_text,
149 const GURL& logo,
150 const int user_group) {
151 PrefService* local_state = g_browser_process->local_state();
152 local_state->SetString(prefs::kNTPWebStorePromoId, id);
153 local_state->SetString(prefs::kNTPWebStorePromoButton, button_text);
154 local_state->SetString(prefs::kNTPWebStorePromoHeader, header_text);
155 local_state->SetString(prefs::kNTPWebStorePromoLink, link.spec());
156 local_state->SetString(prefs::kNTPWebStorePromoLogo, logo.spec());
157 local_state->SetString(prefs::kNTPWebStorePromoExpire, expire_text);
158 local_state->SetInteger(prefs::kNTPWebStorePromoUserGroup, user_group);
159 }
160
161 // static
162 void AppsPromo::SetWebStoreSupportedForLocale(bool supported) { 131 void AppsPromo::SetWebStoreSupportedForLocale(bool supported) {
163 PrefService* local_state = g_browser_process->local_state(); 132 PrefService* local_state = g_browser_process->local_state();
164 local_state->SetBoolean(prefs::kNTPWebStoreEnabled, supported); 133 local_state->SetBoolean(prefs::kNTPWebStoreEnabled, supported);
165 } 134 }
166 135
136 // static
137 void AppsPromo::ClearPromo() {
138 PrefService* local_state = g_browser_process->local_state();
139 local_state->ClearPref(prefs::kNTPWebStoreEnabled);
140 local_state->ClearPref(prefs::kNTPWebStorePromoId);
141 local_state->ClearPref(prefs::kNTPWebStorePromoHeader);
142 local_state->ClearPref(prefs::kNTPWebStorePromoButton);
143 local_state->ClearPref(prefs::kNTPWebStorePromoLink);
144 local_state->ClearPref(prefs::kNTPWebStorePromoLogo);
145 local_state->ClearPref(prefs::kNTPWebStorePromoLogoOriginal);
146 local_state->ClearPref(prefs::kNTPWebStorePromoExpire);
147 local_state->ClearPref(prefs::kNTPWebStorePromoUserGroup);
148 }
149
150 // static
151 AppsPromo::PromoData AppsPromo::GetPromo() {
152 PromoData data;
153 PrefService* local_state = g_browser_process->local_state();
154
155 data.id = GetStringPref(prefs::kNTPWebStorePromoId, "");
156 data.link = GURL(GetStringPref(prefs::kNTPWebStorePromoLink, kDefaultLink));
157 data.user_group = local_state->GetInteger(prefs::kNTPWebStorePromoUserGroup);
158 data.header = GetStringPref(prefs::kNTPWebStorePromoHeader, kDefaultHeader);
159 data.button = GetStringPref(prefs::kNTPWebStorePromoButton, kDefaultButton);
160 data.expire = GetStringPref(prefs::kNTPWebStorePromoExpire, kDefaultExpire);
161
162 GURL logo_url(local_state->GetString(prefs::kNTPWebStorePromoLogo));
163 if (logo_url.is_valid() && logo_url.SchemeIs(chrome::kDataScheme))
164 data.logo = logo_url;
165 else
166 data.logo = GURL(kDefaultLogo);
167
168 return data;
169 }
170
171 // static
172 void AppsPromo::SetPromo(AppsPromo::PromoData data) {
173 PrefService* local_state = g_browser_process->local_state();
174 local_state->SetString(prefs::kNTPWebStorePromoId, data.id);
175 local_state->SetString(prefs::kNTPWebStorePromoButton, data.button);
176 local_state->SetString(prefs::kNTPWebStorePromoHeader, data.header);
177 local_state->SetString(prefs::kNTPWebStorePromoLink, data.link.spec());
178 local_state->SetString(prefs::kNTPWebStorePromoLogo, data.logo.spec());
179 local_state->SetString(prefs::kNTPWebStorePromoExpire, data.expire);
180 local_state->SetInteger(prefs::kNTPWebStorePromoUserGroup, data.user_group);
181 }
182
183 // static
184 GURL AppsPromo::GetPromoLogoOriginal() {
185 return GURL(GetStringPref(prefs::kNTPWebStorePromoLogoOriginal, ""));
186 }
187
188 // static
189 void AppsPromo::SetPromoLogoOriginal(GURL logo_original) {
190 PrefService* local_state = g_browser_process->local_state();
191 local_state->SetString(prefs::kNTPWebStorePromoLogoOriginal,
192 logo_original.spec());
193 }
194
167 AppsPromo::AppsPromo(PrefService* prefs) 195 AppsPromo::AppsPromo(PrefService* prefs)
168 : prefs_(prefs) { 196 : prefs_(prefs) {
169 // Poppit, Entanglement 197 // Poppit, Entanglement
170 old_default_app_ids_.insert("mcbkbpnkkkipelfledbfocopglifcfmi"); 198 old_default_app_ids_.insert("mcbkbpnkkkipelfledbfocopglifcfmi");
171 old_default_app_ids_.insert("aciahcmjmecflokailenpkdchphgkefd"); 199 old_default_app_ids_.insert("aciahcmjmecflokailenpkdchphgkefd");
172 } 200 }
173 201
174 AppsPromo::~AppsPromo() {} 202 AppsPromo::~AppsPromo() {}
175 203
176 bool AppsPromo::ShouldShowPromo(const ExtensionIdSet& installed_ids, 204 bool AppsPromo::ShouldShowPromo(const ExtensionIdSet& installed_ids,
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after
239 // current locale. 267 // current locale.
240 return IsWebStoreSupportedForLocale(); 268 return IsWebStoreSupportedForLocale();
241 #endif 269 #endif
242 } 270 }
243 271
244 void AppsPromo::ExpireDefaultApps() { 272 void AppsPromo::ExpireDefaultApps() {
245 SetPromoCounter(kDefaultAppsCounterMax + 1); 273 SetPromoCounter(kDefaultAppsCounterMax + 1);
246 } 274 }
247 275
248 void AppsPromo::MaximizeAppsIfNecessary() { 276 void AppsPromo::MaximizeAppsIfNecessary() {
249 std::string promo_id = GetPromoId(); 277 PromoData promo = GetPromo();
250 int maximize_setting = GetPromoUserGroup();
251 278
252 // Maximize the apps section of the NTP if this is the first time viewing the 279 // Maximize the apps section of the NTP if this is the first time viewing the
253 // specific promo and the current user group is targetted. 280 // specific promo and the current user group is targetted.
254 if (GetLastPromoId() != promo_id) { 281 if (GetLastPromoId() != promo.id) {
255 if ((maximize_setting & GetCurrentUserGroup()) != 0) 282 if ((promo.user_group & GetCurrentUserGroup()) != 0)
256 ShownSectionsHandler::SetShownSection(prefs_, APPS); 283 ShownSectionsHandler::SetShownSection(prefs_, APPS);
257 SetLastPromoId(promo_id); 284 SetLastPromoId(promo.id);
258 } 285 }
259 } 286 }
260 287
261 void AppsPromo::HidePromo() { 288 void AppsPromo::HidePromo() {
262 UMA_HISTOGRAM_ENUMERATION(extension_misc::kAppsPromoHistogram, 289 UMA_HISTOGRAM_ENUMERATION(extension_misc::kAppsPromoHistogram,
263 extension_misc::PROMO_CLOSE, 290 extension_misc::PROMO_CLOSE,
264 extension_misc::PROMO_BUCKET_BOUNDARY); 291 extension_misc::PROMO_BUCKET_BOUNDARY);
265 292
266 // Put the apps section into menu mode, and maximize the recent section. 293 // Put the apps section into menu mode, and maximize the recent section.
267 ShownSectionsHandler::SetShownSection(prefs_, MENU_APPS); 294 ShownSectionsHandler::SetShownSection(prefs_, MENU_APPS);
(...skipping 22 matching lines...) Expand all
290 bool AppsPromo::GetDefaultAppsInstalled() const { 317 bool AppsPromo::GetDefaultAppsInstalled() const {
291 return prefs_->GetBoolean(prefs::kDefaultAppsInstalled); 318 return prefs_->GetBoolean(prefs::kDefaultAppsInstalled);
292 } 319 }
293 320
294 AppsPromo::UserGroup AppsPromo::GetCurrentUserGroup() const { 321 AppsPromo::UserGroup AppsPromo::GetCurrentUserGroup() const {
295 const PrefService::Preference* last_promo_id 322 const PrefService::Preference* last_promo_id
296 = prefs_->FindPreference(prefs::kNTPWebStorePromoLastId); 323 = prefs_->FindPreference(prefs::kNTPWebStorePromoLastId);
297 CHECK(last_promo_id); 324 CHECK(last_promo_id);
298 return last_promo_id->IsDefaultValue() ? USERS_NEW : USERS_EXISTING; 325 return last_promo_id->IsDefaultValue() ? USERS_NEW : USERS_EXISTING;
299 } 326 }
327
328 AppsPromoLogoDownloader::AppsPromoLogoDownloader(
329 Profile* profile,
330 AppsPromo::PromoData promo_data)
331 : profile_(profile),
332 promo_data_(promo_data) {
333 if (ShouldFetchLogo()) {
334 // Record the original URL so we know when it has changed.
335 AppsPromo::SetPromoLogoOriginal(promo_data_.logo);
336 FetchLogo();
337 } else {
338 // We only care about the original logo URL when we need to fetch it.
339 AppsPromo::SetPromoLogoOriginal(GURL());
340 SavePromo();
341 }
342 }
343
344 AppsPromoLogoDownloader::~AppsPromoLogoDownloader() {}
345
346 void AppsPromoLogoDownloader::OnURLFetchComplete(const URLFetcher* source) {
347 std::string data;
348 std::string base64_data;
349
350 CHECK(source == url_fetcher_.get());
351 CHECK(source->GetResponseAsString(&data));
352
353 if (source->status().is_success() &&
354 source->response_code() == kHttpSuccess &&
355 base::Base64Encode(data, &base64_data)) {
356 promo_data_.logo = GURL(kPNGDataURLPrefix + base64_data);
357 } else {
358 // The logo wasn't downloaded correctly or we failed to encode it in
359 // base64. Reset the original URL so we fetch it again next time. AppsPromo
360 // will revert to the default logo.
361 AppsPromo::SetPromoLogoOriginal(GURL());
362 }
363
364 SavePromo();
365 }
366
367 void AppsPromoLogoDownloader::FetchLogo() {
368 CHECK(promo_data_.logo.scheme() == chrome::kHttpsScheme);
369
370 url_fetcher_.reset(URLFetcher::Create(
371 0, promo_data_.logo, URLFetcher::GET, this));
372 url_fetcher_->set_request_context(
373 g_browser_process->system_request_context());
374 url_fetcher_->set_load_flags(net::LOAD_DO_NOT_SEND_COOKIES |
375 net::LOAD_DO_NOT_SAVE_COOKIES);
376 url_fetcher_->Start();
377 }
378
379 void AppsPromoLogoDownloader::SavePromo() {
380 AppsPromo::SetPromo(promo_data_);
381
382 NotificationService::current()->Notify(
383 chrome::NOTIFICATION_WEB_STORE_PROMO_LOADED,
384 Source<Profile>(profile_),
385 NotificationService::NoDetails());
386 }
387
388 bool AppsPromoLogoDownloader::ShouldFetchLogo() {
389 URLPattern allowed_urls(URLPattern::SCHEME_HTTPS, kValidLogoPattern);
390 if (!allowed_urls.MatchesURL(promo_data_.logo))
391 return false;
392
393 // To prevent us from downloading the same image over and over again, we only
394 // fetch the logo if the original URL of the existing logo is different from
395 // the new logo URL.
396 return promo_data_.logo != AppsPromo::GetPromoLogoOriginal();
397 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698