Bug 1883367 - Disable editor commands for read-only editors. r=masayuki

The purpose of this patch is to prevent users to make changes to
read-only editors.

This patch forces `IsCommandEnabled()` to return false for read-only
editors for "standard" editor commands that could have an associated
icon or menu entry and are therefore user-accessible.

This patch does not modify/test any paste commands or any of the
advanced internal editor commands.

Differential Revision: https://phabricator.services.mozilla.com/D203754
This commit is contained in:
John Bieling 2024-03-08 17:55:57 +00:00
parent 417ec57c47
commit 89637d1080
4 changed files with 171 additions and 18 deletions

View File

@ -269,7 +269,8 @@ bool UndoCommand::IsCommandEnabled(Command aCommand,
if (!aEditorBase) {
return false;
}
return aEditorBase->IsSelectionEditable() && aEditorBase->CanUndo();
return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable() &&
aEditorBase->CanUndo();
}
nsresult UndoCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
@ -297,7 +298,8 @@ bool RedoCommand::IsCommandEnabled(Command aCommand,
if (!aEditorBase) {
return false;
}
return aEditorBase->IsSelectionEditable() && aEditorBase->CanRedo();
return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable() &&
aEditorBase->CanRedo();
}
nsresult RedoCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
@ -548,7 +550,7 @@ bool SwitchTextDirectionCommand::IsCommandEnabled(
if (!aEditorBase) {
return false;
}
return aEditorBase->IsSelectionEditable();
return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
}
nsresult SwitchTextDirectionCommand::DoCommand(Command aCommand,
@ -581,7 +583,8 @@ bool DeleteCommand::IsCommandEnabled(Command aCommand,
// We can generally delete whenever the selection is editable. However,
// cmd_delete doesn't make sense if the selection is collapsed because it's
// directionless.
bool isEnabled = aEditorBase->IsSelectionEditable();
bool isEnabled =
aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
if (aCommand == Command::Delete && isEnabled) {
return aEditorBase->CanDeleteSelection();
@ -820,7 +823,7 @@ bool InsertPlaintextCommand::IsCommandEnabled(Command aCommand,
if (!aEditorBase) {
return false;
}
return aEditorBase->IsSelectionEditable();
return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
}
nsresult InsertPlaintextCommand::DoCommand(Command aCommand,
@ -877,7 +880,7 @@ bool InsertParagraphCommand::IsCommandEnabled(Command aCommand,
if (!aEditorBase || aEditorBase->IsSingleLineEditor()) {
return false;
}
return aEditorBase->IsSelectionEditable();
return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
}
nsresult InsertParagraphCommand::DoCommand(Command aCommand,
@ -918,7 +921,7 @@ bool InsertLineBreakCommand::IsCommandEnabled(Command aCommand,
if (!aEditorBase || aEditorBase->IsSingleLineEditor()) {
return false;
}
return aEditorBase->IsSelectionEditable();
return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
}
nsresult InsertLineBreakCommand::DoCommand(Command aCommand,

View File

@ -52,7 +52,7 @@ bool StateUpdatingCommandBase::IsCommandEnabled(Command aCommand,
if (!htmlEditor) {
return false;
}
if (!htmlEditor->IsSelectionEditable()) {
if (!htmlEditor->IsModifiable() || !htmlEditor->IsSelectionEditable()) {
return false;
}
if (aCommand == Command::FormatAbsolutePosition) {
@ -355,8 +355,7 @@ bool RemoveListCommand::IsCommandEnabled(Command aCommand,
if (!htmlEditor) {
return false;
}
if (!htmlEditor->IsSelectionEditable()) {
if (!htmlEditor->IsModifiable() || !htmlEditor->IsSelectionEditable()) {
return false;
}
@ -401,7 +400,7 @@ bool IndentCommand::IsCommandEnabled(Command aCommand,
if (!htmlEditor) {
return false;
}
return htmlEditor->IsSelectionEditable();
return htmlEditor->IsModifiable() && htmlEditor->IsSelectionEditable();
}
nsresult IndentCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
@ -434,7 +433,7 @@ bool OutdentCommand::IsCommandEnabled(Command aCommand,
if (!htmlEditor) {
return false;
}
return htmlEditor->IsSelectionEditable();
return htmlEditor->IsModifiable() && htmlEditor->IsSelectionEditable();
}
nsresult OutdentCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
@ -467,7 +466,7 @@ bool MultiStateCommandBase::IsCommandEnabled(Command aCommand,
return false;
}
// should be disabled sometimes, like if the current selection is an image
return htmlEditor->IsSelectionEditable();
return htmlEditor->IsModifiable() && htmlEditor->IsSelectionEditable();
}
nsresult MultiStateCommandBase::DoCommand(Command aCommand,
@ -1047,7 +1046,7 @@ bool RemoveStylesCommand::IsCommandEnabled(Command aCommand,
return false;
}
// test if we have any styles?
return htmlEditor->IsSelectionEditable();
return htmlEditor->IsModifiable() && htmlEditor->IsSelectionEditable();
}
nsresult RemoveStylesCommand::DoCommand(Command aCommand,
@ -1085,7 +1084,7 @@ bool IncreaseFontSizeCommand::IsCommandEnabled(Command aCommand,
return false;
}
// test if we are at max size?
return htmlEditor->IsSelectionEditable();
return htmlEditor->IsModifiable() && htmlEditor->IsSelectionEditable();
}
nsresult IncreaseFontSizeCommand::DoCommand(Command aCommand,
@ -1121,7 +1120,7 @@ bool DecreaseFontSizeCommand::IsCommandEnabled(Command aCommand,
return false;
}
// test if we are at min size?
return htmlEditor->IsSelectionEditable();
return htmlEditor->IsModifiable() && htmlEditor->IsSelectionEditable();
}
nsresult DecreaseFontSizeCommand::DoCommand(Command aCommand,
@ -1156,7 +1155,7 @@ bool InsertHTMLCommand::IsCommandEnabled(Command aCommand,
if (!htmlEditor) {
return false;
}
return htmlEditor->IsSelectionEditable();
return htmlEditor->IsModifiable() && htmlEditor->IsSelectionEditable();
}
nsresult InsertHTMLCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
@ -1213,7 +1212,7 @@ bool InsertTagCommand::IsCommandEnabled(Command aCommand,
if (!htmlEditor) {
return false;
}
return htmlEditor->IsSelectionEditable();
return htmlEditor->IsModifiable() && htmlEditor->IsSelectionEditable();
}
// corresponding STATE_ATTRIBUTE is: src (img) and href (a)

View File

@ -418,6 +418,8 @@ skip-if = ["os == 'android'"] #Bug 1575739
["test_cmd_paragraphState.html"]
["test_command_state_when_readonly.html"]
["test_composition_event_created_in_chrome.html"]
["test_composition_with_highlight_in_texteditor.html"]

View File

@ -0,0 +1,149 @@
<!doctype html>
<title>Test for nsIEditor.isCommandEnabled for normal and read-only editors</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
<div contenteditable></div>
<script>
let node = document.querySelector("div");
node.focus();
let htmlEditor =
SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
// Supported environments for each command. Supported values for each
// environment property:
// content: "empty", "non-empty", "cleared"
// selected: true, false
// readonly: true, false
//
// If an environment definition does not state a certain property, the command
// supports all possible values for that property. The following definition:
// "cmd_copy": [{content: "non-empty", selected: true}],
// is equivalent to:
// "cmd_copy": [
// {content: "non-empty", selected: true, readonly: true},
// {content: "non-empty", selected: true, readonly: false},
// ],
const TEST_COMMANDS = {
"cmd_selectAll": [{content: "non-empty"}],
"cmd_copy": [{content: "non-empty", selected: true}],
"cmd_cut": [{content: "non-empty", selected: true, readonly: false}],
"cmd_delete": [{content: "non-empty", selected: true, readonly: false}],
"cmd_removeList":[{content: "non-empty", selected: true, readonly: false}],
"cmd_undo": [{content: "cleared", readonly: false}],
"cmd_redo": [{content: "cleared", readonly: false}],
"cmd_switchTextDirection": [{readonly: false}],
"cmd_bold": [{readonly: false}],
"cmd_italic": [{readonly: false}],
"cmd_underline": [{readonly: false}],
"cmd_em": [{readonly: false}],
"cmd_strong": [{readonly: false}],
"cmd_strikethrough": [{readonly: false}],
"cmd_superscript": [{readonly: false}],
"cmd_subscript": [{readonly: false}],
"cmd_indent": [{readonly: false}],
"cmd_outdent": [{readonly: false}],
"cmd_formatBlock": [{readonly: false}],
"cmd_paragraphState": [{readonly: false}],
"cmd_fontFace": [{readonly: false}],
"cmd_fontSize": [{readonly: false}],
"cmd_fontColor": [{readonly: false}],
"cmd_backgroundColor": [{readonly: false}],
"cmd_highlight": [{readonly: false}],
"cmd_align": [{readonly: false}],
"cmd_removeStyles": [{readonly: false}],
"cmd_increaseFont": [{readonly: false}],
"cmd_decreaseFont": [{readonly: false}],
"cmd_insertHR": [{readonly: false}],
"cmd_insertHTML": [{readonly: false}],
"cmd_insertText": [{readonly: false}],
"cmd_insertParagraph": [{readonly: false}],
"cmd_insertLineBreak": [{readonly: false}],
"cmd_tt":[{readonly: false}],
"cmd_nobreak":[{readonly: false}],
"cmd_cite":[{readonly: false}],
"cmd_abbr":[{readonly: false}],
"cmd_acronym":[{readonly: false}],
"cmd_code":[{readonly: false}],
"cmd_samp":[{readonly: false}],
"cmd_var":[{readonly: false}],
"cmd_removeLinks":[{readonly: false}],
"cmd_ol":[{readonly: false}],
"cmd_ul":[{readonly: false}],
"cmd_dt":[{readonly: false}],
"cmd_dd":[{readonly: false}],
// InsertTagCommand
"cmd_insertImageNoUI": [{readonly: false}],
"cmd_insertLinkNoUI": [{readonly: false}],
};
function testCommands(content) {
for (let readonly of [true, false]){
if (readonly) {
htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
} else {
htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
}
for (let selected of [true, false]) {
let selection = window.getSelection();
selection.collapse(node);
if (selected) {
if (content == "non-empty") {
// The command cmd_removeList needs selected text inside a list. It
// does not matter for all other commands, so lets just select that.
let range = document.createRange();
let li = document.querySelector("li");
range.selectNodeContents(li);
selection.removeAllRanges();
selection.addRange(range);
} else {
document.execCommand("selectAll");
}
}
for (let [cmd, supports] of Object.entries(TEST_COMMANDS)) {
// Check if the command should support this environment.
let expected = supports.some(supported =>
content == (supported?.content ?? content) &&
readonly == (supported?.readonly ?? readonly) &&
selected == (supported?.selected ?? selected)
)
is(
SpecialPowers.isCommandEnabled(window, cmd),
expected,
`Enabled state of command ${cmd} should be ${
expected ? "TRUE" : "FALSE"
} for ${JSON.stringify({content, selected, readonly})}`
);
}
}
}
}
testCommands("empty");
// The cmd_removeList command needs a list.
node.innerHTML = "<ul><li><span>abcd</span></li></ul>";
testCommands("non-empty");
// Make some content modifications to enable undo and redo.
node.innerText = "ABC";
is(node.innerText.trim(), "ABC", "phase 1");
document.execCommand("selectAll");
synthesizeKey("KEY_Backspace");
is(node.innerText.trim(), "", "phase 2");
synthesizeKey("3");
is(node.innerText.trim(), "3", "phase 3");
SpecialPowers.doCommand(window, "cmd_undo");
is(node.innerText.trim(), "", "phase 4");
node.innerHTML = "";
testCommands("cleared");
</script>