Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 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 | // The file contains the implementation of |
| 6 | // fileBrowserHandlerInternal.selectFile extension function. |
| 7 | // When invoked, the function does the following: |
| 8 | // - Verifies that the extension function was invoked as a result of user |
| 9 | // gesture. |
| 10 | // - Display 'save as' dialog using FileSelectorImpl which waits for the user |
| 11 | // feedback. |
| 12 | // - Once the user selects the file path (or cancels the selection), |
| 13 | // FileSelectorImpl notifies FileBrowserHandlerInternalSelectFileFunction of |
| 14 | // the selection result by calling FileHandlerSelectFile::OnFilePathSelected. |
| 15 | // - If the selection was canceled, |
| 16 | // FileBrowserHandlerInternalSelectFileFunction returns reporting failure. |
| 17 | // - If the file path was selected, the function opens external file system |
| 18 | // needed to create FileEntry object for the selected path |
| 19 | // (opening file system will create file system name and root url for the |
| 20 | // caller's external file system). |
| 21 | // - The function grants permissions needed to read/write/create file under the |
| 22 | // selected path. To grant permissions to the caller, caller's extension ID |
| 23 | // has to be allowed to access the files virtual path (e.g. /Downloads/foo) |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 24 | // in ExternalFileSystemBackend. Additionally, the callers render |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 25 | // process ID has to be granted read, write and create permissions for the |
| 26 | // selected file's full filesystem path (e.g. |
| 27 | // /home/chronos/user/Downloads/foo) in ChildProcessSecurityPolicy. |
| 28 | // - After the required file access permissions are granted, result object is |
| 29 | // created and returned back. |
| 30 | |
| 31 | #include "chrome/browser/chromeos/extensions/file_manager/file_browser_handler_api.h" |
| 32 | |
| 33 | #include "base/bind.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 34 | #include "base/files/file_path.h" |
| 35 | #include "base/memory/scoped_ptr.h" |
Torne (Richard Coles) | b2df76e | 2013-05-13 16:52:09 +0100 | [diff] [blame] | 36 | #include "base/message_loop/message_loop_proxy.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 37 | #include "base/platform_file.h" |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 38 | #include "chrome/browser/chromeos/extensions/file_manager/file_tasks.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 39 | #include "chrome/browser/profiles/profile.h" |
| 40 | #include "chrome/browser/ui/browser.h" |
| 41 | #include "chrome/browser/ui/browser_window.h" |
| 42 | #include "chrome/browser/ui/chrome_select_file_policy.h" |
| 43 | #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| 44 | #include "chrome/common/extensions/api/file_browser_handler_internal.h" |
| 45 | #include "content/public/browser/browser_thread.h" |
| 46 | #include "content/public/browser/child_process_security_policy.h" |
| 47 | #include "content/public/browser/render_process_host.h" |
| 48 | #include "content/public/browser/render_view_host.h" |
| 49 | #include "content/public/browser/storage_partition.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 50 | #include "ui/shell_dialogs/select_file_dialog.h" |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 51 | #include "webkit/browser/fileapi/file_system_backend.h" |
Torne (Richard Coles) | 90dce4d | 2013-05-29 14:40:03 +0100 | [diff] [blame] | 52 | #include "webkit/browser/fileapi/file_system_context.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 53 | |
| 54 | using content::BrowserContext; |
| 55 | using content::BrowserThread; |
| 56 | using extensions::api::file_browser_handler_internal::FileEntryInfo; |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 57 | using file_manager::FileSelector; |
| 58 | using file_manager::FileSelectorFactory; |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 59 | |
| 60 | namespace SelectFile = |
| 61 | extensions::api::file_browser_handler_internal::SelectFile; |
| 62 | |
| 63 | namespace { |
| 64 | |
| 65 | const char kNoUserGestureError[] = |
| 66 | "This method can only be called in response to user gesture, such as a " |
| 67 | "mouse click or key press."; |
| 68 | |
| 69 | // Converts file extensions to a ui::SelectFileDialog::FileTypeInfo. |
| 70 | ui::SelectFileDialog::FileTypeInfo ConvertExtensionsToFileTypeInfo( |
| 71 | const std::vector<std::string>& extensions) { |
| 72 | ui::SelectFileDialog::FileTypeInfo file_type_info; |
| 73 | |
| 74 | for (size_t i = 0; i < extensions.size(); ++i) { |
| 75 | base::FilePath::StringType allowed_extension = |
| 76 | base::FilePath::FromUTF8Unsafe(extensions[i]).value(); |
| 77 | |
| 78 | // FileTypeInfo takes a nested vector like [["htm", "html"], ["txt"]] to |
| 79 | // group equivalent extensions, but we don't use this feature here. |
| 80 | std::vector<base::FilePath::StringType> inner_vector; |
| 81 | inner_vector.push_back(allowed_extension); |
| 82 | file_type_info.extensions.push_back(inner_vector); |
| 83 | } |
| 84 | |
| 85 | return file_type_info; |
| 86 | } |
| 87 | |
| 88 | // File selector implementation. |
| 89 | // When |SelectFile| is invoked, it will show save as dialog and listen for user |
| 90 | // action. When user selects the file (or closes the dialog), the function's |
| 91 | // |OnFilePathSelected| method will be called with the result. |
| 92 | // SelectFile should be called only once, because the class instance takes |
| 93 | // ownership of itself after the first call. It will delete itself after the |
| 94 | // extension function is notified of file selection result. |
| 95 | // Since the extension function object is ref counted, FileSelectorImpl holds |
| 96 | // a reference to it to ensure that the extension function doesn't go away while |
| 97 | // waiting for user action. The reference is released after the function is |
| 98 | // notified of the selection result. |
| 99 | class FileSelectorImpl : public FileSelector, |
| 100 | public ui::SelectFileDialog::Listener { |
| 101 | public: |
| 102 | explicit FileSelectorImpl(); |
| 103 | virtual ~FileSelectorImpl() OVERRIDE; |
| 104 | |
| 105 | protected: |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 106 | // file_manager::FileSelectr overrides. |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 107 | // Shows save as dialog with suggested name in window bound to |browser|. |
| 108 | // |allowed_extensions| specifies the file extensions allowed to be shown, |
| 109 | // and selected. Extensions should not include '.'. |
| 110 | // |
| 111 | // After this method is called, the selector implementation should not be |
| 112 | // deleted by the caller. It will delete itself after it receives response |
| 113 | // from SelectFielDialog. |
| 114 | virtual void SelectFile( |
| 115 | const base::FilePath& suggested_name, |
| 116 | const std::vector<std::string>& allowed_extensions, |
| 117 | Browser* browser, |
| 118 | FileBrowserHandlerInternalSelectFileFunction* function) OVERRIDE; |
| 119 | |
| 120 | // ui::SelectFileDialog::Listener overrides. |
| 121 | virtual void FileSelected(const base::FilePath& path, |
| 122 | int index, |
| 123 | void* params) OVERRIDE; |
| 124 | virtual void MultiFilesSelected(const std::vector<base::FilePath>& files, |
| 125 | void* params) OVERRIDE; |
| 126 | virtual void FileSelectionCanceled(void* params) OVERRIDE; |
| 127 | |
| 128 | private: |
| 129 | // Initiates and shows 'save as' dialog which will be used to prompt user to |
| 130 | // select a file path. The initial selected file name in the dialog will be |
| 131 | // set to |suggested_name|. The dialog will be bound to the tab active in |
| 132 | // |browser|. |
| 133 | // |allowed_extensions| specifies the file extensions allowed to be shown, |
| 134 | // and selected. Extensions should not include '.'. |
| 135 | // |
| 136 | // Returns boolean indicating whether the dialog has been successfully shown |
| 137 | // to the user. |
| 138 | bool StartSelectFile(const base::FilePath& suggested_name, |
| 139 | const std::vector<std::string>& allowed_extensions, |
| 140 | Browser* browser); |
| 141 | |
| 142 | // Reacts to the user action reported by the dialog and notifies |function_| |
| 143 | // about file selection result (by calling |OnFilePathSelected()|). |
| 144 | // The |this| object is self destruct after the function is notified. |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 145 | // |success| indicates whether user has selected the file. |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 146 | // |selected_path| is path that was selected. It is empty if the file wasn't |
| 147 | // selected. |
| 148 | void SendResponse(bool success, const base::FilePath& selected_path); |
| 149 | |
| 150 | // Dialog that is shown by selector. |
| 151 | scoped_refptr<ui::SelectFileDialog> dialog_; |
| 152 | |
| 153 | // Extension function that uses the selector. |
| 154 | scoped_refptr<FileBrowserHandlerInternalSelectFileFunction> function_; |
| 155 | |
| 156 | DISALLOW_COPY_AND_ASSIGN(FileSelectorImpl); |
| 157 | }; |
| 158 | |
| 159 | FileSelectorImpl::FileSelectorImpl() {} |
| 160 | |
| 161 | FileSelectorImpl::~FileSelectorImpl() { |
| 162 | if (dialog_.get()) |
| 163 | dialog_->ListenerDestroyed(); |
| 164 | // Send response if needed. |
Torne (Richard Coles) | 7d4cd47 | 2013-06-19 11:58:07 +0100 | [diff] [blame] | 165 | if (function_.get()) |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 166 | SendResponse(false, base::FilePath()); |
| 167 | } |
| 168 | |
| 169 | void FileSelectorImpl::SelectFile( |
| 170 | const base::FilePath& suggested_name, |
| 171 | const std::vector<std::string>& allowed_extensions, |
| 172 | Browser* browser, |
| 173 | FileBrowserHandlerInternalSelectFileFunction* function) { |
| 174 | // We will hold reference to the function until it is notified of selection |
| 175 | // result. |
| 176 | function_ = function; |
| 177 | |
| 178 | if (!StartSelectFile(suggested_name, allowed_extensions, browser)) { |
| 179 | // If the dialog wasn't launched, let's asynchronously report failure to the |
| 180 | // function. |
| 181 | base::MessageLoopProxy::current()->PostTask(FROM_HERE, |
| 182 | base::Bind(&FileSelectorImpl::FileSelectionCanceled, |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 183 | base::Unretained(this), static_cast<void*>(NULL))); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 184 | } |
| 185 | } |
| 186 | |
| 187 | bool FileSelectorImpl::StartSelectFile( |
| 188 | const base::FilePath& suggested_name, |
| 189 | const std::vector<std::string>& allowed_extensions, |
| 190 | Browser* browser) { |
| 191 | DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 192 | DCHECK(!dialog_.get()); |
| 193 | DCHECK(browser); |
| 194 | |
| 195 | if (!browser->window()) |
| 196 | return false; |
| 197 | |
| 198 | content::WebContents* web_contents = |
| 199 | browser->tab_strip_model()->GetActiveWebContents(); |
| 200 | if (!web_contents) |
| 201 | return false; |
| 202 | |
| 203 | dialog_ = ui::SelectFileDialog::Create( |
| 204 | this, new ChromeSelectFilePolicy(web_contents)); |
| 205 | |
| 206 | // Convert |allowed_extensions| to ui::SelectFileDialog::FileTypeInfo. |
| 207 | ui::SelectFileDialog::FileTypeInfo allowed_file_info = |
| 208 | ConvertExtensionsToFileTypeInfo(allowed_extensions); |
| 209 | allowed_file_info.support_drive = true; |
| 210 | |
| 211 | dialog_->SelectFile(ui::SelectFileDialog::SELECT_SAVEAS_FILE, |
| 212 | string16() /* dialog title*/, |
| 213 | suggested_name, |
| 214 | &allowed_file_info, |
| 215 | 0 /* file type index */, |
| 216 | std::string() /* default file extension */, |
| 217 | browser->window()->GetNativeWindow(), NULL /* params */); |
| 218 | |
| 219 | return dialog_->IsRunning(browser->window()->GetNativeWindow()); |
| 220 | } |
| 221 | |
| 222 | void FileSelectorImpl::FileSelected( |
| 223 | const base::FilePath& path, int index, void* params) { |
| 224 | SendResponse(true, path); |
| 225 | delete this; |
| 226 | } |
| 227 | |
| 228 | void FileSelectorImpl::MultiFilesSelected( |
| 229 | const std::vector<base::FilePath>& files, |
| 230 | void* params) { |
| 231 | // Only single file should be selected in save-as dialog. |
| 232 | NOTREACHED(); |
| 233 | } |
| 234 | |
| 235 | void FileSelectorImpl::FileSelectionCanceled( |
| 236 | void* params) { |
| 237 | SendResponse(false, base::FilePath()); |
| 238 | delete this; |
| 239 | } |
| 240 | |
| 241 | void FileSelectorImpl::SendResponse(bool success, |
| 242 | const base::FilePath& selected_path) { |
| 243 | DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 244 | |
| 245 | // We don't want to send multiple responses. |
| 246 | if (function_.get()) |
| 247 | function_->OnFilePathSelected(success, selected_path); |
| 248 | function_ = NULL; |
| 249 | } |
| 250 | |
| 251 | // FileSelectorFactory implementation. |
| 252 | class FileSelectorFactoryImpl : public FileSelectorFactory { |
| 253 | public: |
| 254 | FileSelectorFactoryImpl() {} |
| 255 | virtual ~FileSelectorFactoryImpl() {} |
| 256 | |
| 257 | // FileSelectorFactory implementation. |
| 258 | // Creates new FileSelectorImplementation for the function. |
| 259 | virtual FileSelector* CreateFileSelector() const OVERRIDE { |
| 260 | return new FileSelectorImpl(); |
| 261 | } |
| 262 | |
| 263 | private: |
| 264 | DISALLOW_COPY_AND_ASSIGN(FileSelectorFactoryImpl); |
| 265 | }; |
| 266 | |
| 267 | typedef base::Callback<void (bool success, |
| 268 | const std::string& file_system_name, |
| 269 | const GURL& file_system_root)> |
| 270 | FileSystemOpenCallback; |
| 271 | |
| 272 | // Relays callback from file system open operation by translating file error |
| 273 | // returned by the operation to success boolean. |
| 274 | void RunOpenFileSystemCallback( |
| 275 | const FileSystemOpenCallback& callback, |
| 276 | base::PlatformFileError error, |
| 277 | const std::string& file_system_name, |
| 278 | const GURL& file_system_root) { |
| 279 | bool success = (error == base::PLATFORM_FILE_OK); |
| 280 | callback.Run(success, file_system_name, file_system_root); |
| 281 | } |
| 282 | |
| 283 | } // namespace |
| 284 | |
| 285 | FileBrowserHandlerInternalSelectFileFunction:: |
| 286 | FileBrowserHandlerInternalSelectFileFunction() |
| 287 | : file_selector_factory_(new FileSelectorFactoryImpl()), |
| 288 | user_gesture_check_enabled_(true) { |
| 289 | } |
| 290 | |
| 291 | FileBrowserHandlerInternalSelectFileFunction:: |
| 292 | FileBrowserHandlerInternalSelectFileFunction( |
| 293 | FileSelectorFactory* file_selector_factory, |
| 294 | bool enable_user_gesture_check) |
| 295 | : file_selector_factory_(file_selector_factory), |
| 296 | user_gesture_check_enabled_(enable_user_gesture_check) { |
| 297 | DCHECK(file_selector_factory); |
| 298 | } |
| 299 | |
| 300 | FileBrowserHandlerInternalSelectFileFunction:: |
| 301 | ~FileBrowserHandlerInternalSelectFileFunction() {} |
| 302 | |
| 303 | bool FileBrowserHandlerInternalSelectFileFunction::RunImpl() { |
| 304 | scoped_ptr<SelectFile::Params> params(SelectFile::Params::Create(*args_)); |
| 305 | |
| 306 | base::FilePath suggested_name(params->selection_params.suggested_name); |
| 307 | std::vector<std::string> allowed_extensions; |
| 308 | if (params->selection_params.allowed_file_extensions.get()) |
| 309 | allowed_extensions = *params->selection_params.allowed_file_extensions; |
| 310 | |
| 311 | if (!user_gesture() && user_gesture_check_enabled_) { |
| 312 | error_ = kNoUserGestureError; |
| 313 | return false; |
| 314 | } |
| 315 | |
| 316 | FileSelector* file_selector = file_selector_factory_->CreateFileSelector(); |
| 317 | file_selector->SelectFile(suggested_name.BaseName(), |
| 318 | allowed_extensions, |
| 319 | GetCurrentBrowser(), |
| 320 | this); |
| 321 | return true; |
| 322 | } |
| 323 | |
| 324 | void FileBrowserHandlerInternalSelectFileFunction::OnFilePathSelected( |
| 325 | bool success, |
| 326 | const base::FilePath& full_path) { |
| 327 | if (!success) { |
| 328 | Respond(false); |
| 329 | return; |
| 330 | } |
| 331 | |
| 332 | full_path_ = full_path; |
| 333 | |
| 334 | // We have to open file system in order to create a FileEntry object for the |
| 335 | // selected file path. |
| 336 | content::SiteInstance* site_instance = render_view_host()->GetSiteInstance(); |
| 337 | BrowserContext::GetStoragePartition(profile_, site_instance)-> |
| 338 | GetFileSystemContext()->OpenFileSystem( |
Torne (Richard Coles) | 90dce4d | 2013-05-29 14:40:03 +0100 | [diff] [blame] | 339 | source_url_.GetOrigin(), fileapi::kFileSystemTypeExternal, |
| 340 | fileapi::OPEN_FILE_SYSTEM_FAIL_IF_NONEXISTENT, |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 341 | base::Bind( |
| 342 | &RunOpenFileSystemCallback, |
| 343 | base::Bind(&FileBrowserHandlerInternalSelectFileFunction:: |
| 344 | OnFileSystemOpened, |
| 345 | this))); |
| 346 | }; |
| 347 | |
| 348 | void FileBrowserHandlerInternalSelectFileFunction::OnFileSystemOpened( |
| 349 | bool success, |
| 350 | const std::string& file_system_name, |
| 351 | const GURL& file_system_root) { |
| 352 | DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 353 | |
| 354 | if (!success) { |
| 355 | Respond(false); |
| 356 | return; |
| 357 | } |
| 358 | |
| 359 | // Remember opened file system's parameters. |
| 360 | file_system_name_ = file_system_name; |
| 361 | file_system_root_ = file_system_root; |
| 362 | |
| 363 | GrantPermissions(); |
| 364 | |
| 365 | Respond(true); |
| 366 | } |
| 367 | |
| 368 | void FileBrowserHandlerInternalSelectFileFunction::GrantPermissions() { |
| 369 | content::SiteInstance* site_instance = render_view_host()->GetSiteInstance(); |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 370 | fileapi::ExternalFileSystemBackend* external_backend = |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 371 | BrowserContext::GetStoragePartition(profile_, site_instance)-> |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 372 | GetFileSystemContext()->external_backend(); |
| 373 | DCHECK(external_backend); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 374 | |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 375 | external_backend->GetVirtualPath(full_path_, &virtual_path_); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 376 | DCHECK(!virtual_path_.empty()); |
| 377 | |
| 378 | // Grant access to this particular file to target extension. This will |
| 379 | // ensure that the target extension can access only this FS entry and |
| 380 | // prevent from traversing FS hierarchy upward. |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 381 | external_backend->GrantFileAccessToExtension(extension_id(), virtual_path_); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 382 | |
| 383 | // Grant access to the selected file to target extensions render view process. |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 384 | content::ChildProcessSecurityPolicy::GetInstance()->GrantCreateReadWriteFile( |
| 385 | render_view_host()->GetProcess()->GetID(), full_path_); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 386 | } |
| 387 | |
| 388 | void FileBrowserHandlerInternalSelectFileFunction::Respond(bool success) { |
| 389 | scoped_ptr<SelectFile::Results::Result> result( |
| 390 | new SelectFile::Results::Result()); |
| 391 | result->success = success; |
| 392 | |
| 393 | // If the file was selected, add 'entry' object which will be later used to |
| 394 | // create a FileEntry instance for the selected file. |
| 395 | if (success) { |
| 396 | result->entry.reset(new FileEntryInfo()); |
| 397 | result->entry->file_system_name = file_system_name_; |
| 398 | result->entry->file_system_root = file_system_root_.spec(); |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 399 | result->entry->file_full_path = "/" + virtual_path_.AsUTF8Unsafe(); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 400 | result->entry->file_is_directory = false; |
| 401 | } |
| 402 | |
| 403 | results_ = SelectFile::Results::Create(*result); |
| 404 | SendResponse(true); |
| 405 | } |