Re-subimission of https://codereview.chromium.org/1041213003/
[chromium-blink-merge.git] / content / browser / web_contents / web_drag_source_mac.mm
blob57e12a0b04ceb8575c9703a386726e8499f0a4c0
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.
5 #import "content/browser/web_contents/web_drag_source_mac.h"
7 #include <sys/param.h>
9 #include "base/bind.h"
10 #include "base/files/file.h"
11 #include "base/files/file_path.h"
12 #include "base/mac/foundation_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"
36 using base::SysNSStringToUTF8;
37 using base::SysUTF8ToNSString;
38 using base::SysUTF16ToNSString;
39 using content::BrowserThread;
40 using content::DragDownloadFile;
41 using content::DropData;
42 using content::PromiseFileFinalizer;
43 using content::RenderViewHostImpl;
45 namespace {
47 // An unofficial standard pasteboard title type to be provided alongside the
48 // |NSURLPboardType|.
49 NSString* const kNSURLTitlePboardType = @"public.url-name";
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.
54 base::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);
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.
65 base::FilePath GetFileNameFromDragData(const DropData& drop_data) {
66   base::FilePath file_name(
67       FilePathFromFilename(drop_data.file_description_filename));
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   }
80   return file_name;
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.
86 void 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());
93 }  // namespace
96 @interface WebDragSource(Private)
98 - (void)fillPasteboard;
99 - (NSImage*)dragImage;
101 @end  // @interface WebDragSource(Private)
104 @implementation WebDragSource
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_);
117     contentsView_ = contentsView;
118     DCHECK(contentsView_);
120     dropData_.reset(new DropData(*dropData));
121     DCHECK(dropData_.get());
123     dragImage_.reset([image retain]);
124     imageOffset_ = offset;
126     pasteboard_.reset([pboard retain]);
127     DCHECK(pasteboard_.get());
129     dragOperationMask_ = dragOperationMask;
131     [self fillPasteboard];
132   }
134   return self;
137 - (void)clearWebContentsView {
138   contents_ = nil;
139   contentsView_ = nil;
142 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
143   return dragOperationMask_;
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\">");
152   // Be extra paranoid; avoid crashing.
153   if (!dropData_) {
154     NOTREACHED();
155     return;
156   }
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];
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];
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())];
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];
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];
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];
214   // Oops!
215   } else {
216     // Unknown drag pasteboard type.
217     NOTREACHED();
218   }
221 - (NSPoint)convertScreenPoint:(NSPoint)screenPoint {
222   DCHECK([contentsView_ window]);
223   NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint];
224   return [contentsView_ convertPoint:basePoint fromView:nil];
227 - (void)startDrag {
228   NSEvent* currentEvent = [NSApp currentEvent];
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];
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];
261 - (void)endDragAt:(NSPoint)screenPoint
262         operation:(NSDragOperation)operation {
263   if (!contents_)
264     return;
265   contents_->SystemDragEnded();
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;
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;
286     contents_->DragSourceEndedAt(localPoint.x, localPoint.y, screenPoint.x,
287         screenPoint.y, static_cast<blink::WebDragOperation>(operation));
288   }
290   // Make sure the pasteboard owner isn't us.
291   [pasteboard_ declareTypes:[NSArray array] owner:nil];
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   }
301   base::FilePath filePath(SysNSStringToUTF8(path));
302   filePath = filePath.Append(downloadFileName_);
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;
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_));
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   }
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());
340 @end  // @implementation WebDragSource
342 @implementation WebDragSource (Private)
344 - (void)fillPasteboard {
345   DCHECK(pasteboard_.get());
347   [pasteboard_ declareTypes:@[ ui::kChromeDragDummyPboardType ]
348                       owner:contentsView_];
350   // URL (and title).
351   if (dropData_->url.is_valid()) {
352     [pasteboard_ addTypes:@[ NSURLPboardType, kNSURLTitlePboardType ]
353                     owner:contentsView_];
354   }
356   // MIME type.
357   std::string mimeType;
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     }
388     if (!mimeType.empty()) {
389       base::ScopedCFTypeRef<CFStringRef> mimeTypeCF(
390           base::SysUTF8ToCFStringRef(mimeType));
391       fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag(
392           kUTTagClassMIMEType, mimeTypeCF.get(), NULL));
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];
419       if (!dropData_->file_contents.empty())
420         [pasteboard_ addTypes:fileUTIList owner:contentsView_];
421     }
422   }
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   }
445   // Plain text.
446   if (!dropData_->text.string().empty()) {
447     [pasteboard_ addTypes:@[ NSStringPboardType ]
448                     owner:contentsView_];
449   }
451   if (!dropData_->custom_data.empty()) {
452     [pasteboard_ addTypes:@[ ui::kWebCustomDataPboardType ]
453                     owner:contentsView_];
454   }
457 - (NSImage*)dragImage {
458   if (dragImage_)
459     return dragImage_;
461   // Default to returning a generic image.
462   return content::GetContentClient()->GetNativeImageNamed(
463       IDR_DEFAULT_FAVICON).ToNSImage();
466 @end  // @implementation WebDragSource (Private)