diff --git a/.reuse/dep5 b/.reuse/dep5 index 6d1808dff6..ecc04c0557 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -6,3 +6,7 @@ Source: https://invent.kde.org/plasma/plasma-desktop/ Files: emojier/data/*.dict Copyright: Unicode, Inc. License: Unicode-DFS-2016 + +Files: kcms/runners/plugininstaller/autotests +Copyright: Alexander Lohnau +License: CC0-1.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index e0a0d521ea..d83d4cda63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,10 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS IconThemes XmlGui ) +if(BUILD_TESTING) + find_package(Qt6Test ${QT_MIN_VERSION} CONFIG REQUIRED) + include(ECMAddTests) +endif() # TODO re-enable once KAccounts is ported to Qt6 # find_package(AccountsQt6 ${ACCOUNTSQT_DEP_VERSION} CONFIG) diff --git a/containments/desktop/plugins/folder/autotests/CMakeLists.txt b/containments/desktop/plugins/folder/autotests/CMakeLists.txt index d17cc32e39..71f828baf2 100644 --- a/containments/desktop/plugins/folder/autotests/CMakeLists.txt +++ b/containments/desktop/plugins/folder/autotests/CMakeLists.txt @@ -1,7 +1,3 @@ -find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED Test) - -include(ECMAddTests) - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) ecm_add_tests( diff --git a/kcms/keyboard/CMakeLists.txt b/kcms/keyboard/CMakeLists.txt index 01f39b709a..a97ee0a182 100644 --- a/kcms/keyboard/CMakeLists.txt +++ b/kcms/keyboard/CMakeLists.txt @@ -145,7 +145,6 @@ configure_file(config-keyboard.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keyboa # Unit tests if(BUILD_TESTING) - find_package(Qt6Test ${QT_MIN_VERSION} CONFIG REQUIRED) add_subdirectory( tests ) endif() diff --git a/kcms/runners/plugininstaller/CMakeLists.txt b/kcms/runners/plugininstaller/CMakeLists.txt index 16c0c618d8..0f95ffb73e 100644 --- a/kcms/runners/plugininstaller/CMakeLists.txt +++ b/kcms/runners/plugininstaller/CMakeLists.txt @@ -2,18 +2,20 @@ # SPDX-License-Identifier: BSD-3-Clause add_executable(krunner-plugininstaller - main.cpp - AbstractJob.cpp - ScriptJob.cpp - ZypperRPMJob.cpp) + main.cpp + AbstractJob.cpp + ScriptJob.cpp + ZypperRPMJob.cpp + KRunnerPluginInstallerRcJob.cpp +) if(HAVE_PACKAGEKIT) target_sources(krunner-plugininstaller PUBLIC PackageKitJob.cpp) endif() target_link_libraries(krunner-plugininstaller + Qt::Widgets KF6::CoreAddons KF6::I18n - Qt::Widgets KF6::Service KF6::KIOCore KF6::KIOWidgets @@ -22,4 +24,8 @@ if(HAVE_PACKAGEKIT) target_link_libraries(krunner-plugininstaller PK::packagekitqt6) endif() +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() + install(TARGETS krunner-plugininstaller DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/kcms/runners/plugininstaller/KRunnerPluginInstallerRcJob.cpp b/kcms/runners/plugininstaller/KRunnerPluginInstallerRcJob.cpp new file mode 100644 index 0000000000..485f66ed1b --- /dev/null +++ b/kcms/runners/plugininstaller/KRunnerPluginInstallerRcJob.cpp @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2023 Alexander Lohnau + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "KRunnerPlugininstallerRcJob.h" + +#include "ScriptConfirmationDialog.h" +#include +#include +#include +#include +#include + +static QString installDBusServicePath(const QString &dbusService) +{ + const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + QDir dbusDir(dataLocation + "/dbus-1/services/"); + dbusDir.mkpath((".")); + return dbusDir.filePath(dbusService + ".service"); +} + +static QString installMetaDataPath(const QString &filePath) +{ + const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + QDir krunnerPluginsDir(dataLocation + "/krunner/dbusplugins/"); + krunnerPluginsDir.mkpath((".")); + // Get the filename in case the .krunner-plugininstallerrc has specified the file to be in a subdir + return krunnerPluginsDir.filePath(QFileInfo(filePath).fileName()); +} + +static InstallerInfo getInstallerInfo(const QFileInfo dirInfo) +{ + const QDir rootDir = dirInfo.absoluteDir(); + QString installerConfigFile = dirInfo.filePath(); + Q_ASSERT(installerConfigFile.endsWith("krunner-plugininstallerrc")); + + KConfig cfg(installerConfigFile); + KConfigGroup grp = cfg.group(""); + const QString exec = grp.readEntry("Exec").replace(QStringLiteral("%{PROJECTDIR}"), dirInfo.absolutePath()); + const QString metaDataFile = grp.readEntry("MetaDataFile"); + KDesktopFile desktopFile(rootDir.absoluteFilePath(metaDataFile)); + QString dbusService = desktopFile.desktopGroup().readEntry("X-Plasma-DBusRunner-Service"); + + return {exec, dbusService, rootDir.absoluteFilePath(metaDataFile)}; +} + +void KRunnerPluginInstallerRcJob::executeOperation(const QFileInfo &fileInfo, const QString & /*mimeType*/, bool install) +{ + const InstallerInfo info = getInstallerInfo(fileInfo); + if (info.exec.isEmpty() || info.desktopFilePath.isEmpty() || info.desktopFilePath.isEmpty()) { + Q_EMIT error("The provided installer file is invalid. Please contact the author."); + return; + } + if (install) { + if (m_skipConfirmDialog || ScriptConfirmationDialog(QString(), install, fileInfo.absolutePath(), info).exec() == QDialog::Accepted) { + doInstall(info); + } else { + Q_EMIT error(QString()); + } + } else { + doUninstall(info); + } +} + +void KRunnerPluginInstallerRcJob::doInstall(const InstallerInfo &info) +{ + QFile serviceFile(installDBusServicePath(info.dbusService)); + + if (serviceFile.open(QIODevice::WriteOnly)) { + serviceFile.write( + QLatin1String("# autogenerated by krunner-plugininstaller\n[D-BUS Service]\nName=%1\nExec=%2\n").arg(info.dbusService, info.exec).toLocal8Bit()); + serviceFile.close(); + qInfo() << "Wrote autostart file" << serviceFile.fileName(); + } else { + Q_EMIT error(serviceFile.errorString()); + return; + } + + const QString targetFilePath = installMetaDataPath(info.desktopFilePath); + // In case we update an entry, simply remove a previous file + if (QFileInfo::exists(targetFilePath)) { + qInfo() << "Trying to remove old desktop file" << targetFilePath; + QFile::remove(targetFilePath); + } + + if (!QFile::copy(info.desktopFilePath, targetFilePath)) { + qWarning() << "Failed to copy " << info.desktopFilePath << targetFilePath; + Q_EMIT error(i18n("Failed to copy file %1 to %2", info.desktopFilePath, targetFilePath)); + } else { + qInfo() << "Copied desktop file to" << targetFilePath; + Q_EMIT finished(); + } +} +void KRunnerPluginInstallerRcJob::doUninstall(const InstallerInfo &info) +{ + QFile dbusFile(installDBusServicePath(info.dbusService)); + QFile metaDataFile(installMetaDataPath(info.desktopFilePath)); + + if (metaDataFile.remove()) { + if (dbusFile.remove()) { + Q_EMIT finished(); + } else { + Q_EMIT error(xi18nc("@info", "Failed to remove file %1:%2", dbusFile.fileName(), dbusFile.errorString())); + } + } else { + Q_EMIT error(xi18nc("@info", "Failed to remove file %1:%2", metaDataFile.fileName(), metaDataFile.errorString())); + } +} diff --git a/kcms/runners/plugininstaller/KRunnerPlugininstallerRcJob.h b/kcms/runners/plugininstaller/KRunnerPlugininstallerRcJob.h new file mode 100644 index 0000000000..b4d402be66 --- /dev/null +++ b/kcms/runners/plugininstaller/KRunnerPlugininstallerRcJob.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2023 Alexander Lohnau + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "AbstractJob.h" +struct InstallerInfo; + +class KRunnerPluginInstallerRcJob : public AbstractJob +{ +public: + explicit KRunnerPluginInstallerRcJob(bool skipConfigmDialog) + : m_skipConfirmDialog(skipConfigmDialog) + { + } + void executeOperation(const QFileInfo &fileInfo, const QString &mimeType, bool install) override; + +private: + void doInstall(const InstallerInfo &info); + void doUninstall(const InstallerInfo &info); + const bool m_skipConfirmDialog; +}; diff --git a/kcms/runners/plugininstaller/ScriptConfirmationDialog.h b/kcms/runners/plugininstaller/ScriptConfirmationDialog.h index 429eb8bc02..5aa0065f81 100644 --- a/kcms/runners/plugininstaller/ScriptConfirmationDialog.h +++ b/kcms/runners/plugininstaller/ScriptConfirmationDialog.h @@ -4,6 +4,8 @@ SPDX-License-Identifier: LGPL-2.0-or-later */ +#include +#include #include #include #include @@ -14,11 +16,21 @@ #include #include #include +#include +struct InstallerInfo { + QString exec; + QString dbusService; + QString desktopFilePath; +}; class ScriptConfirmationDialog : public QDialog { public: - ScriptConfirmationDialog(const QString &installerPath, bool install, const QString &dir, QWidget *parent = nullptr) + ScriptConfirmationDialog(const QString &installerPath, + bool install, + const QString &dir, + const std::optional &info = std::nullopt, + QWidget *parent = nullptr) : QDialog(parent) { const auto readmes = QDir(dir).entryList({QStringLiteral("README*")}); @@ -27,7 +39,10 @@ public: const bool noInstaller = installerPath.isEmpty(); QVBoxLayout *layout = new QVBoxLayout(this); QString msg; - if (!install && noInstaller && readmes.isEmpty()) { + if (info) { + Q_ASSERT(install); // We do not need the warnings for uninstall + msg = xi18nc("@info", "This plugin executes the following command for starting the \"%1\" service:%2", info->dbusService, info->exec); + } else if (!install && noInstaller && readmes.isEmpty()) { msg = xi18nc("@info", "This plugin does not provide an uninstallation script. Please contact the author. " "You can try to uninstall the plugin manually." @@ -71,9 +86,9 @@ public: connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QString okText; - if (noInstaller && !install) { + if (noInstaller && !install && !info) { okText = i18nc("@action:button", "Mark entry as uninstalled"); - } else if (noInstaller) { + } else if (noInstaller && !info) { okText = i18nc("@action:button", "Mark entry as installed"); } else { okText = i18nc("@action:button", "Accept Risk And Continue"); diff --git a/kcms/runners/plugininstaller/autotests/CMakeLists.txt b/kcms/runners/plugininstaller/autotests/CMakeLists.txt new file mode 100644 index 0000000000..933d9ae410 --- /dev/null +++ b/kcms/runners/plugininstaller/autotests/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2022 Alexander Lohnau +# SPDX-License-Identifier: BSD-3-Clause +ecm_add_test(plugininstallerrctest.cpp) +target_link_libraries(plugininstallerrctest Qt::Test) diff --git a/kcms/runners/plugininstaller/autotests/plugininstallerrctest.cpp b/kcms/runners/plugininstaller/autotests/plugininstallerrctest.cpp new file mode 100644 index 0000000000..6fea05711d --- /dev/null +++ b/kcms/runners/plugininstaller/autotests/plugininstallerrctest.cpp @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2023 Alexander Lohnau + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include +#include +#include +#include +#include +#include + +bool fileContainsLine(const QString &fileName, const QString &line) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + + QTextStream in(&file); + while (!in.atEnd()) { + if (in.readLine() == line) { + return true; + } + } + + return false; +} + +class PluginInstallerRcTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + QStandardPaths::setTestModeEnabled(true); + qputenv("PLUGININSTALLER_TEST_MODE", "true"); + } + void testInstall() + { + const QString executable = QFINDTESTDATA("krunner-plugininstaller"); + QProcess installProcess; + installProcess.start(executable, {"install", QFINDTESTDATA("testplugin"), "--no-confirm"}); + + installProcess.waitForFinished(); + qDebug() << installProcess.readAllStandardError(); + QCOMPARE(installProcess.exitCode(), 0); + + const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + const QString desktopLocation = dataLocation + "/krunner/dbusplugins/org.kde.testplugin.desktop"; + const QString serviceLocation = dataLocation + "/dbus-1/services/org.kde.testplugin.service"; + QVERIFY(QFileInfo::exists(desktopLocation)); + QVERIFY(QFileInfo::exists(serviceLocation)); + QVERIFY(fileContainsLine(desktopLocation, "X-Plasma-DBusRunner-Service=org.kde.testplugin")); + QVERIFY(fileContainsLine(serviceLocation, QLatin1String("Exec=python3 %1/main.py").arg(QFINDTESTDATA("testplugin")))); + + QProcess uninstallProcess; + uninstallProcess.start(executable, {"uninstall", QFINDTESTDATA("testplugin")}); + + uninstallProcess.waitForFinished(); + qDebug() << uninstallProcess.readAllStandardError(); + QVERIFY(!QFileInfo::exists(desktopLocation)); + QVERIFY(!QFileInfo::exists(serviceLocation)); + } +}; + +QTEST_MAIN(PluginInstallerRcTest) + +#include "plugininstallerrctest.moc" diff --git a/kcms/runners/plugininstaller/autotests/testplugin/krunner-plugininstallerrc b/kcms/runners/plugininstaller/autotests/testplugin/krunner-plugininstallerrc new file mode 100644 index 0000000000..fc97491bc9 --- /dev/null +++ b/kcms/runners/plugininstaller/autotests/testplugin/krunner-plugininstallerrc @@ -0,0 +1,2 @@ +MetaDataFile=org.kde.testplugin.desktop +Exec=python3 %{PROJECTDIR}/main.py diff --git a/kcms/runners/plugininstaller/autotests/testplugin/main.py b/kcms/runners/plugininstaller/autotests/testplugin/main.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kcms/runners/plugininstaller/autotests/testplugin/org.kde.testplugin.desktop b/kcms/runners/plugininstaller/autotests/testplugin/org.kde.testplugin.desktop new file mode 100644 index 0000000000..a0d71ac87f --- /dev/null +++ b/kcms/runners/plugininstaller/autotests/testplugin/org.kde.testplugin.desktop @@ -0,0 +1,2 @@ +[Desktop Entry] +X-Plasma-DBusRunner-Service=org.kde.testplugin diff --git a/kcms/runners/plugininstaller/main.cpp b/kcms/runners/plugininstaller/main.cpp index 4b48a04c0b..37e859942e 100644 --- a/kcms/runners/plugininstaller/main.cpp +++ b/kcms/runners/plugininstaller/main.cpp @@ -1,6 +1,5 @@ /* SPDX-FileCopyrightText: 2020 Alexander Lohnau - SPDX-License-Identifier: LGPL-2.0-or-later */ @@ -10,10 +9,13 @@ #include #include #include +#include #include #include +#include #include +#include "KRunnerPlugininstallerRcJob.h" #include "config-workspace.h" #if HAVE_PACKAGEKIT #include "PackageKitJob.h" @@ -38,9 +40,15 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); app.setQuitOnLastWindowClosed(false); + if (qEnvironmentVariableIsSet("PLUGININSTALLER_TEST_MODE")) { + QStandardPaths::setTestModeEnabled(true); + } + QCommandLineParser parser; parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall.")); parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive.")); + QCommandLineOption noConfirm(QStringLiteral("no-confirm"), i18nc("@info:shell", "Do not show a visual confirmation dialog")); + parser.addOption(noConfirm); parser.process(app); const QStringList args = parser.positionalArguments(); @@ -82,6 +90,9 @@ int main(int argc, char *argv[]) #else fail(i18nc("@info", "No PackageKit support")); #endif + } else if (const QString rcPath = QDir(file).filePath("krunner-plugininstallerrc"); fileInfo.isDir() && QFileInfo::exists(rcPath)) { + fileInfo = QFileInfo(rcPath); + job.reset(new KRunnerPluginInstallerRcJob(parser.isSet(noConfirm))); } else { job.reset(new ScriptJob()); }