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(AtomicString("button" , AtomicString::ConstructFromLiteral)); |
93 | setPseudo(AtomicString("-webkit-file-upload-button" , AtomicString::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 AtomicString& 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(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 = std::make_unique<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(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 |
490 | |