web_drag_source_mac.mm revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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/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_.get()) {
154    NOTREACHED();
155    return;
156  }
157
158  // HTML.
159  if ([type isEqualToString:NSHTMLPboardType]) {
160    DCHECK(!dropData_->html.string().empty());
161    // See comment on |kHtmlHeader| above.
162    [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->html.string())
163              forType:NSHTMLPboardType];
164
165  // URL.
166  } else if ([type isEqualToString:NSURLPboardType]) {
167    DCHECK(dropData_->url.is_valid());
168    NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())];
169    // If NSURL creation failed, check for a badly-escaped JavaScript URL.
170    // Strip out any existing escapes and then re-escape uniformly.
171    if (!url && dropData_->url.SchemeIs(chrome::kJavaScriptScheme)) {
172      net::UnescapeRule::Type unescapeRules =
173          net::UnescapeRule::SPACES |
174          net::UnescapeRule::URL_SPECIAL_CHARS |
175          net::UnescapeRule::CONTROL_CHARS;
176      std::string unescapedUrlString =
177          net::UnescapeURLComponent(dropData_->url.spec(), unescapeRules);
178      std::string escapedUrlString =
179          net::EscapeUrlEncodedData(unescapedUrlString, false);
180      url = [NSURL URLWithString:SysUTF8ToNSString(escapedUrlString)];
181    }
182    [url writeToPasteboard:pboard];
183  // URL title.
184  } else if ([type isEqualToString:kNSURLTitlePboardType]) {
185    [pboard setString:SysUTF16ToNSString(dropData_->url_title)
186              forType:kNSURLTitlePboardType];
187
188  // File contents.
189  } else if ([type isEqualToString:base::mac::CFToNSCast(fileUTI_.get())]) {
190    [pboard setData:[NSData dataWithBytes:dropData_->file_contents.data()
191                                   length:dropData_->file_contents.length()]
192            forType:base::mac::CFToNSCast(fileUTI_.get())];
193
194  // Plain text.
195  } else if ([type isEqualToString:NSStringPboardType]) {
196    DCHECK(!dropData_->text.string().empty());
197    [pboard setString:SysUTF16ToNSString(dropData_->text.string())
198              forType:NSStringPboardType];
199
200  // Custom MIME data.
201  } else if ([type isEqualToString:ui::kWebCustomDataPboardType]) {
202    Pickle pickle;
203    ui::WriteCustomDataToPickle(dropData_->custom_data, &pickle);
204    [pboard setData:[NSData dataWithBytes:pickle.data() length:pickle.size()]
205            forType:ui::kWebCustomDataPboardType];
206
207  // Dummy type.
208  } else if ([type isEqualToString:ui::kChromeDragDummyPboardType]) {
209    // The dummy type _was_ promised and someone decided to call the bluff.
210    [pboard setData:[NSData data]
211            forType:ui::kChromeDragDummyPboardType];
212
213  // Oops!
214  } else {
215    // Unknown drag pasteboard type.
216    NOTREACHED();
217  }
218}
219
220- (NSPoint)convertScreenPoint:(NSPoint)screenPoint {
221  DCHECK([contentsView_ window]);
222  NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint];
223  return [contentsView_ convertPoint:basePoint fromView:nil];
224}
225
226- (void)startDrag {
227  NSEvent* currentEvent = [NSApp currentEvent];
228
229  // Synthesize an event for dragging, since we can't be sure that
230  // [NSApp currentEvent] will return a valid dragging event.
231  NSWindow* window = [contentsView_ window];
232  NSPoint position = [window mouseLocationOutsideOfEventStream];
233  NSTimeInterval eventTime = [currentEvent timestamp];
234  NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged
235                                          location:position
236                                     modifierFlags:NSLeftMouseDraggedMask
237                                         timestamp:eventTime
238                                      windowNumber:[window windowNumber]
239                                           context:nil
240                                       eventNumber:0
241                                        clickCount:1
242                                          pressure:1.0];
243
244  if (dragImage_) {
245    position.x -= imageOffset_.x;
246    // Deal with Cocoa's flipped coordinate system.
247    position.y -= [dragImage_.get() size].height - imageOffset_.y;
248  }
249  // Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in
250  // third_party/WebKit/Source/WebKit/mac/Misc/WebNSViewExtras.m.
251  [window dragImage:[self dragImage]
252                 at:position
253             offset:NSZeroSize
254              event:dragEvent
255         pasteboard:pasteboard_
256             source:contentsView_
257          slideBack:YES];
258}
259
260- (void)endDragAt:(NSPoint)screenPoint
261        operation:(NSDragOperation)operation {
262  if (!contents_)
263    return;
264  contents_->SystemDragEnded();
265
266  RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>(
267      contents_->GetRenderViewHost());
268  if (rvh) {
269    // Convert |screenPoint| to view coordinates and flip it.
270    NSPoint localPoint = NSMakePoint(0, 0);
271    if ([contentsView_ window])
272      localPoint = [self convertScreenPoint:screenPoint];
273    NSRect viewFrame = [contentsView_ frame];
274    localPoint.y = viewFrame.size.height - localPoint.y;
275    // Flip |screenPoint|.
276    NSRect screenFrame = [[[contentsView_ window] screen] frame];
277    screenPoint.y = screenFrame.size.height - screenPoint.y;
278
279    // If AppKit returns a copy and move operation, mask off the move bit
280    // because WebCore does not understand what it means to do both, which
281    // results in an assertion failure/renderer crash.
282    if (operation == (NSDragOperationMove | NSDragOperationCopy))
283      operation &= ~NSDragOperationMove;
284
285    rvh->DragSourceEndedAt(localPoint.x, localPoint.y,
286                           screenPoint.x, screenPoint.y,
287                           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    rvh->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_.get()) {
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.get())
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_
369      declareTypes:[NSArray arrayWithObject:ui::kChromeDragDummyPboardType]
370             owner:contentsView_];
371
372  // URL (and title).
373  if (dropData_->url.is_valid())
374    [pasteboard_ addTypes:[NSArray arrayWithObjects:NSURLPboardType,
375                                                    kNSURLTitlePboardType, nil]
376                    owner:contentsView_];
377
378  // MIME type.
379  std::string mimeType;
380
381  // File extension.
382  std::string fileExtension;
383
384  // File.
385  if (!dropData_->file_contents.empty() ||
386      !dropData_->download_metadata.empty()) {
387    if (dropData_->download_metadata.empty()) {
388      fileExtension = GetFileNameFromDragData(*dropData_).Extension();
389      net::GetMimeTypeFromExtension(fileExtension, &mimeType);
390    } else {
391      string16 mimeType16;
392      base::FilePath fileName;
393      if (content::ParseDownloadMetadata(
394              dropData_->download_metadata,
395              &mimeType16,
396              &fileName,
397              &downloadURL_)) {
398        // Generate the file name based on both mime type and proposed file
399        // name.
400        std::string defaultName =
401            content::GetContentClient()->browser()->GetDefaultDownloadName();
402        downloadFileName_ =
403            net::GenerateFileName(downloadURL_,
404                                  std::string(),
405                                  std::string(),
406                                  fileName.value(),
407                                  UTF16ToUTF8(mimeType16),
408                                  defaultName);
409        mimeType = UTF16ToUTF8(mimeType16);
410        fileExtension = downloadFileName_.Extension();
411      }
412    }
413
414    if (!mimeType.empty()) {
415      base::mac::ScopedCFTypeRef<CFStringRef> mimeTypeCF(
416          base::SysUTF8ToCFStringRef(mimeType));
417      fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag(
418          kUTTagClassMIMEType, mimeTypeCF.get(), NULL));
419
420      // File (HFS) promise.
421      // TODO(avi): Can we switch to kPasteboardTypeFilePromiseContent?
422      NSArray* types = @[NSFilesPromisePboardType];
423      [pasteboard_ addTypes:types owner:contentsView_];
424
425      // For the file promise, we need to specify the extension.
426      [pasteboard_ setPropertyList:@[SysUTF8ToNSString(fileExtension.substr(1))]
427                           forType:NSFilesPromisePboardType];
428
429      if (!dropData_->file_contents.empty()) {
430        NSArray* types = @[base::mac::CFToNSCast(fileUTI_.get())];
431        [pasteboard_ addTypes:types owner:contentsView_];
432      }
433    }
434  }
435
436  // HTML.
437  bool hasHTMLData = !dropData_->html.string().empty();
438  // Mail.app and TextEdit accept drags that have both HTML and image flavors on
439  // them, but don't process them correctly <http://crbug.com/55879>. Therefore,
440  // omit the HTML flavor if there is an image flavor. (The only time that
441  // WebKit fills in the WebDropData::file_contents is with an image drop, but
442  // the MIME time is tested anyway for paranoia's sake.)
443  bool hasImageData = !dropData_->file_contents.empty() &&
444                      fileUTI_ &&
445                      UTTypeConformsTo(fileUTI_.get(), kUTTypeImage);
446  if (hasHTMLData && !hasImageData)
447    [pasteboard_ addTypes:[NSArray arrayWithObject:NSHTMLPboardType]
448                    owner:contentsView_];
449
450  // Plain text.
451  if (!dropData_->text.string().empty())
452    [pasteboard_ addTypes:[NSArray arrayWithObject:NSStringPboardType]
453                    owner:contentsView_];
454
455  if (!dropData_->custom_data.empty()) {
456    [pasteboard_
457        addTypes:[NSArray arrayWithObject:ui::kWebCustomDataPboardType]
458           owner:contentsView_];
459  }
460}
461
462- (NSImage*)dragImage {
463  if (dragImage_)
464    return dragImage_;
465
466  // Default to returning a generic image.
467  return content::GetContentClient()->GetNativeImageNamed(
468      IDR_DEFAULT_FAVICON).ToNSImage();
469}
470
471@end  // @implementation WebDragSource (Private)
472