web_drag_source_mac.mm revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
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_path.h"
11#include "base/mac/mac_util.h"
12#include "base/pickle.h"
13#include "base/string_util.h"
14#include "base/strings/sys_string_conversions.h"
15#include "base/threading/thread.h"
16#include "base/threading/thread_restrictions.h"
17#include "base/utf_string_conversions.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/url_constants.h"
26#include "grit/ui_resources.h"
27#include "net/base/escape.h"
28#include "net/base/file_stream.h"
29#include "net/base/mime_util.h"
30#include "net/base/net_util.h"
31#include "ui/base/clipboard/custom_data_helper.h"
32#include "ui/base/dragdrop/cocoa_dnd_util.h"
33#include "ui/gfx/image/image.h"
34#include "webkit/glue/webdropdata.h"
35
36using base::SysNSStringToUTF8;
37using base::SysUTF8ToNSString;
38using base::SysUTF16ToNSString;
39using content::BrowserThread;
40using content::DragDownloadFile;
41using content::PromiseFileFinalizer;
42using content::RenderViewHostImpl;
43using net::FileStream;
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 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 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 WebDropData& 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    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 WebDropData& drop_data,
87                         scoped_ptr<FileStream> file_stream) {
88  DCHECK(file_stream);
89  file_stream->WriteSync(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 WebDropData*)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 WebDropData(*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 string16 kHtmlHeader = 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(chrome::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 = NSMakePoint(0, 0);
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<WebKit::WebDragOperation>(operation));
288  }
289
290  // Make sure the pasteboard owner isn't us.
291  [pasteboard_ declareTypes:[NSArray array] owner:nil];
292}
293
294- (void)moveDragTo:(NSPoint)screenPoint {
295  if (!contents_)
296    return;
297  RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>(
298      contents_->GetRenderViewHost());
299  if (rvh) {
300    // Convert |screenPoint| to view coordinates and flip it.
301    NSPoint localPoint = NSMakePoint(0, 0);
302    if ([contentsView_ window])
303      localPoint = [self convertScreenPoint:screenPoint];
304    NSRect viewFrame = [contentsView_ frame];
305    localPoint.y = viewFrame.size.height - localPoint.y;
306    // Flip |screenPoint|.
307    NSRect screenFrame = [[[contentsView_ window] screen] frame];
308    screenPoint.y = screenFrame.size.height - screenPoint.y;
309
310    contents_->DragSourceMovedTo(localPoint.x, localPoint.y,
311                                 screenPoint.x, screenPoint.y);
312  }
313}
314
315- (NSString*)dragPromisedFileTo:(NSString*)path {
316  // Be extra paranoid; avoid crashing.
317  if (!dropData_) {
318    NOTREACHED() << "No drag-and-drop data available for promised file.";
319    return nil;
320  }
321
322  base::FilePath fileName = downloadFileName_.empty() ?
323      GetFileNameFromDragData(*dropData_) : downloadFileName_;
324  base::FilePath filePath(SysNSStringToUTF8(path));
325  filePath = filePath.Append(fileName);
326
327  // CreateFileStreamForDrop() will call file_util::PathExists(),
328  // which is blocking.  Since this operation is already blocking the
329  // UI thread on OSX, it should be reasonable to let it happen.
330  base::ThreadRestrictions::ScopedAllowIO allowIO;
331  scoped_ptr<FileStream> fileStream(content::CreateFileStreamForDrop(
332      &filePath, content::GetContentClient()->browser()->GetNetLog()));
333  if (!fileStream)
334    return nil;
335
336  if (downloadURL_.is_valid()) {
337    scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile(
338        filePath,
339        fileStream.Pass(),
340        downloadURL_,
341        content::Referrer(contents_->GetURL(), dropData_->referrer_policy),
342        contents_->GetEncoding(),
343        contents_));
344
345    // The finalizer will take care of closing and deletion.
346    dragFileDownloader->Start(new PromiseFileFinalizer(dragFileDownloader));
347  } else {
348    // The writer will take care of closing and deletion.
349    BrowserThread::PostTask(BrowserThread::FILE,
350                            FROM_HERE,
351                            base::Bind(&PromiseWriterHelper,
352                                       *dropData_,
353                                       base::Passed(&fileStream)));
354  }
355
356  // Once we've created the file, we should return the file name.
357  return SysUTF8ToNSString(filePath.BaseName().value());
358}
359
360@end  // @implementation WebDragSource
361
362
363@implementation WebDragSource (Private)
364
365- (void)fillPasteboard {
366  DCHECK(pasteboard_.get());
367
368  [pasteboard_ declareTypes:@[ui::kChromeDragDummyPboardType]
369                      owner:contentsView_];
370
371  // URL (and title).
372  if (dropData_->url.is_valid()) {
373    [pasteboard_ addTypes:@[NSURLPboardType, kNSURLTitlePboardType]
374                    owner:contentsView_];
375  }
376
377  // MIME type.
378  std::string mimeType;
379
380  // File extension.
381  std::string fileExtension;
382
383  // File.
384  if (!dropData_->file_contents.empty() ||
385      !dropData_->download_metadata.empty()) {
386    if (dropData_->download_metadata.empty()) {
387      fileExtension = GetFileNameFromDragData(*dropData_).Extension();
388      net::GetMimeTypeFromExtension(fileExtension, &mimeType);
389    } else {
390      string16 mimeType16;
391      base::FilePath fileName;
392      if (content::ParseDownloadMetadata(
393              dropData_->download_metadata,
394              &mimeType16,
395              &fileName,
396              &downloadURL_)) {
397        // Generate the file name based on both mime type and proposed file
398        // name.
399        std::string defaultName =
400            content::GetContentClient()->browser()->GetDefaultDownloadName();
401        downloadFileName_ =
402            net::GenerateFileName(downloadURL_,
403                                  std::string(),
404                                  std::string(),
405                                  fileName.value(),
406                                  UTF16ToUTF8(mimeType16),
407                                  defaultName);
408        mimeType = UTF16ToUTF8(mimeType16);
409        fileExtension = downloadFileName_.Extension();
410      }
411    }
412
413    if (!mimeType.empty()) {
414      base::mac::ScopedCFTypeRef<CFStringRef> mimeTypeCF(
415          base::SysUTF8ToCFStringRef(mimeType));
416      fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag(
417          kUTTagClassMIMEType, mimeTypeCF.get(), NULL));
418
419      // File (HFS) promise.
420      // TODO(avi): Can we switch to kPasteboardTypeFilePromiseContent?
421      NSArray* types = @[NSFilesPromisePboardType];
422      [pasteboard_ addTypes:types owner:contentsView_];
423
424      // For the file promise, we need to specify the extension.
425      [pasteboard_ setPropertyList:@[SysUTF8ToNSString(fileExtension.substr(1))]
426                           forType:NSFilesPromisePboardType];
427
428      if (!dropData_->file_contents.empty()) {
429        NSArray* types = @[base::mac::CFToNSCast(fileUTI_.get())];
430        [pasteboard_ addTypes:types owner:contentsView_];
431      }
432    }
433  }
434
435  // HTML.
436  bool hasHTMLData = !dropData_->html.string().empty();
437  // Mail.app and TextEdit accept drags that have both HTML and image flavors on
438  // them, but don't process them correctly <http://crbug.com/55879>. Therefore,
439  // if there is an image flavor, don't put the HTML data on as HTML, but rather
440  // put it on as this Chrome-only flavor.
441  //
442  // (The only time that Blink fills in the WebDropData::file_contents is with
443  // an image drop, but the MIME time is tested anyway for paranoia's sake.)
444  bool hasImageData = !dropData_->file_contents.empty() &&
445                      fileUTI_ &&
446                      UTTypeConformsTo(fileUTI_.get(), kUTTypeImage);
447  if (hasHTMLData) {
448    if (hasImageData) {
449      [pasteboard_ addTypes:@[ui::kChromeDragImageHTMLPboardType]
450                      owner:contentsView_];
451    } else {
452      [pasteboard_ addTypes:@[NSHTMLPboardType] owner:contentsView_];
453    }
454  }
455
456  // Plain text.
457  if (!dropData_->text.string().empty()) {
458    [pasteboard_ addTypes:@[NSStringPboardType]
459                    owner:contentsView_];
460  }
461
462  if (!dropData_->custom_data.empty()) {
463    [pasteboard_ addTypes:@[ui::kWebCustomDataPboardType]
464                    owner:contentsView_];
465  }
466}
467
468- (NSImage*)dragImage {
469  if (dragImage_)
470    return dragImage_;
471
472  // Default to returning a generic image.
473  return content::GetContentClient()->GetNativeImageNamed(
474      IDR_DEFAULT_FAVICON).ToNSImage();
475}
476
477@end  // @implementation WebDragSource (Private)
478