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)
include(ECMAddAppIcon)
+set(ADMIN_WORKER_PACKAGE_NAME "kio-admin")
configure_file(config-dolphin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-dolphin.h)
add_definitions(
dolphincontextmenu.cpp
dolphinnavigatorswidgetaction.cpp
dolphintabbar.cpp
+ dolphinpackageinstaller.cpp
dolphinplacesmodelsingleton.cpp
dolphinrecenttabsmenu.cpp
dolphintabpage.cpp
dolphincontextmenu.h
dolphinnavigatorswidgetaction.h
dolphintabbar.h
+ dolphinpackageinstaller.h
dolphinplacesmodelsingleton.h
dolphinrecenttabsmenu.h
dolphintabpage.h
)
endif()
+if(HAVE_PACKAGEKIT)
+ target_link_libraries(
+ dolphinstatic
+ PK::packagekitqt6
+ )
+endif()
+
if (HAVE_KUSERFEEDBACK)
target_link_libraries(
dolphinstatic
#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(
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);
}
*/
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()
+/** 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@"
--- /dev/null
+/*
+ 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
--- /dev/null
+/*
+ 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
* 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
migrate.migrate();
#endif
- KLocalizedString::setApplicationDomain("dolphin");
+ KLocalizedString::setApplicationDomain(dolphinTranslationDomain);
KAboutData aboutData(QStringLiteral("dolphin"),
i18n("Dolphin"),
"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"));
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
mainWindow->setSessionAutoSaveEnabled(GeneralSettings::rememberOpenedTabs());
+ if (adminWorkerInfoWanted) {
+ Admin::guideUserTowardsUsingAdminWorker();
+ }
+
#if HAVE_KUSERFEEDBACK
auto feedbackProvider = DolphinFeedbackProvider::instance();
Q_UNUSED(feedbackProvider)