/* -*- 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 "mozilla/dom/HTMLFormElement.h" #include #include "Attr.h" #include "jsapi.h" #include "mozilla/AutoRestore.h" #include "mozilla/BasePrincipal.h" #include "mozilla/BinarySearch.h" #include "mozilla/Components.h" #include "mozilla/ContentEvents.h" #include "mozilla/EventDispatcher.h" #include "mozilla/PresShell.h" #include "mozilla/UniquePtr.h" #include "mozilla/dom/BindContext.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/CustomEvent.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/HTMLFormControlsCollection.h" #include "mozilla/dom/HTMLFormElementBinding.h" #include "mozilla/dom/TreeOrderedArrayInlines.h" #include "mozilla/dom/nsCSPContext.h" #include "mozilla/dom/nsCSPUtils.h" #include "mozilla/dom/nsMixedContentBlocker.h" #include "nsCOMArray.h" #include "nsContentList.h" #include "nsContentUtils.h" #include "nsDOMAttributeMap.h" #include "nsDocShell.h" #include "nsDocShellLoadState.h" #include "nsError.h" #include "nsFocusManager.h" #include "nsGkAtoms.h" #include "nsHTMLDocument.h" #include "nsIFormControlFrame.h" #include "nsInterfaceHashtable.h" #include "nsPresContext.h" #include "nsQueryObject.h" #include "nsStyleConsts.h" #include "nsTArray.h" // form submission #include "HTMLFormSubmissionConstants.h" #include "mozilla/dom/FormData.h" #include "mozilla/dom/FormDataEvent.h" #include "mozilla/dom/SubmitEvent.h" #include "mozilla/Telemetry.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_prompts.h" #include "nsCategoryManagerUtils.h" #include "nsIContentInlines.h" #include "nsISimpleEnumerator.h" #include "nsRange.h" #include "nsIScriptError.h" #include "nsIScriptSecurityManager.h" #include "nsNetUtil.h" #include "nsIInterfaceRequestorUtils.h" #include "nsIDocShell.h" #include "nsIPromptService.h" #include "nsISecurityUITelemetry.h" #include "nsIStringBundle.h" // radio buttons #include "mozilla/dom/HTMLInputElement.h" #include "mozilla/dom/HTMLButtonElement.h" #include "mozilla/dom/HTMLSelectElement.h" #include "nsIRadioVisitor.h" #include "RadioNodeList.h" #include "nsLayoutUtils.h" #include "mozAutoDocUpdate.h" #include "nsIHTMLCollection.h" #include "nsIConstraintValidation.h" #include "nsSandboxFlags.h" #include "mozilla/dom/HTMLAnchorElement.h" // images #include "mozilla/dom/HTMLImageElement.h" #include "mozilla/dom/HTMLButtonElement.h" // construction, destruction NS_IMPL_NS_NEW_HTML_ELEMENT(Form) namespace mozilla::dom { static const uint8_t NS_FORM_AUTOCOMPLETE_ON = 1; static const uint8_t NS_FORM_AUTOCOMPLETE_OFF = 0; static const nsAttrValue::EnumTable kFormAutocompleteTable[] = { {"on", NS_FORM_AUTOCOMPLETE_ON}, {"off", NS_FORM_AUTOCOMPLETE_OFF}, {nullptr, 0}}; // Default autocomplete value is 'on'. static const nsAttrValue::EnumTable* kFormDefaultAutocomplete = &kFormAutocompleteTable[0]; HTMLFormElement::HTMLFormElement( already_AddRefed&& aNodeInfo) : nsGenericHTMLElement(std::move(aNodeInfo)), mControls(new HTMLFormControlsCollection(this)), mPendingSubmission(nullptr), mDefaultSubmitElement(nullptr), mFirstSubmitInElements(nullptr), mFirstSubmitNotInElements(nullptr), mImageNameLookupTable(FORM_CONTROL_LIST_HASHTABLE_LENGTH), mPastNameLookupTable(FORM_CONTROL_LIST_HASHTABLE_LENGTH), mSubmitPopupState(PopupBlocker::openAbused), mInvalidElementsCount(0), mFormNumber(-1), mGeneratingSubmit(false), mGeneratingReset(false), mDeferSubmission(false), mNotifiedObservers(false), mNotifiedObserversResult(false), mIsConstructingEntryList(false), mIsFiringSubmissionEvents(false) { // We start out valid. AddStatesSilently(ElementState::VALID); } HTMLFormElement::~HTMLFormElement() { if (mControls) { mControls->DropFormReference(); } Clear(); } // nsISupports NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLFormElement) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLFormElement, nsGenericHTMLElement) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControls) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImageNameLookupTable) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPastNameLookupTable) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelList) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTargetContext) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLFormElement, nsGenericHTMLElement) NS_IMPL_CYCLE_COLLECTION_UNLINK(mRelList) NS_IMPL_CYCLE_COLLECTION_UNLINK(mTargetContext) tmp->Clear(); tmp->mExpandoAndGeneration.OwnerUnlinked(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLFormElement, nsGenericHTMLElement) // EventTarget void HTMLFormElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) { if (aEvent->mEventType == u"DOMFormHasPassword"_ns) { mHasPendingPasswordEvent = false; } else if (aEvent->mEventType == u"DOMFormHasPossibleUsername"_ns) { mHasPendingPossibleUsernameEvent = false; } } nsDOMTokenList* HTMLFormElement::RelList() { if (!mRelList) { mRelList = new nsDOMTokenList(this, nsGkAtoms::rel, sAnchorAndFormRelValues); } return mRelList; } NS_IMPL_ELEMENT_CLONE(HTMLFormElement) HTMLFormControlsCollection* HTMLFormElement::Elements() { return mControls; } void HTMLFormElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, const nsAttrValue* aValue, bool aNotify) { if (aNamespaceID == kNameSpaceID_None) { if (aName == nsGkAtoms::action || aName == nsGkAtoms::target) { // Don't forget we've notified the password manager already if the // page sets the action/target in the during submit. (bug 343182) bool notifiedObservers = mNotifiedObservers; ForgetCurrentSubmission(); mNotifiedObservers = notifiedObservers; } } return nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue, aNotify); } void HTMLFormElement::GetAutocomplete(nsAString& aValue) { GetEnumAttr(nsGkAtoms::autocomplete, kFormDefaultAutocomplete->tag, aValue); } void HTMLFormElement::GetEnctype(nsAString& aValue) { GetEnumAttr(nsGkAtoms::enctype, kFormDefaultEnctype->tag, aValue); } void HTMLFormElement::GetMethod(nsAString& aValue) { GetEnumAttr(nsGkAtoms::method, kFormDefaultMethod->tag, aValue); } void HTMLFormElement::ReportInvalidUnfocusableElements( const nsTArray>&& aInvalidElements) { RefPtr focusManager = nsFocusManager::GetFocusManager(); MOZ_ASSERT(focusManager); for (const auto& element : aInvalidElements) { bool isFocusable = false; // MOZ_KnownLive because 'aInvalidElements' is guaranteed to keep it alive. // This can go away once bug 1620312 is fixed. focusManager->ElementIsFocusable(MOZ_KnownLive(element), 0, &isFocusable); if (!isFocusable) { nsTArray params; nsAutoCString messageName("InvalidFormControlUnfocusable"); if (Attr* nameAttr = element->GetAttributes()->GetNamedItem(u"name"_ns)) { nsAutoString name; nameAttr->GetValue(name); params.AppendElement(name); messageName = "InvalidNamedFormControlUnfocusable"; } nsContentUtils::ReportToConsole( nsIScriptError::errorFlag, "DOM"_ns, element->GetOwnerDocument(), nsContentUtils::eDOM_PROPERTIES, messageName.get(), params, element->GetBaseURI()); } } } // https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit void HTMLFormElement::MaybeSubmit(Element* aSubmitter) { #ifdef DEBUG if (aSubmitter) { nsCOMPtr fc = do_QueryInterface(aSubmitter); MOZ_ASSERT(fc); MOZ_ASSERT(fc->IsSubmitControl(), "aSubmitter is not a submit control?"); } #endif // 1-4 of // https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit Document* doc = GetComposedDoc(); if (mIsConstructingEntryList || !doc || (doc->GetSandboxFlags() & SANDBOXED_FORMS)) { return; } // 5.1. If form's firing submission events is true, then return. if (mIsFiringSubmissionEvents) { return; } // 5.2. Set form's firing submission events to true. AutoRestore resetFiringSubmissionEventsFlag(mIsFiringSubmissionEvents); mIsFiringSubmissionEvents = true; // Flag elements as user-interacted. // FIXME: Should be specified, see: // https://github.com/whatwg/html/issues/10066 { for (nsGenericHTMLFormElement* el : mControls->mElements.AsList()) { el->SetUserInteracted(true); } for (nsGenericHTMLFormElement* el : mControls->mNotInElements.AsList()) { el->SetUserInteracted(true); } } // 5.3. If the submitter element's no-validate state is false, then // interactively validate the constraints of form and examine the result. // If the result is negative (i.e., the constraint validation concluded // that there were invalid fields and probably informed the user of this) bool noValidateState = HasAttr(nsGkAtoms::novalidate) || (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formnovalidate)); if (!noValidateState && !CheckValidFormSubmission()) { return; } RefPtr presShell = doc->GetPresShell(); if (!presShell) { // We need the nsPresContext for dispatching the submit event. In some // rare cases we need to flush notifications to force creation of the // nsPresContext here (for example when a script calls form.requestSubmit() // from script early during page load). We only flush the notifications // if the PresShell hasn't been created yet, to limit the performance // impact. doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames); presShell = doc->GetPresShell(); } // If |PresShell::Destroy| has been called due to handling the event the pres // context will return a null pres shell. See bug 125624. Using presShell to // dispatch the event. It makes sure that event is not handled if the window // is being destroyed. if (presShell) { SubmitEventInit init; init.mBubbles = true; init.mCancelable = true; init.mSubmitter = aSubmitter ? nsGenericHTMLElement::FromNode(aSubmitter) : nullptr; RefPtr event = SubmitEvent::Constructor(this, u"submit"_ns, init); event->SetTrusted(true); nsEventStatus status = nsEventStatus_eIgnore; presShell->HandleDOMEventWithTarget(this, event, &status); } } void HTMLFormElement::MaybeReset(Element* aSubmitter) { // If |PresShell::Destroy| has been called due to handling the event the pres // context will return a null pres shell. See bug 125624. Using presShell to // dispatch the event. It makes sure that event is not handled if the window // is being destroyed. if (RefPtr presShell = OwnerDoc()->GetPresShell()) { InternalFormEvent event(true, eFormReset); event.mOriginator = aSubmitter; nsEventStatus status = nsEventStatus_eIgnore; presShell->HandleDOMEventWithTarget(this, &event, &status); } } void HTMLFormElement::Submit(ErrorResult& aRv) { aRv = DoSubmit(); } // https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit void HTMLFormElement::RequestSubmit(nsGenericHTMLElement* aSubmitter, ErrorResult& aRv) { // 1. If submitter is not null, then: if (aSubmitter) { nsCOMPtr fc = do_QueryObject(aSubmitter); // 1.1. If submitter is not a submit button, then throw a TypeError. if (!fc || !fc->IsSubmitControl()) { aRv.ThrowTypeError("The submitter is not a submit button."); return; } // 1.2. If submitter's form owner is not this form element, then throw a // "NotFoundError" DOMException. if (fc->GetForm() != this) { aRv.ThrowNotFoundError("The submitter is not owned by this form."); return; } } // 2. Otherwise, set submitter to this form element. // 3. Submit this form element, from submitter. MaybeSubmit(aSubmitter); } void HTMLFormElement::Reset() { InternalFormEvent event(true, eFormReset); EventDispatcher::Dispatch(this, nullptr, &event); } bool HTMLFormElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue, nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) { if (aNamespaceID == kNameSpaceID_None) { if (aAttribute == nsGkAtoms::method) { return aResult.ParseEnumValue(aValue, kFormMethodTable, false); } if (aAttribute == nsGkAtoms::enctype) { return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false); } if (aAttribute == nsGkAtoms::autocomplete) { return aResult.ParseEnumValue(aValue, kFormAutocompleteTable, false); } } return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); } nsresult HTMLFormElement::BindToTree(BindContext& aContext, nsINode& aParent) { nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); NS_ENSURE_SUCCESS(rv, rv); if (IsInUncomposedDoc() && aContext.OwnerDoc().IsHTMLOrXHTML()) { aContext.OwnerDoc().AsHTMLDocument()->AddedForm(); } return rv; } template static void MarkOrphans(const nsTArray& aArray) { uint32_t length = aArray.Length(); for (uint32_t i = 0; i < length; ++i) { aArray[i]->SetFlags(MAYBE_ORPHAN_FORM_ELEMENT); } } static void CollectOrphans(nsINode* aRemovalRoot, const nsTArray& aArray #ifdef DEBUG , HTMLFormElement* aThisForm #endif ) { // Put a script blocker around all the notifications we're about to do. nsAutoScriptBlocker scriptBlocker; // Walk backwards so that if we remove elements we can just keep iterating uint32_t length = aArray.Length(); for (uint32_t i = length; i > 0; --i) { nsGenericHTMLFormElement* node = aArray[i - 1]; // Now if MAYBE_ORPHAN_FORM_ELEMENT is not set, that would mean that the // node is in fact a descendant of the form and hence should stay in the // form. If it _is_ set, then we need to check whether the node is a // descendant of aRemovalRoot. If it is, we leave it in the form. #ifdef DEBUG bool removed = false; #endif if (node->HasFlag(MAYBE_ORPHAN_FORM_ELEMENT)) { node->UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT); if (!node->IsInclusiveDescendantOf(aRemovalRoot)) { nsCOMPtr fc = do_QueryInterface(node); MOZ_ASSERT(fc); fc->ClearForm(true, false); #ifdef DEBUG removed = true; #endif } } #ifdef DEBUG if (!removed) { nsCOMPtr fc = do_QueryInterface(node); MOZ_ASSERT(fc); HTMLFormElement* form = fc->GetForm(); NS_ASSERTION(form == aThisForm, "How did that happen?"); } #endif /* DEBUG */ } } static void CollectOrphans(nsINode* aRemovalRoot, const nsTArray& aArray #ifdef DEBUG , HTMLFormElement* aThisForm #endif ) { // Walk backwards so that if we remove elements we can just keep iterating uint32_t length = aArray.Length(); for (uint32_t i = length; i > 0; --i) { HTMLImageElement* node = aArray[i - 1]; // Now if MAYBE_ORPHAN_FORM_ELEMENT is not set, that would mean that the // node is in fact a descendant of the form and hence should stay in the // form. If it _is_ set, then we need to check whether the node is a // descendant of aRemovalRoot. If it is, we leave it in the form. #ifdef DEBUG bool removed = false; #endif if (node->HasFlag(MAYBE_ORPHAN_FORM_ELEMENT)) { node->UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT); if (!node->IsInclusiveDescendantOf(aRemovalRoot)) { node->ClearForm(true); #ifdef DEBUG removed = true; #endif } } #ifdef DEBUG if (!removed) { HTMLFormElement* form = node->GetForm(); NS_ASSERTION(form == aThisForm, "How did that happen?"); } #endif /* DEBUG */ } } void HTMLFormElement::UnbindFromTree(UnbindContext& aContext) { MaybeFireFormRemoved(); // Note, this is explicitly using uncomposed doc, since we count // only forms in document. RefPtr oldDocument = GetUncomposedDoc(); // Mark all of our controls as maybe being orphans MarkOrphans(mControls->mElements.AsList()); MarkOrphans(mControls->mNotInElements.AsList()); MarkOrphans(mImageElements.AsList()); nsGenericHTMLElement::UnbindFromTree(aContext); nsINode* ancestor = this; nsINode* cur; do { cur = ancestor->GetParentNode(); if (!cur) { break; } ancestor = cur; } while (true); CollectOrphans(ancestor, mControls->mElements #ifdef DEBUG , this #endif ); CollectOrphans(ancestor, mControls->mNotInElements #ifdef DEBUG , this #endif ); CollectOrphans(ancestor, mImageElements #ifdef DEBUG , this #endif ); if (oldDocument && oldDocument->IsHTMLOrXHTML()) { oldDocument->AsHTMLDocument()->RemovedForm(); } ForgetCurrentSubmission(); } static bool CanSubmit(WidgetEvent& aEvent) { // According to the UI events spec section "Trusted events", we shouldn't // trigger UA default action with an untrusted event except click. // However, there are still some sites depending on sending untrusted event // to submit form, see Bug 1370630. return !StaticPrefs::dom_forms_submit_trusted_event_only() || aEvent.IsTrusted(); } void HTMLFormElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { aVisitor.mWantsWillHandleEvent = true; if (aVisitor.mEvent->mOriginalTarget == static_cast(this) && CanSubmit(*aVisitor.mEvent)) { uint32_t msg = aVisitor.mEvent->mMessage; if (msg == eFormSubmit) { if (mGeneratingSubmit) { aVisitor.mCanHandle = false; return; } mGeneratingSubmit = true; // XXXedgar, the untrusted event would trigger form submission, in this // case, form need to handle defer flag and flushing pending submission by // itself. This could be removed after Bug 1370630. if (!aVisitor.mEvent->IsTrusted()) { // let the form know that it needs to defer the submission, // that means that if there are scripted submissions, the // latest one will be deferred until after the exit point of the // handler. mDeferSubmission = true; } } else if (msg == eFormReset) { if (mGeneratingReset) { aVisitor.mCanHandle = false; return; } mGeneratingReset = true; } } nsGenericHTMLElement::GetEventTargetParent(aVisitor); } void HTMLFormElement::WillHandleEvent(EventChainPostVisitor& aVisitor) { // If this is the bubble stage and there is a nested form below us which // received a submit event we do *not* want to handle the submit event // for this form too. if ((aVisitor.mEvent->mMessage == eFormSubmit || aVisitor.mEvent->mMessage == eFormReset) && aVisitor.mEvent->mFlags.mInBubblingPhase && aVisitor.mEvent->mOriginalTarget != static_cast(this)) { aVisitor.mEvent->StopPropagation(); } } nsresult HTMLFormElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { if (aVisitor.mEvent->mOriginalTarget == static_cast(this) && CanSubmit(*aVisitor.mEvent)) { EventMessage msg = aVisitor.mEvent->mMessage; if (aVisitor.mEventStatus == nsEventStatus_eIgnore) { switch (msg) { case eFormReset: { DoReset(); break; } case eFormSubmit: { if (!aVisitor.mEvent->IsTrusted()) { // Warning about the form submission is from untrusted event. OwnerDoc()->WarnOnceAbout( DeprecatedOperations::eFormSubmissionUntrustedEvent); } RefPtr event = aVisitor.mDOMEvent; DoSubmit(event); break; } default: break; } } // XXXedgar, the untrusted event would trigger form submission, in this // case, form need to handle defer flag and flushing pending submission by // itself. This could be removed after Bug 1370630. if (msg == eFormSubmit && !aVisitor.mEvent->IsTrusted()) { // let the form know not to defer subsequent submissions mDeferSubmission = false; // tell the form to flush a possible pending submission. FlushPendingSubmission(); } if (msg == eFormSubmit) { mGeneratingSubmit = false; } else if (msg == eFormReset) { mGeneratingReset = false; } } return NS_OK; } nsresult HTMLFormElement::DoReset() { // Make sure the presentation is up-to-date Document* doc = GetComposedDoc(); if (doc) { doc->FlushPendingNotifications(FlushType::ContentAndNotify); } // JBK walk the elements[] array instead of form frame controls - bug 34297 uint32_t numElements = mControls->Length(); for (uint32_t elementX = 0; elementX < numElements; ++elementX) { // Hold strong ref in case the reset does something weird nsCOMPtr controlNode = do_QueryInterface( mControls->mElements->SafeElementAt(elementX, nullptr)); if (controlNode) { controlNode->Reset(); } } return NS_OK; } #define NS_ENSURE_SUBMIT_SUCCESS(rv) \ if (NS_FAILED(rv)) { \ ForgetCurrentSubmission(); \ return rv; \ } nsresult HTMLFormElement::DoSubmit(Event* aEvent) { Document* doc = GetComposedDoc(); NS_ASSERTION(doc, "Should never get here without a current doc"); // Make sure the presentation is up-to-date if (doc) { doc->FlushPendingNotifications(FlushType::ContentAndNotify); } // Don't submit if we're not in a document or if we're in // a sandboxed frame and form submit is disabled. if (mIsConstructingEntryList || !doc || (doc->GetSandboxFlags() & SANDBOXED_FORMS)) { return NS_OK; } if (IsSubmitting()) { NS_WARNING("Preventing double form submission"); // XXX Should this return an error? return NS_OK; } mTargetContext = nullptr; mCurrentLoadId = Nothing(); UniquePtr submission; // // prepare the submission object // nsresult rv = BuildSubmission(getter_Transfers(submission), aEvent); // Don't raise an error if form cannot navigate. if (rv == NS_ERROR_NOT_AVAILABLE) { return NS_OK; } NS_ENSURE_SUCCESS(rv, rv); // XXXbz if the script global is that for an sXBL/XBL2 doc, it won't // be a window... nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow(); if (window) { mSubmitPopupState = PopupBlocker::GetPopupControlState(); } else { mSubmitPopupState = PopupBlocker::openAbused; } // // perform the submission // if (!submission) { #ifdef DEBUG HTMLDialogElement* dialog = nullptr; for (nsIContent* parent = GetParent(); parent; parent = parent->GetParent()) { dialog = HTMLDialogElement::FromNodeOrNull(parent); if (dialog) { break; } } MOZ_ASSERT(!dialog || !dialog->Open()); #endif return NS_OK; } if (DialogFormSubmission* dialogSubmission = submission->GetAsDialogSubmission()) { return SubmitDialog(dialogSubmission); } if (mDeferSubmission) { // we are in an event handler, JS submitted so we have to // defer this submission. let's remember it and return // without submitting mPendingSubmission = std::move(submission); return NS_OK; } return SubmitSubmission(submission.get()); } nsresult HTMLFormElement::BuildSubmission(HTMLFormSubmission** aFormSubmission, Event* aEvent) { // Get the submitter element nsGenericHTMLElement* submitter = nullptr; if (aEvent) { SubmitEvent* submitEvent = aEvent->AsSubmitEvent(); if (submitEvent) { submitter = submitEvent->GetSubmitter(); } } nsresult rv; // // Walk over the form elements and call SubmitNamesValues() on them to get // their data. // auto encoding = GetSubmitEncoding()->OutputEncoding(); RefPtr formData = new FormData(GetOwnerGlobal(), encoding, submitter); rv = ConstructEntryList(formData); NS_ENSURE_SUBMIT_SUCCESS(rv); // Step 9. If form cannot navigate, then return. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm if (!GetComposedDoc()) { return NS_ERROR_NOT_AVAILABLE; } // // Get the submission object // rv = HTMLFormSubmission::GetFromForm(this, submitter, encoding, aFormSubmission); NS_ENSURE_SUBMIT_SUCCESS(rv); // // Dump the data into the submission object // if (!(*aFormSubmission)->GetAsDialogSubmission()) { rv = formData->CopySubmissionDataTo(*aFormSubmission); NS_ENSURE_SUBMIT_SUCCESS(rv); } return NS_OK; } nsresult HTMLFormElement::SubmitSubmission( HTMLFormSubmission* aFormSubmission) { MOZ_ASSERT(!mDeferSubmission); MOZ_ASSERT(!mPendingSubmission); nsCOMPtr actionURI = aFormSubmission->GetActionURL(); if (!actionURI) { return NS_OK; } // If there is no link handler, then we won't actually be able to submit. Document* doc = GetComposedDoc(); RefPtr container = doc ? nsDocShell::Cast(doc->GetDocShell()) : nullptr; if (!container || IsEditable()) { return NS_OK; } // javascript URIs are not really submissions; they just call a function. // Also, they may synchronously call submit(), and we want them to be able to // do so while still disallowing other double submissions. (Bug 139798) // Note that any other URI types that are of equivalent type should also be // added here. // XXXbz this is a mess. The real issue here is that nsJSChannel sets the // LOAD_BACKGROUND flag, so doesn't notify us, compounded by the fact that // the JS executes before we forget the submission in OnStateChange on // STATE_STOP. As a result, we have to make sure that we simply pretend // we're not submitting when submitting to a JS URL. That's kinda bogus, but // there we are. bool schemeIsJavaScript = actionURI->SchemeIs("javascript"); // // Notify observers of submit // nsresult rv; bool cancelSubmit = false; if (mNotifiedObservers) { cancelSubmit = mNotifiedObserversResult; } else { rv = NotifySubmitObservers(actionURI, &cancelSubmit, true); NS_ENSURE_SUBMIT_SUCCESS(rv); } if (cancelSubmit) { return NS_OK; } cancelSubmit = false; rv = NotifySubmitObservers(actionURI, &cancelSubmit, false); NS_ENSURE_SUBMIT_SUCCESS(rv); if (cancelSubmit) { return NS_OK; } // // Submit // uint64_t currentLoadId = 0; { AutoPopupStatePusher popupStatePusher(mSubmitPopupState); AutoHandlingUserInputStatePusher userInpStatePusher( aFormSubmission->IsInitiatedFromUserInput()); nsCOMPtr postDataStream; rv = aFormSubmission->GetEncodedSubmission( actionURI, getter_AddRefs(postDataStream), actionURI); NS_ENSURE_SUBMIT_SUCCESS(rv); nsAutoString target; aFormSubmission->GetTarget(target); RefPtr loadState = new nsDocShellLoadState(actionURI); loadState->SetTarget(target); loadState->SetPostDataStream(postDataStream); loadState->SetFirstParty(true); loadState->SetIsFormSubmission(true); loadState->SetTriggeringPrincipal(NodePrincipal()); loadState->SetPrincipalToInherit(NodePrincipal()); loadState->SetCsp(GetCsp()); loadState->SetAllowFocusMove(UserActivation::IsHandlingUserInput()); nsCOMPtr nodePrincipal = NodePrincipal(); rv = container->OnLinkClickSync(this, loadState, false, nodePrincipal); NS_ENSURE_SUBMIT_SUCCESS(rv); mTargetContext = loadState->TargetBrowsingContext().GetMaybeDiscarded(); currentLoadId = loadState->GetLoadIdentifier(); } // Even if the submit succeeds, it's possible for there to be no // browsing context; for example, if it's to a named anchor within // the same page the submit will not really do anything. if (mTargetContext && !mTargetContext->IsDiscarded() && !schemeIsJavaScript) { mCurrentLoadId = Some(currentLoadId); } else { ForgetCurrentSubmission(); } return rv; } // https://html.spec.whatwg.org/#concept-form-submit step 11 nsresult HTMLFormElement::SubmitDialog(DialogFormSubmission* aFormSubmission) { // Close the dialog subject. If there is a result, let that be the return // value. HTMLDialogElement* dialog = aFormSubmission->DialogElement(); MOZ_ASSERT(dialog); Optional retValue; retValue = &aFormSubmission->ReturnValue(); dialog->Close(retValue); return NS_OK; } nsresult HTMLFormElement::DoSecureToInsecureSubmitCheck(nsIURI* aActionURL, bool* aCancelSubmit) { *aCancelSubmit = false; if (!StaticPrefs::security_warn_submit_secure_to_insecure()) { return NS_OK; } // Only ask the user about posting from a secure URI to an insecure URI if // this element is in the root document. When this is not the case, the mixed // content blocker will take care of security for us. if (!OwnerDoc()->IsTopLevelContentDocument()) { return NS_OK; } if (nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aActionURL)) { return NS_OK; } if (nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(aActionURL)) { return NS_OK; } if (nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aActionURL)) { return NS_OK; } nsCOMPtr window = OwnerDoc()->GetWindow(); if (!window) { return NS_ERROR_FAILURE; } // Now that we know the action URI is insecure check if we're submitting from // a secure URI and if so fall thru and prompt user about posting. if (nsCOMPtr innerWindow = OwnerDoc()->GetInnerWindow()) { if (!innerWindow->IsSecureContext()) { return NS_OK; } } // Bug 1351358: While file URIs are considered to be secure contexts we allow // submitting a form to an insecure URI from a file URI without an alert in an // attempt to avoid compatibility issues. if (window->GetDocumentURI()->SchemeIs("file")) { return NS_OK; } nsCOMPtr docShell = window->GetDocShell(); if (!docShell) { return NS_ERROR_FAILURE; } nsresult rv; nsCOMPtr promptSvc = do_GetService("@mozilla.org/prompter;1", &rv); if (NS_FAILED(rv)) { return rv; } nsCOMPtr stringBundle; nsCOMPtr stringBundleService = mozilla::components::StringBundle::Service(); if (!stringBundleService) { return NS_ERROR_FAILURE; } rv = stringBundleService->CreateBundle( "chrome://global/locale/browser.properties", getter_AddRefs(stringBundle)); if (NS_FAILED(rv)) { return rv; } nsAutoString title; nsAutoString message; nsAutoString cont; stringBundle->GetStringFromName("formPostSecureToInsecureWarning.title", title); stringBundle->GetStringFromName("formPostSecureToInsecureWarning.message", message); stringBundle->GetStringFromName("formPostSecureToInsecureWarning.continue", cont); int32_t buttonPressed; bool checkState = false; // this is unused (ConfirmEx requires this parameter) rv = promptSvc->ConfirmExBC( docShell->GetBrowsingContext(), StaticPrefs::prompts_modalType_insecureFormSubmit(), title.get(), message.get(), (nsIPromptService::BUTTON_TITLE_IS_STRING * nsIPromptService::BUTTON_POS_0) + (nsIPromptService::BUTTON_TITLE_CANCEL * nsIPromptService::BUTTON_POS_1), cont.get(), nullptr, nullptr, nullptr, &checkState, &buttonPressed); if (NS_FAILED(rv)) { return rv; } *aCancelSubmit = (buttonPressed == 1); uint32_t telemetryBucket = nsISecurityUITelemetry::WARNING_CONFIRM_POST_TO_INSECURE_FROM_SECURE; mozilla::Telemetry::Accumulate(mozilla::Telemetry::SECURITY_UI, telemetryBucket); if (!*aCancelSubmit) { // The user opted to continue, so note that in the next telemetry bucket. mozilla::Telemetry::Accumulate(mozilla::Telemetry::SECURITY_UI, telemetryBucket + 1); } return NS_OK; } nsresult HTMLFormElement::NotifySubmitObservers(nsIURI* aActionURL, bool* aCancelSubmit, bool aEarlyNotify) { if (!aEarlyNotify) { nsresult rv = DoSecureToInsecureSubmitCheck(aActionURL, aCancelSubmit); if (NS_FAILED(rv)) { return rv; } if (*aCancelSubmit) { return NS_OK; } } bool defaultAction = true; nsresult rv = nsContentUtils::DispatchEventOnlyToChrome( OwnerDoc(), static_cast(this), aEarlyNotify ? u"DOMFormBeforeSubmit"_ns : u"DOMFormSubmit"_ns, CanBubble::eYes, Cancelable::eYes, &defaultAction); *aCancelSubmit = !defaultAction; if (*aCancelSubmit) { return NS_OK; } return rv; } nsresult HTMLFormElement::ConstructEntryList(FormData* aFormData) { MOZ_ASSERT(aFormData, "Must have FormData!"); if (mIsConstructingEntryList) { // Step 2.2 of https://xhr.spec.whatwg.org/#dom-formdata. return NS_ERROR_DOM_INVALID_STATE_ERR; } AutoRestore resetConstructingEntryList(mIsConstructingEntryList); mIsConstructingEntryList = true; // This shouldn't be called recursively, so use a rather large value // for the preallocated buffer. AutoTArray, 100> sortedControls; nsresult rv = mControls->GetSortedControls(sortedControls); NS_ENSURE_SUCCESS(rv, rv); // Walk the list of nodes and call SubmitNamesValues() on the controls for (nsGenericHTMLFormElement* control : sortedControls) { // Disabled elements don't submit if (!control->IsDisabled()) { nsCOMPtr fc = do_QueryInterface(control); MOZ_ASSERT(fc); // Tell the control to submit its name/value pairs to the submission fc->SubmitNamesValues(aFormData); } } FormDataEventInit init; init.mBubbles = true; init.mCancelable = false; init.mFormData = aFormData; RefPtr event = FormDataEvent::Constructor(this, u"formdata"_ns, init); event->SetTrusted(true); EventDispatcher::DispatchDOMEvent(this, nullptr, event, nullptr, nullptr); return NS_OK; } NotNull HTMLFormElement::GetSubmitEncoding() { nsAutoString acceptCharsetValue; GetAttr(nsGkAtoms::acceptcharset, acceptCharsetValue); int32_t charsetLen = acceptCharsetValue.Length(); if (charsetLen > 0) { int32_t offset = 0; int32_t spPos = 0; // get charset from charsets one by one do { spPos = acceptCharsetValue.FindChar(char16_t(' '), offset); int32_t cnt = ((-1 == spPos) ? (charsetLen - offset) : (spPos - offset)); if (cnt > 0) { nsAutoString uCharset; acceptCharsetValue.Mid(uCharset, offset, cnt); auto encoding = Encoding::ForLabelNoReplacement(uCharset); if (encoding) { return WrapNotNull(encoding); } } offset = spPos + 1; } while (spPos != -1); } // if there are no accept-charset or all the charset are not supported // Get the charset from document Document* doc = GetComposedDoc(); if (doc) { return doc->GetDocumentCharacterSet(); } return UTF_8_ENCODING; } Element* HTMLFormElement::IndexedGetter(uint32_t aIndex, bool& aFound) { Element* element = mControls->mElements->SafeElementAt(aIndex, nullptr); aFound = element != nullptr; return element; } #ifdef DEBUG /** * Checks that all form elements are in document order. Asserts if any pair of * consecutive elements are not in increasing document order. * * @param aControls List of form controls to check. * @param aForm Parent form of the controls. */ /* static */ void HTMLFormElement::AssertDocumentOrder( const nsTArray& aControls, nsIContent* aForm) { // TODO: remove the if directive with bug 598468. // This is done to prevent asserts in some edge cases. # if 0 // Only iterate if aControls is not empty, since otherwise // |aControls.Length() - 1| will be a very large unsigned number... not what // we want here. if (!aControls.IsEmpty()) { for (uint32_t i = 0; i < aControls.Length() - 1; ++i) { NS_ASSERTION( CompareFormControlPosition(aControls[i], aControls[i + 1], aForm) < 0, "Form controls not ordered correctly"); } } # endif } /** * Copy of the above function, but with RefPtrs. * * @param aControls List of form controls to check. * @param aForm Parent form of the controls. */ /* static */ void HTMLFormElement::AssertDocumentOrder( const nsTArray>& aControls, nsIContent* aForm) { // TODO: remove the if directive with bug 598468. // This is done to prevent asserts in some edge cases. # if 0 // Only iterate if aControls is not empty, since otherwise // |aControls.Length() - 1| will be a very large unsigned number... not what // we want here. if (!aControls.IsEmpty()) { for (uint32_t i = 0; i < aControls.Length() - 1; ++i) { NS_ASSERTION( CompareFormControlPosition(aControls[i], aControls[i + 1], aForm) < 0, "Form controls not ordered correctly"); } } # endif } #endif nsresult HTMLFormElement::AddElement(nsGenericHTMLFormElement* aChild, bool aUpdateValidity, bool aNotify) { // If an element has a @form, we can assume it *might* be able to not have // a parent and still be in the form. NS_ASSERTION(aChild->HasAttr(nsGkAtoms::form) || aChild->GetParent(), "Form control should have a parent"); nsCOMPtr fc = do_QueryObject(aChild); MOZ_ASSERT(fc); // Determine whether to add the new element to the elements or // the not-in-elements list. bool childInElements = HTMLFormControlsCollection::ShouldBeInElements(fc); TreeOrderedArray& controlList = childInElements ? mControls->mElements : mControls->mNotInElements; const size_t insertedIndex = controlList.Insert(*aChild, this); const bool lastElement = controlList->Length() == insertedIndex + 1; #ifdef DEBUG AssertDocumentOrder(controlList, this); #endif auto type = fc->ControlType(); // Default submit element handling if (fc->IsSubmitControl()) { // Update mDefaultSubmitElement, mFirstSubmitInElements, // mFirstSubmitNotInElements. nsGenericHTMLFormElement** firstSubmitSlot = childInElements ? &mFirstSubmitInElements : &mFirstSubmitNotInElements; // The new child is the new first submit in its list if the firstSubmitSlot // is currently empty or if the child is before what's currently in the // slot. Note that if we already have a control in firstSubmitSlot and // we're appending this element can't possibly replace what's currently in // the slot. Also note that aChild can't become the mDefaultSubmitElement // unless it replaces what's in the slot. If it _does_ replace what's in // the slot, it becomes the default submit if either the default submit is // what's in the slot or the child is earlier than the default submit. if (!*firstSubmitSlot || (!lastElement && nsContentUtils::CompareTreePosition( aChild, *firstSubmitSlot, this) < 0)) { // Update mDefaultSubmitElement if it's currently in a valid state. // Valid state means either non-null or null because there are in fact // no submit elements around. if ((mDefaultSubmitElement || (!mFirstSubmitInElements && !mFirstSubmitNotInElements)) && (*firstSubmitSlot == mDefaultSubmitElement || nsContentUtils::CompareTreePosition( aChild, mDefaultSubmitElement, this) < 0)) { SetDefaultSubmitElement(aChild); } *firstSubmitSlot = aChild; } MOZ_ASSERT(mDefaultSubmitElement == mFirstSubmitInElements || mDefaultSubmitElement == mFirstSubmitNotInElements || !mDefaultSubmitElement, "What happened here?"); } // If the element is subject to constraint validaton and is invalid, we need // to update our internal counter. if (aUpdateValidity) { nsCOMPtr cvElmt = do_QueryObject(aChild); if (cvElmt && cvElmt->IsCandidateForConstraintValidation() && !cvElmt->IsValid()) { UpdateValidity(false); } } // Notify the radio button it's been added to a group // This has to be done _after_ UpdateValidity() call to prevent the element // being count twice. if (type == FormControlType::InputRadio) { RefPtr radio = static_cast(aChild); radio->AddToRadioGroup(); } return NS_OK; } nsresult HTMLFormElement::AddElementToTable(nsGenericHTMLFormElement* aChild, const nsAString& aName) { return mControls->AddElementToTable(aChild, aName); } void HTMLFormElement::SetDefaultSubmitElement( nsGenericHTMLFormElement* aElement) { if (mDefaultSubmitElement) { // It just so happens that a radio button or an