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}