|  | // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "extensions/browser/api/file_handlers/app_file_handler_util.h" | 
|  |  | 
|  | #include <set> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/files/file.h" | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/memory/scoped_refptr.h" | 
|  | #include "base/task/post_task.h" | 
|  | #include "base/task/task_traits.h" | 
|  | #include "base/task/thread_pool.h" | 
|  | #include "build/build_config.h" | 
|  | #include "build/chromeos_buildflags.h" | 
|  | #include "components/services/app_service/public/cpp/file_handler.h" | 
|  | #include "components/services/app_service/public/cpp/file_handler_info.h" | 
|  | #include "content/public/browser/browser_context.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "content/public/browser/child_process_security_policy.h" | 
|  | #include "extensions/browser/api/extensions_api_client.h" | 
|  | #include "extensions/browser/entry_info.h" | 
|  | #include "extensions/browser/extension_prefs.h" | 
|  | #include "extensions/browser/granted_file_entry.h" | 
|  | #include "extensions/common/permissions/permissions_data.h" | 
|  | #include "net/base/mime_util.h" | 
|  | #include "storage/browser/file_system/isolated_context.h" | 
|  | #include "storage/common/file_system/file_system_mount_option.h" | 
|  | #include "storage/common/file_system/file_system_types.h" | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS_ASH) | 
|  | #include "extensions/browser/api/file_handlers/non_native_file_system_delegate.h" | 
|  | #endif | 
|  |  | 
|  | namespace extensions { | 
|  |  | 
|  | namespace app_file_handler_util { | 
|  |  | 
|  | const char kInvalidParameters[] = "Invalid parameters"; | 
|  | const char kSecurityError[] = "Security error"; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | bool FileHandlerCanHandleFileWithExtension(const apps::FileHandlerInfo& handler, | 
|  | const base::FilePath& path) { | 
|  | for (auto extension = handler.extensions.cbegin(); | 
|  | extension != handler.extensions.cend(); ++extension) { | 
|  | if (*extension == "*") | 
|  | return true; | 
|  |  | 
|  | // Accept files whose extension or combined extension (e.g. ".tar.gz") | 
|  | // match the supported extensions of file handler. | 
|  | base::FilePath::StringType handler_extention( | 
|  | base::FilePath::kExtensionSeparator + | 
|  | base::FilePath::FromUTF8Unsafe(*extension).value()); | 
|  | if (base::FilePath::CompareEqualIgnoreCase(handler_extention, | 
|  | path.Extension()) || | 
|  | base::FilePath::CompareEqualIgnoreCase(handler_extention, | 
|  | path.FinalExtension())) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Also accept files with no extension for handlers that support an | 
|  | // empty extension, i.e. both "foo" and "foo." match. | 
|  | if (extension->empty() && | 
|  | path.MatchesExtension(base::FilePath::StringType())) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool FileHandlerCanHandleFileWithMimeType(const apps::FileHandlerInfo& handler, | 
|  | const std::string& mime_type) { | 
|  | for (auto type = handler.types.cbegin(); type != handler.types.cend(); | 
|  | ++type) { | 
|  | if (net::MatchesMimeType(*type, mime_type)) | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool WebAppFileHandlerCanHandleFileWithExtension( | 
|  | const apps::FileHandler& file_handler, | 
|  | const base::FilePath& path) { | 
|  | // Build a list of file extensions supported by the handler. | 
|  | // | 
|  | // TODO(crbug.com/938103): Duplicates functionality from | 
|  | // FileHandlerManager::GetFileExtensionsFromFileHandlers. | 
|  | std::set<std::string> file_extensions; | 
|  | for (const auto& accept_entry : file_handler.accept) | 
|  | file_extensions.insert(accept_entry.file_extensions.begin(), | 
|  | accept_entry.file_extensions.end()); | 
|  |  | 
|  | for (const auto& file_extension : file_extensions) { | 
|  | if (file_extension == "*") | 
|  | return true; | 
|  |  | 
|  | // Accept files whose extensions or combined extensions (e.g. ".tar.gz") | 
|  | // match the supported extensions of the file handler. | 
|  | base::FilePath::StringType file_extension_stringtype( | 
|  | base::FilePath::FromUTF8Unsafe(file_extension).value()); | 
|  | if (base::FilePath::CompareEqualIgnoreCase(file_extension_stringtype, | 
|  | path.Extension()) || | 
|  | base::FilePath::CompareEqualIgnoreCase(file_extension_stringtype, | 
|  | path.FinalExtension())) | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool WebAppFileHandlerCanHandleFileWithMimeType( | 
|  | const apps::FileHandler& file_handler, | 
|  | const std::string& mime_type) { | 
|  | for (const auto& accept_entry : file_handler.accept) { | 
|  | if (net::MatchesMimeType(accept_entry.mime_type, mime_type)) | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool PrepareNativeLocalFileForWritableApp(const base::FilePath& path, | 
|  | bool is_directory) { | 
|  | // Don't allow links. | 
|  | if (base::PathExists(path) && base::IsLink(path)) | 
|  | return false; | 
|  |  | 
|  | if (is_directory) | 
|  | return base::DirectoryExists(path); | 
|  |  | 
|  | // Create the file if it doesn't already exist. | 
|  | int creation_flags = base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ; | 
|  | base::File file(path, creation_flags); | 
|  |  | 
|  | return file.IsValid(); | 
|  | } | 
|  |  | 
|  | // Checks whether a list of paths are all OK for writing and calls a provided | 
|  | // on_success or on_failure callback when done. A path is OK for writing if it | 
|  | // is not a symlink, is not in a blocklisted path and can be opened for writing. | 
|  | // Creates files if they do not exist, but fails for non-existent directory | 
|  | // paths. On Chrome OS, also fails for non-local files that don't already exist. | 
|  | class WritableFileChecker | 
|  | : public base::RefCountedThreadSafe<WritableFileChecker> { | 
|  | public: | 
|  | WritableFileChecker( | 
|  | const std::vector<base::FilePath>& paths, | 
|  | content::BrowserContext* context, | 
|  | const std::set<base::FilePath>& directory_paths, | 
|  | base::OnceClosure on_success, | 
|  | base::OnceCallback<void(const base::FilePath&)> on_failure); | 
|  |  | 
|  | void Check(); | 
|  |  | 
|  | private: | 
|  | friend class base::RefCountedThreadSafe<WritableFileChecker>; | 
|  | virtual ~WritableFileChecker(); | 
|  |  | 
|  | // Called when a work item is completed. If all work items are done, this | 
|  | // calls the success or failure callback. | 
|  | void TaskDone(); | 
|  |  | 
|  | // Reports an error in completing a work item. This may be called more than | 
|  | // once, but only the last message will be retained. | 
|  | void Error(const base::FilePath& error_path); | 
|  |  | 
|  | void CheckLocalWritableFiles(); | 
|  |  | 
|  | // Called when processing a file is completed with either a success or an | 
|  | // error. | 
|  | void OnPrepareFileDone(const base::FilePath& path, bool success); | 
|  |  | 
|  | const std::vector<base::FilePath> paths_; | 
|  | content::BrowserContext* context_; | 
|  | const std::set<base::FilePath> directory_paths_; | 
|  | size_t outstanding_tasks_; | 
|  | base::FilePath error_path_; | 
|  | base::OnceClosure on_success_; | 
|  | base::OnceCallback<void(const base::FilePath&)> on_failure_; | 
|  | }; | 
|  |  | 
|  | WritableFileChecker::WritableFileChecker( | 
|  | const std::vector<base::FilePath>& paths, | 
|  | content::BrowserContext* context, | 
|  | const std::set<base::FilePath>& directory_paths, | 
|  | base::OnceClosure on_success, | 
|  | base::OnceCallback<void(const base::FilePath&)> on_failure) | 
|  | : paths_(paths), | 
|  | context_(context), | 
|  | directory_paths_(directory_paths), | 
|  | outstanding_tasks_(1), | 
|  | on_success_(std::move(on_success)), | 
|  | on_failure_(std::move(on_failure)) {} | 
|  |  | 
|  | void WritableFileChecker::Check() { | 
|  | outstanding_tasks_ = paths_.size(); | 
|  | for (const auto& path : paths_) { | 
|  | bool is_directory = directory_paths_.find(path) != directory_paths_.end(); | 
|  | #if BUILDFLAG(IS_CHROMEOS_ASH) | 
|  | NonNativeFileSystemDelegate* delegate = | 
|  | ExtensionsAPIClient::Get()->GetNonNativeFileSystemDelegate(); | 
|  | if (delegate && delegate->IsUnderNonNativeLocalPath(context_, path)) { | 
|  | if (is_directory) { | 
|  | delegate->IsNonNativeLocalPathDirectory( | 
|  | context_, path, | 
|  | base::BindOnce(&WritableFileChecker::OnPrepareFileDone, this, | 
|  | path)); | 
|  | } else { | 
|  | delegate->PrepareNonNativeLocalFileForWritableApp( | 
|  | context_, path, | 
|  | base::BindOnce(&WritableFileChecker::OnPrepareFileDone, this, | 
|  | path)); | 
|  | } | 
|  | continue; | 
|  | } | 
|  | #endif | 
|  | base::ThreadPool::PostTaskAndReplyWithResult( | 
|  | FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()}, | 
|  | base::BindOnce(&PrepareNativeLocalFileForWritableApp, path, | 
|  | is_directory), | 
|  | base::BindOnce(&WritableFileChecker::OnPrepareFileDone, this, path)); | 
|  | } | 
|  | } | 
|  |  | 
|  | WritableFileChecker::~WritableFileChecker() {} | 
|  |  | 
|  | void WritableFileChecker::TaskDone() { | 
|  | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | 
|  | if (--outstanding_tasks_ == 0) { | 
|  | if (error_path_.empty()) | 
|  | std::move(on_success_).Run(); | 
|  | else | 
|  | std::move(on_failure_).Run(error_path_); | 
|  | on_success_.Reset(); | 
|  | on_failure_.Reset(); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Reports an error in completing a work item. This may be called more than | 
|  | // once, but only the last message will be retained. | 
|  | void WritableFileChecker::Error(const base::FilePath& error_path) { | 
|  | DCHECK(!error_path.empty()); | 
|  | error_path_ = error_path; | 
|  | TaskDone(); | 
|  | } | 
|  |  | 
|  | void WritableFileChecker::OnPrepareFileDone(const base::FilePath& path, | 
|  | bool success) { | 
|  | if (success) | 
|  | TaskDone(); | 
|  | else | 
|  | Error(path); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | WebAppFileHandlerMatch::WebAppFileHandlerMatch( | 
|  | const apps::FileHandler* file_handler) | 
|  | : file_handler_(file_handler) {} | 
|  | WebAppFileHandlerMatch::~WebAppFileHandlerMatch() = default; | 
|  |  | 
|  | const apps::FileHandler& WebAppFileHandlerMatch::file_handler() const { | 
|  | return *file_handler_; | 
|  | } | 
|  |  | 
|  | bool WebAppFileHandlerMatch::matched_mime_type() const { | 
|  | return matched_mime_type_; | 
|  | } | 
|  |  | 
|  | bool WebAppFileHandlerMatch::matched_file_extension() const { | 
|  | return matched_file_extension_; | 
|  | } | 
|  |  | 
|  | bool WebAppFileHandlerMatch::DoMatch(const EntryInfo& entry) { | 
|  | // TODO(crbug.com/1060026): At the moment, apps::FileHandler doesn't have | 
|  | // an include_directories flag. It may be necessary to add one as this new | 
|  | // representation replaces apps::FileHandlerInfo. | 
|  | if (entry.is_directory) | 
|  | return false; | 
|  |  | 
|  | if (WebAppFileHandlerCanHandleFileWithMimeType(*file_handler_, | 
|  | entry.mime_type)) { | 
|  | matched_mime_type_ = true; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | if (WebAppFileHandlerCanHandleFileWithExtension(*file_handler_, entry.path)) { | 
|  | matched_file_extension_ = true; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return false; | 
|  | } | 
|  |  | 
|  | const apps::FileHandlerInfo* FileHandlerForId(const Extension& app, | 
|  | const std::string& handler_id) { | 
|  | const FileHandlersInfo* file_handlers = FileHandlers::GetFileHandlers(&app); | 
|  | if (!file_handlers) | 
|  | return NULL; | 
|  |  | 
|  | for (auto i = file_handlers->cbegin(); i != file_handlers->cend(); i++) { | 
|  | if (i->id == handler_id) | 
|  | return &*i; | 
|  | } | 
|  | return NULL; | 
|  | } | 
|  |  | 
|  | std::vector<FileHandlerMatch> FindFileHandlerMatchesForEntries( | 
|  | const Extension& app, | 
|  | const std::vector<EntryInfo>& entries) { | 
|  | if (entries.empty()) | 
|  | return std::vector<FileHandlerMatch>(); | 
|  |  | 
|  | // Look for file handlers which can handle all the MIME types | 
|  | // or file name extensions specified. | 
|  | const FileHandlersInfo* file_handlers = FileHandlers::GetFileHandlers(&app); | 
|  | if (!file_handlers) | 
|  | return std::vector<FileHandlerMatch>(); | 
|  |  | 
|  | return MatchesFromFileHandlersForEntries(*file_handlers, entries); | 
|  | } | 
|  |  | 
|  | std::vector<FileHandlerMatch> MatchesFromFileHandlersForEntries( | 
|  | const FileHandlersInfo& file_handlers, | 
|  | const std::vector<EntryInfo>& entries) { | 
|  | std::vector<FileHandlerMatch> matches; | 
|  |  | 
|  | for (const apps::FileHandlerInfo& handler : file_handlers) { | 
|  | bool handles_all_types = true; | 
|  | FileHandlerMatch match; | 
|  |  | 
|  | // Lifetime of the handler should be the same as usage of the matches | 
|  | // so the pointer shouldn't end up stale. | 
|  | match.handler = &handler; | 
|  | match.matched_mime = match.matched_file_extension = false; | 
|  | for (const auto& entry : entries) { | 
|  | if (entry.is_directory) { | 
|  | if (!handler.include_directories) { | 
|  | handles_all_types = false; | 
|  | break; | 
|  | } | 
|  | } else { | 
|  | match.matched_mime = | 
|  | FileHandlerCanHandleFileWithMimeType(handler, entry.mime_type); | 
|  | if (!match.matched_mime) { | 
|  | match.matched_file_extension = | 
|  | FileHandlerCanHandleFileWithExtension(handler, entry.path); | 
|  | if (!match.matched_file_extension) { | 
|  | handles_all_types = false; | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | if (handles_all_types) { | 
|  | matches.push_back(match); | 
|  | } | 
|  | } | 
|  | return matches; | 
|  | } | 
|  |  | 
|  | std::vector<WebAppFileHandlerMatch> MatchesFromWebAppFileHandlersForEntries( | 
|  | const apps::FileHandlers& file_handlers, | 
|  | const std::vector<EntryInfo>& entries) { | 
|  | std::vector<WebAppFileHandlerMatch> matches; | 
|  |  | 
|  | for (const auto& file_handler : file_handlers) { | 
|  | bool handles_all_types = true; | 
|  |  | 
|  | // The lifetime of the file handler should be the same as the usage of the | 
|  | // matches, so the pointer shouldn't end up stale. | 
|  | WebAppFileHandlerMatch match(&file_handler); | 
|  |  | 
|  | for (const auto& entry : entries) { | 
|  | if (!match.DoMatch(entry)) { | 
|  | handles_all_types = false; | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (handles_all_types) | 
|  | matches.push_back(match); | 
|  | } | 
|  |  | 
|  | return matches; | 
|  | } | 
|  |  | 
|  | bool FileHandlerCanHandleEntry(const apps::FileHandlerInfo& handler, | 
|  | const EntryInfo& entry) { | 
|  | if (entry.is_directory) | 
|  | return handler.include_directories; | 
|  |  | 
|  | return FileHandlerCanHandleFileWithMimeType(handler, entry.mime_type) || | 
|  | FileHandlerCanHandleFileWithExtension(handler, entry.path); | 
|  | } | 
|  |  | 
|  | bool WebAppFileHandlerCanHandleEntry(const apps::FileHandler& handler, | 
|  | const EntryInfo& entry) { | 
|  | // TODO(crbug.com/938103): At the moment, apps::FileHandler doesn't have an | 
|  | // include_directories flag. It may be necessary to add one as this new | 
|  | // representation replaces apps::FileHandlerInfo. | 
|  | if (entry.is_directory) | 
|  | return false; | 
|  |  | 
|  | return WebAppFileHandlerCanHandleFileWithMimeType(handler, entry.mime_type) || | 
|  | WebAppFileHandlerCanHandleFileWithExtension(handler, entry.path); | 
|  | } | 
|  |  | 
|  | GrantedFileEntry CreateFileEntry(content::BrowserContext* context, | 
|  | const Extension* extension, | 
|  | int renderer_id, | 
|  | const base::FilePath& path, | 
|  | bool is_directory) { | 
|  | GrantedFileEntry result; | 
|  | storage::IsolatedContext* isolated_context = | 
|  | storage::IsolatedContext::GetInstance(); | 
|  | DCHECK(isolated_context); | 
|  |  | 
|  | storage::IsolatedContext::ScopedFSHandle filesystem = | 
|  | isolated_context->RegisterFileSystemForPath( | 
|  | storage::kFileSystemTypeLocalForPlatformApp, std::string(), path, | 
|  | &result.registered_name); | 
|  | result.filesystem_id = filesystem.id(); | 
|  |  | 
|  | content::ChildProcessSecurityPolicy* policy = | 
|  | content::ChildProcessSecurityPolicy::GetInstance(); | 
|  | policy->GrantReadFileSystem(renderer_id, result.filesystem_id); | 
|  | if (HasFileSystemWritePermission(extension)) { | 
|  | if (is_directory) { | 
|  | policy->GrantCreateReadWriteFileSystem(renderer_id, result.filesystem_id); | 
|  | } else { | 
|  | policy->GrantWriteFileSystem(renderer_id, result.filesystem_id); | 
|  | policy->GrantDeleteFromFileSystem(renderer_id, result.filesystem_id); | 
|  | } | 
|  | } | 
|  |  | 
|  | result.id = result.filesystem_id + ":" + result.registered_name; | 
|  | return result; | 
|  | } | 
|  |  | 
|  | void PrepareFilesForWritableApp( | 
|  | const std::vector<base::FilePath>& paths, | 
|  | content::BrowserContext* context, | 
|  | const std::set<base::FilePath>& directory_paths, | 
|  | base::OnceClosure on_success, | 
|  | base::OnceCallback<void(const base::FilePath&)> on_failure) { | 
|  | auto checker = base::MakeRefCounted<WritableFileChecker>( | 
|  | paths, context, directory_paths, std::move(on_success), | 
|  | std::move(on_failure)); | 
|  | checker->Check(); | 
|  | } | 
|  |  | 
|  | bool HasFileSystemWritePermission(const Extension* extension) { | 
|  | return extension->permissions_data()->HasAPIPermission( | 
|  | mojom::APIPermissionID::kFileSystemWrite); | 
|  | } | 
|  |  | 
|  | bool ValidateFileEntryAndGetPath(const std::string& filesystem_name, | 
|  | const std::string& filesystem_path, | 
|  | int render_process_id, | 
|  | base::FilePath* file_path, | 
|  | std::string* error) { | 
|  | if (filesystem_path.empty()) { | 
|  | *error = kInvalidParameters; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | std::string filesystem_id; | 
|  | if (!storage::CrackIsolatedFileSystemName(filesystem_name, &filesystem_id)) { | 
|  | *error = kInvalidParameters; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Only return the display path if the process has read access to the | 
|  | // filesystem. | 
|  | content::ChildProcessSecurityPolicy* policy = | 
|  | content::ChildProcessSecurityPolicy::GetInstance(); | 
|  | if (!policy->CanReadFileSystem(render_process_id, filesystem_id)) { | 
|  | *error = kSecurityError; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | storage::IsolatedContext* context = storage::IsolatedContext::GetInstance(); | 
|  | base::FilePath relative_path = | 
|  | base::FilePath::FromUTF8Unsafe(filesystem_path); | 
|  | base::FilePath virtual_path = | 
|  | context->CreateVirtualRootPath(filesystem_id).Append(relative_path); | 
|  | storage::FileSystemType type; | 
|  | storage::FileSystemMountOption mount_option; | 
|  | std::string cracked_id; | 
|  | if (!context->CrackVirtualPath(virtual_path, &filesystem_id, &type, | 
|  | &cracked_id, file_path, &mount_option)) { | 
|  | *error = kInvalidParameters; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // The file system API is only intended to operate on file entries that | 
|  | // correspond to a native file, selected by the user so only allow file | 
|  | // systems returned by the file system API or from a drag and drop operation. | 
|  | if (type != storage::kFileSystemTypeLocalForPlatformApp && | 
|  | type != storage::kFileSystemTypeDragged) { | 
|  | *error = kInvalidParameters; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | }  // namespace app_file_handler_util | 
|  |  | 
|  | }  // namespace extensions |