Skip to content

Commit

Permalink
REGRESSION(257749@main): Copying table in Confluence on Safari doesn'…
Browse files Browse the repository at this point in the history
…t copy header rows

https://bugs.webkit.org/show_bug.cgi?id=259091

Reviewed by Wenson Hsieh.

Add a Confluence specific quirk to copy `-webkit-user-select: none` contents.

* LayoutTests/editing/pasteboard/copy-content-with-user-select-none-quirks-expected.txt: Added.
* LayoutTests/editing/pasteboard/copy-content-with-user-select-none-quirks.html: Added.
* Source/WebCore/editing/Editor.cpp:
(WebCore::Editor::selectedText const):
(WebCore::Editor::selectedTextForDataTransfer const):
* Source/WebCore/editing/cocoa/HTMLConverter.mm:
(HTMLConverter::HTMLConverter):
(HTMLConverter::_enterElement):
(HTMLConverter::_processText):
* Source/WebCore/editing/markup.cpp:
(WebCore::StyledMarkupAccumulator::StyledMarkupAccumulator):
* Source/WebCore/html/HTMLMetaElement.cpp:
(WebCore::HTMLMetaElement::process):
* Source/WebCore/page/DOMSelection.cpp:
(WebCore::DOMSelection::toString const):
* Source/WebCore/page/Quirks.h:
(WebCore::Quirks::needsToCopyUserSelectNoneQuirk const):
(WebCore::Quirks::setNeedsToCopyUserSelectNoneQuirk):
* Tools/TestWebKitAPI/Tests/WebKitCocoa/CopyRTF.mm:
(TEST):

Canonical link: https://commits.webkit.org/265939@main
  • Loading branch information
rniwa committed Jul 11, 2023
1 parent af3c301 commit 6f13250
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
This tests copying excludes content with user-select: none.
To manually test, copy "hello world foo bar" below then paste.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS getSelection().toString().includes("hello") is true
PASS getSelection().toString().includes("world") is true
PASS getSelection().toString().includes("WebKit") is true
PASS getSelection().toString().includes("rocks") is true
PASS getSelection().toString().includes("because") is true
PASS getSelection().toString().includes("foo") is true
PASS getSelection().toString().includes("bar") is true
PASS event.clipboardData.getData("text/plain").includes("hello") is true
PASS event.clipboardData.getData("text/plain").includes("world") is true
PASS event.clipboardData.getData("text/plain").includes("WebKit") is true
PASS event.clipboardData.getData("text/plain").includes("rocks") is true
PASS event.clipboardData.getData("text/plain").includes("because") is true
PASS event.clipboardData.getData("text/plain").includes("foo") is true
PASS event.clipboardData.getData("text/plain").includes("bar") is true
PASS event.clipboardData.getData("text/html").includes("hello") is true
PASS event.clipboardData.getData("text/html").includes("world") is true
PASS event.clipboardData.getData("text/html").includes("<i") is true
PASS event.clipboardData.getData("text/html").includes("</i>") is true
PASS event.clipboardData.getData("text/html").includes("WebKit") is true
PASS event.clipboardData.getData("text/html").includes("<b ") is true
PASS event.clipboardData.getData("text/html").includes("</b>") is true
PASS event.clipboardData.getData("text/html").includes("rocks") is true
PASS event.clipboardData.getData("text/html").includes("<q>") is true
PASS event.clipboardData.getData("text/html").includes("</q>") is true
PASS event.clipboardData.getData("text/html").includes("because") is true
PASS event.clipboardData.getData("text/html").includes("<s>") is true
PASS event.clipboardData.getData("text/html").includes("</s>") is true
PASS event.clipboardData.getData("text/html").includes("<em>") is true
PASS event.clipboardData.getData("text/html").includes("</em>") is true
PASS event.clipboardData.getData("text/html").includes("foo") is true
PASS event.clipboardData.getData("text/html").includes("bar") is true
PASS successfullyParsed is true

