// Copyright (C) 2015 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 "qwinrtwebview_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Microsoft::WRL; using namespace Microsoft::WRL::Wrappers; using namespace ABI::Windows::Foundation; using namespace ABI::Windows::Foundation::Collections; using namespace ABI::Windows::Graphics::Display; using namespace ABI::Windows::System; using namespace ABI::Windows::UI; using namespace ABI::Windows::UI::Xaml; using namespace ABI::Windows::UI::Xaml::Controls; using namespace ABI::Windows::UI::Xaml::Markup; using namespace ABI::Windows::Web; class HStringListIterator : public RuntimeClass, IIterator> { public: HStringListIterator(HSTRING *data, int size) : data(data), size(size), pos(0) { } HRESULT __stdcall get_Current(HSTRING *current) override { *current = pos >= size ? nullptr : data[pos]; return S_OK; } HRESULT __stdcall get_HasCurrent(boolean *hasCurrent) override { *hasCurrent = pos < size; return S_OK; } HRESULT __stdcall MoveNext(boolean *hasCurrent) override { *hasCurrent = ++pos < size; return S_OK; } HRESULT __stdcall GetMany(unsigned capacity, HSTRING *value, unsigned *actual) { unsigned i = 0; for (; i < qMin(capacity, unsigned(size)); ++i) value[i] = data[pos + i]; *actual = i; pos += i; return S_OK; } private: HSTRING *data; int size; int pos; }; class HStringList : public RuntimeClass, IIterable> { public: HStringList(const QList &stringList) { d.resize(stringList.size()); for (int i = 0; i < stringList.size(); ++i) { const QString qString = stringList.at(i).trimmed(); HStringReference hString(reinterpret_cast(qString.utf16()), qString.length()); hString.CopyTo(&d[i++]); } } ~HStringList() { for (const HSTRING &hString : std::as_const(d)) WindowsDeleteString(hString); } HRESULT __stdcall First(IIterator **first) override { ComPtr it = Make(d.data(), d.size()); return it.Get()->QueryInterface(IID_PPV_ARGS(first)); } private: QList d; }; static QUrl qurlFromUri(IUriRuntimeClass *uri) { HRESULT hr; HString uriString; hr = uri->get_AbsoluteUri(uriString.GetAddressOf()); Q_ASSERT_SUCCEEDED(hr); quint32 uriStringLength; const wchar_t *uriStringBuffer = uriString.GetRawBuffer(&uriStringLength); return QUrl(QString::fromWCharArray(uriStringBuffer, uriStringLength)); } static QString webErrorStatusString(WebErrorStatus status) { switch (status) { case WebErrorStatus_Unknown: return QStringLiteral("Unknown"); case WebErrorStatus_CertificateCommonNameIsIncorrect: return QStringLiteral("CertificateCommonNameIsIncorrect"); case WebErrorStatus_CertificateExpired: return QStringLiteral("CertificateExpired"); case WebErrorStatus_CertificateContainsErrors: return QStringLiteral("CertificateContainsErrors"); case WebErrorStatus_CertificateRevoked: return QStringLiteral("CertificateRevoked"); case WebErrorStatus_CertificateIsInvalid: return QStringLiteral("CertificateIsInvalid"); case WebErrorStatus_ServerUnreachable: return QStringLiteral("ServerUnreachable"); case WebErrorStatus_Timeout: return QStringLiteral("Timeout"); case WebErrorStatus_ErrorHttpInvalidServerResponse: return QStringLiteral("ErrorHttpInvalidServerResponse"); case WebErrorStatus_ConnectionAborted: return QStringLiteral("ConnectionAborted"); case WebErrorStatus_ConnectionReset: return QStringLiteral("ConnectionReset"); case WebErrorStatus_Disconnected: return QStringLiteral("Disconnected"); case WebErrorStatus_HttpToHttpsOnRedirection: return QStringLiteral("HttpToHttpsOnRedirection"); case WebErrorStatus_HttpsToHttpOnRedirection: return QStringLiteral("HttpsToHttpOnRedirection"); case WebErrorStatus_CannotConnect: return QStringLiteral("CannotConnect"); case WebErrorStatus_HostNameNotResolved: return QStringLiteral("HostNameNotResolved"); case WebErrorStatus_OperationCanceled: return QStringLiteral("OperationCanceled"); case WebErrorStatus_RedirectFailed: return QStringLiteral("RedirectFailed"); case WebErrorStatus_UnexpectedStatusCode: return QStringLiteral("UnexpectedStatusCode"); case WebErrorStatus_UnexpectedRedirection: return QStringLiteral("UnexpectedRedirection"); case WebErrorStatus_UnexpectedClientError: return QStringLiteral("UnexpectedClientError"); case WebErrorStatus_UnexpectedServerError: return QStringLiteral("UnexpectedServerError"); case WebErrorStatus_MultipleChoices: return QStringLiteral("MultipleChoices"); case WebErrorStatus_MovedPermanently: return QStringLiteral("MovedPermanently"); case WebErrorStatus_Found: return QStringLiteral("Found"); case WebErrorStatus_SeeOther: return QStringLiteral("SeeOther"); case WebErrorStatus_NotModified: return QStringLiteral("NotModified"); case WebErrorStatus_UseProxy: return QStringLiteral("UseProxy"); case WebErrorStatus_TemporaryRedirect: return QStringLiteral("TemporaryRedirect"); case WebErrorStatus_BadRequest: return QStringLiteral("BadRequest"); case WebErrorStatus_Unauthorized: return QStringLiteral("Unauthorized"); case WebErrorStatus_PaymentRequired: return QStringLiteral("PaymentRequired"); case WebErrorStatus_Forbidden: return QStringLiteral("Forbidden"); case WebErrorStatus_NotFound: return QStringLiteral("NotFound"); case WebErrorStatus_MethodNotAllowed: return QStringLiteral("MethodNotAllowed"); case WebErrorStatus_NotAcceptable: return QStringLiteral("NotAcceptable"); case WebErrorStatus_ProxyAuthenticationRequired: return QStringLiteral("ProxyAuthenticationRequired"); case WebErrorStatus_RequestTimeout: return QStringLiteral("RequestTimeout"); case WebErrorStatus_Conflict: return QStringLiteral("Conflict"); case WebErrorStatus_Gone: return QStringLiteral("Gone"); case WebErrorStatus_LengthRequired: return QStringLiteral("LengthRequired"); case WebErrorStatus_PreconditionFailed: return QStringLiteral("PreconditionFailed"); case WebErrorStatus_RequestEntityTooLarge: return QStringLiteral("RequestEntityTooLarge"); case WebErrorStatus_RequestUriTooLong: return QStringLiteral("RequestUriTooLong"); case WebErrorStatus_UnsupportedMediaType: return QStringLiteral("UnsupportedMediaType"); case WebErrorStatus_RequestedRangeNotSatisfiable: return QStringLiteral("RequestedRangeNotSatisfiable"); case WebErrorStatus_ExpectationFailed: return QStringLiteral("ExpectationFailed"); case WebErrorStatus_InternalServerError: return QStringLiteral("InternalServerError"); case WebErrorStatus_NotImplemented: return QStringLiteral("NotImplemented"); case WebErrorStatus_BadGateway: return QStringLiteral("BadGateway"); case WebErrorStatus_ServiceUnavailable: return QStringLiteral("ServiceUnavailable"); case WebErrorStatus_GatewayTimeout: return QStringLiteral("GatewayTimeout"); case WebErrorStatus_HttpVersionNotSupported: return QStringLiteral("HttpVersionNotSupported"); default: break; } return QString(); } struct WinRTWebView { ComPtr base; ComPtr ext; ComPtr host; ComPtr canvas; ComPtr uriFactory; ComPtr displayInformation; ComPtr launcherStatics; QPointer window; QHash *, int> callbacks; EventRegistrationToken navigationStartingToken; EventRegistrationToken navigationCompletedToken; EventRegistrationToken unviewableContentToken; bool isLoading : 1; }; #define LSTRING(str) L#str static const wchar_t webviewXaml[] = LSTRING( ); QWinRTWebViewPrivate::QWinRTWebViewPrivate(QObject *parent) : QAbstractWebView(parent), d(new WinRTWebView) { d->isLoading = false; QEventDispatcherWinRT::runOnXamlThread([this]() { HRESULT hr; ComPtr xamlReader; hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_UI_Xaml_Markup_XamlReader).Get(), IID_PPV_ARGS(&xamlReader)); Q_ASSERT_SUCCEEDED(hr); // Directly instantiating a WebView works, but it throws an exception // when navigating. Using a XamlReader appears to set it up properly. ComPtr element; hr = xamlReader->Load(HString::MakeReference(webviewXaml).Get(), &element); Q_ASSERT_SUCCEEDED(hr); hr = element.As(&d->base); Q_ASSERT_SUCCEEDED(hr); hr = d->base.As(&d->ext); Q_ASSERT_SUCCEEDED(hr); hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_System_Launcher).Get(), IID_PPV_ARGS(&d->launcherStatics)); Q_ASSERT_SUCCEEDED(hr); hr = d->ext->add_NavigationStarting( Callback>(this, &QWinRTWebViewPrivate::onNavigationStarted).Get(), &d->navigationStartingToken); Q_ASSERT_SUCCEEDED(hr); hr = d->ext->add_NavigationCompleted( Callback>(this, &QWinRTWebViewPrivate::onNavigationCompleted).Get(), &d->navigationCompletedToken); Q_ASSERT_SUCCEEDED(hr); hr = d->ext->add_UnviewableContentIdentified( Callback>(this, &QWinRTWebViewPrivate::onUnviewableContent).Get(), &d->unviewableContentToken); Q_ASSERT_SUCCEEDED(hr); ComPtr displayInformationStatics; hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Graphics_Display_DisplayInformation).Get(), IID_PPV_ARGS(&displayInformationStatics)); Q_ASSERT_SUCCEEDED(hr); hr = displayInformationStatics->GetForCurrentView(&d->displayInformation); Q_ASSERT_SUCCEEDED(hr); return hr; }); HRESULT hr; hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_UI_Xaml_Controls_Canvas).Get(), IID_PPV_ARGS(&d->canvas)); Q_ASSERT_SUCCEEDED(hr); hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Foundation_Uri).Get(), IID_PPV_ARGS(&d->uriFactory)); Q_ASSERT_SUCCEEDED(hr); } QWinRTWebViewPrivate::~QWinRTWebViewPrivate() { QEventDispatcherWinRT::runOnXamlThread([this]() { HRESULT hr; hr = d->ext->remove_NavigationStarting(d->navigationStartingToken); Q_ASSERT_SUCCEEDED(hr); hr = d->ext->remove_NavigationCompleted(d->navigationCompletedToken); Q_ASSERT_SUCCEEDED(hr); hr = d->ext->remove_UnviewableContentIdentified(d->unviewableContentToken); Q_ASSERT_SUCCEEDED(hr); ComPtr> children; hr = d->host->get_Children(&children); Q_ASSERT_SUCCEEDED(hr); ComPtr uiElement; hr = d->base.As(&uiElement); Q_ASSERT_SUCCEEDED(hr); quint32 index; boolean found; hr = children->IndexOf(uiElement.Get(), &index, &found); Q_ASSERT_SUCCEEDED(hr); if (found) { hr = children->RemoveAt(index); Q_ASSERT_SUCCEEDED(hr); } return hr; }); } QString QWinRTWebViewPrivate::httpUserAgent() const { #ifdef QT_WINRT_URLMKGETSESSIONOPTION_NOT_AVAILABLE qWarning() << "Used Windows SDK version (" << QString::number(QT_UCRTVERSION) << ") does not " "support getting or setting of the user agent property. Consider updating to a more recent Windows 10 " "SDK (16299 or above)."; return ""; #else char buffer[255]; unsigned long stringLength; HRESULT hr = UrlMkGetSessionOption(0x10000001,&buffer,255,&stringLength,0); Q_ASSERT_SUCCEEDED(hr); return QString(buffer); #endif } void QWinRTWebViewPrivate::setHttpUserAgent(const QString &userAgent) { #ifdef QT_WINRT_URLMKSETSESSIONOPTION_NOT_AVAILABLE Q_UNUSED(userAgent); qWarning() << "Used Windows SDK version (" << QString::number(QT_UCRTVERSION) << ") does not " "support getting or setting of the user agent property. Consider updating to a more recent Windows 10 " "SDK (16299 or above)."; #else HRESULT hr = UrlMkSetSessionOption(0x10000001,userAgent.toLocal8Bit().data(),userAgent.size(),0); Q_ASSERT_SUCCEEDED(hr); emit httpUserAgentChanged(userAgent); #endif } void QWinRTWebViewPrivate::setUrl(const QUrl &url) { QEventDispatcherWinRT::runOnXamlThread([this, &url]() { HRESULT hr; const QString urlString = url.toString(); ComPtr uri; HStringReference uriString(reinterpret_cast(urlString.utf16()), urlString.length()); hr = d->uriFactory->CreateUri(uriString.Get(), &uri); Q_ASSERT_SUCCEEDED(hr); hr = d->base->Navigate(uri.Get()); // Directly running into an abort means, that the URI is not supported. Ask the user what to do if (hr == E_ABORT) { ComPtr> op; hr = d->launcherStatics->LaunchUriAsync(uri.Get(), &op); } Q_ASSERT_SUCCEEDED(hr); return hr; }); } bool QWinRTWebViewPrivate::canGoBack() const { boolean canGoBack; QEventDispatcherWinRT::runOnXamlThread([this, &canGoBack]() { HRESULT hr; hr = d->ext->get_CanGoBack(&canGoBack); Q_ASSERT_SUCCEEDED(hr); return hr; }); return canGoBack; } bool QWinRTWebViewPrivate::canGoForward() const { boolean canGoForward; QEventDispatcherWinRT::runOnXamlThread([this, &canGoForward]() { HRESULT hr; hr = d->ext->get_CanGoForward(&canGoForward); Q_ASSERT_SUCCEEDED(hr); return hr; }); return canGoForward; } QString QWinRTWebViewPrivate::title() const { HString title; QEventDispatcherWinRT::runOnXamlThread([this, &title]() { HRESULT hr; hr = d->ext->get_DocumentTitle(title.GetAddressOf()); Q_ASSERT_SUCCEEDED(hr); return hr; }); quint32 titleLength; const wchar_t *titleBuffer = title.GetRawBuffer(&titleLength); return QString::fromWCharArray(titleBuffer, titleLength); } int QWinRTWebViewPrivate::loadProgress() const { return d->isLoading ? 0 : 100; } bool QWinRTWebViewPrivate::isLoading() const { return d->isLoading; } QWindow *QWinRTWebViewPrivate::nativeWindow() { return d->window; } void QWinRTWebViewPrivate::goBack() { QEventDispatcherWinRT::runOnXamlThread([this]() { HRESULT hr; hr = d->ext->GoBack(); Q_ASSERT_SUCCEEDED(hr); return hr; }); } void QWinRTWebViewPrivate::goForward() { QEventDispatcherWinRT::runOnXamlThread([this]() { HRESULT hr; hr = d->ext->GoForward(); Q_ASSERT_SUCCEEDED(hr); return hr; }); } void QWinRTWebViewPrivate::reload() { QEventDispatcherWinRT::runOnXamlThread([this]() { HRESULT hr; hr = d->ext->Refresh(); Q_ASSERT_SUCCEEDED(hr); return hr; }); } void QWinRTWebViewPrivate::stop() { QEventDispatcherWinRT::runOnXamlThread([this]() { HRESULT hr; hr = d->ext->Stop(); Q_ASSERT_SUCCEEDED(hr); return hr; }); } void QWinRTWebViewPrivate::loadHtml(const QString &html, const QUrl &baseUrl) { if (!baseUrl.isEmpty()) qWarning("Base URLs for loadHtml() are not supported under WinRT."); HRESULT hr; HStringReference htmlText(reinterpret_cast(html.utf16()), html.length()); hr = d->base->NavigateToString(htmlText.Get()); Q_ASSERT_SUCCEEDED(hr); } void QWinRTWebViewPrivate::runJavaScriptPrivate(const QString &script, int callbackId) { static QRegularExpression functionTemplate(QStringLiteral("^(.*)\\((.*)\\)[\\s;]*?$")); QRegularExpressionMatch match = functionTemplate.match(script); if (!match.hasMatch()) { qWarning("The WinRT WebView only supports calling global functions, so please make your call" " in the form myFunction(a, b, c). Also note that only string arguments can be passed."); return; } const QString method = match.captured(1).trimmed(); HStringReference methodString(reinterpret_cast(method.utf16()), method.length()); ComPtr argumentStrings = Make(match.captured(2).split(QLatin1Char(','))); QEventDispatcherWinRT::runOnXamlThread([this, &methodString, &argumentStrings, &callbackId]() { HRESULT hr; ComPtr> op; hr = d->ext->InvokeScriptAsync(methodString.Get(), argumentStrings.Get(), &op); Q_ASSERT_SUCCEEDED(hr); d->callbacks.insert(op.Get(), callbackId); hr = op->put_Completed(Callback>([this](IAsyncOperation *op, AsyncStatus status) { int callbackId = d->callbacks.take(op); HRESULT hr; if (status != Completed) { ComPtr info; hr = op->QueryInterface(IID_PPV_ARGS(&info)); Q_ASSERT_SUCCEEDED(hr); HRESULT errorCode; hr = info->get_ErrorCode(&errorCode); Q_ASSERT_SUCCEEDED(hr); emit javaScriptResult(callbackId, qt_error_string(errorCode)); return S_OK; } HString result; hr = op->GetResults(result.GetAddressOf()); Q_ASSERT_SUCCEEDED(hr); quint32 resultLength; const wchar_t *resultBuffer = result.GetRawBuffer(&resultLength); emit javaScriptResult(callbackId, QString::fromWCharArray(resultBuffer, resultLength)); return S_OK; }).Get()); Q_ASSERT_SUCCEEDED(hr); return hr; }); } HRESULT QWinRTWebViewPrivate::onNavigationStarted(IWebView *, IWebViewNavigationStartingEventArgs *args) { d->isLoading = true; HRESULT hr; ComPtr uri; hr = args->get_Uri(&uri); Q_ASSERT_SUCCEEDED(hr); const QUrl url = qurlFromUri(uri.Get()); emit loadingChanged(QWebViewLoadRequestPrivate(url, QWebView::LoadStartedStatus, QString())); emit loadProgressChanged(0); return S_OK; } HRESULT QWinRTWebViewPrivate::onNavigationCompleted(IWebView *, IWebViewNavigationCompletedEventArgs *args) { d->isLoading = false; HRESULT hr; ComPtr uri; hr = args->get_Uri(&uri); Q_ASSERT_SUCCEEDED(hr); const QUrl url = qurlFromUri(uri.Get()); boolean isSuccess; hr = args->get_IsSuccess(&isSuccess); Q_ASSERT_SUCCEEDED(hr); const QWebView::LoadStatus status = isSuccess ? QWebView::LoadSucceededStatus : QWebView::LoadFailedStatus; WebErrorStatus errorStatus; hr = args->get_WebErrorStatus(&errorStatus); Q_ASSERT_SUCCEEDED(hr); const QString errorString = webErrorStatusString(errorStatus); emit loadingChanged(QWebViewLoadRequestPrivate(url, status, errorString)); emit titleChanged(title()); emit loadProgressChanged(100); return S_OK; } HRESULT QWinRTWebViewPrivate::onUnviewableContent(IWebView *, IWebViewUnviewableContentIdentifiedEventArgs *args) { HRESULT hr; ComPtr uri; hr = args->get_Uri(&uri); Q_ASSERT_SUCCEEDED(hr); ComPtr> op; hr = d->launcherStatics->LaunchUriAsync(uri.Get(), &op); Q_ASSERT_SUCCEEDED(hr); return S_OK; }