From fa8c446d0cb3f6f956079afaa9e36cbb0f99b3ef Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 12 Mar 2021 16:22:45 +0000 Subject: [PATCH] URL: Added InputStreamOptions and enable POST data to be sent with URL-encoded parameters. - Added a new URL::createInputStream() overload that takes an InputStreamOptions helper class to simplify stream creation. - Modified the internals of URL and WebInputStream so that parameters are only added to the request body when ParameterHandling is set to inPostData. This allows POST data to be added via URL::withPOSTData() and sent with URL-encoded parameters. --- .../juce_core/native/juce_android_Network.cpp | 20 +- .../juce_core/native/juce_curl_Network.cpp | 25 +-- .../juce_core/native/juce_linux_Network.cpp | 34 ++-- modules/juce_core/native/juce_mac_Network.mm | 16 +- .../juce_core/native/juce_win32_Network.cpp | 21 +- modules/juce_core/network/juce_URL.cpp | 175 +++++++++++++---- modules/juce_core/network/juce_URL.h | 185 ++++++++++++------ .../juce_core/network/juce_WebInputStream.cpp | 32 ++- .../juce_core/network/juce_WebInputStream.h | 8 +- 9 files changed, 353 insertions(+), 163 deletions(-) diff --git a/modules/juce_core/native/juce_android_Network.cpp b/modules/juce_core/native/juce_android_Network.cpp index 2ddd79f977..791c8dbbbc 100644 --- a/modules/juce_core/native/juce_android_Network.cpp +++ b/modules/juce_core/native/juce_android_Network.cpp @@ -324,12 +324,13 @@ class WebInputStream::Pimpl public: enum { contentStreamCacheSize = 1024 }; - Pimpl (WebInputStream&, const URL& urlToCopy, bool shouldBePost) + Pimpl (WebInputStream&, const URL& urlToCopy, bool isPOSTLike) : url (urlToCopy), isContentURL (urlToCopy.getScheme() == "content"), - isPost (shouldBePost), - httpRequest (isPost ? "POST" : "GET") - {} + addParametersToRequestBody (isPOSTLike), + httpRequest (isPOSTLike || url.hasPOSTData() ? "POST" : "GET") + { + } ~Pimpl() { @@ -373,14 +374,14 @@ public: } else { - String address = url.toString (! isPost); + String address = url.toString (! addParametersToRequestBody); if (! address.contains ("://")) address = "http://" + address; MemoryBlock postData; - if (isPost) - WebInputStream::createHeadersAndPostData (url, headers, postData); + if (url.hasPOSTData()) + WebInputStream::createHeadersAndPostData (url, headers, postData, addParametersToRequestBody); jbyteArray postDataArray = nullptr; @@ -406,7 +407,7 @@ public: stream = GlobalRef (LocalRef (env->CallStaticObjectMethod (HTTPStream, HTTPStream.createHTTPStream, javaString (address).get(), - (jboolean) isPost, + (jboolean) addParametersToRequestBody, postDataArray, javaString (headers).get(), (jint) timeOutMs, @@ -535,7 +536,8 @@ public: private: const URL url; - bool isContentURL, isPost, eofStreamReached = false; + const bool isContentURL, addParametersToRequestBody; + bool eofStreamReached = false; int numRedirectsToFollow = 5, timeOutMs = 0; String httpRequest, headers; StringPairArray responseHeaders; diff --git a/modules/juce_core/native/juce_curl_Network.cpp b/modules/juce_core/native/juce_curl_Network.cpp index 00d4283508..47e36e0135 100644 --- a/modules/juce_core/native/juce_curl_Network.cpp +++ b/modules/juce_core/native/juce_curl_Network.cpp @@ -110,9 +110,12 @@ private: class WebInputStream::Pimpl { public: - Pimpl (WebInputStream& ownerStream, const URL& urlToCopy, bool shouldUsePost) - : owner (ownerStream), url (urlToCopy), isPost (shouldUsePost), - httpRequest (isPost ? "POST" : "GET") + Pimpl (WebInputStream& ownerStream, const URL& urlToCopy, bool isPOSTLike) + : owner (ownerStream), + url (urlToCopy), + addParametersToRequestBody (isPOSTLike), + hasPOSTData (url.hasPOSTData()), + httpRequest (isPOSTLike || url.hasPOSTData() ? "POST" : "GET") { jassert (symbols); // Unable to load libcurl! @@ -220,7 +223,7 @@ public: //============================================================================== bool setOptions() { - auto address = url.toString (! isPost); + auto address = url.toString (! addParametersToRequestBody); curl_version_info_data* data = symbols->curl_version_info (CURLVERSION_NOW); jassert (data != nullptr); @@ -228,8 +231,8 @@ public: if (! requestHeaders.endsWithChar ('\n')) requestHeaders << "\r\n"; - if (isPost) - WebInputStream::createHeadersAndPostData (url, requestHeaders, headersAndPostData); + if (hasPOSTData) + WebInputStream::createHeadersAndPostData (url, requestHeaders, headersAndPostData, addParametersToRequestBody); if (! requestHeaders.endsWithChar ('\n')) requestHeaders << "\r\n"; @@ -244,7 +247,7 @@ public: && symbols->curl_easy_setopt (curl, CURLOPT_USERAGENT, userAgent.toRawUTF8()) == CURLE_OK && symbols->curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, (maxRedirects > 0 ? 1 : 0)) == CURLE_OK) { - if (isPost) + if (hasPOSTData) { if (symbols->curl_easy_setopt (curl, CURLOPT_READDATA, this) != CURLE_OK || symbols->curl_easy_setopt (curl, CURLOPT_READFUNCTION, StaticCurlRead) != CURLE_OK) @@ -256,7 +259,7 @@ public: } // handle special http request commands - bool hasSpecialRequestCmd = isPost ? (httpRequest != "POST") : (httpRequest != "GET"); + const auto hasSpecialRequestCmd = hasPOSTData ? (httpRequest != "POST") : (httpRequest != "GET"); if (hasSpecialRequestCmd) if (symbols->curl_easy_setopt (curl, CURLOPT_CUSTOMREQUEST, httpRequest.toRawUTF8()) != CURLE_OK) @@ -323,7 +326,7 @@ public: listener = webInputListener; - if (isPost) + if (hasPOSTData) postBuffer = &headersAndPostData; size_t lastPos = static_cast (-1); @@ -342,7 +345,7 @@ public: singleStep(); // call callbacks if this is a post request - if (isPost && listener != nullptr && lastPos != postPosition) + if (hasPOSTData && listener != nullptr && lastPos != postPosition) { lastPos = postPosition; @@ -613,7 +616,7 @@ public: // Options int timeOutMs = 0; int maxRedirects = 5; - const bool isPost; + const bool addParametersToRequestBody, hasPOSTData; String httpRequest; //============================================================================== diff --git a/modules/juce_core/native/juce_linux_Network.cpp b/modules/juce_core/native/juce_linux_Network.cpp index f323139114..a786baa89d 100644 --- a/modules/juce_core/native/juce_linux_Network.cpp +++ b/modules/juce_core/native/juce_linux_Network.cpp @@ -70,10 +70,13 @@ bool JUCE_CALLTYPE Process::openEmailWithAttachments (const String& /* targetEma class WebInputStream::Pimpl { public: - Pimpl (WebInputStream& pimplOwner, const URL& urlToCopy, bool shouldUsePost) - : owner (pimplOwner), url (urlToCopy), - isPost (shouldUsePost), httpRequestCmd (shouldUsePost ? "POST" : "GET") - {} + Pimpl (WebInputStream& pimplOwner, const URL& urlToCopy, bool isPOSTLike) + : owner (pimplOwner), + url (urlToCopy), + addParametersToRequestBody (isPOSTLike), + httpRequestCmd (isPOSTLike || url.hasPOSTData() ? "POST" : "GET") + { + } ~Pimpl() { @@ -127,7 +130,7 @@ public: return false; } - address = url.toString (! isPost); + address = url.toString (! addParametersToRequestBody); statusCode = createConnection (listener, numRedirectsToFollow); return statusCode != 0; @@ -256,7 +259,7 @@ private: MemoryBlock postData; int64 contentLength = -1, position = 0; bool finished = false; - const bool isPost; + const bool addParametersToRequestBody; int timeOutMs = 0; int numRedirectsToFollow = 5; String httpRequestCmd; @@ -285,8 +288,8 @@ private: { closeSocket (false); - if (isPost) - WebInputStream::createHeadersAndPostData (url, headers, postData); + if (url.hasPOSTData()) + WebInputStream::createHeadersAndPostData (url, headers, postData, addParametersToRequestBody); auto timeOutTime = Time::getMillisecondCounter(); @@ -367,8 +370,8 @@ private: freeaddrinfo (result); { - const MemoryBlock requestHeader (createRequestHeader (hostName, hostPort, proxyName, proxyPort, hostPath, - address, headers, postData, isPost, httpRequestCmd)); + const MemoryBlock requestHeader (createRequestHeader (hostName, hostPort, proxyName, proxyPort, hostPath, address, + headers, postData, httpRequestCmd)); if (! sendHeader (socketHandle, requestHeader, timeOutTime, owner, listener)) { @@ -474,7 +477,7 @@ private: const String& proxyName, int proxyPort, const String& hostPath, const String& originalURL, const String& userHeaders, const MemoryBlock& postData, - bool isPost, const String& httpRequestCmd) + const String& httpRequestCmd) { MemoryOutputStream header; @@ -488,15 +491,18 @@ private: "." JUCE_STRINGIFY(JUCE_BUILDNUMBER)); writeValueIfNotPresent (header, userHeaders, "Connection:", "close"); - if (isPost) - writeValueIfNotPresent (header, userHeaders, "Content-Length:", String ((int) postData.getSize())); + const auto postDataSize = postData.getSize(); + const auto hasPostData = postDataSize > 0; + + if (hasPostData) + writeValueIfNotPresent (header, userHeaders, "Content-Length:", String ((int) postDataSize)); if (userHeaders.isNotEmpty()) header << "\r\n" << userHeaders; header << "\r\n\r\n"; - if (isPost) + if (hasPostData) header << postData; return header.getMemoryBlock(); diff --git a/modules/juce_core/native/juce_mac_Network.mm b/modules/juce_core/native/juce_mac_Network.mm index a092a58398..3a77615c1a 100644 --- a/modules/juce_core/native/juce_mac_Network.mm +++ b/modules/juce_core/native/juce_mac_Network.mm @@ -943,9 +943,11 @@ JUCE_END_IGNORE_WARNINGS_GCC_LIKE class WebInputStream::Pimpl { public: - Pimpl (WebInputStream& pimplOwner, const URL& urlToUse, bool shouldBePost) - : owner (pimplOwner), url (urlToUse), isPost (shouldBePost), - httpRequestCmd (shouldBePost ? "POST" : "GET") + Pimpl (WebInputStream& pimplOwner, const URL& urlToUse, bool isPOSTLike) + : owner (pimplOwner), + url (urlToUse), + addParametersToRequestBody (isPOSTLike), + httpRequestCmd (isPOSTLike || url.hasPOSTData() ? "POST" : "GET") { } @@ -1089,7 +1091,7 @@ private: MemoryBlock postData; int64 position = 0; bool finished = false; - const bool isPost; + const bool addParametersToRequestBody; int timeOutMs = 0; int numRedirectsToFollow = 5; String httpRequestCmd; @@ -1101,7 +1103,7 @@ private: { jassert (connection == nullptr); - if (NSURL* nsURL = [NSURL URLWithString: juceStringToNS (url.toString (! isPost))]) + if (NSURL* nsURL = [NSURL URLWithString: juceStringToNS (url.toString (! addParametersToRequestBody))]) { if (NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL: nsURL cachePolicy: NSURLRequestReloadIgnoringLocalCacheData @@ -1111,9 +1113,9 @@ private: { [req setHTTPMethod: httpMethod]; - if (isPost) + if (url.hasPOSTData()) { - WebInputStream::createHeadersAndPostData (url, headers, postData); + WebInputStream::createHeadersAndPostData (url, headers, postData, addParametersToRequestBody); if (postData.getSize() > 0) [req setHTTPBody: [NSData dataWithBytes: postData.getData() diff --git a/modules/juce_core/native/juce_win32_Network.cpp b/modules/juce_core/native/juce_win32_Network.cpp index c0289fc2fc..eb3a533bed 100644 --- a/modules/juce_core/native/juce_win32_Network.cpp +++ b/modules/juce_core/native/juce_win32_Network.cpp @@ -35,10 +35,13 @@ namespace juce class WebInputStream::Pimpl { public: - Pimpl (WebInputStream& pimplOwner, const URL& urlToCopy, bool shouldBePost) - : statusCode (0), owner (pimplOwner), url (urlToCopy), isPost (shouldBePost), - httpRequestCmd (isPost ? "POST" : "GET") - {} + Pimpl (WebInputStream& pimplOwner, const URL& urlToCopy, bool isPOSTLike) + : owner (pimplOwner), + url (urlToCopy), + addParametersToRequestBody (isPOSTLike), + httpRequestCmd (isPOSTLike || url.hasPOSTData() ? "POST" : "GET") + { + } ~Pimpl() { @@ -75,7 +78,7 @@ public: return false; } - String address = url.toString (! isPost); + auto address = url.toString (! addParametersToRequestBody); while (numRedirectsToFollow-- >= 0) { @@ -226,7 +229,7 @@ public: return true; } - int statusCode; + int statusCode = 0; private: //============================================================================== @@ -237,7 +240,7 @@ private: MemoryBlock postData; int64 position = 0; bool finished = false; - const bool isPost; + const bool addParametersToRequestBody; int timeOutMs = 0; String httpRequestCmd; int numRedirectsToFollow = 5; @@ -288,8 +291,8 @@ private: uc.lpszPassword = password; uc.dwPasswordLength = passwordNumChars; - if (isPost) - WebInputStream::createHeadersAndPostData (url, headers, postData); + if (url.hasPOSTData()) + WebInputStream::createHeadersAndPostData (url, headers, postData, addParametersToRequestBody); if (InternetCrackUrl (address.toWideCharPointer(), 0, 0, &uc)) openConnection (uc, sessionHandle, address, listener); diff --git a/modules/juce_core/network/juce_URL.cpp b/modules/juce_core/network/juce_URL.cpp index 36307db8d6..e9dd39ab83 100644 --- a/modules/juce_core/network/juce_URL.cpp +++ b/modules/juce_core/network/juce_URL.cpp @@ -377,6 +377,11 @@ String URL::getFileName() const } #endif +URL::ParameterHandling URL::toHandling (bool usePostData) +{ + return usePostData ? ParameterHandling::inPostData : ParameterHandling::inAddress; +} + File URL::fileFromFileSchemeURL (const URL& fileURL) { if (! fileURL.isLocalFile()) @@ -447,7 +452,9 @@ URL URL::getChildURL (const String& subPath) const return u; } -void URL::createHeadersAndPostData (String& headers, MemoryBlock& postDataToWrite) const +void URL::createHeadersAndPostData (String& headers, + MemoryBlock& postDataToWrite, + bool addParametersToBody) const { MemoryOutputStream data (postDataToWrite, false); @@ -491,8 +498,10 @@ void URL::createHeadersAndPostData (String& headers, MemoryBlock& postDataToWrit } else { - data << URLHelpers::getMangledParameters (*this) - << postData; + if (addParametersToBody) + data << URLHelpers::getMangledParameters (*this) << postData; + else + data << postData; // if the user-supplied headers didn't contain a content-type, add one now.. if (! headers.containsIgnoreCase ("Content-Type")) @@ -656,73 +665,127 @@ private: } }; #endif +//============================================================================== +template +static URL::InputStreamOptions with (URL::InputStreamOptions options, Member&& member, Item&& item) +{ + options.*member = std::forward (item); + return options; +} + +URL::InputStreamOptions::InputStreamOptions (ParameterHandling handling) : parameterHandling (handling) {} + +URL::InputStreamOptions URL::InputStreamOptions::withProgressCallback (std::function cb) const +{ + return with (*this, &InputStreamOptions::progressCallback, std::move (cb)); +} + +URL::InputStreamOptions URL::InputStreamOptions::withExtraHeaders (const String& headers) const +{ + return with (*this, &InputStreamOptions::extraHeaders, headers); +} + +URL::InputStreamOptions URL::InputStreamOptions::withConnectionTimeoutMs (int timeout) const +{ + return with (*this, &InputStreamOptions::connectionTimeOutMs, timeout); +} + +URL::InputStreamOptions URL::InputStreamOptions::withResponseHeaders (StringPairArray* headers) const +{ + return with (*this, &InputStreamOptions::responseHeaders, headers); +} + +URL::InputStreamOptions URL::InputStreamOptions::withStatusCode (int* status) const +{ + return with (*this, &InputStreamOptions::statusCode, status); +} + +URL::InputStreamOptions URL::InputStreamOptions::withNumRedirectsToFollow (int numRedirects) const +{ + return with (*this, &InputStreamOptions::numRedirectsToFollow, numRedirects); +} + +URL::InputStreamOptions URL::InputStreamOptions::withHttpRequestCmd (const String& cmd) const +{ + return with (*this, &InputStreamOptions::httpRequestCmd, cmd); +} //============================================================================== -std::unique_ptr URL::createInputStream (bool usePostCommand, - OpenStreamProgressCallback* progressCallback, - void* progressCallbackContext, - String headers, - int timeOutMs, - StringPairArray* responseHeaders, - int* statusCode, - int numRedirectsToFollow, - String httpRequestCmd) const +std::unique_ptr URL::createInputStream (const InputStreamOptions& options) const { if (isLocalFile()) { #if JUCE_IOS // We may need to refresh the embedded bookmark. - return std::make_unique> (const_cast(*this)); + return std::make_unique> (const_cast (*this)); #else return getLocalFile().createInputStream(); #endif } - auto wi = std::make_unique (*this, usePostCommand); + auto webInputStream = [&] + { + const auto usePost = options.getParameterHandling() == ParameterHandling::inPostData; + auto stream = std::make_unique (*this, usePost); + + auto extraHeaders = options.getExtraHeaders(); + + if (extraHeaders.isNotEmpty()) + stream->withExtraHeaders (extraHeaders); + + auto timeout = options.getConnectionTimeoutMs(); + + if (timeout != 0) + stream->withConnectionTimeout (timeout); + + auto requestCmd = options.getHttpRequestCmd(); + + if (requestCmd.isNotEmpty()) + stream->withCustomRequestCommand (requestCmd); + + stream->withNumRedirectsToFollow (options.getNumRedirectsToFollow()); + + return stream; + }(); struct ProgressCallbackCaller : public WebInputStream::Listener { - ProgressCallbackCaller (OpenStreamProgressCallback* progressCallbackToUse, void* progressCallbackContextToUse) - : callback (progressCallbackToUse), data (progressCallbackContextToUse) - {} + ProgressCallbackCaller (std::function progressCallbackToUse) + : callback (std::move (progressCallbackToUse)) + { + } bool postDataSendProgress (WebInputStream&, int bytesSent, int totalBytes) override { - return callback (data, bytesSent, totalBytes); + return callback (bytesSent, totalBytes); } - OpenStreamProgressCallback* callback; - void* const data; + std::function callback; }; - std::unique_ptr callbackCaller - (progressCallback != nullptr ? new ProgressCallbackCaller (progressCallback, progressCallbackContext) : nullptr); - - if (headers.isNotEmpty()) - wi->withExtraHeaders (headers); - - if (timeOutMs != 0) - wi->withConnectionTimeout (timeOutMs); - - if (httpRequestCmd.isNotEmpty()) - wi->withCustomRequestCommand (httpRequestCmd); + auto callbackCaller = [&options]() -> std::unique_ptr + { + if (auto progressCallback = options.getProgressCallback()) + return std::make_unique (progressCallback); - wi->withNumRedirectsToFollow (numRedirectsToFollow); + return {}; + }(); - bool success = wi->connect (callbackCaller.get()); + auto success = webInputStream->connect (callbackCaller.get()); - if (statusCode != nullptr) - *statusCode = wi->getStatusCode(); + if (auto* status = options.getStatusCode()) + *status = webInputStream->getStatusCode(); - if (responseHeaders != nullptr) - *responseHeaders = wi->getResponseHeaders(); + if (auto* responseHeaders = options.getResponseHeaders()) + *responseHeaders = webInputStream->getResponseHeaders(); - if (! success || wi->isError()) + if (! success || webInputStream->isError()) return nullptr; - // Older GCCs complain about binding unique_ptr&& to unique_ptr - // if we just `return wi` here. - return std::unique_ptr (std::move (wi)); + // std::move() needed here for older compilers + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wredundant-move") + return std::move (webInputStream); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE } #if JUCE_ANDROID @@ -752,7 +815,7 @@ std::unique_ptr URL::createOutputStream() const bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) const { const std::unique_ptr in (isLocalFile() ? getLocalFile().createInputStream() - : createInputStream (usePostCommand)); + : createInputStream (InputStreamOptions (toHandling (usePostCommand)))); if (in != nullptr) { @@ -766,7 +829,7 @@ bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) co String URL::readEntireTextStream (bool usePostCommand) const { const std::unique_ptr in (isLocalFile() ? getLocalFile().createInputStream() - : createInputStream (usePostCommand)); + : createInputStream (InputStreamOptions (toHandling (usePostCommand)))); if (in != nullptr) return in->readEntireStreamAsString(); @@ -911,4 +974,30 @@ bool URL::launchInDefaultBrowser() const return Process::openDocument (u, {}); } +//============================================================================== +std::unique_ptr URL::createInputStream (bool usePostCommand, + OpenStreamProgressCallback* cb, + void* context, + String headers, + int timeOutMs, + StringPairArray* responseHeaders, + int* statusCode, + int numRedirectsToFollow, + String httpRequestCmd) const +{ + std::function callback; + + if (cb != nullptr) + callback = [context, cb] (int sent, int total) { return cb (context, sent, total); }; + + return createInputStream (InputStreamOptions (toHandling (usePostCommand)) + .withProgressCallback (std::move (callback)) + .withExtraHeaders (headers) + .withConnectionTimeoutMs (timeOutMs) + .withResponseHeaders (responseHeaders) + .withStatusCode (statusCode) + .withNumRedirectsToFollow(numRedirectsToFollow) + .withHttpRequestCmd (httpRequestCmd)); +} + } // namespace juce diff --git a/modules/juce_core/network/juce_URL.h b/modules/juce_core/network/juce_URL.h index 2d03bd4bdf..293903a8b4 100644 --- a/modules/juce_core/network/juce_URL.h +++ b/modules/juce_core/network/juce_URL.h @@ -258,38 +258,33 @@ public: /** Returns a copy of this URL, with a block of data to send as the POST data. - If you're setting the POST data, be careful not to have any parameters set - as well, otherwise it'll all get thrown in together, and might not have the - desired effect. - If the URL already contains some POST data, this will replace it, rather than being appended to it. - This data will only be used if you specify a post operation when you call - createInputStream(). + If no HTTP command is set when calling createInputStream() to read from + this URL and some data has been set, it will do a POST request. */ URL withPOSTData (const String& postData) const; /** Returns a copy of this URL, with a block of data to send as the POST data. - If you're setting the POST data, be careful not to have any parameters set - as well, otherwise it'll all get thrown in together, and might not have the - desired effect. - If the URL already contains some POST data, this will replace it, rather than being appended to it. - This data will only be used if you specify a post operation when you call - createInputStream(). + If no HTTP command is set when calling createInputStream() to read from + this URL and some data has been set, it will do a POST request. */ URL withPOSTData (const MemoryBlock& postData) const; /** Returns the data that was set using withPOSTData(). */ String getPostData() const { return postData.toString(); } - /** Returns the data that was set using withPOSTData() as MemoryBlock. */ + /** Returns the data that was set using withPOSTData() as a MemoryBlock. */ const MemoryBlock& getPostDataAsMemoryBlock() const noexcept { return postData; } + /** Returns true if this URL has POST data set using withPOSTData(). */ + bool hasPOSTData() const noexcept { return postData.getSize() > 0; } + //============================================================================== /** Tries to launch the system's default browser to open the URL. @@ -308,13 +303,103 @@ public: */ static bool isProbablyAnEmailAddress (const String& possibleEmailAddress); - //============================================================================== - /** This callback function can be used by the createInputStream() method. + enum class ParameterHandling + { + inAddress, + inPostData + }; - It allows your app to receive progress updates during a lengthy POST operation. If you - want to continue the operation, this should return true, or false to abort. + //============================================================================== + /** Class used to create a set of options to pass to the createInputStream() method. + + You can chain together a series of calls to this class's methods to create + a set of whatever options you want to specify, e.g. + @code + if (auto inputStream = URL ("http://www.xyz.com/foobar") + .createInputStream (URL::InputStreamOptions (false).withConnectionTimeoutMs (1000) + .withNumRedirectsToFollow (0))) + { + ... + } + @endcode */ - using OpenStreamProgressCallback = bool (void* context, int bytesSent, int totalBytes); + class JUCE_API InputStreamOptions + { + public: + /** Constructor. + + If parameterHandling is ParameterHandling::inPostData and the URL contains some + POST data to send set via one of its withPOSTData() methods, the URL parameters + will be transferred via the request body data. Otherwise the parameters will + be added to the URL address. + */ + explicit InputStreamOptions (ParameterHandling parameterHandling); + + //============================================================================== + /** A callback function to keep track of the operation's progress. + + This can be useful for lengthy POST operations, so that you can provide user feedback. + */ + InputStreamOptions withProgressCallback (std::function progressCallback) const; + + /** A string that will be appended onto the headers that are used for the request. + + It must be a valid set of HTML header directives, separated by newlines. + */ + InputStreamOptions withExtraHeaders (const String& extraHeaders) const; + + /** Specifies a timeout for the request in milliseconds. + + If 0, this will use whatever default setting the OS chooses. If a negative + number, it will be infinite. + */ + InputStreamOptions withConnectionTimeoutMs (int connectionTimeoutMs) const; + + /** If this is non-null, all the (key, value) pairs received as headers + in the response will be stored in this array. + */ + InputStreamOptions withResponseHeaders (StringPairArray* responseHeaders) const; + + /** If this is non-null, it will get set to the http status code, if one + is known, or 0 if a code isn't available. + */ + InputStreamOptions withStatusCode (int* statusCode) const; + + /** Specifies the number of redirects that will be followed before returning a response. + + N.B. This will be ignored on Android which follows up to 5 redirects. + */ + InputStreamOptions withNumRedirectsToFollow (int numRedirectsToFollow) const; + + /** Specifies which HTTP request to use. + + If this is not set, then this will be determined by the value of `doPostLikeRequest` + or the presence of POST data set via URL::withPOSTData(). + */ + InputStreamOptions withHttpRequestCmd (const String& httpRequestCmd) const; + + //============================================================================== + ParameterHandling getParameterHandling() const noexcept { return parameterHandling; } + std::function getProgressCallback() const noexcept { return progressCallback; } + String getExtraHeaders() const noexcept { return extraHeaders; } + int getConnectionTimeoutMs() const noexcept { return connectionTimeOutMs; } + StringPairArray* getResponseHeaders() const noexcept { return responseHeaders; } + int* getStatusCode() const noexcept { return statusCode; } + int getNumRedirectsToFollow() const noexcept { return numRedirectsToFollow; } + String getHttpRequestCmd() const noexcept { return httpRequestCmd; } + + private: + //============================================================================== + const ParameterHandling parameterHandling; + + std::function progressCallback = nullptr; + String extraHeaders; + int connectionTimeOutMs = 0; + StringPairArray* responseHeaders = nullptr; + int* statusCode = nullptr; + int numRedirectsToFollow = 5; + String httpRequestCmd; + }; /** Attempts to open a stream that can read from this URL. @@ -330,43 +415,11 @@ public: If the URL represents a local file, then this method simply returns a FileInputStream. - @param doPostLikeRequest if true, the parameters added to this class will be transferred - via the HTTP headers which is typical for POST requests. Otherwise - the parameters will be added to the URL address. Additionally, - if the parameter httpRequestCmd is not specified (or empty) then this - parameter will determine which HTTP request command will be used - (POST or GET). - @param progressCallback if this is not a nullptr, it lets you supply a callback function - to keep track of the operation's progress. This can be useful - for lengthy POST operations, so that you can provide user feedback. - @param progressCallbackContext if a callback is specified, this value will be passed to - the function - @param extraHeaders if not empty, this string is appended onto the headers that - are used for the request. It must therefore be a valid set of HTML - header directives, separated by newlines. - @param connectionTimeOutMs if 0, this will use whatever default setting the OS chooses. If - a negative number, it will be infinite. Otherwise it specifies a - time in milliseconds. - @param responseHeaders if this is non-null, all the (key, value) pairs received as headers - in the response will be stored in this array - @param statusCode if this is non-null, it will get set to the http status code, if one - is known, or 0 if a code isn't available - @param numRedirectsToFollow specifies the number of redirects that will be followed before - returning a response (ignored for Android which follows up to 5 redirects) - @param httpRequestCmd Specify which HTTP Request to use. If this is empty, then doPostRequest - will determine the HTTP request. - - @returns a valid input stream, or nullptr if there was an error trying to open it. - */ - std::unique_ptr createInputStream (bool doPostLikeRequest, - OpenStreamProgressCallback* progressCallback = nullptr, - void* progressCallbackContext = nullptr, - String extraHeaders = {}, - int connectionTimeOutMs = 0, - StringPairArray* responseHeaders = nullptr, - int* statusCode = nullptr, - int numRedirectsToFollow = 5, - String httpRequestCmd = {}) const; + @param options a set of options that will be used when opening the stream. + + @returns a valid input stream, or nullptr if there was an error trying to open it. + */ + std::unique_ptr createInputStream (const InputStreamOptions& options) const; /** Attempts to open an output stream to a URL for writing @@ -558,6 +611,25 @@ public: */ static URL createWithoutParsing (const String& url); + //============================================================================== + using OpenStreamProgressCallback = bool (void* context, int bytesSent, int totalBytes); + + /** This method has been deprecated. + + New code should use the method which takes an InputStreamOptions argument instead. + + @see InputStreamOptions + */ + std::unique_ptr createInputStream (bool doPostLikeRequest, + OpenStreamProgressCallback* progressCallback = nullptr, + void* progressCallbackContext = nullptr, + String extraHeaders = {}, + int connectionTimeOutMs = 0, + StringPairArray* responseHeaders = nullptr, + int* statusCode = nullptr, + int numRedirectsToFollow = 5, + String httpRequestCmd = {}) const; + private: //============================================================================== #if JUCE_IOS @@ -594,9 +666,10 @@ private: URL (const String&, int); void init(); void addParameter (const String&, const String&); - void createHeadersAndPostData (String&, MemoryBlock&) const; + void createHeadersAndPostData (String&, MemoryBlock&, bool) const; URL withUpload (Upload*) const; + static ParameterHandling toHandling (bool); static File fileFromFileSchemeURL (const URL&); String getDomainInternal (bool) const; diff --git a/modules/juce_core/network/juce_WebInputStream.cpp b/modules/juce_core/network/juce_WebInputStream.cpp index 38eda1502d..5d6d3a04bf 100644 --- a/modules/juce_core/network/juce_WebInputStream.cpp +++ b/modules/juce_core/network/juce_WebInputStream.cpp @@ -24,13 +24,12 @@ namespace juce { WebInputStream::WebInputStream (const URL& url, const bool usePost) - : pimpl (new Pimpl (*this, url, usePost)), hasCalledConnect (false) + : pimpl (std::make_unique (*this, url, usePost)) { } WebInputStream::~WebInputStream() { - delete pimpl; } WebInputStream& WebInputStream::withExtraHeaders (const String& extra) { pimpl->withExtraHeaders (extra); return *this; } @@ -60,28 +59,41 @@ bool WebInputStream::connect (Listener* listener) StringPairArray WebInputStream::parseHttpHeaders (const String& headerData) { StringPairArray headerPairs; - StringArray headerLines = StringArray::fromLines (headerData); + auto headerLines = StringArray::fromLines (headerData); // ignore the first line as this is the status line for (int i = 1; i < headerLines.size(); ++i) { - const String& headersEntry = headerLines[i]; + const auto& headersEntry = headerLines[i]; if (headersEntry.isNotEmpty()) { - const String key (headersEntry.upToFirstOccurrenceOf (": ", false, false)); - const String value (headersEntry.fromFirstOccurrenceOf (": ", false, false)); - const String previousValue (headerPairs [key]); - headerPairs.set (key, previousValue.isEmpty() ? value : (previousValue + "," + value)); + const auto key = headersEntry.upToFirstOccurrenceOf (": ", false, false); + + auto value = [&headersEntry, &headerPairs, &key] + { + const auto currentValue = headersEntry.fromFirstOccurrenceOf (": ", false, false); + const auto previousValue = headerPairs [key]; + + if (previousValue.isNotEmpty()) + return previousValue + "," + currentValue; + + return currentValue; + }(); + + headerPairs.set (key, value); } } return headerPairs; } -void WebInputStream::createHeadersAndPostData (const URL& aURL, String& headers, MemoryBlock& data) +void WebInputStream::createHeadersAndPostData (const URL& aURL, + String& headers, + MemoryBlock& data, + bool addParametersToBody) { - aURL.createHeadersAndPostData (headers, data); + aURL.createHeadersAndPostData (headers, data, addParametersToBody); } } // namespace juce diff --git a/modules/juce_core/network/juce_WebInputStream.h b/modules/juce_core/network/juce_WebInputStream.h index 126fcecf68..f7cf04292a 100644 --- a/modules/juce_core/network/juce_WebInputStream.h +++ b/modules/juce_core/network/juce_WebInputStream.h @@ -220,14 +220,14 @@ class JUCE_API WebInputStream : public InputStream bool setPosition (int64 wantedPos) override; private: - static void createHeadersAndPostData (const URL&, String&, MemoryBlock&); - static StringPairArray parseHttpHeaders (const String& headerData); + static void createHeadersAndPostData (const URL&, String&, MemoryBlock&, bool); + static StringPairArray parseHttpHeaders (const String&); class Pimpl; friend class Pimpl; - Pimpl* const pimpl; - bool hasCalledConnect; + std::unique_ptr pimpl; + bool hasCalledConnect = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebInputStream) };