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 ArrayList<Element> linkedEls = new ArrayList<>();
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     Removes all child nodes from this form in the DOM, leaving the form itself in place.
072     Controls associated with this form but located elsewhere in the document remain unchanged.
073
074     @return this form
075     */
076    @Override
077    public Element empty() {
078        // clear descendant associations before their parent links are removed
079        linkedEls.removeIf(this::isDescendant);
080        return super.empty();
081    }
082
083    private boolean isDescendant(Element element) {
084        for (Element parent = element.parent(); parent != null; parent = parent.parent()) {
085            if (parent == this) return true;
086        }
087        return false;
088    }
089
090    /**
091     Prepare to submit this form. A Connection object is created with the request set up from the form values. This
092     Connection will inherit the settings and the cookies (etc) of the connection/session used to request this Document
093     (if any), as available in {@link Document#connection()}
094     <p>You can then set up other options (like user-agent, timeout, cookies), then execute it.</p>
095
096     @return a connection prepared from the values of this form, in the same session as the one used to request it
097     @throws IllegalArgumentException if the form's absolute action URL cannot be determined. Make sure you pass the
098     document's base URI when parsing.
099     */
100    public Connection submit() {
101        String action = hasAttr("action") ? absUrl("action") : baseUri();
102        Validate.notEmpty(action, "Could not determine a form action URL for submit. Ensure you set a base URI when parsing.");
103        Connection.Method method = attr("method").equalsIgnoreCase("POST") ?
104                Connection.Method.POST : Connection.Method.GET;
105
106        Document owner = ownerDocument();
107        Connection connection = owner != null? owner.connection().newRequest() : Jsoup.newSession();
108        return connection.url(action)
109                .data(formData())
110                .method(method);
111    }
112
113    /**
114     * Get the data that this form submits. The returned list is a copy of the data, and changes to the contents of the
115     * list will not be reflected in the DOM.
116     * @return a list of key vals
117     */
118    public List<Connection.KeyVal> formData() {
119        ArrayList<Connection.KeyVal> data = new ArrayList<>();
120
121        // iterate the form control elements and accumulate their values
122        Elements formEls = elements();
123        for (Element el: formEls) {
124            if (!el.tag().isFormSubmittable()) continue; // contents are form listable, superset of submitable
125            if (el.hasAttr("disabled")) continue; // skip disabled form inputs
126            String name = el.attr("name");
127            if (name.length() == 0) continue;
128            String type = el.attr("type");
129
130            if (type.equalsIgnoreCase("button") || type.equalsIgnoreCase("image")) continue; // browsers don't submit these
131
132            if (el.nameIs("select")) {
133                Elements options = el.select("option[selected]");
134                boolean set = false;
135                for (Element option: options) {
136                    data.add(HttpConnection.KeyVal.create(name, option.val()));
137                    set = true;
138                }
139                if (!set) {
140                    Element option = el.selectFirst("option");
141                    if (option != null)
142                        data.add(HttpConnection.KeyVal.create(name, option.val()));
143                }
144            } else if ("checkbox".equalsIgnoreCase(type) || "radio".equalsIgnoreCase(type)) {
145                // only add checkbox or radio if they have the checked attribute
146                if (el.hasAttr("checked")) {
147                    final String val = el.val().length() >  0 ? el.val() : "on";
148                    data.add(HttpConnection.KeyVal.create(name, val));
149                }
150            } else {
151                data.add(HttpConnection.KeyVal.create(name, el.val()));
152            }
153        }
154        return data;
155    }
156
157    @Override
158    public FormElement clone() {
159        return (FormElement) super.clone();
160    }
161}