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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Masayuki Nakano 2019-07-29 06:21:42 +00:00
Родитель ffbb14909f
Коммит fafe168f04
11 изменённых файлов: 229 добавлений и 92 удалений

Просмотреть файл

@ -53,12 +53,14 @@
#endif
#include "mozilla/ContentEvents.h"
#include "mozilla/dom/Element.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/Preferences.h"
#include "mozilla/PresShell.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/TextEditor.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/Selection.h"
using namespace mozilla;
using namespace mozilla::dom;
@ -878,11 +880,17 @@ bool nsCopySupport::FireClipboardEvent(EventMessage aEventMessage,
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;
}
}
}

Просмотреть файл

@ -19,7 +19,10 @@
#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"
@ -315,6 +318,11 @@ nsPlainTextSerializer::AppendText(nsIContent* aText, int32_t aStartOffset,
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

Просмотреть файл

@ -7,11 +7,11 @@
#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"
@ -506,71 +506,11 @@ static void ConvertToNativeNewlines(nsString& aString) {
#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);
}
}
@ -580,7 +520,7 @@ static void AppendSubString(nsString& aString, Text* aText, uint32_t aXPOffset,
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);
}
}

Просмотреть файл

@ -1133,7 +1133,7 @@ EditorBase::CanCut(bool* aCanCut) {
if (NS_WARN_IF(!aCanCut)) {
return NS_ERROR_INVALID_ARG;
}
*aCanCut = AsTextEditor()->CanCut();
*aCanCut = AsTextEditor()->IsCutCommandEnabled();
return NS_OK;
}
@ -1145,7 +1145,7 @@ EditorBase::CanCopy(bool* aCanCopy) {
if (NS_WARN_IF(!aCanCopy)) {
return NS_ERROR_INVALID_ARG;
}
*aCanCopy = AsTextEditor()->CanCopy();
*aCanCopy = AsTextEditor()->IsCopyCommandEnabled();
return NS_OK;
}

Просмотреть файл

@ -327,7 +327,8 @@ bool CutCommand::IsCommandEnabled(Command aCommand,
if (!aTextEditor) {
return false;
}
return aTextEditor->IsSelectionEditable() && aTextEditor->CanCut();
return aTextEditor->IsSelectionEditable() &&
aTextEditor->IsCutCommandEnabled();
}
nsresult CutCommand::DoCommand(Command aCommand, TextEditor& aTextEditor,
@ -391,7 +392,7 @@ bool CopyCommand::IsCommandEnabled(Command aCommand,
if (!aTextEditor) {
return false;
}
return aTextEditor->CanCopy();
return aTextEditor->IsCopyCommandEnabled();
}
nsresult CopyCommand::DoCommand(Command aCommand, TextEditor& aTextEditor,

Просмотреть файл

@ -8,7 +8,10 @@
#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"
@ -111,4 +114,60 @@ bool EditorUtils::IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
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

Просмотреть файл

@ -497,6 +497,17 @@ class EditorUtils final {
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

Просмотреть файл

@ -1732,7 +1732,7 @@ nsresult TextEditor::RedoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) {
return NS_OK;
}
bool TextEditor::CanCutOrCopy() const {
bool TextEditor::IsCopyToClipboardAllowedInternal() const {
MOZ_ASSERT(IsEditActionDataAvailable());
if (SelectionRefPtr()->IsCollapsed()) {
return false;
@ -1804,7 +1804,7 @@ nsresult TextEditor::CutAsAction(nsIPrincipal* aPrincipal) {
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;
@ -1818,7 +1818,7 @@ bool TextEditor::CanCut() const {
return true;
}
return IsModifiable() && CanCutOrCopy();
return IsModifiable() && IsCopyToClipboardAllowedInternal();
}
NS_IMETHODIMP
@ -1835,7 +1835,7 @@ TextEditor::Copy() {
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;
@ -1849,7 +1849,7 @@ bool TextEditor::CanCopy() const {
return true;
}
return CanCutOrCopy();
return IsCopyToClipboardAllowedInternal();
}
bool TextEditor::CanDeleteSelection() const {

Просмотреть файл

@ -95,24 +95,35 @@ class TextEditor : public EditorBase,
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
@ -714,10 +725,9 @@ class TextEditor : public EditorBase,
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);

Просмотреть файл

@ -271,6 +271,8 @@ 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]

Просмотреть файл

@ -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>