diff options
Diffstat (limited to 'khtml/ecma/xmlhttprequest.cpp')
-rw-r--r-- | khtml/ecma/xmlhttprequest.cpp | 810 |
1 files changed, 810 insertions, 0 deletions
diff --git a/khtml/ecma/xmlhttprequest.cpp b/khtml/ecma/xmlhttprequest.cpp new file mode 100644 index 000000000..de77f3d7e --- /dev/null +++ b/khtml/ecma/xmlhttprequest.cpp @@ -0,0 +1,810 @@ +// -*- c-basic-offset: 2 -*- +/* + * This file is part of the KDE libraries + * Copyright (C) 2003 Apple Computer, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "xmlhttprequest.h" +#include "xmlhttprequest.lut.h" +#include "kjs_window.h" +#include "kjs_events.h" + +#include "dom/dom_doc.h" +#include "dom/dom_exception.h" +#include "dom/dom_string.h" +#include "misc/loader.h" +#include "html/html_documentimpl.h" +#include "xml/dom2_eventsimpl.h" + +#include "khtml_part.h" +#include "khtmlview.h" + +#include <kio/scheduler.h> +#include <kio/job.h> +#include <qobject.h> +#include <kdebug.h> + +#ifdef APPLE_CHANGES +#include "KWQLoader.h" +#else +#include <kio/netaccess.h> +using KIO::NetAccess; +#endif + +#define BANNED_HTTP_HEADERS "authorization,proxy-authorization,"\ + "content-length,host,connect,copy,move,"\ + "delete,head,trace,put,propfind,proppatch,"\ + "mkcol,lock,unlock,options,via,"\ + "accept-charset,accept-encoding,expect,date,"\ + "keep-alive,te,trailer,"\ + "transfer-encoding,upgrade" + +using khtml::Decoder; + +namespace KJS { + +////////////////////// XMLHttpRequest Object //////////////////////// + +/* Source for XMLHttpRequestProtoTable. +@begin XMLHttpRequestProtoTable 7 + abort XMLHttpRequest::Abort DontDelete|Function 0 + getAllResponseHeaders XMLHttpRequest::GetAllResponseHeaders DontDelete|Function 0 + getResponseHeader XMLHttpRequest::GetResponseHeader DontDelete|Function 1 + open XMLHttpRequest::Open DontDelete|Function 5 + overrideMimeType XMLHttpRequest::OverrideMIMEType DontDelete|Function 1 + send XMLHttpRequest::Send DontDelete|Function 1 + setRequestHeader XMLHttpRequest::SetRequestHeader DontDelete|Function 2 +@end +*/ +KJS_DEFINE_PROTOTYPE(XMLHttpRequestProto) +IMPLEMENT_PROTOFUNC_DOM(XMLHttpRequestProtoFunc) +KJS_IMPLEMENT_PROTOTYPE("XMLHttpRequest", XMLHttpRequestProto,XMLHttpRequestProtoFunc) + + +XMLHttpRequestQObject::XMLHttpRequestQObject(XMLHttpRequest *_jsObject) +{ + jsObject = _jsObject; +} + +#ifdef APPLE_CHANGES +void XMLHttpRequestQObject::slotData( KIO::Job* job, const char *data, int size ) +{ + jsObject->slotData(job, data, size); +} +#else +void XMLHttpRequestQObject::slotData( KIO::Job* job, const QByteArray &data ) +{ + jsObject->slotData(job, data); +} +#endif + +void XMLHttpRequestQObject::slotFinished( KIO::Job* job ) +{ + jsObject->slotFinished(job); +} + +void XMLHttpRequestQObject::slotRedirection( KIO::Job* job, const KURL& url) +{ + jsObject->slotRedirection( job, url ); +} + +XMLHttpRequestConstructorImp::XMLHttpRequestConstructorImp(ExecState *, const DOM::Document &d) + : ObjectImp(), doc(d) +{ +} + +bool XMLHttpRequestConstructorImp::implementsConstruct() const +{ + return true; +} + +Object XMLHttpRequestConstructorImp::construct(ExecState *exec, const List &) +{ + return Object(new XMLHttpRequest(exec, doc)); +} + +const ClassInfo XMLHttpRequest::info = { "XMLHttpRequest", 0, &XMLHttpRequestTable, 0 }; + + +/* Source for XMLHttpRequestTable. +@begin XMLHttpRequestTable 7 + readyState XMLHttpRequest::ReadyState DontDelete|ReadOnly + responseText XMLHttpRequest::ResponseText DontDelete|ReadOnly + responseXML XMLHttpRequest::ResponseXML DontDelete|ReadOnly + status XMLHttpRequest::Status DontDelete|ReadOnly + statusText XMLHttpRequest::StatusText DontDelete|ReadOnly + onreadystatechange XMLHttpRequest::Onreadystatechange DontDelete + onload XMLHttpRequest::Onload DontDelete +@end +*/ + +Value XMLHttpRequest::tryGet(ExecState *exec, const Identifier &propertyName) const +{ + return DOMObjectLookupGetValue<XMLHttpRequest,DOMObject>(exec, propertyName, &XMLHttpRequestTable, this); +} + +Value XMLHttpRequest::getValueProperty(ExecState *exec, int token) const +{ + switch (token) { + case ReadyState: + return Number(state); + case ResponseText: + return getString(DOM::DOMString(response)); + case ResponseXML: + if (state != Completed) { + return Null(); + } + if (!createdDocument) { + QString mimeType = "text/xml"; + + if (!m_mimeTypeOverride.isEmpty()) { + mimeType = m_mimeTypeOverride; + } else { + Value header = getResponseHeader("Content-Type"); + if (header.type() != UndefinedType) { + mimeType = QStringList::split(";", header.toString(exec).qstring())[0].stripWhiteSpace(); + } + } + + if (mimeType == "text/xml" || mimeType == "application/xml" || mimeType == "application/xhtml+xml") { + responseXML = DOM::Document(doc->implementation()->createDocument()); + + DOM::DocumentImpl *docImpl = static_cast<DOM::DocumentImpl *>(responseXML.handle()); + + docImpl->open(); + docImpl->write(response); + docImpl->finishParsing(); + docImpl->close(); + + typeIsXML = true; + } else { + typeIsXML = false; + } + createdDocument = true; + } + + if (!typeIsXML) { + return Undefined(); + } + + return getDOMNode(exec,responseXML); + case Status: + return getStatus(); + case StatusText: + return getStatusText(); + case Onreadystatechange: + if (onReadyStateChangeListener && onReadyStateChangeListener->listenerObjImp()) { + return onReadyStateChangeListener->listenerObj(); + } else { + return Null(); + } + case Onload: + if (onLoadListener && onLoadListener->listenerObjImp()) { + return onLoadListener->listenerObj(); + } else { + return Null(); + } + default: + kdWarning() << "XMLHttpRequest::getValueProperty unhandled token " << token << endl; + return Value(); + } +} + +void XMLHttpRequest::tryPut(ExecState *exec, const Identifier &propertyName, const Value& value, int attr) +{ + DOMObjectLookupPut<XMLHttpRequest,DOMObject>(exec, propertyName, value, attr, &XMLHttpRequestTable, this ); +} + +void XMLHttpRequest::putValueProperty(ExecState *exec, int token, const Value& value, int /*attr*/) +{ + JSEventListener* newListener; + switch(token) { + case Onreadystatechange: + newListener = Window::retrieveActive(exec)->getJSEventListener(value, true); + if (newListener != onReadyStateChangeListener) { + if (onReadyStateChangeListener) onReadyStateChangeListener->deref(); + onReadyStateChangeListener = newListener; + if (onReadyStateChangeListener) onReadyStateChangeListener->ref(); + } + break; + case Onload: + newListener = Window::retrieveActive(exec)->getJSEventListener(value, true); + if (newListener != onLoadListener) { + if (onLoadListener) onLoadListener->deref(); + onLoadListener = newListener; + if (onLoadListener) onLoadListener->ref(); + } + break; + default: + kdWarning() << "XMLHttpRequest::putValue unhandled token " << token << endl; + } +} + +XMLHttpRequest::XMLHttpRequest(ExecState *exec, const DOM::Document &d) + : DOMObject(XMLHttpRequestProto::self(exec)), + qObject(new XMLHttpRequestQObject(this)), + doc(static_cast<DOM::DocumentImpl*>(d.handle())), + async(true), + contentType(QString::null), + job(0), + state(Uninitialized), + onReadyStateChangeListener(0), + onLoadListener(0), + decoder(0), + createdDocument(false), + aborted(false) +{ +} + +XMLHttpRequest::~XMLHttpRequest() +{ + if (onReadyStateChangeListener) + onReadyStateChangeListener->deref(); + if (onLoadListener) + onLoadListener->deref(); + delete qObject; + qObject = 0; + delete decoder; + decoder = 0; +} + +void XMLHttpRequest::changeState(XMLHttpRequestState newState) +{ + if (state != newState) { + state = newState; + + ref(); + + if (onReadyStateChangeListener != 0 && doc->view() && doc->view()->part()) { + DOM::Event ev = doc->view()->part()->document().createEvent("HTMLEvents"); + ev.initEvent("readystatechange", true, true); + onReadyStateChangeListener->handleEvent(ev); + } + + if (state == Completed && onLoadListener != 0 && doc->view() && doc->view()->part()) { + DOM::Event ev = doc->view()->part()->document().createEvent("HTMLEvents"); + ev.initEvent("load", true, true); + onLoadListener->handleEvent(ev); + } + + deref(); + } +} + +bool XMLHttpRequest::urlMatchesDocumentDomain(const KURL& _url) const +{ + // No need to do work if _url is not valid... + if (!_url.isValid()) + return false; + + KURL documentURL(doc->URL()); + + // a local file can load anything + if (documentURL.protocol().lower() == "file") { + return true; + } + + // but a remote document can only load from the same port on the server + if (documentURL.protocol().lower() == _url.protocol().lower() && + documentURL.host().lower() == _url.host().lower() && + documentURL.port() == _url.port()) { + return true; + } + + return false; +} + +void XMLHttpRequest::open(const QString& _method, const KURL& _url, bool _async) +{ + abort(); + aborted = false; + + // clear stuff from possible previous load + requestHeaders.clear(); + responseHeaders = QString(); + response = QString(); + createdDocument = false; + responseXML = DOM::Document(); + + changeState(Uninitialized); + + if (aborted) { + return; + } + + if (!urlMatchesDocumentDomain(_url)) { + return; + } + + + method = _method.lower(); + url = _url; + async = _async; + + changeState(Loading); +} + +void XMLHttpRequest::send(const QString& _body) +{ + aborted = false; + + if (method == "post") { + QString protocol = url.protocol().lower(); + + // Abondon the request when the protocol is other than "http", + // instead of blindly changing it to a "get" request. + if (!protocol.startsWith("http") && !protocol.startsWith("webdav")) + { + abort(); + return; + } + + // FIXME: determine post encoding correctly by looking in headers + // for charset. + QByteArray buf; + QCString str = _body.utf8(); + buf.duplicate(str.data(), str.size() - 1); + + job = KIO::http_post( url, buf, false ); + if(contentType.isNull()) + job->addMetaData( "content-type", "Content-type: text/plain" ); + else + job->addMetaData( "content-type", contentType ); + } + else { + job = KIO::get( url, false, false ); + } + + if (!requestHeaders.isEmpty()) { + QString rh; + QMap<QString, QString>::ConstIterator begin = requestHeaders.begin(); + QMap<QString, QString>::ConstIterator end = requestHeaders.end(); + for (QMap<QString, QString>::ConstIterator i = begin; i != end; ++i) { + QString key = i.key(); + QString value = i.data(); + if (key == "accept") { + // The HTTP KIO slave supports an override this way + job->addMetaData("accept", value); + } else { + if (i != begin) + rh += "\r\n"; + rh += key + ": " + value; + } + } + + job->addMetaData("customHTTPHeader", rh); + } + + job->addMetaData("PropagateHttpHeader", "true"); + + // Set the default referrer if one is not already supplied + // through setRequestHeader. NOTE: the user can still disable + // this feature at the protocol level (kio_http). + // ### does find() ever succeed? the headers are stored in lower case! + if (requestHeaders.find("Referer") == requestHeaders.end()) { + KURL documentURL(doc->URL()); + documentURL.setPass(QString::null); + documentURL.setUser(QString::null); + job->addMetaData("referrer", documentURL.url()); + // kdDebug() << "Adding referrer: " << documentURL << endl; + } + + if (!async) { + QByteArray data; + KURL finalURL; + QString headers; + +#ifdef APPLE_CHANGES + data = KWQServeSynchronousRequest(khtml::Cache::loader(), doc->docLoader(), job, finalURL, headers); +#else + QMap<QString, QString> metaData; + if ( NetAccess::synchronousRun( job, 0, &data, &finalURL, &metaData ) ) { + headers = metaData[ "HTTP-Headers" ]; + } +#endif + job = 0; + processSyncLoadResults(data, finalURL, headers); + return; + } + + qObject->connect( job, SIGNAL( result( KIO::Job* ) ), + SLOT( slotFinished( KIO::Job* ) ) ); +#ifdef APPLE_CHANGES + qObject->connect( job, SIGNAL( data( KIO::Job*, const char*, int ) ), + SLOT( slotData( KIO::Job*, const char*, int ) ) ); +#else + qObject->connect( job, SIGNAL( data( KIO::Job*, const QByteArray& ) ), + SLOT( slotData( KIO::Job*, const QByteArray& ) ) ); +#endif + qObject->connect( job, SIGNAL(redirection(KIO::Job*, const KURL& ) ), + SLOT( slotRedirection(KIO::Job*, const KURL&) ) ); + +#ifdef APPLE_CHANGES + KWQServeRequest(khtml::Cache::loader(), doc->docLoader(), job); +#else + KIO::Scheduler::scheduleJob( job ); +#endif +} + +void XMLHttpRequest::abort() +{ + if (job) { + job->kill(); + job = 0; + } + delete decoder; + decoder = 0; + aborted = true; +} + +void XMLHttpRequest::overrideMIMEType(const QString& override) +{ + m_mimeTypeOverride = override; +} + +void XMLHttpRequest::setRequestHeader(const QString& _name, const QString &value) +{ + QString name = _name.lower().stripWhiteSpace(); + + // Content-type needs to be set seperately from the other headers + if(name == "content-type") { + contentType = "Content-type: " + value; + return; + } + + // Sanitize the referrer header to protect against spoofing... + if(name == "referer") { + KURL referrerURL(value); + if (urlMatchesDocumentDomain(referrerURL)) + requestHeaders[name] = referrerURL.url(); + return; + } + + // Sanitize the request headers below and handle them as if they are + // calls to open. Otherwise, we will end up ignoring them all together! + // TODO: Do something about "put" which kio_http sort of supports and + // the webDAV headers such as PROPFIND etc... + if (name == "get" || name == "post") { + KURL reqURL (doc->URL(), value.stripWhiteSpace()); + open(name, reqURL, async); + return; + } + + // Reject all banned headers. See BANNED_HTTP_HEADERS above. + // kdDebug() << "Banned HTTP Headers: " << BANNED_HTTP_HEADERS << endl; + QStringList bannedHeaders = QStringList::split(',', + QString::fromLatin1(BANNED_HTTP_HEADERS)); + + if (bannedHeaders.contains(name)) + return; // Denied + + requestHeaders[name] = value.stripWhiteSpace(); +} + +Value XMLHttpRequest::getAllResponseHeaders() const +{ + if (responseHeaders.isEmpty()) { + return Undefined(); + } + + int endOfLine = responseHeaders.find("\n"); + + if (endOfLine == -1) { + return Undefined(); + } + + return String(responseHeaders.mid(endOfLine + 1) + "\n"); +} + +Value XMLHttpRequest::getResponseHeader(const QString& name) const +{ + if (responseHeaders.isEmpty()) { + return Undefined(); + } + + QRegExp headerLinePattern(name + ":", false); + + int matchLength; + int headerLinePos = headerLinePattern.search(responseHeaders, 0); + matchLength = headerLinePattern.matchedLength(); + while (headerLinePos != -1) { + if (headerLinePos == 0 || responseHeaders[headerLinePos-1] == '\n') { + break; + } + + headerLinePos = headerLinePattern.search(responseHeaders, headerLinePos + 1); + matchLength = headerLinePattern.matchedLength(); + } + + + if (headerLinePos == -1) { + return Undefined(); + } + + int endOfLine = responseHeaders.find("\n", headerLinePos + matchLength); + + return String(responseHeaders.mid(headerLinePos + matchLength, endOfLine - (headerLinePos + matchLength)).stripWhiteSpace()); +} + +static Value httpStatus(const QString& response, bool textStatus = false) +{ + if (response.isEmpty()) { + return Undefined(); + } + + int endOfLine = response.find("\n"); + QString firstLine = (endOfLine == -1) ? response : response.left(endOfLine); + int codeStart = firstLine.find(" "); + int codeEnd = firstLine.find(" ", codeStart + 1); + + if (codeStart == -1 || codeEnd == -1) { + return Undefined(); + } + + if (textStatus) { + QString statusText = firstLine.mid(codeEnd + 1, endOfLine - (codeEnd + 1)).stripWhiteSpace(); + return String(statusText); + } + + QString number = firstLine.mid(codeStart + 1, codeEnd - (codeStart + 1)); + + bool ok = false; + int code = number.toInt(&ok); + if (!ok) { + return Undefined(); + } + + return Number(code); +} + +Value XMLHttpRequest::getStatus() const +{ + return httpStatus(responseHeaders); +} + +Value XMLHttpRequest::getStatusText() const +{ + return httpStatus(responseHeaders, true); +} + +void XMLHttpRequest::processSyncLoadResults(const QByteArray &data, const KURL &finalURL, const QString &headers) +{ + if (!urlMatchesDocumentDomain(finalURL)) { + abort(); + return; + } + + responseHeaders = headers; + changeState(Loaded); + if (aborted) { + return; + } + +#ifdef APPLE_CHANGES + const char *bytes = (const char *)data.data(); + int len = (int)data.size(); + + slotData(0, bytes, len); +#else + slotData(0, data); +#endif + + if (aborted) { + return; + } + + slotFinished(0); +} + +void XMLHttpRequest::slotFinished(KIO::Job *) +{ + if (decoder) { + response += decoder->flush(); + } + + // make sure to forget about the job before emitting completed, + // since changeState triggers JS code, which might e.g. call abort. + job = 0; + changeState(Completed); + + delete decoder; + decoder = 0; +} + +void XMLHttpRequest::slotRedirection(KIO::Job*, const KURL& url) +{ + if (!urlMatchesDocumentDomain(url)) { + abort(); + } +} + +#ifdef APPLE_CHANGES +void XMLHttpRequest::slotData( KIO::Job*, const char *data, int len ) +#else +void XMLHttpRequest::slotData(KIO::Job*, const QByteArray &_data) +#endif +{ + if (state < Loaded ) { + responseHeaders = job->queryMetaData("HTTP-Headers"); + + // NOTE: Replace a 304 response with a 200! Both IE and Mozilla do this. + // Problem first reported through bug# 110272. + int codeStart = responseHeaders.find("304"); + if ( codeStart != -1) { + int codeEnd = responseHeaders.find("\n", codeStart+3); + if (codeEnd != -1) + responseHeaders.replace(codeStart, (codeEnd-codeStart), "200 OK"); + } + + changeState(Loaded); + } + +#ifndef APPLE_CHANGES + const char *data = (const char *)_data.data(); + int len = (int)_data.size(); +#endif + + if ( decoder == NULL ) { + int pos = responseHeaders.find("content-type:", 0, false); + + if ( pos > -1 ) { + pos += 13; + int index = responseHeaders.find('\n', pos); + QString type = responseHeaders.mid(pos, (index-pos)); + index = type.find (';'); + if (index > -1) + encoding = type.mid( index+1 ).remove(QRegExp("charset[ ]*=[ ]*", false)).stripWhiteSpace(); + } + + decoder = new Decoder; + if (!encoding.isNull()) + decoder->setEncoding(encoding.latin1(), Decoder::EncodingFromHTTPHeader); + else { + // Per section 2 of W3C working draft spec, fall back to "UTF-8". + decoder->setEncoding("UTF-8", Decoder::DefaultEncoding); + } + } + if (len == 0) + return; + + if (len == -1) + len = strlen(data); + + QString decoded = decoder->decode(data, len); + + response += decoded; + + if (!aborted) { + changeState(Interactive); + } +} + +Value XMLHttpRequestProtoFunc::tryCall(ExecState *exec, Object &thisObj, const List &args) +{ + if (!thisObj.inherits(&XMLHttpRequest::info)) { + Object err = Error::create(exec,TypeError); + exec->setException(err); + return err; + } + + XMLHttpRequest *request = static_cast<XMLHttpRequest *>(thisObj.imp()); + switch (id) { + case XMLHttpRequest::Abort: + request->abort(); + return Undefined(); + case XMLHttpRequest::GetAllResponseHeaders: + if (args.size() != 0) { + return Undefined(); + } + + return request->getAllResponseHeaders(); + case XMLHttpRequest::GetResponseHeader: + if (args.size() != 1) { + return Undefined(); + } + + return request->getResponseHeader(args[0].toString(exec).qstring()); + case XMLHttpRequest::Open: + { + if (args.size() < 2 || args.size() > 5) { + return Undefined(); + } + + QString method = args[0].toString(exec).qstring(); + KHTMLPart *part = ::qt_cast<KHTMLPart *>(Window::retrieveActive(exec)->part()); + if (!part) + return Undefined(); + KURL url = KURL(part->document().completeURL(args[1].toString(exec).qstring()).string()); + + bool async = true; + if (args.size() >= 3) { + async = args[2].toBoolean(exec); + } + + if (args.size() >= 4) { + url.setUser(args[3].toString(exec).qstring()); + } + + if (args.size() >= 5) { + url.setPass(args[4].toString(exec).qstring()); + } + + request->open(method, url, async); + + return Undefined(); + } + case XMLHttpRequest::Send: + { + if (args.size() > 1) { + return Undefined(); + } + + if (request->state != Loading) { + return Undefined(); + } + + QString body; + if (args.size() >= 1) { + Object obj = Object::dynamicCast(args[0]); + if (obj.isValid() && obj.inherits(&DOMDocument::info)) { + DOM::Node docNode = static_cast<KJS::DOMDocument *>(obj.imp())->toNode(); + DOM::DocumentImpl *doc = static_cast<DOM::DocumentImpl *>(docNode.handle()); + + try { + body = doc->toString().string(); + // FIXME: also need to set content type, including encoding! + + } catch(DOM::DOMException& e) { + Object err = Error::create(exec, GeneralError, "Exception serializing document"); + exec->setException(err); + } + } else { + body = args[0].toString(exec).qstring(); + } + } + + request->send(body); + + return Undefined(); + } + case XMLHttpRequest::SetRequestHeader: + if (args.size() != 2) { + return Undefined(); + } + + request->setRequestHeader(args[0].toString(exec).qstring(), args[1].toString(exec).qstring()); + + return Undefined(); + + case XMLHttpRequest::OverrideMIMEType: + if (args.size() < 1) { + Object err = Error::create(exec, SyntaxError, "Not enough arguments"); + exec->setException(err); + return err; + } + + request->overrideMIMEType(args[0].toString(exec).qstring()); + return Undefined(); + } + + return Undefined(); +} + +} // end namespace + + +#include "xmlhttprequest.moc" |