LibWeb: Disallow Editing API calls on non-HTML documents

This is not directly mentioned in the Editing API spec, but all major
browsers do this and there is a WPT for this behavior.
This commit is contained in:
Tim Ledbetter 2025-01-21 10:13:14 +00:00 committed by Jelle Raaijmakers
parent 2cee6aeba3
commit a0b0e91d4f
Notes: github-actions[bot] 2025-01-21 18:09:45 +00:00
8 changed files with 89 additions and 31 deletions

View file

@ -581,12 +581,12 @@ public:
void set_previous_document_unload_timing(DocumentUnloadTimingInfo const& previous_document_unload_timing) { m_previous_document_unload_timing = previous_document_unload_timing; }
// https://w3c.github.io/editing/docs/execCommand/
bool exec_command(FlyString const& command, bool show_ui, String const& value);
bool query_command_enabled(FlyString const& command);
bool query_command_indeterm(FlyString const& command);
bool query_command_state(FlyString const& command);
bool query_command_supported(FlyString const& command);
String query_command_value(FlyString const& command);
WebIDL::ExceptionOr<bool> exec_command(FlyString const& command, bool show_ui, String const& value);
WebIDL::ExceptionOr<bool> query_command_enabled(FlyString const& command);
WebIDL::ExceptionOr<bool> query_command_indeterm(FlyString const& command);
WebIDL::ExceptionOr<bool> query_command_state(FlyString const& command);
WebIDL::ExceptionOr<bool> query_command_supported(FlyString const& command);
WebIDL::ExceptionOr<String> query_command_value(FlyString const& command);
// https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event
bool has_scheduled_selectionchange_event() const { return m_has_scheduled_selectionchange_event; }

View file

