chromium/ui/base/clipboard/clipboard_ios.mm

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

#include "ui/base/clipboard/clipboard_ios.h"

#import <UIKit/UIKit.h>

#include <string_view>

#include "base/apple/foundation_util.h"
#include "base/containers/span.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "skia/ext/skia_utils_base.h"
#include "skia/ext/skia_utils_ios.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/clipboard_metrics.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/gfx/image/image.h"

namespace ui {

namespace {

UIPasteboard* GetPasteboard() {
  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
  return pasteboard;
}

NSData* GetDataWithTypeFromPasteboard(UIPasteboard* pasteboard,
                                      NSString* type) {
  DCHECK(pasteboard);
  auto items = [pasteboard dataForPasteboardType:type inItemSet:nil];
  if (!items) {
    return nullptr;
  }
  return [items firstObject];
}

}  // namespace

Clipboard* Clipboard::Create() {
  return new ClipboardIOS;
}

// ClipboardIOS implementation.
ClipboardIOS::ClipboardIOS() {
  DCHECK(CalledOnValidThread());
}

ClipboardIOS::~ClipboardIOS() {
  DCHECK(CalledOnValidThread());
}

void ClipboardIOS::OnPreShutdown() {}

// DataTransferEndpoint is not used on this platform.
std::optional<DataTransferEndpoint> ClipboardIOS::GetSource(
    ClipboardBuffer buffer) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  return std::nullopt;
}

const ClipboardSequenceNumberToken& ClipboardIOS::GetSequenceNumber(
    ClipboardBuffer buffer) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);

  NSInteger sequence_number = [GetPasteboard() changeCount];
  if (sequence_number != clipboard_sequence_.sequence_number) {
    // Generate a unique token associated with the current sequence number.
    clipboard_sequence_ = {sequence_number, ClipboardSequenceNumberToken()};
  }
  return clipboard_sequence_.token;
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
std::vector<std::u16string> ClipboardIOS::GetStandardFormats(
    ClipboardBuffer buffer,
    const DataTransferEndpoint* data_dst) const {
  std::vector<std::u16string> types;
  if (IsFormatAvailable(ClipboardFormatType::PlainTextType(), buffer,
                        data_dst)) {
    types.push_back(base::UTF8ToUTF16(kMimeTypeText));
  }
  if (IsFormatAvailable(ClipboardFormatType::HtmlType(), buffer, data_dst)) {
    types.push_back(base::UTF8ToUTF16(kMimeTypeHTML));
  }
  if (IsFormatAvailable(ClipboardFormatType::SvgType(), buffer, data_dst)) {
    types.push_back(base::UTF8ToUTF16(kMimeTypeSvg));
  }
  if (IsFormatAvailable(ClipboardFormatType::RtfType(), buffer, data_dst)) {
    types.push_back(base::UTF8ToUTF16(kMimeTypeRTF));
  }
  if (IsFormatAvailable(ClipboardFormatType::FilenamesType(), buffer,
                        data_dst)) {
    types.push_back(base::UTF8ToUTF16(kMimeTypeURIList));
  }
  return types;
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
bool ClipboardIOS::IsFormatAvailable(
    const ClipboardFormatType& format,
    ClipboardBuffer buffer,
    const DataTransferEndpoint* data_dst) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);

  return [GetPasteboard() containsPasteboardTypes:@[ format.ToNSString() ]
                                        inItemSet:nil];
}

