diff options
Diffstat (limited to 'client/SDL/aad')
-rw-r--r-- | client/SDL/aad/CMakeLists.txt | 75 | ||||
-rw-r--r-- | client/SDL/aad/dummy.cpp | 0 | ||||
-rw-r--r-- | client/SDL/aad/qt/webview_impl.cpp | 105 | ||||
-rw-r--r-- | client/SDL/aad/sdl_config.hpp.in | 3 | ||||
-rw-r--r-- | client/SDL/aad/sdl_webview.cpp | 129 | ||||
-rw-r--r-- | client/SDL/aad/sdl_webview.hpp | 38 | ||||
-rw-r--r-- | client/SDL/aad/webview_impl.hpp | 24 | ||||
-rw-r--r-- | client/SDL/aad/wrapper/README | 1 | ||||
-rw-r--r-- | client/SDL/aad/wrapper/webview.h | 2781 | ||||
-rw-r--r-- | client/SDL/aad/wrapper/webview_impl.cpp | 82 |
10 files changed, 3238 insertions, 0 deletions
diff --git a/client/SDL/aad/CMakeLists.txt b/client/SDL/aad/CMakeLists.txt new file mode 100644 index 0000000..2286542 --- /dev/null +++ b/client/SDL/aad/CMakeLists.txt @@ -0,0 +1,75 @@ +set(WITH_WEBVIEW_DEFAULT OFF) +if (UNIX AND NOT APPLE) + set(WITH_WEBVIEW_DEFAULT ON) +endif() + +option(WITH_WEBVIEW "Build with WebView support for AAD login popup browser" ${WITH_WEBVIEW_DEFAULT}) +if (WITH_WEBVIEW) + option(WITH_WEBVIEW_QT "Build with QtWebEngine support for AAD login broweser popup" OFF) + + set(SRCS + sdl_webview.hpp + webview_impl.hpp + sdl_webview.cpp + ) + set(LIBS + winpr + ) + + if (WITH_WEBVIEW_QT) + find_package(Qt5 COMPONENTS WebEngineWidgets REQUIRED) + + list(APPEND SRCS + qt/webview_impl.cpp + ) + + list(APPEND LIBS + Qt5::WebEngineWidgets + ) + else() + list(APPEND SRCS + wrapper/webview.h + wrapper/webview_impl.cpp + ) + + if (WIN32) + find_package(unofficial-webview2 CONFIG REQUIRED) + list(APPEND LIBS + unofficial::webview2::webview2 + ) + elseif(APPLE) + find_library(WEBKIT Webkit REQUIRED) + list(APPEND LIBS + ${WEBKIT} + ) + else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(WEBVIEW_GTK webkit2gtk-4.0 REQUIRED) + include_directories(${WEBVIEW_GTK_INCLUDE_DIRS}) + list(APPEND LIBS + ${WEBVIEW_GTK_LIBRARIES} + ) + endif() + endif() +else() + set(SRCS + dummy.cpp + ) +endif() + +configure_file(sdl_config.hpp.in sdl_config.hpp @ONLY) + +add_library(aad-view STATIC + ${SRCS} +) +target_include_directories(aad-view PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) +target_link_libraries(aad-view + PRIVATE + ${LIBS} +) +target_compile_definitions( + aad-view + PUBLIC + ${DEFINITIONS} +) + diff --git a/client/SDL/aad/dummy.cpp b/client/SDL/aad/dummy.cpp new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/client/SDL/aad/dummy.cpp diff --git a/client/SDL/aad/qt/webview_impl.cpp b/client/SDL/aad/qt/webview_impl.cpp new file mode 100644 index 0000000..e70cc46 --- /dev/null +++ b/client/SDL/aad/qt/webview_impl.cpp @@ -0,0 +1,105 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QApplication> +#include <QWebEngineView> +#include <QWebEngineProfile> +#include <QWebEngineUrlScheme> +#include <QWebEngineUrlSchemeHandler> +#include <QWebEngineUrlRequestJob> + +#include <string> +#include <cstdlib> +#include <cstdarg> +#include <winpr/string.h> +#include <winpr/assert.h> +#include <freerdp/log.h> +#include <freerdp/build-config.h> + +#include "../webview_impl.hpp" + +#define TAG CLIENT_TAG("sdl.webview") + +class SchemeHandler : public QWebEngineUrlSchemeHandler +{ + public: + explicit SchemeHandler(QObject* parent = nullptr) : QWebEngineUrlSchemeHandler(parent) + { + } + + void requestStarted(QWebEngineUrlRequestJob* request) override + { + QUrl url = request->requestUrl(); + + int rc = -1; + for (auto& param : url.query().split('&')) + { + QStringList pair = param.split('='); + + if (pair.size() != 2 || pair[0] != QLatin1String("code")) + continue; + + auto qc = pair[1]; + m_code = qc.toStdString(); + rc = 0; + break; + } + qApp->exit(rc); + } + + [[nodiscard]] std::string code() const + { + return m_code; + } + + private: + std::string m_code{}; +}; + +bool webview_impl_run(const std::string& title, const std::string& url, std::string& code) +{ + int argc = 1; + const auto vendor = QString::fromUtf8(FREERDP_VENDOR_STRING); + const auto product = QString::fromUtf8(FREERDP_PRODUCT_STRING); + QWebEngineUrlScheme::registerScheme(QWebEngineUrlScheme("ms-appx-web")); + + std::string wtitle = title; + char* argv[] = { wtitle.data() }; + QCoreApplication::setOrganizationName(vendor); + QCoreApplication::setApplicationName(product); + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); + + SchemeHandler handler; + QWebEngineProfile::defaultProfile()->installUrlSchemeHandler("ms-appx-web", &handler); + + QWebEngineView webview; + webview.load(QUrl(QString::fromStdString(url))); + webview.show(); + + if (app.exec() != 0) + return false; + + auto val = handler.code(); + if (val.empty()) + return false; + code = val; + + return !code.empty(); +} diff --git a/client/SDL/aad/sdl_config.hpp.in b/client/SDL/aad/sdl_config.hpp.in new file mode 100644 index 0000000..34d0751 --- /dev/null +++ b/client/SDL/aad/sdl_config.hpp.in @@ -0,0 +1,3 @@ +#pragma once + +#cmakedefine WITH_WEBVIEW diff --git a/client/SDL/aad/sdl_webview.cpp b/client/SDL/aad/sdl_webview.cpp new file mode 100644 index 0000000..b4df75b --- /dev/null +++ b/client/SDL/aad/sdl_webview.cpp @@ -0,0 +1,129 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> +#include <sstream> +#include <cstdlib> +#include <winpr/string.h> +#include <freerdp/log.h> + +#include "sdl_webview.hpp" +#include "webview_impl.hpp" + +#define TAG CLIENT_TAG("SDL.webview") + +static BOOL sdl_webview_get_rdsaad_access_token(freerdp* instance, const char* scope, + const char* req_cnf, char** token) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(scope); + WINPR_ASSERT(req_cnf); + WINPR_ASSERT(token); + + WINPR_UNUSED(instance); + + std::string client_id = "5177bc73-fd99-4c77-a90c-76844c9b6999"; + std::string redirect_uri = + "ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin%2f5177bc73-fd99-4c77-a90c-76844c9b6999"; + + *token = nullptr; + + auto url = + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + client_id + + "&response_type=code&scope=" + scope + "&redirect_uri=" + redirect_uri; + + const std::string title = "FreeRDP WebView - AAD access token"; + std::string code; + auto rc = webview_impl_run(title, url, code); + if (!rc || code.empty()) + return FALSE; + + auto token_request = "grant_type=authorization_code&code=" + code + "&client_id=" + client_id + + "&scope=" + scope + "&redirect_uri=" + redirect_uri + + "&req_cnf=" + req_cnf; + return client_common_get_access_token(instance, token_request.c_str(), token); +} + +static BOOL sdl_webview_get_avd_access_token(freerdp* instance, char** token) +{ + WINPR_ASSERT(token); + + std::string client_id = "a85cf173-4192-42f8-81fa-777a763e6e2c"; + std::string redirect_uri = + "ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin%2fa85cf173-4192-42f8-81fa-777a763e6e2c"; + std::string scope = "https%3A%2F%2Fwww.wvd.microsoft.com%2F.default"; + + *token = nullptr; + + auto url = + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + client_id + + "&response_type=code&scope=" + scope + "&redirect_uri=" + redirect_uri; + const std::string title = "FreeRDP WebView - AVD access token"; + std::string code; + auto rc = webview_impl_run(title, url, code); + if (!rc || code.empty()) + return FALSE; + + auto token_request = "grant_type=authorization_code&code=" + code + "&client_id=" + client_id + + "&scope=" + scope + "&redirect_uri=" + redirect_uri; + return client_common_get_access_token(instance, token_request.c_str(), token); +} + +BOOL sdl_webview_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token, + size_t count, ...) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(token); + switch (tokenType) + { + case ACCESS_TOKEN_TYPE_AAD: + { + if (count < 2) + { + WLog_ERR(TAG, + "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz + ", aborting", + count); + return FALSE; + } + else if (count > 2) + WLog_WARN(TAG, + "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz + ", ignoring", + count); + va_list ap; + va_start(ap, count); + const char* scope = va_arg(ap, const char*); + const char* req_cnf = va_arg(ap, const char*); + const BOOL rc = sdl_webview_get_rdsaad_access_token(instance, scope, req_cnf, token); + va_end(ap); + return rc; + } + case ACCESS_TOKEN_TYPE_AVD: + if (count != 0) + WLog_WARN(TAG, + "ACCESS_TOKEN_TYPE_AVD expected 0 additional arguments, but got %" PRIuz + ", ignoring", + count); + return sdl_webview_get_avd_access_token(instance, token); + default: + WLog_ERR(TAG, "Unexpected value for AccessTokenType [%" PRIuz "], aborting", tokenType); + return FALSE; + } +} diff --git a/client/SDL/aad/sdl_webview.hpp b/client/SDL/aad/sdl_webview.hpp new file mode 100644 index 0000000..49461d6 --- /dev/null +++ b/client/SDL/aad/sdl_webview.hpp @@ -0,0 +1,38 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <freerdp/freerdp.h> + +#include <sdl_config.hpp> + +#if defined(WITH_WEBVIEW) +#ifdef __cplusplus +extern "C" +{ +#endif + + BOOL sdl_webview_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token, + size_t count, ...); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/client/SDL/aad/webview_impl.hpp b/client/SDL/aad/webview_impl.hpp new file mode 100644 index 0000000..25bca3c --- /dev/null +++ b/client/SDL/aad/webview_impl.hpp @@ -0,0 +1,24 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +bool webview_impl_run(const std::string& title, const std::string& url, std::string& code); diff --git a/client/SDL/aad/wrapper/README b/client/SDL/aad/wrapper/README new file mode 100644 index 0000000..da906ba --- /dev/null +++ b/client/SDL/aad/wrapper/README @@ -0,0 +1 @@ +upstream at https://github.com/webview/webview/ diff --git a/client/SDL/aad/wrapper/webview.h b/client/SDL/aad/wrapper/webview.h new file mode 100644 index 0000000..4919265 --- /dev/null +++ b/client/SDL/aad/wrapper/webview.h @@ -0,0 +1,2781 @@ +/* + * MIT License + * + * Copyright (c) 2017 Serge Zaitsev + * Copyright (c) 2022 Steffen André Langnes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef WEBVIEW_H +#define WEBVIEW_H + +#ifndef WEBVIEW_API +#define WEBVIEW_API extern +#endif + +#ifndef WEBVIEW_VERSION_MAJOR +// The current library major version. +#define WEBVIEW_VERSION_MAJOR 0 +#endif + +#ifndef WEBVIEW_VERSION_MINOR +// The current library minor version. +#define WEBVIEW_VERSION_MINOR 10 +#endif + +#ifndef WEBVIEW_VERSION_PATCH +// The current library patch version. +#define WEBVIEW_VERSION_PATCH 0 +#endif + +#ifndef WEBVIEW_VERSION_PRE_RELEASE +// SemVer 2.0.0 pre-release labels prefixed with "-". +#define WEBVIEW_VERSION_PRE_RELEASE "" +#endif + +#ifndef WEBVIEW_VERSION_BUILD_METADATA +// SemVer 2.0.0 build metadata prefixed with "+". +#define WEBVIEW_VERSION_BUILD_METADATA "" +#endif + +// Utility macro for stringifying a macro argument. +#define WEBVIEW_STRINGIFY(x) #x + +// Utility macro for stringifying the result of a macro argument expansion. +#define WEBVIEW_EXPAND_AND_STRINGIFY(x) WEBVIEW_STRINGIFY(x) + +// SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. +#define WEBVIEW_VERSION_NUMBER \ + WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MAJOR) \ + "." WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MINOR) "." WEBVIEW_EXPAND_AND_STRINGIFY( \ + WEBVIEW_VERSION_PATCH) + +// Holds the elements of a MAJOR.MINOR.PATCH version number. +typedef struct +{ + // Major version. + unsigned int major; + // Minor version. + unsigned int minor; + // Patch version. + unsigned int patch; +} webview_version_t; + +// Holds the library's version information. +typedef struct +{ + // The elements of the version number. + webview_version_t version; + // SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. + char version_number[32]; + // SemVer 2.0.0 pre-release labels prefixed with "-" if specified, otherwise + // an empty string. + char pre_release[48]; + // SemVer 2.0.0 build metadata prefixed with "+", otherwise an empty string. + char build_metadata[48]; +} webview_version_info_t; + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef void* webview_t; + + // Creates a new webview instance. If debug is non-zero - developer tools will + // be enabled (if the platform supports them). Window parameter can be a + // pointer to the native window handle. If it's non-null - then child WebView + // is embedded into the given parent window. Otherwise a new window is created. + // Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be + // passed here. Returns null on failure. Creation can fail for various reasons + // such as when required runtime dependencies are missing or when window creation + // fails. + WEBVIEW_API webview_t webview_create(int debug, void* window); + + // Destroys a webview and closes the native window. + WEBVIEW_API void webview_destroy(webview_t w); + + // Runs the main loop until it's terminated. After this function exits - you + // must destroy the webview. + WEBVIEW_API void webview_run(webview_t w); + + // Stops the main loop. It is safe to call this function from another other + // background thread. + WEBVIEW_API void webview_terminate(webview_t w); + + // Posts a function to be executed on the main thread. You normally do not need + // to call this function, unless you want to tweak the native window. + WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t w, void* arg), void* arg); + + // Returns a native window handle pointer. When using GTK backend the pointer + // is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow + // pointer, when using Win32 backend the pointer is HWND pointer. + WEBVIEW_API void* webview_get_window(webview_t w); + + // Updates the title of the native window. Must be called from the UI thread. + WEBVIEW_API void webview_set_title(webview_t w, const char* title); + +// Window size hints +#define WEBVIEW_HINT_NONE 0 // Width and height are default size +#define WEBVIEW_HINT_MIN 1 // Width and height are minimum bounds +#define WEBVIEW_HINT_MAX 2 // Width and height are maximum bounds +#define WEBVIEW_HINT_FIXED 3 // Window size can not be changed by a user + // Updates native window size. See WEBVIEW_HINT constants. + WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints); + + // Navigates webview to the given URL. URL may be a properly encoded data URI. + // Examples: + // webview_navigate(w, "https://github.com/webview/webview"); + // webview_navigate(w, "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E"); + // webview_navigate(w, "data:text/html;base64,PGgxPkhlbGxvPC9oMT4="); + WEBVIEW_API void webview_navigate(webview_t w, const char* url); + + // Set webview HTML directly. + // Example: webview_set_html(w, "<h1>Hello</h1>"); + WEBVIEW_API void webview_set_html(webview_t w, const char* html); + + // Injects JavaScript code at the initialization of the new page. Every time + // the webview will open a the new page - this initialization code will be + // executed. It is guaranteed that code is executed before window.onload. + WEBVIEW_API void webview_init(webview_t w, const char* js); + + // Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also + // the result of the expression is ignored. Use RPC bindings if you want to + // receive notifications about the results of the evaluation. + WEBVIEW_API void webview_eval(webview_t w, const char* js); + + // Binds a native C callback so that it will appear under the given name as a + // global JavaScript function. Internally it uses webview_init(). Callback + // receives a request string and a user-provided argument pointer. Request + // string is a JSON array of all the arguments passed to the JavaScript + // function. + WEBVIEW_API void webview_bind(webview_t w, const char* name, + void (*fn)(const char* seq, const char* req, void* arg), + void* arg); + + // Removes a native C callback that was previously set by webview_bind. + WEBVIEW_API void webview_unbind(webview_t w, const char* name); + + // Allows to return a value from the native binding. Original request pointer + // must be provided to help internal RPC engine match requests with responses. + // If status is zero - result is expected to be a valid JSON result value. + // If status is not zero - result is an error JSON object. + WEBVIEW_API void webview_return(webview_t w, const char* seq, int status, const char* result); + + // Get the library's version information. + // @since 0.10 + WEBVIEW_API const webview_version_info_t* webview_version(); + +#ifdef __cplusplus +} + +#ifndef WEBVIEW_HEADER + +#if !defined(WEBVIEW_GTK) && !defined(WEBVIEW_COCOA) && !defined(WEBVIEW_EDGE) +#if defined(__APPLE__) +#define WEBVIEW_COCOA +#elif defined(__unix__) +#define WEBVIEW_GTK +#elif defined(_WIN32) +#define WEBVIEW_EDGE +#else +#error "please, specify webview backend" +#endif +#endif + +#ifndef WEBVIEW_DEPRECATED +#if __cplusplus >= 201402L +#define WEBVIEW_DEPRECATED(reason) [[deprecated(reason)]] +#elif defined(_MSC_VER) +#define WEBVIEW_DEPRECATED(reason) __declspec(deprecated(reason)) +#else +#define WEBVIEW_DEPRECATED(reason) __attribute__((deprecated(reason))) +#endif +#endif + +#ifndef WEBVIEW_DEPRECATED_PRIVATE +#define WEBVIEW_DEPRECATED_PRIVATE WEBVIEW_DEPRECATED("Private API should not be used") +#endif + +#include <array> +#include <atomic> +#include <functional> +#include <future> +#include <map> +#include <string> +#include <utility> +#include <vector> +#include <locale> +#include <codecvt> +#include <cstring> + +namespace webview +{ + + using dispatch_fn_t = std::function<void()>; + + namespace detail + { + + // The library's version information. + constexpr const webview_version_info_t library_version_info{ + { WEBVIEW_VERSION_MAJOR, WEBVIEW_VERSION_MINOR, WEBVIEW_VERSION_PATCH }, + WEBVIEW_VERSION_NUMBER, + WEBVIEW_VERSION_PRE_RELEASE, + WEBVIEW_VERSION_BUILD_METADATA + }; + + inline int json_parse_c(const char* s, size_t sz, const char* key, size_t keysz, + const char** value, size_t* valuesz) + { + enum + { + JSON_STATE_VALUE, + JSON_STATE_LITERAL, + JSON_STATE_STRING, + JSON_STATE_ESCAPE, + JSON_STATE_UTF8 + } state = JSON_STATE_VALUE; + const char* k = nullptr; + int index = 1; + int depth = 0; + int utf8_bytes = 0; + + *value = nullptr; + *valuesz = 0; + + if (key == nullptr) + { + index = static_cast<decltype(index)>(keysz); + if (index < 0) + { + return -1; + } + keysz = 0; + } + + for (; sz > 0; s++, sz--) + { + enum + { + JSON_ACTION_NONE, + JSON_ACTION_START, + JSON_ACTION_END, + JSON_ACTION_START_STRUCT, + JSON_ACTION_END_STRUCT + } action = JSON_ACTION_NONE; + auto c = static_cast<unsigned char>(*s); + switch (state) + { + case JSON_STATE_VALUE: + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || c == ':') + { + continue; + } + else if (c == '"') + { + action = JSON_ACTION_START; + state = JSON_STATE_STRING; + } + else if (c == '{' || c == '[') + { + action = JSON_ACTION_START_STRUCT; + } + else if (c == '}' || c == ']') + { + action = JSON_ACTION_END_STRUCT; + } + else if (c == 't' || c == 'f' || c == 'n' || c == '-' || + (c >= '0' && c <= '9')) + { + action = JSON_ACTION_START; + state = JSON_STATE_LITERAL; + } + else + { + return -1; + } + break; + case JSON_STATE_LITERAL: + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || + c == ']' || c == '}' || c == ':') + { + state = JSON_STATE_VALUE; + s--; + sz++; + action = JSON_ACTION_END; + } + else if (c < 32 || c > 126) + { + return -1; + } // fallthrough + case JSON_STATE_STRING: + if (c < 32 || (c > 126 && c < 192)) + { + return -1; + } + else if (c == '"') + { + action = JSON_ACTION_END; + state = JSON_STATE_VALUE; + } + else if (c == '\\') + { + state = JSON_STATE_ESCAPE; + } + else if (c >= 192 && c < 224) + { + utf8_bytes = 1; + state = JSON_STATE_UTF8; + } + else if (c >= 224 && c < 240) + { + utf8_bytes = 2; + state = JSON_STATE_UTF8; + } + else if (c >= 240 && c < 247) + { + utf8_bytes = 3; + state = JSON_STATE_UTF8; + } + else if (c >= 128 && c < 192) + { + return -1; + } + break; + case JSON_STATE_ESCAPE: + if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' || + c == 'r' || c == 't' || c == 'u') + { + state = JSON_STATE_STRING; + } + else + { + return -1; + } + break; + case JSON_STATE_UTF8: + if (c < 128 || c > 191) + { + return -1; + } + utf8_bytes--; + if (utf8_bytes == 0) + { + state = JSON_STATE_STRING; + } + break; + default: + return -1; + } + + if (action == JSON_ACTION_END_STRUCT) + { + depth--; + } + + if (depth == 1) + { + if (action == JSON_ACTION_START || action == JSON_ACTION_START_STRUCT) + { + if (index == 0) + { + *value = s; + } + else if (keysz > 0 && index == 1) + { + k = s; + } + else + { + index--; + } + } + else if (action == JSON_ACTION_END || action == JSON_ACTION_END_STRUCT) + { + if (*value != nullptr && index == 0) + { + *valuesz = (size_t)(s + 1 - *value); + return 0; + } + else if (keysz > 0 && k != nullptr) + { + if (keysz == (size_t)(s - k - 1) && memcmp(key, k + 1, keysz) == 0) + { + index = 0; + } + else + { + index = 2; + } + k = nullptr; + } + } + } + + if (action == JSON_ACTION_START_STRUCT) + { + depth++; + } + } + return -1; + } + + inline std::string json_escape(const std::string& s) + { + // TODO: implement + return '"' + s + '"'; + } + + inline int json_unescape(const char* s, size_t n, char* out) + { + int r = 0; + if (*s++ != '"') + { + return -1; + } + while (n > 2) + { + char c = *s; + if (c == '\\') + { + s++; + n--; + switch (*s) + { + case 'b': + c = '\b'; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case '\\': + c = '\\'; + break; + case '/': + c = '/'; + break; + case '\"': + c = '\"'; + break; + default: // TODO: support unicode decoding + return -1; + } + } + if (out != nullptr) + { + *out++ = c; + } + s++; + n--; + r++; + } + if (*s != '"') + { + return -1; + } + if (out != nullptr) + { + *out = '\0'; + } + return r; + } + + inline std::string json_parse(const std::string& s, const std::string& key, const int index) + { + const char* value = nullptr; + size_t value_sz = 0; + if (key.empty()) + { + json_parse_c(s.c_str(), s.length(), nullptr, index, &value, &value_sz); + } + else + { + json_parse_c(s.c_str(), s.length(), key.c_str(), key.length(), &value, &value_sz); + } + if (value != nullptr) + { + if (value[0] != '"') + { + return { value, value_sz }; + } + int n = json_unescape(value, value_sz, nullptr); + if (n > 0) + { + char* decoded = new char[n + 1]; + json_unescape(value, value_sz, decoded); + std::string result(decoded, n); + delete[] decoded; + return result; + } + } + return ""; + } + + } // namespace detail + + WEBVIEW_DEPRECATED_PRIVATE + inline int json_parse_c(const char* s, size_t sz, const char* key, size_t keysz, + const char** value, size_t* valuesz) + { + return detail::json_parse_c(s, sz, key, keysz, value, valuesz); + } + + WEBVIEW_DEPRECATED_PRIVATE + inline std::string json_escape(const std::string& s) + { + return detail::json_escape(s); + } + + WEBVIEW_DEPRECATED_PRIVATE + inline int json_unescape(const char* s, size_t n, char* out) + { + return detail::json_unescape(s, n, out); + } + + WEBVIEW_DEPRECATED_PRIVATE + inline std::string json_parse(const std::string& s, const std::string& key, const int index) + { + return detail::json_parse(s, key, index); + } + +} // namespace webview + +#if defined(WEBVIEW_GTK) +// +// ==================================================================== +// +// This implementation uses webkit2gtk backend. It requires gtk+3.0 and +// webkit2gtk-4.0 libraries. Proper compiler flags can be retrieved via: +// +// pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0 +// +// ==================================================================== +// +#include <JavaScriptCore/JavaScript.h> +#include <gtk/gtk.h> +#include <webkit2/webkit2.h> + +namespace webview +{ + namespace detail + { + + class gtk_webkit_engine + { + public: + gtk_webkit_engine(bool debug, void* window) : m_window(static_cast<GtkWidget*>(window)) + { + if (gtk_init_check(nullptr, nullptr) == FALSE) + { + return; + } + m_window = static_cast<GtkWidget*>(window); + if (m_window == nullptr) + { + m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + } + g_signal_connect(G_OBJECT(m_window), "destroy", + G_CALLBACK(+[](GtkWidget*, gpointer arg) { + static_cast<gtk_webkit_engine*>(arg)->terminate(); + }), + this); + // Initialize webview widget + m_webview = webkit_web_view_new(); + WebKitUserContentManager* manager = + webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); + g_signal_connect(manager, "script-message-received::external", + G_CALLBACK(+[](WebKitUserContentManager*, + WebKitJavascriptResult* r, gpointer arg) { + auto* w = static_cast<gtk_webkit_engine*>(arg); + char* s = get_string_from_js_result(r); + w->on_message(s); + g_free(s); + }), + this); + webkit_user_content_manager_register_script_message_handler(manager, "external"); + init("window.external={invoke:function(s){window.webkit.messageHandlers." + "external.postMessage(s);}}"); + + gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview)); + gtk_widget_grab_focus(GTK_WIDGET(m_webview)); + + WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview)); + webkit_settings_set_javascript_can_access_clipboard(settings, true); + if (debug) + { + webkit_settings_set_enable_write_console_messages_to_stdout(settings, true); + webkit_settings_set_enable_developer_extras(settings, true); + } + + gtk_widget_show_all(m_window); + } + virtual ~gtk_webkit_engine() = default; + void* window() + { + return (void*)m_window; + } + void run() + { + gtk_main(); + } + void terminate() + { + gtk_main_quit(); + } + void dispatch(std::function<void()> f) + { + g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void* f) -> int { + (*static_cast<dispatch_fn_t*>(f))(); + return G_SOURCE_REMOVE; + }), + new std::function<void()>(f), + [](void* f) { delete static_cast<dispatch_fn_t*>(f); }); + } + + void set_title(const std::string& title) + { + gtk_window_set_title(GTK_WINDOW(m_window), title.c_str()); + } + + void set_size(int width, int height, int hints) + { + gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED); + if (hints == WEBVIEW_HINT_NONE) + { + gtk_window_resize(GTK_WINDOW(m_window), width, height); + } + else if (hints == WEBVIEW_HINT_FIXED) + { + gtk_widget_set_size_request(m_window, width, height); + } + else + { + GdkGeometry g; + g.min_width = g.max_width = width; + g.min_height = g.max_height = height; + GdkWindowHints h = + (hints == WEBVIEW_HINT_MIN ? GDK_HINT_MIN_SIZE : GDK_HINT_MAX_SIZE); + // This defines either MIN_SIZE, or MAX_SIZE, but not both: + gtk_window_set_geometry_hints(GTK_WINDOW(m_window), nullptr, &g, h); + } + } + + void navigate(const std::string& url) + { + webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str()); + } + + void add_navigate_listener(std::function<void(const std::string&, void*)> callback, + void* arg) + { + g_signal_connect(WEBKIT_WEB_VIEW(m_webview), "load-changed", + G_CALLBACK(on_load_changed), this); + navigateCallbackArg = arg; + navigateCallback = std::move(callback); + } + + void add_scheme_handler(const std::string& scheme, + std::function<void(const std::string&, void*)> callback, + void* arg) + { + auto view = WEBKIT_WEB_VIEW(m_webview); + auto context = webkit_web_view_get_context(view); + + scheme_handlers.insert({ scheme, { .arg = arg, .fkt = callback } }); + webkit_web_context_register_uri_scheme(context, scheme.c_str(), scheme_handler, + static_cast<gpointer>(this), nullptr); + } + + void set_html(const std::string& html) + { + webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_webview), html.c_str(), nullptr); + } + + void init(const std::string& js) + { + WebKitUserContentManager* manager = + webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); + webkit_user_content_manager_add_script( + manager, webkit_user_script_new( + js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, + WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nullptr, nullptr)); + } + + void eval(const std::string& js) + { + webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), nullptr, + nullptr, nullptr); + } + + private: + virtual void on_message(const std::string& msg) = 0; + + struct handler_t + { + void* arg; + std::function<void(const std::string&, void*)> fkt; + }; + + std::map<std::string, handler_t> scheme_handlers; + + void scheme_handler_call(const std::string& scheme, const std::string& url) + { + auto handler = scheme_handlers.find(scheme); + if (handler != scheme_handlers.end()) + { + const auto& arg = handler->second; + arg.fkt(url, arg.arg); + } + } + + static void scheme_handler(WebKitURISchemeRequest* request, gpointer user_data) + { + auto _this = static_cast<gtk_webkit_engine*>(user_data); + + auto scheme = webkit_uri_scheme_request_get_scheme(request); + auto uri = webkit_uri_scheme_request_get_uri(request); + _this->scheme_handler_call(scheme, uri); + } + + static char* get_string_from_js_result(WebKitJavascriptResult* r) + { + char* s = nullptr; +#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22 + JSCValue* value = webkit_javascript_result_get_js_value(r); + s = jsc_value_to_string(value); +#else + JSGlobalContextRef ctx = webkit_javascript_result_get_global_context(r); + JSValueRef value = webkit_javascript_result_get_value(r); + JSStringRef js = JSValueToStringCopy(ctx, value, nullptr); + size_t n = JSStringGetMaximumUTF8CStringSize(js); + s = g_new(char, n); + JSStringGetUTF8CString(js, s, n); + JSStringRelease(js); +#endif + return s; + } + + GtkWidget* m_window; + GtkWidget* m_webview; + + void* navigateCallbackArg = nullptr; + std::function<void(const std::string&, void*)> navigateCallback = nullptr; + + static void on_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event, + gpointer arg) + { + if (load_event == WEBKIT_LOAD_FINISHED) + { + auto inst = static_cast<gtk_webkit_engine*>(arg); + inst->navigateCallback(webkit_web_view_get_uri(web_view), + inst->navigateCallbackArg); + } + } + }; + + } // namespace detail + + using browser_engine = detail::gtk_webkit_engine; + +} // namespace webview + +#elif defined(WEBVIEW_COCOA) + +// +// ==================================================================== +// +// This implementation uses Cocoa WKWebView backend on macOS. It is +// written using ObjC runtime and uses WKWebView class as a browser runtime. +// You should pass "-framework Webkit" flag to the compiler. +// +// ==================================================================== +// + +#include <CoreGraphics/CoreGraphics.h> +#include <objc/NSObjCRuntime.h> +#include <objc/objc-runtime.h> + +namespace webview +{ + namespace detail + { + namespace objc + { + + // A convenient template function for unconditionally casting the specified + // C-like function into a function that can be called with the given return + // type and arguments. Caller takes full responsibility for ensuring that + // the function call is valid. It is assumed that the function will not + // throw exceptions. + template <typename Result, typename Callable, typename... Args> + Result invoke(Callable callable, Args... args) noexcept + { + return reinterpret_cast<Result (*)(Args...)>(callable)(args...); + } + + // Calls objc_msgSend. + template <typename Result, typename... Args> Result msg_send(Args... args) noexcept + { + return invoke<Result>(objc_msgSend, args...); + } + + } // namespace objc + + enum NSBackingStoreType : NSUInteger + { + NSBackingStoreBuffered = 2 + }; + + enum NSWindowStyleMask : NSUInteger + { + NSWindowStyleMaskTitled = 1, + NSWindowStyleMaskClosable = 2, + NSWindowStyleMaskMiniaturizable = 4, + NSWindowStyleMaskResizable = 8 + }; + + enum NSApplicationActivationPolicy : NSInteger + { + NSApplicationActivationPolicyRegular = 0 + }; + + enum WKUserScriptInjectionTime : NSInteger + { + WKUserScriptInjectionTimeAtDocumentStart = 0 + }; + + enum NSModalResponse : NSInteger + { + NSModalResponseOK = 1 + }; + + // Convenient conversion of string literals. + inline id operator"" _cls(const char* s, std::size_t) + { + return (id)objc_getClass(s); + } + inline SEL operator"" _sel(const char* s, std::size_t) + { + return sel_registerName(s); + } + inline id operator"" _str(const char* s, std::size_t) + { + return objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, s); + } + + class cocoa_wkwebview_engine + { + public: + cocoa_wkwebview_engine(bool debug, void* window) + : m_debug{ debug }, m_parent_window{ window } + { + auto app = get_shared_application(); + auto delegate = create_app_delegate(); + objc_setAssociatedObject(delegate, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN); + objc::msg_send<void>(app, "setDelegate:"_sel, delegate); + + // See comments related to application lifecycle in create_app_delegate(). + if (window) + { + on_application_did_finish_launching(delegate, app); + } + else + { + // Start the main run loop so that the app delegate gets the + // NSApplicationDidFinishLaunchingNotification notification after the run + // loop has started in order to perform further initialization. + // We need to return from this constructor so this run loop is only + // temporary. + objc::msg_send<void>(app, "run"_sel); + } + } + virtual ~cocoa_wkwebview_engine() = default; + void* window() + { + return (void*)m_window; + } + void terminate() + { + auto app = get_shared_application(); + objc::msg_send<void>(app, "terminate:"_sel, nullptr); + } + void run() + { + auto app = get_shared_application(); + objc::msg_send<void>(app, "run"_sel); + } + void dispatch(std::function<void()> f) + { + dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f), + (dispatch_function_t)([](void* arg) { + auto f = static_cast<dispatch_fn_t*>(arg); + (*f)(); + delete f; + })); + } + void set_title(const std::string& title) + { + objc::msg_send<void>( + m_window, "setTitle:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, title.c_str())); + } + void set_size(int width, int height, int hints) + { + auto style = static_cast<NSWindowStyleMask>(NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable); + if (hints != WEBVIEW_HINT_FIXED) + { + style = static_cast<NSWindowStyleMask>(style | NSWindowStyleMaskResizable); + } + objc::msg_send<void>(m_window, "setStyleMask:"_sel, style); + + if (hints == WEBVIEW_HINT_MIN) + { + objc::msg_send<void>(m_window, "setContentMinSize:"_sel, + CGSizeMake(width, height)); + } + else if (hints == WEBVIEW_HINT_MAX) + { + objc::msg_send<void>(m_window, "setContentMaxSize:"_sel, + CGSizeMake(width, height)); + } + else + { + objc::msg_send<void>(m_window, "setFrame:display:animate:"_sel, + CGRectMake(0, 0, width, height), YES, NO); + } + objc::msg_send<void>(m_window, "center"_sel); + } + void navigate(const std::string& url) + { + auto nsurl = objc::msg_send<id>( + "NSURL"_cls, "URLWithString:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, url.c_str())); + + objc::msg_send<void>( + m_webview, "loadRequest:"_sel, + objc::msg_send<id>("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl)); + } + + void add_navigate_listener(std::function<void(const std::string&, void*)> callback, + void* arg) + { + m_navigateCallback = callback; + m_navigateCallbackArg = arg; + } + + void set_html(const std::string& html) + { + objc::msg_send<void>( + m_webview, "loadHTMLString:baseURL:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, html.c_str()), + nullptr); + } + void init(const std::string& js) + { + // Equivalent Obj-C: + // [m_manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString + // stringWithUTF8String:js.c_str()] + // injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]] + objc::msg_send<void>( + m_manager, "addUserScript:"_sel, + objc::msg_send<id>( + objc::msg_send<id>("WKUserScript"_cls, "alloc"_sel), + "initWithSource:injectionTime:forMainFrameOnly:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()), + WKUserScriptInjectionTimeAtDocumentStart, YES)); + } + void eval(const std::string& js) + { + objc::msg_send<void>( + m_webview, "evaluateJavaScript:completionHandler:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()), + nullptr); + } + + private: + virtual void on_message(const std::string& msg) = 0; + id create_app_delegate() + { + // Note: Avoid registering the class name "AppDelegate" as it is the + // default name in projects created with Xcode, and using the same name + // causes objc_registerClassPair to crash. + auto cls = + objc_allocateClassPair((Class) "NSResponder"_cls, "WebviewAppDelegate", 0); + class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider")); + class_addMethod(cls, "applicationShouldTerminateAfterLastWindowClosed:"_sel, + (IMP)(+[](id, SEL, id) -> BOOL { return 1; }), "c@:@"); + // If the library was not initialized with an existing window then the user + // is likely managing the application lifecycle and we would not get the + // "applicationDidFinishLaunching:" message and therefore do not need to + // add this method. + if (!m_parent_window) + { + class_addMethod(cls, "applicationDidFinishLaunching:"_sel, + (IMP)(+[](id self, SEL, id notification) { + auto app = objc::msg_send<id>(notification, "object"_sel); + auto w = get_associated_webview(self); + w->on_application_did_finish_launching(self, app); + }), + "v@:@"); + } + objc_registerClassPair(cls); + return objc::msg_send<id>((id)cls, "new"_sel); + } + id create_script_message_handler() + { + auto cls = objc_allocateClassPair((Class) "NSResponder"_cls, + "WebkitScriptMessageHandler", 0); + class_addProtocol(cls, objc_getProtocol("WKScriptMessageHandler")); + class_addMethod(cls, "userContentController:didReceiveScriptMessage:"_sel, + (IMP)(+[](id self, SEL, id, id msg) { + auto w = get_associated_webview(self); + w->on_message(objc::msg_send<const char*>( + objc::msg_send<id>(msg, "body"_sel), "UTF8String"_sel)); + }), + "v@:@@"); + objc_registerClassPair(cls); + auto instance = objc::msg_send<id>((id)cls, "new"_sel); + objc_setAssociatedObject(instance, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN); + return instance; + } + static id create_webkit_ui_delegate() + { + auto cls = objc_allocateClassPair((Class) "NSObject"_cls, "WebkitUIDelegate", 0); + class_addProtocol(cls, objc_getProtocol("WKUIDelegate")); + class_addMethod( + cls, + "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:"_sel, + (IMP)(+[](id, SEL, id, id parameters, id, id completion_handler) { + auto allows_multiple_selection = + objc::msg_send<BOOL>(parameters, "allowsMultipleSelection"_sel); + auto allows_directories = + objc::msg_send<BOOL>(parameters, "allowsDirectories"_sel); + + // Show a panel for selecting files. + auto panel = objc::msg_send<id>("NSOpenPanel"_cls, "openPanel"_sel); + objc::msg_send<void>(panel, "setCanChooseFiles:"_sel, YES); + objc::msg_send<void>(panel, "setCanChooseDirectories:"_sel, + allows_directories); + objc::msg_send<void>(panel, "setAllowsMultipleSelection:"_sel, + allows_multiple_selection); + auto modal_response = + objc::msg_send<NSModalResponse>(panel, "runModal"_sel); + + // Get the URLs for the selected files. If the modal was canceled + // then we pass null to the completion handler to signify + // cancellation. + id urls = modal_response == NSModalResponseOK + ? objc::msg_send<id>(panel, "URLs"_sel) + : nullptr; + + // Invoke the completion handler block. + auto sig = objc::msg_send<id>("NSMethodSignature"_cls, + "signatureWithObjCTypes:"_sel, "v@?@"); + auto invocation = objc::msg_send<id>( + "NSInvocation"_cls, "invocationWithMethodSignature:"_sel, sig); + objc::msg_send<void>(invocation, "setTarget:"_sel, completion_handler); + objc::msg_send<void>(invocation, "setArgument:atIndex:"_sel, &urls, 1); + objc::msg_send<void>(invocation, "invoke"_sel); + }), + "v@:@@@@"); + objc_registerClassPair(cls); + return objc::msg_send<id>((id)cls, "new"_sel); + } + id create_webkit_navigation_delegate() + { + auto cls = + objc_allocateClassPair((Class) "NSObject"_cls, "WebkitNavigationDelegate", 0); + class_addProtocol(cls, objc_getProtocol("WKNavigationDelegate")); + class_addMethod(cls, "webView:didFinishNavigation:"_sel, + (IMP)(+[](id delegate, SEL sel, id webview, id navigation) { + auto w = get_associated_webview(delegate); + auto url = objc::msg_send<id>(webview, "URL"_sel); + auto nstr = objc::msg_send<id>(url, "absoluteString"_sel); + auto str = objc::msg_send<char*>(nstr, "UTF8String"_sel); + w->m_navigateCallback(str, w->m_navigateCallbackArg); + }), + "v@:@"); + objc_registerClassPair(cls); + auto instance = objc::msg_send<id>((id)cls, "new"_sel); + objc_setAssociatedObject(instance, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN); + return instance; + } + static id get_shared_application() + { + return objc::msg_send<id>("NSApplication"_cls, "sharedApplication"_sel); + } + static cocoa_wkwebview_engine* get_associated_webview(id object) + { + auto w = (cocoa_wkwebview_engine*)objc_getAssociatedObject(object, "webview"); + assert(w); + return w; + } + static id get_main_bundle() noexcept + { + return objc::msg_send<id>("NSBundle"_cls, "mainBundle"_sel); + } + static bool is_app_bundled() noexcept + { + auto bundle = get_main_bundle(); + if (!bundle) + { + return false; + } + auto bundle_path = objc::msg_send<id>(bundle, "bundlePath"_sel); + auto bundled = objc::msg_send<BOOL>(bundle_path, "hasSuffix:"_sel, ".app"_str); + return !!bundled; + } + void on_application_did_finish_launching(id /*delegate*/, id app) + { + // See comments related to application lifecycle in create_app_delegate(). + if (!m_parent_window) + { + // Stop the main run loop so that we can return + // from the constructor. + objc::msg_send<void>(app, "stop:"_sel, nullptr); + } + + // Activate the app if it is not bundled. + // Bundled apps launched from Finder are activated automatically but + // otherwise not. Activating the app even when it has been launched from + // Finder does not seem to be harmful but calling this function is rarely + // needed as proper activation is normally taken care of for us. + // Bundled apps have a default activation policy of + // NSApplicationActivationPolicyRegular while non-bundled apps have a + // default activation policy of NSApplicationActivationPolicyProhibited. + if (!is_app_bundled()) + { + // "setActivationPolicy:" must be invoked before + // "activateIgnoringOtherApps:" for activation to work. + objc::msg_send<void>(app, "setActivationPolicy:"_sel, + NSApplicationActivationPolicyRegular); + // Activate the app regardless of other active apps. + // This can be obtrusive so we only do it when necessary. + objc::msg_send<void>(app, "activateIgnoringOtherApps:"_sel, YES); + } + + // Main window + if (!m_parent_window) + { + m_window = objc::msg_send<id>("NSWindow"_cls, "alloc"_sel); + auto style = NSWindowStyleMaskTitled; + m_window = objc::msg_send<id>( + m_window, "initWithContentRect:styleMask:backing:defer:"_sel, + CGRectMake(0, 0, 0, 0), style, NSBackingStoreBuffered, NO); + } + else + { + m_window = (id)m_parent_window; + } + + // Webview + auto config = objc::msg_send<id>("WKWebViewConfiguration"_cls, "new"_sel); + m_manager = objc::msg_send<id>(config, "userContentController"_sel); + m_webview = objc::msg_send<id>("WKWebView"_cls, "alloc"_sel); + + if (m_debug) + { + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"developerExtrasEnabled"]; + objc::msg_send<id>( + objc::msg_send<id>(config, "preferences"_sel), "setValue:forKey:"_sel, + objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES), + "developerExtrasEnabled"_str); + } + + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"fullScreenEnabled"]; + objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel), + "setValue:forKey:"_sel, + objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES), + "fullScreenEnabled"_str); + + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"javaScriptCanAccessClipboard"]; + objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel), + "setValue:forKey:"_sel, + objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES), + "javaScriptCanAccessClipboard"_str); + + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"DOMPasteAllowed"]; + objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel), + "setValue:forKey:"_sel, + objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES), + "DOMPasteAllowed"_str); + + auto ui_delegate = create_webkit_ui_delegate(); + objc::msg_send<void>(m_webview, "initWithFrame:configuration:"_sel, + CGRectMake(0, 0, 0, 0), config); + objc::msg_send<void>(m_webview, "setUIDelegate:"_sel, ui_delegate); + + auto navigation_delegate = create_webkit_navigation_delegate(); + objc::msg_send<void>(m_webview, "setNavigationDelegate:"_sel, navigation_delegate); + auto script_message_handler = create_script_message_handler(); + objc::msg_send<void>(m_manager, "addScriptMessageHandler:name:"_sel, + script_message_handler, "external"_str); + + init(R""( + window.external = { + invoke: function(s) { + window.webkit.messageHandlers.external.postMessage(s); + }, + }; + )""); + objc::msg_send<void>(m_window, "setContentView:"_sel, m_webview); + objc::msg_send<void>(m_window, "makeKeyAndOrderFront:"_sel, nullptr); + } + bool m_debug; + void* m_parent_window; + id m_window; + id m_webview; + id m_manager; + void* m_navigateCallbackArg = nullptr; + std::function<void(const std::string&, void*)> m_navigateCallback = 0; + }; + + } // namespace detail + + using browser_engine = detail::cocoa_wkwebview_engine; + +} // namespace webview + +#elif defined(WEBVIEW_EDGE) + +// +// ==================================================================== +// +// This implementation uses Win32 API to create a native window. It +// uses Edge/Chromium webview2 backend as a browser engine. +// +// ==================================================================== +// + +#define WIN32_LEAN_AND_MEAN +#include <shlobj.h> +#include <shlwapi.h> +#include <stdlib.h> +#include <windows.h> + +#include "WebView2.h" + +#ifdef _MSC_VER +#pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "version.lib") +#endif + +namespace webview +{ + namespace detail + { + + using msg_cb_t = std::function<void(const std::string)>; + + // Converts a narrow (UTF-8-encoded) string into a wide (UTF-16-encoded) string. + inline std::wstring widen_string(const std::string& input) + { + if (input.empty()) + { + return std::wstring(); + } + UINT cp = CP_UTF8; + DWORD flags = MB_ERR_INVALID_CHARS; + auto input_c = input.c_str(); + auto input_length = static_cast<int>(input.size()); + auto required_length = + MultiByteToWideChar(cp, flags, input_c, input_length, nullptr, 0); + if (required_length > 0) + { + std::wstring output(static_cast<std::size_t>(required_length), L'\0'); + if (MultiByteToWideChar(cp, flags, input_c, input_length, &output[0], + required_length) > 0) + { + return output; + } + } + // Failed to convert string from UTF-8 to UTF-16 + return std::wstring(); + } + + // Converts a wide (UTF-16-encoded) string into a narrow (UTF-8-encoded) string. + inline std::string narrow_string(const std::wstring& input) + { + if (input.empty()) + { + return std::string(); + } + UINT cp = CP_UTF8; + DWORD flags = WC_ERR_INVALID_CHARS; + auto input_c = input.c_str(); + auto input_length = static_cast<int>(input.size()); + auto required_length = + WideCharToMultiByte(cp, flags, input_c, input_length, nullptr, 0, nullptr, nullptr); + if (required_length > 0) + { + std::string output(static_cast<std::size_t>(required_length), '\0'); + if (WideCharToMultiByte(cp, flags, input_c, input_length, &output[0], + required_length, nullptr, nullptr) > 0) + { + return output; + } + } + // Failed to convert string from UTF-16 to UTF-8 + return std::string(); + } + + // Parses a version string with 1-4 integral components, e.g. "1.2.3.4". + // Missing or invalid components default to 0, and excess components are ignored. + template <typename T> + std::array<unsigned int, 4> parse_version(const std::basic_string<T>& version) noexcept + { + auto parse_component = [](auto sb, auto se) -> unsigned int { + try + { + auto n = std::stol(std::basic_string<T>(sb, se)); + return n < 0 ? 0 : n; + } + catch (std::exception&) + { + return 0; + } + }; + auto end = version.end(); + auto sb = version.begin(); // subrange begin + auto se = sb; // subrange end + unsigned int ci = 0; // component index + std::array<unsigned int, 4> components{}; + while (sb != end && se != end && ci < components.size()) + { + if (*se == static_cast<T>('.')) + { + components[ci++] = parse_component(sb, se); + sb = ++se; + continue; + } + ++se; + } + if (sb < se && ci < components.size()) + { + components[ci] = parse_component(sb, se); + } + return components; + } + + template <typename T, std::size_t Length> + auto parse_version(const T (&version)[Length]) noexcept + { + return parse_version(std::basic_string<T>(version, Length)); + } + + std::wstring get_file_version_string(const std::wstring& file_path) noexcept + { + DWORD dummy_handle; // Unused + DWORD info_buffer_length = GetFileVersionInfoSizeW(file_path.c_str(), &dummy_handle); + if (info_buffer_length == 0) + { + return std::wstring(); + } + std::vector<char> info_buffer; + info_buffer.reserve(info_buffer_length); + if (!GetFileVersionInfoW(file_path.c_str(), 0, info_buffer_length, info_buffer.data())) + { + return std::wstring(); + } + auto sub_block = L"\\StringFileInfo\\040904B0\\ProductVersion"; + LPWSTR version = nullptr; + unsigned int version_length = 0; + if (!VerQueryValueW(info_buffer.data(), sub_block, reinterpret_cast<LPVOID*>(&version), + &version_length)) + { + return std::wstring(); + } + if (!version || version_length == 0) + { + return std::wstring(); + } + return std::wstring(version, version_length); + } + + // A wrapper around COM library initialization. Calls CoInitializeEx in the + // constructor and CoUninitialize in the destructor. + class com_init_wrapper + { + public: + com_init_wrapper(DWORD dwCoInit) + { + // We can safely continue as long as COM was either successfully + // initialized or already initialized. + // RPC_E_CHANGED_MODE means that CoInitializeEx was already called with + // a different concurrency model. + switch (CoInitializeEx(nullptr, dwCoInit)) + { + case S_OK: + case S_FALSE: + m_initialized = true; + break; + } + } + + ~com_init_wrapper() + { + if (m_initialized) + { + CoUninitialize(); + m_initialized = false; + } + } + + com_init_wrapper(const com_init_wrapper& other) = delete; + com_init_wrapper& operator=(const com_init_wrapper& other) = delete; + com_init_wrapper(com_init_wrapper&& other) = delete; + com_init_wrapper& operator=(com_init_wrapper&& other) = delete; + + bool is_initialized() const + { + return m_initialized; + } + + private: + bool m_initialized = false; + }; + + // Holds a symbol name and associated type for code clarity. + template <typename T> class library_symbol + { + public: + using type = T; + + constexpr explicit library_symbol(const char* name) : m_name(name) + { + } + constexpr const char* get_name() const + { + return m_name; + } + + private: + const char* m_name; + }; + + // Loads a native shared library and allows one to get addresses for those + // symbols. + class native_library + { + public: + explicit native_library(const wchar_t* name) : m_handle(LoadLibraryW(name)) + { + } + + ~native_library() + { + if (m_handle) + { + FreeLibrary(m_handle); + m_handle = nullptr; + } + } + + native_library(const native_library& other) = delete; + native_library& operator=(const native_library& other) = delete; + native_library(native_library&& other) = default; + native_library& operator=(native_library&& other) = default; + + // Returns true if the library is currently loaded; otherwise false. + operator bool() const + { + return is_loaded(); + } + + // Get the address for the specified symbol or nullptr if not found. + template <typename Symbol> typename Symbol::type get(const Symbol& symbol) const + { + if (is_loaded()) + { + return reinterpret_cast<typename Symbol::type>( + GetProcAddress(m_handle, symbol.get_name())); + } + return nullptr; + } + + // Returns true if the library is currently loaded; otherwise false. + bool is_loaded() const + { + return !!m_handle; + } + + void detach() + { + m_handle = nullptr; + } + + private: + HMODULE m_handle = nullptr; + }; + + struct user32_symbols + { + using DPI_AWARENESS_CONTEXT = HANDLE; + using SetProcessDpiAwarenessContext_t = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT); + using SetProcessDPIAware_t = BOOL(WINAPI*)(); + + static constexpr auto SetProcessDpiAwarenessContext = + library_symbol<SetProcessDpiAwarenessContext_t>("SetProcessDpiAwarenessContext"); + static constexpr auto SetProcessDPIAware = + library_symbol<SetProcessDPIAware_t>("SetProcessDPIAware"); + }; + + struct shcore_symbols + { + typedef enum + { + PROCESS_PER_MONITOR_DPI_AWARE = 2 + } PROCESS_DPI_AWARENESS; + using SetProcessDpiAwareness_t = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); + + static constexpr auto SetProcessDpiAwareness = + library_symbol<SetProcessDpiAwareness_t>("SetProcessDpiAwareness"); + }; + + class reg_key + { + public: + explicit reg_key(HKEY root_key, const wchar_t* sub_key, DWORD options, + REGSAM sam_desired) + { + HKEY handle; + auto status = RegOpenKeyExW(root_key, sub_key, options, sam_desired, &handle); + if (status == ERROR_SUCCESS) + { + m_handle = handle; + } + } + + explicit reg_key(HKEY root_key, const std::wstring& sub_key, DWORD options, + REGSAM sam_desired) + : reg_key(root_key, sub_key.c_str(), options, sam_desired) + { + } + + virtual ~reg_key() + { + if (m_handle) + { + RegCloseKey(m_handle); + m_handle = nullptr; + } + } + + reg_key(const reg_key& other) = delete; + reg_key& operator=(const reg_key& other) = delete; + reg_key(reg_key&& other) = delete; + reg_key& operator=(reg_key&& other) = delete; + + bool is_open() const + { + return !!m_handle; + } + bool get_handle() const + { + return m_handle; + } + + std::wstring query_string(const wchar_t* name) const + { + DWORD buf_length = 0; + // Get the size of the data in bytes. + auto status = + RegQueryValueExW(m_handle, name, nullptr, nullptr, nullptr, &buf_length); + if (status != ERROR_SUCCESS && status != ERROR_MORE_DATA) + { + return std::wstring(); + } + // Read the data. + std::wstring result(buf_length / sizeof(wchar_t), 0); + auto buf = reinterpret_cast<LPBYTE>(&result[0]); + status = RegQueryValueExW(m_handle, name, nullptr, nullptr, buf, &buf_length); + if (status != ERROR_SUCCESS) + { + return std::wstring(); + } + // Remove trailing null-characters. + for (std::size_t length = result.size(); length > 0; --length) + { + if (result[length - 1] != 0) + { + result.resize(length); + break; + } + } + return result; + } + + private: + HKEY m_handle = nullptr; + }; + + inline bool enable_dpi_awareness() + { + auto user32 = native_library(L"user32.dll"); + if (auto fn = user32.get(user32_symbols::SetProcessDpiAwarenessContext)) + { + if (fn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) + { + return true; + } + return GetLastError() == ERROR_ACCESS_DENIED; + } + if (auto shcore = native_library(L"shcore.dll")) + { + if (auto fn = shcore.get(shcore_symbols::SetProcessDpiAwareness)) + { + auto result = fn(shcore_symbols::PROCESS_PER_MONITOR_DPI_AWARE); + return result == S_OK || result == E_ACCESSDENIED; + } + } + if (auto fn = user32.get(user32_symbols::SetProcessDPIAware)) + { + return !!fn(); + } + return true; + } + +// Enable built-in WebView2Loader implementation by default. +#ifndef WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL +#define WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL 1 +#endif + +// Link WebView2Loader.dll explicitly by default only if the built-in +// implementation is enabled. +#ifndef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK +#define WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL +#endif + +// Explicit linking of WebView2Loader.dll should be used along with +// the built-in implementation. +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 && WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK != 1 +#undef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK +#error Please set WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK=1. +#endif + +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + // Gets the last component of a Windows native file path. + // For example, if the path is "C:\a\b" then the result is "b". + template <typename T> + std::basic_string<T> get_last_native_path_component(const std::basic_string<T>& path) + { + if (auto pos = path.find_last_of(static_cast<T>('\\')); + pos != std::basic_string<T>::npos) + { + return path.substr(pos + 1); + } + return std::basic_string<T>(); + } +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + + template <typename T> struct cast_info_t + { + using type = T; + IID iid; + }; + + namespace mswebview2 + { + static constexpr IID IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler{ + 0x6C4819F3, 0xC9B7, 0x4260, 0x81, 0x27, 0xC9, 0xF5, 0xBD, 0xE7, 0xF6, 0x8C + }; + static constexpr IID IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler{ + 0x4E8A3389, 0xC9D8, 0x4BD2, 0xB6, 0xB5, 0x12, 0x4F, 0xEE, 0x6C, 0xC1, 0x4D + }; + static constexpr IID IID_ICoreWebView2PermissionRequestedEventHandler{ + 0x15E1C6A3, 0xC72A, 0x4DF3, 0x91, 0xD7, 0xD0, 0x97, 0xFB, 0xEC, 0x6B, 0xFD + }; + static constexpr IID IID_ICoreWebView2WebMessageReceivedEventHandler{ + 0x57213F19, 0x00E6, 0x49FA, 0x8E, 0x07, 0x89, 0x8E, 0xA0, 0x1E, 0xCB, 0xD2 + }; + +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + enum class webview2_runtime_type + { + installed = 0, + embedded = 1 + }; + + namespace webview2_symbols + { + using CreateWebViewEnvironmentWithOptionsInternal_t = HRESULT(STDMETHODCALLTYPE*)( + bool, webview2_runtime_type, PCWSTR, IUnknown*, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*); + using DllCanUnloadNow_t = HRESULT(STDMETHODCALLTYPE*)(); + + static constexpr auto CreateWebViewEnvironmentWithOptionsInternal = + library_symbol<CreateWebViewEnvironmentWithOptionsInternal_t>( + "CreateWebViewEnvironmentWithOptionsInternal"); + static constexpr auto DllCanUnloadNow = + library_symbol<DllCanUnloadNow_t>("DllCanUnloadNow"); + } // namespace webview2_symbols +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + namespace webview2_symbols + { + using CreateCoreWebView2EnvironmentWithOptions_t = HRESULT(STDMETHODCALLTYPE*)( + PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions*, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*); + using GetAvailableCoreWebView2BrowserVersionString_t = + HRESULT(STDMETHODCALLTYPE*)(PCWSTR, LPWSTR*); + + static constexpr auto CreateCoreWebView2EnvironmentWithOptions = + library_symbol<CreateCoreWebView2EnvironmentWithOptions_t>( + "CreateCoreWebView2EnvironmentWithOptions"); + static constexpr auto GetAvailableCoreWebView2BrowserVersionString = + library_symbol<GetAvailableCoreWebView2BrowserVersionString_t>( + "GetAvailableCoreWebView2BrowserVersionString"); + } // namespace webview2_symbols +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + + class loader + { + public: + HRESULT create_environment_with_options( + PCWSTR browser_dir, PCWSTR user_data_dir, + ICoreWebView2EnvironmentOptions* env_options, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* created_handler) + const + { +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + if (m_lib.is_loaded()) + { + if (auto fn = m_lib.get( + webview2_symbols::CreateCoreWebView2EnvironmentWithOptions)) + { + return fn(browser_dir, user_data_dir, env_options, created_handler); + } + } +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + return create_environment_with_options_impl(browser_dir, user_data_dir, + env_options, created_handler); +#else + return S_FALSE; +#endif +#else + return ::CreateCoreWebView2EnvironmentWithOptions(browser_dir, user_data_dir, + env_options, created_handler); +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + } + + HRESULT + get_available_browser_version_string(PCWSTR browser_dir, LPWSTR* version) const + { +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + if (m_lib.is_loaded()) + { + if (auto fn = m_lib.get( + webview2_symbols::GetAvailableCoreWebView2BrowserVersionString)) + { + return fn(browser_dir, version); + } + } +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + return get_available_browser_version_string_impl(browser_dir, version); +#else + return S_FALSE; +#endif +#else + return ::GetAvailableCoreWebView2BrowserVersionString(browser_dir, version); +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + } + + private: +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + struct client_info_t + { + bool found = false; + std::wstring dll_path; + std::wstring version; + webview2_runtime_type runtime_type; + }; + + HRESULT create_environment_with_options_impl( + PCWSTR browser_dir, PCWSTR user_data_dir, + ICoreWebView2EnvironmentOptions* env_options, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* created_handler) + const + { + auto found_client = find_available_client(browser_dir); + if (!found_client.found) + { + return -1; + } + auto client_dll = native_library(found_client.dll_path.c_str()); + if (auto fn = client_dll.get( + webview2_symbols::CreateWebViewEnvironmentWithOptionsInternal)) + { + return fn(true, found_client.runtime_type, user_data_dir, env_options, + created_handler); + } + if (auto fn = client_dll.get(webview2_symbols::DllCanUnloadNow)) + { + if (!fn()) + { + client_dll.detach(); + } + } + return ERROR_SUCCESS; + } + + HRESULT + get_available_browser_version_string_impl(PCWSTR browser_dir, LPWSTR* version) const + { + if (!version) + { + return -1; + } + auto found_client = find_available_client(browser_dir); + if (!found_client.found) + { + return -1; + } + auto info_length_bytes = + found_client.version.size() * sizeof(found_client.version[0]); + auto info = static_cast<LPWSTR>(CoTaskMemAlloc(info_length_bytes)); + if (!info) + { + return -1; + } + CopyMemory(info, found_client.version.c_str(), info_length_bytes); + *version = info; + return 0; + } + + client_info_t find_available_client(PCWSTR browser_dir) const + { + if (browser_dir) + { + return find_embedded_client(api_version, browser_dir); + } + auto found_client = + find_installed_client(api_version, true, default_release_channel_guid); + if (!found_client.found) + { + found_client = + find_installed_client(api_version, false, default_release_channel_guid); + } + return found_client; + } + + std::wstring make_client_dll_path(const std::wstring& dir) const + { + auto dll_path = dir; + if (!dll_path.empty()) + { + auto last_char = dir[dir.size() - 1]; + if (last_char != L'\\' && last_char != L'/') + { + dll_path += L'\\'; + } + } + dll_path += L"EBWebView\\"; +#if defined(_M_X64) || defined(__x86_64__) + dll_path += L"x64"; +#elif defined(_M_IX86) || defined(__i386__) + dll_path += L"x86"; +#elif defined(_M_ARM64) || defined(__aarch64__) + dll_path += L"arm64"; +#else +#error WebView2 integration for this platform is not yet supported. +#endif + dll_path += L"\\EmbeddedBrowserWebView.dll"; + return dll_path; + } + + client_info_t find_installed_client(unsigned int min_api_version, bool system, + const std::wstring& release_channel) const + { + std::wstring sub_key = client_state_reg_sub_key; + sub_key += release_channel; + auto root_key = system ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; + reg_key key(root_key, sub_key, 0, KEY_READ | KEY_WOW64_32KEY); + if (!key.is_open()) + { + return {}; + } + auto ebwebview_value = key.query_string(L"EBWebView"); + + auto client_version_string = get_last_native_path_component(ebwebview_value); + auto client_version = parse_version(client_version_string); + if (client_version[2] < min_api_version) + { + // Our API version is greater than the runtime API version. + return {}; + } + + auto client_dll_path = make_client_dll_path(ebwebview_value); + return { true, client_dll_path, client_version_string, + webview2_runtime_type::installed }; + } + + client_info_t find_embedded_client(unsigned int min_api_version, + const std::wstring& dir) const + { + auto client_dll_path = make_client_dll_path(dir); + + auto client_version_string = get_file_version_string(client_dll_path); + auto client_version = parse_version(client_version_string); + if (client_version[2] < min_api_version) + { + // Our API version is greater than the runtime API version. + return {}; + } + + return { true, client_dll_path, client_version_string, + webview2_runtime_type::embedded }; + } + + // The minimum WebView2 API version we need regardless of the SDK release + // actually used. The number comes from the SDK release version, + // e.g. 1.0.1150.38. To be safe the SDK should have a number that is greater + // than or equal to this number. The Edge browser webview client must + // have a number greater than or equal to this number. + static constexpr unsigned int api_version = 1150; + + static constexpr auto client_state_reg_sub_key = + L"SOFTWARE\\Microsoft\\EdgeUpdate\\ClientState\\"; + + // GUID for the stable release channel. + static constexpr auto stable_release_guid = + L"{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"; + + static constexpr auto default_release_channel_guid = stable_release_guid; +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + native_library m_lib{ L"WebView2Loader.dll" }; +#endif + }; + + namespace cast_info + { + static constexpr auto controller_completed = + cast_info_t<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>{ + IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler + }; + + static constexpr auto environment_completed = + cast_info_t<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>{ + IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler + }; + + static constexpr auto message_received = + cast_info_t<ICoreWebView2WebMessageReceivedEventHandler>{ + IID_ICoreWebView2WebMessageReceivedEventHandler + }; + + static constexpr auto permission_requested = + cast_info_t<ICoreWebView2PermissionRequestedEventHandler>{ + IID_ICoreWebView2PermissionRequestedEventHandler + }; + } // namespace cast_info + } // namespace mswebview2 + + class webview2_com_handler + : public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, + public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, + public ICoreWebView2WebMessageReceivedEventHandler, + public ICoreWebView2PermissionRequestedEventHandler, + public ICoreWebView2NavigationCompletedEventHandler + { + using webview2_com_handler_cb_t = + std::function<void(ICoreWebView2Controller*, ICoreWebView2* webview)>; + + public: + webview2_com_handler(HWND hwnd, msg_cb_t msgCb, webview2_com_handler_cb_t cb) + : m_window(hwnd), m_msgCb(msgCb), m_cb(cb) + { + } + + virtual ~webview2_com_handler() = default; + webview2_com_handler(const webview2_com_handler& other) = delete; + webview2_com_handler& operator=(const webview2_com_handler& other) = delete; + webview2_com_handler(webview2_com_handler&& other) = delete; + webview2_com_handler& operator=(webview2_com_handler&& other) = delete; + + ULONG STDMETHODCALLTYPE AddRef() + { + return ++m_ref_count; + } + ULONG STDMETHODCALLTYPE Release() + { + if (m_ref_count > 1) + { + return --m_ref_count; + } + delete this; + return 0; + } + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppv) + { + using namespace mswebview2::cast_info; + + if (!ppv) + { + return E_POINTER; + } + + // All of the COM interfaces we implement should be added here regardless + // of whether they are required. + // This is just to be on the safe side in case the WebView2 Runtime ever + // requests a pointer to an interface we implement. + // The WebView2 Runtime must at the very least be able to get a pointer to + // ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler when we use + // our custom WebView2 loader implementation, and observations have shown + // that it is the only interface requested in this case. None have been + // observed to be requested when using the official WebView2 loader. + + if (cast_if_equal_iid(riid, controller_completed, ppv) || + cast_if_equal_iid(riid, environment_completed, ppv) || + cast_if_equal_iid(riid, message_received, ppv) || + cast_if_equal_iid(riid, permission_requested, ppv)) + { + return S_OK; + } + + return E_NOINTERFACE; + } + HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Environment* env) + { + if (SUCCEEDED(res)) + { + res = env->CreateCoreWebView2Controller(m_window, this); + if (SUCCEEDED(res)) + { + return S_OK; + } + } + try_create_environment(); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Controller* controller) + { + if (FAILED(res)) + { + // See try_create_environment() regarding + // HRESULT_FROM_WIN32(ERROR_INVALID_STATE). + // The result is E_ABORT if the parent window has been destroyed already. + switch (res) + { + case HRESULT_FROM_WIN32(ERROR_INVALID_STATE): + case E_ABORT: + return S_OK; + } + try_create_environment(); + return S_OK; + } + + ICoreWebView2* webview; + ::EventRegistrationToken token; + controller->get_CoreWebView2(&webview); + webview->add_WebMessageReceived(this, &token); + webview->add_PermissionRequested(this, &token); + webview->add_NavigationCompleted(this, &token); + + m_cb(controller, webview); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender, + ICoreWebView2WebMessageReceivedEventArgs* args) + { + LPWSTR message; + args->TryGetWebMessageAsString(&message); + m_msgCb(narrow_string(message)); + sender->PostWebMessageAsString(message); + + CoTaskMemFree(message); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender, + ICoreWebView2PermissionRequestedEventArgs* args) + { + COREWEBVIEW2_PERMISSION_KIND kind; + args->get_PermissionKind(&kind); + if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ) + { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + return S_OK; + } + HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender, + ICoreWebView2NavigationCompletedEventArgs* args) + { + PWSTR uri = nullptr; + auto hr = sender->get_Source(&uri); + if (SUCCEEDED(hr)) + { + auto curi = std::wstring_convert<std::codecvt_utf8<wchar_t> >().to_bytes(uri); + if (navigateCallback) + navigateCallback(curi, navigateCallbackArg); + } + CoTaskMemFree(uri); + return hr; + } + + // Checks whether the specified IID equals the IID of the specified type and + // if so casts the "this" pointer to T and returns it. Returns nullptr on + // mismatching IIDs. + // If ppv is specified then the pointer will also be assigned to *ppv. + template <typename T> + T* cast_if_equal_iid(REFIID riid, const cast_info_t<T>& info, + LPVOID* ppv = nullptr) noexcept + { + T* ptr = nullptr; + if (IsEqualIID(riid, info.iid)) + { + ptr = static_cast<T*>(this); + ptr->AddRef(); + } + if (ppv) + { + *ppv = ptr; + } + return ptr; + } + + // Set the function that will perform the initiating logic for creating + // the WebView2 environment. + void set_attempt_handler(std::function<HRESULT()> attempt_handler) noexcept + { + m_attempt_handler = attempt_handler; + } + + // Retry creating a WebView2 environment. + // The initiating logic for creating the environment is defined by the + // caller of set_attempt_handler(). + void try_create_environment() noexcept + { + // WebView creation fails with HRESULT_FROM_WIN32(ERROR_INVALID_STATE) if + // a running instance using the same user data folder exists, and the + // Environment objects have different EnvironmentOptions. + // Source: + // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environment?view=webview2-1.0.1150.38 + if (m_attempts < m_max_attempts) + { + ++m_attempts; + auto res = m_attempt_handler(); + if (SUCCEEDED(res)) + { + return; + } + // Not entirely sure if this error code only applies to + // CreateCoreWebView2Controller so we check here as well. + if (res == HRESULT_FROM_WIN32(ERROR_INVALID_STATE)) + { + return; + } + try_create_environment(); + return; + } + // Give up. + m_cb(nullptr, nullptr); + } + + void STDMETHODCALLTYPE add_navigate_listener( + std::function<void(const std::string&, void*)> callback, void* arg) + { + navigateCallback = std::move(callback); + navigateCallbackArg = arg; + } + + private: + HWND m_window; + msg_cb_t m_msgCb; + webview2_com_handler_cb_t m_cb; + std::atomic<ULONG> m_ref_count{ 1 }; + std::function<HRESULT()> m_attempt_handler; + unsigned int m_max_attempts = 5; + unsigned int m_attempts = 0; + void* navigateCallbackArg = nullptr; + std::function<void(const std::string&, void*)> navigateCallback = 0; + }; + + class win32_edge_engine + { + public: + win32_edge_engine(bool debug, void* window) + { + if (!is_webview2_available()) + { + return; + } + if (!m_com_init.is_initialized()) + { + return; + } + enable_dpi_awareness(); + if (window == nullptr) + { + HINSTANCE hInstance = GetModuleHandle(nullptr); + HICON icon = (HICON)LoadImage(hInstance, IDI_APPLICATION, IMAGE_ICON, + GetSystemMetrics(SM_CXICON), + GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR); + + WNDCLASSEXW wc; + ZeroMemory(&wc, sizeof(WNDCLASSEX)); + wc.cbSize = sizeof(WNDCLASSEX); + wc.hInstance = hInstance; + wc.lpszClassName = L"webview"; + wc.hIcon = icon; + wc.lpfnWndProc = + (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> LRESULT { + auto w = (win32_edge_engine*)GetWindowLongPtr(hwnd, GWLP_USERDATA); + switch (msg) + { + case WM_SIZE: + w->resize(hwnd); + break; + case WM_CLOSE: + DestroyWindow(hwnd); + break; + case WM_DESTROY: + w->terminate(); + break; + case WM_GETMINMAXINFO: + { + auto lpmmi = (LPMINMAXINFO)lp; + if (w == nullptr) + { + return 0; + } + if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0) + { + lpmmi->ptMaxSize = w->m_maxsz; + lpmmi->ptMaxTrackSize = w->m_maxsz; + } + if (w->m_minsz.x > 0 && w->m_minsz.y > 0) + { + lpmmi->ptMinTrackSize = w->m_minsz; + } + } + break; + default: + return DefWindowProcW(hwnd, msg, wp, lp); + } + return 0; + }); + RegisterClassExW(&wc); + m_window = CreateWindowW(L"webview", L"", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, + CW_USEDEFAULT, 640, 480, nullptr, nullptr, hInstance, + nullptr); + if (m_window == nullptr) + { + return; + } + SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); + } + else + { + m_window = *(static_cast<HWND*>(window)); + } + + ShowWindow(m_window, SW_SHOW); + UpdateWindow(m_window); + SetFocus(m_window); + + auto cb = std::bind(&win32_edge_engine::on_message, this, std::placeholders::_1); + + embed(m_window, debug, cb); + resize(m_window); + m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); + } + + virtual ~win32_edge_engine() + { + if (m_com_handler) + { + m_com_handler->Release(); + m_com_handler = nullptr; + } + if (m_webview) + { + m_webview->Release(); + m_webview = nullptr; + } + if (m_controller) + { + m_controller->Release(); + m_controller = nullptr; + } + } + + win32_edge_engine(const win32_edge_engine& other) = delete; + win32_edge_engine& operator=(const win32_edge_engine& other) = delete; + win32_edge_engine(win32_edge_engine&& other) = delete; + win32_edge_engine& operator=(win32_edge_engine&& other) = delete; + + void run() + { + MSG msg; + BOOL res; + while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1) + { + if (msg.hwnd) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + continue; + } + if (msg.message == WM_APP) + { + auto f = (dispatch_fn_t*)(msg.lParam); + (*f)(); + delete f; + } + else if (msg.message == WM_QUIT) + { + return; + } + } + } + void* window() + { + return (void*)m_window; + } + void terminate() + { + PostQuitMessage(0); + } + void dispatch(dispatch_fn_t f) + { + PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f)); + } + + void set_title(const std::string& title) + { + SetWindowTextW(m_window, widen_string(title).c_str()); + } + + void set_size(int width, int height, int hints) + { + auto style = GetWindowLong(m_window, GWL_STYLE); + if (hints == WEBVIEW_HINT_FIXED) + { + style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX); + } + else + { + style |= (WS_THICKFRAME | WS_MAXIMIZEBOX); + } + SetWindowLong(m_window, GWL_STYLE, style); + + if (hints == WEBVIEW_HINT_MAX) + { + m_maxsz.x = width; + m_maxsz.y = height; + } + else if (hints == WEBVIEW_HINT_MIN) + { + m_minsz.x = width; + m_minsz.y = height; + } + else + { + RECT r; + r.left = r.top = 0; + r.right = width; + r.bottom = height; + AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0); + SetWindowPos(m_window, nullptr, r.left, r.top, r.right - r.left, + r.bottom - r.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED); + resize(m_window); + } + } + + void navigate(const std::string& url) + { + auto wurl = widen_string(url); + m_webview->Navigate(wurl.c_str()); + } + + void init(const std::string& js) + { + auto wjs = widen_string(js); + m_webview->AddScriptToExecuteOnDocumentCreated(wjs.c_str(), nullptr); + } + + void eval(const std::string& js) + { + auto wjs = widen_string(js); + m_webview->ExecuteScript(wjs.c_str(), nullptr); + } + + void add_navigate_listener(std::function<void(const std::string&, void*)> callback, + void* arg) + { + m_com_handler->add_navigate_listener(callback, arg); + } + + void set_html(const std::string& html) + { + m_webview->NavigateToString(widen_string(html).c_str()); + } + + private: + bool embed(HWND wnd, bool debug, msg_cb_t cb) + { + std::atomic_flag flag = ATOMIC_FLAG_INIT; + flag.test_and_set(); + + wchar_t currentExePath[MAX_PATH]; + GetModuleFileNameW(nullptr, currentExePath, MAX_PATH); + wchar_t* currentExeName = PathFindFileNameW(currentExePath); + + wchar_t dataPath[MAX_PATH]; + if (!SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, dataPath))) + { + return false; + } + wchar_t userDataFolder[MAX_PATH]; + PathCombineW(userDataFolder, dataPath, currentExeName); + + m_com_handler = new webview2_com_handler( + wnd, cb, [&](ICoreWebView2Controller* controller, ICoreWebView2* webview) { + if (!controller || !webview) + { + flag.clear(); + return; + } + controller->AddRef(); + webview->AddRef(); + m_controller = controller; + m_webview = webview; + flag.clear(); + }); + + m_com_handler->set_attempt_handler([&] { + return m_webview2_loader.create_environment_with_options( + nullptr, userDataFolder, nullptr, m_com_handler); + }); + m_com_handler->try_create_environment(); + + MSG msg = {}; + while (flag.test_and_set() && GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + if (!m_controller || !m_webview) + { + return false; + } + ICoreWebView2Settings* settings = nullptr; + auto res = m_webview->get_Settings(&settings); + if (res != S_OK) + { + return false; + } + res = settings->put_AreDevToolsEnabled(debug ? TRUE : FALSE); + if (res != S_OK) + { + return false; + } + init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}"); + return true; + } + + void resize(HWND wnd) + { + if (m_controller == nullptr) + { + return; + } + RECT bounds; + GetClientRect(wnd, &bounds); + m_controller->put_Bounds(bounds); + } + + bool is_webview2_available() const noexcept + { + LPWSTR version_info = nullptr; + auto res = + m_webview2_loader.get_available_browser_version_string(nullptr, &version_info); + // The result will be equal to HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) + // if the WebView2 runtime is not installed. + auto ok = SUCCEEDED(res) && version_info; + if (version_info) + { + CoTaskMemFree(version_info); + } + return ok; + } + + virtual void on_message(const std::string& msg) = 0; + + // The app is expected to call CoInitializeEx before + // CreateCoreWebView2EnvironmentWithOptions. + // Source: + // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions + com_init_wrapper m_com_init{ COINIT_APARTMENTTHREADED }; + HWND m_window = nullptr; + POINT m_minsz = POINT{ 0, 0 }; + POINT m_maxsz = POINT{ 0, 0 }; + DWORD m_main_thread = GetCurrentThreadId(); + ICoreWebView2* m_webview = nullptr; + ICoreWebView2Controller* m_controller = nullptr; + webview2_com_handler* m_com_handler = nullptr; + mswebview2::loader m_webview2_loader; + }; + + } // namespace detail + + using browser_engine = detail::win32_edge_engine; + +} // namespace webview + +#endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */ + +namespace webview +{ + + class webview : public browser_engine + { + public: + webview(bool debug = false, void* wnd = nullptr) : browser_engine(debug, wnd) + { + } + + void navigate(const std::string& url) + { + if (url.empty()) + { + browser_engine::navigate("about:blank"); + return; + } + browser_engine::navigate(url); + } + + using binding_t = std::function<void(std::string, std::string, void*)>; + class binding_ctx_t + { + public: + binding_ctx_t(binding_t callback, void* arg) : callback(callback), arg(arg) + { + } + // This function is called upon execution of the bound JS function + binding_t callback; + // This user-supplied argument is passed to the callback + void* arg; + }; + + using sync_binding_t = std::function<std::string(std::string)>; + + // Synchronous bind + void bind(const std::string& name, sync_binding_t fn) + { + auto wrapper = [this, fn](const std::string& seq, const std::string& req, + void* /*arg*/) { resolve(seq, 0, fn(req)); }; + bind(name, wrapper, nullptr); + } + + // Asynchronous bind + void bind(const std::string& name, binding_t fn, void* arg) + { + if (bindings.count(name) > 0) + { + return; + } + bindings.emplace(name, binding_ctx_t(fn, arg)); + auto js = "(function() { var name = '" + name + "';" + R""( + var RPC = window._rpc = (window._rpc || {nextSeq: 1}); + window[name] = function() { + var seq = RPC.nextSeq++; + var promise = new Promise(function(resolve, reject) { + RPC[seq] = { + resolve: resolve, + reject: reject, + }; + }); + window.external.invoke(JSON.stringify({ + id: seq, + method: name, + params: Array.prototype.slice.call(arguments), + })); + return promise; + } + })())""; + init(js); + eval(js); + } + + void unbind(const std::string& name) + { + auto found = bindings.find(name); + if (found != bindings.end()) + { + auto js = "delete window['" + name + "'];"; + init(js); + eval(js); + bindings.erase(found); + } + } + + void resolve(const std::string& seq, int status, const std::string& result) + { + dispatch([seq, status, result, this]() { + if (status == 0) + { + eval("window._rpc[" + seq + "].resolve(" + result + "); delete window._rpc[" + + seq + "]"); + } + else + { + eval("window._rpc[" + seq + "].reject(" + result + "); delete window._rpc[" + + seq + "]"); + } + }); + } + + private: + void on_message(const std::string& msg) override + { + auto seq = detail::json_parse(msg, "id", 0); + auto name = detail::json_parse(msg, "method", 0); + auto args = detail::json_parse(msg, "params", 0); + auto found = bindings.find(name); + if (found == bindings.end()) + { + return; + } + const auto& context = found->second; + context.callback(seq, args, context.arg); + } + + std::map<std::string, binding_ctx_t> bindings; + }; +} // namespace webview + +WEBVIEW_API webview_t webview_create(int debug, void* wnd) +{ + auto w = new webview::webview(debug, wnd); + if (!w->window()) + { + delete w; + return nullptr; + } + return w; +} + +WEBVIEW_API void webview_destroy(webview_t w) +{ + delete static_cast<webview::webview*>(w); +} + +WEBVIEW_API void webview_run(webview_t w) +{ + static_cast<webview::webview*>(w)->run(); +} + +WEBVIEW_API void webview_terminate(webview_t w) +{ + static_cast<webview::webview*>(w)->terminate(); +} + +WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t, void*), void* arg) +{ + static_cast<webview::webview*>(w)->dispatch([=]() { fn(w, arg); }); +} + +WEBVIEW_API void* webview_get_window(webview_t w) +{ + return static_cast<webview::webview*>(w)->window(); +} + +WEBVIEW_API void webview_set_title(webview_t w, const char* title) +{ + static_cast<webview::webview*>(w)->set_title(title); +} + +WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints) +{ + static_cast<webview::webview*>(w)->set_size(width, height, hints); +} + +WEBVIEW_API void webview_navigate(webview_t w, const char* url) +{ + static_cast<webview::webview*>(w)->navigate(url); +} + +WEBVIEW_API void webview_set_html(webview_t w, const char* html) +{ + static_cast<webview::webview*>(w)->set_html(html); +} + +WEBVIEW_API void webview_init(webview_t w, const char* js) +{ + static_cast<webview::webview*>(w)->init(js); +} + +WEBVIEW_API void webview_eval(webview_t w, const char* js) +{ + static_cast<webview::webview*>(w)->eval(js); +} + +WEBVIEW_API void webview_bind(webview_t w, const char* name, + void (*fn)(const char* seq, const char* req, void* arg), void* arg) +{ + static_cast<webview::webview*>(w)->bind( + name, + [=](const std::string& seq, const std::string& req, void* arg) { + fn(seq.c_str(), req.c_str(), arg); + }, + arg); +} + +WEBVIEW_API void webview_unbind(webview_t w, const char* name) +{ + static_cast<webview::webview*>(w)->unbind(name); +} + +WEBVIEW_API void webview_return(webview_t w, const char* seq, int status, const char* result) +{ + static_cast<webview::webview*>(w)->resolve(seq, status, result); +} + +WEBVIEW_API const webview_version_info_t* webview_version() +{ + return &webview::detail::library_version_info; +} + +#endif /* WEBVIEW_HEADER */ +#endif /* __cplusplus */ +#endif /* WEBVIEW_H */ diff --git a/client/SDL/aad/wrapper/webview_impl.cpp b/client/SDL/aad/wrapper/webview_impl.cpp new file mode 100644 index 0000000..5f4d3d5 --- /dev/null +++ b/client/SDL/aad/wrapper/webview_impl.cpp @@ -0,0 +1,82 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "webview.h" + +#include <assert.h> +#include <string> +#include <vector> +#include <map> +#include <regex> +#include <sstream> +#include "../webview_impl.hpp" + +static std::vector<std::string> split(const std::string& input, const std::string& regex) +{ + // passing -1 as the submatch index parameter performs splitting + std::regex re(regex); + std::sregex_token_iterator first{ input.begin(), input.end(), re, -1 }; + std::sregex_token_iterator last; + return { first, last }; +} + +static std::map<std::string, std::string> urlsplit(const std::string& url) +{ + auto pos = url.find("?"); + if (pos == std::string::npos) + return {}; + auto surl = url.substr(pos); + auto args = split(surl, "&"); + + std::map<std::string, std::string> argmap; + for (const auto& arg : args) + { + auto kv = split(arg, "="); + if (kv.size() == 2) + argmap.insert({ kv[0], kv[1] }); + } + return argmap; +} + +static void fkt(const std::string& url, void* arg) +{ + auto args = urlsplit(url); + auto val = args.find("code"); + if (val == args.end()) + return; + + assert(arg); + auto rcode = static_cast<std::string*>(arg); + *rcode = val->second; +} + +bool webview_impl_run(const std::string& title, const std::string& url, std::string& code) +{ + webview::webview w(false, nullptr); + + w.set_title(title); + w.set_size(640, 480, WEBVIEW_HINT_NONE); + + std::string scheme; + w.add_scheme_handler("ms-appx-web", fkt, &scheme); + w.add_navigate_listener(fkt, &code); + w.navigate(url); + w.run(); + return !code.empty(); +} |