2 ==============================================================================
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
7 JUCE is an open source library subject to commercial or open-source
10 The code included in this file is provided under the terms of the ISC license
11 http://www.isc.org/downloads/software-support-policy/isc-license. Permission
12 To use, copy, modify, and/or distribute this software for any purpose with or
13 without fee is hereby granted provided that the above copyright notice and
14 this permission notice appear in all copies.
16 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
17 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
20 ==============================================================================
26 //==============================================================================
29 URL::URL (const String
& u
) : url (u
)
34 URL::URL (File localFile
)
36 if (localFile
== File())
40 bool isUncPath
= localFile
.getFullPathName().startsWith ("\\\\");
43 while (! localFile
.isRoot())
45 url
= "/" + addEscapeChars (localFile
.getFileName(), false) + url
;
46 localFile
= localFile
.getParentDirectory();
49 url
= addEscapeChars (localFile
.getFileName(), false) + url
;
54 url
= url
.fromFirstOccurrenceOf ("/", false, false);
59 if (! url
.startsWithChar (L
'/'))
63 url
= "file://" + url
;
65 jassert (isWellFormed());
70 auto i
= url
.indexOfChar ('?');
76 auto nextAmp
= url
.indexOfChar (i
+ 1, '&');
77 auto equalsPos
= url
.indexOfChar (i
+ 1, '=');
81 addParameter (removeEscapeChars (equalsPos
< 0 ? url
.substring (i
+ 1) : url
.substring (i
+ 1, equalsPos
)),
82 equalsPos
< 0 ? String() : removeEscapeChars (url
.substring (equalsPos
+ 1)));
84 else if (nextAmp
> 0 && equalsPos
< nextAmp
)
86 addParameter (removeEscapeChars (equalsPos
< 0 ? url
.substring (i
+ 1, nextAmp
) : url
.substring (i
+ 1, equalsPos
)),
87 equalsPos
< 0 ? String() : removeEscapeChars (url
.substring (equalsPos
+ 1, nextAmp
)));
94 url
= url
.upToFirstOccurrenceOf ("?", false, false);
98 URL::URL (const String
& u
, int) : url (u
) {}
100 URL
URL::createWithoutParsing (const String
& u
)
105 bool URL::operator== (const URL
& other
) const
107 return url
== other
.url
108 && postData
== other
.postData
109 && parameterNames
== other
.parameterNames
110 && parameterValues
== other
.parameterValues
111 && filesToUpload
== other
.filesToUpload
;
114 bool URL::operator!= (const URL
& other
) const
116 return ! operator== (other
);
121 static String
getMangledParameters (const URL
& url
)
123 jassert (url
.getParameterNames().size() == url
.getParameterValues().size());
126 for (int i
= 0; i
< url
.getParameterNames().size(); ++i
)
131 auto val
= url
.getParameterValues()[i
];
133 p
<< URL::addEscapeChars (url
.getParameterNames()[i
], true);
135 if (val
.isNotEmpty())
136 p
<< '=' << URL::addEscapeChars (val
, true);
142 static int findEndOfScheme (const String
& url
)
146 while (CharacterFunctions::isLetterOrDigit (url
[i
])
147 || url
[i
] == '+' || url
[i
] == '-' || url
[i
] == '.')
150 return url
.substring (i
).startsWith ("://") ? i
+ 1 : 0;
153 static int findStartOfNetLocation (const String
& url
)
155 int start
= findEndOfScheme (url
);
157 while (url
[start
] == '/')
163 static int findStartOfPath (const String
& url
)
165 return url
.indexOfChar (findStartOfNetLocation (url
), '/') + 1;
168 static void concatenatePaths (String
& path
, const String
& suffix
)
170 if (! path
.endsWithChar ('/'))
173 if (suffix
.startsWithChar ('/'))
174 path
+= suffix
.substring (1);
179 static String
removeLastPathSection (const String
& url
)
181 auto startOfPath
= findStartOfPath (url
);
182 auto lastSlash
= url
.lastIndexOfChar ('/');
184 if (lastSlash
> startOfPath
&& lastSlash
== url
.length() - 1)
185 return removeLastPathSection (url
.dropLastCharacters (1));
190 return url
.substring (0, std::max (startOfPath
, lastSlash
));
194 void URL::addParameter (const String
& name
, const String
& value
)
196 parameterNames
.add (name
);
197 parameterValues
.add (value
);
200 String
URL::toString (bool includeGetParameters
) const
202 if (includeGetParameters
)
203 return url
+ getQueryString();
208 bool URL::isEmpty() const noexcept
210 return url
.isEmpty();
213 bool URL::isWellFormed() const
216 return url
.isNotEmpty();
219 String
URL::getDomain() const
221 return getDomainInternal (false);
224 String
URL::getSubPath (bool includeGetParameters
) const
226 auto startOfPath
= URLHelpers::findStartOfPath (url
);
227 auto subPath
= startOfPath
<= 0 ? String()
228 : url
.substring (startOfPath
);
230 if (includeGetParameters
)
231 subPath
+= getQueryString();
236 String
URL::getQueryString() const
238 if (parameterNames
.size() > 0)
239 return "?" + URLHelpers::getMangledParameters (*this);
244 String
URL::getScheme() const
246 return url
.substring (0, URLHelpers::findEndOfScheme (url
) - 1);
250 bool URL::isLocalFile() const
252 return getScheme() == "file";
255 File
URL::getLocalFile() const
257 return fileFromFileSchemeURL (*this);
260 String
URL::getFileName() const
262 return toString (false).fromLastOccurrenceOf ("/", false, true);
266 URL::ParameterHandling
URL::toHandling (bool usePostData
)
268 return usePostData
? ParameterHandling::inPostData
: ParameterHandling::inAddress
;
271 File
URL::fileFromFileSchemeURL (const URL
& fileURL
)
273 if (! fileURL
.isLocalFile())
279 auto path
= removeEscapeChars (fileURL
.getDomainInternal (true)).replace ("+", "%2B");
282 bool isUncPath
= (! fileURL
.url
.startsWith ("file:///"));
284 path
= File::getSeparatorString() + path
;
287 auto urlElements
= StringArray::fromTokens (fileURL
.getSubPath(), "/", "");
289 for (auto urlElement
: urlElements
)
290 path
+= File::getSeparatorString() + removeEscapeChars (urlElement
.replace ("+", "%2B"));
294 path
= "\\\\" + path
;
300 int URL::getPort() const
302 auto colonPos
= url
.indexOfChar (URLHelpers::findStartOfNetLocation (url
), ':');
304 return colonPos
> 0 ? url
.substring (colonPos
+ 1).getIntValue() : 0;
307 URL
URL::withNewDomainAndPath (const String
& newURL
) const
314 URL
URL::withNewSubPath (const String
& newPath
) const
318 auto startOfPath
= URLHelpers::findStartOfPath (url
);
321 u
.url
= url
.substring (0, startOfPath
);
323 URLHelpers::concatenatePaths (u
.url
, newPath
);
327 URL
URL::getParentURL() const
330 u
.url
= URLHelpers::removeLastPathSection (u
.url
);
334 URL
URL::getChildURL (const String
& subPath
) const
337 URLHelpers::concatenatePaths (u
.url
, subPath
);
341 bool URL::hasBodyDataToSend() const
343 return filesToUpload
.size() > 0 || ! postData
.isEmpty();
346 void URL::createHeadersAndPostData (String
& headers
,
347 MemoryBlock
& postDataToWrite
,
348 bool addParametersToBody
) const
350 MemoryOutputStream
data (postDataToWrite
, false);
352 if (filesToUpload
.size() > 0)
354 // (this doesn't currently support mixing custom post-data with uploads..)
355 jassert (postData
.isEmpty());
357 auto boundary
= String::toHexString (Random::getSystemRandom().nextInt64());
359 headers
<< "Content-Type: multipart/form-data; boundary=" << boundary
<< "\r\n";
361 data
<< "--" << boundary
;
363 for (int i
= 0; i
< parameterNames
.size(); ++i
)
365 data
<< "\r\nContent-Disposition: form-data; name=\"" << parameterNames
[i
]
366 << "\"\r\n\r\n" << parameterValues
[i
]
367 << "\r\n--" << boundary
;
370 for (auto* f
: filesToUpload
)
372 data
<< "\r\nContent-Disposition: form-data; name=\"" << f
->parameterName
373 << "\"; filename=\"" << f
->filename
<< "\"\r\n";
375 if (f
->mimeType
.isNotEmpty())
376 data
<< "Content-Type: " << f
->mimeType
<< "\r\n";
378 data
<< "Content-Transfer-Encoding: binary\r\n\r\n";
380 if (f
->data
!= nullptr)
385 data
<< "\r\n--" << boundary
;
392 if (addParametersToBody
)
393 data
<< URLHelpers::getMangledParameters (*this);
397 // if the user-supplied headers didn't contain a content-type, add one now..
398 if (! headers
.containsIgnoreCase ("Content-Type"))
399 headers
<< "Content-Type: application/x-www-form-urlencoded\r\n";
401 headers
<< "Content-length: " << (int) data
.getDataSize() << "\r\n";
405 //==============================================================================
406 bool URL::isProbablyAWebsiteURL (const String
& possibleURL
)
408 for (auto* protocol
: { "http:", "https:", "ftp:" })
409 if (possibleURL
.startsWithIgnoreCase (protocol
))
412 if (possibleURL
.containsChar ('@') || possibleURL
.containsChar (' '))
415 auto topLevelDomain
= possibleURL
.upToFirstOccurrenceOf ("/", false, false)
416 .fromLastOccurrenceOf (".", false, false);
418 return topLevelDomain
.isNotEmpty() && topLevelDomain
.length() <= 3;
421 bool URL::isProbablyAnEmailAddress (const String
& possibleEmailAddress
)
423 auto atSign
= possibleEmailAddress
.indexOfChar ('@');
426 && possibleEmailAddress
.lastIndexOfChar ('.') > (atSign
+ 1)
427 && ! possibleEmailAddress
.endsWithChar ('.');
430 String
URL::getDomainInternal (bool ignorePort
) const
432 auto start
= URLHelpers::findStartOfNetLocation (url
);
433 auto end1
= url
.indexOfChar (start
, '/');
434 auto end2
= ignorePort
? -1 : url
.indexOfChar (start
, ':');
436 auto end
= (end1
< 0 && end2
< 0) ? std::numeric_limits
<int>::max()
437 : ((end1
< 0 || end2
< 0) ? jmax (end1
, end2
)
438 : jmin (end1
, end2
));
439 return url
.substring (start
, end
);
443 URL::Bookmark::Bookmark (void* bookmarkToUse
) : data (bookmarkToUse
)
447 URL::Bookmark::~Bookmark()
449 [(NSData
*) data release
];
452 void setURLBookmark (URL
& u
, void* bookmark
)
454 u
.bookmark
= new URL::Bookmark (bookmark
);
457 void* getURLBookmark (URL
& u
)
459 if (u
.bookmark
.get() == nullptr)
462 return u
.bookmark
.get()->data
;
465 template <typename Stream
> struct iOSFileStreamWrapperFlush
{ static void flush (Stream
*) {} };
466 template <> struct iOSFileStreamWrapperFlush
<FileOutputStream
> { static void flush (OutputStream
* o
) { o
->flush(); } };
468 template <typename Stream
>
469 class iOSFileStreamWrapper
: public Stream
472 iOSFileStreamWrapper (URL
& urlToUse
)
473 : Stream (getLocalFileAccess (urlToUse
)),
477 ~iOSFileStreamWrapper()
479 iOSFileStreamWrapperFlush
<Stream
>::flush (this);
481 if (NSData
* bookmark
= (NSData
*) getURLBookmark (url
))
483 BOOL isBookmarkStale
= false;
484 NSError
* error
= nil
;
486 auto nsURL
= [NSURL URLByResolvingBookmarkData
: bookmark
489 bookmarkDataIsStale
: &isBookmarkStale
495 updateStaleBookmark (nsURL
, url
);
497 [nsURL stopAccessingSecurityScopedResource
];
501 auto desc
= [error localizedDescription
];
510 bool securityAccessSucceeded
= false;
512 File
getLocalFileAccess (URL
& urlToUse
)
514 if (NSData
* bookmark
= (NSData
*) getURLBookmark (urlToUse
))
516 BOOL isBookmarkStale
= false;
517 NSError
* error
= nil
;
519 auto nsURL
= [NSURL URLByResolvingBookmarkData
: bookmark
522 bookmarkDataIsStale
: &isBookmarkStale
527 securityAccessSucceeded
= [nsURL startAccessingSecurityScopedResource
];
530 updateStaleBookmark (nsURL
, urlToUse
);
532 return urlToUse
.getLocalFile();
535 auto desc
= [error localizedDescription
];
540 return urlToUse
.getLocalFile();
543 void updateStaleBookmark (NSURL
* nsURL
, URL
& juceUrl
)
545 NSError
* error
= nil
;
547 NSData
* bookmark
= [nsURL bookmarkDataWithOptions
: NSURLBookmarkCreationSuitableForBookmarkFile
548 includingResourceValuesForKeys
: nil
553 setURLBookmark (juceUrl
, (void*) bookmark
);
559 //==============================================================================
560 template <typename Member
, typename Item
>
561 static URL::InputStreamOptions
with (URL::InputStreamOptions options
, Member
&& member
, Item
&& item
)
563 options
.*member
= std::forward
<Item
> (item
);
567 URL::InputStreamOptions::InputStreamOptions (ParameterHandling handling
) : parameterHandling (handling
) {}
569 URL::InputStreamOptions
URL::InputStreamOptions::withProgressCallback (std::function
<bool (int, int)> cb
) const
571 return with (*this, &InputStreamOptions::progressCallback
, std::move (cb
));
574 URL::InputStreamOptions
URL::InputStreamOptions::withExtraHeaders (const String
& headers
) const
576 return with (*this, &InputStreamOptions::extraHeaders
, headers
);
579 URL::InputStreamOptions
URL::InputStreamOptions::withConnectionTimeoutMs (int timeout
) const
581 return with (*this, &InputStreamOptions::connectionTimeOutMs
, timeout
);
584 URL::InputStreamOptions
URL::InputStreamOptions::withResponseHeaders (StringPairArray
* headers
) const
586 return with (*this, &InputStreamOptions::responseHeaders
, headers
);
589 URL::InputStreamOptions
URL::InputStreamOptions::withStatusCode (int* status
) const
591 return with (*this, &InputStreamOptions::statusCode
, status
);
594 URL::InputStreamOptions
URL::InputStreamOptions::withNumRedirectsToFollow (int numRedirects
) const
596 return with (*this, &InputStreamOptions::numRedirectsToFollow
, numRedirects
);
599 URL::InputStreamOptions
URL::InputStreamOptions::withHttpRequestCmd (const String
& cmd
) const
601 return with (*this, &InputStreamOptions::httpRequestCmd
, cmd
);
604 //==============================================================================
605 std::unique_ptr
<InputStream
> URL::createInputStream (const InputStreamOptions
& options
) const
610 // We may need to refresh the embedded bookmark.
611 return std::make_unique
<iOSFileStreamWrapper
<FileInputStream
>> (const_cast<URL
&> (*this));
613 return getLocalFile().createInputStream();
620 std::unique_ptr
<OutputStream
> URL::createOutputStream() const
625 // We may need to refresh the embedded bookmark.
626 return std::make_unique
<iOSFileStreamWrapper
<FileOutputStream
>> (const_cast<URL
&> (*this));
628 return std::make_unique
<FileOutputStream
> (getLocalFile());
635 //==============================================================================
636 bool URL::readEntireBinaryStream (MemoryBlock
& destData
, bool usePostCommand
) const
638 const std::unique_ptr
<InputStream
> in (isLocalFile() ? getLocalFile().createInputStream()
639 : createInputStream (InputStreamOptions (toHandling (usePostCommand
))));
643 in
->readIntoMemoryBlock (destData
);
650 String
URL::readEntireTextStream (bool usePostCommand
) const
652 const std::unique_ptr
<InputStream
> in (isLocalFile() ? getLocalFile().createInputStream()
653 : createInputStream (InputStreamOptions (toHandling (usePostCommand
))));
656 return in
->readEntireStreamAsString();
661 std::unique_ptr
<XmlElement
> URL::readEntireXmlStream (bool usePostCommand
) const
663 return parseXML (readEntireTextStream (usePostCommand
));
666 //==============================================================================
667 URL
URL::withParameter (const String
& parameterName
,
668 const String
& parameterValue
) const
671 u
.addParameter (parameterName
, parameterValue
);
675 URL
URL::withParameters (const StringPairArray
& parametersToAdd
) const
679 for (int i
= 0; i
< parametersToAdd
.size(); ++i
)
680 u
.addParameter (parametersToAdd
.getAllKeys()[i
],
681 parametersToAdd
.getAllValues()[i
]);
686 URL
URL::withPOSTData (const String
& newPostData
) const
688 return withPOSTData (MemoryBlock (newPostData
.toRawUTF8(), newPostData
.getNumBytesAsUTF8()));
691 URL
URL::withPOSTData (const MemoryBlock
& newPostData
) const
694 u
.postData
= newPostData
;
698 URL::Upload::Upload (const String
& param
, const String
& name
,
699 const String
& mime
, const File
& f
, MemoryBlock
* mb
)
700 : parameterName (param
), filename (name
), mimeType (mime
), file (f
), data (mb
)
702 jassert (mimeType
.isNotEmpty()); // You need to supply a mime type!
705 URL
URL::withUpload (Upload
* const f
) const
709 for (int i
= u
.filesToUpload
.size(); --i
>= 0;)
710 if (u
.filesToUpload
.getObjectPointerUnchecked (i
)->parameterName
== f
->parameterName
)
711 u
.filesToUpload
.remove (i
);
713 u
.filesToUpload
.add (f
);
717 URL
URL::withFileToUpload (const String
& parameterName
, const File
& fileToUpload
,
718 const String
& mimeType
) const
720 return withUpload (new Upload (parameterName
, fileToUpload
.getFileName(),
721 mimeType
, fileToUpload
, nullptr));
724 URL
URL::withDataToUpload (const String
& parameterName
, const String
& filename
,
725 const MemoryBlock
& fileContentToUpload
, const String
& mimeType
) const
727 return withUpload (new Upload (parameterName
, filename
, mimeType
, File(),
728 new MemoryBlock (fileContentToUpload
)));
731 //==============================================================================
732 String
URL::removeEscapeChars (const String
& s
)
734 auto result
= s
.replaceCharacter ('+', ' ');
736 if (! result
.containsChar ('%'))
739 // We need to operate on the string as raw UTF8 chars, and then recombine them into unicode
740 // after all the replacements have been made, so that multi-byte chars are handled.
741 Array
<char> utf8 (result
.toRawUTF8(), (int) result
.getNumBytesAsUTF8());
743 for (int i
= 0; i
< utf8
.size(); ++i
)
745 if (utf8
.getUnchecked(i
) == '%')
747 auto hexDigit1
= CharacterFunctions::getHexDigitValue ((juce_wchar
) (uint8
) utf8
[i
+ 1]);
748 auto hexDigit2
= CharacterFunctions::getHexDigitValue ((juce_wchar
) (uint8
) utf8
[i
+ 2]);
750 if (hexDigit1
>= 0 && hexDigit2
>= 0)
752 utf8
.set (i
, (char) ((hexDigit1
<< 4) + hexDigit2
));
753 utf8
.removeRange (i
+ 1, 2);
758 return String::fromUTF8 (utf8
.getRawDataPointer(), utf8
.size());
761 String
URL::addEscapeChars (const String
& s
, bool isParameter
, bool roundBracketsAreLegal
)
763 String
legalChars (isParameter
? "_-.~"
766 if (roundBracketsAreLegal
)
769 Array
<char> utf8 (s
.toRawUTF8(), (int) s
.getNumBytesAsUTF8());
771 for (int i
= 0; i
< utf8
.size(); ++i
)
773 auto c
= utf8
.getUnchecked(i
);
775 if (! (CharacterFunctions::isLetterOrDigit (c
)
776 || legalChars
.containsChar ((juce_wchar
) c
)))
779 utf8
.insert (++i
, "0123456789ABCDEF" [((uint8
) c
) >> 4]);
780 utf8
.insert (++i
, "0123456789ABCDEF" [c
& 15]);
784 return String::fromUTF8 (utf8
.getRawDataPointer(), utf8
.size());
787 //==============================================================================
788 bool URL::launchInDefaultBrowser() const
790 auto u
= toString (true);
792 if (u
.containsChar ('@') && ! u
.containsChar (':'))
795 return Process::openDocument (u
, {});
798 //==============================================================================
799 std::unique_ptr
<InputStream
> URL::createInputStream (bool usePostCommand
,
800 OpenStreamProgressCallback
* cb
,
804 StringPairArray
* responseHeaders
,
806 int numRedirectsToFollow
,
807 String httpRequestCmd
) const
809 std::function
<bool (int, int)> callback
;
812 callback
= [context
, cb
] (int sent
, int total
) { return cb (context
, sent
, total
); };
814 return createInputStream (InputStreamOptions (toHandling (usePostCommand
))
815 .withProgressCallback (std::move (callback
))
816 .withExtraHeaders (headers
)
817 .withConnectionTimeoutMs (timeOutMs
)
818 .withResponseHeaders (responseHeaders
)
819 .withStatusCode (statusCode
)
820 .withNumRedirectsToFollow(numRedirectsToFollow
)
821 .withHttpRequestCmd (httpRequestCmd
));
824 std::unique_ptr
<URL::DownloadTask
> URL::downloadToFile (const File
& targetLocation
,
826 DownloadTask::Listener
* listener
,