web_drag_source_mac.mm revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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/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_.get()) { 154 NOTREACHED(); 155 return; 156 } 157 158 // HTML. 159 if ([type isEqualToString:NSHTMLPboardType]) { 160 DCHECK(!dropData_->html.string().empty()); 161 // See comment on |kHtmlHeader| above. 162 [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->html.string()) 163 forType:NSHTMLPboardType]; 164 165 // URL. 166 } else if ([type isEqualToString:NSURLPboardType]) { 167 DCHECK(dropData_->url.is_valid()); 168 NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())]; 169 // If NSURL creation failed, check for a badly-escaped JavaScript URL. 170 // Strip out any existing escapes and then re-escape uniformly. 171 if (!url && dropData_->url.SchemeIs(chrome::kJavaScriptScheme)) { 172 net::UnescapeRule::Type unescapeRules = 173 net::UnescapeRule::SPACES | 174 net::UnescapeRule::URL_SPECIAL_CHARS | 175 net::UnescapeRule::CONTROL_CHARS; 176 std::string unescapedUrlString = 177 net::UnescapeURLComponent(dropData_->url.spec(), unescapeRules); 178 std::string escapedUrlString = 179 net::EscapeUrlEncodedData(unescapedUrlString, false); 180 url = [NSURL URLWithString:SysUTF8ToNSString(escapedUrlString)]; 181 } 182 [url writeToPasteboard:pboard]; 183 // URL title. 184 } else if ([type isEqualToString:kNSURLTitlePboardType]) { 185 [pboard setString:SysUTF16ToNSString(dropData_->url_title) 186 forType:kNSURLTitlePboardType]; 187 188 // File contents. 189 } else if ([type isEqualToString:base::mac::CFToNSCast(fileUTI_.get())]) { 190 [pboard setData:[NSData dataWithBytes:dropData_->file_contents.data() 191 length:dropData_->file_contents.length()] 192 forType:base::mac::CFToNSCast(fileUTI_.get())]; 193 194 // Plain text. 195 } else if ([type isEqualToString:NSStringPboardType]) { 196 DCHECK(!dropData_->text.string().empty()); 197 [pboard setString:SysUTF16ToNSString(dropData_->text.string()) 198 forType:NSStringPboardType]; 199 200 // Custom MIME data. 201 } else if ([type isEqualToString:ui::kWebCustomDataPboardType]) { 202 Pickle pickle; 203 ui::WriteCustomDataToPickle(dropData_->custom_data, &pickle); 204 [pboard setData:[NSData dataWithBytes:pickle.data() length:pickle.size()] 205 forType:ui::kWebCustomDataPboardType]; 206 207 // Dummy type. 208 } else if ([type isEqualToString:ui::kChromeDragDummyPboardType]) { 209 // The dummy type _was_ promised and someone decided to call the bluff. 210 [pboard setData:[NSData data] 211 forType:ui::kChromeDragDummyPboardType]; 212 213 // Oops! 214 } else { 215 // Unknown drag pasteboard type. 216 NOTREACHED(); 217 } 218} 219 220- (NSPoint)convertScreenPoint:(NSPoint)screenPoint { 221 DCHECK([contentsView_ window]); 222 NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint]; 223 return [contentsView_ convertPoint:basePoint fromView:nil]; 224} 225 226- (void)startDrag { 227 NSEvent* currentEvent = [NSApp currentEvent]; 228 229 // Synthesize an event for dragging, since we can't be sure that 230 // [NSApp currentEvent] will return a valid dragging event. 231 NSWindow* window = [contentsView_ window]; 232 NSPoint position = [window mouseLocationOutsideOfEventStream]; 233 NSTimeInterval eventTime = [currentEvent timestamp]; 234 NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged 235 location:position 236 modifierFlags:NSLeftMouseDraggedMask 237 timestamp:eventTime 238 windowNumber:[window windowNumber] 239 context:nil 240 eventNumber:0 241 clickCount:1 242 pressure:1.0]; 243 244 if (dragImage_) { 245 position.x -= imageOffset_.x; 246 // Deal with Cocoa's flipped coordinate system. 247 position.y -= [dragImage_.get() size].height - imageOffset_.y; 248 } 249 // Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in 250 // third_party/WebKit/Source/WebKit/mac/Misc/WebNSViewExtras.m. 251 [window dragImage:[self dragImage] 252 at:position 253 offset:NSZeroSize 254 event:dragEvent 255 pasteboard:pasteboard_ 256 source:contentsView_ 257 slideBack:YES]; 258} 259 260- (void)endDragAt:(NSPoint)screenPoint 261 operation:(NSDragOperation)operation { 262 if (!contents_) 263 return; 264 contents_->SystemDragEnded(); 265 266 RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>( 267 contents_->GetRenderViewHost()); 268 if (rvh) { 269 // Convert |screenPoint| to view coordinates and flip it. 270 NSPoint localPoint = NSMakePoint(0, 0); 271 if ([contentsView_ window]) 272 localPoint = [self convertScreenPoint:screenPoint]; 273 NSRect viewFrame = [contentsView_ frame]; 274 localPoint.y = viewFrame.size.height - localPoint.y; 275 // Flip |screenPoint|. 276 NSRect screenFrame = [[[contentsView_ window] screen] frame]; 277 screenPoint.y = screenFrame.size.height - screenPoint.y; 278 279 // If AppKit returns a copy and move operation, mask off the move bit 280 // because WebCore does not understand what it means to do both, which 281 // results in an assertion failure/renderer crash. 282 if (operation == (NSDragOperationMove | NSDragOperationCopy)) 283 operation &= ~NSDragOperationMove; 284 285 rvh->DragSourceEndedAt(localPoint.x, localPoint.y, 286 screenPoint.x, screenPoint.y, 287 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 rvh->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_.get()) { 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.get()) 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_ 369 declareTypes:[NSArray arrayWithObject:ui::kChromeDragDummyPboardType] 370 owner:contentsView_]; 371 372 // URL (and title). 373 if (dropData_->url.is_valid()) 374 [pasteboard_ addTypes:[NSArray arrayWithObjects:NSURLPboardType, 375 kNSURLTitlePboardType, nil] 376 owner:contentsView_]; 377 378 // MIME type. 379 std::string mimeType; 380 381 // File extension. 382 std::string fileExtension; 383 384 // File. 385 if (!dropData_->file_contents.empty() || 386 !dropData_->download_metadata.empty()) { 387 if (dropData_->download_metadata.empty()) { 388 fileExtension = GetFileNameFromDragData(*dropData_).Extension(); 389 net::GetMimeTypeFromExtension(fileExtension, &mimeType); 390 } else { 391 string16 mimeType16; 392 base::FilePath fileName; 393 if (content::ParseDownloadMetadata( 394 dropData_->download_metadata, 395 &mimeType16, 396 &fileName, 397 &downloadURL_)) { 398 // Generate the file name based on both mime type and proposed file 399 // name. 400 std::string defaultName = 401 content::GetContentClient()->browser()->GetDefaultDownloadName(); 402 downloadFileName_ = 403 net::GenerateFileName(downloadURL_, 404 std::string(), 405 std::string(), 406 fileName.value(), 407 UTF16ToUTF8(mimeType16), 408 defaultName); 409 mimeType = UTF16ToUTF8(mimeType16); 410 fileExtension = downloadFileName_.Extension(); 411 } 412 } 413 414 if (!mimeType.empty()) { 415 base::mac::ScopedCFTypeRef<CFStringRef> mimeTypeCF( 416 base::SysUTF8ToCFStringRef(mimeType)); 417 fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag( 418 kUTTagClassMIMEType, mimeTypeCF.get(), NULL)); 419 420 // File (HFS) promise. 421 // TODO(avi): Can we switch to kPasteboardTypeFilePromiseContent? 422 NSArray* types = @[NSFilesPromisePboardType]; 423 [pasteboard_ addTypes:types owner:contentsView_]; 424 425 // For the file promise, we need to specify the extension. 426 [pasteboard_ setPropertyList:@[SysUTF8ToNSString(fileExtension.substr(1))] 427 forType:NSFilesPromisePboardType]; 428 429 if (!dropData_->file_contents.empty()) { 430 NSArray* types = @[base::mac::CFToNSCast(fileUTI_.get())]; 431 [pasteboard_ addTypes:types owner:contentsView_]; 432 } 433 } 434 } 435 436 // HTML. 437 bool hasHTMLData = !dropData_->html.string().empty(); 438 // Mail.app and TextEdit accept drags that have both HTML and image flavors on 439 // them, but don't process them correctly <http://crbug.com/55879>. Therefore, 440 // omit the HTML flavor if there is an image flavor. (The only time that 441 // WebKit fills in the WebDropData::file_contents is with an image drop, but 442 // the MIME time is tested anyway for paranoia's sake.) 443 bool hasImageData = !dropData_->file_contents.empty() && 444 fileUTI_ && 445 UTTypeConformsTo(fileUTI_.get(), kUTTypeImage); 446 if (hasHTMLData && !hasImageData) 447 [pasteboard_ addTypes:[NSArray arrayWithObject:NSHTMLPboardType] 448 owner:contentsView_]; 449 450 // Plain text. 451 if (!dropData_->text.string().empty()) 452 [pasteboard_ addTypes:[NSArray arrayWithObject:NSStringPboardType] 453 owner:contentsView_]; 454 455 if (!dropData_->custom_data.empty()) { 456 [pasteboard_ 457 addTypes:[NSArray arrayWithObject:ui::kWebCustomDataPboardType] 458 owner:contentsView_]; 459 } 460} 461 462- (NSImage*)dragImage { 463 if (dragImage_) 464 return dragImage_; 465 466 // Default to returning a generic image. 467 return content::GetContentClient()->GetNativeImageNamed( 468 IDR_DEFAULT_FAVICON).ToNSImage(); 469} 470 471@end // @implementation WebDragSource (Private) 472