/* -*- 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/ClearOnShutdown.h" #include "mozilla/ErrorResult.h" #include "mozilla/EventListenerManager.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Event.h" // for Event #include "mozilla/dom/HTMLInputElement.h" #include "mozilla/dom/KeyboardEvent.h" #include "mozilla/dom/KeyboardEventBinding.h" #include "mozilla/dom/MouseEvent.h" #include "mozilla/dom/PageTransitionEvent.h" #include "mozilla/Logging.h" #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 "nsIDocument.h" #include "nsIContent.h" #include "nsIPresShell.h" #include "nsRect.h" #include "nsILoginManager.h" #include "mozilla/ModuleUtils.h" #include "nsToolkitCompsCID.h" #include "nsEmbedCID.h" #include "nsContentUtils.h" #include "nsGenericHTMLElement.h" #include "nsILoadContext.h" #include "nsIFrame.h" #include "nsIScriptSecurityManager.h" #include "nsFocusManager.h" using namespace mozilla; using namespace mozilla::dom; using mozilla::ErrorResult; using mozilla::LogLevel; static mozilla::LazyLogModule sLogger("satchel"); static nsIFormAutoComplete* GetFormAutoComplete() { static nsCOMPtr sInstance; static bool sInitialized = false; if (!sInitialized) { nsresult rv; sInstance = do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv); if (NS_SUCCEEDED(rv)) { ClearOnShutdown(&sInstance); sInitialized = true; } } return sInstance; } NS_IMPL_CYCLE_COLLECTION(nsFormFillController, mController, mLoginManager, mLoginReputationService, 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), mListNode(nullptr), // The amount of time a context menu event supresses showing a // popup from a focus event in ms. This matches the threshold in // toolkit/components/passwordmgr/LoginManagerContent.jsm. mFocusAfterRightClickThreshold(400), mTimeout(50), mMinResultsForPopup(1), mMaxRows(0), mLastRightClickTimeStamp(TimeStamp()), 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 (mFocusedInput) { MaybeRemoveMutationObserver(mFocusedInput); 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 window = GetWindowForDocShell(mDocShells[i]); RemoveWindowListeners(window); } } //////////////////////////////////////////////////////////////////////// //// nsIMutationObserver // void nsFormFillController::AttributeChanged(mozilla::dom::Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType, const nsAttrValue* aOldValue) { if ((aAttribute == nsGkAtoms::type || aAttribute == nsGkAtoms::readonly || aAttribute == nsGkAtoms::autocomplete) && aNameSpaceID == kNameSpaceID_None) { RefPtr 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 event = mozilla::NewRunnableMethod>( "nsFormFillController::MaybeStartControllingInput", this, &nsFormFillController::MaybeStartControllingInput, focusedInput); aElement->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); } if (mListNode && mListNode->Contains(aElement)) { RevalidateDataList(); } } void nsFormFillController::ContentAppended(nsIContent* aChild) { if (mListNode && mListNode->Contains(aChild->GetParent())) { RevalidateDataList(); } } void nsFormFillController::ContentInserted(nsIContent* aChild) { if (mListNode && mListNode->Contains(aChild->GetParent())) { RevalidateDataList(); } } void nsFormFillController::ContentRemoved(nsIContent* aChild, nsIContent* aPreviousSibling) { if (mListNode && mListNode->Contains(aChild->GetParent())) { RevalidateDataList(); } } void nsFormFillController::CharacterDataWillChange(nsIContent* aContent, const CharacterDataChangeInfo&) { } void nsFormFillController::CharacterDataChanged(nsIContent* aContent, const CharacterDataChangeInfo&) { } void nsFormFillController::AttributeWillChange(mozilla::dom::Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType, const nsAttrValue* aNewValue) { } void nsFormFillController::NativeAnonymousChildListChange(nsIContent* aContent, bool aIsRemove) { } void nsFormFillController::ParentChainChanged(nsIContent* aContent) { } void nsFormFillController::NodeWillBeDestroyed(const nsINode* aNode) { MOZ_LOG(sLogger, LogLevel::Verbose, ("NodeWillBeDestroyed: %p", aNode)); mPwmgrInputs.Remove(aNode); mAutofillInputs.Remove(aNode); if (aNode == mListNode) { mListNode = nullptr; RevalidateDataList(); } else if (aNode == mFocusedInput) { 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) && !mAutofillInputs.Get(aNode)) { aNode->RemoveMutationObserver(this); } } //////////////////////////////////////////////////////////////////////// //// nsIFormFillController NS_IMETHODIMP nsFormFillController::AttachToBrowser(nsIDocShell *aDocShell, nsIAutoCompletePopup *aPopup) { MOZ_LOG(sLogger, LogLevel::Debug, ("AttachToBrowser for docShell %p with popup %p", aDocShell, 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 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 window = GetWindowForDocShell(mDocShells.SafeElementAt(index)); RemoveWindowListeners(window); mDocShells.RemoveElementAt(index); mPopups.RemoveElementAt(index); return NS_OK; } NS_IMETHODIMP nsFormFillController::MarkAsLoginManagerField(HTMLInputElement *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). */ NS_ENSURE_STATE(aInput); // If the field was already marked, we don't want to show the popup again. if (mPwmgrInputs.Get(aInput)) { return NS_OK; } mPwmgrInputs.Put(aInput, true); aInput->AddMutationObserverUnlessExists(this); nsFocusManager *fm = nsFocusManager::GetFocusManager(); if (fm) { nsCOMPtr focusedContent = fm->GetFocusedElement(); if (focusedContent == aInput) { if (!mFocusedInput) { MaybeStartControllingInput(aInput); } } } if (!mLoginManager) { mLoginManager = do_GetService("@mozilla.org/login-manager;1"); } return NS_OK; } NS_IMETHODIMP nsFormFillController::MarkAsAutofillField(HTMLInputElement *aInput) { /* * Support other components implementing form autofill and handle autocomplete * for the field. */ NS_ENSURE_STATE(aInput); MOZ_LOG(sLogger, LogLevel::Verbose, ("MarkAsAutofillField: aInput = %p", aInput)); if (mAutofillInputs.Get(aInput)) { return NS_OK; } mAutofillInputs.Put(aInput, true); aInput->AddMutationObserverUnlessExists(this); aInput->EnablePreview(); nsFocusManager *fm = nsFocusManager::GetFocusManager(); if (fm) { nsCOMPtr focusedContent = fm->GetFocusedElement(); if (focusedContent == aInput) { MaybeStartControllingInput(aInput); } } return NS_OK; } NS_IMETHODIMP nsFormFillController::GetFocusedInput(HTMLInputElement **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 content = mFocusedInput; NS_ENSURE_STATE(content); nsCOMPtr docShell = GetDocShellForInput(mFocusedInput); NS_ENSURE_STATE(docShell); nsCOMPtr 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) { mFocusedPopup->OpenAutocompletePopup(this, mFocusedInput); } } 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::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()) { mFocusedInput->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) { if (mAutofillInputs.Get(mFocusedInput)) { MOZ_LOG(sLogger, LogLevel::Debug, ("GetSearchAt: autofill-profiles field")); nsCOMPtr profileSearch = do_GetService("@mozilla.org/autocomplete/search;1?name=autofill-profiles"); if (profileSearch) { _retval.AssignLiteral("autofill-profiles"); return NS_OK; } } MOZ_LOG(sLogger, LogLevel::Debug, ("GetSearchAt: form-history field")); _retval.AssignLiteral("form-history"); return NS_OK; } NS_IMETHODIMP nsFormFillController::GetTextValue(nsAString & aTextValue) { if (mFocusedInput) { mFocusedInput->GetValue(aTextValue, CallerType::System); } else { aTextValue.Truncate(); } return NS_OK; } NS_IMETHODIMP nsFormFillController::SetTextValue(const nsAString & aTextValue) { if (mFocusedInput) { mSuppressOnInput = true; mFocusedInput->SetUserInput(aTextValue, *nsContentUtils::GetSystemPrincipal()); 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) { return NS_ERROR_UNEXPECTED; } ErrorResult rv; *aSelectionStart = mFocusedInput->GetSelectionStartIgnoringType(rv); return rv.StealNSResult(); } NS_IMETHODIMP nsFormFillController::GetSelectionEnd(int32_t *aSelectionEnd) { if (!mFocusedInput) { return NS_ERROR_UNEXPECTED; } ErrorResult rv; *aSelectionEnd = mFocusedInput->GetSelectionEndIgnoringType(rv); return rv.StealNSResult(); } NS_IMETHODIMP nsFormFillController::SelectTextRange(int32_t aStartIndex, int32_t aEndIndex) { if (!mFocusedInput) { return NS_ERROR_UNEXPECTED; } ErrorResult rv; mFocusedInput->SetSelectionRange(aStartIndex, aEndIndex, Optional(), rv); return rv.StealNSResult(); } NS_IMETHODIMP nsFormFillController::OnSearchBegin() { return NS_OK; } NS_IMETHODIMP nsFormFillController::OnSearchComplete() { return NS_OK; } NS_IMETHODIMP nsFormFillController::OnTextEntered(Event* aEvent, bool* aPrevent) { NS_ENSURE_ARG(aPrevent); NS_ENSURE_TRUE(mFocusedInput, NS_OK); // Fire off a DOMAutoComplete event IgnoredErrorResult ignored; RefPtr event = mFocusedInput->OwnerDoc()-> CreateEvent(NS_LITERAL_STRING("Events"), CallerType::System, ignored); 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); bool defaultActionEnabled = mFocusedInput->DispatchEvent(*event, CallerType::System, IgnoreErrors()); *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 doc = mFocusedInput->OwnerDoc(); nsCOMPtr 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) { MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch for %p", mFocusedInput)); nsresult rv; // 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 (mFocusedInput && (mPwmgrInputs.Get(mFocusedInput) || mFocusedInput->ControlType() == NS_FORM_INPUT_PASSWORD)) { MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: login field")); // 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 { MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: non-login field")); mLastListener = aListener; nsCOMPtr datalistResult; if (mFocusedInput) { rv = PerformInputListAutoComplete(aSearchString, getter_AddRefs(datalistResult)); NS_ENSURE_SUCCESS(rv, rv); } auto formAutoComplete = GetFormAutoComplete(); NS_ENSURE_TRUE(formAutoComplete, NS_ERROR_FAILURE); formAutoComplete->AutoCompleteSearchAsync(aSearchParam, aSearchString, mFocusedInput, aPreviousResult, datalistResult, this); mLastFormAutoComplete = formAutoComplete; } return NS_OK; } nsresult nsFormFillController::PerformInputListAutoComplete(const nsAString& aSearch, nsIAutoCompleteResult** aResult) { // If an is focused, check if it has a list="" which can // provide the list of suggestions. MOZ_ASSERT(!mPwmgrInputs.Get(mFocusedInput)); nsresult rv; nsCOMPtr 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) { Element* list = mFocusedInput->GetList(); // Add a mutation observer to check for changes to the items in the // and update the suggestions accordingly. if (mListNode != list) { if (mListNode) { mListNode->RemoveMutationObserver(this); mListNode = nullptr; } if (list) { list->AddMutationObserverUnlessExists(this); mListNode = list; } } } return NS_OK; } void nsFormFillController::RevalidateDataList() { if (!mLastListener) { return; } nsCOMPtr controller(do_QueryInterface(mLastListener)); if (!controller) { return; } controller->StartSearch(mLastSearchString); } 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; } nsresult nsFormFillController::StartQueryLoginReputation(HTMLInputElement *aInput) { 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(Event* aEvent) { WidgetEvent* internalEvent = aEvent->WidgetEventPtr(); NS_ENSURE_STATE(internalEvent); switch (internalEvent->mMessage) { case eFocus: return Focus(aEvent); case eMouseDown: return MouseDown(aEvent); case eKeyDown: return KeyDown(aEvent); case eKeyPress: return KeyPress(aEvent); case eEditorInput: { nsCOMPtr input = do_QueryInterface(aEvent->GetTarget()); if (!IsTextControl(input)) { return NS_OK; } bool unused = false; return (!mSuppressOnInput && mController && mFocusedInput) ? mController->HandleText(&unused) : NS_OK; } case eBlur: if (mFocusedInput) { StopControllingInput(); } return NS_OK; case eCompositionStart: NS_ASSERTION(mController, "should have a controller!"); if (mController && mFocusedInput) { mController->HandleStartComposition(); } return NS_OK; case eCompositionEnd: NS_ASSERTION(mController, "should have a controller!"); if (mController && mFocusedInput) { mController->HandleEndComposition(); } return NS_OK; case eContextMenu: if (mFocusedPopup) { mFocusedPopup->ClosePopup(); } return NS_OK; case ePageHide: { nsCOMPtr doc = do_QueryInterface(aEvent->GetTarget()); if (!doc) { return NS_OK; } if (mFocusedInput) { if (doc == mFocusedInput->OwnerDoc()) { StopControllingInput(); } } // Only remove the observer notifications and marked autofill and password // manager fields if the page isn't going to be persisted (i.e. it's being // unloaded) so that appropriate autocomplete handling works with bfcache. bool persisted = aEvent->AsPageTransitionEvent()->Persisted(); if (!persisted) { RemoveForDocument(doc); } } break; default: // Handling the default case to shut up stupid -Wswitch warnings. // One day compilers will be smarter... break; } return NS_OK; } void nsFormFillController::RemoveForDocument(nsIDocument* aDoc) { MOZ_LOG(sLogger, LogLevel::Verbose, ("RemoveForDocument: %p", aDoc)); for (auto iter = mPwmgrInputs.Iter(); !iter.Done(); iter.Next()) { const nsINode* key = iter.Key(); if (key && (!aDoc || key->OwnerDoc() == aDoc)) { // mFocusedInput's observer is tracked separately, so don't remove it // here. if (key != mFocusedInput) { const_cast(key)->RemoveMutationObserver(this); } iter.Remove(); } } for (auto iter = mAutofillInputs.Iter(); !iter.Done(); iter.Next()) { const nsINode* key = iter.Key(); if (key && (!aDoc || key->OwnerDoc() == aDoc)) { // mFocusedInput's observer is tracked separately, so don't remove it // here. if (key != mFocusedInput) { const_cast(key)->RemoveMutationObserver(this); } iter.Remove(); } } } bool nsFormFillController::IsTextControl(nsINode* aNode) { nsCOMPtr formControl = do_QueryInterface(aNode); return formControl && formControl->IsSingleLineTextControl(false); } void nsFormFillController::MaybeStartControllingInput(HTMLInputElement* aInput) { MOZ_LOG(sLogger, LogLevel::Verbose, ("MaybeStartControllingInput for %p", aInput)); if (!aInput) { return; } if (!IsTextControl(aInput)) { return; } if (aInput->ReadOnly()) { return; } bool autocomplete = nsContentUtils::IsAutocompleteEnabled(aInput); bool hasList = aInput->GetList() != nullptr; bool isPwmgrInput = false; if (mPwmgrInputs.Get(aInput) || aInput->ControlType() == NS_FORM_INPUT_PASSWORD) { isPwmgrInput = true; } bool isAutofillInput = false; if (mAutofillInputs.Get(aInput)) { isAutofillInput = true; } if (isAutofillInput || isPwmgrInput || hasList || autocomplete) { StartControllingInput(aInput); } #ifdef NIGHTLY_BUILD // Trigger an asynchronous login reputation query when user focuses on the // password field. if (aInput->ControlType() == NS_FORM_INPUT_PASSWORD) { StartQueryLoginReputation(aInput); } #endif } nsresult nsFormFillController::Focus(Event* aEvent) { nsCOMPtr input = do_QueryInterface(aEvent->GetTarget()); MaybeStartControllingInput(HTMLInputElement::FromNodeOrNull(input)); // Bail if we didn't start controlling the input. if (!mFocusedInput) { return NS_OK; } #ifndef ANDROID // If this focus doesn't follow a right click within our specified // threshold then show the autocomplete popup for all password fields. // This is done to avoid showing both the context menu and the popup // at the same time. // We use a timestamp instead of a bool to avoid complexity when dealing with // multiple input forms and the fact that a mousedown into an already focused // field does not trigger another focus. if (mFocusedInput->ControlType() != NS_FORM_INPUT_PASSWORD) { return NS_OK; } // If we have not seen a right click yet, just show the popup. if (mLastRightClickTimeStamp.IsNull()) { ShowPopup(); return NS_OK; } uint64_t timeDiff = (TimeStamp::Now() - mLastRightClickTimeStamp).ToMilliseconds(); if (timeDiff > mFocusAfterRightClickThreshold) { ShowPopup(); } #endif return NS_OK; } nsresult nsFormFillController::KeyDown(Event* aEvent) { NS_ASSERTION(mController, "should have a controller!"); if (!mFocusedInput || !mController) { return NS_OK; } RefPtr keyEvent = aEvent->AsKeyboardEvent(); if (!keyEvent) { return NS_ERROR_FAILURE; } bool cancel = false; uint32_t k = keyEvent->KeyCode(); switch (k) { case KeyboardEvent_Binding::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 == KeyboardEvent_Binding::DOM_VK_RETURN) { aEvent->StopPropagation(); } } return NS_OK; } nsresult nsFormFillController::KeyPress(Event* aEvent) { NS_ASSERTION(mController, "should have a controller!"); if (!mFocusedInput || !mController) { return NS_OK; } RefPtr keyEvent = aEvent->AsKeyboardEvent(); if (!keyEvent) { return NS_ERROR_FAILURE; } bool cancel = false; bool unused = false; uint32_t k = keyEvent->KeyCode(); switch (k) { case KeyboardEvent_Binding::DOM_VK_DELETE: #ifndef XP_MACOSX mController->HandleDelete(&cancel); break; case KeyboardEvent_Binding::DOM_VK_BACK_SPACE: mController->HandleText(&unused); break; #else case KeyboardEvent_Binding::DOM_VK_BACK_SPACE: { if (keyEvent->ShiftKey()) { mController->HandleDelete(&cancel); } else { mController->HandleText(&unused); } break; } #endif case KeyboardEvent_Binding::DOM_VK_PAGE_UP: case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN: { if (keyEvent->CtrlKey() || keyEvent->AltKey() || keyEvent->MetaKey()) { break; } } MOZ_FALLTHROUGH; case KeyboardEvent_Binding::DOM_VK_UP: case KeyboardEvent_Binding::DOM_VK_DOWN: case KeyboardEvent_Binding::DOM_VK_LEFT: case KeyboardEvent_Binding::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 (mFocusedInput) { nsIFrame *frame = mFocusedInput->GetPrimaryFrame(); if (frame) { wm = frame->GetWritingMode(); } } if (wm.IsVertical()) { switch (k) { case KeyboardEvent_Binding::DOM_VK_LEFT: k = wm.IsVerticalLR() ? KeyboardEvent_Binding::DOM_VK_UP : KeyboardEvent_Binding::DOM_VK_DOWN; break; case KeyboardEvent_Binding::DOM_VK_RIGHT: k = wm.IsVerticalLR() ? KeyboardEvent_Binding::DOM_VK_DOWN : KeyboardEvent_Binding::DOM_VK_UP; break; case KeyboardEvent_Binding::DOM_VK_UP: k = KeyboardEvent_Binding::DOM_VK_LEFT; break; case KeyboardEvent_Binding::DOM_VK_DOWN: k = KeyboardEvent_Binding::DOM_VK_RIGHT; break; } } } mController->HandleKeyNavigation(k, &cancel); break; case KeyboardEvent_Binding::DOM_VK_ESCAPE: mController->HandleEscape(&cancel); break; case KeyboardEvent_Binding::DOM_VK_TAB: mController->HandleTab(); cancel = false; break; } if (cancel) { aEvent->PreventDefault(); } return NS_OK; } nsresult nsFormFillController::MouseDown(Event* aEvent) { MouseEvent* mouseEvent = aEvent->AsMouseEvent(); if (!mouseEvent) { return NS_ERROR_FAILURE; } nsCOMPtr targetNode = do_QueryInterface(aEvent->GetTarget()); if (!HTMLInputElement::FromNodeOrNull(targetNode)) { return NS_OK; } int16_t button = mouseEvent->Button(); // In case of a right click we set a timestamp that // will be checked in Focus() to avoid showing // both contextmenu and popup at the same time. if (button == 2) { mLastRightClickTimeStamp = TimeStamp::Now(); return NS_OK; } if (button != 0) { return NS_OK; } return ShowPopup(); } NS_IMETHODIMP nsFormFillController::ShowPopup() { bool isOpen = false; GetPopupOpen(&isOpen); if (isOpen) { return SetPopupOpen(false); } nsCOMPtr 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(KeyboardEvent_Binding::DOM_VK_DOWN, &cancel); } return NS_OK; } //////////////////////////////////////////////////////////////////////// //// nsFormFillController void nsFormFillController::AddWindowListeners(nsPIDOMWindowOuter* aWindow) { MOZ_LOG(sLogger, LogLevel::Debug, ("AddWindowListeners for window %p", aWindow)); if (!aWindow) { return; } EventTarget* target = aWindow->GetChromeEventHandler(); if (!target) { return; } EventListenerManager* elm = target->GetOrCreateListenerManager(); if (NS_WARN_IF(!elm)) { return; } elm->AddEventListenerByType(this, NS_LITERAL_STRING("focus"), TrustedEventsAtCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("blur"), TrustedEventsAtCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("pagehide"), TrustedEventsAtCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("mousedown"), TrustedEventsAtCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("input"), TrustedEventsAtCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("keydown"), TrustedEventsAtCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("keypress"), TrustedEventsAtSystemGroupCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("compositionstart"), TrustedEventsAtCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("compositionend"), TrustedEventsAtCapture()); elm->AddEventListenerByType(this, NS_LITERAL_STRING("contextmenu"), TrustedEventsAtCapture()); // 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) { MOZ_LOG(sLogger, LogLevel::Debug, ("RemoveWindowListeners for window %p", aWindow)); if (!aWindow) { return; } StopControllingInput(); nsCOMPtr doc = aWindow->GetDoc(); RemoveForDocument(doc); EventTarget* target = aWindow->GetChromeEventHandler(); if (!target) { return; } EventListenerManager* elm = target->GetOrCreateListenerManager(); if (NS_WARN_IF(!elm)) { return; } elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("focus"), TrustedEventsAtCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("blur"), TrustedEventsAtCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("pagehide"), TrustedEventsAtCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("mousedown"), TrustedEventsAtCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("input"), TrustedEventsAtCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("keydown"), TrustedEventsAtCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("keypress"), TrustedEventsAtSystemGroupCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("compositionstart"), TrustedEventsAtCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("compositionend"), TrustedEventsAtCapture()); elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("contextmenu"), TrustedEventsAtCapture()); } void nsFormFillController::StartControllingInput(HTMLInputElement *aInput) { MOZ_LOG(sLogger, LogLevel::Verbose, ("StartControllingInput for %p", aInput)); // Make sure we're not still attached to an input StopControllingInput(); if (!mController) { return; } // Find the currently focused docShell nsCOMPtr docShell = GetDocShellForInput(aInput); int32_t index = GetIndexOfDocShell(docShell); if (index < 0) { return; } MOZ_ASSERT(aInput, "How did we get a docshell index??"); // Cache the popup for the focused docShell mFocusedPopup = mPopups.SafeElementAt(index); aInput->AddMutationObserverUnlessExists(this); mFocusedInput = aInput; Element* list = mFocusedInput->GetList(); if (list) { list->AddMutationObserverUnlessExists(this); mListNode = list; } 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 input; mController->GetInput(getter_AddRefs(input)); if (input == this) { MOZ_LOG(sLogger, LogLevel::Verbose, ("StopControllingInput: Nulled controller input for %p", this)); mController->SetInput(nullptr); } } MOZ_LOG(sLogger, LogLevel::Verbose, ("StopControllingInput: Stopped controlling %p", mFocusedInput)); if (mFocusedInput) { MaybeRemoveMutationObserver(mFocusedInput); mFocusedInput = nullptr; } if (mFocusedPopup) { mFocusedPopup->ClosePopup(); } mFocusedPopup = nullptr; } nsIDocShell * nsFormFillController::GetDocShellForInput(HTMLInputElement *aInput) { NS_ENSURE_TRUE(aInput, nullptr); nsCOMPtr win = aInput->OwnerDoc()->GetWindow(); NS_ENSURE_TRUE(win, nullptr); return win->GetDocShell(); } nsPIDOMWindowOuter* nsFormFillController::GetWindowForDocShell(nsIDocShell *aDocShell) { nsCOMPtr contentViewer; aDocShell->GetContentViewer(getter_AddRefs(contentViewer)); NS_ENSURE_TRUE(contentViewer, nullptr); nsCOMPtr doc = contentViewer->GetDocument(); 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 treeItem = aDocShell; nsCOMPtr parentItem; treeItem->GetParent(getter_AddRefs(parentItem)); if (parentItem) { nsCOMPtr 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;