DOM event handling should pass Event around by reference.
[WebKit-https.git] / Source / WebCore / html / FileInputType.cpp
1 /*
2  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 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 "DragData.h"
27 #include "Event.h"
28 #include "File.h"
29 #include "FileList.h"
30 #include "FileSystem.h"
31 #include "FormController.h"
32 #include "FormDataList.h"
33 #include "Frame.h"
34 #include "HTMLInputElement.h"
35 #include "HTMLNames.h"
36 #include "Icon.h"
37 #include "InputTypeNames.h"
38 #include "LocalizedStrings.h"
39 #include "RenderFileUploadControl.h"
40 #include "ScriptController.h"
41 #include "ShadowRoot.h"
42 #include <wtf/text/StringBuilder.h>
43
44 namespace WebCore {
45
46 using namespace HTMLNames;
47
48 class UploadButtonElement final : public HTMLInputElement {
49 public:
50     static Ref<UploadButtonElement> create(Document&);
51     static Ref<UploadButtonElement> createForMultiple(Document&);
52
53 private:
54     UploadButtonElement(Document&);
55 };
56
57 Ref<UploadButtonElement> UploadButtonElement::create(Document& document)
58 {
59     Ref<UploadButtonElement> button = adoptRef(*new UploadButtonElement(document));
60     button->setValue(fileButtonChooseFileLabel());
61     return button;
62 }
63
64 Ref<UploadButtonElement> UploadButtonElement::createForMultiple(Document& document)
65 {
66     Ref<UploadButtonElement> button = adoptRef(*new UploadButtonElement(document));
67     button->setValue(fileButtonChooseMultipleFilesLabel());
68     return button;
69 }
70
71 UploadButtonElement::UploadButtonElement(Document& document)
72     : HTMLInputElement(inputTag, document, 0, false)
73 {
74     setType(AtomicString("button", AtomicString::ConstructFromLiteral));
75     setPseudo(AtomicString("-webkit-file-upload-button", AtomicString::ConstructFromLiteral));
76 }
77
78 FileInputType::FileInputType(HTMLInputElement& element)
79     : BaseClickableWithKeyInputType(element)
80     , m_fileList(FileList::create())
81 {
82 }
83
84 FileInputType::~FileInputType()
85 {
86     if (m_fileChooser)
87         m_fileChooser->invalidate();
88
89 #if !PLATFORM(IOS)
90     // FIXME: Is this correct? Why don't we do this on iOS?
91     if (m_fileIconLoader)
92         m_fileIconLoader->invalidate();
93 #endif
94 }
95
96 Vector<FileChooserFileInfo> FileInputType::filesFromFormControlState(const FormControlState& state)
97 {
98     Vector<FileChooserFileInfo> files;
99     for (size_t i = 0; i < state.valueSize(); i += 2) {
100         if (!state[i + 1].isEmpty())
101             files.append(FileChooserFileInfo(state[i], state[i + 1]));
102         else
103             files.append(FileChooserFileInfo(state[i]));
104     }
105     return files;
106 }
107
108 const AtomicString& FileInputType::formControlType() const
109 {
110     return InputTypeNames::file();
111 }
112
113 FormControlState FileInputType::saveFormControlState() const
114 {
115     if (m_fileList->isEmpty())
116         return FormControlState();
117     FormControlState state;
118     unsigned numFiles = m_fileList->length();
119     for (unsigned i = 0; i < numFiles; ++i) {
120         state.append(m_fileList->item(i)->path());
121         state.append(m_fileList->item(i)->name());
122     }
123     return state;
124 }
125
126 void FileInputType::restoreFormControlState(const FormControlState& state)
127 {
128     if (state.valueSize() % 2)
129         return;
130     filesChosen(filesFromFormControlState(state));
131 }
132
133 bool FileInputType::appendFormData(FormDataList& encoding, bool multipart) const
134 {
135     FileList* fileList = element().files();
136     unsigned numFiles = fileList->length();
137     if (!multipart) {
138         // Send only the basenames.
139         // 4.10.16.4 and 4.10.16.6 sections in HTML5.
140
141         // Unlike the multipart case, we have no special handling for the empty
142         // fileList because Netscape doesn't support for non-multipart
143         // submission of file inputs, and Firefox doesn't add "name=" query
144         // parameter.
145         for (unsigned i = 0; i < numFiles; ++i)
146             encoding.appendData(element().name(), fileList->item(i)->name());
147         return true;
148     }
149
150     // If no filename at all is entered, return successful but empty.
151     // Null would be more logical, but Netscape posts an empty file. Argh.
152     if (!numFiles) {
153         encoding.appendBlob(element().name(), File::create(emptyString()));
154         return true;
155     }
156
157     for (unsigned i = 0; i < numFiles; ++i)
158         encoding.appendBlob(element().name(), *fileList->item(i));
159     return true;
160 }
161
162 bool FileInputType::valueMissing(const String& value) const
163 {
164     return element().isRequired() && value.isEmpty();
165 }
166
167 String FileInputType::valueMissingText() const
168 {
169     return element().multiple() ? validationMessageValueMissingForMultipleFileText() : validationMessageValueMissingForFileText();
170 }
171
172 void FileInputType::handleDOMActivateEvent(Event& event)
173 {
174     if (element().isDisabledFormControl())
175         return;
176
177     if (!ScriptController::processingUserGesture())
178         return;
179
180     if (Chrome* chrome = this->chrome()) {
181         FileChooserSettings settings;
182         HTMLInputElement& input = element();
183         settings.allowsMultipleFiles = input.hasAttributeWithoutSynchronization(multipleAttr);
184         settings.acceptMIMETypes = input.acceptMIMETypes();
185         settings.acceptFileExtensions = input.acceptFileExtensions();
186         settings.selectedFiles = m_fileList->paths();
187 #if ENABLE(MEDIA_CAPTURE)
188         settings.mediaCaptureType = input.mediaCaptureType();
189 #endif
190
191         applyFileChooserSettings(settings);
192         chrome->runOpenPanel(input.document().frame(), m_fileChooser);
193     }
194
195     event.setDefaultHandled();
196 }
197
198 RenderPtr<RenderElement> FileInputType::createInputRenderer(RenderStyle&& style)
199 {
200     return createRenderer<RenderFileUploadControl>(element(), WTFMove(style));
201 }
202
203 bool FileInputType::canSetStringValue() const
204 {
205     return false;
206 }
207
208 bool FileInputType::canChangeFromAnotherType() const
209 {
210     // Don't allow the type to be changed to file after the first type change.
211     // In other engines this might mean a JavaScript programmer could set a text
212     // field's value to something like /etc/passwd and then change it to a file input.
213     // I don't think this would actually occur in WebKit, but this rule still may be
214     // important for compatibility.
215     return false;
216 }
217
218 FileList* FileInputType::files()
219 {
220     return m_fileList.get();
221 }
222
223 bool FileInputType::canSetValue(const String& value)
224 {
225     // For security reasons, we don't allow setting the filename, but we do allow clearing it.
226     // The HTML5 spec (as of the 10/24/08 working draft) says that the value attribute isn't
227     // applicable to the file upload control at all, but for now we are keeping this behavior
228     // to avoid breaking existing websites that may be relying on this.
229     return value.isEmpty();
230 }
231
232 bool FileInputType::getTypeSpecificValue(String& value)
233 {
234     if (m_fileList->isEmpty()) {
235         value = String();
236         return true;
237     }
238
239     // HTML5 tells us that we're supposed to use this goofy value for
240     // file input controls. Historically, browsers revealed the real
241     // file path, but that's a privacy problem. Code on the web
242     // decided to try to parse the value by looking for backslashes
243     // (because that's what Windows file paths use). To be compatible
244     // with that code, we make up a fake path for the file.
245     value = "C:\\fakepath\\" + m_fileList->item(0)->name();
246     return true;
247 }
248
249 void FileInputType::setValue(const String&, bool, TextFieldEventBehavior)
250 {
251     // FIXME: Should we clear the file list, or replace it with a new empty one here? This is observable from JavaScript through custom properties.
252     m_fileList->clear();
253     m_icon = nullptr;
254     element().setNeedsStyleRecalc();
255 }
256
257 PassRefPtr<FileList> FileInputType::createFileList(const Vector<FileChooserFileInfo>& files) const
258 {
259     Vector<RefPtr<File>> fileObjects;
260     for (const FileChooserFileInfo& info : files)
261         fileObjects.append(File::createWithName(info.path, info.displayName));
262
263     return FileList::create(WTFMove(fileObjects));
264 }
265
266 bool FileInputType::isFileUpload() const
267 {
268     return true;
269 }
270
271 void FileInputType::createShadowSubtree()
272 {
273     ASSERT(element().shadowRoot());
274     element().userAgentShadowRoot()->appendChild(element().multiple() ? UploadButtonElement::createForMultiple(element().document()): UploadButtonElement::create(element().document()), IGNORE_EXCEPTION);
275 }
276
277 void FileInputType::disabledAttributeChanged()
278 {
279     ASSERT(element().shadowRoot());
280     UploadButtonElement* button = static_cast<UploadButtonElement*>(element().userAgentShadowRoot()->firstChild());
281     if (button)
282         button->setBooleanAttribute(disabledAttr, element().isDisabledFormControl());
283 }
284
285 void FileInputType::multipleAttributeChanged()
286 {
287     ASSERT(element().shadowRoot());
288     UploadButtonElement* button = static_cast<UploadButtonElement*>(element().userAgentShadowRoot()->firstChild());
289     if (button)
290         button->setValue(element().multiple() ? fileButtonChooseMultipleFilesLabel() : fileButtonChooseFileLabel());
291 }
292
293 void FileInputType::requestIcon(const Vector<String>& paths)
294 {
295 #if PLATFORM(IOS)
296     UNUSED_PARAM(paths);
297 #else
298     if (!paths.size()) {
299         updateRendering(nullptr);
300         return;
301     }
302
303     Chrome* chrome = this->chrome();
304     if (!chrome)
305         return;
306
307     if (m_fileIconLoader)
308         m_fileIconLoader->invalidate();
309
310     m_fileIconLoader = std::make_unique<FileIconLoader>(static_cast<FileIconLoaderClient&>(*this));
311
312     chrome->loadIconForFiles(paths, m_fileIconLoader.get());
313 #endif
314 }
315
316 void FileInputType::applyFileChooserSettings(const FileChooserSettings& settings)
317 {
318     if (m_fileChooser)
319         m_fileChooser->invalidate();
320
321     m_fileChooser = FileChooser::create(this, settings);
322 }
323
324 void FileInputType::setFiles(PassRefPtr<FileList> files)
325 {
326     if (!files)
327         return;
328
329     Ref<HTMLInputElement> input(element());
330
331     bool pathsChanged = false;
332     if (files->length() != m_fileList->length())
333         pathsChanged = true;
334     else {
335         for (unsigned i = 0; i < files->length(); ++i) {
336             if (files->item(i)->path() != m_fileList->item(i)->path()) {
337                 pathsChanged = true;
338                 break;
339             }
340         }
341     }
342
343     m_fileList = files;
344
345     input->setFormControlValueMatchesRenderer(true);
346     input->updateValidity();
347
348     Vector<String> paths;
349     for (unsigned i = 0; i < m_fileList->length(); ++i)
350         paths.append(m_fileList->item(i)->path());
351     requestIcon(paths);
352
353     if (input->renderer())
354         input->renderer()->repaint();
355
356     if (pathsChanged) {
357         // This call may cause destruction of this instance.
358         // input instance is safe since it is ref-counted.
359         input->dispatchChangeEvent();
360     }
361     input->setChangedSinceLastFormControlChangeEvent(false);
362 }
363
364 #if PLATFORM(IOS)
365 void FileInputType::filesChosen(const Vector<FileChooserFileInfo>& paths, const String& displayString, Icon* icon)
366 {
367     m_displayString = displayString;
368     filesChosen(paths);
369     updateRendering(icon);
370 }
371
372 String FileInputType::displayString() const
373 {
374     return m_displayString;
375 }
376 #endif
377
378 void FileInputType::filesChosen(const Vector<FileChooserFileInfo>& files)
379 {
380     setFiles(createFileList(files));
381 }
382
383 void FileInputType::updateRendering(PassRefPtr<Icon> icon)
384 {
385     if (m_icon == icon)
386         return;
387
388     m_icon = icon;
389     if (element().renderer())
390         element().renderer()->repaint();
391 }
392
393 #if ENABLE(DRAG_SUPPORT)
394 bool FileInputType::receiveDroppedFiles(const DragData& dragData)
395 {
396     Vector<String> paths;
397     dragData.asFilenames(paths);
398     if (paths.isEmpty())
399         return false;
400
401     HTMLInputElement* input = &element();
402
403     Vector<FileChooserFileInfo> files;
404     for (auto& path : paths)
405         files.append(FileChooserFileInfo(path));
406
407     if (input->hasAttributeWithoutSynchronization(multipleAttr))
408         filesChosen(files);
409     else {
410         Vector<FileChooserFileInfo> firstFileOnly;
411         firstFileOnly.append(files[0]);
412         filesChosen(firstFileOnly);
413     }
414     return true;
415 }
416 #endif // ENABLE(DRAG_SUPPORT)
417
418 Icon* FileInputType::icon() const
419 {
420     return m_icon.get();
421 }
422
423 String FileInputType::defaultToolTip() const
424 {
425     FileList* fileList = m_fileList.get();
426     unsigned listSize = fileList->length();
427     if (!listSize) {
428         if (element().multiple())
429             return fileButtonNoFilesSelectedLabel();
430         return fileButtonNoFileSelectedLabel();
431     }
432
433     StringBuilder names;
434     for (size_t i = 0; i < listSize; ++i) {
435         names.append(fileList->item(i)->name());
436         if (i != listSize - 1)
437             names.append('\n');
438     }
439     return names.toString();
440 }
441
442
443 } // namespace WebCore