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
22 #include "XMLHttpRequest.h"
25 #include "DOMImplementation.h"
27 #include "EventException.h"
28 #include "EventListener.h"
29 #include "EventNames.h"
31 #include "FrameLoader.h"
32 #include "HTTPParsers.h"
35 #include "SubresourceLoader.h"
36 #include "TextResourceDecoder.h"
37 #include "XMLHttpRequestException.h"
38 #include "kjs_binding.h"
42 using namespace EventNames
;
44 typedef HashSet
<XMLHttpRequest
*> RequestsSet
;
46 static HashMap
<Document
*, RequestsSet
*>& requestsByDocument()
48 static HashMap
<Document
*, RequestsSet
*> map
;
52 static void addToRequestsByDocument(Document
* doc
, XMLHttpRequest
* req
)
57 RequestsSet
* requests
= requestsByDocument().get(doc
);
59 requests
= new RequestsSet
;
60 requestsByDocument().set(doc
, requests
);
63 ASSERT(!requests
->contains(req
));
67 static void removeFromRequestsByDocument(Document
* doc
, XMLHttpRequest
* req
)
72 RequestsSet
* requests
= requestsByDocument().get(doc
);
74 ASSERT(requests
->contains(req
));
75 requests
->remove(req
);
76 if (requests
->isEmpty()) {
77 requestsByDocument().remove(doc
);
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
++) {
116 if (c
>= 127 || c
<= 32)
119 if (c
== '(' || c
== ')' || c
== '<' || c
== '>' || c
== '@' ||
120 c
== ',' || c
== ';' || c
== ':' || c
== '\\' || c
== '\"' ||
121 c
== '/' || c
== '[' || c
== ']' || c
== '?' || c
== '=' ||
122 c
== '{' || c
== '}')
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
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
)
152 if (!m_createdDocument
) {
153 if (m_response
.isHTTP() && !responseIsXML()) {
154 // The W3C spec requires this.
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())
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
);
202 ListenerVector
& listeners
= iter
->second
;
203 for (ListenerVector::iterator listenerIter
= listeners
.begin(); listenerIter
!= listeners
.end(); ++listenerIter
)
204 if (*listenerIter
== eventListener
)
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())
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());
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
;
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)
249 , m_state(Uninitialized
)
251 , m_createdDocument(false)
255 addToRequestsByDocument(m_doc
, this);
258 XMLHttpRequest::~XMLHttpRequest()
261 removeFromRequestsByDocument(m_doc
, this);
264 void XMLHttpRequest::changeState(XMLHttpRequestState newState
)
266 if (m_state
!= newState
) {
268 callReadyStateChangeListener();
272 void XMLHttpRequest::callReadyStateChangeListener()
274 if (!m_doc
|| !m_doc
->frame())
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);
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);
301 bool XMLHttpRequest::urlMatchesDocumentDomain(const KURL
& url
) const
303 // a local file can load anything
304 if (m_doc
->isAllowedToLoadLocalResources())
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())
317 void XMLHttpRequest::open(const String
& method
, const KURL
& url
, bool async
, ExceptionCode
& ec
)
322 // clear stuff from possible previous load
323 m_requestHeaders
.clear();
324 m_response
= ResourceResponse();
329 m_createdDocument
= false;
332 ASSERT(m_state
== Uninitialized
);
334 if (!urlMatchesDocumentDomain(url
)) {
335 ec
= XMLHttpRequestException::PERMISSION_DENIED
;
339 if (!isValidToken(method
)) {
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
;
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
;
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
)
389 if (m_state
!= Open
) {
390 ec
= INVALID_STATE_ERR
;
394 // FIXME: Should this abort or raise an exception instead if we already have a m_loader going?
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
);
411 setRequestHeader("Content-Type", "application/xml", ec
);
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
);
431 ResourceResponse response
;
434 // avoid deadlock in case the loader wants to use JS on a background thread
435 KJS::JSLock::DropAllLocks dropLocks
;
437 m_doc
->frame()->loader()->loadResourceSynchronously(request
, error
, response
, data
);
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
);
447 ec
= XMLHttpRequestException::NETWORK_ERR
;
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.
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
;
486 m_state
= Uninitialized
;
489 void XMLHttpRequest::dropProtection()
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.
504 KJS::Collector::reportExtraMemoryCost(m_responseText
.size() * 2);
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())
522 ec
= INVALID_STATE_ERR
;
526 if (!isValidToken(name
) || !isValidHeaderValue(value
)) {
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());
537 if (!m_requestHeaders
.contains(name
)) {
538 m_requestHeaders
.set(name
, value
);
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
;
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
;
580 if (!isValidToken(name
))
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"));
593 mimeType
= m_response
.mimeType();
595 if (mimeType
.isEmpty())
596 mimeType
= "text/xml";
601 bool XMLHttpRequest::responseIsXML() const
603 return DOMImplementation::isXMLMIMEType(responseMIMEType());
606 int XMLHttpRequest::getStatus(ExceptionCode
& ec
) const
608 if (m_state
== Uninitialized
)
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
)
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
;
632 // FIXME: should try to preserve status text in response
636 void XMLHttpRequest::processSyncLoadResults(const Vector
<char>& data
, const ResourceResponse
& response
)
638 if (!urlMatchesDocumentDomain(response
.url())) {
643 didReceiveResponse(0, response
);
648 const char* bytes
= static_cast<const char*>(data
.data());
649 int len
= static_cast<int>(data
.size());
651 didReceiveData(0, bytes
, len
);
658 void XMLHttpRequest::didFail(SubresourceLoader
* loader
, const ResourceError
&)
660 didFinishLoading(loader
);
663 void XMLHttpRequest::didFinishLoading(SubresourceLoader
* loader
)
668 ASSERT(loader
== m_loader
);
676 m_responseText
+= m_decoder
->flush();
679 bool hadLoader
= m_loader
;
689 void XMLHttpRequest::willSendRequest(SubresourceLoader
*, ResourceRequest
& request
, const ResourceResponse
& redirectResponse
)
691 if (!urlMatchesDocumentDomain(request
.url()))
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
)
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");
723 m_decoder
= new TextResourceDecoder("text/plain", "UTF-8");
731 String decoded
= m_decoder
->decode(data
, len
);
735 m_responseText
+= decoded
;
739 if (m_state
!= Receiving
)
740 changeState(Receiving
);
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
);
752 RequestsSet copy
= *requests
;
753 RequestsSet::const_iterator end
= copy
.end();
754 for (RequestsSet::const_iterator it
= copy
.begin(); it
!= end
; ++it
)
758 void XMLHttpRequest::detachRequests(Document
* m_doc
)
760 RequestsSet
* requests
= requestsByDocument().get(m_doc
);
763 requestsByDocument().remove(m_doc
);
764 RequestsSet::const_iterator end
= requests
->end();
765 for (RequestsSet::const_iterator it
= requests
->begin(); it
!= end
; ++it
) {