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