@ -52,7 +52,7 @@ bool command_back_color_action(DOM::Document& document, String const& value)
bool command_bold_action(DOM::Document& document, String const&)
{
// If queryCommandState("bold") returns true, set the selection's value to "normal".
if (document.query_command_state(CommandNames::bold)) {
if (MUST(document.query_command_state(CommandNames::bold))) {
set_the_selections_value(document, CommandNames::bold, "normal"_string);
}
@ -1864,7 +1864,7 @@ bool command_insert_unordered_list_state(DOM::Document const& document)
bool command_italic_action(DOM::Document& document, String const&)
{
// If queryCommandState("italic") returns true, set the selection's value to "normal".
if (document.query_command_state(CommandNames::italic)) {
if (MUST(document.query_command_state(CommandNames::italic))) {
set_the_selections_value(document, CommandNames::italic, "normal"_string);
}
@ -2256,7 +2256,7 @@ bool command_select_all_action(DOM::Document& document, String const&)
bool command_strikethrough_action(DOM::Document& document, String const&)
{
// If queryCommandState("strikethrough") returns true, set the selection's value to null.
if (document.query_command_state(CommandNames::strikethrough)) {
if (MUST(document.query_command_state(CommandNames::strikethrough))) {
set_the_selections_value(document, CommandNames::strikethrough, {});
}
@ -2291,7 +2291,7 @@ bool command_style_with_css_state(DOM::Document const& document)
bool command_subscript_action(DOM::Document& document, String const&)
{
// 1. Call queryCommandState("subscript"), and let state be the result.
auto state = document.query_command_state(CommandNames::subscript);
auto state = MUST(document.query_command_state(CommandNames::subscript));
// 2. Set the selection's value to null.
set_the_selections_value(document, CommandNames::subscript, {});
@ -2344,7 +2344,7 @@ bool command_subscript_indeterminate(DOM::Document const& document)
bool command_superscript_action(DOM::Document& document, String const&)
{
// 1. Call queryCommandState("superscript"), and let state be the result.
auto state = document.query_command_state(CommandNames::superscript);
auto state = MUST(document.query_command_state(CommandNames::superscript));
// 2. Set the selection's value to null.
set_the_selections_value(document, CommandNames::superscript, {});
@ -2397,7 +2397,7 @@ bool command_superscript_indeterminate(DOM::Document const& document)
bool command_underline_action(DOM::Document& document, String const&)
{
// If queryCommandState("underline") returns true, set the selection's value to null.
if (document.query_command_state(CommandNames::underline)) {
if (MUST(document.query_command_state(CommandNames::underline))) {
set_the_selections_value(document, CommandNames::underline, {});
}

View file

@ -14,15 +14,19 @@
namespace Web::DOM {
// https://w3c.github.io/editing/docs/execCommand/#execcommand()
bool Document::exec_command(FlyString const& command, [[maybe_unused]] bool show_ui, String const& value)
WebIDL::ExceptionOr<bool> Document::exec_command(FlyString const& command, [[maybe_unused]] bool show_ui, String const& value)
{
// AD-HOC: This is not directly mentioned in the spec, but all major browsers limit editing API calls to HTML documents
if (!is_html_document())
return WebIDL::InvalidStateError::create(realm(), "execCommand is only supported on HTML documents"_string);
// 1. If only one argument was provided, let show UI be false.
// 2. If only one or two arguments were provided, let value be the empty string.
// NOTE: these steps are dealt by the default values for both show_ui and value
// 3. If command is not supported or not enabled, return false.
// NOTE: query_command_enabled() also checks if command is supported
if (!query_command_enabled(command))
if (!MUST(query_command_enabled(command)))
return false;
// 4. If command is not in the Miscellaneous commands section:
@ -54,7 +58,7 @@ bool Document::exec_command(FlyString const& command, [[maybe_unused]] bool show
//
// We have to check again whether the command is enabled, because the beforeinput handler
// might have done something annoying like getSelection().removeAllRanges().
if (!query_command_enabled(command))
if (!MUST(query_command_enabled(command)))
return false;
// FIXME: 5. Let affected editing host be the editing host that is an inclusive ancestor of the
@ -99,10 +103,14 @@ bool Document::exec_command(FlyString const& command, [[maybe_unused]] bool show
}
// https://w3c.github.io/editing/docs/execCommand/#querycommandenabled()
bool Document::query_command_enabled(FlyString const& command)
WebIDL::ExceptionOr<bool> Document::query_command_enabled(FlyString const& command)
{
// AD-HOC: This is not directly mentioned in the spec, but all major browsers limit editing API calls to HTML documents
if (!is_html_document())
return WebIDL::InvalidStateError::create(realm(), "queryCommandEnabled is only supported on HTML documents"_string);
// 2. Return true if command is both supported and enabled, false otherwise.
if (!query_command_supported(command))
if (!MUST(query_command_supported(command)))
return false;
// https://w3c.github.io/editing/docs/execCommand/#enabled
@ -119,7 +127,7 @@ bool Document::query_command_enabled(FlyString const& command)
// AD-HOC: selectAll requires a selection object to exist.
if (command == Editing::CommandNames::selectAll)
return get_selection();
return get_selection() != nullptr;
// The other commands defined here are enabled if the active range is not null,
auto active_range = Editing::active_range(*this);
@ -179,8 +187,12 @@ bool Document::query_command_enabled(FlyString const& command)
}
// https://w3c.github.io/editing/docs/execCommand/#querycommandindeterm()
bool Document::query_command_indeterm(FlyString const& command)
WebIDL::ExceptionOr<bool> Document::query_command_indeterm(FlyString const& command)
{
// AD-HOC: This is not directly mentioned in the spec, but all major browsers limit editing API calls to HTML documents
if (!is_html_document())
return WebIDL::InvalidStateError::create(realm(), "queryCommandIndeterm is only supported on HTML documents"_string);
// 1. If command is not supported or has no indeterminacy, return false.
auto optional_command = Editing::find_command_definition(command);
if (!optional_command.has_value())
@ -251,8 +263,12 @@ bool Document::query_command_indeterm(FlyString const& command)
}
// https://w3c.github.io/editing/docs/execCommand/#querycommandstate()
bool Document::query_command_state(FlyString const& command)
WebIDL::ExceptionOr<bool> Document::query_command_state(FlyString const& command)
{
// AD-HOC: This is not directly mentioned in the spec, but all major browsers limit editing API calls to HTML documents
if (!is_html_document())
return WebIDL::InvalidStateError::create(realm(), "queryCommandState is only supported on HTML documents"_string);
// 1. If command is not supported or has no state, return false.
auto optional_command = Editing::find_command_definition(command);
if (!optional_command.has_value())
@ -293,8 +309,12 @@ bool Document::query_command_state(FlyString const& command)
}
// https://w3c.github.io/editing/docs/execCommand/#querycommandsupported()
bool Document::query_command_supported(FlyString const& command)
WebIDL::ExceptionOr<bool> Document::query_command_supported(FlyString const& command)
{
// AD-HOC: This is not directly mentioned in the spec, but all major browsers limit editing API calls to HTML documents
if (!is_html_document())
return WebIDL::InvalidStateError::create(realm(), "queryCommandSupported is only supported on HTML documents"_string);
// When the queryCommandSupported(command) method on the Document interface is invoked, the
// user agent must return true if command is supported and available within the current script
// on the current site, and false otherwise.
@ -302,16 +322,20 @@ bool Document::query_command_supported(FlyString const& command)
}
// https://w3c.github.io/editing/docs/execCommand/#querycommandvalue()
String Document::query_command_value(FlyString const& command)
WebIDL::ExceptionOr<String> Document::query_command_value(FlyString const& command)
{
// AD-HOC: This is not directly mentioned in the spec, but all major browsers limit editing API calls to HTML documents
if (!is_html_document())
return WebIDL::InvalidStateError::create(realm(), "queryCommandValue is only supported on HTML documents"_string);
// 1. If command is not supported or has no value, return the empty string.
auto optional_command = Editing::find_command_definition(command);
if (!optional_command.has_value())
return {};
return String {};
auto const& command_definition = optional_command.release_value();
auto value_override = command_value_override(command);
if (!command_definition.value && !value_override.has_value())
return {};
return String {};
// 2. If command is "fontSize" and its value override is set, convert the value override to an
// integer number of pixels and return the legacy font size for the result.

View file

@ -3323,7 +3323,7 @@ Vector<RecordedOverride> record_current_states_and_values(DOM::Document const& d
// 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order: add (command, command's value)
// to overrides.
for (auto const& command : { CommandNames::fontName, CommandNames::foreColor, CommandNames::hiliteColor })
overrides.empend(command, node->document().query_command_value(command));
overrides.empend(command, MUST(node->document().query_command_value(command)));
// 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
effective_value = effective_command_value(node, CommandNames::fontSize);
@ -3506,7 +3506,7 @@ void restore_states_and_values(DOM::Document& document, Vector<RecordedOverride>
for (auto override : overrides) {
// 1. If override is a boolean, and queryCommandState(command) returns something different from override,
// take the action for command, with value equal to the empty string.
if (override.value.has<bool>() && document.query_command_state(override.command) != override.value.get<bool>()) {
if (override.value.has<bool>() && MUST(document.query_command_state(override.command)) != override.value.get<bool>()) {
take_the_action_for_command(document, override.command, {});
}
@ -3514,7 +3514,7 @@ void restore_states_and_values(DOM::Document& document, Vector<RecordedOverride>
// queryCommandValue(command) returns something not equivalent to override, take the action for command,
// with value equal to override.
else if (override.value.has<String>() && !override.command.is_one_of(CommandNames::createLink, CommandNames::fontSize)
&& document.query_command_value(override.command) != override.value.get<String>()) {
&& MUST(document.query_command_value(override.command)) != override.value.get<String>()) {
take_the_action_for_command(document, override.command, override.value.get<String>());
}
@ -3761,7 +3761,7 @@ void set_the_selections_value(DOM::Document& document, FlyString const& command,
}
// 5. Otherwise, if command is "createLink" or it has a value specified, set the value override to new value.
else if (command == CommandNames::createLink || !document.query_command_value(CommandNames::createLink).is_empty()) {
else if (command == CommandNames::createLink || !MUST(document.query_command_value(CommandNames::createLink)).is_empty()) {
document.set_command_value_override(command, new_value.value());
}

View file

@ -2,6 +2,6 @@ No range.
DIV 0 - DIV 0
BODY 0 - BODY 5
true
false
queryCommandEnabled threw exception of type: InvalidStateError
false
Did not crash!

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass editing APIs on an XML document should be disabled

View file

@ -41,8 +41,12 @@
document.implementation.createHTMLDocument(),
];
for (const doc of documents) {
println(doc.queryCommandEnabled('selectAll'));
doc.execCommand('selectAll');
try {
println(doc.queryCommandEnabled('selectAll'));
doc.execCommand('selectAll');
} catch (e) {
println(`queryCommandEnabled threw exception of type: ${e.name}`);
}
}
println('Did not crash!');
});

View file

@ -0,0 +1,24 @@
<!doctype html>
<meta charset=utf-8>
<title>Non-HTML document tests</title>
<script src=../../resources/testharness.js></script>
<script src=../../resources/testharnessreport.js></script>
<script>
test(function() {
let xmldoc =
document.implementation.createDocument("http://www.w3.org/1999/xlink",
"html", null);
for (let f of [
() => xmldoc.execCommand("bold"),
() => xmldoc.queryCommandEnabled("bold"),
() => xmldoc.queryCommandIndeterm("bold"),
() => xmldoc.queryCommandState("bold"),
() => xmldoc.queryCommandSupported("bold"),
() => xmldoc.queryCommandValue("bold"),
]) {
assert_throws_dom("InvalidStateError", f);
}
}, "editing APIs on an XML document should be disabled");
</script>