blob: 60d0b1a9068287f38927e1d2dde043c681907fe4 [file] [log] [blame]
// Copyright (c) 2012 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.
#import "content/browser/web_contents/web_drag_source_mac.h"
#include <sys/param.h>
#include <utility>
#include "base/bind.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/mac/foundation_util.h"
#include "base/pickle.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task_scheduler/post_task.h"
#include "base/threading/thread.h"
#include "base/threading/thread_restrictions.h"
#include "content/browser/download/drag_download_file.h"
#include "content/browser/download/drag_download_util.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/common/drop_data.h"
#include "net/base/escape.h"
#include "net/base/filename_util.h"
#include "net/base/mime_util.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/cocoa/cocoa_base_utils.h"
#include "ui/base/dragdrop/cocoa_dnd_util.h"
#include "ui/gfx/image/image.h"
#include "ui/resources/grit/ui_resources.h"
#include "url/url_constants.h"
using base::SysNSStringToUTF8;
using base::SysUTF8ToNSString;
using base::SysUTF16ToNSString;
using content::BrowserThread;
using content::DragDownloadFile;
using content::DropData;
using content::PromiseFileFinalizer;
using content::RenderViewHostImpl;
namespace {
// An unofficial standard pasteboard title type to be provided alongside the
// |NSURLPboardType|.
NSString* const kNSURLTitlePboardType = @"public.url-name";
// This helper's sole task is to write out data for a promised file; the caller
// is responsible for opening the file. It takes the drop data and an open file
// stream.
void PromiseWriterHelper(const DropData& drop_data,
base::File file) {
DCHECK(file.IsValid());
file.WriteAtCurrentPos(drop_data.file_contents.data(),
drop_data.file_contents.length());
}
} // namespace
@interface WebDragSource(Private)
- (void)fillPasteboard;
- (NSImage*)dragImage;
@end // @interface WebDragSource(Private)
@implementation WebDragSource
- (id)initWithContents:(content::WebContentsImpl*)contents
view:(NSView*)contentsView
dropData:(const DropData*)dropData
sourceRWH:(content::RenderWidgetHostImpl*)sourceRWH
image:(NSImage*)image
offset:(NSPoint)offset
pasteboard:(NSPasteboard*)pboard
dragOperationMask:(NSDragOperation)dragOperationMask {
if ((self = [super init])) {
contents_ = contents;
DCHECK(contents_);
contentsView_ = contentsView;
DCHECK(contentsView_);
dropData_.reset(new DropData(*dropData));
DCHECK(dropData_.get());
dragStartRWH_ = sourceRWH->GetWeakPtr();
dragImage_.reset([image retain]);
imageOffset_ = offset;
pasteboard_.reset([pboard retain]);
DCHECK(pasteboard_.get());
dragOperationMask_ = dragOperationMask;
[self fillPasteboard];
}
return self;
}
- (void)clearWebContentsView {
contents_ = nil;
contentsView_ = nil;
}
- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
return dragOperationMask_;
}
- (void)lazyWriteToPasteboard:(NSPasteboard*)pboard forType:(NSString*)type {
// NSHTMLPboardType requires the character set to be declared. Otherwise, it
// assumes US-ASCII. Awesome.
const base::string16 kHtmlHeader = base::ASCIIToUTF16(
"<meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">");
// Be extra paranoid; avoid crashing.
if (!dropData_) {
NOTREACHED();
return;
}
// HTML.
if ([type isEqualToString:NSHTMLPboardType] ||
[type isEqualToString:ui::kChromeDragImageHTMLPboardType]) {
DCHECK(!dropData_->html.string().empty());
// See comment on |kHtmlHeader| above.
[pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->html.string())
forType:type];
// URL.
} else if ([type isEqualToString:NSURLPboardType]) {
DCHECK(dropData_->url.is_valid());
NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())];
// If NSURL creation failed, check for a badly-escaped JavaScript URL.
// Strip out any existing escapes and then re-escape uniformly.
if (!url && dropData_->url.SchemeIs(url::kJavaScriptScheme)) {
net::UnescapeRule::Type unescapeRules =
net::UnescapeRule::SPACES | net::UnescapeRule::PATH_SEPARATORS |
net::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS |
net::UnescapeRule::SPOOFING_AND_CONTROL_CHARS;
std::string unescapedUrlString =
net::UnescapeURLComponent(dropData_->url.spec(), unescapeRules);
std::string escapedUrlString =
net::EscapeUrlEncodedData(unescapedUrlString, false);
url = [NSURL URLWithString:SysUTF8ToNSString(escapedUrlString)];
}
[url writeToPasteboard:pboard];
// URL title.
} else if ([type isEqualToString:kNSURLTitlePboardType]) {
[pboard setString:SysUTF16ToNSString(dropData_->url_title)
forType:kNSURLTitlePboardType];
// File contents.
} else if ([type isEqualToString:base::mac::CFToNSCast(fileUTI_)]) {
[pboard setData:[NSData dataWithBytes:dropData_->file_contents.data()
length:dropData_->file_contents.length()]
forType:base::mac::CFToNSCast(fileUTI_.get())];
// Plain text.
} else if ([type isEqualToString:NSStringPboardType]) {
DCHECK(!dropData_->text.string().empty());
[pboard setString:SysUTF16ToNSString(dropData_->text.string())
forType:NSStringPboardType];
// Custom MIME data.
} else if ([type isEqualToString:ui::kWebCustomDataPboardType]) {
base::Pickle pickle;
ui::WriteCustomDataToPickle(dropData_->custom_data, &pickle);
[pboard setData:[NSData dataWithBytes:pickle.data() length:pickle.size()]
forType:ui::kWebCustomDataPboardType];
// Dummy type.
} else if ([type isEqualToString:ui::kChromeDragDummyPboardType]) {
// The dummy type _was_ promised and someone decided to call the bluff.
[pboard setData:[NSData data]
forType:ui::kChromeDragDummyPboardType];
// Oops!
} else {
// Unknown drag pasteboard type.
NOTREACHED();
}
}
- (NSPoint)convertScreenPoint:(NSPoint)screenPoint {
DCHECK([contentsView_ window]);
NSPoint basePoint =
ui::ConvertPointFromScreenToWindow([contentsView_ window], screenPoint);
return [contentsView_ convertPoint:basePoint fromView:nil];
}
- (void)startDrag {
if (!contentsView_)
return;
NSEvent* currentEvent = [NSApp currentEvent];
// Synthesize an event for dragging, since we can't be sure that
// [NSApp currentEvent] will return a valid dragging event.
NSWindow* window = [contentsView_ window];
NSPoint position = [window mouseLocationOutsideOfEventStream];
NSTimeInterval eventTime = [currentEvent timestamp];
NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged
location:position
modifierFlags:NSLeftMouseDraggedMask
timestamp:eventTime
windowNumber:[window windowNumber]
context:nil
eventNumber:0
clickCount:1
pressure:1.0];
if (dragImage_) {
position.x -= imageOffset_.x;
// Deal with Cocoa's flipped coordinate system.
position.y -= [dragImage_.get() size].height - imageOffset_.y;
}
// Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in
// third_party/WebKit/Source/WebKit/mac/Misc/WebNSViewExtras.m.
[window dragImage:[self dragImage]
at:position
offset:NSZeroSize
event:dragEvent
pasteboard:pasteboard_
source:contentsView_
slideBack:YES];
}
- (void)endDragAt:(NSPoint)screenPoint
operation:(NSDragOperation)operation {
if (!contents_ || !contentsView_)
return;
contents_->SystemDragEnded(dragStartRWH_.get());
// Convert |screenPoint| to view coordinates and flip it.
NSPoint localPoint = NSZeroPoint;
if ([contentsView_ window])
localPoint = [self convertScreenPoint:screenPoint];
NSRect viewFrame = [contentsView_ frame];
localPoint.y = viewFrame.size.height - localPoint.y;
// Flip |screenPoint|.
NSRect screenFrame = [[[contentsView_ window] screen] frame];
screenPoint.y = screenFrame.size.height - screenPoint.y;
// If AppKit returns a copy and move operation, mask off the move bit
// because WebCore does not understand what it means to do both, which
// results in an assertion failure/renderer crash.
if (operation == (NSDragOperationMove | NSDragOperationCopy))
operation &= ~NSDragOperationMove;
// |localPoint| and |screenPoint| are in the root coordinate space, for
// non-root RenderWidgetHosts they need to be transformed.
gfx::Point transformedPoint = gfx::Point(localPoint.x, localPoint.y);
gfx::Point transformedScreenPoint = gfx::Point(screenPoint.x, screenPoint.y);
if (dragStartRWH_ && contents_->GetRenderWidgetHostView()) {
content::RenderWidgetHostViewBase* contentsViewBase =
static_cast<content::RenderWidgetHostViewBase*>(
contents_->GetRenderWidgetHostView());
content::RenderWidgetHostViewBase* dragStartViewBase =
static_cast<content::RenderWidgetHostViewBase*>(
dragStartRWH_->GetView());
contentsViewBase->TransformPointToCoordSpaceForView(
gfx::Point(localPoint.x, localPoint.y), dragStartViewBase,
&transformedPoint);
contentsViewBase->TransformPointToCoordSpaceForView(
gfx::Point(screenPoint.x, screenPoint.y), dragStartViewBase,
&transformedScreenPoint);
}
contents_->DragSourceEndedAt(
transformedPoint.x(), transformedPoint.y(), transformedScreenPoint.x(),
transformedScreenPoint.y(),
static_cast<blink::WebDragOperation>(operation), dragStartRWH_.get());
// Make sure the pasteboard owner isn't us.
[pasteboard_ declareTypes:[NSArray array] owner:nil];
}
- (NSString*)dragPromisedFileTo:(NSString*)path {
// Be extra paranoid; avoid crashing.
if (!dropData_) {
NOTREACHED() << "No drag-and-drop data available for promised file.";
return nil;
}
base::FilePath filePath(SysNSStringToUTF8(path));
filePath = filePath.Append(downloadFileName_);
// CreateFileForDrop() will call base::PathExists(),
// which is blocking. Since this operation is already blocking the
// UI thread on OSX, it should be reasonable to let it happen.
base::ThreadRestrictions::ScopedAllowIO allowIO;
base::File file(content::CreateFileForDrop(&filePath));
if (!file.IsValid())
return nil;
if (downloadURL_.is_valid() && contents_) {
scoped_refptr<DragDownloadFile> dragFileDownloader(
new DragDownloadFile(filePath, std::move(file), downloadURL_,
content::Referrer(contents_->GetLastCommittedURL(),
dropData_->referrer_policy),
contents_->GetEncoding(), contents_));
// The finalizer will take care of closing and deletion.
dragFileDownloader->Start(new PromiseFileFinalizer(
dragFileDownloader.get()));
} else {
// The writer will take care of closing and deletion.
base::PostTaskWithTraits(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::Bind(&PromiseWriterHelper, *dropData_, base::Passed(&file)));
}
// The DragDownloadFile constructor may have altered the value of |filePath|
// if, say, an existing file at the drop site has the same name. Return the
// actual name that was used to write the file.
return SysUTF8ToNSString(filePath.BaseName().value());
}
@end // @implementation WebDragSource
@implementation WebDragSource (Private)
- (void)fillPasteboard {
if (!contentsView_)
return;
DCHECK(pasteboard_.get());
[pasteboard_ declareTypes:@[ ui::kChromeDragDummyPboardType ]
owner:contentsView_];
// URL (and title).
if (dropData_->url.is_valid()) {
[pasteboard_ addTypes:@[ NSURLPboardType, kNSURLTitlePboardType ]
owner:contentsView_];
}
// MIME type.
std::string mimeType;
// File.
if (!dropData_->file_contents.empty() ||
!dropData_->download_metadata.empty()) {
if (dropData_->download_metadata.empty()) {
base::Optional<base::FilePath> suggestedFilename =
dropData_->GetSafeFilenameForImageFileContents();
if (suggestedFilename) {
downloadFileName_ = std::move(*suggestedFilename);
net::GetMimeTypeFromExtension(downloadFileName_.Extension(), &mimeType);
}
} else {
base::string16 mimeType16;
base::FilePath fileName;
if (content::ParseDownloadMetadata(
dropData_->download_metadata,
&mimeType16,
&fileName,
&downloadURL_)) {
// Generate the file name based on both mime type and proposed file
// name.
std::string defaultName =
content::GetContentClient()->browser()->GetDefaultDownloadName();
mimeType = base::UTF16ToUTF8(mimeType16);
downloadFileName_ =
net::GenerateFileName(downloadURL_,
std::string(),
std::string(),
fileName.value(),
mimeType,
defaultName);
}
}
if (!mimeType.empty()) {
base::ScopedCFTypeRef<CFStringRef> mimeTypeCF(
base::SysUTF8ToCFStringRef(mimeType));
fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag(
kUTTagClassMIMEType, mimeTypeCF.get(), NULL));
// File (HFS) promise.
// There are two ways to drag/drop files. NSFilesPromisePboardType is the
// deprecated way, and kPasteboardTypeFilePromiseContent is the way that
// does not work. kPasteboardTypeFilePromiseContent is thoroughly broken:
// * API: There is no good way to get the location for the drop.
// <http://lists.apple.com/archives/cocoa-dev/2012/Feb/msg00706.html>
// <rdar://14943849> <http://openradar.me/14943849>
// * Behavior: A file dropped in the Finder is not selected. This can be
// worked around by selecting the file in the Finder using AppleEvents,
// but the drop target window will come to the front of the Finder's
// window list (unlike the previous behavior). <http://crbug.com/278515>
// <rdar://14943865> <http://openradar.me/14943865>
// * Behavior: Files dragged over app icons in the dock do not highlight
// the dock icons, and the dock icons do not accept the drop.
// <http://crbug.com/282916> <rdar://14943872>
// <http://openradar.me/14943872>
// * Behavior: A file dropped onto the desktop is positioned at the upper
// right of the desktop rather than at the position at which it was
// dropped. <http://crbug.com/284942> <rdar://14943881>
// <http://openradar.me/14943881>
NSArray* fileUTIList = @[ base::mac::CFToNSCast(fileUTI_.get()) ];
[pasteboard_ addTypes:@[ NSFilesPromisePboardType ] owner:contentsView_];
[pasteboard_ setPropertyList:fileUTIList
forType:NSFilesPromisePboardType];
if (!dropData_->file_contents.empty())
[pasteboard_ addTypes:fileUTIList owner:contentsView_];
}
}
// HTML.
bool hasHTMLData = !dropData_->html.string().empty();
// Mail.app and TextEdit accept drags that have both HTML and image flavors on
// them, but don't process them correctly <http://crbug.com/55879>. Therefore,
// if there is an image flavor, don't put the HTML data on as HTML, but rather
// put it on as this Chrome-only flavor.
//
// (The only time that Blink fills in the DropData::file_contents is with
// an image drop, but the MIME time is tested anyway for paranoia's sake.)
bool hasImageData = !dropData_->file_contents.empty() &&
fileUTI_ &&
UTTypeConformsTo(fileUTI_.get(), kUTTypeImage);
if (hasHTMLData) {
if (hasImageData) {
[pasteboard_ addTypes:@[ ui::kChromeDragImageHTMLPboardType ]
owner:contentsView_];
} else {
[pasteboard_ addTypes:@[ NSHTMLPboardType ] owner:contentsView_];
}
}
// Plain text.
if (!dropData_->text.string().empty()) {
[pasteboard_ addTypes:@[ NSStringPboardType ]
owner:contentsView_];
}
if (!dropData_->custom_data.empty()) {
[pasteboard_ addTypes:@[ ui::kWebCustomDataPboardType ]
owner:contentsView_];
}
}
- (NSImage*)dragImage {
if (dragImage_)
return dragImage_;
// Default to returning a generic image.
return content::GetContentClient()->GetNativeImageNamed(
IDR_DEFAULT_FAVICON).ToNSImage();
}
@end // @implementation WebDragSource (Private)