// 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.
#include "ui/base/clipboard/clipboard_mac.h"
#import <Cocoa/Cocoa.h>
#include <stdint.h>
#include <limits>
#include <string_view>
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/memory/ref_counted_memory.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "net/base/filename_util.h"
#include "skia/ext/skia_utils_base.h"
#include "skia/ext/skia_utils_mac.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/clipboard_metrics.h"
#include "ui/base/clipboard/clipboard_monitor.h"
#include "ui/base/clipboard/clipboard_util_mac.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
#include "url/gurl.h"
namespace ui {
namespace {
NSPasteboard* GetPasteboard() {
// The pasteboard can always be nil, since there is a finite amount of storage
// that must be shared between all pasteboards.
NSPasteboard* pasteboard = NSPasteboard.generalPasteboard;
return pasteboard;
}
NSImage* GetNSImage(NSPasteboard* pasteboard) {
// If the pasteboard's image data is not to its liking, the guts of NSImage
// may throw, and that exception will leak. Prevent a crash in that case;
// a blank image is better.
NSImage* image;
@try {
if (pasteboard) {
image = [[NSImage alloc] initWithPasteboard:pasteboard];
}
} @catch (id exception) {
}
if (!image) {
return nil;
}
if (image.representations.count == 0u) {
return nil;
}
return image;
}
// Read raw PNG bytes from the clipboard.
std::vector<uint8_t> GetPngFromPasteboard(NSPasteboard* pasteboard) {
if (!pasteboard) {
return {};
}
NSData* data = [pasteboard dataForType:NSPasteboardTypePNG];
auto span = base::apple::NSDataToSpan(data);
return {span.begin(), span.end()};
}
std::vector<uint8_t> EncodeGfxImageToPng(gfx::Image image) {
base::AssertLongCPUWorkAllowed();
if (image.IsEmpty()) {
return {};
}
scoped_refptr<base::RefCountedMemory> mem = image.As1xPNGBytes();
std::vector<uint8_t> image_data(mem->data(), mem->data() + mem->size());
return image_data;
}
} // namespace
// Clipboard factory method.
// static
Clipboard* Clipboard::Create() {
return new ClipboardMac;
}
// ClipboardMac implementation.
ClipboardMac::ClipboardMac() {
DCHECK(CalledOnValidThread());
}
ClipboardMac::~ClipboardMac() {
DCHECK(CalledOnValidThread());
}
void ClipboardMac::OnPreShutdown() {}
std::optional<DataTransferEndpoint> ClipboardMac::GetSource(
ClipboardBuffer buffer) const {
return GetSourceInternal(buffer, GetPasteboard());
}
std::optional<DataTransferEndpoint> ClipboardMac::GetSourceInternal(
ClipboardBuffer buffer,
NSPasteboard* pasteboard) const {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
NSString* source_url = [pasteboard stringForType:kUTTypeChromiumSourceURL];
if (!source_url) {
return std::nullopt;
}
GURL gurl(base::SysNSStringToUTF8(source_url));
if (!gurl.is_valid()) {
return std::nullopt;
}
return DataTransferEndpoint(std::move(gurl));
}
const ClipboardSequenceNumberToken& ClipboardMac::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.
bool ClipboardMac::IsFormatAvailable(
const ClipboardFormatType& format,
ClipboardBuffer buffer,
const DataTransferEndpoint* data_dst) const {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
// https://crbug.com/1016740#c21: The pasteboard types array may end up going
// away; make a copy.
NSArray* types = [GetPasteboard().types copy];
// Safari only places RTF on the pasteboard, never HTML. We can convert RTF
// to HTML, so the presence of either indicates success when looking for HTML.
if (format == ClipboardFormatType::HtmlType()) {
return [types containsObject:NSPasteboardTypeHTML] ||
[types containsObject:NSPasteboardTypeRTF];
}
// Chrome can retrieve an image from the clipboard as either a bitmap or PNG.
if (format == ClipboardFormatType::PngType() ||
format == ClipboardFormatType::BitmapType()) {
return [types containsObject:NSPasteboardTypePNG] ||
[types containsObject:NSPasteboardTypeTIFF];
}
return [types containsObject:format.ToNSString()];
}
bool ClipboardMac::IsMarkedByOriginatorAsConfidential() const {
DCHECK(CalledOnValidThread());
NSPasteboardType type =
[GetPasteboard() availableTypeFromArray:@[ kUTTypeConfidentialData ]];
if (type)
return true;
return false;
}
void ClipboardMac::Clear(ClipboardBuffer buffer) {
ClearInternal(buffer, GetPasteboard());
}
void ClipboardMac::ClearInternal(ClipboardBuffer buffer,
NSPasteboard* pasteboard) {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
[pasteboard clearContents];
}
std::vector<std::u16string> ClipboardMac::GetStandardFormats(
ClipboardBuffer buffer,
const DataTransferEndpoint* data_dst) const {
std::vector<std::u16string> types;
NSPasteboard* pb = GetPasteboard();
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));
} else if (pb && [NSImage canInitWithPasteboard:pb]) {
// Finder Cmd+C places both file and icon onto the clipboard
// (http://crbug.com/553686), so ignore images if we have detected files.
// This means that if an image is present with file content, we will always
// ignore the image, but this matches observable Safari behavior.
types.push_back(base::UTF8ToUTF16(kMimeTypePNG));
}
return types;
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::ReadAvailableTypes(
ClipboardBuffer buffer,
const DataTransferEndpoint* data_dst,
std::vector<std::u16string>* types) const {
DCHECK(CalledOnValidThread());
DCHECK(types);
NSPasteboard* pb = GetPasteboard();
types->clear();
*types = GetStandardFormats(buffer, data_dst);
if ([pb.types containsObject:kUTTypeChromiumDataTransferCustomData]) {
NSData* data = [pb dataForType:kUTTypeChromiumDataTransferCustomData];
if ([data length]) {
ReadCustomDataTypes(base::apple::NSDataToSpan(data), types);
}
}
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::ReadText(ClipboardBuffer buffer,
const DataTransferEndpoint* data_dst,
std::u16string* result) const {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
RecordRead(ClipboardFormatMetric::kText);
NSString* contents = [GetPasteboard() stringForType:NSPasteboardTypeString];
*result = base::SysNSStringToUTF16(contents);
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::ReadAsciiText(ClipboardBuffer buffer,
const DataTransferEndpoint* data_dst,
std::string* result) const {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
RecordRead(ClipboardFormatMetric::kText);
NSString* contents = [GetPasteboard() stringForType:NSPasteboardTypeString];
if (!contents)
result->clear();
else
result->assign(base::SysNSStringToUTF8(contents));
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::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);
// TODO(avi): src_url?
markup->clear();
if (src_url)
src_url->clear();
NSPasteboard* pb = GetPasteboard();
NSArray* supportedTypes =
@[ NSPasteboardTypeHTML, NSPasteboardTypeRTF, NSPasteboardTypeString ];
NSString* bestType = [pb availableTypeFromArray:supportedTypes];
if (bestType) {
NSString* contents;
if ([bestType isEqualToString:NSPasteboardTypeRTF]) {
contents = clipboard_util::GetHTMLFromRTFOnPasteboard(pb);
} else {
contents = [pb stringForType:bestType];
}
*markup = base::SysNSStringToUTF16(contents);
}
*fragment_start = 0;
DCHECK_LE(markup->length(), std::numeric_limits<uint32_t>::max());
*fragment_end = static_cast<uint32_t>(markup->length());
}
void ClipboardMac::ReadSvg(ClipboardBuffer buffer,
const DataTransferEndpoint* data_dst,
std::u16string* result) const {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
RecordRead(ClipboardFormatMetric::kSvg);
NSString* contents = [GetPasteboard()
stringForType:ClipboardFormatType::SvgType().ToNSString()];
*result = base::SysNSStringToUTF16(contents);
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::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 ClipboardMac::ReadPng(ClipboardBuffer buffer,
const DataTransferEndpoint* data_dst,
ReadPngCallback callback) const {
RecordRead(ClipboardFormatMetric::kPng);
ReadPngInternal(buffer, GetPasteboard(), std::move(callback));
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::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);
NSPasteboard* pb = GetPasteboard();
if ([[pb types] containsObject:kUTTypeChromiumDataTransferCustomData]) {
NSData* data = [pb dataForType:kUTTypeChromiumDataTransferCustomData];
if ([data length]) {
if (std::optional<std::u16string> maybe_result =
ReadCustomDataForType(base::apple::NSDataToSpan(data), type);
maybe_result) {
*result = std::move(*maybe_result);
}
}
}
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::ReadFilenames(ClipboardBuffer buffer,
const DataTransferEndpoint* data_dst,
std::vector<ui::FileInfo>* result) const {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
RecordRead(ClipboardFormatMetric::kFilenames);
std::vector<ui::FileInfo> files =
clipboard_util::FilesFromPasteboard(GetPasteboard());
base::ranges::move(files, std::back_inserter(*result));
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::ReadBookmark(const DataTransferEndpoint* data_dst,
std::u16string* title,
std::string* url) const {
DCHECK(CalledOnValidThread());
RecordRead(ClipboardFormatMetric::kBookmark);
NSPasteboard* pb = GetPasteboard();
if (title) {
NSString* contents = [pb stringForType:kUTTypeURLName];
*title = base::SysNSStringToUTF16(contents);
}
if (url) {
NSString* url_string = [pb stringForType:NSPasteboardTypeURL];
if (!url_string)
url->clear();
else
url->assign(base::SysNSStringToUTF8(url_string));
}
}
// |data_dst| is not used. It's only passed to be consistent with other
// platforms.
void ClipboardMac::ReadData(const ClipboardFormatType& format,
const DataTransferEndpoint* data_dst,
std::string* result) const {
DCHECK(CalledOnValidThread());
RecordRead(ClipboardFormatMetric::kData);
NSData* data = [GetPasteboard() dataForType:format.ToNSString()];
if ([data length])
*result = as_string_view(base::apple::NSDataToSpan(data));
}
void ClipboardMac::WritePortableAndPlatformRepresentations(
ClipboardBuffer buffer,
const ObjectMap& objects,
std::vector<Clipboard::PlatformRepresentation> platform_representations,
std::unique_ptr<DataTransferEndpoint> data_src,
uint32_t privacy_types) {
WritePortableAndPlatformRepresentationsInternal(
buffer, objects, std::move(platform_representations), std::move(data_src),
GetPasteboard(), privacy_types);
}
void ClipboardMac::WritePortableAndPlatformRepresentationsInternal(
ClipboardBuffer buffer,
const ObjectMap& objects,
std::vector<Clipboard::PlatformRepresentation> platform_representations,
std::unique_ptr<DataTransferEndpoint> data_src,
NSPasteboard* pasteboard,
uint32_t privacy_types) {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
if (privacy_types & Clipboard::PrivacyTypes::kNoCloudClipboard) {
WriteUploadCloudClipboard();
}
[pasteboard declareTypes:@[] owner:nil];
DispatchPlatformRepresentations(std::move(platform_representations));
for (const auto& object : objects)
DispatchPortableRepresentation(object.second);
if (data_src && data_src->IsUrlType()) {
[pasteboard setString:base::SysUTF8ToNSString(data_src->GetURL()->spec())
forType:kUTTypeChromiumSourceURL];
}
if (privacy_types & Clipboard::PrivacyTypes::kNoDisplay) {
WriteConfidentialDataForPassword();
}
ClipboardMonitor::GetInstance()->NotifyClipboardDataChanged();
}
void ClipboardMac::WriteText(std::string_view text) {
[GetPasteboard() setString:base::SysUTF8ToNSString(text)
forType:NSPasteboardTypeString];
}
void ClipboardMac::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.append(markup);
NSString* html_fragment = base::SysUTF8ToNSString(html_fragment_str);
[GetPasteboard() setString:html_fragment forType:NSPasteboardTypeHTML];
}
void ClipboardMac::WriteSvg(std::string_view markup) {
[GetPasteboard() setString:base::SysUTF8ToNSString(markup)
forType:ClipboardFormatType::SvgType().ToNSString()];
}
void ClipboardMac::WriteRTF(std::string_view rtf) {
WriteData(ClipboardFormatType::RtfType(),
base::as_bytes(base::make_span(rtf)));
}
void ClipboardMac::WriteFilenames(std::vector<ui::FileInfo> filenames) {
clipboard_util::WriteFilesToPasteboard(GetPasteboard(), filenames);
}
void ClipboardMac::WriteBookmark(std::string_view title, std::string_view url) {
NSArray<NSPasteboardItem*>* items = clipboard_util::PasteboardItemsFromUrls(
@[ base::SysUTF8ToNSString(url) ], @[ base::SysUTF8ToNSString(title) ]);
clipboard_util::AddDataToPasteboard(GetPasteboard(), items.firstObject);
}
void ClipboardMac::WriteBitmap(const SkBitmap& bitmap) {
// The bitmap type is sanitized to be N32 before we get here. The conversion
// to an NSImage would not explode if we got this wrong, so this is not a
// security CHECK.
DCHECK_EQ(bitmap.colorType(), kN32_SkColorType);
WriteBitmapInternal(bitmap, GetPasteboard());
}
void ClipboardMac::WriteData(const ClipboardFormatType& format,
base::span<const uint8_t> data) {
[GetPasteboard() setData:[NSData dataWithBytes:data.data() length:data.size()]
forType:format.ToNSString()];
}
void ClipboardMac::WriteClipboardHistory() {
// TODO(crbug.com/40945200): Add support for this.
}
void ClipboardMac::WriteUploadCloudClipboard() {
// Make the pasteboard content current host only.
[GetPasteboard()
prepareForNewContentsWithOptions:NSPasteboardContentsCurrentHostOnly];
}
void ClipboardMac::WriteConfidentialDataForPassword() {
DCHECK(CalledOnValidThread());
[GetPasteboard() setData:nil forType:kUTTypeConfidentialData];
}
// Write an extra flavor that signifies WebKit was the last to modify the
// pasteboard. This flavor has no data.
void ClipboardMac::WriteWebSmartPaste() {
NSString* format = ClipboardFormatType::WebKitSmartPasteType().ToNSString();
[GetPasteboard() setData:nil forType:format];
}
void ClipboardMac::ReadPngInternal(ClipboardBuffer buffer,
NSPasteboard* pasteboard,
ReadPngCallback callback) const {
DCHECK(CalledOnValidThread());
DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
std::vector<uint8_t> png = GetPngFromPasteboard(pasteboard);
if (!png.empty()) {
std::move(callback).Run(std::move(png));
return;
}
// If we can’t read a PNG, try reading for an NSImage, and if successful,
// transcode it to PNG.
NSImage* image = GetNSImage(pasteboard);
if (!image) {
std::move(callback).Run({});
return;
}
auto gfx_image = gfx::Image(image);
if (gfx_image.IsEmpty()) {
std::move(callback).Run({});
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
base::BindOnce(&EncodeGfxImageToPng, std::move(gfx_image)),
std::move(callback));
}
void ClipboardMac::WriteBitmapInternal(const SkBitmap& bitmap,
NSPasteboard* pasteboard) {
// The bitmap type is sanitized to be N32 before we get here. The conversion
// to an NSImage would not explode if we got this wrong, so this is not a
// security CHECK.
DCHECK_EQ(bitmap.colorType(), kN32_SkColorType);
NSBitmapImageRep* image_rep = skia::SkBitmapToNSBitmapImageRep(bitmap);
CHECK(image_rep) << "SkBitmapToNSBitmapImageRep failed";
// Attempt to format the image representation as a PNG, and write it directly
// to the clipboard if this succeeds. This will write both a PNG and a TIFF.
NSData* data = [image_rep representationUsingType:NSBitmapImageFileTypePNG
properties:@{}];
if (data) {
NSPasteboardItem* pasteboard_item = [[NSPasteboardItem alloc] init];
[pasteboard_item setData:data forType:NSPasteboardTypePNG];
if ([pasteboard writeObjects:@[ pasteboard_item ]]) {
return;
}
}
// Otherwise, fall back to writing the NSImage directly to the clipboard,
// which will write only a TIFF.
NSImage* image = [[NSImage alloc] init];
[image addRepresentation:image_rep];
[image setSize:NSMakeSize(bitmap.width(), bitmap.height())];
[pasteboard writeObjects:@[ image ]];
}
} // namespace ui