Bug 1957916 - avoid extra Content Analysis call when pasting into Google Sheets r=edgar

If aDataTransfer is non-null we should always use it in
EditorBase::PasteAsAction(), even if we aren't dispatching the event.

This correctly handles a corner case when the focus is changed by the
paste handler (around line 1929) and PasteAsAction() calls itself again.

Differential Revision: https://phabricator.services.mozilla.com/D244235
This commit is contained in:
Greg Stoll
2025-04-09 14:55:58 +00:00
parent fb30235b10
commit f84749bbc7
4 changed files with 387 additions and 7 deletions

View File

@@ -1855,13 +1855,10 @@ nsresult EditorBase::PasteAsAction(nsIClipboard::ClipboardType aClipboardType,
// the clipboard event and the call to HandlePaste below. This prevents // the clipboard event and the call to HandlePaste below. This prevents
// race conditions with Content Analysis on like we see in bug 1918027. // race conditions with Content Analysis on like we see in bug 1918027.
// Note that this is not needed if we're not going to dispatch the paste // Note that this is not needed if we're not going to dispatch the paste
// event. // event and no aDataTransfer was passed in.
RefPtr<DataTransfer> dataTransfer; RefPtr<DataTransfer> dataTransfer = aDataTransfer;
if (aDispatchPasteEvent == DispatchPasteEvent::Yes) { if (!aDataTransfer && aDispatchPasteEvent == DispatchPasteEvent::Yes) {
dataTransfer = aDataTransfer dataTransfer = CreateDataTransferForPaste(ePaste, aClipboardType);
? RefPtr<DataTransfer>(aDataTransfer)
: RefPtr<DataTransfer>(CreateDataTransferForPaste(
ePaste, aClipboardType));
} }
AutoEditActionDataSetter editActionData(*this, EditAction::ePaste, AutoEditActionDataSetter editActionData(*this, EditAction::ePaste,
aPrincipal); aPrincipal);

View File

@@ -43,6 +43,11 @@ support-files = [
"clipboard_paste_prompt.html", "clipboard_paste_prompt.html",
] ]
["browser_clipboard_paste_redirect_focus_in_paste_event_listener.js"]
support-files = [
"clipboard_paste_redirect_focus_in_paste_event_listener.html",
]
["browser_clipboard_read_async_content_analysis.js"] ["browser_clipboard_read_async_content_analysis.js"]
support-files = [ support-files = [
"clipboard_read_async.html", "clipboard_read_async.html",

View File

@@ -0,0 +1,236 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let mockCA = makeMockContentAnalysis();
add_setup(async function test_setup() {
mockCA = await mockContentAnalysisService(mockCA);
});
// Based on editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html
const PAGE_URL =
"https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_redirect_focus_in_paste_event_listener.html";
const CLIPBOARD_TEXT_STRING = "plain text";
function setClipboardData(clipboardString) {
const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
Ci.nsITransferable
);
trans.init(null);
trans.addDataFlavor("text/plain");
const str = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
str.data = clipboardString;
trans.setTransferData("text/plain", str);
// Write to clipboard.
Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
}
async function testPasteWithElementId(elementId, browser) {
let resultPromise = SpecialPowers.spawn(browser, [], () => {
return new Promise(resolve => {
content.document.addEventListener(
"testresult",
event => {
resolve(event.detail.result);
},
{ once: true }
);
});
});
// Paste into content
await SpecialPowers.spawn(browser, [elementId], async elementId => {
content.document.getElementById(elementId).focus();
});
await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser);
let result = await resultPromise;
is(result, undefined, "Got unexpected result from page");
is(
mockCA.calls.length,
1,
"Correct number of calls to Content Analysis"
);
assertContentAnalysisRequest(
mockCA.calls[0],
CLIPBOARD_TEXT_STRING,
mockCA.calls[0].userActionId,
1
);
mockCA.clearCalls();
//TODO
let value = await getElementValue(browser, elementId);
is(
value,
CLIPBOARD_TEXT_STRING,
"element has correct value"
);
}
function assertContentAnalysisRequest(
request,
expectedText,
expectedUserActionId,
expectedRequestsCount
) {
is(request.url.spec, PAGE_URL, "request has correct URL");
is(
request.analysisType,
Ci.nsIContentAnalysisRequest.eBulkDataEntry,
"request has correct analysisType"
);
is(
request.reason,
Ci.nsIContentAnalysisRequest.eClipboardPaste,
"request has correct reason"
);
is(
request.operationTypeForDisplay,
Ci.nsIContentAnalysisRequest.eClipboard,
"request has correct operationTypeForDisplay"
);
is(request.filePath, "", "request filePath should match");
is(request.textContent, expectedText, "request textContent should match");
is(
request.userActionRequestsCount,
expectedRequestsCount,
"request userActionRequestsCount should match"
);
is(
request.userActionId,
expectedUserActionId,
"request userActionId should match"
);
ok(request.userActionId.length, "request userActionId should not be empty");
is(request.printDataHandle, 0, "request printDataHandle should not be 0");
is(request.printDataSize, 0, "request printDataSize should not be 0");
ok(!!request.requestToken.length, "request requestToken should not be empty");
}
// Must be called from inside SpecialPowers.spawn()
function getElementValue(elementId) {
let elem = content.document.getElementById(elementId);
let tagName = elem.tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
return elem.value;
}
return elem.textContent;
}
// Must be called from inside SpecialPowers.spawn()
function setElementValue(elementId, value) {
let elem = content.document.getElementById(elementId);
let tagName = elem.tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
elem.value = value;
} else {
elem.innerHTML = value;
}
}
add_task(async function testClipboardPasteWithRedirectFocus() {
mockCA.setupForTest(true);
setClipboardData(CLIPBOARD_TEXT_STRING);
const transferable =
SpecialPowers.Cc["@mozilla.org/widget/transferable;1"].createInstance(SpecialPowers.Ci.nsITransferable);
transferable.init(
SpecialPowers.wrap(window).docShell.QueryInterface(SpecialPowers.Ci.nsILoadContext)
);
const supportString =
SpecialPowers.Cc["@mozilla.org/supports-string;1"].createInstance(SpecialPowers.Ci.nsISupportsString);
supportString.data = "plain text";
transferable.setTransferData("text/plain", supportString);
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
let browser = tab.linkedBrowser;
for (const command of [
"cmd_paste",
"cmd_pasteNoFormatting",
"cmd_pasteQuote",
"cmd_pasteTransferable"
]) {
for (const editableSelector of [
"#src > input",
"#src > textarea",
"#src > div[contenteditable]"
]) {
await SpecialPowers.spawn(browser, [editableSelector], async (editableSelector) => {
const editableElement = content.document.querySelector(editableSelector);
await (async () => {
const input = content.document.querySelector("#dest > input");
editableElement.focus();
editableElement.addEventListener(
"paste",
() => input.focus(),
{once: true}
);
})();
});
await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser);
await SpecialPowers.spawn(browser, [command, editableSelector], async (command, editableSelector) => {
// Must be called from inside SpecialPowers.spawn()
function getElementValue(elem) {
let tagName = elem.tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
return elem.value;
}
return elem.textContent;
}
function setElementValue(elem, value) {
let tagName = elem.tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
elem.value = value;
} else {
elem.innerHTML = value;
}
}
const editableElement = content.document.querySelector(editableSelector);
const editableElementDesc = `<${
editableElement.tagName.toLocaleLowerCase()
}${editableElement.hasAttribute("contenteditable") ? " contenteditable" : ""}>`;
const input = content.document.querySelector("#dest > input");
is(
getElementValue(editableElement).replace(/\n/g, ""),
"",
`${command}: ${
editableElementDesc
} should not have the pasted text because focus is redirected to <input> in a "paste" event listener`
);
is(
input.value.replace("> ", ""),
"plain text",
`${command}: new focused <input> (moved from ${
editableElementDesc
}) should have the pasted text`
);
setElementValue(editableElement, "");
input.value = "";
});
is(
mockCA.calls.length,
1,
"Correct number of calls to Content Analysis"
);
assertContentAnalysisRequest(
mockCA.calls[0],
CLIPBOARD_TEXT_STRING,
mockCA.calls[0].userActionId,
1
);
mockCA.clearCalls();
}
}
BrowserTestUtils.removeTab(tab);
});

