7bc418a87e142e295d8102114fbf5115521b7c40
[WebKit-https.git] / WebCore / xml / xmlhttprequest.cpp
1 /*
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>
5  *
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.
10  *
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.
15  *
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
19  */
20
21 #include "config.h"
22 #include "xmlhttprequest.h"
23
24 #include "Cache.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"
31 #include "formdata.h"
32 #include "HTMLDocument.h"
33 #include "kjs_binding.h"
34 #include "TransferJob.h"
35 #include <kjs/protect.h>
36 #include <qregexp.h>
37 #include "TextEncoding.h"
38
39 using namespace KIO;
40
41 namespace WebCore {
42
43 using namespace EventNames;
44
45 typedef HashSet<XMLHttpRequest*> RequestsSet;
46
47 static HashMap<Document*, RequestsSet*>& requestsByDocument()
48 {
49     static HashMap<Document*, RequestsSet*> map;
50     return map;
51 }
52
53 static void addToRequestsByDocument(Document* doc, XMLHttpRequest* req)
54 {
55     ASSERT(doc);
56     ASSERT(req);
57
58     RequestsSet* requests = requestsByDocument().get(doc);
59     if (!requests) {
60         requests = new RequestsSet;
61         requestsByDocument().set(doc, requests);
62     }
63
64     ASSERT(!requests->contains(req));
65     requests->add(req);
66 }
67
68 static void removeFromRequestsByDocument(Document* doc, XMLHttpRequest* req)
69 {
70     ASSERT(doc);
71     ASSERT(req);
72
73     RequestsSet* requests = requestsByDocument().get(doc);
74     ASSERT(requests);
75     ASSERT(requests->contains(req));
76     requests->remove(req);
77     if (requests->isEmpty()) {
78         requestsByDocument().remove(doc);
79         delete requests;
80     }
81 }
82
83 static inline String getMIMEType(const String& contentTypeString)
84 {
85     String mimeType;
86     unsigned length = contentTypeString.length();
87     for (unsigned offset = 0; offset < length; offset++) {
88         QChar c = contentTypeString[offset];
89         if (c == ';')
90             break;
91         else if (c.isSpace())
92             continue;
93         mimeType += String(c);
94     }
95     return mimeType;
96 }
97
98 static String getCharset(const String& contentTypeString)
99 {
100     int pos = 0;
101     int length = (int)contentTypeString.length();
102     
103     while (pos < length) {
104         pos = contentTypeString.find("charset", pos, false);
105         if (pos <= 0)
106             return String();
107         
108         // is what we found a beginning of a word?
109         if (contentTypeString[pos-1] > ' ' && contentTypeString[pos-1] != ';') {
110             pos += 7;
111             continue;
112         }
113         
114         pos += 7;
115
116         // skip whitespace
117         while (pos != length && contentTypeString[pos] <= ' ')
118             ++pos;
119     
120         if (contentTypeString[pos++] != '=') // this "charset" substring wasn't a parameter name, but there may be others
121             continue;
122
123         while (pos != length && (contentTypeString[pos] <= ' ' || contentTypeString[pos] == '"' || contentTypeString[pos] == '\''))
124             ++pos;
125
126         // we don't handle spaces within quoted parameter values, because charset names cannot have any
127         int endpos = pos;
128         while (pos != length && contentTypeString[endpos] > ' ' && contentTypeString[endpos] != '"' && contentTypeString[endpos] != '\'' && contentTypeString[endpos] != ';')
129             ++endpos;
130     
131         return contentTypeString.substring(pos, endpos-pos);
132     }
133     
134     return String();
135 }
136
137 XMLHttpRequestState XMLHttpRequest::getReadyState() const
138 {
139     return m_state;
140 }
141
142 String XMLHttpRequest::getResponseText() const
143 {
144     return m_response;
145 }
146
147 Document* XMLHttpRequest::getResponseXML() const
148 {
149     if (m_state != Completed)
150         return 0;
151
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();
159         }
160         m_createdDocument = true;
161     }
162
163     return m_responseXML.get();
164 }
165
166 EventListener* XMLHttpRequest::onReadyStateChangeListener() const
167 {
168     return m_onReadyStateChangeListener.get();
169 }
170
171 void XMLHttpRequest::setOnReadyStateChangeListener(EventListener* eventListener)
172 {
173     m_onReadyStateChangeListener = eventListener;
174 }
175
176 EventListener* XMLHttpRequest::onLoadListener() const
177 {
178     return m_onLoadListener.get();
179 }
180
181 void XMLHttpRequest::setOnLoadListener(EventListener* eventListener)
182 {
183     m_onLoadListener = eventListener;
184 }
185
186 XMLHttpRequest::XMLHttpRequest(Document *d)
187     : m_doc(d)
188     , m_async(true)
189     , m_job(0)
190     , m_state(Uninitialized)
191     , m_createdDocument(false)
192     , m_aborted(false)
193 {
194     ASSERT(m_doc);
195     addToRequestsByDocument(m_doc, this);
196 }
197
198 XMLHttpRequest::~XMLHttpRequest()
199 {
200     if (m_doc)
201         removeFromRequestsByDocument(m_doc, this);
202 }
203
204 void XMLHttpRequest::changeState(XMLHttpRequestState newState)
205 {
206     if (m_state != newState) {
207         m_state = newState;
208         callReadyStateChangeListener();
209     }
210 }
211
212 void XMLHttpRequest::callReadyStateChangeListener()
213 {
214     if (m_doc && m_doc->frame() && m_onReadyStateChangeListener) {
215         ExceptionCode ec;
216         RefPtr<Event> ev = m_doc->createEvent("HTMLEvents", ec);
217         ev->initEvent(readystatechangeEvent, true, true);
218         m_onReadyStateChangeListener->handleEvent(ev.get(), true);
219     }
220     
221     if (m_doc && m_doc->frame() && m_state == Completed && m_onLoadListener) {
222         ExceptionCode ec;
223         RefPtr<Event> ev = m_doc->createEvent("HTMLEvents", ec);
224         ev->initEvent(loadEvent, true, true);
225         m_onLoadListener->handleEvent(ev.get(), true);
226     }
227 }
228
229 bool XMLHttpRequest::urlMatchesDocumentDomain(const KURL& url) const
230 {
231   KURL documentURL(m_doc->URL());
232
233     // a local file can load anything
234     if (documentURL.protocol().lower() == "file")
235         return true;
236
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())
241         return true;
242
243     return false;
244 }
245
246 void XMLHttpRequest::open(const String& method, const KURL& url, bool async, const String& user, const String& password)
247 {
248     abort();
249     m_aborted = false;
250
251     // clear stuff from possible previous load
252     m_requestHeaders = DeprecatedString();
253     m_responseHeaders = String();
254     m_response = DeprecatedString();
255     m_createdDocument = false;
256     m_responseXML = 0;
257
258     changeState(Uninitialized);
259
260     if (m_aborted)
261         return;
262
263     if (!urlMatchesDocumentDomain(url))
264         return;
265
266     m_url = url;
267
268     if (!user.isNull())
269         m_url.setUser(user.deprecatedString());
270
271     if (!password.isNull())
272         m_url.setPass(password.deprecatedString());
273
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();
281     else
282         m_method = method.deprecatedString();
283
284     m_async = async;
285
286     changeState(Loading);
287 }
288
289 void XMLHttpRequest::send(const String& body)
290 {
291     if (!m_doc)
292         return;
293
294     if (m_state != Loading)
295         return;
296   
297     // FIXME: Should this abort instead if we already have a m_job going?
298     if (m_job)
299         return;
300
301     m_aborted = false;
302
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");
305         String charset;
306         if (contentType.isEmpty())
307             setRequestHeader("Content-Type", "application/xml");
308         else
309             charset = getCharset(contentType);
310       
311         if (charset.isEmpty())
312             charset = "UTF-8";
313       
314         TextEncoding m_encoding = TextEncoding(charset.deprecatedString().latin1());
315         if (!m_encoding.isValid())   // FIXME: report an error?
316             m_encoding = TextEncoding(UTF8Encoding);
317
318         m_job = new TransferJob(m_async ? this : 0, m_method, m_url, m_encoding.fromUnicode(body.deprecatedString()));
319     } else {
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")
322             m_method = "GET";
323         m_job = new TransferJob(m_async ? this : 0, m_method, m_url);
324     }
325
326     if (m_requestHeaders.length())
327         m_job->addMetaData("customHTTPHeader", m_requestHeaders);
328
329     if (!m_async) {
330         DeprecatedByteArray data;
331         KURL finalURL;
332         DeprecatedString headers;
333
334         {
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);
338         }
339
340         m_job = 0;
341         processSyncLoadResults(data, finalURL, headers);
342     
343         return;
344     }
345
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.
349     ref();
350     {
351         KJS::JSLock lock;
352         gcProtectNullTolerant(KJS::ScriptInterpreter::getDOMObject(this));
353     }
354   
355     m_job->start(m_doc->docLoader());
356 }
357
358 void XMLHttpRequest::abort()
359 {
360     bool hadJob = m_job;
361
362     if (hadJob) {
363         m_job->kill();
364         m_job = 0;
365     }
366     m_decoder = 0;
367     m_aborted = true;
368
369     if (hadJob) {
370         {
371             KJS::JSLock lock;
372             gcUnprotectNullTolerant(KJS::ScriptInterpreter::getDOMObject(this));
373         }
374         deref();
375     }
376 }
377
378 void XMLHttpRequest::overrideMIMEType(const String& override)
379 {
380     m_mimeTypeOverride = override;
381 }
382
383 void XMLHttpRequest::setRequestHeader(const String& name, const String& value)
384 {
385     if (m_requestHeaders.length() > 0)
386         m_requestHeaders += "\r\n";
387     m_requestHeaders += name.deprecatedString();
388     m_requestHeaders += ": ";
389     m_requestHeaders += value.deprecatedString();
390 }
391
392 DeprecatedString XMLHttpRequest::getRequestHeader(const DeprecatedString& name) const
393 {
394     return getSpecificHeader(m_requestHeaders, name);
395 }
396
397 String XMLHttpRequest::getAllResponseHeaders() const
398 {
399     if (m_responseHeaders.isEmpty())
400         return String();
401
402     int endOfLine = m_responseHeaders.find("\n");
403     if (endOfLine == -1)
404         return String();
405
406     return m_responseHeaders.substring(endOfLine + 1) + "\n";
407 }
408
409 String XMLHttpRequest::getResponseHeader(const String& name) const
410 {
411     return getSpecificHeader(m_responseHeaders.deprecatedString(), name.deprecatedString());
412 }
413
414 DeprecatedString XMLHttpRequest::getSpecificHeader(const DeprecatedString& headers, const DeprecatedString& name)
415 {
416     if (headers.isEmpty())
417         return DeprecatedString();
418
419     RegularExpression headerLinePattern(name + ":", false);
420
421     int matchLength;
422     int headerLinePos = headerLinePattern.match(headers, 0, &matchLength);
423     while (headerLinePos != -1) {
424         if (headerLinePos == 0 || headers[headerLinePos-1] == '\n')
425             break;
426         headerLinePos = headerLinePattern.match(headers, headerLinePos + 1, &matchLength);
427     }
428     if (headerLinePos == -1)
429         return DeprecatedString();
430     
431     int endOfLine = headers.find("\n", headerLinePos + matchLength);
432     return headers.mid(headerLinePos + matchLength, endOfLine - (headerLinePos + matchLength)).stripWhiteSpace();
433 }
434
435 bool XMLHttpRequest::responseIsXML() const
436 {
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);
443 }
444
445 int XMLHttpRequest::getStatus() const
446 {
447     if (m_responseHeaders.isEmpty())
448         return -1;
449   
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)
455         return -1;
456   
457     String number = firstLine.substring(codeStart + 1, codeEnd - (codeStart + 1));
458     bool ok = false;
459     int code = number.toInt(&ok);
460     if (!ok)
461         return -1;
462     return code;
463 }
464
465 String XMLHttpRequest::getStatusText() const
466 {
467     if (m_responseHeaders.isEmpty())
468         return String();
469   
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)
475         return String();
476   
477     return firstLine.substring(codeEnd + 1, endOfLine - (codeEnd + 1)).deprecatedString().stripWhiteSpace();
478 }
479
480 void XMLHttpRequest::processSyncLoadResults(const DeprecatedByteArray &data, const KURL &finalURL, const DeprecatedString &headers)
481 {
482   if (!urlMatchesDocumentDomain(finalURL)) {
483     abort();
484     return;
485   }
486   
487   m_responseHeaders = headers;
488   changeState(Loaded);
489   if (m_aborted)
490     return;
491   
492   const char *bytes = (const char *)data.data();
493   int len = (int)data.size();
494
495   receivedData(0, bytes, len);
496   if (m_aborted)
497     return;
498
499   receivedAllData(0);
500 }
501
502 void XMLHttpRequest::receivedAllData(TransferJob*)
503 {
504     if (m_responseHeaders.isEmpty() && m_job)
505         m_responseHeaders = m_job->queryMetaData("HTTP-Headers");
506
507     if (m_state < Loaded)
508         changeState(Loaded);
509
510     if (m_decoder)
511         m_response += m_decoder->flush();
512
513     bool hadJob = m_job;
514     m_job = 0;
515
516     changeState(Completed);
517     m_decoder = 0;
518
519     if (hadJob) {
520         {
521             KJS::JSLock lock;
522             gcUnprotectNullTolerant(KJS::ScriptInterpreter::getDOMObject(this));
523         }
524         deref();
525     }
526 }
527
528 void XMLHttpRequest::receivedRedirect(TransferJob*, const KURL& m_url)
529 {
530     if (!urlMatchesDocumentDomain(m_url))
531         abort();
532 }
533
534 void XMLHttpRequest::receivedData(TransferJob*, const char *data, int len)
535 {
536     if (m_responseHeaders.isEmpty() && m_job)
537         m_responseHeaders = m_job->queryMetaData("HTTP-Headers");
538
539     if (m_state < Loaded)
540         changeState(Loaded);
541   
542     if (!m_decoder) {
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");
548     
549         m_decoder = new Decoder;
550         if (!m_encoding.isEmpty())
551             m_decoder->setEncodingName(m_encoding.deprecatedString().latin1(), Decoder::EncodingFromHTTPHeader);
552         else
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);
555     }
556     if (len == 0)
557         return;
558
559     if (len == -1)
560         len = strlen(data);
561
562     DeprecatedString decoded = m_decoder->decode(data, len);
563
564     m_response += decoded;
565
566     if (!m_aborted) {
567         if (m_state != Interactive)
568             changeState(Interactive);
569         else
570             // Firefox calls readyStateChanged every time it receives data, 4449442
571             callReadyStateChangeListener();
572     }
573 }
574
575 void XMLHttpRequest::cancelRequests(Document* m_doc)
576 {
577     RequestsSet* requests = requestsByDocument().get(m_doc);
578     if (!requests)
579         return;
580     RequestsSet copy = *requests;
581     RequestsSet::const_iterator end = copy.end();
582     for (RequestsSet::const_iterator it = copy.begin(); it != end; ++it)
583         (*it)->abort();
584 }
585
586 void XMLHttpRequest::detachRequests(Document* m_doc)
587 {
588     RequestsSet* requests = requestsByDocument().get(m_doc);
589     if (!requests)
590         return;
591     requestsByDocument().remove(m_doc);
592     RequestsSet::const_iterator end = requests->end();
593     for (RequestsSet::const_iterator it = requests->begin(); it != end; ++it) {
594         (*it)->m_doc = 0;
595         (*it)->abort();
596     }
597     delete requests;
598 }
599
600 } // end namespace