chromium/content/app_shim_remote_cocoa/web_drag_source_mac.mm

// 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