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             String contentType;
111             if (contentType_ == null) {
112                 contentType = getPage().getWebClient().getBrowserVersion().getUploadMimeType(file);
113                 if (StringUtils.isEmptyOrNull(contentType)) {
114                     contentType = MimeType.APPLICATION_OCTET_STREAM;
115                 }
116             }
117             else {
118                 contentType = contentType_;
119             }
120             final Charset charset = getPage().getCharset();
121             final KeyDataPair keyDataPair = new KeyDataPair(getNameAttribute(), file, null, contentType, charset);
122             keyDataPair.setData(data_);
123             list.add(keyDataPair);
124         }
125         return list.toArray(new NameValuePair[0]);
126     }
127 
128     /**
129      * Sets the content type value that should be sent together with the uploaded file.
130      * If content type is not explicitly set, HtmlUnit will try to guess it from the file content.
131      * @param contentType the content type ({@code null} resets it)
132      */
133     public void setContentType(final String contentType) {
134         contentType_ = contentType;
135     }
136 
137     /**
138      * Gets the content type that should be sent together with the uploaded file.
139      * @return the content type, or {@code null} if this has not been explicitly set
140      *         and should be guessed from file content
141      */
142     public String getContentType() {
143         return contentType_;
144     }
145 
146     /**
147      * {@inheritDoc}
148      */
149     @Override
150     public String getValue() {
151         final File[] files = getFiles();
152         if (files == null || files.length == 0) {
153             return ATTRIBUTE_NOT_DEFINED;
154         }
155         final File first = files[0];
156         final String name = first.getName();
157         if (name.isEmpty()) {
158             return name;
159         }
160         return "C:\\fakepath\\" + name;
161     }
162 
163     /**
164      * {@inheritDoc}
165      *
166      * @see SubmittableElement#setDefaultValue(String)
167      */
168     @Override
169     public void setDefaultValue(final String defaultValue) {
170         final String oldDefaultValue = getDefaultValue();
171         // overwritten because we have the overwritten setValueAttribute()
172         super.setValueAttribute(defaultValue);
173 
174         if (oldDefaultValue.equals(getValue())) {
175             setRawValue(defaultValue);
176         }
177     }
178 
179     /**
180      * {@inheritDoc}
181      */
182     @Override
183     public void setValue(final String newValue) {
184         if (StringUtils.isEmptyOrNull(newValue)) {
185             setFiles();
186             return;
187         }
188 
189         final File file = new File(newValue);
190         if (file.isDirectory()) {
191             setDirectory(file);
192             return;
193         }
194 
195         setFiles(file);
196     }
197 
198     /**
199      * Used to specify {@code multiple} files to upload.
200      * <p>
201      * We may follow WebDriver solution, once made,
202      * see https://code.google.com/p/selenium/issues/detail?id=2239
203      * </p>
204      * @param files the list of files to upload
205      */
206     public void setFiles(final File... files) {
207         if (files.length > 1 && ATTRIBUTE_NOT_DEFINED == getAttributeDirect("multiple")) {
208             throw new IllegalStateException("HtmlFileInput - 'multiple' is not set.");
209         }
210 
211         for (int i = 0; i < files.length; i++) {
212             files[i] = normalizeFile(files[i]);
213         }
214         files_ = files;
215         fireEvent(Event.TYPE_CHANGE);
216     }
217 
218     /**
219      * Used to specify the upload directory.
220      *
221      * @param directory the directory to upload all files
222      */
223     public void setDirectory(final File directory) {
224         if (directory == null) {
225             return;
226         }
227 
228         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect("webkitdirectory")) {
229             throw new IllegalStateException("HtmlFileInput - 'webkitdirectory' is not set.");
230         }
231 
232         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect("multiple")) {
233             throw new IllegalStateException("HtmlFileInput - 'multiple' is not set.");
234         }
235 
236         if (!directory.isDirectory()) {
237             throw new IllegalStateException("HtmlFileInput - the provided directory '"
238                         + directory.getAbsolutePath() + "' is not a directory.");
239         }
240 
241         final Collection<File> fileColl = FileUtils.listFiles(directory, null, true);
242         final File[] files = new File[fileColl.size()];
243         int i = 0;
244         for (final File file : fileColl) {
245             files[i++] = normalizeFile(file);
246         }
247         files_ = files;
248         fireEvent(Event.TYPE_CHANGE);
249     }
250 
251     /**
252      * To tolerate {@code file://}.
253      */
254     private static File normalizeFile(final File file) {
255         File f = null;
256         String path = file.getPath().replace('\\', '/');
257         if (path.startsWith("file:/")) {
258             if (path.startsWith("file://") && !path.startsWith("file:///")) {
259                 path = "file:///" + path.substring(7);
260             }
261             try {
262                 f = new File(new URI(path));
263             }
264             catch (final URISyntaxException ignored) {
265                 // nothing here
266             }
267         }
268         if (f == null) {
269             f = new File(path);
270         }
271         return f;
272     }
273 
274     /**
275      * Returns the files.
276      * @return the array of {@link File}s
277      */
278     public File[] getFiles() {
279         return files_;
280     }
281 
282     /**
283      * Returns whether this element satisfies all form validation constraints set.
284      * @return whether this element satisfies all form validation constraints set
285      */
286     @Override
287     public boolean isValid() {
288         return isCustomValidityValid()
289                 && (!isRequiredSupported()
290                         || ATTRIBUTE_NOT_DEFINED == getAttributeDirect("required")
291                         || files_.length > 0);
292     }
293 
294     @Override
295     protected void adjustValueAfterTypeChange(final HtmlInput oldInput, final BrowserVersion browserVersion) {
296         setValue("");
297     }
298 }