diff options
Diffstat (limited to 'toolkit/components/satchel/nsFormFillController.cpp')
-rw-r--r-- | toolkit/components/satchel/nsFormFillController.cpp | 1382 |
1 files changed, 1382 insertions, 0 deletions
diff --git a/toolkit/components/satchel/nsFormFillController.cpp b/toolkit/components/satchel/nsFormFillController.cpp new file mode 100644 index 000000000..d70036635 --- /dev/null +++ b/toolkit/components/satchel/nsFormFillController.cpp @@ -0,0 +1,1382 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsFormFillController.h" + +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent() +#include "nsIFormAutoComplete.h" +#include "nsIInputListAutoComplete.h" +#include "nsIAutoCompleteSimpleResult.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsIServiceManager.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIDocShellTreeItem.h" +#include "nsPIDOMWindow.h" +#include "nsIWebNavigation.h" +#include "nsIContentViewer.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIFormControl.h" +#include "nsIDocument.h" +#include "nsIContent.h" +#include "nsIPresShell.h" +#include "nsRect.h" +#include "nsIDOMHTMLFormElement.h" +#include "nsILoginManager.h" +#include "nsIDOMMouseEvent.h" +#include "mozilla/ModuleUtils.h" +#include "nsToolkitCompsCID.h" +#include "nsEmbedCID.h" +#include "nsIDOMNSEditableElement.h" +#include "nsContentUtils.h" +#include "nsILoadContext.h" +#include "nsIFrame.h" +#include "nsIScriptSecurityManager.h" +#include "nsFocusManager.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION(nsFormFillController, + mController, mLoginManager, mFocusedPopup, mDocShells, + mPopups, mLastListener, mLastFormAutoComplete) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFormFillController) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFormFillController) + NS_INTERFACE_MAP_ENTRY(nsIFormFillController) + NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteInput) + NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteSearch) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsIFormAutoCompleteObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFormFillController) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFormFillController) + + + +nsFormFillController::nsFormFillController() : + mFocusedInput(nullptr), + mFocusedInputNode(nullptr), + mListNode(nullptr), + mTimeout(50), + mMinResultsForPopup(1), + mMaxRows(0), + mContextMenuFiredBeforeFocus(false), + mDisableAutoComplete(false), + mCompleteDefaultIndex(false), + mCompleteSelectedIndex(false), + mForceComplete(false), + mSuppressOnInput(false) +{ + mController = do_GetService("@mozilla.org/autocomplete/controller;1"); + MOZ_ASSERT(mController); +} + +nsFormFillController::~nsFormFillController() +{ + if (mListNode) { + mListNode->RemoveMutationObserver(this); + mListNode = nullptr; + } + if (mFocusedInputNode) { + MaybeRemoveMutationObserver(mFocusedInputNode); + mFocusedInputNode = nullptr; + mFocusedInput = nullptr; + } + RemoveForDocument(nullptr); + + // Remove ourselves as a focus listener from all cached docShells + uint32_t count = mDocShells.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsCOMPtr<nsPIDOMWindowOuter> window = GetWindowForDocShell(mDocShells[i]); + RemoveWindowListeners(window); + } +} + +//////////////////////////////////////////////////////////////////////// +//// nsIMutationObserver +// + +void +nsFormFillController::AttributeChanged(nsIDocument* aDocument, + mozilla::dom::Element* aElement, + int32_t aNameSpaceID, + nsIAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) +{ + if ((aAttribute == nsGkAtoms::type || aAttribute == nsGkAtoms::readonly || + aAttribute == nsGkAtoms::autocomplete) && + aNameSpaceID == kNameSpaceID_None) { + nsCOMPtr<nsIDOMHTMLInputElement> focusedInput(mFocusedInput); + // Reset the current state of the controller, unconditionally. + StopControllingInput(); + // Then restart based on the new values. We have to delay this + // to avoid ending up in an endless loop due to re-registering our + // mutation observer (which would notify us again for *this* event). + nsCOMPtr<nsIRunnable> event = + mozilla::NewRunnableMethod<nsCOMPtr<nsIDOMHTMLInputElement>> + (this, &nsFormFillController::MaybeStartControllingInput, focusedInput); + NS_DispatchToCurrentThread(event); + } + + if (mListNode && mListNode->Contains(aElement)) { + RevalidateDataList(); + } +} + +void +nsFormFillController::ContentAppended(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild, + int32_t aIndexInContainer) +{ + if (mListNode && mListNode->Contains(aContainer)) { + RevalidateDataList(); + } +} + +void +nsFormFillController::ContentInserted(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild, + int32_t aIndexInContainer) +{ + if (mListNode && mListNode->Contains(aContainer)) { + RevalidateDataList(); + } +} + +void +nsFormFillController::ContentRemoved(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild, + int32_t aIndexInContainer, + nsIContent* aPreviousSibling) +{ + if (mListNode && mListNode->Contains(aContainer)) { + RevalidateDataList(); + } +} + +void +nsFormFillController::CharacterDataWillChange(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ +} + +void +nsFormFillController::CharacterDataChanged(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ +} + +void +nsFormFillController::AttributeWillChange(nsIDocument* aDocument, + mozilla::dom::Element* aElement, + int32_t aNameSpaceID, + nsIAtom* aAttribute, int32_t aModType, + const nsAttrValue* aNewValue) +{ +} + +void +nsFormFillController::NativeAnonymousChildListChange(nsIDocument* aDocument, + nsIContent* aContent, + bool aIsRemove) +{ +} + +void +nsFormFillController::ParentChainChanged(nsIContent* aContent) +{ +} + +void +nsFormFillController::NodeWillBeDestroyed(const nsINode* aNode) +{ + mPwmgrInputs.Remove(aNode); + if (aNode == mListNode) { + mListNode = nullptr; + RevalidateDataList(); + } else if (aNode == mFocusedInputNode) { + mFocusedInputNode = nullptr; + mFocusedInput = nullptr; + } +} + +void +nsFormFillController::MaybeRemoveMutationObserver(nsINode* aNode) +{ + // Nodes being tracked in mPwmgrInputs will have their observers removed when + // they stop being tracked. + if (!mPwmgrInputs.Get(aNode)) { + aNode->RemoveMutationObserver(this); + } +} + +//////////////////////////////////////////////////////////////////////// +//// nsIFormFillController + +NS_IMETHODIMP +nsFormFillController::AttachToBrowser(nsIDocShell *aDocShell, nsIAutoCompletePopup *aPopup) +{ + NS_ENSURE_TRUE(aDocShell && aPopup, NS_ERROR_ILLEGAL_VALUE); + + mDocShells.AppendElement(aDocShell); + mPopups.AppendElement(aPopup); + + // Listen for focus events on the domWindow of the docShell + nsCOMPtr<nsPIDOMWindowOuter> window = GetWindowForDocShell(aDocShell); + AddWindowListeners(window); + + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::DetachFromBrowser(nsIDocShell *aDocShell) +{ + int32_t index = GetIndexOfDocShell(aDocShell); + NS_ENSURE_TRUE(index >= 0, NS_ERROR_FAILURE); + + // Stop listening for focus events on the domWindow of the docShell + nsCOMPtr<nsPIDOMWindowOuter> window = + GetWindowForDocShell(mDocShells.SafeElementAt(index)); + RemoveWindowListeners(window); + + mDocShells.RemoveElementAt(index); + mPopups.RemoveElementAt(index); + + return NS_OK; +} + + +NS_IMETHODIMP +nsFormFillController::MarkAsLoginManagerField(nsIDOMHTMLInputElement *aInput) +{ + /* + * The Login Manager can supply autocomplete results for username fields, + * when a user has multiple logins stored for a site. It uses this + * interface to indicate that the form manager shouldn't handle the + * autocomplete. The form manager also checks for this tag when saving + * form history (so it doesn't save usernames). + */ + nsCOMPtr<nsINode> node = do_QueryInterface(aInput); + NS_ENSURE_STATE(node); + + // If the field was already marked, we don't want to show the popup again. + if (mPwmgrInputs.Get(node)) { + return NS_OK; + } + + mPwmgrInputs.Put(node, true); + node->AddMutationObserverUnlessExists(this); + + nsFocusManager *fm = nsFocusManager::GetFocusManager(); + if (fm) { + nsCOMPtr<nsIContent> focusedContent = fm->GetFocusedContent(); + if (SameCOMIdentity(focusedContent, node)) { + nsCOMPtr<nsIDOMHTMLInputElement> input = do_QueryInterface(node); + if (!mFocusedInput) { + MaybeStartControllingInput(input); + } + } + } + + if (!mLoginManager) + mLoginManager = do_GetService("@mozilla.org/login-manager;1"); + + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetFocusedInput(nsIDOMHTMLInputElement **aInput) { + *aInput = mFocusedInput; + NS_IF_ADDREF(*aInput); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +//// nsIAutoCompleteInput + +NS_IMETHODIMP +nsFormFillController::GetPopup(nsIAutoCompletePopup **aPopup) +{ + *aPopup = mFocusedPopup; + NS_IF_ADDREF(*aPopup); + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetController(nsIAutoCompleteController **aController) +{ + *aController = mController; + NS_IF_ADDREF(*aController); + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetPopupOpen(bool *aPopupOpen) +{ + if (mFocusedPopup) + mFocusedPopup->GetPopupOpen(aPopupOpen); + else + *aPopupOpen = false; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SetPopupOpen(bool aPopupOpen) +{ + if (mFocusedPopup) { + if (aPopupOpen) { + // make sure input field is visible before showing popup (bug 320938) + nsCOMPtr<nsIContent> content = do_QueryInterface(mFocusedInput); + NS_ENSURE_STATE(content); + nsCOMPtr<nsIDocShell> docShell = GetDocShellForInput(mFocusedInput); + NS_ENSURE_STATE(docShell); + nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell(); + NS_ENSURE_STATE(presShell); + presShell->ScrollContentIntoView(content, + nsIPresShell::ScrollAxis( + nsIPresShell::SCROLL_MINIMUM, + nsIPresShell::SCROLL_IF_NOT_VISIBLE), + nsIPresShell::ScrollAxis( + nsIPresShell::SCROLL_MINIMUM, + nsIPresShell::SCROLL_IF_NOT_VISIBLE), + nsIPresShell::SCROLL_OVERFLOW_HIDDEN); + // mFocusedPopup can be destroyed after ScrollContentIntoView, see bug 420089 + if (mFocusedPopup) { + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput); + mFocusedPopup->OpenAutocompletePopup(this, element); + } + } else + mFocusedPopup->ClosePopup(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetDisableAutoComplete(bool *aDisableAutoComplete) +{ + *aDisableAutoComplete = mDisableAutoComplete; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SetDisableAutoComplete(bool aDisableAutoComplete) +{ + mDisableAutoComplete = aDisableAutoComplete; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetCompleteDefaultIndex(bool *aCompleteDefaultIndex) +{ + *aCompleteDefaultIndex = mCompleteDefaultIndex; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SetCompleteDefaultIndex(bool aCompleteDefaultIndex) +{ + mCompleteDefaultIndex = aCompleteDefaultIndex; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetCompleteSelectedIndex(bool *aCompleteSelectedIndex) +{ + *aCompleteSelectedIndex = mCompleteSelectedIndex; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SetCompleteSelectedIndex(bool aCompleteSelectedIndex) +{ + mCompleteSelectedIndex = aCompleteSelectedIndex; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetForceComplete(bool *aForceComplete) +{ + *aForceComplete = mForceComplete; + return NS_OK; +} + +NS_IMETHODIMP nsFormFillController::SetForceComplete(bool aForceComplete) +{ + mForceComplete = aForceComplete; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetMinResultsForPopup(uint32_t *aMinResultsForPopup) +{ + *aMinResultsForPopup = mMinResultsForPopup; + return NS_OK; +} + +NS_IMETHODIMP nsFormFillController::SetMinResultsForPopup(uint32_t aMinResultsForPopup) +{ + mMinResultsForPopup = aMinResultsForPopup; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetMaxRows(uint32_t *aMaxRows) +{ + *aMaxRows = mMaxRows; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SetMaxRows(uint32_t aMaxRows) +{ + mMaxRows = aMaxRows; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetShowImageColumn(bool *aShowImageColumn) +{ + *aShowImageColumn = false; + return NS_OK; +} + +NS_IMETHODIMP nsFormFillController::SetShowImageColumn(bool aShowImageColumn) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + + +NS_IMETHODIMP +nsFormFillController::GetShowCommentColumn(bool *aShowCommentColumn) +{ + *aShowCommentColumn = false; + return NS_OK; +} + +NS_IMETHODIMP nsFormFillController::SetShowCommentColumn(bool aShowCommentColumn) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsFormFillController::GetTimeout(uint32_t *aTimeout) +{ + *aTimeout = mTimeout; + return NS_OK; +} + +NS_IMETHODIMP nsFormFillController::SetTimeout(uint32_t aTimeout) +{ + mTimeout = aTimeout; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SetSearchParam(const nsAString &aSearchParam) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsFormFillController::GetSearchParam(nsAString &aSearchParam) +{ + if (!mFocusedInput) { + NS_WARNING("mFocusedInput is null for some reason! avoiding a crash. should find out why... - ben"); + return NS_ERROR_FAILURE; // XXX why? fix me. + } + + mFocusedInput->GetName(aSearchParam); + if (aSearchParam.IsEmpty()) { + nsCOMPtr<nsIDOMHTMLElement> element = do_QueryInterface(mFocusedInput); + element->GetId(aSearchParam); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetSearchCount(uint32_t *aSearchCount) +{ + *aSearchCount = 1; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetSearchAt(uint32_t index, nsACString & _retval) +{ + _retval.AssignLiteral("form-history"); + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetTextValue(nsAString & aTextValue) +{ + if (mFocusedInput) { + nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput; + input->GetValue(aTextValue); + } else { + aTextValue.Truncate(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SetTextValue(const nsAString & aTextValue) +{ + nsCOMPtr<nsIDOMNSEditableElement> editable = do_QueryInterface(mFocusedInput); + if (editable) { + mSuppressOnInput = true; + editable->SetUserInput(aTextValue); + mSuppressOnInput = false; + } + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SetTextValueWithReason(const nsAString & aTextValue, + uint16_t aReason) +{ + return SetTextValue(aTextValue); +} + +NS_IMETHODIMP +nsFormFillController::GetSelectionStart(int32_t *aSelectionStart) +{ + if (mFocusedInput) { + nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput; + input->GetSelectionStart(aSelectionStart); + } + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetSelectionEnd(int32_t *aSelectionEnd) +{ + if (mFocusedInput) { + nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput; + input->GetSelectionEnd(aSelectionEnd); + } + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::SelectTextRange(int32_t aStartIndex, int32_t aEndIndex) +{ + if (mFocusedInput) { + nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput; + input->SetSelectionRange(aStartIndex, aEndIndex, EmptyString()); + } + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::OnSearchBegin() +{ + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::OnSearchComplete() +{ + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::OnTextEntered(nsIDOMEvent* aEvent, + bool* aPrevent) +{ + NS_ENSURE_ARG(aPrevent); + NS_ENSURE_TRUE(mFocusedInput, NS_OK); + // Fire off a DOMAutoComplete event + nsCOMPtr<nsIDOMDocument> domDoc; + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput); + element->GetOwnerDocument(getter_AddRefs(domDoc)); + NS_ENSURE_STATE(domDoc); + + nsCOMPtr<nsIDOMEvent> event; + domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event)); + NS_ENSURE_STATE(event); + + event->InitEvent(NS_LITERAL_STRING("DOMAutoComplete"), true, true); + + // XXXjst: We mark this event as a trusted event, it's up to the + // callers of this to ensure that it's only called from trusted + // code. + event->SetTrusted(true); + + nsCOMPtr<EventTarget> targ = do_QueryInterface(mFocusedInput); + + bool defaultActionEnabled; + targ->DispatchEvent(event, &defaultActionEnabled); + *aPrevent = !defaultActionEnabled; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::OnTextReverted(bool *_retval) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetConsumeRollupEvent(bool *aConsumeRollupEvent) +{ + *aConsumeRollupEvent = false; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetInPrivateContext(bool *aInPrivateContext) +{ + if (!mFocusedInput) { + *aInPrivateContext = false; + return NS_OK; + } + + nsCOMPtr<nsIDOMDocument> inputDoc; + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput); + element->GetOwnerDocument(getter_AddRefs(inputDoc)); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(inputDoc); + nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext(); + *aInPrivateContext = loadContext && loadContext->UsePrivateBrowsing(); + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetNoRollupOnCaretMove(bool *aNoRollupOnCaretMove) +{ + *aNoRollupOnCaretMove = false; + return NS_OK; +} + +NS_IMETHODIMP +nsFormFillController::GetUserContextId(uint32_t* aUserContextId) +{ + *aUserContextId = nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +//// nsIAutoCompleteSearch + +NS_IMETHODIMP +nsFormFillController::StartSearch(const nsAString &aSearchString, const nsAString &aSearchParam, + nsIAutoCompleteResult *aPreviousResult, nsIAutoCompleteObserver *aListener) +{ + nsresult rv; + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(mFocusedInputNode); + + // If the login manager has indicated it's responsible for this field, let it + // handle the autocomplete. Otherwise, handle with form history. + // This method is sometimes called in unit tests and from XUL without a focused node. + if (mFocusedInputNode && (mPwmgrInputs.Get(mFocusedInputNode) || + formControl->GetType() == NS_FORM_INPUT_PASSWORD)) { + + // Handle the case where a password field is focused but + // MarkAsLoginManagerField wasn't called because password manager is disabled. + if (!mLoginManager) { + mLoginManager = do_GetService("@mozilla.org/login-manager;1"); + } + + if (NS_WARN_IF(!mLoginManager)) { + return NS_ERROR_FAILURE; + } + + // XXX aPreviousResult shouldn't ever be a historyResult type, since we're not letting + // satchel manage the field? + mLastListener = aListener; + rv = mLoginManager->AutoCompleteSearchAsync(aSearchString, + aPreviousResult, + mFocusedInput, + this); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mLastListener = aListener; + + nsCOMPtr<nsIAutoCompleteResult> datalistResult; + if (mFocusedInput) { + rv = PerformInputListAutoComplete(aSearchString, + getter_AddRefs(datalistResult)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr <nsIFormAutoComplete> formAutoComplete = + do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + formAutoComplete->AutoCompleteSearchAsync(aSearchParam, + aSearchString, + mFocusedInput, + aPreviousResult, + datalistResult, + this); + mLastFormAutoComplete = formAutoComplete; + } + + return NS_OK; +} + +nsresult +nsFormFillController::PerformInputListAutoComplete(const nsAString& aSearch, + nsIAutoCompleteResult** aResult) +{ + // If an <input> is focused, check if it has a list="<datalist>" which can + // provide the list of suggestions. + + MOZ_ASSERT(!mPwmgrInputs.Get(mFocusedInputNode)); + nsresult rv; + + nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete = + do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = inputListAutoComplete->AutoCompleteSearch(aSearch, + mFocusedInput, + aResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (mFocusedInput) { + nsCOMPtr<nsIDOMHTMLElement> list; + mFocusedInput->GetList(getter_AddRefs(list)); + + // Add a mutation observer to check for changes to the items in the <datalist> + // and update the suggestions accordingly. + nsCOMPtr<nsINode> node = do_QueryInterface(list); + if (mListNode != node) { + if (mListNode) { + mListNode->RemoveMutationObserver(this); + mListNode = nullptr; + } + if (node) { + node->AddMutationObserverUnlessExists(this); + mListNode = node; + } + } + } + + return NS_OK; +} + +class UpdateSearchResultRunnable : public mozilla::Runnable +{ +public: + UpdateSearchResultRunnable(nsIAutoCompleteObserver* aObserver, + nsIAutoCompleteSearch* aSearch, + nsIAutoCompleteResult* aResult) + : mObserver(aObserver) + , mSearch(aSearch) + , mResult(aResult) + { + MOZ_ASSERT(mResult, "Should have a valid result"); + MOZ_ASSERT(mObserver, "You shouldn't call this runnable with a null observer!"); + } + + NS_IMETHOD Run() override { + mObserver->OnUpdateSearchResult(mSearch, mResult); + return NS_OK; + } + +private: + nsCOMPtr<nsIAutoCompleteObserver> mObserver; + nsCOMPtr<nsIAutoCompleteSearch> mSearch; + nsCOMPtr<nsIAutoCompleteResult> mResult; +}; + +void nsFormFillController::RevalidateDataList() +{ + if (!mLastListener) { + return; + } + + if (XRE_IsContentProcess()) { + nsCOMPtr<nsIAutoCompleteController> controller(do_QueryInterface(mLastListener)); + if (!controller) { + return; + } + + controller->StartSearch(mLastSearchString); + return; + } + + nsresult rv; + nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete = + do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv); + + nsCOMPtr<nsIAutoCompleteResult> result; + + rv = inputListAutoComplete->AutoCompleteSearch(mLastSearchString, + mFocusedInput, + getter_AddRefs(result)); + + nsCOMPtr<nsIRunnable> event = + new UpdateSearchResultRunnable(mLastListener, this, result); + NS_DispatchToCurrentThread(event); +} + +NS_IMETHODIMP +nsFormFillController::StopSearch() +{ + // Make sure to stop and clear this, otherwise the controller will prevent + // mLastFormAutoComplete from being deleted. + if (mLastFormAutoComplete) { + mLastFormAutoComplete->StopAutoCompleteSearch(); + mLastFormAutoComplete = nullptr; + } else if (mLoginManager) { + mLoginManager->StopSearch(); + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +//// nsIFormAutoCompleteObserver + +NS_IMETHODIMP +nsFormFillController::OnSearchCompletion(nsIAutoCompleteResult *aResult) +{ + nsAutoString searchString; + aResult->GetSearchString(searchString); + + mLastSearchString = searchString; + + if (mLastListener) { + mLastListener->OnSearchResult(this, aResult); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +//// nsIDOMEventListener + +NS_IMETHODIMP +nsFormFillController::HandleEvent(nsIDOMEvent* aEvent) +{ + nsAutoString type; + aEvent->GetType(type); + + if (type.EqualsLiteral("focus")) { + return Focus(aEvent); + } + if (type.EqualsLiteral("mousedown")) { + return MouseDown(aEvent); + } + if (type.EqualsLiteral("keypress")) { + return KeyPress(aEvent); + } + if (type.EqualsLiteral("input")) { + bool unused = false; + return (!mSuppressOnInput && mController && mFocusedInput) ? + mController->HandleText(&unused) : NS_OK; + } + if (type.EqualsLiteral("blur")) { + if (mFocusedInput) + StopControllingInput(); + return NS_OK; + } + if (type.EqualsLiteral("compositionstart")) { + NS_ASSERTION(mController, "should have a controller!"); + if (mController && mFocusedInput) + mController->HandleStartComposition(); + return NS_OK; + } + if (type.EqualsLiteral("compositionend")) { + NS_ASSERTION(mController, "should have a controller!"); + if (mController && mFocusedInput) + mController->HandleEndComposition(); + return NS_OK; + } + if (type.EqualsLiteral("contextmenu")) { + mContextMenuFiredBeforeFocus = true; + if (mFocusedPopup) + mFocusedPopup->ClosePopup(); + return NS_OK; + } + if (type.EqualsLiteral("pagehide")) { + + nsCOMPtr<nsIDocument> doc = do_QueryInterface( + aEvent->InternalDOMEvent()->GetTarget()); + if (!doc) + return NS_OK; + + if (mFocusedInput) { + if (doc == mFocusedInputNode->OwnerDoc()) + StopControllingInput(); + } + + RemoveForDocument(doc); + } + + return NS_OK; +} + +void +nsFormFillController::RemoveForDocument(nsIDocument* aDoc) +{ + for (auto iter = mPwmgrInputs.Iter(); !iter.Done(); iter.Next()) { + const nsINode* key = iter.Key(); + if (key && (!aDoc || key->OwnerDoc() == aDoc)) { + // mFocusedInputNode's observer is tracked separately, so don't remove it + // here. + if (key != mFocusedInputNode) { + const_cast<nsINode*>(key)->RemoveMutationObserver(this); + } + iter.Remove(); + } + } +} + +void +nsFormFillController::MaybeStartControllingInput(nsIDOMHTMLInputElement* aInput) +{ + nsCOMPtr<nsINode> inputNode = do_QueryInterface(aInput); + if (!inputNode) + return; + + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(aInput); + if (!formControl || !formControl->IsSingleLineTextControl(false)) + return; + + bool isReadOnly = false; + aInput->GetReadOnly(&isReadOnly); + if (isReadOnly) + return; + + bool autocomplete = nsContentUtils::IsAutocompleteEnabled(aInput); + + nsCOMPtr<nsIDOMHTMLElement> datalist; + aInput->GetList(getter_AddRefs(datalist)); + bool hasList = datalist != nullptr; + + bool isPwmgrInput = false; + if (mPwmgrInputs.Get(inputNode) || + formControl->GetType() == NS_FORM_INPUT_PASSWORD) { + isPwmgrInput = true; + } + + if (isPwmgrInput || hasList || autocomplete) { + StartControllingInput(aInput); + } +} + +nsresult +nsFormFillController::Focus(nsIDOMEvent* aEvent) +{ + nsCOMPtr<nsIDOMHTMLInputElement> input = do_QueryInterface( + aEvent->InternalDOMEvent()->GetTarget()); + MaybeStartControllingInput(input); + + // Bail if we didn't start controlling the input. + if (!mFocusedInputNode) { + mContextMenuFiredBeforeFocus = false; + return NS_OK; + } + +#ifndef ANDROID + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(mFocusedInputNode); + MOZ_ASSERT(formControl); + + // If this focus doesn't immediately follow a contextmenu event then show + // the autocomplete popup for all password fields. + if (!mContextMenuFiredBeforeFocus + && formControl->GetType() == NS_FORM_INPUT_PASSWORD) { + ShowPopup(); + } +#endif + + mContextMenuFiredBeforeFocus = false; + return NS_OK; +} + +nsresult +nsFormFillController::KeyPress(nsIDOMEvent* aEvent) +{ + NS_ASSERTION(mController, "should have a controller!"); + if (!mFocusedInput || !mController) + return NS_OK; + + nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent); + if (!keyEvent) + return NS_ERROR_FAILURE; + + bool cancel = false; + bool unused = false; + + uint32_t k; + keyEvent->GetKeyCode(&k); + switch (k) { + case nsIDOMKeyEvent::DOM_VK_DELETE: +#ifndef XP_MACOSX + mController->HandleDelete(&cancel); + break; + case nsIDOMKeyEvent::DOM_VK_BACK_SPACE: + mController->HandleText(&unused); + break; +#else + case nsIDOMKeyEvent::DOM_VK_BACK_SPACE: + { + bool isShift = false; + keyEvent->GetShiftKey(&isShift); + + if (isShift) { + mController->HandleDelete(&cancel); + } else { + mController->HandleText(&unused); + } + + break; + } +#endif + case nsIDOMKeyEvent::DOM_VK_PAGE_UP: + case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN: + { + bool isCtrl, isAlt, isMeta; + keyEvent->GetCtrlKey(&isCtrl); + keyEvent->GetAltKey(&isAlt); + keyEvent->GetMetaKey(&isMeta); + if (isCtrl || isAlt || isMeta) + break; + } + MOZ_FALLTHROUGH; + case nsIDOMKeyEvent::DOM_VK_UP: + case nsIDOMKeyEvent::DOM_VK_DOWN: + case nsIDOMKeyEvent::DOM_VK_LEFT: + case nsIDOMKeyEvent::DOM_VK_RIGHT: + { + // Get the writing-mode of the relevant input element, + // so that we can remap arrow keys if necessary. + mozilla::WritingMode wm; + if (mFocusedInputNode && mFocusedInputNode->IsElement()) { + mozilla::dom::Element *elem = mFocusedInputNode->AsElement(); + nsIFrame *frame = elem->GetPrimaryFrame(); + if (frame) { + wm = frame->GetWritingMode(); + } + } + if (wm.IsVertical()) { + switch (k) { + case nsIDOMKeyEvent::DOM_VK_LEFT: + k = wm.IsVerticalLR() ? nsIDOMKeyEvent::DOM_VK_UP + : nsIDOMKeyEvent::DOM_VK_DOWN; + break; + case nsIDOMKeyEvent::DOM_VK_RIGHT: + k = wm.IsVerticalLR() ? nsIDOMKeyEvent::DOM_VK_DOWN + : nsIDOMKeyEvent::DOM_VK_UP; + break; + case nsIDOMKeyEvent::DOM_VK_UP: + k = nsIDOMKeyEvent::DOM_VK_LEFT; + break; + case nsIDOMKeyEvent::DOM_VK_DOWN: + k = nsIDOMKeyEvent::DOM_VK_RIGHT; + break; + } + } + } + mController->HandleKeyNavigation(k, &cancel); + break; + case nsIDOMKeyEvent::DOM_VK_ESCAPE: + mController->HandleEscape(&cancel); + break; + case nsIDOMKeyEvent::DOM_VK_TAB: + mController->HandleTab(); + cancel = false; + break; + case nsIDOMKeyEvent::DOM_VK_RETURN: + mController->HandleEnter(false, aEvent, &cancel); + break; + } + + if (cancel) { + aEvent->PreventDefault(); + // Don't let the page see the RETURN event when the popup is open + // (indicated by cancel=true) so sites don't manually submit forms + // (e.g. via submit.click()) without the autocompleted value being filled. + // Bug 286933 will fix this for other key events. + if (k == nsIDOMKeyEvent::DOM_VK_RETURN) { + aEvent->StopPropagation(); + } + } + + return NS_OK; +} + +nsresult +nsFormFillController::MouseDown(nsIDOMEvent* aEvent) +{ + nsCOMPtr<nsIDOMMouseEvent> mouseEvent(do_QueryInterface(aEvent)); + if (!mouseEvent) + return NS_ERROR_FAILURE; + + nsCOMPtr<nsIDOMHTMLInputElement> targetInput = do_QueryInterface( + aEvent->InternalDOMEvent()->GetTarget()); + if (!targetInput) + return NS_OK; + + int16_t button; + mouseEvent->GetButton(&button); + if (button != 0) + return NS_OK; + + return ShowPopup(); +} + +NS_IMETHODIMP +nsFormFillController::ShowPopup() +{ + bool isOpen = false; + GetPopupOpen(&isOpen); + if (isOpen) { + return SetPopupOpen(false); + } + + nsCOMPtr<nsIAutoCompleteInput> input; + mController->GetInput(getter_AddRefs(input)); + if (!input) + return NS_OK; + + nsAutoString value; + input->GetTextValue(value); + if (value.Length() > 0) { + // Show the popup with a filtered result set + mController->SetSearchString(EmptyString()); + bool unused = false; + mController->HandleText(&unused); + } else { + // Show the popup with the complete result set. Can't use HandleText() + // because it doesn't display the popup if the input is blank. + bool cancel = false; + mController->HandleKeyNavigation(nsIDOMKeyEvent::DOM_VK_DOWN, &cancel); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +//// nsFormFillController + +void +nsFormFillController::AddWindowListeners(nsPIDOMWindowOuter* aWindow) +{ + if (!aWindow) + return; + + EventTarget* target = aWindow->GetChromeEventHandler(); + + if (!target) + return; + + target->AddEventListener(NS_LITERAL_STRING("focus"), this, + true, false); + target->AddEventListener(NS_LITERAL_STRING("blur"), this, + true, false); + target->AddEventListener(NS_LITERAL_STRING("pagehide"), this, + true, false); + target->AddEventListener(NS_LITERAL_STRING("mousedown"), this, + true, false); + target->AddEventListener(NS_LITERAL_STRING("input"), this, + true, false); + target->AddEventListener(NS_LITERAL_STRING("keypress"), this, true, false); + target->AddEventListener(NS_LITERAL_STRING("compositionstart"), this, + true, false); + target->AddEventListener(NS_LITERAL_STRING("compositionend"), this, + true, false); + target->AddEventListener(NS_LITERAL_STRING("contextmenu"), this, + true, false); + + // Note that any additional listeners added should ensure that they ignore + // untrusted events, which might be sent by content that's up to no good. +} + +void +nsFormFillController::RemoveWindowListeners(nsPIDOMWindowOuter* aWindow) +{ + if (!aWindow) + return; + + StopControllingInput(); + + nsCOMPtr<nsIDocument> doc = aWindow->GetDoc(); + RemoveForDocument(doc); + + EventTarget* target = aWindow->GetChromeEventHandler(); + + if (!target) + return; + + target->RemoveEventListener(NS_LITERAL_STRING("focus"), this, true); + target->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true); + target->RemoveEventListener(NS_LITERAL_STRING("pagehide"), this, true); + target->RemoveEventListener(NS_LITERAL_STRING("mousedown"), this, true); + target->RemoveEventListener(NS_LITERAL_STRING("input"), this, true); + target->RemoveEventListener(NS_LITERAL_STRING("keypress"), this, true); + target->RemoveEventListener(NS_LITERAL_STRING("compositionstart"), this, + true); + target->RemoveEventListener(NS_LITERAL_STRING("compositionend"), this, + true); + target->RemoveEventListener(NS_LITERAL_STRING("contextmenu"), this, true); +} + +void +nsFormFillController::StartControllingInput(nsIDOMHTMLInputElement *aInput) +{ + // Make sure we're not still attached to an input + StopControllingInput(); + + if (!mController) { + return; + } + + // Find the currently focused docShell + nsCOMPtr<nsIDocShell> docShell = GetDocShellForInput(aInput); + int32_t index = GetIndexOfDocShell(docShell); + if (index < 0) + return; + + // Cache the popup for the focused docShell + mFocusedPopup = mPopups.SafeElementAt(index); + + nsCOMPtr<nsINode> node = do_QueryInterface(aInput); + if (!node) { + return; + } + + node->AddMutationObserverUnlessExists(this); + mFocusedInputNode = node; + mFocusedInput = aInput; + + nsCOMPtr<nsIDOMHTMLElement> list; + mFocusedInput->GetList(getter_AddRefs(list)); + nsCOMPtr<nsINode> listNode = do_QueryInterface(list); + if (listNode) { + listNode->AddMutationObserverUnlessExists(this); + mListNode = listNode; + } + + mController->SetInput(this); +} + +void +nsFormFillController::StopControllingInput() +{ + if (mListNode) { + mListNode->RemoveMutationObserver(this); + mListNode = nullptr; + } + + if (mController) { + // Reset the controller's input, but not if it has been switched + // to another input already, which might happen if the user switches + // focus by clicking another autocomplete textbox + nsCOMPtr<nsIAutoCompleteInput> input; + mController->GetInput(getter_AddRefs(input)); + if (input == this) + mController->SetInput(nullptr); + } + + if (mFocusedInputNode) { + MaybeRemoveMutationObserver(mFocusedInputNode); + + nsresult rv; + nsCOMPtr <nsIFormAutoComplete> formAutoComplete = + do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv); + if (formAutoComplete) { + formAutoComplete->StopControllingInput(mFocusedInput); + } + + mFocusedInputNode = nullptr; + mFocusedInput = nullptr; + } + + if (mFocusedPopup) { + mFocusedPopup->ClosePopup(); + } + mFocusedPopup = nullptr; +} + +nsIDocShell * +nsFormFillController::GetDocShellForInput(nsIDOMHTMLInputElement *aInput) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aInput); + NS_ENSURE_TRUE(node, nullptr); + + nsCOMPtr<nsPIDOMWindowOuter> win = node->OwnerDoc()->GetWindow(); + NS_ENSURE_TRUE(win, nullptr); + + return win->GetDocShell(); +} + +nsPIDOMWindowOuter* +nsFormFillController::GetWindowForDocShell(nsIDocShell *aDocShell) +{ + nsCOMPtr<nsIContentViewer> contentViewer; + aDocShell->GetContentViewer(getter_AddRefs(contentViewer)); + NS_ENSURE_TRUE(contentViewer, nullptr); + + nsCOMPtr<nsIDOMDocument> domDoc; + contentViewer->GetDOMDocument(getter_AddRefs(domDoc)); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc); + NS_ENSURE_TRUE(doc, nullptr); + + return doc->GetWindow(); +} + +int32_t +nsFormFillController::GetIndexOfDocShell(nsIDocShell *aDocShell) +{ + if (!aDocShell) + return -1; + + // Loop through our cached docShells looking for the given docShell + uint32_t count = mDocShells.Length(); + for (uint32_t i = 0; i < count; ++i) { + if (mDocShells[i] == aDocShell) + return i; + } + + // Recursively check the parent docShell of this one + nsCOMPtr<nsIDocShellTreeItem> treeItem = do_QueryInterface(aDocShell); + nsCOMPtr<nsIDocShellTreeItem> parentItem; + treeItem->GetParent(getter_AddRefs(parentItem)); + if (parentItem) { + nsCOMPtr<nsIDocShell> parentShell = do_QueryInterface(parentItem); + return GetIndexOfDocShell(parentShell); + } + + return -1; +} + +NS_GENERIC_FACTORY_CONSTRUCTOR(nsFormFillController) + +NS_DEFINE_NAMED_CID(NS_FORMFILLCONTROLLER_CID); + +static const mozilla::Module::CIDEntry kSatchelCIDs[] = { + { &kNS_FORMFILLCONTROLLER_CID, false, nullptr, nsFormFillControllerConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kSatchelContracts[] = { + { "@mozilla.org/satchel/form-fill-controller;1", &kNS_FORMFILLCONTROLLER_CID }, + { NS_FORMHISTORYAUTOCOMPLETE_CONTRACTID, &kNS_FORMFILLCONTROLLER_CID }, + { nullptr } +}; + +static const mozilla::Module kSatchelModule = { + mozilla::Module::kVersion, + kSatchelCIDs, + kSatchelContracts +}; + +NSMODULE_DEFN(satchel) = &kSatchelModule; |