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