file_system_api.cc revision 5821806d5e7f356e8fa4b058a389a808ea183019
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 3// found in the LICENSE file. 4 5#include "chrome/browser/extensions/api/file_system/file_system_api.h" 6 7#include "base/bind.h" 8#include "base/file_path.h" 9#include "base/file_util.h" 10#include "base/logging.h" 11#include "base/path_service.h" 12#include "base/string_util.h" 13#include "base/sys_string_conversions.h" 14#include "base/utf_string_conversions.h" 15#include "chrome/browser/extensions/shell_window_registry.h" 16#include "chrome/browser/platform_util.h" 17#include "chrome/browser/ui/chrome_select_file_policy.h" 18#include "chrome/browser/ui/extensions/shell_window.h" 19#include "chrome/common/extensions/api/file_system.h" 20#include "chrome/common/extensions/permissions/api_permission.h" 21#include "grit/generated_resources.h" 22#include "net/base/mime_util.h" 23#include "content/public/browser/child_process_security_policy.h" 24#include "content/public/browser/render_view_host.h" 25#include "content/public/browser/render_process_host.h" 26#include "content/public/browser/web_contents.h" 27#include "webkit/fileapi/file_system_types.h" 28#include "webkit/fileapi/file_system_util.h" 29#include "webkit/fileapi/isolated_context.h" 30#include "ui/base/l10n/l10n_util.h" 31#include "ui/base/dialogs/select_file_dialog.h" 32 33#if defined(OS_MACOSX) 34#include "base/mac/foundation_util.h" 35#include <CoreFoundation/CoreFoundation.h> 36#endif 37 38using fileapi::IsolatedContext; 39 40const char kInvalidParameters[] = "Invalid parameters"; 41const char kSecurityError[] = "Security error"; 42const char kInvalidCallingPage[] = "Invalid calling page"; 43const char kUserCancelled[] = "User cancelled"; 44const char kWritableFileError[] = "Invalid file for writing"; 45const char kRequiresFileSystemWriteError[] = 46 "Operation requires fileSystem.write permission"; 47const char kUnknownChooseEntryType[] = "Unknown type"; 48 49const char kOpenFileOption[] = "openFile"; 50const char kOpenWritableFileOption[] ="openWritableFile"; 51const char kSaveFileOption[] = "saveFile"; 52 53namespace file_system = extensions::api::file_system; 54namespace ChooseEntry = file_system::ChooseEntry; 55 56namespace { 57 58#if defined(OS_MACOSX) 59// Retrieves the localized display name for the base name of the given path. 60// If the path is not localized, this will just return the base name. 61std::string GetDisplayBaseName(const FilePath& path) { 62 base::mac::ScopedCFTypeRef<CFURLRef> url( 63 CFURLCreateFromFileSystemRepresentation( 64 NULL, 65 (const UInt8*)path.value().c_str(), 66 path.value().length(), 67 true)); 68 if (!url) 69 return path.BaseName().value(); 70 71 CFStringRef str; 72 if (LSCopyDisplayNameForURL(url, &str) != noErr) 73 return path.BaseName().value(); 74 75 std::string result(base::SysCFStringRefToUTF8(str)); 76 CFRelease(str); 77 return result; 78} 79 80// Prettifies |source_path| for OS X, by localizing every component of the 81// path. Additionally, if the path is inside the user's home directory, then 82// replace the home directory component with "~". 83FilePath PrettifyPath(const FilePath& source_path) { 84 FilePath home_path; 85 PathService::Get(base::DIR_HOME, &home_path); 86 DCHECK(source_path.IsAbsolute()); 87 88 // Break down the incoming path into components, and grab the display name 89 // for every component. This will match app bundles, ".localized" folders, 90 // and localized subfolders of the user's home directory. 91 // Don't grab the display name of the first component, i.e., "/", as it'll 92 // show up as the HDD name. 93 std::vector<FilePath::StringType> components; 94 source_path.GetComponents(&components); 95 FilePath display_path = FilePath(components[0]); 96 FilePath actual_path = display_path; 97 for (std::vector<FilePath::StringType>::iterator i = components.begin() + 1; 98 i != components.end(); ++i) { 99 actual_path = actual_path.Append(*i); 100 if (actual_path == home_path) { 101 display_path = FilePath("~"); 102 home_path = FilePath(); 103 continue; 104 } 105 std::string display = GetDisplayBaseName(actual_path); 106 display_path = display_path.Append(display); 107 } 108 DCHECK_EQ(actual_path.value(), source_path.value()); 109 return display_path; 110} 111#else // defined(OS_MACOSX) 112// Prettifies |source_path|, by replacing the user's home directory with "~" 113// (if applicable). 114FilePath PrettifyPath(const FilePath& source_path) { 115#if defined(OS_WIN) || defined(OS_POSIX) 116#if defined(OS_WIN) 117 int home_key = base::DIR_PROFILE; 118#elif defined(OS_POSIX) 119 int home_key = base::DIR_HOME; 120#endif 121 FilePath home_path; 122 FilePath display_path = FilePath::FromUTF8Unsafe("~"); 123 if (PathService::Get(home_key, &home_path) 124 && home_path.AppendRelativePath(source_path, &display_path)) 125 return display_path; 126#endif 127 return source_path; 128} 129#endif // defined(OS_MACOSX) 130 131bool g_skip_picker_for_test = false; 132FilePath* g_path_to_be_picked_for_test; 133 134bool GetFilePathOfFileEntry(const std::string& filesystem_name, 135 const std::string& filesystem_path, 136 const content::RenderViewHost* render_view_host, 137 FilePath* file_path, 138 std::string* error) { 139 std::string filesystem_id; 140 if (!fileapi::CrackIsolatedFileSystemName(filesystem_name, &filesystem_id)) { 141 *error = kInvalidParameters; 142 return false; 143 } 144 145 // Only return the display path if the process has read access to the 146 // filesystem. 147 content::ChildProcessSecurityPolicy* policy = 148 content::ChildProcessSecurityPolicy::GetInstance(); 149 if (!policy->CanReadFileSystem(render_view_host->GetProcess()->GetID(), 150 filesystem_id)) { 151 *error = kSecurityError; 152 return false; 153 } 154 155 IsolatedContext* context = IsolatedContext::GetInstance(); 156 FilePath relative_path = FilePath::FromUTF8Unsafe(filesystem_path); 157 FilePath virtual_path = context->CreateVirtualRootPath(filesystem_id) 158 .Append(relative_path); 159 if (!context->CrackIsolatedPath(virtual_path, 160 &filesystem_id, 161 NULL, 162 file_path)) { 163 *error = kInvalidParameters; 164 return false; 165 } 166 167 return true; 168} 169 170bool DoCheckWritableFile(const FilePath& path) { 171 // Don't allow links. 172 if (file_util::PathExists(path) && file_util::IsLink(path)) 173 return false; 174 175 // Create the file if it doesn't already exist. 176 base::PlatformFileError error = base::PLATFORM_FILE_OK; 177 int creation_flags = base::PLATFORM_FILE_CREATE | 178 base::PLATFORM_FILE_READ | 179 base::PLATFORM_FILE_WRITE; 180 base::CreatePlatformFile(path, creation_flags, NULL, &error); 181 return error == base::PLATFORM_FILE_OK || 182 error == base::PLATFORM_FILE_ERROR_EXISTS; 183} 184 185// Expand the mime-types and extensions provided in an AcceptOption, returning 186// them within the passed extension vector. Returns false if no valid types 187// were found. 188bool GetFileTypesFromAcceptOption( 189 const file_system::AcceptOption& accept_option, 190 std::vector<FilePath::StringType>* extensions, 191 string16* description) { 192 std::set<FilePath::StringType> extension_set; 193 int description_id = 0; 194 195 if (accept_option.mime_types.get()) { 196 std::vector<std::string>* list = accept_option.mime_types.get(); 197 bool valid_type = false; 198 for (std::vector<std::string>::const_iterator iter = list->begin(); 199 iter != list->end(); ++iter) { 200 std::vector<FilePath::StringType> inner; 201 std::string accept_type = *iter; 202 StringToLowerASCII(&accept_type); 203 net::GetExtensionsForMimeType(accept_type, &inner); 204 if (inner.empty()) 205 continue; 206 207 if (valid_type) 208 description_id = 0; // We already have an accept type with label; if 209 // we find another, give up and use the default. 210 else if (accept_type == "image/*") 211 description_id = IDS_IMAGE_FILES; 212 else if (accept_type == "audio/*") 213 description_id = IDS_AUDIO_FILES; 214 else if (accept_type == "video/*") 215 description_id = IDS_VIDEO_FILES; 216 217 extension_set.insert(inner.begin(), inner.end()); 218 valid_type = true; 219 } 220 } 221 222 if (accept_option.extensions.get()) { 223 std::vector<std::string>* list = accept_option.extensions.get(); 224 for (std::vector<std::string>::const_iterator iter = list->begin(); 225 iter != list->end(); ++iter) { 226 std::string extension = *iter; 227 StringToLowerASCII(&extension); 228#if defined(OS_WIN) 229 extension_set.insert(UTF8ToWide(*iter)); 230#else 231 extension_set.insert(*iter); 232#endif 233 } 234 } 235 236 extensions->assign(extension_set.begin(), extension_set.end()); 237 if (extensions->empty()) 238 return false; 239 240 if (accept_option.description.get()) 241 *description = UTF8ToUTF16(*accept_option.description.get()); 242 else if (description_id) 243 *description = l10n_util::GetStringUTF16(description_id); 244 245 return true; 246} 247 248} // namespace 249 250namespace extensions { 251 252bool FileSystemGetDisplayPathFunction::RunImpl() { 253 std::string filesystem_name; 254 std::string filesystem_path; 255 EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &filesystem_name)); 256 EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &filesystem_path)); 257 258 FilePath file_path; 259 if (!GetFilePathOfFileEntry(filesystem_name, filesystem_path, 260 render_view_host_, &file_path, &error_)) 261 return false; 262 263 file_path = PrettifyPath(file_path); 264 SetResult(base::Value::CreateStringValue(file_path.value())); 265 return true; 266} 267 268bool FileSystemEntryFunction::HasFileSystemWritePermission() { 269 const extensions::Extension* extension = GetExtension(); 270 if (!extension) 271 return false; 272 273 return extension->HasAPIPermission(APIPermission::kFileSystemWrite); 274} 275 276void FileSystemEntryFunction::CheckWritableFile(const FilePath& path) { 277 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 278 if (DoCheckWritableFile(path)) { 279 content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, 280 base::Bind(&FileSystemEntryFunction::RegisterFileSystemAndSendResponse, 281 this, path, WRITABLE)); 282 return; 283 } 284 285 content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, 286 base::Bind(&FileSystemEntryFunction::HandleWritableFileError, this)); 287} 288 289void FileSystemEntryFunction::RegisterFileSystemAndSendResponse( 290 const FilePath& path, EntryType entry_type) { 291 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 292 293 fileapi::IsolatedContext* isolated_context = 294 fileapi::IsolatedContext::GetInstance(); 295 DCHECK(isolated_context); 296 297 std::string registered_name; 298 std::string filesystem_id = isolated_context->RegisterFileSystemForPath( 299 fileapi::kFileSystemTypeNativeLocal, path, ®istered_name); 300 301 content::ChildProcessSecurityPolicy* policy = 302 content::ChildProcessSecurityPolicy::GetInstance(); 303 int renderer_id = render_view_host_->GetProcess()->GetID(); 304 if (entry_type == WRITABLE) 305 policy->GrantReadWriteFileSystem(renderer_id, filesystem_id); 306 else 307 policy->GrantReadFileSystem(renderer_id, filesystem_id); 308 309 // We only need file level access for reading FileEntries. Saving FileEntries 310 // just needs the file system to have read/write access, which is granted 311 // above if required. 312 if (!policy->CanReadFile(renderer_id, path)) 313 policy->GrantReadFile(renderer_id, path); 314 315 DictionaryValue* dict = new DictionaryValue(); 316 SetResult(dict); 317 dict->SetString("fileSystemId", filesystem_id); 318 dict->SetString("baseName", registered_name); 319 SendResponse(true); 320} 321 322void FileSystemEntryFunction::HandleWritableFileError() { 323 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 324 error_ = kWritableFileError; 325 SendResponse(false); 326} 327 328bool FileSystemGetWritableEntryFunction::RunImpl() { 329 std::string filesystem_name; 330 std::string filesystem_path; 331 EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &filesystem_name)); 332 EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &filesystem_path)); 333 334 if (!HasFileSystemWritePermission()) { 335 error_ = kRequiresFileSystemWriteError; 336 return false; 337 } 338 339 FilePath path; 340 if (!GetFilePathOfFileEntry(filesystem_name, filesystem_path, 341 render_view_host_, &path, &error_)) 342 return false; 343 344 content::BrowserThread::PostTask(content::BrowserThread::FILE, FROM_HERE, 345 base::Bind(&FileSystemGetWritableEntryFunction::CheckWritableFile, 346 this, path)); 347 return true; 348} 349 350bool FileSystemIsWritableEntryFunction::RunImpl() { 351 std::string filesystem_name; 352 std::string filesystem_path; 353 EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &filesystem_name)); 354 EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &filesystem_path)); 355 356 std::string filesystem_id; 357 if (!fileapi::CrackIsolatedFileSystemName(filesystem_name, &filesystem_id)) { 358 error_ = kInvalidParameters; 359 return false; 360 } 361 362 content::ChildProcessSecurityPolicy* policy = 363 content::ChildProcessSecurityPolicy::GetInstance(); 364 int renderer_id = render_view_host_->GetProcess()->GetID(); 365 bool is_writable = policy->CanReadWriteFileSystem(renderer_id, 366 filesystem_id); 367 368 SetResult(base::Value::CreateBooleanValue(is_writable)); 369 return true; 370} 371 372// Handles showing a dialog to the user to ask for the filename for a file to 373// save or open. 374class FileSystemChooseEntryFunction::FilePicker 375 : public ui::SelectFileDialog::Listener { 376 public: 377 FilePicker(FileSystemChooseEntryFunction* function, 378 content::WebContents* web_contents, 379 const FilePath& suggested_name, 380 const ui::SelectFileDialog::FileTypeInfo& file_type_info, 381 ui::SelectFileDialog::Type picker_type, 382 EntryType entry_type) 383 : suggested_name_(suggested_name), 384 entry_type_(entry_type), 385 function_(function) { 386 select_file_dialog_ = ui::SelectFileDialog::Create( 387 this, new ChromeSelectFilePolicy(web_contents)); 388 gfx::NativeWindow owning_window = web_contents ? 389 platform_util::GetTopLevel(web_contents->GetNativeView()) : NULL; 390 391 if (g_skip_picker_for_test) { 392 if (g_path_to_be_picked_for_test) { 393 content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, 394 base::Bind( 395 &FileSystemChooseEntryFunction::FilePicker::FileSelected, 396 base::Unretained(this), *g_path_to_be_picked_for_test, 1, 397 static_cast<void*>(NULL))); 398 } else { 399 content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, 400 base::Bind( 401 &FileSystemChooseEntryFunction::FilePicker:: 402 FileSelectionCanceled, 403 base::Unretained(this), static_cast<void*>(NULL))); 404 } 405 return; 406 } 407 408 select_file_dialog_->SelectFile(picker_type, 409 string16(), 410 suggested_name, 411 &file_type_info, 0, FILE_PATH_LITERAL(""), 412 owning_window, NULL); 413 } 414 415 virtual ~FilePicker() {} 416 417 private: 418 // ui::SelectFileDialog::Listener implementation. 419 virtual void FileSelected(const FilePath& path, 420 int index, 421 void* params) OVERRIDE { 422 function_->FileSelected(path, entry_type_); 423 delete this; 424 } 425 426 virtual void FileSelectionCanceled(void* params) OVERRIDE { 427 function_->FileSelectionCanceled(); 428 delete this; 429 } 430 431 FilePath suggested_name_; 432 433 EntryType entry_type_; 434 435 scoped_refptr<ui::SelectFileDialog> select_file_dialog_; 436 scoped_refptr<FileSystemChooseEntryFunction> function_; 437 438 DISALLOW_COPY_AND_ASSIGN(FilePicker); 439}; 440 441bool FileSystemChooseEntryFunction::ShowPicker( 442 const FilePath& suggested_name, 443 const ui::SelectFileDialog::FileTypeInfo& file_type_info, 444 ui::SelectFileDialog::Type picker_type, 445 EntryType entry_type) { 446 ShellWindowRegistry* registry = ShellWindowRegistry::Get(profile()); 447 DCHECK(registry); 448 ShellWindow* shell_window = registry->GetShellWindowForRenderViewHost( 449 render_view_host()); 450 if (!shell_window) { 451 error_ = kInvalidCallingPage; 452 return false; 453 } 454 455 // The file picker will hold a reference to this function instance, preventing 456 // its destruction (and subsequent sending of the function response) until the 457 // user has selected a file or cancelled the picker. At that point, the picker 458 // will delete itself, which will also free the function instance. 459 new FilePicker(this, shell_window->web_contents(), suggested_name, 460 file_type_info, picker_type, entry_type); 461 return true; 462} 463 464// static 465void FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest( 466 FilePath* path) { 467 g_skip_picker_for_test = true; 468 g_path_to_be_picked_for_test = path; 469} 470 471// static 472void FileSystemChooseEntryFunction::SkipPickerAndAlwaysCancelForTest() { 473 g_skip_picker_for_test = true; 474 g_path_to_be_picked_for_test = NULL; 475} 476 477// static 478void FileSystemChooseEntryFunction::StopSkippingPickerForTest() { 479 g_skip_picker_for_test = false; 480} 481 482void FileSystemChooseEntryFunction::FileSelected(const FilePath& path, 483 EntryType entry_type) { 484 if (entry_type == WRITABLE) { 485 content::BrowserThread::PostTask(content::BrowserThread::FILE, FROM_HERE, 486 base::Bind(&FileSystemChooseEntryFunction::CheckWritableFile, 487 this, path)); 488 return; 489 } 490 491 // Don't need to check the file, it's for reading. 492 RegisterFileSystemAndSendResponse(path, READ_ONLY); 493} 494 495void FileSystemChooseEntryFunction::FileSelectionCanceled() { 496 error_ = kUserCancelled; 497 SendResponse(false); 498} 499 500void FileSystemChooseEntryFunction::BuildFileTypeInfo( 501 ui::SelectFileDialog::FileTypeInfo* file_type_info, 502 const FilePath::StringType& suggested_extension, 503 const AcceptOptions* accepts, 504 const bool* acceptsAllTypes) { 505 file_type_info->include_all_files = true; 506 if (acceptsAllTypes) 507 file_type_info->include_all_files = *acceptsAllTypes; 508 509 bool need_suggestion = !file_type_info->include_all_files && 510 !suggested_extension.empty(); 511 512 if (accepts) { 513 typedef file_system::AcceptOption AcceptOption; 514 for (std::vector<linked_ptr<AcceptOption> >::const_iterator iter = 515 accepts->begin(); iter != accepts->end(); ++iter) { 516 string16 description; 517 std::vector<FilePath::StringType> extensions; 518 519 if (!GetFileTypesFromAcceptOption(**iter, &extensions, &description)) 520 continue; // No extensions were found. 521 522 file_type_info->extensions.push_back(extensions); 523 file_type_info->extension_description_overrides.push_back(description); 524 525 // If we still need to find suggested_extension, hunt for it inside the 526 // extensions returned from GetFileTypesFromAcceptOption. 527 if (need_suggestion && std::find(extensions.begin(), 528 extensions.end(), suggested_extension) != extensions.end()) { 529 need_suggestion = false; 530 } 531 } 532 } 533 534 // If there's nothing in our accepted extension list or we couldn't find the 535 // suggested extension required, then default to accepting all types. 536 if (file_type_info->extensions.empty() || need_suggestion) 537 file_type_info->include_all_files = true; 538} 539 540void FileSystemChooseEntryFunction::BuildSuggestion( 541 const std::string *opt_name, 542 FilePath* suggested_name, 543 FilePath::StringType* suggested_extension) { 544 if (opt_name) { 545 *suggested_name = FilePath::FromUTF8Unsafe(*opt_name); 546 547 // Don't allow any path components; shorten to the base name. This should 548 // result in a relative path, but in some cases may not. Clear the 549 // suggestion for safety if this is the case. 550 *suggested_name = suggested_name->BaseName(); 551 if (suggested_name->IsAbsolute()) 552 *suggested_name = FilePath(); 553 554 *suggested_extension = suggested_name->Extension(); 555 if (!suggested_extension->empty()) 556 suggested_extension->erase(suggested_extension->begin()); // drop the . 557 } 558} 559 560bool FileSystemChooseEntryFunction::RunImpl() { 561 scoped_ptr<ChooseEntry::Params> params(ChooseEntry::Params::Create(*args_)); 562 EXTENSION_FUNCTION_VALIDATE(params.get()); 563 564 FilePath suggested_name; 565 ui::SelectFileDialog::FileTypeInfo file_type_info; 566 EntryType entry_type = READ_ONLY; 567 ui::SelectFileDialog::Type picker_type = 568 ui::SelectFileDialog::SELECT_OPEN_FILE; 569 570 file_system::ChooseEntryOptions* options = params->options.get(); 571 if (options) { 572 if (options->type.get()) { 573 if (*options->type == kOpenWritableFileOption) { 574 entry_type = WRITABLE; 575 } else if (*options->type == kSaveFileOption) { 576 entry_type = WRITABLE; 577 picker_type = ui::SelectFileDialog::SELECT_SAVEAS_FILE; 578 } else if (*options->type != kOpenFileOption) { 579 error_ = kUnknownChooseEntryType; 580 return false; 581 } 582 } 583 584 FilePath::StringType suggested_extension; 585 BuildSuggestion(options->suggested_name.get(), &suggested_name, 586 &suggested_extension); 587 588 BuildFileTypeInfo(&file_type_info, suggested_extension, 589 options->accepts.get(), options->accepts_all_types.get()); 590 } 591 592 if (entry_type == WRITABLE && !HasFileSystemWritePermission()) { 593 error_ = kRequiresFileSystemWriteError; 594 return false; 595 } 596 597 return ShowPicker(suggested_name, file_type_info, picker_type, entry_type); 598} 599 600} // namespace extensions 601