1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
  235
  236
  237
  238
  239
  240
  241
  242
  243
  244
  245
  246
  247
  248
  249
  250
  251
  252
  253
  254
  255
  256
  257
  258
  259
  260
  261
  262
  263
  264
  265
  266
  267
  268
  269
  270
  271
  272
  273
  274
  275
  276
  277
  278
  279
  280
  281
  282
  283
  284
  285
  286
  287
  288
  289
  290
  291
  292
  293
  294
  295
  296
  297
  298
  299
  300
  301
  302
  303
  304
  305

content / app_shim_remote_cocoa / web_drag_source_mac.mm [blame]

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "content/app_shim_remote_cocoa/web_drag_source_mac.h"

#include <Cocoa/Cocoa.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <sys/param.h>

#include <memory>
#include <utility>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/pickle.h"
#include "base/strings/escape.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/download/drag_download_file.h"
#include "content/browser/download/drag_download_util.h"
#include "content/common/web_contents_ns_view_bridge.mojom.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/common/drop_data.h"
#include "net/base/apple/url_conversions.h"
#include "net/base/filename_util.h"
#include "net/base/mime_util.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/cocoa/cocoa_base_utils.h"
#include "url/origin.h"
#include "url/url_constants.h"

@implementation WebDragSource {
  // The host through which to communicate with the WebContents. Owns
  // this object. This pointer gets reset when the WebContents goes away with
  // `webContentsIsGone`.
  raw_ptr<remote_cocoa::mojom::WebContentsNSViewHost> _host;

  // The drop data.
  content::DropData _dropData;

  // The source origin the drop data came from.
  url::Origin _sourceOrigin;

  // Whether to mark the drag as having come from a privileged WebContents.
  BOOL _privileged;

  // The file name to be saved to for a drag-out download.
  base::FilePath _downloadFileName;

  // The URL to download from for a drag-out download.
  GURL _downloadURL;

  // The file type associated with the file drag, if any.
  UTType* __strong _fileType;
}

- (instancetype)initWithHost:(remote_cocoa::mojom::WebContentsNSViewHost*)host
                    dropData:(const content::DropData&)dropData
                sourceOrigin:(const url::Origin&)sourceOrigin
                isPrivileged:(BOOL)privileged {
  if ((self = [super init])) {
    _host = host;
    _dropData = dropData;
    _sourceOrigin = sourceOrigin;
    _privileged = privileged;
  }

  return self;
}

- (void)webContentsIsGone {
  _host = nullptr;
}

