The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

459 lines
17KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 6 technical preview.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For this technical preview, this file is not subject to commercial licensing.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. #pragma once
  14. //==============================================================================
  15. struct NetWorkerThread : public Thread,
  16. private AsyncUpdater
  17. {
  18. NetWorkerThread() : Thread ("License") {}
  19. ~NetWorkerThread() override
  20. {
  21. JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
  22. signalThreadShouldExit();
  23. cancelPendingUpdate();
  24. finished.signal();
  25. {
  26. ScopedLock lock (weakReferenceLock);
  27. if (currentInputStream != nullptr)
  28. currentInputStream->cancel();
  29. }
  30. waitForThreadToExit (-1);
  31. }
  32. //==============================================================================
  33. void executeOnMessageThreadAndBlock (std::function<void()> f, bool signalWhenFinished = true)
  34. {
  35. // only call this on the worker thread
  36. jassert (Thread::getCurrentThreadId() == getThreadId());
  37. if (! isWaiting)
  38. {
  39. ScopedValueSetter<bool> reentrant (isWaiting, true);
  40. finished.reset();
  41. if (! threadShouldExit())
  42. {
  43. functionToExecute = [signalWhenFinished, f, this] () { f(); if (signalWhenFinished) finished.signal(); };
  44. triggerAsyncUpdate();
  45. finished.wait (-1);
  46. }
  47. }
  48. else
  49. {
  50. // only one task at a time
  51. jassertfalse;
  52. return;
  53. }
  54. }
  55. WebInputStream* getSharedWebInputStream (const URL& url, const bool usePost)
  56. {
  57. ScopedLock lock (weakReferenceLock);
  58. if (threadShouldExit())
  59. return nullptr;
  60. jassert (currentInputStream == nullptr);
  61. return (currentInputStream = new WeakWebInputStream (*this, url, usePost));
  62. }
  63. bool isWaiting = false;
  64. WaitableEvent finished;
  65. private:
  66. //==============================================================================
  67. void handleAsyncUpdate() override
  68. {
  69. if (functionToExecute)
  70. {
  71. std::function<void()> f;
  72. std::swap (f, functionToExecute);
  73. if (! threadShouldExit())
  74. f();
  75. }
  76. }
  77. //==============================================================================
  78. struct WeakWebInputStream : public WebInputStream
  79. {
  80. WeakWebInputStream (NetWorkerThread& workerThread, const URL& url, const bool usePost)
  81. : WebInputStream (url, usePost), owner (workerThread) {}
  82. ~WeakWebInputStream()
  83. {
  84. ScopedLock lock (owner.weakReferenceLock);
  85. owner.currentInputStream = nullptr;
  86. }
  87. NetWorkerThread& owner;
  88. WeakReference<WeakWebInputStream>::Master masterReference;
  89. friend class WeakReference<WeakWebInputStream>;
  90. };
  91. //==============================================================================
  92. friend struct WeakWebInputStream;
  93. std::function<void()> functionToExecute;
  94. CriticalSection weakReferenceLock;
  95. WebInputStream* currentInputStream = nullptr;
  96. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NetWorkerThread)
  97. };
  98. //==============================================================================
  99. //==============================================================================
  100. //==============================================================================
  101. struct LicenseThread : NetWorkerThread
  102. {
  103. LicenseThread (LicenseController& licenseController, bool shouldSelectNewLicense)
  104. : owner (licenseController), selectNewLicense (shouldSelectNewLicense)
  105. {
  106. startThread();
  107. }
  108. String getAuthToken()
  109. {
  110. if (owner.state.authToken.isNotEmpty())
  111. return owner.state.authToken;
  112. selectNewLicense = false;
  113. HashMap<String, String> result;
  114. if (! queryWebview ("https://auth.roli.com/signin/projucer?redirect=projucer://receive-auth-token?token=",
  115. "receive-auth-token", result))
  116. return {};
  117. return result["token"];
  118. }
  119. // returns true if any information was updated
  120. void updateUserInfo (LicenseState& stateToUpdate)
  121. {
  122. jassert (stateToUpdate.authToken.isNotEmpty());
  123. auto accessTokenHeader = "x-access-token: " + stateToUpdate.authToken;
  124. std::unique_ptr<WebInputStream> shared (getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user"), false));
  125. if (shared != nullptr)
  126. {
  127. const int statusCode = shared->withExtraHeaders (accessTokenHeader).getStatusCode();
  128. if (statusCode == 200)
  129. {
  130. var result = JSON::parse (shared->readEntireStreamAsString());
  131. shared.reset();
  132. auto newState = licenseStateFromJSON (result, stateToUpdate.authToken, stateToUpdate.avatar);
  133. if (newState.type != LicenseState::Type::notLoggedIn)
  134. stateToUpdate = newState;
  135. }
  136. else if (statusCode == 401)
  137. {
  138. selectNewLicense = false;
  139. // un-authorised: token has expired
  140. stateToUpdate = LicenseState();
  141. }
  142. }
  143. }
  144. void updateLicenseType (LicenseState& stateToUpdate)
  145. {
  146. bool requiredWebview = false;
  147. String licenseChooserPage = "https://juce.com/webviews/select_license";
  148. jassert (stateToUpdate.authToken.isNotEmpty());
  149. jassert (stateToUpdate.type != LicenseState::Type::notLoggedIn);
  150. auto accessTokenHeader = "x-access-token: " + stateToUpdate.authToken;
  151. StringArray licenses;
  152. while ((licenses.isEmpty() || selectNewLicense) && ! threadShouldExit())
  153. {
  154. static Identifier licenseTypeIdentifier ("type");
  155. static Identifier licenseStatusIdentifier ("status");
  156. static Identifier projucerLicenseTypeIdentifier ("licence_type");
  157. static Identifier productNameIdentifier ("product_name");
  158. static Identifier licenseIdentifier ("licence");
  159. static Identifier serialIdentifier ("serial_number");
  160. static Identifier versionIdentifier ("product_version");
  161. static Identifier searchInternalIdentifier ("search_internal_id");
  162. if (! selectNewLicense)
  163. {
  164. std::unique_ptr<WebInputStream> shared (getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/licences?search_internal_id=com.roli.projucer&version=5"),
  165. false));
  166. if (shared == nullptr)
  167. break;
  168. var json = JSON::parse (shared->withExtraHeaders (accessTokenHeader)
  169. .readEntireStreamAsString());
  170. shared.reset();
  171. if (auto* jsonLicenses = json.getArray())
  172. {
  173. for (auto& v : *jsonLicenses)
  174. {
  175. if (auto* obj = v.getDynamicObject())
  176. {
  177. const String& productType = obj->getProperty (projucerLicenseTypeIdentifier);
  178. const String& status = obj->getProperty (licenseStatusIdentifier);
  179. if (productType.isNotEmpty() && (status.isEmpty() || status == "active"))
  180. licenses.add (productType);
  181. }
  182. }
  183. }
  184. else
  185. {
  186. // no internet -> then use the last valid license
  187. if (stateToUpdate.type != LicenseState::Type::notLoggedIn
  188. && stateToUpdate.type != LicenseState::Type::noLicenseChosenYet)
  189. return;
  190. }
  191. if (! licenses.isEmpty())
  192. break;
  193. }
  194. // ask the user to select a license
  195. HashMap<String, String> result;
  196. requiredWebview = true;
  197. if (! queryWebview (licenseChooserPage, {}, result))
  198. break;
  199. const String& redirectURL = result["page-redirect"];
  200. const String& productKey = result["register-product"];
  201. const String& chosenLicenseType = result["redeem-licence-type"];
  202. if (redirectURL.isNotEmpty())
  203. {
  204. licenseChooserPage = "https://juce.com/webviews/register-product";
  205. continue;
  206. }
  207. if (productKey.isNotEmpty())
  208. {
  209. DynamicObject::Ptr redeemObject (new DynamicObject());
  210. redeemObject->setProperty (serialIdentifier, productKey);
  211. String postData (JSON::toString (var (redeemObject.get())));
  212. std::unique_ptr<WebInputStream> shared (getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/products").withPOSTData (postData),
  213. true));
  214. if (shared == nullptr)
  215. break;
  216. int statusCode = shared->withExtraHeaders (accessTokenHeader)
  217. .withExtraHeaders ("Content-Type: application/json")
  218. .getStatusCode();
  219. licenseChooserPage = String ("https://juce.com/webviews/register-product?error=")
  220. + String (statusCode == 404 ? "invalid" : "server");
  221. if (statusCode == 200)
  222. selectNewLicense = false;
  223. continue;
  224. }
  225. if (chosenLicenseType.isNotEmpty())
  226. {
  227. // redeem the license
  228. DynamicObject::Ptr jsonLicenseObject (new DynamicObject());
  229. jsonLicenseObject->setProperty (projucerLicenseTypeIdentifier, chosenLicenseType);
  230. jsonLicenseObject->setProperty (versionIdentifier, 5);
  231. DynamicObject::Ptr jsonLicenseRequest (new DynamicObject());
  232. jsonLicenseRequest->setProperty (licenseIdentifier, var (jsonLicenseObject.get()));
  233. jsonLicenseRequest->setProperty (searchInternalIdentifier, "com.roli.projucer");
  234. jsonLicenseRequest->setProperty (licenseTypeIdentifier, "software");
  235. String postData (JSON::toString (var (jsonLicenseRequest.get())));
  236. std::unique_ptr<WebInputStream> shared (getSharedWebInputStream (URL ("https://api.roli.com/api/v1/user/products/redeem")
  237. .withPOSTData (postData),
  238. true));
  239. if (shared != nullptr)
  240. {
  241. int statusCode = shared->withExtraHeaders (accessTokenHeader)
  242. .withExtraHeaders ("Content-Type: application/json")
  243. .getStatusCode();
  244. if (statusCode == 200)
  245. selectNewLicense = false;
  246. continue;
  247. }
  248. }
  249. break;
  250. }
  251. HashMap<String, String> result;
  252. if (requiredWebview && ! threadShouldExit())
  253. queryWebview ("https://juce.com/webviews/registration-complete", "licence_provisioned", result);
  254. stateToUpdate.type = getBestLicenseTypeFromLicenses (licenses);
  255. }
  256. //==============================================================================
  257. void run() override
  258. {
  259. LicenseState workState (owner.state);
  260. while (! threadShouldExit())
  261. {
  262. workState.authToken = getAuthToken();
  263. if (workState.authToken.isEmpty())
  264. return;
  265. // read the user information
  266. updateUserInfo (workState);
  267. if (threadShouldExit())
  268. return;
  269. updateIfChanged (workState);
  270. // if the last step logged us out then retry
  271. if (workState.authToken.isEmpty())
  272. continue;
  273. // check if the license has changed
  274. updateLicenseType (workState);
  275. if (threadShouldExit())
  276. return;
  277. updateIfChanged (workState);
  278. closeWebviewOnMessageThread (0);
  279. finished.wait (60 * 5 * 1000);
  280. }
  281. }
  282. //==============================================================================
  283. LicenseState licenseStateFromJSON (const var& json, const String& authToken, const Image& fallbackAvatar)
  284. {
  285. static Identifier usernameIdentifier ("username");
  286. static Identifier emailIdentifier ("email");
  287. static Identifier avatarURLIdentifier ("avatar_url");
  288. LicenseState result;
  289. if (auto* obj = json.getDynamicObject())
  290. {
  291. result.type = LicenseState::Type::noLicenseChosenYet;
  292. result.username = obj->getProperty (usernameIdentifier);
  293. result.authToken = authToken;
  294. result.email = obj->getProperty (emailIdentifier);
  295. result.avatar = fallbackAvatar;
  296. String avatarURL = obj->getProperty (avatarURLIdentifier);
  297. if (avatarURL.isNotEmpty())
  298. {
  299. std::unique_ptr<WebInputStream> shared (getSharedWebInputStream (URL (avatarURL), false));
  300. if (shared != nullptr)
  301. {
  302. MemoryBlock mb;
  303. shared->readIntoMemoryBlock (mb);
  304. result.avatar = ImageFileFormat::loadFrom (mb.getData(), mb.getSize());
  305. }
  306. }
  307. }
  308. return result;
  309. }
  310. //==============================================================================
  311. bool queryWebview (const String& startURL, const String& valueToQuery, HashMap<String, String>& result)
  312. {
  313. executeOnMessageThreadAndBlock ([&] () { owner.queryWebview (startURL, valueToQuery, result); }, false);
  314. return (! threadShouldExit());
  315. }
  316. void closeWebviewOnMessageThread (int result)
  317. {
  318. executeOnMessageThreadAndBlock ([this, result] () { owner.closeWebview (result); });
  319. }
  320. static bool stringArrayContainsSubstring (const StringArray& stringArray, const String& substring)
  321. {
  322. jassert (substring.isNotEmpty());
  323. for (auto element : stringArray)
  324. if (element.containsIgnoreCase (substring))
  325. return true;
  326. return false;
  327. }
  328. static LicenseState::Type getBestLicenseTypeFromLicenses (const StringArray& licenses)
  329. {
  330. if (stringArrayContainsSubstring (licenses, "juce-pro")) return LicenseState::Type::pro;
  331. else if (stringArrayContainsSubstring (licenses, "juce-indie")) return LicenseState::Type::indie;
  332. else if (stringArrayContainsSubstring (licenses, "juce-personal")) return LicenseState::Type::personal;
  333. else if (stringArrayContainsSubstring (licenses, "juce-edu")) return LicenseState::Type::edu;
  334. return LicenseState::Type::noLicenseChosenYet;
  335. }
  336. void updateIfChanged (const LicenseState& newState)
  337. {
  338. LicenseState updatedState (owner.state);
  339. bool changed = false;
  340. bool shouldUpdateLicenseType = (newState.type != LicenseState::Type::noLicenseChosenYet
  341. || updatedState.type == LicenseState::Type::notLoggedIn);
  342. if (newState.type != LicenseState::Type::notLoggedIn) updatedState.avatar = newState.avatar;
  343. if (owner.state.type != newState.type && shouldUpdateLicenseType) { updatedState.type = newState.type; changed = true; }
  344. if (owner.state.authToken != newState.authToken) { updatedState.authToken = newState.authToken; changed = true; }
  345. if (owner.state.username != newState.username) { updatedState.username = newState.username; changed = true; }
  346. if (owner.state.email != newState.email) { updatedState.email = newState.email; changed = true; }
  347. if (owner.state.avatar.isValid() != newState.avatar.isValid()) { changed = true; }
  348. if (changed)
  349. executeOnMessageThreadAndBlock ([this, updatedState] { owner.updateState (updatedState); });
  350. }
  351. //==============================================================================
  352. LicenseController& owner;
  353. bool selectNewLicense;
  354. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseThread)
  355. };