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 "core/html/forms/FileInputType.h" 24 25#include "bindings/core/v8/ExceptionStatePlaceholder.h" 26#include "core/HTMLNames.h" 27#include "core/InputTypeNames.h" 28#include "core/dom/shadow/ShadowRoot.h" 29#include "core/events/Event.h" 30#include "core/fileapi/File.h" 31#include "core/fileapi/FileList.h" 32#include "core/html/FormDataList.h" 33#include "core/html/HTMLInputElement.h" 34#include "core/html/forms/FormController.h" 35#include "core/page/Chrome.h" 36#include "core/page/DragData.h" 37#include "core/rendering/RenderFileUploadControl.h" 38#include "platform/FileMetadata.h" 39#include "platform/RuntimeEnabledFeatures.h" 40#include "platform/UserGestureIndicator.h" 41#include "platform/text/PlatformLocale.h" 42#include "wtf/PassOwnPtr.h" 43#include "wtf/text/StringBuilder.h" 44#include "wtf/text/WTFString.h" 45 46namespace blink { 47 48using blink::WebLocalizedString; 49using namespace HTMLNames; 50 51inline FileInputType::FileInputType(HTMLInputElement& element) 52 : BaseClickableWithKeyInputType(element) 53 , m_fileList(FileList::create()) 54{ 55} 56 57PassRefPtrWillBeRawPtr<InputType> FileInputType::create(HTMLInputElement& element) 58{ 59 return adoptRefWillBeNoop(new FileInputType(element)); 60} 61 62void FileInputType::trace(Visitor* visitor) 63{ 64 visitor->trace(m_fileList); 65 BaseClickableWithKeyInputType::trace(visitor); 66} 67 68Vector<FileChooserFileInfo> FileInputType::filesFromFormControlState(const FormControlState& state) 69{ 70 Vector<FileChooserFileInfo> files; 71 for (size_t i = 0; i < state.valueSize(); i += 2) { 72 if (!state[i + 1].isEmpty()) 73 files.append(FileChooserFileInfo(state[i], state[i + 1])); 74 else 75 files.append(FileChooserFileInfo(state[i])); 76 } 77 return files; 78} 79 80const AtomicString& FileInputType::formControlType() const 81{ 82 return InputTypeNames::file; 83} 84 85FormControlState FileInputType::saveFormControlState() const 86{ 87 if (m_fileList->isEmpty()) 88 return FormControlState(); 89 FormControlState state; 90 unsigned numFiles = m_fileList->length(); 91 for (unsigned i = 0; i < numFiles; ++i) { 92 if (m_fileList->item(i)->hasBackingFile()) { 93 state.append(m_fileList->item(i)->path()); 94 state.append(m_fileList->item(i)->name()); 95 } 96 // FIXME: handle Blob-backed File instances, see http://crbug.com/394948 97 } 98 return state; 99} 100 101void FileInputType::restoreFormControlState(const FormControlState& state) 102{ 103 if (state.valueSize() % 2) 104 return; 105 filesChosen(filesFromFormControlState(state)); 106} 107 108bool FileInputType::appendFormData(FormDataList& encoding, bool multipart) const 109{ 110 FileList* fileList = element().files(); 111 unsigned numFiles = fileList->length(); 112 if (!multipart) { 113 // Send only the basenames. 114 // 4.10.16.4 and 4.10.16.6 sections in HTML5. 115 116 // Unlike the multipart case, we have no special handling for the empty 117 // fileList because Netscape doesn't support for non-multipart 118 // submission of file inputs, and Firefox doesn't add "name=" query 119 // parameter. 120 for (unsigned i = 0; i < numFiles; ++i) 121 encoding.appendData(element().name(), fileList->item(i)->name()); 122 return true; 123 } 124 125 // If no filename at all is entered, return successful but empty. 126 // Null would be more logical, but Netscape posts an empty file. Argh. 127 if (!numFiles) { 128 encoding.appendBlob(element().name(), File::create("")); 129 return true; 130 } 131 132 for (unsigned i = 0; i < numFiles; ++i) 133 encoding.appendBlob(element().name(), fileList->item(i)); 134 return true; 135} 136 137bool FileInputType::valueMissing(const String& value) const 138{ 139 return element().isRequired() && value.isEmpty(); 140} 141 142String FileInputType::valueMissingText() const 143{ 144 return locale().queryString(element().multiple() ? WebLocalizedString::ValidationValueMissingForMultipleFile : WebLocalizedString::ValidationValueMissingForFile); 145} 146 147void FileInputType::handleDOMActivateEvent(Event* event) 148{ 149 if (element().isDisabledFormControl()) 150 return; 151 152 if (!UserGestureIndicator::processingUserGesture()) 153 return; 154 155 if (Chrome* chrome = this->chrome()) { 156 FileChooserSettings settings; 157 HTMLInputElement& input = element(); 158 settings.allowsDirectoryUpload = input.fastHasAttribute(webkitdirectoryAttr); 159 settings.allowsMultipleFiles = settings.allowsDirectoryUpload || input.fastHasAttribute(multipleAttr); 160 settings.acceptMIMETypes = input.acceptMIMETypes(); 161 settings.acceptFileExtensions = input.acceptFileExtensions(); 162 settings.selectedFiles = m_fileList->pathsForUserVisibleFiles(); 163 settings.useMediaCapture = RuntimeEnabledFeatures::mediaCaptureEnabled() && input.fastHasAttribute(captureAttr); 164 chrome->runOpenPanel(input.document().frame(), newFileChooser(settings)); 165 } 166 event->setDefaultHandled(); 167} 168 169RenderObject* FileInputType::createRenderer(RenderStyle*) const 170{ 171 return new RenderFileUploadControl(&element()); 172} 173 174bool FileInputType::canSetStringValue() const 175{ 176 return false; 177} 178 179FileList* FileInputType::files() 180{ 181 return m_fileList.get(); 182} 183 184bool FileInputType::canSetValue(const String& value) 185{ 186 // For security reasons, we don't allow setting the filename, but we do allow clearing it. 187 // The HTML5 spec (as of the 10/24/08 working draft) says that the value attribute isn't 188 // applicable to the file upload control at all, but for now we are keeping this behavior 189 // to avoid breaking existing websites that may be relying on this. 190 return value.isEmpty(); 191} 192 193bool FileInputType::getTypeSpecificValue(String& value) 194{ 195 if (m_fileList->isEmpty()) { 196 value = String(); 197 return true; 198 } 199 200 // HTML5 tells us that we're supposed to use this goofy value for 201 // file input controls. Historically, browsers revealed the real 202 // file path, but that's a privacy problem. Code on the web 203 // decided to try to parse the value by looking for backslashes 204 // (because that's what Windows file paths use). To be compatible 205 // with that code, we make up a fake path for the file. 206 value = "C:\\fakepath\\" + m_fileList->item(0)->name(); 207 return true; 208} 209 210void FileInputType::setValue(const String&, bool valueChanged, TextFieldEventBehavior) 211{ 212 if (!valueChanged) 213 return; 214 215 m_fileList->clear(); 216 element().setNeedsStyleRecalc(SubtreeStyleChange); 217 element().setNeedsValidityCheck(); 218} 219 220PassRefPtrWillBeRawPtr<FileList> FileInputType::createFileList(const Vector<FileChooserFileInfo>& files) const 221{ 222 RefPtrWillBeRawPtr<FileList> fileList(FileList::create()); 223 size_t size = files.size(); 224 225 // If a directory is being selected, the UI allows a directory to be chosen 226 // and the paths provided here share a root directory somewhere up the tree; 227 // we want to store only the relative paths from that point. 228 if (size && element().fastHasAttribute(webkitdirectoryAttr)) { 229 // Find the common root path. 230 String rootPath = directoryName(files[0].path); 231 for (size_t i = 1; i < size; i++) { 232 while (!files[i].path.startsWith(rootPath)) 233 rootPath = directoryName(rootPath); 234 } 235 rootPath = directoryName(rootPath); 236 ASSERT(rootPath.length()); 237 int rootLength = rootPath.length(); 238 if (rootPath[rootLength - 1] != '\\' && rootPath[rootLength - 1] != '/') 239 rootLength += 1; 240 for (size_t i = 0; i < size; i++) { 241 // Normalize backslashes to slashes before exposing the relative path to script. 242 String relativePath = files[i].path.substring(rootLength).replace('\\', '/'); 243 fileList->append(File::createWithRelativePath(files[i].path, relativePath)); 244 } 245 return fileList; 246 } 247 248 for (size_t i = 0; i < size; i++) 249 fileList->append(File::createForUserProvidedFile(files[i].path, files[i].displayName)); 250 return fileList; 251} 252 253void FileInputType::createShadowSubtree() 254{ 255 ASSERT(element().shadow()); 256 RefPtrWillBeRawPtr<HTMLInputElement> button = HTMLInputElement::create(element().document(), 0, false); 257 button->setType(InputTypeNames::button); 258 button->setAttribute(valueAttr, AtomicString(locale().queryString(element().multiple() ? WebLocalizedString::FileButtonChooseMultipleFilesLabel : WebLocalizedString::FileButtonChooseFileLabel))); 259 button->setShadowPseudoId(AtomicString("-webkit-file-upload-button", AtomicString::ConstructFromLiteral)); 260 element().userAgentShadowRoot()->appendChild(button.release()); 261} 262 263void FileInputType::disabledAttributeChanged() 264{ 265 ASSERT(element().shadow()); 266 if (Element* button = toElement(element().userAgentShadowRoot()->firstChild())) 267 button->setBooleanAttribute(disabledAttr, element().isDisabledFormControl()); 268} 269 270void FileInputType::multipleAttributeChanged() 271{ 272 ASSERT(element().shadow()); 273 if (Element* button = toElement(element().userAgentShadowRoot()->firstChild())) 274 button->setAttribute(valueAttr, AtomicString(locale().queryString(element().multiple() ? WebLocalizedString::FileButtonChooseMultipleFilesLabel : WebLocalizedString::FileButtonChooseFileLabel))); 275} 276 277void FileInputType::setFiles(PassRefPtrWillBeRawPtr<FileList> files) 278{ 279 if (!files) 280 return; 281 282 RefPtrWillBeRawPtr<HTMLInputElement> input(element()); 283 284 bool pathsChanged = false; 285 if (files->length() != m_fileList->length()) { 286 pathsChanged = true; 287 } else { 288 for (unsigned i = 0; i < files->length(); ++i) { 289 if (files->item(i)->path() != m_fileList->item(i)->path()) { 290 pathsChanged = true; 291 break; 292 } 293 } 294 } 295 296 m_fileList = files; 297 298 input->notifyFormStateChanged(); 299 input->setNeedsValidityCheck(); 300 301 if (input->renderer()) 302 input->renderer()->setShouldDoFullPaintInvalidation(true); 303 304 if (pathsChanged) { 305 // This call may cause destruction of this instance. 306 // input instance is safe since it is ref-counted. 307 input->dispatchChangeEvent(); 308 } 309 input->setChangedSinceLastFormControlChangeEvent(false); 310} 311 312void FileInputType::filesChosen(const Vector<FileChooserFileInfo>& files) 313{ 314 setFiles(createFileList(files)); 315} 316 317void FileInputType::receiveDropForDirectoryUpload(const Vector<String>& paths) 318{ 319 if (Chrome* chrome = this->chrome()) { 320 FileChooserSettings settings; 321 HTMLInputElement& input = element(); 322 settings.allowsDirectoryUpload = true; 323 settings.allowsMultipleFiles = true; 324 settings.selectedFiles.append(paths[0]); 325 settings.acceptMIMETypes = input.acceptMIMETypes(); 326 settings.acceptFileExtensions = input.acceptFileExtensions(); 327 chrome->enumerateChosenDirectory(newFileChooser(settings)); 328 } 329} 330 331bool FileInputType::receiveDroppedFiles(const DragData* dragData) 332{ 333 Vector<String> paths; 334 dragData->asFilenames(paths); 335 if (paths.isEmpty()) 336 return false; 337 338 HTMLInputElement& input = element(); 339 if (input.fastHasAttribute(webkitdirectoryAttr)) { 340 receiveDropForDirectoryUpload(paths); 341 return true; 342 } 343 344 m_droppedFileSystemId = dragData->droppedFileSystemId(); 345 346 Vector<FileChooserFileInfo> files; 347 for (unsigned i = 0; i < paths.size(); ++i) 348 files.append(FileChooserFileInfo(paths[i])); 349 350 if (input.fastHasAttribute(multipleAttr)) { 351 filesChosen(files); 352 } else { 353 Vector<FileChooserFileInfo> firstFileOnly; 354 firstFileOnly.append(files[0]); 355 filesChosen(firstFileOnly); 356 } 357 return true; 358} 359 360String FileInputType::droppedFileSystemId() 361{ 362 return m_droppedFileSystemId; 363} 364 365String FileInputType::defaultToolTip() const 366{ 367 FileList* fileList = m_fileList.get(); 368 unsigned listSize = fileList->length(); 369 if (!listSize) { 370 return locale().queryString(WebLocalizedString::FileButtonNoFileSelectedLabel); 371 } 372 373 StringBuilder names; 374 for (size_t i = 0; i < listSize; ++i) { 375 names.append(fileList->item(i)->name()); 376 if (i != listSize - 1) 377 names.append('\n'); 378 } 379 return names.toString(); 380} 381 382} // namespace blink 383