blob: 48361f4cbb953030b92af4634bf9218e050bc529 [file] [log] [blame]
/*
* Copyright (C) 1999 Lars Knoll (knoll@kde.org)
* (C) 1999 Antti Koivisto (koivisto@kde.org)
* (C) 2001 Dirk Mueller (mueller@kde.org)
* Copyright (C) 2004, 2005, 2006, 2007 Apple Inc. All rights reserved.
* (C) 2006 Alexey Proskuryakov (ap@nypop.com)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "config.h"
#include "HTMLFormElement.h"
#include "Base64.h"
#include "CSSHelper.h"
#include "CString.h"
#include "Event.h"
#include "EventNames.h"
#include "FormData.h"
#include "FormDataList.h"
#include "Frame.h"
#include "FrameLoader.h"
#include "HTMLDocument.h"
#include "HTMLFormCollection.h"
#include "HTMLImageElement.h"
#include "HTMLInputElement.h"
#include "HTMLNames.h"
#include "MIMETypeRegistry.h"
#include "RenderTextControl.h"
#if PLATFORM(QT)
#include <QtCore/QFileInfo>
#endif
#if PLATFORM(WX)
#include <wx/defs.h>
#include <wx/filename.h>
#endif
#if PLATFORM(WIN_OS)
#include <shlwapi.h>
#endif
namespace WebCore {
using namespace EventNames;
using namespace HTMLNames;
HTMLFormElement::HTMLFormElement(Document* doc)
: HTMLElement(formTag, doc)
, m_elementAliases(0)
, collectionInfo(0)
, m_enctype("application/x-www-form-urlencoded")
, m_post(false)
, m_multipart(false)
, m_autocomplete(true)
, m_insubmit(false)
, m_doingsubmit(false)
, m_inreset(false)
, m_malformed(false)
{
}
HTMLFormElement::~HTMLFormElement()
{
if (!m_autocomplete)
document()->unregisterForCacheCallbacks(this);
delete m_elementAliases;
delete collectionInfo;
for (unsigned i = 0; i < formElements.size(); ++i)
formElements[i]->formDestroyed();
for (unsigned i = 0; i < imgElements.size(); ++i)
imgElements[i]->m_form = 0;
}
bool HTMLFormElement::formWouldHaveSecureSubmission(const String &url)
{
if (url.isNull()) {
return false;
}
return document()->completeURL(url.deprecatedString()).startsWith("https:", false);
}
void HTMLFormElement::attach()
{
HTMLElement::attach();
}
void HTMLFormElement::insertedIntoDocument()
{
if (document()->isHTMLDocument()) {
HTMLDocument *doc = static_cast<HTMLDocument *>(document());
doc->addNamedItem(oldNameAttr);
}
HTMLElement::insertedIntoDocument();
}
void HTMLFormElement::removedFromDocument()
{
if (document()->isHTMLDocument()) {
HTMLDocument *doc = static_cast<HTMLDocument *>(document());
doc->removeNamedItem(oldNameAttr);
}
HTMLElement::removedFromDocument();
}
void HTMLFormElement::handleLocalEvents(Event* event, bool useCapture)
{
EventTargetNode* targetNode = event->target()->toNode();
if (!useCapture && targetNode && targetNode != this && (event->type() == submitEvent || event->type() == resetEvent)) {
event->stopPropagation();
return;
}
HTMLElement::handleLocalEvents(event, useCapture);
}
unsigned HTMLFormElement::length() const
{
int len = 0;
for (unsigned i = 0; i < formElements.size(); ++i)
if (formElements[i]->isEnumeratable())
++len;
return len;
}
Node* HTMLFormElement::item(unsigned index)
{
return elements()->item(index);
}
void HTMLFormElement::submitClick(Event* event)
{
bool submitFound = false;
for (unsigned i = 0; i < formElements.size(); ++i) {
if (formElements[i]->hasLocalName(inputTag)) {
HTMLInputElement *element = static_cast<HTMLInputElement *>(formElements[i]);
if (element->isSuccessfulSubmitButton() && element->renderer()) {
submitFound = true;
element->dispatchSimulatedClick(event);
break;
}
}
}
if (!submitFound) // submit the form without a submit or image input
prepareSubmit(event);
}
static DeprecatedCString encodeCString(const CString& cstr)
{
DeprecatedCString e = cstr.deprecatedCString();
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
// same safe characters as Netscape for compatibility
static const char *safe = "-._*";
int elen = e.length();
DeprecatedCString encoded((elen + e.contains('\n')) * 3 + 1);
int enclen = 0;
for (int pos = 0; pos < elen; pos++) {
unsigned char c = e[pos];
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || strchr(safe, c))
encoded[enclen++] = c;
else if (c == ' ')
encoded[enclen++] = '+';
else if (c == '\n' || (c == '\r' && e[pos + 1] != '\n')) {
encoded[enclen++] = '%';
encoded[enclen++] = '0';
encoded[enclen++] = 'D';
encoded[enclen++] = '%';
encoded[enclen++] = '0';
encoded[enclen++] = 'A';
} else if (c != '\r') {
encoded[enclen++] = '%';
unsigned int h = c / 16;
h += (h > 9) ? ('A' - 10) : '0';
encoded[enclen++] = h;
unsigned int l = c % 16;
l += (l > 9) ? ('A' - 10) : '0';
encoded[enclen++] = l;
}
}
encoded[enclen++] = '\0';
encoded.truncate(enclen);
return encoded;
}
static int randomNumber()
{
static bool randomSeeded = false;
#if PLATFORM(DARWIN)
if (!randomSeeded) {
srandomdev();
randomSeeded = true;
}
return random();
#else
if (!randomSeeded) {
srand(static_cast<unsigned>(time(0)));
randomSeeded = true;
}
return rand();
#endif
}
// Warning: this helper doesn't currently have a reliable cross-platform behavior in certain edge cases
// (see basename(3) specification for examples).
// Consider this if it ever needs to become a general purpose method.
static String pathGetFilename(String path)
{
#if PLATFORM(QT)
return QFileInfo(path).fileName();
#elif PLATFORM(WX)
return wxFileName(path).GetFullName();
#elif PLATFORM(WIN_OS)
return String(PathFindFileName(path.charactersWithNullTermination()));
#else
return path.substring(path.reverseFind('/') + 1);
#endif
}
TextEncoding HTMLFormElement::dataEncoding() const
{
if (isMailtoForm())
return UTF8Encoding();
TextEncoding encoding;
String str = m_acceptcharset;
str.replace(',', ' ');
Vector<String> charsets = str.split(' ');
Vector<String>::const_iterator end = charsets.end();
for (Vector<String>::const_iterator it = charsets.begin(); it != end; ++it)
if ((encoding = TextEncoding(*it)).isValid())
return encoding;
if (Frame* frame = document()->frame())
return frame->loader()->encoding();
return Latin1Encoding();
}
PassRefPtr<FormData> HTMLFormElement::formData(const char* boundary) const
{
DeprecatedCString enc_string = "";
TextEncoding encoding = dataEncoding();
RefPtr<FormData> result = new FormData;
for (unsigned i = 0; i < formElements.size(); ++i) {
HTMLGenericFormElement* current = formElements[i];
FormDataList lst(encoding);
if (!current->disabled() && current->appendFormData(lst, m_multipart)) {
size_t ln = lst.list().size();
for (size_t j = 0; j < ln; ++j) {
const FormDataListItem& item = lst.list()[j];
if (!m_multipart) {
// handle ISINDEX / <input name=isindex> special
// but only if its the first entry
if (enc_string.isEmpty() && item.m_data == "isindex") {
enc_string += encodeCString(lst.list()[j + 1].m_data);
++j;
} else {
if (!enc_string.isEmpty())
enc_string += '&';
enc_string += encodeCString(item.m_data);
enc_string += "=";
enc_string += encodeCString(lst.list()[j + 1].m_data);
++j;
}
}
else
{
DeprecatedCString hstr("--");
hstr += boundary;
hstr += "\r\n";
hstr += "Content-Disposition: form-data; name=\"";
hstr += item.m_data.data();
hstr += "\"";
// if the current type is FILE, then we also need to
// include the filename
if (current->hasLocalName(inputTag) &&
static_cast<HTMLInputElement*>(current)->inputType() == HTMLInputElement::FILE) {
String path = static_cast<HTMLInputElement*>(current)->value();
String filename = pathGetFilename(path);
// FIXME: This won't work if the filename includes a " mark,
// or control characters like CR or LF. This also does strange
// things if the filename includes characters you can't encode
// in the website's character set.
hstr += "; filename=\"";
hstr += encoding.encode(reinterpret_cast<const UChar*>(filename.characters()), filename.length(), true).data();
hstr += "\"";
if (!static_cast<HTMLInputElement*>(current)->value().isEmpty()) {
DeprecatedString mimeType = MIMETypeRegistry::getMIMETypeForPath(path).deprecatedString();
if (!mimeType.isEmpty()) {
hstr += "\r\nContent-Type: ";
hstr += mimeType.ascii();
}
}
}
hstr += "\r\n\r\n";
// append body
result->appendData(hstr.data(), hstr.length());
const FormDataListItem& item = lst.list()[j + 1];
if (size_t dataSize = item.m_data.length())
result->appendData(item.m_data.data(), dataSize);
else if (!item.m_path.isEmpty())
result->appendFile(item.m_path);
result->appendData("\r\n", 2);
++j;
}
}
}
}
if (m_multipart) {
enc_string = "--";
enc_string += boundary;
enc_string += "--\r\n";
}
result->appendData(enc_string.data(), enc_string.length());
return result;
}
void HTMLFormElement::parseEnctype(const String& type)
{
if(type.contains("multipart", false) || type.contains("form-data", false)) {
m_enctype = "multipart/form-data";
m_multipart = true;
} else if (type.contains("text", false) || type.contains("plain", false)) {
m_enctype = "text/plain";
m_multipart = false;
} else {
m_enctype = "application/x-www-form-urlencoded";
m_multipart = false;
}
}
bool HTMLFormElement::isMailtoForm() const
{
return m_url.startsWith("mailto:", false);
}
bool HTMLFormElement::prepareSubmit(Event* event)
{
Frame* frame = document()->frame();
if (m_insubmit || !frame)
return m_insubmit;
m_insubmit = true;
m_doingsubmit = false;
if (dispatchHTMLEvent(submitEvent, true, true) && !m_doingsubmit)
m_doingsubmit = true;
m_insubmit = false;
if (m_doingsubmit)
submit(event, true);
return m_doingsubmit;
}
void HTMLFormElement::submit()
{
submit(0, false);
}
// Returns a 0-terminated C string in the vector.
static void getUniqueBoundaryString(Vector<char>& boundary)
{
// The RFC 2046 spec says the AlphaNumeric characters plus the following characters
// are legal for boundaries: '()+_,-./:=?
// However the following characters, though legal, cause some sites to fail:
// (),./:=
// http://bugs.webkit.org/show_bug.cgi?id=13352
static const char AlphaNumericEncMap[64] =
{
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
0x77, 0x78, 0x79, 0x7A, 0x30, 0x31, 0x32, 0x33,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2B, 0x41
// FIXME <rdar://problem/5252577> gmail does not accept legal characters in the form boundary
// As stated above, some legal characters cause, sites to fail. Specifically
// the / character which was the last character in the above array. I have
// replaced the last character with another character already in the array
// (notice the first and last values are both 0x41, A). Instead of picking
// another unique legal character for boundary strings that, because it has
// never been tested, may or may not break other sites, I simply
// replaced / with A. This means A is twice as likely to occur in our boundary
// strings than any other character but I think this is fine for the time being.
// The FIXME here is about restoring the / character once the aforementioned
// radar has been resolved.
};
// Start with an informative prefix.
const char boundaryPrefix[] = "----WebKitFormBoundary";
boundary.append(boundaryPrefix, strlen(boundaryPrefix));
// Append 16 random 7bit ascii AlphaNumeric characters.
Vector<char> randomBytes;
for (int i = 0; i < 4; ++i) {
int randomness = randomNumber();
randomBytes.append(AlphaNumericEncMap[(randomness >> 24) & 0x3F]);
randomBytes.append(AlphaNumericEncMap[(randomness >> 16) & 0x3F]);
randomBytes.append(AlphaNumericEncMap[(randomness >> 8) & 0x3F]);
randomBytes.append(AlphaNumericEncMap[randomness & 0x3F]);
}
boundary.append(randomBytes);
boundary.append(0); // Add a 0 at the end so we can use this as a C-style string.
}
void HTMLFormElement::submit(Event* event, bool activateSubmitButton)
{
FrameView *view = document()->view();
Frame *frame = document()->frame();
if (!view || !frame)
return;
if (m_insubmit) {
m_doingsubmit = true;
return;
}
m_insubmit = true;
HTMLGenericFormElement* firstSuccessfulSubmitButton = 0;
bool needButtonActivation = activateSubmitButton; // do we need to activate a submit button?
frame->loader()->clearRecordedFormValues();
for (unsigned i = 0; i < formElements.size(); ++i) {
HTMLGenericFormElement* current = formElements[i];
if (current->hasLocalName(inputTag)) {
HTMLInputElement* input = static_cast<HTMLInputElement*>(current);
if (input->isTextField()) {
frame->loader()->recordFormValue(input->name(), input->value(), this);
if (input->isSearchField())
input->addSearchResult();
}
}
if (needButtonActivation) {
if (current->isActivatedSubmit())
needButtonActivation = false;
else if (firstSuccessfulSubmitButton == 0 && current->isSuccessfulSubmitButton())
firstSuccessfulSubmitButton = current;
}
}
if (needButtonActivation && firstSuccessfulSubmitButton)
firstSuccessfulSubmitButton->setActivatedSubmit(true);
if (!m_url)
m_url = document()->url();
if (m_post) {
if (m_multipart && isMailtoForm()) {
setEnctype("application/x-www-form-urlencoded");
m_multipart = false;
}
if (!m_multipart) {
RefPtr<FormData> data = formData(0);
if (isMailtoForm()) {
String body = data->flattenToString();
if (equalIgnoringCase(enctype(), "text/plain")) {
// Convention seems to be to decode, and s/&/\r\n/. Also, spaces are encoded as %20.
body = KURL::decode_string(body.replace('&', "\r\n").replace('+', ' ').deprecatedString() + "\r\n");
}
data = new FormData((String("body=") + encodeCString(body.utf8())).replace('+', "%20").latin1());
}
frame->loader()->submitForm("POST", m_url, data, m_target, enctype(), String(), event);
} else {
Vector<char> boundary;
getUniqueBoundaryString(boundary);
frame->loader()->submitForm("POST", m_url, formData(boundary.data()), m_target, enctype(), boundary.data(), event);
}
} else {
m_multipart = false;
frame->loader()->submitForm("GET", m_url, formData(0), m_target, String(), String(), event);
}
if (needButtonActivation && firstSuccessfulSubmitButton)
firstSuccessfulSubmitButton->setActivatedSubmit(false);
m_doingsubmit = m_insubmit = false;
}
void HTMLFormElement::reset()
{
Frame *frame = document()->frame();
if (m_inreset || !frame)
return;
m_inreset = true;
// ### DOM2 labels this event as not cancelable, however
// common browsers( sick! ) allow it be cancelled.
if ( !dispatchHTMLEvent(resetEvent,true, true) ) {
m_inreset = false;
return;
}
for (unsigned i = 0; i < formElements.size(); ++i)
formElements[i]->reset();
m_inreset = false;
}
void HTMLFormElement::parseMappedAttribute(MappedAttribute *attr)
{
if (attr->name() == actionAttr)
m_url = parseURL(attr->value());
else if (attr->name() == targetAttr)
m_target = attr->value();
else if (attr->name() == methodAttr) {
if (equalIgnoringCase(attr->value(), "post"))
m_post = true;
else if (equalIgnoringCase(attr->value(), "get"))
m_post = false;
} else if (attr->name() == enctypeAttr)
parseEnctype(attr->value());
else if (attr->name() == accept_charsetAttr)
// space separated list of charsets the server
// accepts - see rfc2045
m_acceptcharset = attr->value();
else if (attr->name() == acceptAttr) {
// ignore this one for the moment...
} else if (attr->name() == autocompleteAttr) {
m_autocomplete = !equalIgnoringCase(attr->value(), "off");
if (!m_autocomplete)
document()->registerForCacheCallbacks(this);
else
document()->unregisterForCacheCallbacks(this);
} else if (attr->name() == onsubmitAttr)
setHTMLEventListener(submitEvent, attr);
else if (attr->name() == onresetAttr)
setHTMLEventListener(resetEvent, attr);
else if (attr->name() == nameAttr) {
String newNameAttr = attr->value();
if (inDocument() && document()->isHTMLDocument()) {
HTMLDocument *doc = static_cast<HTMLDocument *>(document());
doc->removeNamedItem(oldNameAttr);
doc->addNamedItem(newNameAttr);
}
oldNameAttr = newNameAttr;
} else
HTMLElement::parseMappedAttribute(attr);
}
template<class T, size_t n> static void removeFromVector(Vector<T*, n> & vec, T* item)
{
size_t size = vec.size();
for (size_t i = 0; i != size; ++i)
if (vec[i] == item) {
vec.remove(i);
break;
}
}
unsigned HTMLFormElement::formElementIndex(HTMLGenericFormElement *e)
{
// Check for the special case where this element is the very last thing in
// the form's tree of children; we don't want to walk the entire tree in that
// common case that occurs during parsing; instead we'll just return a value
// that says "add this form element to the end of the array".
if (e->traverseNextNode(this)) {
unsigned i = 0;
for (Node *node = this; node; node = node->traverseNextNode(this)) {
if (node == e)
return i;
if (node->isHTMLElement()
&& static_cast<HTMLElement *>(node)->isGenericFormElement()
&& static_cast<HTMLGenericFormElement *>(node)->form() == this)
++i;
}
}
return formElements.size();
}
void HTMLFormElement::registerFormElement(HTMLGenericFormElement* e)
{
document()->checkedRadioButtons().removeButton(e);
m_checkedRadioButtons.addButton(e);
formElements.insert(formElementIndex(e), e);
}
void HTMLFormElement::removeFormElement(HTMLGenericFormElement* e)
{
m_checkedRadioButtons.removeButton(e);
removeFromVector(formElements, e);
}
bool HTMLFormElement::isURLAttribute(Attribute *attr) const
{
return attr->name() == actionAttr;
}
void HTMLFormElement::registerImgElement(HTMLImageElement *e)
{
imgElements.append(e);
}
void HTMLFormElement::removeImgElement(HTMLImageElement *e)
{
removeFromVector(imgElements, e);
}
PassRefPtr<HTMLCollection> HTMLFormElement::elements()
{
return new HTMLFormCollection(this);
}
String HTMLFormElement::name() const
{
return getAttribute(nameAttr);
}
void HTMLFormElement::setName(const String &value)
{
setAttribute(nameAttr, value);
}
String HTMLFormElement::acceptCharset() const
{
return getAttribute(accept_charsetAttr);
}
void HTMLFormElement::setAcceptCharset(const String &value)
{
setAttribute(accept_charsetAttr, value);
}
String HTMLFormElement::action() const
{
return getAttribute(actionAttr);
}
void HTMLFormElement::setAction(const String &value)
{
setAttribute(actionAttr, value);
}
void HTMLFormElement::setEnctype(const String &value)
{
setAttribute(enctypeAttr, value);
}
String HTMLFormElement::method() const
{
return getAttribute(methodAttr);
}
void HTMLFormElement::setMethod(const String &value)
{
setAttribute(methodAttr, value);
}
String HTMLFormElement::target() const
{
return getAttribute(targetAttr);
}
void HTMLFormElement::setTarget(const String &value)
{
setAttribute(targetAttr, value);
}
PassRefPtr<HTMLGenericFormElement> HTMLFormElement::elementForAlias(const AtomicString& alias)
{
if (alias.isEmpty() || !m_elementAliases)
return 0;
return m_elementAliases->get(alias.impl());
}
void HTMLFormElement::addElementAlias(HTMLGenericFormElement* element, const AtomicString& alias)
{
if (alias.isEmpty())
return;
if (!m_elementAliases)
m_elementAliases = new AliasMap;
m_elementAliases->set(alias.impl(), element);
}
void HTMLFormElement::getNamedElements(const AtomicString& name, Vector<RefPtr<Node> >& namedItems)
{
elements()->namedItems(name, namedItems);
// see if we have seen something with this name before
RefPtr<HTMLGenericFormElement> aliasElem;
if (aliasElem = elementForAlias(name)) {
bool found = false;
for (unsigned n = 0; n < namedItems.size(); n++) {
if (namedItems[n] == aliasElem.get()) {
found = true;
break;
}
}
if (!found)
// we have seen it before but it is gone now. still, we need to return it.
namedItems.append(aliasElem.get());
}
// name has been accessed, remember it
if (namedItems.size() && aliasElem != namedItems.first())
addElementAlias(static_cast<HTMLGenericFormElement*>(namedItems.first().get()), name);
}
void HTMLFormElement::didRestoreFromCache()
{
ASSERT(!m_autocomplete);
for (unsigned i = 0; i < formElements.size(); ++i)
formElements[i]->reset();
}
void HTMLFormElement::willMoveToNewOwnerDocument()
{
if (!m_autocomplete)
document()->unregisterForCacheCallbacks(this);
}
void HTMLFormElement::didMoveToNewOwnerDocument()
{
if(m_autocomplete)
document()->registerForCacheCallbacks(this);
}
void HTMLFormElement::CheckedRadioButtons::addButton(HTMLGenericFormElement* element)
{
// We only want to add radio buttons.
if (!element->isRadioButton())
return;
// Without a name, there is no group.
if (element->name().isEmpty())
return;
HTMLInputElement* inputElement = static_cast<HTMLInputElement*>(element);
// We only track checked buttons.
if (!inputElement->checked())
return;
if (!m_nameToCheckedRadioButtonMap)
m_nameToCheckedRadioButtonMap.set(new NameToInputMap);
pair<NameToInputMap::iterator, bool> result = m_nameToCheckedRadioButtonMap->add(element->name().impl(), inputElement);
if (result.second)
return;
HTMLInputElement* oldCheckedButton = result.first->second;
if (oldCheckedButton == inputElement)
return;
result.first->second = inputElement;
oldCheckedButton->setChecked(false);
}
HTMLInputElement* HTMLFormElement::CheckedRadioButtons::checkedButtonForGroup(const AtomicString& name) const
{
if (!m_nameToCheckedRadioButtonMap)
return 0;
return m_nameToCheckedRadioButtonMap->get(name.impl());
}
void HTMLFormElement::CheckedRadioButtons::removeButton(HTMLGenericFormElement* element)
{
if (element->name().isEmpty() || !m_nameToCheckedRadioButtonMap)
return;
NameToInputMap::iterator it = m_nameToCheckedRadioButtonMap->find(element->name().impl());
if (it == m_nameToCheckedRadioButtonMap->end() || it->second != element)
return;
ASSERT(element->isRadioButton());
ASSERT(element->isChecked());
m_nameToCheckedRadioButtonMap->remove(it);
if (m_nameToCheckedRadioButtonMap->isEmpty())
m_nameToCheckedRadioButtonMap.clear();
}
} // namespace