/* ============================================================================== This file is part of the JUCE library - "Jules' Utility Class Extensions" Copyright 2004-9 by Raw Material Software Ltd. ------------------------------------------------------------------------------ JUCE can be redistributed and/or modified under the terms of the GNU General Public License (Version 2), as published by the Free Software Foundation. A copy of the license is included in the JUCE distribution, or can be found online at www.gnu.org/licenses. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.rawmaterialsoftware.com/juce for more information. ============================================================================== */ #include "../../core/juce_StandardHeader.h" BEGIN_JUCE_NAMESPACE #include "juce_URL.h" #include "../../core/juce_Random.h" #include "../../text/juce_XmlDocument.h" //============================================================================== URL::URL() { } URL::URL (const String& url_) : url (url_) { int i = url.indexOfChar ('?'); if (i >= 0) { do { const int nextAmp = url.indexOfChar (i + 1, '&'); const int equalsPos = url.indexOfChar (i + 1, '='); if (equalsPos > i + 1) { if (nextAmp < 0) { parameters.set (removeEscapeChars (url.substring (i + 1, equalsPos)), removeEscapeChars (url.substring (equalsPos + 1))); } else if (nextAmp > 0 && equalsPos < nextAmp) { parameters.set (removeEscapeChars (url.substring (i + 1, equalsPos)), removeEscapeChars (url.substring (equalsPos + 1, nextAmp))); } } i = nextAmp; } while (i >= 0); url = url.upToFirstOccurrenceOf (T("?"), false, false); } } URL::URL (const URL& other) : url (other.url), postData (other.postData), parameters (other.parameters), filesToUpload (other.filesToUpload), mimeTypes (other.mimeTypes) { } URL& URL::operator= (const URL& other) { url = other.url; postData = other.postData; parameters = other.parameters; filesToUpload = other.filesToUpload; mimeTypes = other.mimeTypes; return *this; } URL::~URL() { } static const String getMangledParameters (const StringPairArray& parameters) { String p; for (int i = 0; i < parameters.size(); ++i) { if (i > 0) p += '&'; p << URL::addEscapeChars (parameters.getAllKeys() [i], true) << '=' << URL::addEscapeChars (parameters.getAllValues() [i], true); } return p; } const String URL::toString (const bool includeGetParameters) const { if (includeGetParameters && parameters.size() > 0) return url + "?" + getMangledParameters (parameters); else return url; } bool URL::isWellFormed() const { //xxx TODO return url.isNotEmpty(); } static int findStartOfDomain (const String& url) { int i = 0; while (CharacterFunctions::isLetterOrDigit (url[i]) || CharacterFunctions::indexOfChar (T("+-."), url[i], false) >= 0) ++i; return url[i] == ':' ? i + 1 : 0; } const String URL::getDomain() const { int start = findStartOfDomain (url); while (url[start] == '/') ++start; const int end1 = url.indexOfChar (start, '/'); const int end2 = url.indexOfChar (start, ':'); const int end = (end1 < 0 || end2 < 0) ? jmax (end1, end2) : jmin (end1, end2); return url.substring (start, end); } const String URL::getSubPath() const { int start = findStartOfDomain (url); while (url[start] == '/') ++start; const int startOfPath = url.indexOfChar (start, '/') + 1; return startOfPath <= 0 ? String::empty : url.substring (startOfPath); } const String URL::getScheme() const { return url.substring (0, findStartOfDomain (url) - 1); } const URL URL::withNewSubPath (const String& newPath) const { int start = findStartOfDomain (url); while (url[start] == '/') ++start; const int startOfPath = url.indexOfChar (start, '/') + 1; URL u (*this); if (startOfPath > 0) u.url = url.substring (0, startOfPath); if (! u.url.endsWithChar ('/')) u.url << '/'; if (newPath.startsWithChar ('/')) u.url << newPath.substring (1); else u.url << newPath; return u; } //============================================================================== bool URL::isProbablyAWebsiteURL (const String& possibleURL) { if (possibleURL.startsWithIgnoreCase (T("http:")) || possibleURL.startsWithIgnoreCase (T("ftp:"))) return true; if (possibleURL.startsWithIgnoreCase (T("file:")) || possibleURL.containsChar ('@') || possibleURL.endsWithChar ('.') || (! possibleURL.containsChar ('.'))) return false; if (possibleURL.startsWithIgnoreCase (T("www.")) && possibleURL.substring (5).containsChar ('.')) return true; const char* commonTLDs[] = { "com", "net", "org", "uk", "de", "fr", "jp" }; for (int i = 0; i < numElementsInArray (commonTLDs); ++i) if ((possibleURL + "/").containsIgnoreCase ("." + String (commonTLDs[i]) + "/")) return true; return false; } bool URL::isProbablyAnEmailAddress (const String& possibleEmailAddress) { const int atSign = possibleEmailAddress.indexOfChar ('@'); return atSign > 0 && possibleEmailAddress.lastIndexOfChar ('.') > (atSign + 1) && (! possibleEmailAddress.endsWithChar ('.')); } //============================================================================== void* juce_openInternetFile (const String& url, const String& headers, const MemoryBlock& optionalPostData, const bool isPost, URL::OpenStreamProgressCallback* callback, void* callbackContext, int timeOutMs); void juce_closeInternetFile (void* handle); int juce_readFromInternetFile (void* handle, void* dest, int bytesToRead); int juce_seekInInternetFile (void* handle, int newPosition); int64 juce_getInternetFileContentLength (void* handle); //============================================================================== class WebInputStream : public InputStream { public: //============================================================================== WebInputStream (const URL& url, const bool isPost_, URL::OpenStreamProgressCallback* const progressCallback_, void* const progressCallbackContext_, const String& extraHeaders, int timeOutMs_) : position (0), finished (false), isPost (isPost_), progressCallback (progressCallback_), progressCallbackContext (progressCallbackContext_), timeOutMs (timeOutMs_) { server = url.toString (! isPost); if (isPost_) createHeadersAndPostData (url); headers += extraHeaders; if (! headers.endsWithChar ('\n')) headers << "\r\n"; handle = juce_openInternetFile (server, headers, postData, isPost, progressCallback_, progressCallbackContext_, timeOutMs); } ~WebInputStream() { juce_closeInternetFile (handle); } bool isError() const { return handle == 0; } //============================================================================== int64 getTotalLength() { return juce_getInternetFileContentLength (handle); } bool isExhausted() { return finished; } int read (void* dest, int bytes) { if (finished || isError()) { return 0; } else { const int bytesRead = juce_readFromInternetFile (handle, dest, bytes); position += bytesRead; if (bytesRead == 0) finished = true; return bytesRead; } } int64 getPosition() { return position; } bool setPosition (int64 wantedPos) { if (wantedPos != position) { finished = false; const int actualPos = juce_seekInInternetFile (handle, (int) wantedPos); if (actualPos == wantedPos) { position = wantedPos; } else { if (wantedPos < position) { juce_closeInternetFile (handle); position = 0; finished = false; handle = juce_openInternetFile (server, headers, postData, isPost, progressCallback, progressCallbackContext, timeOutMs); } skipNextBytes (wantedPos - position); } } return true; } //============================================================================== juce_UseDebuggingNewOperator private: String server, headers; MemoryBlock postData; int64 position; bool finished; const bool isPost; void* handle; URL::OpenStreamProgressCallback* const progressCallback; void* const progressCallbackContext; const int timeOutMs; void createHeadersAndPostData (const URL& url) { if (url.getFilesToUpload().size() > 0) { // need to upload some files, so do it as multi-part... String boundary (String::toHexString (Random::getSystemRandom().nextInt64())); headers << "Content-Type: multipart/form-data; boundary=" << boundary << "\r\n"; appendUTF8ToPostData ("--" + boundary); int i; for (i = 0; i < url.getParameters().size(); ++i) { String s; s << "\r\nContent-Disposition: form-data; name=\"" << url.getParameters().getAllKeys() [i] << "\"\r\n\r\n" << url.getParameters().getAllValues() [i] << "\r\n--" << boundary; appendUTF8ToPostData (s); } for (i = 0; i < url.getFilesToUpload().size(); ++i) { const File f (url.getFilesToUpload().getAllValues() [i]); const String paramName (url.getFilesToUpload().getAllKeys() [i]); String s; s << "\r\nContent-Disposition: form-data; name=\"" << paramName << "\"; filename=\"" << f.getFileName() << "\"\r\n"; const String mimeType (url.getMimeTypesOfUploadFiles() .getValue (paramName, String::empty)); if (mimeType.isNotEmpty()) s << "Content-Type: " << mimeType << "\r\n"; s << "Content-Transfer-Encoding: binary\r\n\r\n"; appendUTF8ToPostData (s); f.loadFileAsData (postData); s = "\r\n--" + boundary; appendUTF8ToPostData (s); } appendUTF8ToPostData ("--\r\n"); } else { appendUTF8ToPostData (getMangledParameters (url.getParameters())); appendUTF8ToPostData (url.getPostData()); // just a short text attachment, so use simple url encoding.. headers = "Content-Type: application/x-www-form-urlencoded\r\nContent-length: " + String ((unsigned int) postData.getSize()) + "\r\n"; } } void appendUTF8ToPostData (const String& text) { postData.append (text.toUTF8(), (int) strlen (text.toUTF8())); } WebInputStream (const WebInputStream&); WebInputStream& operator= (const WebInputStream&); }; InputStream* URL::createInputStream (const bool usePostCommand, OpenStreamProgressCallback* const progressCallback, void* const progressCallbackContext, const String& extraHeaders, const int timeOutMs) const { ScopedPointer wi (new WebInputStream (*this, usePostCommand, progressCallback, progressCallbackContext, extraHeaders, timeOutMs)); return wi->isError() ? 0 : wi.release(); } //============================================================================== bool URL::readEntireBinaryStream (MemoryBlock& destData, const bool usePostCommand) const { const ScopedPointer in (createInputStream (usePostCommand)); if (in != 0) { in->readIntoMemoryBlock (destData, -1); return true; } return false; } const String URL::readEntireTextStream (const bool usePostCommand) const { const ScopedPointer in (createInputStream (usePostCommand)); if (in != 0) return in->readEntireStreamAsString(); return String::empty; } XmlElement* URL::readEntireXmlStream (const bool usePostCommand) const { XmlDocument doc (readEntireTextStream (usePostCommand)); return doc.getDocumentElement(); } //============================================================================== const URL URL::withParameter (const String& parameterName, const String& parameterValue) const { URL u (*this); u.parameters.set (parameterName, parameterValue); return u; } const URL URL::withFileToUpload (const String& parameterName, const File& fileToUpload, const String& mimeType) const { jassert (mimeType.isNotEmpty()); // You need to supply a mime type! URL u (*this); u.filesToUpload.set (parameterName, fileToUpload.getFullPathName()); u.mimeTypes.set (parameterName, mimeType); return u; } const URL URL::withPOSTData (const String& postData_) const { URL u (*this); u.postData = postData_; return u; } const StringPairArray& URL::getParameters() const { return parameters; } const StringPairArray& URL::getFilesToUpload() const { return filesToUpload; } const StringPairArray& URL::getMimeTypesOfUploadFiles() const { return mimeTypes; } //============================================================================== const String URL::removeEscapeChars (const String& s) { String result (s.replaceCharacter ('+', ' ')); int nextPercent = 0; for (;;) { nextPercent = result.indexOfChar (nextPercent, '%'); if (nextPercent < 0) break; juce_wchar replacementChar = result.substring (nextPercent + 1, nextPercent + 3).getHexValue32(); result = result.replaceSection (nextPercent, 3, String::charToString (replacementChar)); ++nextPercent; } return result; } const String URL::addEscapeChars (const String& s, const bool isParameter) { String result; result.preallocateStorage (s.length() + 8); const char* utf8 = s.toUTF8(); const char* legalChars = isParameter ? "_-.*!'()" : "_-$.*!'(),"; while (*utf8 != 0) { const char c = *utf8++; if (CharacterFunctions::isLetterOrDigit (c) || CharacterFunctions::indexOfChar (legalChars, c, false) >= 0) { result << c; } else { const int v = (int) (uint8) c; result << (v < 0x10 ? "%0" : "%") << String::toHexString (v); } } return result; } //============================================================================== extern bool juce_launchFile (const String& fileName, const String& parameters); bool URL::launchInDefaultBrowser() const { String u (toString (true)); if (u.contains (T("@")) && ! u.contains (T(":"))) u = "mailto:" + u; return juce_launchFile (u, String::empty); } END_JUCE_NAMESPACE