Bug 1960237 - Only offer "Add Search Engine" for forms with an explicit action attribute. r=search-reviewers,scunnane

Differential Revision: https://phabricator.services.mozilla.com/D247775
This commit is contained in:
Moritz Beier
2025-05-12 09:05:40 +00:00
committed by mbeier@mozilla.com
parent 69dee15e68
commit 094ffaecef
5 changed files with 123 additions and 41 deletions

View File

@@ -241,7 +241,7 @@ export class ContextMenuChild extends JSWindowActorChild {
!node.name || !node.name ||
(method != "POST" && method != "GET") || (method != "POST" && method != "GET") ||
node.form.enctype != "application/x-www-form-urlencoded" || node.form.enctype != "application/x-www-form-urlencoded" ||
formData.entries().some(([k, v]) => !k && typeof v != "string") formData.values().some(v => typeof v != "string")
) { ) {
// This should never happen since these conditions are checked in // This should never happen since these conditions are checked in
// `isTargetASearchEngineField`. // `isTargetASearchEngineField`.

View File

@@ -3,6 +3,10 @@
"use strict"; "use strict";
const { SpellCheckHelper } = ChromeUtils.importESModule(
"resource://gre/modules/InlineSpellChecker.sys.mjs"
);
ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
const { UrlbarTestUtils: module } = ChromeUtils.importESModule( const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
"resource://testing-common/UrlbarTestUtils.sys.mjs" "resource://testing-common/UrlbarTestUtils.sys.mjs"
@@ -66,12 +70,6 @@ const URL_UTF_8 =
const URL_WINDOWS1252 = const URL_WINDOWS1252 =
"https://example.org/browser/browser/components/search/test/browser/test_windows1252.html"; "https://example.org/browser/browser/components/search/test/browser/test_windows1252.html";
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.update2.engineAliasRefresh", true]],
});
});
async function addEngine(browser, selector, name, alias) { async function addEngine(browser, selector, name, alias) {
let contextMenu = document.getElementById("contentAreaContextMenu"); let contextMenu = document.getElementById("contentAreaContextMenu");
let addEngineItem = document.getElementById("context-add-engine"); let addEngineItem = document.getElementById("context-add-engine");
@@ -164,7 +162,27 @@ async function navigateToCharset(charset) {
} }
} }
add_task(async function () { function postDataToString(postData) {
if (!postData) {
return undefined;
}
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(postData.data);
return binaryStream
.readBytes(binaryStream.available())
.replace("searchTerms", "%s");
}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.update2.engineAliasRefresh", true]],
});
});
add_task(async function testAddingEngines() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
let browser = tab.linkedBrowser; let browser = tab.linkedBrowser;
@@ -187,7 +205,7 @@ add_task(async function () {
"Submission URI is correct" "Submission URI is correct"
); );
Assert.equal( Assert.equal(
postDataToString(submission.postData), SearchTestUtils.getPostDataString(submission),
args.expectedPost, args.expectedPost,
"Submission post data is correct." "Submission post data is correct."
); );
@@ -202,16 +220,56 @@ add_task(async function () {
BrowserTestUtils.removeTab(gBrowser.selectedTab); BrowserTestUtils.removeTab(gBrowser.selectedTab);
}); });
function postDataToString(postData) { add_task(async function testSearchFieldDetection() {
if (!postData) { let form = document.createElement("form");
return undefined; form.method = "GET";
} form.action = "/";
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(postData.data);
return binaryStream let input = document.createElement("input");
.readBytes(binaryStream.available()) input.type = "search";
.replace("searchTerms", "%s"); input.name = "q";
} form.appendChild(input);
let isSearchField = SpellCheckHelper.isTargetASearchEngineField(
input,
window
);
Assert.equal(isSearchField, true, "Is search field initially");
// We test mostly non search fields here since valid search fields
// are already tested in testAddingEngines.
delete form.removeAttribute("action");
isSearchField = SpellCheckHelper.isTargetASearchEngineField(input, window);
Assert.equal(isSearchField, false, "Missing action means no search field");
form.action = "/";
form.method = "dialog";
isSearchField = SpellCheckHelper.isTargetASearchEngineField(input, window);
Assert.equal(isSearchField, false, "Method=dialog means no search field");
form.method = "POST";
delete input.removeAttribute("name");
isSearchField = SpellCheckHelper.isTargetASearchEngineField(input, window);
Assert.equal(isSearchField, false, "Missing name means no search field");
input.name = "q";
input.type = "url";
isSearchField = SpellCheckHelper.isTargetASearchEngineField(input, window);
Assert.equal(isSearchField, false, "Url input means no search field");
input.type = "text";
let fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.name = "file-input";
form.appendChild(fileInput);
isSearchField = SpellCheckHelper.isTargetASearchEngineField(input, window);
Assert.equal(isSearchField, false, "File input means no search field");
fileInput.remove();
isSearchField = SpellCheckHelper.isTargetASearchEngineField(input, window);
Assert.equal(isSearchField, true, "Is search field again");
});

