// Copyright (c) 2011 The Chromium Embedded Framework Authors. // Portions copyright (c) 2011 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 "include/cef.h" #include "browser_drag_delegate_win.h" #include "browser_impl.h" #include "browser_webview_delegate.h" #include "cef_thread.h" #include "drag_download_file.h" #include "web_drag_source_win.h" #include "web_drag_utils_win.h" #include "web_drop_target_win.h" #include #include #include "base/file_path.h" #include "base/message_loop.h" #include "base/string_util.h" #include "base/task.h" #include "base/threading/platform_thread.h" #include "base/threading/thread.h" #include "base/threading/thread_restrictions.h" #include "base/utf_string_conversions.h" #include "net/base/file_stream.h" #include "net/base/mime_util.h" #include "net/base/net_util.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h" #include "views/drag_utils.h" #include "webkit/glue/webdropdata.h" using WebKit::WebDragOperationsMask; using WebKit::WebDragOperationCopy; using WebKit::WebDragOperationLink; using WebKit::WebDragOperationMove; using WebKit::WebView; namespace { HHOOK msg_hook = NULL; DWORD drag_out_thread_id = 0; bool mouse_up_received = false; LRESULT CALLBACK MsgFilterProc(int code, WPARAM wparam, LPARAM lparam) { if (code == base::MessagePumpForUI::kMessageFilterCode && !mouse_up_received) { MSG* msg = reinterpret_cast(lparam); // We do not care about WM_SYSKEYDOWN and WM_SYSKEYUP because when ALT key // is pressed down on drag-and-drop, it means to create a link. if (msg->message == WM_MOUSEMOVE || msg->message == WM_LBUTTONUP || msg->message == WM_KEYDOWN || msg->message == WM_KEYUP) { // Forward the message from the UI thread to the drag-and-drop thread. PostThreadMessage(drag_out_thread_id, msg->message, msg->wParam, msg->lParam); // If the left button is up, we do not need to forward the message any // more. if (msg->message == WM_LBUTTONUP || !(GetKeyState(VK_LBUTTON) & 0x8000)) mouse_up_received = true; return TRUE; } } return CallNextHookEx(msg_hook, code, wparam, lparam); } // Parse the download metadata set in DataTransfer.setData. The metadata // consists of a set of the following values separated by ":" // * MIME type // * File name // * URL // If the file name contains special characters, they need to be escaped // appropriately. // For example, we can have // text/plain:example.txt:http://example.com/example.txt // From chrome/browser/download/drag_download_util.cc bool ParseDownloadMetadata(const string16& metadata, string16* mime_type, FilePath* file_name, GURL* url) { const char16 separator = L':'; size_t mime_type_end_pos = metadata.find(separator); if (mime_type_end_pos == string16::npos) return false; size_t file_name_end_pos = metadata.find(separator, mime_type_end_pos + 1); if (file_name_end_pos == string16::npos) return false; GURL parsed_url = GURL(metadata.substr(file_name_end_pos + 1)); if (!parsed_url.is_valid()) return false; if (mime_type) *mime_type = metadata.substr(0, mime_type_end_pos); if (file_name) { string16 file_name_str = metadata.substr( mime_type_end_pos + 1, file_name_end_pos - mime_type_end_pos - 1); #if defined(OS_WIN) *file_name = FilePath(file_name_str); #else *file_name = FilePath(UTF16ToUTF8(file_name_str)); #endif } if (url) *url = parsed_url; return true; } #if defined(OS_WIN) // Returns whether the specified extension is automatically integrated into the // windows shell. // From chrome/browser/download/download_util.cc bool IsShellIntegratedExtension(const string16& extension) { string16 extension_lower = StringToLowerASCII(extension); static const wchar_t* const integrated_extensions[] = { // See . L"local", // Right-clicking on shortcuts can be magical. L"lnk", }; for (int i = 0; i < arraysize(integrated_extensions); ++i) { if (extension_lower == integrated_extensions[i]) return true; } // See . // That vulnerability report is not exactly on point, but files become magical // if their end in a CLSID. Here we block extensions that look like CLSIDs. if (extension_lower.size() > 0 && extension_lower.at(0) == L'{' && extension_lower.at(extension_lower.length() - 1) == L'}') return true; return false; } // Returns whether the specified file name is a reserved name on windows. // This includes names like "com2.zip" (which correspond to devices) and // desktop.ini and thumbs.db which have special meaning to the windows shell. // From chrome/browser/download/download_util.cc bool IsReservedName(const string16& filename) { // This list is taken from the MSDN article "Naming a file" // http://msdn2.microsoft.com/en-us/library/aa365247(VS.85).aspx // I also added clock$ because GetSaveFileName seems to consider it as a // reserved name too. static const wchar_t* const known_devices[] = { L"con", L"prn", L"aux", L"nul", L"com1", L"com2", L"com3", L"com4", L"com5", L"com6", L"com7", L"com8", L"com9", L"lpt1", L"lpt2", L"lpt3", L"lpt4", L"lpt5", L"lpt6", L"lpt7", L"lpt8", L"lpt9", L"clock$" }; string16 filename_lower = StringToLowerASCII(filename); for (int i = 0; i < arraysize(known_devices); ++i) { // Exact match. if (filename_lower == known_devices[i]) return true; // Starts with "DEVICE.". if (filename_lower.find(string16(known_devices[i]) + L".") == 0) return true; } static const wchar_t* const magic_names[] = { // These file names are used by the "Customize folder" feature of the shell. L"desktop.ini", L"thumbs.db", }; for (int i = 0; i < arraysize(magic_names); ++i) { if (filename_lower == magic_names[i]) return true; } return false; } #endif // OS_WIN // Create an extension based on the file name and mime type. // From chrome/browser/download/download_util.cc void GenerateExtension(const FilePath& file_name, const std::string& mime_type, FilePath::StringType* generated_extension) { // We're worried about two things here: // // 1) Usability. If the site fails to provide a file extension, we want to // guess a reasonable file extension based on the content type. // // 2) Shell integration. Some file extensions automatically integrate with // the shell. We block these extensions to prevent a malicious web site // from integrating with the user's shell. // See if our file name already contains an extension. FilePath::StringType extension = file_name.Extension(); if (!extension.empty()) extension.erase(extension.begin()); // Erase preceding '.'. #if defined(OS_WIN) static const FilePath::CharType default_extension[] = FILE_PATH_LITERAL("download"); // Rename shell-integrated extensions. if (IsShellIntegratedExtension(extension)) extension.assign(default_extension); #endif if (extension.empty()) { // The GetPreferredExtensionForMimeType call will end up going to disk. Do // this on another thread to avoid slowing the IO thread. // http://crbug.com/61827 base::ThreadRestrictions::ScopedAllowIO allow_io; net::GetPreferredExtensionForMimeType(mime_type, &extension); } generated_extension->swap(extension); } // Used to make sure we have a safe file extension and filename for a // download. |file_name| can either be just the file name or it can be a // full path to a file. // From chrome/browser/download/download_util.cc void GenerateSafeFileName(const std::string& mime_type, FilePath* file_name) { // Make sure we get the right file extension FilePath::StringType extension; GenerateExtension(*file_name, mime_type, &extension); *file_name = file_name->ReplaceExtension(extension); #if defined(OS_WIN) // Prepend "_" to the file name if it's a reserved name FilePath::StringType leaf_name = file_name->BaseName().value(); DCHECK(!leaf_name.empty()); if (IsReservedName(leaf_name)) { leaf_name = FilePath::StringType(FILE_PATH_LITERAL("_")) + leaf_name; *file_name = file_name->DirName(); if (file_name->value() == FilePath::kCurrentDirectory) { *file_name = FilePath(leaf_name); } else { *file_name = file_name->Append(leaf_name); } } #endif } // Create a file name based on the response from the server. // From chrome/browser/download/download_util.cc void GenerateFileName(const GURL& url, const std::string& content_disposition, const std::string& referrer_charset, const std::string& mime_type, FilePath* generated_name) { string16 new_name = net::GetSuggestedFilename(GURL(url), content_disposition, referrer_charset, string16(L"download")); // TODO(evan): this code is totally wrong -- we should just generate // Unicode filenames and do all this encoding switching at the end. // However, I'm just shuffling wrong code around, at least not adding // to it. #if defined(OS_WIN) *generated_name = FilePath(new_name); #else *generated_name = FilePath( base::SysWideToNativeMB(UTF16ToWide(new_name))); #endif DCHECK(!generated_name->empty()); GenerateSafeFileName(mime_type, generated_name); } } // namespace class DragDropThread : public base::Thread { public: explicit DragDropThread(BrowserDragDelegate* drag_handler) : base::Thread("Chrome_DragDropThread"), drag_handler_(drag_handler) { } virtual ~DragDropThread() { Thread::Stop(); } protected: // base::Thread implementations: virtual void Init() { int ole_result = OleInitialize(NULL); DCHECK(ole_result == S_OK); } virtual void CleanUp() { OleUninitialize(); } private: // Hold a reference count to BrowserDragDelegate to make sure that it is always // alive in the thread lifetime. scoped_refptr drag_handler_; DISALLOW_COPY_AND_ASSIGN(DragDropThread); }; BrowserDragDelegate::BrowserDragDelegate(BrowserWebViewDelegate* view) : drag_drop_thread_id_(0), view_(view), drag_ended_(false), old_drop_target_suspended_state_(false) { } BrowserDragDelegate::~BrowserDragDelegate() { DCHECK(CefThread::CurrentlyOn(CefThread::UI)); DCHECK(!drag_drop_thread_.get()); } void BrowserDragDelegate::StartDragging(const WebDropData& drop_data, WebDragOperationsMask ops, const SkBitmap& image, const gfx::Point& image_offset) { DCHECK(CefThread::CurrentlyOn(CefThread::UI)); CefBrowserImpl* browser = view_->GetBrowser(); WebView* web_view = browser->UIT_GetWebView(); drag_source_ = new WebDragSource(browser->UIT_GetWebViewWndHandle(), web_view); const GURL& page_url = web_view->mainFrame()->url(); const std::string& page_encoding = web_view->mainFrame()->encoding().utf8(); // If it is not drag-out, do the drag-and-drop in the current UI thread. if (drop_data.download_metadata.empty()) { DoDragging(drop_data, ops, page_url, page_encoding, image, image_offset); EndDragging(false); return; } // We do not want to drag and drop the download to itself. old_drop_target_suspended_state_ = view_->drop_target()->suspended(); view_->drop_target()->set_suspended(true); // Start a background thread to do the drag-and-drop. DCHECK(!drag_drop_thread_.get()); drag_drop_thread_.reset(new DragDropThread(this)); base::Thread::Options options; options.message_loop_type = MessageLoop::TYPE_UI; if (drag_drop_thread_->StartWithOptions(options)) { drag_drop_thread_->message_loop()->PostTask( FROM_HERE, NewRunnableMethod(this, &BrowserDragDelegate::StartBackgroundDragging, drop_data, ops, page_url, page_encoding, image, image_offset)); } // Install a hook procedure to monitor the messages so that we can forward // the appropriate ones to the background thread. drag_out_thread_id = drag_drop_thread_->thread_id(); mouse_up_received = false; DCHECK(!msg_hook); msg_hook = SetWindowsHookEx(WH_MSGFILTER, MsgFilterProc, NULL, GetCurrentThreadId()); // Attach the input state of the background thread to the UI thread so that // SetCursor can work from the background thread. AttachThreadInput(drag_out_thread_id, GetCurrentThreadId(), TRUE); } void BrowserDragDelegate::StartBackgroundDragging( const WebDropData& drop_data, WebDragOperationsMask ops, const GURL& page_url, const std::string& page_encoding, const SkBitmap& image, const gfx::Point& image_offset) { drag_drop_thread_id_ = base::PlatformThread::CurrentId(); DoDragging(drop_data, ops, page_url, page_encoding, image, image_offset); CefThread::PostTask( CefThread::UI, FROM_HERE, NewRunnableMethod(this, &BrowserDragDelegate::EndDragging, true)); } void BrowserDragDelegate::PrepareDragForDownload( const WebDropData& drop_data, ui::OSExchangeData* data, const GURL& page_url, const std::string& page_encoding) { // Parse the download metadata. string16 mime_type; FilePath file_name; GURL download_url; if (!ParseDownloadMetadata(drop_data.download_metadata, &mime_type, &file_name, &download_url)) return; // Generate the download filename. std::string content_disposition = "attachment; filename=" + UTF16ToUTF8(file_name.value()); FilePath generated_file_name; GenerateFileName(download_url, content_disposition, std::string(), UTF16ToUTF8(mime_type), &generated_file_name); // Provide the data as file (CF_HDROP). A temporary download file with the // Zone.Identifier ADS (Alternate Data Stream) attached will be created. linked_ptr empty_file_stream; scoped_refptr download_file = new DragDownloadFile(generated_file_name, empty_file_stream, download_url, page_url, page_encoding, view_); ui::OSExchangeData::DownloadFileInfo file_download(FilePath(), download_file.get()); data->SetDownloadFileInfo(file_download); // Enable asynchronous operation. ui::OSExchangeDataProviderWin::GetIAsyncOperation(*data)->SetAsyncMode(TRUE); } void BrowserDragDelegate::PrepareDragForFileContents( const WebDropData& drop_data, ui::OSExchangeData* data) { // Images without ALT text will only have a file extension so we need to // synthesize one from the provided extension and URL. FilePath file_name(drop_data.file_description_filename); file_name = file_name.BaseName().RemoveExtension(); if (file_name.value().empty()) { // Retrieve the name from the URL. file_name = FilePath( net::GetSuggestedFilename(drop_data.url, "", "", string16())); if (file_name.value().size() + drop_data.file_extension.size() + 1 > MAX_PATH) { file_name = FilePath(file_name.value().substr( 0, MAX_PATH - drop_data.file_extension.size() - 2)); } } file_name = file_name.ReplaceExtension(drop_data.file_extension); data->SetFileContents(file_name, drop_data.file_contents); } void BrowserDragDelegate::PrepareDragForUrl(const WebDropData& drop_data, ui::OSExchangeData* data) { if (drop_data.url.SchemeIs("javascript")) { // We don't want to allow javascript URLs to be dragged to the desktop. } else { data->SetURL(drop_data.url, drop_data.url_title); } } void BrowserDragDelegate::DoDragging(const WebDropData& drop_data, WebDragOperationsMask ops, const GURL& page_url, const std::string& page_encoding, const SkBitmap& image, const gfx::Point& image_offset) { ui::OSExchangeData data; if (!drop_data.download_metadata.empty()) { PrepareDragForDownload(drop_data, &data, page_url, page_encoding); // Set the observer. ui::OSExchangeDataProviderWin::GetDataObjectImpl(data)->set_observer(this); } else { // We set the file contents before the URL because the URL also sets file // contents (to a .URL shortcut). We want to prefer file content data over // a shortcut so we add it first. if (!drop_data.file_contents.empty()) PrepareDragForFileContents(drop_data, &data); if (!drop_data.text_html.empty()) data.SetHtml(drop_data.text_html, drop_data.html_base_url); // We set the text contents before the URL because the URL also sets text // content. if (!drop_data.plain_text.empty()) data.SetString(drop_data.plain_text); if (drop_data.url.is_valid()) PrepareDragForUrl(drop_data, &data); } // Set drag image. if (!image.isNull()) { drag_utils::SetDragImageOnDataObject( image, gfx::Size(image.width(), image.height()), image_offset, &data); } // We need to enable recursive tasks on the message loop so we can get // updates while in the system DoDragDrop loop. bool old_state = MessageLoop::current()->NestableTasksAllowed(); MessageLoop::current()->SetNestableTasksAllowed(true); DWORD effect; DoDragDrop(ui::OSExchangeDataProviderWin::GetIDataObject(data), drag_source_, web_drag_utils_win::WebDragOpMaskToWinDragOpMask(ops), &effect); MessageLoop::current()->SetNestableTasksAllowed(old_state); // This works because WebDragSource::OnDragSourceDrop uses PostTask to // dispatch the actual event. drag_source_->set_effect(effect); } void BrowserDragDelegate::EndDragging(bool restore_suspended_state) { DCHECK(CefThread::CurrentlyOn(CefThread::UI)); if (drag_ended_) return; drag_ended_ = true; if (restore_suspended_state) view_->drop_target()->set_suspended(old_drop_target_suspended_state_); if (msg_hook) { AttachThreadInput(drag_out_thread_id, GetCurrentThreadId(), FALSE); UnhookWindowsHookEx(msg_hook); msg_hook = NULL; } view_->EndDragging(); } void BrowserDragDelegate::CancelDrag() { DCHECK(CefThread::CurrentlyOn(CefThread::UI)); drag_source_->CancelDrag(); } void BrowserDragDelegate::CloseThread() { DCHECK(CefThread::CurrentlyOn(CefThread::UI)); drag_drop_thread_.reset(); } void BrowserDragDelegate::OnWaitForData() { DCHECK(drag_drop_thread_id_ == base::PlatformThread::CurrentId()); // When the left button is released and we start to wait for the data, end // the dragging before DoDragDrop returns. This makes the page leave the drag // mode so that it can start to process the normal input events. CefThread::PostTask( CefThread::UI, FROM_HERE, NewRunnableMethod(this, &BrowserDragDelegate::EndDragging, true)); } void BrowserDragDelegate::OnDataObjectDisposed() { DCHECK(drag_drop_thread_id_ == base::PlatformThread::CurrentId()); // The drag-and-drop thread is only closed after OLE is done with // DataObjectImpl. CefThread::PostTask( CefThread::UI, FROM_HERE, NewRunnableMethod(this, &BrowserDragDelegate::CloseThread)); }