2 * This file is part of the KDE libraries
3 * Copyright (C) 2004, 2006 Apple Computer, Inc.
4 * Copyright (C) 2005, 2006 Alexey Proskuryakov <ap@nypop.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 #include "xmlhttprequest.h"
25 #include "DOMImplementation.h"
26 #include "EventListener.h"
27 #include "EventNames.h"
28 #include "KWQLoader.h"
29 #include "dom2_eventsimpl.h"
30 #include "PlatformString.h"
32 #include "HTMLDocument.h"
33 #include "kjs_binding.h"
34 #include "TransferJob.h"
35 #include <kjs/protect.h>
37 #include "TextEncoding.h"
43 using namespace EventNames;
45 typedef HashSet<XMLHttpRequest*> RequestsSet;
47 static HashMap<Document*, RequestsSet*>& requestsByDocument()
49 static HashMap<Document*, RequestsSet*> map;
53 static void addToRequestsByDocument(Document* doc, XMLHttpRequest* req)
58 RequestsSet* requests = requestsByDocument().get(doc);
60 requests = new RequestsSet;
61 requestsByDocument().set(doc, requests);
64 ASSERT(!requests->contains(req));
68 static void removeFromRequestsByDocument(Document* doc, XMLHttpRequest* req)
73 RequestsSet* requests = requestsByDocument().get(doc);
75 ASSERT(requests->contains(req));
76 requests->remove(req);
77 if (requests->isEmpty()) {
78 requestsByDocument().remove(doc);
83 static inline String getMIMEType(const String& contentTypeString)
86 unsigned length = contentTypeString.length();
87 for (unsigned offset = 0; offset < length; offset++) {
88 QChar c = contentTypeString[offset];
93 mimeType += String(c);
98 static String getCharset(const String& contentTypeString)
101 int length = (int)contentTypeString.length();
103 while (pos < length) {
104 pos = contentTypeString.find("charset", pos, false);
108 // is what we found a beginning of a word?
109 if (contentTypeString[pos-1] > ' ' && contentTypeString[pos-1] != ';') {
117 while (pos != length && contentTypeString[pos] <= ' ')
120 if (contentTypeString[pos++] != '=') // this "charset" substring wasn't a parameter name, but there may be others
123 while (pos != length && (contentTypeString[pos] <= ' ' || contentTypeString[pos] == '"' || contentTypeString[pos] == '\''))
126 // we don't handle spaces within quoted parameter values, because charset names cannot have any
128 while (pos != length && contentTypeString[endpos] > ' ' && contentTypeString[endpos] != '"' && contentTypeString[endpos] != '\'' && contentTypeString[endpos] != ';')
131 return contentTypeString.substring(pos, endpos-pos);
137 XMLHttpRequestState XMLHttpRequest::getReadyState() const
142 String XMLHttpRequest::getResponseText() const
147 Document* XMLHttpRequest::getResponseXML() const
149 if (m_state != Completed)
152 if (!m_createdDocument) {
153 if (responseIsXML()) {
154 m_responseXML = m_doc->implementation()->createDocument();
155 m_responseXML->open();
156 m_responseXML->write(m_response);
157 m_responseXML->finishParsing();
158 m_responseXML->close();
160 m_createdDocument = true;
163 return m_responseXML.get();
166 EventListener* XMLHttpRequest::onReadyStateChangeListener() const
168 return m_onReadyStateChangeListener.get();
171 void XMLHttpRequest::setOnReadyStateChangeListener(EventListener* eventListener)
173 m_onReadyStateChangeListener = eventListener;
176 EventListener* XMLHttpRequest::onLoadListener() const
178 return m_onLoadListener.get();
181 void XMLHttpRequest::setOnLoadListener(EventListener* eventListener)
183 m_onLoadListener = eventListener;
186 XMLHttpRequest::XMLHttpRequest(Document *d)
190 , m_state(Uninitialized)
191 , m_createdDocument(false)
195 addToRequestsByDocument(m_doc, this);
198 XMLHttpRequest::~XMLHttpRequest()
201 removeFromRequestsByDocument(m_doc, this);
204 void XMLHttpRequest::changeState(XMLHttpRequestState newState)
206 if (m_state != newState) {
208 callReadyStateChangeListener();
212 void XMLHttpRequest::callReadyStateChangeListener()
214 if (m_doc && m_doc->frame() && m_onReadyStateChangeListener) {
216 RefPtr<Event> ev = m_doc->createEvent("HTMLEvents", ec);
217 ev->initEvent(readystatechangeEvent, true, true);
218 m_onReadyStateChangeListener->handleEvent(ev.get(), true);
221 if (m_doc && m_doc->frame() && m_state == Completed && m_onLoadListener) {
223 RefPtr<Event> ev = m_doc->createEvent("HTMLEvents", ec);
224 ev->initEvent(loadEvent, true, true);
225 m_onLoadListener->handleEvent(ev.get(), true);
229 bool XMLHttpRequest::urlMatchesDocumentDomain(const KURL& url) const
231 KURL documentURL(m_doc->URL());
233 // a local file can load anything
234 if (documentURL.protocol().lower() == "file")
237 // but a remote document can only load from the same port on the server
238 if (documentURL.protocol().lower() == url.protocol().lower()
239 && documentURL.host().lower() == url.host().lower()
240 && documentURL.port() == url.port())
246 void XMLHttpRequest::open(const String& method, const KURL& url, bool async, const String& user, const String& password)
251 // clear stuff from possible previous load
252 m_requestHeaders = DeprecatedString();
253 m_responseHeaders = String();
254 m_response = DeprecatedString();
255 m_createdDocument = false;
258 changeState(Uninitialized);
263 if (!urlMatchesDocumentDomain(url))
269 m_url.setUser(user.deprecatedString());
271 if (!password.isNull())
272 m_url.setPass(password.deprecatedString());
274 // Methods names are case-sensitive, but Firefox uppercases methods it knows
275 String methodUpper(method.upper());
276 if (methodUpper == "CONNECT" || methodUpper == "COPY" || methodUpper == "DELETE" || methodUpper == "GET" || methodUpper == "HEAD"
277 || methodUpper == "INDEX" || methodUpper == "LOCK" || methodUpper == "M-POST" || methodUpper == "MKCOL" || methodUpper == "MOVE"
278 || methodUpper == "OPTIONS" || methodUpper == "POST" || methodUpper == "PROPFIND" || methodUpper == "PROPPATCH" || methodUpper == "PUT"
279 || methodUpper == "TRACE" || methodUpper == "UNLOCK")
280 m_method = methodUpper.deprecatedString();
282 m_method = method.deprecatedString();
286 changeState(Loading);
289 void XMLHttpRequest::send(const String& body)
294 if (m_state != Loading)
297 // FIXME: Should this abort instead if we already have a m_job going?
303 if (!body.isNull() && m_method != "GET" && m_method != "HEAD" && (m_url.protocol().lower() == "http" || m_url.protocol().lower() == "https")) {
304 String contentType = getRequestHeader("Content-Type");
306 if (contentType.isEmpty())
307 setRequestHeader("Content-Type", "application/xml");
309 charset = getCharset(contentType);
311 if (charset.isEmpty())
314 TextEncoding m_encoding = TextEncoding(charset.deprecatedString().latin1());
315 if (!m_encoding.isValid()) // FIXME: report an error?
316 m_encoding = TextEncoding(UTF8Encoding);
318 m_job = new TransferJob(m_async ? this : 0, m_method, m_url, m_encoding.fromUnicode(body.deprecatedString()));
320 // FIXME: HEAD requests just crash; see <rdar://4460899> and the commented out tests in http/tests/xmlhttprequest/methods.html.
321 if (m_method == "HEAD")
323 m_job = new TransferJob(m_async ? this : 0, m_method, m_url);
326 if (m_requestHeaders.length())
327 m_job->addMetaData("customHTTPHeader", m_requestHeaders);
330 DeprecatedByteArray data;
332 DeprecatedString headers;
335 // avoid deadlock in case the loader wants to use JS on a background thread
336 KJS::JSLock::DropAllLocks dropLocks;
337 data = KWQServeSynchronousRequest(Cache::loader(), m_doc->docLoader(), m_job, finalURL, headers);
341 processSyncLoadResults(data, finalURL, headers);
346 // Neither this object nor the JavaScript wrapper should be deleted while
347 // a request is in progress because we need to keep the listeners alive,
348 // and they are referenced by the JavaScript wrapper.
352 gcProtectNullTolerant(KJS::ScriptInterpreter::getDOMObject(this));
355 m_job->start(m_doc->docLoader());
358 void XMLHttpRequest::abort()
372 gcUnprotectNullTolerant(KJS::ScriptInterpreter::getDOMObject(this));
378 void XMLHttpRequest::overrideMIMEType(const String& override)
380 m_mimeTypeOverride = override;
383 void XMLHttpRequest::setRequestHeader(const String& name, const String& value)
385 if (m_requestHeaders.length() > 0)
386 m_requestHeaders += "\r\n";
387 m_requestHeaders += name.deprecatedString();
388 m_requestHeaders += ": ";
389 m_requestHeaders += value.deprecatedString();
392 DeprecatedString XMLHttpRequest::getRequestHeader(const DeprecatedString& name) const
394 return getSpecificHeader(m_requestHeaders, name);
397 String XMLHttpRequest::getAllResponseHeaders() const
399 if (m_responseHeaders.isEmpty())
402 int endOfLine = m_responseHeaders.find("\n");
406 return m_responseHeaders.substring(endOfLine + 1) + "\n";
409 String XMLHttpRequest::getResponseHeader(const String& name) const
411 return getSpecificHeader(m_responseHeaders.deprecatedString(), name.deprecatedString());
414 DeprecatedString XMLHttpRequest::getSpecificHeader(const DeprecatedString& headers, const DeprecatedString& name)
416 if (headers.isEmpty())
417 return DeprecatedString();
419 RegularExpression headerLinePattern(name + ":", false);
422 int headerLinePos = headerLinePattern.match(headers, 0, &matchLength);
423 while (headerLinePos != -1) {
424 if (headerLinePos == 0 || headers[headerLinePos-1] == '\n')
426 headerLinePos = headerLinePattern.match(headers, headerLinePos + 1, &matchLength);
428 if (headerLinePos == -1)
429 return DeprecatedString();
431 int endOfLine = headers.find("\n", headerLinePos + matchLength);
432 return headers.mid(headerLinePos + matchLength, endOfLine - (headerLinePos + matchLength)).stripWhiteSpace();
435 bool XMLHttpRequest::responseIsXML() const
437 String mimeType = getMIMEType(m_mimeTypeOverride);
438 if (mimeType.isEmpty())
439 mimeType = getMIMEType(getResponseHeader("Content-Type"));
440 if (mimeType.isEmpty())
441 mimeType = "text/xml";
442 return DOMImplementation::isXMLMIMEType(mimeType);
445 int XMLHttpRequest::getStatus() const
447 if (m_responseHeaders.isEmpty())
450 int endOfLine = m_responseHeaders.find("\n");
451 String firstLine = endOfLine == -1 ? m_responseHeaders : m_responseHeaders.substring(0, endOfLine);
452 int codeStart = firstLine.find(" ");
453 int codeEnd = firstLine.find(" ", codeStart + 1);
454 if (codeStart == -1 || codeEnd == -1)
457 String number = firstLine.substring(codeStart + 1, codeEnd - (codeStart + 1));
459 int code = number.toInt(&ok);
465 String XMLHttpRequest::getStatusText() const
467 if (m_responseHeaders.isEmpty())
470 int endOfLine = m_responseHeaders.find("\n");
471 String firstLine = endOfLine == -1 ? m_responseHeaders : m_responseHeaders.substring(0, endOfLine);
472 int codeStart = firstLine.find(" ");
473 int codeEnd = firstLine.find(" ", codeStart + 1);
474 if (codeStart == -1 || codeEnd == -1)
477 return firstLine.substring(codeEnd + 1, endOfLine - (codeEnd + 1)).deprecatedString().stripWhiteSpace();
480 void XMLHttpRequest::processSyncLoadResults(const DeprecatedByteArray &data, const KURL &finalURL, const DeprecatedString &headers)
482 if (!urlMatchesDocumentDomain(finalURL)) {
487 m_responseHeaders = headers;
492 const char *bytes = (const char *)data.data();
493 int len = (int)data.size();
495 receivedData(0, bytes, len);
502 void XMLHttpRequest::receivedAllData(TransferJob*)
504 if (m_responseHeaders.isEmpty() && m_job)
505 m_responseHeaders = m_job->queryMetaData("HTTP-Headers");
507 if (m_state < Loaded)
511 m_response += m_decoder->flush();
516 changeState(Completed);
522 gcUnprotectNullTolerant(KJS::ScriptInterpreter::getDOMObject(this));
528 void XMLHttpRequest::receivedRedirect(TransferJob*, const KURL& m_url)
530 if (!urlMatchesDocumentDomain(m_url))
534 void XMLHttpRequest::receivedData(TransferJob*, const char *data, int len)
536 if (m_responseHeaders.isEmpty() && m_job)
537 m_responseHeaders = m_job->queryMetaData("HTTP-Headers");
539 if (m_state < Loaded)
543 m_encoding = getCharset(m_mimeTypeOverride);
544 if (m_encoding.isEmpty())
545 m_encoding = getCharset(getResponseHeader("Content-Type"));
546 if (m_encoding.isEmpty() && m_job)
547 m_encoding = m_job->queryMetaData("charset");
549 m_decoder = new Decoder;
550 if (!m_encoding.isEmpty())
551 m_decoder->setEncodingName(m_encoding.deprecatedString().latin1(), Decoder::EncodingFromHTTPHeader);
553 // only allow Decoder to look inside the m_response if it's XML
554 m_decoder->setEncodingName("UTF-8", responseIsXML() ? Decoder::DefaultEncoding : Decoder::EncodingFromHTTPHeader);
562 DeprecatedString decoded = m_decoder->decode(data, len);
564 m_response += decoded;
567 if (m_state != Interactive)
568 changeState(Interactive);
570 // Firefox calls readyStateChanged every time it receives data, 4449442
571 callReadyStateChangeListener();
575 void XMLHttpRequest::cancelRequests(Document* m_doc)
577 RequestsSet* requests = requestsByDocument().get(m_doc);
580 RequestsSet copy = *requests;
581 RequestsSet::const_iterator end = copy.end();
582 for (RequestsSet::const_iterator it = copy.begin(); it != end; ++it)
586 void XMLHttpRequest::detachRequests(Document* m_doc)
588 RequestsSet* requests = requestsByDocument().get(m_doc);
591 requestsByDocument().remove(m_doc);
592 RequestsSet::const_iterator end = requests->end();
593 for (RequestsSet::const_iterator it = requests->begin(); it != end; ++it) {