// Copyright (C) 2025 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qwebview2webview_p.h" #include #include #include QString WebErrorStatusToString(COREWEBVIEW2_WEB_ERROR_STATUS status) { switch (status) { #define STATUS_ENTRY(statusValue) \ case statusValue: \ return QString(L#statusValue); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_COMMON_NAME_IS_INCORRECT); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_EXPIRED); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CLIENT_CERTIFICATE_CONTAINS_ERRORS); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_REVOKED); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_IS_INVALID); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_SERVER_UNREACHABLE); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_TIMEOUT); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_ERROR_HTTP_INVALID_SERVER_RESPONSE); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CONNECTION_ABORTED); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CONNECTION_RESET); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_DISCONNECTED); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CANNOT_CONNECT); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_HOST_NAME_NOT_RESOLVED); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_OPERATION_CANCELED); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_REDIRECT_FAILED); STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_UNEXPECTED_ERROR); #undef STATUS_ENTRY case COREWEBVIEW2_WEB_ERROR_STATUS_VALID_AUTHENTICATION_CREDENTIALS_REQUIRED: case COREWEBVIEW2_WEB_ERROR_STATUS_VALID_PROXY_AUTHENTICATION_REQUIRED: break; } return QString(L"ERROR"); } QWebview2WebViewSettingsPrivate::QWebview2WebViewSettingsPrivate(QObject *p) : QAbstractWebViewSettings(p) { } void QWebview2WebViewSettingsPrivate::init(ICoreWebView2Controller* viewController) { if (viewController != nullptr) { m_webviewController = viewController; HRESULT hr = m_webviewController->get_CoreWebView2(&m_webview); Q_ASSERT_SUCCEEDED(hr); } } bool QWebview2WebViewSettingsPrivate::localStorageEnabled() const { return true; } bool QWebview2WebViewSettingsPrivate::javaScriptEnabled() const { return m_javaScriptEnabled; } bool QWebview2WebViewSettingsPrivate::localContentCanAccessFileUrls() const { return m_allowFileAccess; } bool QWebview2WebViewSettingsPrivate::allowFileAccess() const { return m_allowFileAccess; } void QWebview2WebViewSettingsPrivate::setLocalContentCanAccessFileUrls(bool enabled) { Q_UNUSED(enabled); qWarning("setLocalContentCanAccessFileUrls() not supported on this platform"); } void QWebview2WebViewSettingsPrivate::setJavaScriptEnabled(bool enabled) { m_javaScriptEnabled = enabled; if (!m_webview) return; ComPtr settings; HRESULT hr = m_webview->get_Settings(&settings); Q_ASSERT_SUCCEEDED(hr); hr = settings->put_IsScriptEnabled(enabled); Q_ASSERT_SUCCEEDED(hr); } void QWebview2WebViewSettingsPrivate::setLocalStorageEnabled(bool enabled) { Q_UNUSED(enabled); qWarning("setLocalStorageEnabled() not supported on this platform"); } void QWebview2WebViewSettingsPrivate::setAllowFileAccess(bool enabled) { m_allowFileAccess = enabled; } QWebView2WebViewPrivate::QWebView2WebViewPrivate(QObject *parent) : QAbstractWebView(parent), m_settings(new QWebview2WebViewSettingsPrivate(this)), m_window(new QWindow), m_isLoading(false) { // Create a QWindow without a parent // This window is used for initializing the WebView2 m_window->setFlag(Qt::Tool); m_window->setFlag(Qt::FramelessWindowHint); // No border m_window->setFlag(Qt::WindowDoesNotAcceptFocus); // No focus m_window->setVisible(true); // create platform window HWND hWnd = (HWND)m_window->winId(); QTimer::singleShot(0, this, [this, hWnd]() { emit initialize(hWnd); }); }; void QWebView2WebViewPrivate::initialize(HWND hWnd) { connect(m_window, &QWindow::widthChanged, this, &QWebView2WebViewPrivate::updateWindowGeometry, Qt::QueuedConnection); connect(m_window, &QWindow::heightChanged, this, &QWebView2WebViewPrivate::updateWindowGeometry, Qt::QueuedConnection); connect(m_window, &QWindow::screenChanged, this, &QWebView2WebViewPrivate::updateWindowGeometry, Qt::QueuedConnection); QPointer thisPtr = this; const QString userDataFolder = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) % QDir::separator() % QLatin1StringView("WebView2"); using W2ControllerCallback = ICoreWebView2CreateCoreWebView2ControllerCompletedHandler; auto controllerCallback = Microsoft::WRL::Callback( [thisPtr, this](HRESULT result, ICoreWebView2Controller *controller) -> HRESULT { if (thisPtr.isNull()) return S_FALSE; if (!controller) return S_FALSE; HRESULT hr; m_webviewController = controller; hr = m_webviewController->get_CoreWebView2(&m_webview); Q_ASSERT_SUCCEEDED(hr); ComPtr webview2; hr = m_webview->QueryInterface(IID_PPV_ARGS(&webview2)); Q_ASSERT_SUCCEEDED(hr); hr = webview2->get_CookieManager(&m_cookieManager); Q_ASSERT_SUCCEEDED(hr); m_settings->init(m_webviewController.Get()); // Add a few settings for the webview ComPtr settings; hr = m_webview->get_Settings(&settings); Q_ASSERT_SUCCEEDED(hr); hr = settings->put_IsScriptEnabled(m_settings->javaScriptEnabled()); Q_ASSERT_SUCCEEDED(hr); hr = settings->put_AreDefaultScriptDialogsEnabled(TRUE); Q_ASSERT_SUCCEEDED(hr); hr = settings->put_IsWebMessageEnabled(TRUE); Q_ASSERT_SUCCEEDED(hr); QMetaObject::invokeMethod(this, "updateWindowGeometry", Qt::QueuedConnection); // Schedule an async task to navigate to the url // Because this is a callback and it might be triggered with a delay if (!m_url.isEmpty() && m_url.isValid() && !m_url.scheme().isEmpty()) { hr = m_webview->Navigate((wchar_t *)m_url.toString().utf16()); Q_ASSERT_SUCCEEDED(hr); } else if (!m_initData.m_html.isEmpty()) { hr = m_webview->NavigateToString((wchar_t *)m_initData.m_html.utf16()); Q_ASSERT_SUCCEEDED(hr); } if (m_initData.m_cookies.size() > 0) { for (auto it = m_initData.m_cookies.constBegin(); it != m_initData.m_cookies.constEnd(); ++it) setCookie(it->domain, it->name, it.value().value); } if (!m_initData.m_httpUserAgent.isEmpty()) { ComPtr settings2; hr = settings->QueryInterface(IID_PPV_ARGS(&settings2)); if (settings2) { hr = settings2->put_UserAgent( (wchar_t *)m_initData.m_httpUserAgent.utf16()); if (SUCCEEDED(hr)) QTimer::singleShot(0, thisPtr, [thisPtr] { if (!thisPtr.isNull()) emit thisPtr->httpUserAgentChanged( thisPtr->m_initData.m_httpUserAgent); }); } } EventRegistrationToken token; hr = m_webview->add_NavigationStarting( Microsoft::WRL::Callback( [this](ICoreWebView2 *webview, ICoreWebView2NavigationStartingEventArgs *args) -> HRESULT { return this->onNavigationStarting(webview, args); }) .Get(), &token); Q_ASSERT_SUCCEEDED(hr); hr = m_webview->add_NavigationCompleted( Microsoft::WRL::Callback( [this](ICoreWebView2 *webview, ICoreWebView2NavigationCompletedEventArgs *args) -> HRESULT { return this->onNavigationCompleted(webview, args); }) .Get(), &token); Q_ASSERT_SUCCEEDED(hr); m_webview->add_WebResourceRequested( Microsoft::WRL::Callback( [this](ICoreWebView2 *webview, ICoreWebView2WebResourceRequestedEventArgs *args) -> HRESULT { return this->onWebResourceRequested(webview, args); }) .Get(), &token); hr = m_webview->add_ContentLoading( Microsoft::WRL::Callback( [this](ICoreWebView2 *webview, ICoreWebView2ContentLoadingEventArgs *args) -> HRESULT { return this->onContentLoading(webview, args); }) .Get(), &token); Q_ASSERT_SUCCEEDED(hr); ComPtr webview22; hr = m_webview->QueryInterface(IID_PPV_ARGS(&webview22)); Q_ASSERT_SUCCEEDED(hr); hr = webview22->AddWebResourceRequestedFilterWithRequestSourceKinds( L"file://*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL, COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL); Q_ASSERT_SUCCEEDED(hr); QTimer::singleShot(0, this, &QWebView2WebViewPrivate::updateWindowGeometry); return S_OK; }); using W2EnvironmentCallback = ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler; auto environmentCallback = Microsoft::WRL::Callback( [hWnd, thisPtr, controllerCallback, this](HRESULT result, ICoreWebView2Environment *env) -> HRESULT { env->CreateCoreWebView2Controller(hWnd, controllerCallback.Get()); return S_OK; }); CreateCoreWebView2EnvironmentWithOptions(nullptr, userDataFolder.toStdWString().c_str(), nullptr, environmentCallback.Get()); } QWebView2WebViewPrivate::~QWebView2WebViewPrivate() { m_window->destroy(); m_webviewController = nullptr; m_webview = nullptr; } QString QWebView2WebViewPrivate::httpUserAgent() const { if (m_webviewController) { ComPtr settings; HRESULT hr = m_webview->get_Settings(&settings); Q_ASSERT_SUCCEEDED(hr); ComPtr settings2; hr = settings->QueryInterface(IID_PPV_ARGS(&settings2)); if (settings2) { wchar_t *userAgent; hr = settings2->get_UserAgent(&userAgent); Q_ASSERT_SUCCEEDED(hr); QString userAgentString(userAgent); CoTaskMemFree(userAgent); return userAgentString; } qWarning() << "No http user agent available."; return ""; } return m_initData.m_httpUserAgent; } void QWebView2WebViewPrivate::setHttpUserAgent(const QString &userAgent) { if (m_webviewController) { ComPtr settings; HRESULT hr = m_webview->get_Settings(&settings); Q_ASSERT_SUCCEEDED(hr); ComPtr settings2; hr = settings->QueryInterface(IID_PPV_ARGS(&settings2)); if (settings2) { hr = settings2->put_UserAgent((wchar_t*)userAgent.utf16()); if (SUCCEEDED(hr)) emit httpUserAgentChanged(userAgent); return; } else { qWarning() << "No http user agent setting available."; } } else { m_initData.m_httpUserAgent = userAgent; } } void QWebView2WebViewPrivate::setUrl(const QUrl &url) { m_url = url; if (m_webview) { HRESULT hr = m_webview->Navigate((wchar_t*)url.toString().utf16()); if (FAILED(hr)) { emit loadProgressChanged(100); emit loadingChanged(QWebViewLoadRequestPrivate(url, QWebView::LoadFailedStatus, QString())); } } } bool QWebView2WebViewPrivate::canGoBack() const { BOOL canGoBack = false; if (m_webviewController) { HRESULT hr = m_webview->get_CanGoBack(&canGoBack); Q_ASSERT_SUCCEEDED(hr); } return canGoBack; } bool QWebView2WebViewPrivate::canGoForward() const { BOOL canGoForward = false; if (m_webviewController) { HRESULT hr = m_webview->get_CanGoForward(&canGoForward); Q_ASSERT_SUCCEEDED(hr); } return canGoForward; } QString QWebView2WebViewPrivate::title() const { if (m_webviewController) { wchar_t *title; HRESULT hr = m_webview->get_DocumentTitle(&title); Q_ASSERT_SUCCEEDED(hr); QString titleString(title); CoTaskMemFree(title); return titleString; } return QString(); } int QWebView2WebViewPrivate::loadProgress() const { return m_isLoading ? 0 : 100; } bool QWebView2WebViewPrivate::isLoading() const { return m_isLoading; } QWindow* QWebView2WebViewPrivate::nativeWindow() const { return m_window; } void QWebView2WebViewPrivate::goBack() { if (m_webview) { const HRESULT hr = m_webview->GoBack(); Q_ASSERT_SUCCEEDED(hr); } } void QWebView2WebViewPrivate::goForward() { if (m_webview) { const HRESULT hr = m_webview->GoForward(); Q_ASSERT_SUCCEEDED(hr); } } void QWebView2WebViewPrivate::reload() { if (m_webview) { const HRESULT hr = m_webview->Reload(); Q_ASSERT_SUCCEEDED(hr); } } void QWebView2WebViewPrivate::stop() { if (m_webview) { const HRESULT hr = m_webview->Stop(); Q_ASSERT_SUCCEEDED(hr); } } void QWebView2WebViewPrivate::loadHtml(const QString &html, const QUrl &baseUrl) { if (!baseUrl.isEmpty()) qWarning("Base URLs for loadHtml() are not supported on this platform. The document will be treated as coming from 'about:blank'."); QByteArray encoded("data:text/html;charset=UTF-8,"); encoded.append(html.toUtf8().toPercentEncoding()); m_url = QUrl(encoded); if (m_webview) { const HRESULT hr = m_webview->NavigateToString((wchar_t *)html.utf16()); Q_ASSERT_SUCCEEDED(hr); } else { m_initData.m_html = html; } } void QWebView2WebViewPrivate::setCookie(const QString &domain, const QString &name, const QString &value) { if (m_webview) { if (m_cookieManager) { ComPtr cookie; HRESULT hr = m_cookieManager->CreateCookie((wchar_t*)name.utf16(), (wchar_t*)value.utf16(), (wchar_t*)domain.utf16(), L"", &cookie); Q_ASSERT_SUCCEEDED(hr); hr = m_cookieManager->AddOrUpdateCookie(cookie.Get()); Q_ASSERT_SUCCEEDED(hr); emit cookieAdded(domain, name); } } else { m_initData.m_cookies.insert(domain + "/" + name, {domain, name, value}); } } void QWebView2WebViewPrivate::deleteCookie(const QString &domainName, const QString &cookieName) { if (m_webview) { if (m_cookieManager) { QString uri = domainName; if (!uri.startsWith("http")) uri = "https://" + uri; HRESULT hr = m_cookieManager->GetCookies((wchar_t*)uri.utf16(), Microsoft::WRL::Callback( [cookieName, domainName, this] (HRESULT result, ICoreWebView2CookieList* cookieList)->HRESULT { UINT count = 0; cookieList->get_Count(&count); for (UINT i = 0; i < count; ++i) { ComPtr cookie; if (SUCCEEDED(cookieList->GetValueAtIndex(i, &cookie))) { wchar_t *domainPtr; wchar_t *namePtr; if (SUCCEEDED(cookie->get_Domain(&domainPtr)) && SUCCEEDED(cookie->get_Name(&namePtr))) { QString domain(domainPtr); QString name(namePtr); CoTaskMemFree(namePtr); CoTaskMemFree(domainPtr); if (domainName == domain && cookieName == name) { emit cookieRemoved(domain, cookieName); return S_OK; } } } } return S_OK; }).Get()); Q_ASSERT_SUCCEEDED(hr); hr = m_cookieManager->DeleteCookiesWithDomainAndPath((wchar_t*)cookieName.utf16(), (wchar_t*)domainName.utf16(), L""); Q_ASSERT_SUCCEEDED(hr); } } else { m_initData.m_cookies.remove(domainName + "/" + cookieName); } } void QWebView2WebViewPrivate::deleteAllCookies() { if (m_webview) { if (m_cookieManager) { HRESULT hr = m_cookieManager->GetCookies(L"", Microsoft::WRL::Callback( [this](HRESULT result, ICoreWebView2CookieList* cookieList) -> HRESULT { UINT count = 0; cookieList->get_Count(&count); for (UINT i = 0; i < count; ++i) { ComPtr cookie; if (SUCCEEDED(cookieList->GetValueAtIndex(i, &cookie))) { wchar_t *domain; wchar_t *name; if (SUCCEEDED(cookie->get_Domain(&domain))) { if (SUCCEEDED(cookie->get_Name(&name))) { emit cookieRemoved(QString(domain), QString(name)); CoTaskMemFree(name); } CoTaskMemFree(domain); } } } return S_OK; }).Get()); Q_ASSERT_SUCCEEDED(hr); hr = m_cookieManager->DeleteAllCookies(); Q_ASSERT_SUCCEEDED(hr); } } else { m_initData.m_cookies.clear(); } } HRESULT QWebView2WebViewPrivate::onNavigationStarting(ICoreWebView2* webview, ICoreWebView2NavigationStartingEventArgs* args) { wchar_t *uri; HRESULT hr = args->get_Uri(&uri); Q_ASSERT_SUCCEEDED(hr); std::wstring_view source(uri); m_url = QString(source); emit urlChanged(m_url); CoTaskMemFree(uri); return S_OK; } HRESULT QWebView2WebViewPrivate::onNavigationCompleted(ICoreWebView2* webview, ICoreWebView2NavigationCompletedEventArgs* args) { m_isLoading = false; BOOL isSuccess; HRESULT hr = args->get_IsSuccess(&isSuccess); Q_ASSERT_SUCCEEDED(hr); const QWebView::LoadStatus status = isSuccess ? QWebView::LoadSucceededStatus : QWebView::LoadFailedStatus; COREWEBVIEW2_WEB_ERROR_STATUS errorStatus; hr = args->get_WebErrorStatus(&errorStatus); Q_ASSERT_SUCCEEDED(hr); if (errorStatus != COREWEBVIEW2_WEB_ERROR_STATUS_OPERATION_CANCELED) { const QString errorStr = isSuccess ? "" : WebErrorStatusToString(errorStatus); emit titleChanged(title()); emit loadProgressChanged(100); emit loadingChanged(QWebViewLoadRequestPrivate(m_url, status, errorStr)); } else { emit loadingChanged(QWebViewLoadRequestPrivate(m_url, QWebView::LoadStoppedStatus, QString())); } return S_OK; } HRESULT QWebView2WebViewPrivate::onWebResourceRequested(ICoreWebView2* sender, ICoreWebView2WebResourceRequestedEventArgs* args) { ComPtr request; ComPtr response; HRESULT hr = args->get_Request(&request); Q_ASSERT_SUCCEEDED(hr); wchar_t *uri; hr = request->get_Uri(&uri); std::wstring_view source(uri); if (!m_settings->allowFileAccess()) { ComPtr environment; ComPtr webview2; m_webview->QueryInterface(IID_PPV_ARGS(&webview2)); webview2->get_Environment(&environment); hr = environment->CreateWebResourceResponse(nullptr, 403, L"Access Denied", L"", &response); Q_ASSERT_SUCCEEDED(hr) hr = args->put_Response(response.Get()); Q_ASSERT_SUCCEEDED(hr) } CoTaskMemFree(uri); return S_OK; } HRESULT QWebView2WebViewPrivate::onContentLoading(ICoreWebView2* webview, ICoreWebView2ContentLoadingEventArgs* args) { m_isLoading = true; emit loadingChanged(QWebViewLoadRequestPrivate(m_url, QWebView::LoadStartedStatus, QString())); emit loadProgressChanged(0); return S_OK; } void QWebView2WebViewPrivate::updateWindowGeometry() { if (m_webviewController) { RECT bounds; GetClientRect((HWND)m_window->winId(), &bounds); const HRESULT hr = m_webviewController->put_Bounds(bounds); Q_ASSERT_SUCCEEDED(hr); } } void QWebView2WebViewPrivate::runJavaScriptPrivate(const QString &script, int callbackId) { if (m_webview) { const HRESULT hr = m_webview->ExecuteScript( (wchar_t *)script.utf16(), Microsoft::WRL::Callback( [this, callbackId](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT { QString resultStr = QString::fromWCharArray(resultObjectAsJson); QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(resultStr.toUtf8(), &parseError); QVariant resultVariant; if (parseError.error == QJsonParseError::NoError) { resultVariant = jsonDoc.toVariant(); } else { QString wrapped = QString("{\"value\":%1}").arg(resultStr); jsonDoc = QJsonDocument::fromJson(wrapped.toUtf8(), &parseError); if (parseError.error == QJsonParseError::NoError) { resultVariant = jsonDoc.object().value("value").toVariant(); } else { QJsonValue val = QJsonValue::fromVariant(resultStr); resultVariant = val.toVariant(); } } if (errorCode != S_OK) emit javaScriptResult(callbackId, qt_error_string(errorCode)); else emit javaScriptResult(callbackId, resultVariant); return errorCode; }) .Get()); Q_ASSERT_SUCCEEDED(hr); } } QAbstractWebViewSettings *QWebView2WebViewPrivate::getSettings() const { return m_settings; }