WebCore:
[webkit/qt.git] / WebCore / xml / XMLHttpRequest.cpp
blob403803c22eac0ec89d765ff5075ff0d7015349c3
1 /*
2 * Copyright (C) 2004, 2006, 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2005-2007 Alexey Proskuryakov <ap@webkit.org>
4 * Copyright (C) 2007 Julien Chaffraix <julien.chaffraix@gmail.com>
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 #include "config.h"
22 #include "XMLHttpRequest.h"
24 #include "CString.h"
25 #include "DOMImplementation.h"
26 #include "Event.h"
27 #include "EventException.h"
28 #include "EventListener.h"
29 #include "EventNames.h"
30 #include "Frame.h"
31 #include "FrameLoader.h"
32 #include "HTTPParsers.h"
33 #include "Page.h"
34 #include "Settings.h"
35 #include "SubresourceLoader.h"
36 #include "TextResourceDecoder.h"
37 #include "XMLHttpRequestException.h"
38 #include "kjs_binding.h"
40 namespace WebCore {
42 using namespace EventNames;
44 typedef HashSet<XMLHttpRequest*> RequestsSet;
46 static HashMap<Document*, RequestsSet*>& requestsByDocument()
48 static HashMap<Document*, RequestsSet*> map;
49 return map;
52 static void addToRequestsByDocument(Document* doc, XMLHttpRequest* req)
54 ASSERT(doc);
55 ASSERT(req);
57 RequestsSet* requests = requestsByDocument().get(doc);
58 if (!requests) {
59 requests = new RequestsSet;
60 requestsByDocument().set(doc, requests);
63 ASSERT(!requests->contains(req));
64 requests->add(req);
67 static void removeFromRequestsByDocument(Document* doc, XMLHttpRequest* req)
69 ASSERT(doc);
70 ASSERT(req);
72 RequestsSet* requests = requestsByDocument().get(doc);
73 ASSERT(requests);
74 ASSERT(requests->contains(req));
75 requests->remove(req);
76 if (requests->isEmpty()) {
77 requestsByDocument().remove(doc);
78 delete requests;
82 static bool canSetRequestHeader(const String& name)
84 static HashSet<String, CaseFoldingHash> forbiddenHeaders;
85 static String proxyString("proxy-");
87 if (forbiddenHeaders.isEmpty()) {
88 forbiddenHeaders.add("accept-charset");
89 forbiddenHeaders.add("accept-encoding");
90 forbiddenHeaders.add("connection");
91 forbiddenHeaders.add("content-length");
92 forbiddenHeaders.add("content-transfer-encoding");
93 forbiddenHeaders.add("date");
94 forbiddenHeaders.add("expect");
95 forbiddenHeaders.add("host");
96 forbiddenHeaders.add("keep-alive");
97 forbiddenHeaders.add("referer");
98 forbiddenHeaders.add("te");
99 forbiddenHeaders.add("trailer");
100 forbiddenHeaders.add("transfer-encoding");
101 forbiddenHeaders.add("upgrade");
102 forbiddenHeaders.add("via");
105 return !forbiddenHeaders.contains(name) && !name.startsWith(proxyString, false);
108 // Determines if a string is a valid token, as defined by
109 // "token" in section 2.2 of RFC 2616.
110 static bool isValidToken(const String& name)
112 unsigned length = name.length();
113 for (unsigned i = 0; i < length; i++) {
114 UChar c = name[i];
116 if (c >= 127 || c <= 32)
117 return false;
119 if (c == '(' || c == ')' || c == '<' || c == '>' || c == '@' ||
120 c == ',' || c == ';' || c == ':' || c == '\\' || c == '\"' ||
121 c == '/' || c == '[' || c == ']' || c == '?' || c == '=' ||
122 c == '{' || c == '}')
123 return false;
126 return true;
129 static bool isValidHeaderValue(const String& name)
131 // FIXME: This should really match name against
132 // field-value in section 4.2 of RFC 2616.
134 return !name.contains('\r') && !name.contains('\n');
137 XMLHttpRequestState XMLHttpRequest::getReadyState() const
139 return m_state;
142 const KJS::UString& XMLHttpRequest::getResponseText(ExceptionCode& ec) const
144 return m_responseText;
147 Document* XMLHttpRequest::getResponseXML(ExceptionCode& ec) const
149 if (m_state != Loaded)
150 return 0;
152 if (!m_createdDocument) {
153 if (m_response.isHTTP() && !responseIsXML()) {
154 // The W3C spec requires this.
155 m_responseXML = 0;
156 } else {
157 m_responseXML = m_doc->implementation()->createDocument(0);
158 m_responseXML->open();
159 m_responseXML->setURL(m_url);
160 // FIXME: set Last-Modified and cookies (currently, those are only available for HTMLDocuments).
161 m_responseXML->write(String(m_responseText));
162 m_responseXML->finishParsing();
163 m_responseXML->close();
165 if (!m_responseXML->wellFormed())
166 m_responseXML = 0;
168 m_createdDocument = true;
171 return m_responseXML.get();
174 EventListener* XMLHttpRequest::onReadyStateChangeListener() const
176 return m_onReadyStateChangeListener.get();
179 void XMLHttpRequest::setOnReadyStateChangeListener(EventListener* eventListener)
181 m_onReadyStateChangeListener = eventListener;
184 EventListener* XMLHttpRequest::onLoadListener() const
186 return m_onLoadListener.get();
189 void XMLHttpRequest::setOnLoadListener(EventListener* eventListener)
191 m_onLoadListener = eventListener;
194 void XMLHttpRequest::addEventListener(const AtomicString& eventType, PassRefPtr<EventListener> eventListener, bool)
196 EventListenersMap::iterator iter = m_eventListeners.find(eventType.impl());
197 if (iter == m_eventListeners.end()) {
198 ListenerVector listeners;
199 listeners.append(eventListener);
200 m_eventListeners.add(eventType.impl(), listeners);
201 } else {
202 ListenerVector& listeners = iter->second;
203 for (ListenerVector::iterator listenerIter = listeners.begin(); listenerIter != listeners.end(); ++listenerIter)
204 if (*listenerIter == eventListener)
205 return;
207 listeners.append(eventListener);
208 m_eventListeners.add(eventType.impl(), listeners);
212 void XMLHttpRequest::removeEventListener(const AtomicString& eventType, EventListener* eventListener, bool)
214 EventListenersMap::iterator iter = m_eventListeners.find(eventType.impl());
215 if (iter == m_eventListeners.end())
216 return;
218 ListenerVector& listeners = iter->second;
219 for (ListenerVector::const_iterator listenerIter = listeners.begin(); listenerIter != listeners.end(); ++listenerIter)
220 if (*listenerIter == eventListener) {
221 listeners.remove(listenerIter - listeners.begin());
222 return;
226 bool XMLHttpRequest::dispatchEvent(PassRefPtr<Event> evt, ExceptionCode& ec, bool /*tempEvent*/)
228 // FIXME: check for other error conditions enumerated in the spec.
229 if (evt->type().isEmpty()) {
230 ec = EventException::UNSPECIFIED_EVENT_TYPE_ERR;
231 return true;
234 ListenerVector listenersCopy = m_eventListeners.get(evt->type().impl());
235 for (ListenerVector::const_iterator listenerIter = listenersCopy.begin(); listenerIter != listenersCopy.end(); ++listenerIter) {
236 evt->setTarget(this);
237 evt->setCurrentTarget(this);
238 listenerIter->get()->handleEvent(evt.get(), false);
241 return !evt->defaultPrevented();
244 XMLHttpRequest::XMLHttpRequest(Document* d)
245 : RefCounted<XMLHttpRequest>(0)
246 , m_doc(d)
247 , m_async(true)
248 , m_loader(0)
249 , m_state(Uninitialized)
250 , m_responseText("")
251 , m_createdDocument(false)
252 , m_aborted(false)
254 ASSERT(m_doc);
255 addToRequestsByDocument(m_doc, this);
258 XMLHttpRequest::~XMLHttpRequest()
260 if (m_doc)
261 removeFromRequestsByDocument(m_doc, this);
264 void XMLHttpRequest::changeState(XMLHttpRequestState newState)
266 if (m_state != newState) {
267 m_state = newState;
268 callReadyStateChangeListener();
272 void XMLHttpRequest::callReadyStateChangeListener()
274 if (!m_doc || !m_doc->frame())
275 return;
277 RefPtr<Event> evt = new Event(readystatechangeEvent, false, false);
278 if (m_onReadyStateChangeListener) {
279 evt->setTarget(this);
280 evt->setCurrentTarget(this);
281 m_onReadyStateChangeListener->handleEvent(evt.get(), false);
284 ExceptionCode ec = 0;
285 dispatchEvent(evt.release(), ec, false);
286 ASSERT(!ec);
288 if (m_state == Loaded) {
289 evt = new Event(loadEvent, false, false);
290 if (m_onLoadListener) {
291 evt->setTarget(this);
292 evt->setCurrentTarget(this);
293 m_onLoadListener->handleEvent(evt.get(), false);
296 dispatchEvent(evt, ec, false);
297 ASSERT(!ec);
301 bool XMLHttpRequest::urlMatchesDocumentDomain(const KURL& url) const
303 // a local file can load anything
304 if (m_doc->isAllowedToLoadLocalResources())
305 return true;
307 // but a remote document can only load from the same port on the server
308 KURL documentURL(m_doc->url());
309 if (documentURL.protocol().lower() == url.protocol().lower()
310 && documentURL.host().lower() == url.host().lower()
311 && documentURL.port() == url.port())
312 return true;
314 return false;
317 void XMLHttpRequest::open(const String& method, const KURL& url, bool async, ExceptionCode& ec)
319 abort();
320 m_aborted = false;
322 // clear stuff from possible previous load
323 m_requestHeaders.clear();
324 m_response = ResourceResponse();
326 KJS::JSLock lock;
327 m_responseText = "";
329 m_createdDocument = false;
330 m_responseXML = 0;
332 ASSERT(m_state == Uninitialized);
334 if (!urlMatchesDocumentDomain(url)) {
335 ec = XMLHttpRequestException::PERMISSION_DENIED;
336 return;
339 if (!isValidToken(method)) {
340 ec = SYNTAX_ERR;
341 return;
344 // Method names are case sensitive. But since Firefox uppercases method names it knows, we'll do the same.
345 String methodUpper(method.upper());
347 if (methodUpper == "TRACE" || methodUpper == "TRACK" || methodUpper == "CONNECT") {
348 ec = XMLHttpRequestException::PERMISSION_DENIED;
349 return;
352 m_url = url;
354 if (methodUpper == "COPY" || methodUpper == "DELETE" || methodUpper == "GET" || methodUpper == "HEAD"
355 || methodUpper == "INDEX" || methodUpper == "LOCK" || methodUpper == "M-POST" || methodUpper == "MKCOL" || methodUpper == "MOVE"
356 || methodUpper == "OPTIONS" || methodUpper == "POST" || methodUpper == "PROPFIND" || methodUpper == "PROPPATCH" || methodUpper == "PUT"
357 || methodUpper == "UNLOCK")
358 m_method = methodUpper;
359 else
360 m_method = method;
362 m_async = async;
364 changeState(Open);
367 void XMLHttpRequest::open(const String& method, const KURL& url, bool async, const String& user, ExceptionCode& ec)
369 KURL urlWithCredentials(url);
370 urlWithCredentials.setUser(user);
372 open(method, urlWithCredentials, async, ec);
375 void XMLHttpRequest::open(const String& method, const KURL& url, bool async, const String& user, const String& password, ExceptionCode& ec)
377 KURL urlWithCredentials(url);
378 urlWithCredentials.setUser(user);
379 urlWithCredentials.setPass(password);
381 open(method, urlWithCredentials, async, ec);
384 void XMLHttpRequest::send(const String& body, ExceptionCode& ec)
386 if (!m_doc)
387 return;
389 if (m_state != Open) {
390 ec = INVALID_STATE_ERR;
391 return;
394 // FIXME: Should this abort or raise an exception instead if we already have a m_loader going?
395 if (m_loader)
396 return;
398 m_aborted = false;
400 ResourceRequest request(m_url);
401 request.setHTTPMethod(m_method);
403 if (!body.isNull() && m_method != "GET" && m_method != "HEAD" && (m_url.protocol().lower() == "http" || m_url.protocol().lower() == "https")) {
404 String contentType = getRequestHeader("Content-Type");
405 if (contentType.isEmpty()) {
406 ExceptionCode ec = 0;
407 Settings* settings = m_doc->settings();
408 if (settings && settings->usesDashboardBackwardCompatibilityMode())
409 setRequestHeader("Content-Type", "application/x-www-form-urlencoded", ec);
410 else
411 setRequestHeader("Content-Type", "application/xml", ec);
412 ASSERT(ec == 0);
415 // FIXME: must use xmlEncoding for documents.
416 String charset = "UTF-8";
418 TextEncoding m_encoding(charset);
419 if (!m_encoding.isValid()) // FIXME: report an error?
420 m_encoding = UTF8Encoding();
422 request.setHTTPBody(PassRefPtr<FormData>(new FormData(m_encoding.encode(body.characters(), body.length()))));
425 if (m_requestHeaders.size() > 0)
426 request.addHTTPHeaderFields(m_requestHeaders);
428 if (!m_async) {
429 Vector<char> data;
430 ResourceError error;
431 ResourceResponse response;
434 // avoid deadlock in case the loader wants to use JS on a background thread
435 KJS::JSLock::DropAllLocks dropLocks;
436 if (m_doc->frame())
437 m_doc->frame()->loader()->loadResourceSynchronously(request, error, response, data);
440 m_loader = 0;
442 // No exception for file:/// resources, see <rdar://problem/4962298>.
443 // Also, if we have an HTTP response, then it wasn't a network error in fact.
444 if (error.isNull() || request.url().isLocalFile() || response.httpStatusCode() > 0)
445 processSyncLoadResults(data, response);
446 else
447 ec = XMLHttpRequestException::NETWORK_ERR;
449 return;
452 // Neither this object nor the JavaScript wrapper should be deleted while
453 // a request is in progress because we need to keep the listeners alive,
454 // and they are referenced by the JavaScript wrapper.
455 ref();
457 KJS::JSLock lock;
458 gcProtectNullTolerant(ScriptInterpreter::getDOMObject(this));
461 // SubresourceLoader::create can return null here, for example if we're no longer attached to a page.
462 // This is true while running onunload handlers.
463 // FIXME: We need to be able to send XMLHttpRequests from onunload, <http://bugs.webkit.org/show_bug.cgi?id=10904>.
464 // FIXME: Maybe create can return null for other reasons too?
465 // We need to keep content sniffing enabled for local files due to CFNetwork not providing a MIME type
466 // for local files otherwise, <rdar://problem/5671813>.
467 m_loader = SubresourceLoader::create(m_doc->frame(), this, request, false, true, request.url().isLocalFile());
470 void XMLHttpRequest::abort()
472 bool hadLoader = m_loader;
474 m_aborted = true;
476 if (hadLoader) {
477 m_loader->cancel();
478 m_loader = 0;
481 m_decoder = 0;
483 if (hadLoader)
484 dropProtection();
486 m_state = Uninitialized;
489 void XMLHttpRequest::dropProtection()
492 KJS::JSLock lock;
493 KJS::JSValue* wrapper = ScriptInterpreter::getDOMObject(this);
494 KJS::gcUnprotectNullTolerant(wrapper);
496 // the XHR object itself holds on to the responseText, and
497 // thus has extra cost even independent of any
498 // responseText or responseXML objects it has handed
499 // out. But it is protected from GC while loading, so this
500 // can't be recouped until the load is done, so only
501 // report the extra cost at that point.
503 if (wrapper)
504 KJS::Collector::reportExtraMemoryCost(m_responseText.size() * 2);
507 deref();
510 void XMLHttpRequest::overrideMIMEType(const String& override)
512 m_mimeTypeOverride = override;
515 void XMLHttpRequest::setRequestHeader(const String& name, const String& value, ExceptionCode& ec)
517 if (m_state != Open) {
518 Settings* settings = m_doc ? m_doc->settings() : 0;
519 if (settings && settings->usesDashboardBackwardCompatibilityMode())
520 return;
522 ec = INVALID_STATE_ERR;
523 return;
526 if (!isValidToken(name) || !isValidHeaderValue(value)) {
527 ec = SYNTAX_ERR;
528 return;
531 if (!canSetRequestHeader(name)) {
532 if (m_doc && m_doc->frame() && m_doc->frame()->page())
533 m_doc->frame()->page()->chrome()->addMessageToConsole(JSMessageSource, ErrorMessageLevel, "Refused to set unsafe header " + name, 1, String());
534 return;
537 if (!m_requestHeaders.contains(name)) {
538 m_requestHeaders.set(name, value);
539 return;
542 String oldValue = m_requestHeaders.get(name);
543 m_requestHeaders.set(name, oldValue + ", " + value);
546 String XMLHttpRequest::getRequestHeader(const String& name) const
548 return m_requestHeaders.get(name);
551 String XMLHttpRequest::getAllResponseHeaders(ExceptionCode& ec) const
553 if (m_state < Receiving) {
554 ec = INVALID_STATE_ERR;
555 return "";
558 Vector<UChar> stringBuilder;
559 String separator(": ");
561 HTTPHeaderMap::const_iterator end = m_response.httpHeaderFields().end();
562 for (HTTPHeaderMap::const_iterator it = m_response.httpHeaderFields().begin(); it!= end; ++it) {
563 stringBuilder.append(it->first.characters(), it->first.length());
564 stringBuilder.append(separator.characters(), separator.length());
565 stringBuilder.append(it->second.characters(), it->second.length());
566 stringBuilder.append((UChar)'\r');
567 stringBuilder.append((UChar)'\n');
570 return String::adopt(stringBuilder);
573 String XMLHttpRequest::getResponseHeader(const String& name, ExceptionCode& ec) const
575 if (m_state < Receiving) {
576 ec = INVALID_STATE_ERR;
577 return "";
580 if (!isValidToken(name))
581 return "";
583 return m_response.httpHeaderField(name);
586 String XMLHttpRequest::responseMIMEType() const
588 String mimeType = extractMIMETypeFromMediaType(m_mimeTypeOverride);
589 if (mimeType.isEmpty()) {
590 if (m_response.isHTTP())
591 mimeType = extractMIMETypeFromMediaType(m_response.httpHeaderField("Content-Type"));
592 else
593 mimeType = m_response.mimeType();
595 if (mimeType.isEmpty())
596 mimeType = "text/xml";
598 return mimeType;
601 bool XMLHttpRequest::responseIsXML() const
603 return DOMImplementation::isXMLMIMEType(responseMIMEType());
606 int XMLHttpRequest::getStatus(ExceptionCode& ec) const
608 if (m_state == Uninitialized)
609 return 0;
611 if (m_response.httpStatusCode() == 0) {
612 if (m_state != Receiving && m_state != Loaded)
613 // status MUST be available in these states, but we don't get any headers from non-HTTP requests
614 ec = INVALID_STATE_ERR;
617 return m_response.httpStatusCode();
620 String XMLHttpRequest::getStatusText(ExceptionCode& ec) const
622 if (m_state == Uninitialized)
623 return "";
625 if (m_response.httpStatusCode() == 0) {
626 if (m_state != Receiving && m_state != Loaded)
627 // statusText MUST be available in these states, but we don't get any headers from non-HTTP requests
628 ec = INVALID_STATE_ERR;
629 return String();
632 // FIXME: should try to preserve status text in response
633 return "OK";
636 void XMLHttpRequest::processSyncLoadResults(const Vector<char>& data, const ResourceResponse& response)
638 if (!urlMatchesDocumentDomain(response.url())) {
639 abort();
640 return;
643 didReceiveResponse(0, response);
644 changeState(Sent);
645 if (m_aborted)
646 return;
648 const char* bytes = static_cast<const char*>(data.data());
649 int len = static_cast<int>(data.size());
651 didReceiveData(0, bytes, len);
652 if (m_aborted)
653 return;
655 didFinishLoading(0);
658 void XMLHttpRequest::didFail(SubresourceLoader* loader, const ResourceError&)
660 didFinishLoading(loader);
663 void XMLHttpRequest::didFinishLoading(SubresourceLoader* loader)
665 if (m_aborted)
666 return;
668 ASSERT(loader == m_loader);
670 if (m_state < Sent)
671 changeState(Sent);
674 KJS::JSLock lock;
675 if (m_decoder)
676 m_responseText += m_decoder->flush();
679 bool hadLoader = m_loader;
680 m_loader = 0;
682 changeState(Loaded);
683 m_decoder = 0;
685 if (hadLoader)
686 dropProtection();
689 void XMLHttpRequest::willSendRequest(SubresourceLoader*, ResourceRequest& request, const ResourceResponse& redirectResponse)
691 if (!urlMatchesDocumentDomain(request.url()))
692 abort();
695 void XMLHttpRequest::didReceiveResponse(SubresourceLoader*, const ResourceResponse& response)
697 m_response = response;
698 m_encoding = extractCharsetFromMediaType(m_mimeTypeOverride);
699 if (m_encoding.isEmpty())
700 m_encoding = response.textEncodingName();
704 void XMLHttpRequest::receivedCancellation(SubresourceLoader*, const AuthenticationChallenge& challenge)
706 m_response = challenge.failureResponse();
709 void XMLHttpRequest::didReceiveData(SubresourceLoader*, const char* data, int len)
711 if (m_state < Sent)
712 changeState(Sent);
714 if (!m_decoder) {
715 if (!m_encoding.isEmpty())
716 m_decoder = new TextResourceDecoder("text/plain", m_encoding);
717 // allow TextResourceDecoder to look inside the m_response if it's XML or HTML
718 else if (responseIsXML())
719 m_decoder = new TextResourceDecoder("application/xml");
720 else if (responseMIMEType() == "text/html")
721 m_decoder = new TextResourceDecoder("text/html", "UTF-8");
722 else
723 m_decoder = new TextResourceDecoder("text/plain", "UTF-8");
725 if (len == 0)
726 return;
728 if (len == -1)
729 len = strlen(data);
731 String decoded = m_decoder->decode(data, len);
734 KJS::JSLock lock;
735 m_responseText += decoded;
738 if (!m_aborted) {
739 if (m_state != Receiving)
740 changeState(Receiving);
741 else
742 // Firefox calls readyStateChanged every time it receives data, 4449442
743 callReadyStateChangeListener();
747 void XMLHttpRequest::cancelRequests(Document* m_doc)
749 RequestsSet* requests = requestsByDocument().get(m_doc);
750 if (!requests)
751 return;
752 RequestsSet copy = *requests;
753 RequestsSet::const_iterator end = copy.end();
754 for (RequestsSet::const_iterator it = copy.begin(); it != end; ++it)
755 (*it)->abort();
758 void XMLHttpRequest::detachRequests(Document* m_doc)
760 RequestsSet* requests = requestsByDocument().get(m_doc);
761 if (!requests)
762 return;
763 requestsByDocument().remove(m_doc);
764 RequestsSet::const_iterator end = requests->end();
765 for (RequestsSet::const_iterator it = requests->begin(); it != end; ++it) {
766 (*it)->m_doc = 0;
767 (*it)->abort();
769 delete requests;
772 } // end namespace