View File

@@ -0,0 +1,142 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<!-- Based on editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html -->
<title>Testing handling "paste" command when a "paste" event listener moves focus</title>
<script>
"use strict";
/*SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(async () => {
info("Waiting for initializing clipboard...");
await SimpleTest.promiseClipboardChange(
"plain text",
() => SpecialPowers.clipboardCopyString("plain text")
);
const transferable =
SpecialPowers.Cc["@mozilla.org/widget/transferable;1"].createInstance(SpecialPowers.Ci.nsITransferable);
transferable.init(
SpecialPowers.wrap(window).docShell.QueryInterface(SpecialPowers.Ci.nsILoadContext)
);
const supportString =
SpecialPowers.Cc["@mozilla.org/supports-string;1"].createInstance(SpecialPowers.Ci.nsISupportsString);
supportString.data = "plain text";
transferable.setTransferData("text/plain", supportString);
function getValue(aElement) {
if (aElement.tagName.toLowerCase() == "input" ||
aElement.tagName.toLowerCase() == "textarea") {
return aElement.value;
}
return aElement.textContent;
}
function setValue(aElement, aValue) {
if (aElement.tagName.toLowerCase() == "input" ||
aElement.tagName.toLowerCase() == "textarea") {
aElement.value = aValue;
return;
}
aElement.innerHTML = aValue === "" ? "<br>" : aValue;
}
for (const command of [
"cmd_paste",
"cmd_pasteNoFormatting",
"cmd_pasteQuote",
"cmd_pasteTransferable"
]) {
for (const editableSelector of [
"#src > input",
"#src > textarea",
"#src > div[contenteditable]"
]) {
const editableElement = document.querySelector(editableSelector);
const editableElementDesc = `<${
editableElement.tagName.toLocaleLowerCase()
}${editableElement.hasAttribute("contenteditable") ? " contenteditable" : ""}>`;
(() => {
const input = document.querySelector("#dest > input");
editableElement.focus();
editableElement.addEventListener(
"paste",
() => input.focus(),
{once: true}
);
SpecialPowers.doCommand(window, command, transferable);
is(
getValue(editableElement).replace(/\n/g, ""),
"",
`${command}: ${
editableElementDesc
} should not have the pasted text because focus is redirected to <input> in a "paste" event listener`
);
is(
input.value.replace("> ", ""),
"plain text",
`${command}: new focused <input> (moved from ${
editableElementDesc
}) should have the pasted text`
);
setValue(editableElement, "");
input.value = "";
})();
(() => {
const contentEditable = document.querySelector("#dest > div[contenteditable]");
editableElement.focus();
editableElement.addEventListener(
"paste",
() => contentEditable.focus(),
{once: true}
);
SpecialPowers.doCommand(window, command, transferable);
is(
getValue(editableElement).replace(/\n/g, ""),
"",
`${command}: ${
editableElementDesc
} should not have the pasted text because focus is redirected to <div contenteditable> in a "paste" event listener`
);
is(
contentEditable.textContent.replace(/\n/g, "").replace("> ", ""),
"plain text",
`${command}: new focused <div contenteditable> (moved from ${
editableElementDesc
}) should have the pasted text`
);
setValue(editableElement, "");
contentEditable.innerHTML = "<br>";
})();
(() => {
const button = document.querySelector("#dest > button");
editableElement.focus();
editableElement.addEventListener(
"paste",
() => button.focus(),
{once: true}
);
SpecialPowers.doCommand(window, command, transferable);
is(
getValue(editableElement).replace(/\n/g, ""),
"",
`${command}: ${
editableElementDesc
} should not have the pasted text because focus is redirected to <button> in a "paste" event listener`
);
setValue(editableElement, "");
})();
}
}
SimpleTest.finish();
});*/
</script>
</head>
<body>
<div id="src"><input><textarea></textarea><div contenteditable><br></div></div>
<div id="dest"><input><div contenteditable><br></div><button>button</button></div>
</body>
</html>