diff options
author | Martin Negyokru <[email protected]> | 2024-10-31 16:07:11 +0100 |
---|---|---|
committer | Martin Negyokru <[email protected]> | 2025-05-29 08:54:07 +0200 |
commit | 91f45c8e59bc4b9afd7b3a52d3c05e339a19ce71 (patch) | |
tree | a07418490848e27e46389d396fa628768000b671 | |
parent | 7b22a723e8515329dd334d35377f86e371f29c7d (diff) |
Introduce QWebEngineExtensionManager and QWebEngineExtensionInfo.
The manager has methods to load and install Chrome extensions from the filesystem.
QWebEngineExtensionInfo provides information about a loaded extension.
The current state of our js extension API support is very limited
meaning most of the extensions downloaded from Chrome extension store
won't work. Adding support for these APIs will be done in followup patches.
Fixes: QTBUG-118452
Task-number: QTBUG-61676
Change-Id: I017ad5e8d2ba963afbd2f31ac36fee9451a951bd
Reviewed-by: Allan Sandfeld Jensen <[email protected]>
37 files changed, 2002 insertions, 5 deletions
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index e885e16bc..8dff712b5 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -276,16 +276,21 @@ foreach(arch ${archs}) common/extensions/extensions_api_provider_qt.cpp common/extensions/extensions_api_provider_qt.h common/extensions/extensions_client_qt.cpp common/extensions/extensions_client_qt.h extensions/component_extension_resource_manager_qt.cpp extensions/component_extension_resource_manager_qt.h + extensions/extension_action_manager.cpp extensions/extension_action_manager.h extensions/extension_host_delegate_qt.cpp extensions/extension_host_delegate_qt.h extensions/extension_system_factory_qt.cpp extensions/extension_system_factory_qt.h extensions/extension_system_qt.cpp extensions/extension_system_qt.h extensions/extension_web_contents_observer_qt.cpp extensions/extension_web_contents_observer_qt.h extensions/extensions_api_client_qt.cpp extensions/extensions_api_client_qt.h extensions/extensions_browser_client_qt.cpp extensions/extensions_browser_client_qt.h + extensions/extension_manager.cpp extensions/extension_manager.h + extensions/extension_loader.cpp extensions/extension_loader.h + extensions/extension_installer.cpp extensions/extension_installer.h extensions/file_system_delegate_qt.cpp extensions/file_system_delegate_qt.h extensions/messaging_delegate_qt.cpp extensions/messaging_delegate_qt.h extensions/mime_handler_view_guest_delegate_qt.cpp extensions/mime_handler_view_guest_delegate_qt.h extensions/plugin_service_filter_qt.cpp extensions/plugin_service_filter_qt.h + extensions/unpacked_extension_installer.h extensions/unpacked_extension_installer.cpp net/plugin_response_interceptor_url_loader_throttle.cpp net/plugin_response_interceptor_url_loader_throttle.h renderer/extensions/extensions_renderer_api_provider_qt.cpp renderer/extensions/extensions_renderer_api_provider_qt.h renderer/extensions/extensions_renderer_client_qt.cpp renderer/extensions/extensions_renderer_client_qt.h diff --git a/src/core/api/CMakeLists.txt b/src/core/api/CMakeLists.txt index 34a467d16..b13aa0260 100644 --- a/src/core/api/CMakeLists.txt +++ b/src/core/api/CMakeLists.txt @@ -17,6 +17,8 @@ qt_internal_add_module(WebEngineCore qwebenginecookiestore.cpp qwebenginecookiestore.h qwebenginecookiestore_p.h qwebenginedesktopmediarequest.cpp qwebenginedesktopmediarequest.h qwebenginedesktopmediarequest_p.h qwebenginedownloadrequest.cpp qwebenginedownloadrequest.h qwebenginedownloadrequest_p.h + qwebengineextensioninfo.cpp qwebengineextensioninfo.h qwebengineextensioninfo_p.h + qwebengineextensionmanager.cpp qwebengineextensionmanager.h qwebenginefilesystemaccessrequest.cpp qwebenginefilesystemaccessrequest.h qwebenginefindtextresult.cpp qwebenginefindtextresult.h qwebengineframe.cpp qwebengineframe.h diff --git a/src/core/api/qwebengineextensioninfo.cpp b/src/core/api/qwebengineextensioninfo.cpp new file mode 100644 index 000000000..33af3d3cc --- /dev/null +++ b/src/core/api/qwebengineextensioninfo.cpp @@ -0,0 +1,230 @@ +// 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 "qwebengineextensioninfo.h" +#include "qwebengineextensioninfo_p.h" + +#if QT_CONFIG(webengine_extensions) +#include <QtCore/qfileinfo.h> + +#include "extensions/extension_manager.h" + +using namespace Qt::StringLiterals; + +QT_BEGIN_NAMESPACE +QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QWebEngineExtensionInfoPrivate) + +/*! + \class QWebEngineExtensionInfo + \brief The QWebEngineExtensionInfo provides information about a browser extension. + + \since 6.10 + \inmodule QtWebEngineCore + + QWebEngineExtensionInfo provides information of an extension loaded into \QWE. + Extensions can be loaded via the \l QWebEngineExtensionManager. + You can check if the extension was successfully loaded using its \l isLoaded() property. + The \l error() property contains error messages if the loading process failed. + Extensions are always loaded in disabled state. To run an extension, it has to be enabled via + \l QWebEngineExtensionManager::setExtensionEnabled(). + + An extension can be removed using \l QWebEngineExtensionManager::unloadExtension(). + + You can access extensions with \l QWebEngineExtensionManager::extensions() which provides a list + of the loaded extensions, or connect to the manager's signals to be notified if the loading + process is complete. + + \sa QWebEngineExtensionManager, QWebEngineProfile::extensionManager() +*/ + +QWebEngineExtensionInfoPrivate::QWebEngineExtensionInfoPrivate( + const ExtensionData &data, QtWebEngineCore::ExtensionManager *manager) + : QSharedData(), m_data(data), m_manager(manager) +{ +} + +QWebEngineExtensionInfoPrivate::~QWebEngineExtensionInfoPrivate() = default; + +QString QWebEngineExtensionInfoPrivate::name() const +{ + return m_data.name; +} + +std::string QWebEngineExtensionInfoPrivate::id() const +{ + return m_data.id; +} + +QString QWebEngineExtensionInfoPrivate::description() const +{ + return m_data.description; +} + +QString QWebEngineExtensionInfoPrivate::path() const +{ + return m_data.path; +} + +QString QWebEngineExtensionInfoPrivate::error() const +{ + return m_data.error; +} + +QUrl QWebEngineExtensionInfoPrivate::actionPopupUrl() const +{ + return m_data.actionPopupUrl; +} + +bool QWebEngineExtensionInfoPrivate::isEnabled() const +{ + return m_manager->isExtensionEnabled(id()); +} + +bool QWebEngineExtensionInfoPrivate::isLoaded() const +{ + return m_manager->isExtensionLoaded(id()); +} + +bool QWebEngineExtensionInfoPrivate::isInstalled() const +{ + return QFileInfo(m_data.path).path() == m_manager->installDirectory(); +} + +QWebEngineExtensionInfo::QWebEngineExtensionInfo() : d_ptr(nullptr) { } + +QWebEngineExtensionInfo::QWebEngineExtensionInfo(QWebEngineExtensionInfoPrivate *d) : d_ptr(d) { } + +QWebEngineExtensionInfo::QWebEngineExtensionInfo(const QWebEngineExtensionInfo &other) noexcept = + default; +QWebEngineExtensionInfo::QWebEngineExtensionInfo(QWebEngineExtensionInfo &&other) noexcept = + default; +QWebEngineExtensionInfo & +QWebEngineExtensionInfo::operator=(const QWebEngineExtensionInfo &other) noexcept = default; + +QWebEngineExtensionInfo::~QWebEngineExtensionInfo() = default; + +/*! + \property QWebEngineExtensionInfo::name + \brief The name of the extension. + + Acquired from the extension's manifest file's name property. + + Empty if the load failed. +*/ +QString QWebEngineExtensionInfo::name() const +{ + return d_ptr ? d_ptr->name() : ""_L1; +} + +/*! + \property QWebEngineExtensionInfo::id + \brief The id of the extension. + + Generated at load time. Multiple QWebEngineExtensionInfo objects with the same id + represent the same underlying extension. + + The id is generated from the filesystem path where the extension was loaded from + and the extensions manfiest file. Loading the same extension from the same path + always have the same id. + + Empty if the load failed. +*/ +QString QWebEngineExtensionInfo::id() const +{ + return d_ptr ? QString::fromStdString(d_ptr->id()) : ""_L1; +} + +/*! + \property QWebEngineExtensionInfo::name + \brief The description of the extension. + + Acquired from the extension's manifest file's description property. + + Empty if the load failed. +*/ +QString QWebEngineExtensionInfo::description() const +{ + return d_ptr ? d_ptr->description() : ""_L1; +} + +/*! + \property QWebEngineExtensionInfo::path + \brief The install path of the extension. + + The filesystem path where the extension was loaded from. +*/ +QString QWebEngineExtensionInfo::path() const +{ + return d_ptr ? d_ptr->path() : ""_L1; +} + +/*! + \property QWebEngineExtensionInfo::error + \brief Errors happened during loading, installing or uninstalling the extension. + + Multiple errors can happen during load time, like missing manifest, invalid file format + or path. The loading process stops at the first error. + + Empty if the load succeeded. +*/ +QString QWebEngineExtensionInfo::error() const +{ + return d_ptr ? d_ptr->error() : ""_L1; +} + +/*! + \property QWebEngineExtensionInfo::actionPopupUrl + \brief Returns the url of the extension's popup. + + Extension developers usually provide a popup menu where users can control + their extension. The menu can be accessed via this url. + + Empty if the load failed. +*/ +QUrl QWebEngineExtensionInfo::actionPopupUrl() const +{ + return d_ptr ? d_ptr->actionPopupUrl() : QUrl(""_L1); +} + +/*! + \property QWebEngineExtensionInfo::isEnabled + \brief This property holds whether the extension is enabled. + + \sa QWebEngineExtensionManager::setExtensionEnabled() +*/ +bool QWebEngineExtensionInfo::isEnabled() const +{ + return d_ptr && d_ptr->isEnabled(); +} + +/*! + \property QWebEngineExtensionInfo::isLoaded + \brief This property holds whether the extension is loaded. + + If the extension was loaded or installed successfully this property returns \c true. + Returns false if the extension was unloaded, uninstalled or the loading process failed. + + \sa QWebEngineExtensionManager::loadExtension(), QWebEngineExtensionManager::unloadExtension() +*/ + +bool QWebEngineExtensionInfo::isLoaded() const +{ + return d_ptr && d_ptr->isLoaded(); +} + +/* + \property QWebEngineExtensionInfo::isInstalled + \brief This property holds whether the extension is installed in the profile's install + directory. + + \sa QWebEngineExtensionManager::installDirectory(), + QWebEngineExtensionManager::installExtension(), QWebEngineExtensionManager::uninstallExtension() +*/ +bool QWebEngineExtensionInfo::isInstalled() const +{ + return d_ptr && d_ptr->isInstalled(); +} + +QT_END_NAMESPACE + +#endif // QT_CONFIG(webengine_extensions) diff --git a/src/core/api/qwebengineextensioninfo.h b/src/core/api/qwebengineextensioninfo.h new file mode 100644 index 000000000..3d4c1996d --- /dev/null +++ b/src/core/api/qwebengineextensioninfo.h @@ -0,0 +1,78 @@ +// 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 + +#ifndef QWEBENGINEEXTENSION_H_ +#define QWEBENGINEEXTENSION_H_ + +#include <QtWebEngineCore/qtwebenginecoreglobal.h> + +#if QT_CONFIG(webengine_extensions) +#include <QtCore/qshareddata.h> +#include <QtCore/qstring.h> +#include <QtCore/qurl.h> +#include <QtQml/qqmlregistration.h> + +namespace QtWebEngineCore { +class ExtensionManager; +} + +QT_BEGIN_NAMESPACE +class QWebEngineExtensionInfoPrivate; +class QWebEngineExtensionManager; +QT_DECLARE_QESDP_SPECIALIZATION_DTOR(QWebEngineExtensionInfoPrivate) + +class QWebEngineExtensionInfo +{ + Q_GADGET_EXPORT(Q_WEBENGINECORE_EXPORT) + Q_PROPERTY(QString name READ name FINAL) + Q_PROPERTY(QString id READ id FINAL) + Q_PROPERTY(QString description READ description FINAL) + Q_PROPERTY(QString path READ path FINAL) + Q_PROPERTY(QString error READ error FINAL) + Q_PROPERTY(QUrl actionPopupUrl READ actionPopupUrl FINAL) + Q_PROPERTY(bool isEnabled READ isEnabled FINAL) + Q_PROPERTY(bool isLoaded READ isLoaded FINAL) + Q_PROPERTY(bool isInstalled READ isInstalled FINAL) + +public: + QML_VALUE_TYPE(webEngineExtension) + QML_ADDED_IN_VERSION(6, 10) + + Q_WEBENGINECORE_EXPORT QWebEngineExtensionInfo(); + + Q_WEBENGINECORE_EXPORT + QWebEngineExtensionInfo(const QWebEngineExtensionInfo &other) noexcept; + Q_WEBENGINECORE_EXPORT + QWebEngineExtensionInfo(QWebEngineExtensionInfo &&other) noexcept; + Q_WEBENGINECORE_EXPORT + QWebEngineExtensionInfo &operator=(const QWebEngineExtensionInfo &other) noexcept; + Q_WEBENGINECORE_EXPORT ~QWebEngineExtensionInfo(); + + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QWebEngineExtensionInfo) + void swap(QWebEngineExtensionInfo &other) noexcept { d_ptr.swap(other.d_ptr); } + + Q_WEBENGINECORE_EXPORT QString name() const; + Q_WEBENGINECORE_EXPORT QString id() const; + Q_WEBENGINECORE_EXPORT QString description() const; + Q_WEBENGINECORE_EXPORT QString path() const; + Q_WEBENGINECORE_EXPORT QString error() const; + Q_WEBENGINECORE_EXPORT QUrl actionPopupUrl() const; + Q_WEBENGINECORE_EXPORT bool isEnabled() const; + Q_WEBENGINECORE_EXPORT bool isLoaded() const; + Q_WEBENGINECORE_EXPORT bool isInstalled() const; + +private: + friend class QtWebEngineCore::ExtensionManager; + friend class QWebEngineExtensionManager; + + Q_WEBENGINECORE_EXPORT + QWebEngineExtensionInfo(QWebEngineExtensionInfoPrivate *d); + + QExplicitlySharedDataPointer<QWebEngineExtensionInfoPrivate> d_ptr; +}; + +Q_DECLARE_SHARED(QWebEngineExtensionInfo) +QT_END_NAMESPACE + +#endif // QT_CONFIG(webengine_extensions) +#endif // QWEBENGINEEXTENSION_H_ diff --git a/src/core/api/qwebengineextensioninfo_p.h b/src/core/api/qwebengineextensioninfo_p.h new file mode 100644 index 000000000..79f505cf8 --- /dev/null +++ b/src/core/api/qwebengineextensioninfo_p.h @@ -0,0 +1,65 @@ +// 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 + +#ifndef QWEBENGINEEXTENSION_P_H_ +#define QWEBENGINEEXTENSION_P_H_ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtWebEngineCore/qtwebenginecoreglobal.h> + +#if QT_CONFIG(webengine_extensions) +#include <QtCore/qsharedpointer.h> +#include <QtCore/qstring.h> +#include <QtCore/qurl.h> + +namespace QtWebEngineCore { +class ExtensionManager; +} + +QT_BEGIN_NAMESPACE + +class QWebEngineExtensionInfoPrivate : public QSharedData +{ +public: + struct ExtensionData + { + std::string id; + QString name; + QString description; + QString path; + QString error; + QUrl actionPopupUrl; + }; + + QWebEngineExtensionInfoPrivate(const ExtensionData &data, + QtWebEngineCore::ExtensionManager *manager); + ~QWebEngineExtensionInfoPrivate(); + std::string id() const; + QString name() const; + QString description() const; + QString path() const; + QString error() const; + QUrl actionPopupUrl() const; + bool isEnabled() const; + bool isLoaded() const; + bool isInstalled() const; + +private: + ExtensionData m_data; + QtWebEngineCore::ExtensionManager *m_manager; +}; + +QT_END_NAMESPACE + +#endif // QT_CONFIG(webengine_extensions) +#endif // QWEBENGINEEXTENSION_P_H_ diff --git a/src/core/api/qwebengineextensionmanager.cpp b/src/core/api/qwebengineextensionmanager.cpp new file mode 100644 index 000000000..0d27482dd --- /dev/null +++ b/src/core/api/qwebengineextensionmanager.cpp @@ -0,0 +1,166 @@ +// 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 "qwebengineextensionmanager.h" + +#if QT_CONFIG(webengine_extensions) +#include "qwebengineextensioninfo_p.h" +#include "extensions/extension_manager.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QWebEngineExtensionManager + \brief The QWebEngineExtensionManager class allows applications to install and load Chrome + extensions from the filesystem. + + \since 6.10 + \inmodule QtWebEngineCore + + QWebEngineExtensionManager can load or install Chrome extensions. + Extensions can be loaded via \l loadExtension. Extensions loaded this way are not + remembered by the associated profile and has to be manually loaded in every new browsing + session. To preserve extensions between browsing sessions, applications can install zipped or + unpacked extensions via \l installExtension. In this case the manager will unpack the extension + to the profile's directory and load it from there. Installed extensions are always loaded at + startup, after the profile is initialized. + + You can access the loaded extensions with \l extensions() which provides a list of \l + QWebEngineExtensionInfo, or connect to the manager's signals to get notified about the state of + the load or install processes. + + Each \l QWebEngineProfile has its own \l QWebEngineExtensionManager, so every page that shares + the same profile will share the same extensions too. + Extensions can't be loaded into off-the-record profiles. + + \note Only ManifestV3 extensions are supported, other versions won't be loaded nor installed + + \sa QWebEngineProfile::extensionManager, QWebEngineExtensionInfo +*/ + +QWebEngineExtensionManager::QWebEngineExtensionManager(QtWebEngineCore::ExtensionManager *d) + : d_ptr(d) +{ + d->q_ptr = this; +} + +QWebEngineExtensionManager::~QWebEngineExtensionManager() { } + +/*! + Loads an unpacked extension from \a path + + The \l QWebEngineExtensionManager::extensionLoadFinished signal is emitted when an extension + is loaded or the load failed. If the load succeeded \l QWebEngineExtensionInfo::isLoaded() will + return true otherwise \l QWebEngineExtensionInfo::error() will contain information where the + loading process failed. + + Extensions are always loaded in disabled state, users have to enable them manually. + Loading an already loaded extension from the same path will reload the extension. + + \sa QWebEngineExtensionInfo::isLoaded(), QWebEngineExtensionInfo::error() +*/ +void QWebEngineExtensionManager::loadExtension(const QString &path) +{ + d_ptr->loadExtension(path); +} + +/*! + Installs an extension from \a path to the profile's directory and loads it + + The \l QWebEngineExtensionManager::extensionInstallFinished signal is emitted after an + extension is installed or the install failed. If the install succeeded \l + QWebEngineExtensionInfo::isInstalled() will return true, otherwise \l + QWebEngineExtensionInfo::error() will contain information how the install process failed. + + Extensions are loaded in disabled state after the install succeded. + Installed extensions are automatically loaded at every starutup in disabled state. + The install directory can be queried with \l installDirectory(). + + The installer is capable of installing zipped or unpacked extensions. + The \a path parameter should point to a directory or a zip file containing the extension's + manifest file. If the manifest is missing from the top level directory, the install process will + abort. + + Installing an already loaded or installed extension from the same path will install a new + extension. + + \sa QWebEngineExtensionInfo::isInstalled(), QWebEngineExtensionInfo::error(), installDirectory() +*/ +void QWebEngineExtensionManager::installExtension(const QString &path) +{ + d_ptr->installExtension(path); +} + +/*! + Unloads the \a extension + + Removes all the extension's data from memory. + + The \l QWebEngineExtensionManager::extensionUnloadFinished signal is emitted after the unload + process finished. + + \note It is also possible to unload internal extensions like Hangouts and PDF, + but they will be loaded at next startup like other installed extensions. + + \sa QWebEngineExtensionInfo::isLoaded() +*/ +void QWebEngineExtensionManager::unloadExtension(const QWebEngineExtensionInfo &extension) +{ + d_ptr->unloadExtension(extension.d_ptr->id()); +} + +/*! + Uninstalls the \a extension + + Removes the extension's files from the install directory and unloads + the extension. + The \l QWebEngineExtensionManager::extensionUninstallFinished signal is emitted + after the process finished. + + \sa QWebEngineExtensionManager::installDirectory(), QWebEngineExtensionInfo::isInstalled(), + QWebEngineExtensionInfo::error() +*/ +void QWebEngineExtensionManager::uninstallExtension(const QWebEngineExtensionInfo &extension) +{ + d_ptr->uninstallExtension(extension.d_ptr->id()); +} + +/*! + Allows to turn on/off the \a extension at runtime + + The \a enabled argument determines whether the extension should be enabled or disabled. + \note It is also possible to disable internal extensions like Hangouts and PDF. + + \sa QWebEngineExtensionInfo::isEnabled() +*/ +void QWebEngineExtensionManager::setExtensionEnabled(const QWebEngineExtensionInfo &extension, + bool enabled) +{ + d_ptr->setExtensionEnabled(extension.d_ptr->id(), enabled); +} + +/*! + \property QWebEngineExtensionManager::installDirectory + \brief Returns the directory's path where the extensions are installed. + + \sa installExtension(), QWebEngineExtensionInfo::isInstalled() +*/ + +QString QWebEngineExtensionManager::installDirectory() +{ + return d_ptr->installDirectory(); +} + +/*! + \property QWebEngineExtensionManager::extensions + \brief Returns a list of the loaded extensions. + + \sa QWebEngineExtensionInfo +*/ +QList<QWebEngineExtensionInfo> QWebEngineExtensionManager::extensions() +{ + return d_ptr->extensions(); +} + +QT_END_NAMESPACE +#endif // QT_CONFIG(webengine_extensions) diff --git a/src/core/api/qwebengineextensionmanager.h b/src/core/api/qwebengineextensionmanager.h new file mode 100644 index 000000000..4bff8c304 --- /dev/null +++ b/src/core/api/qwebengineextensionmanager.h @@ -0,0 +1,60 @@ +// 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 + +#ifndef QWEBENGINEEXTENSIONMANAGER_H_ +#define QWEBENGINEEXTENSIONMANAGER_H_ + +#include <QtWebEngineCore/qtwebenginecoreglobal.h> + +#if QT_CONFIG(webengine_extensions) + +#include <QtCore/qlist.h> +#include <QtCore/qstring.h> +#include <QtCore/qobject.h> +#include <QtWebEngineCore/qwebengineextensioninfo.h> + +namespace QtWebEngineCore { +class ExtensionManager; +class ProfileAdapter; +} + +QT_BEGIN_NAMESPACE + +class Q_WEBENGINECORE_EXPORT QWebEngineExtensionManager : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString installDirectory READ installDirectory FINAL) + Q_PROPERTY(QList<QWebEngineExtensionInfo> extensions READ extensions FINAL) +public: + QML_NAMED_ELEMENT(WebEngineExtensionManager) + QML_UNCREATABLE("") + QML_ADDED_IN_VERSION(6, 10) + + ~QWebEngineExtensionManager() override; + Q_INVOKABLE void loadExtension(const QString &path); + Q_INVOKABLE void installExtension(const QString &path); + Q_INVOKABLE void unloadExtension(const QWebEngineExtensionInfo &extension); + Q_INVOKABLE void uninstallExtension(const QWebEngineExtensionInfo &extension); + Q_INVOKABLE void setExtensionEnabled(const QWebEngineExtensionInfo &extension, bool enabled); + + QString installDirectory(); + QList<QWebEngineExtensionInfo> extensions(); + +Q_SIGNALS: + void extensionLoadFinished(const QWebEngineExtensionInfo &extension); + void extensionInstallFinished(const QWebEngineExtensionInfo &extension); + void extensionUnloadFinished(const QWebEngineExtensionInfo &extension); + void extensionUninstallFinished(const QWebEngineExtensionInfo &extension); + +private: + friend class QtWebEngineCore::ProfileAdapter; + Q_DISABLE_COPY(QWebEngineExtensionManager) + + QWebEngineExtensionManager(QtWebEngineCore::ExtensionManager *d); + QtWebEngineCore::ExtensionManager *d_ptr; +}; + +QT_END_NAMESPACE + +#endif // QT_CONFIG(webengine_extensions) +#endif // QWEBENGINEEXTENSIONMANAGER_H_ diff --git a/src/core/api/qwebengineprofile.cpp b/src/core/api/qwebengineprofile.cpp index ceeff460a..6683d51c1 100644 --- a/src/core/api/qwebengineprofile.cpp +++ b/src/core/api/qwebengineprofile.cpp @@ -7,6 +7,7 @@ #include "qwebenginecookiestore.h" #include "qwebenginedownloadrequest.h" #include "qwebenginedownloadrequest_p.h" +#include "qwebengineextensionmanager.h" #include "qwebenginenotification.h" #include "qwebenginesettings.h" #include "qwebenginescriptcollection.h" @@ -1094,6 +1095,22 @@ QWebEngineClientHints *QWebEngineProfile::clientHints() const return d->m_clientHints.data(); } +/*! + Returns the extension manager associated with this browsing context. + + \since 6.10 + \sa QWebEngineExtensionManager +*/ +QWebEngineExtensionManager *QWebEngineProfile::extensionManager() +{ +#if QT_CONFIG(webengine_extensions) + Q_D(QWebEngineProfile); + return d->profileAdapter()->extensionManager(); +#else + return nullptr; +#endif +} + QT_END_NAMESPACE #include "moc_qwebengineprofile.cpp" diff --git a/src/core/api/qwebengineprofile.h b/src/core/api/qwebengineprofile.h index a596c8e46..2688dcc61 100644 --- a/src/core/api/qwebengineprofile.h +++ b/src/core/api/qwebengineprofile.h @@ -22,6 +22,7 @@ class QWebEngineClientCertificateStore; class QWebEngineClientHints; class QWebEngineCookieStore; class QWebEngineDownloadRequest; +class QWebEngineExtensionManager; class QWebEngineNotification; class QWebEngineProfilePrivate; class QWebEngineSettings; @@ -128,6 +129,8 @@ public: QList<QWebEnginePermission> listPermissionsForOrigin(const QUrl &securityOrigin) const; QList<QWebEnginePermission> listPermissionsForPermissionType(QWebEnginePermission::PermissionType permissionType) const; + QWebEngineExtensionManager *extensionManager(); + static QWebEngineProfile *defaultProfile(); Q_SIGNALS: diff --git a/src/core/configure/BUILD.root.gn.in b/src/core/configure/BUILD.root.gn.in index bd4a31843..9ece7cd56 100644 --- a/src/core/configure/BUILD.root.gn.in +++ b/src/core/configure/BUILD.root.gn.in @@ -451,6 +451,10 @@ source_set("qtwebengine_sources") { "//chrome/common/extensions/permissions/chrome_permission_message_provider.h", "//chrome/common/extensions/permissions/chrome_permission_message_rules.cc", "//chrome/common/extensions/permissions/chrome_permission_message_rules.h", + "//extensions/browser/zipfile_installer.cc", + "//extensions/browser/zipfile_installer.h", + "//components/services/unzip/unzipper_impl.cc", + "//components/services/unzip/unzipper_impl.h", ] } else { sources += [ diff --git a/src/core/content_utility_client_qt.cpp b/src/core/content_utility_client_qt.cpp index 7af90c7a1..849e7a5e2 100644 --- a/src/core/content_utility_client_qt.cpp +++ b/src/core/content_utility_client_qt.cpp @@ -3,6 +3,7 @@ #include "content_utility_client_qt.h" +#include <QtWebEngineCore/qtwebenginecoreglobal.h> #include "mojo/public/cpp/bindings/service_factory.h" #include "services/proxy_resolver/proxy_resolver_factory_impl.h" @@ -11,6 +12,11 @@ #include "services/proxy_resolver_win/windows_system_proxy_resolver_impl.h" #endif +#if QT_CONFIG(webengine_extensions) +#include "components/services/unzip/public/mojom/unzipper.mojom.h" +#include "components/services/unzip/unzipper_impl.h" +#endif + namespace QtWebEngineCore { ContentUtilityClientQt::ContentUtilityClientQt() @@ -41,4 +47,18 @@ void ContentUtilityClientQt::RegisterIOThreadServices(mojo::ServiceFactory &serv #endif } +#if QT_CONFIG(webengine_extensions) +auto RunUnzipper(mojo::PendingReceiver<unzip::mojom::Unzipper> receiver) +{ + return std::make_unique<unzip::UnzipperImpl>(std::move(receiver)); +} +#endif + +void ContentUtilityClientQt::RegisterMainThreadServices(mojo::ServiceFactory &services) +{ +#if QT_CONFIG(webengine_extensions) + services.Add(RunUnzipper); +#endif +} + } // namespace diff --git a/src/core/content_utility_client_qt.h b/src/core/content_utility_client_qt.h index f2d287cf5..8db06cdd4 100644 --- a/src/core/content_utility_client_qt.h +++ b/src/core/content_utility_client_qt.h @@ -18,6 +18,7 @@ public: // content::ContentUtilityClient: void RegisterIOThreadServices(mojo::ServiceFactory &services) override; + void RegisterMainThreadServices(mojo::ServiceFactory &services) override; }; } // namespace diff --git a/src/core/extensions/extension_action_manager.cpp b/src/core/extensions/extension_action_manager.cpp new file mode 100644 index 000000000..30708935b --- /dev/null +++ b/src/core/extensions/extension_action_manager.cpp @@ -0,0 +1,35 @@ +// 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 "extension_action_manager.h" + +#include "extensions/common/extension.h" + +using namespace extensions; + +namespace QtWebEngineCore { +void ExtensionActionManager::removeExtensionAction(const std::string &id) +{ + m_actions.erase(id); +} + +ExtensionAction *ExtensionActionManager::getExtensionAction(const Extension *extension) +{ + if (!extension) + return nullptr; + // Todo: adapt and use "extensions/browser/extension_action_manager.h" instead + // which is used by some of the js extension apis. + auto iter = m_actions.find(extension->id()); + if (iter != m_actions.end()) + return iter->second.get(); + + const ActionInfo *action_info = ActionInfo::GetExtensionActionInfo(extension); + if (!action_info) + return nullptr; + + auto action = std::make_unique<ExtensionAction>(*extension, *action_info); + ExtensionAction *raw_action = action.get(); + m_actions[extension->id()] = std::move(action); + return raw_action; +} +} // namespace QtWebEngineCore diff --git a/src/core/extensions/extension_action_manager.h b/src/core/extensions/extension_action_manager.h new file mode 100644 index 000000000..06c8f4cd2 --- /dev/null +++ b/src/core/extensions/extension_action_manager.h @@ -0,0 +1,30 @@ +// 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 + +#ifndef EXTENSION_ACTION_MANAGER_H_ +#define EXTENSION_ACTION_MANAGER_H_ + +#include <map> +#include <memory> +#include <string> + +#include "extensions/browser/extension_action.h" + +namespace extensions { +class Extension; +} +namespace QtWebEngineCore { +class ExtensionActionManager +{ +public: + ExtensionActionManager() = default; + void removeExtensionAction(const std::string &id); + extensions::ExtensionAction *getExtensionAction(const extensions::Extension *extension); + +private: + using ExtensionActionMap = std::map<std::string, std::unique_ptr<extensions::ExtensionAction>>; + ExtensionActionMap m_actions{}; +}; +} + +#endif // EXTENSION_ACTION_MANAGER_H_ diff --git a/src/core/extensions/extension_installer.cpp b/src/core/extensions/extension_installer.cpp new file mode 100644 index 000000000..81c57f6ab --- /dev/null +++ b/src/core/extensions/extension_installer.cpp @@ -0,0 +1,192 @@ +// 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 "extension_installer.h" + +#include "extension_manager.h" +#include "type_conversion.h" + +#include "base/files/file_util.h" +#include "content/public/browser/browser_context.h" +#include "extensions/browser/extension_file_task_runner.h" +#include "extensions/browser/zipfile_installer.h" +#include "extensions/common/constants.h" +#include "unpacked_extension_installer.h" + +using namespace extensions; + +namespace QtWebEngineCore { +namespace { +bool uninstallExtensionOnFileThread(const base::FilePath &dirToDelete, + const base::FilePath &profileDir, + const base::FilePath &extensionInstallDir) +{ + // The below conditions are asserting that we should only be deleting + // directories that are inside the `extensionInstallDir` which should be + // inside the profile directory. Anything outside of that would be considered + // invalid and dangerous since this is effectively an `rm -rf + // <extension_delete_path>`. + + if (!base::DirectoryExists(dirToDelete)) { + return false; + } + + // Confirm that all the directories involved are not empty and are absolute so + // that the subsequent comparisons have some value. + if (profileDir.empty() || extensionInstallDir.empty() || dirToDelete.empty() + || !profileDir.IsAbsolute() || !extensionInstallDir.IsAbsolute() + || !dirToDelete.IsAbsolute()) { + return false; + } + + // Confirm the directory where we install extensions is a direct subdir of the + // profile dir. + if (extensionInstallDir.DirName() != profileDir) + return false; + + // Confirm the directory we are obliterating is a direct subdir of the + // extensions install directory. + if (dirToDelete.DirName() != extensionInstallDir) + return false; + + // In POSIX environment and if `dirToDelete` is a symbolic link, this deletes only + // the symlink. (even if the symlink points to a non-existent file) + return base::DeletePathRecursively(dirToDelete); +} + +bool cleanupBrokenInstallOnFileThread(const base::FilePath &dirToDelete, + const base::FilePath &profileDir, + const base::FilePath &extensionInstallDir) +{ + if (base::DirectoryExists(dirToDelete)) + return uninstallExtensionOnFileThread(dirToDelete, profileDir, extensionInstallDir); + return true; +} + +ExtensionInstaller::ExtensionFormat getExtensionFormatOnFileThread(const base::FilePath &path) +{ + if (!base::PathExists(path)) + return ExtensionInstaller::ExtensionFormat::Invalid; + if (path.MatchesExtension(FILE_PATH_LITERAL(".zip"))) + return ExtensionInstaller::ExtensionFormat::Zip; + if (base::DirectoryExists(path)) + return ExtensionInstaller::ExtensionFormat::Unpacked; + return ExtensionInstaller::ExtensionFormat::Invalid; +} +} // namespace + +ExtensionInstaller::ExtensionInstaller(content::BrowserContext *context, ExtensionManager *manager) + : m_browserContext(context), m_manager(manager) +{ +} + +void ExtensionInstaller::installExtension(const base::FilePath &path) +{ + if (m_browserContext->IsOffTheRecord()) { + m_manager->onExtensionInstallError(toQt(path), "Cannot install in off-the-record mode"); + return; + } + + GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult( + FROM_HERE, base::BindOnce(&getExtensionFormatOnFileThread, path), + base::BindOnce(&ExtensionInstaller::installExtensionInternal, + m_weakFactory.GetWeakPtr(), path)); +} + +void ExtensionInstaller::installExtensionInternal(const base::FilePath &path, + ExtensionFormat format) +{ + switch (format) { + case ExtensionFormat::Zip: + ZipFileInstaller::Create( + GetExtensionFileTaskRunner(), + base::BindOnce(&ExtensionInstaller::installDone, m_weakFactory.GetWeakPtr())) + ->InstallZipFileToUnpackedExtensionsDir(path, installDirectory()); + break; + case ExtensionFormat::Unpacked: + UnpackedExtensionInstaller::Create( + GetExtensionFileTaskRunner(), + base::BindOnce(&ExtensionInstaller::installDone, m_weakFactory.GetWeakPtr())) + ->install(path, installDirectory()); + break; + case ExtensionFormat::Invalid: + default: + m_manager->onExtensionInstallError(toQt(path), "Invalid file format"); + } +} + +void ExtensionInstaller::installDone(const base::FilePath &source, const base::FilePath &installDir, + const std::string &error) +{ + if (!error.empty()) { + cleanupBrokenInstall(installDir, error); + m_manager->onExtensionInstallError(toQt(source), error); + return; + } + + GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult( + FROM_HERE, base::BindOnce(&ExtensionLoader::loadExtensionOnFileThread, installDir), + base::BindOnce(&ExtensionInstaller::loadFinished, m_weakFactory.GetWeakPtr(), source)); +} + +void ExtensionInstaller::loadFinished(const base::FilePath &source, + const ExtensionLoader::LoadingInfo &loadingInfo) +{ + auto error = loadingInfo.error; + if (!error.empty()) { + auto install_path = loadingInfo.path; + cleanupBrokenInstall(install_path, error); + m_manager->onExtensionInstallError(toQt(source), error); + return; + } + + auto extension = loadingInfo.extension; + m_manager->onExtensionInstalled(extension.get()); +} + +void ExtensionInstaller::uninstallExtension(scoped_refptr<const Extension> extension) +{ + GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult( + FROM_HERE, + base::BindOnce(&uninstallExtensionOnFileThread, extension->path(), + m_browserContext->GetPath(), installDirectory()), + base::BindOnce(&ExtensionInstaller::uninstallFinished, m_weakFactory.GetWeakPtr(), + extension->id())); +} + +void ExtensionInstaller::uninstallFinished(const std::string &id, bool success) +{ + if (!success) { + m_manager->onExtensionUninstallError(id, "Invalid install directory"); + return; + } + + m_manager->onExtensionUninstalled(id); +} + +base::FilePath ExtensionInstaller::installDirectory() const +{ + return m_browserContext->GetPath().AppendASCII(extensions::kInstallDirectoryName); +} + +void ExtensionInstaller::cleanupBrokenInstall(const base::FilePath &dirToDelete, + const std::string &error) +{ + GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult( + FROM_HERE, + base::BindOnce(&cleanupBrokenInstallOnFileThread, dirToDelete, + m_browserContext->GetPath(), installDirectory()), + base::BindOnce(&ExtensionInstaller::onInstallFailure, m_weakFactory.GetWeakPtr(), + dirToDelete, error)); +} + +void ExtensionInstaller::onInstallFailure(const base::FilePath &brokenInstallDir, + const std::string &error, bool cleanupSucceded) +{ + + if (!cleanupSucceded) + qWarning("Failed to clean up broken extension install in %ls", + qUtf16Printable(toQt(brokenInstallDir))); +} + +} // namespace QtWebEngineCore diff --git a/src/core/extensions/extension_installer.h b/src/core/extensions/extension_installer.h new file mode 100644 index 000000000..02c26c9ca --- /dev/null +++ b/src/core/extensions/extension_installer.h @@ -0,0 +1,60 @@ +// 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 + +#ifndef EXTENSION_INSTALLER_H_ +#define EXTENSION_INSTALLER_H_ + +#include "extension_loader.h" + +#include "api/qwebengineextensioninfo.h" + +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" + +namespace content { +class BrowserContext; +} + +namespace QtWebEngineCore { +class ExtensionManager; +} + +namespace QtWebEngineCore { +class ExtensionInstaller +{ +public: + ExtensionInstaller(content::BrowserContext *context, ExtensionManager *manager); + ~ExtensionInstaller() { } + + enum class ExtensionFormat { + Invalid, + Zip, + Unpacked, + }; + + void installExtension(const base::FilePath &path); + void uninstallExtension(scoped_refptr<const extensions::Extension> extension); + + base::FilePath installDirectory() const; + +private: + void installExtensionInternal(const base::FilePath &path, ExtensionFormat format); + void installDone(const base::FilePath &source, const base::FilePath &installDir, + const std::string &error); + void loadFinished(const base::FilePath &source, + const ExtensionLoader::LoadingInfo &loadingInfo); + bool uninstallInternal(const base::FilePath &dirToDelete); + void uninstallFinished(const std::string &id, bool success); + void cleanupBrokenInstall(const base::FilePath &dirToDelete, const std::string &error); + void onInstallFailure(const base::FilePath &brokenInstallDir, const std::string &error, + bool cleanupSucceded); + + raw_ptr<content::BrowserContext> m_browserContext; + ExtensionManager *m_manager; + base::WeakPtrFactory<ExtensionInstaller> m_weakFactory{ this }; +}; +} // namespace QtWebEngineCore + +#endif // EXTENSION_INSTALLER_H_ diff --git a/src/core/extensions/extension_loader.cpp b/src/core/extensions/extension_loader.cpp new file mode 100644 index 000000000..d7f21ade8 --- /dev/null +++ b/src/core/extensions/extension_loader.cpp @@ -0,0 +1,136 @@ +// 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 "extension_loader.h" + +#include "extension_manager.h" +#include "type_conversion.h" + +#include "base/files/file_util.h" +#include "base/task/sequenced_task_runner.h" +#include "content/public/browser/browser_context.h" +#include "extensions/browser/extension_file_task_runner.h" +#include "extensions/browser/extension_prefs.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/browser/extension_registry_observer.h" +#include "extensions/common/file_util.h" + +using namespace extensions; + +static constexpr int kSupportedManifestVersion = 3; + +namespace QtWebEngineCore { +ExtensionLoader::ExtensionLoader(content::BrowserContext *context, ExtensionManager *manager) + : m_browserContext(context) + , m_extensionRegistrar(context, this) + , m_extensionRegistry(ExtensionRegistry::Get(context)) + , m_manager(manager) +{ +} + +ExtensionLoader::~ExtensionLoader() { } + +// static +ExtensionLoader::LoadingInfo ExtensionLoader::loadExtensionOnFileThread(const base::FilePath &path) +{ + ExtensionLoader::LoadingInfo result; + result.path = path; + + if (!base::DirectoryExists(path)) { + result.error = "Directory not exists: " + path.AsUTF8Unsafe(); + return result; + } + + // Fixme: investigate whats the correct flag here + int loadFlags = Extension::NO_FLAGS; + std::string error; + // Fixme: currently only kComponent extension can run on custom schemes + // use kUnpacked once it's fixed + scoped_refptr<Extension> extension = + file_util::LoadExtension(path, mojom::ManifestLocation::kComponent, loadFlags, &error); + if (!extension.get()) { + result.error = error; + return result; + } + + if (extension->manifest_version() != kSupportedManifestVersion) { + result.error = "Unsupported manifest version"; + return result; + } + + result.extension = extension; + return result; +} + +void ExtensionLoader::loadExtension(const base::FilePath &path) +{ + if (m_browserContext->IsOffTheRecord()) { + m_manager->onExtensionLoadError(toQt(path), "Can't load in off-the-record mode"); + return; + } + GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult( + FROM_HERE, base::BindOnce(&loadExtensionOnFileThread, path), + base::BindOnce(&ExtensionLoader::loadExtensionFinished, m_weakFactory.GetWeakPtr())); +} + +void ExtensionLoader::addExtension(scoped_refptr<const Extension> extension) +{ + if (extensions().Contains(extension->id())) + m_extensionRegistrar.ReloadExtension(extension->id(), + ExtensionRegistrar::LoadErrorBehavior::kQuiet); + else + m_extensionRegistry->AddDisabled(extension); +} + +void ExtensionLoader::loadExtensionFinished(const LoadingInfo &loadingInfo) +{ + if (!loadingInfo.error.empty()) { + m_manager->onExtensionLoadError(toQt(loadingInfo.path), loadingInfo.error); + return; + } + + scoped_refptr<const Extension> extension = loadingInfo.extension; + Q_ASSERT(extension); + + addExtension(extension); + m_manager->onExtensionLoaded(extension.get()); +} + +void ExtensionLoader::unloadExtension(const std::string &id) +{ + m_extensionRegistrar.RemoveExtension(id, UnloadedExtensionReason::UNINSTALL); +} + +ExtensionSet ExtensionLoader::extensions() const +{ + return m_extensionRegistry->GenerateInstalledExtensionsSet(); +} + +void ExtensionLoader::disableExtension(const std::string &id) +{ + if (isExtensionLoaded(id) && isExtensionEnabled(id)) + m_extensionRegistrar.DisableExtension(id, extensions::disable_reason::DISABLE_USER_ACTION); +} + +void ExtensionLoader::enableExtension(const std::string &id) +{ + if (isExtensionLoaded(id) && !isExtensionEnabled(id)) + m_extensionRegistrar.EnableExtension(id); +} + +bool ExtensionLoader::isExtensionEnabled(const std::string &id) +{ + return m_extensionRegistry->enabled_extensions().Contains(id); +} + +bool ExtensionLoader::isExtensionLoaded(const std::string &id) +{ + return extensions().Contains(id); +} + +scoped_refptr<const Extension> ExtensionLoader::getExtensionById(const std::string &id) +{ + return isExtensionLoaded(id) ? extensions().GetByID(id) : nullptr; +} + +} // namespace QtWebEngineCore diff --git a/src/core/extensions/extension_loader.h b/src/core/extensions/extension_loader.h new file mode 100644 index 000000000..2863f310d --- /dev/null +++ b/src/core/extensions/extension_loader.h @@ -0,0 +1,79 @@ +// 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 + +#ifndef EXTENSION_LOADER_H_ +#define EXTENSION_LOADER_H_ + +#include <string> + +#include "base/files/file_path.h" +#include "base/functional/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" +#include "extensions/browser/extension_registrar.h" +#include "extensions/common/extension_set.h" + +namespace content { +class BrowserContext; +} +namespace extensions { +class Extension; +class ExtensionRegistry; +} + +namespace QtWebEngineCore { +class ExtensionManager; +} + +namespace QtWebEngineCore { +class ExtensionLoader : public extensions::ExtensionRegistrar::Delegate +{ +public: + struct LoadingInfo + { + scoped_refptr<const extensions::Extension> extension{}; + std::string error{}; + base::FilePath path{}; + }; + static LoadingInfo loadExtensionOnFileThread(const base::FilePath &path); + + ExtensionLoader(content::BrowserContext *context, ExtensionManager *manager); + ~ExtensionLoader(); + + void loadExtension(const base::FilePath &path); + void unloadExtension(const std::string &id); + + void addExtension(scoped_refptr<const extensions::Extension> extension); + extensions::ExtensionSet extensions() const; + + void enableExtension(const std::string &id); + void disableExtension(const std::string &id); + bool isExtensionEnabled(const std::string &id); + bool isExtensionLoaded(const std::string &id); + scoped_refptr<const extensions::Extension> getExtensionById(const std::string &id); + +private: + void loadExtensionFinished(const LoadingInfo &loadingInfo); + + // ExtensionRegistrar::Delegate: + void PreAddExtension(const extensions::Extension *extension, + const extensions::Extension *old_extension) override { }; + void PostActivateExtension(scoped_refptr<const extensions::Extension> extension) override { }; + void PostDeactivateExtension(scoped_refptr<const extensions::Extension> extension) override { }; + void LoadExtensionForReload( + const extensions::ExtensionId &extension_id, const base::FilePath &path, + extensions::ExtensionRegistrar::LoadErrorBehavior load_error_behavior) override { }; + bool CanEnableExtension(const extensions::Extension *extension) override { return true; }; + bool CanDisableExtension(const extensions::Extension *extension) override { return true; }; + bool ShouldBlockExtension(const extensions::Extension *extension) override { return false; }; + + raw_ptr<content::BrowserContext> m_browserContext; + extensions::ExtensionRegistrar m_extensionRegistrar; + extensions::ExtensionRegistry *m_extensionRegistry; + ExtensionManager *m_manager; + base::WeakPtrFactory<ExtensionLoader> m_weakFactory{ this }; +}; +} // namespace QtWebEngineCore + +#endif // EXTENSION_LOADER_H_ diff --git a/src/core/extensions/extension_manager.cpp b/src/core/extensions/extension_manager.cpp new file mode 100644 index 000000000..39748b870 --- /dev/null +++ b/src/core/extensions/extension_manager.cpp @@ -0,0 +1,187 @@ +// 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 "extension_manager.h" + +#include <QDirListing> +#include <QUrl> + +#include "api/qwebengineextensioninfo.h" +#include "api/qwebengineextensioninfo_p.h" +#include "api/qwebengineextensionmanager.h" +#include "extension_action_manager.h" +#include "extension_loader.h" +#include "extension_installer.h" +#include "type_conversion.h" + +#include "base/functional/callback.h" +#include "content/public/browser/browser_context.h" +#include "extensions/browser/extension_file_task_runner.h" + +using namespace extensions; + +namespace QtWebEngineCore { +namespace { +QWebEngineExtensionInfoPrivate * +createWebEngineExtensionData(ExtensionManager *manager, scoped_refptr<const Extension> extension, + const std::string &error = {}) +{ + QWebEngineExtensionInfoPrivate::ExtensionData data{ + .id = extension->id(), + .name = toQt(extension->name()), + .description = toQt(extension->description()), + .path = toQt(extension->path()), + .error = toQt(error), + .actionPopupUrl = manager->actionPopupUrl(extension->id()) + }; + return new QWebEngineExtensionInfoPrivate(data, manager); +} + +QWebEngineExtensionInfoPrivate *createWebEngineExtensionData(ExtensionManager *manager, + const QString &path, + const std::string &error) +{ + QWebEngineExtensionInfoPrivate::ExtensionData data{ + .path = path, + .error = toQt(error) + }; + return new QWebEngineExtensionInfoPrivate(data, manager); +} +} // namespace + +ExtensionManager::ExtensionManager(content::BrowserContext *context) + : m_loader(new ExtensionLoader(context, this)) + , m_installer(new ExtensionInstaller(context, this)) + , m_actionManager(new ExtensionActionManager()) +{ + for (auto dir : QDirListing(installDirectory(), QDirListing::IteratorFlag::DirsOnly)) { + loadExtension(dir.filePath()); + } +} + +ExtensionManager::~ExtensionManager() { } + +void ExtensionManager::loadExtension(const QString &path) +{ + m_loader->loadExtension(toFilePath(path)); +} + +void ExtensionManager::installExtension(const QString &path) +{ + m_installer->installExtension(toFilePath(path)); +} + +void ExtensionManager::setExtensionEnabled(const std::string &id, bool enabled) +{ + if (enabled) + m_loader->enableExtension(id); + else + m_loader->disableExtension(id); +} + +void ExtensionManager::unloadExtension(const std::string &id) +{ + if (!isExtensionLoaded(id)) + return; + + scoped_refptr<const Extension> extension = m_loader->getExtensionById(id); + m_actionManager->removeExtensionAction(extension->id()); + m_loader->unloadExtension(extension->id()); + Q_Q(QWebEngineExtensionManager); + Q_EMIT q->extensionUnloadFinished( + QWebEngineExtensionInfo(createWebEngineExtensionData(this, extension))); +} + +void ExtensionManager::uninstallExtension(const std::string &id) +{ + scoped_refptr<const Extension> extension = m_loader->getExtensionById(id); + if (extension->path().DirName() == m_installer->installDirectory()) { + m_installer->uninstallExtension(extension); + } else { + Q_Q(QWebEngineExtensionManager); + Q_EMIT q->extensionUninstallFinished(QWebEngineExtensionInfo( + createWebEngineExtensionData(this, extension, "This extension was not installed"))); + } +} + +bool ExtensionManager::isExtensionEnabled(const std::string &id) const +{ + return m_loader->isExtensionEnabled(id); +} + +bool ExtensionManager::isExtensionLoaded(const std::string &id) const +{ + return m_loader->isExtensionLoaded(id); +} + +QUrl ExtensionManager::actionPopupUrl(const std::string &id) const +{ + scoped_refptr<const Extension> extension = m_loader->getExtensionById(id); + if (auto *extensionAction = m_actionManager->getExtensionAction(extension.get())) + return toQt(extensionAction->GetPopupUrl(-1)); + return QUrl(); +} + +QString ExtensionManager::installDirectory() const +{ + return toQt(m_installer->installDirectory()); +} + +QList<QWebEngineExtensionInfo> ExtensionManager::extensions() +{ + QList<QWebEngineExtensionInfo> extension_list; + for (auto extension : m_loader->extensions()) + extension_list.append( + QWebEngineExtensionInfo(createWebEngineExtensionData(this, extension))); + return extension_list; +} + +void ExtensionManager::onExtensionLoaded(const Extension *extension) +{ + Q_Q(QWebEngineExtensionManager); + Q_EMIT q->extensionLoadFinished( + QWebEngineExtensionInfo(createWebEngineExtensionData(this, extension))); +} + +void ExtensionManager::onExtensionLoadError(const QString &path, const std::string &error) +{ + Q_Q(QWebEngineExtensionManager); + Q_EMIT q->extensionLoadFinished( + QWebEngineExtensionInfo(createWebEngineExtensionData(this, path, error))); +} + +void ExtensionManager::onExtensionInstalled(const Extension *extension) +{ + m_loader->addExtension(extension); + Q_Q(QWebEngineExtensionManager); + Q_EMIT q->extensionInstallFinished( + QWebEngineExtensionInfo(createWebEngineExtensionData(this, extension))); +} + +void ExtensionManager::onExtensionUninstalled(const std::string &id) +{ + scoped_refptr<const Extension> extension = m_loader->getExtensionById(id); + m_actionManager->removeExtensionAction(extension->id()); + m_loader->unloadExtension(extension->id()); + + Q_Q(QWebEngineExtensionManager); + Q_EMIT q->extensionUninstallFinished( + QWebEngineExtensionInfo(createWebEngineExtensionData(this, extension))); +} + +void ExtensionManager::onExtensionInstallError(const QString &path, const std::string &error) +{ + Q_Q(QWebEngineExtensionManager); + Q_EMIT q->extensionInstallFinished( + QWebEngineExtensionInfo(createWebEngineExtensionData(this, path, error))); +} + +void ExtensionManager::onExtensionUninstallError(const std::string &id, const std::string &error) +{ + scoped_refptr<const Extension> extension = m_loader->getExtensionById(id); + Q_Q(QWebEngineExtensionManager); + Q_EMIT q->extensionInstallFinished( + QWebEngineExtensionInfo(createWebEngineExtensionData(this, extension, error))); +} + +} // namespace QtWebEngineCore diff --git a/src/core/extensions/extension_manager.h b/src/core/extensions/extension_manager.h new file mode 100644 index 000000000..b99209b7e --- /dev/null +++ b/src/core/extensions/extension_manager.h @@ -0,0 +1,69 @@ +// 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 + +#ifndef EXTENSION_MANAGER_H_ +#define EXTENSION_MANAGER_H_ + +#include <memory> + +#include "api/qwebengineextensioninfo.h" +#include "api/qwebengineextensioninfo_p.h" + +#include <QList> +#include <QString> +#include <QObject> +#include <QtWebEngineCore/private/qtwebenginecoreglobal_p.h> + +namespace content { +class BrowserContext; +} + +namespace extensions { +class Extension; +} + +QT_BEGIN_NAMESPACE +class QWebEngineExtensionManager; +QT_END_NAMESPACE + +namespace QtWebEngineCore { +class ExtensionActionManager; +class ExtensionLoader; +class ExtensionInstaller; + +class Q_WEBENGINECORE_EXPORT ExtensionManager +{ +public: + Q_DECLARE_PUBLIC(QWebEngineExtensionManager) + QWebEngineExtensionManager *q_ptr; + + ExtensionManager(content::BrowserContext *context); + ~ExtensionManager(); + + void loadExtension(const QString &path); + void installExtension(const QString &path); + void setExtensionEnabled(const std::string &id, bool enabled); + void unloadExtension(const std::string &id); + void uninstallExtension(const std::string &id); + + bool isExtensionEnabled(const std::string &id) const; + bool isExtensionLoaded(const std::string &id) const; + QUrl actionPopupUrl(const std::string &id) const; + QString installDirectory() const; + QList<QWebEngineExtensionInfo> extensions(); + + void onExtensionLoaded(const extensions::Extension *); + void onExtensionInstalled(const extensions::Extension *); + void onExtensionUninstalled(const std::string &id); + void onExtensionLoadError(const QString &path, const std::string &error); + void onExtensionInstallError(const QString &path, const std::string &error); + void onExtensionUninstallError(const std::string &id, const std::string &error); + +private: + std::unique_ptr<ExtensionLoader> m_loader; + std::unique_ptr<ExtensionInstaller> m_installer; + std::unique_ptr<ExtensionActionManager> m_actionManager; +}; +} // namespace QtWebEngineCore + +#endif // EXTENSION_MANAGER_H_ diff --git a/src/core/extensions/extension_system_factory_qt.cpp b/src/core/extensions/extension_system_factory_qt.cpp index 63d88fcaf..fe6d1df6c 100644 --- a/src/core/extensions/extension_system_factory_qt.cpp +++ b/src/core/extensions/extension_system_factory_qt.cpp @@ -36,6 +36,7 @@ ExtensionSystemFactoryQt::ExtensionSystemFactoryQt() DCHECK(ExtensionsBrowserClient::Get()) << "ExtensionSystemFactory must be initialized after BrowserProcess"; DependsOn(ExtensionPrefsFactory::GetInstance()); DependsOn(ExtensionRegistryFactory::GetInstance()); + DependsOn(ProcessManagerFactory::GetInstance()); } ExtensionSystemFactoryQt::~ExtensionSystemFactoryQt() diff --git a/src/core/extensions/extension_system_qt.cpp b/src/core/extensions/extension_system_qt.cpp index b14239e9d..529bbaf4f 100644 --- a/src/core/extensions/extension_system_qt.cpp +++ b/src/core/extensions/extension_system_qt.cpp @@ -7,6 +7,8 @@ #include "extension_system_qt.h" +#include "extension_manager.h" + #include <algorithm> #include "base/base_paths.h" @@ -117,6 +119,11 @@ public: void Shutdown() override {} }; +QtWebEngineCore::ExtensionManager *ExtensionSystemQt::extensionManager() +{ + return extension_manager_.get(); +} + void ExtensionSystemQt::LoadExtension(const base::Value::Dict &manifest, const base::FilePath &directory) { int flags = Extension::REQUIRE_KEY; @@ -290,6 +297,7 @@ void ExtensionSystemQt::Init(bool extensions_enabled) service_worker_manager_ = std::make_unique<ServiceWorkerManager>(browser_context_); user_script_manager_ = std::make_unique<UserScriptManager>(browser_context_); quota_service_ = std::make_unique<QuotaService>(); + extension_manager_ = std::make_unique<QtWebEngineCore::ExtensionManager>(browser_context_); // Make the chrome://extension-icon/ resource available. // content::URLDataSource::Add(browser_context_, new ExtensionIconSource(browser_context_)); diff --git a/src/core/extensions/extension_system_qt.h b/src/core/extensions/extension_system_qt.h index de4a9826e..9445a655a 100644 --- a/src/core/extensions/extension_system_qt.h +++ b/src/core/extensions/extension_system_qt.h @@ -23,6 +23,10 @@ namespace value_store { class ValueStoreFactory; } +namespace QtWebEngineCore { +class ExtensionManager; +} + namespace extensions { class ExtensionRegistry; @@ -73,6 +77,8 @@ public: void PerformActionBasedOnOmahaAttributes(const std::string &, const base::Value::Dict &) override { /* fixme? */} + QtWebEngineCore::ExtensionManager *extensionManager(); + private: void NotifyExtensionLoaded(const Extension *extension); void LoadExtension(const base::Value::Dict &manifest, const base::FilePath &directory); @@ -91,6 +97,7 @@ private: ExtensionRegistry *extension_registry_; extensions::RendererStartupHelper *renderer_helper_; bool initialized_; + std::unique_ptr<QtWebEngineCore::ExtensionManager> extension_manager_; base::WeakPtrFactory<ExtensionSystemQt> weak_ptr_factory_; }; diff --git a/src/core/extensions/unpacked_extension_installer.cpp b/src/core/extensions/unpacked_extension_installer.cpp new file mode 100644 index 000000000..9a5eeafdc --- /dev/null +++ b/src/core/extensions/unpacked_extension_installer.cpp @@ -0,0 +1,92 @@ +// 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 "unpacked_extension_installer.h" + +#include "base/files/file_util.h" + +namespace QtWebEngineCore { +UnpackedExtensionInstaller::UnpackedExtensionInstaller( + const scoped_refptr<base::SequencedTaskRunner> &taskRunner, DoneCallback doneCallback) + : m_taskRunner(taskRunner), m_doneCallback(std::move(doneCallback)) +{ +} + +// static +scoped_refptr<UnpackedExtensionInstaller> +UnpackedExtensionInstaller::Create(const scoped_refptr<base::SequencedTaskRunner> &taskRunner, + DoneCallback doneCallback) +{ + return base::WrapRefCounted( + new UnpackedExtensionInstaller(taskRunner, std::move(doneCallback))); +} + +// static +UnpackedExtensionInstaller::InstallInfo +UnpackedExtensionInstaller::installUnpackedExtensionOnFileThread(const base::FilePath &src, + const base::FilePath &installDir) +{ + InstallInfo installInfo; + if (!base::DirectoryExists(installDir)) { + if (!base::CreateDirectory(installDir)) { + installInfo.error = "Install directory does not exists"; + return installInfo; + } + } + + // The installed dir format is `dirName_XXXXXX` where `XXXXXX` is populated with + // mktemp() logic to match the output format of the zip installer. + base::FilePath extensionInstallPath; + base::FilePath::StringType dirName = src.BaseName().value() + FILE_PATH_LITERAL("_"); + if (!base::CreateTemporaryDirInDir(installDir, dirName, &extensionInstallPath)) { + installInfo.error = "Failed to create install directory for extension"; + return installInfo; + } + + installInfo.extensionInstallPath = extensionInstallPath; + + // This performs a 'cp -r src installDir/', we need to rename the copied directory later. + if (!base::CopyDirectory(src, installDir, true)) { + installInfo.error = "Copy directory failed"; + return installInfo; + } + + auto copyPath = installDir.Append(src.BaseName()); + CHECK(base::DirectoryExists(copyPath)); + + if (!base::Move(copyPath, extensionInstallPath)) + installInfo.error = "Move directory failed"; + return installInfo; +} + +void UnpackedExtensionInstaller::install(const base::FilePath &src, + const base::FilePath &installDir) +{ + // Verify the extension before doing any file operations by preloading it. + m_taskRunner->PostTaskAndReplyWithResult( + FROM_HERE, + base::BindOnce(&QtWebEngineCore::ExtensionLoader::loadExtensionOnFileThread, src), + base::BindOnce(&UnpackedExtensionInstaller::installInternal, this, src, installDir)); +} + +void UnpackedExtensionInstaller::installInternal(const base::FilePath &src, + const base::FilePath &installDir, + const ExtensionLoader::LoadingInfo &loadingInfo) +{ + if (!loadingInfo.error.empty()) { + std::move(m_doneCallback).Run(src, installDir, loadingInfo.error); + return; + } + + m_taskRunner->PostTaskAndReplyWithResult( + FROM_HERE, base::BindOnce(installUnpackedExtensionOnFileThread, src, installDir), + base::BindOnce(&UnpackedExtensionInstaller::installDone, this, src)); +} + +void UnpackedExtensionInstaller::installDone(const base::FilePath &src, + const InstallInfo &installInfo) +{ + std::move(m_doneCallback).Run(src, installInfo.extensionInstallPath, installInfo.error); +} + +} // namespace QtWebEngineCore diff --git a/src/core/extensions/unpacked_extension_installer.h b/src/core/extensions/unpacked_extension_installer.h new file mode 100644 index 000000000..93ae33892 --- /dev/null +++ b/src/core/extensions/unpacked_extension_installer.h @@ -0,0 +1,47 @@ +// 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 + +#ifndef UNPACKED_EXTENSION_INSTALLER_H_ +#define UNPACKED_EXTENSION_INSTALLER_H_ + +#include <string> + +#include "extension_loader.h" + +#include "base/files/file_path.h" +#include "base/functional/callback.h" +#include "base/memory/ref_counted.h" +#include "base/task/sequenced_task_runner.h" + +namespace QtWebEngineCore { +class UnpackedExtensionInstaller : public base::RefCountedThreadSafe<UnpackedExtensionInstaller> +{ +public: + using DoneCallback = base::OnceCallback<void(const base::FilePath &src, + const base::FilePath &extensionsInstallDir, + const std::string &error)>; + static scoped_refptr<UnpackedExtensionInstaller> + Create(const scoped_refptr<base::SequencedTaskRunner> &taskRunner, DoneCallback doneCallback); + + struct InstallInfo + { + std::string error{}; + base::FilePath extensionInstallPath{}; + }; + + void install(const base::FilePath &src, const base::FilePath &installDir); + static InstallInfo installUnpackedExtensionOnFileThread(const base::FilePath &src, + const base::FilePath &installDir); + +private: + UnpackedExtensionInstaller(const scoped_refptr<base::SequencedTaskRunner> &taskRunner, + DoneCallback doneCallback); + void installInternal(const base::FilePath &src, const base::FilePath &installDir, + const ExtensionLoader::LoadingInfo &loadingInfo); + void installDone(const base::FilePath &src, const InstallInfo &installInfo); + + scoped_refptr<base::SequencedTaskRunner> m_taskRunner; + DoneCallback m_doneCallback; +}; +} // namespace QtWebEngineCore +#endif // UNPACKED_EXTENSION_INSTALLER_H_ diff --git a/src/core/pref_service_adapter.cpp b/src/core/pref_service_adapter.cpp index 0e1105839..b1e45582b 100644 --- a/src/core/pref_service_adapter.cpp +++ b/src/core/pref_service_adapter.cpp @@ -41,8 +41,10 @@ #if BUILDFLAG(ENABLE_EXTENSIONS) #include "components/guest_view/browser/guest_view_manager.h" +#include "extensions/browser/extension_prefs.h" #include "extensions/browser/extension_protocols.h" #include "extensions/browser/pref_names.h" +#include "extensions/browser/pref_types.h" #include "extensions/browser/process_manager.h" #include "extensions/common/constants.h" #endif @@ -122,6 +124,9 @@ void PrefServiceAdapter::setup(const ProfileAdapter &profileAdapter) registry->RegisterListPref(extensions::pref_names::kNativeMessagingAllowlist); registry->RegisterBooleanPref(extensions::pref_names::kNativeMessagingUserLevelHosts, true); registry->RegisterListPref(extensions::pref_names::kExtendedBackgroundLifetimeForPortConnectionsToUrls); + registry->RegisterDictionaryPref(extensions::kUserPermissions.name); + // Should match "kExternalUninstalls" in extensions/browser/extension_prefs.cc + registry->RegisterListPref("extensions.external_uninstalls"); #endif // BUILDFLAG(ENABLE_EXTENSIONS) // Media device salt id key diff --git a/src/core/profile_adapter.cpp b/src/core/profile_adapter.cpp index 941cc62a8..f0cabc088 100644 --- a/src/core/profile_adapter.cpp +++ b/src/core/profile_adapter.cpp @@ -20,6 +20,7 @@ #include "services/network/public/mojom/network_context.mojom.h" #include "url/url_util.h" +#include "api/qwebengineextensionmanager.h" #include "api/qwebengineurlscheme.h" #include "content_browser_client_qt.h" #include "download_manager_delegate_qt.h" @@ -122,6 +123,9 @@ ProfileAdapter::ProfileAdapter(const QString &storageName, const QString &dataPa m_customUrlSchemeHandlers.insert(QByteArrayLiteral("qrc"), &m_qrcHandler); m_cancelableTaskTracker.reset(new base::CancelableTaskTracker()); +#if QT_CONFIG(webengine_extensions) + m_extensionManager.reset(new QWebEngineExtensionManager(m_profile->extensionManager())); +#endif m_profile->DoFinalInit(); } @@ -1020,5 +1024,11 @@ void ProfileAdapter::requestIconForIconURL(const QUrl &iconUrl, touchIconsEnabled), m_cancelableTaskTracker.get()); } +#if QT_CONFIG(webengine_extensions) +QWebEngineExtensionManager *ProfileAdapter::extensionManager() +{ + return m_extensionManager.get(); +} +#endif } // namespace QtWebEngineCore diff --git a/src/core/profile_adapter.h b/src/core/profile_adapter.h index 75e83cb49..b6357ce1c 100644 --- a/src/core/profile_adapter.h +++ b/src/core/profile_adapter.h @@ -26,6 +26,7 @@ #include <QtWebEngineCore/qwebengineclientcertificatestore.h> #include <QtWebEngineCore/qwebenginecookiestore.h> +#include <QtWebEngineCore/qwebengineextensionmanager.h> #include <QtWebEngineCore/qwebengineurlrequestinterceptor.h> #include <QtWebEngineCore/qwebengineurlschemehandler.h> #include <QtWebEngineCore/qwebenginepermission.h> @@ -202,7 +203,9 @@ public: void resetClientHints(); void clearHttpCache(); - +#if QT_CONFIG(webengine_extensions) + QWebEngineExtensionManager *extensionManager(); +#endif #if QT_CONFIG(ssl) QWebEngineClientCertificateStore *clientCertificateStore(); #endif @@ -260,6 +263,9 @@ private: int m_httpCacheMaxSize; QrcUrlSchemeHandler m_qrcHandler; std::unique_ptr<base::CancelableTaskTracker> m_cancelableTaskTracker; +#if QT_CONFIG(webengine_extensions) + std::unique_ptr<QWebEngineExtensionManager> m_extensionManager; +#endif Q_DISABLE_COPY(ProfileAdapter) }; diff --git a/src/core/profile_qt.cpp b/src/core/profile_qt.cpp index 57bc91942..b0069aac7 100644 --- a/src/core/profile_qt.cpp +++ b/src/core/profile_qt.cpp @@ -38,6 +38,7 @@ #include "extensions/browser/extension_prefs_factory.h" #include "extensions/browser/extensions_browser_client.h" +#include "extensions/extension_manager.h" #include "extensions/extension_system_qt.h" #endif @@ -342,4 +343,12 @@ content::PlatformNotificationService *ProfileQt::GetPlatformNotificationService( return m_platformNotificationService.get(); } +#if QT_CONFIG(webengine_extensions) +ExtensionManager *ProfileQt::extensionManager() +{ + return static_cast<extensions::ExtensionSystemQt *>(extensions::ExtensionSystem::Get(this)) + ->extensionManager(); +} +#endif + } // namespace QtWebEngineCore diff --git a/src/core/profile_qt.h b/src/core/profile_qt.h index 1d8b746aa..8c3be09a9 100644 --- a/src/core/profile_qt.h +++ b/src/core/profile_qt.h @@ -11,13 +11,10 @@ class PrefService; -namespace extensions { -class ExtensionSystemQt; -} - namespace QtWebEngineCore { class BrowsingDataRemoverDelegateQt; +class ExtensionManager; class PermissionManagerQt; class ProfileAdapter; class ProfileIODataQt; @@ -77,6 +74,9 @@ public: void initUserAgentMetadata(); const blink::UserAgentMetadata &userAgentMetadata(); +#if QT_CONFIG(webengine_extensions) + ExtensionManager *extensionManager(); +#endif private: std::unique_ptr<BrowsingDataRemoverDelegateQt> m_removerDelegate; diff --git a/src/core/type_conversion.h b/src/core/type_conversion.h index 1a21351ce..db1c308dc 100644 --- a/src/core/type_conversion.h +++ b/src/core/type_conversion.h @@ -191,6 +191,11 @@ inline QColor toQt(const SkColor &c) return QColor(SkColorGetR(c), SkColorGetG(c), SkColorGetB(c), SkColorGetA(c)); } +inline QString toQt(const base::FilePath &path) +{ + return toQt(path.value()); +} + inline SkColor toSk(const QColor &c) { return c.rgba(); diff --git a/src/webenginequick/api/qquickwebengineprofile.cpp b/src/webenginequick/api/qquickwebengineprofile.cpp index 7eb59d748..f3be4e6df 100644 --- a/src/webenginequick/api/qquickwebengineprofile.cpp +++ b/src/webenginequick/api/qquickwebengineprofile.cpp @@ -29,6 +29,10 @@ using QtWebEngineCore::ProfileAdapter; +#if QT_CONFIG(webengine_extensions) +#include <QtWebEngineCore/qwebengineextensionmanager.h> +#endif + QT_BEGIN_NAMESPACE /*! @@ -1170,6 +1174,16 @@ QWebEngineClientHints *QQuickWebEngineProfile::clientHints() const return d->m_clientHints.data(); } +QWebEngineExtensionManager *QQuickWebEngineProfile::extensionManager() +{ +#if QT_CONFIG(webengine_extensions) + Q_D(QQuickWebEngineProfile); + return d->profileAdapter()->extensionManager(); +#else + return nullptr; +#endif +} + /*! \fn QQuickWebEngineProfile::queryPermission(const QUrl &securityOrigin, QWebEnginePermission::PermissionType permissionType) const diff --git a/src/webenginequick/api/qquickwebengineprofile.h b/src/webenginequick/api/qquickwebengineprofile.h index 17a054868..b52db9182 100644 --- a/src/webenginequick/api/qquickwebengineprofile.h +++ b/src/webenginequick/api/qquickwebengineprofile.h @@ -18,6 +18,7 @@ class QQuickWebEngineSettings; class QWebEngineClientCertificateStore; class QWebEngineClientHints; class QWebEngineCookieStore; +class QWebEngineExtensionManager; class QWebEngineNotification; class QWebEngineUrlRequestInterceptor; class QWebEngineUrlSchemeHandler; @@ -42,6 +43,10 @@ class Q_WEBENGINEQUICK_EXPORT QQuickWebEngineProfile : public QObject { Q_PROPERTY(QString downloadPath READ downloadPath WRITE setDownloadPath NOTIFY downloadPathChanged FINAL REVISION(1,5)) Q_PROPERTY(bool isPushServiceEnabled READ isPushServiceEnabled WRITE setPushServiceEnabled NOTIFY pushServiceEnabledChanged FINAL REVISION(6,5)) Q_PROPERTY(QWebEngineClientHints *clientHints READ clientHints FINAL REVISION(6,8)) +#if QT_CONFIG(webengine_extensions) + Q_PROPERTY(QWebEngineExtensionManager *extensionManager READ extensionManager FINAL + REVISION(6, 10)) +#endif QML_NAMED_ELEMENT(WebEngineProfile) QML_ADDED_IN_VERSION(1, 1) QML_EXTRA_VERSION(2, 0) @@ -129,6 +134,7 @@ public: QWebEngineClientCertificateStore *clientCertificateStore(); QWebEngineClientHints *clientHints() const; + QWebEngineExtensionManager *extensionManager(); Q_REVISION(6,8) Q_INVOKABLE QWebEnginePermission queryPermission(const QUrl &securityOrigin, QWebEnginePermission::PermissionType permissionType) const; Q_REVISION(6,8) Q_INVOKABLE QList<QWebEnginePermission> listAllPermissions() const; diff --git a/tests/auto/quick/publicapi/tst_publicapi.cpp b/tests/auto/quick/publicapi/tst_publicapi.cpp index 71844c23d..55f32e15d 100644 --- a/tests/auto/quick/publicapi/tst_publicapi.cpp +++ b/tests/auto/quick/publicapi/tst_publicapi.cpp @@ -10,6 +10,8 @@ #include <QtTest/QtTest> #include <QtWebEngineCore/QWebEngineCertificateError> #include <QtWebEngineCore/QWebEngineDesktopMediaRequest> +#include <QtWebEngineCore/QWebEngineExtensionInfo> +#include <QtWebEngineCore/QWebEngineExtensionManager> #include <QtWebEngineCore/QWebEngineFileSystemAccessRequest> #include <QtWebEngineCore/QWebEngineFindTextResult> #include <QtWebEngineCore/QWebEngineFullScreenRequest> @@ -84,6 +86,10 @@ static const QList<const QMetaObject *> typesToCheck = QList<const QMetaObject * << &QWebEngineFrame::staticMetaObject << &QWebEngineClientHints::staticMetaObject << &QQuickWebEngineProfilePrototype::staticMetaObject +#if QT_CONFIG(webengine_extensions) + << &QWebEngineExtensionInfo::staticMetaObject + << &QWebEngineExtensionManager::staticMetaObject +#endif ; static QList<QMetaEnum> knownEnumNames = QList<QMetaEnum>() @@ -104,6 +110,9 @@ static const QStringList hardcodedTypes = QStringList() << "QQmlComponent*" << "QMultiMap<QByteArray,QByteArray>" << "QList<QWebEnginePermission>" +#if QT_CONFIG(webengine_extensions) + << "QList<QWebEngineExtensionInfo>" +#endif ; static const QStringList expectedAPI = QStringList() @@ -300,6 +309,28 @@ static const QStringList expectedAPI = QStringList() << "QWebEngineDesktopMediaRequest.selectScreen(QModelIndex) --> void" << "QWebEngineDesktopMediaRequest.selectWindow(QModelIndex) --> void" << "QWebEngineDesktopMediaRequest.cancel() --> void" +#if QT_CONFIG(webengine_extensions) + << "QWebEngineExtensionInfo.name --> QString" + << "QWebEngineExtensionInfo.id --> QString" + << "QWebEngineExtensionInfo.description --> QString" + << "QWebEngineExtensionInfo.path --> QString" + << "QWebEngineExtensionInfo.error --> QString" + << "QWebEngineExtensionInfo.actionPopupUrl --> QUrl" + << "QWebEngineExtensionInfo.isEnabled --> bool" + << "QWebEngineExtensionInfo.isLoaded --> bool" + << "QWebEngineExtensionInfo.isInstalled --> bool" + << "QWebEngineExtensionManager.extensions --> QList<QWebEngineExtensionInfo>" + << "QWebEngineExtensionManager.loadExtension(QString) --> void" + << "QWebEngineExtensionManager.installExtension(QString) --> void" + << "QWebEngineExtensionManager.unloadExtension(QWebEngineExtensionInfo) --> void" + << "QWebEngineExtensionManager.uninstallExtension(QWebEngineExtensionInfo) --> void" + << "QWebEngineExtensionManager.setExtensionEnabled(QWebEngineExtensionInfo,bool) --> void" + << "QWebEngineExtensionManager.installDirectory --> QString" + << "QWebEngineExtensionManager.extensionLoadFinished(QWebEngineExtensionInfo) --> void" + << "QWebEngineExtensionManager.extensionUnloadFinished(QWebEngineExtensionInfo) --> void" + << "QWebEngineExtensionManager.extensionInstallFinished(QWebEngineExtensionInfo) --> void" + << "QWebEngineExtensionManager.extensionUninstallFinished(QWebEngineExtensionInfo) --> void" +#endif << "QWebEngineFullScreenRequest.accept() --> void" << "QWebEngineFullScreenRequest.origin --> QUrl" << "QWebEngineFullScreenRequest.reject() --> void" @@ -448,6 +479,9 @@ static const QStringList expectedAPI = QStringList() << "QQuickWebEngineProfile.storageName --> QString" << "QQuickWebEngineProfile.storageNameChanged() --> void" << "QQuickWebEngineProfile.userScripts --> QQuickWebEngineScriptCollection*" +#if QT_CONFIG(webengine_extensions) + << "QQuickWebEngineProfile.extensionManager --> QWebEngineExtensionManager*" +#endif << "QQuickWebEngineSettings.AllowAllUnknownUrlSchemes --> UnknownUrlSchemePolicy" << "QQuickWebEngineSettings.AllowUnknownUrlSchemesFromUserInteraction --> UnknownUrlSchemePolicy" << "QQuickWebEngineSettings.DisallowUnknownUrlSchemes --> UnknownUrlSchemePolicy" diff --git a/tests/manual/widgets/CMakeLists.txt b/tests/manual/widgets/CMakeLists.txt index 94ee1c138..6d6555d83 100644 --- a/tests/manual/widgets/CMakeLists.txt +++ b/tests/manual/widgets/CMakeLists.txt @@ -8,3 +8,6 @@ endif() if(TARGET Qt6::HttpServer) add_subdirectory(webrtc) endif() +if(QT_FEATURE_webengine_extensions) + add_subdirectory(extensions) +endif() diff --git a/tests/manual/widgets/extensions/CMakeLists.txt b/tests/manual/widgets/extensions/CMakeLists.txt new file mode 100644 index 000000000..a2d916f1f --- /dev/null +++ b/tests/manual/widgets/extensions/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(extensions LANGUAGES CXX) + find_package(Qt6BuildInternals COMPONENTS STANDALONE_TEST) + endif() + +qt_internal_add_manual_test(extensions + SOURCES + main.cpp +) + +set_target_properties(extensions PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(extensions PUBLIC + Qt::Core + Qt::Gui + Qt::WebEngineWidgets +) diff --git a/tests/manual/widgets/extensions/main.cpp b/tests/manual/widgets/extensions/main.cpp new file mode 100644 index 000000000..938bf1fab --- /dev/null +++ b/tests/manual/widgets/extensions/main.cpp @@ -0,0 +1,287 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QAbstractListModel> +#include <QApplication> +#include <QDirListing> +#include <QFileDialog> +#include <QHBoxLayout> +#include <QListView> +#include <QMainWindow> +#include <QMessageBox> +#include <QPushButton> +#include <QTemporaryDir> +#include <QTimer> +#include <QWebEngineProfileBuilder> +#include <QWebEngineView> +#include <QWebEngineProfile> +#include <QWidget> + +#include <QWebEngineExtensionManager> +#include <QWebEngineExtensionInfo> + +// Test for extension management APIs using QWebEngineExtensionManager +// and QWebEngineExtensionInfo + +class ExtensionsListModel : public QAbstractListModel +{ +public: + ExtensionsListModel(const QList<QWebEngineExtensionInfo> &extensions) + : m_extensionsList(std::move(extensions)) + { + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + Q_UNUSED(parent) + return m_extensionsList.count(); + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) + return QVariant(); + switch (role) { + case Qt::DisplayRole: { + auto extension = m_extensionsList.at(index.row()); + QString enabled = extension.isEnabled() ? " enabled" : " disabled"; + return extension.name() + enabled + "\nId: " + extension.id() + + "\npath: " + extension.path(); + } + case Qt::UserRole: { + QWebEngineExtensionInfo extension = m_extensionsList.at(index.row()); + return QVariant::fromValue(extension); + } + default: + break; + } + return QVariant(); + } + +private: + QList<QWebEngineExtensionInfo> m_extensionsList; +}; + +class ExtensionsWidget : public QWidget +{ +public: + ExtensionsWidget(QWebEngineProfile *profile, QWebEngineExtensionManager *manager) + : m_profile(profile), m_extensionManager(manager) + { + update(); + + setLayout(new QVBoxLayout); + m_extensionsView.setSpacing(2); + m_extensionsView.setWrapping(true); + layout()->addWidget(&m_extensionsView); + + auto *actionsBtn = new QPushButton("open actions menu"); + QObject::connect(actionsBtn, &QPushButton::clicked, this, + &ExtensionsWidget::openActionsMenu); + layout()->addWidget(actionsBtn); + + auto *enabeBtn = new QPushButton("enable selected"); + QObject::connect(enabeBtn, &QPushButton::clicked, this, &ExtensionsWidget::enable); + layout()->addWidget(enabeBtn); + + auto *disableBtn = new QPushButton("disable selected"); + QObject::connect(disableBtn, &QPushButton::clicked, this, &ExtensionsWidget::disable); + layout()->addWidget(disableBtn); + + auto *loadBtn = new QPushButton("load unpacked"); + QObject::connect(loadBtn, &QPushButton::clicked, this, &ExtensionsWidget::loadUnpacked); + layout()->addWidget(loadBtn); + + auto *installBtn = new QPushButton("install packed"); + QObject::connect(installBtn, &QPushButton::clicked, this, [this]() { install(true); }); + layout()->addWidget(installBtn); + + auto *installUnpackedBtn = new QPushButton("install unpacked"); + QObject::connect(installUnpackedBtn, &QPushButton::clicked, this, + [this]() { install(false); }); + layout()->addWidget(installUnpackedBtn); + + auto *unLoadBtn = new QPushButton("unload"); + QObject::connect(unLoadBtn, &QPushButton::clicked, this, &ExtensionsWidget::unload); + layout()->addWidget(unLoadBtn); + + auto *uninstallBtn = new QPushButton("uninstall"); + QObject::connect(uninstallBtn, &QPushButton::clicked, this, &ExtensionsWidget::uninstall); + layout()->addWidget(uninstallBtn); + + QObject::connect(m_extensionManager, &QWebEngineExtensionManager::extensionLoadFinished, + [this](QWebEngineExtensionInfo extension) { + if (!extension.isLoaded()) { + showInfoDialog("Failed to load extension\n\nFile:" + + extension.path() + + "\nError: " + extension.error()); + return; + } + m_extensionManager->setExtensionEnabled(extension, true); + showInfoDialog("Extension loaded\n\nName:" + extension.name() + + "\nFile: " + extension.path()); + update(); + }); + + QObject::connect(m_extensionManager, &QWebEngineExtensionManager::extensionInstallFinished, + [this](QWebEngineExtensionInfo extension) { + if (!extension.isInstalled()) { + showInfoDialog("Failed to install extension\n\nFile: " + + extension.path() + + "\nError: " + extension.error()); + return; + } + showInfoDialog("Extension installed\n\nName:" + extension.name() + + "\nFile: " + extension.path()); + m_extensionManager->setExtensionEnabled(extension, true); + update(); + }); + QObject::connect(m_extensionManager, &QWebEngineExtensionManager::extensionUnloadFinished, + [this](QWebEngineExtensionInfo extension) { + if (!extension.error().isEmpty()) { + showInfoDialog("Failed to unload " + extension.name() + + "\n\nFile: " + extension.path() + + "\nError: " + extension.error()); + return; + } + showInfoDialog("Extension unloaded\n\nName: " + extension.name() + + "\nFile: " + extension.path()); + update(); + }); + QObject::connect( + m_extensionManager, &QWebEngineExtensionManager::extensionUninstallFinished, + [this](QWebEngineExtensionInfo extension) { + if (!extension.error().isEmpty()) { + showInfoDialog("Failed to uninstall " + extension.name() + "\n\nFile: " + + extension.path() + "\nError: " + extension.error()); + return; + } + showInfoDialog("Extension uninstalled\n\nName: " + extension.name() + + "\nFile: " + extension.path()); + update(); + }); + } + +private: + void update() + { + auto *oldModel = m_extensionsView.selectionModel(); + m_extensionsView.setModel( + new ExtensionsListModel(std::move(m_extensionManager->extensions()))); + if (oldModel) + delete oldModel; + } + + QWebEngineExtensionInfo getSelectedExtension() + { + QModelIndex idx = m_extensionsView.currentIndex(); + QVariant var = m_extensionsView.model()->data(idx, Qt::UserRole); + QWebEngineExtensionInfo extension = var.value<QWebEngineExtensionInfo>(); + return extension; + } + + void enable() + { + m_extensionManager->setExtensionEnabled(getSelectedExtension(), true); + update(); + } + + void disable() + { + m_extensionManager->setExtensionEnabled(getSelectedExtension(), false); + update(); + } + + void loadUnpacked() + { + QFileDialog dialog(this); + dialog.setFileMode(QFileDialog::Directory); + if (dialog.exec()) + m_extensionManager->loadExtension(dialog.selectedFiles()[0]); + } + + void install(bool packed) + { + QFileDialog dialog(this); + if (packed) { + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setNameFilter("Extensions(*.zip)"); + } else { + dialog.setFileMode(QFileDialog::Directory); + } + if (dialog.exec()) + m_extensionManager->installExtension(dialog.selectedFiles()[0]); + } + + void unload() + { + m_extensionManager->unloadExtension(getSelectedExtension()); + update(); + } + + void uninstall() + { + m_extensionManager->uninstallExtension(getSelectedExtension()); + update(); + } + + void openActionsMenu() + { + const auto url = getSelectedExtension().actionPopupUrl(); + if (url.isEmpty()) { + showInfoDialog("No popup page set for this extension"); + return; + } + + auto *view = new QWebEngineView(m_profile); + view->setAttribute(Qt::WA_DeleteOnClose, true); + view->load(url); + view->show(); + } + + void showInfoDialog(const QString &msg) + { + QMessageBox *msgBox = new QMessageBox; + msgBox->setWindowModality(Qt::NonModal); + msgBox->setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog); + msgBox->setAttribute(Qt::WA_DeleteOnClose, true); + msgBox->resize(400, 100); + msgBox->setText(msg); + QTimer::singleShot(4000, msgBox, &QMessageBox::close); + msgBox->show(); + } + + QWebEngineProfile *m_profile; + QWebEngineExtensionManager *m_extensionManager; + QListView m_extensionsView; +}; + +int main(int argc, char *argv[]) +{ + QCoreApplication::setOrganizationName("QtExamples"); + QApplication app(argc, argv); + QMainWindow window; + window.setCentralWidget(new QWidget); + + auto *layout = new QHBoxLayout; + window.centralWidget()->setLayout(layout); + + QTemporaryDir tempDir; + QWebEngineProfileBuilder profileBuilder; + QWebEngineProfile *profile = profileBuilder.createProfile("ExtensionsManualTest"); + + auto *extensionManager = profile->extensionManager(); + qDebug() << "installDir" << extensionManager->installDirectory(); + + QWebEngineView view(profile); + view.setUrl(QUrl(QStringLiteral("https://www.google.com/"))); + view.resize(1024, 750); + + window.centralWidget()->layout()->addWidget(&view); + window.centralWidget()->layout()->addWidget( + new ExtensionsWidget(profile, extensionManager)); + + window.show(); + + return app.exec(); +} |