Bug 1560032 - part 1: Make `TextEditor` for password allow to copy password when selected range is in unmasked range r=m_kato
authorMasayuki Nakano <masayuki@d-toybox.com>
Mon, 29 Jul 2019 06:21:14 +0000
changeset 485036 4a1e400e7077c7a9ea7b1d0a5b58c180af7ae835
parent 485035 85cbee6084e1a8c094b9a0afc2fb97fe091d04e7
child 485037 8b92e575e1c3b8e0d9b30686f9c539465fb1f7f8
push id36358
push userrgurzau@mozilla.com
push dateMon, 29 Jul 2019 09:50:23 +0000
treeherdermozilla-central@4274af431ede [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersm_kato
bugs1560032
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1560032 - part 1: Make `TextEditor` for password allow to copy password when selected range is in unmasked range r=m_kato It does not make sense to copy masked password with mask characters. Therefore, we should allow users to copy/cut in password fields only when selected range is in unmasked range. Note that for web-compat, copy/cut are always enabled in HTML/XHTML document in content. Therefore this patch changes the behavior only in chrome's password fields. Additionally, only the test uses `nsIEditor.canDelete()`. Therefore, this removes it and make the test use `nsIDocShell.isCommandEnabled()` instead. Unfortunately, `nsIEditor.canCopy()` and `nsIEditor.canCut()` are used by BlueGriffon, therefore, we cannot get rid of them for now. Differential Revision: https://phabricator.services.mozilla.com/D38999
editor/libeditor/EditorBase.cpp
editor/libeditor/EditorCommands.cpp
editor/libeditor/TextEditor.cpp
editor/libeditor/TextEditor.h
editor/libeditor/tests/chrome.ini
editor/libeditor/tests/mochitest.ini
editor/libeditor/tests/test_bug1067255.html
editor/libeditor/tests/test_cut_copy_delete_command_enabled.html
editor/libeditor/tests/test_cut_copy_delete_command_enabled.xul
editor/nsIEditor.idl
testing/specialpowers/content/SpecialPowersAPI.jsm
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -1145,25 +1145,16 @@ EditorBase::CanCopy(bool* aCanCopy) {
   if (NS_WARN_IF(!aCanCopy)) {
     return NS_ERROR_INVALID_ARG;
   }
   *aCanCopy = AsTextEditor()->CanCopy();
   return NS_OK;
 }
 
 NS_IMETHODIMP
-EditorBase::CanDelete(bool* aCanDelete) {
-  if (NS_WARN_IF(!aCanDelete)) {
-    return NS_ERROR_INVALID_ARG;
-  }
-  *aCanDelete = AsTextEditor()->CanDelete();
-  return NS_OK;
-}
-
-NS_IMETHODIMP
 EditorBase::Paste(int32_t aClipboardType) {
   nsresult rv =
       MOZ_KnownLive(AsTextEditor())->PasteAsAction(aClipboardType, true);
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to do Paste");
   return rv;
 }
 
 NS_IMETHODIMP
--- a/editor/libeditor/EditorCommands.cpp
+++ b/editor/libeditor/EditorCommands.cpp
@@ -569,21 +569,21 @@ StaticRefPtr<DeleteCommand> DeleteComman
 
 bool DeleteCommand::IsCommandEnabled(Command aCommand,
                                      TextEditor* aTextEditor) const {
   if (!aTextEditor) {
     return false;
   }
   // 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, which is the same condition under which we can't cut.
+  // directionless.
   bool isEnabled = aTextEditor->IsSelectionEditable();
 
   if (aCommand == Command::Delete && isEnabled) {
-    return aTextEditor->CanDelete();
+    return aTextEditor->CanDeleteSelection();
   }
   return isEnabled;
 }
 
 nsresult DeleteCommand::DoCommand(Command aCommand, TextEditor& aTextEditor,
                                   nsIPrincipal* aPrincipal) const {
   nsIEditor::EDirection deleteDir = nsIEditor::eNone;
   switch (aCommand) {
--- a/editor/libeditor/TextEditor.cpp
+++ b/editor/libeditor/TextEditor.cpp
@@ -1727,25 +1727,43 @@ nsresult TextEditor::RedoAsAction(uint32
 
   NotifyEditorObservers(eNotifyEditorObserversOfEnd);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return EditorBase::ToGenericNSResult(rv);
   }
   return NS_OK;
 }
 
-bool TextEditor::CanCutOrCopy(
-    PasswordFieldAllowed aPasswordFieldAllowed) const {
+bool TextEditor::CanCutOrCopy() const {
   MOZ_ASSERT(IsEditActionDataAvailable());
-
-  if (aPasswordFieldAllowed == ePasswordFieldNotAllowed && IsPasswordEditor()) {
+  if (SelectionRefPtr()->IsCollapsed()) {
     return false;
   }
 
-  return !SelectionRefPtr()->IsCollapsed();
+  if (!IsSingleLineEditor() || !IsPasswordEditor()) {
+    return true;
+  }
+
+  // If we're a password editor, we should allow selected text to be copied
+  // to the clipboard only when selection range is in unmasked range.
+  if (IsAllMasked() || IsMaskingPassword() || mUnmaskedLength == 0) {
+    return false;
+  }
+
+  // If there are 2 or more ranges, we don't allow to copy/cut for now since
+  // we need to check whether all ranges are in unmasked range or not.
+  // Anyway, such operation in password field does not make sense.
+  if (SelectionRefPtr()->RangeCount() > 1) {
+    return false;
+  }
+
+  uint32_t selectionStart = 0, selectionEnd = 0;
+  nsContentUtils::GetSelectionInTextControl(SelectionRefPtr(), mRootElement,
+                                            selectionStart, selectionEnd);
+  return mUnmaskedStart <= selectionStart && UnmaskedEnd() >= selectionEnd;
 }
 
 bool TextEditor::FireClipboardEvent(EventMessage aEventMessage,
                                     int32_t aSelectionType,
                                     bool* aActionTaken) {
   MOZ_ASSERT(IsEditActionDataAvailable());
 
   if (aEventMessage == ePaste) {
@@ -1795,17 +1813,17 @@ bool TextEditor::CanCut() const {
   // Cut is always enabled in HTML documents, but if the document is chrome,
   // let it control it.
   Document* document = GetDocument();
   if (document && document->IsHTMLOrXHTML() &&
       !nsContentUtils::IsChromeDoc(document)) {
     return true;
   }
 
-  return IsModifiable() && CanCutOrCopy(ePasswordFieldNotAllowed);
+  return IsModifiable() && CanCutOrCopy();
 }
 
 NS_IMETHODIMP
 TextEditor::Copy() {
   AutoEditActionDataSetter editActionData(*this, EditAction::eCopy);
   if (NS_WARN_IF(!editActionData.CanHandle())) {
     return NS_ERROR_NOT_INITIALIZED;
   }
@@ -1826,26 +1844,26 @@ bool TextEditor::CanCopy() const {
   // Copy is always enabled in HTML documents, but if the document is chrome,
   // let it control it.
   Document* document = GetDocument();
   if (document && document->IsHTMLOrXHTML() &&
       !nsContentUtils::IsChromeDoc(document)) {
     return true;
   }
 
-  return CanCutOrCopy(ePasswordFieldNotAllowed);
+  return CanCutOrCopy();
 }
 
-bool TextEditor::CanDelete() const {
+bool TextEditor::CanDeleteSelection() const {
   AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
   if (NS_WARN_IF(!editActionData.CanHandle())) {
     return false;
   }
 
-  return IsModifiable() && CanCutOrCopy(ePasswordFieldAllowed);
+  return IsModifiable() && !SelectionRefPtr()->IsCollapsed();
 }
 
 already_AddRefed<nsIDocumentEncoder> TextEditor::GetAndInitDocEncoder(
     const nsAString& aFormatType, uint32_t aDocumentEncoderFlags,
     const nsACString& aCharset) const {
   MOZ_ASSERT(IsEditActionDataAvailable());
 
   nsCOMPtr<nsIDocumentEncoder> docEncoder;
--- a/editor/libeditor/TextEditor.h
+++ b/editor/libeditor/TextEditor.h
@@ -89,27 +89,48 @@ class TextEditor : public EditorBase,
    * Do "cut".
    *
    * @param aPrincipal          If you know current context is subject
    *                            principal or system principal, set it.
    *                            When nullptr, this checks it automatically.
    */
   MOZ_CAN_RUN_SCRIPT nsresult CutAsAction(nsIPrincipal* aPrincipal = nullptr);
 
+  /**
+   * CanCut() always returns true if we're in non-chrome HTML/XHTML document.
+   * Otherwise, returns true when:
+   * - `Selection` is not collapsed and we're not a password editor.
+   * - `Selection` is not collapsed and we're a password editor but selection
+   *   range in unmasked range.
+   */
   bool CanCut() const;
+
   NS_IMETHOD Copy() override;
+
+  /**
+   * CanCopy() always returns true if we're in non-chrome HTML/XHTML document.
+   * Otherwise, returns true when:
+   * - `Selection` is not collapsed and we're not a password editor.
+   * - `Selection` is not collapsed and we're a password editor but selection
+   *   range in unmasked range.
+   */
   bool CanCopy() const;
-  bool CanDelete() const;
+
+  /**
+   * CanDeleteSelection() returns true if `Selection` is not collapsed and
+   * it's allowed to be removed.
+   */
+  bool CanDeleteSelection() const;
+
   virtual bool CanPaste(int32_t aClipboardType) const;
 
   // Shouldn't be used internally, but we need these using declarations for
   // avoiding warnings of clang.
   using EditorBase::CanCopy;
   using EditorBase::CanCut;
-  using EditorBase::CanDelete;
   using EditorBase::CanPaste;
 
   /**
    * Paste aTransferable at Selection.
    *
    * @param aTransferable       Must not be nullptr.
    * @param aPrincipal          Set subject principal if it may be called by
    *                            JS.  If set to nullptr, will be treated as
@@ -687,18 +708,22 @@ class TextEditor : public EditorBase,
 
   /**
    * Shared outputstring; returns whether selection is collapsed and resulting
    * string.
    */
   nsresult SharedOutputString(uint32_t aFlags, bool* aIsCollapsed,
                               nsAString& aResult);
 
-  enum PasswordFieldAllowed { ePasswordFieldAllowed, ePasswordFieldNotAllowed };
-  bool CanCutOrCopy(PasswordFieldAllowed aPasswordFieldAllowed) const;
+  /**
+   * CanCutOrCopy() returns true if "cut" or "copy" command is available
+   * right now.
+   */
+  bool CanCutOrCopy() const;
+
   bool FireClipboardEvent(EventMessage aEventMessage, int32_t aSelectionType,
                           bool* aActionTaken = nullptr);
 
   MOZ_CAN_RUN_SCRIPT bool UpdateMetaCharset(Document& aDocument,
                                             const nsACString& aCharacterSet);
 
   /**
    * EnsureComposition() should be called by composition event handlers.  This
--- a/editor/libeditor/tests/chrome.ini
+++ b/editor/libeditor/tests/chrome.ini
@@ -5,12 +5,13 @@ support-files = green.png
 [test_bug489202.xul]
 [test_bug599983.xul]
 [test_bug607584.xul]
 [test_bug616590.xul]
 [test_bug780908.xul]
 [test_bug1386222.xul]
 [test_bug1397412.xul]
 [test_contenteditable_text_input_handling.html]
+[test_cut_copy_delete_command_enabled.xul]
 [test_htmleditor_keyevent_handling.html]
 [test_texteditor_keyevent_handling.html]
 skip-if = (debug && os=='win') || (os == 'linux') # Bug 1116205, leaks on windows debug, fails delete key on linux
 [test_pasteImgTextarea.xul]
--- a/editor/libeditor/tests/mochitest.ini
+++ b/editor/libeditor/tests/mochitest.ini
@@ -198,17 +198,16 @@ skip-if = toolkit == 'android' && e10s
 skip-if = toolkit == 'android'
 [test_bug966155.html]
 skip-if = os != "win"
 [test_bug966552.html]
 skip-if = os != "win"
 [test_bug998188.html]
 [test_bug1026397.html]
 [test_bug1053048.html]
-[test_bug1067255.html]
 [test_bug1068979.html]
 tags = clipboard
 [test_bug1094000.html]
 [test_bug1100966.html]
 skip-if = os == 'android'
 [test_bug1102906.html]
 skip-if = os == 'android'
 [test_bug1109465.html]
@@ -266,16 +265,17 @@ skip-if = toolkit == 'android'
 [test_abs_positioner_appearance.html]
 [test_abs_positioner_positioning_elements.html]
 skip-if = os == 'android' # Bug 1525959
 [test_CF_HTML_clipboard.html]
 tags = clipboard
 skip-if = os != 'mac' # bug 574005
 [test_composition_event_created_in_chrome.html]
 [test_contenteditable_focus.html]
+[test_cut_copy_delete_command_enabled.html]
 [test_documentCharacterSet.html]
 [test_dom_input_event_on_htmleditor.html]
 [test_dom_input_event_on_texteditor.html]
 [test_dragdrop.html]
 skip-if = os == 'android'
 [test_handle_new_lines.html]
 tags = clipboard
 skip-if = android_version == '24'
rename from editor/libeditor/tests/test_bug1067255.html
rename to editor/libeditor/tests/test_cut_copy_delete_command_enabled.html
--- a/editor/libeditor/tests/test_bug1067255.html
+++ b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.html
@@ -3,17 +3,17 @@
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=1067255
 -->
 
 <head>
-  <title>Test for Bug 1067255</title>
+  <title>Test for enabled state of cut/copy/delete commands</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <script src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 
 <body onload="doTest();">
   <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1067255">Mozilla Bug 1067255</a>
 
@@ -27,29 +27,43 @@ https://bugzilla.mozilla.org/show_bug.cg
         var password = $("password-field");
 
         var editor1 = SpecialPowers.wrap(text).editor;
         var editor2 = SpecialPowers.wrap(password).editor;
 
         text.focus();
         text.select();
 
-        ok(editor1.canCopy(), "can copy, text");
-        ok(editor1.canCut(), "can cut, text");
-        ok(editor1.canDelete(), "can delete, text");
+        ok(editor1.canCopy(),
+           "nsIEditor.canCopy() should return true in <input type=text>");
+        ok(editor1.canCut(),
+           "nsIEditor.canCut() should return true in <input type=text>");
+        ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+           "cmd_copy command should be enabled in <input type=text>");
+        ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+           "cmd_cut command should be enabled in <input type=text>");
+        ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+           "cmd_delete command should be enabled in <input type=text>");
 
         password.focus();
         password.select();
 
-        // Copy and cut commands don't do anything on passoword fields by default,
+        // Copy and cut commands don't do anything on password fields by default,
         // but webpages can hook up event handlers to the event, and thus, we have to
         // always keep the cut and copy event enabled in HTML/XHTML documents.
-        ok(editor2.canCopy(), "can copy, password");
-        ok(editor2.canCut(), "can cut, password");
-        ok(editor2.canDelete(), "can delete, password");
+        ok(editor2.canCopy(),
+           "nsIEditor.canCopy() should return true in <input type=password>");
+        ok(editor2.canCut(),
+           "nsIEditor.canCut() should return true in <input type=password>");
+        ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+           "cmd_copy command should be enabled in <input type=password>");
+        ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+           "cmd_cut command should be enabled in <input type=password>");
+        ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+           "cmd_delete command should be enabled in <input type=password>");
 
         SimpleTest.finish();
       }
    </script>
   </pre>
 
   <input type="text" value="Gonzo says hi" id="text-field" />
   <input type="password" value="Jan also" id="password-field" />
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.xul
@@ -0,0 +1,215 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="Test for enabled state of cut/copy/delete commands">
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+
+  <script type="application/javascript">
+  <![CDATA[
+    SimpleTest.waitForExplicitFinish();
+    SimpleTest.waitForFocus(() => {
+      let text = document.getElementById("textbox");
+      let password = document.getElementById("password");
+
+      let editor1 = text.editor;
+      let editor2 = password.editor;
+
+      text.focus();
+      text.select();
+
+      ok(editor1.canCopy(),
+         "nsIEditor.canCopy() should return true in <textbox>");
+      ok(editor1.canCut(),
+         "nsIEditor.canCut() should return true in <textbox>");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be enabled in <textbox>");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be enabled in <textbox>");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox>");
+
+      password.focus();
+      password.select();
+
+      // Copy and cut commands should be disabled on password fields.
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password>");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password>");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password>");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password>");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password>");
+
+      // If selection is in unmasked range, allow to copy the selected
+      // password into the clipboard.
+      editor2.unmask(0);
+      ok(editor2.canCopy(),
+         "nsIEditor.canCopy() should return true in <textbox type=password> if the password is unmasked");
+      ok(editor2.canCut(),
+         "nsIEditor.canCut() should return true in <textbox type=password> if the password is unmasked");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be enabled in <textbox type=password> if the password is unmasked");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be enabled in <textbox type=password> if the password is unmasked");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> if the password is unmasked");
+
+      // If unmasked range will be masked automatically, we shouldn't allow to
+      // copy the selected password since the state may be changed during
+      // showing edit menu or something.
+      editor2.unmask(0, 13, 1000);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> if the password is unmasked but will be masked automatically");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> if the password is unmasked but will be masked automatically");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> if the password is unmasked but will be masked automatically");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> if the password is unmasked but will be masked automatically");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> if the password is unmasked but will be masked automatically");
+
+      // <textbox type="password"> does not support setSelectionRange() oddly.
+      function setSelectionRange(aEditor, aStart, aEnd) {
+        let container = aEditor.rootElement.firstChild;
+        aEditor.selection.setBaseAndExtent(container, aStart, container, aEnd);
+      }
+
+      // Check the range boundaries.
+      editor2.unmask(3, 9);
+      setSelectionRange(editor2, 0, 2);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> (unmasked range 3-9, selected range 0-2)");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> (unmasked range 3-9, selected range 0-2)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 0-2)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 0-2)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 0-2)");
+
+      setSelectionRange(editor2, 2, 3);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> (unmasked range 3-9, selected range 2-3)");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> (unmasked range 3-9, selected range 2-3)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 2-3)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 2-3)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 2-3)");
+
+      setSelectionRange(editor2, 2, 5);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> (unmasked range 3-9, selected range 2-5)");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> (unmasked range 3-9, selected range 2-5)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 2-5)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 2-5)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 2-5)");
+
+      setSelectionRange(editor2, 2, 10);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> (unmasked range 3-9, selected range 2-10)");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> (unmasked range 3-9, selected range 2-10)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 2-10)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 2-10)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 2-10)");
+
+      setSelectionRange(editor2, 2, 10);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> (unmasked range 3-9, selected range 3-10)");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> (unmasked range 3-9, selected range 3-10)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 3-10)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 3-10)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 3-10)");
+
+      setSelectionRange(editor2, 8, 12);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> (unmasked range 3-9, selected range 8-12)");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> (unmasked range 3-9, selected range 8-12)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 8-12)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 8-12)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 8-12)");
+
+      setSelectionRange(editor2, 9, 12);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> (unmasked range 3-9, selected range 9-12)");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> (unmasked range 3-9, selected range 9-12)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 9-12)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 9-12)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 9-12)");
+
+      setSelectionRange(editor2, 10, 12);
+      ok(!editor2.canCopy(),
+         "nsIEditor.canCopy() should return false in <textbox type=password> (unmasked range 3-9, selected range 10-12)");
+      ok(!editor2.canCut(),
+         "nsIEditor.canCut() should return false in <textbox type=password> (unmasked range 3-9, selected range 10-12)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 10-12)");
+      ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be disabled in <textbox type=password> (unmasked range 3-9, selected range 10-12)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 10-12)");
+
+      setSelectionRange(editor2, 3, 9);
+      ok(editor2.canCopy(),
+         "nsIEditor.canCopy() should return true in <textbox type=password> (unmasked range 3-9, selected range 3-9)");
+      ok(editor2.canCut(),
+         "nsIEditor.canCut() should return true in <textbox type=password> (unmasked range 3-9, selected range 3-9)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 3-9)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 3-9)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 3-9)");
+
+      setSelectionRange(editor2, 4, 8);
+      ok(editor2.canCopy(),
+         "nsIEditor.canCopy() should return true in <textbox type=password> (unmasked range 3-9, selected range 4-8)");
+      ok(editor2.canCut(),
+         "nsIEditor.canCut() should return true in <textbox type=password> (unmasked range 3-9, selected range 4-8)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+         "cmd_copy command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 4-8)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+         "cmd_cut command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 4-8)");
+      ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+         "cmd_delete command should be enabled in <textbox type=password> (unmasked range 3-9, selected range 4-8)");
+
+      SimpleTest.finish();
+    });
+  ]]></script>
+
+  <vbox flex="1">
+    <textbox id="textbox" value="normal text"/>
+    <textbox id="password" type="password" value="password text"/>
+  </vbox>
+
+</window>
\ No newline at end of file
--- a/editor/nsIEditor.idl
+++ b/editor/nsIEditor.idl
@@ -269,36 +269,41 @@ interface nsIEditor  : nsISupports
   /** cut the currently selected text, putting it into the OS clipboard
     * What if no text is selected?
     * What about mixed selections?
     * What are the clipboard formats?
     */
   [can_run_script]
   void cut();
 
-  /** Can we cut? True if the doc is modifiable, and we have a non-
-    * collapsed selection.
-    */
+  /**
+   * canCut() returns true if selected content is allowed to be copied to the
+   * clipboard and to be removed.
+   * Note that this always returns true if the editor is in a non-chrome
+   * HTML/XHTML document.
+   * FYI: Current user in script is only BlueGriffon.
+   */
   boolean canCut();
 
   /** copy the currently selected text, putting it into the OS clipboard
     * What if no text is selected?
     * What about mixed selections?
     * What are the clipboard formats?
     */
   void copy();
 
-  /** Can we copy? True if we have a non-collapsed selection.
-    */
+  /**
+   * canCopy() returns true if selected content is allowed to be copied to
+   * the clipboard.
+   * Note that this always returns true if the editor is in a non-chrome
+   * HTML/XHTML document.
+   * FYI: Current user in script is only BlueGriffon.
+   */
   boolean canCopy();
 
-  /** Can we delete? True if we have a non-collapsed selection.
-    */
-  boolean canDelete();
-
   /** paste the text in the OS clipboard at the cursor position, replacing
     * the selected text (if any)
     */
   [can_run_script]
   void paste(in long aClipboardType);
 
   /** Paste the text in |aTransferable| at the cursor position, replacing the
     * selected text (if any).
--- a/testing/specialpowers/content/SpecialPowersAPI.jsm
+++ b/testing/specialpowers/content/SpecialPowersAPI.jsm
@@ -1884,16 +1884,20 @@ class SpecialPowersAPI extends JSWindowA
       subtree,
     });
   }
 
   doCommand(window, cmd) {
     return window.docShell.doCommand(cmd);
   }
 
+  isCommandEnabled(window, cmd) {
+    return window.docShell.isCommandEnabled(cmd);
+  }
+
   setCommandNode(window, node) {
     return window.docShell.contentViewer
       .QueryInterface(Ci.nsIContentViewerEdit)
       .setCommandNode(node);
   }
 
   /* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by
    * SystemGroup, but some test cases may run as web content. That would assert