1 // Copyright 2018 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/chromeos/arc/fileapi/arc_select_files_handler.h"
6
7 #include <utility>
8
9 #include "base/bind.h"
10 #include "base/json/string_escape.h"
11 #include "base/logging.h"
12 #include "base/strings/stringprintf.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/chromeos/arc/fileapi/arc_content_file_system_url_util.h"
15 #include "chrome/browser/chromeos/arc/fileapi/arc_documents_provider_util.h"
16 #include "chrome/browser/chromeos/arc/fileapi/arc_select_files_util.h"
17 #include "chrome/browser/chromeos/file_manager/app_id.h"
18 #include "chrome/browser/chromeos/file_manager/fileapi_util.h"
19 #include "chrome/browser/chromeos/file_manager/path_util.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
22 #include "chrome/browser/ui/chrome_select_file_policy.h"
23 #include "chrome/browser/ui/views/select_file_dialog_extension.h"
24 #include "chrome/common/chrome_isolated_world_ids.h"
25 #include "components/arc/arc_util.h"
26 #include "content/public/browser/browser_thread.h"
27 #include "content/public/browser/render_frame_host.h"
28 #include "content/public/browser/render_view_host.h"
29 #include "content/public/common/url_constants.h"
30 #include "net/base/filename_util.h"
31 #include "net/base/mime_util.h"
32 #include "storage/browser/file_system/file_system_context.h"
33 #include "storage/browser/file_system/file_system_url.h"
34 #include "ui/aura/window.h"
35 #include "url/gurl.h"
36
37 namespace arc {
38
39 // Script for clicking OK button on the selector.
40 const char kScriptClickOk[] =
41 "(function() { document.querySelector('#ok-button').click(); })();";
42
43 // Script for clicking Cancel button on the selector.
44 const char kScriptClickCancel[] =
45 "(function() { document.querySelector('#cancel-button').click(); })();";
46
47 // Script for clicking a directory element in the left pane of the selector.
48 // %s should be replaced by the target directory name wrapped by double-quotes.
49 const char kScriptClickDirectory[] =
50 "(function() {"
51 " var dirs = document.querySelectorAll('#directory-tree .entry-name');"
52 " Array.from(dirs).filter(a => a.innerText === %s)[0].click();"
53 "})();";
54
55 // Script for clicking a file element in the right pane of the selector.
56 // %s should be replaced by the target file name wrapped by double-quotes.
57 const char kScriptClickFile[] =
58 "(function() {"
59 " var evt = document.createEvent('MouseEvents');"
60 " evt.initMouseEvent('mousedown', true, false);"
61 " var files = document.querySelectorAll('#file-list .file');"
62 " Array.from(files).filter(a => a.getAttribute('file-name') === %s)[0]"
63 " .dispatchEvent(evt);"
64 "})();";
65
66 // Script for querying UI elements (directories and files) shown on the selector.
67 const char kScriptGetElements[] =
68 "(function() {"
69 " var dirs = document.querySelectorAll('#directory-tree .entry-name');"
70 " var files = document.querySelectorAll('#file-list .file');"
71 " return {dirNames: Array.from(dirs, a => a.innerText),"
72 " fileNames: Array.from(files, a => a.getAttribute('file-name'))};"
73 "})();";
74
75 namespace {
76
ConvertToElementVector(const base::Value * list_value,std::vector<mojom::FileSelectorElementPtr> * elements)77 void ConvertToElementVector(
78 const base::Value* list_value,
79 std::vector<mojom::FileSelectorElementPtr>* elements) {
80 if (!list_value || !list_value->is_list())
81 return;
82
83 for (const base::Value& value : list_value->GetList()) {
84 mojom::FileSelectorElementPtr element = mojom::FileSelectorElement::New();
85 element->name = value.GetString();
86 elements->push_back(std::move(element));
87 }
88 }
89
OnGetElementsScriptResults(mojom::FileSystemHost::GetFileSelectorElementsCallback callback,base::Value value)90 void OnGetElementsScriptResults(
91 mojom::FileSystemHost::GetFileSelectorElementsCallback callback,
92 base::Value value) {
93 mojom::FileSelectorElementsPtr result = mojom::FileSelectorElements::New();
94 if (value.is_dict()) {
95 ConvertToElementVector(value.FindKey("dirNames"),
96 &result->directory_elements);
97 ConvertToElementVector(value.FindKey("fileNames"), &result->file_elements);
98 // TODO(niwa): Fill result->search_query.
99 }
100 std::move(callback).Run(std::move(result));
101 }
102
ContentUrlsResolved(mojom::FileSystemHost::SelectFilesCallback callback,const std::vector<GURL> & content_urls)103 void ContentUrlsResolved(mojom::FileSystemHost::SelectFilesCallback callback,
104 const std::vector<GURL>& content_urls) {
105 mojom::SelectFilesResultPtr result = mojom::SelectFilesResult::New();
106 for (const GURL& content_url : content_urls) {
107 result->urls.push_back(content_url);
108 }
109 std::move(callback).Run(std::move(result));
110 }
111
GetDialogType(const mojom::SelectFilesRequestPtr & request)112 ui::SelectFileDialog::Type GetDialogType(
113 const mojom::SelectFilesRequestPtr& request) {
114 switch (request->action_type) {
115 case mojom::SelectFilesActionType::GET_CONTENT:
116 case mojom::SelectFilesActionType::OPEN_DOCUMENT:
117 return request->allow_multiple
118 ? ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE
119 : ui::SelectFileDialog::SELECT_OPEN_FILE;
120 case mojom::SelectFilesActionType::OPEN_DOCUMENT_TREE:
121 return ui::SelectFileDialog::SELECT_EXISTING_FOLDER;
122 case mojom::SelectFilesActionType::CREATE_DOCUMENT:
123 return ui::SelectFileDialog::SELECT_SAVEAS_FILE;
124 }
125 NOTREACHED();
126 }
127
GetInitialFilePath(const mojom::SelectFilesRequestPtr & request)128 base::FilePath GetInitialFilePath(const mojom::SelectFilesRequestPtr& request) {
129 const mojom::DocumentPathPtr& document_path = request->initial_document_path;
130 if (!document_path)
131 return base::FilePath();
132
133 if (document_path->path.empty()) {
134 LOG(ERROR) << "path should at least contain root Document ID.";
135 return base::FilePath();
136 }
137
138 const std::string& root_document_id = document_path->path[0];
139 // TODO(niwa): Convert non-root document IDs to the relative path and append.
140 return arc::GetDocumentsProviderMountPath(document_path->authority,
141 root_document_id);
142 }
143
BuildFileTypeInfo(const mojom::SelectFilesRequestPtr & request,ui::SelectFileDialog::FileTypeInfo * file_type_info)144 void BuildFileTypeInfo(const mojom::SelectFilesRequestPtr& request,
145 ui::SelectFileDialog::FileTypeInfo* file_type_info) {
146 file_type_info->allowed_paths = ui::SelectFileDialog::FileTypeInfo::ANY_PATH;
147 for (const std::string& mime_type : request->mime_types) {
148 std::vector<base::FilePath::StringType> extensions;
149 net::GetExtensionsForMimeType(mime_type, &extensions);
150 file_type_info->extensions.push_back(extensions);
151
152 // Enable "Select from all files" option if GetExtensionsForMimeType
153 // can't find any matching extensions or specified MIME type contains an
154 // asterisk. This is because some extensions used in Android (e.g. .DNG) are
155 // not covered by GetExtensionsForMimeType. (crbug.com/1034874)
156 if (extensions.empty() ||
157 base::EndsWith(mime_type, "/*", base::CompareCase::SENSITIVE)) {
158 file_type_info->include_all_files = true;
159 }
160 }
161 }
162
163 } // namespace
164
ArcSelectFilesHandlersManager(content::BrowserContext * context)165 ArcSelectFilesHandlersManager::ArcSelectFilesHandlersManager(
166 content::BrowserContext* context)
167 : context_(context) {}
168
169 ArcSelectFilesHandlersManager::~ArcSelectFilesHandlersManager() = default;
170
SelectFiles(const mojom::SelectFilesRequestPtr & request,mojom::FileSystemHost::SelectFilesCallback callback)171 void ArcSelectFilesHandlersManager::SelectFiles(
172 const mojom::SelectFilesRequestPtr& request,
173 mojom::FileSystemHost::SelectFilesCallback callback) {
174 int task_id = request->task_id;
175 if (handlers_by_task_id_.find(task_id) != handlers_by_task_id_.end()) {
176 LOG(ERROR) << "SelectFileDialog is already shown for task ID : " << task_id;
177 std::move(callback).Run(mojom::SelectFilesResult::New());
178 return;
179 }
180
181 auto handler = std::make_unique<ArcSelectFilesHandler>(context_);
182 auto* handler_ptr = handler.get();
183 handlers_by_task_id_.emplace(task_id, std::move(handler));
184
185 // Make sure that the handler is erased when the SelectFileDialog is closed.
186 handler_ptr->SelectFiles(
187 std::move(request),
188 base::BindOnce(&ArcSelectFilesHandlersManager::EraseHandlerAndRunCallback,
189 weak_ptr_factory_.GetWeakPtr(), task_id,
190 std::move(callback)));
191 }
192
OnFileSelectorEvent(mojom::FileSelectorEventPtr event,mojom::FileSystemHost::OnFileSelectorEventCallback callback)193 void ArcSelectFilesHandlersManager::OnFileSelectorEvent(
194 mojom::FileSelectorEventPtr event,
195 mojom::FileSystemHost::OnFileSelectorEventCallback callback) {
196 int task_id = event->creator_task_id;
197 auto iter = handlers_by_task_id_.find(task_id);
198 if (iter == handlers_by_task_id_.end()) {
199 LOG(ERROR) << "Can't find a SelectFileDialog for task ID : " << task_id;
200 std::move(callback).Run();
201 return;
202 }
203 iter->second->OnFileSelectorEvent(std::move(event), std::move(callback));
204 }
205
GetFileSelectorElements(mojom::GetFileSelectorElementsRequestPtr request,mojom::FileSystemHost::GetFileSelectorElementsCallback callback)206 void ArcSelectFilesHandlersManager::GetFileSelectorElements(
207 mojom::GetFileSelectorElementsRequestPtr request,
208 mojom::FileSystemHost::GetFileSelectorElementsCallback callback) {
209 int task_id = request->creator_task_id;
210 auto iter = handlers_by_task_id_.find(task_id);
211 if (iter == handlers_by_task_id_.end()) {
212 LOG(ERROR) << "Can't find a SelectFileDialog for task ID : " << task_id;
213 std::move(callback).Run(mojom::FileSelectorElements::New());
214 return;
215 }
216 iter->second->GetFileSelectorElements(std::move(request),
217 std::move(callback));
218 }
219
EraseHandlerAndRunCallback(int task_id,mojom::FileSystemHost::SelectFilesCallback callback,mojom::SelectFilesResultPtr result)220 void ArcSelectFilesHandlersManager::EraseHandlerAndRunCallback(
221 int task_id,
222 mojom::FileSystemHost::SelectFilesCallback callback,
223 mojom::SelectFilesResultPtr result) {
224 handlers_by_task_id_.erase(task_id);
225 std::move(callback).Run(std::move(result));
226 }
227
ArcSelectFilesHandler(content::BrowserContext * context)228 ArcSelectFilesHandler::ArcSelectFilesHandler(content::BrowserContext* context)
229 : profile_(Profile::FromBrowserContext(context)) {
230 dialog_holder_ = std::make_unique<SelectFileDialogHolder>(this);
231 }
232
~ArcSelectFilesHandler()233 ArcSelectFilesHandler::~ArcSelectFilesHandler() {
234 // Make sure to close SelectFileDialog when the handler is destroyed.
235 dialog_holder_->ExecuteJavaScript(kScriptClickCancel, {});
236 }
237
SelectFiles(const mojom::SelectFilesRequestPtr & request,mojom::FileSystemHost::SelectFilesCallback callback)238 void ArcSelectFilesHandler::SelectFiles(
239 const mojom::SelectFilesRequestPtr& request,
240 mojom::FileSystemHost::SelectFilesCallback callback) {
241 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
242
243 callback_ = std::move(callback);
244
245 // TODO(niwa): Convert all request options.
246 ui::SelectFileDialog::Type dialog_type = GetDialogType(request);
247 ui::SelectFileDialog::FileTypeInfo file_type_info;
248 BuildFileTypeInfo(request, &file_type_info);
249 base::FilePath default_path = GetInitialFilePath(request);
250 std::string search_query = request->search_query.value_or(std::string());
251
252 // Android picker apps should be shown in GET_CONTENT mode.
253 bool show_android_picker_apps =
254 request->action_type == mojom::SelectFilesActionType::GET_CONTENT;
255
256 bool success = dialog_holder_->SelectFile(
257 dialog_type, default_path, &file_type_info, request->task_id,
258 search_query, show_android_picker_apps);
259 if (!success) {
260 std::move(callback_).Run(mojom::SelectFilesResult::New());
261 }
262 }
263
FileSelected(const base::FilePath & path,int index,void * params)264 void ArcSelectFilesHandler::FileSelected(const base::FilePath& path,
265 int index,
266 void* params) {
267 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
268 DCHECK(callback_);
269
270 const std::string& activity = ConvertFilePathToAndroidActivity(path);
271 if (!activity.empty()) {
272 // The user selected an Android picker activity instead of a file.
273 mojom::SelectFilesResultPtr result = mojom::SelectFilesResult::New();
274 result->picker_activity = activity;
275 std::move(callback_).Run(std::move(result));
276 return;
277 }
278
279 std::vector<base::FilePath> files;
280 files.push_back(path);
281 FilesSelectedInternal(files, params);
282 }
283
MultiFilesSelected(const std::vector<base::FilePath> & files,void * params)284 void ArcSelectFilesHandler::MultiFilesSelected(
285 const std::vector<base::FilePath>& files,
286 void* params) {
287 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
288 FilesSelectedInternal(files, params);
289 }
290
FileSelectionCanceled(void * params)291 void ArcSelectFilesHandler::FileSelectionCanceled(void* params) {
292 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
293 DCHECK(callback_);
294 // Returns an empty result if the user cancels file selection.
295 std::move(callback_).Run(mojom::SelectFilesResult::New());
296 }
297
FilesSelectedInternal(const std::vector<base::FilePath> & files,void * params)298 void ArcSelectFilesHandler::FilesSelectedInternal(
299 const std::vector<base::FilePath>& files,
300 void* params) {
301 DCHECK(callback_);
302
303 storage::FileSystemContext* file_system_context =
304 file_manager::util::GetFileSystemContextForExtensionId(
305 profile_, file_manager::kFileManagerAppId);
306
307 std::vector<storage::FileSystemURL> file_system_urls;
308 for (const base::FilePath& file_path : files) {
309 GURL gurl;
310 file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
311 profile_, file_path, file_manager::kFileManagerAppId, &gurl);
312 file_system_urls.push_back(file_system_context->CrackURL(gurl));
313 }
314
315 file_manager::util::ConvertToContentUrls(
316 file_system_urls,
317 base::BindOnce(&ContentUrlsResolved, std::move(callback_)));
318 }
319
OnFileSelectorEvent(mojom::FileSelectorEventPtr event,mojom::FileSystemHost::OnFileSelectorEventCallback callback)320 void ArcSelectFilesHandler::OnFileSelectorEvent(
321 mojom::FileSelectorEventPtr event,
322 mojom::FileSystemHost::OnFileSelectorEventCallback callback) {
323 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
324
325 std::string quotedClickTargetName =
326 base::GetQuotedJSONString(event->click_target->name.c_str());
327 std::string script;
328 switch (event->type) {
329 case mojom::FileSelectorEventType::CLICK_OK:
330 script = kScriptClickOk;
331 break;
332 case mojom::FileSelectorEventType::CLICK_CANCEL:
333 script = kScriptClickCancel;
334 break;
335 case mojom::FileSelectorEventType::CLICK_DIRECTORY:
336 script = base::StringPrintf(kScriptClickDirectory,
337 quotedClickTargetName.c_str());
338 break;
339 case mojom::FileSelectorEventType::CLICK_FILE:
340 script =
341 base::StringPrintf(kScriptClickFile, quotedClickTargetName.c_str());
342 break;
343 }
344 dialog_holder_->ExecuteJavaScript(script, {});
345
346 std::move(callback).Run();
347 }
348
GetFileSelectorElements(mojom::GetFileSelectorElementsRequestPtr request,mojom::FileSystemHost::GetFileSelectorElementsCallback callback)349 void ArcSelectFilesHandler::GetFileSelectorElements(
350 mojom::GetFileSelectorElementsRequestPtr request,
351 mojom::FileSystemHost::GetFileSelectorElementsCallback callback) {
352 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
353
354 dialog_holder_->ExecuteJavaScript(
355 kScriptGetElements,
356 base::BindOnce(&OnGetElementsScriptResults, std::move(callback)));
357 }
358
SetDialogHolderForTesting(std::unique_ptr<SelectFileDialogHolder> dialog_holder)359 void ArcSelectFilesHandler::SetDialogHolderForTesting(
360 std::unique_ptr<SelectFileDialogHolder> dialog_holder) {
361 dialog_holder_ = std::move(dialog_holder);
362 }
363
SelectFileDialogHolder(ui::SelectFileDialog::Listener * listener)364 SelectFileDialogHolder::SelectFileDialogHolder(
365 ui::SelectFileDialog::Listener* listener) {
366 select_file_dialog_ = static_cast<SelectFileDialogExtension*>(
367 ui::SelectFileDialog::Create(listener, nullptr).get());
368 }
369
~SelectFileDialogHolder()370 SelectFileDialogHolder::~SelectFileDialogHolder() {
371 // select_file_dialog_ can be nullptr only in unit tests.
372 if (select_file_dialog_.get())
373 select_file_dialog_->ListenerDestroyed();
374 }
375
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)376 bool SelectFileDialogHolder::SelectFile(
377 ui::SelectFileDialog::Type type,
378 const base::FilePath& default_path,
379 const ui::SelectFileDialog::FileTypeInfo* file_types,
380 int task_id,
381 const std::string& search_query,
382 bool show_android_picker_apps) {
383 aura::Window* owner_window = nullptr;
384 for (auto* window : ChromeLauncherController::instance()->GetArcWindows()) {
385 if (arc::GetWindowTaskId(window) == task_id) {
386 owner_window = window;
387 break;
388 }
389 }
390 if (!owner_window) {
391 LOG(ERROR) << "Can't find the ARC window for task ID : " << task_id;
392 return false;
393 }
394
395 // TODO(niwa): Pass search query as well.
396 SelectFileDialogExtension::Owner owner;
397 owner.window = owner_window;
398 owner.android_task_id = task_id;
399 select_file_dialog_->SelectFileWithFileManagerParams(
400 type,
401 /*title=*/base::string16(), default_path, file_types,
402 /*file_type_index=*/0,
403 /*params=*/nullptr, owner, search_query, show_android_picker_apps);
404 return true;
405 }
406
ExecuteJavaScript(const std::string & script,content::RenderFrameHost::JavaScriptResultCallback callback)407 void SelectFileDialogHolder::ExecuteJavaScript(
408 const std::string& script,
409 content::RenderFrameHost::JavaScriptResultCallback callback) {
410 content::RenderViewHost* view_host = select_file_dialog_->GetRenderViewHost();
411 content::RenderFrameHost* frame_host =
412 view_host ? view_host->GetMainFrame() : nullptr;
413
414 if (!frame_host) {
415 LOG(ERROR) << "Can't execute a script. SelectFileDialog is not ready.";
416 if (callback)
417 std::move(callback).Run(base::Value());
418 return;
419 }
420
421 frame_host->ExecuteJavaScriptInIsolatedWorld(
422 base::UTF8ToUTF16(script), std::move(callback),
423 ISOLATED_WORLD_ID_CHROME_INTERNAL);
424 }
425
426 } // namespace arc
427