/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #pragma once //============================================================================== class LicenseQueryThread : public Thread { public: LicenseQueryThread (const String& userEmail, const String& userPassword, std::function&& cb) : Thread ("LicenseQueryThread"), email (userEmail), password (userPassword), completionCallback (std::move (cb)) { startThread(); } ~LicenseQueryThread() override { signalThreadShouldExit(); waitForThreadToExit (6000); } void run() override { LicenseState state; auto errorMessage = runJob (std::make_unique (email, password), state); if (errorMessage.isEmpty()) errorMessage = runJob (std::make_unique (state.authToken), state); if (errorMessage.isNotEmpty()) state = {}; WeakReference weakThis (this); MessageManager::callAsync ([this, weakThis, state, errorMessage] { if (weakThis != nullptr) completionCallback (state, errorMessage); }); } private: //============================================================================== struct AccountEnquiryBase { virtual ~AccountEnquiryBase() = default; virtual bool isPOSTLikeRequest() const = 0; virtual String getEndpointURLSuffix() const = 0; virtual StringPairArray getParameterNamesAndValues() const = 0; virtual int getSuccessCode() const = 0; virtual String errorCodeToString (int) const = 0; virtual bool parseServerResponse (const String&, LicenseState&) = 0; }; struct UserLogin : public AccountEnquiryBase { UserLogin (const String& e, const String& p) : userEmail (e), userPassword (p) { } bool isPOSTLikeRequest() const override { return true; } String getEndpointURLSuffix() const override { return "/authenticate"; } int getSuccessCode() const override { return 200; } StringPairArray getParameterNamesAndValues() const override { StringPairArray namesAndValues; namesAndValues.set ("email", userEmail); namesAndValues.set ("password", userPassword); return namesAndValues; } String errorCodeToString (int errorCode) const override { switch (errorCode) { case 400: return "Please enter your email and password to log in."; case 401: return "Your email and password are incorrect."; case 451: return "Access denied."; default: return "Something went wrong, please try again."; } } bool parseServerResponse (const String& serverResponse, LicenseState& licenseState) override { auto json = JSON::parse (serverResponse); licenseState.authToken = json.getProperty ("token", {}).toString(); licenseState.username = json.getProperty ("user", {}).getProperty ("username", {}).toString(); auto avatarURL = json.getProperty ("user", {}).getProperty ("avatar_url", {}).toString(); if (avatarURL.isNotEmpty()) { URL url (avatarURL); if (auto stream = url.createInputStream (false, nullptr, nullptr, {}, 5000)) { MemoryBlock mb; stream->readIntoMemoryBlock (mb); licenseState.avatar = ImageFileFormat::loadFrom (mb.getData(), mb.getSize()); } } return (licenseState.authToken.isNotEmpty() && licenseState.username.isNotEmpty()); } String userEmail, userPassword; }; struct UserLicenseQuery : public AccountEnquiryBase { UserLicenseQuery (const String& authToken) : userAuthToken (authToken) { } bool isPOSTLikeRequest() const override { return false; } String getEndpointURLSuffix() const override { return "/user/licences"; } int getSuccessCode() const override { return 200; } StringPairArray getParameterNamesAndValues() const override { StringPairArray namesAndValues; namesAndValues.set ("token", userAuthToken); return namesAndValues; } String errorCodeToString (int errorCode) const override { switch (errorCode) { case 401: return "User not found or could not be verified."; default: return "User licenses info fetch failed (unknown error)."; } } bool parseServerResponse (const String& serverResponse, LicenseState& licenseState) override { auto json = JSON::parse (serverResponse); if (auto* licensesJson = json.getArray()) { StringArray licenseTypes; for (auto& license : *licensesJson) { auto name = license.getProperty ("product_name", {}).toString(); auto status = license.getProperty ("status", {}).toString(); if (name == "Projucer" && status == "active") licenseTypes.add (license.getProperty ("licence_type", {}).toString()); } licenseTypes.removeEmptyStrings(); licenseTypes.removeDuplicates (false); licenseState.type = [licenseTypes] () { if (licenseTypes.contains ("juce-pro")) return LicenseState::Type::pro; else if (licenseTypes.contains ("juce-indie")) return LicenseState::Type::indie; else if (licenseTypes.contains ("juce-personal")) return LicenseState::Type::personal; else if (licenseTypes.contains ("juce-edu")) return LicenseState::Type::educational; return LicenseState::Type::none; }(); return (licenseState.type != LicenseState::Type::none); } return false; } String userAuthToken; }; //============================================================================== static String postDataStringAsJSON (const StringPairArray& parameters) { DynamicObject::Ptr d (new DynamicObject()); for (auto& key : parameters.getAllKeys()) d->setProperty (key, parameters[key]); return JSON::toString (var (d.get())); } String runJob (std::unique_ptr accountEnquiryJob, LicenseState& state) { const String endpointURL = "https://api.roli.com/api/v1"; const String extraHeaders = "Content-Type: application/json"; auto url = URL (endpointURL + accountEnquiryJob->getEndpointURLSuffix()); auto isPOST = accountEnquiryJob->isPOSTLikeRequest(); if (isPOST) url = url.withPOSTData (postDataStringAsJSON (accountEnquiryJob->getParameterNamesAndValues())); else url = url.withParameters (accountEnquiryJob->getParameterNamesAndValues()); if (threadShouldExit()) return "Cancelled."; int statusCode = 0; auto urlStream = url.createInputStream (isPOST, nullptr, nullptr, extraHeaders, 5000, nullptr, &statusCode); if (urlStream == nullptr) return "Failed to connect to the web server."; if (statusCode != accountEnquiryJob->getSuccessCode()) return accountEnquiryJob->errorCodeToString (statusCode); if (threadShouldExit()) return "Cancelled."; String response; for (;;) { char buffer [8192]; auto num = urlStream->read (buffer, sizeof (buffer)); if (threadShouldExit()) return "Cancelled."; if (num <= 0) break; response += buffer; } if (threadShouldExit()) return "Cancelled."; if (! accountEnquiryJob->parseServerResponse (response, state)) return "Failed to parse server response."; return {}; } //============================================================================== const String email, password; const std::function completionCallback; //============================================================================== JUCE_DECLARE_WEAK_REFERENCEABLE (LicenseQueryThread) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseQueryThread) };