/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2017 - ROLI Ltd. 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 //============================================================================== struct NetWorkerThread : public Thread, private AsyncUpdater { NetWorkerThread() : Thread ("License") {} ~NetWorkerThread() override { JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED signalThreadShouldExit(); cancelPendingUpdate(); finished.signal(); { ScopedLock lock (weakReferenceLock); if (currentInputStream != nullptr) currentInputStream->cancel(); } waitForThreadToExit (-1); } //============================================================================== void executeOnMessageThreadAndBlock (std::function f, bool signalWhenFinished = true) { // only call this on the worker thread jassert (Thread::getCurrentThreadId() == getThreadId()); if (! isWaiting) { ScopedValueSetter reentrant (isWaiting, true); finished.reset(); if (! threadShouldExit()) { functionToExecute = [signalWhenFinished, f, this] () { f(); if (signalWhenFinished) finished.signal(); }; triggerAsyncUpdate(); finished.wait (-1); } } else { // only one task at a time jassertfalse; return; } } WebInputStream* getSharedWebInputStream (const URL& url, const bool usePost) { ScopedLock lock (weakReferenceLock); if (threadShouldExit()) return nullptr; jassert (currentInputStream == nullptr); return (currentInputStream = new WeakWebInputStream (*this, url, usePost)); } bool isWaiting = false; WaitableEvent finished; private: //============================================================================== void handleAsyncUpdate() override { if (functionToExecute) { std::function f; std::swap (f, functionToExecute); if (! threadShouldExit()) f(); } } //============================================================================== struct WeakWebInputStream : public WebInputStream { WeakWebInputStream (NetWorkerThread& workerThread, const URL& url, const bool usePost) : WebInputStream (url, usePost), owner (workerThread) {} ~WeakWebInputStream() { ScopedLock lock (owner.weakReferenceLock); owner.currentInputStream = nullptr; } NetWorkerThread& owner; WeakReference::Master masterReference; friend class WeakReference; }; //============================================================================== friend struct WeakWebInputStream; std::function functionToExecute; CriticalSection weakReferenceLock; WebInputStream* currentInputStream = nullptr; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NetWorkerThread) }; //============================================================================== //============================================================================== //============================================================================== struct LicenseThread : NetWorkerThread { LicenseThread (LicenseController& licenseController, bool shouldSelectNewLicense) : owner (licenseController), selectNewLicense (shouldSelectNewLicense) { startThread(); } String getAuthToken() { if (owner.state.authToken.isNotEmpty()) return owner.state.authToken; selectNewLicense = false; HashMap result; if (! queryWebview ("https://auth.roli.com/signin/projucer?redirect=projucer://receive-auth-token?token=", "receive-auth-token", result)) return {}; return result["token"]; } // returns true if any information was updated void updateUserInfo (LicenseState& stateToUpdate) { jassert (stateToUpdate.authToken.isNotEmpty()); auto accessTokenHeader = "x-access-token: " + stateToUpdate.authToken; std::unique_ptr shared (getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user"), false)); if (shared != nullptr) { const int statusCode = shared->withExtraHeaders (accessTokenHeader).getStatusCode(); if (statusCode == 200) { var result = JSON::parse (shared->readEntireStreamAsString()); shared.reset(); auto newState = licenseStateFromJSON (result, stateToUpdate.authToken, stateToUpdate.avatar); if (newState.type != LicenseState::Type::notLoggedIn) stateToUpdate = newState; } else if (statusCode == 401) { selectNewLicense = false; // un-authorised: token has expired stateToUpdate = LicenseState(); } } } void updateLicenseType (LicenseState& stateToUpdate) { bool requiredWebview = false; String licenseChooserPage = "https://juce.com/webviews/select_license"; jassert (stateToUpdate.authToken.isNotEmpty()); jassert (stateToUpdate.type != LicenseState::Type::notLoggedIn); auto accessTokenHeader = "x-access-token: " + stateToUpdate.authToken; StringArray licenses; while ((licenses.isEmpty() || selectNewLicense) && ! threadShouldExit()) { static Identifier licenseTypeIdentifier ("type"); static Identifier licenseStatusIdentifier ("status"); static Identifier projucerLicenseTypeIdentifier ("licence_type"); static Identifier productNameIdentifier ("product_name"); static Identifier licenseIdentifier ("licence"); static Identifier serialIdentifier ("serial_number"); static Identifier versionIdentifier ("product_version"); static Identifier searchInternalIdentifier ("search_internal_id"); if (! selectNewLicense) { std::unique_ptr shared (getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/licences?search_internal_id=com.roli.projucer&version=5"), false)); if (shared == nullptr) break; var json = JSON::parse (shared->withExtraHeaders (accessTokenHeader) .readEntireStreamAsString()); shared.reset(); if (auto* jsonLicenses = json.getArray()) { for (auto& v : *jsonLicenses) { if (auto* obj = v.getDynamicObject()) { const String& productType = obj->getProperty (projucerLicenseTypeIdentifier); const String& status = obj->getProperty (licenseStatusIdentifier); if (productType.isNotEmpty() && (status.isEmpty() || status == "active")) licenses.add (productType); } } } else { // no internet -> then use the last valid license if (stateToUpdate.type != LicenseState::Type::notLoggedIn && stateToUpdate.type != LicenseState::Type::noLicenseChosenYet) return; } if (! licenses.isEmpty()) break; } // ask the user to select a license HashMap result; requiredWebview = true; if (! queryWebview (licenseChooserPage, {}, result)) break; const String& redirectURL = result["page-redirect"]; const String& productKey = result["register-product"]; const String& chosenLicenseType = result["redeem-licence-type"]; if (redirectURL.isNotEmpty()) { licenseChooserPage = "https://juce.com/webviews/register-product"; continue; } if (productKey.isNotEmpty()) { DynamicObject::Ptr redeemObject (new DynamicObject()); redeemObject->setProperty (serialIdentifier, productKey); String postData (JSON::toString (var (redeemObject.get()))); std::unique_ptr shared (getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/products").withPOSTData (postData), true)); if (shared == nullptr) break; int statusCode = shared->withExtraHeaders (accessTokenHeader) .withExtraHeaders ("Content-Type: application/json") .getStatusCode(); licenseChooserPage = String ("https://juce.com/webviews/register-product?error=") + String (statusCode == 404 ? "invalid" : "server"); if (statusCode == 200) selectNewLicense = false; continue; } if (chosenLicenseType.isNotEmpty()) { // redeem the license DynamicObject::Ptr jsonLicenseObject (new DynamicObject()); jsonLicenseObject->setProperty (projucerLicenseTypeIdentifier, chosenLicenseType); jsonLicenseObject->setProperty (versionIdentifier, 5); DynamicObject::Ptr jsonLicenseRequest (new DynamicObject()); jsonLicenseRequest->setProperty (licenseIdentifier, var (jsonLicenseObject.get())); jsonLicenseRequest->setProperty (searchInternalIdentifier, "com.roli.projucer"); jsonLicenseRequest->setProperty (licenseTypeIdentifier, "software"); String postData (JSON::toString (var (jsonLicenseRequest.get()))); std::unique_ptr shared (getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/products/redeem") .withPOSTData (postData), true)); if (shared != nullptr) { int statusCode = shared->withExtraHeaders (accessTokenHeader) .withExtraHeaders ("Content-Type: application/json") .getStatusCode(); if (statusCode == 200) selectNewLicense = false; continue; } } break; } HashMap result; if (requiredWebview && ! threadShouldExit()) queryWebview ("https://juce.com/webviews/registration-complete", "licence_provisioned", result); stateToUpdate.type = getBestLicenseTypeFromLicenses (licenses); } //============================================================================== void run() override { LicenseState workState (owner.state); while (! threadShouldExit()) { workState.authToken = getAuthToken(); if (workState.authToken.isEmpty()) return; // read the user information updateUserInfo (workState); if (threadShouldExit()) return; updateIfChanged (workState); // if the last step logged us out then retry if (workState.authToken.isEmpty()) continue; // check if the license has changed updateLicenseType (workState); if (threadShouldExit()) return; updateIfChanged (workState); closeWebviewOnMessageThread (0); finished.wait (60 * 5 * 1000); } } //============================================================================== LicenseState licenseStateFromJSON (const var& json, const String& authToken, const Image& fallbackAvatar) { static Identifier usernameIdentifier ("username"); static Identifier emailIdentifier ("email"); static Identifier avatarURLIdentifier ("avatar_url"); LicenseState result; if (auto* obj = json.getDynamicObject()) { result.type = LicenseState::Type::noLicenseChosenYet; result.username = obj->getProperty (usernameIdentifier); result.authToken = authToken; result.email = obj->getProperty (emailIdentifier); result.avatar = fallbackAvatar; String avatarURL = obj->getProperty (avatarURLIdentifier); if (avatarURL.isNotEmpty()) { std::unique_ptr shared (getSharedWebInputStream (URL (avatarURL), false)); if (shared != nullptr) { MemoryBlock mb; shared->readIntoMemoryBlock (mb); result.avatar = ImageFileFormat::loadFrom (mb.getData(), mb.getSize()); } } } return result; } //============================================================================== bool queryWebview (const String& startURL, const String& valueToQuery, HashMap& result) { executeOnMessageThreadAndBlock ([&] () { owner.queryWebview (startURL, valueToQuery, result); }, false); return (! threadShouldExit()); } void closeWebviewOnMessageThread (int result) { executeOnMessageThreadAndBlock ([this, result] () { owner.closeWebview (result); }); } static bool stringArrayContainsSubstring (const StringArray& stringArray, const String& substring) { jassert (substring.isNotEmpty()); for (auto element : stringArray) if (element.containsIgnoreCase (substring)) return true; return false; } static LicenseState::Type getBestLicenseTypeFromLicenses (const StringArray& licenses) { if (stringArrayContainsSubstring (licenses, "juce-pro")) return LicenseState::Type::pro; else if (stringArrayContainsSubstring (licenses, "juce-indie")) return LicenseState::Type::indie; else if (stringArrayContainsSubstring (licenses, "juce-personal")) return LicenseState::Type::personal; else if (stringArrayContainsSubstring (licenses, "juce-edu")) return LicenseState::Type::edu; return LicenseState::Type::noLicenseChosenYet; } void updateIfChanged (const LicenseState& newState) { LicenseState updatedState (owner.state); bool changed = false; bool shouldUpdateLicenseType = (newState.type != LicenseState::Type::noLicenseChosenYet || updatedState.type == LicenseState::Type::notLoggedIn); if (newState.type != LicenseState::Type::notLoggedIn) updatedState.avatar = newState.avatar; if (owner.state.type != newState.type && shouldUpdateLicenseType) { updatedState.type = newState.type; changed = true; } if (owner.state.authToken != newState.authToken) { updatedState.authToken = newState.authToken; changed = true; } if (owner.state.username != newState.username) { updatedState.username = newState.username; changed = true; } if (owner.state.email != newState.email) { updatedState.email = newState.email; changed = true; } if (owner.state.avatar.isValid() != newState.avatar.isValid()) { changed = true; } if (changed) executeOnMessageThreadAndBlock ([this, updatedState] { owner.updateState (updatedState); }); } //============================================================================== LicenseController& owner; bool selectNewLicense; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseThread) };