void ClipboardIOS::Clear(ClipboardBuffer buffer) {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);

  [GetPasteboard() setItems:@[]];
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadAvailableTypes(
    ClipboardBuffer buffer,
    const DataTransferEndpoint* data_dst,
    std::vector<std::u16string>* types) const {
  DCHECK(CalledOnValidThread());
  DCHECK(types);

  types->clear();
  *types = GetStandardFormats(buffer, data_dst);

  NSData* data = GetDataWithTypeFromPasteboard(
      GetPasteboard(), (NSString*)kUTTypeChromiumDataTransferCustomData);
  if (data) {
    ReadCustomDataTypes(
        base::span(reinterpret_cast<const uint8_t*>([data bytes]),
                   [data length]),
        types);
  }
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadText(ClipboardBuffer buffer,
                            const DataTransferEndpoint* data_dst,
                            std::u16string* result) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  RecordRead(ClipboardFormatMetric::kText);

  NSData* data = GetDataWithTypeFromPasteboard(
      GetPasteboard(), ClipboardFormatType::PlainTextType().ToNSString());
  if (data) {
    NSString* contents = [[NSString alloc] initWithData:data
                                               encoding:NSUTF8StringEncoding];
    result->assign(base::SysNSStringToUTF16(contents));
  }
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadAsciiText(ClipboardBuffer buffer,
                                 const DataTransferEndpoint* data_dst,
                                 std::string* result) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  RecordRead(ClipboardFormatMetric::kText);

  NSData* data = GetDataWithTypeFromPasteboard(
      GetPasteboard(), ClipboardFormatType::PlainTextType().ToNSString());
  if (data) {
    NSString* contents = [[NSString alloc] initWithData:data
                                               encoding:NSUTF8StringEncoding];
    result->assign(base::SysNSStringToUTF8(contents));
  }
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadHTML(ClipboardBuffer buffer,
                            const DataTransferEndpoint* data_dst,
                            std::u16string* markup,
                            std::string* src_url,
                            uint32_t* fragment_start,
                            uint32_t* fragment_end) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  RecordRead(ClipboardFormatMetric::kHtml);

  NSString* best_type = nil;
  for (NSString* type in @[
         ClipboardFormatType::HtmlType().ToNSString(),
         ClipboardFormatType::RtfType().ToNSString(),
         ClipboardFormatType::PlainTextType().ToNSString()
       ]) {
    if ([GetPasteboard() containsPasteboardTypes:@[ type ]]) {
      best_type = type;
      break;
    }
  }

  NSData* data = GetDataWithTypeFromPasteboard(GetPasteboard(), best_type);
  if (data) {
    NSString* contents = [[NSString alloc] initWithData:data
                                               encoding:NSUTF8StringEncoding];
    markup->assign(base::SysNSStringToUTF16(contents));
  }

  *fragment_start = 0;
  DCHECK_LE(markup->length(), std::numeric_limits<uint32_t>::max());
  *fragment_end = static_cast<uint32_t>(markup->length());

  // TODO: src_url
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadSvg(ClipboardBuffer buffer,
                           const DataTransferEndpoint* data_dst,
                           std::u16string* result) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  RecordRead(ClipboardFormatMetric::kSvg);

  NSData* data = GetDataWithTypeFromPasteboard(
      GetPasteboard(), ClipboardFormatType::SvgType().ToNSString());
  if (data) {
    NSString* contents = [[NSString alloc] initWithData:data
                                               encoding:NSUTF8StringEncoding];
    result->assign(base::SysNSStringToUTF16(contents));
  }
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadRTF(ClipboardBuffer buffer,
                           const DataTransferEndpoint* data_dst,
                           std::string* result) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  RecordRead(ClipboardFormatMetric::kRtf);

  return ReadData(ClipboardFormatType::RtfType(), data_dst, result);
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadPng(ClipboardBuffer buffer,
                           const DataTransferEndpoint* data_dst,
                           ReadPngCallback callback) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  RecordRead(ClipboardFormatMetric::kPng);

  std::move(callback).Run(ReadPngInternal(buffer, GetPasteboard()));
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadDataTransferCustomData(
    ClipboardBuffer buffer,
    const std::u16string& type,
    const DataTransferEndpoint* data_dst,
    std::u16string* result) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  RecordRead(ClipboardFormatMetric::kCustomData);

  NSData* data = GetDataWithTypeFromPasteboard(
      GetPasteboard(), (NSString*)kUTTypeChromiumDataTransferCustomData);
  if (data) {
    if (std::optional<std::u16string> maybe_result = ReadCustomDataForType(
            base::span(reinterpret_cast<const uint8_t*>([data bytes]),
                       [data length]),
            type);
        maybe_result) {
      *result = std::move(*maybe_result);
    }
  }
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadFilenames(ClipboardBuffer buffer,
                                 const DataTransferEndpoint* data_dst,
                                 std::vector<ui::FileInfo>* result) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  RecordRead(ClipboardFormatMetric::kFilenames);

  auto items = [GetPasteboard()
      dataForPasteboardType:ClipboardFormatType::FilenamesType().ToNSString()
                  inItemSet:nil];
  if (!items) {
    return;
  }

  std::vector<ui::FileInfo> files;
  for (NSData* item : items) {
    if (item) {
      NSString* file_str = [[NSString alloc] initWithData:item
                                                 encoding:NSUTF8StringEncoding];
      NSURL* file_url = [NSURL URLWithString:file_str];
      files.emplace_back(
          base::apple::NSURLToFilePath(file_url),
          base::apple::NSStringToFilePath(file_url.lastPathComponent));
    }
  }
  base::ranges::move(files, std::back_inserter(*result));
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadBookmark(const DataTransferEndpoint* data_dst,
                                std::u16string* title,
                                std::string* url) const {
  DCHECK(CalledOnValidThread());
  RecordRead(ClipboardFormatMetric::kBookmark);

  NSData* url_data = GetDataWithTypeFromPasteboard(
      GetPasteboard(), ClipboardFormatType::UrlType().ToNSString());
  if (url_data) {
    NSString* contents = [[NSString alloc] initWithData:url_data
                                               encoding:NSUTF8StringEncoding];
    url->assign(base::SysNSStringToUTF8(contents));
  }

  NSData* title_data =
      GetDataWithTypeFromPasteboard(GetPasteboard(), kUTTypeURLName);
  if (title_data) {
    NSString* contents = [[NSString alloc] initWithData:title_data
                                               encoding:NSUTF8StringEncoding];
    title->assign(base::SysNSStringToUTF16(contents));
  }
}

// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::ReadData(const ClipboardFormatType& format,
                            const DataTransferEndpoint* data_dst,
                            std::string* result) const {
  DCHECK(CalledOnValidThread());
  RecordRead(ClipboardFormatMetric::kData);

  NSData* data =
      GetDataWithTypeFromPasteboard(GetPasteboard(), format.ToNSString());
  if (data) {
    result->assign(static_cast<const char*>([data bytes]), [data length]);
  }
}

