eae34e4157e95d315cf0cc51916e13cbb3194786
[WebKit-https.git] / Source / WebCore / html / FileInputType.cpp
1 /*
2  * Copyright (C) 2004-2017 Apple Inc. All rights reserved.
3  * Copyright (C) 2010 Google Inc. All rights reserved.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public License
16  * along with this library; see the file COPYING.LIB.  If not, write to
17  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19  *
20  */
21
22 #include "config.h"
23 #include "FileInputType.h"
24
25 #include "Chrome.h"
26 #include "DOMFormData.h"
27 #include "DragData.h"
28 #include "ElementChildIterator.h"
29 #include "Event.h"
30 #include "File.h"
31 #include "FileList.h"
32 #include "FileListCreator.h"
33 #include "FileSystem.h"
34 #include "FormController.h"
35 #include "Frame.h"
36 #include "HTMLInputElement.h"
37 #include "HTMLNames.h"
38 #include "Icon.h"
39 #include "InputTypeNames.h"
40 #include "LocalizedStrings.h"
41 #include "RenderFileUploadControl.h"
42 #include "RuntimeEnabledFeatures.h"
43 #include "ScriptController.h"
44 #include "Settings.h"
45 #include "ShadowRoot.h"
46 #include <wtf/TypeCasts.h>
47 #include <wtf/text/StringBuilder.h>
48
49
50 namespace WebCore {
51 class UploadButtonElement;
52 }
53
54 SPECIALIZE_TYPE_TRAITS_BEGIN(WebCore::UploadButtonElement)
55     static bool isType(const WebCore::Element& element) { return element.isUploadButton(); }
56     static bool isType(const WebCore::Node& node) { return is<WebCore::Element>(node) && isType(downcast<WebCore::Element>(node)); }
57 SPECIALIZE_TYPE_TRAITS_END()
58
59 namespace WebCore {
60
61 using namespace HTMLNames;
62
63 class UploadButtonElement final : public HTMLInputElement {
64 public:
65     static Ref<UploadButtonElement> create(Document&);
66     static Ref<UploadButtonElement> createForMultiple(Document&);
67
68 private:
69     bool isUploadButton() const override { return true; }
70     
71     UploadButtonElement(Document&);
72 };
73
74 Ref<UploadButtonElement> UploadButtonElement::create(Document& document)
75 {
76     auto button = adoptRef(*new UploadButtonElement(document));
77     button->setValue(fileButtonChooseFileLabel());
78     return button;
79 }
80
81 Ref<UploadButtonElement> UploadButtonElement::createForMultiple(Document& document)
82 {
83     auto button = adoptRef(*new UploadButtonElement(document));
84     button->setValue(fileButtonChooseMultipleFilesLabel());
85     return button;
86 }
87
88 UploadButtonElement::UploadButtonElement(Document& document)
89     : HTMLInputElement(inputTag, document, 0, false)
90 {
91     setType(AtomicString("button", AtomicString::ConstructFromLiteral));
92     setPseudo(AtomicString("-webkit-file-upload-button", AtomicString::ConstructFromLiteral));
93 }
94
95 FileInputType::FileInputType(HTMLInputElement& element)
96     : BaseClickableWithKeyInputType(element)
97     , m_fileList(FileList::create())
98 {
99 }
100
101 FileInputType::~FileInputType()
102 {
103     if (m_fileListCreator)
104         m_fileListCreator->cancel();
105
106     if (m_fileChooser)
107         m_fileChooser->invalidate();
108
109     if (m_fileIconLoader)
110         m_fileIconLoader->invalidate();
111 }
112
113 Vector<FileChooserFileInfo> FileInputType::filesFromFormControlState(const FormControlState& state)
114 {
115     Vector<FileChooserFileInfo> files;
116     for (size_t i = 0; i < state.valueSize(); i += 2) {
117         if (!state[i + 1].isEmpty())
118             files.append({ state[i], state[i + 1] });
119         else
120             files.append({ state[i] });
121     }
122     return files;
123 }
124
125 const AtomicString& FileInputType::formControlType() const
126 {
127     return InputTypeNames::file();
128 }
129
130 FormControlState FileInputType::saveFormControlState() const
131 {
132     if (m_fileList->isEmpty())
133         return { };
134
135     auto length = Checked<size_t>(m_fileList->files().size()) * Checked<size_t>(2);
136
137     Vector<String> stateVector;
138     stateVector.reserveInitialCapacity(length.unsafeGet());
139     for (auto& file : m_fileList->files()) {
140         stateVector.uncheckedAppend(file->path());
141         stateVector.uncheckedAppend(file->name());
142     }
143     return FormControlState { WTFMove(stateVector) };
144 }
145
146 void FileInputType::restoreFormControlState(const FormControlState& state)
147 {
148     if (state.valueSize() % 2)
149         return;
150     filesChosen(filesFromFormControlState(state));
151 }
152
153 bool FileInputType::appendFormData(DOMFormData& formData, bool multipart) const
154 {
155     auto* fileList = element().files();
156     ASSERT(fileList);
157
158     auto name = element().name();
159
160     if (!multipart) {
161         // Send only the basenames.
162         // 4.10.16.4 and 4.10.16.6 sections in HTML5.
163
164         // Unlike the multipart case, we have no special handling for the empty
165         // fileList because Netscape doesn't support for non-multipart
166         // submission of file inputs, and Firefox doesn't add "name=" query
167         // parameter.
168         for (auto& file : fileList->files())
169             formData.append(name, file->name());
170         return true;
171     }
172
173     // If no filename at all is entered, return successful but empty.
174     // Null would be more logical, but Netscape posts an empty file. Argh.
175     if (fileList->isEmpty()) {
176         formData.append(name, File::create(emptyString()));
177         return true;
178     }
179
180
181     for (auto& file : fileList->files())
182         formData.append(name, file.get());
183     return true;
184 }
185
186 bool FileInputType::valueMissing(const String& value) const
187 {
188     return element().isRequired() && value.isEmpty();
189 }
190
191 String FileInputType::valueMissingText() const
192 {
193     return element().multiple() ? validationMessageValueMissingForMultipleFileText() : validationMessageValueMissingForFileText();
194 }
195
196 void FileInputType::handleDOMActivateEvent(Event& event)
197 {
198     auto& input = element();
199
200     if (input.isDisabledFormControl())
201         return;
202
203     if (!ScriptController::processingUserGesture())
204         return;
205
206     if (auto* chrome = this->chrome()) {
207         FileChooserSettings settings;
208         settings.allowsDirectories = allowsDirectories();
209         settings.allowsMultipleFiles = input.hasAttributeWithoutSynchronization(multipleAttr);
210         settings.acceptMIMETypes = input.acceptMIMETypes();
211         settings.acceptFileExtensions = input.acceptFileExtensions();
212         settings.selectedFiles = m_fileList->paths();
213 #if ENABLE(MEDIA_CAPTURE)
214         settings.mediaCaptureType = input.mediaCaptureType();
215 #endif
216         applyFileChooserSettings(settings);
217         chrome->runOpenPanel(*input.document().frame(), *m_fileChooser);
218     }
219
220     event.setDefaultHandled();
221 }
222
223 RenderPtr<RenderElement> FileInputType::createInputRenderer(RenderStyle&& style)
224 {
225     return createRenderer<RenderFileUploadControl>(element(), WTFMove(style));
226 }
227
228 bool FileInputType::canSetStringValue() const
229 {
230     return false;
231 }
232
233 FileList* FileInputType::files()
234 {
235     return m_fileList.ptr();
236 }
237
238 bool FileInputType::canSetValue(const String& value)
239 {
240     // For security reasons, we don't allow setting the filename, but we do allow clearing it.
241     // The HTML5 spec (as of the 10/24/08 working draft) says that the value attribute isn't
242     // applicable to the file upload control at all, but for now we are keeping this behavior
243     // to avoid breaking existing websites that may be relying on this.
244     return value.isEmpty();
245 }
246
247 bool FileInputType::getTypeSpecificValue(String& value)
248 {
249     if (m_fileList->isEmpty()) {
250         value = { };
251         return true;
252     }
253
254     // HTML5 tells us that we're supposed to use this goofy value for
255     // file input controls. Historically, browsers revealed the real
256     // file path, but that's a privacy problem. Code on the web
257     // decided to try to parse the value by looking for backslashes
258     // (because that's what Windows file paths use). To be compatible
259     // with that code, we make up a fake path for the file.
260     value = makeString("C:\\fakepath\\", m_fileList->file(0).name());
261     return true;
262 }
263
264 void FileInputType::setValue(const String&, bool, TextFieldEventBehavior)
265 {
266     // FIXME: Should we clear the file list, or replace it with a new empty one here? This is observable from JavaScript through custom properties.
267     m_fileList->clear();
268     m_icon = nullptr;
269     element().invalidateStyleForSubtree();
270 }
271
272 bool FileInputType::isFileUpload() const
273 {
274     return true;
275 }
276
277 void FileInputType::createShadowSubtree()
278 {
279     ASSERT(element().shadowRoot());
280     element().userAgentShadowRoot()->appendChild(element().multiple() ? UploadButtonElement::createForMultiple(element().document()): UploadButtonElement::create(element().document()));
281 }
282
283 void FileInputType::disabledAttributeChanged()
284 {
285     ASSERT(element().shadowRoot());
286
287     auto root = element().userAgentShadowRoot();
288     if (!root)
289         return;
290     
291     if (auto* button = childrenOfType<UploadButtonElement>(*root).first())
292         button->setBooleanAttribute(disabledAttr, element().isDisabledFormControl());
293 }
294
295 void FileInputType::multipleAttributeChanged()
296 {
297     ASSERT(element().shadowRoot());
298
299     auto root = element().userAgentShadowRoot();
300     if (!root)
301         return;
302
303     if (auto* button = childrenOfType<UploadButtonElement>(*root).first())
304         button->setValue(element().multiple() ? fileButtonChooseMultipleFilesLabel() : fileButtonChooseFileLabel());
305 }
306
307 void FileInputType::requestIcon(const Vector<String>& paths)
308 {
309     if (!paths.size()) {
310         iconLoaded(nullptr);
311         return;
312     }
313
314     auto* chrome = this->chrome();
315     if (!chrome) {
316         iconLoaded(nullptr);
317         return;
318     }
319
320     if (m_fileIconLoader)
321         m_fileIconLoader->invalidate();
322
323     FileIconLoaderClient& client = *this;
324     m_fileIconLoader = std::make_unique<FileIconLoader>(client);
325
326     chrome->loadIconForFiles(paths, *m_fileIconLoader);
327 }
328
329 void FileInputType::applyFileChooserSettings(const FileChooserSettings& settings)
330 {
331     if (m_fileChooser)
332         m_fileChooser->invalidate();
333
334     m_fileChooser = FileChooser::create(this, settings);
335 }
336
337 bool FileInputType::allowsDirectories() const
338 {
339     if (!RuntimeEnabledFeatures::sharedFeatures().directoryUploadEnabled())
340         return false;
341     return element().hasAttributeWithoutSynchronization(webkitdirectoryAttr);
342 }
343
344 void FileInputType::setFiles(RefPtr<FileList>&& files)
345 {
346     setFiles(WTFMove(files), RequestIcon::Yes);
347 }
348
349 void FileInputType::setFiles(RefPtr<FileList>&& files, RequestIcon shouldRequestIcon)
350 {
351     if (!files)
352         return;
353
354     Ref<HTMLInputElement> input(element());
355
356     unsigned length = files->length();
357
358     bool pathsChanged = false;
359     if (length != m_fileList->length())
360         pathsChanged = true;
361     else {
362         for (unsigned i = 0; i < length; ++i) {
363             if (files->file(i).path() != m_fileList->file(i).path()) {
364                 pathsChanged = true;
365                 break;
366             }
367         }
368     }
369
370     m_fileList = files.releaseNonNull();
371
372     input->setFormControlValueMatchesRenderer(true);
373     input->updateValidity();
374
375     if (shouldRequestIcon == RequestIcon::Yes) {
376         Vector<String> paths;
377         paths.reserveInitialCapacity(length);
378         for (auto& file : m_fileList->files())
379             paths.uncheckedAppend(file->path());
380         requestIcon(paths);
381     }
382
383     if (input->renderer())
384         input->renderer()->repaint();
385
386     if (pathsChanged) {
387         // This call may cause destruction of this instance.
388         // input instance is safe since it is ref-counted.
389         input->dispatchChangeEvent();
390     }
391     input->setChangedSinceLastFormControlChangeEvent(false);
392 }
393
394 void FileInputType::filesChosen(const Vector<FileChooserFileInfo>& paths, const String& displayString, Icon* icon)
395 {
396     if (!displayString.isEmpty())
397         m_displayString = displayString;
398
399     if (m_fileListCreator)
400         m_fileListCreator->cancel();
401
402     auto shouldResolveDirectories = allowsDirectories() ? FileListCreator::ShouldResolveDirectories::Yes : FileListCreator::ShouldResolveDirectories::No;
403     auto shouldRequestIcon = icon ? RequestIcon::Yes : RequestIcon::No;
404     m_fileListCreator = FileListCreator::create(paths, shouldResolveDirectories, [this, shouldRequestIcon](Ref<FileList>&& fileList) {
405         setFiles(WTFMove(fileList), shouldRequestIcon);
406         m_fileListCreator = nullptr;
407     });
408
409     if (icon)
410         iconLoaded(icon);
411 }
412
413 String FileInputType::displayString() const
414 {
415     return m_displayString;
416 }
417
418 void FileInputType::iconLoaded(RefPtr<Icon>&& icon)
419 {
420     if (m_icon == icon)
421         return;
422
423     m_icon = WTFMove(icon);
424     if (element().renderer())
425         element().renderer()->repaint();
426 }
427
428 #if ENABLE(DRAG_SUPPORT)
429 bool FileInputType::receiveDroppedFiles(const DragData& dragData)
430 {
431     auto paths = dragData.asFilenames();
432     if (paths.isEmpty())
433         return false;
434
435     if (element().hasAttributeWithoutSynchronization(multipleAttr)) {
436         Vector<FileChooserFileInfo> files;
437         files.reserveInitialCapacity(paths.size());
438         for (auto& path : paths)
439             files.uncheckedAppend({ path });
440
441         filesChosen(files);
442     } else
443         filesChosen({ FileChooserFileInfo { paths[0] } });
444
445     return true;
446 }
447 #endif // ENABLE(DRAG_SUPPORT)
448
449 Icon* FileInputType::icon() const
450 {
451     return m_icon.get();
452 }
453
454 String FileInputType::defaultToolTip() const
455 {
456     unsigned listSize = m_fileList->length();
457     if (!listSize) {
458         if (element().multiple())
459             return fileButtonNoFilesSelectedLabel();
460         return fileButtonNoFileSelectedLabel();
461     }
462
463     StringBuilder names;
464     for (unsigned i = 0; i < listSize; ++i) {
465         names.append(m_fileList->file(i).name());
466         if (i != listSize - 1)
467             names.append('\n');
468     }
469     return names.toString();
470 }
471
472
473 } // namespace WebCore