blob: 347aa7ef39257c2e79eae25c83dbc330c4d7dde9 [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/app_shim_remote_cocoa/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 "content/browser/download/drag_download_file.h"
#include "content/browser/download/drag_download_util.h"
#include "content/common/web_contents_ns_view_bridge.mojom.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/clipboard_constants.h"
#include "ui/base/clipboard/clipboard_util_mac.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::DropData;
@interface WebDragSource(Private)
- (void)fillPasteboard;
- (NSImage*)dragImage;
@end // @interface WebDragSource(Private)
@implementation WebDragSource
- (id)initWithHost:(remote_cocoa::mojom::WebContentsNSViewHost*)host
view:(NSView*)contentsView
dropData:(const DropData*)dropData
image:(NSImage*)image
offset:(NSPoint)offset
pasteboard:(NSPasteboard*)pboard
dragOperationMask:(NSDragOperation)dragOperationMask {
if ((self = [super init])) {
_host = host;
_contentsView = contentsView;
DCHECK(_contentsView);
_dropData.reset(new DropData(*dropData));
DCHECK(_dropData.get());
_dragImage.reset([image retain]);
_imageOffset = offset;
_pasteboard.reset([pboard retain]);
DCHECK(_pasteboard.get());
_dragOperationMask = dragOperationMask;
[self fillPasteboard];
}
return self;
}
- (void)clearHostAndWebContentsView {
_host = nullptr;
_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 && !_dropData->html->empty());
// See comment on |kHtmlHeader| above.
[pboard setString:SysUTF16ToNSString(kHtmlHeader + *_dropData->html)
forType:type];
// URL.
} else if ([type isEqualToString:NSURLPboardType] ||
[type isEqualToString:base::mac::CFToNSCast(kUTTypeURL)]) {
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)) {
std::string unescapedUrlString =
net::UnescapeBinaryURLComponent(_dropData->url.spec());
std::string escapedUrlString =
net::EscapeUrlEncodedData(unescapedUrlString, false);
url = [NSURL URLWithString:SysUTF8ToNSString(escapedUrlString)];
}
[url writeToPasteboard:pboard];
// URL title.
} else if ([type isEqualToString:ui::kUTTypeURLName]) {
[pboard setString:SysUTF16ToNSString(_dropData->url_title)
forType:ui::kUTTypeURLName];
// 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 && !_dropData->text->empty());
[pboard setString:SysUTF16ToNSString(*_dropData->text)
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 (!_host || !_contentsView)
return;
if (_dragImage) {
screenPoint.x += _imageOffset.x;
// Deal with Cocoa's flipped coordinate system.
screenPoint.y += [_dragImage.get() size].height - _imageOffset.y;
}
// Convert |screenPoint| to view coordinates and flip it.
NSPoint localPoint = NSZeroPoint;
if ([_contentsView window])
localPoint = [self convertScreenPoint:screenPoint];
NSRect viewFrame = [_contentsView frame];
// Flip |screenPoint|.
NSRect screenFrame = [[[_contentsView window] screen] frame];
// 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;
_host->EndDrag(
operation,
gfx::PointF(localPoint.x, viewFrame.size.height - localPoint.y),
gfx::PointF(screenPoint.x, screenFrame.size.height - screenPoint.y));
// Make sure the pasteboard owner isn't us.
[_pasteboard declareTypes:[NSArray array] owner:nil];
}
- (NSString*)dragPromisedFileTo:(NSString*)path {
if (!_host)
return nil;
// 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);
_host->DragPromisedFileTo(filePath, *_dropData, _downloadURL, &filePath);
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, ui::kUTTypeURLName, base::mac::CFToNSCast(kUTTypeURL)
]
owner:_contentsView];
}
// MIME type.
std::string mimeType;
// File.
if (!_dropData->file_contents.empty() ||
!_dropData->download_metadata.empty()) {
// TODO(https://crbug.com/898608): The |downloadFileName_| and
// |downloadURL_| values should be computed by the caller.
if (_dropData->download_metadata.empty()) {
base::Optional<base::FilePath> suggestedFilename =
_dropData->GetSafeFilenameForImageFileContents();
if (suggestedFilename) {
_downloadFileName = std::move(*suggestedFilename);
net::GetMimeTypeFromFile(_downloadFileName, &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 && !_dropData->html->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 && !_dropData->text->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)