Bug 1560032 - part 2: Make cut/copy in password field available r=m_kato,smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Mon, 29 Jul 2019 06:21:42 +0000
changeset 485054 8b92e575e1c3b8e0d9b30686f9c539465fb1f7f8
parent 485053 4a1e400e7077c7a9ea7b1d0a5b58c180af7ae835
child 485055 584b03dfe25e20b4d5ec72ee82f11ec84ddfcb2a
push id91081
push usermasayuki@d-toybox.com
push dateMon, 29 Jul 2019 06:24:11 +0000
treeherderautoland@584b03dfe25e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersm_kato, smaug
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 2: Make cut/copy in password field available r=m_kato,smaug First, we need to make `nsCopySupport::FireClipboardEvent()` keep handling `eCopy` and `eCut` event even in password field, only if `TextEditor` allows them. Then, we need to make `nsPlainTextSerializer::AppendText()` not expose masked password for making users safer. Although `TextEditor` does not allow `eCopy` nor `eCut` when selection is not in unmasked range. Fortunately, retrieving masked and unmasked password from `nsTextFragment` has already been implemented in `ContentEventHandler.cpp`. This patch moves it into `EditorUtils` and makes `ContentEventHandler.cpp` and `nsPlaintextSerializer` share it. Differential Revision: https://phabricator.services.mozilla.com/D39000
dom/base/nsCopySupport.cpp
dom/base/nsPlainTextSerializer.cpp
dom/events/ContentEventHandler.cpp
editor/libeditor/EditorBase.cpp
editor/libeditor/EditorCommands.cpp
editor/libeditor/EditorUtils.cpp
editor/libeditor/EditorUtils.h
editor/libeditor/TextEditor.cpp
editor/libeditor/TextEditor.h
editor/libeditor/tests/mochitest.ini
editor/libeditor/tests/test_cut_copy_password.html
--- a/dom/base/nsCopySupport.cpp
+++ b/dom/base/nsCopySupport.cpp
@@ -48,22 +48,24 @@
 #  include "nsIMIMEInfo.h"
 #  include "nsIMIMEService.h"
 #  include "nsIURL.h"
 #  include "nsReadableUtils.h"
 #  include "nsXULAppAPI.h"
 #endif
 
 #include "mozilla/ContentEvents.h"
-#include "mozilla/dom/Element.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/PresShell.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/Selection.h"
-#include "mozilla/IntegerRange.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 static NS_DEFINE_CID(kCClipboardCID, NS_CLIPBOARD_CID);
 static NS_DEFINE_CID(kCTransferableCID, NS_TRANSFERABLE_CID);
 static NS_DEFINE_CID(kHTMLConverterCID, NS_HTMLFORMATCONVERTER_CID);
 
