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