001package org.jsoup.nodes;
002
003import org.jsoup.Connection;
004import org.jsoup.Jsoup;
005import org.jsoup.helper.HttpConnection;
006import org.jsoup.helper.Validate;
007import org.jsoup.internal.SharedConstants;
008import org.jsoup.internal.StringUtil;
009import org.jsoup.parser.Tag;
010import org.jsoup.select.Elements;
011import org.jsoup.select.Evaluator;
012import org.jsoup.select.Selector;
013import org.jspecify.annotations.Nullable;
014
015import java.util.ArrayList;
016import java.util.List;
017
018/**
019 * An HTML Form Element provides ready access to the form fields/controls that are associated with it. It also allows a
020 * form to easily be submitted.
021 */
022public class FormElement extends Element {
023    private final Elements linkedEls = new Elements();
024    // contains form submittable elements that were linked during the parse (and due to parse rules, may no longer be a child of this form)
025    private static final Evaluator submittable = Selector.evaluatorOf(StringUtil.join(SharedConstants.FormSubmitTags, ", "));
026
027    /**
028     * Create a new, standalone form element.
029     *
030     * @param tag        tag of this element
031     * @param baseUri    the base URI
032     * @param attributes initial attributes
033     */
034    public FormElement(Tag tag, @Nullable String baseUri, @Nullable Attributes attributes) {
035        super(tag, baseUri, attributes);
036    }
037
038    /**
039     * Get the list of form control elements associated with this form.
040     * @return form controls associated with this element.
041     */
042    public Elements elements() {
043        // As elements may have been added or removed from the DOM after parse, prepare a new list that unions them:
044        Elements els = select(submittable); // current form children
045        for (Element linkedEl : linkedEls) {
046            if (linkedEl.ownerDocument() != null && !els.contains(linkedEl)) {
047                els.add(linkedEl); // adds previously linked elements, that weren't previously removed from the DOM
048            }
049        }
050
051        return els;
052    }
053
054    /**
055     * Add a form control element to this form.
056     * @param element form control to add
057     * @return this form element, for chaining
058     */
059    public FormElement addElement(Element element) {
060        linkedEls.add(element);
061        return this;
062    }
063
064    @Override
065    protected void removeChild(Node out) {
066        super.removeChild(out);
067        linkedEls.remove(out);
068    }
069
070    /**
071     Prepare to submit this form. A Connection object is created with the request set up from the form values. This
072     Connection will inherit the settings and the cookies (etc) of the connection/session used to request this Document
073     (if any), as available in {@link Document#connection()}
074     <p>You can then set up other options (like user-agent, timeout, cookies), then execute it.</p>
075
076     @return a connection prepared from the values of this form, in the same session as the one used to request it
077     @throws IllegalArgumentException if the form's absolute action URL cannot be determined. Make sure you pass the
078     document's base URI when parsing.
079     */
080    public Connection submit() {
081        String action = hasAttr("action") ? absUrl("action") : baseUri();
082        Validate.notEmpty(action, "Could not determine a form action URL for submit. Ensure you set a base URI when parsing.");
083        Connection.Method method = attr("method").equalsIgnoreCase("POST") ?
084                Connection.Method.POST : Connection.Method.GET;
085
086        Document owner = ownerDocument();
087        Connection connection = owner != null? owner.connection().newRequest() : Jsoup.newSession();
088        return connection.url(action)
089                .data(formData())
090                .method(method);
091    }
092
093    /**
094     * Get the data that this form submits. The returned list is a copy of the data, and changes to the contents of the
095     * list will not be reflected in the DOM.
096     * @return a list of key vals
097     */
098    public List<Connection.KeyVal> formData() {
099        ArrayList<Connection.KeyVal> data = new ArrayList<>();
100
101        // iterate the form control elements and accumulate their values
102        Elements formEls = elements();
103        for (Element el: formEls) {
104            if (!el.tag().isFormSubmittable()) continue; // contents are form listable, superset of submitable
105            if (el.hasAttr("disabled")) continue; // skip disabled form inputs
106            String name = el.attr("name");
107            if (name.length() == 0) continue;
108            String type = el.attr("type");
109
110            if (type.equalsIgnoreCase("button") || type.equalsIgnoreCase("image")) continue; // browsers don't submit these
111
112            if (el.nameIs("select")) {
113                Elements options = el.select("option[selected]");
114                boolean set = false;
115                for (Element option: options) {
116                    data.add(HttpConnection.KeyVal.create(name, option.val()));
117                    set = true;
118                }
119                if (!set) {
120                    Element option = el.selectFirst("option");
121                    if (option != null)
122                        data.add(HttpConnection.KeyVal.create(name, option.val()));
123                }
124            } else if ("checkbox".equalsIgnoreCase(type) || "radio".equalsIgnoreCase(type)) {
125                // only add checkbox or radio if they have the checked attribute
126                if (el.hasAttr("checked")) {
127                    final String val = el.val().length() >  0 ? el.val() : "on";
128                    data.add(HttpConnection.KeyVal.create(name, val));
129                }
130            } else {
131                data.add(HttpConnection.KeyVal.create(name, el.val()));
132            }
133        }
134        return data;
135    }
136
137    @Override
138    public FormElement clone() {
139        return (FormElement) super.clone();
140    }
141}