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.

453 lines
17KB

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