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.

467 lines
18KB

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