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