web_drag_source.mm revision ddb351dbec246cf1fab5ec20d2d5520909041de1
1// Copyright (c) 2011 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 "chrome/browser/ui/cocoa/tab_contents/web_drag_source.h"
6
7#include <sys/param.h>
8
9#include "app/mac/nsimage_cache.h"
10#include "base/file_path.h"
11#include "base/string_util.h"
12#include "base/sys_string_conversions.h"
13#include "base/task.h"
14#include "base/threading/thread.h"
15#include "base/threading/thread_restrictions.h"
16#include "base/utf_string_conversions.h"
17#import "chrome/app/breakpad_mac.h"
18#include "chrome/browser/browser_process.h"
19#include "chrome/browser/download/download_manager.h"
20#include "chrome/browser/download/download_util.h"
21#include "chrome/browser/download/drag_download_file.h"
22#include "chrome/browser/download/drag_download_util.h"
23#include "chrome/browser/tab_contents/tab_contents_view_mac.h"
24#include "content/browser/renderer_host/render_view_host.h"
25#include "content/browser/tab_contents/tab_contents.h"
26#include "net/base/file_stream.h"
27#include "net/base/net_util.h"
28#import "third_party/mozilla/NSPasteboard+Utils.h"
29#include "webkit/glue/webdropdata.h"
30
31using base::SysNSStringToUTF8;
32using base::SysUTF8ToNSString;
33using base::SysUTF16ToNSString;
34using net::FileStream;
35
36
37namespace {
38
39// An unofficial standard pasteboard title type to be provided alongside the
40// |NSURLPboardType|.
41NSString* const kNSURLTitlePboardType = @"public.url-name";
42
43// Converts a string16 into a FilePath. Use this method instead of
44// -[NSString fileSystemRepresentation] to prevent exceptions from being thrown.
45// See http://crbug.com/78782 for more info.
46FilePath FilePathFromFilename(const string16& filename) {
47  NSString* str = SysUTF16ToNSString(filename);
48  char buf[MAXPATHLEN];
49  if (![str getFileSystemRepresentation:buf maxLength:sizeof(buf)])
50    return FilePath();
51  return FilePath(buf);
52}
53
54// Returns a filename appropriate for the drop data
55// TODO(viettrungluu): Refactor to make it common across platforms,
56// and move it somewhere sensible.
57FilePath GetFileNameFromDragData(const WebDropData& drop_data) {
58  // Set a breakpad key for the scope of this function to help debug
59  // http://crbug.com/78782
60  static NSString* const kUrlKey = @"drop_data_url";
61  NSString* value = SysUTF8ToNSString(drop_data.url.spec());
62  ScopedCrashKey key(kUrlKey, value);
63
64  // Images without ALT text will only have a file extension so we need to
65  // synthesize one from the provided extension and URL.
66  FilePath file_name(FilePathFromFilename(drop_data.file_description_filename));
67  file_name = file_name.BaseName().RemoveExtension();
68
69  if (file_name.empty()) {
70    // Retrieve the name from the URL.
71    string16 suggested_filename =
72        net::GetSuggestedFilename(drop_data.url, "", "", string16());
73    file_name = FilePathFromFilename(suggested_filename);
74  }
75
76  file_name = file_name.ReplaceExtension(UTF16ToUTF8(drop_data.file_extension));
77
78  return file_name;
79}
80
81// This class's sole task is to write out data for a promised file; the caller
82// is responsible for opening the file.
83class PromiseWriterTask : public Task {
84 public:
85  // Assumes ownership of file_stream.
86  PromiseWriterTask(const WebDropData& drop_data,
87                    FileStream* file_stream);
88  virtual ~PromiseWriterTask();
89  virtual void Run();
90
91 private:
92  WebDropData drop_data_;
93
94  // This class takes ownership of file_stream_ and will close and delete it.
95  scoped_ptr<FileStream> file_stream_;
96};
97
98// Takes the drop data and an open file stream (which it takes ownership of and
99// will close and delete).
100PromiseWriterTask::PromiseWriterTask(const WebDropData& drop_data,
101                                     FileStream* file_stream) :
102    drop_data_(drop_data) {
103  file_stream_.reset(file_stream);
104  DCHECK(file_stream_.get());
105}
106
107PromiseWriterTask::~PromiseWriterTask() {
108  DCHECK(file_stream_.get());
109  if (file_stream_.get())
110    file_stream_->Close();
111}
112
113void PromiseWriterTask::Run() {
114  CHECK(file_stream_.get());
115  file_stream_->Write(drop_data_.file_contents.data(),
116                      drop_data_.file_contents.length(),
117                      NULL);
118
119  // Let our destructor take care of business.
120}
121
122}  // namespace
123
124
125@interface WebDragSource(Private)
126
127- (void)fillPasteboard;
128- (NSImage*)dragImage;
129
130@end  // @interface WebDragSource(Private)
131
132
133@implementation WebDragSource
134
135- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView
136                  dropData:(const WebDropData*)dropData
137                     image:(NSImage*)image
138                    offset:(NSPoint)offset
139                pasteboard:(NSPasteboard*)pboard
140         dragOperationMask:(NSDragOperation)dragOperationMask {
141  if ((self = [super init])) {
142    contentsView_ = contentsView;
143    DCHECK(contentsView_);
144
145    dropData_.reset(new WebDropData(*dropData));
146    DCHECK(dropData_.get());
147
148    dragImage_.reset([image retain]);
149    imageOffset_ = offset;
150
151    pasteboard_.reset([pboard retain]);
152    DCHECK(pasteboard_.get());
153
154    dragOperationMask_ = dragOperationMask;
155
156    [self fillPasteboard];
157  }
158
159  return self;
160}
161
162- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
163  return dragOperationMask_;
164}
165
166- (void)lazyWriteToPasteboard:(NSPasteboard*)pboard forType:(NSString*)type {
167  // NSHTMLPboardType requires the character set to be declared. Otherwise, it
168  // assumes US-ASCII. Awesome.
169  static const string16 kHtmlHeader =
170      ASCIIToUTF16("<meta http-equiv=\"Content-Type\" "
171                   "content=\"text/html;charset=UTF-8\">");
172
173  // Be extra paranoid; avoid crashing.
174  if (!dropData_.get()) {
175    NOTREACHED() << "No drag-and-drop data available for lazy write.";
176    return;
177  }
178
179  // HTML.
180  if ([type isEqualToString:NSHTMLPboardType]) {
181    DCHECK(!dropData_->text_html.empty());
182    // See comment on |kHtmlHeader| above.
183    [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->text_html)
184              forType:NSHTMLPboardType];
185
186  // URL.
187  } else if ([type isEqualToString:NSURLPboardType]) {
188    DCHECK(dropData_->url.is_valid());
189    NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())];
190    [url writeToPasteboard:pboard];
191
192  // URL title.
193  } else if ([type isEqualToString:kNSURLTitlePboardType]) {
194    [pboard setString:SysUTF16ToNSString(dropData_->url_title)
195              forType:kNSURLTitlePboardType];
196
197  // File contents.
198  } else if ([type isEqualToString:NSFileContentsPboardType] ||
199      [type isEqualToString:NSCreateFileContentsPboardType(
200              SysUTF16ToNSString(dropData_->file_extension))]) {
201    // TODO(viettrungluu: find something which is known to accept
202    // NSFileContentsPboardType to check that this actually works!
203    scoped_nsobject<NSFileWrapper> file_wrapper(
204        [[NSFileWrapper alloc] initRegularFileWithContents:[NSData
205                dataWithBytes:dropData_->file_contents.data()
206                       length:dropData_->file_contents.length()]]);
207    [file_wrapper setPreferredFilename:SysUTF8ToNSString(
208            GetFileNameFromDragData(*dropData_).value())];
209    [pboard writeFileWrapper:file_wrapper];
210
211  // TIFF.
212  } else if ([type isEqualToString:NSTIFFPboardType]) {
213    // TODO(viettrungluu): This is a bit odd since we rely on Cocoa to render
214    // our image into a TIFF. This is also suboptimal since this is all done
215    // synchronously. I'm not sure there's much we can easily do about it.
216    scoped_nsobject<NSImage> image(
217        [[NSImage alloc] initWithData:[NSData
218                dataWithBytes:dropData_->file_contents.data()
219                       length:dropData_->file_contents.length()]]);
220    [pboard setData:[image TIFFRepresentation] forType:NSTIFFPboardType];
221
222  // Plain text.
223  } else if ([type isEqualToString:NSStringPboardType]) {
224    DCHECK(!dropData_->plain_text.empty());
225    [pboard setString:SysUTF16ToNSString(dropData_->plain_text)
226              forType:NSStringPboardType];
227
228  // Oops!
229  } else {
230    NOTREACHED() << "Asked for a drag pasteboard type we didn't offer.";
231  }
232}
233
234- (NSPoint)convertScreenPoint:(NSPoint)screenPoint {
235  DCHECK([contentsView_ window]);
236  NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint];
237  return [contentsView_ convertPoint:basePoint fromView:nil];
238}
239
240- (void)startDrag {
241  NSEvent* currentEvent = [NSApp currentEvent];
242
243  // Synthesize an event for dragging, since we can't be sure that
244  // [NSApp currentEvent] will return a valid dragging event.
245  NSWindow* window = [contentsView_ window];
246  NSPoint position = [window mouseLocationOutsideOfEventStream];
247  NSTimeInterval eventTime = [currentEvent timestamp];
248  NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged
249                                          location:position
250                                     modifierFlags:NSLeftMouseDraggedMask
251                                         timestamp:eventTime
252                                      windowNumber:[window windowNumber]
253                                           context:nil
254                                       eventNumber:0
255                                        clickCount:1
256                                          pressure:1.0];
257
258  if (dragImage_) {
259    position.x -= imageOffset_.x;
260    // Deal with Cocoa's flipped coordinate system.
261    position.y -= [dragImage_.get() size].height - imageOffset_.y;
262  }
263  // Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in
264  // third_party/WebKit/Source/WebKit/mac/Misc/WebNSViewExtras.m.
265  [window dragImage:[self dragImage]
266                 at:position
267             offset:NSZeroSize
268              event:dragEvent
269         pasteboard:pasteboard_
270             source:contentsView_
271          slideBack:YES];
272}
273
274- (void)endDragAt:(NSPoint)screenPoint
275        operation:(NSDragOperation)operation {
276  [contentsView_ tabContents]->SystemDragEnded();
277
278  RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host();
279  if (rvh) {
280    // Convert |screenPoint| to view coordinates and flip it.
281    NSPoint localPoint = NSMakePoint(0, 0);
282    if ([contentsView_ window])
283      localPoint = [self convertScreenPoint:screenPoint];
284    NSRect viewFrame = [contentsView_ frame];
285    localPoint.y = viewFrame.size.height - localPoint.y;
286    // Flip |screenPoint|.
287    NSRect screenFrame = [[[contentsView_ window] screen] frame];
288    screenPoint.y = screenFrame.size.height - screenPoint.y;
289
290    // If AppKit returns a copy and move operation, mask off the move bit
291    // because WebCore does not understand what it means to do both, which
292    // results in an assertion failure/renderer crash.
293    if (operation == (NSDragOperationMove | NSDragOperationCopy))
294      operation &= ~NSDragOperationMove;
295
296    rvh->DragSourceEndedAt(localPoint.x, localPoint.y,
297                           screenPoint.x, screenPoint.y,
298                           static_cast<WebKit::WebDragOperation>(operation));
299  }
300
301  // Make sure the pasteboard owner isn't us.
302  [pasteboard_ declareTypes:[NSArray array] owner:nil];
303}
304
305- (void)moveDragTo:(NSPoint)screenPoint {
306  RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host();
307  if (rvh) {
308    // Convert |screenPoint| to view coordinates and flip it.
309    NSPoint localPoint = NSMakePoint(0, 0);
310    if ([contentsView_ window])
311      localPoint = [self convertScreenPoint:screenPoint];
312    NSRect viewFrame = [contentsView_ frame];
313    localPoint.y = viewFrame.size.height - localPoint.y;
314    // Flip |screenPoint|.
315    NSRect screenFrame = [[[contentsView_ window] screen] frame];
316    screenPoint.y = screenFrame.size.height - screenPoint.y;
317
318    rvh->DragSourceMovedTo(localPoint.x, localPoint.y,
319                           screenPoint.x, screenPoint.y);
320  }
321}
322
323- (NSString*)dragPromisedFileTo:(NSString*)path {
324  // Be extra paranoid; avoid crashing.
325  if (!dropData_.get()) {
326    NOTREACHED() << "No drag-and-drop data available for promised file.";
327    return nil;
328  }
329
330  FilePath fileName = downloadFileName_.empty() ?
331      GetFileNameFromDragData(*dropData_) : downloadFileName_;
332  FilePath filePath(SysNSStringToUTF8(path));
333  filePath = filePath.Append(fileName);
334
335  // CreateFileStreamForDrop() will call file_util::PathExists(),
336  // which is blocking.  Since this operation is already blocking the
337  // UI thread on OSX, it should be reasonable to let it happen.
338  base::ThreadRestrictions::ScopedAllowIO allowIO;
339  FileStream* fileStream =
340      drag_download_util::CreateFileStreamForDrop(&filePath);
341  if (!fileStream)
342    return nil;
343
344  if (downloadURL_.is_valid()) {
345    TabContents* tabContents = [contentsView_ tabContents];
346    scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile(
347        filePath,
348        linked_ptr<net::FileStream>(fileStream),
349        downloadURL_,
350        tabContents->GetURL(),
351        tabContents->encoding(),
352        tabContents));
353
354    // The finalizer will take care of closing and deletion.
355    dragFileDownloader->Start(
356        new drag_download_util::PromiseFileFinalizer(dragFileDownloader));
357  } else {
358    // The writer will take care of closing and deletion.
359    g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE,
360        new PromiseWriterTask(*dropData_, fileStream));
361  }
362
363  // Once we've created the file, we should return the file name.
364  return SysUTF8ToNSString(filePath.BaseName().value());
365}
366
367@end  // @implementation WebDragSource
368
369
370@implementation WebDragSource (Private)
371
372- (void)fillPasteboard {
373  DCHECK(pasteboard_.get());
374
375  [pasteboard_ declareTypes:[NSArray array] owner:contentsView_];
376
377  // HTML.
378  if (!dropData_->text_html.empty())
379    [pasteboard_ addTypes:[NSArray arrayWithObject:NSHTMLPboardType]
380                    owner:contentsView_];
381
382  // URL (and title).
383  if (dropData_->url.is_valid())
384    [pasteboard_ addTypes:[NSArray arrayWithObjects:NSURLPboardType,
385                                                    kNSURLTitlePboardType, nil]
386                    owner:contentsView_];
387
388  // File.
389  if (!dropData_->file_contents.empty() ||
390      !dropData_->download_metadata.empty()) {
391    NSString* fileExtension = 0;
392
393    if (dropData_->download_metadata.empty()) {
394      // |dropData_->file_extension| comes with the '.', which we must strip.
395      fileExtension = (dropData_->file_extension.length() > 0) ?
396          SysUTF16ToNSString(dropData_->file_extension.substr(1)) : @"";
397    } else {
398      string16 mimeType;
399      FilePath fileName;
400      if (drag_download_util::ParseDownloadMetadata(
401              dropData_->download_metadata,
402              &mimeType,
403              &fileName,
404              &downloadURL_)) {
405        std::string contentDisposition =
406            "attachment; filename=" + fileName.value();
407        download_util::GenerateFileName(downloadURL_,
408                                        contentDisposition,
409                                        std::string(),
410                                        UTF16ToUTF8(mimeType),
411                                        &downloadFileName_);
412        fileExtension = SysUTF8ToNSString(downloadFileName_.Extension());
413      }
414    }
415
416    if (fileExtension) {
417      // File contents (with and without specific type), file (HFS) promise,
418      // TIFF.
419      // TODO(viettrungluu): others?
420      [pasteboard_ addTypes:[NSArray arrayWithObjects:
421                                  NSFileContentsPboardType,
422                                  NSCreateFileContentsPboardType(fileExtension),
423                                  NSFilesPromisePboardType,
424                                  NSTIFFPboardType,
425                                  nil]
426                      owner:contentsView_];
427
428      // For the file promise, we need to specify the extension.
429      [pasteboard_ setPropertyList:[NSArray arrayWithObject:fileExtension]
430                           forType:NSFilesPromisePboardType];
431    }
432  }
433
434  // Plain text.
435  if (!dropData_->plain_text.empty())
436    [pasteboard_ addTypes:[NSArray arrayWithObject:NSStringPboardType]
437                    owner:contentsView_];
438}
439
440- (NSImage*)dragImage {
441  if (dragImage_)
442    return dragImage_;
443
444  // Default to returning a generic image.
445  return app::mac::GetCachedImageWithName(@"nav.pdf");
446}
447
448@end  // @implementation WebDragSource (Private)
449