| 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 | |