| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/arc/fileapi/arc_select_files_handler.h" |
| |
| #include <utility> |
| |
| #include "ash/components/arc/arc_util.h" |
| #include "base/functional/bind.h" |
| #include "base/json/string_escape.h" |
| #include "base/logging.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "chrome/browser/ash/arc/arc_util.h" |
| #include "chrome/browser/ash/arc/fileapi/arc_content_file_system_url_util.h" |
| #include "chrome/browser/ash/arc/fileapi/arc_documents_provider_util.h" |
| #include "chrome/browser/ash/arc/fileapi/arc_select_files_util.h" |
| #include "chrome/browser/ash/file_manager/app_id.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/policy/dlp/dlp_files_controller.h" |
| #include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h" |
| #include "chrome/browser/ui/chrome_select_file_policy.h" |
| #include "chrome/browser/ui/views/select_file_dialog_extension.h" |
| #include "chrome/common/chrome_isolated_world_ids.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/common/url_constants.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/mime_util.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| #include "ui/aura/window.h" |
| #include "url/gurl.h" |
| |
| namespace arc { |
| |
| // Script for clicking OK button on the selector. |
| const char kScriptClickOk[] = |
| "(function() { document.querySelector('#ok-button').click(); })();"; |
| |
| // Script for clicking Cancel button on the selector. |
| const char kScriptClickCancel[] = |
| "(function() { document.querySelector('#cancel-button').click(); })();"; |
| |
| // Script for clicking a directory element in the left pane of the selector. |
| // %s should be replaced by the target directory name wrapped by double-quotes. |
| const char kScriptClickDirectory[] = |
| "(function() {" |
| " var dirs = document.querySelectorAll('#directory-tree .entry-name');" |
| " Array.from(dirs).filter(a => a.innerText === %s)[0].click();" |
| "})();"; |
| |
| // Script for clicking a file element in the right pane of the selector. |
| // %s should be replaced by the target file name wrapped by double-quotes. |
| const char kScriptClickFile[] = |
| "(function() {" |
| " var evt = document.createEvent('MouseEvents');" |
| " evt.initMouseEvent('mousedown', true, false);" |
| " var files = document.querySelectorAll('#file-list .file');" |
| " Array.from(files).filter(a => a.getAttribute('file-name') === %s)[0]" |
| " .dispatchEvent(evt);" |
| "})();"; |
| |
| // Script for querying UI elements (directories and files) shown on the selector. |
| const char kScriptGetElements[] = |
| "(function() {" |
| " var dirs = document.querySelectorAll('#directory-tree .entry-name');" |
| " var files = document.querySelectorAll('#file-list .file');" |
| " return {dirNames: Array.from(dirs, a => a.innerText)," |
| " fileNames: Array.from(files, a => a.getAttribute('file-name'))};" |
| "})();"; |
| |
| namespace { |
| |
| constexpr char kRecentAllFakePath[] = "/.fake-entry/recent/all"; |
| |
| void ConvertToElementVector( |
| const base::Value* list_value, |
| std::vector<mojom::FileSelectorElementPtr>* elements) { |
| if (!list_value || !list_value->is_list()) |
| return; |
| |
| for (const base::Value& value : list_value->GetList()) { |
| mojom::FileSelectorElementPtr element = mojom::FileSelectorElement::New(); |
| element->name = value.GetString(); |
| elements->push_back(std::move(element)); |
| } |
| } |
| |
| void OnGetElementsScriptResults( |
| mojom::FileSystemHost::GetFileSelectorElementsCallback callback, |
| base::Value value) { |
| mojom::FileSelectorElementsPtr result = mojom::FileSelectorElements::New(); |
| if (value.is_dict()) { |
| ConvertToElementVector(value.GetDict().Find("dirNames"), |
| &result->directory_elements); |
| ConvertToElementVector(value.GetDict().Find("fileNames"), |
| &result->file_elements); |
| // TODO(niwa): Fill result->search_query. |
| } |
| std::move(callback).Run(std::move(result)); |
| } |
| |
| void ContentUrlsResolved(mojom::FileSystemHost::SelectFilesCallback callback, |
| const std::vector<GURL>& content_urls) { |
| mojom::SelectFilesResultPtr result = mojom::SelectFilesResult::New(); |
| for (const GURL& content_url : content_urls) { |
| result->urls.push_back(content_url); |
| } |
| std::move(callback).Run(std::move(result)); |
| } |
| |
| ui::SelectFileDialog::Type GetDialogType( |
| const mojom::SelectFilesRequestPtr& request) { |
| switch (request->action_type) { |
| case mojom::SelectFilesActionType::GET_CONTENT: |
| case mojom::SelectFilesActionType::OPEN_DOCUMENT: |
| case mojom::SelectFilesActionType::OPEN_MEDIA_STORE_FILES: |
| return request->allow_multiple |
| ? ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE |
| : ui::SelectFileDialog::SELECT_OPEN_FILE; |
| case mojom::SelectFilesActionType::OPEN_DOCUMENT_TREE: |
| return ui::SelectFileDialog::SELECT_EXISTING_FOLDER; |
| case mojom::SelectFilesActionType::CREATE_DOCUMENT: |
| return ui::SelectFileDialog::SELECT_SAVEAS_FILE; |
| } |
| NOTREACHED(); |
| } |
| |
| base::FilePath GetInitialFilePath(const mojom::SelectFilesRequestPtr& request) { |
| const mojom::DocumentPathPtr& document_path = request->initial_document_path; |
| if (!document_path) |
| return base::FilePath(kRecentAllFakePath); |
| |
| if (document_path->path.empty()) { |
| LOG(ERROR) << "path should at least contain root Document ID."; |
| return base::FilePath(kRecentAllFakePath); |
| } |
| |
| const std::string& root_document_id = document_path->path[0]; |
| // TODO(niwa): Convert non-root document IDs to the relative path and append. |
| return arc::GetDocumentsProviderMountPath(document_path->authority, |
| root_document_id); |
| } |
| |
| void BuildFileTypeInfo(const mojom::SelectFilesRequestPtr& request, |
| ui::SelectFileDialog::FileTypeInfo* file_type_info) { |
| file_type_info->allowed_paths = ui::SelectFileDialog::FileTypeInfo::ANY_PATH; |
| for (const std::string& mime_type : request->mime_types) { |
| const std::vector<base::FilePath::StringType> extensions = |
| GetExtensionsForArcMimeType(mime_type); |
| if (!extensions.empty()) { |
| file_type_info->extensions.push_back(extensions); |
| } |
| |
| // Enable "Select from all files" option if GetExtensionsForArcMimeType |
| // can't find any matching extensions or specified MIME type contains an |
| // asterisk. This is to support extensions that are not covered by |
| // GetExtensionsForArcMimeType. (crbug.com/1034874) |
| if (extensions.empty() || |
| base::EndsWith(mime_type, "/*", base::CompareCase::SENSITIVE)) { |
| file_type_info->include_all_files = true; |
| } |
| } |
| } |
| |
| } // namespace |
| |
| ArcSelectFilesHandlersManager::ArcSelectFilesHandlersManager( |
| content::BrowserContext* context) |
| : context_(context) {} |
| |
| ArcSelectFilesHandlersManager::~ArcSelectFilesHandlersManager() = default; |
| |
| void ArcSelectFilesHandlersManager::SelectFiles( |
| const mojom::SelectFilesRequestPtr& request, |
| mojom::FileSystemHost::SelectFilesCallback callback) { |
| int task_id = request->task_id; |
| if (handlers_by_task_id_.find(task_id) != handlers_by_task_id_.end()) { |
| LOG(ERROR) << "SelectFileDialog is already shown for task ID : " << task_id; |
| std::move(callback).Run(mojom::SelectFilesResult::New()); |
| return; |
| } |
| |
| auto handler = std::make_unique<ArcSelectFilesHandler>(context_); |
| auto* handler_ptr = handler.get(); |
| handlers_by_task_id_.emplace(task_id, std::move(handler)); |
| |
| // Make sure that the handler is erased when the SelectFileDialog is closed. |
| handler_ptr->SelectFiles( |
| std::move(request), |
| base::BindOnce(&ArcSelectFilesHandlersManager::EraseHandlerAndRunCallback, |
| weak_ptr_factory_.GetWeakPtr(), task_id, |
| std::move(callback))); |
| } |
| |
| void ArcSelectFilesHandlersManager::OnFileSelectorEvent( |
| mojom::FileSelectorEventPtr event, |
| mojom::FileSystemHost::OnFileSelectorEventCallback callback) { |
| int task_id = event->creator_task_id; |
| auto iter = handlers_by_task_id_.find(task_id); |
| if (iter == handlers_by_task_id_.end()) { |
| LOG(ERROR) << "Can't find a SelectFileDialog for task ID : " << task_id; |
| std::move(callback).Run(); |
| return; |
| } |
| iter->second->OnFileSelectorEvent(std::move(event), std::move(callback)); |
| } |
| |
| void ArcSelectFilesHandlersManager::GetFileSelectorElements( |
| mojom::GetFileSelectorElementsRequestPtr request, |
| mojom::FileSystemHost::GetFileSelectorElementsCallback callback) { |
| int task_id = request->creator_task_id; |
| auto iter = handlers_by_task_id_.find(task_id); |
| if (iter == handlers_by_task_id_.end()) { |
| LOG(ERROR) << "Can't find a SelectFileDialog for task ID : " << task_id; |
| std::move(callback).Run(mojom::FileSelectorElements::New()); |
| return; |
| } |
| iter->second->GetFileSelectorElements(std::move(request), |
| std::move(callback)); |
| } |
| |
| void ArcSelectFilesHandlersManager::EraseHandlerAndRunCallback( |
| int task_id, |
| mojom::FileSystemHost::SelectFilesCallback callback, |
| mojom::SelectFilesResultPtr result) { |
| handlers_by_task_id_.erase(task_id); |
| std::move(callback).Run(std::move(result)); |
| } |
| |
| ArcSelectFilesHandler::ArcSelectFilesHandler(content::BrowserContext* context) |
| : profile_(Profile::FromBrowserContext(context)) { |
| dialog_holder_ = std::make_unique<SelectFileDialogHolder>(this); |
| } |
| |
| ArcSelectFilesHandler::~ArcSelectFilesHandler() { |
| // Make sure to close SelectFileDialog when the handler is destroyed. |
| dialog_holder_->ExecuteJavaScript(kScriptClickCancel, {}); |
| } |
| |
| void ArcSelectFilesHandler::SelectFiles( |
| const mojom::SelectFilesRequestPtr& request, |
| mojom::FileSystemHost::SelectFilesCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| callback_ = std::move(callback); |
| |
| // TODO(niwa): Convert all request options. |
| ui::SelectFileDialog::Type dialog_type = GetDialogType(request); |
| ui::SelectFileDialog::FileTypeInfo file_type_info; |
| BuildFileTypeInfo(request, &file_type_info); |
| base::FilePath default_path = GetInitialFilePath(request); |
| std::string search_query = request->search_query.value_or(std::string()); |
| |
| // Android picker apps should be shown in GET_CONTENT mode. |
| bool show_android_picker_apps = |
| request->action_type == mojom::SelectFilesActionType::GET_CONTENT; |
| |
| // In OPEN_MEDIA_STORE_FILES mode, only show volumes indexed in Android's |
| // MediaStore. |
| bool use_media_store_filter = |
| request->action_type == |
| mojom::SelectFilesActionType::OPEN_MEDIA_STORE_FILES; |
| |
| bool success = dialog_holder_->SelectFile( |
| dialog_type, default_path, &file_type_info, request->task_id, |
| search_query, show_android_picker_apps, use_media_store_filter); |
| if (!success) { |
| std::move(callback_).Run(mojom::SelectFilesResult::New()); |
| } |
| } |
| |
| void ArcSelectFilesHandler::FileSelected(const base::FilePath& path, |
| int index, |
| void* params) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(callback_); |
| |
| const std::string& activity = ConvertFilePathToAndroidActivity(path); |
| if (!activity.empty()) { |
| // The user selected an Android picker activity instead of a file. |
| mojom::SelectFilesResultPtr result = mojom::SelectFilesResult::New(); |
| result->picker_activity = activity; |
| std::move(callback_).Run(std::move(result)); |
| return; |
| } |
| |
| std::vector<base::FilePath> files; |
| files.push_back(path); |
| FilesSelectedInternal(files, params); |
| } |
| |
| void ArcSelectFilesHandler::MultiFilesSelected( |
| const std::vector<base::FilePath>& files, |
| void* params) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| FilesSelectedInternal(files, params); |
| } |
| |
| void ArcSelectFilesHandler::FileSelectionCanceled(void* params) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(callback_); |
| // Returns an empty result if the user cancels file selection. |
| std::move(callback_).Run(mojom::SelectFilesResult::New()); |
| } |
| |
| void ArcSelectFilesHandler::FilesSelectedInternal( |
| const std::vector<base::FilePath>& files, |
| void* params) { |
| DCHECK(callback_); |
| |
| storage::FileSystemContext* file_system_context = |
| file_manager::util::GetFileManagerFileSystemContext(profile_); |
| |
| std::vector<storage::FileSystemURL> file_system_urls; |
| for (const base::FilePath& file_path : files) { |
| GURL gurl; |
| file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl( |
| profile_, file_path, file_manager::util::GetFileManagerURL(), &gurl); |
| file_system_urls.push_back( |
| file_system_context->CrackURLInFirstPartyContext(gurl)); |
| } |
| |
| arc::ConvertToContentUrlsAndShare( |
| ProfileManager::GetPrimaryUserProfile(), file_system_urls, |
| base::BindOnce(&ContentUrlsResolved, std::move(callback_))); |
| } |
| |
| void ArcSelectFilesHandler::OnFileSelectorEvent( |
| mojom::FileSelectorEventPtr event, |
| mojom::FileSystemHost::OnFileSelectorEventCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| std::string quotedClickTargetName = |
| base::GetQuotedJSONString(event->click_target->name.c_str()); |
| std::string script; |
| switch (event->type) { |
| case mojom::FileSelectorEventType::CLICK_OK: |
| script = kScriptClickOk; |
| break; |
| case mojom::FileSelectorEventType::CLICK_CANCEL: |
| script = kScriptClickCancel; |
| break; |
| case mojom::FileSelectorEventType::CLICK_DIRECTORY: |
| script = base::StringPrintf(kScriptClickDirectory, |
| quotedClickTargetName.c_str()); |
| break; |
| case mojom::FileSelectorEventType::CLICK_FILE: |
| script = |
| base::StringPrintf(kScriptClickFile, quotedClickTargetName.c_str()); |
| break; |
| } |
| dialog_holder_->ExecuteJavaScript(script, {}); |
| |
| std::move(callback).Run(); |
| } |
| |
| void ArcSelectFilesHandler::GetFileSelectorElements( |
| mojom::GetFileSelectorElementsRequestPtr request, |
| mojom::FileSystemHost::GetFileSelectorElementsCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| dialog_holder_->ExecuteJavaScript( |
| kScriptGetElements, |
| base::BindOnce(&OnGetElementsScriptResults, std::move(callback))); |
| } |
| |
| void ArcSelectFilesHandler::SetDialogHolderForTesting( |
| std::unique_ptr<SelectFileDialogHolder> dialog_holder) { |
| dialog_holder_ = std::move(dialog_holder); |
| } |
| |
| SelectFileDialogHolder::SelectFileDialogHolder( |
| ui::SelectFileDialog::Listener* listener) { |
| select_file_dialog_ = static_cast<SelectFileDialogExtension*>( |
| ui::SelectFileDialog::Create(listener, nullptr).get()); |
| } |
| |
| SelectFileDialogHolder::~SelectFileDialogHolder() { |
| // select_file_dialog_ can be nullptr only in unit tests. |
| if (select_file_dialog_.get()) |
| select_file_dialog_->ListenerDestroyed(); |
| } |
| |
| bool SelectFileDialogHolder::SelectFile( |
| ui::SelectFileDialog::Type type, |
| const base::FilePath& default_path, |
| const ui::SelectFileDialog::FileTypeInfo* file_types, |
| int task_id, |
| const std::string& search_query, |
| bool show_android_picker_apps, |
| bool use_media_store_filter) { |
| aura::Window* owner_window = nullptr; |
| for (auto* window : ChromeShelfController::instance()->GetArcWindows()) { |
| if (arc::GetWindowTaskId(window) == task_id) { |
| owner_window = window; |
| break; |
| } |
| } |
| if (!owner_window) { |
| LOG(ERROR) << "Can't find the ARC window for task ID : " << task_id; |
| return false; |
| } |
| |
| SelectFileDialogExtension::Owner owner; |
| owner.window = owner_window; |
| owner.android_task_id = task_id; |
| owner.dialog_caller = policy::DlpFilesController::DlpFileDestination( |
| policy::DlpRulesManager::Component::kArc); |
| select_file_dialog_->SelectFileWithFileManagerParams( |
| type, |
| /*title=*/std::u16string(), default_path, file_types, |
| /*file_type_index=*/0, |
| /*params=*/nullptr, owner, search_query, show_android_picker_apps, |
| use_media_store_filter); |
| return true; |
| } |
| |
| void SelectFileDialogHolder::ExecuteJavaScript( |
| const std::string& script, |
| content::RenderFrameHost::JavaScriptResultCallback callback) { |
| content::RenderFrameHost* frame_host = |
| select_file_dialog_->GetPrimaryMainFrame(); |
| |
| if (!frame_host || !frame_host->IsRenderFrameLive()) { |
| LOG(ERROR) << "Can't execute a script. SelectFileDialog is not ready."; |
| if (callback) |
| std::move(callback).Run(base::Value()); |
| return; |
| } |
| |
| frame_host->ExecuteJavaScriptInIsolatedWorld( |
| base::UTF8ToUTF16(script), std::move(callback), |
| ISOLATED_WORLD_ID_CHROME_INTERNAL); |
| } |
| |
| } // namespace arc |