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}