web_drag_source_mac.mm revision c5cede9ae108bb15f6b7a8aea21c7e1fefa2834c
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.h"
11#include "base/files/file_path.h"
12#include "base/mac/mac_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 "content/public/common/url_constants.h"
28#include "grit/ui_resources.h"
29#include "net/base/escape.h"
30#include "net/base/filename_util.h"
31#include "net/base/mime_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"
35
36using base::SysNSStringToUTF8;
37using base::SysUTF8ToNSString;
38using base::SysUTF16ToNSString;
39using content::BrowserThread;
40using content::DragDownloadFile;
41using content::DropData;
42using content::PromiseFileFinalizer;
43using content::RenderViewHostImpl;
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 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.
54base::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);
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 DropData& 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    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  }
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 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());
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 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_);
116
117    contentsView_ = contentsView;
118    DCHECK(contentsView_);
119
120    dropData_.reset(new DropData(*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 base::string16 kHtmlHeader = base::ASCIIToUTF16(
150      "<meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">");
151
152  // Be extra paranoid; avoid crashing.
153  if (!dropData_) {
154    NOTREACHED();
155    return;
156  }
157
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];
165
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(content::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];
188
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())];
194
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];
200
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];
207
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];
213
214  // Oops!
215  } else {
216    // Unknown drag pasteboard type.
217    NOTREACHED();
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  if (!contents_)
264    return;
265  contents_->SystemDragEnded();
266
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;
279
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;
285
286    contents_->DragSourceEndedAt(localPoint.x, localPoint.y, screenPoint.x,
287        screenPoint.y, static_cast<blink::WebDragOperation>(operation));
288  }
289
290  // Make sure the pasteboard owner isn't us.
291  [pasteboard_ declareTypes:[NSArray array] owner:nil];
292}
293
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  }
300
301  base::FilePath filePath(SysNSStringToUTF8(path));
302  filePath = filePath.Append(downloadFileName_);
303
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;
311
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_));
321
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  }
333
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());
338}
339
340@end  // @implementation WebDragSource
341
342@implementation WebDragSource (Private)
343
344- (void)fillPasteboard {
345  DCHECK(pasteboard_.get());
346
347  [pasteboard_ declareTypes:@[ ui::kChromeDragDummyPboardType ]
348                      owner:contentsView_];
349
350  // URL (and title).
351  if (dropData_->url.is_valid()) {
352    [pasteboard_ addTypes:@[ NSURLPboardType, kNSURLTitlePboardType ]
353                    owner:contentsView_];
354  }
355
356  // MIME type.
357  std::string mimeType;
358
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    }
387
388    if (!mimeType.empty()) {
389      base::ScopedCFTypeRef<CFStringRef> mimeTypeCF(
390          base::SysUTF8ToCFStringRef(mimeType));
391      fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag(
392          kUTTagClassMIMEType, mimeTypeCF.get(), NULL));
393
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];
418
419      if (!dropData_->file_contents.empty())
420        [pasteboard_ addTypes:fileUTIList owner:contentsView_];
421    }
422  }
423
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  }
444
445  // Plain text.
446  if (!dropData_->text.string().empty()) {
447    [pasteboard_ addTypes:@[ NSStringPboardType ]
448                    owner:contentsView_];
449  }
450
451  if (!dropData_->custom_data.empty()) {
452    [pasteboard_ addTypes:@[ ui::kWebCustomDataPboardType ]
453                    owner:contentsView_];
454  }
455}
456
457- (NSImage*)dragImage {
458  if (dragImage_)
459    return dragImage_;
460
461  // Default to returning a generic image.
462  return content::GetContentClient()->GetNativeImageNamed(
463      IDR_DEFAULT_FAVICON).ToNSImage();
464}
465
466@end  // @implementation WebDragSource (Private)
467