Revision control

++ // 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 "qohoswebcomponentlistener.h"
++ #include "qohoswebresourceerror.h"
++ #include "qohoswebview_p.h"
++ #include "qohoswebviewcontroller.h"
++ #include <QtWebView/private/qwebviewfactory_p.h>
++ #include <QtCore/private/qohoslogger_p.h>
++ #include <QtCore/qfileinfo.h>
++ #include <QtCore/qjsonobject.h>
++ #include <QtGui/qpa/qplatformwindow_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace {
++
++ class QOhosWebViewSettingPrivate : public QWebViewSettingsPrivate
++ {
++ public:
++ explicit QOhosWebViewSettingPrivate(std::shared_ptr<QOhosWebViewController> controller);
++
++ private:
++ bool doTestAttribute(WebAttribute attribute) const final;
++ void doSetAttribute(WebAttribute attribute, bool value) final;
++
++ std::shared_ptr<QOhosWebViewController> m_webViewController;
++ };
++
++ class QOhosWebViewPrivate : public QWebViewPrivate
++ {
++ Q_OBJECT
++ public:
++ explicit QOhosWebViewPrivate(QWebView *view);
++ ~QOhosWebViewPrivate() override;
++
++ QString httpUserAgent() const override;
++ void setHttpUserAgent(const QString &httpUserAgent) override;
++ QUrl url() const override;
++ void setUrl(const QUrl &url) override;
++ bool canGoBack() const override;
++ bool canGoForward() const override;
++ QString title() const override;
++ int loadProgress() const override;
++ bool isLoading() const override;
++
++ void initialize(QObject *context) override;
++ void setCookie(const QString &domain, const QString &name, const QString &value) override;
++ void deleteCookie(const QString &domain, const QString &name) override;
++ void deleteAllCookies() override;
++ QWindow *nativeWindow() const override;
++
++ void onErrorReceived(const QString &url, const WebResourceError &error);
++ void onPageBegan(const QString &url);
++ void onPageEnded(const QString &url);
++ void onProgressChanged(int progress);
++ void onTitleUpdated(const QString &title, bool isRealTitle);
++
++ public Q_SLOTS:
++ void goBack() override;
++ void goForward() override;
++ void reload() override;
++ void stop() override;
++ void loadHtml(const QString &html, const QUrl &baseUrl) override;
++
++ protected:
++ void runJavaScript(const QString &script,
++ const std::function<void(const QVariant &)> &resultCallback) override;
++ QWebViewSettingsPrivate *settings() const override;
++
++ private:
++ void emitLoadingChangeSignalAndUpdateLoadStatus(const QWebViewLoadingInfo &loadingInfo);
++ void emitSyntheticLoadFailure(const QUrl &url, const QString &errorMessage);
++
++ QPointer<QWindow> m_webViewWindow;
++ std::shared_ptr<QOhosWebViewController> m_webViewController;
++ std::shared_ptr<QOhosWebComponentListener> m_webComponentListener;
++
++ int m_loadProgress = 0;
++ QWebViewLoadingInfo::LoadStatus m_loadStatus = QWebViewLoadingInfo::LoadStatus::Stopped;
++ QString m_url;
++ QString m_title;
++ bool m_loadFailed = false;
++ };
++
++ class QOhosWebComponentListenerImpl : public QOhosWebComponentListener
++ {
++ public:
++ QOhosWebComponentListenerImpl(QPointer<QOhosWebViewPrivate> webViewPrivate);
++ ~QOhosWebComponentListenerImpl();
++
++ void onErrorReceived(const std::string &url, const WebResourceError &error) override;
++ void onPageBegan(const std::string &url) override;
++ void onPageEnded(const std::string &url) override;
++ void onProgressChanged(int progress) override;
++ void onTitleReceived(const std::string &title, bool isRealTitle) override;
++
++ private:
++ QPointer<QOhosWebViewPrivate> m_webViewPrivate;
++ };
++
++ QOhosWebViewSettingPrivate::QOhosWebViewSettingPrivate(std::shared_ptr<QOhosWebViewController> controller)
++ : m_webViewController(controller)
++ {
++ }
++
++ bool QOhosWebViewSettingPrivate::doTestAttribute(WebAttribute attribute) const
++ {
++ return m_webViewController->testAttribute(attribute);
++ }
++
++ void QOhosWebViewSettingPrivate::doSetAttribute(WebAttribute attribute, bool value)
++ {
++ m_webViewController->setAttribute(attribute, value);
++ }
++
++ QWindow *createEmbeddedWebViewWindow(
++ std::shared_ptr<QOhosWebViewController> webViewController,
++ std::shared_ptr<QOhosWebComponentListener> webComponentListener,
++ QObject *webComponentListenerContext)
++ {
++ auto webViewNode = webViewController->createEmbeddedWebComponentNodeOrFail(
++ webComponentListener, webComponentListenerContext);
++
++ return QNativeInterface::Private::QOhosWindow::fromNative(webViewNode);
++ }
++
++ QVariant convertJavaScriptResultToQVariantOrNull(std::optional<std::string> javaScriptResult)
++ {
++ if (!javaScriptResult.has_value())
++ return QVariant();
++
++ const QByteArray jsonData = "{ \"data\": " + QByteArray(javaScriptResult.value().c_str()) + " }";
++
++ QJsonParseError parseError;
++ const auto jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
++ if (parseError.error != QJsonParseError::NoError) {
++ qOhosCritical(QtForOhos)
++ << Q_FUNC_INFO
++ << "Failed to parse javaScript result, reason:"
++ << parseError.errorString();
++ return QVariant();
++ }
++
++ return jsonDoc.isObject()
++ ? jsonDoc.object()[QStringLiteral("data")].toVariant()
++ : QVariant();
++ }
++
++ std::string cookieUrlFromDomain(const QString &domain)
++ {
++ QString host = domain;
++ if (host.startsWith(u'.'))
++ host.remove(0, 1);
++ if (!host.contains(QLatin1String("://")))
++ host.prepend(QLatin1String("https://"));
++ return host.toStdString();
++ }
++
++ bool cookieHeaderHasName(const QString &cookieHeader, const QString &name)
++ {
++ const QStringList parts = cookieHeader.split(u';', Qt::SkipEmptyParts);
++ for (const QString &part : parts) {
++ const QString token = part.trimmed();
++ const qsizetype eq = token.indexOf(u'=');
++ const QString key = (eq < 0 ? token : token.left(eq)).trimmed();
++ if (key == name)
++ return true;
++ }
++ return false;
++ }
++
++ QOhosWebViewPrivate::QOhosWebViewPrivate(QWebView *view)
++ : QWebViewPrivate(view)
++ , m_webViewController(makeOhosWebViewController())
++ {
++ m_webComponentListener = std::make_shared<QOhosWebComponentListenerImpl>(this);
++ m_webViewWindow = createEmbeddedWebViewWindow(m_webViewController, m_webComponentListener, this);
++ if (m_webViewWindow) {
++ m_webViewController->bindQrcSchemeHandler();
++ m_webViewWindow->setParent(view);
++ connect(view, &QWindow::widthChanged, m_webViewWindow.data(), &QWindow::setWidth);
++ connect(view, &QWindow::heightChanged, m_webViewWindow.data(), &QWindow::setHeight);
++ connect(view, &QWindow::visibleChanged, m_webViewWindow.data(), &QWindow::setVisible);
++ }
++ }
++
++ QOhosWebViewPrivate::~QOhosWebViewPrivate() = default;
++
++ QString QOhosWebViewPrivate::httpUserAgent() const
++ {
++ return convertJavaScriptResultToQVariantOrNull(
++ m_webViewController->tryRunJavaScript("navigator.userAgent")).toString();
++ }
++
++ void QOhosWebViewPrivate::setHttpUserAgent(const QString &httpUserAgent)
++ {
++ if (m_webViewController->trySetCustomUserAgent(httpUserAgent.toStdString()))
++ Q_EMIT q_ptr->httpUserAgentStringChanged(httpUserAgent);
++ }
++
++ QUrl QOhosWebViewPrivate::url() const
++ {
++ auto controllerUrl = QString::fromStdString(m_webViewController->getUrl());
++
++ return QFileInfo::exists(controllerUrl)
++ ? QUrl::fromLocalFile(controllerUrl)
++ : QUrl::fromEncoded(controllerUrl.toUtf8());
++ }
++
++ void QOhosWebViewPrivate::setUrl(const QUrl &url)
++ {
++ auto controllerUrl = url.isLocalFile()
++ ? QFileInfo(url.toLocalFile()).absoluteFilePath().toStdString()
++ : url.toString().toStdString();
++
++ if (!m_webViewController->tryLoadUrl(controllerUrl)) {
++ qOhosWarning(QtForOhos) << Q_FUNC_INFO << "Failed to set url:" << controllerUrl.c_str();
++ emitSyntheticLoadFailure(
++ url, QStringLiteral("Failed to load: %1").arg(url.toString()));
++ }
++ }
++
++ void QOhosWebViewPrivate::loadHtml(const QString &html, const QUrl &baseUrl)
++ {
++ const std::string mimeType = "text/html;charset=UTF-8";
++ const std::string encoding = "UTF-8";
++
++ bool ok;
++ if (baseUrl.isEmpty() || baseUrl.scheme() == QLatin1String("data")) {
++ const std::string data = QUrl::toPercentEncoding(html).toStdString();
++ ok = m_webViewController->tryLoadHtml(data, mimeType, encoding, {}, {});
++ } else {
++ const std::string base = baseUrl.toString().toStdString();
++ ok = m_webViewController->tryLoadHtml(html.toStdString(), mimeType, encoding, base, base);
++ }
++ if (!ok)
++ qOhosWarning(QtForOhos) << Q_FUNC_INFO << "Failed to load html:" << html;
++ }
++
++ bool QOhosWebViewPrivate::canGoBack() const
++ {
++ return m_webViewController->canGoBack();
++ }
++
++ void QOhosWebViewPrivate::goBack()
++ {
++ m_webViewController->goBack();
++ }
++
++ bool QOhosWebViewPrivate::canGoForward() const
++ {
++ return m_webViewController->canGoForward();
++ }
++
++ void QOhosWebViewPrivate::goForward()
++ {
++ m_webViewController->goForward();
++ }
++
++ void QOhosWebViewPrivate::reload()
++ {
++ m_webViewController->refresh();
++ }
++
++ QString QOhosWebViewPrivate::title() const
++ {
++ return m_title;
++ }
++
++ void QOhosWebViewPrivate::runJavaScript(const QString &script,
++ const std::function<void(const QVariant &)> &resultCallback)
++ {
++ auto result = convertJavaScriptResultToQVariantOrNull(m_webViewController->tryRunJavaScript(script.toStdString()));
++ if (resultCallback)
++ resultCallback(result);
++ }
++
++ QWebViewSettingsPrivate *QOhosWebViewPrivate::settings() const
++ {
++ return new QOhosWebViewSettingPrivate(m_webViewController);
++ }
++
++ int QOhosWebViewPrivate::loadProgress() const
++ {
++ return m_loadProgress;
++ }
++
++ bool QOhosWebViewPrivate::isLoading() const
++ {
++ return m_loadStatus == QWebViewLoadingInfo::LoadStatus::Started;
++ }
++
++ void QOhosWebViewPrivate::initialize(QObject *context)
++ {
++ Q_UNUSED(context);
++ }
++
++ void QOhosWebViewPrivate::setCookie(const QString &domain, const QString &name, const QString &value)
++ {
++ const std::string url = cookieUrlFromDomain(domain);
++ const std::string cookie = QStringLiteral("%1=%2").arg(name, value).toStdString();
++
++ if (m_webViewController->trySetCookie(url, cookie))
++ Q_EMIT q_ptr->cookieAdded(domain, name);
++ }
++
++ void QOhosWebViewPrivate::deleteCookie(const QString &domain, const QString &name)
++ {
++ const std::string url = cookieUrlFromDomain(domain);
++
++ const std::optional<std::string> existing = m_webViewController->tryFetchCookie(url);
++ if (!existing || !cookieHeaderHasName(QString::fromStdString(*existing), name))
++ return;
++
++ const std::string expired = QStringLiteral("%1=; Max-Age=0; Path=/").arg(name).toStdString();
++ if (m_webViewController->trySetCookie(url, expired))
++ Q_EMIT q_ptr->cookieRemoved(domain, name);
++ }
++
++ void QOhosWebViewPrivate::deleteAllCookies()
++ {
++ const auto cookies = m_webViewController->fetchAllCookies();
++ if (!m_webViewController->tryClearAllCookies())
++ return;
++
++ for (const auto &[domain, name] : cookies)
++ Q_EMIT q_ptr->cookieRemoved(QString::fromStdString(domain), QString::fromStdString(name));
++ }
++
++ QWindow *QOhosWebViewPrivate::nativeWindow() const
++ {
++ return m_webViewWindow.data();
++ }
++
++ void QOhosWebViewPrivate::stop()
++ {
++ m_webViewController->stop();
++
++ QMetaObject::invokeMethod(
++ this,
++ [this]() {
++ emitLoadingChangeSignalAndUpdateLoadStatus(
++ QWebViewFactory::LoadingInfo::create(QUrl(m_url), QWebViewLoadingInfo::LoadStatus::Stopped, QString()));
++ },
++ Qt::QueuedConnection);
++ }
++
++ void QOhosWebViewPrivate::onErrorReceived(const QString &url, const WebResourceError &error)
++ {
++ auto errorMessage =
++ QString(QLatin1String("Error %1: '%3'")).arg(error.errorCode()).arg(error.errorInfo().c_str());
++
++ if (m_url != url) {
++ qOhosWarning(QtForOhos)
++ << Q_FUNC_INFO << "Received onErrorReceive event on other url:" << url << errorMessage;
++ return;
++ }
++
++ qOhosCritical(QtForOhos) << Q_FUNC_INFO << "Received onErrorReceive for main page:" << errorMessage;
++
++ m_loadFailed = true;
++ emitLoadingChangeSignalAndUpdateLoadStatus(
++ QWebViewFactory::LoadingInfo::create(QUrl(m_url), QWebViewLoadingInfo::LoadStatus::Failed, errorMessage));
++ }
++
++ void QOhosWebViewPrivate::onPageBegan(const QString &url)
++ {
++ m_url = url;
++ m_loadFailed = false;
++
++ Q_EMIT q_ptr->urlChanged(QUrl(url));
++ emitLoadingChangeSignalAndUpdateLoadStatus(
++ QWebViewFactory::LoadingInfo::create(QUrl(url), QWebViewLoadingInfo::LoadStatus::Started, QString()));
++ onProgressChanged(0);
++ }
++
++ void QOhosWebViewPrivate::onPageEnded(const QString &url)
++ {
++ if (m_loadFailed) {
++ qOhosWarning(QtForOhos)
++ << Q_FUNC_INFO
++ << "Skipping LoadSucceededStatus; load already failed, url:" << url;
++ return;
++ }
++
++ if (m_url != url) {
++ qOhosWarning(QtForOhos) << Q_FUNC_INFO << "Received onPageEnd event from other url:" << url;
++ return;
++ }
++
++ emitLoadingChangeSignalAndUpdateLoadStatus(
++ QWebViewFactory::LoadingInfo::create(QUrl(url), QWebViewLoadingInfo::LoadStatus::Succeeded, QString()));
++ }
++
++ void QOhosWebViewPrivate::onProgressChanged(int progress)
++ {
++ if (m_loadProgress == progress) {
++ return;
++ }
++
++ m_loadProgress = progress;
++ Q_EMIT q_ptr->loadProgressChanged(progress);
++ }
++
++ void QOhosWebViewPrivate::onTitleUpdated(const QString &title, bool isRealTitle)
++ {
++ if (!isRealTitle) {
++ qOhosDebug(QtForOhos) << Q_FUNC_INFO << "Ignoring URL-generated title:" << title;
++ return;
++ }
++
++ if (m_title == title)
++ return;
++
++ m_title = title;
++ Q_EMIT q_ptr->titleChanged(title);
++ }
++
++ void QOhosWebViewPrivate::emitLoadingChangeSignalAndUpdateLoadStatus(
++ const QWebViewLoadingInfo &loadingInfo)
++ {
++ m_loadStatus = loadingInfo.status();
++ Q_EMIT q_ptr->loadingChanged(loadingInfo);
++ }
++
++ void QOhosWebViewPrivate::emitSyntheticLoadFailure(const QUrl &url, const QString &errorMessage)
++ {
++ QMetaObject::invokeMethod(
++ this,
++ [this, url, errorMessage]() {
++ m_url = url.toString();
++ Q_EMIT q_ptr->urlChanged(url);
++ emitLoadingChangeSignalAndUpdateLoadStatus(
++ QWebViewFactory::LoadingInfo::create(
++ url, QWebViewLoadingInfo::LoadStatus::Started, QString()));
++ emitLoadingChangeSignalAndUpdateLoadStatus(
++ QWebViewFactory::LoadingInfo::create(
++ url, QWebViewLoadingInfo::LoadStatus::Failed, errorMessage));
++ },
++ Qt::QueuedConnection);
++ }
++
++ QOhosWebComponentListenerImpl::QOhosWebComponentListenerImpl(QPointer<QOhosWebViewPrivate> webViewPrivate)
++ : QOhosWebComponentListener()
++ , m_webViewPrivate(webViewPrivate)
++ {
++ }
++
++ QOhosWebComponentListenerImpl::~QOhosWebComponentListenerImpl() = default;
++
++ void QOhosWebComponentListenerImpl::onErrorReceived(const std::string &url, const WebResourceError &error)
++ {
++ if (m_webViewPrivate)
++ m_webViewPrivate->onErrorReceived(QString::fromStdString(url), error);
++ }
++
++ void QOhosWebComponentListenerImpl::onPageBegan(const std::string &url)
++ {
++ if (m_webViewPrivate)
++ m_webViewPrivate->onPageBegan(QString::fromStdString(url));
++ }
++
++ void QOhosWebComponentListenerImpl::onPageEnded(const std::string &url)
++ {
++ if (m_webViewPrivate)
++ m_webViewPrivate->onPageEnded(QString::fromStdString(url));
++ }
++
++ void QOhosWebComponentListenerImpl::onProgressChanged(int progress)
++ {
++ if (m_webViewPrivate)
++ m_webViewPrivate->onProgressChanged(progress);
++ }
++
++ void QOhosWebComponentListenerImpl::onTitleReceived(const std::string &title, bool isRealTitle)
++ {
++ if (m_webViewPrivate)
++ m_webViewPrivate->onTitleUpdated(QString::fromStdString(title), isRealTitle);
++ }
++
++ }
++
++ QWebViewPrivate *makeOhosWebView(QWebView *view)
++ {
++ return new QOhosWebViewPrivate(view);
++ }
++
++ void prepareOhosWebView()
++ {
++ initializeOhosWebEngine();
++ }
++
++ QT_END_NAMESPACE
++
++ #include "qohoswebview.moc"