]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Guide users to using kio-admin instead of sudo
authorFelix Ernst <felixernst@kde.org>
Mon, 1 Jul 2024 12:03:22 +0000 (12:03 +0000)
committerFelix Ernst <felixernst@kde.org>
Mon, 1 Jul 2024 12:03:22 +0000 (12:03 +0000)
This commit adds a guided setup that leads users from a situation
in which they try to "sudo dolphin" towards them successfully
setting up and using kio-admin.

1. When users enter "sudo dolphin", they are told to start Dolphin
   by typing "dolphin --sudo" or "dolphin --admin" instead.
2. When Dolphin is started with "--sudo" or "--admin" it checks
   whether an "admin" protocol is installed. If not, a guided
   setup leads users towards installing it.
3. After that, Dolphin starts with an installed "admin" protocoll
   like kio-admin. Now a non-modal information dialog appears that
   explains how to activate and use kio-admin.

CMakeLists.txt
src/CMakeLists.txt
src/admin/workerintegration.cpp
src/admin/workerintegration.h
src/config-dolphin.h.cmake
src/dolphinpackageinstaller.cpp [new file with mode: 0644]
src/dolphinpackageinstaller.h [new file with mode: 0644]
src/main.cpp

index 5a4018351bea911cf030a2f6bdd89900f9d8af98..d7f1cb57af8497ca4103bb275ae774bb0cc9fd98 100644 (file)
@@ -103,7 +103,7 @@ find_package(Phonon4Qt6 CONFIG REQUIRED)
 find_package(PackageKitQt6)
 set_package_properties(PackageKitQt6
         PROPERTIES DESCRIPTION "Software Manager integration"
-        TYPE OPTIONAL
+        TYPE RECOMMENDED
         PURPOSE "Used in the service menu installer"
         )
 if(PackageKitQt6_FOUND)
index 2f11c33535a6871c074c0d0289bf962cd8921153..fe9ac1f45149d78dae4b35bbc17fd4bed4e977ad 100644 (file)
@@ -1,5 +1,6 @@
 include(ECMAddAppIcon)
 
