| // Copyright 2012 The Chromium Authors |
| // 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/containers/contains.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.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 kFallbackMimeType[] = "application/octet-stream"; |
| 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) { |
| std::set<std::string> file_extensions = |
| apps::GetFileExtensionsFromFileHandler(file_handler); |
| |
| 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_; |
| raw_ptr<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 nullptr; |
| |
| for (const auto& file_handler : *file_handlers) { |
| if (file_handler.id == handler_id) |
| return &file_handler; |
| } |
| return nullptr; |
| } |
| |
| 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 CreateFileEntryWithPermissions(int renderer_id, |
| const base::FilePath& path, |
| bool can_write, |
| bool can_create, |
| bool can_delete) { |
| 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 (can_create) { |
| DCHECK(can_write); |
| policy->GrantCreateReadWriteFileSystem(renderer_id, result.filesystem_id); |
| } else if (can_write) { |
| policy->GrantWriteFileSystem(renderer_id, result.filesystem_id); |
| } |
| if (can_delete) { |
| DCHECK(can_write); |
| policy->GrantDeleteFromFileSystem(renderer_id, result.filesystem_id); |
| } |
| |
| result.id = result.filesystem_id + ":" + result.registered_name; |
| return result; |
| } |
| |
| GrantedFileEntry CreateFileEntry(content::BrowserContext* /* context */, |
| const Extension* extension, |
| int renderer_id, |
| const base::FilePath& path, |
| bool is_directory) { |
| bool can_write = HasFileSystemWritePermission(extension); |
| return CreateFileEntryWithPermissions( |
| renderer_id, path, can_write, |
| /* can_create */ can_write && is_directory, |
| /* can_delete */ can_write && !is_directory); |
| } |
| |
| 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; |
| } |
| |
| std::vector<extensions::EntryInfo> CreateEntryInfos( |
| const std::vector<base::FilePath>& entry_paths, |
| const std::vector<std::string>& mime_types, |
| const std::set<base::FilePath>& directory_paths) { |
| CHECK_EQ(entry_paths.size(), mime_types.size()); |
| std::vector<extensions::EntryInfo> entry_infos; |
| for (size_t i = 0; i < entry_paths.size(); ++i) { |
| const std::string mime_type = |
| mime_types[i].empty() ? kFallbackMimeType : mime_types[i]; |
| bool is_directory = base::Contains(directory_paths, entry_paths[i]); |
| entry_infos.emplace_back(entry_paths[i], mime_type, is_directory); |
| } |
| return entry_infos; |
| } |
| |
| } // namespace app_file_handler_util |
| |
| } // namespace extensions |