| OLD | NEW |
| (Empty) |
| 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/tab_contents/web_drag_source_mac.h" | |
| 6 | |
| 7 #include <sys/param.h> | |
| 8 | |
| 9 #include "base/bind.h" | |
| 10 #include "base/file_path.h" | |
| 11 #include "base/pickle.h" | |
| 12 #include "base/string_util.h" | |
| 13 #include "base/sys_string_conversions.h" | |
| 14 #include "base/threading/thread.h" | |
| 15 #include "base/threading/thread_restrictions.h" | |
| 16 #include "base/utf_string_conversions.h" | |
| 17 #include "content/browser/browser_thread_impl.h" | |
| 18 #include "content/browser/download/drag_download_file.h" | |
| 19 #include "content/browser/download/drag_download_util.h" | |
| 20 #include "content/browser/renderer_host/render_view_host_impl.h" | |
| 21 #include "content/browser/tab_contents/tab_contents.h" | |
| 22 #include "content/public/browser/content_browser_client.h" | |
| 23 #include "content/public/common/content_client.h" | |
| 24 #include "content/public/common/url_constants.h" | |
| 25 #include "net/base/file_stream.h" | |
| 26 #include "net/base/net_util.h" | |
| 27 #include "ui/base/clipboard/custom_data_helper.h" | |
| 28 #include "ui/gfx/mac/nsimage_cache.h" | |
| 29 #include "webkit/glue/webdropdata.h" | |
| 30 | |
| 31 using base::SysNSStringToUTF8; | |
| 32 using base::SysUTF8ToNSString; | |
| 33 using base::SysUTF16ToNSString; | |
| 34 using content::BrowserThread; | |
| 35 using content::RenderViewHostImpl; | |
| 36 using net::FileStream; | |
| 37 | |
| 38 namespace { | |
| 39 | |
| 40 // An unofficial standard pasteboard title type to be provided alongside the | |
| 41 // |NSURLPboardType|. | |
| 42 NSString* const kNSURLTitlePboardType = @"public.url-name"; | |
| 43 | |
| 44 // Converts a string16 into a FilePath. Use this method instead of | |
| 45 // -[NSString fileSystemRepresentation] to prevent exceptions from being thrown. | |
| 46 // See http://crbug.com/78782 for more info. | |
| 47 FilePath FilePathFromFilename(const string16& filename) { | |
| 48 NSString* str = SysUTF16ToNSString(filename); | |
| 49 char buf[MAXPATHLEN]; | |
| 50 if (![str getFileSystemRepresentation:buf maxLength:sizeof(buf)]) | |
| 51 return FilePath(); | |
| 52 return FilePath(buf); | |
| 53 } | |
| 54 | |
| 55 // Returns a filename appropriate for the drop data | |
| 56 // TODO(viettrungluu): Refactor to make it common across platforms, | |
| 57 // and move it somewhere sensible. | |
| 58 FilePath GetFileNameFromDragData(const WebDropData& drop_data) { | |
| 59 FilePath file_name(FilePathFromFilename(drop_data.file_description_filename)); | |
| 60 std::string extension = file_name.Extension(); | |
| 61 file_name = file_name.BaseName().RemoveExtension(); | |
| 62 | |
| 63 // Images without ALT text will only have a file extension so we need to | |
| 64 // synthesize one from the provided extension and URL. | |
| 65 if (file_name.empty()) { | |
| 66 // Retrieve the name from the URL. | |
| 67 string16 suggested_filename = | |
| 68 net::GetSuggestedFilename(drop_data.url, "", "", "", "", ""); | |
| 69 file_name = FilePathFromFilename(suggested_filename); | |
| 70 } | |
| 71 | |
| 72 return file_name.ReplaceExtension(extension); | |
| 73 } | |
| 74 | |
| 75 // This helper's sole task is to write out data for a promised file; the caller | |
| 76 // is responsible for opening the file. It takes the drop data and an open file | |
| 77 // stream. | |
| 78 void PromiseWriterHelper(const WebDropData& drop_data, | |
| 79 FileStream* file_stream) { | |
| 80 DCHECK(file_stream); | |
| 81 file_stream->WriteSync(drop_data.file_contents.data(), | |
| 82 drop_data.file_contents.length()); | |
| 83 | |
| 84 if (file_stream) | |
| 85 file_stream->CloseSync(); | |
| 86 } | |
| 87 | |
| 88 } // namespace | |
| 89 | |
| 90 | |
| 91 @interface WebDragSource(Private) | |
| 92 | |
| 93 - (void)fillPasteboard; | |
| 94 - (NSImage*)dragImage; | |
| 95 | |
| 96 @end // @interface WebDragSource(Private) | |
| 97 | |
| 98 | |
| 99 @implementation WebDragSource | |
| 100 | |
| 101 - (id)initWithContents:(TabContents*)contents | |
| 102 view:(NSView*)contentsView | |
| 103 dropData:(const WebDropData*)dropData | |
| 104 image:(NSImage*)image | |
| 105 offset:(NSPoint)offset | |
| 106 pasteboard:(NSPasteboard*)pboard | |
| 107 dragOperationMask:(NSDragOperation)dragOperationMask { | |
| 108 if ((self = [super init])) { | |
| 109 contents_ = contents; | |
| 110 DCHECK(contents_); | |
| 111 | |
| 112 contentsView_ = contentsView; | |
| 113 DCHECK(contentsView_); | |
| 114 | |
| 115 dropData_.reset(new WebDropData(*dropData)); | |
| 116 DCHECK(dropData_.get()); | |
| 117 | |
| 118 dragImage_.reset([image retain]); | |
| 119 imageOffset_ = offset; | |
| 120 | |
| 121 pasteboard_.reset([pboard retain]); | |
| 122 DCHECK(pasteboard_.get()); | |
| 123 | |
| 124 dragOperationMask_ = dragOperationMask; | |
| 125 | |
| 126 fileExtension_ = nil; | |
| 127 | |
| 128 [self fillPasteboard]; | |
| 129 } | |
| 130 | |
| 131 return self; | |
| 132 } | |
| 133 | |
| 134 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal { | |
| 135 return dragOperationMask_; | |
| 136 } | |
| 137 | |
| 138 - (void)lazyWriteToPasteboard:(NSPasteboard*)pboard forType:(NSString*)type { | |
| 139 // NSHTMLPboardType requires the character set to be declared. Otherwise, it | |
| 140 // assumes US-ASCII. Awesome. | |
| 141 const string16 kHtmlHeader = ASCIIToUTF16( | |
| 142 "<meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">"); | |
| 143 | |
| 144 // Be extra paranoid; avoid crashing. | |
| 145 if (!dropData_.get()) { | |
| 146 NOTREACHED() << "No drag-and-drop data available for lazy write."; | |
| 147 return; | |
| 148 } | |
| 149 | |
| 150 // HTML. | |
| 151 if ([type isEqualToString:NSHTMLPboardType]) { | |
| 152 DCHECK(!dropData_->text_html.empty()); | |
| 153 // See comment on |kHtmlHeader| above. | |
| 154 [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->text_html) | |
| 155 forType:NSHTMLPboardType]; | |
| 156 | |
| 157 // URL. | |
| 158 } else if ([type isEqualToString:NSURLPboardType]) { | |
| 159 DCHECK(dropData_->url.is_valid()); | |
| 160 NSString* urlStr = SysUTF8ToNSString(dropData_->url.spec()); | |
| 161 NSURL* url = [NSURL URLWithString:urlStr]; | |
| 162 // If NSURL creation failed, check for a badly-escaped JavaScript URL. | |
| 163 // Strip out any existing escapes and then re-escape uniformly. | |
| 164 if (!url && urlStr && dropData_->url.SchemeIs(chrome::kJavaScriptScheme)) { | |
| 165 NSString* unEscapedStr = [urlStr | |
| 166 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; | |
| 167 NSString* escapedStr = [unEscapedStr | |
| 168 stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; | |
| 169 url = [NSURL URLWithString:escapedStr]; | |
| 170 } | |
| 171 [url writeToPasteboard:pboard]; | |
| 172 // URL title. | |
| 173 } else if ([type isEqualToString:kNSURLTitlePboardType]) { | |
| 174 [pboard setString:SysUTF16ToNSString(dropData_->url_title) | |
| 175 forType:kNSURLTitlePboardType]; | |
| 176 | |
| 177 // File contents. | |
| 178 } else if ([type isEqualToString:NSFileContentsPboardType] || | |
| 179 (fileExtension_ && | |
| 180 [type isEqualToString:NSCreateFileContentsPboardType(fileExtension_)])) { | |
| 181 // TODO(viettrungluu: find something which is known to accept | |
| 182 // NSFileContentsPboardType to check that this actually works! | |
| 183 scoped_nsobject<NSFileWrapper> file_wrapper( | |
| 184 [[NSFileWrapper alloc] initRegularFileWithContents:[NSData | |
| 185 dataWithBytes:dropData_->file_contents.data() | |
| 186 length:dropData_->file_contents.length()]]); | |
| 187 [file_wrapper setPreferredFilename:SysUTF8ToNSString( | |
| 188 GetFileNameFromDragData(*dropData_).value())]; | |
| 189 [pboard writeFileWrapper:file_wrapper]; | |
| 190 | |
| 191 // TIFF. | |
| 192 } else if ([type isEqualToString:NSTIFFPboardType]) { | |
| 193 // TODO(viettrungluu): This is a bit odd since we rely on Cocoa to render | |
| 194 // our image into a TIFF. This is also suboptimal since this is all done | |
| 195 // synchronously. I'm not sure there's much we can easily do about it. | |
| 196 scoped_nsobject<NSImage> image( | |
| 197 [[NSImage alloc] initWithData:[NSData | |
| 198 dataWithBytes:dropData_->file_contents.data() | |
| 199 length:dropData_->file_contents.length()]]); | |
| 200 [pboard setData:[image TIFFRepresentation] forType:NSTIFFPboardType]; | |
| 201 | |
| 202 // Plain text. | |
| 203 } else if ([type isEqualToString:NSStringPboardType]) { | |
| 204 DCHECK(!dropData_->plain_text.empty()); | |
| 205 [pboard setString:SysUTF16ToNSString(dropData_->plain_text) | |
| 206 forType:NSStringPboardType]; | |
| 207 | |
| 208 // Custom MIME data. | |
| 209 } else if ([type isEqualToString:ui::kWebCustomDataPboardType]) { | |
| 210 Pickle pickle; | |
| 211 ui::WriteCustomDataToPickle(dropData_->custom_data, &pickle); | |
| 212 [pboard setData:[NSData dataWithBytes:pickle.data() length:pickle.size()] | |
| 213 forType:ui::kWebCustomDataPboardType]; | |
| 214 | |
| 215 // Oops! | |
| 216 } else { | |
| 217 NOTREACHED() << "Asked for a drag pasteboard type we didn't offer."; | |
| 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 contents_->SystemDragEnded(); | |
| 264 | |
| 265 RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>( | |
| 266 contents_->GetRenderViewHost()); | |
| 267 if (rvh) { | |
| 268 // Convert |screenPoint| to view coordinates and flip it. | |
| 269 NSPoint localPoint = NSMakePoint(0, 0); | |
| 270 if ([contentsView_ window]) | |
| 271 localPoint = [self convertScreenPoint:screenPoint]; | |
| 272 NSRect viewFrame = [contentsView_ frame]; | |
| 273 localPoint.y = viewFrame.size.height - localPoint.y; | |
| 274 // Flip |screenPoint|. | |
| 275 NSRect screenFrame = [[[contentsView_ window] screen] frame]; | |
| 276 screenPoint.y = screenFrame.size.height - screenPoint.y; | |
| 277 | |
| 278 // If AppKit returns a copy and move operation, mask off the move bit | |
| 279 // because WebCore does not understand what it means to do both, which | |
| 280 // results in an assertion failure/renderer crash. | |
| 281 if (operation == (NSDragOperationMove | NSDragOperationCopy)) | |
| 282 operation &= ~NSDragOperationMove; | |
| 283 | |
| 284 rvh->DragSourceEndedAt(localPoint.x, localPoint.y, | |
| 285 screenPoint.x, screenPoint.y, | |
| 286 static_cast<WebKit::WebDragOperation>(operation)); | |
| 287 } | |
| 288 | |
| 289 // Make sure the pasteboard owner isn't us. | |
| 290 [pasteboard_ declareTypes:[NSArray array] owner:nil]; | |
| 291 } | |
| 292 | |
| 293 - (void)moveDragTo:(NSPoint)screenPoint { | |
| 294 RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>( | |
| 295 contents_->GetRenderViewHost()); | |
| 296 if (rvh) { | |
| 297 // Convert |screenPoint| to view coordinates and flip it. | |
| 298 NSPoint localPoint = NSMakePoint(0, 0); | |
| 299 if ([contentsView_ window]) | |
| 300 localPoint = [self convertScreenPoint:screenPoint]; | |
| 301 NSRect viewFrame = [contentsView_ frame]; | |
| 302 localPoint.y = viewFrame.size.height - localPoint.y; | |
| 303 // Flip |screenPoint|. | |
| 304 NSRect screenFrame = [[[contentsView_ window] screen] frame]; | |
| 305 screenPoint.y = screenFrame.size.height - screenPoint.y; | |
| 306 | |
| 307 rvh->DragSourceMovedTo(localPoint.x, localPoint.y, | |
| 308 screenPoint.x, screenPoint.y); | |
| 309 } | |
| 310 } | |
| 311 | |
| 312 - (NSString*)dragPromisedFileTo:(NSString*)path { | |
| 313 // Be extra paranoid; avoid crashing. | |
| 314 if (!dropData_.get()) { | |
| 315 NOTREACHED() << "No drag-and-drop data available for promised file."; | |
| 316 return nil; | |
| 317 } | |
| 318 | |
| 319 FilePath fileName = downloadFileName_.empty() ? | |
| 320 GetFileNameFromDragData(*dropData_) : downloadFileName_; | |
| 321 FilePath filePath(SysNSStringToUTF8(path)); | |
| 322 filePath = filePath.Append(fileName); | |
| 323 | |
| 324 // CreateFileStreamForDrop() will call file_util::PathExists(), | |
| 325 // which is blocking. Since this operation is already blocking the | |
| 326 // UI thread on OSX, it should be reasonable to let it happen. | |
| 327 base::ThreadRestrictions::ScopedAllowIO allowIO; | |
| 328 FileStream* fileStream = | |
| 329 drag_download_util::CreateFileStreamForDrop( | |
| 330 &filePath, content::GetContentClient()->browser()->GetNetLog()); | |
| 331 if (!fileStream) | |
| 332 return nil; | |
| 333 | |
| 334 if (downloadURL_.is_valid()) { | |
| 335 scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile( | |
| 336 filePath, | |
| 337 linked_ptr<net::FileStream>(fileStream), | |
| 338 downloadURL_, | |
| 339 contents_->GetURL(), | |
| 340 contents_->GetEncoding(), | |
| 341 contents_)); | |
| 342 | |
| 343 // The finalizer will take care of closing and deletion. | |
| 344 dragFileDownloader->Start( | |
| 345 new drag_download_util::PromiseFileFinalizer(dragFileDownloader)); | |
| 346 } else { | |
| 347 // The writer will take care of closing and deletion. | |
| 348 BrowserThread::PostTask(BrowserThread::FILE, | |
| 349 FROM_HERE, | |
| 350 base::Bind(&PromiseWriterHelper, | |
| 351 *dropData_, | |
| 352 base::Owned(fileStream))); | |
| 353 } | |
| 354 | |
| 355 // Once we've created the file, we should return the file name. | |
| 356 return SysUTF8ToNSString(filePath.BaseName().value()); | |
| 357 } | |
| 358 | |
| 359 @end // @implementation WebDragSource | |
| 360 | |
| 361 | |
| 362 @implementation WebDragSource (Private) | |
| 363 | |
| 364 - (void)fillPasteboard { | |
| 365 DCHECK(pasteboard_.get()); | |
| 366 | |
| 367 [pasteboard_ declareTypes:[NSArray array] owner:contentsView_]; | |
| 368 | |
| 369 // HTML. | |
| 370 if (!dropData_->text_html.empty()) | |
| 371 [pasteboard_ addTypes:[NSArray arrayWithObject:NSHTMLPboardType] | |
| 372 owner:contentsView_]; | |
| 373 | |
| 374 // URL (and title). | |
| 375 if (dropData_->url.is_valid()) | |
| 376 [pasteboard_ addTypes:[NSArray arrayWithObjects:NSURLPboardType, | |
| 377 kNSURLTitlePboardType, nil] | |
| 378 owner:contentsView_]; | |
| 379 | |
| 380 std::string fileExtension; | |
| 381 | |
| 382 // File. | |
| 383 if (!dropData_->file_contents.empty() || | |
| 384 !dropData_->download_metadata.empty()) { | |
| 385 if (dropData_->download_metadata.empty()) { | |
| 386 fileExtension = GetFileNameFromDragData(*dropData_).Extension(); | |
| 387 } else { | |
| 388 string16 mimeType; | |
| 389 FilePath fileName; | |
| 390 if (drag_download_util::ParseDownloadMetadata( | |
| 391 dropData_->download_metadata, | |
| 392 &mimeType, | |
| 393 &fileName, | |
| 394 &downloadURL_)) { | |
| 395 // Generate the file name based on both mime type and proposed file | |
| 396 // name. | |
| 397 std::string defaultName = | |
| 398 content::GetContentClient()->browser()->GetDefaultDownloadName(); | |
| 399 downloadFileName_ = | |
| 400 net::GenerateFileName(downloadURL_, | |
| 401 std::string(), | |
| 402 std::string(), | |
| 403 fileName.value(), | |
| 404 UTF16ToUTF8(mimeType), | |
| 405 defaultName); | |
| 406 fileExtension = downloadFileName_.Extension(); | |
| 407 } | |
| 408 } | |
| 409 | |
| 410 if (!fileExtension.empty()) { | |
| 411 // Strip the leading dot. | |
| 412 fileExtension_ = SysUTF8ToNSString(fileExtension.substr(1)); | |
| 413 // File contents (with and without specific type), and file (HFS) promise. | |
| 414 // TODO(viettrungluu): others? | |
| 415 NSArray* types = [NSArray arrayWithObjects: | |
| 416 NSFileContentsPboardType, | |
| 417 NSCreateFileContentsPboardType(fileExtension_), | |
| 418 NSFilesPromisePboardType, | |
| 419 nil]; | |
| 420 [pasteboard_ addTypes:types owner:contentsView_]; | |
| 421 | |
| 422 if (!dropData_->file_contents.empty()) { | |
| 423 [pasteboard_ addTypes:[NSArray arrayWithObject:NSTIFFPboardType] | |
| 424 owner:contentsView_]; | |
| 425 } | |
| 426 | |
| 427 // For the file promise, we need to specify the extension. | |
| 428 [pasteboard_ setPropertyList:[NSArray arrayWithObject:fileExtension_] | |
| 429 forType:NSFilesPromisePboardType]; | |
| 430 } | |
| 431 } | |
| 432 | |
| 433 // Plain text. | |
| 434 if (!dropData_->plain_text.empty()) | |
| 435 [pasteboard_ addTypes:[NSArray arrayWithObject:NSStringPboardType] | |
| 436 owner:contentsView_]; | |
| 437 | |
| 438 if (!dropData_->custom_data.empty()) { | |
| 439 [pasteboard_ | |
| 440 addTypes:[NSArray arrayWithObject:ui::kWebCustomDataPboardType] | |
| 441 owner:contentsView_]; | |
| 442 } | |
| 443 } | |
| 444 | |
| 445 - (NSImage*)dragImage { | |
| 446 if (dragImage_) | |
| 447 return dragImage_; | |
| 448 | |
| 449 // Default to returning a generic image. | |
| 450 return gfx::GetCachedImageWithName(@"nav.pdf"); | |
| 451 } | |
| 452 | |
| 453 @end // @implementation WebDragSource (Private) | |
| OLD | NEW |