- (NSArray<NSPasteboardType>*)writableTypesForPasteboard:
    (NSPasteboard*)pasteboard {
  NSMutableArray<NSPasteboardType>* writableTypes = [NSMutableArray array];

  // Always add kUTTypeChromiumInitiatedDrag to mark this drag as something to
  // accept.
  [writableTypes addObject:ui::kUTTypeChromiumInitiatedDrag];

  // Always add kUTTypeChromiumRendererInitiatedDrag as all drags initiated here
  // are drags from the web.
  [writableTypes addObject:ui::kUTTypeChromiumRendererInitiatedDrag];

  // Tag the drag as coming from a privileged WebContents if needed.
  if (_privileged) {
    [writableTypes addObject:ui::kUTTypeChromiumPrivilegedInitiatedDrag];
  }

  // URL (and title).
  if (_dropData.url.is_valid()) {
    [writableTypes addObject:NSPasteboardTypeURL];
    [writableTypes addObject:ui::kUTTypeURLName];
  }

  // File.
  if (!_dropData.file_contents.empty() ||
      !_dropData.download_metadata.empty()) {
    std::string mimeType;

    // TODO(crbug.com/40599578): The |downloadFileName_| and
    // |downloadURL_| values should be computed by the caller.
    if (_dropData.download_metadata.empty()) {
      std::optional<base::FilePath> suggestedFilename =
          _dropData.GetSafeFilenameForImageFileContents();
      if (suggestedFilename) {
        _downloadFileName = std::move(*suggestedFilename);
        net::GetMimeTypeFromFile(_downloadFileName, &mimeType);
      }
    } else {
      std::u16string mimeType16;
      base::FilePath filename;
      if (content::ParseDownloadMetadata(_dropData.download_metadata,
                                         &mimeType16, &filename,
                                         &_downloadURL)) {
        // Generate the file name based on both mime type and proposed file
        // name.
        std::string defaultName = content::GetContentClient()->browser()
                                      ? content::GetContentClient()
                                            ->browser()
                                            ->GetDefaultDownloadName()
                                      : std::string();
        mimeType = base::UTF16ToUTF8(mimeType16);
        _downloadFileName =
            net::GenerateFileName(_downloadURL, std::string(), std::string(),
                                  filename.value(), mimeType, defaultName);
      }
    }

    if (!mimeType.empty()) {
      _fileType = [UTType typeWithMIMEType:base::SysUTF8ToNSString(mimeType)];

      // Promise both the file's contents...
      if (!_dropData.file_contents.empty()) {
        [writableTypes addObject:_fileType.identifier];
      }

      // ... and materialization of the file if requested.

      // NB: Why not use `NSFilePromiseProvider`? Its design is fundamentally
      // broken. It insists on being added to the pasteboard as its own object,
      // but this code needs to add many, many flavors as one object. The only
      // way to get it to share a pasteboard item with other flavors is to play
      // the game of subclassing it, but that would involve a big rewrite of all
      // of this code. FB11876926
      //
      // https://buckleyisms.com/blog/how-to-actually-implement-file-dragging-from-your-app-on-mac/

      [writableTypes
          addObject:base::apple::CFToNSPtrCast(kPasteboardTypeFileURLPromise)];
      [writableTypes addObject:base::apple::CFToNSPtrCast(
                                   kPasteboardTypeFilePromiseContent)];
    }
  }

  // HTML.
  bool hasHTMLData = _dropData.html && !_dropData.html->empty();
  // Mail.app and TextEdit accept drags that have both HTML and image flavors on
  // them, but don't process them correctly <http://crbug.com/55879>. Therefore,
  // if there is an image flavor, don't put the HTML data on as HTML, but rather
  // put it on as this Chrome-only flavor.
  //
  // (The only time that Blink fills in the DropData::file_contents is with
  // an image drop, but the MIME time is tested anyway for paranoia's sake.)
  bool hasImageData = !_dropData.file_contents.empty() && _fileType &&
                      [_fileType conformsToType:UTTypeImage];
  if (hasHTMLData) {
    if (hasImageData) {
      [writableTypes addObject:ui::kUTTypeChromiumImageAndHTML];
    } else {
      [writableTypes addObject:NSPasteboardTypeHTML];
    }
  }

  // Plain text.
  if (_dropData.text && !_dropData.text->empty()) {
    [writableTypes addObject:NSPasteboardTypeString];
  }

  if (!_dropData.custom_data.empty()) {
    [writableTypes addObject:ui::kUTTypeChromiumDataTransferCustomData];
  }

  return writableTypes;
}

