| /* |
| * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| * (C) 1999 Antti Koivisto (koivisto@kde.org) |
| * (C) 2001 Dirk Mueller (mueller@kde.org) |
| * Copyright (C) 2004-2010, 2012-2016 Apple Inc. All rights reserved. |
| * (C) 2006 Alexey Proskuryakov (ap@nypop.com) |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| * |
| */ |
| |
| #include "config.h" |
| #include "HTMLFormElement.h" |
| |
| #include "DOMFormData.h" |
| #include "DOMTokenList.h" |
| #include "DOMWindow.h" |
| #include "DiagnosticLoggingClient.h" |
| #include "Document.h" |
| #include "ElementIterator.h" |
| #include "Event.h" |
| #include "EventNames.h" |
| #include "FormController.h" |
| #include "FormData.h" |
| #include "FormDataEvent.h" |
| #include "Frame.h" |
| #include "FrameLoader.h" |
| #include "FrameLoaderClient.h" |
| #include "HTMLDialogElement.h" |
| #include "HTMLFieldSetElement.h" |
| #include "HTMLFormControlsCollection.h" |
| #include "HTMLImageElement.h" |
| #include "HTMLInputElement.h" |
| #include "HTMLNames.h" |
| #include "HTMLObjectElement.h" |
| #include "HTMLParserIdioms.h" |
| #include "HTMLTableElement.h" |
| #include "InputTypeNames.h" |
| #include "MixedContentChecker.h" |
| #include "NodeRareData.h" |
| #include "Page.h" |
| #include "PseudoClassChangeInvalidation.h" |
| #include "RadioNodeList.h" |
| #include "RenderTextControl.h" |
| #include "ScriptDisallowedScope.h" |
| #include "Settings.h" |
| #include "SubmitEvent.h" |
| #include "UserGestureIndicator.h" |
| #include <limits> |
| #include <wtf/IsoMallocInlines.h> |
| #include <wtf/Ref.h> |
| #include <wtf/SetForScope.h> |
| |
| namespace WebCore { |
| |
| WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLFormElement); |
| |
| using namespace HTMLNames; |
| |
| struct FormRelAttributes { |
| bool noopener { false }; |
| bool noreferrer { false }; |
| bool opener { false }; |
| }; |
| |
| static FormRelAttributes parseFormRelAttributes(StringView string) |
| { |
| FormRelAttributes attributes; |
| for (auto token : string.split(' ')) { |
| if (equalIgnoringASCIICase(token, "noopener")) |
| attributes.noopener = true; |
| else if (equalIgnoringASCIICase(token, "noreferrer")) |
| attributes.noreferrer = true; |
| else if (equalIgnoringASCIICase(token, "opener")) |
| attributes.opener = true; |
| } |
| return attributes; |
| } |
| |
| HTMLFormElement::HTMLFormElement(const QualifiedName& tagName, Document& document) |
| : HTMLElement(tagName, document) |
| { |
| ASSERT(hasTagName(formTag)); |
| } |
| |
| Ref<HTMLFormElement> HTMLFormElement::create(Document& document) |
| { |
| return adoptRef(*new HTMLFormElement(formTag, document)); |
| } |
| |
| Ref<HTMLFormElement> HTMLFormElement::create(const QualifiedName& tagName, Document& document) |
| { |
| return adoptRef(*new HTMLFormElement(tagName, document)); |
| } |
| |
| HTMLFormElement::~HTMLFormElement() |
| { |
| document().formController().willDeleteForm(*this); |
| if (!shouldAutocomplete()) |
| document().unregisterForDocumentSuspensionCallbacks(*this); |
| |
| m_defaultButton = nullptr; |
| for (auto& weakElement : m_associatedElements) { |
| RefPtr element { weakElement.get() }; |
| ASSERT(element); |
| auto* associatedElement = element->asFormAssociatedElement(); |
| ASSERT(associatedElement); |
| associatedElement->formWillBeDestroyed(); |
| } |
| for (auto& imageElement : m_imageElements) |
| imageElement->m_form = nullptr; |
| } |
| |
| bool HTMLFormElement::formWouldHaveSecureSubmission(const String& url) |
| { |
| return document().completeURL(url).protocolIs("https"); |
| } |
| |
| bool HTMLFormElement::rendererIsNeeded(const RenderStyle& style) |
| { |
| if (!m_wasDemoted) |
| return HTMLElement::rendererIsNeeded(style); |
| |
| auto parent = parentNode(); |
| auto parentRenderer = parent->renderer(); |
| |
| if (!parentRenderer) |
| return false; |
| |
| // FIXME: Shouldn't we also check for table caption (see |formIsTablePart| below). |
| bool parentIsTableElementPart = (parentRenderer->isTable() && is<HTMLTableElement>(*parent)) |
| || (parentRenderer->isTableRow() && parent->hasTagName(trTag)) |
| || (parentRenderer->isTableSection() && parent->hasTagName(tbodyTag)) |
| || (parentRenderer->isRenderTableCol() && parent->hasTagName(colTag)) |
| || (parentRenderer->isTableCell() && parent->hasTagName(trTag)); |
| |
| if (!parentIsTableElementPart) |
| return true; |
| |
| DisplayType display = style.display(); |
| bool formIsTablePart = display == DisplayType::Table || display == DisplayType::InlineTable || display == DisplayType::TableRowGroup |
| || display == DisplayType::TableHeaderGroup || display == DisplayType::TableFooterGroup || display == DisplayType::TableRow |
| || display == DisplayType::TableColumnGroup || display == DisplayType::TableColumn || display == DisplayType::TableCell |
| || display == DisplayType::TableCaption; |
| |
| return formIsTablePart; |
| } |
| |
| Node::InsertedIntoAncestorResult HTMLFormElement::insertedIntoAncestor(InsertionType insertionType, ContainerNode& parentOfInsertedTree) |
| { |
| HTMLElement::insertedIntoAncestor(insertionType, parentOfInsertedTree); |
| if (insertionType.connectedToDocument) |
| document().didAssociateFormControl(*this); |
| return InsertedIntoAncestorResult::Done; |
| } |
| |
| void HTMLFormElement::removedFromAncestor(RemovalType removalType, ContainerNode& oldParentOfRemovedTree) |
| { |
| Node& root = traverseToRootNode(); // Do not rely on rootNode() because our IsInTreeScope is outdated. |
| auto associatedElements = copyAssociatedElementsVector(); |
| for (auto& associatedElement : associatedElements) |
| associatedElement->formOwnerRemovedFromTree(root); |
| HTMLElement::removedFromAncestor(removalType, oldParentOfRemovedTree); |
| } |
| |
| unsigned HTMLFormElement::length() const |
| { |
| unsigned length = 0; |
| for (auto& weakElement : m_associatedElements) { |
| RefPtr element { weakElement.get() }; |
| ASSERT(element); |
| auto* associatedElement = element->asFormAssociatedElement(); |
| ASSERT(associatedElement); |
| if (associatedElement->isEnumeratable()) |
| ++length; |
| } |
| return length; |
| } |
| |
| HTMLElement* HTMLFormElement::item(unsigned index) |
| { |
| return elements()->item(index); |
| } |
| |
| std::optional<std::variant<RefPtr<RadioNodeList>, RefPtr<Element>>> HTMLFormElement::namedItem(const AtomString& name) |
| { |
| auto namedItems = namedElements(name); |
| |
| if (namedItems.isEmpty()) |
| return std::nullopt; |
| if (namedItems.size() == 1) |
| return std::variant<RefPtr<RadioNodeList>, RefPtr<Element>> { RefPtr<Element> { WTFMove(namedItems[0]) } }; |
| |
| return std::variant<RefPtr<RadioNodeList>, RefPtr<Element>> { RefPtr<RadioNodeList> { radioNodeList(name) } }; |
| } |
| |
| Vector<AtomString> HTMLFormElement::supportedPropertyNames() const |
| { |
| // FIXME: Should be implemented (only needed for enumeration with includeDontEnumProperties mode |
| // since this class is annotated with LegacyUnenumerableNamedProperties). |
| return { }; |
| } |
| |
| void HTMLFormElement::submitImplicitly(Event& event, bool fromImplicitSubmissionTrigger) |
| { |
| unsigned submissionTriggerCount = 0; |
| for (auto& formAssociatedElement : m_associatedElements) { |
| if (!is<HTMLFormControlElement>(*formAssociatedElement)) |
| continue; |
| HTMLFormControlElement& formElement = downcast<HTMLFormControlElement>(*formAssociatedElement); |
| if (formElement.isSuccessfulSubmitButton()) { |
| if (formElement.renderer()) { |
| formElement.dispatchSimulatedClick(&event); |
| return; |
| } |
| } else if (formElement.canTriggerImplicitSubmission()) |
| ++submissionTriggerCount; |
| } |
| |
| if (!submissionTriggerCount) |
| return; |
| |
| // Older iOS apps using WebViews expect the behavior of auto submitting multi-input forms. |
| if (fromImplicitSubmissionTrigger && (submissionTriggerCount == 1 || document().settings().allowMultiElementImplicitSubmission())) |
| submitIfPossible(&event); |
| } |
| |
| bool HTMLFormElement::validateInteractively() |
| { |
| for (auto& associatedElement : m_associatedElements) { |
| if (is<HTMLFormControlElement>(*associatedElement)) |
| downcast<HTMLFormControlElement>(*associatedElement).hideVisibleValidationMessage(); |
| } |
| |
| Vector<RefPtr<HTMLFormControlElement>> unhandledInvalidControls; |
| if (!checkInvalidControlsAndCollectUnhandled(unhandledInvalidControls)) |
| return true; |
| // Because the form has invalid controls, we abort the form submission and |
| // show a validation message on a focusable form control. |
| |
| // Make sure layout is up-to-date in case we call isFocusable() (which |
| // has !renderer()->needsLayout() assertion). |
| ASSERT(!document().view() || !document().view()->needsLayout()); |
| |
| Ref<HTMLFormElement> protectedThis(*this); |
| |
| // Focus on the first focusable control and show a validation message. |
| for (auto& control : unhandledInvalidControls) { |
| if (control->isConnected() && control->isFocusable()) { |
| control->focusAndShowValidationMessage(); |
| break; |
| } |
| } |
| |
| // Warn about all of unfocusable controls. |
| if (document().frame()) { |
| for (auto& control : unhandledInvalidControls) { |
| if (control->isConnected() && control->isFocusable()) |
| continue; |
| auto message = makeString("An invalid form control with name='", control->name(), "' is not focusable."); |
| document().addConsoleMessage(MessageSource::Rendering, MessageLevel::Error, message); |
| } |
| } |
| |
| return false; |
| } |
| |
| void HTMLFormElement::submitIfPossible(Event* event, HTMLFormControlElement* submitter, FormSubmissionTrigger trigger) |
| { |
| // https://html.spec.whatwg.org/#form-submission-algorithm |
| if (!isConnected()) |
| return; |
| |
| RefPtr<Frame> frame = document().frame(); |
| if (m_isSubmittingOrPreparingForSubmission || !frame) |
| return; |
| |
| m_isSubmittingOrPreparingForSubmission = true; |
| m_shouldSubmit = false; |
| |
| bool shouldValidate = document().page() && document().page()->settings().interactiveFormValidationEnabled() && !noValidate(); |
| if (shouldValidate) { |
| RefPtr submitElement = submitter ? submitter : findSubmitter(event); |
| if (submitElement && submitElement->formNoValidate()) |
| shouldValidate = false; |
| } |
| |
| // Interactive validation must be done before dispatching the submit event. |
| if (shouldValidate && !validateInteractively()) { |
| m_isSubmittingOrPreparingForSubmission = false; |
| return; |
| } |
| |
| auto targetFrame = frame->loader().findFrameForNavigation(effectiveTarget(event, submitter), &document()); |
| if (!targetFrame) |
| targetFrame = frame.get(); |
| auto formState = FormState::create(*this, textFieldValues(), document(), NotSubmittedByJavaScript); |
| targetFrame->loader().client().dispatchWillSendSubmitEvent(WTFMove(formState)); |
| |
| Ref protectedThis { *this }; |
| |
| auto submitEvent = SubmitEvent::create(submitter); |
| dispatchEvent(submitEvent); |
| |
| // Event handling could have resulted in m_shouldSubmit becoming true as a side effect, too. |
| if (!submitEvent->defaultPrevented()) |
| m_shouldSubmit = true; |
| |
| m_isSubmittingOrPreparingForSubmission = false; |
| |
| if (m_shouldSubmit) |
| submit(event, true, !submitter, trigger, submitter); |
| } |
| |
| void HTMLFormElement::submit() |
| { |
| submit(nullptr, false, true, NotSubmittedByJavaScript); |
| } |
| |
| void HTMLFormElement::submitFromJavaScript() |
| { |
| submit(nullptr, false, UserGestureIndicator::processingUserGesture(), SubmittedByJavaScript); |
| } |
| |
| ExceptionOr<void> HTMLFormElement::requestSubmit(HTMLElement* submitter) |
| { |
| // Update layout before processing form actions in case the style changes |
| // the form or button relationships. |
| document().updateLayoutIgnorePendingStylesheets(); |
| |
| RefPtr<HTMLFormControlElement> control; |
| if (submitter) { |
| // https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit |
| if (!is<HTMLFormControlElement>(submitter)) |
| return Exception { TypeError }; |
| control = downcast<HTMLFormControlElement>(submitter); |
| if (!control->isSubmitButton()) |
| return Exception { TypeError }; |
| if (control->form() != this) |
| return Exception { NotFoundError }; |
| } |
| |
| submitIfPossible(nullptr, control.get(), SubmittedByJavaScript); |
| return { }; |
| } |
| |
| StringPairVector HTMLFormElement::textFieldValues() const |
| { |
| StringPairVector result; |
| result.reserveInitialCapacity(m_associatedElements.size()); |
| for (auto& weakElement : m_associatedElements) { |
| RefPtr element { weakElement.get() }; |
| if (!is<HTMLInputElement>(element)) |
| continue; |
| auto& input = downcast<HTMLInputElement>(*element); |
| if (!input.isTextField()) |
| continue; |
| result.uncheckedAppend({ input.name().string(), input.value() }); |
| } |
| return result; |
| } |
| |
| RefPtr<HTMLFormControlElement> HTMLFormElement::findSubmitButton(HTMLFormControlElement* submitter, bool needButtonActivation) |
| { |
| if (submitter) |
| return submitter; |
| if (!needButtonActivation) |
| return nullptr; |
| RefPtr<HTMLFormControlElement> firstSuccessfulSubmitButton; |
| for (auto& associatedElement : m_associatedElements) { |
| if (!is<HTMLFormControlElement>(*associatedElement)) |
| continue; |
| auto& control = downcast<HTMLFormControlElement>(*associatedElement); |
| if (control.isActivatedSubmit()) |
| return nullptr; |
| if (!firstSuccessfulSubmitButton && control.isSuccessfulSubmitButton()) |
| firstSuccessfulSubmitButton = &control; |
| } |
| return firstSuccessfulSubmitButton; |
| } |
| |
| void HTMLFormElement::submit(Event* event, bool activateSubmitButton, bool processingUserGesture, FormSubmissionTrigger trigger, HTMLFormControlElement* submitter) |
| { |
| // The submitIfPossible function also does this check, but we need to do it here |
| // too, since there are some code paths that bypass that function. |
| if (!isConnected()) |
| return; |
| |
| if (m_isConstructingEntryList) |
| return; |
| |
| RefPtr<FrameView> view = document().view(); |
| RefPtr<Frame> frame = document().frame(); |
| if (!view || !frame) |
| return; |
| |
| if (m_isSubmittingOrPreparingForSubmission) { |
| m_shouldSubmit = true; |
| return; |
| } |
| |
| m_isSubmittingOrPreparingForSubmission = true; |
| m_wasUserSubmitted = processingUserGesture; |
| |
| auto firstSuccessfulSubmitButton = findSubmitButton(submitter, activateSubmitButton); |
| if (firstSuccessfulSubmitButton) |
| firstSuccessfulSubmitButton->setActivatedSubmit(true); |
| |
| Ref protectedThis { *this }; // Form submission can execute arbitary JavaScript. |
| |
| auto shouldLockHistory = processingUserGesture ? LockHistory::No : LockHistory::Yes; |
| auto formSubmission = FormSubmission::create(*this, submitter, m_attributes, event, shouldLockHistory, trigger); |
| |
| if (!isConnected()) |
| return; |
| |
| auto relAttributes = parseFormRelAttributes(getAttribute(HTMLNames::relAttr)); |
| if (relAttributes.noopener || relAttributes.noreferrer || (!relAttributes.opener && document().settings().blankAnchorTargetImpliesNoOpenerEnabled() && isBlankTargetFrameName(formSubmission->target()) && !formSubmission->requestURL().protocolIsJavaScript())) |
| formSubmission->setNewFrameOpenerPolicy(NewFrameOpenerPolicy::Suppress); |
| if (relAttributes.noreferrer) |
| formSubmission->setReferrerPolicy(ReferrerPolicy::NoReferrer); |
| |
| if (m_plannedFormSubmission) |
| m_plannedFormSubmission->cancel(); |
| |
| m_plannedFormSubmission = formSubmission; |
| |
| if (document().settings().dialogElementEnabled() && formSubmission->method() == FormSubmission::Method::Dialog) |
| submitDialog(WTFMove(formSubmission)); |
| else |
| frame->loader().submitForm(WTFMove(formSubmission)); |
| |
| if (firstSuccessfulSubmitButton) |
| firstSuccessfulSubmitButton->setActivatedSubmit(false); |
| |
| m_shouldSubmit = false; |
| m_isSubmittingOrPreparingForSubmission = false; |
| } |
| |
| // https://html.spec.whatwg.org/#submit-dialog |
| void HTMLFormElement::submitDialog(Ref<FormSubmission>&& formSubmission) |
| { |
| // Let subject be the nearest ancestor dialog element of form, if any. |
| RefPtr dialog = ancestorsOfType<HTMLDialogElement>(*this).first(); |
| |
| // If there isn't one, or if it does not have an open attribute, do nothing. |
| if (!dialog || !dialog->isOpen()) |
| return; |
| |
| // Then, close the dialog subject. If there is a result, let that be the return value. |
| dialog->close(formSubmission->returnValue()); |
| } |
| |
| void HTMLFormElement::reset() |
| { |
| if (m_isInResetFunction) |
| return; |
| |
| RefPtr<Frame> protectedFrame = document().frame(); |
| if (!protectedFrame) |
| return; |
| |
| Ref<HTMLFormElement> protectedThis(*this); |
| |
| SetForScope<bool> isInResetFunctionRestorer(m_isInResetFunction, true); |
| |
| auto event = Event::create(eventNames().resetEvent, Event::CanBubble::Yes, Event::IsCancelable::Yes); |
| dispatchEvent(event); |
| if (!event->defaultPrevented()) |
| resetAssociatedFormControlElements(); |
| } |
| |
| void HTMLFormElement::resetAssociatedFormControlElements() |
| { |
| // Event handling can cause associated elements to be added or deleted while iterating |
| // over this collection. Protect these elements until we are done notifying them of |
| // the reset operation. |
| Vector<Ref<HTMLFormControlElement>> associatedFormControlElements; |
| associatedFormControlElements.reserveInitialCapacity(m_associatedElements.size()); |
| for (auto& weakElement : m_associatedElements) { |
| auto* element = weakElement.get(); |
| ASSERT(element); |
| if (is<HTMLFormControlElement>(element)) |
| associatedFormControlElements.uncheckedAppend(downcast<HTMLFormControlElement>(*element)); |
| } |
| |
| for (auto& associatedFormControlElement : associatedFormControlElements) |
| associatedFormControlElement->reset(); |
| } |
| |
| #if ENABLE(AUTOCORRECT) |
| |
| // FIXME: We should look to share this code with class HTMLFormControlElement instead of duplicating the logic. |
| |
| bool HTMLFormElement::shouldAutocorrect() const |
| { |
| const AtomString& autocorrectValue = attributeWithoutSynchronization(autocorrectAttr); |
| if (!autocorrectValue.isEmpty()) |
| return !equalLettersIgnoringASCIICase(autocorrectValue, "off"); |
| if (RefPtr<HTMLFormElement> form = this->form()) |
| return form->shouldAutocorrect(); |
| return true; |
| } |
| |
| #endif |
| |
| void HTMLFormElement::parseAttribute(const QualifiedName& name, const AtomString& value) |
| { |
| if (name == actionAttr) { |
| m_attributes.parseAction(value); |
| |
| if (!m_attributes.action().isEmpty()) { |
| if (RefPtr<Frame> f = document().frame()) { |
| Frame& topFrame = f->tree().top(); |
| MixedContentChecker::checkFormForMixedContent(topFrame, topFrame.document()->securityOrigin(), document().completeURL(m_attributes.action())); |
| } |
| } |
| } else if (name == targetAttr) |
| m_attributes.setTarget(value); |
| else if (name == methodAttr) |
| m_attributes.updateMethodType(value, document().settings().dialogElementEnabled()); |
| else if (name == enctypeAttr) |
| m_attributes.updateEncodingType(value); |
| else if (name == accept_charsetAttr) |
| m_attributes.setAcceptCharset(value); |
| else if (name == autocompleteAttr) { |
| if (!shouldAutocomplete()) |
| document().registerForDocumentSuspensionCallbacks(*this); |
| else |
| document().unregisterForDocumentSuspensionCallbacks(*this); |
| } else if (name == relAttr) { |
| if (m_relList) |
| m_relList->associatedAttributeValueChanged(value); |
| } else |
| HTMLElement::parseAttribute(name, value); |
| } |
| |
| unsigned HTMLFormElement::formElementIndexWithFormAttribute(Element* element, unsigned rangeStart, unsigned rangeEnd) |
| { |
| if (m_associatedElements.isEmpty()) |
| return 0; |
| |
| ASSERT(rangeStart <= rangeEnd); |
| |
| if (rangeStart == rangeEnd) |
| return rangeStart; |
| |
| unsigned left = rangeStart; |
| unsigned right = rangeEnd - 1; |
| unsigned short position; |
| |
| // Does binary search on m_associatedElements in order to find the index |
| // to be inserted. |
| while (left != right) { |
| unsigned middle = left + ((right - left) / 2); |
| ASSERT(middle < m_associatedElementsBeforeIndex || middle >= m_associatedElementsAfterIndex); |
| position = element->compareDocumentPosition(*m_associatedElements[middle]); |
| if (position & DOCUMENT_POSITION_FOLLOWING) |
| right = middle; |
| else |
| left = middle + 1; |
| } |
| |
| ASSERT(left < m_associatedElementsBeforeIndex || left >= m_associatedElementsAfterIndex); |
| position = element->compareDocumentPosition(*m_associatedElements[left]); |
| if (position & DOCUMENT_POSITION_FOLLOWING) |
| return left; |
| return left + 1; |
| } |
| |
| unsigned HTMLFormElement::formElementIndex(FormAssociatedElement* associatedElement) |
| { |
| ASSERT(associatedElement); |
| |
| HTMLElement& associatedHTMLElement = associatedElement->asHTMLElement(); |
| |
| // Treats separately the case where this element has the form attribute |
| // for performance consideration. |
| if (associatedHTMLElement.hasAttributeWithoutSynchronization(formAttr) && associatedHTMLElement.isConnected()) { |
| unsigned short position = compareDocumentPosition(associatedHTMLElement); |
| ASSERT(!(position & DOCUMENT_POSITION_DISCONNECTED)); |
| if (position & DOCUMENT_POSITION_PRECEDING) { |
| ++m_associatedElementsBeforeIndex; |
| ++m_associatedElementsAfterIndex; |
| return HTMLFormElement::formElementIndexWithFormAttribute(&associatedHTMLElement, 0, m_associatedElementsBeforeIndex - 1); |
| } |
| if (position & DOCUMENT_POSITION_FOLLOWING && !(position & DOCUMENT_POSITION_CONTAINED_BY)) |
| return HTMLFormElement::formElementIndexWithFormAttribute(&associatedHTMLElement, m_associatedElementsAfterIndex, m_associatedElements.size()); |
| } |
| |
| unsigned currentAssociatedElementsAfterIndex = m_associatedElementsAfterIndex; |
| ++m_associatedElementsAfterIndex; |
| |
| if (!associatedHTMLElement.isDescendantOf(*this)) |
| return currentAssociatedElementsAfterIndex; |
| |
| auto descendants = descendantsOfType<HTMLElement>(*this); |
| |
| // Check for the special case where this element is the very last thing in |
| // the form's tree of children; we don't want to walk the entire tree in that |
| // common case that occurs during parsing; instead we'll just return a value |
| // that says "add this form element to the end of the array". |
| if (!++descendants.beginAt(associatedHTMLElement)) |
| return currentAssociatedElementsAfterIndex; |
| |
| unsigned i = m_associatedElementsBeforeIndex; |
| for (auto& element : descendants) { |
| if (&element == &associatedHTMLElement) |
| return i; |
| if (!is<HTMLFormControlElement>(element) && !is<HTMLObjectElement>(element)) |
| continue; |
| if (element.form() != this) |
| continue; |
| ++i; |
| } |
| return currentAssociatedElementsAfterIndex; |
| } |
| |
| void HTMLFormElement::registerFormElement(FormAssociatedElement* e) |
| { |
| m_associatedElements.insert(formElementIndex(e), e->asHTMLElement()); |
| |
| if (is<HTMLFormControlElement>(e)) { |
| HTMLFormControlElement& control = downcast<HTMLFormControlElement>(*e); |
| if (control.isSuccessfulSubmitButton()) { |
| if (!m_defaultButton) |
| control.invalidateStyleForSubtree(); |
| else |
| resetDefaultButton(); |
| } |
| } |
| } |
| |
| void HTMLFormElement::removeFormElement(FormAssociatedElement* e) |
| { |
| unsigned index = m_associatedElements.find(&e->asHTMLElement()); |
| ASSERT(index < m_associatedElements.size()); |
| if (index < m_associatedElementsBeforeIndex) |
| --m_associatedElementsBeforeIndex; |
| if (index < m_associatedElementsAfterIndex) |
| --m_associatedElementsAfterIndex; |
| removeFromPastNamesMap(e); |
| m_associatedElements.remove(index); |
| |
| if (auto* nodeLists = this->nodeLists()) |
| nodeLists->invalidateCaches(); |
| |
| if (e == m_defaultButton) |
| resetDefaultButton(); |
| } |
| |
| void HTMLFormElement::registerInvalidAssociatedFormControl(const HTMLFormControlElement& formControlElement) |
| { |
| ASSERT_WITH_MESSAGE(!is<HTMLFieldSetElement>(formControlElement), "FieldSet are never candidates for constraint validation."); |
| ASSERT(static_cast<const Element&>(formControlElement).matchesInvalidPseudoClass()); |
| |
| std::optional<Style::PseudoClassChangeInvalidation> styleInvalidation; |
| if (m_invalidAssociatedFormControls.computesEmpty()) |
| emplace(styleInvalidation, *this, { { CSSSelector::PseudoClassValid, false }, { CSSSelector::PseudoClassInvalid, true } }); |
| |
| m_invalidAssociatedFormControls.add(const_cast<HTMLFormControlElement&>(formControlElement)); |
| } |
| |
| void HTMLFormElement::removeInvalidAssociatedFormControlIfNeeded(const HTMLFormControlElement& formControlElement) |
| { |
| if (!m_invalidAssociatedFormControls.contains(formControlElement)) |
| return; |
| |
| std::optional<Style::PseudoClassChangeInvalidation> styleInvalidation; |
| if (m_invalidAssociatedFormControls.computeSize() == 1) |
| emplace(styleInvalidation, *this, { { CSSSelector::PseudoClassValid, true }, { CSSSelector::PseudoClassInvalid, false } }); |
| |
| m_invalidAssociatedFormControls.remove(formControlElement); |
| } |
| |
| bool HTMLFormElement::isURLAttribute(const Attribute& attribute) const |
| { |
| return attribute.name() == actionAttr || HTMLElement::isURLAttribute(attribute); |
| } |
| |
| void HTMLFormElement::registerImgElement(HTMLImageElement* e) |
| { |
| ASSERT(m_imageElements.find(e) == notFound); |
| m_imageElements.append(e); |
| } |
| |
| void HTMLFormElement::removeImgElement(HTMLImageElement* e) |
| { |
| removeFromPastNamesMap(e); |
| bool removed = m_imageElements.removeFirst(e); |
| ASSERT_UNUSED(removed, removed); |
| } |
| |
| Ref<HTMLFormControlsCollection> HTMLFormElement::elements() |
| { |
| return ensureRareData().ensureNodeLists().addCachedCollection<HTMLFormControlsCollection>(*this, FormControls); |
| } |
| |
| Ref<HTMLCollection> HTMLFormElement::elementsForNativeBindings() |
| { |
| return elements(); |
| } |
| |
| String HTMLFormElement::name() const |
| { |
| return getNameAttribute(); |
| } |
| |
| bool HTMLFormElement::noValidate() const |
| { |
| return hasAttributeWithoutSynchronization(novalidateAttr); |
| } |
| |
| String HTMLFormElement::action() const |
| { |
| auto& value = attributeWithoutSynchronization(actionAttr); |
| if (value.isEmpty()) |
| return document().url().string(); |
| return document().completeURL(stripLeadingAndTrailingHTMLSpaces(value)).string(); |
| } |
| |
| void HTMLFormElement::setAction(const String& value) |
| { |
| setAttributeWithoutSynchronization(actionAttr, value); |
| } |
| |
| void HTMLFormElement::setEnctype(const String& value) |
| { |
| setAttributeWithoutSynchronization(enctypeAttr, value); |
| } |
| |
| String HTMLFormElement::method() const |
| { |
| return FormSubmission::Attributes::methodString(m_attributes.method(), document().settings().dialogElementEnabled()); |
| } |
| |
| void HTMLFormElement::setMethod(const String& value) |
| { |
| setAttributeWithoutSynchronization(methodAttr, value); |
| } |
| |
| DOMTokenList& HTMLFormElement::relList() |
| { |
| if (!m_relList) { |
| m_relList = makeUnique<DOMTokenList>(*this, HTMLNames::relAttr, [](Document&, StringView token) { |
| return equalIgnoringASCIICase(token, "noreferrer") || equalIgnoringASCIICase(token, "noopener") || equalIgnoringASCIICase(token, "opener"); |
| }); |
| } |
| return *m_relList; |
| } |
| |
| String HTMLFormElement::target() const |
| { |
| return attributeWithoutSynchronization(targetAttr); |
| } |
| |
| String HTMLFormElement::effectiveTarget(const Event* event, HTMLFormControlElement* overrideSubmitter) const |
| { |
| if (RefPtr submitter = overrideSubmitter ? overrideSubmitter : findSubmitter(event)) { |
| auto targetValue = submitter->attributeWithoutSynchronization(formtargetAttr); |
| if (!targetValue.isNull()) |
| return targetValue; |
| } |
| |
| auto targetValue = target(); |
| if (!targetValue.isNull()) |
| return targetValue; |
| |
| return document().baseTarget(); |
| } |
| |
| bool HTMLFormElement::wasUserSubmitted() const |
| { |
| return m_wasUserSubmitted; |
| } |
| |
| HTMLFormControlElement* HTMLFormElement::findSubmitter(const Event* event) const |
| { |
| if (!event || !is<Node>(event->target())) |
| return nullptr; |
| auto& node = downcast<Node>(*event->target()); |
| auto* element = is<Element>(node) ? &downcast<Element>(node) : node.parentElement(); |
| return element ? lineageOfType<HTMLFormControlElement>(*element).first() : nullptr; |
| } |
| |
| HTMLFormControlElement* HTMLFormElement::defaultButton() const |
| { |
| if (m_defaultButton) |
| return m_defaultButton.get(); |
| for (auto& associatedElement : m_associatedElements) { |
| if (!is<HTMLFormControlElement>(*associatedElement)) |
| continue; |
| HTMLFormControlElement& control = downcast<HTMLFormControlElement>(*associatedElement); |
| if (control.isSuccessfulSubmitButton()) { |
| m_defaultButton = control; |
| return &control; |
| } |
| } |
| return nullptr; |
| } |
| |
| void HTMLFormElement::resetDefaultButton() |
| { |
| if (!m_defaultButton) { |
| // Computing the default button is not cheap, we don't want to do it unless needed. |
| // If there was no default button set, the only style to invalidate is the element |
| // being added to the form. This is done explicitly in registerFormElement(). |
| return; |
| } |
| |
| ScriptDisallowedScope::InMainThread scriptDisallowedScope; |
| |
| auto oldDefault = WTFMove(m_defaultButton); |
| defaultButton(); |
| if (m_defaultButton != oldDefault) { |
| if (oldDefault) |
| oldDefault->invalidateStyleForSubtree(); |
| if (m_defaultButton) |
| m_defaultButton->invalidateStyleForSubtree(); |
| } |
| } |
| |
| bool HTMLFormElement::checkValidity() |
| { |
| Vector<RefPtr<HTMLFormControlElement>> controls; |
| return !checkInvalidControlsAndCollectUnhandled(controls); |
| } |
| |
| bool HTMLFormElement::checkInvalidControlsAndCollectUnhandled(Vector<RefPtr<HTMLFormControlElement>>& unhandledInvalidControls) |
| { |
| Ref<HTMLFormElement> protectedThis(*this); |
| // Copy m_associatedElements because event handlers called from |
| // HTMLFormControlElement::checkValidity() might change m_associatedElements. |
| auto elements = copyAssociatedElementsVector(); |
| bool hasInvalidControls = false; |
| for (auto& element : elements) { |
| if (element->form() == this && is<HTMLFormControlElement>(element)) { |
| HTMLFormControlElement& control = downcast<HTMLFormControlElement>(element.get()); |
| if (!control.checkValidity(&unhandledInvalidControls) && control.form() == this) |
| hasInvalidControls = true; |
| } |
| } |
| return hasInvalidControls; |
| } |
| |
| bool HTMLFormElement::reportValidity() |
| { |
| Ref<HTMLFormElement> protectedThis(*this); |
| |
| // Update layout before processing form actions in case the style changes |
| // the form or button relationships. |
| document().updateLayoutIgnorePendingStylesheets(); |
| |
| return validateInteractively(); |
| } |
| |
| #if ASSERT_ENABLED |
| void HTMLFormElement::assertItemCanBeInPastNamesMap(FormNamedItem* item) const |
| { |
| ASSERT(item); |
| HTMLElement& element = item->asHTMLElement(); |
| ASSERT(element.form() == this); |
| |
| if (item->isFormAssociatedElement()) { |
| ASSERT(m_associatedElements.find(&element) != notFound); |
| return; |
| } |
| |
| ASSERT(element.hasTagName(imgTag)); |
| ASSERT(m_imageElements.find(&downcast<HTMLImageElement>(element)) != notFound); |
| } |
| #endif |
| |
| RefPtr<HTMLElement> HTMLFormElement::elementFromPastNamesMap(const AtomString& pastName) const |
| { |
| if (pastName.isEmpty() || m_pastNamesMap.isEmpty()) |
| return nullptr; |
| auto weakElement = m_pastNamesMap.get(pastName); |
| if (!weakElement) |
| return nullptr; |
| RefPtr element { weakElement.get() }; |
| #if ASSERT_ENABLED |
| assertItemCanBeInPastNamesMap(element->asFormNamedItem()); |
| #endif |
| return element; |
| } |
| |
| void HTMLFormElement::addToPastNamesMap(FormNamedItem* item, const AtomString& pastName) |
| { |
| #if ASSERT_ENABLED |
| assertItemCanBeInPastNamesMap(item); |
| #endif |
| if (pastName.isEmpty()) |
| return; |
| m_pastNamesMap.set(pastName.impl(), item->asHTMLElement()); |
| } |
| |
| void HTMLFormElement::removeFromPastNamesMap(FormNamedItem* item) |
| { |
| ASSERT(item); |
| if (m_pastNamesMap.isEmpty()) |
| return; |
| |
| m_pastNamesMap.removeIf([&element = item->asHTMLElement()] (auto& iterator) { |
| return iterator.value == &element; |
| }); |
| } |
| |
| bool HTMLFormElement::matchesValidPseudoClass() const |
| { |
| return m_invalidAssociatedFormControls.computesEmpty(); |
| } |
| |
| bool HTMLFormElement::matchesInvalidPseudoClass() const |
| { |
| return !matchesValidPseudoClass(); |
| } |
| |
| // FIXME: Use Ref<HTMLElement> for the function result since there are no non-HTML elements returned here. |
| Vector<Ref<Element>> HTMLFormElement::namedElements(const AtomString& name) |
| { |
| // http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#dom-form-nameditem |
| Vector<Ref<Element>> namedItems = elements()->namedItems(name); |
| |
| auto elementFromPast = elementFromPastNamesMap(name); |
| if (namedItems.size() == 1 && namedItems.first().ptr() != elementFromPast) |
| addToPastNamesMap(downcast<HTMLElement>(namedItems.first().get()).asFormNamedItem(), name); |
| else if (elementFromPast && namedItems.isEmpty()) |
| namedItems.append(*elementFromPast); |
| |
| return namedItems; |
| } |
| |
| void HTMLFormElement::resumeFromDocumentSuspension() |
| { |
| ASSERT(!shouldAutocomplete()); |
| |
| document().postTask([formElement = Ref { *this }] (ScriptExecutionContext&) { |
| formElement->resetAssociatedFormControlElements(); |
| }); |
| } |
| |
| void HTMLFormElement::didMoveToNewDocument(Document& oldDocument, Document& newDocument) |
| { |
| if (!shouldAutocomplete()) { |
| oldDocument.unregisterForDocumentSuspensionCallbacks(*this); |
| newDocument.registerForDocumentSuspensionCallbacks(*this); |
| } |
| |
| HTMLElement::didMoveToNewDocument(oldDocument, newDocument); |
| } |
| |
| bool HTMLFormElement::shouldAutocomplete() const |
| { |
| return !equalLettersIgnoringASCIICase(attributeWithoutSynchronization(autocompleteAttr), "off"); |
| } |
| |
| void HTMLFormElement::finishParsingChildren() |
| { |
| HTMLElement::finishParsingChildren(); |
| document().formController().restoreControlStateIn(*this); |
| } |
| |
| const Vector<WeakPtr<HTMLElement>>& HTMLFormElement::unsafeAssociatedElements() const |
| { |
| ASSERT(ScriptDisallowedScope::InMainThread::hasDisallowedScope()); |
| return m_associatedElements; |
| } |
| |
| Vector<Ref<FormAssociatedElement>> HTMLFormElement::copyAssociatedElementsVector() const |
| { |
| return WTF::map(m_associatedElements, [] (auto& weakElement) { |
| RefPtr element { weakElement.get() }; |
| ASSERT(element); |
| auto* formAssociatedElement = element->asFormAssociatedElement(); |
| ASSERT(formAssociatedElement); |
| return Ref<FormAssociatedElement>(*formAssociatedElement); |
| }); |
| } |
| |
| void HTMLFormElement::copyNonAttributePropertiesFromElement(const Element& source) |
| { |
| m_wasDemoted = static_cast<const HTMLFormElement&>(source).m_wasDemoted; |
| HTMLElement::copyNonAttributePropertiesFromElement(source); |
| } |
| |
| HTMLFormElement* HTMLFormElement::findClosestFormAncestor(const Element& startElement) |
| { |
| return const_cast<HTMLFormElement*>(ancestorsOfType<HTMLFormElement>(startElement).first()); |
| } |
| |
| void HTMLFormElement::setAutocomplete(const AtomString& value) |
| { |
| setAttributeWithoutSynchronization(autocompleteAttr, value); |
| } |
| |
| const AtomString& HTMLFormElement::autocomplete() const |
| { |
| static MainThreadNeverDestroyed<const AtomString> on("on", AtomString::ConstructFromLiteral); |
| static MainThreadNeverDestroyed<const AtomString> off("off", AtomString::ConstructFromLiteral); |
| |
| return equalIgnoringASCIICase(attributeWithoutSynchronization(autocompleteAttr), "off") ? off : on; |
| } |
| |
| RefPtr<DOMFormData> HTMLFormElement::constructEntryList(Ref<DOMFormData>&& domFormData, StringPairVector* formValues) |
| { |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-form-data-set |
| ASSERT(isMainThread()); |
| |
| if (m_isConstructingEntryList) |
| return nullptr; |
| |
| SetForScope<bool> isConstructingEntryListScope(m_isConstructingEntryList, true); |
| |
| for (auto& control : this->copyAssociatedElementsVector()) { |
| auto& element = control->asHTMLElement(); |
| if (!element.isDisabledFormControl()) |
| control->appendFormData(domFormData.get()); |
| if (formValues && is<HTMLInputElement>(element)) { |
| auto& input = downcast<HTMLInputElement>(element); |
| if (input.isTextField()) { |
| formValues->append({ input.name(), input.value() }); |
| input.addSearchResult(); |
| } |
| } |
| } |
| |
| dispatchEvent(FormDataEvent::create(eventNames().formdataEvent, Event::CanBubble::Yes, Event::IsCancelable::No, Event::IsComposed::No, domFormData.copyRef())); |
| |
| return domFormData->clone(); |
| } |
| |
| } // namespace |