@@ -873,21 +875,27 @@ bool nsCopySupport::FireClipboardEvent(E
   uint32_t count = 0;
   if (doDefault) {
     // find the focused node
     nsIContent* sourceContent = targetElement.get();
     if (targetElement->IsInNativeAnonymousSubtree()) {
       sourceContent = targetElement->FindFirstNonChromeOnlyAccessContent();
     }
 
-    // check if we are looking at a password input
-    nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(sourceContent);
-    if (formControl) {
-      if (formControl->ControlType() == NS_FORM_INPUT_PASSWORD) {
-        return false;
+    // If it's <input type="password"> and there is no unmasked range or
+    // there is unmasked range but it's collapsed or it'll be masked
+    // automatically, the selected password shouldn't be copied into the
+    // clipboard.
+    if (HTMLInputElement* inputElement =
+            HTMLInputElement::FromNodeOrNull(sourceContent)) {
+      if (TextEditor* textEditor = inputElement->GetTextEditor()) {
+        if (textEditor->IsPasswordEditor() &&
+            !textEditor->IsCopyToClipboardAllowed()) {
+          return false;
+        }
       }
     }
 
     // when cutting non-editable content, do nothing
     // XXX this is probably the wrong editable flag to check
     if (originalEventMessage != eCut || targetElement->IsEditable()) {
       // get the data from the selection if any
       if (sel->IsCollapsed()) {
--- a/dom/base/nsPlainTextSerializer.cpp
+++ b/dom/base/nsPlainTextSerializer.cpp
@@ -14,17 +14,20 @@
 #include "nsIServiceManager.h"
 #include "nsGkAtoms.h"
 #include "nsNameSpaceManager.h"
 #include "nsTextFragment.h"
 #include "nsContentUtils.h"
 #include "nsReadableUtils.h"
 #include "nsUnicharUtils.h"
 #include "nsCRT.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/dom/CharacterData.h"
 #include "mozilla/dom/Element.h"
+#include "mozilla/dom/Text.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/BinarySearch.h"
 #include "nsComputedDOMStyle.h"
 
 namespace mozilla {
 class Encoding;
 }
 
@@ -310,16 +313,21 @@ nsPlainTextSerializer::AppendText(nsICon
   if (frag->Is2b()) {
     textstr.Assign(frag->Get2b() + aStartOffset, length);
   } else {
     // AssignASCII is for 7-bit character only, so don't use it
     const char* data = frag->Get1b();
     CopyASCIItoUTF16(Substring(data + aStartOffset, data + endoffset), textstr);
   }
 
+  // Mask the text if the text node is in a password field.
+  if (content->HasFlag(NS_MAYBE_MASKED)) {
+    EditorUtils::MaskString(textstr, content->AsText(), 0, aStartOffset);
+  }
+
   mOutputString = &aStr;
 
   // We have to split the string across newlines
   // to match parser behavior
   int32_t start = 0;
   int32_t offset = textstr.FindCharInSet("\n\r");
   while (offset != kNotFound) {
     if (offset > start) {
--- a/dom/events/ContentEventHandler.cpp
+++ b/dom/events/ContentEventHandler.cpp
@@ -2,21 +2,21 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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/. */
 
 #include "ContentEventHandler.h"
 
 #include "mozilla/ContentIterator.h"
+#include "mozilla/EditorUtils.h"
 #include "mozilla/IMEStateManager.h"
 #include "mozilla/PresShell.h"
 #include "mozilla/RangeUtils.h"
 #include "mozilla/TextComposition.h"
-#include "mozilla/TextEditor.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/HTMLUnknownElement.h"
 #include "mozilla/dom/Selection.h"
 #include "mozilla/dom/Text.h"
 #include "nsCaret.h"
 #include "nsCOMPtr.h"
 #include "nsContentUtils.h"
@@ -501,91 +501,31 @@ static bool IsMozBR(nsIContent* aContent
 }
 
 static void ConvertToNativeNewlines(nsString& aString) {
 #if defined(XP_WIN)
   aString.ReplaceSubstring(NS_LITERAL_STRING("\n"), NS_LITERAL_STRING("\r\n"));
 #endif
 }
 
-// Helper method for `AppendString()` and `AppendSubString()`.  This should
-// be called only when `aText` is in a password field.  This method masks
-// A part of or all of `aText` (`aStartOffsetInText` and later) should've
-// been copied (apppended) to `aString`.  `aStartOffsetInString` is where
-// the password was appended into `aString`.
-static void MaskString(nsString& aString, Text* aText,
-                       uint32_t aStartOffsetInString,
-                       uint32_t aStartOffsetInText) {
-  MOZ_ASSERT(aText->HasFlag(NS_MAYBE_MASKED));
-  MOZ_ASSERT(aStartOffsetInString == 0 || aStartOffsetInText == 0);
-
-  uint32_t unmaskStart = UINT32_MAX, unmaskLength = 0;
-  TextEditor* textEditor =
-      nsContentUtils::GetTextEditorFromAnonymousNodeWithoutCreation(aText);
-  if (textEditor && textEditor->UnmaskedLength() > 0) {
-    unmaskStart = textEditor->UnmaskedStart();
-    unmaskLength = textEditor->UnmaskedLength();
-    // If text is copied from after unmasked range, we can treat this case
-    // as mask all.
-    if (aStartOffsetInText >= unmaskStart + unmaskLength) {
-      unmaskLength = 0;
-      unmaskStart = UINT32_MAX;
-    } else {
-      // If text is copied from middle of unmasked range, reduce the length
-      // and adjust start offset.
-      if (aStartOffsetInText > unmaskStart) {
-        unmaskLength = unmaskStart + unmaskLength - aStartOffsetInText;
-        unmaskStart = 0;
-      }
-      // If text is copied from before start of unmasked range, just adjust
-      // the start offset.
-      else {
-        unmaskStart -= aStartOffsetInText;
-      }
-      // Make the range is in the string.
-      unmaskStart += aStartOffsetInString;
-    }
-  }
-
-  const char16_t kPasswordMask = TextEditor::PasswordMask();
-  for (uint32_t i = aStartOffsetInString; i < aString.Length(); ++i) {
-    bool isSurrogatePair = NS_IS_HIGH_SURROGATE(aString.CharAt(i)) &&
-                           i < aString.Length() - 1 &&
-                           NS_IS_LOW_SURROGATE(aString.CharAt(i + 1));
-    if (i < unmaskStart || i >= unmaskStart + unmaskLength) {
-      if (isSurrogatePair) {
-        aString.SetCharAt(kPasswordMask, i);
-        aString.SetCharAt(kPasswordMask, i + 1);
-      } else {
-        aString.SetCharAt(kPasswordMask, i);
-      }
-    }
-
-    // Skip the following low surrogate.
-    if (isSurrogatePair) {
-      ++i;
-    }
-  }
-}
-
 static void AppendString(nsString& aString, Text* aText) {
   uint32_t oldXPLength = aString.Length();
   aText->TextFragment().AppendTo(aString);
   if (aText->HasFlag(NS_MAYBE_MASKED)) {
-    MaskString(aString, aText, oldXPLength, 0);
+    EditorUtils::MaskString(aString, aText, oldXPLength, 0);
   }
 }
 
 static void AppendSubString(nsString& aString, Text* aText, uint32_t aXPOffset,
                             uint32_t aXPLength) {
   uint32_t oldXPLength = aString.Length();
   aText->TextFragment().AppendTo(aString, static_cast<int32_t>(aXPOffset),
                                  static_cast<int32_t>(aXPLength));
   if (aText->HasFlag(NS_MAYBE_MASKED)) {
-    MaskString(aString, aText, oldXPLength, aXPOffset);
+    EditorUtils::MaskString(aString, aText, oldXPLength, aXPOffset);
   }
 }
 
 #if defined(XP_WIN)
 static uint32_t CountNewlinesInXPLength(Text* aText, uint32_t aXPLength) {
   const nsTextFragment* text = &aText->TextFragment();
   // For automated tests, we should abort on debug build.
   MOZ_ASSERT(aXPLength == UINT32_MAX || aXPLength <= text->GetLength(),
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -1128,29 +1128,29 @@ EditorBase::Cut() {
   return rv;
 }
 
 NS_IMETHODIMP
 EditorBase::CanCut(bool* aCanCut) {
   if (NS_WARN_IF(!aCanCut)) {
     return NS_ERROR_INVALID_ARG;
   }
-  *aCanCut = AsTextEditor()->CanCut();
+  *aCanCut = AsTextEditor()->IsCutCommandEnabled();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 EditorBase::Copy() { return NS_ERROR_NOT_IMPLEMENTED; }
 
 NS_IMETHODIMP
 EditorBase::CanCopy(bool* aCanCopy) {
   if (NS_WARN_IF(!aCanCopy)) {
     return NS_ERROR_INVALID_ARG;
   }
-  *aCanCopy = AsTextEditor()->CanCopy();
+  *aCanCopy = AsTextEditor()->IsCopyCommandEnabled();
   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");
--- a/editor/libeditor/EditorCommands.cpp
+++ b/editor/libeditor/EditorCommands.cpp
@@ -322,17 +322,18 @@ nsresult RedoCommand::GetCommandStatePar
 
 StaticRefPtr<CutCommand> CutCommand::sInstance;
 
 bool CutCommand::IsCommandEnabled(Command aCommand,
                                   TextEditor* aTextEditor) const {
   if (!aTextEditor) {
     return false;
   }
-  return aTextEditor->IsSelectionEditable() && aTextEditor->CanCut();
+  return aTextEditor->IsSelectionEditable() &&
+         aTextEditor->IsCutCommandEnabled();
 }
 
 nsresult CutCommand::DoCommand(Command aCommand, TextEditor& aTextEditor,
                                nsIPrincipal* aPrincipal) const {
   nsresult rv = aTextEditor.CutAsAction(aPrincipal);
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "CutAsAction() failed");
   return rv;
 }
@@ -386,17 +387,17 @@ nsresult CutOrDeleteCommand::GetCommandS
 
 StaticRefPtr<CopyCommand> CopyCommand::sInstance;
 
 bool CopyCommand::IsCommandEnabled(Command aCommand,
                                    TextEditor* aTextEditor) const {
   if (!aTextEditor) {
     return false;
   }
-  return aTextEditor->CanCopy();
+  return aTextEditor->IsCopyCommandEnabled();
 }
 
 nsresult CopyCommand::DoCommand(Command aCommand, TextEditor& aTextEditor,
                                 nsIPrincipal* aPrincipal) const {
   // Shouldn't cause "beforeinput" event so that we don't need to specify
   // the given principal.
   return aTextEditor.Copy();
 }
--- a/editor/libeditor/EditorUtils.cpp
+++ b/editor/libeditor/EditorUtils.cpp
@@ -3,17 +3,20 @@
  * 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/. */
 
 #include "mozilla/EditorUtils.h"
 
 #include "mozilla/ContentIterator.h"
 #include "mozilla/EditorDOMPoint.h"
 #include "mozilla/OwningNonNull.h"
+#include "mozilla/TextEditor.h"
 #include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Text.h"
+#include "nsContentUtils.h"
 #include "nsComponentManagerUtils.h"
 #include "nsError.h"
 #include "nsIContent.h"
 #include "nsIDocShell.h"
 #include "mozilla/dom/Document.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsINode.h"
 
@@ -106,9 +109,65 @@ bool EditorUtils::IsDescendantOf(const n
       aOutPoint->Set(node->AsContent());
       return true;
     }
   }
 
   return false;
 }
 
+// static
+void EditorUtils::MaskString(nsString& aString, Text* aText,
+                             uint32_t aStartOffsetInString,
+                             uint32_t aStartOffsetInText) {
+  MOZ_ASSERT(aText->HasFlag(NS_MAYBE_MASKED));
+  MOZ_ASSERT(aStartOffsetInString == 0 || aStartOffsetInText == 0);
+
+  uint32_t unmaskStart = UINT32_MAX, unmaskLength = 0;
+  TextEditor* textEditor =
+      nsContentUtils::GetTextEditorFromAnonymousNodeWithoutCreation(aText);
+  if (textEditor && textEditor->UnmaskedLength() > 0) {
+    unmaskStart = textEditor->UnmaskedStart();
+    unmaskLength = textEditor->UnmaskedLength();
+    // If text is copied from after unmasked range, we can treat this case
+    // as mask all.
+    if (aStartOffsetInText >= unmaskStart + unmaskLength) {
+      unmaskLength = 0;
+      unmaskStart = UINT32_MAX;
+    } else {
+      // If text is copied from middle of unmasked range, reduce the length
+      // and adjust start offset.
+      if (aStartOffsetInText > unmaskStart) {
+        unmaskLength = unmaskStart + unmaskLength - aStartOffsetInText;
+        unmaskStart = 0;
+      }
+      // If text is copied from before start of unmasked range, just adjust
+      // the start offset.
+      else {
+        unmaskStart -= aStartOffsetInText;
+      }
+      // Make the range is in the string.
+      unmaskStart += aStartOffsetInString;
+    }
+  }
+
+  const char16_t kPasswordMask = TextEditor::PasswordMask();
+  for (uint32_t i = aStartOffsetInString; i < aString.Length(); ++i) {
+    bool isSurrogatePair = NS_IS_HIGH_SURROGATE(aString.CharAt(i)) &&
+                           i < aString.Length() - 1 &&
+                           NS_IS_LOW_SURROGATE(aString.CharAt(i + 1));
+    if (i < unmaskStart || i >= unmaskStart + unmaskLength) {
+      if (isSurrogatePair) {
+        aString.SetCharAt(kPasswordMask, i);
+        aString.SetCharAt(kPasswordMask, i + 1);
+      } else {
+        aString.SetCharAt(kPasswordMask, i);
+      }
+    }
+
+    // Skip the following low surrogate.
+    if (isSurrogatePair) {
+      ++i;
+    }
+  }
+}
+
 }  // namespace mozilla
--- a/editor/libeditor/EditorUtils.h
+++ b/editor/libeditor/EditorUtils.h
@@ -492,13 +492,24 @@ class EditorUtils final {
    * aOutPoint is set to the child of aParent.
    *
    * @return            true if aNode is a child or a descendant of aParent.
    */
   static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
                              EditorRawDOMPoint* aOutPoint = nullptr);
   static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
                              EditorDOMPoint* aOutPoint);
+
+  /**
+   * Helper method for `AppendString()` and `AppendSubString()`.  This should
+   * be called only when `aText` is in a password field.  This method masks
+   * A part of or all of `aText` (`aStartOffsetInText` and later) should've
+   * been copied (apppended) to `aString`.  `aStartOffsetInString` is where
+   * the password was appended into `aString`.
+   */
+  static void MaskString(nsString& aString, dom::Text* aText,
+                         uint32_t aStartOffsetInString,
+                         uint32_t aStartOffsetInText);
 };
 
 }  // namespace mozilla
 
 #endif  // #ifndef mozilla_EditorUtils_h
--- a/editor/libeditor/TextEditor.cpp
+++ b/editor/libeditor/TextEditor.cpp
@@ -1727,17 +1727,17 @@ nsresult TextEditor::RedoAsAction(uint32
 
   NotifyEditorObservers(eNotifyEditorObserversOfEnd);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return EditorBase::ToGenericNSResult(rv);
   }
   return NS_OK;
 }
 
-bool TextEditor::CanCutOrCopy() const {
+bool TextEditor::IsCopyToClipboardAllowedInternal() const {
   MOZ_ASSERT(IsEditActionDataAvailable());
   if (SelectionRefPtr()->IsCollapsed()) {
     return false;
   }
 
   if (!IsSingleLineEditor() || !IsPasswordEditor()) {
     return true;
   }
@@ -1799,62 +1799,62 @@ nsresult TextEditor::CutAsAction(nsIPrin
     AutoPlaceholderBatch treatAsOneTransaction(*this,
                                                *nsGkAtoms::DeleteTxnName);
     DeleteSelectionAsSubAction(eNone, eStrip);
   }
   return EditorBase::ToGenericNSResult(
       actionTaken ? NS_OK : NS_ERROR_EDITOR_ACTION_CANCELED);
 }
 
-bool TextEditor::CanCut() const {
+bool TextEditor::IsCutCommandEnabled() const {
   AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
   if (NS_WARN_IF(!editActionData.CanHandle())) {
     return false;
   }
 
   // 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();
+  return IsModifiable() && IsCopyToClipboardAllowedInternal();
 }
 
 NS_IMETHODIMP
 TextEditor::Copy() {
   AutoEditActionDataSetter editActionData(*this, EditAction::eCopy);
   if (NS_WARN_IF(!editActionData.CanHandle())) {
     return NS_ERROR_NOT_INITIALIZED;
   }
 
   bool actionTaken = false;
   FireClipboardEvent(eCopy, nsIClipboard::kGlobalClipboard, &actionTaken);
 
   return EditorBase::ToGenericNSResult(
       actionTaken ? NS_OK : NS_ERROR_EDITOR_ACTION_CANCELED);
 }
 
-bool TextEditor::CanCopy() const {
+bool TextEditor::IsCopyCommandEnabled() const {
   AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
   if (NS_WARN_IF(!editActionData.CanHandle())) {
     return false;
   }
 
   // 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();
+  return IsCopyToClipboardAllowedInternal();
 }
 
 bool TextEditor::CanDeleteSelection() const {
   AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
   if (NS_WARN_IF(!editActionData.CanHandle())) {
     return false;
   }
 
--- a/editor/libeditor/TextEditor.h
+++ b/editor/libeditor/TextEditor.h
@@ -90,34 +90,45 @@ class TextEditor : public EditorBase,
    *
    * @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.
+   * IsCutCommandEnabled() returns whether cut command can be enabled or
+   * disabled.  This always returns true if we're in non-chrome HTML/XHTML
+   * document.  Otherwise, same as the result of `IsCopyToClipboardAllowed()`.
    */
-  bool CanCut() const;
+  bool IsCutCommandEnabled() const;
 
   NS_IMETHOD Copy() override;
 
   /**
-   * CanCopy() always returns true if we're in non-chrome HTML/XHTML document.
-   * Otherwise, returns true when:
+   * IsCopyCommandEnabled() returns copy command can be enabled or disabled.
+   * This always returns true if we're in non-chrome HTML/XHTML document.
+   * Otherwise, same as the result of `IsCopyToClipboardAllowed()`.
+   */
+  bool IsCopyCommandEnabled() const;
+
+  /**
+   * IsCopyToClipboardAllowed() returns true if the selected content can
+   * be copied into the clipboard.  This 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.
+   *   range is in unmasked range.
    */
-  bool CanCopy() const;
+  bool IsCopyToClipboardAllowed() const {
+    AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+    if (NS_WARN_IF(!editActionData.CanHandle())) {
+      return false;
+    }
+    return IsCopyToClipboardAllowedInternal();
+  }
 
   /**
    * 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;
@@ -709,20 +720,19 @@ class TextEditor : public EditorBase,
   /**
    * Shared outputstring; returns whether selection is collapsed and resulting
    * string.
    */
   nsresult SharedOutputString(uint32_t aFlags, bool* aIsCollapsed,
                               nsAString& aResult);
 
   /**
-   * CanCutOrCopy() returns true if "cut" or "copy" command is available
-   * right now.
+   * See comment of IsCopyToClipboardAllowed() for the detail.
    */
-  bool CanCutOrCopy() const;
+  bool IsCopyToClipboardAllowedInternal() const;
 
   bool FireClipboardEvent(EventMessage aEventMessage, int32_t aSelectionType,
                           bool* aActionTaken = nullptr);
 
   MOZ_CAN_RUN_SCRIPT bool UpdateMetaCharset(Document& aDocument,
                                             const nsACString& aCharacterSet);
 
   /**
--- a/editor/libeditor/tests/mochitest.ini
+++ b/editor/libeditor/tests/mochitest.ini
@@ -266,16 +266,18 @@ skip-if = toolkit == 'android'
 [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_cut_copy_password.html]
+tags = clipboard
 [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'
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/tests/test_cut_copy_password.html
@@ -0,0 +1,98 @@
+<!doctype html>
+<html>
+<head>
+  <title>Test for cut/copy in password field</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <input type="password">
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+  let input = document.getElementsByTagName("input")[0];
+  let editor = SpecialPowers.wrap(input).editor;
+  const kMask = editor.passwordMask;
+  function copyToClipboard(aExpectedValue) {
+    return new Promise(async resolve => {
+      try {
+        await SimpleTest.promiseClipboardChange(
+            aExpectedValue, () => { SpecialPowers.doCommand(window, "cmd_copy"); },
+            undefined, undefined, aExpectedValue === null);
+      } catch (e) {
+        console.error(e);
+      }
+      resolve();
+    });
+  }
+  function cutToClipboard(aExpectedValue) {
+    return new Promise(async resolve => {
+      try {
+        await SimpleTest.promiseClipboardChange(
+            aExpectedValue, () => { SpecialPowers.doCommand(window, "cmd_cut"); },
+            undefined, undefined, aExpectedValue === null);
+      } catch (e) {
+        console.error(e);
+      }
+      resolve();
+    });
+  }
+  input.value = "abcdef";
+  input.focus();
+
+  input.setSelectionRange(0, 6);
+  ok(true, "Trying to copy masked password...");
+  await copyToClipboard(null);
+  isnot(SpecialPowers.getClipboardData("text/unicode"), "abcdef",
+        "Copying masked password shouldn't copy raw value into the clipboard");
+  isnot(SpecialPowers.getClipboardData("text/unicode"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+        "Copying masked password shouldn't copy masked value into the clipboard");
+  ok(true, "Trying to cut masked password...");
+  await cutToClipboard(null);
+  isnot(SpecialPowers.getClipboardData("text/unicode"), "abcdef",
+        "Cutting masked password shouldn't copy raw value into the clipboard");
+  isnot(SpecialPowers.getClipboardData("text/unicode"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+        "Cutting masked password shouldn't copy masked value into the clipboard");
+  is(input.value, "abcdef",
+     "Cutting masked password shouldn't modify the value");
+
+  editor.unmask(2, 4);
+  input.setSelectionRange(0, 6);
+  ok(true, "Trying to copy partially masked password...");
+  await copyToClipboard(null);
+  isnot(SpecialPowers.getClipboardData("text/unicode"), "abcdef",
+        "Copying partially masked password shouldn't copy raw value into the clipboard");
+  isnot(SpecialPowers.getClipboardData("text/unicode"), `${kMask}${kMask}cd${kMask}${kMask}`,
+        "Copying partially masked password shouldn't copy partially masked value into the clipboard");
+  isnot(SpecialPowers.getClipboardData("text/unicode"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+        "Copying partially masked password shouldn't copy masked value into the clipboard");
+  ok(true, "Trying to cut partially masked password...");
+  await cutToClipboard(null);
+  isnot(SpecialPowers.getClipboardData("text/unicode"), "abcdef",
+        "Cutting partially masked password shouldn't copy raw value into the clipboard");
+  isnot(SpecialPowers.getClipboardData("text/unicode"), `${kMask}${kMask}cd${kMask}${kMask}`,
+        "Cutting partially masked password shouldn't copy partially masked value into the clipboard");
+  isnot(SpecialPowers.getClipboardData("text/unicode"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+        "Cutting partially masked password shouldn't copy masked value into the clipboard");
+  is(input.value, "abcdef",
+     "Cutting partially masked password shouldn't modify the value");
+
+  input.setSelectionRange(2, 4);
+  ok(true, "Trying to copy unmasked password...");
+  await copyToClipboard("cd");
+  is(input.value, "abcdef",
+     "Copying unmasked password shouldn't modify the value");
+
+  input.value = "012345";
+  editor.unmask(2, 4);
+  input.setSelectionRange(2, 4);
+  ok(true, "Trying to cut unmasked password...");
+  await cutToClipboard("23");
+  is(input.value, "0145",
+     "Cutting unmasked password should modify the value");
+
+  SimpleTest.finish();
+});
+</script>
+</body>
+</html>