Home | History | Annotate | Download | only in tab_contents
      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 
     31 using base::SysNSStringToUTF8;
     32 using base::SysUTF8ToNSString;
     33 using base::SysUTF16ToNSString;
     34 using net::FileStream;
     35 
     36 
     37 namespace {
     38 
     39 // An unofficial standard pasteboard title type to be provided alongside the
     40 // |NSURLPboardType|.
     41 NSString* 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.
     46 FilePath 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.
     57 FilePath 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.
     83 class 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).
    100 PromiseWriterTask::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 
    107 PromiseWriterTask::~PromiseWriterTask() {
    108   DCHECK(file_stream_.get());
    109   if (file_stream_.get())
    110     file_stream_->Close();
    111 }
    112 
    113 void 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