Reviewed by Darin.
[WebKit-https.git] / WebCore / khtml / ecma / xmlhttprequest.cpp
1 // -*- c-basic-offset: 2 -*-
2 /*
3  *  This file is part of the KDE libraries
4  *  Copyright (C) 2003 Apple Computer, Inc.
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 "xmlhttprequest.h"
22 #include "xmlhttprequest.lut.h"
23 #include "kjs_window.h"
24 #include "kjs_events.h"
25
26 #include "dom/dom_doc.h"
27 #include "dom/dom_exception.h"
28 #include "dom/dom_string.h"
29 #include "misc/loader.h"
30 #include "html/html_documentimpl.h"
31 #include "xml/dom2_eventsimpl.h"
32
33 #include "khtml_part.h"
34 #include "khtmlview.h"
35
36 #include <kdebug.h>
37 #include <kio/job.h>
38 #include <qobject.h>
39
40 #ifdef APPLE_CHANGES
41 #include "KWQLoader.h"
42 #endif
43
44 using namespace KJS;
45 using khtml::Decoder;
46
47 ////////////////////// XMLHttpRequest Object ////////////////////////
48
49 /* Source for XMLHttpRequestProtoTable.
50 @begin XMLHttpRequestProtoTable 7
51   abort                 XMLHttpRequest::Abort                   DontDelete|Function 0
52   getAllResponseHeaders XMLHttpRequest::GetAllResponseHeaders   DontDelete|Function 0
53   getResponseHeader     XMLHttpRequest::GetResponseHeader       DontDelete|Function 1
54   open                  XMLHttpRequest::Open                    DontDelete|Function 5
55   overrideMimeType      XMLHttpRequest::OverrideMIMEType        DontDelete|Function 1
56   send                  XMLHttpRequest::Send                    DontDelete|Function 1
57   setRequestHeader      XMLHttpRequest::SetRequestHeader        DontDelete|Function 2
58 @end
59 */
60 DEFINE_PROTOTYPE("XMLHttpRequest",XMLHttpRequestProto)
61 IMPLEMENT_PROTOFUNC(XMLHttpRequestProtoFunc)
62 IMPLEMENT_PROTOTYPE(XMLHttpRequestProto,XMLHttpRequestProtoFunc)
63
64 namespace KJS {
65
66 XMLHttpRequestQObject::XMLHttpRequestQObject(XMLHttpRequest *_jsObject) 
67 {
68   jsObject = _jsObject; 
69 }
70
71 #if APPLE_CHANGES
72 void XMLHttpRequestQObject::slotData( KIO::Job* job, const char *data, int size )
73 {
74   jsObject->slotData(job, data, size);
75 }
76 #else
77 void XMLHttpRequestQObject::slotData( KIO::Job* job, const QByteArray &data )
78 {
79   jsObject->slotData(job, data);
80 }
81 #endif
82
83 void XMLHttpRequestQObject::slotFinished( KIO::Job* job )
84 {
85   jsObject->slotFinished(job); 
86 }
87
88 void XMLHttpRequestQObject::slotRedirection( KIO::Job* job, const KURL& url)
89
90   jsObject->slotRedirection( job, url ); 
91 }
92
93 XMLHttpRequestConstructorImp::XMLHttpRequestConstructorImp(ExecState *, const DOM::Document &d)
94     : doc(d)
95 {
96 }
97
98 bool XMLHttpRequestConstructorImp::implementsConstruct() const
99 {
100   return true;
101 }
102
103 Object XMLHttpRequestConstructorImp::construct(ExecState *exec, const List &)
104 {
105   return Object(new XMLHttpRequest(exec, doc));
106 }
107
108 const ClassInfo XMLHttpRequest::info = { "XMLHttpRequest", 0, &XMLHttpRequestTable, 0 };
109
110 /* Source for XMLHttpRequestTable.
111 @begin XMLHttpRequestTable 7
112   readyState            XMLHttpRequest::ReadyState              DontDelete|ReadOnly
113   responseText          XMLHttpRequest::ResponseText            DontDelete|ReadOnly
114   responseXML           XMLHttpRequest::ResponseXML             DontDelete|ReadOnly
115   status                XMLHttpRequest::Status                  DontDelete|ReadOnly
116   statusText            XMLHttpRequest::StatusText              DontDelete|ReadOnly
117   onreadystatechange    XMLHttpRequest::Onreadystatechange      DontDelete
118   onload                XMLHttpRequest::Onload                  DontDelete
119 @end
120 */
121
122 Value XMLHttpRequest::tryGet(ExecState *exec, const Identifier &propertyName) const
123 {
124   return DOMObjectLookupGetValue<XMLHttpRequest,DOMObject>(exec, propertyName, &XMLHttpRequestTable, this);
125 }
126
127 Value XMLHttpRequest::getValueProperty(ExecState *exec, int token) const
128 {
129   switch (token) {
130   case ReadyState:
131     return Number(state);
132   case ResponseText:
133     return getStringOrNull(DOM::DOMString(response));
134   case ResponseXML:
135     if (state != Completed) {
136       return Undefined();
137     }
138     if (!createdDocument) {
139       QString mimeType;
140       
141       if (MIMETypeOverride.isEmpty()) {
142         Value header = getResponseHeader("Content-Type");
143         if (header.type() == UndefinedType) {
144           mimeType = "text/xml";
145         } else {
146           mimeType = QStringList::split(";", header.toString(exec).qstring())[0].stripWhiteSpace();
147         }
148       } else {
149         mimeType = MIMETypeOverride;
150       }
151       
152       if (mimeType == "text/xml" || mimeType == "application/xml" || mimeType == "application/xhtml+xml" ||
153           mimeType == "text/xsl") {
154         responseXML = DOM::Document(doc->implementation()->createDocument());
155
156         DOM::DocumentImpl *docImpl = static_cast<DOM::DocumentImpl *>(responseXML.handle());
157         
158         docImpl->open();
159         docImpl->write(response);
160         docImpl->finishParsing();
161         docImpl->close();
162         typeIsXML = true;
163       } else {
164         typeIsXML = false;
165       }
166       createdDocument = true;
167     }
168
169     if (!typeIsXML) {
170       return Undefined();
171     }
172
173     return getDOMNode(exec,responseXML);
174   case Status:
175     return getStatus();
176   case StatusText:
177     return getStatusText();
178   case Onreadystatechange:
179    if (onReadyStateChangeListener && onReadyStateChangeListener->listenerObjImp()) {
180      return onReadyStateChangeListener->listenerObj();
181    } else {
182      return Null();
183    }
184   case Onload:
185    if (onLoadListener && onLoadListener->listenerObjImp()) {
186      return onLoadListener->listenerObj();
187    } else {
188      return Null();
189    }
190   default:
191     kdWarning() << "XMLHttpRequest::getValueProperty unhandled token " << token << endl;
192     return Value();
193   }
194 }
195
196 void XMLHttpRequest::tryPut(ExecState *exec, const Identifier &propertyName, const Value& value, int attr)
197 {
198   DOMObjectLookupPut<XMLHttpRequest,DOMObject>(exec, propertyName, value, attr, &XMLHttpRequestTable, this );
199 }
200
201 void XMLHttpRequest::putValue(ExecState *exec, int token, const Value& value, int /*attr*/)
202 {
203   switch(token) {
204   case Onreadystatechange:
205     onReadyStateChangeListener = Window::retrieveActive(exec)->getJSEventListener(value, true);
206     if (onReadyStateChangeListener) onReadyStateChangeListener->ref();
207     break;
208   case Onload:
209     onLoadListener = Window::retrieveActive(exec)->getJSEventListener(value, true);
210     if (onLoadListener) onLoadListener->ref();
211     break;
212   default:
213     kdWarning() << "HTMLDocument::putValue unhandled token " << token << endl;
214   }
215 }
216
217 XMLHttpRequest::XMLHttpRequest(ExecState *exec, const DOM::Document &d)
218   : DOMObject(XMLHttpRequestProto::self(exec)),
219     qObject(new XMLHttpRequestQObject(this)),
220     doc(static_cast<DOM::DocumentImpl*>(d.handle())),
221     async(true),
222     job(0),
223     state(Uninitialized),
224     onReadyStateChangeListener(0),
225     onLoadListener(0),
226     decoder(0),
227     createdDocument(false),
228     aborted(false)
229 {
230 }
231
232 XMLHttpRequest::~XMLHttpRequest()
233 {
234   delete qObject;
235   if (decoder) {
236     decoder->deref();
237   }
238 }
239
240 void XMLHttpRequest::changeState(XMLHttpRequestState newState)
241 {
242   if (state != newState) {
243     state = newState;
244     
245     if (onReadyStateChangeListener != 0 && doc->part()) {
246       DOM::Event ev = doc->part()->document().createEvent("HTMLEvents");
247       ev.initEvent("readystatechange", true, true);
248       onReadyStateChangeListener->handleEvent(ev, true);
249     }
250     
251     if (state == Completed && onLoadListener != 0 && doc->part()) {
252       DOM::Event ev = doc->part()->document().createEvent("HTMLEvents");
253       ev.initEvent("load", true, true);
254       onLoadListener->handleEvent(ev, true);
255     }
256   }
257 }
258
259 bool XMLHttpRequest::urlMatchesDocumentDomain(const KURL& _url) const
260 {
261   KURL documentURL(doc->URL());
262
263   // a local file can load anything
264   if (documentURL.protocol().lower() == "file") {
265     return true;
266   }
267
268   // but a remote document can only load from the same port on the server
269   if (documentURL.protocol().lower() == _url.protocol().lower() &&
270       documentURL.host().lower() == _url.host().lower() &&
271       documentURL.port() == _url.port()) {
272     return true;
273   }
274
275   return false;
276 }
277
278 void XMLHttpRequest::open(const QString& _method, const KURL& _url, bool _async)
279 {
280   abort();
281   aborted = false;
282
283   // clear stuff from possible previous load
284   requestHeaders = QString();
285   responseHeaders = QString();
286   response = QString();
287   createdDocument = false;
288   responseXML = DOM::Document();
289
290   changeState(Uninitialized);
291
292   if (aborted) {
293     return;
294   }
295
296   if (!urlMatchesDocumentDomain(_url)) {
297     return;
298   }
299
300   
301   method = _method;
302   url = _url;
303   async = _async;
304
305   changeState(Loading);
306 }
307
308 void XMLHttpRequest::send(const QString& _body)
309 {
310   aborted = false;
311
312 #if !APPLE_CHANGES
313   if (!async) {
314     return;
315   }
316 #endif
317
318   if (method.lower() == "post" && (url.protocol().lower() == "http" || url.protocol().lower() == "https") ) {
319       // FIXME: determine post encoding correctly by looking in headers for charset
320       job = KIO::http_post( url, QCString(_body.utf8()), false );
321   }
322   else
323   {
324      job = KIO::get( url, false, false );
325   }
326   if (requestHeaders.length() > 0) {
327     job->addMetaData("customHTTPHeader", requestHeaders);
328   }
329
330 #if APPLE_CHANGES
331   if (!async) {
332     QByteArray data;
333     KURL finalURL;
334     QString headers;
335
336     data = KWQServeSynchronousRequest(khtml::Cache::loader(), doc->docLoader(), job, finalURL, headers);
337     job = 0;
338     processSyncLoadResults(data, finalURL, headers);
339     return;
340   }
341 #endif
342
343   qObject->connect( job, SIGNAL( result( KIO::Job* ) ),
344                     SLOT( slotFinished( KIO::Job* ) ) );
345 #if APPLE_CHANGES
346   qObject->connect( job, SIGNAL( data( KIO::Job*, const char*, int ) ),
347                     SLOT( slotData( KIO::Job*, const char*, int ) ) );
348 #else
349   qObject->connect( job, SIGNAL( data( KIO::Job*, const QByteArray& ) ),
350                     SLOT( slotData( KIO::Job*, const QByteArray& ) ) );
351 #endif
352   qObject->connect( job, SIGNAL(redirection(KIO::Job*, const KURL& ) ),
353                     SLOT( slotRedirection(KIO::Job*, const KURL&) ) );
354
355 #ifdef APPLE_CHANGES
356   KWQServeRequest(khtml::Cache::loader(), doc->docLoader(), job);
357 #else 
358   KIO::Scheduler::scheduleJob( job );
359 #endif
360 }
361
362 void XMLHttpRequest::abort()
363 {
364   if (job) {
365     job->kill();
366     job = 0;
367   }
368   if (decoder) {
369     decoder->deref();
370     decoder = 0;
371   }
372   aborted = true;
373 }
374
375 void XMLHttpRequest::setRequestHeader(const QString& name, const QString &value)
376 {
377   if (requestHeaders.length() > 0) {
378     requestHeaders += "\r\n";
379   }
380   requestHeaders += name;
381   requestHeaders += ": ";
382   requestHeaders += value;
383 }
384
385 Value XMLHttpRequest::getAllResponseHeaders() const
386 {
387   if (responseHeaders.isEmpty()) {
388     return Undefined();
389   }
390
391   int endOfLine = responseHeaders.find("\n");
392
393   if (endOfLine == -1) {
394     return Undefined();
395   }
396
397   return String(responseHeaders.mid(endOfLine + 1) + "\n");
398 }
399
400 Value XMLHttpRequest::getResponseHeader(const QString& name) const
401 {
402   if (responseHeaders.isEmpty()) {
403     return Undefined();
404   }
405
406   QRegExp headerLinePattern(name + ":", false);
407
408   int matchLength;
409   int headerLinePos = headerLinePattern.match(responseHeaders, 0, &matchLength);
410   while (headerLinePos != -1) {
411     if (headerLinePos == 0 || responseHeaders[headerLinePos-1] == '\n') {
412       break;
413     }
414     
415     headerLinePos = headerLinePattern.match(responseHeaders, headerLinePos + 1, &matchLength);
416   }
417
418
419   if (headerLinePos == -1) {
420     return Undefined();
421   }
422     
423   int endOfLine = responseHeaders.find("\n", headerLinePos + matchLength);
424
425   return String(responseHeaders.mid(headerLinePos + matchLength, endOfLine - (headerLinePos + matchLength)).stripWhiteSpace());
426 }
427
428 Value XMLHttpRequest::getStatus() const
429 {
430   if (responseHeaders.isEmpty()) {
431     return Undefined();
432   }
433   
434   int endOfLine = responseHeaders.find("\n");
435   QString firstLine = endOfLine == -1 ? responseHeaders : responseHeaders.left(endOfLine);
436   int codeStart = firstLine.find(" ");
437   int codeEnd = firstLine.find(" ", codeStart + 1);
438   
439   if (codeStart == -1 || codeEnd == -1) {
440     return Undefined();
441   }
442   
443   QString number = firstLine.mid(codeStart + 1, codeEnd - (codeStart + 1));
444   
445   bool ok = false;
446   int code = number.toInt(&ok);
447   if (!ok) {
448     return Undefined();
449   }
450
451   return Number(code);
452 }
453
454 Value XMLHttpRequest::getStatusText() const
455 {
456   if (responseHeaders.isEmpty()) {
457     return Undefined();
458   }
459   
460   int endOfLine = responseHeaders.find("\n");
461   QString firstLine = endOfLine == -1 ? responseHeaders : responseHeaders.left(endOfLine);
462   int codeStart = firstLine.find(" ");
463   int codeEnd = firstLine.find(" ", codeStart + 1);
464
465   if (codeStart == -1 || codeEnd == -1) {
466     return Undefined();
467   }
468   
469   QString statusText = firstLine.mid(codeEnd + 1, endOfLine - (codeEnd + 1)).stripWhiteSpace();
470   
471   return String(statusText);
472 }
473
474 #if APPLE_CHANGES   
475 void XMLHttpRequest::processSyncLoadResults(const QByteArray &data, const KURL &finalURL, const QString &headers)
476 {
477   if (!urlMatchesDocumentDomain(finalURL)) {
478     abort();
479     return;
480   }
481   
482   responseHeaders = headers;
483   changeState(Loaded);
484   if (aborted) {
485     return;
486   }
487   
488   const char *bytes = (const char *)data.data();
489   int len = (int)data.size();
490
491   slotData(0, bytes, len);
492
493   if (aborted) {
494     return;
495   }
496
497   slotFinished(0);
498 }
499 #endif
500
501 void XMLHttpRequest::slotFinished(KIO::Job *)
502 {
503   if (decoder) {
504     response += decoder->flush();
505   }
506
507   changeState(Completed);
508   job = 0;
509   
510   if (decoder) {
511     decoder->deref();
512     decoder = 0;
513   }
514 }
515
516 void XMLHttpRequest::slotRedirection(KIO::Job*, const KURL& url)
517 {
518   if (!urlMatchesDocumentDomain(url)) {
519     abort();
520   }
521 }
522
523 #if APPLE_CHANGES
524 void XMLHttpRequest::slotData( KIO::Job*, const char *data, int len )
525 #else
526 void XMLHttpRequest::slotData(KIO::Job*, const QByteArray &_data)
527 #endif
528 {
529   if (state < Loaded) {
530     responseHeaders = job->queryMetaData("HTTP-Headers");
531     encoding = job->queryMetaData("charset");
532     changeState(Loaded);
533   }
534   
535 #if !APPLE_CHANGES
536   const char *data = (const char *)_data.data();
537   int len = (int)_data.size();
538 #endif
539
540   if ( decoder == NULL ) {
541     decoder = new Decoder;
542     if (!encoding.isNull())
543       decoder->setEncoding(encoding.latin1(), Decoder::EncodingFromHTTPHeader);
544     else {
545       // FIXME: Inherit the default encoding from the parent document?
546     }
547   }
548   if (len == 0)
549     return;
550
551   if (len == -1)
552     len = strlen(data);
553
554   QString decoded = decoder->decode(data, len);
555
556   response += decoded;
557
558   if (!aborted) {
559     changeState(Interactive);
560   }
561 }
562
563 Value XMLHttpRequestProtoFunc::tryCall(ExecState *exec, Object &thisObj, const List &args)
564 {
565   if (!thisObj.inherits(&XMLHttpRequest::info)) {
566     Object err = Error::create(exec,TypeError);
567     exec->setException(err);
568     return err;
569   }
570
571   XMLHttpRequest *request = static_cast<XMLHttpRequest *>(thisObj.imp());
572
573   switch (id) {
574   case XMLHttpRequest::Abort:
575     request->abort();
576     return Undefined();
577   case XMLHttpRequest::GetAllResponseHeaders:
578     if (args.size() != 0) {
579       return Undefined();
580     }
581
582     return request->getAllResponseHeaders();
583   case XMLHttpRequest::GetResponseHeader:
584     if (args.size() != 1) {
585       return Undefined();
586     }
587
588     return request->getResponseHeader(args[0].toString(exec).qstring());
589   case XMLHttpRequest::Open: 
590     {
591       if (args.size() < 2 || args.size() > 5) {
592         return Undefined();
593       }
594     
595       QString method = args[0].toString(exec).qstring();
596       KURL url = KURL(Window::retrieveActive(exec)->part()->document().completeURL(args[1].toString(exec).qstring()).string());
597
598       bool async = true;
599       if (args.size() >= 3) {
600         async = args[2].toBoolean(exec);
601       }
602     
603       if (args.size() >= 4) {
604         url.setUser(args[3].toString(exec).qstring());
605       }
606       
607       if (args.size() >= 5) {
608         url.setPass(args[4].toString(exec).qstring());
609       }
610
611       request->open(method, url, async);
612
613       return Undefined();
614     }
615   case XMLHttpRequest::Send:
616     {
617       if (args.size() > 1) {
618         return Undefined();
619       }
620
621       if (request->state != Loading) {
622         return Undefined();
623       }
624
625       QString body;
626
627       if (args.size() >= 1) {
628         if (args[0].toObject(exec).inherits(&DOMDocument::info)) {
629           DOM::Node docNode = static_cast<KJS::DOMDocument *>(args[0].toObject(exec).imp())->toNode();
630           DOM::DocumentImpl *doc = static_cast<DOM::DocumentImpl *>(docNode.handle());
631           
632           try {
633             body = doc->toString().string();
634             // FIXME: also need to set content type, including encoding!
635
636           } catch(DOM::DOMException& e) {
637              Object err = Error::create(exec, GeneralError, "Exception serializing document");
638              exec->setException(err);
639           }
640         } else {
641           // converting certain values (like null) to object can set an exception
642           exec->clearException();
643           body = args[0].toString(exec).qstring();
644         }
645       }
646
647       request->send(body);
648
649       return Undefined();
650     }
651   case XMLHttpRequest::SetRequestHeader:
652     if (args.size() != 2) {
653       return Undefined();
654     }
655     
656     request->setRequestHeader(args[0].toString(exec).qstring(), args[1].toString(exec).qstring());
657     
658     return Undefined();
659   case XMLHttpRequest::OverrideMIMEType:
660     if (args.size() != 1) {
661       return Undefined();
662     }
663     request->MIMETypeOverride = args[0].toString(exec).qstring();
664     return Undefined();
665   }
666
667   return Undefined();
668 }
669
670 }; // end namespace