View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html;
16  
17  import java.io.File;
18  import java.net.URI;
19  import java.net.URISyntaxException;
20  import java.nio.charset.Charset;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.apache.commons.io.FileUtils;
27  import org.htmlunit.BrowserVersion;
28  import org.htmlunit.SgmlPage;
29  import org.htmlunit.javascript.host.event.Event;
30  import org.htmlunit.util.KeyDataPair;
31  import org.htmlunit.util.MimeType;
32  import org.htmlunit.util.NameValuePair;
33  import org.htmlunit.util.StringUtils;
34  
35  /**
36   * Wrapper for the HTML element "input".
37   *
38   * @author Mike Bowler
39   * @author Christian Sell
40   * @author Daniel Gredler
41   * @author Ahmed Ashour
42   * @author Marc Guillemot
43   * @author Frank Danek
44   * @author Ronald Brill
45   */
46  public class HtmlFileInput extends HtmlInput implements LabelableElement {
47  
48      private String contentType_;
49      private byte[] data_;
50      private File[] files_ = new File[0];
51  
52      /**
53       * Creates an instance.
54       *
55       * @param qualifiedName the qualified name of the element type to instantiate
56       * @param page the page that contains this element
57       * @param attributes the initial attributes
58       */
59      HtmlFileInput(final String qualifiedName, final SgmlPage page,
60              final Map<String, DomAttr> attributes) {
61          super(qualifiedName, page, attributes);
62  
63          final DomAttr valueAttrib = attributes.get(VALUE_ATTRIBUTE);
64          if (valueAttrib != null) {
65              setDefaultValue(valueAttrib.getNodeValue());
66          }
67      }
68  
69      /**
70       * Returns the in-memory data assigned to this file input element, if any.
71       * @return {@code null} if {@link #setData(byte[])} hasn't been used
72       */
73      public final byte[] getData() {
74          return data_;
75      }
76  
77      /**
78       * <p>Assigns in-memory data to this file input element. During submission, instead
79       * of loading data from a file, the data is read from in-memory byte array.</p>
80       *
81       * <p>NOTE: Only use this method if you wish to upload in-memory data; if you instead
82       * wish to upload the contents of an actual file, use {@link #setValueAttribute(String)},
83       * passing in the path to the file.</p>
84       *
85       * @param data the in-memory data assigned to this file input element
86       */
87      public final void setData(final byte[] data) {
88          data_ = data;
89      }
90  
91      /**
92       * {@inheritDoc}
93       */
94      @Override
95      public void setDefaultChecked(final boolean defaultChecked) {
96          // Empty.
97      }
98  
99      /**
100      * {@inheritDoc}
101      */
102     @Override
103     public NameValuePair[] getSubmitNameValuePairs() {
104         if (files_ == null || files_.length == 0) {
105             return new NameValuePair[] {new KeyDataPair(getNameAttribute(), null, null, null, (Charset) null)};
106         }
107 
108         final List<NameValuePair> list = new ArrayList<>();
109         for (final File file : files_) {
110             final Charset charset = getPage().getCharset();
111 
112             final KeyDataPair keyDataPair;
113             if (data_ == null) {
114                 String contentType;
115                 if (contentType_ == null) {
116                     contentType = getPage().getWebClient().getBrowserVersion().getUploadMimeType(file);
117                     if (StringUtils.isEmptyOrNull(contentType)) {
118                         contentType = MimeType.APPLICATION_OCTET_STREAM;
119                     }
120                 }
121                 else {
122                     contentType = contentType_;
123                 }
124 
125                 keyDataPair = new KeyDataPair(getNameAttribute(), file, null, contentType, charset);
126             }
127             else {
128                 keyDataPair = new KeyDataPair(getNameAttribute(), null, file.getName(),
129                                                 MimeType.APPLICATION_OCTET_STREAM, charset);
130                 keyDataPair.setData(data_);
131             }
132             list.add(keyDataPair);
133         }
134         return list.toArray(new NameValuePair[0]);
135     }
136 
137     /**
138      * Sets the content type value that should be sent together with the uploaded file.
139      * If content type is not explicitly set, HtmlUnit will try to guess it from the file content.
140      * @param contentType the content type ({@code null} resets it)
141      */
142     public void setContentType(final String contentType) {
143         contentType_ = contentType;
144     }
145 
146     /**
147      * Gets the content type that should be sent together with the uploaded file.
148      * @return the content type, or {@code null} if this has not been explicitly set
149      *         and should be guessed from file content
150      */
151     public String getContentType() {
152         return contentType_;
153     }
154 
155     /**
156      * {@inheritDoc}
157      */
158     @Override
159     public String getValue() {
160         final File[] files = getFiles();
161         if (files == null || files.length == 0) {
162             return ATTRIBUTE_NOT_DEFINED;
163         }
164         final File first = files[0];
165         final String name = first.getName();
166         if (name.isEmpty()) {
167             return name;
168         }
169         return "C:\\fakepath\\" + name;
170     }
171 
172     /**
173      * {@inheritDoc}
174      *
175      * @see SubmittableElement#setDefaultValue(String)
176      */
177     @Override
178     public void setDefaultValue(final String defaultValue) {
179         final String oldDefaultValue = getDefaultValue();
180         // overwritten because we have the overwritten setValueAttribute()
181         super.setValueAttribute(defaultValue);
182 
183         if (oldDefaultValue.equals(getValue())) {
184             setRawValue(defaultValue);
185         }
186     }
187 
188     /**
189      * {@inheritDoc}
190      */
191     @Override
192     public void setValue(final String newValue) {
193         if (StringUtils.isEmptyOrNull(newValue)) {
194             setFiles();
195             return;
196         }
197 
198         final File file = new File(newValue);
199         if (file.isDirectory()) {
200             setDirectory(file);
201             return;
202         }
203 
204         setFiles(file);
205     }
206 
207     /**
208      * Used to specify {@code multiple} files to upload.
209      * <p>
210      * We may follow WebDriver solution, once made,
211      * see https://code.google.com/p/selenium/issues/detail?id=2239
212      * </p>
213      * @param files the list of files to upload
214      */
215     public void setFiles(final File... files) {
216         if (files.length > 1 && ATTRIBUTE_NOT_DEFINED == getAttributeDirect("multiple")) {
217             throw new IllegalStateException("HtmlFileInput - 'multiple' is not set.");
218         }
219 
220         for (int i = 0; i < files.length; i++) {
221             files[i] = normalizeFile(files[i]);
222         }
223         files_ = files;
224         fireEvent(Event.TYPE_CHANGE);
225     }
226 
227     /**
228      * Used to specify the upload directory.
229      *
230      * @param directory the directory to upload all files
231      */
232     public void setDirectory(final File directory) {
233         if (directory == null) {
234             return;
235         }
236 
237         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect("webkitdirectory")) {
238             throw new IllegalStateException("HtmlFileInput - 'webkitdirectory' is not set.");
239         }
240 
241         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect("multiple")) {
242             throw new IllegalStateException("HtmlFileInput - 'multiple' is not set.");
243         }
244 
245         if (!directory.isDirectory()) {
246             throw new IllegalStateException("HtmlFileInput - the provided directory '"
247                         + directory.getAbsolutePath() + "' is not a directory.");
248         }
249 
250         final Collection<File> fileColl = FileUtils.listFiles(directory, null, true);
251         final File[] files = new File[fileColl.size()];
252         int i = 0;
253         for (final File file : fileColl) {
254             files[i++] = normalizeFile(file);
255         }
256         files_ = files;
257         fireEvent(Event.TYPE_CHANGE);
258     }
259 
260     /**
261      * To tolerate {@code file://}.
262      */
263     private static File normalizeFile(final File file) {
264         File f = null;
265         String path = file.getPath().replace('\\', '/');
266         if (path.startsWith("file:/")) {
267             if (path.startsWith("file://") && !path.startsWith("file:///")) {
268                 path = "file:///" + path.substring(7);
269             }
270             try {
271                 f = new File(new URI(path));
272             }
273             catch (final URISyntaxException ignored) {
274                 // nothing here
275             }
276         }
277         if (f == null) {
278             f = new File(path);
279         }
280         return f;
281     }
282 
283     /**
284      * Returns the files.
285      * @return the array of {@link File}s
286      */
287     public File[] getFiles() {
288         return files_;
289     }
290 
291     /**
292      * {@inheritDoc}
293      * Per spec, the reset algorithm for a file upload control is special-cased:
294      * it simply empties the list of selected files. Unlike other input types,
295      * there is no "restore to the value attribute" step at all -- a browser can
296      * never resurrect a specific file selection from markup, for the same
297      * security reason the 'value' attribute is unsettable and fake-pathed in
298      * the first place. Deliberately does NOT call super.reset() (which would
299      * try to reconstruct a fake File from the 'value' attribute via setValue())
300      * and deliberately does NOT fire a change event, matching the same
301      * "reset fires a reset event, not a change event" principle already applied
302      * to HtmlInput/HtmlTextArea.
303      * @see SubmittableElement#reset()
304      */
305     @Override
306     public void reset() {
307         files_ = new File[0];
308     }
309 
310     /**
311      * Returns whether this element satisfies all form validation constraints set.
312      * @return whether this element satisfies all form validation constraints set
313      */
314     @Override
315     public boolean isValid() {
316         if (isDisabled()) {
317             return true;
318         }
319 
320         return isCustomValidityValid()
321                 && (!isRequiredSupported()
322                         || ATTRIBUTE_NOT_DEFINED == getAttributeDirect("required")
323                         || files_.length > 0);
324     }
325 
326     @Override
327     protected void adjustValueAfterTypeChange(final HtmlInput oldInput, final BrowserVersion browserVersion) {
328         setValue("");
329     }
330 }