- (id)pasteboardPropertyListForType:(NSPasteboardType)type {
  // HTML.
  if ([type isEqualToString:NSPasteboardTypeHTML] ||
      [type isEqualToString:ui::kUTTypeChromiumImageAndHTML]) {
    DCHECK(_dropData.html && !_dropData.html->empty());

    // NSPasteboardTypeHTML requires the character set to be declared.
    // Otherwise, it assumes US-ASCII. Awesome.
    static constexpr char16_t kHtmlHeader[] =
        u"<meta http-equiv=\"Content-Type\" "
        u"content=\"text/html;charset=UTF-8\">";
    return base::SysUTF16ToNSString(kHtmlHeader + *_dropData.html);
  }

  // URL.
  if ([type isEqualToString:NSPasteboardTypeURL]) {
    DCHECK(_dropData.url.is_valid());
    NSURL* url = net::NSURLWithGURL(_dropData.url);
    // If NSURL creation failed, check for a badly-escaped JavaScript URL.
    // Strip out any existing escapes and then re-escape uniformly.
    if (!url && _dropData.url.SchemeIs(url::kJavaScriptScheme)) {
      std::string unescapedUrlString =
          base::UnescapeBinaryURLComponent(_dropData.url.spec());
      std::string escapedUrlString =
          base::EscapeUrlEncodedData(unescapedUrlString, false);
      url = [NSURL URLWithString:base::SysUTF8ToNSString(escapedUrlString)];
    }
    return url.absoluteString;
  }

  // URL title.
  if ([type isEqualToString:ui::kUTTypeURLName]) {
    return base::SysUTF16ToNSString(_dropData.url_title);
  }

  // File contents.
  if ([type isEqualToString:_fileType.identifier]) {
    return [NSData dataWithBytes:_dropData.file_contents.data()
                          length:_dropData.file_contents.length()];
  }

  // File instantiation promise.
  if ([type isEqualToString:base::apple::CFToNSPtrCast(
                                kPasteboardTypeFilePromiseContent)]) {
    return _fileType.identifier;
  }
  if ([type isEqualToString:base::apple::CFToNSPtrCast(
                                kPasteboardTypeFileURLPromise)]) {
    // The official way of getting the drop destination is to call
    // `PasteboardCopyPasteLocation` on the Carbon Pasteboard Manager, but what
    // that function does is pull the location from "com.apple.pastelocation".
    // Therefore, do that directly rather than indirecting to a different API
    // set that does no useful bridging.
    NSPasteboard* pasteboard =
        [NSPasteboard pasteboardWithName:NSPasteboardNameDrag];
    NSString* dropDestination =
        [pasteboard stringForType:@"com.apple.pastelocation"];
    if (!dropDestination || !_host) {
      // Something has gone wrong, but understandably. Chromium leaves the data
      // around on the pasteboard after the drag, and it's possible that some
      // app is rummaging around for what it can find. Silently fail in this
      // case.
      return [NSData data];
    }

    base::FilePath filePath =
        base::apple::NSURLToFilePath([NSURL URLWithString:dropDestination]);
    filePath = filePath.Append(_downloadFileName);
    _host->DragPromisedFileTo(filePath, _dropData, _downloadURL, _sourceOrigin,
                              &filePath);

    // The process of writing the file may have altered the value of
    // `filePath` if, say, an existing file at the drop site already had that
    // name. Return the actual URL to the file that was written.
    return base::apple::FilePathToNSURL(filePath).absoluteString;
  }

  // Plain text.
  if ([type isEqualToString:NSPasteboardTypeString]) {
    DCHECK(_dropData.text && !_dropData.text->empty());
    return base::SysUTF16ToNSString(*_dropData.text);
  }

  // Custom MIME data.
  if ([type isEqualToString:ui::kUTTypeChromiumDataTransferCustomData]) {
    base::Pickle pickle;
    ui::WriteCustomDataToPickle(_dropData.custom_data, &pickle);
    return [NSData dataWithBytes:pickle.data() length:pickle.size()];
  }

  // Source origin of the drop data.
  if ([type isEqualToString:ui::kUTTypeChromiumRendererInitiatedDrag]) {
    return _sourceOrigin.opaque()
               ? [NSString string]
               : base::SysUTF8ToNSString(_sourceOrigin.Serialize());
  }

  // Flavors used to tag.
  if ([type isEqualToString:ui::kUTTypeChromiumInitiatedDrag] ||
      [type isEqualToString:ui::kUTTypeChromiumPrivilegedInitiatedDrag]) {
    // The type _was_ promised and someone decided to call the bluff.
    return [NSData data];
  }

  // Oops! Unknown drag pasteboard type.
  NOTREACHED_IN_MIGRATION();
  return [NSData data];
}

@end  // @implementation WebDragSource