View File

@@ -761,6 +761,26 @@ class _SearchTestUtils {
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
} }
/**
* Extracts post data string from an nsISearchSubmission.
* If there is no post data, returns null.
*
* @param {?nsISearchSubmission} submission
* @returns {?string}
*/
getPostDataString(submission) {
if (!submission.postData) {
return null;
}
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(submission.postData.data);
return binaryStream.readBytes(binaryStream.available());
}
} }
export const SearchTestUtils = new _SearchTestUtils(); export const SearchTestUtils = new _SearchTestUtils();

View File

@@ -110,23 +110,9 @@ add_task(async function test_encoding_of_spaces() {
let submission = engine.getSubmission("f o o"); let submission = engine.getSubmission("f o o");
Assert.equal(submission.uri.spec, "https://example.com/user"); Assert.equal(submission.uri.spec, "https://example.com/user");
Assert.equal( Assert.equal(
postDataToString(submission.postData), SearchTestUtils.getPostDataString(submission),
"q=f+o+o", "q=f+o+o",
"Encodes spaces in body as +." "Encodes spaces in body as +."
); );
await Services.search.removeEngine(engine); await Services.search.removeEngine(engine);
}); });
function postDataToString(postData) {
if (!postData) {
return undefined;
}
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(postData.data);
return binaryStream
.readBytes(binaryStream.available())
.replace("searchTerms", "%s");
}

View File

@@ -473,21 +473,39 @@ export var SpellCheckHelper = {
); );
}, },
/**
* Returns whether the element is counted as a search engine field.
*
* @param {HTMLInputElement} aNode
* The input to check.
* @param {Window} window
* The element's window.
* @returns {boolean}
* Whether it should count as a search engine field.
*/
isTargetASearchEngineField(aNode, window) { isTargetASearchEngineField(aNode, window) {
if (!window.HTMLInputElement.isInstance(aNode)) { if (
!window.HTMLInputElement.isInstance(aNode) ||
(aNode.type != "text" && aNode.type != "search") ||
aNode.readOnly ||
!aNode.name ||
!aNode.form
) {
return false; return false;
} }
let form = aNode.form; let form = aNode.form;
if (!form || aNode.type == "password" || !aNode.name) {
return false;
}
let method = form.method.toUpperCase(); let method = form.method.toUpperCase();
return ( return (
// Forms without an explicit action often don't work, see Bug 1960237.
form.hasAttribute("action") &&
// The only other method is dialog.
(method == "GET" || method == "POST") && (method == "GET" || method == "POST") &&
// SearchEngine objects currently only support urlencoded requests.
form.enctype == "application/x-www-form-urlencoded" && form.enctype == "application/x-www-form-urlencoded" &&
new FormData(form).entries().every(([k, v]) => k && typeof v == "string") // Don't allow forms with file inputs.
new FormData(form).values().every(v => typeof v == "string")
); );
}, },