diff --git a/data/js/tomahawk.js b/data/js/tomahawk.js index a5efb28da..1b58d716b 100644 --- a/data/js/tomahawk.js +++ b/data/js/tomahawk.js @@ -277,6 +277,35 @@ Tomahawk.syncRequest = function (url, extraHeaders, options) { } }; +/** + * Internal counter used to identify asyncRequest callback from native code. + */ +Tomahawk.asyncRequestIdCounter = 0; +/** + * Internal map used to map asyncRequestIds to the respective javascript + * callback functions. + */ +Tomahawk.asyncRequestCallbacks = {}; + +/** + * Pass the natively retrived reply back to the javascript callback + * and augment the fake XMLHttpRequest object. + * + * Internal use only! + */ +Tomahawk.nativeAsyncRequestDone = function (reqId, xhr) { + // Check we have a matching callback stored. + if (!Tomahawk.asyncRequestCallbacks.hasOwnProperty(reqId)) { + return; + } + + // Call the real callback + Tomahawk.asyncRequestCallbacks[reqId](xhr); + + // Callback are only used once. + delete Tomahawk.asyncRequestCallbacks[reqId]; +}; + /** * Possible options: * - method: The HTTP request method (default: GET) @@ -289,26 +318,40 @@ Tomahawk.asyncRequest = function (url, callback, extraHeaders, options) { // unpack options var opt = options || {}; var method = opt.method || 'GET'; + var doNativeRequest = false; - var xmlHttpRequest = new XMLHttpRequest(); - xmlHttpRequest.open(method, url, true, opt.username, opt.password); - if (extraHeaders) { - for (var headerName in extraHeaders) { - xmlHttpRequest.setRequestHeader(headerName, extraHeaders[headerName]); - } + if (extraHeaders && (extraHeaders.hasOwnProperty("Referer") || extraHeaders.hasOwnProperty("referer"))) { + doNativeRequest = true; } - xmlHttpRequest.onreadystatechange = function () { - if (xmlHttpRequest.readyState == 4 && xmlHttpRequest.status == 200) { - callback.call(window, xmlHttpRequest); - } else if (xmlHttpRequest.readyState === 4) { - Tomahawk.log("Failed to do " + method + " request: to: " + url); - Tomahawk.log("Status Code was: " + xmlHttpRequest.status); - if (opt.hasOwnProperty('errorHandler')) { - opt.errorHandler.call(window, xmlHttpRequest); + + if (doNativeRequest) { + // Assign a request Id to the callback so we can use it when we are + // returning from the native call. + var reqId = Tomahawk.asyncRequestIdCounter; + Tomahawk.asyncRequestIdCounter++; + Tomahawk.asyncRequestCallbacks[reqId] = callback; + Tomahawk.nativeAsyncRequest(reqId, url, extraHeaders, options); + } else { + var xmlHttpRequest = new XMLHttpRequest(); + xmlHttpRequest.open(method, url, true, opt.username, opt.password); + if (extraHeaders) { + for (var headerName in extraHeaders) { + xmlHttpRequest.setRequestHeader(headerName, extraHeaders[headerName]); } } - }; - xmlHttpRequest.send(opt.data || null); + xmlHttpRequest.onreadystatechange = function () { + if (xmlHttpRequest.readyState == 4 && xmlHttpRequest.status == 200) { + callback.call(window, xmlHttpRequest); + } else if (xmlHttpRequest.readyState === 4) { + Tomahawk.log("Failed to do " + method + " request: to: " + url); + Tomahawk.log("Status Code was: " + xmlHttpRequest.status); + if (opt.hasOwnProperty('errorHandler')) { + opt.errorHandler.call(window, xmlHttpRequest); + } + } + }; + xmlHttpRequest.send(opt.data || null); + } }; Tomahawk.sha256 = Tomahawk.sha256 || CryptoJS.SHA256; diff --git a/src/libtomahawk/resolvers/JSResolverHelper.cpp b/src/libtomahawk/resolvers/JSResolverHelper.cpp index b09ca07ec..3ceed5dba 100644 --- a/src/libtomahawk/resolvers/JSResolverHelper.cpp +++ b/src/libtomahawk/resolvers/JSResolverHelper.cpp @@ -28,6 +28,7 @@ #include "resolvers/ScriptEngine.h" #include "network/Servent.h" #include "utils/Closure.h" +#include "utils/Json.h" #include "utils/NetworkAccessManager.h" #include "utils/NetworkReply.h" #include "utils/Logger.h" @@ -487,6 +488,69 @@ JSResolverHelper::reportStreamUrl( const QString& qid, } +void +JSResolverHelper::nativeAsyncRequest( const int requestId, const QString& url, + const QVariantMap& headers, + const QVariantMap& options ) +{ + QNetworkRequest req( url ); + foreach ( const QString& key , headers.keys() ) { + req.setRawHeader( key.toLatin1(), headers[key].toString().toLatin1() ); + } + + if ( options.contains( "username" ) && options.contains( "password" ) ) + { + // If we have sufficient authentication data, we will send + // username+password as HTTP Basic Auth + QString credentials = QString( "Basic %1" ) + .arg( QString( QString("%1:%2") + .arg( options["username"].toString() ) + .arg( options["password"].toString() ) + .toLatin1().toBase64() ) + ); + req.setRawHeader( "Authorization", credentials.toLatin1() ); + } + + NetworkReply* reply = NULL; + if ( options.contains( "method") && options["method"].toString().toUpper() == "POST" ) { + QByteArray data; + if ( options.contains( "data" ) ) { + data = options["data"].toString().toLatin1(); + } + reply = new NetworkReply( Tomahawk::Utils::nam()->post( req, data ) ); + } else if ( options.contains( "method") && options["method"].toString().toUpper() == "HEAD" ) { + reply = new NetworkReply( Tomahawk::Utils::nam()->head( req ) ); + } else { + reply = new NetworkReply( Tomahawk::Utils::nam()->get( req ) ); + } + + NewClosure( reply , SIGNAL( finished() ), this, SLOT( nativeAsyncRequestDone( int, NetworkReply* ) ), requestId, reply ); +} + + +void +JSResolverHelper::nativeAsyncRequestDone( int requestId, NetworkReply* reply ) +{ + QVariantMap map; + map["response"] = QString::fromUtf8( reply->reply()->readAll() ); + map["responseText"] = map["response"]; + map["responseType"] = QString(); // Default, indicates a string in map["response"] + map["readyState"] = 4; + map["status"] = reply->reply()->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + map["statusText"] = QString("%1 %2").arg( map["status"].toString() ) + .arg( reply->reply()->attribute( QNetworkRequest::HttpReasonPhraseAttribute ).toString() ); + + bool ok = false; + QString json = QString::fromUtf8( TomahawkUtils::toJson( map, &ok ) ); + Q_ASSERT( ok ); + + QString javascript = QString( "Tomahawk.nativeAsyncRequestDone( %1, %2 );" ) + .arg( QString::number( requestId ) ) + .arg( json ); + m_resolver->d_func()->engine->mainFrame()->evaluateJavaScript( javascript ); +} + + bool JSResolverHelper::hasFuzzyIndex() { diff --git a/src/libtomahawk/resolvers/JSResolverHelper.h b/src/libtomahawk/resolvers/JSResolverHelper.h index 3169f2eb4..2518dc26b 100644 --- a/src/libtomahawk/resolvers/JSResolverHelper.h +++ b/src/libtomahawk/resolvers/JSResolverHelper.h @@ -49,6 +49,19 @@ public: Q_INVOKABLE void reportStreamUrl( const QString& qid, const QString& streamUrl ); Q_INVOKABLE void reportStreamUrl( const QString& qid, const QString& streamUrl, const QVariantMap& headers ); + /** + * Native handler for asynchronous HTTP requests. + * + * This handler shall only be used if we cannot achieve the request with + * XMLHttpRequest as that would be more efficient. + * Use cases are: + * * Referer header: Stripped on MacOS and the specification says it + * should be stripped + */ + Q_INVOKABLE void nativeAsyncRequest( int requestId, const QString& url, + const QVariantMap& headers, + const QVariantMap& options ); + /** * Clucene indices for JS resolvers **/ @@ -89,6 +102,7 @@ private slots: void gotStreamUrl( IODeviceCallback callback, NetworkReply* reply ); void tracksAdded( const QList& tracks, const Tomahawk::ModelMode, const Tomahawk::collection_ptr& collection ); void pltemplateTracksLoadedForUrl( const QString& url, const Tomahawk::playlisttemplate_ptr& pltemplate ); + void nativeAsyncRequestDone( int requestId, NetworkReply* reply ); private: Tomahawk::query_ptr parseTrack( const QVariantMap& track );