Bug 1548389 - part 1: Implement API to get/set unmask-range of password editor r=m_kato

This patch creates editor API to get/set unmask-range of password field.
Its design is similar to `setSelectionRange()` and `selectionStart`/
`selectionEnd` attributes.   The unmasked range is automatically
masked if `aTimeout` of `unmask()` is set to non-zero.
Otherwise, unmasked range won't be masked automatically even after user
or web apps modifies the editor, and inserting new character expands
unmasking range.

The following patch makes `TextEditRules` use these API to implement
delayed masking of password.

Note that editor has never supported dynamic `eEditorPasswordMask` change.
E.g., if you have typed some characters into an editor and toggle the flag,
the characters are not unmasked nor masked.  Then, if you type new characters,
only they are correctly masked or unmasked.  This patch puts `MOZ_ASSERT()`
to reject this feature completely on debug build for making the unmasking
implementation simpler.

Differential Revision: https://phabricator.services.mozilla.com/D38004

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Masayuki Nakano 2019-07-22 03:53:36 +00:00
Родитель ca49cc9d2d
Коммит 010cde10c1
8 изменённых файлов: 637 добавлений и 28 удалений

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

@ -54,6 +54,13 @@ DeleteNodeTransaction::DoTransaction() {
return NS_OK;
}
if (!mEditorBase->AsHTMLEditor() && mNodeToDelete->IsText()) {
uint32_t length = mNodeToDelete->AsText()->TextLength();
if (length > 0) {
mEditorBase->AsTextEditor()->WillDeleteText(length, 0, length);
}
}
// Remember which child mNodeToDelete was (by remembering which child was
// next). Note that mRefNode can be nullptr.
mRefNode = mNodeToDelete->GetNextSibling();
@ -68,6 +75,7 @@ DeleteNodeTransaction::DoTransaction() {
return error.StealNSResult();
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHODIMP
DeleteNodeTransaction::UndoTransaction() {
if (NS_WARN_IF(!CanDoIt())) {
@ -75,9 +83,25 @@ DeleteNodeTransaction::UndoTransaction() {
return NS_OK;
}
ErrorResult error;
RefPtr<EditorBase> editorBase = mEditorBase;
nsCOMPtr<nsINode> parent = mParentNode;
nsCOMPtr<nsINode> nodeToDelete = mNodeToDelete;
nsCOMPtr<nsIContent> refNode = mRefNode;
mParentNode->InsertBefore(*mNodeToDelete, refNode, error);
return error.StealNSResult();
parent->InsertBefore(*nodeToDelete, refNode, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
if (!editorBase->AsHTMLEditor() && nodeToDelete->IsText()) {
uint32_t length = nodeToDelete->AsText()->TextLength();
if (length > 0) {
error = MOZ_KnownLive(editorBase->AsTextEditor())
->DidInsertText(length, 0, length);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
}
return NS_OK;
}
NS_IMETHODIMP
@ -87,6 +111,13 @@ DeleteNodeTransaction::RedoTransaction() {
return NS_OK;
}
if (!mEditorBase->AsHTMLEditor() && mNodeToDelete->IsText()) {
uint32_t length = mNodeToDelete->AsText()->TextLength();
if (length > 0) {
mEditorBase->AsTextEditor()->WillDeleteText(length, 0, length);
}
}
mEditorBase->RangeUpdaterRef().SelAdjDeleteNode(mNodeToDelete);
ErrorResult error;

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

@ -218,14 +218,8 @@ nsresult EditorBase::Init(Document& aDocument, Element* aRoot,
"Initializing during an edit action is an error");
// First only set flags, but other stuff shouldn't be initialized now.
// Don't move this call after initializing mDocument.
// SetFlags() can check whether it's called during initialization or not by
// them. Note that SetFlags() will be called by PostCreate().
#ifdef DEBUG
nsresult rv =
#endif
SetFlags(aFlags);
NS_ASSERTION(NS_SUCCEEDED(rv), "SetFlags() failed");
// Note that SetFlags() will be called by PostCreate().
mFlags = aFlags;
mDocument = &aDocument;
// HTML editors currently don't have their own selection controller,
@ -470,6 +464,12 @@ void EditorBase::PreDestroy(bool aDestroyingFrames) {
return;
}
if (IsPasswordEditor() && !AsTextEditor()->IsAllMasked()) {
// Mask all range for now because if layout accessed the range, that
// would cause showing password accidentary or crash.
AsTextEditor()->MaskAllCharacters();
}
Selection* selection = GetSelection();
if (selection) {
selection->RemoveSelectionListener(this);
@ -527,6 +527,12 @@ EditorBase::SetFlags(uint32_t aFlags) {
return NS_OK;
}
DebugOnly<bool> changingPasswordEditorFlagDynamically =
mFlags != ~aFlags &&
((mFlags ^ aFlags) & nsIPlaintextEditor::eEditorPasswordMask);
MOZ_ASSERT(
!changingPasswordEditorFlagDynamically,
"TextEditor does not support dynamic eEditorPasswordMask flag change");
bool spellcheckerWasEnabled = CanEnableSpellCheck();
mFlags = aFlags;
@ -2303,14 +2309,26 @@ void EditorBase::DoInsertText(Text& aText, uint32_t aOffset,
const nsAString& aStringToInsert,
ErrorResult& aRv) {
aText.InsertData(aOffset, aStringToInsert, aRv);
NS_WARNING_ASSERTION(!aRv.Failed(), "Text::InsertData() failed");
if (NS_WARN_IF(Destroyed())) {
aRv = NS_ERROR_EDITOR_DESTROYED;
return;
}
if (NS_WARN_IF(aRv.Failed())) {
return;
}
if (!AsHTMLEditor() && !aStringToInsert.IsEmpty()) {
aRv = MOZ_KnownLive(AsTextEditor())
->DidInsertText(aText.TextLength(), aOffset,
aStringToInsert.Length());
NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed");
}
}
void EditorBase::DoDeleteText(Text& aText, uint32_t aOffset, uint32_t aCount,
ErrorResult& aRv) {
if (!AsHTMLEditor() && aCount > 0) {
AsTextEditor()->WillDeleteText(aText.TextLength(), aOffset, aCount);
}
aText.DeleteData(aOffset, aCount, aRv);
NS_WARNING_ASSERTION(!aRv.Failed(), "Text::DeleteData() failed");
if (NS_WARN_IF(Destroyed())) {
@ -2321,19 +2339,46 @@ void EditorBase::DoDeleteText(Text& aText, uint32_t aOffset, uint32_t aCount,
void EditorBase::DoReplaceText(Text& aText, uint32_t aOffset, uint32_t aCount,
const nsAString& aStringToInsert,
ErrorResult& aRv) {
if (!AsHTMLEditor() && aCount > 0) {
AsTextEditor()->WillDeleteText(aText.TextLength(), aOffset, aCount);
}
aText.ReplaceData(aOffset, aCount, aStringToInsert, aRv);
NS_WARNING_ASSERTION(!aRv.Failed(), "Text::ReplaceData() failed");
if (NS_WARN_IF(Destroyed())) {
aRv = NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(aRv.Failed())) {
return;
}
if (!AsHTMLEditor() && !aStringToInsert.IsEmpty()) {
aRv = MOZ_KnownLive(AsTextEditor())
->DidInsertText(aText.TextLength(), aOffset,
aStringToInsert.Length());
NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed");
}
}
void EditorBase::DoSetText(Text& aText, const nsAString& aStringToSet,
ErrorResult& aRv) {
if (!AsHTMLEditor()) {
uint32_t length = aText.TextLength();
if (length > 0) {
AsTextEditor()->WillDeleteText(length, 0, length);
}
}
aText.SetData(aStringToSet, aRv);
NS_WARNING_ASSERTION(!aRv.Failed(), "Text::SetData() failed");
if (NS_WARN_IF(Destroyed())) {
aRv = NS_ERROR_EDITOR_DESTROYED;
return;
}
if (NS_WARN_IF(aRv.Failed())) {
return;
}
if (!AsHTMLEditor() && !aStringToSet.IsEmpty()) {
aRv = MOZ_KnownLive(AsTextEditor())
->DidInsertText(aText.Length(), 0, aStringToSet.Length());
NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed");
}
}
@ -4895,6 +4940,81 @@ void EditorBase::HideCaret(bool aHide) {
}
}
NS_IMETHODIMP EditorBase::Unmask(uint32_t aStart, int64_t aEnd,
uint32_t aTimeout, uint8_t aArgc) {
if (NS_WARN_IF(!IsPasswordEditor())) {
return NS_ERROR_NOT_AVAILABLE;
}
if (NS_WARN_IF(aStart == UINT32_MAX) || NS_WARN_IF(aArgc >= 1 && aEnd == 0) ||
NS_WARN_IF(aArgc >= 1 && aEnd > 0 && aStart >= aEnd)) {
return NS_ERROR_INVALID_ARG;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
uint32_t length = aArgc < 1 || aEnd < 0 ? UINT32_MAX : aEnd - aStart;
uint32_t timeout = aArgc < 2 ? 0 : aTimeout;
nsresult rv = MOZ_KnownLive(AsTextEditor())
->SetUnmaskRangeAndNotify(aStart, length, timeout);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditorBase::ToGenericNSResult(rv);
}
// Flush pending layout right now since the caller may access us before
// doing it.
if (RefPtr<PresShell> presShell = GetPresShell()) {
presShell->FlushPendingNotifications(FlushType::Layout);
}
return NS_OK;
}
NS_IMETHODIMP EditorBase::Mask() {
if (NS_WARN_IF(!IsPasswordEditor())) {
return NS_ERROR_NOT_AVAILABLE;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv = MOZ_KnownLive(AsTextEditor())->MaskAllCharactersAndNotify();
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditorBase::ToGenericNSResult(rv);
}
// Flush pending layout right now since the caller may access us before
// doing it.
if (RefPtr<PresShell> presShell = GetPresShell()) {
presShell->FlushPendingNotifications(FlushType::Layout);
}
return NS_OK;
}
NS_IMETHODIMP EditorBase::GetUnmaskedStart(uint32_t* aResult) {
if (NS_WARN_IF(!IsPasswordEditor())) {
*aResult = 0;
return NS_ERROR_NOT_AVAILABLE;
}
*aResult =
AsTextEditor()->IsAllMasked() ? 0 : AsTextEditor()->UnmaskedStart();
return NS_OK;
}
NS_IMETHODIMP EditorBase::GetUnmaskedEnd(uint32_t* aResult) {
if (NS_WARN_IF(!IsPasswordEditor())) {
*aResult = 0;
return NS_ERROR_NOT_AVAILABLE;
}
*aResult = AsTextEditor()->IsAllMasked() ? 0 : AsTextEditor()->UnmaskedEnd();
return NS_OK;
}
/******************************************************************************
* EditorBase::AutoSelectionRestorer
*****************************************************************************/

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

@ -62,6 +62,7 @@ NS_IMPL_RELEASE_INHERITED(InsertNodeTransaction, EditTransactionBase)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertNodeTransaction)
NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHODIMP
InsertNodeTransaction::DoTransaction() {
if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mContentToInsert) ||
@ -91,16 +92,30 @@ InsertNodeTransaction::DoTransaction() {
}
}
mEditorBase->MarkNodeDirty(mContentToInsert);
RefPtr<EditorBase> editorBase = mEditorBase;
nsCOMPtr<nsIContent> contentToInsert = mContentToInsert;
nsCOMPtr<nsINode> container = mPointToInsert.GetContainer();
nsCOMPtr<nsIContent> refChild = mPointToInsert.GetChild();
editorBase->MarkNodeDirty(contentToInsert);
ErrorResult error;
mPointToInsert.GetContainer()->InsertBefore(*mContentToInsert,
mPointToInsert.GetChild(), error);
container->InsertBefore(*contentToInsert, refChild, error);
error.WouldReportJSException();
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
if (!editorBase->AsHTMLEditor() && contentToInsert->IsText()) {
uint32_t length = contentToInsert->AsText()->TextLength();
if (length > 0) {
error = MOZ_KnownLive(editorBase->AsTextEditor())
->DidInsertText(length, 0, length);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
}
if (!mEditorBase->AllowsTransactionsToChangeSelection()) {
return NS_OK;
}
@ -124,9 +139,16 @@ InsertNodeTransaction::DoTransaction() {
NS_IMETHODIMP
InsertNodeTransaction::UndoTransaction() {
if (NS_WARN_IF(!mContentToInsert) || NS_WARN_IF(!mPointToInsert.IsSet())) {
if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mContentToInsert) ||
NS_WARN_IF(!mPointToInsert.IsSet())) {
return NS_ERROR_NOT_INITIALIZED;
}
if (!mEditorBase->AsHTMLEditor() && mContentToInsert->IsText()) {
uint32_t length = mContentToInsert->TextLength();
if (length > 0) {
mEditorBase->AsTextEditor()->WillDeleteText(length, 0, length);
}
}
// XXX If the inserted node has been moved to different container node or
// just removed from the DOM tree, this always fails.
ErrorResult error;

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

@ -107,6 +107,8 @@ class TextEditRules : public nsITimerCallback, public nsINamed {
*/
virtual bool DocumentIsEmpty();
bool DontEchoPassword() const;
protected:
virtual ~TextEditRules();
@ -373,7 +375,6 @@ class TextEditRules : public nsITimerCallback, public nsINamed {
bool IsReadonly() const;
bool IsDisabled() const;
bool IsMailEditor() const;
bool DontEchoPassword() const;
private:
TextEditor* MOZ_NON_OWNING_REF mTextEditor;

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

@ -23,9 +23,10 @@
#include "mozilla/TextComposition.h"
#include "mozilla/TextEvents.h"
#include "mozilla/TextServicesDocument.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Selection.h"
#include "nsAString.h"
#include "nsCRT.h"
#include "nsCaret.h"
@ -69,15 +70,15 @@ TextEditor::TextEditor()
: mWrapColumn(0),
mMaxTextLength(-1),
mInitTriggerCounter(0),
mNewlineHandling(nsIPlaintextEditor::eNewlinesPasteToFirst)
mNewlineHandling(nsIPlaintextEditor::eNewlinesPasteToFirst),
#ifdef XP_WIN
,
mCaretStyle(1)
mCaretStyle(1),
#else
,
mCaretStyle(0)
mCaretStyle(0),
#endif
{
mUnmaskedStart(UINT32_MAX),
mUnmaskedLength(0),
mIsMaskingPassword(true) {
// printf("Size of TextEditor: %zu\n", sizeof(TextEditor));
static_assert(
sizeof(TextEditor) <= 512,
@ -99,14 +100,21 @@ TextEditor::~TextEditor() {
NS_IMPL_CYCLE_COLLECTION_CLASS(TextEditor)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TextEditor, EditorBase)
if (tmp->mRules) tmp->mRules->DetachEditor();
if (tmp->mRules) {
tmp->mRules->DetachEditor();
}
if (tmp->mMaskTimer) {
tmp->mMaskTimer->Cancel();
}
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRules)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedDocumentEncoder)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaskTimer)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRules)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedDocumentEncoder)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaskTimer)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_ADDREF_INHERITED(TextEditor, EditorBase)
@ -114,6 +122,8 @@ NS_IMPL_RELEASE_INHERITED(TextEditor, EditorBase)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextEditor)
NS_INTERFACE_MAP_ENTRY(nsIPlaintextEditor)
NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
NS_INTERFACE_MAP_ENTRY(nsINamed)
NS_INTERFACE_MAP_END_INHERITING(EditorBase)
nsresult TextEditor::Init(Document& aDoc, Element* aRoot,
@ -2247,4 +2257,228 @@ nsresult TextEditor::HideLastPasswordInput() {
return NS_OK;
}
nsresult TextEditor::SetUnmaskRangeInternal(uint32_t aStart, uint32_t aLength,
uint32_t aTimeout, bool aNotify,
bool aForceStartMasking) {
mIsMaskingPassword = aForceStartMasking || aTimeout != 0;
// We cannot manage multiple unmasked ranges so that shrink the previous
// range first.
if (!IsAllMasked()) {
mUnmaskedLength = 0;
if (mMaskTimer) {
mMaskTimer->Cancel();
}
}
// If we're not a password editor, return error since this call does not
// make sense.
if (!IsPasswordEditor()) {
if (mMaskTimer) {
mMaskTimer = nullptr;
}
return NS_ERROR_NOT_AVAILABLE;
}
Element* rootElement = GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_NOT_INITIALIZED;
}
nsIContent* firstChild = rootElement->GetFirstChild();
if (!firstChild || firstChild->IsElement()) {
// There is no anonymous text node in the editor.
return aStart > 0 ? NS_ERROR_INVALID_ARG : NS_OK;
}
if (aStart < UINT32_MAX) {
uint32_t valueLength = firstChild->Length();
if (aStart >= valueLength) {
return NS_ERROR_INVALID_ARG; // There is no character can be masked.
}
mUnmaskedStart = aStart;
mUnmaskedLength = std::min(valueLength - mUnmaskedStart, aLength);
// If it's first time to mask the unmasking characters with timer, create
// the timer now. Then, we'll keep using it for saving the creation cost.
if (!mMaskTimer && aLength && aTimeout && mUnmaskedLength) {
mMaskTimer = NS_NewTimer();
}
} else {
if (NS_WARN_IF(aLength != 0)) {
return NS_ERROR_INVALID_ARG;
}
mUnmaskedStart = UINT32_MAX;
mUnmaskedLength = 0;
}
// Notify nsTextFrame of this update if the caller wants this to do it.
// Only in this case, script may run.
if (aNotify) {
MOZ_ASSERT(IsEditActionDataAvailable());
RefPtr<Document> document = GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_NOT_INITIALIZED;
}
// Notify nsTextFrame of masking range change.
if (PresShell* presShell = document->GetObservingPresShell()) {
CharacterDataChangeInfo changeInfo = {false, 0, firstChild->Length(),
firstChild->Length(), nullptr};
presShell->CharacterDataChanged(firstChild, changeInfo);
}
// Scroll caret into the view since masking or unmasking character may
// move caret to outside of the view.
ScrollSelectionIntoView(false);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
}
if (!IsAllMasked() && aTimeout != 0) {
// Initialize the timer to mask the range automatically.
MOZ_ASSERT(mMaskTimer);
mMaskTimer->InitWithCallback(this, aTimeout, nsITimer::TYPE_ONE_SHOT);
}
return NS_OK;
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHODIMP
TextEditor::Notify(nsITimer* aTimer) {
// Check whether our text editor's password flag was changed before this
// "hide password character" timer actually fires.
if (!IsPasswordEditor()) {
return NS_OK;
}
if (IsAllMasked()) {
return NS_OK;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
// Mask all characters.
nsresult rv = MaskAllCharactersAndNotify();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to mask all characters");
return EditorBase::ToGenericNSResult(rv);
}
NS_IMETHODIMP
TextEditor::GetName(nsACString& aName) {
aName.AssignLiteral("TextEditor");
return NS_OK;
}
void TextEditor::WillDeleteText(uint32_t aCurrentLength,
uint32_t aRemoveStartOffset,
uint32_t aRemoveLength) {
MOZ_ASSERT(IsEditActionDataAvailable());
if (!IsPasswordEditor() || IsAllMasked()) {
return;
}
// Adjust unmasked range before deletion since DOM mutation may cause
// layout referring the range in old text.
// If we need to mask automatically, mask all now.
if (mIsMaskingPassword) {
DebugOnly<nsresult> rvIgnored = MaskAllCharacters();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "MaskAllCharacters() failed");
return;
}
if (aRemoveStartOffset < mUnmaskedStart) {
// If removing range is before the unmasked range, move it.
if (aRemoveStartOffset + aRemoveLength <= mUnmaskedStart) {
DebugOnly<nsresult> rvIgnored =
SetUnmaskRange(mUnmaskedStart - aRemoveLength, mUnmaskedLength);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "SetUnmaskRange() failed");
return;
}
// If removing range starts before unmasked range, and ends in unmasked
// range, move and shrink the range.
if (aRemoveStartOffset + aRemoveLength < UnmaskedEnd()) {
uint32_t unmaskedLengthInRemovingRange =
aRemoveStartOffset + aRemoveLength - mUnmaskedStart;
DebugOnly<nsresult> rvIgnored = SetUnmaskRange(
aRemoveStartOffset, mUnmaskedLength - unmaskedLengthInRemovingRange);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "SetUnmaskRange() failed");
return;
}
// If removing range includes all unmasked range, collapse it to the
// remove offset.
DebugOnly<nsresult> rvIgnored = SetUnmaskRange(aRemoveStartOffset, 0);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "SetUnmaskRange() failed");
return;
}
if (aRemoveStartOffset < UnmaskedEnd()) {
// If removing range is in unmasked range, shrink the range.
if (aRemoveStartOffset + aRemoveLength <= UnmaskedEnd()) {
DebugOnly<nsresult> rvIgnored =
SetUnmaskRange(mUnmaskedStart, mUnmaskedLength - aRemoveLength);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "SetUnmaskRange() failed");
return;
}
// If removing range starts from unmasked range, and ends after it,
// shrink it.
DebugOnly<nsresult> rvIgnored =
SetUnmaskRange(mUnmaskedStart, aRemoveStartOffset - mUnmaskedStart);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "SetUnmaskRange() failed");
return;
}
// If removing range is after the unmasked range, keep it.
}
nsresult TextEditor::DidInsertText(uint32_t aNewLength,
uint32_t aInsertedOffset,
uint32_t aInsertedLength) {
MOZ_ASSERT(IsEditActionDataAvailable());
if (!IsPasswordEditor() || IsAllMasked()) {
return NS_OK;
}
if (mIsMaskingPassword) {
// If we need to mask password, mask all right now.
nsresult rv = MaskAllCharactersAndNotify();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "MaskAllCharacters() failed");
return rv;
}
if (aInsertedOffset < mUnmaskedStart) {
// If insertion point is before unmasked range, expand the unmasked range
// to include the new text.
nsresult rv = SetUnmaskRangeAndNotify(
aInsertedOffset, UnmaskedEnd() + aInsertedLength - aInsertedOffset);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetUnmaskRange() failed");
return rv;
}
if (aInsertedOffset <= UnmaskedEnd()) {
// If insertion point is in unmasked range, unmask new text.
nsresult rv = SetUnmaskRangeAndNotify(mUnmaskedStart,
mUnmaskedLength + aInsertedLength);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetUnmaskRange() failed");
return rv;
}
// If insertion point is after unmasked range, extend the unmask range to
// include the new text.
nsresult rv = SetUnmaskRangeAndNotify(
mUnmaskedStart, aInsertedOffset + aInsertedLength - mUnmaskedStart);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetUnmaskRange() failed");
return rv;
}
} // namespace mozilla

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

