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
50namespace WebCore {
51class UploadButtonElement;
52}
53
54SPECIALIZE_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)); }
57SPECIALIZE_TYPE_TRAITS_END()
58
59namespace WebCore {
60
61using namespace HTMLNames;
62
63class UploadButtonElement final : public HTMLInputElement {
64 WTF_MAKE_ISO_ALLOCATED_INLINE(UploadButtonElement);
65public:
66 static Ref<UploadButtonElement> create(Document&);
67 static Ref<UploadButtonElement> createForMultiple(Document&);
68
69private:
70 bool isUploadButton() const override { return true; }
71
72 UploadButtonElement(Document&);
73};
74
75Ref<UploadButtonElement> UploadButtonElement::create(Document& document)
76{
77 auto button = adoptRef(*new UploadButtonElement(document));
78 button->setValue(fileButtonChooseFileLabel());
79 return button;
80}
81
82Ref<UploadButtonElement> UploadButtonElement::createForMultiple(Document& document)
83{
84 auto button = adoptRef(*new UploadButtonElement(document));
85 button->setValue(fileButtonChooseMultipleFilesLabel());
86 return button;
87}
88
89UploadButtonElement::UploadButtonElement(Document& document)
90 : HTMLInputElement(inputTag, document, 0, false)
91{
92 setType(AtomicString("button", AtomicString::ConstructFromLiteral));
93 setPseudo(AtomicString("-webkit-file-upload-button", AtomicString::ConstructFromLiteral));
94}
95
96FileInputType::FileInputType(HTMLInputElement& element)
97 : BaseClickableWithKeyInputType(element)
98 , m_fileList(FileList::create())
99{
100}
101
102FileInputType::~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
114Vector<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
128const AtomicString& FileInputType::formControlType() const
129{
130 return InputTypeNames::file();
131}
132
133FormControlState 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
149void FileInputType::restoreFormControlState(const FormControlState& state)
150{
151 filesChosen(filesFromFormControlState(state));
152}
153
154bool 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(emptyString()));
179 return true;
180 }
181
182
183 for (auto& file : fileList->files())
184 formData.append(name, file.get());
185 return true;
186}
187
188bool FileInputType::valueMissing(const String& value) const
189{
190 ASSERT(element());
191 return element()->isRequired() && value.isEmpty();
192}
193
194String FileInputType::valueMissingText() const
195{
196 ASSERT(element());
197 return element()->multiple() ? validationMessageValueMissingForMultipleFileText() : validationMessageValueMissingForFileText();
198}
199
200void 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
228RenderPtr<RenderElement> FileInputType::createInputRenderer(RenderStyle&& style)
229{
230 ASSERT(element());
231 return createRenderer<RenderFileUploadControl>(*element(), WTFMove(style));
232}
233
234bool FileInputType::canSetStringValue() const
235{
236 return false;
237}
238
239FileList* FileInputType::files()
240{
241 return m_fileList.ptr();
242}
243
244bool 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
253bool 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
270void 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
279bool FileInputType::isFileUpload() const
280{
281 return true;
282}
283
284void 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
291void 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
304void 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
318void 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 = std::make_unique<FileIconLoader>(client);
336
337 chrome->loadIconForFiles(paths, *m_fileIconLoader);
338}
339
340void FileInputType::applyFileChooserSettings(const FileChooserSettings& settings)
341{
342 if (m_fileChooser)
343 m_fileChooser->invalidate();
344
345 m_fileChooser = FileChooser::create(this, settings);
346}
347
348bool FileInputType::allowsDirectories() const
349{
350 if (!RuntimeEnabledFeatures::sharedFeatures().directoryUploadEnabled())
351 return false;
352 ASSERT(element());
353 return element()->hasAttributeWithoutSynchronization(webkitdirectoryAttr);
354}
355
356void FileInputType::setFiles(RefPtr<FileList>&& files)
357{
358 setFiles(WTFMove(files), RequestIcon::Yes);
359}
360
361void 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
407void 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(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
426String FileInputType::displayString() const
427{
428 return m_displayString;
429}
430
431void 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)
443bool 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
464Icon* FileInputType::icon() const
465{
466 return m_icon.get();
467}
468
469String 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
490