// |data_src| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardIOS::WritePortableAndPlatformRepresentations(
    ClipboardBuffer buffer,
    const ObjectMap& objects,
    std::vector<Clipboard::PlatformRepresentation> platform_representations,
    std::unique_ptr<DataTransferEndpoint> data_src,
    uint32_t privacy_types) {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);

  [GetPasteboard() setItems:@[]];

  DispatchPlatformRepresentations(std::move(platform_representations));
  for (const auto& object : objects) {
    DispatchPortableRepresentation(object.second);
  }
}

void ClipboardIOS::WriteText(std::string_view text) {
  NSDictionary<NSString*, id>* text_item = @{
    ClipboardFormatType::PlainTextType().ToNSString() :
        base::SysUTF8ToNSString(text)
  };
  [GetPasteboard() addItems:@[ text_item ]];
}

void ClipboardIOS::WriteHTML(std::string_view markup,
                             std::optional<std::string_view> /* source_url */) {
  // We need to mark it as utf-8. (see crbug.com/40759159)
  std::string html_fragment_str("<meta charset='utf-8'>");
  html_fragment_str += markup;
  NSString* html = base::SysUTF8ToNSString(html_fragment_str);

  NSDictionary<NSString*, id>* html_item =
      @{ClipboardFormatType::HtmlType().ToNSString() : html};
  [GetPasteboard() addItems:@[ html_item ]];
}

