Commit bccf7e7d authored by Alexander Lohnau's avatar Alexander Lohnau 💬

Add KRunner KDE Store integration

FEATURE: 422929
FIXED-IN: 5.21
parent 31cef000
......@@ -175,6 +175,16 @@ else()
set(HAVE_BREEZE_DECO FALSE)
endif()
find_package(PackageKitQt5)
set_package_properties(PackageKitQt5
PROPERTIES DESCRIPTION "Software Manager integration"
TYPE OPTIONAL
PURPOSE "Used in the KRunner plugin installer"
)
if(PackageKitQt5_FOUND)
set(HAVE_PACKAGEKIT TRUE)
endif()
include_directories("${CMAKE_CURRENT_BINARY_DIR}")
configure_file(config-workspace.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-workspace.h)
......
......@@ -150,6 +150,9 @@
#define BREEZE_KDECORATION_PLUGIN_ID "${BREEZE_KDECORATION_PLUGIN_ID}"
#endif
/* Define to 1 if you have packagekit available. */
#cmakedefine HAVE_PACKAGEKIT 1
/*
* On HP-UX, the declaration of vsnprintf() is needed every time !
*/
......
......@@ -6,6 +6,19 @@ set(kcm_search_SRCS
)
add_library(kcm_plasmasearch MODULE ${kcm_search_SRCS})
add_executable(plugininstaller
plugininstaller/main.cpp
plugininstaller/AbstractJob.cpp
plugininstaller/ScriptJob.cpp
plugininstaller/ZypperRPMJob.cpp)
if(HAVE_PACKAGEKIT)
target_sources(plugininstaller PUBLIC plugininstaller/PackageKitJob.cpp)
endif()
set_target_properties(plugininstaller PROPERTIES
OUTPUT_NAME "krunner-plugininstaller"
)
target_link_libraries(kcm_plasmasearch
KF5::KIOWidgets
......@@ -13,11 +26,24 @@ target_link_libraries(kcm_plasmasearch
KF5::KCMUtils
KF5::Runner
KF5::I18n
KF5::NewStuff
Qt5::DBus
Qt5::Widgets
)
target_link_libraries(plugininstaller
KF5::CoreAddons
KF5::I18n
Qt5::Widgets
KF5::Service
KF5::KIOCore
KF5::KIOWidgets
)
if(HAVE_PACKAGEKIT)
target_link_libraries(plugininstaller PK::packagekitqt5)
endif()
install(FILES kcm_plasmasearch.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR})
install(TARGETS kcm_plasmasearch DESTINATION ${KDE_INSTALL_PLUGINDIR})
install(FILES krunner.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR})
install(TARGETS plugininstaller DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
......@@ -27,7 +27,7 @@
#include <KLocalizedString>
#include <KRunner/RunnerManager>
#include <KPluginSelector>
#include <KNS3/Button>
#include <QApplication>
#include <QDBusMessage>
#include <QDBusConnection>
......@@ -126,6 +126,21 @@ SearchConfigModule::SearchConfigModule(QWidget* parent, const QVariantList& args
layout->addSpacing(12);
layout->addLayout(headerLayout);
layout->addWidget(m_pluginSelector);
QHBoxLayout *downloadLayout = new QHBoxLayout(this);
KNS3::Button *downloadButton = new KNS3::Button(i18n("Get New Plugins..."), QStringLiteral("krunner.knsrc"), this);
connect(downloadButton, &KNS3::Button::dialogFinished, this, [this](const KNS3::Entry::List &changedEntries) {
if (!changedEntries.isEmpty()) {
m_pluginSelector->clearPlugins();
m_pluginSelector->addPlugins(Plasma::RunnerManager::listRunnerInfo(),
KPluginSelector::ReadConfigFile,
i18n("Available Plugins"), QString(),
KSharedConfig::openConfig(QStringLiteral("krunnerrc")));
}
});
downloadLayout->addStretch();
downloadLayout->addWidget(downloadButton);
layout->addLayout(downloadLayout);
}
void SearchConfigModule::load()
......
# SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
# SPDX-License-Identifier: LGPL-2.0-or-later
[KNewStuff3]
ProvidersUrl=https://download.kde.org/ocs/providers.xml
Categories=App Runners,System Runners,Web Runners
ChecksumPolicy=ifpossible
SignaturePolicy=ifpossible
TargetDir=krunner-sources
Uncompress=subdir-archive
InstallationCommand=krunner-plugininstaller install %f
UninstallCommand=krunner-plugininstaller uninstall %f
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "AbstractJob.h"
#include <KConfig>
#include <KSharedConfig>
#include <KConfigGroup>
#include <KShell>
#include <KLocalizedString>
#include <QProcess>
#include <QDebug>
void AbstractJob::runScriptInTerminal(const QString &script, const QString &pwd)
{
KConfigGroup confGroup(KSharedConfig::openConfig(), "General");
QString exec = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
if (exec == QLatin1String("konsole")) {
exec += QLatin1String(" --noclose --separate");
} else if (exec == QLatin1String("xterm")) {
exec += QLatin1String(" -hold");
}
exec += QLatin1String(" -e ");
exec += KShell::quoteArg(script);
QProcess *process = new QProcess(this);
process->setWorkingDirectory(pwd);
// We don't know if the entry read from the config contains options
// so we just split it at the end
QStringList split = KShell::splitArgs(exec);
const QString program = split.takeFirst();
process->setProgram(program);
process->setArguments(split);
process->start();
connectSignals(process);
}
QString AbstractJob::terminalCloseMessage(bool install)
{
if (install) {
return i18nc("@info", "Installation executed successfully, you may now close this window");
} else {
return i18nc("@info", "Uninstallation executed successfully, you may now close this window");
}
}
void AbstractJob::connectSignals(QProcess *process)
{
connect(process, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
[this] (int, QProcess::ExitStatus exitStatus) {
if (exitStatus == QProcess::NormalExit) {
Q_EMIT finished();
}
});
connect(process, &QProcess::errorOccurred, this,
[this, process] (QProcess::ProcessError) {
Q_EMIT error(i18nc("@info", "Failed to run install script in terminal \"%1\"", process->program()));
});
}
#include "AbstractJob.moc"
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef ABSTRACTJOB_H
#define ABSTRACTJOB_H
#include <QObject>
class QFileInfo;
class QProcess;
class AbstractJob: public QObject
{
Q_OBJECT
public:
/**
* @param fileInfo QFileInfo of the file or directory
* @param mimeType Mime type of the file
* @param install Set to true if the entry should be installed, flase if it should be uninstalled
*/
virtual void executeOperation(const QFileInfo &fileInfo, const QString &mimeType, bool install) = 0;
Q_SIGNALS:
void finished();
void error(const QString &errorMessage);
protected:
void runScriptInTerminal(const QString &script, const QString &pwd);
QString terminalCloseMessage(bool install);
void connectSignals(QProcess *process);
};
#endif // ABSTRACTJOB_H
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QLabel>
#include <QIcon>
#include <KLocalizedString>
#include <KIO/OpenFileManagerWindowJob>
class PackageKitConfirmationDialog : public QDialog {
public:
PackageKitConfirmationDialog(const QString &packagePath, QWidget *parent = nullptr) : QDialog(parent)
{
setWindowTitle(i18nc("@title:window", "Confirm Installation"));
setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
QVBoxLayout *layout = new QVBoxLayout(this);
QString msg = xi18nc("@info", "You are about to install a binary package. You should only install these from a trusted author/packager.");
QLabel *msgLabel = new QLabel(msg, this);
msgLabel->setWordWrap(true);
layout->addWidget(msgLabel);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
buttonBox->button(QDialogButtonBox::Ok)->setIcon(QIcon::fromTheme("emblem-warning"));
buttonBox->button(QDialogButtonBox::Ok)->setText(i18nc("@action:button", "Accept Risk And Continue"));
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
QPushButton *highlightFileButton = new QPushButton(QIcon::fromTheme("document-open-folder"), i18nc("@action:button", "View File"), this);
connect(highlightFileButton, &QPushButton::clicked, this, [packagePath]() {
KIO::highlightInFileManager({QUrl::fromLocalFile(packagePath)});
});
buttonBox->addButton(highlightFileButton, QDialogButtonBox::HelpRole);
buttonBox->button(QDialogButtonBox::Cancel)->setFocus();
layout->addWidget(buttonBox);
}
};
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "PackageKitJob.h"
#include <QFileInfo>
#include <KShell>
#include <PackageKit/Daemon>
#include <PackageKit/Details>
#include <QRegularExpression>
#include <KOSRelease>
#include <QMimeDatabase>
#include <QDBusConnection>
#include <QProcess>
#include "PackageKitConfirmationDialog.h"
void PackageKitJob::executeOperation(const QFileInfo &fileInfo, const QString &mimeType, bool install)
{
if (!supportedPackagekitMimeTypes().contains(mimeType)) {
Q_EMIT error(i18nc("@info", "The mime type %1 is not supported by the packagekit backend", mimeType));
return;
}
if (install) {
PackageKitConfirmationDialog dlg(fileInfo.absoluteFilePath());
if (dlg.exec() == QDialog::Accepted) {
packageKitInstall(fileInfo.absoluteFilePath());
} else {
Q_EMIT error(QString());
}
} else {
packageKitUninstall(fileInfo.absoluteFilePath());
}
}
void PackageKitJob::packageKitInstall(const QString &fileName)
{
PackageKit::Transaction *transaction = PackageKit::Daemon::installFile(fileName, {});
connect(transaction, &PackageKit::Transaction::finished, this, &PackageKitJob::transactionFinished);
connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitJob::transactionError);
}
void PackageKitJob::packageKitUninstall(const QString &fileName)
{
PackageKit::Transaction *transaction = PackageKit::Daemon::getDetailsLocal(fileName);
connect(transaction, &PackageKit::Transaction::details,
this, [this](const PackageKit::Details &details) { removePackage(details.packageId()); });
connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitJob::transactionError);
}
void PackageKitJob::removePackage(const QString &packageId)
{
PackageKit::Transaction *transaction = PackageKit::Daemon::removePackage(packageId);
connect(transaction, &PackageKit::Transaction::finished, this, &PackageKitJob::transactionFinished);
connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitJob::transactionError);
}
void PackageKitJob::transactionError(PackageKit::Transaction::Error, const QString &details)
{
Q_EMIT error(details);
}
void PackageKitJob::transactionFinished(PackageKit::Transaction::Exit status, uint)
{
if (status == PackageKit::Transaction::ExitSuccess) {
Q_EMIT finished();
}
}
QStringList PackageKitJob::supportedPackagekitMimeTypes()
{
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.PackageKit",
"/org/freedesktop/PackageKit",
"org.freedesktop.DBus.Properties",
"Get");
message.setArguments({"org.freedesktop.PackageKit", "MimeTypes"});
QDBusMessage reply = QDBusConnection::systemBus().call(message);
return reply.arguments().at(0).value<QDBusVariant>().variant().toStringList();
}
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "AbstractJob.h"
#ifndef PACKAGEKITJOB_H
#define PACKAGEKITJOB_H
#include <PackageKit/Transaction>
class PackageKitJob : public AbstractJob
{
Q_OBJECT
public:
void executeOperation(const QFileInfo &fileInfo, const QString &mimeType, bool install) override;
private:
QStringList supportedPackagekitMimeTypes();
private Q_SLOTS:
void packageKitInstall(const QString &fileName);
void packageKitUninstall(const QString &fileName);
void removePackage(const QString &packageId);
void transactionError(PackageKit::Transaction::Error, const QString &details);
void transactionFinished(PackageKit::Transaction::Exit status, uint);
};
#endif //PACKAGEKITJOB_H
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QDialog>
#include <QVBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QDialogButtonBox>
#include <QDesktopServices>
#include <QPushButton>
#include <QUrl>
class ScriptConfirmationDialog : public QDialog
{
public:
ScriptConfirmationDialog(const QString &installerPath, bool install, const QString &dir, QWidget *parent = nullptr) : QDialog(parent)
{
const auto readmes = QDir(dir).entryList({QStringLiteral("README*")});
setWindowTitle(i18nc("@title:window", "Confirm Installation"));
setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
const bool noInstaller = installerPath.isEmpty();
QVBoxLayout *layout = new QVBoxLayout(this);
QString msg;
if (!install && noInstaller && readmes.isEmpty()) {
msg = xi18nc("@info", "This plugin does not provide an uninstall script. Please contact the author. "
"You can try to uninstall the plugin manually.<nl/>"
"If you do not feel capable or comfortable with this, click <interface>Cancel</interface> now.");
} else if (!install && noInstaller) {
msg = xi18nc("@info", "This plugin does not provide an uninstallation script. Please contact the author. "
"You can try to uninstall the plugin manually. Please have a look at the README "
"for instructions from the author.<nl/>"
"If you do not feel capable or comfortable with this, click <interface>Cancel</interface> now.");
} else if (noInstaller && readmes.isEmpty()) {
msg = xi18nc("@info", "This plugin does not provide an installation script. Please contact the author. "
"You can try to install the plugin manually.<nl/>"
"If you do not feel capable or comfortable with this, click <interface>Cancel</interface> now.");
} else if (noInstaller) {
msg = xi18nc("@info", "This plugin does not provide an installation script. Please contact the author.<nl/>"
"You can try to install the plugin manually; please have a look at the README "
"for instructions from the author.<nl/>"
"If you do not feel capable or comfortable with this, click <interface>Cancel</interface> now.");
} else if (readmes.isEmpty()) {
msg = xi18nc("@info", "This plugin uses a script for installation which can pose a security risk. "
"Please examine the entire plugin's contents before installing, or at least "
"read the script's source code.<nl/>"
"If you do not feel capable or comfortable with this, click <interface>Cancel</interface> now.");
} else {
msg = xi18nc("@info", "This plugin uses a script for installation which can pose a security risk. "
"Please examine the entire plugin's contents before installing, or at least "
"read the README file and the script's source code.<nl/>"
"If you do not feel capable or comfortable with this, click <interface>Cancel</interface> now.");
}
QLabel *msgLabel = new QLabel(msg, this);
msgLabel->setWordWrap(true);
layout->addWidget(msgLabel);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
buttonBox->button(QDialogButtonBox::Ok)->setIcon(QIcon::fromTheme("emblem-warning"));
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
QString okText;
if (noInstaller && !install) {
okText = i18nc("@action:button", "Mark entry as uninstalled");
} else if (noInstaller) {
okText = i18nc("@action:button", "Mark entry as installed");
} else {
okText = i18nc("@action:button", "Accept Risk And Continue");
}
buttonBox->button(QDialogButtonBox::Ok)->setText(okText);
QHBoxLayout *helpButtonLayout = new QHBoxLayout(this);
if (!noInstaller) {
QPushButton *scriptButton = new QPushButton(QIcon::fromTheme("dialog-scripts"), i18nc("@action:button", "View Script"), this);
connect(scriptButton, &QPushButton::clicked, this, [installerPath]() {
QDesktopServices::openUrl(QUrl::fromLocalFile(installerPath));
});
helpButtonLayout->addWidget(scriptButton);
}
QPushButton *sourceButton = new QPushButton(QIcon::fromTheme("document-open-folder"), i18nc("@action:button", "View Source Directory"), this);
connect(sourceButton, &QPushButton::clicked, this, [dir]() {
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
});
if (readmes.isEmpty() && helpButtonLayout->isEmpty()) {
// If there is no script and readme we can display the button in the same line
buttonBox->addButton(sourceButton, QDialogButtonBox::HelpRole);
} else {
helpButtonLayout->addWidget(sourceButton);
}
if (!readmes.isEmpty()) {
QPushButton *readmeButton = new QPushButton(QIcon::fromTheme("text-x-readme"), i18nc("@action:button", "View %1", readmes.at(0)), this);
connect(readmeButton, &QPushButton::clicked, this, [dir, readmes]() {
QDesktopServices::openUrl(QUrl::fromLocalFile(QDir(dir).absoluteFilePath(readmes.at(0))));
});
helpButtonLayout->addWidget(readmeButton);
}
helpButtonLayout->setAlignment(Qt::AlignRight);
layout->addLayout(helpButtonLayout);
buttonBox->button(QDialogButtonBox::Cancel)->setFocus();
layout->addWidget(buttonBox);
}
};
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "ScriptJob.h"
#include <KLocalizedString>
#include <KShell>
#include <QFileInfoList>
#include <QDir>
#include <QDebug>
#include "ScriptConfirmationDialog.h"
void ScriptJob::executeOperation(const QFileInfo &fileInfo, const QString &mimeType, bool install)
{
Q_UNUSED(mimeType)
QString installerPath;
const QFileInfoList archiveEntries = fileInfo.absoluteDir().entryInfoList(QDir::Files, QDir::Name);
const QString scriptPrefix = install ? "install" : "uninstall";
for (const auto &file : archiveEntries) {
if (file.baseName() == scriptPrefix) {
installerPath = file.absoluteFilePath();
// If the name is exactly install/uninstall we immediately take it
break;
} else if (file.baseName().startsWith(scriptPrefix)) {
installerPath = file.absoluteFilePath();
}
}
// We want the user to be exactly aware of whats going on
if (install || installerPath.isEmpty()) {
ScriptConfirmationDialog dlg(installerPath, install, fileInfo.absolutePath());
if (dlg.exec() == QDialog::Accepted) {
if (installerPath.isEmpty()) {
Q_EMIT finished(); // The "Mark entry as installed" button
} else {
runScriptInTerminal(formatScriptCommand(install, installerPath), fileInfo.absolutePath());
}
} else {
Q_EMIT error(QString());
}
} else {
runScriptInTerminal(formatScriptCommand(install, installerPath), fileInfo.absolutePath());
}
}
QString ScriptJob::formatScriptCommand(bool install, const QString &installerPath)
{
const QString bashCommand = QStringLiteral("echo %1;%1 || $SHELL && echo %2")
.arg(KShell::quoteArg(installerPath), KShell::quoteArg(terminalCloseMessage(install)));
return QStringLiteral("sh -c %1").arg(KShell::quoteArg(bashCommand));
}
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef SCRIPTJOB_H
#define SCRIPTJOB_H
#include "AbstractJob.h"
class ScriptJob : public AbstractJob
{
Q_OBJECT
public:
void executeOperation(const QFileInfo &fileInfo, const QString &mimeType, bool install) override;
private:
QString formatScriptCommand(bool install, const QString &installerPath);
};
#endif //SCRIPTJOB_H
/*
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "ZypperRPMJob.h"
#include <QFileInfo>
#include <QRegularExpression>
#include <QProcess>
#include <QDir>
#include <KShell>
#include <KLocalizedString>
void ZypperRPMJob::executeOperation(const QFileInfo &fileInfo, const QString &mimeType, bool install)
{
Q_UNUSED(mimeType)
if (install) {
const QString command = QStringLiteral("sudo zypper install %1").arg(KShell::quoteArg(fileInfo.absoluteFilePath()));
const QString bashCommand = QStringLiteral("echo %1;%1")
.arg(command, KShell::quoteArg(terminalCloseMessage(install)));