web_drag_source_mac.mm revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
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_path.h" 11#include "base/mac/mac_util.h" 12#include "base/pickle.h" 13#include "base/string_util.h" 14#include "base/strings/sys_string_conversions.h" 15#include "base/threading/thread.h" 16#include "base/threading/thread_restrictions.h" 17#include "base/utf_string_conversions.h" 18#include "content/browser/browser_thread_impl.h" 19#include "content/browser/download/drag_download_file.h" 20#include "content/browser/download/drag_download_util.h" 21#include "content/browser/renderer_host/render_view_host_impl.h" 22#include "content/browser/web_contents/web_contents_impl.h" 23#include "content/public/browser/content_browser_client.h" 24#include "content/public/common/content_client.h" 25#include "content/public/common/url_constants.h" 26#include "grit/ui_resources.h" 27#include "net/base/escape.h" 28#include "net/base/file_stream.h" 29#include "net/base/mime_util.h" 30#include "net/base/net_util.h" 31#include "ui/base/clipboard/custom_data_helper.h" 32#include "ui/base/dragdrop/cocoa_dnd_util.h" 33#include "ui/gfx/image/image.h" 34#include "webkit/glue/webdropdata.h" 35 36using base::SysNSStringToUTF8; 37using base::SysUTF8ToNSString; 38using base::SysUTF16ToNSString; 39using content::BrowserThread; 40using content::DragDownloadFile; 41using content::PromiseFileFinalizer; 42using content::RenderViewHostImpl; 43using net::FileStream; 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 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 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 WebDropData& 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 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 WebDropData& drop_data, 87 scoped_ptr<FileStream> file_stream) { 88 DCHECK(file_stream); 89 file_stream->WriteSync(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 WebDropData*)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 WebDropData(*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 string16 kHtmlHeader = 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(chrome::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 = NSMakePoint(0, 0); 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<WebKit::WebDragOperation>(operation)); 288 } 289 290 // Make sure the pasteboard owner isn't us. 291 [pasteboard_ declareTypes:[NSArray array] owner:nil]; 292} 293 294- (void)moveDragTo:(NSPoint)screenPoint { 295 if (!contents_) 296 return; 297 RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>( 298 contents_->GetRenderViewHost()); 299 if (rvh) { 300 // Convert |screenPoint| to view coordinates and flip it. 301 NSPoint localPoint = NSMakePoint(0, 0); 302 if ([contentsView_ window]) 303 localPoint = [self convertScreenPoint:screenPoint]; 304 NSRect viewFrame = [contentsView_ frame]; 305 localPoint.y = viewFrame.size.height - localPoint.y; 306 // Flip |screenPoint|. 307 NSRect screenFrame = [[[contentsView_ window] screen] frame]; 308 screenPoint.y = screenFrame.size.height - screenPoint.y; 309 310 contents_->DragSourceMovedTo(localPoint.x, localPoint.y, 311 screenPoint.x, screenPoint.y); 312 } 313} 314 315- (NSString*)dragPromisedFileTo:(NSString*)path { 316 // Be extra paranoid; avoid crashing. 317 if (!dropData_) { 318 NOTREACHED() << "No drag-and-drop data available for promised file."; 319 return nil; 320 } 321 322 base::FilePath fileName = downloadFileName_.empty() ? 323 GetFileNameFromDragData(*dropData_) : downloadFileName_; 324 base::FilePath filePath(SysNSStringToUTF8(path)); 325 filePath = filePath.Append(fileName); 326 327 // CreateFileStreamForDrop() will call file_util::PathExists(), 328 // which is blocking. Since this operation is already blocking the 329 // UI thread on OSX, it should be reasonable to let it happen. 330 base::ThreadRestrictions::ScopedAllowIO allowIO; 331 scoped_ptr<FileStream> fileStream(content::CreateFileStreamForDrop( 332 &filePath, content::GetContentClient()->browser()->GetNetLog())); 333 if (!fileStream) 334 return nil; 335 336 if (downloadURL_.is_valid()) { 337 scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile( 338 filePath, 339 fileStream.Pass(), 340 downloadURL_, 341 content::Referrer(contents_->GetURL(), dropData_->referrer_policy), 342 contents_->GetEncoding(), 343 contents_)); 344 345 // The finalizer will take care of closing and deletion. 346 dragFileDownloader->Start(new PromiseFileFinalizer(dragFileDownloader)); 347 } else { 348 // The writer will take care of closing and deletion. 349 BrowserThread::PostTask(BrowserThread::FILE, 350 FROM_HERE, 351 base::Bind(&PromiseWriterHelper, 352 *dropData_, 353 base::Passed(&fileStream))); 354 } 355 356 // Once we've created the file, we should return the file name. 357 return SysUTF8ToNSString(filePath.BaseName().value()); 358} 359 360@end // @implementation WebDragSource 361 362 363@implementation WebDragSource (Private) 364 365- (void)fillPasteboard { 366 DCHECK(pasteboard_.get()); 367 368 [pasteboard_ declareTypes:@[ui::kChromeDragDummyPboardType] 369 owner:contentsView_]; 370 371 // URL (and title). 372 if (dropData_->url.is_valid()) { 373 [pasteboard_ addTypes:@[NSURLPboardType, kNSURLTitlePboardType] 374 owner:contentsView_]; 375 } 376 377 // MIME type. 378 std::string mimeType; 379 380 // File extension. 381 std::string fileExtension; 382 383 // File. 384 if (!dropData_->file_contents.empty() || 385 !dropData_->download_metadata.empty()) { 386 if (dropData_->download_metadata.empty()) { 387 fileExtension = GetFileNameFromDragData(*dropData_).Extension(); 388 net::GetMimeTypeFromExtension(fileExtension, &mimeType); 389 } else { 390 string16 mimeType16; 391 base::FilePath fileName; 392 if (content::ParseDownloadMetadata( 393 dropData_->download_metadata, 394 &mimeType16, 395 &fileName, 396 &downloadURL_)) { 397 // Generate the file name based on both mime type and proposed file 398 // name. 399 std::string defaultName = 400 content::GetContentClient()->browser()->GetDefaultDownloadName(); 401 downloadFileName_ = 402 net::GenerateFileName(downloadURL_, 403 std::string(), 404 std::string(), 405 fileName.value(), 406 UTF16ToUTF8(mimeType16), 407 defaultName); 408 mimeType = UTF16ToUTF8(mimeType16); 409 fileExtension = downloadFileName_.Extension(); 410 } 411 } 412 413 if (!mimeType.empty()) { 414 base::mac::ScopedCFTypeRef<CFStringRef> mimeTypeCF( 415 base::SysUTF8ToCFStringRef(mimeType)); 416 fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag( 417 kUTTagClassMIMEType, mimeTypeCF.get(), NULL)); 418 419 // File (HFS) promise. 420 // TODO(avi): Can we switch to kPasteboardTypeFilePromiseContent? 421 NSArray* types = @[NSFilesPromisePboardType]; 422 [pasteboard_ addTypes:types owner:contentsView_]; 423 424 // For the file promise, we need to specify the extension. 425 [pasteboard_ setPropertyList:@[SysUTF8ToNSString(fileExtension.substr(1))] 426 forType:NSFilesPromisePboardType]; 427 428 if (!dropData_->file_contents.empty()) { 429 NSArray* types = @[base::mac::CFToNSCast(fileUTI_.get())]; 430 [pasteboard_ addTypes:types owner:contentsView_]; 431 } 432 } 433 } 434 435 // HTML. 436 bool hasHTMLData = !dropData_->html.string().empty(); 437 // Mail.app and TextEdit accept drags that have both HTML and image flavors on 438 // them, but don't process them correctly <http://crbug.com/55879>. Therefore, 439 // if there is an image flavor, don't put the HTML data on as HTML, but rather 440 // put it on as this Chrome-only flavor. 441 // 442 // (The only time that Blink fills in the WebDropData::file_contents is with 443 // an image drop, but the MIME time is tested anyway for paranoia's sake.) 444 bool hasImageData = !dropData_->file_contents.empty() && 445 fileUTI_ && 446 UTTypeConformsTo(fileUTI_.get(), kUTTypeImage); 447 if (hasHTMLData) { 448 if (hasImageData) { 449 [pasteboard_ addTypes:@[ui::kChromeDragImageHTMLPboardType] 450 owner:contentsView_]; 451 } else { 452 [pasteboard_ addTypes:@[NSHTMLPboardType] owner:contentsView_]; 453 } 454 } 455 456 // Plain text. 457 if (!dropData_->text.string().empty()) { 458 [pasteboard_ addTypes:@[NSStringPboardType] 459 owner:contentsView_]; 460 } 461 462 if (!dropData_->custom_data.empty()) { 463 [pasteboard_ addTypes:@[ui::kWebCustomDataPboardType] 464 owner:contentsView_]; 465 } 466} 467 468- (NSImage*)dragImage { 469 if (dragImage_) 470 return dragImage_; 471 472 // Default to returning a generic image. 473 return content::GetContentClient()->GetNativeImageNamed( 474 IDR_DEFAULT_FAVICON).ToNSImage(); 475} 476 477@end // @implementation WebDragSource (Private) 478