TEST COMPLETE
hello world WebKit rocks because foo bar
hello world WebKit rocks because foo bar

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html>
<head>
<meta name="confluence-request-time" content="1689029750234">
</head>
<body>
<div id="source">hello <span style="-webkit-user-select: none; user-select: none;"><i>world </i><b style="-webkit-user-select: initial;
user-select: initial;">WebKit <span style="-webkit-user-select: none; user-select: none;"><q>rocks </q></span></b><font color="green" style="-webkit-user-select: none;
user-select: none;"><s>because </s></font></span><span inert><em>foo </em></span></span>bar</div>
<div id="destination" contenteditable></div>
<pre id="output"></pre>
</body>
<script src="../../resources/js-test.js"></script>
<script>

description(`This tests copying excludes content with user-select: none.<br>
To manually test, copy "hello world foo bar" below then paste.`);

jsTestIsAsync = true;
getSelection().setBaseAndExtent(source, 0, source, source.childNodes.length);

source.addEventListener("copy", () => {
shouldBeTrue('getSelection().toString().includes("hello")');
shouldBeTrue('getSelection().toString().includes("world")');
shouldBeTrue('getSelection().toString().includes("WebKit")');
shouldBeTrue('getSelection().toString().includes("rocks")');
shouldBeTrue('getSelection().toString().includes("because")');
shouldBeTrue('getSelection().toString().includes("foo")');
shouldBeTrue('getSelection().toString().includes("bar")');
});

destination.addEventListener("paste", () => {
shouldBeTrue('event.clipboardData.getData("text/plain").includes("hello")');
shouldBeTrue('event.clipboardData.getData("text/plain").includes("world")');
shouldBeTrue('event.clipboardData.getData("text/plain").includes("WebKit")');
shouldBeTrue('event.clipboardData.getData("text/plain").includes("rocks")');
shouldBeTrue('event.clipboardData.getData("text/plain").includes("because")');
shouldBeTrue('event.clipboardData.getData("text/plain").includes("foo")');
shouldBeTrue('event.clipboardData.getData("text/plain").includes("bar")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("hello")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("world")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("<i")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("</i>")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("WebKit")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("<b ")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("</b>")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("rocks")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("<q>")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("</q>")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("because")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("<s>")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("</s>")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("<em>")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("</em>")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("foo")');
shouldBeTrue('event.clipboardData.getData("text/html").includes("bar")');
finishJSTest();
});

if (window.testRunner) {
testRunner.execCommand("Copy");
destination.focus();
testRunner.execCommand("Paste");
} else {
source.addEventListener("copy", () => {
setTimeout(() => destination.focus(), 0);
});
}

</script>
</html>
11 changes: 9 additions & 2 deletions Source/WebCore/editing/Editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
#include "Page.h"
#include "PagePasteboardContext.h"
#include "Pasteboard.h"
#include "Quirks.h"
#include "Range.h"
#include "RemoveFormatCommand.h"
#include "RenderBlock.h"
Expand Down Expand Up @@ -3473,12 +3474,18 @@ void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, O

String Editor::selectedText() const
{
return selectedText({ TextIteratorBehavior::TraversesFlatTree, TextIteratorBehavior::IgnoresUserSelectNone });
auto options = OptionSet { TextIteratorBehavior::TraversesFlatTree };
if (!m_document.quirks().needsToCopyUserSelectNoneQuirk())
options.add(TextIteratorBehavior::IgnoresUserSelectNone);
return selectedText(options);
}

String Editor::selectedTextForDataTransfer() const
{
return selectedText(OptionSet { TextIteratorBehavior::EmitsImageAltText, TextIteratorBehavior::TraversesFlatTree, TextIteratorBehavior::IgnoresUserSelectNone });
auto options = OptionSet { TextIteratorBehavior::EmitsImageAltText, TextIteratorBehavior::TraversesFlatTree };
if (!m_document.quirks().needsToCopyUserSelectNoneQuirk())
options.add(TextIteratorBehavior::IgnoresUserSelectNone);
return selectedText(options);
}

