Home | History | Annotate | Download | only in forms
      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 
     46 namespace blink {
     47 
     48 using blink::WebLocalizedString;
     49 using namespace HTMLNames;
     50 
     51 inline FileInputType::FileInputType(HTMLInputElement& element)
     52     : BaseClickableWithKeyInputType(element)
     53     , m_fileList(FileList::create())
     54 {
     55 }
     56 
     57 PassRefPtrWillBeRawPtr<InputType> FileInputType::create(HTMLInputElement& element)
     58 {
     59     return adoptRefWillBeNoop(new FileInputType(element));
     60 }
     61 
     62 void FileInputType::trace(Visitor* visitor)
     63 {
     64     visitor->trace(m_fileList);
     65     BaseClickableWithKeyInputType::trace(visitor);
     66 }
     67 
     68 Vector<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 
     80 const AtomicString& FileInputType::formControlType() const
     81 {
     82     return InputTypeNames::file;
     83 }
     84 
     85 FormControlState 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 
    101 void FileInputType::restoreFormControlState(const FormControlState& state)
    102 {
    103     if (state.valueSize() % 2)
    104         return;
    105     filesChosen(filesFromFormControlState(state));
    106 }
    107 
    108 bool 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 
    137 bool FileInputType::valueMissing(const String& value) const
    138 {
    139     return element().isRequired() && value.isEmpty();
    140 }
    141 
    142 String FileInputType::valueMissingText() const
    143 {
    144     return locale().queryString(element().multiple() ? WebLocalizedString::ValidationValueMissingForMultipleFile : WebLocalizedString::ValidationValueMissingForFile);
    145 }
    146 
    147 void 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 
    169 RenderObject* FileInputType::createRenderer(RenderStyle*) const
    170 {
    171     return new RenderFileUploadControl(&element());
    172 }
    173 
    174 bool FileInputType::canSetStringValue() const
    175 {
    176     return false;
    177 }
    178 
    179 FileList* FileInputType::files()
    180 {
    181     return m_fileList.get();
    182 }
    183 
    184 bool 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 
    193 bool 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 
    210 void 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 
    220 PassRefPtrWillBeRawPtr<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 
    253 void 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 
    263 void FileInputType::disabledAttributeChanged()
    264 {
    265     ASSERT(element().shadow());
    266     if (Element* button = toElement(element().userAgentShadowRoot()->firstChild()))
    267         button->setBooleanAttribute(disabledAttr, element().isDisabledFormControl());
    268 }
    269 
    270 void 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 
    277 void 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 
    312 void FileInputType::filesChosen(const Vector<FileChooserFileInfo>& files)
    313 {
    314     setFiles(createFileList(files));
    315 }
    316 
    317 void 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 
    331 bool 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 
    360 String FileInputType::droppedFileSystemId()
    361 {
    362     return m_droppedFileSystemId;
    363 }
    364 
    365 String 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