Reviewed by Darin, Geoff.
[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 "CString.h"
25 #include "Cache.h"
26 #include "DOMImplementation.h"
27 #include "TextResourceDecoder.h"
28 #include "Event.h"
29 #include "EventListener.h"
30 #include "EventNames.h"
31 #include "ExceptionCode.h"
32 #include "FormData.h"
33 #include "HTMLDocument.h"
34 #include "LoaderFunctions.h"
35 #include "PlatformString.h"
36 #include "RegularExpression.h"
37 #include "ResourceHandle.h"
38 #include "ResourceRequest.h"
39 #include "TextEncoding.h"
40 #include "kjs_binding.h"
41 #include <kjs/protect.h>
42 #include <wtf/Vector.h>
43
44 namespace WebCore {
45
46 using namespace EventNames;
47
48 typedef HashSet<XMLHttpRequest*> RequestsSet;
49
50 static HashMap<Document*, RequestsSet*>& requestsByDocument()
51 {
52     static HashMap<Document*, RequestsSet*> map;
53     return map;
54 }
55
56 static void addToRequestsByDocument(Document* doc, XMLHttpRequest* req)
57 {
58     ASSERT(doc);
59     ASSERT(req);
60
61     RequestsSet* requests = requestsByDocument().get(doc);
62     if (!requests) {
63         requests = new RequestsSet;
64         requestsByDocument().set(doc, requests);
65     }
66
67     ASSERT(!requests->contains(req));
68     requests->add(req);
69 }
70
71 static void removeFromRequestsByDocument(Document* doc, XMLHttpRequest* req)
72 {
73     ASSERT(doc);
74     ASSERT(req);
75
76     RequestsSet* requests = requestsByDocument().get(doc);
77     ASSERT(requests);
78     ASSERT(requests->contains(req));
79     requests->remove(req);
80     if (requests->isEmpty()) {
81         requestsByDocument().remove(doc);
82         delete requests;
83     }
84 }
85
86 static inline String getMIMEType(const String& contentTypeString)
87 {
88     String mimeType;
89     unsigned length = contentTypeString.length();
90     for (unsigned offset = 0; offset < length; offset++) {
91         UChar c = contentTypeString[offset];
92         if (c == ';')
93             break;
94         else if (DeprecatedChar(c).isSpace()) // FIXME: This seems wrong, " " is an invalid MIME type character according to RFC 2045.  bug 8644
95             continue;
96         // FIXME: This is a very slow way to build a string, given WebCore::String's implementation.
97         mimeType += String(&c, 1);
98     }
99     return mimeType;
100 }
101
102 static String getCharset(const String& contentTypeString)
103 {
104     int pos = 0;
105     int length = (int)contentTypeString.length();
106     
107     while (pos < length) {
108         pos = contentTypeString.find("charset", pos, false);
109         if (pos <= 0)
110             return String();
111         
112         // is what we found a beginning of a word?
113         if (contentTypeString[pos-1] > ' ' && contentTypeString[pos-1] != ';') {
114             pos += 7;
115             continue;
116         }
117         
118         pos += 7;
119
120         // skip whitespace
121         while (pos != length && contentTypeString[pos] <= ' ')
122             ++pos;
123     
124         if (contentTypeString[pos++] != '=') // this "charset" substring wasn't a parameter name, but there may be others
125             continue;
126
127         while (pos != length && (contentTypeString[pos] <= ' ' || contentTypeString[pos] == '"' || contentTypeString[pos] == '\''))
128             ++pos;
129
130         // we don't handle spaces within quoted parameter values, because charset names cannot have any
131         int endpos = pos;
132         while (pos != length && contentTypeString[endpos] > ' ' && contentTypeString[endpos] != '"' && contentTypeString[endpos] != '\'' && contentTypeString[endpos] != ';')
133             ++endpos;
134     
135         return contentTypeString.substring(pos, endpos-pos);
136     }
137     
138     return String();
139 }
140
141 XMLHttpRequestState XMLHttpRequest::getReadyState() const
142 {
143     return m_state;
144 }
145
146 String XMLHttpRequest::getResponseText() const
147 {
148     return m_responseText;
149 }
150
151 Document* XMLHttpRequest::getResponseXML() const
152 {
153     if (m_state != Loaded)
154         return 0;
155
156     if (!m_createdDocument) {
157         if (responseIsXML()) {
158             m_responseXML = m_doc->implementation()->createDocument(0);
159             m_responseXML->open();
160             m_responseXML->write(m_responseText);
161             m_responseXML->finishParsing();
162             m_responseXML->close();
163             
164             if (!m_responseXML->wellFormed())
165                 m_responseXML = 0;
166         }
167         m_createdDocument = true;
168     }
169
170     return m_responseXML.get();
171 }
172
173 EventListener* XMLHttpRequest::onReadyStateChangeListener() const
174 {
175     return m_onReadyStateChangeListener.get();
176 }
177
178 void XMLHttpRequest::setOnReadyStateChangeListener(EventListener* eventListener)
179 {
180     m_onReadyStateChangeListener = eventListener;
181 }
182
183 EventListener* XMLHttpRequest::onLoadListener() const
184 {
185     return m_onLoadListener.get();
186 }
187
188 void XMLHttpRequest::setOnLoadListener(EventListener* eventListener)
189 {
190     m_onLoadListener = eventListener;
191 }
192
193 XMLHttpRequest::XMLHttpRequest(Document* d)
194     : m_doc(d)
195     , m_async(true)
196     , m_loader(0)
197     , m_state(Uninitialized)
198     , m_responseText("", 0)
199     , m_createdDocument(false)
200     , m_aborted(false)
201 {
202     ASSERT(m_doc);
203     addToRequestsByDocument(m_doc, this);
204 }
205
206 XMLHttpRequest::~XMLHttpRequest()
207 {
208     if (m_doc)
209         removeFromRequestsByDocument(m_doc, this);
210 }
211
212 void XMLHttpRequest::changeState(XMLHttpRequestState newState)
213 {
214     if (m_state != newState) {
215         m_state = newState;
216         callReadyStateChangeListener();
217     }
218 }
219
220 void XMLHttpRequest::callReadyStateChangeListener()
221 {
222     if (m_doc && m_doc->frame() && m_onReadyStateChangeListener)
223         m_onReadyStateChangeListener->handleEvent(new Event(readystatechangeEvent, true, true), true);
224     
225     if (m_doc && m_doc->frame() && m_state == Loaded && m_onLoadListener)
226         m_onLoadListener->handleEvent(new Event(loadEvent, true, true), true);
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" || documentURL.protocol().lower() == "applewebdata")
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, ExceptionCode& ec)
247 {
248     abort();
249     m_aborted = false;
250
251     // clear stuff from possible previous load
252     m_requestHeaders.clear();
253     m_response = ResourceResponse();
254     m_responseText = "";
255     m_createdDocument = false;
256     m_responseXML = 0;
257
258     changeState(Uninitialized);
259
260     if (!urlMatchesDocumentDomain(url)) {
261         ec = PERMISSION_DENIED;
262         return;
263     }
264
265     m_url = url;
266
267     if (!user.isNull())
268         m_url.setUser(user.deprecatedString());
269
270     if (!password.isNull())
271         m_url.setPass(password.deprecatedString());
272
273     // Method names are case sensitive. But since Firefox uppercases method names it knows, we'll do the same.
274     String methodUpper(method.upper());
275     if (methodUpper == "CONNECT" || methodUpper == "COPY" || methodUpper == "DELETE" || methodUpper == "GET" || methodUpper == "HEAD"
276         || methodUpper == "INDEX" || methodUpper == "LOCK" || methodUpper == "M-POST" || methodUpper == "MKCOL" || methodUpper == "MOVE" 
277         || methodUpper == "OPTIONS" || methodUpper == "POST" || methodUpper == "PROPFIND" || methodUpper == "PROPPATCH" || methodUpper == "PUT" 
278         || methodUpper == "TRACE" || methodUpper == "UNLOCK")
279         m_method = methodUpper.deprecatedString();
280     else
281         m_method = method.deprecatedString();
282
283     m_async = async;
284
285     changeState(Open);
286 }
287
288 void XMLHttpRequest::send(const String& body, ExceptionCode& ec)
289 {
290     if (!m_doc)
291         return;
292
293     if (m_state != Open) {
294         ec = INVALID_STATE_ERR;
295         return;
296     }
297   
298     // FIXME: Should this abort or raise an exception instead if we already have a m_loader going?
299     if (m_loader)
300         return;
301
302     m_aborted = false;
303
304     ResourceRequest request(m_url);
305     request.setHTTPMethod(m_method);
306     
307     if (!body.isNull() && m_method != "GET" && m_method != "HEAD" && (m_url.protocol().lower() == "http" || m_url.protocol().lower() == "https")) {
308         String contentType = getRequestHeader("Content-Type");
309         String charset;
310         if (contentType.isEmpty()) {
311             ExceptionCode ec = 0;
312             setRequestHeader("Content-Type", "application/xml", ec);
313             ASSERT(ec == 0);
314         } else
315             charset = getCharset(contentType);
316       
317         if (charset.isEmpty())
318             charset = "UTF-8";
319       
320         TextEncoding m_encoding(charset);
321         if (!m_encoding.isValid()) // FIXME: report an error?
322             m_encoding = UTF8Encoding();
323
324         request.setHTTPBody(PassRefPtr<FormData>(new FormData(m_encoding.encode(body.characters(), body.length()))));
325     }
326
327     if (m_requestHeaders.size() > 0)
328         request.addHTTPHeaderFields(m_requestHeaders);
329
330     if (!m_async) {
331         Vector<char> data;
332         ResourceResponse response;
333
334         {
335             // avoid deadlock in case the loader wants to use JS on a background thread
336             KJS::JSLock::DropAllLocks dropLocks;
337             data = ServeSynchronousRequest(cache()->loader(), m_doc->docLoader(), request, response);
338         }
339
340         m_loader = 0;
341         processSyncLoadResults(data, response);
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     // create can return null here, for example if we're no longer attached to a page.
356     // this is true while running onunload handlers
357     // FIXME: Maybe create can return false for other reasons too?
358     m_loader = ResourceHandle::create(request, this, m_doc->docLoader());
359 }
360
361 void XMLHttpRequest::abort()
362 {
363     bool hadLoader = m_loader;
364
365     if (hadLoader)
366         m_loader = 0;
367
368     m_decoder = 0;
369     m_aborted = true;
370
371     if (hadLoader) {
372         {
373             KJS::JSLock lock;
374             gcUnprotectNullTolerant(KJS::ScriptInterpreter::getDOMObject(this));
375         }
376         deref();
377     }
378 }
379
380 void XMLHttpRequest::overrideMIMEType(const String& override)
381 {
382     m_mimeTypeOverride = override;
383 }
384
385 void XMLHttpRequest::setRequestHeader(const String& name, const String& value, ExceptionCode& ec)
386 {
387     if (m_state != Open)
388         // rdar 4758577: XHR spec says an exception should be thrown here.  However, doing so breaks the Business and People widgets.
389         return;
390
391     if (!m_requestHeaders.contains(name)) {
392         m_requestHeaders.set(name, value);
393         return;
394     }
395     
396     String oldValue = m_requestHeaders.get(name);
397     m_requestHeaders.set(name, oldValue + ", " + value);
398 }
399
400 String XMLHttpRequest::getRequestHeader(const String& name) const
401 {
402     return m_requestHeaders.get(name);
403 }
404
405 String XMLHttpRequest::getAllResponseHeaders() const
406 {
407     Vector<UChar> stringBuilder;
408     String separator(": ");
409
410     HTTPHeaderMap::const_iterator end = m_response.httpHeaderFields().end();
411     for (HTTPHeaderMap::const_iterator it = m_response.httpHeaderFields().begin(); it!= end; ++it) {
412         stringBuilder.append(it->first.characters(), it->first.length());
413         stringBuilder.append(separator.characters(), separator.length());
414         stringBuilder.append(it->second.characters(), it->second.length());
415         stringBuilder.append((UChar)'\n');
416     }
417
418     return String::adopt(stringBuilder);
419 }
420
421 String XMLHttpRequest::getResponseHeader(const String& name) const
422 {
423     return m_response.httpHeaderField(name);
424 }
425
426 bool XMLHttpRequest::responseIsXML() const
427 {
428     String mimeType = getMIMEType(m_mimeTypeOverride);
429     if (mimeType.isEmpty())
430         mimeType = getMIMEType(getResponseHeader("Content-Type"));
431     if (mimeType.isEmpty())
432         mimeType = "text/xml";
433     return DOMImplementation::isXMLMIMEType(mimeType);
434 }
435
436 int XMLHttpRequest::getStatus(ExceptionCode& ec) const
437 {
438     if (m_state == Uninitialized)
439         return 0;
440     
441     if (m_response.httpStatusCode() == 0) {
442         if (m_state != Receiving && m_state != Loaded)
443             // status MUST be available in these states, but we don't get any headers from non-HTTP requests
444             ec = INVALID_STATE_ERR;
445     }
446
447     return m_response.httpStatusCode();
448 }
449
450 String XMLHttpRequest::getStatusText(ExceptionCode& ec) const
451 {
452     if (m_state == Uninitialized)
453         return "";
454     
455     if (m_response.httpStatusCode() == 0) {
456         if (m_state != Receiving && m_state != Loaded)
457             // statusText MUST be available in these states, but we don't get any headers from non-HTTP requests
458             ec = INVALID_STATE_ERR;
459         return String();
460     }
461
462     // FIXME: should try to preserve status text in response
463     return "OK";
464 }
465
466 void XMLHttpRequest::processSyncLoadResults(const Vector<char>& data, const ResourceResponse& response)
467 {
468     if (!urlMatchesDocumentDomain(response.url())) {
469         abort();
470         return;
471     }
472
473     didReceiveResponse(0, response);
474     changeState(Sent);
475     if (m_aborted)
476         return;
477
478     const char* bytes = static_cast<const char*>(data.data());
479     int len = static_cast<int>(data.size());
480
481     didReceiveData(0, bytes, len);
482     if (m_aborted)
483         return;
484
485     didFinishLoading(0);
486 }
487
488 void XMLHttpRequest::didFailWithError(ResourceHandle* handle, const ResourceError&)
489 {
490     didFinishLoading(handle);
491 }
492
493 void XMLHttpRequest::didFinishLoading(ResourceHandle* handle)
494 {
495     if (m_aborted)
496         return;
497         
498     ASSERT(handle == m_loader.get());
499
500     if (m_state < Sent)
501         changeState(Sent);
502
503     if (m_decoder)
504         m_responseText += m_decoder->flush();
505
506     bool hadLoader = m_loader;
507     m_loader = 0;
508
509     changeState(Loaded);
510     m_decoder = 0;
511
512     if (hadLoader) {
513         {
514             KJS::JSLock lock;
515             gcUnprotectNullTolerant(KJS::ScriptInterpreter::getDOMObject(this));
516         }
517         deref();
518     }
519 }
520
521 void XMLHttpRequest::willSendRequest(ResourceHandle*, ResourceRequest& request, const ResourceResponse& redirectResponse)
522 {
523     if (!urlMatchesDocumentDomain(request.url()))
524         abort();
525 }
526
527 void XMLHttpRequest::didReceiveResponse(ResourceHandle*, const ResourceResponse& response)
528 {
529     m_response = response;
530     m_encoding = getCharset(m_mimeTypeOverride);
531     if (m_encoding.isEmpty())
532         m_encoding = response.textEncodingName();
533
534 }
535
536 void XMLHttpRequest::didReceiveData(ResourceHandle*, const char* data, int len)
537 {
538     if (m_state < Sent)
539         changeState(Sent);
540   
541     if (!m_decoder) {
542         if (!m_encoding.isEmpty())
543             m_decoder = new TextResourceDecoder("text/plain", m_encoding);
544         else if (responseIsXML())
545             // allow TextResourceDecoder to look inside the m_response if it's XML
546             m_decoder = new TextResourceDecoder("application/xml");
547         else
548             m_decoder = new TextResourceDecoder("text/plain", "UTF-8");
549     }
550     if (len == 0)
551         return;
552
553     if (len == -1)
554         len = strlen(data);
555
556     String decoded = m_decoder->decode(data, len);
557
558     m_responseText += decoded;
559
560     if (!m_aborted) {
561         if (m_state != Receiving)
562             changeState(Receiving);
563         else
564             // Firefox calls readyStateChanged every time it receives data, 4449442
565             callReadyStateChangeListener();
566     }
567 }
568
569 void XMLHttpRequest::cancelRequests(Document* m_doc)
570 {
571     RequestsSet* requests = requestsByDocument().get(m_doc);
572     if (!requests)
573         return;
574     RequestsSet copy = *requests;
575     RequestsSet::const_iterator end = copy.end();
576     for (RequestsSet::const_iterator it = copy.begin(); it != end; ++it)
577         (*it)->abort();
578 }
579
580 void XMLHttpRequest::detachRequests(Document* m_doc)
581 {
582     RequestsSet* requests = requestsByDocument().get(m_doc);
583     if (!requests)
584         return;
585     requestsByDocument().remove(m_doc);
586     RequestsSet::const_iterator end = requests->end();
587     for (RequestsSet::const_iterator it = requests->begin(); it != end; ++it) {
588         (*it)->m_doc = 0;
589         (*it)->abort();
590     }
591     delete requests;
592 }
593
594 } // end namespace