summaryrefslogtreecommitdiffstats
path: root/client/SDL/aad
diff options
context:
space:
mode:
Diffstat (limited to 'client/SDL/aad')
-rw-r--r--client/SDL/aad/CMakeLists.txt75
-rw-r--r--client/SDL/aad/dummy.cpp0
-rw-r--r--client/SDL/aad/qt/webview_impl.cpp105
-rw-r--r--client/SDL/aad/sdl_config.hpp.in3
-rw-r--r--client/SDL/aad/sdl_webview.cpp129
-rw-r--r--client/SDL/aad/sdl_webview.hpp38
-rw-r--r--client/SDL/aad/webview_impl.hpp24
-rw-r--r--client/SDL/aad/wrapper/README1
-rw-r--r--client/SDL/aad/wrapper/webview.h2781
-rw-r--r--client/SDL/aad/wrapper/webview_impl.cpp82
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();
+}