// 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 { if (!m_webview) return false; ComPtr settings; HRESULT hr = m_webview->get_Settings(&settings); Q_ASSERT_SUCCEEDED(hr); BOOL isEnabled; hr = settings->get_IsScriptEnabled(&isEnabled); Q_ASSERT_SUCCEEDED(hr); return isEnabled; } 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) { 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_isLoading(false) { // Create a QWindow without a parent // This window is used for initializing the WebView2 QWindow *hiddenWindow = new QWindow(); hiddenWindow->setFlag(Qt::Tool); hiddenWindow->setFlag(Qt::FramelessWindowHint); // No border hiddenWindow->setFlag(Qt::WindowDoesNotAcceptFocus); // No focus hiddenWindow->setGeometry(0, 0, 1, 1); hiddenWindow->setOpacity(0); hiddenWindow->show(); m_webViewWindow = hiddenWindow; HWND hWnd = (HWND)m_webViewWindow->winId(); // This is the container for WebView2 window after initialization // is finished (WebView2 window initialization seems to be very // sensitive, so we put it in the container after making sure that // the initialization is done successfully). // This needs reconsideration later because it might be the same // kind of problem with this bug (QTBUG-137230) m_window = new QWindow(); 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); CreateCoreWebView2EnvironmentWithOptions(nullptr, nullptr, nullptr, Microsoft::WRL::Callback( [hWnd, this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT { env->CreateCoreWebView2Controller(hWnd, Microsoft::WRL::Callback( [this](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT { HRESULT hr; if (controller != nullptr) { m_webviewController = controller; hr = m_webviewController->get_CoreWebView2(&m_webview); Q_ASSERT_SUCCEEDED(hr); ComPtr webview2; HRESULT 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(TRUE); 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 != nullptr && !m_initData->m_html.isEmpty()) { hr = m_webview->NavigateToString((wchar_t*)m_initData->m_html.utf16()); Q_ASSERT_SUCCEEDED(hr); } if (m_initData != nullptr && 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); } m_initData.release(); 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 webview2; hr = m_webview->QueryInterface(IID_PPV_ARGS(&webview2)); Q_ASSERT_SUCCEEDED(hr); hr = webview2->AddWebResourceRequestedFilterWithRequestSourceKinds( L"file://*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL, COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL); Q_ASSERT_SUCCEEDED(hr); return S_OK; }).Get()); return S_OK; }).Get()); } QWebView2WebViewPrivate::~QWebView2WebViewPrivate() { m_webViewWindow->destroy(); m_window->destroy(); m_webviewController = nullptr; m_webViewWindow = 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 ""; } 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()); Q_ASSERT_SUCCEEDED(hr); emit httpUserAgentChanged(userAgent); return; } } qWarning() << "No http user agent setting available."; } 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 loadingChanged(QWebViewLoadRequestPrivate(url, QWebView::LoadFailedStatus, QString())); emit loadProgressChanged(100); } } } 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) Q_ASSERT_SUCCEEDED(m_webview->GoBack()); } void QWebView2WebViewPrivate::goForward() { if (m_webview) Q_ASSERT_SUCCEEDED(m_webview->GoForward()); } void QWebView2WebViewPrivate::reload() { if (m_webview) Q_ASSERT_SUCCEEDED(m_webview->Reload()); } void QWebView2WebViewPrivate::stop() { if (m_webview) Q_ASSERT_SUCCEEDED(m_webview->Stop()); } 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) { Q_ASSERT_SUCCEEDED(m_webview->NavigateToString((wchar_t*)html.utf16())); } else { if (m_initData == nullptr) m_initData = std::make_unique(); 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 { if (m_initData == nullptr) m_initData = std::make_unique(); m_initData->m_cookies.insert(domain + "/" + name, {domain, name, value}); } } void QWebView2WebViewPrivate::deleteCookie(const QString &domain, const QString &cookieName) { if (m_webview) { if (m_cookieManager) { QString uri = domain; if (!uri.startsWith("http")) uri = "https://" + uri; HRESULT hr = m_cookieManager->GetCookies((wchar_t*)uri.utf16(), Microsoft::WRL::Callback( [cookieName, domain, this](HRESULT result, ICoreWebView2CookieList* cookieList)->HRESULT { UINT count = 0; cookieList->get_Count(&count); bool cookieFound = false; for (UINT i = 0; i < count; ++i) { ComPtr cookie; if (SUCCEEDED(cookieList->GetValueAtIndex(i, &cookie))) { wchar_t *namePtr; if (SUCCEEDED(cookie->get_Name(&namePtr))) { QString name(namePtr); CoTaskMemFree(namePtr); if (cookieName == name) { cookieFound = true; break; } } } } if (cookieFound) { HRESULT hr = m_cookieManager->DeleteCookiesWithDomainAndPath((wchar_t*)cookieName.utf16(), (wchar_t*)domain.utf16(), L""); Q_ASSERT_SUCCEEDED(hr); emit cookieRemoved(domain, cookieName); } return S_OK; }).Get()); Q_ASSERT_SUCCEEDED(hr); } } else if (m_initData != nullptr) { m_initData->m_cookies.remove(domain + "/" + 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 if (m_initData != nullptr) { 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 loadingChanged(QWebViewLoadRequestPrivate(m_url, status, errorStr)); emit titleChanged(title()); emit loadProgressChanged(100); } 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); Q_ASSERT_SUCCEEDED( environment->CreateWebResourceResponse( nullptr, 403, L"Access Denied", L"", &response)); Q_ASSERT_SUCCEEDED(args->put_Response(response.Get())); } 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) return; if (m_webViewWindow->opacity() <= 0) { m_webViewWindow->setFlag(Qt::WindowDoesNotAcceptFocus, false); m_webViewWindow->setParent(m_window); m_webViewWindow->setOpacity(1); } RECT bounds; GetClientRect((HWND)m_window->winId(), &bounds); Q_ASSERT_SUCCEEDED(m_webviewController->put_Bounds(bounds)); m_webViewWindow->setGeometry(0, 0, m_window->width(), m_window->height()); } void QWebView2WebViewPrivate::runJavaScriptPrivate(const QString &script, int callbackId) { QEventLoop loop; if (m_webview) Q_ASSERT_SUCCEEDED(m_webview->ExecuteScript((wchar_t*)script.utf16(), Microsoft::WRL::Callback( [&loop, 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); loop.quit(); return errorCode; }).Get())); loop.exec(); } QAbstractWebViewSettings *QWebView2WebViewPrivate::getSettings() const { return m_settings; }