View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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.apache.commons.lang3.StringUtils;
28  import org.htmlunit.BrowserVersion;
29  import org.htmlunit.SgmlPage;
30  import org.htmlunit.javascript.host.event.Event;
31  import org.htmlunit.util.KeyDataPair;
32  import org.htmlunit.util.MimeType;
33  import org.htmlunit.util.NameValuePair;
34  
35  /**
36   * Wrapper for the HTML element "input".
37   *
38   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
39   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
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 be 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.isEmpty(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.isEmpty(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      * @param files the list of files to upload
204      */
205     public void setFiles(final File... files) {
206         if (files.length > 1 && ATTRIBUTE_NOT_DEFINED == getAttributeDirect("multiple")) {
207             throw new IllegalStateException("HtmlFileInput - 'multiple' is not set.");
208         }
209 
210         for (int i = 0; i < files.length; i++) {
211             files[i] = normalizeFile(files[i]);
212         }
213         files_ = files;
214         fireEvent(Event.TYPE_CHANGE);
215     }
216 
217     /**
218      * Used to specify the upload directory.
219      *
220      * @param directory the directory to upload all files
221      */
222     public void setDirectory(final File directory) {
223         if (directory == null) {
224             return;
225         }
226 
227         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect("webkitdirectory")) {
228             throw new IllegalStateException("HtmlFileInput - 'webkitdirectory' is not set.");
229         }
230 
231         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect("multiple")) {
232             throw new IllegalStateException("HtmlFileInput - 'multiple' is not set.");
233         }
234 
235         if (!directory.isDirectory()) {
236             throw new IllegalStateException("HtmlFileInput - the provided directory '"
237                         + directory.getAbsolutePath() + "' is not a directory.");
238         }
239 
240         final Collection<File> fileColl = FileUtils.listFiles(directory, null, true);
241         final File[] files = new File[fileColl.size()];
242         int i = 0;
243         for (final File file : fileColl) {
244             files[i++] = normalizeFile(file);
245         }
246         files_ = files;
247         fireEvent(Event.TYPE_CHANGE);
248     }
249 
250     /**
251      * To tolerate {@code file://}
252      */
253     private static File normalizeFile(final File file) {
254         File f = null;
255         String path = file.getPath().replace('\\', '/');
256         if (path.startsWith("file:/")) {
257             if (path.startsWith("file://") && !path.startsWith("file:///")) {
258                 path = "file:///" + path.substring(7);
259             }
260             try {
261                 f = new File(new URI(path));
262             }
263             catch (final URISyntaxException ignored) {
264                 // nothing here
265             }
266         }
267         if (f == null) {
268             f = new File(path);
269         }
270         return f;
271     }
272 
273     /**
274      * Returns the files.
275      * @return the array of {@link File}s
276      */
277     public File[] getFiles() {
278         return files_;
279     }
280 
281     /**
282      * Returns whether this element satisfies all form validation constraints set.
283      * @return whether this element satisfies all form validation constraints set
284      */
285     @Override
286     public boolean isValid() {
287         return isCustomValidityValid()
288                 && (!isRequiredSupported()
289                         || ATTRIBUTE_NOT_DEFINED == getAttributeDirect("required")
290                         || files_.length > 0);
291     }
292 
293     @Override
294     protected void adjustValueAfterTypeChange(final HtmlInput oldInput, final BrowserVersion browserVersion) {
295         setValue("");
296     }
297 }