String Editor::selectedText(TextIteratorBehaviors behaviors) const
Expand Down
7 changes: 5 additions & 2 deletions Source/WebCore/editing/cocoa/HTMLConverter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
#import "HTMLTextAreaElement.h"
#import "LoaderNSURLExtras.h"
#import "LocalFrame.h"
#import "Quirks.h"
#import "RenderImage.h"
#import "RenderText.h"
#import "StyleProperties.h"
Expand Down Expand Up @@ -256,6 +257,7 @@ + (void)document:(NSObject **)outDocument attachment:(NSTextAttachment **)outAtt
HashMap<RefPtr<Element>, RetainPtr<NSDictionary>> m_aggregatedAttributesForElements;

UserSelectNoneStateCache m_userSelectNoneStateCache;
bool m_ignoreUserSelectNoneContent { false };

RetainPtr<NSMutableAttributedString> _attrStr;
RetainPtr<NSMutableDictionary> _documentAttrs;
Expand Down Expand Up @@ -328,6 +330,7 @@ + (void)document:(NSObject **)outDocument attachment:(NSTextAttachment **)outAtt
: m_start(makeContainerOffsetPosition(range.start))
, m_end(makeContainerOffsetPosition(range.end))
, m_userSelectNoneStateCache(ComposedTree)
, m_ignoreUserSelectNoneContent(!range.start.document().quirks().needsToCopyUserSelectNoneQuirk())
{
_attrStr = adoptNS([[NSMutableAttributedString alloc] init]);
_documentAttrs = adoptNS([[NSMutableDictionary alloc] init]);
Expand Down Expand Up @@ -1598,7 +1601,7 @@ static NSInteger _colCompare(id block1, id block2, void *)

if (element.hasTagName(headTag) && !embedded)
_processHeadElement(element);
else if (!m_userSelectNoneStateCache.nodeOnlyContainsUserSelectNone(element) && (!displayValue.length() || !(displayValue == noneAtom() || displayValue == "table-column"_s || displayValue == "table-column-group"_s))) {
else if ((!m_ignoreUserSelectNoneContent || !m_userSelectNoneStateCache.nodeOnlyContainsUserSelectNone(element)) && (!displayValue.length() || !(displayValue == noneAtom() || displayValue == "table-column"_s || displayValue == "table-column-group"_s))) {
if (_caches->isBlockElement(element) && !element.hasTagName(brTag) && !(displayValue == "table-cell"_s && ![_textTables count])
&& !([_textLists count] > 0 && displayValue == "block"_s && !element.hasTagName(liTag) && !element.hasTagName(ulTag) && !element.hasTagName(olTag)))
_newParagraphForElement(element, element.tagName(), NO, YES);
Expand Down Expand Up @@ -2074,7 +2077,7 @@ static NSInteger _colCompare(id block1, id block2, void *)

void HTMLConverter::_processText(CharacterData& characterData)
{
if (m_userSelectNoneStateCache.nodeOnlyContainsUserSelectNone(characterData))
if (m_ignoreUserSelectNoneContent && m_userSelectNoneStateCache.nodeOnlyContainsUserSelectNone(characterData))
return;
NSUInteger textLength = [_attrStr length];
unichar lastChar = (textLength > 0) ? [[_attrStr string] characterAtIndex:textLength - 1] : '\n';
Expand Down
3 changes: 2 additions & 1 deletion Source/WebCore/editing/markup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
#include "Page.h"
#include "PageConfiguration.h"
#include "PasteboardItemInfo.h"
#include "Quirks.h"
#include "Range.h"
#include "RenderBlock.h"
#include "ScriptWrappableInlines.h"
Expand Down Expand Up @@ -438,7 +439,7 @@ inline StyledMarkupAccumulator::StyledMarkupAccumulator(const Position& start, c
, m_annotate(annotate)
, m_highestNodeToBeSerialized(highestNodeToBeSerialized)
, m_useComposedTree(serializeComposedTree == SerializeComposedTree::Yes)
, m_ignoresUserSelectNone(ignoreUserSelectNone == IgnoreUserSelectNone::Yes)
, m_ignoresUserSelectNone(ignoreUserSelectNone == IgnoreUserSelectNone::Yes && !start.document()->quirks().needsToCopyUserSelectNoneQuirk())
, m_needsPositionStyleConversion(needsPositionStyleConversion)
, m_standardFontFamilySerializationMode(standardFontFamilySerializationMode)
, m_shouldPreserveMSOList(msoListMode == MSOListMode::Preserve)
Expand Down
3 changes: 3 additions & 0 deletions Source/WebCore/html/HTMLMetaElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "MediaQueryParser.h"
#include "MediaQueryParserContext.h"
#include "NodeName.h"
#include "Quirks.h"
#include "RenderStyle.h"
#include "Settings.h"
#include "StyleResolveForDocument.h"
Expand Down Expand Up @@ -185,6 +186,8 @@ void HTMLMetaElement::process()
#endif
else if (equalLettersIgnoringASCIICase(nameValue, "referrer"_s))
document().processReferrerPolicy(contentValue, ReferrerPolicySource::MetaTag);
else if (equalLettersIgnoringASCIICase(nameValue, "confluence-request-time"_s))
document().quirks().setNeedsToCopyUserSelectNoneQuirk();
}

const AtomString& HTMLMetaElement::content() const
Expand Down
10 changes: 8 additions & 2 deletions Source/WebCore/page/DOMSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "Editing.h"
#include "FrameSelection.h"
#include "LocalFrame.h"
#include "Quirks.h"
#include "Range.h"
#include "Settings.h"
#include "ShadowRoot.h"
Expand Down Expand Up @@ -510,12 +511,17 @@ String DOMSelection::toString() const
auto frame = this->frame();
if (!frame)
return String();

OptionSet<TextIteratorBehavior> options;
if (!frame->document()->quirks().needsToCopyUserSelectNoneQuirk())
options.add(TextIteratorBehavior::IgnoresUserSelectNone);

if (frame->settings().liveRangeSelectionEnabled()) {
auto range = frame->selection().selection().range();
return range ? plainText(*range, TextIteratorBehavior::IgnoresUserSelectNone) : emptyString();
return range ? plainText(*range, options) : emptyString();
}
auto range = frame->selection().selection().firstRange();
return range ? plainText(*range, TextIteratorBehavior::IgnoresUserSelectNone) : emptyString();
return range ? plainText(*range, options) : emptyString();
}

RefPtr<Node> DOMSelection::shadowAdjustedNode(const Position& position) const
Expand Down
5 changes: 5 additions & 0 deletions Source/WebCore/page/Quirks.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ class Quirks {
void setNeedsConfigurableIndexedPropertiesQuirk() { m_needsConfigurableIndexedPropertiesQuirk = true; }
bool needsConfigurableIndexedPropertiesQuirk() const;

// webkit.org/b/259091.
bool needsToCopyUserSelectNoneQuirk() const { return m_needsToCopyUserSelectNoneQuirk; }
void setNeedsToCopyUserSelectNoneQuirk() { m_needsToCopyUserSelectNoneQuirk = true; }

private:
bool needsQuirks() const;

Expand Down Expand Up @@ -232,6 +236,7 @@ class Quirks {
mutable std::optional<bool> m_shouldAdvertiseSupportForHLSSubtitleTypes;
#endif
bool m_needsConfigurableIndexedPropertiesQuirk { false };
bool m_needsToCopyUserSelectNoneQuirk { false };
};

} // namespace WebCore
10 changes: 10 additions & 0 deletions Tools/TestWebKitAPI/Tests/WebKitCocoa/CopyRTF.mm
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,14 @@ static void checkColor(PlatformColor *color, CGFloat red, CGFloat green, CGFloat
EXPECT_WK_STREQ([attributedString string].UTF8String, "hello WebKit bar");
}

TEST(CopyRTF, StripsUserSelectNoneQuirks)
{
auto attributedString = copyAttributedStringFromHTML(@"<meta name='confluence-request-time' content='1689029750234'>"
"hello <span style='-webkit-user-select: none; user-select: none;'>world "
"<span style='-webkit-user-select: initial; user-select: initial;'>WebKit </span></span>"
"<div style='-webkit-user-select: none; user-select: none;'>some<br>user-select-none<br>content</div><span inert>foo </span>bar", false);

EXPECT_WK_STREQ([attributedString string].UTF8String, "hello world WebKit\nsome\nuser-select-none\ncontent\nfoo bar");
}

#endif // PLATFORM(COCOA)

0 comments on commit 6f13250

Please sign in to comment.