1 // Copyright (c) 2011 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 "chrome/browser/ui/cocoa/tab_contents/web_drag_source.h" 6 7 #include <sys/param.h> 8 9 #include "app/mac/nsimage_cache.h" 10 #include "base/file_path.h" 11 #include "base/string_util.h" 12 #include "base/sys_string_conversions.h" 13 #include "base/task.h" 14 #include "base/threading/thread.h" 15 #include "base/threading/thread_restrictions.h" 16 #include "base/utf_string_conversions.h" 17 #import "chrome/app/breakpad_mac.h" 18 #include "chrome/browser/browser_process.h" 19 #include "chrome/browser/download/download_manager.h" 20 #include "chrome/browser/download/download_util.h" 21 #include "chrome/browser/download/drag_download_file.h" 22 #include "chrome/browser/download/drag_download_util.h" 23 #include "chrome/browser/tab_contents/tab_contents_view_mac.h" 24 #include "content/browser/renderer_host/render_view_host.h" 25 #include "content/browser/tab_contents/tab_contents.h" 26 #include "net/base/file_stream.h" 27 #include "net/base/net_util.h" 28 #import "third_party/mozilla/NSPasteboard+Utils.h" 29 #include "webkit/glue/webdropdata.h" 30 31 using base::SysNSStringToUTF8; 32 using base::SysUTF8ToNSString; 33 using base::SysUTF16ToNSString; 34 using net::FileStream; 35 36 37 namespace { 38 39 // An unofficial standard pasteboard title type to be provided alongside the 40 // |NSURLPboardType|. 41 NSString* const kNSURLTitlePboardType = @"public.url-name"; 42 43 // Converts a string16 into a FilePath. Use this method instead of 44 // -[NSString fileSystemRepresentation] to prevent exceptions from being thrown. 45 // See http://crbug.com/78782 for more info. 46 FilePath FilePathFromFilename(const string16& filename) { 47 NSString* str = SysUTF16ToNSString(filename); 48 char buf[MAXPATHLEN]; 49 if (![str getFileSystemRepresentation:buf maxLength:sizeof(buf)]) 50 return FilePath(); 51 return FilePath(buf); 52 } 53 54 // Returns a filename appropriate for the drop data 55 // TODO(viettrungluu): Refactor to make it common across platforms, 56 // and move it somewhere sensible. 57 FilePath GetFileNameFromDragData(const WebDropData& drop_data) { 58 // Set a breakpad key for the scope of this function to help debug 59 // http://crbug.com/78782 60 static NSString* const kUrlKey = @"drop_data_url"; 61 NSString* value = SysUTF8ToNSString(drop_data.url.spec()); 62 ScopedCrashKey key(kUrlKey, value); 63 64 // Images without ALT text will only have a file extension so we need to 65 // synthesize one from the provided extension and URL. 66 FilePath file_name(FilePathFromFilename(drop_data.file_description_filename)); 67 file_name = file_name.BaseName().RemoveExtension(); 68 69 if (file_name.empty()) { 70 // Retrieve the name from the URL. 71 string16 suggested_filename = 72 net::GetSuggestedFilename(drop_data.url, "", "", string16()); 73 file_name = FilePathFromFilename(suggested_filename); 74 } 75 76 file_name = file_name.ReplaceExtension(UTF16ToUTF8(drop_data.file_extension)); 77 78 return file_name; 79 } 80 81 // This class's sole task is to write out data for a promised file; the caller 82 // is responsible for opening the file. 83 class PromiseWriterTask : public Task { 84 public: 85 // Assumes ownership of file_stream. 86 PromiseWriterTask(const WebDropData& drop_data, 87 FileStream* file_stream); 88 virtual ~PromiseWriterTask(); 89 virtual void Run(); 90 91 private: 92 WebDropData drop_data_; 93 94 // This class takes ownership of file_stream_ and will close and delete it. 95 scoped_ptr<FileStream> file_stream_; 96 }; 97 98 // Takes the drop data and an open file stream (which it takes ownership of and 99 // will close and delete). 100 PromiseWriterTask::PromiseWriterTask(const WebDropData& drop_data, 101 FileStream* file_stream) : 102 drop_data_(drop_data) { 103 file_stream_.reset(file_stream); 104 DCHECK(file_stream_.get()); 105 } 106 107 PromiseWriterTask::~PromiseWriterTask() { 108 DCHECK(file_stream_.get()); 109 if (file_stream_.get()) 110 file_stream_->Close(); 111 } 112 113 void PromiseWriterTask::Run() { 114 CHECK(file_stream_.get()); 115 file_stream_->Write(drop_data_.file_contents.data(), 116 drop_data_.file_contents.length(), 117 NULL); 118 119 // Let our destructor take care of business. 120 } 121 122 } // namespace 123 124 125 @interface WebDragSource(Private) 126 127 - (void)fillPasteboard; 128 - (NSImage*)dragImage; 129 130 @end // @interface WebDragSource(Private) 131 132 133 @implementation WebDragSource 134 135 - (id)initWithContentsView:(TabContentsViewCocoa*)contentsView 136 dropData:(const WebDropData*)dropData 137 image:(NSImage*)image 138 offset:(NSPoint)offset 139 pasteboard:(NSPasteboard*)pboard 140 dragOperationMask:(NSDragOperation)dragOperationMask { 141 if ((self = [super init])) { 142 contentsView_ = contentsView; 143 DCHECK(contentsView_); 144 145 dropData_.reset(new WebDropData(*dropData)); 146 DCHECK(dropData_.get()); 147 148 dragImage_.reset([image retain]); 149 imageOffset_ = offset; 150 151 pasteboard_.reset([pboard retain]); 152 DCHECK(pasteboard_.get()); 153 154 dragOperationMask_ = dragOperationMask; 155 156 [self fillPasteboard]; 157 } 158 159 return self; 160 } 161 162 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal { 163 return dragOperationMask_; 164 } 165 166 - (void)lazyWriteToPasteboard:(NSPasteboard*)pboard forType:(NSString*)type { 167 // NSHTMLPboardType requires the character set to be declared. Otherwise, it 168 // assumes US-ASCII. Awesome. 169 static const string16 kHtmlHeader = 170 ASCIIToUTF16("<meta http-equiv=\"Content-Type\" " 171 "content=\"text/html;charset=UTF-8\">"); 172 173 // Be extra paranoid; avoid crashing. 174 if (!dropData_.get()) { 175 NOTREACHED() << "No drag-and-drop data available for lazy write."; 176 return; 177 } 178 179 // HTML. 180 if ([type isEqualToString:NSHTMLPboardType]) { 181 DCHECK(!dropData_->text_html.empty()); 182 // See comment on |kHtmlHeader| above. 183 [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->text_html) 184 forType:NSHTMLPboardType]; 185 186 // URL. 187 } else if ([type isEqualToString:NSURLPboardType]) { 188 DCHECK(dropData_->url.is_valid()); 189 NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())]; 190 [url writeToPasteboard:pboard]; 191 192 // URL title. 193 } else if ([type isEqualToString:kNSURLTitlePboardType]) { 194 [pboard setString:SysUTF16ToNSString(dropData_->url_title) 195 forType:kNSURLTitlePboardType]; 196 197 // File contents. 198 } else if ([type isEqualToString:NSFileContentsPboardType] || 199 [type isEqualToString:NSCreateFileContentsPboardType( 200 SysUTF16ToNSString(dropData_->file_extension))]) { 201 // TODO(viettrungluu: find something which is known to accept 202 // NSFileContentsPboardType to check that this actually works! 203 scoped_nsobject<NSFileWrapper> file_wrapper( 204 [[NSFileWrapper alloc] initRegularFileWithContents:[NSData 205 dataWithBytes:dropData_->file_contents.data() 206 length:dropData_->file_contents.length()]]); 207 [file_wrapper setPreferredFilename:SysUTF8ToNSString( 208 GetFileNameFromDragData(*dropData_).value())]; 209 [pboard writeFileWrapper:file_wrapper]; 210 211 // TIFF. 212 } else if ([type isEqualToString:NSTIFFPboardType]) { 213 // TODO(viettrungluu): This is a bit odd since we rely on Cocoa to render 214 // our image into a TIFF. This is also suboptimal since this is all done 215 // synchronously. I'm not sure there's much we can easily do about it. 216 scoped_nsobject<NSImage> image( 217 [[NSImage alloc] initWithData:[NSData 218 dataWithBytes:dropData_->file_contents.data() 219 length:dropData_->file_contents.length()]]); 220 [pboard setData:[image TIFFRepresentation] forType:NSTIFFPboardType]; 221 222 // Plain text. 223 } else if ([type isEqualToString:NSStringPboardType]) { 224 DCHECK(!dropData_->plain_text.empty()); 225 [pboard setString:SysUTF16ToNSString(dropData_->plain_text) 226 forType:NSStringPboardType]; 227 228 // Oops! 229 } else { 230 NOTREACHED() << "Asked for a drag pasteboard type we didn't offer."; 231 } 232 } 233 234 - (NSPoint)convertScreenPoint:(NSPoint)screenPoint { 235 DCHECK([contentsView_ window]); 236 NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint]; 237 return [contentsView_ convertPoint:basePoint fromView:nil]; 238 } 239 240 - (void)startDrag { 241 NSEvent* currentEvent = [NSApp currentEvent]; 242 243 // Synthesize an event for dragging, since we can't be sure that 244 // [NSApp currentEvent] will return a valid dragging event. 245 NSWindow* window = [contentsView_ window]; 246 NSPoint position = [window mouseLocationOutsideOfEventStream]; 247 NSTimeInterval eventTime = [currentEvent timestamp]; 248 NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged 249 location:position 250 modifierFlags:NSLeftMouseDraggedMask 251 timestamp:eventTime 252 windowNumber:[window windowNumber] 253 context:nil 254 eventNumber:0 255 clickCount:1 256 pressure:1.0]; 257 258 if (dragImage_) { 259 position.x -= imageOffset_.x; 260 // Deal with Cocoa's flipped coordinate system. 261 position.y -= [dragImage_.get() size].height - imageOffset_.y; 262 } 263 // Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in 264 // third_party/WebKit/Source/WebKit/mac/Misc/WebNSViewExtras.m. 265 [window dragImage:[self dragImage] 266 at:position 267 offset:NSZeroSize 268 event:dragEvent 269 pasteboard:pasteboard_ 270 source:contentsView_ 271 slideBack:YES]; 272 } 273 274 - (void)endDragAt:(NSPoint)screenPoint 275 operation:(NSDragOperation)operation { 276 [contentsView_ tabContents]->SystemDragEnded(); 277 278 RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host(); 279 if (rvh) { 280 // Convert |screenPoint| to view coordinates and flip it. 281 NSPoint localPoint = NSMakePoint(0, 0); 282 if ([contentsView_ window]) 283 localPoint = [self convertScreenPoint:screenPoint]; 284 NSRect viewFrame = [contentsView_ frame]; 285 localPoint.y = viewFrame.size.height - localPoint.y; 286 // Flip |screenPoint|. 287 NSRect screenFrame = [[[contentsView_ window] screen] frame]; 288 screenPoint.y = screenFrame.size.height - screenPoint.y; 289 290 // If AppKit returns a copy and move operation, mask off the move bit 291 // because WebCore does not understand what it means to do both, which 292 // results in an assertion failure/renderer crash. 293 if (operation == (NSDragOperationMove | NSDragOperationCopy)) 294 operation &= ~NSDragOperationMove; 295 296 rvh->DragSourceEndedAt(localPoint.x, localPoint.y, 297 screenPoint.x, screenPoint.y, 298 static_cast<WebKit::WebDragOperation>(operation)); 299 } 300 301 // Make sure the pasteboard owner isn't us. 302 [pasteboard_ declareTypes:[NSArray array] owner:nil]; 303 } 304 305 - (void)moveDragTo:(NSPoint)screenPoint { 306 RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host(); 307 if (rvh) { 308 // Convert |screenPoint| to view coordinates and flip it. 309 NSPoint localPoint = NSMakePoint(0, 0); 310 if ([contentsView_ window]) 311 localPoint = [self convertScreenPoint:screenPoint]; 312 NSRect viewFrame = [contentsView_ frame]; 313 localPoint.y = viewFrame.size.height - localPoint.y; 314 // Flip |screenPoint|. 315 NSRect screenFrame = [[[contentsView_ window] screen] frame]; 316 screenPoint.y = screenFrame.size.height - screenPoint.y; 317 318 rvh->DragSourceMovedTo(localPoint.x, localPoint.y, 319 screenPoint.x, screenPoint.y); 320 } 321 } 322 323 - (NSString*)dragPromisedFileTo:(NSString*)path { 324 // Be extra paranoid; avoid crashing. 325 if (!dropData_.get()) { 326 NOTREACHED() << "No drag-and-drop data available for promised file."; 327 return nil; 328 } 329 330 FilePath fileName = downloadFileName_.empty() ? 331 GetFileNameFromDragData(*dropData_) : downloadFileName_; 332 FilePath filePath(SysNSStringToUTF8(path)); 333 filePath = filePath.Append(fileName); 334 335 // CreateFileStreamForDrop() will call file_util::PathExists(), 336 // which is blocking. Since this operation is already blocking the 337 // UI thread on OSX, it should be reasonable to let it happen. 338 base::ThreadRestrictions::ScopedAllowIO allowIO; 339 FileStream* fileStream = 340 drag_download_util::CreateFileStreamForDrop(&filePath); 341 if (!fileStream) 342 return nil; 343 344 if (downloadURL_.is_valid()) { 345 TabContents* tabContents = [contentsView_ tabContents]; 346 scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile( 347 filePath, 348 linked_ptr<net::FileStream>(fileStream), 349 downloadURL_, 350 tabContents->GetURL(), 351 tabContents->encoding(), 352 tabContents)); 353 354 // The finalizer will take care of closing and deletion. 355 dragFileDownloader->Start( 356 new drag_download_util::PromiseFileFinalizer(dragFileDownloader)); 357 } else { 358 // The writer will take care of closing and deletion. 359 g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE, 360 new PromiseWriterTask(*dropData_, fileStream)); 361 } 362 363 // Once we've created the file, we should return the file name. 364 return SysUTF8ToNSString(filePath.BaseName().value()); 365 } 366 367 @end // @implementation WebDragSource 368 369 370 @implementation WebDragSource (Private) 371 372 - (void)fillPasteboard { 373 DCHECK(pasteboard_.get()); 374 375 [pasteboard_ declareTypes:[NSArray array] owner:contentsView_]; 376 377 // HTML. 378 if (!dropData_->text_html.empty()) 379 [pasteboard_ addTypes:[NSArray arrayWithObject:NSHTMLPboardType] 380 owner:contentsView_]; 381 382 // URL (and title). 383 if (dropData_->url.is_valid()) 384 [pasteboard_ addTypes:[NSArray arrayWithObjects:NSURLPboardType, 385 kNSURLTitlePboardType, nil] 386 owner:contentsView_]; 387 388 // File. 389 if (!dropData_->file_contents.empty() || 390 !dropData_->download_metadata.empty()) { 391 NSString* fileExtension = 0; 392 393 if (dropData_->download_metadata.empty()) { 394 // |dropData_->file_extension| comes with the '.', which we must strip. 395 fileExtension = (dropData_->file_extension.length() > 0) ? 396 SysUTF16ToNSString(dropData_->file_extension.substr(1)) : @""; 397 } else { 398 string16 mimeType; 399 FilePath fileName; 400 if (drag_download_util::ParseDownloadMetadata( 401 dropData_->download_metadata, 402 &mimeType, 403 &fileName, 404 &downloadURL_)) { 405 std::string contentDisposition = 406 "attachment; filename=" + fileName.value(); 407 download_util::GenerateFileName(downloadURL_, 408 contentDisposition, 409 std::string(), 410 UTF16ToUTF8(mimeType), 411 &downloadFileName_); 412 fileExtension = SysUTF8ToNSString(downloadFileName_.Extension()); 413 } 414 } 415 416 if (fileExtension) { 417 // File contents (with and without specific type), file (HFS) promise, 418 // TIFF. 419 // TODO(viettrungluu): others? 420 [pasteboard_ addTypes:[NSArray arrayWithObjects: 421 NSFileContentsPboardType, 422 NSCreateFileContentsPboardType(fileExtension), 423 NSFilesPromisePboardType, 424 NSTIFFPboardType, 425 nil] 426 owner:contentsView_]; 427 428 // For the file promise, we need to specify the extension. 429 [pasteboard_ setPropertyList:[NSArray arrayWithObject:fileExtension] 430 forType:NSFilesPromisePboardType]; 431 } 432 } 433 434 // Plain text. 435 if (!dropData_->plain_text.empty()) 436 [pasteboard_ addTypes:[NSArray arrayWithObject:NSStringPboardType] 437 owner:contentsView_]; 438 } 439 440 - (NSImage*)dragImage { 441 if (dragImage_) 442 return dragImage_; 443 444 // Default to returning a generic image. 445 return app::mac::GetCachedImageWithName(@"nav.pdf"); 446 } 447 448 @end // @implementation WebDragSource (Private) 449