1// Copyright (c) 2012 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#import "content/browser/web_contents/web_drag_source_mac.h" 6 7#include <sys/param.h> 8 9#include "base/bind.h" 10#include "base/files/file.h" 11#include "base/files/file_path.h" 12#include "base/mac/mac_util.h" 13#include "base/pickle.h" 14#include "base/strings/string_util.h" 15#include "base/strings/sys_string_conversions.h" 16#include "base/strings/utf_string_conversions.h" 17#include "base/threading/thread.h" 18#include "base/threading/thread_restrictions.h" 19#include "content/browser/browser_thread_impl.h" 20#include "content/browser/download/drag_download_file.h" 21#include "content/browser/download/drag_download_util.h" 22#include "content/browser/renderer_host/render_view_host_impl.h" 23#include "content/browser/web_contents/web_contents_impl.h" 24#include "content/public/browser/content_browser_client.h" 25#include "content/public/common/content_client.h" 26#include "content/public/common/drop_data.h" 27#include "net/base/escape.h" 28#include "net/base/filename_util.h" 29#include "net/base/mime_util.h" 30#include "ui/base/clipboard/custom_data_helper.h" 31#include "ui/base/dragdrop/cocoa_dnd_util.h" 32#include "ui/gfx/image/image.h" 33#include "ui/resources/grit/ui_resources.h" 34#include "url/url_constants.h" 35 36using base::SysNSStringToUTF8; 37using base::SysUTF8ToNSString; 38using base::SysUTF16ToNSString; 39using content::BrowserThread; 40using content::DragDownloadFile; 41using content::DropData; 42using content::PromiseFileFinalizer; 43using content::RenderViewHostImpl; 44 45namespace { 46 47// An unofficial standard pasteboard title type to be provided alongside the 48// |NSURLPboardType|. 49NSString* const kNSURLTitlePboardType = @"public.url-name"; 50 51// Converts a base::string16 into a FilePath. Use this method instead of 52// -[NSString fileSystemRepresentation] to prevent exceptions from being thrown. 53// See http://crbug.com/78782 for more info. 54base::FilePath FilePathFromFilename(const base::string16& filename) { 55 NSString* str = SysUTF16ToNSString(filename); 56 char buf[MAXPATHLEN]; 57 if (![str getFileSystemRepresentation:buf maxLength:sizeof(buf)]) 58 return base::FilePath(); 59 return base::FilePath(buf); 60} 61 62// Returns a filename appropriate for the drop data 63// TODO(viettrungluu): Refactor to make it common across platforms, 64// and move it somewhere sensible. 65base::FilePath GetFileNameFromDragData(const DropData& drop_data) { 66 base::FilePath file_name( 67 FilePathFromFilename(drop_data.file_description_filename)); 68 69 // Images without ALT text will only have a file extension so we need to 70 // synthesize one from the provided extension and URL. 71 if (file_name.empty()) { 72 // Retrieve the name from the URL. 73 base::string16 suggested_filename = 74 net::GetSuggestedFilename(drop_data.url, "", "", "", "", ""); 75 const std::string extension = file_name.Extension(); 76 file_name = FilePathFromFilename(suggested_filename); 77 file_name = file_name.ReplaceExtension(extension); 78 } 79 80 return file_name; 81} 82 83// This helper's sole task is to write out data for a promised file; the caller 84// is responsible for opening the file. It takes the drop data and an open file 85// stream. 86void PromiseWriterHelper(const DropData& drop_data, 87 base::File file) { 88 DCHECK(file.IsValid()); 89 file.WriteAtCurrentPos(drop_data.file_contents.data(), 90 drop_data.file_contents.length()); 91} 92 93} // namespace 94 95 96@interface WebDragSource(Private) 97 98- (void)fillPasteboard; 99- (NSImage*)dragImage; 100 101@end // @interface WebDragSource(Private) 102 103 104@implementation WebDragSource 105 106- (id)initWithContents:(content::WebContentsImpl*)contents 107 view:(NSView*)contentsView 108 dropData:(const DropData*)dropData 109 image:(NSImage*)image 110 offset:(NSPoint)offset 111 pasteboard:(NSPasteboard*)pboard 112 dragOperationMask:(NSDragOperation)dragOperationMask { 113 if ((self = [super init])) { 114 contents_ = contents; 115 DCHECK(contents_); 116 117 contentsView_ = contentsView; 118 DCHECK(contentsView_); 119 120 dropData_.reset(new DropData(*dropData)); 121 DCHECK(dropData_.get()); 122 123 dragImage_.reset([image retain]); 124 imageOffset_ = offset; 125 126 pasteboard_.reset([pboard retain]); 127 DCHECK(pasteboard_.get()); 128 129 dragOperationMask_ = dragOperationMask; 130 131 [self fillPasteboard]; 132 } 133 134 return self; 135} 136 137- (void)clearWebContentsView { 138 contents_ = nil; 139 contentsView_ = nil; 140} 141 142- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal { 143 return dragOperationMask_; 144} 145 146- (void)lazyWriteToPasteboard:(NSPasteboard*)pboard forType:(NSString*)type { 147 // NSHTMLPboardType requires the character set to be declared. Otherwise, it 148 // assumes US-ASCII. Awesome. 149 const base::string16 kHtmlHeader = base::ASCIIToUTF16( 150 "<meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">"); 151 152 // Be extra paranoid; avoid crashing. 153 if (!dropData_) { 154 NOTREACHED(); 155 return; 156 } 157 158 // HTML. 159 if ([type isEqualToString:NSHTMLPboardType] || 160 [type isEqualToString:ui::kChromeDragImageHTMLPboardType]) { 161 DCHECK(!dropData_->html.string().empty()); 162 // See comment on |kHtmlHeader| above. 163 [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->html.string()) 164 forType:type]; 165 166 // URL. 167 } else if ([type isEqualToString:NSURLPboardType]) { 168 DCHECK(dropData_->url.is_valid()); 169 NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())]; 170 // If NSURL creation failed, check for a badly-escaped JavaScript URL. 171 // Strip out any existing escapes and then re-escape uniformly. 172 if (!url && dropData_->url.SchemeIs(url::kJavaScriptScheme)) { 173 net::UnescapeRule::Type unescapeRules = 174 net::UnescapeRule::SPACES | 175 net::UnescapeRule::URL_SPECIAL_CHARS | 176 net::UnescapeRule::CONTROL_CHARS; 177 std::string unescapedUrlString = 178 net::UnescapeURLComponent(dropData_->url.spec(), unescapeRules); 179 std::string escapedUrlString = 180 net::EscapeUrlEncodedData(unescapedUrlString, false); 181 url = [NSURL URLWithString:SysUTF8ToNSString(escapedUrlString)]; 182 } 183 [url writeToPasteboard:pboard]; 184 // URL title. 185 } else if ([type isEqualToString:kNSURLTitlePboardType]) { 186 [pboard setString:SysUTF16ToNSString(dropData_->url_title) 187 forType:kNSURLTitlePboardType]; 188 189 // File contents. 190 } else if ([type isEqualToString:base::mac::CFToNSCast(fileUTI_)]) { 191 [pboard setData:[NSData dataWithBytes:dropData_->file_contents.data() 192 length:dropData_->file_contents.length()] 193 forType:base::mac::CFToNSCast(fileUTI_.get())]; 194 195 // Plain text. 196 } else if ([type isEqualToString:NSStringPboardType]) { 197 DCHECK(!dropData_->text.string().empty()); 198 [pboard setString:SysUTF16ToNSString(dropData_->text.string()) 199 forType:NSStringPboardType]; 200 201 // Custom MIME data. 202 } else if ([type isEqualToString:ui::kWebCustomDataPboardType]) { 203 Pickle pickle; 204 ui::WriteCustomDataToPickle(dropData_->custom_data, &pickle); 205 [pboard setData:[NSData dataWithBytes:pickle.data() length:pickle.size()] 206 forType:ui::kWebCustomDataPboardType]; 207 208 // Dummy type. 209 } else if ([type isEqualToString:ui::kChromeDragDummyPboardType]) { 210 // The dummy type _was_ promised and someone decided to call the bluff. 211 [pboard setData:[NSData data] 212 forType:ui::kChromeDragDummyPboardType]; 213 214 // Oops! 215 } else { 216 // Unknown drag pasteboard type. 217 NOTREACHED(); 218 } 219} 220 221- (NSPoint)convertScreenPoint:(NSPoint)screenPoint { 222 DCHECK([contentsView_ window]); 223 NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint]; 224 return [contentsView_ convertPoint:basePoint fromView:nil]; 225} 226 227- (void)startDrag { 228 NSEvent* currentEvent = [NSApp currentEvent]; 229 230 // Synthesize an event for dragging, since we can't be sure that 231 // [NSApp currentEvent] will return a valid dragging event. 232 NSWindow* window = [contentsView_ window]; 233 NSPoint position = [window mouseLocationOutsideOfEventStream]; 234 NSTimeInterval eventTime = [currentEvent timestamp]; 235 NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged 236 location:position 237 modifierFlags:NSLeftMouseDraggedMask 238 timestamp:eventTime 239 windowNumber:[window windowNumber] 240 context:nil 241 eventNumber:0 242 clickCount:1 243 pressure:1.0]; 244 245 if (dragImage_) { 246 position.x -= imageOffset_.x; 247 // Deal with Cocoa's flipped coordinate system. 248 position.y -= [dragImage_.get() size].height - imageOffset_.y; 249 } 250 // Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in 251 // third_party/WebKit/Source/WebKit/mac/Misc/WebNSViewExtras.m. 252 [window dragImage:[self dragImage] 253 at:position 254 offset:NSZeroSize 255 event:dragEvent 256 pasteboard:pasteboard_ 257 source:contentsView_ 258 slideBack:YES]; 259} 260 261- (void)endDragAt:(NSPoint)screenPoint 262 operation:(NSDragOperation)operation { 263 if (!contents_) 264 return; 265 contents_->SystemDragEnded(); 266 267 RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>( 268 contents_->GetRenderViewHost()); 269 if (rvh) { 270 // Convert |screenPoint| to view coordinates and flip it. 271 NSPoint localPoint = NSZeroPoint; 272 if ([contentsView_ window]) 273 localPoint = [self convertScreenPoint:screenPoint]; 274 NSRect viewFrame = [contentsView_ frame]; 275 localPoint.y = viewFrame.size.height - localPoint.y; 276 // Flip |screenPoint|. 277 NSRect screenFrame = [[[contentsView_ window] screen] frame]; 278 screenPoint.y = screenFrame.size.height - screenPoint.y; 279 280 // If AppKit returns a copy and move operation, mask off the move bit 281 // because WebCore does not understand what it means to do both, which 282 // results in an assertion failure/renderer crash. 283 if (operation == (NSDragOperationMove | NSDragOperationCopy)) 284 operation &= ~NSDragOperationMove; 285 286 contents_->DragSourceEndedAt(localPoint.x, localPoint.y, screenPoint.x, 287 screenPoint.y, static_cast<blink::WebDragOperation>(operation)); 288 } 289 290 // Make sure the pasteboard owner isn't us. 291 [pasteboard_ declareTypes:[NSArray array] owner:nil]; 292} 293 294- (NSString*)dragPromisedFileTo:(NSString*)path { 295 // Be extra paranoid; avoid crashing. 296 if (!dropData_) { 297 NOTREACHED() << "No drag-and-drop data available for promised file."; 298 return nil; 299 } 300 301 base::FilePath filePath(SysNSStringToUTF8(path)); 302 filePath = filePath.Append(downloadFileName_); 303 304 // CreateFileForDrop() will call base::PathExists(), 305 // which is blocking. Since this operation is already blocking the 306 // UI thread on OSX, it should be reasonable to let it happen. 307 base::ThreadRestrictions::ScopedAllowIO allowIO; 308 base::File file(content::CreateFileForDrop(&filePath)); 309 if (!file.IsValid()) 310 return nil; 311 312 if (downloadURL_.is_valid()) { 313 scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile( 314 filePath, 315 file.Pass(), 316 downloadURL_, 317 content::Referrer(contents_->GetLastCommittedURL(), 318 dropData_->referrer_policy), 319 contents_->GetEncoding(), 320 contents_)); 321 322 // The finalizer will take care of closing and deletion. 323 dragFileDownloader->Start(new PromiseFileFinalizer( 324 dragFileDownloader.get())); 325 } else { 326 // The writer will take care of closing and deletion. 327 BrowserThread::PostTask(BrowserThread::FILE, 328 FROM_HERE, 329 base::Bind(&PromiseWriterHelper, 330 *dropData_, 331 base::Passed(&file))); 332 } 333 334 // The DragDownloadFile constructor may have altered the value of |filePath| 335 // if, say, an existing file at the drop site has the same name. Return the 336 // actual name that was used to write the file. 337 return SysUTF8ToNSString(filePath.BaseName().value()); 338} 339 340@end // @implementation WebDragSource 341 342@implementation WebDragSource (Private) 343 344- (void)fillPasteboard { 345 DCHECK(pasteboard_.get()); 346 347 [pasteboard_ declareTypes:@[ ui::kChromeDragDummyPboardType ] 348 owner:contentsView_]; 349 350 // URL (and title). 351 if (dropData_->url.is_valid()) { 352 [pasteboard_ addTypes:@[ NSURLPboardType, kNSURLTitlePboardType ] 353 owner:contentsView_]; 354 } 355 356 // MIME type. 357 std::string mimeType; 358 359 // File. 360 if (!dropData_->file_contents.empty() || 361 !dropData_->download_metadata.empty()) { 362 if (dropData_->download_metadata.empty()) { 363 downloadFileName_ = GetFileNameFromDragData(*dropData_); 364 net::GetMimeTypeFromExtension(downloadFileName_.Extension(), &mimeType); 365 } else { 366 base::string16 mimeType16; 367 base::FilePath fileName; 368 if (content::ParseDownloadMetadata( 369 dropData_->download_metadata, 370 &mimeType16, 371 &fileName, 372 &downloadURL_)) { 373 // Generate the file name based on both mime type and proposed file 374 // name. 375 std::string defaultName = 376 content::GetContentClient()->browser()->GetDefaultDownloadName(); 377 mimeType = base::UTF16ToUTF8(mimeType16); 378 downloadFileName_ = 379 net::GenerateFileName(downloadURL_, 380 std::string(), 381 std::string(), 382 fileName.value(), 383 mimeType, 384 defaultName); 385 } 386 } 387 388 if (!mimeType.empty()) { 389 base::ScopedCFTypeRef<CFStringRef> mimeTypeCF( 390 base::SysUTF8ToCFStringRef(mimeType)); 391 fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag( 392 kUTTagClassMIMEType, mimeTypeCF.get(), NULL)); 393 394 // File (HFS) promise. 395 // There are two ways to drag/drop files. NSFilesPromisePboardType is the 396 // deprecated way, and kPasteboardTypeFilePromiseContent is the way that 397 // does not work. kPasteboardTypeFilePromiseContent is thoroughly broken: 398 // * API: There is no good way to get the location for the drop. 399 // <http://lists.apple.com/archives/cocoa-dev/2012/Feb/msg00706.html> 400 // <rdar://14943849> <http://openradar.me/14943849> 401 // * Behavior: A file dropped in the Finder is not selected. This can be 402 // worked around by selecting the file in the Finder using AppleEvents, 403 // but the drop target window will come to the front of the Finder's 404 // window list (unlike the previous behavior). <http://crbug.com/278515> 405 // <rdar://14943865> <http://openradar.me/14943865> 406 // * Behavior: Files dragged over app icons in the dock do not highlight 407 // the dock icons, and the dock icons do not accept the drop. 408 // <http://crbug.com/282916> <rdar://14943872> 409 // <http://openradar.me/14943872> 410 // * Behavior: A file dropped onto the desktop is positioned at the upper 411 // right of the desktop rather than at the position at which it was 412 // dropped. <http://crbug.com/284942> <rdar://14943881> 413 // <http://openradar.me/14943881> 414 NSArray* fileUTIList = @[ base::mac::CFToNSCast(fileUTI_.get()) ]; 415 [pasteboard_ addTypes:@[ NSFilesPromisePboardType ] owner:contentsView_]; 416 [pasteboard_ setPropertyList:fileUTIList 417 forType:NSFilesPromisePboardType]; 418 419 if (!dropData_->file_contents.empty()) 420 [pasteboard_ addTypes:fileUTIList owner:contentsView_]; 421 } 422 } 423 424 // HTML. 425 bool hasHTMLData = !dropData_->html.string().empty(); 426 // Mail.app and TextEdit accept drags that have both HTML and image flavors on 427 // them, but don't process them correctly <http://crbug.com/55879>. Therefore, 428 // if there is an image flavor, don't put the HTML data on as HTML, but rather 429 // put it on as this Chrome-only flavor. 430 // 431 // (The only time that Blink fills in the DropData::file_contents is with 432 // an image drop, but the MIME time is tested anyway for paranoia's sake.) 433 bool hasImageData = !dropData_->file_contents.empty() && 434 fileUTI_ && 435 UTTypeConformsTo(fileUTI_.get(), kUTTypeImage); 436 if (hasHTMLData) { 437 if (hasImageData) { 438 [pasteboard_ addTypes:@[ ui::kChromeDragImageHTMLPboardType ] 439 owner:contentsView_]; 440 } else { 441 [pasteboard_ addTypes:@[ NSHTMLPboardType ] owner:contentsView_]; 442 } 443 } 444 445 // Plain text. 446 if (!dropData_->text.string().empty()) { 447 [pasteboard_ addTypes:@[ NSStringPboardType ] 448 owner:contentsView_]; 449 } 450 451 if (!dropData_->custom_data.empty()) { 452 [pasteboard_ addTypes:@[ ui::kWebCustomDataPboardType ] 453 owner:contentsView_]; 454 } 455} 456 457- (NSImage*)dragImage { 458 if (dragImage_) 459 return dragImage_; 460 461 // Default to returning a generic image. 462 return content::GetContentClient()->GetNativeImageNamed( 463 IDR_DEFAULT_FAVICON).ToNSImage(); 464} 465 466@end // @implementation WebDragSource (Private) 467