diff --git a/include/gui.hpp b/include/gui.hpp index 1669e39a..78138cea 100644 --- a/include/gui.hpp +++ b/include/gui.hpp @@ -24,6 +24,7 @@ extern float gPixelRatio; void guiInit(); void guiDestroy(); void guiRun(); +void guiClose(); void guiCursorLock(); void guiCursorUnlock(); bool guiIsModPressed(); diff --git a/include/plugin.hpp b/include/plugin.hpp index b7ac227b..e616b549 100644 --- a/include/plugin.hpp +++ b/include/plugin.hpp @@ -37,7 +37,6 @@ extern std::string gToken; void pluginInit(); void pluginDestroy(); - void pluginLogIn(std::string email, std::string password); void pluginLogOut(); void pluginRefresh(); diff --git a/include/util.hpp b/include/util.hpp index 84c40cd1..25c0b279 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -52,6 +52,10 @@ std::string stringf(const char *format, ...); /** Truncates and adds "..." to a string, not exceeding `len` characters */ std::string ellipsize(std::string s, size_t len); +/** Opens a URL, also happens to work with PDFs and folders. +Shell injection is possible, so make sure the URL is trusted or hard coded. +May block, so open in a new thread. +*/ void openBrowser(std::string url); /** Threads which obtain a VIPLock will cause wait() to block for other less important threads. diff --git a/src/app/PluginManagerWidget.cpp b/src/app/PluginManagerWidget.cpp index 214b20b9..ba8e74fc 100644 --- a/src/app/PluginManagerWidget.cpp +++ b/src/app/PluginManagerWidget.cpp @@ -16,7 +16,7 @@ PluginManagerWidget::PluginManagerWidget() { struct RegisterButton : Button { void onAction() { - std::thread t(openBrowser, "http://vcvrack.com/"); + std::thread t(openBrowser, "https://vcvrack.com/"); t.detach(); } }; @@ -80,7 +80,7 @@ PluginManagerWidget::PluginManagerWidget() { struct ManageButton : Button { void onAction() { - std::thread t(openBrowser, "http://vcvrack.com/"); + std::thread t(openBrowser, "https://vcvrack.com/"); t.detach(); } }; diff --git a/src/app/RackScene.cpp b/src/app/RackScene.cpp index 304c17d7..533dc60a 100644 --- a/src/app/RackScene.cpp +++ b/src/app/RackScene.cpp @@ -39,7 +39,7 @@ Widget *RackScene::onHoverKey(Vec pos, int key) { break; case GLFW_KEY_Q: if (guiIsModPressed()) { - glfwSetWindowShouldClose(gWindow, GLFW_TRUE); + guiClose(); return this; } break; diff --git a/src/gui.cpp b/src/gui.cpp index dbb25433..9de937cc 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -301,6 +301,10 @@ void guiRun() { } } +void guiClose() { + glfwSetWindowShouldClose(gWindow, GLFW_TRUE); +} + void guiCursorLock() { #ifdef ARCH_MAC glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); diff --git a/src/plugin.cpp b/src/plugin.cpp index e35f1b41..fa88655e 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include // for MAXPATHLEN @@ -21,7 +22,9 @@ #include #include "plugin.hpp" +#include "gui.hpp" // for guiClose #include "util/request.hpp" +#include "../ext/osdialog/osdialog.h" namespace rack { @@ -34,6 +37,8 @@ static float downloadProgress = 0.0; static std::string downloadName; static std::string loginStatus; +static std::string apiHost = "http://api.vcvrack.com"; + Plugin::~Plugin() { for (Model *model : models) { @@ -41,7 +46,6 @@ Plugin::~Plugin() { } } - static int loadPlugin(std::string slug) { #if ARCH_LIN std::string path = "./plugins/" + slug + "/plugin.so"; @@ -91,35 +95,8 @@ static int loadPlugin(std::string slug) { return 0; } -void pluginInit() { - // Load core - // This function is defined in core.cpp - Plugin *corePlugin = init(); - gPlugins.push_back(corePlugin); - - // Search for plugin libraries - DIR *dir = opendir("plugins"); - if (dir) { - struct dirent *d; - while ((d = readdir(dir))) { - if (d->d_name[0] == '.') - continue; - loadPlugin(d->d_name); - } - closedir(dir); - } -} - -void pluginDestroy() { - for (Plugin *plugin : gPlugins) { - // TODO free shared library handle with `dlclose` or `FreeLibrary` - delete plugin; - } - gPlugins.clear(); -} - //////////////////// -// CURL and libzip helpers +// plugin helpers //////////////////// static int extractZipHandle(zip_t *za, const char *dir) { @@ -179,42 +156,7 @@ static int extractZip(const char *filename, const char *dir) { return err; } -//////////////////// -// plugin manager -//////////////////// - -static std::string apiHost = "http://api.vcvrack.com"; - -void pluginLogIn(std::string email, std::string password) { - json_t *reqJ = json_object(); - json_object_set(reqJ, "email", json_string(email.c_str())); - json_object_set(reqJ, "password", json_string(password.c_str())); - json_t *resJ = requestJson(POST_METHOD, apiHost + "/token", reqJ); - json_decref(reqJ); - - if (resJ) { - json_t *errorJ = json_object_get(resJ, "error"); - if (errorJ) { - const char *errorStr = json_string_value(errorJ); - loginStatus = errorStr; - } - else { - json_t *tokenJ = json_object_get(resJ, "token"); - if (tokenJ) { - const char *tokenStr = json_string_value(tokenJ); - gToken = tokenStr; - loginStatus = ""; - } - } - json_decref(resJ); - } -} - -void pluginLogOut() { - gToken = ""; -} - -static void pluginRefreshPlugin(json_t *pluginJ) { +static void refreshPurchase(json_t *pluginJ) { json_t *slugJ = json_object_get(pluginJ, "slug"); if (!slugJ) return; const char *slug = json_string_value(slugJ); @@ -258,6 +200,92 @@ static void pluginRefreshPlugin(json_t *pluginJ) { downloadName = ""; } +static void checkVersion() { + json_t *resJ = requestJson(GET_METHOD, apiHost + "/version", NULL); + + if (resJ) { + json_t *versionJ = json_object_get(resJ, "version"); + if (versionJ) { + const char *version = json_string_value(versionJ); + if (version && version != gApplicationVersion) { + char text[1024]; + snprintf(text, sizeof(text), "Rack %s is available.\n\nYou have Rack %s.\n\nWould you like to download the new version on the website?", version, gApplicationVersion.c_str()); + if (osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, text)) { + std::thread t(openBrowser, "https://vcvrack.com/"); + t.detach(); + guiClose(); + } + } + } + json_decref(resJ); + } +} + +//////////////////// +// plugin API +//////////////////// + +void pluginInit() { + if (gApplicationVersion != "dev") { + std::thread versionThread(checkVersion); + versionThread.detach(); + } + + // Load core + // This function is defined in core.cpp + Plugin *corePlugin = init(); + gPlugins.push_back(corePlugin); + + // Search for plugin libraries + DIR *dir = opendir("plugins"); + if (dir) { + struct dirent *d; + while ((d = readdir(dir))) { + if (d->d_name[0] == '.') + continue; + loadPlugin(d->d_name); + } + closedir(dir); + } +} + +void pluginDestroy() { + for (Plugin *plugin : gPlugins) { + // TODO free shared library handle with `dlclose` or `FreeLibrary` + delete plugin; + } + gPlugins.clear(); +} + +void pluginLogIn(std::string email, std::string password) { + json_t *reqJ = json_object(); + json_object_set(reqJ, "email", json_string(email.c_str())); + json_object_set(reqJ, "password", json_string(password.c_str())); + json_t *resJ = requestJson(POST_METHOD, apiHost + "/token", reqJ); + json_decref(reqJ); + + if (resJ) { + json_t *errorJ = json_object_get(resJ, "error"); + if (errorJ) { + const char *errorStr = json_string_value(errorJ); + loginStatus = errorStr; + } + else { + json_t *tokenJ = json_object_get(resJ, "token"); + if (tokenJ) { + const char *tokenStr = json_string_value(tokenJ); + gToken = tokenStr; + loginStatus = ""; + } + } + json_decref(resJ); + } +} + +void pluginLogOut() { + gToken = ""; +} + void pluginRefresh() { if (gToken.empty()) return; @@ -282,7 +310,7 @@ void pluginRefresh() { size_t index; json_t *purchaseJ; json_array_foreach(purchasesJ, index, purchaseJ) { - pluginRefreshPlugin(purchaseJ); + refreshPurchase(purchaseJ); } } json_decref(resJ); diff --git a/src/util.cpp b/src/util.cpp index 923d441d..411a57bf 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -54,7 +54,6 @@ std::string ellipsize(std::string s, size_t len) { } void openBrowser(std::string url) { - // shell injection is possible, so make sure the URL is trusted or hard coded #if ARCH_LIN std::string command = "xdg-open " + url; (void)system(command.c_str()); diff --git a/src/util/request.cpp b/src/util/request.cpp index 90d88e83..af9a3a62 100644 --- a/src/util/request.cpp +++ b/src/util/request.cpp @@ -17,18 +17,13 @@ static size_t writeStringCallback(char *ptr, size_t size, size_t nmemb, void *us json_t *requestJson(RequestMethod method, std::string url, json_t *dataJ) { CURL *curl = curl_easy_init(); - if (!curl) - return NULL; + assert(curl); - assert(dataJ); - char *reqStr; - if (method != GET_METHOD) { - reqStr = json_dumps(dataJ, 0); - } + char *reqStr = NULL; - // Set URL - if (method == GET_METHOD) { - if (dataJ) { + // Process data + if (dataJ) { + if (method == GET_METHOD) { // Append ?key=value&... to url url += "?"; bool isFirst = true; @@ -49,7 +44,11 @@ json_t *requestJson(RequestMethod method, std::string url, json_t *dataJ) { } } } + else { + reqStr = json_dumps(dataJ, 0); + } } + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // Set HTTP method @@ -75,7 +74,7 @@ json_t *requestJson(RequestMethod method, std::string url, json_t *dataJ) { curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); // Body callbacks - if (method != GET_METHOD) + if (reqStr) curl_easy_setopt(curl, CURLOPT_POSTFIELDS, reqStr); std::string resText; @@ -88,7 +87,7 @@ json_t *requestJson(RequestMethod method, std::string url, json_t *dataJ) { CURLcode res = curl_easy_perform(curl); // Cleanup - if (method != GET_METHOD) + if (reqStr) free(reqStr); curl_easy_cleanup(curl); curl_slist_free_all(headers);