diff options
author | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
---|---|---|
committer | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
commit | 1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch) | |
tree | 46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebCore/xml/XMLHttpRequest.cpp | |
parent | 32761a6cee1d0dee366b885b7b9c777e67885688 (diff) | |
download | WebKitGtk-tarball-1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c.tar.gz |
webkitgtk-2.16.5HEADwebkitgtk-2.16.5master
Diffstat (limited to 'Source/WebCore/xml/XMLHttpRequest.cpp')
-rw-r--r-- | Source/WebCore/xml/XMLHttpRequest.cpp | 1023 |
1 files changed, 486 insertions, 537 deletions
diff --git a/Source/WebCore/xml/XMLHttpRequest.cpp b/Source/WebCore/xml/XMLHttpRequest.cpp index 41ce508da..8ab3557e8 100644 --- a/Source/WebCore/xml/XMLHttpRequest.cpp +++ b/Source/WebCore/xml/XMLHttpRequest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2004, 2006, 2008 Apple Inc. All rights reserved. + * Copyright (C) 2004-2016 Apple Inc. All rights reserved. * Copyright (C) 2005-2007 Alexey Proskuryakov <ap@webkit.org> * Copyright (C) 2007, 2008 Julien Chaffraix <jchaffraix@webkit.org> * Copyright (C) 2008, 2011 Google Inc. All rights reserved. @@ -24,51 +24,46 @@ #include "XMLHttpRequest.h" #include "Blob.h" -#include "BlobData.h" +#include "CachedResourceRequestInitiators.h" #include "ContentSecurityPolicy.h" #include "CrossOriginAccessControl.h" #include "DOMFormData.h" -#include "DOMImplementation.h" #include "Event.h" -#include "EventException.h" +#include "EventNames.h" #include "ExceptionCode.h" #include "File.h" #include "HTMLDocument.h" +#include "HTTPHeaderNames.h" +#include "HTTPHeaderValues.h" #include "HTTPParsers.h" -#include "HistogramSupport.h" #include "InspectorInstrumentation.h" #include "JSDOMBinding.h" #include "JSDOMWindow.h" +#include "MIMETypeRegistry.h" #include "MemoryCache.h" #include "ParsedContentType.h" #include "ResourceError.h" #include "ResourceRequest.h" -#include "ScriptCallStack.h" #include "ScriptController.h" -#include "ScriptProfile.h" +#include "SecurityOriginPolicy.h" #include "Settings.h" #include "SharedBuffer.h" #include "TextResourceDecoder.h" #include "ThreadableLoader.h" -#include "XMLHttpRequestException.h" +#include "XMLDocument.h" #include "XMLHttpRequestProgressEvent.h" #include "XMLHttpRequestUpload.h" #include "markup.h" -#include <heap/Strong.h> +#include <interpreter/StackVisitor.h> #include <mutex> #include <runtime/ArrayBuffer.h> #include <runtime/ArrayBufferView.h> +#include <runtime/JSCInlines.h> #include <runtime/JSLock.h> -#include <runtime/Operations.h> -#include <wtf/Ref.h> #include <wtf/RefCountedLeakCounter.h> #include <wtf/StdLibExtras.h> #include <wtf/text/CString.h> -#if ENABLE(RESOURCE_TIMING) -#include "CachedResourceRequestInitiators.h" -#endif - namespace WebCore { DEFINE_DEBUG_ONLY_GLOBAL(WTF::RefCountedLeakCounter, xmlHttpRequestCounter, ("XMLHttpRequest")); @@ -80,64 +75,9 @@ enum XMLHttpRequestSendArrayBufferOrView { XMLHttpRequestSendArrayBufferOrViewMax, }; -struct XMLHttpRequestStaticData { - WTF_MAKE_NONCOPYABLE(XMLHttpRequestStaticData); WTF_MAKE_FAST_ALLOCATED; -public: - XMLHttpRequestStaticData(); - const String m_proxyHeaderPrefix; - const String m_secHeaderPrefix; - const HashSet<String, CaseFoldingHash> m_forbiddenRequestHeaders; -}; - -XMLHttpRequestStaticData::XMLHttpRequestStaticData() - : m_proxyHeaderPrefix("proxy-") - , m_secHeaderPrefix("sec-") - , m_forbiddenRequestHeaders({ - "accept-charset", - "accept-encoding", - "access-control-request-headers", - "access-control-request-method", - "connection", - "content-length", - "content-transfer-encoding", - "cookie", - "cookie2", - "date", - "expect", - "host", - "keep-alive", - "origin", - "referer", - "te", - "trailer", - "transfer-encoding", - "upgrade", - "user-agent", - "via", - }) -{ -} - -static const XMLHttpRequestStaticData& staticData() -{ - static std::once_flag onceFlag; - static const XMLHttpRequestStaticData* staticData; - - std::call_once(onceFlag, [] { - staticData = std::make_unique<XMLHttpRequestStaticData>().release(); - }); - - return *staticData; -} - -static bool isSetCookieHeader(const AtomicString& name) -{ - return equalIgnoringCase(name, "set-cookie") || equalIgnoringCase(name, "set-cookie2"); -} - static void replaceCharsetInMediaType(String& mediaType, const String& charsetValue) { - unsigned int pos = 0, len = 0; + unsigned pos = 0, len = 0; findCharsetInMediaType(mediaType, pos, len); @@ -149,7 +89,7 @@ static void replaceCharsetInMediaType(String& mediaType, const String& charsetVa // Found at least one existing charset, replace all occurrences with new charset. while (len) { mediaType.replace(pos, len, charsetValue); - unsigned int start = pos + charsetValue.length(); + unsigned start = pos + charsetValue.length(); findCharsetInMediaType(mediaType, pos, len, start); } } @@ -160,37 +100,22 @@ static void logConsoleError(ScriptExecutionContext* context, const String& messa return; // FIXME: It's not good to report the bad usage without indicating what source line it came from. // We should pass additional parameters so we can tell the console where the mistake occurred. - context->addConsoleMessage(JSMessageSource, ErrorMessageLevel, message); + context->addConsoleMessage(MessageSource::JS, MessageLevel::Error, message); } -PassRefPtr<XMLHttpRequest> XMLHttpRequest::create(ScriptExecutionContext& context) +Ref<XMLHttpRequest> XMLHttpRequest::create(ScriptExecutionContext& context) { - RefPtr<XMLHttpRequest> xmlHttpRequest(adoptRef(new XMLHttpRequest(context))); + auto xmlHttpRequest = adoptRef(*new XMLHttpRequest(context)); xmlHttpRequest->suspendIfNeeded(); - - return xmlHttpRequest.release(); + return xmlHttpRequest; } XMLHttpRequest::XMLHttpRequest(ScriptExecutionContext& context) : ActiveDOMObject(&context) - , m_async(true) - , m_includeCredentials(false) -#if ENABLE(XHR_TIMEOUT) - , m_timeoutMilliseconds(0) -#endif - , m_state(UNSENT) - , m_createdDocument(false) - , m_error(false) - , m_uploadEventsAllowed(true) - , m_uploadComplete(false) - , m_sameOriginRequest(true) - , m_receivedLength(0) - , m_lastSendLineNumber(0) - , m_lastSendColumnNumber(0) - , m_exceptionCode(0) , m_progressEventThrottle(this) - , m_responseTypeCode(ResponseTypeDefault) - , m_responseCacheIsValid(false) + , m_resumeTimer(*this, &XMLHttpRequest::resumeTimerFired) + , m_networkErrorTimer(*this, &XMLHttpRequest::networkErrorTimerFired) + , m_timeoutTimer(*this, &XMLHttpRequest::didReachTimeout) { #ifndef NDEBUG xmlHttpRequestCounter.increment(); @@ -206,8 +131,8 @@ XMLHttpRequest::~XMLHttpRequest() Document* XMLHttpRequest::document() const { - ASSERT_WITH_SECURITY_IMPLICATION(scriptExecutionContext()->isDocument()); - return static_cast<Document*>(scriptExecutionContext()); + ASSERT(scriptExecutionContext()); + return downcast<Document>(scriptExecutionContext()); } SecurityOrigin* XMLHttpRequest::securityOrigin() const @@ -216,13 +141,14 @@ SecurityOrigin* XMLHttpRequest::securityOrigin() const } #if ENABLE(DASHBOARD_SUPPORT) + bool XMLHttpRequest::usesDashboardBackwardCompatibilityMode() const { if (scriptExecutionContext()->isWorkerGlobalScope()) return false; - Settings* settings = document()->settings(); - return settings && settings->usesDashboardBackwardCompatibilityMode(); + return document()->settings().usesDashboardBackwardCompatibilityMode(); } + #endif XMLHttpRequest::State XMLHttpRequest::readyState() const @@ -230,51 +156,51 @@ XMLHttpRequest::State XMLHttpRequest::readyState() const return m_state; } -String XMLHttpRequest::responseText(ExceptionCode& ec) +ExceptionOr<String> XMLHttpRequest::responseText() { - if (m_responseTypeCode != ResponseTypeDefault && m_responseTypeCode != ResponseTypeText) { - ec = INVALID_STATE_ERR; - return ""; - } + if (m_responseType != ResponseType::EmptyString && m_responseType != ResponseType::Text) + return Exception { INVALID_STATE_ERR }; return responseTextIgnoringResponseType(); } -void XMLHttpRequest::didCacheResponseJSON() +void XMLHttpRequest::didCacheResponse() { - ASSERT(m_responseTypeCode == ResponseTypeJSON && doneWithoutErrors()); + ASSERT(doneWithoutErrors()); m_responseCacheIsValid = true; m_responseBuilder.clear(); } -Document* XMLHttpRequest::responseXML(ExceptionCode& ec) +ExceptionOr<Document*> XMLHttpRequest::responseXML() { - if (m_responseTypeCode != ResponseTypeDefault && m_responseTypeCode != ResponseTypeDocument) { - ec = INVALID_STATE_ERR; - return 0; - } + if (m_responseType != ResponseType::EmptyString && m_responseType != ResponseType::Document) + return Exception { INVALID_STATE_ERR }; if (!doneWithoutErrors()) - return 0; + return nullptr; if (!m_createdDocument) { - bool isHTML = equalIgnoringCase(responseMIMEType(), "text/html"); + String mimeType = responseMIMEType(); + bool isHTML = equalLettersIgnoringASCIICase(mimeType, "text/html"); // The W3C spec requires the final MIME type to be some valid XML type, or text/html. // If it is text/html, then the responseType of "document" must have been supplied explicitly. if ((m_response.isHTTP() && !responseIsXML() && !isHTML) - || (isHTML && m_responseTypeCode == ResponseTypeDefault) + || (isHTML && m_responseType == ResponseType::EmptyString) || scriptExecutionContext()->isWorkerGlobalScope()) { - m_responseDocument = 0; + m_responseDocument = nullptr; } else { if (isHTML) m_responseDocument = HTMLDocument::create(0, m_url); else - m_responseDocument = Document::create(0, m_url); + m_responseDocument = XMLDocument::create(0, m_url); // FIXME: Set Last-Modified. m_responseDocument->setContent(m_responseBuilder.toStringPreserveCapacity()); - m_responseDocument->setSecurityOrigin(securityOrigin()); + m_responseDocument->setContextDocument(downcast<Document>(*scriptExecutionContext())); + m_responseDocument->setSecurityOriginPolicy(scriptExecutionContext()->securityOriginPolicy()); + m_responseDocument->overrideMIMEType(mimeType); + if (!m_responseDocument->wellFormed()) - m_responseDocument = 0; + m_responseDocument = nullptr; } m_createdDocument = true; } @@ -282,78 +208,52 @@ Document* XMLHttpRequest::responseXML(ExceptionCode& ec) return m_responseDocument.get(); } -Blob* XMLHttpRequest::responseBlob() +Ref<Blob> XMLHttpRequest::createResponseBlob() { - ASSERT(m_responseTypeCode == ResponseTypeBlob); + ASSERT(m_responseType == ResponseType::Blob); + ASSERT(doneWithoutErrors()); - // We always return null before DONE. - if (m_state != DONE) - return 0; - - if (!m_responseBlob) { - // FIXME: This causes two (or more) unnecessary copies of the data. - // Chromium stores blob data in the browser process, so we're pulling the data - // from the network only to copy it into the renderer to copy it back to the browser. - // Ideally we'd get the blob/file-handle from the ResourceResponse directly - // instead of copying the bytes. Embedders who store blob data in the - // same process as WebCore would at least to teach BlobData to take - // a SharedBuffer, even if they don't get the Blob from the network layer directly. - auto blobData = std::make_unique<BlobData>(); - // If we errored out or got no data, we still return a blob, just an empty one. - size_t size = 0; - if (m_binaryResponseBuilder) { - RefPtr<RawData> rawData = RawData::create(); - size = m_binaryResponseBuilder->size(); - rawData->mutableData()->append(m_binaryResponseBuilder->data(), size); - blobData->appendData(rawData, 0, BlobDataItem::toEndOfFile); - String normalizedContentType = Blob::normalizedContentType(responseMIMEType()); - blobData->setContentType(normalizedContentType); // responseMIMEType defaults to text/xml which may be incorrect. - m_binaryResponseBuilder.clear(); - } - m_responseBlob = Blob::create(std::move(blobData), size); - } + if (!m_binaryResponseBuilder) + return Blob::create(); - return m_responseBlob.get(); + // FIXME: We just received the data from NetworkProcess, and are sending it back. This is inefficient. + Vector<uint8_t> data; + data.append(m_binaryResponseBuilder->data(), m_binaryResponseBuilder->size()); + m_binaryResponseBuilder = nullptr; + String normalizedContentType = Blob::normalizedContentType(responseMIMEType()); // responseMIMEType defaults to text/xml which may be incorrect. + return Blob::create(WTFMove(data), normalizedContentType); } -ArrayBuffer* XMLHttpRequest::responseArrayBuffer() +RefPtr<ArrayBuffer> XMLHttpRequest::createResponseArrayBuffer() { - ASSERT(m_responseTypeCode == ResponseTypeArrayBuffer); - - if (m_state != DONE) - return 0; - - if (!m_responseArrayBuffer) { - if (m_binaryResponseBuilder) - m_responseArrayBuffer = ArrayBuffer::create(const_cast<char*>(m_binaryResponseBuilder->data()), static_cast<unsigned>(m_binaryResponseBuilder->size())); - else - m_responseArrayBuffer = ArrayBuffer::create(nullptr, 0); - m_binaryResponseBuilder.clear(); - } + ASSERT(m_responseType == ResponseType::Arraybuffer); + ASSERT(doneWithoutErrors()); - return m_responseArrayBuffer.get(); + auto result = m_binaryResponseBuilder ? m_binaryResponseBuilder->createArrayBuffer() : ArrayBuffer::create(nullptr, 0); + m_binaryResponseBuilder = nullptr; + return result; } -#if ENABLE(XHR_TIMEOUT) -void XMLHttpRequest::setTimeout(unsigned long timeout, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::setTimeout(unsigned timeout) { - // FIXME: Need to trigger or update the timeout Timer here, if needed. http://webkit.org/b/98156 - // XHR2 spec, 4.7.3. "This implies that the timeout attribute can be set while fetching is in progress. If that occurs it will still be measured relative to the start of fetching." if (scriptExecutionContext()->isDocument() && !m_async) { logConsoleError(scriptExecutionContext(), "XMLHttpRequest.timeout cannot be set for synchronous HTTP(S) requests made from the window context."); - ec = INVALID_ACCESS_ERR; - return; + return Exception { INVALID_ACCESS_ERR }; } m_timeoutMilliseconds = timeout; + if (!m_timeoutTimer.isActive()) + return { }; + + // If timeout is zero, we should use the default network timeout. But we disabled it so let's mimic it with a 60 seconds timeout value. + std::chrono::duration<double> interval = std::chrono::milliseconds { m_timeoutMilliseconds ? m_timeoutMilliseconds : 60000 } - (std::chrono::steady_clock::now() - m_sendingTime); + m_timeoutTimer.startOneShot(std::max(0.0, interval.count())); + return { }; } -#endif -void XMLHttpRequest::setResponseType(const String& responseType, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::setResponseType(ResponseType type) { - if (m_state >= LOADING) { - ec = INVALID_STATE_ERR; - return; - } + if (m_state >= LOADING) + return Exception { INVALID_STATE_ERR }; // Newer functionality is not available to synchronous requests in window contexts, as a spec-mandated // attempt to discourage synchronous XHR use. responseType is one such piece of functionality. @@ -361,43 +261,19 @@ void XMLHttpRequest::setResponseType(const String& responseType, ExceptionCode& // such as file: and data: still make sense to allow. if (!m_async && scriptExecutionContext()->isDocument() && m_url.protocolIsInHTTPFamily()) { logConsoleError(scriptExecutionContext(), "XMLHttpRequest.responseType cannot be changed for synchronous HTTP(S) requests made from the window context."); - ec = INVALID_ACCESS_ERR; - return; + return Exception { INVALID_ACCESS_ERR }; } - if (responseType == "") - m_responseTypeCode = ResponseTypeDefault; - else if (responseType == "text") - m_responseTypeCode = ResponseTypeText; - else if (responseType == "json") - m_responseTypeCode = ResponseTypeJSON; - else if (responseType == "document") - m_responseTypeCode = ResponseTypeDocument; - else if (responseType == "blob") - m_responseTypeCode = ResponseTypeBlob; - else if (responseType == "arraybuffer") - m_responseTypeCode = ResponseTypeArrayBuffer; - else - ASSERT_NOT_REACHED(); -} - -String XMLHttpRequest::responseType() -{ - switch (m_responseTypeCode) { - case ResponseTypeDefault: - return ""; - case ResponseTypeText: - return "text"; - case ResponseTypeJSON: - return "json"; - case ResponseTypeDocument: - return "document"; - case ResponseTypeBlob: - return "blob"; - case ResponseTypeArrayBuffer: - return "arraybuffer"; - } - return ""; + m_responseType = type; + return { }; +} + +String XMLHttpRequest::responseURL() const +{ + URL responseURL(m_response.url()); + responseURL.removeFragmentIdentifier(); + + return responseURL.string(); } void XMLHttpRequest::setLastSendLineAndColumnNumber(unsigned lineNumber, unsigned columnNumber) @@ -426,70 +302,113 @@ void XMLHttpRequest::callReadyStateChangeListener() if (!scriptExecutionContext()) return; - InspectorInstrumentationCookie cookie = InspectorInstrumentation::willDispatchXHRReadyStateChangeEvent(scriptExecutionContext(), this); + // Check whether sending load and loadend events before sending readystatechange event, as it may change m_error/m_state values. + bool shouldSendLoadEvent = (m_state == DONE && !m_error); if (m_async || (m_state <= OPENED || m_state == DONE)) m_progressEventThrottle.dispatchReadyStateChangeEvent(Event::create(eventNames().readystatechangeEvent, false, false), m_state == DONE ? FlushProgressEvent : DoNotFlushProgressEvent); - InspectorInstrumentation::didDispatchXHRReadyStateChangeEvent(cookie); - if (m_state == DONE && !m_error) { - InspectorInstrumentationCookie cookie = InspectorInstrumentation::willDispatchXHRLoadEvent(scriptExecutionContext(), this); + if (shouldSendLoadEvent) { m_progressEventThrottle.dispatchProgressEvent(eventNames().loadEvent); - InspectorInstrumentation::didDispatchXHRLoadEvent(cookie); m_progressEventThrottle.dispatchProgressEvent(eventNames().loadendEvent); } } -void XMLHttpRequest::setWithCredentials(bool value, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::setWithCredentials(bool value) { - if (m_state > OPENED || m_loader) { - ec = INVALID_STATE_ERR; - return; - } + if (m_state > OPENED || m_sendFlag) + return Exception { INVALID_STATE_ERR }; m_includeCredentials = value; + return { }; } bool XMLHttpRequest::isAllowedHTTPMethod(const String& method) { - return !equalIgnoringCase(method, "TRACE") - && !equalIgnoringCase(method, "TRACK") - && !equalIgnoringCase(method, "CONNECT"); + return !equalLettersIgnoringASCIICase(method, "trace") + && !equalLettersIgnoringASCIICase(method, "track") + && !equalLettersIgnoringASCIICase(method, "connect"); } String XMLHttpRequest::uppercaseKnownHTTPMethod(const String& method) { - const char* const methods[] = { "COPY", "DELETE", "GET", "HEAD", "INDEX", "LOCK", "M-POST", "MKCOL", "MOVE", "OPTIONS", "POST", "PROPFIND", "PROPPATCH", "PUT", "UNLOCK" }; - for (unsigned i = 0; i < WTF_ARRAY_LENGTH(methods); ++i) { - if (equalIgnoringCase(method, methods[i])) { + const char* const methods[] = { "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT" }; + for (auto* value : methods) { + if (equalIgnoringASCIICase(method, value)) { // Don't bother allocating a new string if it's already all uppercase. - if (method == methods[i]) + if (method == value) break; - return ASCIILiteral(methods[i]); + return ASCIILiteral(value); } } return method; } +static bool isForbiddenRequestHeader(const String& name) +{ + HTTPHeaderName headerName; + if (!findHTTPHeaderName(name, headerName)) + return false; + + switch (headerName) { + case HTTPHeaderName::AcceptCharset: + case HTTPHeaderName::AcceptEncoding: + case HTTPHeaderName::AccessControlRequestHeaders: + case HTTPHeaderName::AccessControlRequestMethod: + case HTTPHeaderName::Connection: + case HTTPHeaderName::ContentLength: + case HTTPHeaderName::ContentTransferEncoding: + case HTTPHeaderName::Cookie: + case HTTPHeaderName::Cookie2: + case HTTPHeaderName::Date: + case HTTPHeaderName::DNT: + case HTTPHeaderName::Expect: + case HTTPHeaderName::Host: + case HTTPHeaderName::KeepAlive: + case HTTPHeaderName::Origin: + case HTTPHeaderName::Referer: + case HTTPHeaderName::TE: + case HTTPHeaderName::Trailer: + case HTTPHeaderName::TransferEncoding: + case HTTPHeaderName::Upgrade: + case HTTPHeaderName::UserAgent: + case HTTPHeaderName::Via: + return true; + + default: + return false; + } +} + bool XMLHttpRequest::isAllowedHTTPHeader(const String& name) { - return !staticData().m_forbiddenRequestHeaders.contains(name) && !name.startsWith(staticData().m_proxyHeaderPrefix, false) - && !name.startsWith(staticData().m_secHeaderPrefix, false); + if (isForbiddenRequestHeader(name)) + return false; + + if (name.startsWith("proxy-", false)) + return false; + + if (name.startsWith("sec-", false)) + return false; + + return true; } -void XMLHttpRequest::open(const String& method, const URL& url, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::open(const String& method, const String& url) { - open(method, url, true, ec); + // If the async argument is omitted, set async to true. + return open(method, scriptExecutionContext()->completeURL(url), true); } -void XMLHttpRequest::open(const String& method, const URL& url, bool async, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::open(const String& method, const URL& url, bool async) { if (!internalAbort()) - return; + return { }; State previousState = m_state; m_state = UNSENT; m_error = false; + m_sendFlag = false; m_uploadComplete = false; // clear stuff from possible previous load @@ -498,59 +417,33 @@ void XMLHttpRequest::open(const String& method, const URL& url, bool async, Exce ASSERT(m_state == UNSENT); - if (!isValidHTTPToken(method)) { - ec = SYNTAX_ERR; - return; - } + if (!isValidHTTPToken(method)) + return Exception { SYNTAX_ERR }; - if (!isAllowedHTTPMethod(method)) { - ec = SECURITY_ERR; - return; - } - - // FIXME: Convert this to check the isolated world's Content Security Policy once webkit.org/b/104520 is solved. - bool shouldBypassMainWorldContentSecurityPolicy = false; - if (scriptExecutionContext()->isDocument()) { - Document* document = static_cast<Document*>(scriptExecutionContext()); - if (document->frame()) - shouldBypassMainWorldContentSecurityPolicy = document->frame()->script().shouldBypassMainWorldContentSecurityPolicy(); - } - if (!shouldBypassMainWorldContentSecurityPolicy && !scriptExecutionContext()->contentSecurityPolicy()->allowConnectToSource(url)) { - // FIXME: Should this be throwing an exception? - ec = SECURITY_ERR; - return; - } + if (!isAllowedHTTPMethod(method)) + return Exception { SECURITY_ERR }; if (!async && scriptExecutionContext()->isDocument()) { - if (document()->settings() && !document()->settings()->syncXHRInDocumentsEnabled()) { - logConsoleError(scriptExecutionContext(), "Synchronous XMLHttpRequests are disabled for this page."); - ec = INVALID_ACCESS_ERR; - return; - } - // Newer functionality is not available to synchronous requests in window contexts, as a spec-mandated // attempt to discourage synchronous XHR use. responseType is one such piece of functionality. // We'll only disable this functionality for HTTP(S) requests since sync requests for local protocols // such as file: and data: still make sense to allow. - if (url.protocolIsInHTTPFamily() && m_responseTypeCode != ResponseTypeDefault) { + if (url.protocolIsInHTTPFamily() && m_responseType != ResponseType::EmptyString) { logConsoleError(scriptExecutionContext(), "Synchronous HTTP(S) requests made from the window context cannot have XMLHttpRequest.responseType set."); - ec = INVALID_ACCESS_ERR; - return; + return Exception { INVALID_ACCESS_ERR }; } -#if ENABLE(XHR_TIMEOUT) // Similarly, timeouts are disabled for synchronous requests as well. if (m_timeoutMilliseconds > 0) { logConsoleError(scriptExecutionContext(), "Synchronous XMLHttpRequests must not have a timeout value set."); - ec = INVALID_ACCESS_ERR; - return; + return Exception { INVALID_ACCESS_ERR }; } -#endif } m_method = uppercaseKnownHTTPMethod(method); m_url = url; + scriptExecutionContext()->contentSecurityPolicy()->upgradeInsecureRequestIfNeeded(m_url, ContentSecurityPolicy::InsecureRequestType::Load); m_async = async; @@ -562,177 +455,223 @@ void XMLHttpRequest::open(const String& method, const URL& url, bool async, Exce changeState(OPENED); else m_state = OPENED; -} -void XMLHttpRequest::open(const String& method, const URL& url, bool async, const String& user, ExceptionCode& ec) -{ - URL urlWithCredentials(url); - urlWithCredentials.setUser(user); - - open(method, urlWithCredentials, async, ec); + return { }; } -void XMLHttpRequest::open(const String& method, const URL& url, bool async, const String& user, const String& password, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::open(const String& method, const String& url, bool async, const String& user, const String& password) { - URL urlWithCredentials(url); - urlWithCredentials.setUser(user); - urlWithCredentials.setPass(password); + URL urlWithCredentials = scriptExecutionContext()->completeURL(url); + if (!user.isNull()) { + urlWithCredentials.setUser(user); + if (!password.isNull()) + urlWithCredentials.setPass(password); + } - open(method, urlWithCredentials, async, ec); + return open(method, urlWithCredentials, async); } -bool XMLHttpRequest::initSend(ExceptionCode& ec) +std::optional<ExceptionOr<void>> XMLHttpRequest::prepareToSend() { + // A return value other than std::nullopt means we should not try to send, and we should return that value to the caller. + // std::nullopt means we are ready to send and should continue with the send algorithm. + if (!scriptExecutionContext()) - return false; + return ExceptionOr<void> { }; - if (m_state != OPENED || m_loader) { - ec = INVALID_STATE_ERR; - return false; + auto& context = *scriptExecutionContext(); + + if (m_state != OPENED || m_sendFlag) + return ExceptionOr<void> { Exception { INVALID_STATE_ERR } }; + ASSERT(!m_loader); + + // FIXME: Convert this to check the isolated world's Content Security Policy once webkit.org/b/104520 is solved. + if (!context.shouldBypassMainWorldContentSecurityPolicy() && !context.contentSecurityPolicy()->allowConnectToSource(m_url)) { + if (!m_async) + return ExceptionOr<void> { Exception { NETWORK_ERR } }; + setPendingActivity(this); + m_timeoutTimer.stop(); + m_networkErrorTimer.startOneShot(0); + return ExceptionOr<void> { }; } m_error = false; - return true; + return std::nullopt; } -void XMLHttpRequest::send(ExceptionCode& ec) -{ - send(String(), ec); +namespace { + +// FIXME: This should be abstracted out, so that any IDL function can be passed the line/column/url tuple. + +// FIXME: This should probably use ShadowChicken so that we get the right frame even when it did a tail call. +// https://bugs.webkit.org/show_bug.cgi?id=155688 + +class SendFunctor { +public: + SendFunctor() = default; + + unsigned line() const { return m_line; } + unsigned column() const { return m_column; } + String url() const { return m_url; } + + JSC::StackVisitor::Status operator()(JSC::StackVisitor& visitor) const + { + if (!m_hasSkippedFirstFrame) { + m_hasSkippedFirstFrame = true; + return JSC::StackVisitor::Continue; + } + + unsigned line = 0; + unsigned column = 0; + visitor->computeLineAndColumn(line, column); + m_line = line; + m_column = column; + m_url = visitor->sourceURL(); + return JSC::StackVisitor::Done; + } + +private: + mutable bool m_hasSkippedFirstFrame { false }; + mutable unsigned m_line { 0 }; + mutable unsigned m_column { 0 }; + mutable String m_url; +}; + } -void XMLHttpRequest::send(Document* document, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::send(JSC::ExecState& state, std::optional<SendTypes>&& sendType) { - ASSERT(document); + InspectorInstrumentation::willSendXMLHttpRequest(scriptExecutionContext(), url()); - if (!initSend(ec)) - return; + ExceptionOr<void> result; + if (!sendType) + result = send(); + else { + result = WTF::switchOn(sendType.value(), + [this] (const RefPtr<Document>& document) -> ExceptionOr<void> { return send(*document); }, + [this] (const RefPtr<Blob>& blob) -> ExceptionOr<void> { return send(*blob); }, + [this] (const RefPtr<JSC::ArrayBufferView>& arrayBufferView) -> ExceptionOr<void> { return send(*arrayBufferView); }, + [this] (const RefPtr<JSC::ArrayBuffer>& arrayBuffer) -> ExceptionOr<void> { return send(*arrayBuffer); }, + [this] (const RefPtr<DOMFormData>& formData) -> ExceptionOr<void> { return send(*formData); }, + [this] (const String& string) -> ExceptionOr<void> { return send(string); } + ); + } + + SendFunctor functor; + state.iterate(functor); + setLastSendLineAndColumnNumber(functor.line(), functor.column()); + setLastSendURL(functor.url()); + + return result; +} + +ExceptionOr<void> XMLHttpRequest::send(Document& document) +{ + if (auto result = prepareToSend()) + return WTFMove(result.value()); if (m_method != "GET" && m_method != "HEAD" && m_url.protocolIsInHTTPFamily()) { - String contentType = getRequestHeader("Content-Type"); - if (contentType.isEmpty()) { + if (!m_requestHeaders.contains(HTTPHeaderName::ContentType)) { #if ENABLE(DASHBOARD_SUPPORT) if (usesDashboardBackwardCompatibilityMode()) - setRequestHeaderInternal("Content-Type", "application/x-www-form-urlencoded"); + m_requestHeaders.set(HTTPHeaderName::ContentType, ASCIILiteral("application/x-www-form-urlencoded")); else #endif // FIXME: this should include the charset used for encoding. - setRequestHeaderInternal("Content-Type", "application/xml"); + m_requestHeaders.set(HTTPHeaderName::ContentType, document.isHTMLDocument() ? ASCIILiteral("text/html;charset=UTF-8") : ASCIILiteral("application/xml;charset=UTF-8")); } // FIXME: According to XMLHttpRequest Level 2, this should use the Document.innerHTML algorithm // from the HTML5 specification to serialize the document. - String body = createMarkup(*document); - - // FIXME: this should use value of document.inputEncoding to determine the encoding to use. - TextEncoding encoding = UTF8Encoding(); - m_requestEntityBody = FormData::create(encoding.encode(body.deprecatedCharacters(), body.length(), EntitiesForUnencodables)); + m_requestEntityBody = FormData::create(UTF8Encoding().encode(createMarkup(document), EntitiesForUnencodables)); if (m_upload) m_requestEntityBody->setAlwaysStream(true); } - createRequest(ec); + return createRequest(); } -void XMLHttpRequest::send(const String& body, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::send(const String& body) { - if (!initSend(ec)) - return; + if (auto result = prepareToSend()) + return WTFMove(result.value()); if (!body.isNull() && m_method != "GET" && m_method != "HEAD" && m_url.protocolIsInHTTPFamily()) { - String contentType = getRequestHeader("Content-Type"); - if (contentType.isEmpty()) { + String contentType = m_requestHeaders.get(HTTPHeaderName::ContentType); + if (contentType.isNull()) { #if ENABLE(DASHBOARD_SUPPORT) if (usesDashboardBackwardCompatibilityMode()) - setRequestHeaderInternal("Content-Type", "application/x-www-form-urlencoded"); + m_requestHeaders.set(HTTPHeaderName::ContentType, ASCIILiteral("application/x-www-form-urlencoded")); else #endif - setRequestHeaderInternal("Content-Type", "application/xml"); + m_requestHeaders.set(HTTPHeaderName::ContentType, HTTPHeaderValues::textPlainContentType()); } else { replaceCharsetInMediaType(contentType, "UTF-8"); - m_requestHeaders.set("Content-Type", contentType); + m_requestHeaders.set(HTTPHeaderName::ContentType, contentType); } - m_requestEntityBody = FormData::create(UTF8Encoding().encode(body.deprecatedCharacters(), body.length(), EntitiesForUnencodables)); + m_requestEntityBody = FormData::create(UTF8Encoding().encode(body, EntitiesForUnencodables)); if (m_upload) m_requestEntityBody->setAlwaysStream(true); } - createRequest(ec); + return createRequest(); } -void XMLHttpRequest::send(Blob* body, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::send(Blob& body) { - if (!initSend(ec)) - return; + if (auto result = prepareToSend()) + return WTFMove(result.value()); if (m_method != "GET" && m_method != "HEAD" && m_url.protocolIsInHTTPFamily()) { - const String& contentType = getRequestHeader("Content-Type"); - if (contentType.isEmpty()) { - const String& blobType = body->type(); + if (!m_requestHeaders.contains(HTTPHeaderName::ContentType)) { + const String& blobType = body.type(); if (!blobType.isEmpty() && isValidContentType(blobType)) - setRequestHeaderInternal("Content-Type", blobType); + m_requestHeaders.set(HTTPHeaderName::ContentType, blobType); else { // From FileAPI spec, whenever media type cannot be determined, empty string must be returned. - setRequestHeaderInternal("Content-Type", ""); + m_requestHeaders.set(HTTPHeaderName::ContentType, emptyString()); } } - // FIXME: add support for uploading bundles. m_requestEntityBody = FormData::create(); - if (body->isFile()) - m_requestEntityBody->appendFile(toFile(body)->path()); -#if ENABLE(BLOB) - else - m_requestEntityBody->appendBlob(body->url()); -#endif + m_requestEntityBody->appendBlob(body.url()); } - createRequest(ec); + return createRequest(); } -void XMLHttpRequest::send(DOMFormData* body, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::send(DOMFormData& body) { - if (!initSend(ec)) - return; + if (auto result = prepareToSend()) + return WTFMove(result.value()); if (m_method != "GET" && m_method != "HEAD" && m_url.protocolIsInHTTPFamily()) { - m_requestEntityBody = FormData::createMultiPart(*(static_cast<FormDataList*>(body)), body->encoding(), document()); - - // We need to ask the client to provide the generated file names if needed. When FormData fills the element - // for the file, it could set a flag to use the generated file name, i.e. a package file on Mac. + m_requestEntityBody = FormData::createMultiPart(body, body.encoding(), document()); m_requestEntityBody->generateFiles(document()); - - String contentType = getRequestHeader("Content-Type"); - if (contentType.isEmpty()) { - contentType = makeString("multipart/form-data; boundary=", m_requestEntityBody->boundary().data()); - setRequestHeaderInternal("Content-Type", contentType); - } + if (!m_requestHeaders.contains(HTTPHeaderName::ContentType)) + m_requestHeaders.set(HTTPHeaderName::ContentType, makeString("multipart/form-data; boundary=", m_requestEntityBody->boundary().data())); } - createRequest(ec); + return createRequest(); } -void XMLHttpRequest::send(ArrayBuffer* body, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::send(ArrayBuffer& body) { - String consoleMessage("ArrayBuffer is deprecated in XMLHttpRequest.send(). Use ArrayBufferView instead."); - scriptExecutionContext()->addConsoleMessage(JSMessageSource, WarningMessageLevel, consoleMessage); - - HistogramSupport::histogramEnumeration("WebCore.XHR.send.ArrayBufferOrView", XMLHttpRequestSendArrayBuffer, XMLHttpRequestSendArrayBufferOrViewMax); - - sendBytesData(body->data(), body->byteLength(), ec); + ASCIILiteral consoleMessage("ArrayBuffer is deprecated in XMLHttpRequest.send(). Use ArrayBufferView instead."); + scriptExecutionContext()->addConsoleMessage(MessageSource::JS, MessageLevel::Warning, consoleMessage); + return sendBytesData(body.data(), body.byteLength()); } -void XMLHttpRequest::send(ArrayBufferView* body, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::send(ArrayBufferView& body) { - HistogramSupport::histogramEnumeration("WebCore.XHR.send.ArrayBufferOrView", XMLHttpRequestSendArrayBufferView, XMLHttpRequestSendArrayBufferOrViewMax); - - sendBytesData(body->baseAddress(), body->byteLength(), ec); + return sendBytesData(body.baseAddress(), body.byteLength()); } -void XMLHttpRequest::sendBytesData(const void* data, size_t length, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::sendBytesData(const void* data, size_t length) { - if (!initSend(ec)) - return; + if (auto result = prepareToSend()) + return WTFMove(result.value()); if (m_method != "GET" && m_method != "HEAD" && m_url.protocolIsInHTTPFamily()) { m_requestEntityBody = FormData::create(data, length); @@ -740,25 +679,16 @@ void XMLHttpRequest::sendBytesData(const void* data, size_t length, ExceptionCod m_requestEntityBody->setAlwaysStream(true); } - createRequest(ec); -} - -void XMLHttpRequest::sendFromInspector(PassRefPtr<FormData> formData, ExceptionCode& ec) -{ - m_requestEntityBody = formData ? formData->deepCopy() : 0; - createRequest(ec); - m_exceptionCode = ec; + return createRequest(); } -void XMLHttpRequest::createRequest(ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::createRequest() { -#if ENABLE(BLOB) // Only GET request is supported for blob URL. - if (m_url.protocolIs("blob") && m_method != "GET") { - ec = XMLHttpRequestException::NETWORK_ERR; - return; - } -#endif + if (!m_async && m_url.protocolIsBlob() && m_method != "GET") + return Exception { NETWORK_ERR }; + + m_sendFlag = true; // The presence of upload event listeners forces us to use preflighting because POSTing to an URL that does not // permit cross origin requests should look exactly like POSTing to an URL that does not respond at all. @@ -779,34 +709,38 @@ void XMLHttpRequest::createRequest(ExceptionCode& ec) m_uploadEventsAllowed = m_sameOriginRequest || uploadEvents || !isSimpleCrossOriginAccessRequest(m_method, m_requestHeaders); ResourceRequest request(m_url); + request.setRequester(ResourceRequest::Requester::XHR); + request.setInitiatorIdentifier(scriptExecutionContext()->resourceRequestIdentifier()); request.setHTTPMethod(m_method); - InspectorInstrumentation::willLoadXHR(scriptExecutionContext(), this, m_method, m_url, m_async, m_requestEntityBody ? m_requestEntityBody->deepCopy() : 0, m_requestHeaders, m_includeCredentials); - if (m_requestEntityBody) { ASSERT(m_method != "GET"); ASSERT(m_method != "HEAD"); - request.setHTTPBody(m_requestEntityBody.release()); + request.setHTTPBody(WTFMove(m_requestEntityBody)); } - if (m_requestHeaders.size() > 0) - request.addHTTPHeaderFields(m_requestHeaders); + if (!m_requestHeaders.isEmpty()) + request.setHTTPHeaderFields(m_requestHeaders); ThreadableLoaderOptions options; options.sendLoadCallbacks = SendCallbacks; - options.sniffContent = DoNotSniffContent; options.preflightPolicy = uploadEvents ? ForcePreflight : ConsiderPreflight; - options.allowCredentials = (m_sameOriginRequest || m_includeCredentials) ? AllowStoredCredentials : DoNotAllowStoredCredentials; - options.crossOriginRequestPolicy = UseAccessControl; - options.securityOrigin = securityOrigin(); -#if ENABLE(RESOURCE_TIMING) + options.credentials = m_includeCredentials ? FetchOptions::Credentials::Include : FetchOptions::Credentials::SameOrigin; + options.mode = FetchOptions::Mode::Cors; + options.contentSecurityPolicyEnforcement = scriptExecutionContext()->shouldBypassMainWorldContentSecurityPolicy() ? ContentSecurityPolicyEnforcement::DoNotEnforce : ContentSecurityPolicyEnforcement::EnforceConnectSrcDirective; options.initiator = cachedResourceRequestInitiators().xmlhttprequest; -#endif - -#if ENABLE(XHR_TIMEOUT) - if (m_timeoutMilliseconds) - request.setTimeoutInterval(m_timeoutMilliseconds / 1000.0); -#endif + options.sameOriginDataURLFlag = SameOriginDataURLFlag::Set; + options.filteringPolicy = ResponseFilteringPolicy::Enable; + + if (m_timeoutMilliseconds) { + if (!m_async) + request.setTimeoutInterval(m_timeoutMilliseconds / 1000.0); + else { + request.setTimeoutInterval(std::numeric_limits<double>::infinity()); + m_sendingTime = std::chrono::steady_clock::now(); + m_timeoutTimer.startOneShot(std::chrono::milliseconds { m_timeoutMilliseconds }); + } + } m_exceptionCode = 0; m_error = false; @@ -815,34 +749,36 @@ void XMLHttpRequest::createRequest(ExceptionCode& ec) if (m_upload) request.setReportUploadProgress(true); - // ThreadableLoader::create can return null here, for example if we're no longer attached to a page. + // ThreadableLoader::create can return null here, for example if we're no longer attached to a page or if a content blocker blocks the load. // This is true while running onunload handlers. // FIXME: Maybe we need to be able to send XMLHttpRequests from onunload, <http://bugs.webkit.org/show_bug.cgi?id=10904>. - // FIXME: Maybe create() can return null for other reasons too? - m_loader = ThreadableLoader::create(scriptExecutionContext(), this, request, options); - if (m_loader) { - // Neither this object nor the JavaScript wrapper should be deleted while - // a request is in progress because we need to keep the listeners alive, - // and they are referenced by the JavaScript wrapper. + m_loader = ThreadableLoader::create(*scriptExecutionContext(), *this, WTFMove(request), options); + + // Either loader is null or some error was synchronously sent to us. + ASSERT(m_loader || !m_sendFlag); + + // Neither this object nor the JavaScript wrapper should be deleted while + // a request is in progress because we need to keep the listeners alive, + // and they are referenced by the JavaScript wrapper. + if (m_loader) setPendingActivity(this); - } } else { InspectorInstrumentation::willLoadXHRSynchronously(scriptExecutionContext()); - ThreadableLoader::loadResourceSynchronously(scriptExecutionContext(), request, *this, options); + ThreadableLoader::loadResourceSynchronously(*scriptExecutionContext(), WTFMove(request), *this, options); InspectorInstrumentation::didLoadXHRSynchronously(scriptExecutionContext()); } - if (!m_exceptionCode && m_error) - m_exceptionCode = XMLHttpRequestException::NETWORK_ERR; - ec = m_exceptionCode; + if (m_exceptionCode) + return Exception { m_exceptionCode }; + if (m_error) + return Exception { NETWORK_ERR }; + return { }; } void XMLHttpRequest::abort() { // internalAbort() calls dropProtection(), which may release the last reference. - Ref<XMLHttpRequest> protect(*this); - - bool sendFlag = m_loader; + Ref<XMLHttpRequest> protectedThis(*this); if (!internalAbort()) return; @@ -851,16 +787,13 @@ void XMLHttpRequest::abort() // Clear headers as required by the spec m_requestHeaders.clear(); - - if ((m_state <= OPENED && !sendFlag) || m_state == DONE) - m_state = UNSENT; - else { + if ((m_state == OPENED && m_sendFlag) || m_state == HEADERS_RECEIVED || m_state == LOADING) { ASSERT(!m_loader); + m_sendFlag = false; changeState(DONE); - m_state = UNSENT; + dispatchErrorEvents(eventNames().abortEvent); } - - dispatchErrorEvents(eventNames().abortEvent); + m_state = UNSENT; } bool XMLHttpRequest::internalAbort() @@ -870,9 +803,9 @@ bool XMLHttpRequest::internalAbort() // FIXME: when we add the support for multi-part XHR, we will have to think be careful with this initialization. m_receivedLength = 0; - m_decoder = 0; + m_decoder = nullptr; - InspectorInstrumentation::didFailXHRLoading(scriptExecutionContext(), this); + m_timeoutTimer.stop(); if (!m_loader) return true; @@ -880,7 +813,7 @@ bool XMLHttpRequest::internalAbort() // Cancelling m_loader may trigger a window.onload callback which can call open() on the same xhr. // This would create internalAbort reentrant call. // m_loader is set to null before being cancelled to exit early in any reentrant internalAbort() call. - RefPtr<ThreadableLoader> loader = m_loader.release(); + auto loader = WTFMove(m_loader); loader->cancel(); // If window.onload callback calls open() and send() on the same xhr, m_loader is now set to a new value. @@ -905,23 +838,22 @@ void XMLHttpRequest::clearResponseBuffers() m_responseBuilder.clear(); m_responseEncoding = String(); m_createdDocument = false; - m_responseDocument = 0; - m_responseBlob = 0; - m_binaryResponseBuilder.clear(); - m_responseArrayBuffer.clear(); + m_responseDocument = nullptr; + m_binaryResponseBuilder = nullptr; m_responseCacheIsValid = false; } void XMLHttpRequest::clearRequest() { m_requestHeaders.clear(); - m_requestEntityBody = 0; + m_requestEntityBody = nullptr; } void XMLHttpRequest::genericError() { clearResponse(); clearRequest(); + m_sendFlag = false; m_error = true; changeState(DONE); @@ -934,6 +866,12 @@ void XMLHttpRequest::networkError() internalAbort(); } +void XMLHttpRequest::networkErrorTimerFired() +{ + networkError(); + dropProtection(); +} + void XMLHttpRequest::abortError() { genericError(); @@ -948,83 +886,60 @@ void XMLHttpRequest::dropProtection() // out. But it is protected from GC while loading, so this // can't be recouped until the load is done, so only // report the extra cost at that point. - JSC::VM* vm = scriptExecutionContext()->vm(); + JSC::VM& vm = scriptExecutionContext()->vm(); JSC::JSLockHolder lock(vm); - vm->heap.reportExtraMemoryCost(m_responseBuilder.length() * 2); + // FIXME: Adopt reportExtraMemoryVisited, and switch to reportExtraMemoryAllocated. + // https://bugs.webkit.org/show_bug.cgi?id=142595 + vm.heap.deprecatedReportExtraMemory(m_responseBuilder.length() * 2); unsetPendingActivity(this); } -void XMLHttpRequest::overrideMimeType(const String& override) +ExceptionOr<void> XMLHttpRequest::overrideMimeType(const String& override) { + if (m_state == LOADING || m_state == DONE) + return Exception { INVALID_STATE_ERR }; + m_mimeTypeOverride = override; + return { }; } -void XMLHttpRequest::setRequestHeader(const AtomicString& name, const String& value, ExceptionCode& ec) +ExceptionOr<void> XMLHttpRequest::setRequestHeader(const String& name, const String& value) { - if (m_state != OPENED || m_loader) { + if (m_state != OPENED || m_sendFlag) { #if ENABLE(DASHBOARD_SUPPORT) if (usesDashboardBackwardCompatibilityMode()) - return; + return { }; #endif - - ec = INVALID_STATE_ERR; - return; + return Exception { INVALID_STATE_ERR }; } - if (!isValidHTTPToken(name) || !isValidHTTPHeaderValue(value)) { - ec = SYNTAX_ERR; - return; - } + String normalizedValue = stripLeadingAndTrailingHTTPSpaces(value); + if (!isValidHTTPToken(name) || !isValidHTTPHeaderValue(normalizedValue)) + return Exception { SYNTAX_ERR }; // A privileged script (e.g. a Dashboard widget) can set any headers. if (!securityOrigin()->canLoadLocalResources() && !isAllowedHTTPHeader(name)) { logConsoleError(scriptExecutionContext(), "Refused to set unsafe header \"" + name + "\""); - return; + return { }; } - setRequestHeaderInternal(name, value); -} - -void XMLHttpRequest::setRequestHeaderInternal(const AtomicString& name, const String& value) -{ - HTTPHeaderMap::AddResult result = m_requestHeaders.add(name, value); - if (!result.isNewEntry) - result.iterator->value.append(", " + value); -} - -String XMLHttpRequest::getRequestHeader(const AtomicString& name) const -{ - return m_requestHeaders.get(name); + m_requestHeaders.add(name, normalizedValue); + return { }; } String XMLHttpRequest::getAllResponseHeaders() const { if (m_state < HEADERS_RECEIVED || m_error) - return ""; + return emptyString(); StringBuilder stringBuilder; - HTTPHeaderSet accessControlExposeHeaderSet; - parseAccessControlExposeHeadersAllowList(m_response.httpHeaderField("Access-Control-Expose-Headers"), accessControlExposeHeaderSet); - HTTPHeaderMap::const_iterator end = m_response.httpHeaderFields().end(); - for (HTTPHeaderMap::const_iterator it = m_response.httpHeaderFields().begin(); it!= end; ++it) { - // Hide Set-Cookie header fields from the XMLHttpRequest client for these reasons: - // 1) If the client did have access to the fields, then it could read HTTP-only - // cookies; those cookies are supposed to be hidden from scripts. - // 2) There's no known harm in hiding Set-Cookie header fields entirely; we don't - // know any widely used technique that requires access to them. - // 3) Firefox has implemented this policy. - if (isSetCookieHeader(it->key) && !securityOrigin()->canLoadLocalResources()) - continue; - - if (!m_sameOriginRequest && !isOnAccessControlResponseHeaderWhitelist(it->key) && !accessControlExposeHeaderSet.contains(it->key)) - continue; - - stringBuilder.append(it->key); + for (const auto& header : m_response.httpHeaderFields()) { + stringBuilder.append(header.key); stringBuilder.append(':'); stringBuilder.append(' '); - stringBuilder.append(it->value); + stringBuilder.append(header.value); stringBuilder.append('\r'); stringBuilder.append('\n'); } @@ -1032,24 +947,11 @@ String XMLHttpRequest::getAllResponseHeaders() const return stringBuilder.toString(); } -String XMLHttpRequest::getResponseHeader(const AtomicString& name) const +String XMLHttpRequest::getResponseHeader(const String& name) const { if (m_state < HEADERS_RECEIVED || m_error) return String(); - // See comment in getAllResponseHeaders above. - if (isSetCookieHeader(name) && !securityOrigin()->canLoadLocalResources()) { - logConsoleError(scriptExecutionContext(), "Refused to get unsafe header \"" + name + "\""); - return String(); - } - - HTTPHeaderSet accessControlExposeHeaderSet; - parseAccessControlExposeHeadersAllowList(m_response.httpHeaderField("Access-Control-Expose-Headers"), accessControlExposeHeaderSet); - - if (!m_sameOriginRequest && !isOnAccessControlResponseHeaderWhitelist(name) && !accessControlExposeHeaderSet.contains(name)) { - logConsoleError(scriptExecutionContext(), "Refused to get unsafe header \"" + name + "\""); - return String(); - } return m_response.httpHeaderField(name); } @@ -1058,80 +960,72 @@ String XMLHttpRequest::responseMIMEType() const String mimeType = extractMIMETypeFromMediaType(m_mimeTypeOverride); if (mimeType.isEmpty()) { if (m_response.isHTTP()) - mimeType = extractMIMETypeFromMediaType(m_response.httpHeaderField("Content-Type")); + mimeType = extractMIMETypeFromMediaType(m_response.httpHeaderField(HTTPHeaderName::ContentType)); else mimeType = m_response.mimeType(); + if (mimeType.isEmpty()) + mimeType = ASCIILiteral("text/xml"); } - if (mimeType.isEmpty()) - mimeType = "text/xml"; - return mimeType; } bool XMLHttpRequest::responseIsXML() const { - // FIXME: Remove the lower() call when DOMImplementation.isXMLMIMEType() is modified - // to do case insensitive MIME type matching. - return DOMImplementation::isXMLMIMEType(responseMIMEType().lower()); + return MIMETypeRegistry::isXMLMIMEType(responseMIMEType()); } -int XMLHttpRequest::status(ExceptionCode& ec) const +int XMLHttpRequest::status() const { - if (m_response.httpStatusCode()) - return m_response.httpStatusCode(); - - if (m_state == OPENED) { - // Firefox only raises an exception in this state; we match it. - // Note the case of local file requests, where we have no HTTP response code! Firefox never raises an exception for those, but we match HTTP case for consistency. - ec = INVALID_STATE_ERR; - } + if (m_state == UNSENT || m_state == OPENED || m_error) + return 0; - return 0; + return m_response.httpStatusCode(); } -String XMLHttpRequest::statusText(ExceptionCode& ec) const +String XMLHttpRequest::statusText() const { - if (!m_response.httpStatusText().isNull()) - return m_response.httpStatusText(); - - if (m_state == OPENED) { - // See comments in status() above. - ec = INVALID_STATE_ERR; - } + if (m_state == UNSENT || m_state == OPENED || m_error) + return String(); - return String(); + return m_response.httpStatusText(); } void XMLHttpRequest::didFail(const ResourceError& error) { - // If we are already in an error state, for instance we called abort(), bail out early. if (m_error) return; if (error.isCancellation()) { - m_exceptionCode = XMLHttpRequestException::ABORT_ERR; + m_exceptionCode = ABORT_ERR; abortError(); return; } -#if ENABLE(XHR_TIMEOUT) + // In case of worker sync timeouts. if (error.isTimeout()) { - didTimeout(); + didReachTimeout(); return; } -#endif // Network failures are already reported to Web Inspector by ResourceLoader. - if (error.domain() == errorDomainWebKitInternal) - logConsoleError(scriptExecutionContext(), "XMLHttpRequest cannot load " + error.failingURL() + ". " + error.localizedDescription()); - - m_exceptionCode = XMLHttpRequestException::NETWORK_ERR; - networkError(); -} + if (error.domain() == errorDomainWebKitInternal) { + String message = makeString("XMLHttpRequest cannot load ", error.failingURL().string(), ". ", error.localizedDescription()); + logConsoleError(scriptExecutionContext(), message); + } else if (error.isAccessControl()) { + String message = makeString("XMLHttpRequest cannot load ", error.failingURL().string(), " due to access control checks."); + logConsoleError(scriptExecutionContext(), message); + } -void XMLHttpRequest::didFailRedirectCheck() -{ + // In case didFail is called synchronously on an asynchronous XHR call, let's dispatch network error asynchronously + if (m_async && m_sendFlag && !m_loader) { + m_sendFlag = false; + setPendingActivity(this); + m_timeoutTimer.stop(); + m_networkErrorTimer.startOneShot(0); + return; + } + m_exceptionCode = NETWORK_ERR; networkError(); } @@ -1148,14 +1042,20 @@ void XMLHttpRequest::didFinishLoading(unsigned long identifier, double) m_responseBuilder.shrinkToFit(); - InspectorInstrumentation::didFinishXHRLoading(scriptExecutionContext(), this, identifier, m_responseBuilder.toStringPreserveCapacity(), m_url, m_lastSendURL, m_lastSendLineNumber, m_lastSendColumnNumber); + std::optional<String> decodedText; + if (!m_binaryResponseBuilder) + decodedText = m_responseBuilder.toStringPreserveCapacity(); + InspectorInstrumentation::didFinishXHRLoading(scriptExecutionContext(), identifier, decodedText, m_url, m_lastSendURL, m_lastSendLineNumber, m_lastSendColumnNumber); bool hadLoader = m_loader; - m_loader = 0; + m_loader = nullptr; + m_sendFlag = false; changeState(DONE); m_responseEncoding = String(); - m_decoder = 0; + m_decoder = nullptr; + + m_timeoutTimer.stop(); if (hadLoader) dropProtection(); @@ -1177,18 +1077,27 @@ void XMLHttpRequest::didSendData(unsigned long long bytesSent, unsigned long lon } } -void XMLHttpRequest::didReceiveResponse(unsigned long identifier, const ResourceResponse& response) +void XMLHttpRequest::didReceiveResponse(unsigned long, const ResourceResponse& response) { - InspectorInstrumentation::didReceiveXHRResponse(scriptExecutionContext(), identifier); - m_response = response; - if (!m_mimeTypeOverride.isEmpty()) { - m_response.setHTTPHeaderField("Content-Type", m_mimeTypeOverride); - m_responseEncoding = extractCharsetFromMediaType(m_mimeTypeOverride); - } + if (!m_mimeTypeOverride.isEmpty()) + m_response.setHTTPHeaderField(HTTPHeaderName::ContentType, m_mimeTypeOverride); +} - if (m_responseEncoding.isEmpty()) - m_responseEncoding = response.textEncodingName(); +static inline bool shouldDecodeResponse(XMLHttpRequest::ResponseType type) +{ + switch (type) { + case XMLHttpRequest::ResponseType::EmptyString: + case XMLHttpRequest::ResponseType::Document: + case XMLHttpRequest::ResponseType::Json: + case XMLHttpRequest::ResponseType::Text: + return true; + case XMLHttpRequest::ResponseType::Arraybuffer: + case XMLHttpRequest::ResponseType::Blob: + return false; + } + ASSERT_NOT_REACHED(); + return true; } void XMLHttpRequest::didReceiveData(const char* data, int len) @@ -1199,7 +1108,13 @@ void XMLHttpRequest::didReceiveData(const char* data, int len) if (m_state < HEADERS_RECEIVED) changeState(HEADERS_RECEIVED); - bool useDecoder = shouldDecodeResponse(); + // FIXME: Should we update "Content-Type" header field with m_mimeTypeOverride value in case it has changed since didReceiveResponse? + if (!m_mimeTypeOverride.isEmpty()) + m_responseEncoding = extractCharsetFromMediaType(m_mimeTypeOverride); + if (m_responseEncoding.isEmpty()) + m_responseEncoding = m_response.textEncodingName(); + + bool useDecoder = shouldDecodeResponse(m_responseType); if (useDecoder && !m_decoder) { if (!m_responseEncoding.isEmpty()) @@ -1209,7 +1124,7 @@ void XMLHttpRequest::didReceiveData(const char* data, int len) m_decoder = TextResourceDecoder::create("application/xml"); // Don't stop on encoding errors, unlike it is done for other kinds of XML resources. This matches the behavior of previous WebKit versions, Firefox and Opera. m_decoder->useLenientXMLDecoding(); - } else if (equalIgnoringCase(responseMIMEType(), "text/html")) + } else if (equalLettersIgnoringASCIICase(responseMIMEType(), "text/html")) m_decoder = TextResourceDecoder::create("text/html", "UTF-8"); else m_decoder = TextResourceDecoder::create("text/plain", "UTF-8"); @@ -1223,7 +1138,7 @@ void XMLHttpRequest::didReceiveData(const char* data, int len) if (useDecoder) m_responseBuilder.append(m_decoder->decode(data, len)); - else if (m_responseTypeCode == ResponseTypeArrayBuffer || m_responseTypeCode == ResponseTypeBlob) { + else { // Buffer binary data. if (!m_binaryResponseBuilder) m_binaryResponseBuilder = SharedBuffer::create(); @@ -1263,19 +1178,19 @@ void XMLHttpRequest::dispatchErrorEvents(const AtomicString& type) m_progressEventThrottle.dispatchProgressEvent(eventNames().loadendEvent); } -#if ENABLE(XHR_TIMEOUT) -void XMLHttpRequest::didTimeout() +void XMLHttpRequest::didReachTimeout() { // internalAbort() calls dropProtection(), which may release the last reference. - Ref<XMLHttpRequest> protect(*this); + Ref<XMLHttpRequest> protectedThis(*this); if (!internalAbort()) return; clearResponse(); clearRequest(); + m_sendFlag = false; m_error = true; - m_exceptionCode = XMLHttpRequestException::TIMEOUT_ERR; + m_exceptionCode = TIMEOUT_ERR; if (!m_async) { m_state = DONE; @@ -1287,21 +1202,55 @@ void XMLHttpRequest::didTimeout() dispatchErrorEvents(eventNames().timeoutEvent); } -#endif -bool XMLHttpRequest::canSuspend() const +bool XMLHttpRequest::canSuspendForDocumentSuspension() const +{ + // If the load event has not fired yet, cancelling the load in suspend() may cause + // the load event to be fired and arbitrary JS execution, which would be unsafe. + // Therefore, we prevent suspending in this case. + return document()->loadEventFinished(); +} + +const char* XMLHttpRequest::activeDOMObjectName() const { - return !m_loader; + return "XMLHttpRequest"; } -void XMLHttpRequest::suspend(ReasonForSuspension) +void XMLHttpRequest::suspend(ReasonForSuspension reason) { m_progressEventThrottle.suspend(); + + if (m_resumeTimer.isActive()) { + m_resumeTimer.stop(); + m_dispatchErrorOnResuming = true; + } + + if (reason == ActiveDOMObject::PageCache && m_loader) { + // Going into PageCache, abort the request and dispatch a network error on resuming. + genericError(); + m_dispatchErrorOnResuming = true; + bool aborted = internalAbort(); + // It should not be possible to restart the load when aborting in suspend() because + // we are not allowed to execute in JS in suspend(). + ASSERT_UNUSED(aborted, aborted); + } } void XMLHttpRequest::resume() { m_progressEventThrottle.resume(); + + // We are not allowed to execute arbitrary JS in resume() so dispatch + // the error event in a timer. + if (m_dispatchErrorOnResuming && !m_resumeTimer.isActive()) + m_resumeTimer.startOneShot(0); +} + +void XMLHttpRequest::resumeTimerFired() +{ + ASSERT(m_dispatchErrorOnResuming); + m_dispatchErrorOnResuming = false; + dispatchErrorEvents(eventNames().errorEvent); } void XMLHttpRequest::stop() |