blob: 49eb1129cc9ec784311b8d5842a29689c6c8fc74 [file] [log] [blame] [edit]
/*
* Copyright (C) 2017-2025 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. AND ITS CONTRIBUTORS ``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 ITS 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"
#if PLATFORM(COCOA)
#import "PasteboardUtilities.h"
#import "PlatformUtilities.h"
#import "Test.h"
#import "TestWKWebView.h"
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <WebCore/ColorCocoa.h>
#import <WebKit/NSAttributedStringPrivate.h>
#import <WebKit/WKPreferencesPrivate.h>
#import <pal/spi/cocoa/NSAttributedStringSPI.h>
#import <wtf/RetainPtr.h>
#import <wtf/cocoa/TypeCastsCocoa.h>
#import <wtf/text/WTFString.h>
#if PLATFORM(IOS_FAMILY)
#import <MobileCoreServices/MobileCoreServices.h>
#endif
@interface WKWebView ()
- (void)copy:(id)sender;
- (void)paste:(id)sender;
@end
#if PLATFORM(MAC)
@interface WKWebView () <NSServicesMenuRequestor>
@end
NSData *readHTMLDataFromPasteboard()
{
return [[NSPasteboard generalPasteboard] dataForType:NSHTMLPboardType];
}
NSString *readHTMLStringFromPasteboard()
{
return [[NSPasteboard generalPasteboard] stringForType:NSHTMLPboardType];
}
#else
NSData *readHTMLDataFromPasteboard()
{
return [[UIPasteboard generalPasteboard] dataForPasteboardType:UTTypeHTML.identifier];
}
NSString *readHTMLStringFromPasteboard()
{
RetainPtr<id> value = [[UIPasteboard generalPasteboard] valueForPasteboardType:UTTypeHTML.identifier];
if ([value isKindOfClass:[NSData class]])
value = adoptNS([[NSString alloc] initWithData:(NSData *)value encoding:NSUTF8StringEncoding]);
ASSERT([value isKindOfClass:[NSString class]]);
return (NSString *)value.autorelease();
}
#endif
TEST(CopyHTML, Sanitizes)
{
auto webView = createWebViewWithCustomPasteboardDataEnabled();
[webView synchronouslyLoadTestPageNamed:@"copy-html"];
[webView stringByEvaluatingJavaScript:@"HTMLToCopy = '<meta content=\"secret\"><b onmouseover=\"dangerousCode()\">hello</b>"
"<!-- secret-->, world<script>dangerousCode()</script>';"];
[webView copy:nil];
[webView paste:nil];
EXPECT_TRUE([webView stringByEvaluatingJavaScript:@"didCopy"].boolValue);
EXPECT_TRUE([webView stringByEvaluatingJavaScript:@"didPaste"].boolValue);
EXPECT_WK_STREQ("<meta content=\"secret\"><b onmouseover=\"dangerousCode()\">hello</b><!-- secret-->, world<script>dangerousCode()</script>",
[webView stringByEvaluatingJavaScript:@"pastedHTML"]);
String htmlInNativePasteboard = readHTMLStringFromPasteboard();
EXPECT_TRUE(htmlInNativePasteboard.contains("hello"_s));
EXPECT_TRUE(htmlInNativePasteboard.contains(", world"_s));
EXPECT_FALSE(htmlInNativePasteboard.contains("secret"_s));
EXPECT_FALSE(htmlInNativePasteboard.contains("dangerousCode"_s));
}
TEST(CopyHTML, SanitizationPreservesCharacterSetInSelectedText)
{
auto webView = createWebViewWithCustomPasteboardDataEnabled();
[webView synchronouslyLoadHTMLString:@"<!DOCTYPE html>"
"<body>"
"<meta charset='utf-8'>"
"<span id='copy'>我叫謝文昇</span>"
"<script>getSelection().selectAllChildren(copy);</script>"
"</body>"];
[webView copy:nil];
[webView waitForNextPresentationUpdate];
NSString *copiedMarkup = readHTMLStringFromPasteboard();
EXPECT_TRUE([copiedMarkup containsString:@"<span "]);
EXPECT_TRUE([copiedMarkup containsString:@"我叫謝文昇"]);
EXPECT_TRUE([copiedMarkup containsString:@"</span>"]);
auto attributedString = adoptNS([[NSAttributedString alloc] initWithData:readHTMLDataFromPasteboard() options:@{ NSDocumentTypeDocumentOption: NSHTMLTextDocumentType } documentAttributes:nil error:nil]);
EXPECT_WK_STREQ("我叫謝文昇", [attributedString string]);
}
// FIXME: rdar://158345822
#if PLATFORM(IOS)
TEST(CopyHTML, DISABLED_SanitizationPreservesCharacterSet)
#else
TEST(CopyHTML, SanitizationPreservesCharacterSet)
#endif
{
Vector<std::pair<RetainPtr<NSString>, RetainPtr<NSData>>, 3> markupStringsAndData;
auto webView = createWebViewWithCustomPasteboardDataEnabled();
for (NSString *encodingName in @[ @"utf-8", @"windows-1252", @"bogus-encoding" ]) {
[webView synchronouslyLoadHTMLString:[NSString stringWithFormat:@"<!DOCTYPE html>"
"<body>"
"<meta charset='%@'>"
"<p id='copy'>Copy me</p>"
"<script>"
"copy.addEventListener('copy', e => {"
" e.clipboardData.setData('text/html', `<span style='color: red;'>我叫謝文昇</span>`);"
" e.preventDefault();"
"});"
"getSelection().selectAllChildren(copy);"
"</script>"
"</body>", encodingName]];
[webView copy:nil];
[webView waitForNextPresentationUpdate];
markupStringsAndData.append({ readHTMLStringFromPasteboard(), readHTMLDataFromPasteboard() });
}
for (auto& [copiedMarkup, copiedData] : markupStringsAndData) {
EXPECT_TRUE([copiedMarkup containsString:@"<span "]);
EXPECT_TRUE([copiedMarkup containsString:@"color: red;"]);
EXPECT_TRUE([copiedMarkup containsString:@"我叫謝文昇"]);
EXPECT_TRUE([copiedMarkup containsString:@"</span>"]);
NSError *attributedStringConversionError = nil;
auto attributedString = adoptNS([[NSAttributedString alloc] initWithData:copiedData.get() options:@{ NSDocumentTypeDocumentOption: NSHTMLTextDocumentType } documentAttributes:nil error:&attributedStringConversionError]);
EXPECT_WK_STREQ("我叫謝文昇", [attributedString string]);
__block BOOL foundColorAttribute = NO;
[attributedString enumerateAttribute:NSForegroundColorAttributeName inRange:NSMakeRange(0, 5) options:0 usingBlock:^(WebCore::CocoaColor *color, NSRange range, BOOL*) {
CGFloat redComponent = 0;
CGFloat greenComponent = 0;
CGFloat blueComponent = 0;
[color getRed:&redComponent green:&greenComponent blue:&blueComponent alpha:nil];
EXPECT_EQ(1., redComponent);
EXPECT_EQ(0., greenComponent);
EXPECT_EQ(0., blueComponent);
foundColorAttribute = YES;
}];
EXPECT_TRUE(foundColorAttribute);
}
}
static std::pair<RetainPtr<NSAttributedString>, RetainPtr<NSError>> copyAndLoadAttributedStringUsingWebArchive(TestWKWebView *webView)
{
[webView copy:nil];
[webView waitForNextPresentationUpdate];
#if PLATFORM(IOS_FAMILY)
RetainPtr archiveData = [UIPasteboard.generalPasteboard dataForPasteboardType:UTTypeWebArchive.identifier];
#else
RetainPtr archiveData = [NSPasteboard.generalPasteboard dataForType:UTTypeWebArchive.identifier];
#endif
__block bool done = false;
__block RetainPtr<NSAttributedString> resultString;
__block RetainPtr<NSError> resultError;
[NSAttributedString _loadFromHTMLWithOptions:@{ } contentLoader:^(WKWebView *loadingWebView) {
return [loadingWebView loadData:archiveData.get() MIMEType:UTTypeWebArchive.preferredMIMEType characterEncodingName:@"" baseURL:[NSURL URLWithString:@"about:blank"]];
} completionHandler:^(NSAttributedString *string, NSDictionary *, NSError *error) {
resultString = string;
resultError = error;
done = true;
}];
TestWebKitAPI::Util::run(&done);
return { WTF::move(resultString), WTF::move(resultError) };
}
TEST(CopyHTML, SanitizationPreservesRelativeURLInAttributedString)
{
auto webView = createWebViewWithCustomPasteboardDataEnabled();
[webView synchronouslyLoadHTMLString:@"<!DOCTYPE html>"
"<html>"
"<head><base href='https://webkit.org/' /></head>"
"<body><a href='/downloads'>Click</a> for downloads</body>"
"</html>"];
[webView stringByEvaluatingJavaScript:@"getSelection().selectAllChildren(document.body)"];
auto [resultString, resultError] = copyAndLoadAttributedStringUsingWebArchive(webView.get());
auto links = adoptNS([NSMutableArray<NSURL *> new]);
[resultString enumerateAttribute:NSLinkAttributeName inRange:NSMakeRange(0, 5) options:0 usingBlock:^(NSURL *url, NSRange, BOOL*) {
[links addObject:url];
}];
EXPECT_NULL(resultError);
EXPECT_WK_STREQ("Click for downloads", [resultString string]);
EXPECT_EQ(1U, [links count]);
EXPECT_WK_STREQ("https://webkit.org/downloads", [links firstObject].absoluteString);
}
TEST(CopyHTML, CopyingRightToLeftTextPreservesDirection)
{
auto webView = createWebViewWithCustomPasteboardDataEnabled();
[webView synchronouslyLoadTestPageNamed:@"rtl-bidi-text"];
[webView stringByEvaluatingJavaScript:@"getSelection().selectAllChildren(document.querySelector('p'))"];
RetainPtr expectedPlainText = [webView stringByEvaluatingJavaScript:@"getSelection().toString()"];
auto [resultString, resultError] = copyAndLoadAttributedStringUsingWebArchive(webView.get());
__block NSUInteger paragraphCount = 0;
RetainPtr plainText = [resultString string];
[resultString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, [plainText length]) options:0 usingBlock:^(NSParagraphStyle *style, NSRange, BOOL*) {
EXPECT_EQ(NSWritingDirectionRightToLeft, style.baseWritingDirection);
paragraphCount++;
}];
EXPECT_NULL(resultError);
EXPECT_EQ(1U, paragraphCount);
EXPECT_WK_STREQ(plainText.get(), expectedPlainText.get());
}
#if PLATFORM(MAC)
TEST(CopyHTML, ItemTypesWhenCopyingWebContent)
{
auto webView = createWebViewWithCustomPasteboardDataEnabled();
[webView synchronouslyLoadHTMLString:@"<strong style='color: rgb(255, 0, 0);'>This is some text to copy.</strong>"];
[webView stringByEvaluatingJavaScript:@"getSelection().selectAllChildren(document.body)"];
[webView copy:nil];
[webView waitForNextPresentationUpdate];
NSArray<NSPasteboardItem *> *items = NSPasteboard.generalPasteboard.pasteboardItems;
EXPECT_EQ(1U, items.count);
NSArray<NSPasteboardType> *types = items.firstObject.types;
EXPECT_TRUE([types containsObject:UTTypeWebArchive.identifier]);
EXPECT_TRUE([types containsObject:NSPasteboardTypeRTF]);
EXPECT_TRUE([types containsObject:NSPasteboardTypeString]);
EXPECT_TRUE([types containsObject:NSPasteboardTypeHTML]);
}
TEST(CopyHTML, WriteRichTextSelectionToPasteboard)
{
auto webView = createWebViewWithCustomPasteboardDataEnabled();
[webView synchronouslyLoadHTMLString:@"<strong style='color: rgb(255, 0, 0);'>This is some text to copy.</strong>"];
[webView stringByEvaluatingJavaScript:@"getSelection().selectAllChildren(document.body)"];
[webView waitForNextPresentationUpdate];
auto pasteboard = [NSPasteboard pasteboardWithUniqueName];
[webView writeSelectionToPasteboard:pasteboard types:@[ UTTypeWebArchive.identifier ]];
EXPECT_GT([pasteboard dataForType:UTTypeWebArchive.identifier].length, 0U);
}
#endif // PLATFORM(MAC)
TEST(CopyHTML, CopySelectedTextInTextDocument)
{
RetainPtr webView = createWebViewWithCustomPasteboardDataEnabled();
RetainPtr fileURL = [NSBundle.test_resourcesBundle URLForResource:@"test" withExtension:@"json"];
[webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:fileURL.get()]];
[webView stringByEvaluatingJavaScript:@"getSelection().selectAllChildren(document.body)"];
RetainPtr expectedString = [webView stringByEvaluatingJavaScript:@"getSelection().toString()"];
[webView copy:nil];
[webView waitForNextPresentationUpdate];
#if PLATFORM(MAC)
RetainPtr pastedObjects = [[NSPasteboard generalPasteboard] readObjectsForClasses:@[ NSAttributedString.class ] options:nil];
RetainPtr copiedText = dynamic_objc_cast<NSAttributedString>([pastedObjects firstObject]);
#elif !PLATFORM(WATCHOS) && !PLATFORM(APPLETV)
RetainPtr itemProvider = [[[UIPasteboard generalPasteboard] itemProviders] firstObject];
__block bool doneLoading = false;
__block RetainPtr<NSAttributedString> copiedText;
[itemProvider loadObjectOfClass:NSAttributedString.class completionHandler:^(NSAttributedString *result, NSError *) {
copiedText = result;
doneLoading = true;
}];
TestWebKitAPI::Util::run(&doneLoading);
EXPECT_WK_STREQ(expectedString.get(), [copiedText string]);
#endif
}
#endif // PLATFORM(COCOA)