/* -*- 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/Document.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Event.h" // for Event #include "mozilla/dom/HTMLDataListElement.h" #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 "mozilla/PresShell.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_ui.h" #include "nsCRT.h" #include "nsIFormHistoryAutoComplete.h" #include "nsString.h" #include "nsPIDOMWindow.h" #include "nsIAutoCompleteResult.h" #include "nsIContent.h" #include "nsInterfaceHashtable.h" #include "nsContentUtils.h" #include "nsGenericHTMLElement.h" #include "nsILoadContext.h" #include "nsIFrame.h" #include "nsIScriptSecurityManager.h" #include "nsFocusManager.h" #include "nsQueryActor.h" #include "nsQueryObject.h" #include "nsServiceManagerUtils.h" #include "xpcpublic.h" using namespace mozilla; using namespace mozilla::dom; using mozilla::ErrorResult; using mozilla::LogLevel; static mozilla::LazyLogModule sLogger("satchel"); static nsIFormHistoryAutoComplete* GetFormHistoryAutoComplete() { static nsCOMPtr sInstance; static bool sInitialized = false; if (!sInitialized) { nsresult rv; sInstance = do_GetService("@mozilla.org/satchel/form-history-autocomplete;1", &rv); if (NS_SUCCEEDED(rv)) { ClearOnShutdown(&sInstance); sInitialized = true; } } return sInstance; } NS_IMPL_CYCLE_COLLECTION(nsFormFillController, mController, mLoginManagerAC, mFocusedPopup, mPopups, mLastListener, mLastFormHistoryAutoComplete) 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(nsIFormFillCompleteObserver) NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) NS_INTERFACE_MAP_ENTRY(nsIObserver) 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/LoginManagerChild.sys.mjs. mFocusAfterRightClickThreshold(400), mTimeout(50), mMinResultsForPopup(1), mMaxRows(0), mDisableAutoComplete(false), mCompleteDefaultIndex(false), mCompleteSelectedIndex(false), mForceComplete(false), mSuppressOnInput(false), mPasswordPopupAutomaticallyOpened(false) { mController = do_GetService("@mozilla.org/autocomplete/controller;1"); MOZ_ASSERT(mController); nsCOMPtr obs = mozilla::services::GetObserverService(); MOZ_ASSERT(obs); obs->AddObserver(this, "chrome-event-target-created", false); obs->AddObserver(this, "autofill-fill-starting", false); obs->AddObserver(this, "autofill-fill-complete", false); } nsFormFillController::~nsFormFillController() { if (mListNode) { mListNode->RemoveMutationObserver(this); mListNode = nullptr; } if (mFocusedInput) { MaybeRemoveMutationObserver(mFocusedInput); mFocusedInput = nullptr; } RemoveForDocument(nullptr); } /* static */ already_AddRefed nsFormFillController::GetSingleton() { static RefPtr sSingleton; if (!sSingleton) { sSingleton = new nsFormFillController(); ClearOnShutdown(&sSingleton); } return do_AddRef(sSingleton); } //////////////////////////////////////////////////////////////////////// //// nsIMutationObserver // MOZ_CAN_RUN_SCRIPT_BOUNDARY 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(event.forget()); } if (mListNode && mListNode->Contains(aElement)) { RevalidateDataList(); } } MOZ_CAN_RUN_SCRIPT_BOUNDARY void nsFormFillController::ContentAppended(nsIContent* aChild) { if (mListNode && mListNode->Contains(aChild->GetParent())) { RevalidateDataList(); } } MOZ_CAN_RUN_SCRIPT_BOUNDARY void nsFormFillController::ContentInserted(nsIContent* aChild) { if (mListNode && mListNode->Contains(aChild->GetParent())) { RevalidateDataList(); } } MOZ_CAN_RUN_SCRIPT_BOUNDARY 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) {} void nsFormFillController::ParentChainChanged(nsIContent* aContent) {} void nsFormFillController::ARIAAttributeDefaultWillChange( mozilla::dom::Element* aElement, nsAtom* aAttribute, int32_t aModType) {} void nsFormFillController::ARIAAttributeDefaultChanged( mozilla::dom::Element* aElement, nsAtom* aAttribute, int32_t aModType) {} MOZ_CAN_RUN_SCRIPT_BOUNDARY void nsFormFillController::NodeWillBeDestroyed(nsINode* aNode) { MOZ_LOG(sLogger, LogLevel::Verbose, ("NodeWillBeDestroyed: %p", aNode)); mPwmgrInputs.Remove(aNode); mAutofillInputs.Remove(aNode); MaybeRemoveMutationObserver(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::AttachPopupElementToDocument(Document* aDocument, dom::Element* aPopupEl) { if (!xpc::IsInAutomation()) { return NS_ERROR_NOT_AVAILABLE; } MOZ_LOG(sLogger, LogLevel::Debug, ("AttachPopupElementToDocument for document %p with popup %p", aDocument, aPopupEl)); NS_ENSURE_TRUE(aDocument && aPopupEl, NS_ERROR_ILLEGAL_VALUE); nsCOMPtr popup = aPopupEl->AsAutoCompletePopup(); NS_ENSURE_STATE(popup); mPopups.InsertOrUpdate(aDocument, popup); return NS_OK; } NS_IMETHODIMP nsFormFillController::DetachFromDocument(Document* aDocument) { if (!xpc::IsInAutomation()) { return NS_ERROR_NOT_AVAILABLE; } mPopups.Remove(aDocument); 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.InsertOrUpdate(aInput, true); aInput->AddMutationObserverUnlessExists(this); nsFocusManager* fm = nsFocusManager::GetFocusManager(); if (fm) { nsCOMPtr focusedContent = fm->GetFocusedElement(); if (focusedContent == aInput) { if (!mFocusedInput) { MaybeStartControllingInput(aInput); } else { // If we change who is responsible for searching the autocomplete // result, notify the controller that the previous result is not valid // anymore. nsCOMPtr controller = mController; controller->ResetInternalState(); } } } if (!mLoginManagerAC) { mLoginManagerAC = do_GetService("@mozilla.org/login-manager/autocompletesearch;1"); } return NS_OK; } MOZ_CAN_RUN_SCRIPT NS_IMETHODIMP nsFormFillController::IsLoginManagerField( HTMLInputElement* aInput, bool* isLoginManagerField) { *isLoginManagerField = mPwmgrInputs.Get(aInput); 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.InsertOrUpdate(aInput, true); aInput->AddMutationObserverUnlessExists(this); aInput->EnablePreview(); nsFocusManager* fm = nsFocusManager::GetFocusManager(); if (fm) { nsCOMPtr focusedContent = fm->GetFocusedElement(); if (focusedContent == aInput) { if (!mFocusedInput) { MaybeStartControllingInput(aInput); } else { // See `MarkAsLoginManagerField` for why this is needed. nsCOMPtr controller = mController; controller->ResetInternalState(); } } } 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::GetPopupElement(Element** aPopup) { return NS_ERROR_NOT_IMPLEMENTED; } 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); RefPtr presShell = docShell->GetPresShell(); NS_ENSURE_STATE(presShell); presShell->ScrollContentIntoView( content, ScrollAxis(WhereToScroll::Nearest, WhenToScroll::IfNotVisible), ScrollAxis(WhereToScroll::Nearest, WhenToScroll::IfNotVisible), ScrollFlags::ScrollOverflowHidden); // mFocusedPopup can be destroyed after ScrollContentIntoView, see bug // 420089 if (mFocusedPopup) { mFocusedPopup->OpenAutocompletePopup(this, mFocusedInput); } } else { mFocusedPopup->ClosePopup(); mPasswordPopupAutomaticallyOpened = false; } } 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::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(); } MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsFormFillController::SelectTextRange(int32_t aStartIndex, int32_t aEndIndex) { if (!mFocusedInput) { return NS_ERROR_UNEXPECTED; } RefPtr focusedInput(mFocusedInput); ErrorResult rv; focusedInput->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) { NS_ENSURE_TRUE(mFocusedInput, NS_OK); return NS_OK; } NS_IMETHODIMP nsFormFillController::OnTextReverted(bool* _retval) { mPasswordPopupAutomaticallyOpened = false; 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; } RefPtr 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::GetNoRollupOnEmptySearch(bool* aNoRollupOnEmptySearch) { if (mFocusedInput && (mPwmgrInputs.Get(mFocusedInput) || mFocusedInput->HasBeenTypePassword())) { // Don't close the login popup when the field is cleared (bug 1534896). *aNoRollupOnEmptySearch = true; } else { *aNoRollupOnEmptySearch = false; } return NS_OK; } NS_IMETHODIMP nsFormFillController::GetUserContextId(uint32_t* aUserContextId) { *aUserContextId = nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID; return NS_OK; } NS_IMETHODIMP nsFormFillController::GetInvalidatePreviousResult( bool* aInvalidatePreviousResult) { *aInvalidatePreviousResult = mInvalidatePreviousResult; 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->HasBeenTypePassword())) { 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 (!mLoginManagerAC) { mLoginManagerAC = do_GetService("@mozilla.org/login-manager/autocompletesearch;1"); } if (NS_WARN_IF(!mLoginManagerAC)) { 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 = mLoginManagerAC->StartSearch(aSearchString, aPreviousResult, mFocusedInput, this); NS_ENSURE_SUCCESS(rv, rv); } else { MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: non-login field")); mLastListener = aListener; bool addDataList = IsTextControl(mFocusedInput); if (addDataList) { MaybeObserveDataListMutations(); } auto* formHistoryAutoComplete = GetFormHistoryAutoComplete(); NS_ENSURE_TRUE(formHistoryAutoComplete, NS_ERROR_FAILURE); formHistoryAutoComplete->AutoCompleteSearchAsync( aSearchParam, aSearchString, mFocusedInput, aPreviousResult, addDataList, this); mLastFormHistoryAutoComplete = formHistoryAutoComplete; } return NS_OK; } void nsFormFillController::MaybeObserveDataListMutations() { // If an is focused, check if it has a list="" which can // provide the list of suggestions. MOZ_ASSERT(!mPwmgrInputs.Get(mFocusedInput)); 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; } } } } void nsFormFillController::RevalidateDataList() { if (!mLastListener) { return; } nsCOMPtr controller( do_QueryInterface(mLastListener)); if (!controller) { return; } // We cannot use previous result since any items in search target are updated. mInvalidatePreviousResult = true; controller->StartSearch(mLastSearchString); } NS_IMETHODIMP nsFormFillController::StopSearch() { // Make sure to stop and clear this, otherwise the controller will prevent // mLastFormHistoryAutoComplete from being deleted. if (mLastFormHistoryAutoComplete) { mLastFormHistoryAutoComplete->StopAutoCompleteSearch(); mLastFormHistoryAutoComplete = nullptr; } if (mLoginManagerAC) { mLoginManagerAC->StopSearch(); } return NS_OK; } //////////////////////////////////////////////////////////////////////// //// nsIFormFillCompleteObserver NS_IMETHODIMP nsFormFillController::OnSearchCompletion(nsIAutoCompleteResult* aResult) { nsAutoString searchString; aResult->GetSearchString(searchString); mLastSearchString = searchString; if (mLastListener) { nsCOMPtr lastListener = mLastListener; lastListener->OnSearchResult(this, aResult); } return NS_OK; } //////////////////////////////////////////////////////////////////////// //// nsIObserver NS_IMETHODIMP nsFormFillController::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!nsCRT::strcmp(aTopic, "chrome-event-target-created")) { if (RefPtr eventTarget = do_QueryObject(aSubject)) { AttachListeners(eventTarget); } } else if (!nsCRT::strcmp(aTopic, "autofill-fill-starting")) { mAutoCompleteActive = true; } else if (!nsCRT::strcmp(aTopic, "autofill-fill-complete")) { mAutoCompleteActive = false; } return NS_OK; } //////////////////////////////////////////////////////////////////////// //// nsIDOMEventListener NS_IMETHODIMP nsFormFillController::HandleEvent(Event* aEvent) { EventTarget* target = aEvent->GetOriginalTarget(); NS_ENSURE_STATE(target); mInvalidatePreviousResult = false; nsIGlobalObject* global = target->GetOwnerGlobal(); NS_ENSURE_STATE(global); nsPIDOMWindowInner* inner = global->GetAsInnerWindow(); NS_ENSURE_STATE(inner); if (!inner->GetBrowsingContext()->IsContent()) { return NS_OK; } if (aEvent->ShouldIgnoreChromeEventTargetListener()) { return NS_OK; } 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 eEditorInput: { if (!(mAutoCompleteActive || mSuppressOnInput)) { nsCOMPtr input = do_QueryInterface(aEvent->GetComposedTarget()); if (IsTextControl(input) && IsFocusedInputControlled()) { nsCOMPtr controller = mController; bool unused = false; return controller->HandleText(&unused); } } return NS_OK; } case eBlur: if (mFocusedInput && !StaticPrefs::ui_popup_disable_autohide()) { StopControllingInput(); } return NS_OK; case eCompositionStart: NS_ASSERTION(mController, "should have a controller!"); if (IsFocusedInputControlled()) { nsCOMPtr controller = mController; controller->HandleStartComposition(); } return NS_OK; case eCompositionEnd: NS_ASSERTION(mController, "should have a controller!"); if (IsFocusedInputControlled()) { nsCOMPtr controller = mController; controller->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 && 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::AttachListeners(EventTarget* aEventTarget) { EventListenerManager* elm = aEventTarget->GetOrCreateListenerManager(); NS_ENSURE_TRUE_VOID(elm); elm->AddEventListenerByType(this, u"focus"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"blur"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"pagehide"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"mousedown"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"input"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"keydown"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"keypress"_ns, TrustedEventsAtSystemGroupCapture()); elm->AddEventListenerByType(this, u"compositionstart"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"compositionend"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"contextmenu"_ns, TrustedEventsAtCapture()); } void nsFormFillController::RemoveForDocument(Document* 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; } bool hasList = !!aInput->GetList(); if (!IsTextControl(aInput)) { // Even if this is not a text control yet, it can become one in the future if (hasList) { StartControllingInput(aInput); } return; } bool autocomplete = nsContentUtils::IsAutocompleteEnabled(aInput); bool isPwmgrInput = false; if (mPwmgrInputs.Get(aInput) || aInput->HasBeenTypePassword()) { isPwmgrInput = true; } bool isAutofillInput = false; if (mAutofillInputs.Get(aInput)) { isAutofillInput = true; } if (isAutofillInput || isPwmgrInput || hasList || autocomplete) { StartControllingInput(aInput); } } nsresult nsFormFillController::HandleFocus(HTMLInputElement* aInput) { MaybeStartControllingInput(aInput); // Bail if we didn't start controlling the input. if (!mFocusedInput) { return NS_OK; } // 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->HasBeenTypePassword()) { return NS_OK; } // If we have not seen a right click yet, just show the popup. if (mLastRightClickTimeStamp.IsNull()) { mPasswordPopupAutomaticallyOpened = true; ShowPopup(); return NS_OK; } uint64_t timeDiff = (TimeStamp::Now() - mLastRightClickTimeStamp).ToMilliseconds(); if (timeDiff > mFocusAfterRightClickThreshold) { mPasswordPopupAutomaticallyOpened = true; ShowPopup(); } return NS_OK; } nsresult nsFormFillController::Focus(Event* aEvent) { nsCOMPtr input = do_QueryInterface(aEvent->GetComposedTarget()); return HandleFocus(MOZ_KnownLive(HTMLInputElement::FromNodeOrNull(input))); } nsresult nsFormFillController::KeyDown(Event* aEvent) { NS_ASSERTION(mController, "should have a controller!"); mPasswordPopupAutomaticallyOpened = false; if (!IsFocusedInputControlled()) { 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_RETURN: { nsCOMPtr controller = mController; controller->HandleEnter(false, aEvent, &cancel); break; } case KeyboardEvent_Binding::DOM_VK_DELETE: #ifndef XP_MACOSX { nsCOMPtr controller = mController; controller->HandleDelete(&cancel); break; } case KeyboardEvent_Binding::DOM_VK_BACK_SPACE: { nsCOMPtr controller = mController; controller->HandleText(&unused); break; } #else case KeyboardEvent_Binding::DOM_VK_BACK_SPACE: { if (keyEvent->ShiftKey()) { nsCOMPtr controller = mController; controller->HandleDelete(&cancel); } else { nsCOMPtr controller = mController; controller->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; } } [[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; } } nsCOMPtr controller = mController; controller->HandleKeyNavigation(k, &cancel); break; } case KeyboardEvent_Binding::DOM_VK_ESCAPE: { nsCOMPtr controller = mController; controller->HandleEscape(&cancel); break; } case KeyboardEvent_Binding::DOM_VK_TAB: { nsCOMPtr controller = mController; controller->HandleTab(); cancel = false; 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::MouseDown(Event* aEvent) { MouseEvent* mouseEvent = aEvent->AsMouseEvent(); if (!mouseEvent) { return NS_ERROR_FAILURE; } nsCOMPtr targetNode = do_QueryInterface(aEvent->GetComposedTarget()); 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 controller = mController; nsCOMPtr input; controller->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 controller->SetSearchString(u""_ns); bool unused = false; controller->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; controller->HandleKeyNavigation(KeyboardEvent_Binding::DOM_VK_DOWN, &cancel); } return NS_OK; } NS_IMETHODIMP nsFormFillController::GetPasswordPopupAutomaticallyOpened( bool* _retval) { *_retval = mPasswordPopupAutomaticallyOpened; return NS_OK; } 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 || !aInput) { return; } nsCOMPtr popup = mPopups.Get(aInput->OwnerDoc()); if (!popup) { popup = do_QueryActor("AutoComplete", aInput->OwnerDoc()); if (!popup) { return; } } mFocusedPopup = popup; aInput->AddMutationObserverUnlessExists(this); mFocusedInput = aInput; if (Element* list = mFocusedInput->GetList()) { list->AddMutationObserverUnlessExists(this); mListNode = list; } if (!mFocusedInput->ReadOnly()) { nsCOMPtr controller = mController; controller->SetInput(this); } } bool nsFormFillController::IsFocusedInputControlled() const { return mFocusedInput && mController && !mFocusedInput->ReadOnly(); } void nsFormFillController::StopControllingInput() { mPasswordPopupAutomaticallyOpened = false; if (mListNode) { mListNode->RemoveMutationObserver(this); mListNode = nullptr; } if (nsCOMPtr controller = 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; controller->GetInput(getter_AddRefs(input)); if (input == this) { MOZ_LOG(sLogger, LogLevel::Verbose, ("StopControllingInput: Nulled controller input for %p", this)); controller->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(); }