[kde-linux/package-compatibility-helper] src: Grant folder temporary access when a tool is sandboxed away from the file
Hadi Chokr <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 002edf6de6972a995ee9001f49a27b32b521bf4a by Hadi Chokr. Committed on 18/08/2026 at 11:29. Pushed by silverhadch into branch 'master'. Grant folder temporary access when a tool is sandboxed away from the file Closes #6 Co-authored-by: Thomas Duckworth <[email protected]> Signed-off-by: Hadi Chokr <[email protected]> M +76 -6 src/ICompatibilityHelper.cpp M +10 -2 src/ICompatibilityHelper.h M +10 -0 src/contents/ui/Main.qml https://invent.kde.org/kde-linux/package-compatibility-helper/-/commit/002edf6de6972a995ee9001f49a27b32b521bf4a diff --git a/src/ICompatibilityHelper.cpp b/src/ICompatibilityHelper.cpp index a7c6c6e..814d83d 100644 --- a/src/ICompatibilityHelper.cpp +++ b/src/ICompatibilityHelper.cpp @@ -10,7 +10,11 @@ #include <KIO/OpenUrlJob> #include <KLocalizedContext> #include <KLocalizedString> +#include <KShell> +#include <QDir> +#include <QFileInfo> #include <QIcon> +#include <QRegularExpression> #define DOCUMENTATION_URL QUrl(u"https://kde.org/linux/docs/more-software"_s) @@ -25,9 +29,37 @@ void ICompatibilityHelper::openAppInAppStore(const QString &ref) const job->start(); } -void ICompatibilityHelper::openApp(const QString &ref, const QList<QUrl> &urls) const +void ICompatibilityHelper::openApp(const QString &ref, const QList<QUrl> &urls, const QString &transientFolderAccess) const { - KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(ref)); + // Exported Flatpak entries spell out the path to flatpak, e.g. + // "/usr/bin/flatpak run --command=wine --file-forwarding org.winehq.Wine @@u %u @@". + static const QRegularExpression flatpakRun(u"^(?:\\S*/)?flatpak\\s+run\\s+"_s); + static const QRegularExpression fileForwarding(u"\\s--file-forwarding\\b|\\s@@u?(?=\\s|$)"_s); + + KService::Ptr service = KService::serviceByDesktopName(ref); + + if (service && !urls.isEmpty()) { + QString exec = service->exec(); + const QRegularExpressionMatch match = flatpakRun.match(exec); + + if (match.hasMatch()) { + // Document portal paths put the file in a directory of its own, so + // hand over the real path and open its folder instead. + exec.remove(fileForwarding); + exec.replace(u"%u"_s, u"%f"_s); + exec.replace(u"%U"_s, u"%F"_s); + + // Amending the line keeps --branch, --arch and --command. + if (!transientFolderAccess.isEmpty()) { + exec.insert(match.capturedEnd(), u"--filesystem=%1:rw "_s.arg(KShell::quoteArg(transientFolderAccess))); + } + service->setExec(exec); + } else if (!transientFolderAccess.isEmpty()) { + qWarning() << "The desktop entry for" << ref << "does not launch a Flatpak, so its Exec line was left as-is:" << exec; + } + } + + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr)); job->setUrls(urls); job->start(); @@ -204,6 +236,36 @@ QString ICompatibilityHelper::compatibilityToolActionIcon() const return m_compatibilityToolInstaller->icon(); } +QString ICompatibilityHelper::compatibilityToolWarning() const +{ + if (!hasCompatibilityTool()) { + return QString(); + } + + // With two tools on offer the user has not picked one yet, so don't name one. + if (hasSecondaryCompatibilityTool()) { + if (isCompatibilityToolInstalled() && isSecondaryCompatibilityToolInstalled()) { + return i18nc("@info", + "Running this file temporarily gives the compatibility tool access to everything in this folder, in addition to its existing " + "permissions. The file is not checked for safety. <b>Only run files from trusted sources.</b>"); + } + return i18nc("@info", + "Installing a compatibility tool and running this file with it temporarily gives it access to everything in this folder, in addition to " + "the permissions it comes with. The file is not checked for safety. <b>Only run files from trusted sources.</b>"); + } + + if (isCompatibilityToolInstalled()) { + return i18nc("@info %1 is an application name, e.g. \"Wine\"", + "Running this file temporarily gives %1 access to everything in this folder, in addition to its existing permissions. The file is not " + "checked for safety. <b>Only run files from trusted sources.</b>", + compatibilityToolName()); + } + return i18nc("@info %1 is an application name, e.g. \"Wine\"", + "Installing %1 and running this file with it temporarily gives it access to everything in this folder, in addition to the permissions it " + "comes with. The file is not checked for safety. <b>Only run files from trusted sources.</b>", + compatibilityToolName()); +} + void ICompatibilityHelper::launchCompatibilityTool() const { if (!hasCompatibilityTool()) { @@ -215,7 +277,7 @@ void ICompatibilityHelper::launchCompatibilityTool() const return; } m_compatibilityToolInstaller->takeOverMimeTypes(); - openApp(m_compatibilityToolInstaller->appId(), {m_filePath}); + openApp(m_compatibilityToolInstaller->appId(), {m_filePath}, fileFolder()); } void ICompatibilityHelper::compatibilityToolInstallFinished() const @@ -224,7 +286,7 @@ void ICompatibilityHelper::compatibilityToolInstallFinished() const return; } m_compatibilityToolInstaller->runPostInstall(); - openApp(m_compatibilityToolInstaller->appId(), {m_filePath}); + openApp(m_compatibilityToolInstaller->appId(), {m_filePath}, fileFolder()); } bool ICompatibilityHelper::hasSecondaryCompatibilityTool() const @@ -289,7 +351,7 @@ void ICompatibilityHelper::launchSecondaryCompatibilityTool() const } // Deliberately not calling takeOverMimeTypes() here: the secondary tool is // the non-default choice, so it should not claim the MIME type handler. - openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath}); + openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath}, fileFolder()); } void ICompatibilityHelper::secondaryCompatibilityToolInstallFinished() const @@ -298,5 +360,13 @@ void ICompatibilityHelper::secondaryCompatibilityToolInstallFinished() const return; } m_secondaryCompatibilityToolInstaller->runPostInstall(); - openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath}); + openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath}, fileFolder()); +} + +QString ICompatibilityHelper::fileFolder() const +{ + if (!m_filePath.isValid() || !m_filePath.isLocalFile()) { + return QString(); + } + return QDir::cleanPath(QFileInfo(m_filePath.toLocalFile()).absolutePath()); } diff --git a/src/ICompatibilityHelper.h b/src/ICompatibilityHelper.h index 947a16d..7cd6992 100644 --- a/src/ICompatibilityHelper.h +++ b/src/ICompatibilityHelper.h @@ -48,6 +48,9 @@ class ICompatibilityHelper : public QObject Q_PROPERTY(bool compatibilityToolInstalled READ compatibilityToolInstalled CONSTANT) // The Flatpak installer configuration for the compatibility tool. Q_PROPERTY(QObject *compatibilityToolInstaller READ compatibilityToolInstaller CONSTANT) + // Warns about the folder access the tool is given and that the file itself + // is unchecked. Empty means no warning is shown. + Q_PROPERTY(QString compatibilityToolWarning READ compatibilityToolWarning CONSTANT) // A second, optional compatibility tool offered alongside the primary one. // Used when a file type could be handled by either of two tools and the two @@ -86,6 +89,7 @@ public: virtual QString compatibilityToolActionIcon() const; bool compatibilityToolInstalled() const; QObject *compatibilityToolInstaller() const; + virtual QString compatibilityToolWarning() const; virtual bool hasSecondaryCompatibilityTool() const; QString secondaryCompatibilityToolActionText() const; QString secondaryCompatibilityToolActionIcon() const; @@ -148,8 +152,9 @@ protected: // Helper that returns the name of the default app store, e.g. "Discover" or "Bazaar". QString appStoreName() const; - // Helper to open an app. - void openApp(const QString &ref, const QList<QUrl> &urls = {}) const; + // Helper to open an app. A folder named by transientFolderAccess is opened + // for it for that launch only. + void openApp(const QString &ref, const QList<QUrl> &urls = {}, const QString &transientFolderAccess = {}) const; // Helper to check if an app is installed. bool isAppInstalled(const QString &ref) const; @@ -160,6 +165,9 @@ protected: // Helper to return the distro name. QString distroName() const; + // The folder the opened file is in, or an empty string in --install mode. + QString fileFolder() const; + // The file path of the executable/package being opened. QUrl m_filePath; diff --git a/src/contents/ui/Main.qml b/src/contents/ui/Main.qml index 7f9e3fe..2dc1cc0 100644 --- a/src/contents/ui/Main.qml +++ b/src/contents/ui/Main.qml @@ -78,6 +78,7 @@ Kirigami.ApplicationWindow { // Measures the dimensions of the install page without triggering it. Loader { id: installPageMetrics + active: PackageCompatibilityHelper.hasCompatibilityTool visible: false sourceComponent: installPageMetricsComponent } @@ -124,6 +125,15 @@ Kirigami.ApplicationWindow { wrapMode: Text.WordWrap text: PackageCompatibilityHelper.description } + + Kirigami.InlineMessage { + visible: PackageCompatibilityHelper.compatibilityToolWarning !== "" && !PackageCompatibilityHelper.hasNativeApp + Layout.fillWidth: true + Layout.maximumWidth: Math.max(Kirigami.Units.gridUnit * 30, heading.implicitWidth) + Layout.topMargin: Kirigami.Units.smallSpacing + type: Kirigami.MessageType.Warning + text: PackageCompatibilityHelper.compatibilityToolWarning + } } }