Remove PlatformFile from profile_browsertest
[chromium-blink-merge.git] / content / browser / web_contents / web_drag_source_mac.mm
bloba5b51b07d6a1dfff29b43ffe4ec1ced3dcff7d88
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_path.h"
11 #include "base/mac/mac_util.h"
12 #include "base/pickle.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/sys_string_conversions.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "base/threading/thread.h"
17 #include "base/threading/thread_restrictions.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/drop_data.h"
26 #include "content/public/common/url_constants.h"
27 #include "grit/ui_resources.h"
28 #include "net/base/escape.h"
29 #include "net/base/file_stream.h"
30 #include "net/base/mime_util.h"
31 #include "net/base/net_util.h"
32 #include "ui/base/clipboard/custom_data_helper.h"
33 #include "ui/base/dragdrop/cocoa_dnd_util.h"
34 #include "ui/gfx/image/image.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;
44 using net::FileStream;
46 namespace {
48 // An unofficial standard pasteboard title type to be provided alongside the
49 // |NSURLPboardType|.
50 NSString* const kNSURLTitlePboardType = @"public.url-name";
52 // Converts a base::string16 into a FilePath. Use this method instead of
53 // -[NSString fileSystemRepresentation] to prevent exceptions from being thrown.
54 // See http://crbug.com/78782 for more info.
55 base::FilePath FilePathFromFilename(const base::string16& filename) {
56   NSString* str = SysUTF16ToNSString(filename);
57   char buf[MAXPATHLEN];
58   if (![str getFileSystemRepresentation:buf maxLength:sizeof(buf)])
59     return base::FilePath();
60   return base::FilePath(buf);
63 // Returns a filename appropriate for the drop data
64 // TODO(viettrungluu): Refactor to make it common across platforms,
65 // and move it somewhere sensible.
66 base::FilePath GetFileNameFromDragData(const DropData& drop_data) {
67   base::FilePath file_name(
68       FilePathFromFilename(drop_data.file_description_filename));
70   // Images without ALT text will only have a file extension so we need to
71   // synthesize one from the provided extension and URL.
72   if (file_name.empty()) {
73     // Retrieve the name from the URL.
74     base::string16 suggested_filename =
75         net::GetSuggestedFilename(drop_data.url, "", "", "", "", "");
76     const std::string extension = file_name.Extension();
77     file_name = FilePathFromFilename(suggested_filename);
78     file_name = file_name.ReplaceExtension(extension);
79   }
81   return file_name;
84 // This helper's sole task is to write out data for a promised file; the caller
85 // is responsible for opening the file. It takes the drop data and an open file
86 // stream.
87 void PromiseWriterHelper(const DropData& drop_data,
88                          scoped_ptr<FileStream> file_stream) {
89   DCHECK(file_stream);
90   file_stream->WriteSync(drop_data.file_contents.data(),
91                          drop_data.file_contents.length());
94 }  // namespace
97 @interface WebDragSource(Private)
99 - (void)fillPasteboard;
100 - (NSImage*)dragImage;
102 @end  // @interface WebDragSource(Private)
105 @implementation WebDragSource
107 - (id)initWithContents:(content::WebContentsImpl*)contents
108                   view:(NSView*)contentsView
109               dropData:(const DropData*)dropData
110                  image:(NSImage*)image
111                 offset:(NSPoint)offset
112             pasteboard:(NSPasteboard*)pboard
113      dragOperationMask:(NSDragOperation)dragOperationMask {
114   if ((self = [super init])) {
115     contents_ = contents;
116     DCHECK(contents_);
118     contentsView_ = contentsView;
119     DCHECK(contentsView_);
121     dropData_.reset(new DropData(*dropData));
122     DCHECK(dropData_.get());
124     dragImage_.reset([image retain]);
125     imageOffset_ = offset;
127     pasteboard_.reset([pboard retain]);
128     DCHECK(pasteboard_.get());
130     dragOperationMask_ = dragOperationMask;
132     [self fillPasteboard];
133   }
135   return self;
138 - (void)clearWebContentsView {
139   contents_ = nil;
140   contentsView_ = nil;
143 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
144   return dragOperationMask_;
147 - (void)lazyWriteToPasteboard:(NSPasteboard*)pboard forType:(NSString*)type {
148   // NSHTMLPboardType requires the character set to be declared. Otherwise, it
149   // assumes US-ASCII. Awesome.
150   const base::string16 kHtmlHeader = base::ASCIIToUTF16(
151       "<meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">");
153   // Be extra paranoid; avoid crashing.
154   if (!dropData_) {
155     NOTREACHED();
156     return;
157   }
159   // HTML.
160   if ([type isEqualToString:NSHTMLPboardType] ||
161       [type isEqualToString:ui::kChromeDragImageHTMLPboardType]) {
162     DCHECK(!dropData_->html.string().empty());
163     // See comment on |kHtmlHeader| above.
164     [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->html.string())
165               forType:type];
167   // URL.
168   } else if ([type isEqualToString:NSURLPboardType]) {
169     DCHECK(dropData_->url.is_valid());
170     NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())];
171     // If NSURL creation failed, check for a badly-escaped JavaScript URL.
172     // Strip out any existing escapes and then re-escape uniformly.
173     if (!url && dropData_->url.SchemeIs(content::kJavaScriptScheme)) {
174       net::UnescapeRule::Type unescapeRules =
175           net::UnescapeRule::SPACES |
176           net::UnescapeRule::URL_SPECIAL_CHARS |
177           net::UnescapeRule::CONTROL_CHARS;
178       std::string unescapedUrlString =
179           net::UnescapeURLComponent(dropData_->url.spec(), unescapeRules);
180       std::string escapedUrlString =
181           net::EscapeUrlEncodedData(unescapedUrlString, false);
182       url = [NSURL URLWithString:SysUTF8ToNSString(escapedUrlString)];
183     }
184     [url writeToPasteboard:pboard];
185   // URL title.
186   } else if ([type isEqualToString:kNSURLTitlePboardType]) {
187     [pboard setString:SysUTF16ToNSString(dropData_->url_title)
188               forType:kNSURLTitlePboardType];
190   // File contents.
191   } else if ([type isEqualToString:base::mac::CFToNSCast(fileUTI_)]) {
192     [pboard setData:[NSData dataWithBytes:dropData_->file_contents.data()
193                                    length:dropData_->file_contents.length()]
194             forType:base::mac::CFToNSCast(fileUTI_.get())];
196   // Plain text.
197   } else if ([type isEqualToString:NSStringPboardType]) {
198     DCHECK(!dropData_->text.string().empty());
199     [pboard setString:SysUTF16ToNSString(dropData_->text.string())
200               forType:NSStringPboardType];
202   // Custom MIME data.
203   } else if ([type isEqualToString:ui::kWebCustomDataPboardType]) {
204     Pickle pickle;
205     ui::WriteCustomDataToPickle(dropData_->custom_data, &pickle);
206     [pboard setData:[NSData dataWithBytes:pickle.data() length:pickle.size()]
207             forType:ui::kWebCustomDataPboardType];
209   // Dummy type.
210   } else if ([type isEqualToString:ui::kChromeDragDummyPboardType]) {
211     // The dummy type _was_ promised and someone decided to call the bluff.
212     [pboard setData:[NSData data]
213             forType:ui::kChromeDragDummyPboardType];
215   // Oops!
216   } else {
217     // Unknown drag pasteboard type.
218     NOTREACHED();
219   }
222 - (NSPoint)convertScreenPoint:(NSPoint)screenPoint {
223   DCHECK([contentsView_ window]);
224   NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint];
225   return [contentsView_ convertPoint:basePoint fromView:nil];
228 - (void)startDrag {
229   NSEvent* currentEvent = [NSApp currentEvent];
231   // Synthesize an event for dragging, since we can't be sure that
232   // [NSApp currentEvent] will return a valid dragging event.
233   NSWindow* window = [contentsView_ window];
234   NSPoint position = [window mouseLocationOutsideOfEventStream];
235   NSTimeInterval eventTime = [currentEvent timestamp];
236   NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged
237                                           location:position
238                                      modifierFlags:NSLeftMouseDraggedMask
239                                          timestamp:eventTime
240                                       windowNumber:[window windowNumber]
241                                            context:nil
242                                        eventNumber:0
243                                         clickCount:1
244                                           pressure:1.0];
246   if (dragImage_) {
247     position.x -= imageOffset_.x;
248     // Deal with Cocoa's flipped coordinate system.
249     position.y -= [dragImage_.get() size].height - imageOffset_.y;
250   }
251   // Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in
252   // third_party/WebKit/Source/WebKit/mac/Misc/WebNSViewExtras.m.
253   [window dragImage:[self dragImage]
254                  at:position
255              offset:NSZeroSize
256               event:dragEvent
257          pasteboard:pasteboard_
258              source:contentsView_
259           slideBack:YES];
262 - (void)endDragAt:(NSPoint)screenPoint
263         operation:(NSDragOperation)operation {
264   if (!contents_)
265     return;
266   contents_->SystemDragEnded();
268   RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>(
269       contents_->GetRenderViewHost());
270   if (rvh) {
271     // Convert |screenPoint| to view coordinates and flip it.
272     NSPoint localPoint = NSZeroPoint;
273     if ([contentsView_ window])
274       localPoint = [self convertScreenPoint:screenPoint];
275     NSRect viewFrame = [contentsView_ frame];
276     localPoint.y = viewFrame.size.height - localPoint.y;
277     // Flip |screenPoint|.
278     NSRect screenFrame = [[[contentsView_ window] screen] frame];
279     screenPoint.y = screenFrame.size.height - screenPoint.y;
281     // If AppKit returns a copy and move operation, mask off the move bit
282     // because WebCore does not understand what it means to do both, which
283     // results in an assertion failure/renderer crash.
284     if (operation == (NSDragOperationMove | NSDragOperationCopy))
285       operation &= ~NSDragOperationMove;
287     contents_->DragSourceEndedAt(localPoint.x, localPoint.y, screenPoint.x,
288         screenPoint.y, static_cast<blink::WebDragOperation>(operation));
289   }
291   // Make sure the pasteboard owner isn't us.
292   [pasteboard_ declareTypes:[NSArray array] owner:nil];
295 - (void)moveDragTo:(NSPoint)screenPoint {
296   if (!contents_)
297     return;
298   RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>(
299       contents_->GetRenderViewHost());
300   if (rvh) {
301     // Convert |screenPoint| to view coordinates and flip it.
302     NSPoint localPoint = NSZeroPoint;
303     if ([contentsView_ window])
304       localPoint = [self convertScreenPoint:screenPoint];
305     NSRect viewFrame = [contentsView_ frame];
306     localPoint.y = viewFrame.size.height - localPoint.y;
307     // Flip |screenPoint|.
308     NSRect screenFrame = [[[contentsView_ window] screen] frame];
309     screenPoint.y = screenFrame.size.height - screenPoint.y;
311     contents_->DragSourceMovedTo(localPoint.x, localPoint.y,
312                                  screenPoint.x, screenPoint.y);
313   }
316 - (NSString*)dragPromisedFileTo:(NSString*)path {
317   // Be extra paranoid; avoid crashing.
318   if (!dropData_) {
319     NOTREACHED() << "No drag-and-drop data available for promised file.";
320     return nil;
321   }
323   base::FilePath filePath(SysNSStringToUTF8(path));
324   filePath = filePath.Append(downloadFileName_);
326   // CreateFileStreamForDrop() will call base::PathExists(),
327   // which is blocking.  Since this operation is already blocking the
328   // UI thread on OSX, it should be reasonable to let it happen.
329   base::ThreadRestrictions::ScopedAllowIO allowIO;
330   scoped_ptr<FileStream> fileStream(content::CreateFileStreamForDrop(
331       &filePath, content::GetContentClient()->browser()->GetNetLog()));
332   if (!fileStream)
333     return nil;
335   if (downloadURL_.is_valid()) {
336     scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile(
337         filePath,
338         fileStream.Pass(),
339         downloadURL_,
340         content::Referrer(contents_->GetLastCommittedURL(),
341                           dropData_->referrer_policy),
342         contents_->GetEncoding(),
343         contents_));
345     // The finalizer will take care of closing and deletion.
346     dragFileDownloader->Start(new PromiseFileFinalizer(
347         dragFileDownloader.get()));
348   } else {
349     // The writer will take care of closing and deletion.
350     BrowserThread::PostTask(BrowserThread::FILE,
351                             FROM_HERE,
352                             base::Bind(&PromiseWriterHelper,
353                                        *dropData_,
354                                        base::Passed(&fileStream)));
355   }
357   // The DragDownloadFile constructor may have altered the value of |filePath|
358   // if, say, an existing file at the drop site has the same name. Return the
359   // actual name that was used to write the file.
360   return SysUTF8ToNSString(filePath.BaseName().value());
363 @end  // @implementation WebDragSource
365 @implementation WebDragSource (Private)
367 - (void)fillPasteboard {
368   DCHECK(pasteboard_.get());
370   [pasteboard_ declareTypes:@[ ui::kChromeDragDummyPboardType ]
371                       owner:contentsView_];
373   // URL (and title).
374   if (dropData_->url.is_valid()) {
375     [pasteboard_ addTypes:@[ NSURLPboardType, kNSURLTitlePboardType ]
376                     owner:contentsView_];
377   }
379   // MIME type.
380   std::string mimeType;
382   // File.
383   if (!dropData_->file_contents.empty() ||
384       !dropData_->download_metadata.empty()) {
385     if (dropData_->download_metadata.empty()) {
386       downloadFileName_ = GetFileNameFromDragData(*dropData_);
387       net::GetMimeTypeFromExtension(downloadFileName_.Extension(), &mimeType);
388     } else {
389       base::string16 mimeType16;
390       base::FilePath fileName;
391       if (content::ParseDownloadMetadata(
392               dropData_->download_metadata,
393               &mimeType16,
394               &fileName,
395               &downloadURL_)) {
396         // Generate the file name based on both mime type and proposed file
397         // name.
398         std::string defaultName =
399             content::GetContentClient()->browser()->GetDefaultDownloadName();
400         mimeType = base::UTF16ToUTF8(mimeType16);
401         downloadFileName_ =
402             net::GenerateFileName(downloadURL_,
403                                   std::string(),
404                                   std::string(),
405                                   fileName.value(),
406                                   mimeType,
407                                   defaultName);
408       }
409     }
411     if (!mimeType.empty()) {
412       base::ScopedCFTypeRef<CFStringRef> mimeTypeCF(
413           base::SysUTF8ToCFStringRef(mimeType));
414       fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag(
415           kUTTagClassMIMEType, mimeTypeCF.get(), NULL));
417       // File (HFS) promise.
418       // There are two ways to drag/drop files. NSFilesPromisePboardType is the
419       // deprecated way, and kPasteboardTypeFilePromiseContent is the way that
420       // does not work. kPasteboardTypeFilePromiseContent is thoroughly broken:
421       // * API: There is no good way to get the location for the drop.
422       //   <http://lists.apple.com/archives/cocoa-dev/2012/Feb/msg00706.html>
423       //   <rdar://14943849> <http://openradar.me/14943849>
424       // * Behavior: A file dropped in the Finder is not selected. This can be
425       //   worked around by selecting the file in the Finder using AppleEvents,
426       //   but the drop target window will come to the front of the Finder's
427       //   window list (unlike the previous behavior). <http://crbug.com/278515>
428       //   <rdar://14943865> <http://openradar.me/14943865>
429       // * Behavior: Files dragged over app icons in the dock do not highlight
430       //   the dock icons, and the dock icons do not accept the drop.
431       //   <http://crbug.com/282916> <rdar://14943872>
432       //   <http://openradar.me/14943872>
433       // * Behavior: A file dropped onto the desktop is positioned at the upper
434       //   right of the desktop rather than at the position at which it was
435       //   dropped. <http://crbug.com/284942> <rdar://14943881>
436       //   <http://openradar.me/14943881>
437       NSArray* fileUTIList = @[ base::mac::CFToNSCast(fileUTI_.get()) ];
438       [pasteboard_ addTypes:@[ NSFilesPromisePboardType ] owner:contentsView_];
439       [pasteboard_ setPropertyList:fileUTIList
440                            forType:NSFilesPromisePboardType];
442       if (!dropData_->file_contents.empty())
443         [pasteboard_ addTypes:fileUTIList owner:contentsView_];
444     }
445   }
447   // HTML.
448   bool hasHTMLData = !dropData_->html.string().empty();
449   // Mail.app and TextEdit accept drags that have both HTML and image flavors on
450   // them, but don't process them correctly <http://crbug.com/55879>. Therefore,
451   // if there is an image flavor, don't put the HTML data on as HTML, but rather
452   // put it on as this Chrome-only flavor.
453   //
454   // (The only time that Blink fills in the DropData::file_contents is with
455   // an image drop, but the MIME time is tested anyway for paranoia's sake.)
456   bool hasImageData = !dropData_->file_contents.empty() &&
457                       fileUTI_ &&
458                       UTTypeConformsTo(fileUTI_.get(), kUTTypeImage);
459   if (hasHTMLData) {
460     if (hasImageData) {
461       [pasteboard_ addTypes:@[ ui::kChromeDragImageHTMLPboardType ]
462                       owner:contentsView_];
463     } else {
464       [pasteboard_ addTypes:@[ NSHTMLPboardType ] owner:contentsView_];
465     }
466   }
468   // Plain text.
469   if (!dropData_->text.string().empty()) {
470     [pasteboard_ addTypes:@[ NSStringPboardType ]
471                     owner:contentsView_];
472   }
474   if (!dropData_->custom_data.empty()) {
475     [pasteboard_ addTypes:@[ ui::kWebCustomDataPboardType ]
476                     owner:contentsView_];
477   }
480 - (NSImage*)dragImage {
481   if (dragImage_)
482     return dragImage_;
484   // Default to returning a generic image.
485   return content::GetContentClient()->GetNativeImageNamed(
486       IDR_DEFAULT_FAVICON).ToNSImage();
489 @end  // @implementation WebDragSource (Private)