/* * Copyright (C) 2006 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "PlatformPasteboard.h" #if PLATFORM(MAC) #import "Color.h" #import "ColorMac.h" #import "LegacyNSPasteboardTypes.h" #import "Pasteboard.h" #import "SharedBuffer.h" #import #import #import #import #import namespace WebCore { static bool canWritePasteboardType(const String& type) { auto cfString = type.createCFString(); ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (UTTypeIsDeclared(cfString.get()) || UTTypeIsDynamic(cfString.get())) return true; ALLOW_DEPRECATED_DECLARATIONS_END return [(__bridge NSString *)cfString.get() lengthOfBytesUsingEncoding:NSString.defaultCStringEncoding]; } void PlatformPasteboard::performAsDataOwner(DataOwnerType, Function&& actions) { actions(); } PlatformPasteboard::PlatformPasteboard(const String& pasteboardName) : m_pasteboard([NSPasteboard pasteboardWithName:pasteboardName]) { ASSERT(pasteboardName); } void PlatformPasteboard::getTypes(Vector& types) const { types = makeVector([m_pasteboard types]); } RefPtr PlatformPasteboard::bufferForType(const String& pasteboardType) const { NSData *data = [m_pasteboard dataForType:pasteboardType]; if (!data) return nullptr; return SharedBuffer::create(adoptNS([data copy]).get()); } int PlatformPasteboard::numberOfFiles() const { Vector files; NSArray *pasteboardTypes = [m_pasteboard types]; if ([pasteboardTypes containsObject:legacyFilesPromisePasteboardType()]) { // FIXME: legacyFilesPromisePasteboardType() contains file types, not path names, but in // this case we are only concerned with the count of them. The count of types should equal // the count of files, but this isn't guaranteed as some legacy providers might only write // unique file types. getPathnamesForType(files, String(legacyFilesPromisePasteboardType())); return files.size(); } if ([pasteboardTypes containsObject:legacyFilenamesPasteboardType()]) { getPathnamesForType(files, String(legacyFilenamesPasteboardType())); return files.size(); } return 0; } void PlatformPasteboard::getPathnamesForType(Vector& pathnames, const String& pasteboardType) const { id paths = [m_pasteboard propertyListForType:pasteboardType]; if ([paths isKindOfClass:[NSString class]]) { pathnames.append((NSString *)paths); return; } pathnames = makeVector(paths); } static bool pasteboardMayContainFilePaths(NSPasteboard *pasteboard) { for (NSString *type in pasteboard.types) { if ([type isEqualToString:(NSString *)legacyFilenamesPasteboardType()] || [type isEqualToString:(NSString *)legacyFilesPromisePasteboardType()] || Pasteboard::shouldTreatCocoaTypeAsFile(type)) return true; } return false; } String PlatformPasteboard::stringForType(const String& pasteboardType) const { if (pasteboardType == String { legacyURLPasteboardType() }) { String urlString = ([NSURL URLFromPasteboard:m_pasteboard.get()] ?: [NSURL URLWithString:[m_pasteboard stringForType:legacyURLPasteboardType()]]).absoluteString; if (pasteboardMayContainFilePaths(m_pasteboard.get()) && !Pasteboard::canExposeURLToDOMWhenPasteboardContainsFiles(urlString)) return { }; return urlString; } return [m_pasteboard stringForType:pasteboardType]; } static Vector urlStringsFromPasteboard(NSPasteboard *pasteboard) { NSArray *items = pasteboard.pasteboardItems; Vector urlStrings; urlStrings.reserveInitialCapacity(items.count); if (items.count > 1) { for (NSPasteboardItem *item in items) { ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (id propertyList = [item propertyListForType:(__bridge NSString *)kUTTypeURL]) { if (auto urlFromItem = adoptNS([[NSURL alloc] initWithPasteboardPropertyList:propertyList ofType:(__bridge NSString *)kUTTypeURL])) urlStrings.uncheckedAppend([urlFromItem absoluteString]); } ALLOW_DEPRECATED_DECLARATIONS_END } } else if (NSURL *urlFromPasteboard = [NSURL URLFromPasteboard:pasteboard]) urlStrings.uncheckedAppend(urlFromPasteboard.absoluteString); else if (NSString *urlStringFromPasteboard = [pasteboard stringForType:legacyURLPasteboardType()]) urlStrings.uncheckedAppend(urlStringFromPasteboard); bool mayContainFiles = pasteboardMayContainFilePaths(pasteboard); urlStrings.removeAllMatching([&] (auto& urlString) { return urlString.isEmpty() || (mayContainFiles && !Pasteboard::canExposeURLToDOMWhenPasteboardContainsFiles(urlString)); }); return urlStrings; } static String typeIdentifierForPasteboardType(const String& pasteboardType) { ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (UTTypeIsDeclared(pasteboardType.createCFString().get())) return pasteboardType; if (pasteboardType == String(legacyStringPasteboardType())) return kUTTypeUTF8PlainText; if (pasteboardType == String(legacyHTMLPasteboardType())) return kUTTypeHTML; if (pasteboardType == String(legacyURLPasteboardType())) return kUTTypeURL; ALLOW_DEPRECATED_DECLARATIONS_END return { }; } Vector PlatformPasteboard::allStringsForType(const String& pasteboardType) const { auto typeIdentifier = typeIdentifierForPasteboardType(pasteboardType); ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (typeIdentifier == String(kUTTypeURL)) return urlStringsFromPasteboard(m_pasteboard.get()); ALLOW_DEPRECATED_DECLARATIONS_END NSArray *items = [m_pasteboard pasteboardItems]; Vector strings; strings.reserveInitialCapacity(items.count); if (items.count > 1 && !typeIdentifier.isNull()) { for (NSPasteboardItem *item in items) { if (NSString *stringFromItem = [item stringForType:typeIdentifier]) strings.append(stringFromItem); } } else if (NSString *stringFromPasteboard = [m_pasteboard stringForType:pasteboardType]) strings.append(stringFromPasteboard); return strings; } static const char* safeTypeForDOMToReadAndWriteForPlatformType(const String& platformType) { if (platformType == String(legacyStringPasteboardType()) || platformType == String(NSPasteboardTypeString)) return "text/plain"_s; if (platformType == String(legacyURLPasteboardType())) return "text/uri-list"_s; ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (platformType == String(legacyHTMLPasteboardType()) || platformType == String(WebArchivePboardType) || platformType == String(kUTTypeWebArchive) || platformType == String(legacyRTFDPasteboardType()) || platformType == String(legacyRTFPasteboardType())) return "text/html"_s; ALLOW_DEPRECATED_DECLARATIONS_END return nullptr; } Vector PlatformPasteboard::typesSafeForDOMToReadAndWrite(const String& origin) const { ListHashSet domPasteboardTypes; if (NSData *serializedCustomData = [m_pasteboard dataForType:@(PasteboardCustomData::cocoaType().characters())]) { auto data = PasteboardCustomData::fromSharedBuffer(SharedBuffer::create(serializedCustomData).get()); if (data.origin() == origin) { for (auto& type : data.orderedTypes()) domPasteboardTypes.add(type); } } NSArray *allTypes = [m_pasteboard types]; for (NSString *type in allTypes) { if ([type isEqualToString:@(PasteboardCustomData::cocoaType().characters())]) continue; if (Pasteboard::isSafeTypeForDOMToReadAndWrite(type)) domPasteboardTypes.add(type); else if (auto* domType = safeTypeForDOMToReadAndWriteForPlatformType(type)) { auto domTypeAsString = String::fromUTF8(domType); if (domTypeAsString == "text/uri-list"_s && stringForType(legacyURLPasteboardType()).isEmpty()) continue; domPasteboardTypes.add(WTFMove(domTypeAsString)); } } return copyToVector(domPasteboardTypes); } int64_t PlatformPasteboard::write(const PasteboardCustomData& data) { NSMutableArray *types = [NSMutableArray array]; data.forEachType([&] (auto& type) { NSString *platformType = platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type, IncludeImageTypes::Yes); if (platformType.length) [types addObject:platformType]; }); bool shouldWriteCustomData = data.hasSameOriginCustomData() || !data.origin().isEmpty(); if (shouldWriteCustomData) [types addObject:@(PasteboardCustomData::cocoaType().characters())]; [m_pasteboard declareTypes:types owner:nil]; data.forEachPlatformStringOrBuffer([&] (auto& type, auto& stringOrBuffer) { auto platformType = platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type, IncludeImageTypes::Yes); if (platformType.isEmpty()) return; if (std::holds_alternative>(stringOrBuffer)) { if (auto platformData = std::get>(stringOrBuffer)->createNSData()) [m_pasteboard setData:platformData.get() forType:platformType]; } else if (std::holds_alternative(stringOrBuffer)) { auto string = std::get(stringOrBuffer); if (!!string) [m_pasteboard setString:string forType:platformType]; } }); if (shouldWriteCustomData) { if (auto serializedCustomData = data.createSharedBuffer()->createNSData()) [m_pasteboard setData:serializedCustomData.get() forType:@(PasteboardCustomData::cocoaType().characters())]; } return changeCount(); } int64_t PlatformPasteboard::changeCount() const { return [m_pasteboard changeCount]; } String PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(const String& domType, IncludeImageTypes includeImageTypes) { if (domType == "text/plain"_s) return legacyStringPasteboardType(); if (domType == "text/html"_s) return legacyHTMLPasteboardType(); if (domType == "text/uri-list"_s) return legacyURLPasteboardType(); if (includeImageTypes == IncludeImageTypes::Yes && domType == "image/png"_s) return legacyPNGPasteboardType(); return { }; } Color PlatformPasteboard::color() { return colorFromCocoaColor([NSColor colorFromPasteboard:m_pasteboard.get()]); } URL PlatformPasteboard::url() { return [NSURL URLFromPasteboard:m_pasteboard.get()]; } int64_t PlatformPasteboard::copy(const String& fromPasteboard) { NSPasteboard* pasteboard = [NSPasteboard pasteboardWithName:fromPasteboard]; NSArray* types = [pasteboard types]; [m_pasteboard addTypes:types owner:nil]; for (NSUInteger i = 0; i < [types count]; i++) { NSString* type = [types objectAtIndex:i]; if (![m_pasteboard setData:[pasteboard dataForType:type] forType:type]) return 0; } return changeCount(); } int64_t PlatformPasteboard::addTypes(const Vector& pasteboardTypes) { return [m_pasteboard addTypes:createNSArray(pasteboardTypes).get() owner:nil]; } int64_t PlatformPasteboard::setTypes(const Vector& pasteboardTypes) { for (auto& pasteboardType : pasteboardTypes) { if (!canWritePasteboardType(pasteboardType)) return [m_pasteboard declareTypes:@[] owner:nil]; } return [m_pasteboard declareTypes:createNSArray(pasteboardTypes).get() owner:nil]; } int64_t PlatformPasteboard::setBufferForType(SharedBuffer* buffer, const String& pasteboardType) { if (!canWritePasteboardType(pasteboardType)) return 0; BOOL didWriteData = [m_pasteboard setData:buffer ? buffer->createNSData().get() : nil forType:pasteboardType]; if (!didWriteData) return 0; return changeCount(); } int64_t PlatformPasteboard::setURL(const PasteboardURL& pasteboardURL) { auto urlString = [(NSURL *)pasteboardURL.url absoluteString]; if (!urlString) return 0; NSArray *urlWithTitle = @[ @[ urlString ], @[ pasteboardURL.title ] ]; NSString *pasteboardType = [NSString stringWithUTF8String:WebURLsWithTitlesPboardType]; BOOL didWriteData = [m_pasteboard setPropertyList:urlWithTitle forType:pasteboardType]; if (!didWriteData) return 0; return changeCount(); } int64_t PlatformPasteboard::setColor(const Color& color) { [cocoaColor(color) writeToPasteboard:m_pasteboard.get()]; return changeCount(); } int64_t PlatformPasteboard::setStringForType(const String& string, const String& pasteboardType) { if (!canWritePasteboardType(pasteboardType)) return 0; BOOL didWriteData; if (pasteboardType == String(legacyURLPasteboardType())) { // We cannot just use -NSPasteboard writeObjects:], because -declareTypes has been already called, implicitly creating an item. NSURL *url = [NSURL URLWithString:string]; if ([[m_pasteboard types] containsObject:legacyURLPasteboardType()]) { NSURL *base = [url baseURL]; if (base) didWriteData = [m_pasteboard setPropertyList:@[[url relativeString], [base absoluteString]] forType:legacyURLPasteboardType()]; else if (url) didWriteData = [m_pasteboard setPropertyList:@[[url absoluteString], @""] forType:legacyURLPasteboardType()]; else didWriteData = [m_pasteboard setPropertyList:@[@"", @""] forType:legacyURLPasteboardType()]; if (!didWriteData) return 0; } ALLOW_DEPRECATED_DECLARATIONS_BEGIN if ([[m_pasteboard types] containsObject:(NSString *)kUTTypeURL]) { didWriteData = [m_pasteboard setString:[url absoluteString] forType:(NSString *)kUTTypeURL]; if (!didWriteData) return 0; } if ([[m_pasteboard types] containsObject:(NSString *)kUTTypeFileURL] && [url isFileURL]) { didWriteData = [m_pasteboard setString:[url absoluteString] forType:(NSString *)kUTTypeFileURL]; if (!didWriteData) return 0; } ALLOW_DEPRECATED_DECLARATIONS_END } else { didWriteData = [m_pasteboard setString:string forType:pasteboardType]; if (!didWriteData) return 0; } return changeCount(); } static NSPasteboardType modernPasteboardTypeForWebSafeMIMEType(const String& webSafeType) { if (webSafeType == "text/plain"_s) return NSPasteboardTypeString; if (webSafeType == "text/html"_s) return NSPasteboardTypeHTML; if (webSafeType == "text/uri-list"_s) return NSPasteboardTypeURL; if (webSafeType == "image/png"_s) return NSPasteboardTypePNG; return nil; } enum class ContainsFileURL { No, Yes }; static String webSafeMIMETypeForModernPasteboardType(NSPasteboardType platformType, ContainsFileURL containsFileURL) { if ([platformType isEqual:NSPasteboardTypeString] && containsFileURL == ContainsFileURL::No) return "text/plain"_s; if ([platformType isEqual:NSPasteboardTypeHTML] || [platformType isEqual:NSPasteboardTypeRTF] || [platformType isEqual:NSPasteboardTypeRTFD]) return "text/html"_s; if ([platformType isEqual:NSPasteboardTypeURL] && containsFileURL == ContainsFileURL::No) return "text/uri-list"_s; if ([platformType isEqual:NSPasteboardTypePNG] || [platformType isEqual:NSPasteboardTypeTIFF]) return "image/png"_s; return { }; } RefPtr PlatformPasteboard::readBuffer(std::optional index, const String& type) const { if (!index) return bufferForType(type); NSPasteboardItem *item = itemAtIndex(*index); if (!item) return { }; if (NSData *data = [item dataForType:type]) { auto nsData = adoptNS(data.copy); return SharedBuffer::create(nsData.get()); } return nullptr; } String PlatformPasteboard::readString(size_t index, const String& type) const { NSPasteboardItem *item = itemAtIndex(index); if (!item) return { }; return [item stringForType:type]; } URL PlatformPasteboard::readURL(size_t index, String& title) const { title = emptyString(); NSPasteboardItem *item = itemAtIndex(index); if (!item) return { }; RetainPtr url; if (id propertyList = [item propertyListForType:NSPasteboardTypeURL]) url = adoptNS([[NSURL alloc] initWithPasteboardPropertyList:propertyList ofType:NSPasteboardTypeURL]); else if (NSString *absoluteString = [item stringForType:NSPasteboardTypeURL]) url = [NSURL URLWithString:absoluteString]; return { [url isFileURL] ? nil : url.get() }; } int PlatformPasteboard::count() const { return [m_pasteboard pasteboardItems].count; } static RetainPtr createPasteboardItem(const PasteboardCustomData& data) { auto item = adoptNS([[NSPasteboardItem alloc] init]); if (data.hasSameOriginCustomData() || !data.origin().isEmpty()) { if (auto serializedCustomData = data.createSharedBuffer()->createNSData()) [item setData:serializedCustomData.get() forType:@(PasteboardCustomData::cocoaType().characters())]; } data.forEachPlatformStringOrBuffer([&] (auto& type, auto& stringOrBuffer) { auto platformType = modernPasteboardTypeForWebSafeMIMEType(type); if (!platformType) return; if (std::holds_alternative>(stringOrBuffer)) { if (auto platformData = std::get>(stringOrBuffer)->createNSData()) [item setData:platformData.get() forType:platformType]; } else if (std::holds_alternative(stringOrBuffer)) { auto string = std::get(stringOrBuffer); if (!!string) [item setString:string forType:platformType]; } }); return item; } int64_t PlatformPasteboard::write(const Vector& itemData) { if (itemData.size() == 1) return write(itemData.first()); [m_pasteboard clearContents]; [m_pasteboard writeObjects:createNSArray(itemData, [] (auto& data) { return createPasteboardItem(data); }).get()]; return [m_pasteboard changeCount]; } std::optional PlatformPasteboard::informationForItemAtIndex(size_t index, int64_t changeCount) { if (changeCount != [m_pasteboard changeCount]) return std::nullopt; NSPasteboardItem *item = itemAtIndex(index); if (!item) return std::nullopt; PasteboardItemInfo info; NSArray *platformTypes = [item types]; auto containsFileURL = [platformTypes containsObject:NSPasteboardTypeFileURL] ? ContainsFileURL::Yes : ContainsFileURL::No; ListHashSet webSafeTypes; info.platformTypesByFidelity.reserveInitialCapacity(platformTypes.count); for (NSPasteboardType type in platformTypes) { info.platformTypesByFidelity.uncheckedAppend(type); auto webSafeType = webSafeMIMETypeForModernPasteboardType(type, containsFileURL); if (webSafeType.isEmpty()) continue; webSafeTypes.add(WTFMove(webSafeType)); } info.containsFileURLAndFileUploadContent = containsFileURL == ContainsFileURL::Yes; info.webSafeTypesByFidelity = copyToVector(webSafeTypes); return info; } NSPasteboardItem *PlatformPasteboard::itemAtIndex(size_t index) const { NSArray *items = [m_pasteboard pasteboardItems]; return index >= items.count ? nil : items[index]; } bool PlatformPasteboard::containsURLStringSuitableForLoading() { String unusedTitle; return !urlStringSuitableForLoading(unusedTitle).isEmpty(); } } #endif // PLATFORM(MAC)