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, &registered_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