- 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.tags/2021-05-28
@@ -324,12 +324,13 @@ class WebInputStream::Pimpl | |||||
public: | public: | ||||
enum { contentStreamCacheSize = 1024 }; | enum { contentStreamCacheSize = 1024 }; | ||||
Pimpl (WebInputStream&, const URL& urlToCopy, bool shouldBePost) | |||||
Pimpl (WebInputStream&, const URL& urlToCopy, bool isPOSTLike) | |||||
: url (urlToCopy), | : url (urlToCopy), | ||||
isContentURL (urlToCopy.getScheme() == "content"), | isContentURL (urlToCopy.getScheme() == "content"), | ||||
isPost (shouldBePost), | |||||
httpRequest (isPost ? "POST" : "GET") | |||||
{} | |||||
addParametersToRequestBody (isPOSTLike), | |||||
httpRequest (isPOSTLike || url.hasPOSTData() ? "POST" : "GET") | |||||
{ | |||||
} | |||||
~Pimpl() | ~Pimpl() | ||||
{ | { | ||||
@@ -373,14 +374,14 @@ public: | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
String address = url.toString (! isPost); | |||||
String address = url.toString (! addParametersToRequestBody); | |||||
if (! address.contains ("://")) | if (! address.contains ("://")) | ||||
address = "http://" + address; | address = "http://" + address; | ||||
MemoryBlock postData; | MemoryBlock postData; | ||||
if (isPost) | |||||
WebInputStream::createHeadersAndPostData (url, headers, postData); | |||||
if (url.hasPOSTData()) | |||||
WebInputStream::createHeadersAndPostData (url, headers, postData, addParametersToRequestBody); | |||||
jbyteArray postDataArray = nullptr; | jbyteArray postDataArray = nullptr; | ||||
@@ -406,7 +407,7 @@ public: | |||||
stream = GlobalRef (LocalRef<jobject> (env->CallStaticObjectMethod (HTTPStream, | stream = GlobalRef (LocalRef<jobject> (env->CallStaticObjectMethod (HTTPStream, | ||||
HTTPStream.createHTTPStream, | HTTPStream.createHTTPStream, | ||||
javaString (address).get(), | javaString (address).get(), | ||||
(jboolean) isPost, | |||||
(jboolean) addParametersToRequestBody, | |||||
postDataArray, | postDataArray, | ||||
javaString (headers).get(), | javaString (headers).get(), | ||||
(jint) timeOutMs, | (jint) timeOutMs, | ||||
@@ -535,7 +536,8 @@ public: | |||||
private: | private: | ||||
const URL url; | const URL url; | ||||
bool isContentURL, isPost, eofStreamReached = false; | |||||
const bool isContentURL, addParametersToRequestBody; | |||||
bool eofStreamReached = false; | |||||
int numRedirectsToFollow = 5, timeOutMs = 0; | int numRedirectsToFollow = 5, timeOutMs = 0; | ||||
String httpRequest, headers; | String httpRequest, headers; | ||||
StringPairArray responseHeaders; | StringPairArray responseHeaders; | ||||
@@ -110,9 +110,12 @@ private: | |||||
class WebInputStream::Pimpl | class WebInputStream::Pimpl | ||||
{ | { | ||||
public: | 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! | jassert (symbols); // Unable to load libcurl! | ||||
@@ -220,7 +223,7 @@ public: | |||||
//============================================================================== | //============================================================================== | ||||
bool setOptions() | bool setOptions() | ||||
{ | { | ||||
auto address = url.toString (! isPost); | |||||
auto address = url.toString (! addParametersToRequestBody); | |||||
curl_version_info_data* data = symbols->curl_version_info (CURLVERSION_NOW); | curl_version_info_data* data = symbols->curl_version_info (CURLVERSION_NOW); | ||||
jassert (data != nullptr); | jassert (data != nullptr); | ||||
@@ -228,8 +231,8 @@ public: | |||||
if (! requestHeaders.endsWithChar ('\n')) | if (! requestHeaders.endsWithChar ('\n')) | ||||
requestHeaders << "\r\n"; | requestHeaders << "\r\n"; | ||||
if (isPost) | |||||
WebInputStream::createHeadersAndPostData (url, requestHeaders, headersAndPostData); | |||||
if (hasPOSTData) | |||||
WebInputStream::createHeadersAndPostData (url, requestHeaders, headersAndPostData, addParametersToRequestBody); | |||||
if (! requestHeaders.endsWithChar ('\n')) | if (! requestHeaders.endsWithChar ('\n')) | ||||
requestHeaders << "\r\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_USERAGENT, userAgent.toRawUTF8()) == CURLE_OK | ||||
&& symbols->curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, (maxRedirects > 0 ? 1 : 0)) == 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 | if (symbols->curl_easy_setopt (curl, CURLOPT_READDATA, this) != CURLE_OK | ||||
|| symbols->curl_easy_setopt (curl, CURLOPT_READFUNCTION, StaticCurlRead) != CURLE_OK) | || symbols->curl_easy_setopt (curl, CURLOPT_READFUNCTION, StaticCurlRead) != CURLE_OK) | ||||
@@ -256,7 +259,7 @@ public: | |||||
} | } | ||||
// handle special http request commands | // handle special http request commands | ||||
bool hasSpecialRequestCmd = isPost ? (httpRequest != "POST") : (httpRequest != "GET"); | |||||
const auto hasSpecialRequestCmd = hasPOSTData ? (httpRequest != "POST") : (httpRequest != "GET"); | |||||
if (hasSpecialRequestCmd) | if (hasSpecialRequestCmd) | ||||
if (symbols->curl_easy_setopt (curl, CURLOPT_CUSTOMREQUEST, httpRequest.toRawUTF8()) != CURLE_OK) | if (symbols->curl_easy_setopt (curl, CURLOPT_CUSTOMREQUEST, httpRequest.toRawUTF8()) != CURLE_OK) | ||||
@@ -323,7 +326,7 @@ public: | |||||
listener = webInputListener; | listener = webInputListener; | ||||
if (isPost) | |||||
if (hasPOSTData) | |||||
postBuffer = &headersAndPostData; | postBuffer = &headersAndPostData; | ||||
size_t lastPos = static_cast<size_t> (-1); | size_t lastPos = static_cast<size_t> (-1); | ||||
@@ -342,7 +345,7 @@ public: | |||||
singleStep(); | singleStep(); | ||||
// call callbacks if this is a post request | // call callbacks if this is a post request | ||||
if (isPost && listener != nullptr && lastPos != postPosition) | |||||
if (hasPOSTData && listener != nullptr && lastPos != postPosition) | |||||
{ | { | ||||
lastPos = postPosition; | lastPos = postPosition; | ||||
@@ -613,7 +616,7 @@ public: | |||||
// Options | // Options | ||||
int timeOutMs = 0; | int timeOutMs = 0; | ||||
int maxRedirects = 5; | int maxRedirects = 5; | ||||
const bool isPost; | |||||
const bool addParametersToRequestBody, hasPOSTData; | |||||
String httpRequest; | String httpRequest; | ||||
//============================================================================== | //============================================================================== | ||||
@@ -70,10 +70,13 @@ bool JUCE_CALLTYPE Process::openEmailWithAttachments (const String& /* targetEma | |||||
class WebInputStream::Pimpl | class WebInputStream::Pimpl | ||||
{ | { | ||||
public: | 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() | ~Pimpl() | ||||
{ | { | ||||
@@ -127,7 +130,7 @@ public: | |||||
return false; | return false; | ||||
} | } | ||||
address = url.toString (! isPost); | |||||
address = url.toString (! addParametersToRequestBody); | |||||
statusCode = createConnection (listener, numRedirectsToFollow); | statusCode = createConnection (listener, numRedirectsToFollow); | ||||
return statusCode != 0; | return statusCode != 0; | ||||
@@ -256,7 +259,7 @@ private: | |||||
MemoryBlock postData; | MemoryBlock postData; | ||||
int64 contentLength = -1, position = 0; | int64 contentLength = -1, position = 0; | ||||
bool finished = false; | bool finished = false; | ||||
const bool isPost; | |||||
const bool addParametersToRequestBody; | |||||
int timeOutMs = 0; | int timeOutMs = 0; | ||||
int numRedirectsToFollow = 5; | int numRedirectsToFollow = 5; | ||||
String httpRequestCmd; | String httpRequestCmd; | ||||
@@ -285,8 +288,8 @@ private: | |||||
{ | { | ||||
closeSocket (false); | closeSocket (false); | ||||
if (isPost) | |||||
WebInputStream::createHeadersAndPostData (url, headers, postData); | |||||
if (url.hasPOSTData()) | |||||
WebInputStream::createHeadersAndPostData (url, headers, postData, addParametersToRequestBody); | |||||
auto timeOutTime = Time::getMillisecondCounter(); | auto timeOutTime = Time::getMillisecondCounter(); | ||||
@@ -367,8 +370,8 @@ private: | |||||
freeaddrinfo (result); | 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)) | if (! sendHeader (socketHandle, requestHeader, timeOutTime, owner, listener)) | ||||
{ | { | ||||
@@ -474,7 +477,7 @@ private: | |||||
const String& proxyName, int proxyPort, | const String& proxyName, int proxyPort, | ||||
const String& hostPath, const String& originalURL, | const String& hostPath, const String& originalURL, | ||||
const String& userHeaders, const MemoryBlock& postData, | const String& userHeaders, const MemoryBlock& postData, | ||||
bool isPost, const String& httpRequestCmd) | |||||
const String& httpRequestCmd) | |||||
{ | { | ||||
MemoryOutputStream header; | MemoryOutputStream header; | ||||
@@ -488,15 +491,18 @@ private: | |||||
"." JUCE_STRINGIFY(JUCE_BUILDNUMBER)); | "." JUCE_STRINGIFY(JUCE_BUILDNUMBER)); | ||||
writeValueIfNotPresent (header, userHeaders, "Connection:", "close"); | 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()) | if (userHeaders.isNotEmpty()) | ||||
header << "\r\n" << userHeaders; | header << "\r\n" << userHeaders; | ||||
header << "\r\n\r\n"; | header << "\r\n\r\n"; | ||||
if (isPost) | |||||
if (hasPostData) | |||||
header << postData; | header << postData; | ||||
return header.getMemoryBlock(); | return header.getMemoryBlock(); | ||||
@@ -943,9 +943,11 @@ JUCE_END_IGNORE_WARNINGS_GCC_LIKE | |||||
class WebInputStream::Pimpl | class WebInputStream::Pimpl | ||||
{ | { | ||||
public: | 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; | MemoryBlock postData; | ||||
int64 position = 0; | int64 position = 0; | ||||
bool finished = false; | bool finished = false; | ||||
const bool isPost; | |||||
const bool addParametersToRequestBody; | |||||
int timeOutMs = 0; | int timeOutMs = 0; | ||||
int numRedirectsToFollow = 5; | int numRedirectsToFollow = 5; | ||||
String httpRequestCmd; | String httpRequestCmd; | ||||
@@ -1101,7 +1103,7 @@ private: | |||||
{ | { | ||||
jassert (connection == nullptr); | 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 | if (NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL: nsURL | ||||
cachePolicy: NSURLRequestReloadIgnoringLocalCacheData | cachePolicy: NSURLRequestReloadIgnoringLocalCacheData | ||||
@@ -1111,9 +1113,9 @@ private: | |||||
{ | { | ||||
[req setHTTPMethod: httpMethod]; | [req setHTTPMethod: httpMethod]; | ||||
if (isPost) | |||||
if (url.hasPOSTData()) | |||||
{ | { | ||||
WebInputStream::createHeadersAndPostData (url, headers, postData); | |||||
WebInputStream::createHeadersAndPostData (url, headers, postData, addParametersToRequestBody); | |||||
if (postData.getSize() > 0) | if (postData.getSize() > 0) | ||||
[req setHTTPBody: [NSData dataWithBytes: postData.getData() | [req setHTTPBody: [NSData dataWithBytes: postData.getData() | ||||
@@ -35,10 +35,13 @@ namespace juce | |||||
class WebInputStream::Pimpl | class WebInputStream::Pimpl | ||||
{ | { | ||||
public: | 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() | ~Pimpl() | ||||
{ | { | ||||
@@ -75,7 +78,7 @@ public: | |||||
return false; | return false; | ||||
} | } | ||||
String address = url.toString (! isPost); | |||||
auto address = url.toString (! addParametersToRequestBody); | |||||
while (numRedirectsToFollow-- >= 0) | while (numRedirectsToFollow-- >= 0) | ||||
{ | { | ||||
@@ -226,7 +229,7 @@ public: | |||||
return true; | return true; | ||||
} | } | ||||
int statusCode; | |||||
int statusCode = 0; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -237,7 +240,7 @@ private: | |||||
MemoryBlock postData; | MemoryBlock postData; | ||||
int64 position = 0; | int64 position = 0; | ||||
bool finished = false; | bool finished = false; | ||||
const bool isPost; | |||||
const bool addParametersToRequestBody; | |||||
int timeOutMs = 0; | int timeOutMs = 0; | ||||
String httpRequestCmd; | String httpRequestCmd; | ||||
int numRedirectsToFollow = 5; | int numRedirectsToFollow = 5; | ||||
@@ -288,8 +291,8 @@ private: | |||||
uc.lpszPassword = password; | uc.lpszPassword = password; | ||||
uc.dwPasswordLength = passwordNumChars; | 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)) | if (InternetCrackUrl (address.toWideCharPointer(), 0, 0, &uc)) | ||||
openConnection (uc, sessionHandle, address, listener); | openConnection (uc, sessionHandle, address, listener); | ||||
@@ -377,6 +377,11 @@ String URL::getFileName() const | |||||
} | } | ||||
#endif | #endif | ||||
URL::ParameterHandling URL::toHandling (bool usePostData) | |||||
{ | |||||
return usePostData ? ParameterHandling::inPostData : ParameterHandling::inAddress; | |||||
} | |||||
File URL::fileFromFileSchemeURL (const URL& fileURL) | File URL::fileFromFileSchemeURL (const URL& fileURL) | ||||
{ | { | ||||
if (! fileURL.isLocalFile()) | if (! fileURL.isLocalFile()) | ||||
@@ -447,7 +452,9 @@ URL URL::getChildURL (const String& subPath) const | |||||
return u; | return u; | ||||
} | } | ||||
void URL::createHeadersAndPostData (String& headers, MemoryBlock& postDataToWrite) const | |||||
void URL::createHeadersAndPostData (String& headers, | |||||
MemoryBlock& postDataToWrite, | |||||
bool addParametersToBody) const | |||||
{ | { | ||||
MemoryOutputStream data (postDataToWrite, false); | MemoryOutputStream data (postDataToWrite, false); | ||||
@@ -491,8 +498,10 @@ void URL::createHeadersAndPostData (String& headers, MemoryBlock& postDataToWrit | |||||
} | } | ||||
else | 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 the user-supplied headers didn't contain a content-type, add one now.. | ||||
if (! headers.containsIgnoreCase ("Content-Type")) | if (! headers.containsIgnoreCase ("Content-Type")) | ||||
@@ -656,73 +665,127 @@ private: | |||||
} | } | ||||
}; | }; | ||||
#endif | #endif | ||||
//============================================================================== | |||||
template <typename Member, typename Item> | |||||
static URL::InputStreamOptions with (URL::InputStreamOptions options, Member&& member, Item&& item) | |||||
{ | |||||
options.*member = std::forward<Item> (item); | |||||
return options; | |||||
} | |||||
URL::InputStreamOptions::InputStreamOptions (ParameterHandling handling) : parameterHandling (handling) {} | |||||
URL::InputStreamOptions URL::InputStreamOptions::withProgressCallback (std::function<bool (int, int)> 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<InputStream> URL::createInputStream (bool usePostCommand, | |||||
OpenStreamProgressCallback* progressCallback, | |||||
void* progressCallbackContext, | |||||
String headers, | |||||
int timeOutMs, | |||||
StringPairArray* responseHeaders, | |||||
int* statusCode, | |||||
int numRedirectsToFollow, | |||||
String httpRequestCmd) const | |||||
std::unique_ptr<InputStream> URL::createInputStream (const InputStreamOptions& options) const | |||||
{ | { | ||||
if (isLocalFile()) | if (isLocalFile()) | ||||
{ | { | ||||
#if JUCE_IOS | #if JUCE_IOS | ||||
// We may need to refresh the embedded bookmark. | // We may need to refresh the embedded bookmark. | ||||
return std::make_unique<iOSFileStreamWrapper<FileInputStream>> (const_cast<URL&>(*this)); | |||||
return std::make_unique<iOSFileStreamWrapper<FileInputStream>> (const_cast<URL&> (*this)); | |||||
#else | #else | ||||
return getLocalFile().createInputStream(); | return getLocalFile().createInputStream(); | ||||
#endif | #endif | ||||
} | } | ||||
auto wi = std::make_unique<WebInputStream> (*this, usePostCommand); | |||||
auto webInputStream = [&] | |||||
{ | |||||
const auto usePost = options.getParameterHandling() == ParameterHandling::inPostData; | |||||
auto stream = std::make_unique<WebInputStream> (*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 | struct ProgressCallbackCaller : public WebInputStream::Listener | ||||
{ | { | ||||
ProgressCallbackCaller (OpenStreamProgressCallback* progressCallbackToUse, void* progressCallbackContextToUse) | |||||
: callback (progressCallbackToUse), data (progressCallbackContextToUse) | |||||
{} | |||||
ProgressCallbackCaller (std::function<bool (int, int)> progressCallbackToUse) | |||||
: callback (std::move (progressCallbackToUse)) | |||||
{ | |||||
} | |||||
bool postDataSendProgress (WebInputStream&, int bytesSent, int totalBytes) override | bool postDataSendProgress (WebInputStream&, int bytesSent, int totalBytes) override | ||||
{ | { | ||||
return callback (data, bytesSent, totalBytes); | |||||
return callback (bytesSent, totalBytes); | |||||
} | } | ||||
OpenStreamProgressCallback* callback; | |||||
void* const data; | |||||
std::function<bool (int, int)> callback; | |||||
}; | }; | ||||
std::unique_ptr<ProgressCallbackCaller> 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<ProgressCallbackCaller> | |||||
{ | |||||
if (auto progressCallback = options.getProgressCallback()) | |||||
return std::make_unique<ProgressCallbackCaller> (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; | return nullptr; | ||||
// Older GCCs complain about binding unique_ptr<WebInputStream>&& to unique_ptr<InputStream> | |||||
// if we just `return wi` here. | |||||
return std::unique_ptr<InputStream> (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 | #if JUCE_ANDROID | ||||
@@ -752,7 +815,7 @@ std::unique_ptr<OutputStream> URL::createOutputStream() const | |||||
bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) const | bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) const | ||||
{ | { | ||||
const std::unique_ptr<InputStream> in (isLocalFile() ? getLocalFile().createInputStream() | const std::unique_ptr<InputStream> in (isLocalFile() ? getLocalFile().createInputStream() | ||||
: createInputStream (usePostCommand)); | |||||
: createInputStream (InputStreamOptions (toHandling (usePostCommand)))); | |||||
if (in != nullptr) | if (in != nullptr) | ||||
{ | { | ||||
@@ -766,7 +829,7 @@ bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) co | |||||
String URL::readEntireTextStream (bool usePostCommand) const | String URL::readEntireTextStream (bool usePostCommand) const | ||||
{ | { | ||||
const std::unique_ptr<InputStream> in (isLocalFile() ? getLocalFile().createInputStream() | const std::unique_ptr<InputStream> in (isLocalFile() ? getLocalFile().createInputStream() | ||||
: createInputStream (usePostCommand)); | |||||
: createInputStream (InputStreamOptions (toHandling (usePostCommand)))); | |||||
if (in != nullptr) | if (in != nullptr) | ||||
return in->readEntireStreamAsString(); | return in->readEntireStreamAsString(); | ||||
@@ -911,4 +974,30 @@ bool URL::launchInDefaultBrowser() const | |||||
return Process::openDocument (u, {}); | return Process::openDocument (u, {}); | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<InputStream> URL::createInputStream (bool usePostCommand, | |||||
OpenStreamProgressCallback* cb, | |||||
void* context, | |||||
String headers, | |||||
int timeOutMs, | |||||
StringPairArray* responseHeaders, | |||||
int* statusCode, | |||||
int numRedirectsToFollow, | |||||
String httpRequestCmd) const | |||||
{ | |||||
std::function<bool (int, int)> 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 | } // namespace juce |
@@ -258,38 +258,33 @@ public: | |||||
/** Returns a copy of this URL, with a block of data to send as the POST data. | /** 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 | If the URL already contains some POST data, this will replace it, rather | ||||
than being appended to it. | 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; | URL withPOSTData (const String& postData) const; | ||||
/** Returns a copy of this URL, with a block of data to send as the POST data. | /** 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 | If the URL already contains some POST data, this will replace it, rather | ||||
than being appended to it. | 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; | URL withPOSTData (const MemoryBlock& postData) const; | ||||
/** Returns the data that was set using withPOSTData(). */ | /** Returns the data that was set using withPOSTData(). */ | ||||
String getPostData() const { return postData.toString(); } | 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; } | 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. | /** Tries to launch the system's default browser to open the URL. | ||||
@@ -308,13 +303,103 @@ public: | |||||
*/ | */ | ||||
static bool isProbablyAnEmailAddress (const String& possibleEmailAddress); | 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<bool (int bytesSent, int totalBytes)> 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<bool (int, int)> 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<bool (int, int)> 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. | /** 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. | 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<InputStream> 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<InputStream> createInputStream (const InputStreamOptions& options) const; | |||||
/** Attempts to open an output stream to a URL for writing | /** Attempts to open an output stream to a URL for writing | ||||
@@ -558,6 +611,25 @@ public: | |||||
*/ | */ | ||||
static URL createWithoutParsing (const String& url); | 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<InputStream> 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: | private: | ||||
//============================================================================== | //============================================================================== | ||||
#if JUCE_IOS | #if JUCE_IOS | ||||
@@ -594,9 +666,10 @@ private: | |||||
URL (const String&, int); | URL (const String&, int); | ||||
void init(); | void init(); | ||||
void addParameter (const String&, const String&); | void addParameter (const String&, const String&); | ||||
void createHeadersAndPostData (String&, MemoryBlock&) const; | |||||
void createHeadersAndPostData (String&, MemoryBlock&, bool) const; | |||||
URL withUpload (Upload*) const; | URL withUpload (Upload*) const; | ||||
static ParameterHandling toHandling (bool); | |||||
static File fileFromFileSchemeURL (const URL&); | static File fileFromFileSchemeURL (const URL&); | ||||
String getDomainInternal (bool) const; | String getDomainInternal (bool) const; | ||||
@@ -24,13 +24,12 @@ namespace juce | |||||
{ | { | ||||
WebInputStream::WebInputStream (const URL& url, const bool usePost) | WebInputStream::WebInputStream (const URL& url, const bool usePost) | ||||
: pimpl (new Pimpl (*this, url, usePost)), hasCalledConnect (false) | |||||
: pimpl (std::make_unique<Pimpl> (*this, url, usePost)) | |||||
{ | { | ||||
} | } | ||||
WebInputStream::~WebInputStream() | WebInputStream::~WebInputStream() | ||||
{ | { | ||||
delete pimpl; | |||||
} | } | ||||
WebInputStream& WebInputStream::withExtraHeaders (const String& extra) { pimpl->withExtraHeaders (extra); return *this; } | 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 WebInputStream::parseHttpHeaders (const String& headerData) | ||||
{ | { | ||||
StringPairArray headerPairs; | StringPairArray headerPairs; | ||||
StringArray headerLines = StringArray::fromLines (headerData); | |||||
auto headerLines = StringArray::fromLines (headerData); | |||||
// ignore the first line as this is the status line | // ignore the first line as this is the status line | ||||
for (int i = 1; i < headerLines.size(); ++i) | for (int i = 1; i < headerLines.size(); ++i) | ||||
{ | { | ||||
const String& headersEntry = headerLines[i]; | |||||
const auto& headersEntry = headerLines[i]; | |||||
if (headersEntry.isNotEmpty()) | 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; | 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 | } // namespace juce |
@@ -220,14 +220,14 @@ class JUCE_API WebInputStream : public InputStream | |||||
bool setPosition (int64 wantedPos) override; | bool setPosition (int64 wantedPos) override; | ||||
private: | 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; | class Pimpl; | ||||
friend class Pimpl; | friend class Pimpl; | ||||
Pimpl* const pimpl; | |||||
bool hasCalledConnect; | |||||
std::unique_ptr<Pimpl> pimpl; | |||||
bool hasCalledConnect = false; | |||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebInputStream) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebInputStream) | ||||
}; | }; | ||||