| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // 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 "ui/base/clipboard/clipboard.h" |
| |
| #include <limits> |
| |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/mac/mac_util.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #include "base/mac/scoped_nsobject.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/no_destructor.h" |
| #include "base/notreached.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 "net/base/filename_util.h" |
| #include "skia/ext/skia_utils_base.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #import "third_party/mozilla/NSPasteboard+Utils.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_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; |
| } |
| |
| base::scoped_nsobject<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. |
| base::scoped_nsobject<NSImage> image; |
| @try { |
| if (pasteboard) |
| image.reset([[NSImage alloc] initWithPasteboard:pasteboard]); |
| } @catch (id exception) { |
| } |
| if (!image) |
| return base::scoped_nsobject<NSImage>(); |
| if ([[image representations] count] == 0u) |
| return base::scoped_nsobject<NSImage>(); |
| return image; |
| } |
| |
| } // namespace |
| |
| // Clipboard factory method. |
| // static |
| Clipboard* Clipboard::Create() { |
| return new ClipboardMac; |
| } |
| |
| // ClipboardMac implementation. |
| ClipboardMac::ClipboardMac() { |
| DCHECK(CalledOnValidThread()); |
| } |
| |
| ClipboardMac::~ClipboardMac() { |
| DCHECK(CalledOnValidThread()); |
| } |
| |
| void ClipboardMac::OnPreShutdown() {} |
| |
| // DataTransferEndpoint is not used on this platform. |
| DataTransferEndpoint* ClipboardMac::GetSource(ClipboardBuffer buffer) const { |
| DCHECK(CalledOnValidThread()); |
| DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste); |
| return nullptr; |
| } |
| |
| 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 |
| base::scoped_nsobject<NSArray> types([[GetPasteboard() types] retain]); |
| |
| // 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:NSHTMLPboardType] || |
| [types containsObject:NSRTFPboardType]; |
| } |
| // 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:NSTIFFPboardType]; |
| } |
| return [types containsObject:format.ToNSString()]; |
| } |
| |
| bool ClipboardMac::IsMarkedByOriginatorAsConfidential() const { |
| DCHECK(CalledOnValidThread()); |
| |
| NSPasteboardType type = |
| [GetPasteboard() availableTypeFromArray:@[ kUTTypeConfidentialData ]]; |
| |
| if (type) |
| return true; |
| |
| return false; |
| } |
| |
| void ClipboardMac::MarkAsConfidential() { |
| DCHECK(CalledOnValidThread()); |
| |
| [GetPasteboard() setData:nil forType:kUTTypeConfidentialData]; |
| } |
| |
| void ClipboardMac::Clear(ClipboardBuffer buffer) { |
| DCHECK(CalledOnValidThread()); |
| DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste); |
| |
| [GetPasteboard() declareTypes:@[] owner:nil]; |
| } |
| |
| // |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(); |
| 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)); |
| } |
| |
| if ([[pb types] containsObject:kWebCustomDataPboardType]) { |
| NSData* data = [pb dataForType:kWebCustomDataPboardType]; |
| if ([data length]) |
| ReadCustomDataTypes([data bytes], [data length], types); |
| } |
| } |
| |
| // |data_dst| is not used. It's only passed to be consistent with other |
| // platforms. |
| std::vector<std::u16string> |
| ClipboardMac::ReadAvailablePlatformSpecificFormatNames( |
| ClipboardBuffer buffer, |
| const DataTransferEndpoint* data_dst) const { |
| DCHECK(CalledOnValidThread()); |
| DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste); |
| |
| NSArray* types = [GetPasteboard() types]; |
| |
| std::vector<std::u16string> type_names; |
| type_names.reserve([types count]); |
| for (NSString* type in types) |
| type_names.push_back(base::SysNSStringToUTF16(type)); |
| return type_names; |
| } |
| |
| // |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([contents UTF8String]); |
| } |
| |
| // |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 = |
| @[ NSHTMLPboardType, NSRTFPboardType, NSPasteboardTypeString ]; |
| NSString* bestType = [pb availableTypeFromArray:supportedTypes]; |
| if (bestType) { |
| NSString* contents; |
| if ([bestType isEqualToString:NSRTFPboardType]) |
| contents = ClipboardUtil::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:kImageSvg]; |
| |
| *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); |
| std::move(callback).Run(ReadPngInternal(buffer, GetPasteboard())); |
| } |
| |
| // |data_dst| is not used. It's only passed to be consistent with other |
| // platforms. |
| void ClipboardMac::ReadImage(ClipboardBuffer buffer, |
| const DataTransferEndpoint* data_dst, |
| ReadImageCallback callback) const { |
| RecordRead(ClipboardFormatMetric::kImage); |
| std::move(callback).Run(ReadImageInternal(buffer, GetPasteboard())); |
| } |
| |
| // |data_dst| is not used. It's only passed to be consistent with other |
| // platforms. |
| void ClipboardMac::ReadCustomData(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:kWebCustomDataPboardType]) { |
| NSData* data = [pb dataForType:kWebCustomDataPboardType]; |
| if ([data length]) |
| ReadCustomDataForType([data bytes], [data length], type, 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); |
| |
| NSArray* paths = [GetPasteboard() propertyListForType:NSFilenamesPboardType]; |
| for (NSString* path in paths) { |
| result->push_back( |
| ui::FileInfo(base::mac::NSStringToFilePath(path), base::FilePath())); |
| } |
| } |
| |
| // |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 = ClipboardUtil::GetTitleFromPasteboardURL(pb); |
| *title = base::SysNSStringToUTF16(contents); |
| } |
| |
| if (url) { |
| NSString* url_string = ClipboardUtil::GetURLFromPasteboardURL(pb); |
| if (!url_string) |
| url->clear(); |
| else |
| url->assign([url_string UTF8String]); |
| } |
| } |
| |
| // |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->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 ClipboardMac::WritePortableAndPlatformRepresentations( |
| ClipboardBuffer buffer, |
| const ObjectMap& objects, |
| std::vector<Clipboard::PlatformRepresentation> platform_representations, |
| std::unique_ptr<DataTransferEndpoint> data_src) { |
| DCHECK(CalledOnValidThread()); |
| DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste); |
| |
| [GetPasteboard() declareTypes:@[] owner:nil]; |
| |
| DispatchPlatformRepresentations(std::move(platform_representations)); |
| for (const auto& object : objects) |
| DispatchPortableRepresentation(object.first, object.second); |
| } |
| |
| void ClipboardMac::WriteText(const char* text_data, size_t text_len) { |
| std::string text_str(text_data, text_len); |
| NSString* text = base::SysUTF8ToNSString(text_str); |
| [GetPasteboard() setString:text forType:NSPasteboardTypeString]; |
| } |
| |
| void ClipboardMac::WriteHTML(const char* markup_data, |
| size_t markup_len, |
| const char* url_data, |
| size_t url_len) { |
| // We need to mark it as utf-8. (see crbug.com/11957) |
| std::string html_fragment_str("<meta charset='utf-8'>"); |
| html_fragment_str.append(markup_data, markup_len); |
| NSString* html_fragment = base::SysUTF8ToNSString(html_fragment_str); |
| |
| // TODO(avi): url_data? |
| [GetPasteboard() setString:html_fragment forType:NSHTMLPboardType]; |
| } |
| |
| void ClipboardMac::WriteSvg(const char* markup_data, size_t markup_len) { |
| std::string svg_str(markup_data, markup_len); |
| NSString* svg = base::SysUTF8ToNSString(svg_str); |
| [GetPasteboard() setString:svg forType:kImageSvg]; |
| } |
| |
| void ClipboardMac::WriteRTF(const char* rtf_data, size_t data_len) { |
| WriteData(ClipboardFormatType::RtfType(), rtf_data, data_len); |
| } |
| |
| void ClipboardMac::WriteFilenames(std::vector<ui::FileInfo> filenames) { |
| NSMutableArray* paths = [NSMutableArray arrayWithCapacity:filenames.size()]; |
| for (const ui::FileInfo& file : filenames) { |
| NSString* path = base::mac::FilePathToNSString(file.path); |
| [paths addObject:path]; |
| } |
| [GetPasteboard() setPropertyList:paths forType:NSFilenamesPboardType]; |
| } |
| |
| void ClipboardMac::WriteBookmark(const char* title_data, |
| size_t title_len, |
| const char* url_data, |
| size_t url_len) { |
| std::string title_str(title_data, title_len); |
| NSString* title = base::SysUTF8ToNSString(title_str); |
| std::string url_str(url_data, url_len); |
| NSString* url = base::SysUTF8ToNSString(url_str); |
| |
| base::scoped_nsobject<NSPasteboardItem> item( |
| ClipboardUtil::PasteboardItemFromUrl(url, title)); |
| ClipboardUtil::AddDataToPasteboard(GetPasteboard(), item); |
| } |
| |
| 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); |
| |
| NSImage* image = skia::SkBitmapToNSImageWithColorSpace( |
| bitmap, base::mac::GetSystemColorSpace()); |
| if (!image) { |
| NOTREACHED() << "SkBitmapToNSImageWithColorSpace failed"; |
| return; |
| } |
| [GetPasteboard() writeObjects:@[ image ]]; |
| } |
| |
| void ClipboardMac::WriteData(const ClipboardFormatType& format, |
| const char* data_data, |
| size_t data_len) { |
| [GetPasteboard() setData:[NSData dataWithBytes:data_data length:data_len] |
| forType:format.ToNSString()]; |
| } |
| |
| // 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]; |
| } |
| |
| std::vector<uint8_t> ClipboardMac::ReadPngInternal( |
| ClipboardBuffer buffer, |
| NSPasteboard* pasteboard) const { |
| DCHECK(CalledOnValidThread()); |
| DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste); |
| |
| base::scoped_nsobject<NSImage> image = GetNSImage(pasteboard); |
| if (!image) |
| return std::vector<uint8_t>(); |
| |
| scoped_refptr<base::RefCountedMemory> mem = gfx::Image(image).As1xPNGBytes(); |
| std::vector<uint8_t> image_data(mem->data(), mem->data() + mem->size()); |
| return image_data; |
| } |
| |
| SkBitmap ClipboardMac::ReadImageInternal(ClipboardBuffer buffer, |
| NSPasteboard* pasteboard) const { |
| DCHECK(CalledOnValidThread()); |
| DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste); |
| |
| base::scoped_nsobject<NSImage> image = GetNSImage(pasteboard); |
| if (!image) |
| return SkBitmap(); |
| |
| // This logic prevents loss of pixels from retina images, where size != pixel |
| // size. In an ideal world, the concept of "retina-ness" would be plumbed all |
| // the way through to the web, but the clipboard API doesn't support the |
| // additional metainformation. |
| if ([[image representations] count] == 1u) { |
| NSImageRep* rep = [image representations][0]; |
| NSInteger width = [rep pixelsWide]; |
| NSInteger height = [rep pixelsHigh]; |
| if (width != 0 && height != 0) { |
| return skia::NSImageRepToSkBitmapWithColorSpace( |
| rep, NSMakeSize(width, height), /*is_opaque=*/false, |
| base::mac::GetSystemColorSpace()); |
| } |
| } |
| return skia::NSImageToSkBitmapWithColorSpace( |
| image.get(), /*is_opaque=*/false, base::mac::GetSystemColorSpace()); |
| } |
| |
| } // namespace ui |