@ -9,8 +9,10 @@
#include "mozilla/EditorBase.h"
#include "nsCOMPtr.h"
#include "nsCycleCollectionParticipant.h"
#include "nsINamed.h"
#include "nsIPlaintextEditor.h"
#include "nsISupportsImpl.h"
#include "nsITimer.h"
#include "nscore.h"
class nsIContent;
@ -22,6 +24,8 @@ class nsITransferable;
namespace mozilla {
class AutoEditInitRulesTrigger;
class DeleteNodeTransaction;
class InsertNodeTransaction;
enum class EditSubAction : int32_t;
namespace dom {
@ -33,7 +37,10 @@ class Selection;
* The text editor implementation.
* Use to edit text document represented as a DOM tree.
*/
class TextEditor : public EditorBase, public nsIPlaintextEditor {
class TextEditor : public EditorBase,
public nsIPlaintextEditor,
public nsITimerCallback,
public nsINamed {
public:
/****************************************************************************
* NOTE: DO NOT MAKE YOUR NEW METHODS PUBLIC IF they are called by other
@ -50,8 +57,9 @@ class TextEditor : public EditorBase, public nsIPlaintextEditor {
TextEditor();
// nsIPlaintextEditor methods
NS_DECL_NSIPLAINTEXTEDITOR
NS_DECL_NSITIMERCALLBACK
NS_DECL_NSINAMED
// Overrides of nsIEditor
NS_IMETHOD GetDocumentIsEmpty(bool* aDocumentIsEmpty) override;
@ -316,6 +324,38 @@ class TextEditor : public EditorBase, public nsIPlaintextEditor {
*/
void SetWrapColumn(int32_t aWrapColumn) { mWrapColumn = aWrapColumn; }
/**
* The following methods are available only when the instance is a password
* editor. They return whether there is unmasked range or not and range
* start and length.
*/
bool IsAllMasked() const {
MOZ_ASSERT(IsPasswordEditor());
return mUnmaskedStart == UINT32_MAX && mUnmaskedLength == 0;
}
uint32_t UnmaskedStart() const {
MOZ_ASSERT(IsPasswordEditor());
return mUnmaskedStart;
}
uint32_t UnmaskedLength() const {
MOZ_ASSERT(IsPasswordEditor());
return mUnmaskedLength;
}
uint32_t UnmaskedEnd() const {
MOZ_ASSERT(IsPasswordEditor());
return mUnmaskedStart + mUnmaskedLength;
}
/**
* IsMaskingPassword() returns false when the last caller of `Unmask()`
* didn't want to mask again automatically. When this returns true, user
* input causes masking the password even before timed-out.
*/
bool IsMaskingPassword() const {
MOZ_ASSERT(IsPasswordEditor());
return mIsMaskingPassword;
}
protected: // May be called by friends.
/****************************************************************************
* Some classes like TextEditRules, HTMLEditRules, WSRunObject which are
@ -424,6 +464,95 @@ class TextEditor : public EditorBase, public nsIPlaintextEditor {
static void GetDefaultEditorPrefs(int32_t& aNewLineHandling,
int32_t& aCaretStyle);
/**
* SetUnmaskRange() is available only when the instance is a password
* editor. This just updates unmask range. I.e., caller needs to
* guarantee to update the layout.
*
* @param aStart First index to show the character.
* If aLength is 0, this value is ignored.
* @param aLength Optional, Length to show characters.
* If UINT32_MAX, it means unmasking all characters after
* aStart.
* If 0, it means that masking all characters.
* @param aTimeout Optional, specify milliseconds to hide the unmasked
* characters after this call.
* If 0, it means this won't mask the characters
* automatically.
* If aLength is 0, this value is ignored.
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult SetUnmaskRange(
uint32_t aStart, uint32_t aLength = UINT32_MAX, uint32_t aTimeout = 0) {
return SetUnmaskRangeInternal(aStart, aLength, aTimeout, false, false);
}
/**
* SetUnmaskRangeAndNotify() is available only when the instance is a
* password editor. This updates unmask range and notifying the text frame
* to update the visible characters.
*
* @param aStart First index to show the character.
* If UINT32_MAX, it means masking all.
* @param aLength Optional, Length to show characters.
* If UINT32_MAX, it means unmasking all characters after
* aStart.
* @param aTimeout Optional, specify milliseconds to hide the unmasked
* characters after this call.
* If 0, it means this won't mask the characters
* automatically.
* If aLength is 0, this value is ignored.
*/
MOZ_CAN_RUN_SCRIPT nsresult SetUnmaskRangeAndNotify(
uint32_t aStart, uint32_t aLength = UINT32_MAX, uint32_t aTimeout = 0) {
return SetUnmaskRangeInternal(aStart, aLength, aTimeout, true, false);
}
/**
* MaskAllCharacters() is an alias of SetUnmaskRange() to mask all characters.
* In other words, this removes existing unmask range.
* After this is called, TextEditor starts masking password automatically.
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult MaskAllCharacters() {
return SetUnmaskRangeInternal(UINT32_MAX, 0, 0, false, true);
}
/**
* MaskAllCharactersAndNotify() is an alias of SetUnmaskRangeAndNotify() to
* mask all characters and notifies the text frame. In other words, this
* removes existing unmask range.
* After this is called, TextEditor starts masking password automatically.
*/
MOZ_CAN_RUN_SCRIPT nsresult MaskAllCharactersAndNotify() {
return SetUnmaskRangeInternal(UINT32_MAX, 0, 0, true, true);
}
/**
* WillDeleteText() is called before `DeleteTextTransaction` or something
* removes text in a text node. Note that this won't be called if the
* instance is `HTMLEditor` since supporting it makes the code complicated
* due to mutation events.
*
* @param aCurrentLength Current text length of the node.
* @param aRemoveStartOffset Start offset of the range to be removed.
* @param aRemoveLength Length of the range to be removed.
*/
void WillDeleteText(uint32_t aCurrentLength, uint32_t aRemoveStartOffset,
uint32_t aRemoveLength);
/**
* DidInsertText() is called after `InsertTextTransaction` or something
* inserts text into a text node. Note that this won't be called if the
* instance is `HTMLEditor` since supporting it makes the code complicated
* due to mutatione events.
*
* @param aNewLength New text length after the insertion.
* @param aInsertedOffset Start offset of the inserted text.
* @param aInsertedLength Length of the inserted text.
* @return NS_OK or NS_ERROR_EDITOR_DESTROYED.
*/
MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE nsresult DidInsertText(
uint32_t aNewLength, uint32_t aInsertedOffset, uint32_t aInsertedLength);
protected: // Called by helper classes.
virtual void OnStartToHandleTopLevelEditSubAction(
EditSubAction aEditSubAction, nsIEditor::EDirection aDirection) override;
@ -587,16 +716,47 @@ class TextEditor : public EditorBase, public nsIPlaintextEditor {
virtual already_AddRefed<Element> GetInputEventTargetElement() override;
/**
* See SetUnmaskRange() and SetUnmaskRangeAndNotify() for the detail.
*
* @param aForceStartMasking If true, forcibly starts masking. This should
* be used only when `nsIEditor::Mask()` is called.
*/
MOZ_CAN_RUN_SCRIPT nsresult SetUnmaskRangeInternal(uint32_t aStart,
uint32_t aLength,
uint32_t aTimeout,
bool aNotify,
bool aForceStartMasking);
protected:
mutable nsCOMPtr<nsIDocumentEncoder> mCachedDocumentEncoder;
// Timer to mask unmasked characters automatically. Used only when it's
// a password field.
nsCOMPtr<nsITimer> mMaskTimer;
mutable nsString mCachedDocumentEncoderType;
int32_t mWrapColumn;
int32_t mMaxTextLength;
int32_t mInitTriggerCounter;
int32_t mNewlineHandling;
int32_t mCaretStyle;
// Unmasked character range. Used only when it's a password field.
// If mUnmaskedLength is 0, it means there is no unmasked characters.
uint32_t mUnmaskedStart;
uint32_t mUnmaskedLength;
// Set to true if all characters are masked or waiting notification from
// `mMaskTimer`. Otherwise, i.e., part of or all of password is unmasked
// without setting `mMaskTimer`, set to false.
bool mIsMaskingPassword;
friend class AutoEditInitRulesTrigger;
friend class DeleteNodeTransaction;
friend class EditorBase;
friend class InsertNodeTransaction;
friend class TextEditRules;
};

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

@ -503,6 +503,47 @@ interface nsIEditor : nsISupports
*/
readonly attribute boolean composing;
/**
* unmask() is available only when the editor is a passwrod field. This
* unmasks characters in specified by aStart and aEnd. If there have
* already unmasked characters, they are masked when this is called.
* Note that if you calls this without non-zero `aTimeout`, you bear
* responsibility for masking password with calling `mask()`. I.e.,
* user inputting password won't be masked automacitally. If user types
* a new character and echo is enabled, unmasked range is expanded to
* including it.
*
* @param aStart First index to show the character.
* @param aEnd Optional, next index of last unmasked character.
* If omitted or negative value, it means unmasking all
* characters after aStart. Specifying same index
* throws an exception.
* @param aTimeout Optional, specify milliseconds to hide the unmasked
* characters if you want to show them temporarily.
* If omitted or 0, it means this won't mask the characters
* automatically.
*/
[can_run_script, optional_argc] void unmask(
in unsigned long aStart,
[optional] in long long aEnd,
[optional] in unsigned long aTimeout);
/**
* mask() is available only when the editor is a password field. This masks
* all unmasked characters immediately.
*/
[can_run_script] void mask();
/**
* These attributes are available only when the editor is a password field.
* unmaskedStart is first unmasked character index, or 0 if there is no
* unmasked characters.
* unmaskedEnd is next index of the last unmasked character. 0 means there
* is no unmasked characters.
*/
readonly attribute unsigned long unmaskedStart;
readonly attribute unsigned long unmaskedEnd;
%{C++
/**
* AsEditorBase() returns a pointer to EditorBase class.

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

@ -1211,7 +1211,6 @@ function runEditorFlagChangeTests() {
is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED,
description + "IME isn't enabled on HTML editor");
const kIMEStateChangeFlags =
Ci.nsIPlaintextEditor.eEditorPasswordMask |
Ci.nsIPlaintextEditor.eEditorReadonlyMask |
Ci.nsIPlaintextEditor.eEditorDisabledMask;
var editor = window.docShell.editor;
@ -1235,7 +1234,8 @@ function runEditorFlagChangeTests() {
is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED,
description + "#1 IME isn't enabled on HTML editor");
editor.flags |= ~kIMEStateChangeFlags;
editor.flags |=
~(kIMEStateChangeFlags | Ci.nsIPlaintextEditor.eEditorPasswordMask);
ok(editor.composing,
description + "#2 IME composition was committed unexpectedly");
is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED,