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

Side by Side Diff: chrome/browser/extensions/api/media_galleries/media_galleries_api.cc

Issue 93643002: Media Galleries: Add chrome.mediaGalleries.addUserSelectedFolder(). (Closed) Base URL: svn://chrome-svn/chrome/trunk/src/
Patch Set: Created 6 years, 11 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) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 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 // Implements the Chrome Extensions Media Galleries API. 5 // Implements the Chrome Extensions Media Galleries API.
6 6
7 #include "chrome/browser/extensions/api/media_galleries/media_galleries_api.h" 7 #include "chrome/browser/extensions/api/media_galleries/media_galleries_api.h"
8 8
9 #include <set> 9 #include <set>
10 #include <string> 10 #include <string>
11 #include <vector> 11 #include <vector>
12 12
13 #include "apps/shell_window.h" 13 #include "apps/shell_window.h"
14 #include "apps/shell_window_registry.h" 14 #include "apps/shell_window_registry.h"
15 #include "base/platform_file.h" 15 #include "base/platform_file.h"
16 #include "base/stl_util.h" 16 #include "base/stl_util.h"
17 #include "base/strings/string_number_conversions.h" 17 #include "base/strings/string_number_conversions.h"
18 #include "base/strings/utf_string_conversions.h" 18 #include "base/strings/utf_string_conversions.h"
19 #include "base/values.h" 19 #include "base/values.h"
20 #include "chrome/browser/browser_process.h" 20 #include "chrome/browser/browser_process.h"
21 #include "chrome/browser/extensions/api/file_system/file_system_api.h"
21 #include "chrome/browser/extensions/blob_reader.h" 22 #include "chrome/browser/extensions/blob_reader.h"
23 #include "chrome/browser/extensions/extension_prefs.h"
22 #include "chrome/browser/media_galleries/media_file_system_registry.h" 24 #include "chrome/browser/media_galleries/media_file_system_registry.h"
23 #include "chrome/browser/media_galleries/media_galleries_dialog_controller.h" 25 #include "chrome/browser/media_galleries/media_galleries_dialog_controller.h"
24 #include "chrome/browser/media_galleries/media_galleries_histograms.h" 26 #include "chrome/browser/media_galleries/media_galleries_histograms.h"
25 #include "chrome/browser/media_galleries/media_galleries_preferences.h" 27 #include "chrome/browser/media_galleries/media_galleries_preferences.h"
28 #include "chrome/browser/platform_util.h"
26 #include "chrome/browser/profiles/profile.h" 29 #include "chrome/browser/profiles/profile.h"
27 #include "chrome/browser/storage_monitor/storage_info.h" 30 #include "chrome/browser/storage_monitor/storage_info.h"
28 #include "chrome/browser/ui/chrome_select_file_policy.h" 31 #include "chrome/browser/ui/chrome_select_file_policy.h"
29 #include "chrome/common/extensions/api/media_galleries.h" 32 #include "chrome/common/extensions/api/media_galleries.h"
30 #include "chrome/common/extensions/permissions/media_galleries_permission.h" 33 #include "chrome/common/extensions/permissions/media_galleries_permission.h"
31 #include "chrome/common/pref_names.h" 34 #include "chrome/common/pref_names.h"
32 #include "components/web_modal/web_contents_modal_dialog_manager.h" 35 #include "components/web_modal/web_contents_modal_dialog_manager.h"
33 #include "content/public/browser/browser_thread.h" 36 #include "content/public/browser/browser_thread.h"
34 #include "content/public/browser/child_process_security_policy.h" 37 #include "content/public/browser/child_process_security_policy.h"
35 #include "content/public/browser/render_process_host.h" 38 #include "content/public/browser/render_process_host.h"
36 #include "content/public/browser/render_view_host.h" 39 #include "content/public/browser/render_view_host.h"
37 #include "content/public/browser/web_contents.h" 40 #include "content/public/browser/web_contents.h"
41 #include "content/public/browser/web_contents_view.h"
38 #include "extensions/common/extension.h" 42 #include "extensions/common/extension.h"
39 #include "extensions/common/permissions/api_permission.h" 43 #include "extensions/common/permissions/api_permission.h"
40 #include "extensions/common/permissions/permissions_data.h" 44 #include "extensions/common/permissions/permissions_data.h"
45 #include "grit/generated_resources.h"
41 #include "net/base/mime_sniffer.h" 46 #include "net/base/mime_sniffer.h"
47 #include "ui/base/l10n/l10n_util.h"
42 48
43 using content::WebContents; 49 using content::WebContents;
44 using web_modal::WebContentsModalDialogManager; 50 using web_modal::WebContentsModalDialogManager;
45 51
46 namespace extensions { 52 namespace extensions {
47 53
48 namespace MediaGalleries = api::media_galleries; 54 namespace MediaGalleries = api::media_galleries;
49 namespace GetMediaFileSystems = MediaGalleries::GetMediaFileSystems; 55 namespace GetMediaFileSystems = MediaGalleries::GetMediaFileSystems;
50 56
51 namespace { 57 namespace {
(...skipping 17 matching lines...) Expand all
69 return false; 75 return false;
70 } 76 }
71 77
72 return true; 78 return true;
73 } 79 }
74 80
75 MediaFileSystemRegistry* media_file_system_registry() { 81 MediaFileSystemRegistry* media_file_system_registry() {
76 return g_browser_process->media_file_system_registry(); 82 return g_browser_process->media_file_system_registry();
77 } 83 }
78 84
85 base::ListValue* ConstructFileSystemList(
86 content::RenderViewHost* rvh,
87 const Extension* extension,
88 const std::vector<MediaFileSystemInfo>& filesystems) {
89 if (!rvh)
90 return NULL;
91
92 MediaGalleriesPermission::CheckParam read_param(
93 MediaGalleriesPermission::kReadPermission);
94 bool has_read_permission = PermissionsData::CheckAPIPermissionWithParam(
95 extension, APIPermission::kMediaGalleries, &read_param);
96 MediaGalleriesPermission::CheckParam copy_to_param(
97 MediaGalleriesPermission::kCopyToPermission);
98 bool has_copy_to_permission = PermissionsData::CheckAPIPermissionWithParam(
99 extension, APIPermission::kMediaGalleries, &copy_to_param);
100 MediaGalleriesPermission::CheckParam delete_param(
101 MediaGalleriesPermission::kDeletePermission);
102 bool has_delete_permission = PermissionsData::CheckAPIPermissionWithParam(
103 extension, APIPermission::kMediaGalleries, &delete_param);
104
105 const int child_id = rvh->GetProcess()->GetID();
106 scoped_ptr<base::ListValue> list(new base::ListValue());
107 for (size_t i = 0; i < filesystems.size(); ++i) {
108 scoped_ptr<base::DictionaryValue> file_system_dict_value(
109 new base::DictionaryValue());
110
111 // Send the file system id so the renderer can create a valid FileSystem
112 // object.
113 file_system_dict_value->SetStringWithoutPathExpansion(
114 "fsid", filesystems[i].fsid);
115
116 file_system_dict_value->SetStringWithoutPathExpansion(
117 kNameKey, filesystems[i].name);
118 file_system_dict_value->SetStringWithoutPathExpansion(
119 kGalleryIdKey,
120 base::Uint64ToString(filesystems[i].pref_id));
121 if (!filesystems[i].transient_device_id.empty()) {
122 file_system_dict_value->SetStringWithoutPathExpansion(
123 kDeviceIdKey, filesystems[i].transient_device_id);
124 }
125 file_system_dict_value->SetBooleanWithoutPathExpansion(
126 kIsRemovableKey, filesystems[i].removable);
127 file_system_dict_value->SetBooleanWithoutPathExpansion(
128 kIsMediaDeviceKey, filesystems[i].media_device);
129 file_system_dict_value->SetBooleanWithoutPathExpansion(
130 kIsAvailableKey, true);
131
132 list->Append(file_system_dict_value.release());
133
134 if (filesystems[i].path.empty())
135 continue;
136
137 if (has_read_permission) {
138 content::ChildProcessSecurityPolicy* policy =
139 content::ChildProcessSecurityPolicy::GetInstance();
140 policy->GrantReadFileSystem(child_id, filesystems[i].fsid);
141 if (has_delete_permission) {
142 policy->GrantDeleteFromFileSystem(child_id, filesystems[i].fsid);
143 if (has_copy_to_permission) {
144 policy->GrantCopyIntoFileSystem(child_id, filesystems[i].fsid);
145 }
146 }
147 }
148 }
149
150 return list.release();
151 }
152
153 class SelectDirectoryDialog : public ui::SelectFileDialog::Listener,
154 public base::RefCounted<SelectDirectoryDialog> {
155 public:
156 // Selected file path, or an empty path if the user canceled.
157 typedef base::Callback<void(const base::FilePath&)> Callback;
158
159 SelectDirectoryDialog(WebContents* web_contents, const Callback& callback)
160 : web_contents_(web_contents),
161 callback_(callback) {
162 select_file_dialog_ = ui::SelectFileDialog::Create(
163 this, new ChromeSelectFilePolicy(web_contents));
164 }
165
166 void Show(const base::FilePath& default_path) {
167 AddRef(); // Balanced in the two reachable listener outcomes.
168 select_file_dialog_->SelectFile(
169 ui::SelectFileDialog::SELECT_FOLDER,
170 l10n_util::GetStringUTF16(IDS_MEDIA_GALLERIES_DIALOG_ADD_GALLERY_TITLE),
171 default_path,
172 NULL,
173 0,
174 base::FilePath::StringType(),
175 platform_util::GetTopLevel(web_contents_->GetView()->GetNativeView()),
176 NULL);
177 }
178
179 // ui::SelectFileDialog::Listener implementation.
180 virtual void FileSelected(const base::FilePath& path,
181 int index,
182 void* params) OVERRIDE {
183 callback_.Run(path);
184 Release(); // Balanced in Show().
185 }
186
187 virtual void MultiFilesSelected(const std::vector<base::FilePath>& files,
188 void* params) OVERRIDE {
189 NOTREACHED() << "Should not be able to select multiple files";
190 }
191
192 virtual void FileSelectionCanceled(void* params) OVERRIDE {
193 callback_.Run(base::FilePath());
194 Release(); // Balanced in Show().
195 }
196
197 private:
198 friend class base::RefCounted<SelectDirectoryDialog>;
199 virtual ~SelectDirectoryDialog() {}
200
201 scoped_refptr<ui::SelectFileDialog> select_file_dialog_;
202 WebContents* web_contents_;
203 Callback callback_;
204
205 DISALLOW_COPY_AND_ASSIGN(SelectDirectoryDialog);
206 };
207
79 } // namespace 208 } // namespace
80 209
81 MediaGalleriesGetMediaFileSystemsFunction:: 210 MediaGalleriesGetMediaFileSystemsFunction::
82 ~MediaGalleriesGetMediaFileSystemsFunction() {} 211 ~MediaGalleriesGetMediaFileSystemsFunction() {}
83 212
84 bool MediaGalleriesGetMediaFileSystemsFunction::RunImpl() { 213 bool MediaGalleriesGetMediaFileSystemsFunction::RunImpl() {
85 if (!ApiIsAccessible(&error_)) 214 if (!ApiIsAccessible(&error_))
86 return false; 215 return false;
87 216
88 media_galleries::UsageCount(media_galleries::GET_MEDIA_FILE_SYSTEMS); 217 media_galleries::UsageCount(media_galleries::GET_MEDIA_FILE_SYSTEMS);
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after
146 ReturnGalleries(filesystems); 275 ReturnGalleries(filesystems);
147 } 276 }
148 277
149 void MediaGalleriesGetMediaFileSystemsFunction::GetAndReturnGalleries() { 278 void MediaGalleriesGetMediaFileSystemsFunction::GetAndReturnGalleries() {
150 GetMediaFileSystemsForExtension(base::Bind( 279 GetMediaFileSystemsForExtension(base::Bind(
151 &MediaGalleriesGetMediaFileSystemsFunction::ReturnGalleries, this)); 280 &MediaGalleriesGetMediaFileSystemsFunction::ReturnGalleries, this));
152 } 281 }
153 282
154 void MediaGalleriesGetMediaFileSystemsFunction::ReturnGalleries( 283 void MediaGalleriesGetMediaFileSystemsFunction::ReturnGalleries(
155 const std::vector<MediaFileSystemInfo>& filesystems) { 284 const std::vector<MediaFileSystemInfo>& filesystems) {
156 content::RenderViewHost* rvh = render_view_host(); 285 scoped_ptr<base::ListValue> list(
157 if (!rvh) { 286 ConstructFileSystemList(render_view_host(), GetExtension(), filesystems));
287 if (!list.get()) {
158 SendResponse(false); 288 SendResponse(false);
159 return; 289 return;
160 } 290 }
161 MediaGalleriesPermission::CheckParam read_param(
162 MediaGalleriesPermission::kReadPermission);
163 bool has_read_permission = PermissionsData::CheckAPIPermissionWithParam(
164 GetExtension(), APIPermission::kMediaGalleries, &read_param);
165 MediaGalleriesPermission::CheckParam copy_to_param(
166 MediaGalleriesPermission::kCopyToPermission);
167 bool has_copy_to_permission = PermissionsData::CheckAPIPermissionWithParam(
168 GetExtension(), APIPermission::kMediaGalleries, &copy_to_param);
169 MediaGalleriesPermission::CheckParam delete_param(
170 MediaGalleriesPermission::kDeletePermission);
171 bool has_delete_permission = PermissionsData::CheckAPIPermissionWithParam(
172 GetExtension(), APIPermission::kMediaGalleries, &delete_param);
173
174 const int child_id = rvh->GetProcess()->GetID();
175 base::ListValue* list = new base::ListValue();
176 for (size_t i = 0; i < filesystems.size(); i++) {
177 scoped_ptr<base::DictionaryValue> file_system_dict_value(
178 new base::DictionaryValue());
179
180 // Send the file system id so the renderer can create a valid FileSystem
181 // object.
182 file_system_dict_value->SetStringWithoutPathExpansion(
183 "fsid", filesystems[i].fsid);
184
185 file_system_dict_value->SetStringWithoutPathExpansion(
186 kNameKey, filesystems[i].name);
187 file_system_dict_value->SetStringWithoutPathExpansion(
188 kGalleryIdKey,
189 base::Uint64ToString(filesystems[i].pref_id));
190 if (!filesystems[i].transient_device_id.empty()) {
191 file_system_dict_value->SetStringWithoutPathExpansion(
192 kDeviceIdKey, filesystems[i].transient_device_id);
193 }
194 file_system_dict_value->SetBooleanWithoutPathExpansion(
195 kIsRemovableKey, filesystems[i].removable);
196 file_system_dict_value->SetBooleanWithoutPathExpansion(
197 kIsMediaDeviceKey, filesystems[i].media_device);
198 file_system_dict_value->SetBooleanWithoutPathExpansion(
199 kIsAvailableKey, true);
200
201 list->Append(file_system_dict_value.release());
202
203 if (filesystems[i].path.empty())
204 continue;
205
206 if (has_read_permission) {
207 content::ChildProcessSecurityPolicy* policy =
208 content::ChildProcessSecurityPolicy::GetInstance();
209 policy->GrantReadFileSystem(child_id, filesystems[i].fsid);
210 if (has_delete_permission) {
211 policy->GrantDeleteFromFileSystem(child_id, filesystems[i].fsid);
212 if (has_copy_to_permission) {
213 policy->GrantCopyIntoFileSystem(child_id, filesystems[i].fsid);
214 }
215 }
216 }
217 }
218 291
219 // The custom JS binding will use this list to create DOMFileSystem objects. 292 // The custom JS binding will use this list to create DOMFileSystem objects.
220 SetResult(list); 293 SetResult(list.release());
221 SendResponse(true); 294 SendResponse(true);
222 } 295 }
223 296
224 void MediaGalleriesGetMediaFileSystemsFunction::ShowDialog() { 297 void MediaGalleriesGetMediaFileSystemsFunction::ShowDialog() {
225 media_galleries::UsageCount(media_galleries::SHOW_DIALOG); 298 media_galleries::UsageCount(media_galleries::SHOW_DIALOG);
226 WebContents* contents = WebContents::FromRenderViewHost(render_view_host()); 299 WebContents* contents = WebContents::FromRenderViewHost(render_view_host());
227 WebContentsModalDialogManager* web_contents_modal_dialog_manager = 300 WebContentsModalDialogManager* web_contents_modal_dialog_manager =
228 WebContentsModalDialogManager::FromWebContents(contents); 301 WebContentsModalDialogManager::FromWebContents(contents);
229 if (!web_contents_modal_dialog_manager) { 302 if (!web_contents_modal_dialog_manager) {
230 // If there is no WebContentsModalDialogManager, then this contents is 303 // If there is no WebContentsModalDialogManager, then this contents is
(...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after
318 metadata.is_removable = StorageInfo::IsRemovableDevice(gallery.device_id); 391 metadata.is_removable = StorageInfo::IsRemovableDevice(gallery.device_id);
319 metadata.is_media_device = StorageInfo::IsMediaDevice(gallery.device_id); 392 metadata.is_media_device = StorageInfo::IsMediaDevice(gallery.device_id);
320 metadata.is_available = ContainsKey(*available_devices, gallery.device_id); 393 metadata.is_available = ContainsKey(*available_devices, gallery.device_id);
321 list->Append(metadata.ToValue().release()); 394 list->Append(metadata.ToValue().release());
322 } 395 }
323 396
324 SetResult(list); 397 SetResult(list);
325 SendResponse(true); 398 SendResponse(true);
326 } 399 }
327 400
401 MediaGalleriesAddUserSelectedFolderFunction::
402 ~MediaGalleriesAddUserSelectedFolderFunction() {}
403
404 bool MediaGalleriesAddUserSelectedFolderFunction::RunImpl() {
405 if (!ApiIsAccessible(&error_))
406 return false;
407
408 media_galleries::UsageCount(media_galleries::ADD_USER_SELECTED_FOLDER);
409 MediaGalleriesPreferences* preferences =
410 media_file_system_registry()->GetPreferences(GetProfile());
411 preferences->EnsureInitialized(base::Bind(
412 &MediaGalleriesAddUserSelectedFolderFunction::OnPreferencesInit,
413 this));
414 return true;
415 }
416
417 void MediaGalleriesAddUserSelectedFolderFunction::OnPreferencesInit() {
418 WebContents* contents = WebContents::FromRenderViewHost(render_view_host());
vandebo (ex-Chrome) 2014/01/09 00:29:16 nit: pull this window parent finding logic into a
Lei Zhang 2014/01/09 05:28:38 Done.
419 WebContentsModalDialogManager* web_contents_modal_dialog_manager =
420 WebContentsModalDialogManager::FromWebContents(contents);
421 if (!web_contents_modal_dialog_manager) {
422 // If there is no WebContentsModalDialogManager, then this contents is
423 // probably the background page for an app. Try to find a shell window to
424 // host the dialog.
425 apps::ShellWindow* window = apps::ShellWindowRegistry::Get(
426 GetProfile())->GetCurrentShellWindowForApp(GetExtension()->id());
427 if (window)
428 contents = window->web_contents();
429 else
430 contents = NULL;
431 }
432 base::FilePath last_used_path =
433 extensions::file_system_api::GetLastChooseEntryDirectory(
434 extensions::ExtensionPrefs::Get(GetProfile()),
435 GetExtension()->id());
436 SelectDirectoryDialog::Callback callback = base::Bind(
vandebo (ex-Chrome) 2014/01/09 00:29:16 This create a circular reference, but as we discus
437 &MediaGalleriesAddUserSelectedFolderFunction::OnDirectorySelected, this);
438 scoped_refptr<SelectDirectoryDialog> select_directory_dialog =
439 new SelectDirectoryDialog(contents, callback);
440 select_directory_dialog->Show(last_used_path);
441 }
442
443 void MediaGalleriesAddUserSelectedFolderFunction::OnDirectorySelected(
444 const base::FilePath& selected_directory) {
445 if (!selected_directory.empty()) {
446 // User cancelled case.
447 GetMediaFileSystemsForExtension(base::Bind(
448 &MediaGalleriesAddUserSelectedFolderFunction::ReturnGalleriesAndId,
449 this,
450 kInvalidMediaGalleryPrefId));
451 return;
452 }
453
454 extensions::file_system_api::SetLastChooseEntryDirectory(
455 extensions::ExtensionPrefs::Get(GetProfile()),
456 GetExtension()->id(),
457 selected_directory);
458
459 MediaGalleriesPreferences* preferences =
460 media_file_system_registry()->GetPreferences(GetProfile());
461 MediaGalleryPrefId pref_id =
462 preferences->AddGalleryByPath(selected_directory);
463 preferences->SetGalleryPermissionForExtension(*GetExtension(), pref_id, true);
464
465 GetMediaFileSystemsForExtension(base::Bind(
466 &MediaGalleriesAddUserSelectedFolderFunction::ReturnGalleriesAndId,
467 this,
468 pref_id));
469 }
470
471 void MediaGalleriesAddUserSelectedFolderFunction::ReturnGalleriesAndId(
472 MediaGalleryPrefId pref_id,
473 const std::vector<MediaFileSystemInfo>& filesystems) {
474 scoped_ptr<base::ListValue> list(
475 ConstructFileSystemList(render_view_host(), GetExtension(), filesystems));
476 if (!list.get()) {
477 SendResponse(false);
478 return;
479 }
480
481 std::string fsid;
482 if (pref_id != kInvalidMediaGalleryPrefId) {
483 for (size_t i = 0; i < filesystems.size(); ++i) {
484 if (filesystems[i].pref_id == pref_id) {
485 fsid = filesystems[i].fsid;
486 break;
487 }
488 }
489 }
490 list->AppendString(fsid);
vandebo (ex-Chrome) 2014/01/09 00:29:16 I still think this should use another level of dic
Lei Zhang 2014/01/09 05:28:38 Done.
491 SetResult(list.release());
492 SendResponse(true);
493 }
494
495 void
496 MediaGalleriesAddUserSelectedFolderFunction::GetMediaFileSystemsForExtension(
497 const MediaFileSystemsCallback& cb) {
498 if (!render_view_host()) {
499 cb.Run(std::vector<MediaFileSystemInfo>());
500 return;
501 }
502 MediaFileSystemRegistry* registry = media_file_system_registry();
503 DCHECK(registry->GetPreferences(GetProfile())->IsInitialized());
504 registry->GetMediaFileSystemsForExtension(
505 render_view_host(), GetExtension(), cb);
506 }
507
328 MediaGalleriesGetMetadataFunction::~MediaGalleriesGetMetadataFunction() {} 508 MediaGalleriesGetMetadataFunction::~MediaGalleriesGetMetadataFunction() {}
329 509
330 bool MediaGalleriesGetMetadataFunction::RunImpl() { 510 bool MediaGalleriesGetMetadataFunction::RunImpl() {
331 if (!ApiIsAccessible(&error_)) 511 if (!ApiIsAccessible(&error_))
332 return false; 512 return false;
333 513
334 std::string blob_uuid; 514 std::string blob_uuid;
335 EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &blob_uuid)); 515 EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &blob_uuid));
336 516
337 const base::Value* options_value = NULL; 517 const base::Value* options_value = NULL;
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
380 if (mime_type_sniffed) 560 if (mime_type_sniffed)
381 metadata.mime_type = mime_type; 561 metadata.mime_type = mime_type;
382 562
383 // TODO(tommycli): Kick off SafeMediaMetadataParser if |mime_type_only| false. 563 // TODO(tommycli): Kick off SafeMediaMetadataParser if |mime_type_only| false.
384 564
385 SetResult(metadata.ToValue().release()); 565 SetResult(metadata.ToValue().release());
386 SendResponse(true); 566 SendResponse(true);
387 } 567 }
388 568
389 } // namespace extensions 569 } // namespace extensions
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698