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