void ClipboardIOS::WriteSvg(std::string_view markup) {
  NSDictionary<NSString*, id>* svg_item = @{
    ClipboardFormatType::SvgType().ToNSString() :
        base::SysUTF8ToNSString(markup)
  };
  [GetPasteboard() addItems:@[ svg_item ]];
}

void ClipboardIOS::WriteRTF(std::string_view rtf) {
  WriteData(ClipboardFormatType::RtfType(),
            base::as_bytes(base::make_span(rtf)));
}

void ClipboardIOS::WriteFilenames(std::vector<ui::FileInfo> filenames) {
  if (filenames.empty()) {
    return;
  }

  NSMutableArray<NSDictionary<NSString*, id>*>* items =
      [NSMutableArray arrayWithCapacity:filenames.size()];
  for (const auto& file : filenames) {
    NSURL* url = base::apple::FilePathToNSURL(file.path);
    NSString* fileURLType = ClipboardFormatType::FilenamesType().ToNSString();
    NSDictionary<NSString*, id>* item = @{fileURLType : url.absoluteString};
    [items addObject:item];
  }

  [GetPasteboard() addItems:items];
}

void ClipboardIOS::WriteBookmark(std::string_view title, std::string_view url) {
  NSDictionary<NSString*, id>* bookmarkItem = @{
    ClipboardFormatType::UrlType().ToNSString() : base::SysUTF8ToNSString(url),
    kUTTypeURLName : base::SysUTF8ToNSString(title),
  };

  [GetPasteboard() addItems:@[ bookmarkItem ]];
}

// Write an extra flavor that signifies WebKit was the last to modify the
// pasteboard. This flavor has no data.
void ClipboardIOS::WriteWebSmartPaste() {
  NSDictionary<NSString*, id>* item = @{
    ClipboardFormatType::WebKitSmartPasteType().ToNSString() : [NSData data]
  };
  [GetPasteboard() addItems:@[ item ]];
}

void ClipboardIOS::WriteBitmap(const SkBitmap& bitmap) {
  // The bitmap type is sanitized to be N32 before we get here. The conversion
  // to a UIImage would not explode if we got this wrong, so this is not a
  // security CHECK.
  DCHECK_EQ(bitmap.colorType(), kN32_SkColorType);

  base::apple::ScopedCFTypeRef<CGColorSpaceRef> color_space(
      CGColorSpaceCreateDeviceRGB());
  UIImage* image =
      skia::SkBitmapToUIImageWithColorSpace(bitmap, 1.0f, color_space.get());
  CHECK(image) << "SkBitmapToUIImageWithColorSpace failed";

  [GetPasteboard() setImage:image];
}

void ClipboardIOS::WriteData(const ClipboardFormatType& format,
                             base::span<const uint8_t> data) {
  NSDictionary<NSString*, id>* data_item = @{
    format.ToNSString() : [NSData dataWithBytes:data.data() length:data.size()]
  };
  [GetPasteboard() addItems:@[ data_item ]];
}

void ClipboardIOS::WriteClipboardHistory() {
  // TODO(crbug.com/40945200): Add support for this.
}

void ClipboardIOS::WriteUploadCloudClipboard() {
  // TODO(crbug.com/40945200): Add support for this.
}

void ClipboardIOS::WriteConfidentialDataForPassword() {
  // TODO(crbug.com/40945200): Add support for this.
}

std::vector<uint8_t> ClipboardIOS::ReadPngInternal(
    ClipboardBuffer buffer,
    UIPasteboard* pasteboard) const {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
  DCHECK(pasteboard);

  UIImage* image = pasteboard.image;
  if (!image) {
    return std::vector<uint8_t>();
  }

  NSData* png_data = UIImagePNGRepresentation(image);
  if (!png_data) {
    return std::vector<uint8_t>();
  }

  const uint8_t* bytes = (const uint8_t*)png_data.bytes;
  std::vector<uint8_t> png(bytes, bytes + png_data.length);
  return png;
}

}  // namespace ui