+set(ADMIN_WORKER_PACKAGE_NAME "kio-admin")
 configure_file(config-dolphin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-dolphin.h)
 
 add_definitions(
@@ -274,6 +275,7 @@ target_sources(dolphinstatic PRIVATE
     dolphincontextmenu.cpp
     dolphinnavigatorswidgetaction.cpp
     dolphintabbar.cpp
+    dolphinpackageinstaller.cpp
     dolphinplacesmodelsingleton.cpp
     dolphinrecenttabsmenu.cpp
     dolphintabpage.cpp
@@ -334,6 +336,7 @@ target_sources(dolphinstatic PRIVATE
     dolphincontextmenu.h
     dolphinnavigatorswidgetaction.h
     dolphintabbar.h
+    dolphinpackageinstaller.h
     dolphinplacesmodelsingleton.h
     dolphinrecenttabsmenu.h
     dolphintabpage.h
@@ -459,6 +462,13 @@ if (HAVE_PLASMA_ACTIVITIES)
     )
 endif()
 
+if(HAVE_PACKAGEKIT)
+    target_link_libraries(
+        dolphinstatic
+        PK::packagekitqt6
+    )
+endif()
+
 if (HAVE_KUSERFEEDBACK)
     target_link_libraries(
         dolphinstatic
index f9b58739196099fdb63c02fe5671d5e0f76e4fde..038c5209435f45d9c2fb9cd980399b2b9b70a2db 100644 (file)
@@ -7,7 +7,9 @@
 
 #include "workerintegration.h"
 
+#include "config-dolphin.h"
 #include "dolphinmainwindow.h"
+#include "dolphinpackageinstaller.h"
 #include "dolphinviewcontainer.h"
 
 #include <KActionCollection>
 
 #include <QAction>
 
+#include <iostream>
+
 using namespace Admin;
 
+/** Free file-local functions */
+namespace
+{
+/** @returns the translated name of the actAsAdminAction. */
+QString actionName()
+{
+    return i18nc("@action:inmenu", "Act as Administrator");
+};
+
+/** @returns the default keyboard shortcut of the actAsAdminAction. */
+QKeySequence actionDefaultShortcut()
+{
+    return Qt::CTRL | Qt::SHIFT | Qt::ALT | Qt::Key_A;
+};
+
+/** @returns whether any worker for the protocol "admin" is available. */
+bool isWorkerInstalled()
+{
+    return KProtocolInfo::isKnownProtocol(QStringLiteral("admin"));
+}
+}
+
+void Admin::guideUserTowardsInstallingAdminWorker()
+{
+    if (!isWorkerInstalled()) {
+        std::cout << qPrintable(
+            xi18nc("@info:shell",
+                   "<application>Dolphin</application> requires <application>%1</application> to manage system-controlled files, but it is not installed.<nl/>"
+                   "Press %2 to install <application>%1</application> or %3 to cancel.",
+                   ADMIN_WORKER_PACKAGE_NAME,
+                   QKeySequence{Qt::Key_Enter}.toString(QKeySequence::NativeText),
+                   QKeySequence{Qt::CTRL | Qt::Key_C}.toString(QKeySequence::NativeText)));
+        std::cin.ignore();
+
+        /// Installing admin worker
+        DolphinPackageInstaller adminWorkerInstaller{ADMIN_WORKER_PACKAGE_NAME, QUrl(QStringLiteral("appstream://org.kde.kio.admin")), isWorkerInstalled};
+        QObject::connect(&adminWorkerInstaller, &KJob::result, [](KJob *job) {
+            if (job->error()) {
+                std::cout << qPrintable(job->errorString()) << std::endl;
+                exit(1);
+            }
+        });
+        adminWorkerInstaller.exec();
+    }
+}
+
+void Admin::guideUserTowardsUsingAdminWorker()
+{
+    KuitSetup *kuitSetup = &Kuit::setupForDomain("dolphin");
+    kuitSetup->setTagPattern(QStringLiteral("numberedlist"), QStringList{}, Kuit::RichText, ki18nc("tag-format-pattern <numberedlist> rich", "<ol>%1</ol>"));
+    kuitSetup->setTagPattern(QStringLiteral("numbereditem"), QStringList{}, Kuit::RichText, ki18nc("tag-format-pattern <numbereditem> rich", "<li>%1</li>"));
+
+    KMessageBox::information(
+        nullptr,
+        xi18nc("@info",
+               "<para>Make use of your administrator rights in Dolphin:<numberedlist>"
+               "<numbereditem>Navigate to the file or folder you want to change.</numbereditem>"
+               "<numbereditem>Activate the \"%1\" action either under <interface>Open Menu|More|View</interface> or <interface>Menu Bar|View</interface>.<nl/>"
+               "Default shortcut: <shortcut>%2</shortcut></numbereditem>"
+               "<numbereditem>After authorization you can manage files as an administrator.</numbereditem></numberedlist></para>",
+               actionName(),
+               actionDefaultShortcut().toString(QKeySequence::NativeText)),
+        i18nc("@title:window", "How to Administrate"),
+        "",
+        KMessageBox::WindowModal);
+}
+
 QString Admin::warningMessage()
 {
     return xi18nc(
@@ -52,12 +123,12 @@ WorkerIntegration::WorkerIntegration(DolphinMainWindow *parent, QAction *actAsAd
 void WorkerIntegration::createActAsAdminAction(KActionCollection *actionCollection, DolphinMainWindow *dolphinMainWindow)
 {
     Q_ASSERT(!instance);
-    if (KProtocolInfo::isKnownProtocol(QStringLiteral("admin"))) {
+    if (isWorkerInstalled()) {
         QAction *actAsAdminAction = actionCollection->addAction(QStringLiteral("act_as_admin"));
-        actAsAdminAction->setText(i18nc("@action:inmenu", "Act as Administrator"));
+        actAsAdminAction->setText(actionName());
         actAsAdminAction->setIcon(QIcon::fromTheme(QStringLiteral("system-switch-user")));
         actAsAdminAction->setCheckable(true);
-        actionCollection->setDefaultShortcut(actAsAdminAction, Qt::CTRL | Qt::SHIFT | Qt::ALT | Qt::Key_A);
+        actionCollection->setDefaultShortcut(actAsAdminAction, actionDefaultShortcut());
 
         instance = new WorkerIntegration(dolphinMainWindow, actAsAdminAction);
     }
index 51230374460677531680330bcb4bf1d8a37197dd..0c87c2ecf28228780b86c91c45bad623608bf4fd 100644 (file)
@@ -22,6 +22,15 @@ class QUrl;
  */
 namespace Admin
 {
+/**
+ * When a user starts Dolphin with arguments that imply that they want to use administrative rights, this method is called.
+ * This function acts like a command line program that guides users towards installing kio-admin. It will not return until this is accomplished.
+ * This function will do nothing if kio-admin is already installed.
+ */
+void guideUserTowardsInstallingAdminWorker();
+
+void guideUserTowardsUsingAdminWorker();
+
 /**
  * Used with the KMessageBox API so users can disable the warning.
  * @see KMessageBox::saveDontShowAgainContinue()
index 797ea38c5996425f2dee20d8937c3c1a17d6aaa2..903b7e7db96ad678ff0a1f04c11d7ea3ff385991 100644 (file)
@@ -1,6 +1,10 @@
+/** Set whether to build Dolphin with support for these technologies or not. */
 #cmakedefine01 HAVE_BALOO
 #cmakedefine01 HAVE_PLASMA_ACTIVITIES
 #cmakedefine01 HAVE_KUSERFEEDBACK
 #cmakedefine01 HAVE_PACKAGEKIT
 #cmakedefine01 HAVE_TERMINAL
 #cmakedefine01 HAVE_X11
+
+/** The name of the package that needs to be installed so URLs starting with "admin:" can be opened in Dolphin. */
+#cmakedefine ADMIN_WORKER_PACKAGE_NAME "@ADMIN_WORKER_PACKAGE_NAME@"
diff --git a/src/dolphinpackageinstaller.cpp b/src/dolphinpackageinstaller.cpp
new file mode 100644 (file)
index 0000000..b701596
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+    This file is part of the KDE project
+    SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "dolphinpackageinstaller.h"
+
+#include <KLocalizedString>
+
+#if HAVE_PACKAGEKIT
+#include <PackageKit/Daemon>
+#else
+#include <QDesktopServices>
+#endif
+
+#include <QTimer>
+#include <QtAssert>
+
+DolphinPackageInstaller::DolphinPackageInstaller(const QString &packageName,
+                                                 const QUrl &fallBackInstallationPageUrl,
+                                                 std::function<bool()> isPackageInstalledCheck,
+                                                 QObject *parent)
+    : KJob(parent)
+    , m_packageName{packageName}
+    , m_fallBackInstallationPageUrl{fallBackInstallationPageUrl}
+    , m_isPackageInstalledCheck{isPackageInstalledCheck}
+{
+}
+
+void DolphinPackageInstaller::start()
+{
+    if (m_isPackageInstalledCheck()) {
+        emitResult();
+        return;
+    }
+
+#if HAVE_PACKAGEKIT
+    PackageKit::Daemon::setHints(PackageKit::Daemon::hints() + QStringList{QStringLiteral("interactive=true")});
+    const PackageKit::Transaction *resolveTransaction = PackageKit::Daemon::resolve(m_packageName);
+
+    connect(resolveTransaction, &PackageKit::Transaction::errorCode, this, &DolphinPackageInstaller::slotInstallationFailed);
+    connect(resolveTransaction, &PackageKit::Transaction::finished, this, [this]() { // Will be disconnected if we find a package.
+        slotInstallationFailed(PackageKit::Transaction::ErrorPackageNotFound,
+                               i18nc("@info:shell about system packages", "Could not find package %1.", m_packageName));
+    });
+    connect(resolveTransaction,
+            &PackageKit::Transaction::package,
+            this,
+            [this, resolveTransaction](PackageKit::Transaction::Info /* info */, const QString &packageId) {
+                disconnect(resolveTransaction, nullptr, this, nullptr); // We only care about the first package.
+                install(packageId);
+            });
+#else
+    QDesktopServices::openUrl(m_fallBackInstallationPageUrl);
+    auto waitForSuccess = new QTimer(this);
+    connect(waitForSuccess, &QTimer::timeout, this, [this]() {
+        if (m_isPackageInstalledCheck()) {
+            emitResult();
+        }
+    });
+    waitForSuccess->start(3000);
+#endif
+}
+
+#if HAVE_PACKAGEKIT
+void DolphinPackageInstaller::install(const QString &packageId)
+{
+    const PackageKit::Transaction *installTransaction = PackageKit::Daemon::installPackage(packageId);
+    connectTransactionToJobProgress(*installTransaction);
+    connect(installTransaction,
+            &PackageKit::Transaction::errorCode,
+            this,
+            [installTransaction, this](PackageKit::Transaction::Error error, const QString &details) {
+                disconnect(installTransaction, nullptr, this, nullptr); // We only want to emit a result once.
+                slotInstallationFailed(error, details);
+            });
+    connect(installTransaction,
+            &PackageKit::Transaction::finished,
+            this,
+            [installTransaction, this](const PackageKit::Transaction::Exit status, uint /* runtime */) {
+                disconnect(installTransaction, nullptr, this, nullptr); // We only want to emit a result once.
+                if (status == PackageKit::Transaction::ExitSuccess) {
+                    emitResult();
+                } else {
+                    slotInstallationFailed(PackageKit::Transaction::ErrorUnknown,
+                                           i18nc("@info %1 is error code",
+                                                 "Installation exited without reporting success. (%1)",
+                                                 QMetaEnum::fromType<PackageKit::Transaction::Exit>().valueToKey(status)));
+                }
+            });
+}
+
+void DolphinPackageInstaller::connectTransactionToJobProgress(const PackageKit::Transaction &transaction)
+{
+    connect(&transaction, &PackageKit::Transaction::speedChanged, this, [this, &transaction]() {
+        emitSpeed(transaction.speed());
+    });
+    connect(&transaction, &PackageKit::Transaction::percentageChanged, this, [this, &transaction]() {
+        setPercent(transaction.percentage());
+    });
+}
+
+void DolphinPackageInstaller::slotInstallationFailed(PackageKit::Transaction::Error error, const QString &details)
+{
+    setErrorString(xi18nc("@info:shell %1 is package name, %2 is error message, %3 is error e.g. 'ErrorNoNetwork'",
+                          "Installing <application>%1</application> failed: %2 (%3)<nl/>Please try installing <application>%1</application> manually instead.",
+                          m_packageName,
+                          details,
+                          QMetaEnum::fromType<PackageKit::Transaction::Error>().valueToKey(error)));
+    setError(error);
+    emitResult();
+}
+#endif
diff --git a/src/dolphinpackageinstaller.h b/src/dolphinpackageinstaller.h
new file mode 100644 (file)
index 0000000..0cb694a
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+    This file is part of the KDE project
+    SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef dolphinpackageinstaller_H
+#define dolphinpackageinstaller_H
+
+#include "config-dolphin.h"
+
+#if HAVE_PACKAGEKIT
+#include <PackageKit/Transaction>
+#endif
+#include <KJob>
+
+#include <QUrl>
+
+/**
+ * @brief A KJob providing simple means to install a package.
+ */
+class DolphinPackageInstaller : public KJob
+{
+public:
+    /**
+     * @brief Installs a system package.
+     *
+     * @param packageName                   A name that can be resolved to a package.
+     * @param fallBackInstallationPageUrl   This url will be opened if Dolphin was installed without the PackageKit library. A good choice for this parameter
+     *                                      is an appstream url that will be opened in a software store like Discover
+     *                                      e.g. "appstream://org.kde.filelight.desktop". The user is then expected to install the package themselves and
+     *                                      KJob::result() will be emitted when it is detected that the installation finished successfully.
+     * @param isPackageInstalledCheck       A function that can be regularly checked to determine if the installation was already successful.
+     */
+    explicit DolphinPackageInstaller(const QString &packageName,
+                                     const QUrl &fallBackInstallationPageUrl,
+                                     std::function<bool()> isPackageInstalledCheck,
+                                     QObject *parent = nullptr);
+
+    /**
+     * @see KJob::start().
+     * Make sure to connect to the KJob::result() signal and show the KJob::errorString() to users there before calling this.
+     */
+    void start() override;
+
+    /** @see KJob::errorString(). */
+    inline QString errorString() const override
+    {
+        return m_errorString;
+    };
+
+private:
+    /** @see KJob::errorString(). */
+    inline void setErrorString(const QString &errorString)
+    {
+        m_errorString = errorString;
+    };
+
+#if HAVE_PACKAGEKIT
+    /**
+     * Asynchronously installs a package uniquely identified by its @param packageId using PackageKit.
+     * For progress reporting this method will use DolphinPackageInstaller::connectTransactionToJobProgress().
+     * This method will call KJob::emitResult() once it failed or succeeded.
+     */
+    void install(const QString &packageId);
+
+    /**
+     * Makes sure progress signals of @p transaction are forwarded to KJob's progress signals.
+     */
+    void connectTransactionToJobProgress(const PackageKit::Transaction &transaction);
+
+private Q_SLOTS:
+    /** Creates a nice user-facing error message from its parameters and then finishes this job with an @p error. */
+    void slotInstallationFailed(PackageKit::Transaction::Error error, const QString &details);
+#endif
+
+private:
+    /** The name of the package that is supposed to be installed. */
+    const QString m_packageName;
+
+    /** @see DolphinPackageInstaller::DolphinPackageInstaller(). */
+    const QUrl m_fallBackInstallationPageUrl;
+
+    /** @see DolphinPackageInstaller::DolphinPackageInstaller(). */
+    const std::function<bool()> m_isPackageInstalledCheck;
+
+    /** @see KJob::errorString(). */
+    QString m_errorString;
+};
+
+#endif // dolphinpackageinstaller_H
index 6df53b62ebe3c2405b8a10460f1dd945a5ff8e82..d68cc3cc1e8f429d06f8235b8629598038b9999d 100644 (file)
@@ -6,6 +6,7 @@
  * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
+#include "admin/workerintegration.h"
 #include "config-dolphin.h"
 #include "dbusinterface.h"
 #include "dolphin_generalsettings.h"
 #endif
 #include <iostream>
 
+constexpr auto dolphinTranslationDomain{"dolphin"};
+
 int main(int argc, char **argv)
 {
 #ifndef Q_OS_WIN
     // Prohibit using sudo or kdesu (but allow using the root user directly)
-    if (getuid() == 0) {
-        if (!qEnvironmentVariableIsEmpty("SUDO_USER")) {
-            std::cout << "Running Dolphin with sudo is not supported as it can cause bugs and expose you to security vulnerabilities. Instead, install the "
-                         "`kio-admin` package from your distro and use it to manage root-owned locations by right-clicking on them and selecting \"Open as "
-                         "Administrator\"."
-                      << std::endl;
-            return EXIT_FAILURE;
-        } else if (!qEnvironmentVariableIsEmpty("KDESU_USER")) {
-            std::cout << "Running Dolphin with kdesu is not supported as it can cause bugs and expose you to security vulnerabilities. Instead, install the "
-                         "`kio-admin` package from your distro and use it to manage root-owned locations by right-clicking on them and selecting \"Open as "
-                         "Administrator\"."
-                      << std::endl;
-            return EXIT_FAILURE;
-        }
+    if (getuid() == 0 && (!qEnvironmentVariableIsEmpty("SUDO_USER") || !qEnvironmentVariableIsEmpty("KDESU_USER"))) {
+        QCoreApplication app(argc, argv); // Needed for the xi18ndc() call below.
+        std::cout << qPrintable(
+            xi18ndc(dolphinTranslationDomain,
+                    "@info:shell %1 is a terminal command",
+                    "Running <application>Dolphin</application> with <command>sudo</command> is discouraged. Please run <icode>%1</icode> instead.",
+                    QStringLiteral("dolphin --sudo")))
+                  << std::endl;
+        // We could perform a privilege de-escalation here and continue as normal. It is a bit safer though to simply let the user restart without sudo.
+        return EXIT_FAILURE;
     }
 #endif
 
@@ -116,7 +115,7 @@ int main(int argc, char **argv)
     migrate.migrate();
 #endif
 
-    KLocalizedString::setApplicationDomain("dolphin");
+    KLocalizedString::setApplicationDomain(dolphinTranslationDomain);
 
     KAboutData aboutData(QStringLiteral("dolphin"),
                          i18n("Dolphin"),
@@ -164,6 +163,8 @@ int main(int argc, char **argv)
                                               "will be selected.")));
     parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("split"), i18nc("@info:shell", "Dolphin will get started with a split view.")));
     parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("new-window"), i18nc("@info:shell", "Dolphin will explicitly open in a new window.")));
+    parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("sudo") << QStringLiteral("admin"),
+                                        i18nc("@info:shell", "Set up Dolphin for administrative tasks.")));
     parser.addOption(
         QCommandLineOption(QStringList() << QStringLiteral("daemon"), i18nc("@info:shell", "Start Dolphin Daemon (only required for DBus Interface).")));
     parser.addPositionalArgument(QStringLiteral("+[Url]"), i18nc("@info:shell", "Document to open"));
@@ -173,11 +174,18 @@ int main(int argc, char **argv)
 
     const bool splitView = parser.isSet(QStringLiteral("split")) || GeneralSettings::splitView();
     const bool openFiles = parser.isSet(QStringLiteral("select"));
+    const bool adminWorkerInfoWanted = parser.isSet(QStringLiteral("sudo")) || parser.isSet(QStringLiteral("admin"));
     const QStringList args = parser.positionalArguments();
     QList<QUrl> urls = Dolphin::validateUris(args);
     // We later mutate urls, so we need to store if it was empty originally
     const bool startedWithURLs = !urls.isEmpty();
 
+    if (adminWorkerInfoWanted || std::any_of(urls.cbegin(), urls.cend(), [](const QUrl &url) {
+            return url.scheme() == QStringLiteral("admin");
+        })) {
+        Admin::guideUserTowardsInstallingAdminWorker();
+    }
+
     if (parser.isSet(QStringLiteral("daemon"))) {
         // Disable session management for the daemonized version
         // See https://bugs.kde.org/show_bug.cgi?id=417219
@@ -275,6 +283,10 @@ int main(int argc, char **argv)
 
     mainWindow->setSessionAutoSaveEnabled(GeneralSettings::rememberOpenedTabs());
 
+    if (adminWorkerInfoWanted) {
+        Admin::guideUserTowardsUsingAdminWorker();
+    }
+
 #if HAVE_KUSERFEEDBACK
     auto feedbackProvider = DolphinFeedbackProvider::instance();
     Q_UNUSED(feedbackProvider)