diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a4018351b..d7f1cb57af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f11c33535..fe9ac1f451 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/admin/workerintegration.cpp b/src/admin/workerintegration.cpp index f9b5873919..038c520943 100644 --- a/src/admin/workerintegration.cpp +++ b/src/admin/workerintegration.cpp @@ -7,7 +7,9 @@ #include "workerintegration.h" +#include "config-dolphin.h" #include "dolphinmainwindow.h" +#include "dolphinpackageinstaller.h" #include "dolphinviewcontainer.h" #include @@ -18,8 +20,77 @@ #include +#include + 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", + "Dolphin requires %1 to manage system-controlled files, but it is not installed." + "Press %2 to install %1 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 rich", "
    %1
")); + kuitSetup->setTagPattern(QStringLiteral("numbereditem"), QStringList{}, Kuit::RichText, ki18nc("tag-format-pattern rich", "
  • %1
  • ")); + + KMessageBox::information( + nullptr, + xi18nc("@info", + "Make use of your administrator rights in Dolphin:" + "Navigate to the file or folder you want to change." + "Activate the \"%1\" action either under Open Menu|More|View or Menu Bar|View." + "Default shortcut: %2" + "After authorization you can manage files as an administrator.", + 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); } diff --git a/src/admin/workerintegration.h b/src/admin/workerintegration.h index 5123037446..0c87c2ecf2 100644 --- a/src/admin/workerintegration.h +++ b/src/admin/workerintegration.h @@ -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() diff --git a/src/config-dolphin.h.cmake b/src/config-dolphin.h.cmake index 797ea38c59..903b7e7db9 100644 --- a/src/config-dolphin.h.cmake +++ b/src/config-dolphin.h.cmake @@ -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 index 0000000000..b70159663d --- /dev/null +++ b/src/dolphinpackageinstaller.cpp @@ -0,0 +1,115 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2024 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "dolphinpackageinstaller.h" + +#include + +#if HAVE_PACKAGEKIT +#include +#else +#include +#endif + +#include +#include + +DolphinPackageInstaller::DolphinPackageInstaller(const QString &packageName, + const QUrl &fallBackInstallationPageUrl, + std::function 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().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 %1 failed: %2 (%3)Please try installing %1 manually instead.", + m_packageName, + details, + QMetaEnum::fromType().valueToKey(error))); + setError(error); + emitResult(); +} +#endif diff --git a/src/dolphinpackageinstaller.h b/src/dolphinpackageinstaller.h new file mode 100644 index 0000000000..0cb694a19b --- /dev/null +++ b/src/dolphinpackageinstaller.h @@ -0,0 +1,92 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2024 Felix Ernst + + 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 +#endif +#include + +#include + +/** + * @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 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 m_isPackageInstalledCheck; + + /** @see KJob::errorString(). */ + QString m_errorString; +}; + +#endif // dolphinpackageinstaller_H diff --git a/src/main.cpp b/src/main.cpp index 6df53b62eb..d68cc3cc1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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" @@ -54,24 +55,22 @@ #endif #include +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 Dolphin with sudo is discouraged. Please run %1 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 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)