blob: 40b012a9adfbd4f5e265880509e36642669a683c [file] [log] [blame]
/*
* Copyright (C) 2004-2020 Apple Inc. All rights reserved.
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "config.h"
#include "FileInputType.h"
#include "Chrome.h"
#include "DOMFormData.h"
#include "DirectoryFileListCreator.h"
#include "DragData.h"
#include "ElementChildIterator.h"
#include "Event.h"
#include "File.h"
#include "FileList.h"
#include "FormController.h"
#include "Frame.h"
#include "HTMLInputElement.h"
#include "HTMLNames.h"
#include "Icon.h"
#include "InputTypeNames.h"
#include "LocalizedStrings.h"
#include "MIMETypeRegistry.h"
#include "RenderFileUploadControl.h"
#include "RuntimeEnabledFeatures.h"
#include "Settings.h"
#include "ShadowPseudoIds.h"
#include "ShadowRoot.h"
#include "UserGestureIndicator.h"
#include <wtf/FileSystem.h>
#include <wtf/IsoMallocInlines.h>
#include <wtf/TypeCasts.h>
#include <wtf/text/StringBuilder.h>
#if PLATFORM(MAC)
#include "ImageUtilities.h"
#include "UTIUtilities.h"
#endif
namespace WebCore {
class UploadButtonElement;
}
SPECIALIZE_TYPE_TRAITS_BEGIN(WebCore::UploadButtonElement)
static bool isType(const WebCore::Element& element) { return element.isUploadButton(); }
static bool isType(const WebCore::Node& node) { return is<WebCore::Element>(node) && isType(downcast<WebCore::Element>(node)); }
SPECIALIZE_TYPE_TRAITS_END()
namespace WebCore {
using namespace HTMLNames;
// FIXME: This can likely be an HTMLDivElement.
class UploadButtonElement final : public HTMLInputElement {
WTF_MAKE_ISO_ALLOCATED_INLINE(UploadButtonElement);
public:
static Ref<UploadButtonElement> create(Document&);
static Ref<UploadButtonElement> createForMultiple(Document&);
private:
static Ref<UploadButtonElement> createInternal(Document&, const String& value);
bool isUploadButton() const override { return true; }
UploadButtonElement(Document&);
};
Ref<UploadButtonElement> UploadButtonElement::create(Document& document)
{
return createInternal(document, fileButtonChooseFileLabel());
}
Ref<UploadButtonElement> UploadButtonElement::createForMultiple(Document& document)
{
return createInternal(document, fileButtonChooseMultipleFilesLabel());
}
Ref<UploadButtonElement> UploadButtonElement::createInternal(Document& document, const String& value)
{
auto button = adoptRef(*new UploadButtonElement(document));
static MainThreadNeverDestroyed<const AtomString> buttonName("button", AtomString::ConstructFromLiteral);
button->setType(buttonName);
button->setPseudo(ShadowPseudoIds::fileSelectorButton());
button->setValue(value);
return button;
}
UploadButtonElement::UploadButtonElement(Document& document)
: HTMLInputElement(inputTag, document, 0, false)
{
}
FileInputType::FileInputType(HTMLInputElement& element)
: BaseClickableWithKeyInputType(Type::File, element)
, m_fileList(FileList::create())
{
ASSERT(needsShadowSubtree());
}
FileInputType::~FileInputType()
{
if (m_fileChooser)
m_fileChooser->invalidate();
if (m_fileIconLoader)
m_fileIconLoader->invalidate();
}
Vector<FileChooserFileInfo> FileInputType::filesFromFormControlState(const FormControlState& state)
{
Vector<FileChooserFileInfo> files;
size_t size = state.size();
files.reserveInitialCapacity(size / 2);
for (size_t i = 0; i < size; i += 2)
files.uncheckedAppend({ state[i], { }, state[i + 1] });
return files;
}
const AtomString& FileInputType::formControlType() const
{
return InputTypeNames::file();
}
FormControlState FileInputType::saveFormControlState() const
{
if (m_fileList->isEmpty())
return { };
auto length = Checked<size_t>(m_fileList->files().size()) * Checked<size_t>(2);
Vector<String> stateVector;
stateVector.reserveInitialCapacity(length);
for (auto& file : m_fileList->files()) {
stateVector.uncheckedAppend(file->path());
stateVector.uncheckedAppend(file->name());
}
return FormControlState { WTFMove(stateVector) };
}
void FileInputType::restoreFormControlState(const FormControlState& state)
{
filesChosen(filesFromFormControlState(state));
}
bool FileInputType::appendFormData(DOMFormData& formData, bool multipart) const
{
ASSERT(element());
RefPtr fileList = element()->files();
ASSERT(fileList);
auto name = element()->name();
if (!multipart) {
// Send only the basenames.
// 4.10.16.4 and 4.10.16.6 sections in HTML5.
// Unlike the multipart case, we have no special handling for the empty
// fileList because Netscape doesn't support for non-multipart
// submission of file inputs, and Firefox doesn't add "name=" query
// parameter.
for (auto& file : fileList->files())
formData.append(name, file->name());
return true;
}
// If no filename at all is entered, return successful but empty.
// Null would be more logical, but Netscape posts an empty file. Argh.
if (fileList->isEmpty()) {
auto* document = element() ? &element()->document() : nullptr;
formData.append(name, File::create(document, emptyString()));
return true;
}
for (auto& file : fileList->files())
formData.append(name, file.get());
return true;
}
bool FileInputType::valueMissing(const String& value) const
{
ASSERT(element());
return element()->isRequired() && value.isEmpty();
}
String FileInputType::valueMissingText() const
{
ASSERT(element());
return element()->multiple() ? validationMessageValueMissingForMultipleFileText() : validationMessageValueMissingForFileText();
}
void FileInputType::handleDOMActivateEvent(Event& event)
{
ASSERT(element());
auto& input = *element();
if (input.isDisabledFormControl())
return;
if (!UserGestureIndicator::processingUserGesture())
return;
if (auto* chrome = this->chrome()) {
applyFileChooserSettings();
chrome->runOpenPanel(*input.document().frame(), *m_fileChooser);
}
event.setDefaultHandled();
}
RenderPtr<RenderElement> FileInputType::createInputRenderer(RenderStyle&& style)
{
ASSERT(element());
return createRenderer<RenderFileUploadControl>(*element(), WTFMove(style));
}
bool FileInputType::canSetStringValue() const
{
return false;
}
FileList* FileInputType::files()
{
return m_fileList.ptr();
}
bool FileInputType::canSetValue(const String& value)
{
// For security reasons, we don't allow setting the filename, but we do allow clearing it.
// The HTML5 spec (as of the 10/24/08 working draft) says that the value attribute isn't
// applicable to the file upload control at all, but for now we are keeping this behavior
// to avoid breaking existing websites that may be relying on this.
return value.isEmpty();
}
bool FileInputType::getTypeSpecificValue(String& value)
{
if (m_fileList->isEmpty()) {
value = { };
return true;
}
// HTML5 tells us that we're supposed to use this goofy value for
// file input controls. Historically, browsers revealed the real
// file path, but that's a privacy problem. Code on the web
// decided to try to parse the value by looking for backslashes
// (because that's what Windows file paths use). To be compatible
// with that code, we make up a fake path for the file.
value = makeString("C:\\fakepath\\", m_fileList->file(0).name());
return true;
}
void FileInputType::setValue(const String&, bool, TextFieldEventBehavior)
{
// FIXME: Should we clear the file list, or replace it with a new empty one here? This is observable from JavaScript through custom properties.
m_fileList->clear();
m_icon = nullptr;
ASSERT(element());
element()->invalidateStyleForSubtree();
}
void FileInputType::createShadowSubtreeAndUpdateInnerTextElementEditability(ContainerNode::ChildChange::Source source, bool)
{
ASSERT(needsShadowSubtree());
ASSERT(element());
ASSERT(element()->shadowRoot());
element()->userAgentShadowRoot()->appendChild(source, element()->multiple() ? UploadButtonElement::createForMultiple(element()->document()): UploadButtonElement::create(element()->document()));
}
void FileInputType::disabledStateChanged()
{
ASSERT(element());
ASSERT(element()->shadowRoot());
auto root = element()->userAgentShadowRoot();
if (!root)
return;
if (RefPtr button = childrenOfType<UploadButtonElement>(*root).first())
button->setBooleanAttribute(disabledAttr, element()->isDisabledFormControl());
}
void FileInputType::attributeChanged(const QualifiedName& name)
{
if (name == multipleAttr) {
if (auto* element = this->element()) {
ASSERT(element->shadowRoot());
if (auto root = element->userAgentShadowRoot()) {
if (RefPtr button = childrenOfType<UploadButtonElement>(*root).first())
button->setValue(element->multiple() ? fileButtonChooseMultipleFilesLabel() : fileButtonChooseFileLabel());
}
}
}
BaseClickableWithKeyInputType::attributeChanged(name);
}
void FileInputType::requestIcon(const Vector<String>& paths)
{
if (!paths.size()) {
iconLoaded(nullptr);
return;
}
auto* chrome = this->chrome();
if (!chrome) {
iconLoaded(nullptr);
return;
}
if (m_fileIconLoader)
m_fileIconLoader->invalidate();
FileIconLoaderClient& client = *this;
m_fileIconLoader = makeUnique<FileIconLoader>(client);
chrome->loadIconForFiles(paths, *m_fileIconLoader);
}
FileChooserSettings FileInputType::fileChooserSettings() const
{
ASSERT(element());
auto& input = *element();
FileChooserSettings settings;
settings.allowsDirectories = allowsDirectories();
settings.allowsMultipleFiles = input.hasAttributeWithoutSynchronization(multipleAttr);
settings.acceptMIMETypes = input.acceptMIMETypes();
settings.acceptFileExtensions = input.acceptFileExtensions();
settings.selectedFiles = m_fileList->paths();
#if ENABLE(MEDIA_CAPTURE)
settings.mediaCaptureType = input.mediaCaptureType();
#endif
return settings;
}
void FileInputType::applyFileChooserSettings()
{
if (m_fileChooser)
m_fileChooser->invalidate();
m_fileChooser = FileChooser::create(this, fileChooserSettings());
}
bool FileInputType::allowsDirectories() const
{
if (!RuntimeEnabledFeatures::sharedFeatures().directoryUploadEnabled())
return false;
ASSERT(element());
return element()->hasAttributeWithoutSynchronization(webkitdirectoryAttr);
}
void FileInputType::setFiles(RefPtr<FileList>&& files, WasSetByJavaScript wasSetByJavaScript)
{
setFiles(WTFMove(files), RequestIcon::Yes, wasSetByJavaScript);
}
void FileInputType::setFiles(RefPtr<FileList>&& files, RequestIcon shouldRequestIcon, WasSetByJavaScript wasSetByJavaScript)
{
if (!files)
return;
ASSERT(element());
Ref<HTMLInputElement> protectedInputElement(*element());
unsigned length = files->length();
bool pathsChanged = false;
if (length != m_fileList->length())
pathsChanged = true;
else {
for (unsigned i = 0; i < length; ++i) {
if (files->file(i).path() != m_fileList->file(i).path()) {
pathsChanged = true;
break;
}
}
}
m_fileList = files.releaseNonNull();
protectedInputElement->setFormControlValueMatchesRenderer(true);
protectedInputElement->updateValidity();
if (shouldRequestIcon == RequestIcon::Yes) {
Vector<String> paths;
paths.reserveInitialCapacity(length);
for (auto& file : m_fileList->files())
paths.uncheckedAppend(file->path());
requestIcon(paths);
}
if (protectedInputElement->renderer())
protectedInputElement->renderer()->repaint();
if (wasSetByJavaScript == WasSetByJavaScript::Yes)
return;
if (pathsChanged) {
// This call may cause destruction of this instance.
// input instance is safe since it is ref-counted.
protectedInputElement->dispatchInputEvent();
protectedInputElement->dispatchChangeEvent();
}
protectedInputElement->setChangedSinceLastFormControlChangeEvent(false);
}
void FileInputType::filesChosen(const Vector<FileChooserFileInfo>& paths, const String& displayString, Icon* icon)
{
if (!displayString.isEmpty())
m_displayString = displayString;
if (m_directoryFileListCreator)
m_directoryFileListCreator->cancel();
auto* document = element() ? &element()->document() : nullptr;
if (!allowsDirectories()) {
auto files = paths.map([document](auto& fileInfo) {
return File::create(document, fileInfo.path, fileInfo.replacementPath, fileInfo.displayName);
});
didCreateFileList(FileList::create(WTFMove(files)), icon);
return;
}
m_directoryFileListCreator = DirectoryFileListCreator::create([this, weakThis = WeakPtr { *this }, icon = RefPtr { icon }](Ref<FileList>&& fileList) mutable {
ASSERT(isMainThread());
if (!weakThis)
return;
didCreateFileList(WTFMove(fileList), WTFMove(icon));
});
m_directoryFileListCreator->start(document, paths);
}
void FileInputType::filesChosen(const Vector<String>& paths, const Vector<String>& replacementPaths)
{
ASSERT(element());
ASSERT(!paths.isEmpty());
size_t size = element()->hasAttributeWithoutSynchronization(multipleAttr) ? paths.size() : 1;
Vector<FileChooserFileInfo> files;
files.reserveInitialCapacity(size);
for (size_t i = 0; i < size; ++i)
files.uncheckedAppend({ paths[i], i < replacementPaths.size() ? replacementPaths[i] : nullString(), { } });
filesChosen(files);
}
void FileInputType::didCreateFileList(Ref<FileList>&& fileList, RefPtr<Icon>&& icon)
{
Ref protectedThis { *this };
ASSERT(!allowsDirectories() || m_directoryFileListCreator);
m_directoryFileListCreator = nullptr;
setFiles(WTFMove(fileList), icon ? RequestIcon::Yes : RequestIcon::No, WasSetByJavaScript::No);
if (icon && !m_fileList->isEmpty() && element())
iconLoaded(WTFMove(icon));
}
String FileInputType::displayString() const
{
return m_displayString;
}
void FileInputType::iconLoaded(RefPtr<Icon>&& icon)
{
if (m_icon == icon)
return;
m_icon = WTFMove(icon);
ASSERT(element());
if (auto* renderer = element()->renderer())
renderer->repaint();
}
#if ENABLE(DRAG_SUPPORT)
bool FileInputType::receiveDroppedFilesWithImageTranscoding(const Vector<String>& paths)
{
#if PLATFORM(MAC)
auto settings = fileChooserSettings();
auto allowedMIMETypes = MIMETypeRegistry::allowedMIMETypes(settings.acceptMIMETypes, settings.acceptFileExtensions);
auto transcodingPaths = findImagesForTranscoding(paths, allowedMIMETypes);
if (transcodingPaths.isEmpty())
return { };
auto transcodingMIMEType = MIMETypeRegistry::preferredImageMIMETypeForEncoding(allowedMIMETypes, { });
if (transcodingMIMEType.isNull())
return { };
auto transcodingUTI = WebCore::UTIFromMIMEType(transcodingMIMEType);
auto transcodingExtension = WebCore::MIMETypeRegistry::preferredExtensionForMIMEType(transcodingMIMEType);
auto callFilesChosen = [protectedThis = Ref { *this }, paths](const Vector<String>& replacementPaths) {
protectedThis->filesChosen(paths, replacementPaths);
};
sharedImageTranscodingQueue().dispatch([callFilesChosen = WTFMove(callFilesChosen), transcodingPaths = transcodingPaths.isolatedCopy(), transcodingUTI = transcodingUTI.isolatedCopy(), transcodingExtension = transcodingExtension.isolatedCopy()]() mutable {
ASSERT(!RunLoop::isMain());
auto replacementPaths = transcodeImages(transcodingPaths, transcodingUTI, transcodingExtension);
ASSERT(transcodingPaths.size() == replacementPaths.size());
RunLoop::main().dispatch([callFilesChosen = WTFMove(callFilesChosen), replacementPaths = replacementPaths.isolatedCopy()]() {
callFilesChosen(replacementPaths);
});
});
return true;
#else
UNUSED_PARAM(paths);
return false;
#endif
}
bool FileInputType::receiveDroppedFiles(const DragData& dragData)
{
auto paths = dragData.asFilenames();
if (paths.isEmpty())
return false;
if (receiveDroppedFilesWithImageTranscoding(paths))
return true;
filesChosen(paths);
return true;
}
#endif // ENABLE(DRAG_SUPPORT)
Icon* FileInputType::icon() const
{
return m_icon.get();
}
String FileInputType::defaultToolTip() const
{
unsigned listSize = m_fileList->length();
if (!listSize) {
ASSERT(element());
if (element()->multiple())
return fileButtonNoFilesSelectedLabel();
return fileButtonNoFileSelectedLabel();
}
StringBuilder names;
for (unsigned i = 0; i < listSize; ++i) {
names.append(m_fileList->file(i).name());
if (i != listSize - 1)
names.append('\n');
}
return names.toString();
}
} // namespace WebCore