Advance Wayland and KDE package bring-up

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-14 10:51:06 +01:00
parent 51f3c21121
commit cf12defd28
15214 changed files with 20594243 additions and 269 deletions
@@ -0,0 +1,142 @@
configure_file(config-kiogui.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kiogui.h)
add_library(KF6KIOGui)
add_library(KF6::KIOGui ALIAS KF6KIOGui)
set_target_properties(KF6KIOGui PROPERTIES
VERSION ${KIO_VERSION}
SOVERSION ${KIO_SOVERSION}
EXPORT_NAME KIOGui
)
target_sources(KF6KIOGui PRIVATE
applicationlauncherjob.cpp
commandlauncherjob.cpp
dndpopupmenuplugin.cpp
faviconrequestjob.cpp
kcoreurlnavigator.cpp
openurljob.cpp
openwithhandlerinterface.cpp
openorexecutefileinterface.cpp
kprocessrunner.cpp
kterminallauncherjob.cpp
kemailclientlauncherjob.cpp
previewjob.cpp
thumbnailcreator.cpp
gpudetection.cpp
kurifilter.cpp
kurifilterplugin.cpp
openfilemanagerwindowjob.cpp
standardthumbnailjob.cpp
)
if (HAVE_QTDBUS)
target_sources(KF6KIOGui PRIVATE dbusactivationrunner.cpp)
endif ()
if (HAVE_QTDBUS AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(SYSTEMD_DBUS_XMLS
systemd/org.freedesktop.systemd1.Manager.xml
systemd/org.freedesktop.systemd1.Unit.xml
systemd/org.freedesktop.DBus.Properties.xml)
set_source_files_properties(${SYSTEMD_DBUS_XMLS} PROPERTIES INCLUDE systemd/dbustypes.h)
set(kiogui_dbus_SRCS)
qt_add_dbus_interfaces(kiogui_dbus_SRCS ${SYSTEMD_DBUS_XMLS})
target_sources(KF6KIOGui PRIVATE
${kiogui_dbus_SRCS}
systemd/systemdprocessrunner.cpp
systemd/scopedprocessrunner.cpp
)
endif ()
ecm_qt_declare_logging_category(KF6KIOGui
HEADER kiogui_debug.h
IDENTIFIER KIO_GUI
CATEGORY_NAME kf.kio.gui
OLD_CATEGORY_NAMES kf5.kio.gui
DESCRIPTION "KIOGui (KIO)"
EXPORT KIO
)
ecm_qt_declare_logging_category(KF6KIOGui
HEADER favicons_debug.h
IDENTIFIER FAVICONS_LOG
CATEGORY_NAME kf.kio.gui.favicons
OLD_CATEGORY_NAMES kf5.kio.favicons
DESCRIPTION "FavIcons (KIO)"
EXPORT KIO
)
ecm_generate_export_header(KF6KIOGui
BASE_NAME KIOGui
GROUP_BASE_NAME KF
VERSION ${KF_VERSION}
USE_VERSION_HEADER
VERSION_BASE_NAME KIO
DEPRECATED_BASE_VERSION 0
DEPRECATION_VERSIONS
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
)
target_include_directories(KF6KIOGui INTERFACE "$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KIOGui>")
target_link_libraries(KF6KIOGui
PUBLIC
KF6::KIOCore
KF6::ConfigCore
KF6::Service
Qt6::Gui
PRIVATE
KF6::Solid
KF6::I18n
)
target_link_libraries(KF6KIOGui PRIVATE KF6::WindowSystem)
# Headers prefixed with KIO/
ecm_generate_headers(KIOGui_CamelCase_HEADERS
HEADER_NAMES
ApplicationLauncherJob
CommandLauncherJob
DndPopupMenuPlugin
FavIconRequestJob
OpenFileManagerWindowJob
OpenUrlJob
OpenWithHandlerInterface
PreviewJob
ThumbnailCreator
PREFIX KIO
REQUIRED_HEADERS KIO_namespaced_gui_HEADERS
)
# Headers not prefixed with KIO/
ecm_generate_headers(KIOGui_HEADERS
HEADER_NAMES
KCoreUrlNavigator
KEMailClientLauncherJob
KTerminalLauncherJob
KUriFilter
REQUIRED_HEADERS KIOGui_HEADERS
)
install(FILES ${KIOGui_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KIOGui/KIO COMPONENT Devel)
install(TARGETS KF6KIOGui EXPORT KF6KIOTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES
${KIO_namespaced_gui_HEADERS}
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KIOGui/kio COMPONENT Devel)
install(FILES
${KIOGui_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/kiogui_export.h
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KIOGui COMPONENT Devel)
# make available to ecm_add_qch in parent folder
set(KIOGui_QCH_SOURCES ${KIOGui_HEADERS} ${KIO_namespaced_gui_HEADERS} PARENT_SCOPE)
@@ -0,0 +1,317 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "applicationlauncherjob.h"
#include "../core/global.h"
#include "jobuidelegatefactory.h"
#include "kiogui_debug.h"
#include "kprocessrunner_p.h"
#include "mimetypefinderjob.h"
#include "openwithhandlerinterface.h"
#include "untrustedprogramhandlerinterface.h"
#ifdef WITH_QTDBUS
#include "dbusactivationrunner_p.h"
#endif
#include <KAuthorized>
#include <KDesktopFile>
#include <KDesktopFileAction>
#include <KLocalizedString>
#include <QFileInfo>
#include <QPointer>
class KIO::ApplicationLauncherJobPrivate
{
public:
explicit ApplicationLauncherJobPrivate(KIO::ApplicationLauncherJob *job, const KService::Ptr &service)
: m_service(service)
, q(job)
{
}
void slotStarted(qint64 pid)
{
m_pids.append(pid);
if (--m_numProcessesPending == 0) {
q->emitResult();
}
}
void showOpenWithDialogForMimeType();
void showOpenWithDialog();
KService::Ptr m_service;
QString m_serviceEntryPath;
QList<QUrl> m_urls;
KIO::ApplicationLauncherJob::RunFlags m_runFlags;
QString m_suggestedFileName;
QString m_mimeTypeName;
QByteArray m_startupId;
QList<qint64> m_pids;
QList<QPointer<KProcessRunner>> m_processRunners;
int m_numProcessesPending = 0;
KIO::ApplicationLauncherJob *q;
};
KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KService::Ptr &service, QObject *parent)
: KJob(parent)
, d(new ApplicationLauncherJobPrivate(this, service))
{
if (d->m_service) {
// Cache entryPath() because we may call KService::setExec() which will clear entryPath()
d->m_serviceEntryPath = d->m_service->entryPath();
}
}
KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KServiceAction &serviceAction, QObject *parent)
: ApplicationLauncherJob(serviceAction.service(), parent)
{
Q_ASSERT(d->m_service);
d->m_service.detach();
d->m_service->setExec(serviceAction.exec());
}
KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KDesktopFileAction &desktopFileAction, QObject *parent)
: ApplicationLauncherJob(KService::Ptr(new KService(desktopFileAction.desktopFilePath())), parent)
{
Q_ASSERT(d->m_service);
d->m_service.detach();
d->m_service->setExec(desktopFileAction.exec());
}
KIO::ApplicationLauncherJob::ApplicationLauncherJob(QObject *parent)
: KJob(parent)
, d(new ApplicationLauncherJobPrivate(this, {}))
{
}
KIO::ApplicationLauncherJob::~ApplicationLauncherJob()
{
// Do *NOT* delete the KProcessRunner instances here.
// We need it to keep running so it can terminate startup notification on process exit.
}
void KIO::ApplicationLauncherJob::setUrls(const QList<QUrl> &urls)
{
d->m_urls = urls;
}
void KIO::ApplicationLauncherJob::setRunFlags(RunFlags runFlags)
{
d->m_runFlags = runFlags;
}
void KIO::ApplicationLauncherJob::setSuggestedFileName(const QString &suggestedFileName)
{
d->m_suggestedFileName = suggestedFileName;
}
void KIO::ApplicationLauncherJob::setStartupId(const QByteArray &startupId)
{
d->m_startupId = startupId;
}
void KIO::ApplicationLauncherJob::emitUnauthorizedError()
{
setError(KJob::UserDefinedError);
setErrorText(i18n("You are not authorized to execute this file."));
emitResult();
}
void KIO::ApplicationLauncherJob::start()
{
if (!d->m_service) {
d->showOpenWithDialogForMimeType();
return;
}
Q_EMIT description(this, i18nc("Launching application", "Launching %1", d->m_service->name()), {}, {});
// First, the security checks
if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) {
// KIOSK restriction, cannot be circumvented
emitUnauthorizedError();
return;
}
if (!d->m_serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(d->m_serviceEntryPath)) {
// We can use QStandardPaths::findExecutable to resolve relative pathnames
// but that gets rid of the command line arguments.
QString program = QFileInfo(d->m_service->exec()).canonicalFilePath();
if (program.isEmpty()) { // e.g. due to command line arguments
program = d->m_service->exec();
}
auto *untrustedProgramHandler = KIO::delegateExtension<KIO::UntrustedProgramHandlerInterface *>(this);
if (!untrustedProgramHandler) {
emitUnauthorizedError();
return;
}
connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [this, untrustedProgramHandler](bool result) {
if (result) {
// Assume that service is an absolute path since we're being called (relative paths
// would have been allowed unless Kiosk said no, therefore we already know where the
// .desktop file is. Now add a header to it if it doesn't already have one
// and add the +x bit.
QString errorString;
if (untrustedProgramHandler->makeServiceFileExecutable(d->m_serviceEntryPath, errorString)) {
proceedAfterSecurityChecks();
} else {
QString serviceName = d->m_service->name();
if (serviceName.isEmpty()) {
serviceName = d->m_service->genericName();
}
setError(KJob::UserDefinedError);
setErrorText(i18n("Unable to make the service %1 executable, aborting execution.\n%2.", serviceName, errorString));
emitResult();
}
} else {
setError(KIO::ERR_USER_CANCELED);
emitResult();
}
});
untrustedProgramHandler->showUntrustedProgramWarning(this, d->m_service->name());
return;
}
proceedAfterSecurityChecks();
}
void KIO::ApplicationLauncherJob::proceedAfterSecurityChecks()
{
bool startNTimesCondition = d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles();
#ifdef WITH_QTDBUS
startNTimesCondition = startNTimesCondition && !DBusActivationRunner::activationPossible(d->m_service, d->m_runFlags, d->m_suggestedFileName);
#endif
if (startNTimesCondition) {
// We need to launch the application N times.
// We ignore the result for application 2 to N.
// For the first file we launch the application in the
// usual way. The reported result is based on this application.
d->m_numProcessesPending = d->m_urls.count();
d->m_processRunners.reserve(d->m_numProcessesPending);
for (int i = 1; i < d->m_urls.count(); ++i) {
auto *processRunner =
KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, {d->m_urls.at(i)}, d->m_runFlags, d->m_suggestedFileName, QByteArray{});
d->m_processRunners.push_back(processRunner);
connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
d->slotStarted(pid);
});
}
d->m_urls = {d->m_urls.at(0)};
} else {
d->m_numProcessesPending = 1;
}
auto *processRunner =
KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, d->m_urls, d->m_runFlags, d->m_suggestedFileName, d->m_startupId);
d->m_processRunners.push_back(processRunner);
connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) {
setError(KJob::UserDefinedError);
setErrorText(errorText);
emitResult();
});
connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
d->slotStarted(pid);
});
}
// For KRun
bool KIO::ApplicationLauncherJob::waitForStarted()
{
if (error() != KJob::NoError) {
return false;
}
if (d->m_processRunners.isEmpty()) {
// Maybe we're in the security prompt...
// Can't avoid the nested event loop
// This fork of KJob::exec doesn't set QEventLoop::ExcludeUserInputEvents
const bool wasAutoDelete = isAutoDelete();
setAutoDelete(false);
QEventLoop loop;
connect(this, &KJob::result, this, [&](KJob *job) {
loop.exit(job->error());
});
const int ret = loop.exec();
if (wasAutoDelete) {
deleteLater();
}
return ret != KJob::NoError;
}
const bool ret = std::all_of(d->m_processRunners.cbegin(), d->m_processRunners.cend(), [](QPointer<KProcessRunner> r) {
return r.isNull() || r->waitForStarted();
});
for (const auto &r : std::as_const(d->m_processRunners)) {
if (!r.isNull()) {
qApp->sendPostedEvents(r); // so slotStarted gets called
}
}
return ret;
}
qint64 KIO::ApplicationLauncherJob::pid() const
{
return d->m_pids.at(0);
}
QList<qint64> KIO::ApplicationLauncherJob::pids() const
{
return d->m_pids;
}
void KIO::ApplicationLauncherJobPrivate::showOpenWithDialogForMimeType()
{
if (m_urls.size() == 1) {
auto job = new KIO::MimeTypeFinderJob(m_urls[0], q);
job->setFollowRedirections(true);
job->setSuggestedFileName(m_suggestedFileName);
q->connect(job, &KJob::result, q, [this, job]() {
if (!job->error()) {
m_mimeTypeName = job->mimeType();
}
showOpenWithDialog();
});
job->start();
} else {
showOpenWithDialog();
}
}
void KIO::ApplicationLauncherJobPrivate::showOpenWithDialog()
{
if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("You are not authorized to select an application to open this file."));
q->emitResult();
return;
}
auto *openWithHandler = KIO::delegateExtension<KIO::OpenWithHandlerInterface *>(q);
if (!openWithHandler) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("Internal error: could not prompt the user for which application to start"));
q->emitResult();
return;
}
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
q->setError(KIO::ERR_USER_CANCELED);
q->emitResult();
});
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
Q_ASSERT(service);
m_service = service;
q->start();
});
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
q->emitResult();
});
openWithHandler->promptUserForApplication(q, m_urls, m_mimeTypeName);
}
@@ -0,0 +1,175 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KIO_APPLICATIONLAUNCHERJOB_H
#define KIO_APPLICATIONLAUNCHERJOB_H
#include "kiogui_export.h"
#include <KJob>
#include <KService>
#include <QUrl>
class ApplicationLauncherJobTest;
class KDesktopFileAction;
namespace KIO
{
class ApplicationLauncherJobPrivate;
/**
* @class ApplicationLauncherJob applicationlauncherjob.h <KIO/ApplicationLauncherJob>
*
* @brief ApplicationLauncherJob runs an application and watches it while running.
*
* It creates a startup notification and finishes it on success or on error (for the taskbar).
* It also emits an error message if necessary (e.g. "program not found").
*
* When passing multiple URLs to an application that doesn't support opening
* multiple files, the application will be launched once for each URL.
*
* The job finishes when the application is successfully started. At that point you can
* query the PID(s).
*
* For error handling, either connect to the result() signal, or for a simple messagebox on error,
* you can use:
* @code
* // Since 5.98 use:
* job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
* // For older releases use:
* job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
* @endcode
* Using JobUiDelegate (which is widgets based) also enables the feature of asking the user
* in case the executable or desktop file isn't marked as executable. Otherwise the job will
* just refuse executing such files.
*
* To invoke the open-with dialog (from KIOWidgets), construct an ApplicationLauncherJob without
* any arguments or with a null KService.
*
* @since 5.69
*/
class KIOGUI_EXPORT ApplicationLauncherJob : public KJob
{
public:
/**
* Creates an ApplicationLauncherJob.
* @param service the service (application desktop file) to run
* @param parent the parent QObject
*/
explicit ApplicationLauncherJob(const KService::Ptr &service, QObject *parent = nullptr);
/**
* Creates an ApplicationLauncherJob.
* @param serviceAction the service action to run
* @param parent the parent QObject
*/
explicit ApplicationLauncherJob(const KServiceAction &serviceAction, QObject *parent = nullptr);
/**
* @overload
* @since 6.0
*/
explicit ApplicationLauncherJob(const KDesktopFileAction &desktopFileAction, QObject *parent = nullptr);
/**
* Creates an ApplicationLauncherJob which will prompt the user for which application to use
* (via the open-with dialog from KIOWidgets).
* @param parent the parent QObject
* @since 5.71
*/
explicit ApplicationLauncherJob(QObject *parent = nullptr);
/**
* Destructor
*
* Note that jobs auto-delete themselves after emitting result.
* Deleting/killing the job will not stop the started application.
*/
~ApplicationLauncherJob() override;
/**
* Specifies the URLs to be passed to the application.
* @param urls list of files (local or remote) to open
*
* Note that when passing multiple URLs to an application that doesn't support opening
* multiple files, the application will be launched once for each URL.
*/
void setUrls(const QList<QUrl> &urls);
/**
* @see RunFlag
*/
enum RunFlag {
DeleteTemporaryFiles = 0x1, ///< the URLs passed to the service will be deleted when it exits (if the URLs are local files)
};
/**
* Stores a combination of #RunFlag values.
*/
Q_DECLARE_FLAGS(RunFlags, RunFlag)
/**
* Specifies various flags.
* @param runFlags the flags to be set. For instance, whether the URLs are temporary files that should be deleted after execution.
*/
void setRunFlags(RunFlags runFlags);
/**
* Sets the file name to use in the case of downloading the file to a tempfile
* in order to give to a non-URL-aware application.
* Some apps rely on the extension to determine the MIME type of the file.
* Usually the file name comes from the URL, but in the case of the
* HTTP Content-Disposition header, we need to override the file name.
* @param suggestedFileName the file name
*/
void setSuggestedFileName(const QString &suggestedFileName);
/**
* Sets the platform-specific startup id of the application launch.
* @param startupId startup id, if any (otherwise "").
* For X11, this would be the id for the Startup Notification protocol.
* For Wayland, this would be the token for the XDG Activation protocol.
*/
void setStartupId(const QByteArray &startupId);
/**
* Starts the job.
* You must call this, after having done all the setters.
* This is (potentially) a GUI job, never use exec(), it would block user interaction.
*/
void start() override;
/**
* @return the PID of the application that was started
*
* Convenience method for pids().at(0). You should only use this when specifying zero or one URL,
* or when you are sure that the application supports opening multiple files. Otherwise use pids().
* Available after the job emits result().
*/
qint64 pid() const;
/**
* @return the PIDs of the applications that were started
*
* Available after the job emits result().
*/
QList<qint64> pids() const;
private:
friend class ::ApplicationLauncherJobTest;
/**
* Blocks until the process has started.
*/
bool waitForStarted();
KIOGUI_NO_EXPORT void emitUnauthorizedError();
KIOGUI_NO_EXPORT void proceedAfterSecurityChecks();
friend class ApplicationLauncherJobPrivate;
QScopedPointer<ApplicationLauncherJobPrivate> d;
};
} // namespace KIO
#endif
@@ -0,0 +1,159 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "commandlauncherjob.h"
#include "../core/global.h"
#include "kiogui_debug.h"
#include "kprocessrunner_p.h"
#include <KLocalizedString>
#include <KShell>
#include <QPointer>
class KIO::CommandLauncherJobPrivate
{
public:
QString m_command;
QString m_desktopName;
QString m_executable;
QString m_workingDirectory;
QStringList m_arguments;
QByteArray m_startupId;
QPointer<KProcessRunner> m_processRunner;
QProcessEnvironment m_environment{QProcessEnvironment::InheritFromParent};
qint64 m_pid = 0;
};
KIO::CommandLauncherJob::CommandLauncherJob(const QString &command, QObject *parent)
: KJob(parent)
, d(new CommandLauncherJobPrivate())
{
d->m_command = command;
}
KIO::CommandLauncherJob::CommandLauncherJob(const QString &executable, const QStringList &args, QObject *parent)
: KJob(parent)
, d(new CommandLauncherJobPrivate())
{
d->m_executable = executable;
d->m_arguments = args;
}
KIO::CommandLauncherJob::~CommandLauncherJob()
{
// Do *NOT* delete the KProcessRunner instances here.
// We need it to keep running so it can terminate startup notification on process exit.
}
void KIO::CommandLauncherJob::setCommand(const QString &command)
{
d->m_command = command;
}
QString KIO::CommandLauncherJob::command() const
{
if (d->m_command.isEmpty()) {
return KShell::quoteArg(d->m_executable) + QLatin1Char(' ') + KShell::joinArgs(d->m_arguments);
}
return d->m_command;
}
void KIO::CommandLauncherJob::setExecutable(const QString &executable)
{
d->m_executable = executable;
}
void KIO::CommandLauncherJob::setDesktopName(const QString &desktopName)
{
d->m_desktopName = desktopName;
}
void KIO::CommandLauncherJob::setStartupId(const QByteArray &startupId)
{
d->m_startupId = startupId;
}
void KIO::CommandLauncherJob::setWorkingDirectory(const QString &workingDirectory)
{
d->m_workingDirectory = workingDirectory;
}
QString KIO::CommandLauncherJob::workingDirectory() const
{
return d->m_workingDirectory;
}
void KIO::CommandLauncherJob::setProcessEnvironment(const QProcessEnvironment &environment)
{
d->m_environment = environment;
}
void KIO::CommandLauncherJob::start()
{
// Some fallback for lazy callers, not 100% accurate though
if (d->m_executable.isEmpty()) {
const QStringList args = KShell::splitArgs(d->m_command);
if (!args.isEmpty()) {
d->m_executable = args.first();
}
}
if (d->m_executable.isEmpty()) {
setError(KJob::UserDefinedError);
setErrorText(i18nc("An error message", "Empty command provided"));
emitResult();
return;
}
QString displayName = d->m_executable;
KService::Ptr service = KService::serviceByDesktopName(d->m_desktopName);
if (service) {
displayName = service->name();
}
Q_EMIT description(this, i18nc("Launching application", "Launching %1", displayName), {}, {});
if (d->m_command.isEmpty() && !d->m_executable.isEmpty()) {
d->m_processRunner =
KProcessRunner::fromExecutable(d->m_executable, d->m_arguments, d->m_desktopName, d->m_startupId, d->m_workingDirectory, d->m_environment);
if (!d->m_processRunner) {
setError(KIO::ERR_DOES_NOT_EXIST);
setErrorText(d->m_executable);
emitResult();
return;
}
} else {
d->m_processRunner =
KProcessRunner::fromCommand(d->m_command, d->m_desktopName, d->m_executable, d->m_startupId, d->m_workingDirectory, d->m_environment);
}
connect(d->m_processRunner, &KProcessRunner::error, this, [this](const QString &errorText) {
setError(KJob::UserDefinedError);
setErrorText(errorText);
emitResult();
});
connect(d->m_processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
d->m_pid = pid;
emitResult();
});
}
bool KIO::CommandLauncherJob::waitForStarted()
{
if (d->m_processRunner.isNull()) {
return false;
}
const bool ret = d->m_processRunner->waitForStarted();
if (!d->m_processRunner.isNull()) {
qApp->sendPostedEvents(d->m_processRunner); // so slotStarted gets called
}
return ret;
}
qint64 KIO::CommandLauncherJob::pid() const
{
return d->m_pid;
}
@@ -0,0 +1,158 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KIO_COMMANDLAUNCHERJOB_H
#define KIO_COMMANDLAUNCHERJOB_H
#include "kiogui_export.h"
#include <KJob>
class KRunPrivate; // KF6 REMOVE
class CommandLauncherJobTest; // KF6 REMOVE
class QProcessEnvironment;
namespace KIO
{
class CommandLauncherJobPrivate;
/**
* @class CommandLauncherJob commandlauncherjob.h <KIO/CommandLauncherJob>
*
* @brief CommandLauncherJob runs a command and watches it while running.
*
* It creates a startup notification and finishes it on success or on error (for the taskbar).
* It also emits a "program not found" error message if the requested command did not exist.
*
* The job finishes when the command is successfully started; at that point you can
* query the PID with pid(). Note that no other errors are handled automatically after the
* command starts running. As far as CommandLauncherJob is concerned, if the command was
* launched, the result is a success. If you need to query the command for its exit status
* or error text later, it is recommended to use QProcess instead.
*
* For error handling, either connect to the result() signal, or for a simple messagebox on error,
* you can do
* @code
* job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
* @endcode
*
* @since 5.69
*/
class KIOGUI_EXPORT CommandLauncherJob : public KJob
{
public:
/**
* Creates a CommandLauncherJob.
* @param command the shell command to run
* The command is given "as is" to the shell, it must already be quoted if necessary.
* If @p command is instead a filename, consider using the other constructor, even if no args are present.
* @param parent the parent QObject
*
* Please consider also calling setDesktopName() for better startup notification.
*/
explicit CommandLauncherJob(const QString &command, QObject *parent = nullptr);
/**
* Creates a CommandLauncherJob.
* @param executable the name of the executable
* @param args the commandline arguments to pass to the executable
* @param parent the parent QObject
*
* Please consider also calling setDesktopName() for better startup notification.
*/
explicit CommandLauncherJob(const QString &executable, const QStringList &args, QObject *parent = nullptr);
/**
* Destructor
*
* Note that jobs auto-delete themselves after emitting result
*/
~CommandLauncherJob() override;
/**
* Sets the command to execute, this will change the command that was set by any of the constructors.
* @since 5.83
*/
void setCommand(const QString &command);
/**
* Returns the command executed by this job.
* @since 5.83
*/
QString command() const;
/**
* Sets the name of the executable, used in the startup notification
* (see KStartupInfoData::setBin()).
* @param executable executable name, with or without a path
*
* Alternatively, use setDesktopName().
*/
void setExecutable(const QString &executable);
/**
* Set the name of the desktop file (e.g.\ "org.kde.dolphin", without the ".desktop" filename extension).
*
* This is necessary for startup notification to work.
*/
void setDesktopName(const QString &desktopName);
/**
* Sets the platform-specific startup id of the command launch.
* @param startupId startup id, if any (otherwise "").
* For X11, this would be the id for the Startup Notification protocol.
* For Wayland, this would be the token for the XDG Activation protocol.
*/
void setStartupId(const QByteArray &startupId);
/**
* Sets the working directory from which to run the command.
* @param workingDirectory path of a local directory
*/
void setWorkingDirectory(const QString &workingDirectory);
/**
* Returns the working directory, which was previously set with @c setWorkingDirectory().
* @since 5.83
*/
QString workingDirectory() const;
/**
* Can be used to pass environment variables to the child process.
* @param environment set of environment variables to pass to the child process
* @see QProcessEnvironment
* @since 5.82
*/
void setProcessEnvironment(const QProcessEnvironment &environment);
/**
* Starts the job.
* You must call this, after having called all the necessary setters.
*/
void start() override;
/**
* @return the PID of the command that was started
*
* Available after the job emits result().
*/
qint64 pid() const;
private:
friend class ::KRunPrivate; // KF6 REMOVE
friend class ::CommandLauncherJobTest; // KF6 REMOVE
/**
* Blocks until the process has started. Only exists for KRun, will disappear in KF6.
*/
bool waitForStarted();
friend class CommandLauncherJobPrivate;
QScopedPointer<CommandLauncherJobPrivate> d;
};
} // namespace KIO
#endif
@@ -0,0 +1,2 @@
#cmakedefine01 HAVE_X11
#cmakedefine01 HAVE_WAYLAND
@@ -0,0 +1,117 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020-2021 David Redondo <kde@david-redondo.de>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "dbusactivationrunner_p.h"
#include "kiogui_debug.h"
#include <KWindowSystem>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QTimer>
bool DBusActivationRunner::activationPossible(const KService::Ptr service, KIO::ApplicationLauncherJob::RunFlags flags, const QString &suggestedFileName)
{
if (!service->isApplication()) {
return false;
}
if (service->property<bool>(QStringLiteral("DBusActivatable"))) {
if (service->desktopEntryName().count(QLatin1Char('.')) < 2) {
qCWarning(KIO_GUI) << "Cannot activate" << service->desktopEntryName() << "doesn't have enough '.' for a well-formed service name";
return false;
}
if (!suggestedFileName.isEmpty()) {
qCDebug(KIO_GUI) << "Cannot activate" << service->desktopEntryName() << "because suggestedFileName is set";
return false;
}
if (flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles) {
qCDebug(KIO_GUI) << "Cannot activate" << service->desktopEntryName() << "because DeleteTemporaryFiles is set";
return false;
}
return true;
}
return false;
}
DBusActivationRunner::DBusActivationRunner(const QString &action)
: KProcessRunner()
, m_actionName(action)
{
}
void DBusActivationRunner::startProcess()
{
// DBusActivatable as per https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#dbus
const QString objectPath = QStringLiteral("/%1").arg(m_desktopName).replace(QLatin1Char('.'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('_'));
const QString interface = QStringLiteral("org.freedesktop.Application");
QDBusMessage message;
if (m_urls.isEmpty()) {
if (m_actionName.isEmpty()) {
message = QDBusMessage::createMethodCall(m_desktopName, objectPath, interface, QStringLiteral("Activate"));
} else {
message = QDBusMessage::createMethodCall(m_desktopName, objectPath, interface, QStringLiteral("ActivateAction"));
message << m_actionName << QVariantList();
}
} else {
message = QDBusMessage::createMethodCall(m_desktopName, objectPath, interface, QStringLiteral("Open"));
message << QUrl::toStringList(m_urls);
}
if (KWindowSystem::isPlatformX11()) {
#if HAVE_X11
message << QVariantMap{{QStringLiteral("desktop-startup-id"), m_startupId.id()}};
#endif
} else if (KWindowSystem::isPlatformWayland()) {
message << QVariantMap{{QStringLiteral("activation-token"), m_process->processEnvironment().value(QStringLiteral("XDG_ACTIVATION_TOKEN"))}};
}
auto call = QDBusConnection::sessionBus().asyncCall(message);
auto activationWatcher = new QDBusPendingCallWatcher(call, this);
connect(activationWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
if (watcher->isError()) {
Q_EMIT error(watcher->error().message());
terminateStartupNotification();
m_finished = true;
deleteLater();
return;
}
auto call = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("GetConnectionUnixProcessID"), m_desktopName);
auto pidWatcher = new QDBusPendingCallWatcher(call, this);
connect(pidWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
m_finished = true;
QDBusPendingReply<uint> reply = *watcher;
if (reply.isError()) {
Q_EMIT error(watcher->error().message());
terminateStartupNotification();
} else {
Q_EMIT processStarted(reply.value());
}
deleteLater();
});
});
}
bool DBusActivationRunner::waitForStarted(int timeout)
{
if (m_finished) {
return m_pid != 0;
}
QEventLoop loop;
bool success = false;
connect(this, &KProcessRunner::processStarted, [&loop, &success]() {
loop.quit();
success = true;
});
connect(this, &KProcessRunner::error, &loop, &QEventLoop::quit);
QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
loop.exec();
return success;
}
#include "moc_dbusactivationrunner_p.cpp"
@@ -0,0 +1,27 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020-2021 David Redondo <kde@david-redondo.de>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef DBUSACTIVATIONRUNNER_P_H
#define DBUSACTIVATIONRUNNER_P_H
#include "kprocessrunner_p.h"
class DBusActivationRunner : public KProcessRunner
{
Q_OBJECT
public:
explicit DBusActivationRunner(const QString &action);
void startProcess() override;
bool waitForStarted(int timeout = 30000) override;
static bool activationPossible(const KService::Ptr service, KIO::ApplicationLauncherJob::RunFlags flags, const QString &suggestedFileName);
private:
QString m_actionName;
bool m_finished = false;
};
#endif
@@ -0,0 +1,21 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2009 Harald Hvaal <haraldhv@stud.ntnu.no>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only
*/
#include "dndpopupmenuplugin.h"
using namespace KIO;
DndPopupMenuPlugin::DndPopupMenuPlugin(QObject *parent)
: QObject(parent)
{
}
DndPopupMenuPlugin::~DndPopupMenuPlugin()
{
}
#include "moc_dndpopupmenuplugin.cpp"
@@ -0,0 +1,59 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2009 Harald Hvaal <haraldhv@stud.ntnu.no>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only
*/
#ifndef _KIO_DNDPOPUPMENUPLUGIN_H_
#define _KIO_DNDPOPUPMENUPLUGIN_H_
#include "kiogui_export.h"
#include <QList>
#include <QObject>
class KFileItemListProperties;
class QUrl;
class QAction;
namespace KIO
{
/**
* @class KIO::DndPopupMenuPlugin dndpopupmenuplugin.h <KIO/DndPopupMenuPlugin>
*
* Base class for drag and drop popup menus
*
* This can be used for adding dynamic menu items to the normal copy/move/link
* here menu appearing in KIO-based file managers. In the setup method you may check
* the properties of the dropped files, and if applicable, append your own
* QAction that the user may trigger in the menu.
*
* The plugin should have Json metadata and be installed into kf6/kio_dnd/.
*
* @author Harald Hvaal <metellius@gmail.com>
* @since 5.6
*/
class KIOGUI_EXPORT DndPopupMenuPlugin : public QObject
{
Q_OBJECT
public:
/**
* Constructor.
*/
DndPopupMenuPlugin(QObject *parent);
~DndPopupMenuPlugin() override;
/**
* Implement the setup method in the plugin in order to create actions
* in the given actionCollection and add it to the menu using menu->addAction().
* The popup menu will be set as parent of the actions.
*
* @param popupMenuInfo all the information about the source URLs being dropped
* @param destination the URL to where the file(s) were dropped
* @return a QList with the QActions that will be plugged into the menu.
*/
virtual QList<QAction *> setup(const KFileItemListProperties &popupMenuInfo, const QUrl &destination) = 0;
};
}
#endif
@@ -0,0 +1,200 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2016 David Faure <faure@kde.org>
SPDX-FileCopyrightText: 2001 Malte Starostik <malte@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "faviconrequestjob.h"
#include <faviconscache_p.h>
#include "favicons_debug.h"
#include <KConfig>
#include <KIO/TransferJob>
#include <KLocalizedString>
#include <QBuffer>
#include <QCache>
#include <QDate>
#include <QFileInfo>
#include <QImage>
#include <QImageReader>
#include <QSaveFile>
#include <QStandardPaths>
#include <QUrl>
using namespace KIO;
static bool isIconOld(const QString &icon)
{
const QFileInfo info(icon);
if (!info.exists()) {
qCDebug(FAVICONS_LOG) << "isIconOld" << icon << "yes, no such file";
return true; // Trigger a new download on error
}
const QDate date = info.lastModified().date();
qCDebug(FAVICONS_LOG) << "isIconOld" << icon << "?";
return date.daysTo(QDate::currentDate()) > 7; // arbitrary value (one week)
}
class KIO::FavIconRequestJobPrivate
{
public:
FavIconRequestJobPrivate(const QUrl &hostUrl, KIO::LoadType reload)
: m_hostUrl(hostUrl)
, m_reload(reload)
{
}
// slots
void slotData(KIO::Job *job, const QByteArray &data);
QUrl m_hostUrl;
QUrl m_iconUrl;
QString m_iconFile;
QByteArray m_iconData;
KIO::LoadType m_reload;
};
FavIconRequestJob::FavIconRequestJob(const QUrl &hostUrl, LoadType reload, QObject *parent)
: KCompositeJob(parent)
, d(new FavIconRequestJobPrivate(hostUrl, reload))
{
QMetaObject::invokeMethod(this, &FavIconRequestJob::doStart, Qt::QueuedConnection);
}
FavIconRequestJob::~FavIconRequestJob() = default;
void FavIconRequestJob::setIconUrl(const QUrl &iconUrl)
{
d->m_iconUrl = iconUrl;
}
QString FavIconRequestJob::iconFile() const
{
return d->m_iconFile;
}
QUrl FavIconRequestJob::hostUrl() const
{
return d->m_hostUrl;
}
void FavIconRequestJob::doStart()
{
KIO::FavIconsCache *cache = KIO::FavIconsCache::instance();
QUrl iconUrl = d->m_iconUrl;
const bool isNewIconUrl = !iconUrl.isEmpty();
if (isNewIconUrl) {
cache->setIconForUrl(d->m_hostUrl, d->m_iconUrl);
} else {
iconUrl = cache->iconUrlForUrl(d->m_hostUrl);
}
if (d->m_reload == NoReload) {
const QString iconFile = cache->cachePathForIconUrl(iconUrl);
if (!isIconOld(iconFile)) {
qCDebug(FAVICONS_LOG) << "existing icon not old, reload not requested -> doing nothing";
d->m_iconFile = iconFile;
emitResult();
return;
}
if (cache->isFailedDownload(iconUrl)) {
qCDebug(FAVICONS_LOG) << iconUrl << "already in failedDownloads, emitting error";
setError(KIO::ERR_DOES_NOT_EXIST);
setErrorText(i18n("No favicon found for %1", d->m_hostUrl.host()));
emitResult();
return;
}
}
qCDebug(FAVICONS_LOG) << "downloading" << iconUrl;
KIO::TransferJob *job = KIO::get(iconUrl, d->m_reload, KIO::HideProgressInfo);
QMap<QString, QString> metaData;
metaData.insert(QStringLiteral("ssl_no_client_cert"), QStringLiteral("true"));
metaData.insert(QStringLiteral("ssl_no_ui"), QStringLiteral("true"));
metaData.insert(QStringLiteral("UseCache"), QStringLiteral("false"));
metaData.insert(QStringLiteral("cookies"), QStringLiteral("none"));
metaData.insert(QStringLiteral("no-www-auth"), QStringLiteral("true"));
job->addMetaData(metaData);
QObject::connect(job, &KIO::TransferJob::data, this, [this](KIO::Job *job, const QByteArray &data) {
d->slotData(job, data);
});
addSubjob(job);
}
void FavIconRequestJob::slotResult(KJob *job)
{
KIO::TransferJob *tjob = static_cast<KIO::TransferJob *>(job);
const QUrl &iconUrl = tjob->url();
KIO::FavIconsCache *cache = KIO::FavIconsCache::instance();
if (!job->error()) {
QBuffer buffer(&d->m_iconData);
buffer.open(QIODevice::ReadOnly);
QImageReader ir(&buffer);
QSize desired(16, 16);
if (ir.canRead()) {
while (ir.imageCount() > 1 && ir.currentImageRect() != QRect(0, 0, desired.width(), desired.height())) {
if (!ir.jumpToNextImage()) {
break;
}
}
ir.setScaledSize(desired);
const QImage img = ir.read();
if (!img.isNull()) {
cache->ensureCacheExists();
const QString localPath = cache->cachePathForIconUrl(iconUrl);
qCDebug(FAVICONS_LOG) << "Saving image to" << localPath;
QSaveFile saveFile(localPath);
if (saveFile.open(QIODevice::WriteOnly) && img.save(&saveFile, "PNG") && saveFile.commit()) {
d->m_iconFile = localPath;
} else {
setError(KIO::ERR_CANNOT_WRITE);
setErrorText(i18n("Error saving image to %1", localPath));
}
} else {
qCDebug(FAVICONS_LOG) << "QImageReader read() returned a null image";
}
} else {
qCDebug(FAVICONS_LOG) << "QImageReader canRead returned false";
}
} else if (job->error() == KJob::KilledJobError) { // we killed it in slotData
setError(KIO::ERR_WORKER_DEFINED);
setErrorText(i18n("Icon file too big, download aborted"));
} else {
setError(job->error());
setErrorText(job->errorString()); // not errorText(), because "this" is a KJob, with no errorString building logic
}
d->m_iconData.clear(); // release memory
if (d->m_iconFile.isEmpty()) {
qCDebug(FAVICONS_LOG) << "adding" << iconUrl << "to failed downloads due to error:" << errorString();
cache->addFailedDownload(iconUrl);
} else {
cache->removeFailedDownload(iconUrl);
}
KCompositeJob::removeSubjob(job);
emitResult();
}
void FavIconRequestJobPrivate::slotData(Job *job, const QByteArray &data)
{
KIO::TransferJob *tjob = static_cast<KIO::TransferJob *>(job);
unsigned int oldSize = m_iconData.size();
// Size limit. Stop downloading if the file is huge.
// Testcase (as of june 2008, at least): http://planet-soc.com/favicon.ico, 136K and strange format.
// Another case: sites which redirect from "/favicon.ico" to "/" and return the main page.
if (oldSize > 0x10000) { // 65K
qCDebug(FAVICONS_LOG) << "Favicon too big, aborting download of" << tjob->url();
const QUrl iconUrl = tjob->url();
KIO::FavIconsCache::instance()->addFailedDownload(iconUrl);
tjob->kill(KJob::EmitResult);
} else {
m_iconData.resize(oldSize + data.size());
memcpy(m_iconData.data() + oldSize, data.data(), data.size());
}
}
#include "moc_faviconrequestjob.cpp"
@@ -0,0 +1,120 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2016 David Faure <faure@kde.org>
SPDX-FileCopyrightText: 2001 Malte Starostik <malte@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KIO_FAVICONREQUESTJOB_H
#define KIO_FAVICONREQUESTJOB_H
#include "kiogui_export.h"
#include <kio/job_base.h> // for LoadType
#include <memory>
class QUrl;
namespace KIO
{
class FavIconRequestJobPrivate;
/**
* @class FavIconRequestJob faviconrequestjob.h <KIO/FavIconRequestJob>
*
* FavIconRequestJob handles the retrieval of a favicon (either from the local cache or from the internet)
*
* For instance, the icon for http://www.google.com exists at http://www.google.com/favicon.ico
* This job will (the first time) download the favicon, and make it available as a local PNG
* for fast lookups afterwards.
*
* Usage:
* Create a FavIconRequestJob, connect to result(KJob *), and from there use iconFile().
*
* @code
* // Let's say we want to show the icon for QUrl m_url
* KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(m_url);
* connect(job, &KIO::FavIconRequestJob::result, this, [job, this](KJob *){
* if (!job->error()) {
* // show the icon using QIcon(job->iconFile())
* }
* });
* @endcode
*
* For a given HTTP URL, you can find out if a favicon is available by calling KIO::favIconForUrl() in KIOCore.
* It is however not necessary to check this first, FavIconRequestJob will do this
* first and emit result right away if a cached icon is available and not too old.
*
* In Web Browsers, additional information exists: the HTML for a given page can
* specify something like
* &lt;link rel="shortcut icon" href="another_favicon.ico" /&gt;
* To handle this, call job->setIconUrl(iconUrl).
* (KParts-based web engines use the signal BrowserExtension::setIconUrl to call back
* into the web browser application, which should then call this).
* The signal urlIconChanged will be emitted once the icon has been downloaded.
*
* The on-disk cache is shared between processes.
*
* @since 5.19
*/
class KIOGUI_EXPORT FavIconRequestJob : public KCompositeJob
{
Q_OBJECT
public:
/**
* @brief FavIconRequestJob constructor
* @param hostUrl The web page URL. We only use the scheme and host.
* @param reload set this to reload to skip the cache and force a refresh of the favicon.
* @param parent parent object
*/
explicit FavIconRequestJob(const QUrl &hostUrl, KIO::LoadType reload = KIO::NoReload, QObject *parent = nullptr);
/**
* Destructor. You do not need to delete the job, it will delete automatically,
* unless you call setAutoDelete(false).
*/
~FavIconRequestJob() override;
/**
* @brief setIconUrl allows to set, for a specific URL, a different icon URL
* than the default one for the host (http://host/favicon.ico)
*
* This information is stored in the on-disk cache, so that
* other FavIconRequestJobs for this url and KIO::favIconForUrl
* will return the icon specified here.
*
* @param iconUrl the URL to the icon, usually parsed from the HTML
*/
void setIconUrl(const QUrl &iconUrl);
/**
* Returns the full local path to the icon from the cache.
* Only call this in the slot connected to the result(KJob*) signal.
* @return the path to the icon file
*/
QString iconFile() const;
/**
* Returns the URL passed to the constructor
* @since 5.20
*/
QUrl hostUrl() const;
/**
* @internal
* Do not call start(), KIO jobs are autostarted
*/
void start() override
{
}
private Q_SLOTS:
KIOGUI_NO_EXPORT void doStart(); // not called start() so that exec() doesn't call it too
void slotResult(KJob *job) override;
private:
std::unique_ptr<FavIconRequestJobPrivate> const d;
};
}
#endif
@@ -0,0 +1,119 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2023 Dave Vasilevsky <dave@vasilevsky.ca>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only
*/
#include "gpudetection_p.h"
#ifdef WITH_QTDBUS
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusVariant>
#endif
#include <QMap>
#include <QString>
#include <KProcess>
static void checkGpu();
// Returns true if switcheroo present
static bool checkGpuWithSwitcheroo();
static void checkGpuWithSolid();
// TODO: GPUs are hot-swappable, watch for changes using dbus PropertiesChanged
enum class GpuCheck {
NotChecked,
Present,
Absent,
};
static GpuCheck s_gpuCheck = GpuCheck::NotChecked;
static QProcessEnvironment s_gpuEnv;
static void checkGpu()
{
if (s_gpuCheck == GpuCheck::NotChecked) {
if (!checkGpuWithSwitcheroo()) {
checkGpuWithSolid();
}
}
}
static bool checkGpuWithSwitcheroo()
{
#ifdef WITH_QTDBUS
QDBusInterface switcheroo(QStringLiteral("net.hadess.SwitcherooControl"),
QStringLiteral("/net/hadess/SwitcherooControl"),
QStringLiteral("org.freedesktop.DBus.Properties"),
QDBusConnection::systemBus());
if (!switcheroo.isValid()) {
return false;
}
QDBusReply<QDBusVariant> reply = switcheroo.call(QStringLiteral("Get"), QStringLiteral("net.hadess.SwitcherooControl"), QStringLiteral("GPUs"));
if (!reply.isValid()) {
return false;
}
QDBusArgument arg = qvariant_cast<QDBusArgument>(reply.value().variant());
QList<QVariantMap> gpus;
arg >> gpus;
for (const auto &gpu : gpus) {
bool defaultGpu = qvariant_cast<bool>(gpu[QStringLiteral("Default")]);
if (!defaultGpu) {
s_gpuCheck = GpuCheck::Present;
QStringList envList = qvariant_cast<QStringList>(gpu[QStringLiteral("Environment")]);
for (int i = 0; i + 1 < envList.size(); i += 2) {
s_gpuEnv.insert(envList[i], envList[i + 1]);
}
return true;
}
}
#endif
// No non-default GPU found
s_gpuCheck = GpuCheck::Absent;
return true;
}
static void checkGpuWithSolid()
{
#ifdef WITH_QTDBUS
// TODO: Consider moving this check into kio, instead of using Solid
QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"),
QStringLiteral("/org/kde/Solid/PowerManagement"),
QStringLiteral("org.kde.Solid.PowerManagement"),
QDBusConnection::sessionBus());
if (iface.isValid()) {
QDBusReply<bool> reply = iface.call(QStringLiteral("hasDualGpu"));
if (reply.isValid() && reply.value()) {
s_gpuCheck = GpuCheck::Present;
s_gpuEnv.insert(QStringLiteral("DRI_PRIME"), QStringLiteral("1"));
return;
}
}
s_gpuCheck = GpuCheck::Absent;
#endif
}
namespace KIO
{
bool hasDiscreteGpu()
{
checkGpu();
return s_gpuCheck == GpuCheck::Present;
}
QProcessEnvironment discreteGpuEnvironment()
{
checkGpu();
return s_gpuEnv;
}
}
@@ -0,0 +1,28 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2023 Dave Vasilevsky <dave@vasilevsky.ca>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only
*/
#ifndef _KIO_GPUDETECTION_H_
#define _KIO_GPUDETECTION_H_
#include "kiogui_export.h"
class QProcessEnvironment;
namespace KIO
{
/**
* Detects whether the system has a discrete GPU.
*/
KIOGUI_EXPORT bool hasDiscreteGpu();
/**
* Environment variables that make a process run with the discrete GPU.
*/
KIOGUI_NO_EXPORT QProcessEnvironment discreteGpuEnvironment();
}
#endif
@@ -0,0 +1,262 @@
/*
SPDX-FileCopyrightText: 2006-2010 Peter Penz <peter.penz@gmx.at>
SPDX-FileCopyrightText: 2006 Aaron J. Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
SPDX-FileCopyrightText: 2007 Urs Wolfer <uwolfer @ kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kcoreurlnavigator.h"
#include "urlutil_p.h"
#include <KIO/StatJob>
#include <KLocalizedString>
#include <kfileitem.h>
#include <kprotocolinfo.h>
#include <QDir>
#include <QMimeData>
#include <QMimeDatabase>
#include <algorithm>
#include <numeric>
struct LocationData {
QUrl url;
QVariant state;
};
class KCoreUrlNavigatorPrivate
{
public:
KCoreUrlNavigatorPrivate(KCoreUrlNavigator *qq);
~KCoreUrlNavigatorPrivate()
{
}
/**
* Returns true, if the MIME type of the path represents a
* compressed file like TAR or ZIP, as listed in @p archiveMimetypes
*/
bool isCompressedPath(const QUrl &path, const QStringList &archiveMimetypes) const;
/**
* Returns the current history index, if \a historyIndex is
* smaller than 0. If \a historyIndex is greater or equal than
* the number of available history items, the largest possible
* history index is returned. For the other cases just \a historyIndex
* is returned.
*/
int adjustedHistoryIndex(int historyIndex) const;
KCoreUrlNavigator *const q;
QList<LocationData> m_history;
int m_historyIndex = 0;
};
KCoreUrlNavigatorPrivate::KCoreUrlNavigatorPrivate(KCoreUrlNavigator *qq)
: q(qq)
{
}
bool KCoreUrlNavigatorPrivate::isCompressedPath(const QUrl &url, const QStringList &archiveMimetypes) const
{
QMimeDatabase db;
const QMimeType mime = db.mimeTypeForUrl(QUrl(url.toString(QUrl::StripTrailingSlash)));
return std::any_of(archiveMimetypes.begin(), archiveMimetypes.end(), [mime](const QString &archiveType) {
return mime.inherits(archiveType);
});
}
int KCoreUrlNavigatorPrivate::adjustedHistoryIndex(int historyIndex) const
{
const int historySize = m_history.size();
if (historyIndex < 0) {
historyIndex = m_historyIndex;
} else if (historyIndex >= historySize) {
historyIndex = historySize - 1;
Q_ASSERT(historyIndex >= 0); // m_history.size() must always be > 0
}
return historyIndex;
}
// ------------------------------------------------------------------------------------------------
KCoreUrlNavigator::KCoreUrlNavigator(const QUrl &url, QObject *parent)
: QObject(parent)
, d(new KCoreUrlNavigatorPrivate(this))
{
d->m_history.prepend(LocationData{url.adjusted(QUrl::NormalizePathSegments), {}});
}
KCoreUrlNavigator::~KCoreUrlNavigator()
{
}
QUrl KCoreUrlNavigator::locationUrl(int historyIndex) const
{
historyIndex = d->adjustedHistoryIndex(historyIndex);
return d->m_history.at(historyIndex).url;
}
void KCoreUrlNavigator::saveLocationState(const QVariant &state)
{
d->m_history[d->m_historyIndex].state = state;
}
QVariant KCoreUrlNavigator::locationState(int historyIndex) const
{
historyIndex = d->adjustedHistoryIndex(historyIndex);
return d->m_history.at(historyIndex).state;
}
bool KCoreUrlNavigator::goBack()
{
const int count = d->m_history.size();
if (d->m_historyIndex < count - 1) {
const QUrl newUrl = locationUrl(d->m_historyIndex + 1);
Q_EMIT currentUrlAboutToChange(newUrl);
++d->m_historyIndex;
Q_EMIT historyIndexChanged();
Q_EMIT historyChanged();
Q_EMIT currentLocationUrlChanged();
return true;
}
return false;
}
bool KCoreUrlNavigator::goForward()
{
if (d->m_historyIndex > 0) {
const QUrl newUrl = locationUrl(d->m_historyIndex - 1);
Q_EMIT currentUrlAboutToChange(newUrl);
--d->m_historyIndex;
Q_EMIT historyIndexChanged();
Q_EMIT historyChanged();
Q_EMIT currentLocationUrlChanged();
return true;
}
return false;
}
bool KCoreUrlNavigator::goUp()
{
const QUrl currentUrl = locationUrl();
const QUrl upUrl = KIO::upUrl(currentUrl);
if (!currentUrl.matches(upUrl, QUrl::StripTrailingSlash)) {
setCurrentLocationUrl(upUrl);
return true;
}
return false;
}
QUrl KCoreUrlNavigator::currentLocationUrl() const
{
return locationUrl();
}
void KCoreUrlNavigator::setCurrentLocationUrl(const QUrl &newUrl)
{
if (newUrl == locationUrl()) {
return;
}
QUrl url = newUrl.adjusted(QUrl::NormalizePathSegments);
// This will be used below; we define it here because in the lower part of the
// code locationUrl() and url become the same URLs
QUrl firstChildUrl = KIO::UrlUtil::firstChildUrl(locationUrl(), url);
const QString scheme = url.scheme();
if (!scheme.isEmpty()) {
// Check if the URL represents a tar-, zip- or 7z-file, or an archive file supported by krarc.
const QStringList archiveMimetypes = KProtocolInfo::archiveMimetypes(scheme);
if (!archiveMimetypes.isEmpty()) {
// Check whether the URL is really part of the archive file, otherwise
// replace it by the local path again.
bool insideCompressedPath = d->isCompressedPath(url, archiveMimetypes);
if (!insideCompressedPath) {
QUrl prevUrl = url;
QUrl parentUrl = KIO::upUrl(url);
while (parentUrl != prevUrl) {
if (d->isCompressedPath(parentUrl, archiveMimetypes)) {
insideCompressedPath = true;
break;
}
prevUrl = parentUrl;
parentUrl = KIO::upUrl(parentUrl);
}
}
if (!insideCompressedPath) {
// drop the tar:, zip:, sevenz: or krarc: protocol since we are not
// inside the compressed path
url.setScheme(QStringLiteral("file"));
firstChildUrl.setScheme(QStringLiteral("file"));
}
}
}
// Check whether current history element has the same URL.
// If this is the case, just ignore setting the URL.
const LocationData &data = d->m_history.at(d->m_historyIndex);
const bool isUrlEqual = url.matches(locationUrl(), QUrl::StripTrailingSlash) || (!url.isValid() && url.matches(data.url, QUrl::StripTrailingSlash));
if (isUrlEqual) {
return;
}
Q_EMIT currentUrlAboutToChange(url);
if (d->m_historyIndex > 0) {
// If an URL is set when the history index is not at the end (= 0),
// then clear all previous history elements so that a new history
// tree is started from the current position.
auto begin = d->m_history.begin();
auto end = begin + d->m_historyIndex;
d->m_history.erase(begin, end);
d->m_historyIndex = 0;
}
Q_ASSERT(d->m_historyIndex == 0);
d->m_history.insert(0, LocationData{url, {}});
// Prevent an endless growing of the history: remembering
// the last 100 Urls should be enough...
const int historyMax = 100;
if (d->m_history.size() > historyMax) {
auto begin = d->m_history.begin() + historyMax;
auto end = d->m_history.end();
d->m_history.erase(begin, end);
}
Q_EMIT historyIndexChanged();
Q_EMIT historySizeChanged();
Q_EMIT historyChanged();
Q_EMIT currentLocationUrlChanged();
if (firstChildUrl.isValid()) {
Q_EMIT urlSelectionRequested(firstChildUrl);
}
}
int KCoreUrlNavigator::historySize() const
{
return d->m_history.count();
}
int KCoreUrlNavigator::historyIndex() const
{
return d->m_historyIndex;
}
#include "moc_kcoreurlnavigator.cpp"
@@ -0,0 +1,157 @@
/*
SPDX-FileCopyrightText: 2006-2010 Peter Penz <peter.penz@gmx.at>
SPDX-FileCopyrightText: 2006 Aaron J. Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
SPDX-FileCopyrightText: 2007 Urs Wolfer <uwolfer @ kde.org>
SPDX-FileCopyrightText: 2022 Carson Black <uhhadd@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KCOREURLNAVIGATOR_H
#define KCOREURLNAVIGATOR_H
#include "kiogui_export.h"
#include <QByteArray>
#include <QObject>
#include <QUrl>
#include <memory>
class QMouseEvent;
class KFilePlacesModel;
class KUrlComboBox;
class KCoreUrlNavigatorPrivate;
/**
* @class KCoreUrlNavigator kcoreurlnavigator.h <KCoreUrlNavigator>
*
* @brief Object that helps with keeping track of URLs in file-manager like interfaces.
*
* @since 5.93
*/
class KIOGUI_EXPORT KCoreUrlNavigator : public QObject
{
Q_OBJECT
public:
KCoreUrlNavigator(const QUrl &url = QUrl(), QObject *parent = nullptr);
~KCoreUrlNavigator() override;
Q_PROPERTY(QUrl currentLocationUrl READ currentLocationUrl WRITE setCurrentLocationUrl NOTIFY currentLocationUrlChanged)
QUrl currentLocationUrl() const;
void setCurrentLocationUrl(const QUrl &url);
Q_SIGNAL void currentLocationUrlChanged();
/**
* Is emitted, before the location URL is going to be changed to \a newUrl.
* The signal KCoreUrlNavigator::urlChanged() will be emitted after the change
* has been done. Connecting to this signal is useful to save the state
* of a view with KCoreUrlNavigator::saveLocationState().
*/
Q_SIGNAL void currentUrlAboutToChange(const QUrl &newUrl);
/**
* The amount of locations in the history. The data for each
* location can be retrieved by KCoreUrlNavigator::locationUrl() and
* KCoreUrlNavigator::locationState().
*/
Q_PROPERTY(int historySize READ historySize NOTIFY historySizeChanged)
int historySize() const;
Q_SIGNAL void historySizeChanged();
/**
* When the URL is changed and the new URL (e.g.\ /home/user1/)
* is a parent of the previous URL (e.g.\ /home/user1/data/stuff),
* then this signal is emitted and \p url is set to the child
* directory of the new URL which is an ancestor of the old URL
* (in the example paths this would be /home/user1/data/).
* This signal allows file managers to pre-select the directory
* that the user is navigating up from.
* @since 5.95
*/
Q_SIGNAL void urlSelectionRequested(const QUrl &url);
/**
* The history index of the current location, where
* 0 <= history index < KCoreUrlNavigator::historySize(). 0 is the most
* recent history entry.
*/
Q_PROPERTY(int historyIndex READ historyIndex NOTIFY historyIndexChanged)
int historyIndex() const;
Q_SIGNAL void historyIndexChanged();
/**
* Is emitted, if the history has been changed. Usually
* the history is changed if a new URL has been selected.
*/
Q_SIGNAL void historyChanged();
/**
* @return URL of the location given by the \a historyIndex. If \a historyIndex
* is smaller than 0, the URL of the current location is returned.
*/
Q_INVOKABLE QUrl locationUrl(int historyIndex = -1) const;
/**
* Saves the location state described by \a state for the current location. It is recommended
* that at least the scroll position of a view is remembered and restored when traversing
* through the history. Saving the location state should be done when the signal
* KCoreUrlNavigator::urlAboutToBeChanged() has been emitted. Restoring the location state (see
* KCoreUrlNavigator::locationState()) should be done when the signal KCoreUrlNavigator::urlChanged()
* has been emitted.
*
* Example:
* \code
* urlNavigator->saveLocationState(QPoint(x, y));
* \endcode
*
*/
Q_INVOKABLE void saveLocationState(const QVariant &state);
/**
* @return Location state given by \a historyIndex. If \a historyIndex
* is smaller than 0, the state of the current location is returned.
* @see KCoreUrlNavigator::saveLocationState()
*/
Q_INVOKABLE QVariant locationState(int historyIndex = -1) const;
/**
* Goes back one step in the URL history. The signals
* KCoreUrlNavigator::urlAboutToBeChanged(), KCoreUrlNavigator::urlChanged() and
* KCoreUrlNavigator::historyChanged() are emitted if true is returned. False is returned
* if the beginning of the history has already been reached and hence going back was
* not possible. The history index (see KCoreUrlNavigator::historyIndex()) is
* increased by one if the operation was successful.
*/
Q_INVOKABLE bool goBack();
/**
* Goes forward one step in the URL history. The signals
* KCoreUrlNavigator::urlAboutToBeChanged(), KCoreUrlNavigator::urlChanged() and
* KCoreUrlNavigator::historyChanged() are emitted if true is returned. False is returned
* if the end of the history has already been reached and hence going forward
* was not possible. The history index (see KCoreUrlNavigator::historyIndex()) is
* decreased by one if the operation was successful.
*/
Q_INVOKABLE bool goForward();
/**
* Goes up one step of the URL path and remembers the old path
* in the history. The signals KCoreUrlNavigator::urlAboutToBeChanged(),
* KCoreUrlNavigator::urlChanged() and KCoreUrlNavigator::historyChanged() are
* emitted if true is returned. False is returned if going up was not
* possible as the root has been reached.
*/
Q_INVOKABLE bool goUp();
private:
friend class KCoreUrlNavigatorPrivate;
std::unique_ptr<KCoreUrlNavigatorPrivate> const d;
};
#endif
@@ -0,0 +1,189 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2021 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "kemailclientlauncherjob.h"
#include <KApplicationTrader>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KMacroExpander>
#include <KService>
#include <KSharedConfig>
#include <KShell>
#include <QProcessEnvironment>
#include <QUrlQuery>
#include "desktopexecparser.h"
#include <KIO/ApplicationLauncherJob>
#include <KIO/CommandLauncherJob>
#ifdef Q_OS_WIN
#include <windows.h> // Must be included before shellapi.h
#include <shellapi.h>
#endif
class KEMailClientLauncherJobPrivate
{
public:
QStringList m_to;
QStringList m_cc;
QStringList m_bcc;
QString m_subject;
QString m_body;
QList<QUrl> m_attachments;
QByteArray m_startupId;
};
KEMailClientLauncherJob::KEMailClientLauncherJob(QObject *parent)
: KJob(parent)
, d(new KEMailClientLauncherJobPrivate)
{
}
KEMailClientLauncherJob::~KEMailClientLauncherJob() = default;
void KEMailClientLauncherJob::setTo(const QStringList &to)
{
d->m_to = to;
}
void KEMailClientLauncherJob::setCc(const QStringList &cc)
{
d->m_cc = cc;
}
void KEMailClientLauncherJob::setBcc(const QStringList &bcc)
{
d->m_bcc = bcc;
}
void KEMailClientLauncherJob::setSubject(const QString &subject)
{
d->m_subject = subject;
}
void KEMailClientLauncherJob::setBody(const QString &body)
{
d->m_body = body;
}
void KEMailClientLauncherJob::setAttachments(const QList<QUrl> &urls)
{
d->m_attachments = urls;
}
void KEMailClientLauncherJob::setStartupId(const QByteArray &startupId)
{
d->m_startupId = startupId;
}
void KEMailClientLauncherJob::start()
{
#ifndef Q_OS_WIN
KService::Ptr service = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/mailto"));
if (!service) {
setError(KJob::UserDefinedError);
setErrorText(i18n("No mail client found"));
emitDelayedResult();
return;
}
const QString entryPath = service->entryPath().toLower();
if (entryPath.contains(QLatin1String("thunderbird")) || entryPath.contains(QLatin1String("dovecot"))) {
const QString exec = KIO::DesktopExecParser::executableName(service->exec());
auto *subjob = new KIO::CommandLauncherJob(exec, thunderbirdArguments(), this);
subjob->setStartupId(d->m_startupId);
connect(subjob, &KJob::result, this, &KEMailClientLauncherJob::emitResult);
subjob->start();
} else {
auto *subjob = new KIO::ApplicationLauncherJob(service, this);
subjob->setUrls({mailToUrl()});
subjob->setStartupId(d->m_startupId);
connect(subjob, &KJob::result, this, &KEMailClientLauncherJob::emitResult);
subjob->start();
}
#else
const QString url = mailToUrl().toString();
const QString sOpen = QStringLiteral("open");
ShellExecuteW(0, (LPCWSTR)sOpen.utf16(), (LPCWSTR)url.utf16(), 0, 0, SW_NORMAL);
emitDelayedResult();
#endif
}
void KEMailClientLauncherJob::emitDelayedResult()
{
// Use delayed invocation so the caller has time to connect to the signal
QMetaObject::invokeMethod(this, &KEMailClientLauncherJob::emitResult, Qt::QueuedConnection);
}
QUrl KEMailClientLauncherJob::mailToUrl() const
{
QUrl url;
QUrlQuery query;
for (const QString &to : std::as_const(d->m_to)) {
if (url.path().isEmpty()) {
url.setPath(to);
} else {
query.addQueryItem(QStringLiteral("to"), to);
}
}
for (const QString &cc : std::as_const(d->m_cc)) {
query.addQueryItem(QStringLiteral("cc"), cc);
}
for (const QString &bcc : std::as_const(d->m_bcc)) {
query.addQueryItem(QStringLiteral("bcc"), bcc);
}
for (const QUrl &url : std::as_const(d->m_attachments)) {
query.addQueryItem(QStringLiteral("attach"), url.toString());
}
if (!d->m_subject.isEmpty()) {
query.addQueryItem(QStringLiteral("subject"), d->m_subject);
}
if (!d->m_body.isEmpty()) {
query.addQueryItem(QStringLiteral("body"), d->m_body);
}
url.setQuery(query);
if (!url.path().isEmpty() || url.hasQuery()) {
url.setScheme(QStringLiteral("mailto"));
}
return url;
}
QStringList KEMailClientLauncherJob::thunderbirdArguments() const
{
// Thunderbird supports mailto URLs, but refuses attachments for security reasons
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1613425)
// It however supports a "command-line" syntax (also used by xdg-email)
// which includes attachments.
QString arg;
const QChar quote = QLatin1Char('\'');
auto addString = [&](const char *token, const QString &str) {
if (!str.isEmpty()) {
arg += QLatin1String(token) + quote + str + quote;
}
};
auto addList = [&](const char *token, const QStringList &list) {
if (!list.isEmpty()) {
arg += QLatin1String(token) + quote + list.join(QLatin1Char(',')) + quote;
}
};
addList(",to=", d->m_to);
addList(",cc=", d->m_cc);
addList(",bcc=", d->m_bcc);
addList(",attachment=", QUrl::toStringList(d->m_attachments));
addString(",subject=", d->m_subject);
addString(",body=", d->m_body);
QStringList resultArgs{QLatin1String("-compose")};
if (!arg.isEmpty()) {
resultArgs.push_back(arg.mid(1)); // remove first comma
}
return resultArgs;
}
#include "moc_kemailclientlauncherjob.cpp"
@@ -0,0 +1,109 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2021 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KEMAILCLIENTLAUNCHERJOB_H
#define KEMAILCLIENTLAUNCHERJOB_H
#include "kiogui_export.h"
#include <KJob>
#include <memory>
class KEMailClientLauncherJobPrivate;
/**
* @class KEMailClientLauncherJob kemailclientlauncherjob.h <KEMailClientLauncherJob>
*
* @brief KEMailClientLauncherJob starts a mail client in order to compose a new mail.
*
* It creates a startup notification and finishes it on success or on error (for the taskbar).
* It also emits an error message if necessary (e.g. "program not found").
*
* The job finishes when the application is successfully started.
* For error handling, either connect to the result() signal, or for a simple messagebox on error,
* you can do
* @code
* job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
* @endcode
*
* @since 5.87
*/
class KIOGUI_EXPORT KEMailClientLauncherJob : public KJob
{
Q_OBJECT
public:
/**
* Creates a KEMailClientLauncherJob.
* @param parent the parent QObject
*/
explicit KEMailClientLauncherJob(QObject *parent = nullptr);
/**
* Destructor
*
* Note that jobs auto-delete themselves after emitting result
*/
~KEMailClientLauncherJob() override;
/**
* Sets the email address(es) that will be used in the To field for the email
* @param to recipients; each entry can use the format "someone@example.com" or "John Doe <someone@example.com>"
*/
void setTo(const QStringList &to);
/**
* Sets the email address(es) that will be used in the CC field for the email
* @param cc recipients; each entry can use the format "someone@example.com" or "John Doe <someone@example.com>"
*/
void setCc(const QStringList &cc);
/**
* Sets the email address(es) that will be used in the Bcc field for the email
* @param bcc recipients; each entry can use the format "someone@example.com" or "John Doe <someone@example.com>"
* @since 5.96
*/
void setBcc(const QStringList &bcc);
/**
* Sets the subject for the email
* @param subject the email subject
*/
void setSubject(const QString &subject);
/**
* Sets the body for the email
* @param body the email body
*/
void setBody(const QString &body);
/**
* Sets attachments for the email
* @param urls URLs of the attachments for the email
* Remember to use QUrl::fromLocalFile() to construct those URLs from local file paths.
*/
void setAttachments(const QList<QUrl> &urls);
/**
* Sets the platform-specific startup id of the mail client launch.
* @param startupId startup id, if any (otherwise "").
* For X11, this would be the id for the Startup Notification protocol.
* For Wayland, this would be the token for the XDG Activation protocol.
*/
void setStartupId(const QByteArray &startupId);
/**
* Starts the job.
* You must call this, after having called all the necessary setters.
*/
void start() override;
private:
friend class KEMailClientLauncherJobTest;
QUrl mailToUrl() const; // for the unittest
QStringList thunderbirdArguments() const; // for the unittest
KIOGUI_NO_EXPORT void emitDelayedResult();
friend class KEMailClientLauncherJobPrivate;
std::unique_ptr<KEMailClientLauncherJobPrivate> d;
};
#endif // KEMAILCLIENTLAUNCHERJOB_H
@@ -0,0 +1,513 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "kprocessrunner_p.h"
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
#include "systemd/scopedprocessrunner_p.h"
#include "systemd/systemdprocessrunner_p.h"
#endif
#include "config-kiogui.h"
#include "kiogui_debug.h"
#include "desktopexecparser.h"
#include "gpudetection_p.h"
#include "krecentdocument.h"
#include <KDesktopFile>
#include <KLocalizedString>
#include <KWindowSystem>
#if HAVE_WAYLAND
#include <KWaylandExtras>
#endif
#ifdef WITH_QTDBUS
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusReply>
#include "dbusactivationrunner_p.h"
#endif
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QProcess>
#include <QStandardPaths>
#include <QString>
#include <QTimer>
#include <QUuid>
#ifdef Q_OS_WIN
#include "windows.h"
#include "shellapi.h" // Must be included after "windows.h"
#endif
static int s_instanceCount = 0; // for the unittest
KProcessRunner::KProcessRunner()
: m_process{new KProcess}
{
++s_instanceCount;
}
static KProcessRunner *makeInstance()
{
#if defined(Q_OS_LINUX) && defined(WITH_QTDBUS)
switch (SystemdProcessRunner::modeAvailable()) {
case KProcessRunner::SystemdAsService:
return new SystemdProcessRunner();
case KProcessRunner::SystemdAsScope:
return new ScopedProcessRunner();
default:
#else
{
#endif
return new ForkingProcessRunner();
}
}
#ifndef Q_OS_ANDROID
static void modifyEnv(KProcess &process, QProcessEnvironment mod)
{
QProcessEnvironment env = process.processEnvironment();
if (env.isEmpty()) {
env = QProcessEnvironment::systemEnvironment();
}
env.insert(mod);
process.setProcessEnvironment(env);
}
#endif
KProcessRunner *KProcessRunner::fromApplication(const KService::Ptr &service,
const QString &serviceEntryPath,
const QList<QUrl> &urls,
KIO::ApplicationLauncherJob::RunFlags flags,
const QString &suggestedFileName,
const QByteArray &asn)
{
#ifdef WITH_QTDBUS
// special case for applicationlauncherjob
// FIXME: KProcessRunner is currently broken and fails to prepare the m_urls member
// DBusActivationRunner uses, which then only calls "Activate", not "Open".
// Possibly will need some special mode of DesktopExecParser
// for the D-Bus activation call scenario to handle URLs with protocols
// the invoked service/executable might not support.
KProcessRunner *instance;
const bool notYetSupportedOpenActivationNeeded = !urls.isEmpty();
if (!notYetSupportedOpenActivationNeeded && DBusActivationRunner::activationPossible(service, flags, suggestedFileName)) {
const auto actions = service->actions();
auto action = std::find_if(actions.cbegin(), actions.cend(), [service](const KServiceAction &action) {
return action.exec() == service->exec();
});
instance = new DBusActivationRunner(action != actions.cend() ? action->name() : QString());
} else {
instance = makeInstance();
}
#else
KProcessRunner *instance = makeInstance();
#endif
if (!service->isValid()) {
instance->emitDelayedError(i18n("The desktop entry file\n%1\nis not valid.", serviceEntryPath));
return instance;
}
instance->m_executable = KIO::DesktopExecParser::executablePath(service->exec());
KIO::DesktopExecParser execParser(*service, urls);
execParser.setUrlsAreTempFiles(flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
execParser.setSuggestedFileName(suggestedFileName);
const QStringList args = execParser.resultingArguments();
if (args.isEmpty()) {
instance->emitDelayedError(execParser.errorMessage());
return instance;
}
qCDebug(KIO_GUI) << "Starting process:" << args;
*instance->m_process << args;
#ifndef Q_OS_ANDROID
if (service->runOnDiscreteGpu()) {
modifyEnv(*instance->m_process, KIO::discreteGpuEnvironment());
}
#endif
QString workingDir(service->workingDirectory());
if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) {
// systemd requires working directory to be normalized, or '~'
workingDir = QFileInfo(urls.first().toLocalFile()).canonicalPath();
}
instance->m_process->setWorkingDirectory(workingDir);
if ((flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles) == 0) {
// Remember we opened those urls, for the "recent documents" menu in kicker
for (const QUrl &url : urls) {
KRecentDocument::add(url, service->desktopEntryName());
}
}
instance->init(service, serviceEntryPath, service->name(), asn);
return instance;
}
KProcessRunner *KProcessRunner::fromCommand(const QString &cmd,
const QString &desktopName,
const QString &execName,
const QByteArray &asn,
const QString &workingDirectory,
const QProcessEnvironment &environment)
{
auto instance = makeInstance();
instance->m_executable = KIO::DesktopExecParser::executablePath(execName);
instance->m_cmd = cmd;
#ifdef Q_OS_WIN
if (cmd.startsWith(QLatin1String("wt.exe")) || cmd.startsWith(QLatin1String("pwsh.exe")) || cmd.startsWith(QLatin1String("powershell.exe"))) {
instance->m_process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments *args) {
args->flags |= CREATE_NEW_CONSOLE;
args->startupInfo->dwFlags &= ~STARTF_USESTDHANDLES;
});
const int firstSpace = cmd.indexOf(QLatin1Char(' '));
instance->m_process->setProgram(cmd.left(firstSpace));
instance->m_process->setNativeArguments(cmd.mid(firstSpace + 1));
} else
#endif
instance->m_process->setShellCommand(cmd);
instance->initFromDesktopName(desktopName, execName, asn, workingDirectory, environment);
return instance;
}
KProcessRunner *KProcessRunner::fromExecutable(const QString &executable,
const QStringList &args,
const QString &desktopName,
const QByteArray &asn,
const QString &workingDirectory,
const QProcessEnvironment &environment)
{
const QString actualExec = QStandardPaths::findExecutable(executable);
if (actualExec.isEmpty()) {
qCInfo(KIO_GUI) << "Could not find an executable named:" << executable;
return {};
}
auto instance = makeInstance();
instance->m_executable = KIO::DesktopExecParser::executablePath(executable);
instance->m_process->setProgram(actualExec, args);
instance->initFromDesktopName(desktopName, executable, asn, workingDirectory, environment);
return instance;
}
void KProcessRunner::initFromDesktopName(const QString &desktopName,
const QString &execName,
const QByteArray &asn,
const QString &workingDirectory,
const QProcessEnvironment &environment)
{
if (!workingDirectory.isEmpty()) {
m_process->setWorkingDirectory(workingDirectory);
}
m_process->setProcessEnvironment(environment);
if (!desktopName.isEmpty()) {
KService::Ptr service = KService::serviceByDesktopName(desktopName);
if (service) {
if (m_executable.isEmpty()) {
m_executable = KIO::DesktopExecParser::executablePath(service->exec());
}
init(service, service->entryPath(), service->name(), asn);
return;
}
}
init(KService::Ptr(), QString{}, execName /*user-visible name*/, asn);
}
void KProcessRunner::init(const KService::Ptr &service, const QString &serviceEntryPath, const QString &userVisibleName, const QByteArray &asn)
{
m_serviceEntryPath = serviceEntryPath;
if (service && !serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(serviceEntryPath)) {
qCWarning(KIO_GUI) << "No authorization to execute" << serviceEntryPath;
emitDelayedError(i18n("You are not authorized to execute this file."));
return;
}
if (service) {
m_service = service;
// Store the desktop name, used by debug output and for the systemd unit name
m_desktopName = service->menuId();
if (m_desktopName.isEmpty() && m_executable == QLatin1String("systemsettings")) {
m_desktopName = QStringLiteral("systemsettings.desktop");
}
if (m_desktopName.endsWith(QLatin1String(".desktop"))) { // always true, in theory
m_desktopName.chop(strlen(".desktop"));
}
if (m_desktopName.isEmpty()) { // desktop files not in the menu
// desktopEntryName is lowercase so this is only a fallback
m_desktopName = service->desktopEntryName();
}
m_desktopFilePath = QFileInfo(serviceEntryPath).absoluteFilePath();
m_description = service->name();
if (!service->genericName().isEmpty()) {
m_description.append(QStringLiteral(" - %1").arg(service->genericName()));
}
} else {
m_description = userVisibleName;
}
#if HAVE_X11
static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb");
if (isX11) {
bool silent;
QByteArray wmclass;
const bool startup_notify = (asn != "0" && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass));
if (startup_notify) {
m_startupId.initId(asn);
m_startupId.setupStartupEnv();
KStartupInfoData data;
data.setHostname();
// When it comes from a desktop file, m_executable can be a full shell command, so <bin> here is not 100% reliable.
// E.g. it could be "cd", which isn't an existing binary. It's just a heuristic anyway.
const QString bin = KIO::DesktopExecParser::executableName(m_executable);
data.setBin(bin);
if (!userVisibleName.isEmpty()) {
data.setName(userVisibleName);
} else if (service && !service->name().isEmpty()) {
data.setName(service->name());
}
data.setDescription(i18n("Launching %1", data.name()));
if (service && !service->icon().isEmpty()) {
data.setIcon(service->icon());
}
if (!wmclass.isEmpty()) {
data.setWMClass(wmclass);
}
if (silent) {
data.setSilent(KStartupInfoData::Yes);
}
if (service && !serviceEntryPath.isEmpty()) {
data.setApplicationId(serviceEntryPath);
}
KStartupInfo::sendStartup(m_startupId, data);
}
}
#else
Q_UNUSED(userVisibleName);
#endif
#if HAVE_WAYLAND
if (KWindowSystem::isPlatformWayland()) {
if (!asn.isEmpty()) {
m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), QString::fromUtf8(asn));
} else {
auto window = qGuiApp->focusWindow();
if (!window && !qGuiApp->allWindows().isEmpty()) {
window = qGuiApp->allWindows().constFirst();
}
if (window) {
const int launchedSerial = KWaylandExtras::lastInputSerial(window);
m_waitingForXdgToken = true;
connect(
KWaylandExtras::self(),
&KWaylandExtras::xdgActivationTokenArrived,
m_process.get(),
[this, launchedSerial](int tokenSerial, const QString &token) {
if (tokenSerial == launchedSerial) {
m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), token);
m_waitingForXdgToken = false;
startProcess();
}
},
Qt::SingleShotConnection);
KWaylandExtras::requestXdgActivationToken(window, launchedSerial, resolveServiceAlias());
}
}
}
#endif
if (!m_waitingForXdgToken) {
startProcess();
}
}
void ForkingProcessRunner::startProcess()
{
connect(m_process.get(), &QProcess::finished, this, &ForkingProcessRunner::slotProcessExited);
connect(m_process.get(), &QProcess::started, this, &ForkingProcessRunner::slotProcessStarted, Qt::QueuedConnection);
connect(m_process.get(), &QProcess::errorOccurred, this, &ForkingProcessRunner::slotProcessError);
m_process->start();
}
bool ForkingProcessRunner::waitForStarted(int timeout)
{
if (m_process->state() == QProcess::NotRunning && m_waitingForXdgToken) {
QEventLoop loop;
QObject::connect(m_process.get(), &QProcess::stateChanged, &loop, &QEventLoop::quit);
QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
loop.exec();
}
return m_process->waitForStarted(timeout);
}
void ForkingProcessRunner::slotProcessError(QProcess::ProcessError errorCode)
{
// E.g. the process crashed.
// This is unlikely to happen while the ApplicationLauncherJob is still connected to the KProcessRunner.
// So the emit does nothing, this is really just for debugging.
qCDebug(KIO_GUI) << name() << "error=" << errorCode << m_process->errorString();
Q_EMIT error(m_process->errorString());
}
void ForkingProcessRunner::slotProcessStarted()
{
setPid(m_process->processId());
}
void KProcessRunner::setPid(qint64 pid)
{
if (!m_pid && pid) {
qCDebug(KIO_GUI) << "Setting PID" << pid << "for:" << name();
m_pid = pid;
#if HAVE_X11
if (!m_startupId.isNull()) {
KStartupInfoData data;
data.addPid(static_cast<int>(m_pid));
KStartupInfo::sendChange(m_startupId, data);
KStartupInfo::resetStartupEnv();
}
#endif
Q_EMIT processStarted(pid);
}
}
KProcessRunner::~KProcessRunner()
{
// This destructor deletes m_process, since it's a unique_ptr.
--s_instanceCount;
}
int KProcessRunner::instanceCount()
{
return s_instanceCount;
}
void KProcessRunner::terminateStartupNotification()
{
#if HAVE_X11
if (!m_startupId.isNull()) {
KStartupInfoData data;
data.addPid(static_cast<int>(m_pid)); // announce this pid for the startup notification has finished
data.setHostname();
KStartupInfo::sendFinish(m_startupId, data);
}
#endif
}
QString KProcessRunner::name() const
{
return !m_desktopName.isEmpty() ? m_desktopName : m_executable;
}
// Only alphanum, ':' and '_' allowed in systemd unit names
QString KProcessRunner::escapeUnitName(const QString &input)
{
QString res;
const QByteArray bytes = input.toUtf8();
for (const unsigned char c : bytes) {
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == ':' || c == '_' || c == '.') {
res += QLatin1Char(c);
} else {
res += QStringLiteral("\\x%1").arg(c, 2, 16, QLatin1Char('0'));
}
}
return res;
}
QString KProcessRunner::resolveServiceAlias() const
{
// Don't actually load aliased desktop file to avoid having to deal with recursion
QString servName = m_service ? m_service->aliasFor() : QString{};
if (servName.isEmpty()) {
servName = name();
}
return servName;
}
void KProcessRunner::emitDelayedError(const QString &errorMsg)
{
qCWarning(KIO_GUI) << name() << errorMsg;
terminateStartupNotification();
// Use delayed invocation so the caller has time to connect to the signal
auto func = [this, errorMsg]() {
Q_EMIT error(errorMsg);
deleteLater();
};
QMetaObject::invokeMethod(this, func, Qt::QueuedConnection);
}
void ForkingProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus)
{
qCDebug(KIO_GUI) << name() << "exitCode=" << exitCode << "exitStatus=" << exitStatus;
#ifdef Q_OS_UNIX
if (exitCode == 127) {
#else
if (exitCode == 9009) {
#endif
const QStringList args = m_cmd.split(QLatin1Char(' '));
emitDelayedError(xi18nc("@info", "The command <command>%1</command> could not be found.", args[0])); // Calls deleteLater().
} else {
terminateStartupNotification();
deleteLater();
}
}
bool KIOGuiPrivate::checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg)
{
bool silent = false;
QByteArray wmclass;
if (service && service->startupNotify().has_value()) {
silent = !service->startupNotify().value();
wmclass = service->property<QByteArray>(QStringLiteral("StartupWMClass"));
} else { // non-compliant app
if (service) {
if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant
wmclass = "0"; // krazy:exclude=doublequote_chars
} else {
return false; // no startup notification at all
}
} else {
#if 0
// Create startup notification even for apps for which there shouldn't be any,
// just without any visual feedback. This will ensure they'll be positioned on the proper
// virtual desktop, and will get user timestamp from the ASN ID.
wmclass = '0';
silent = true;
#else // That unfortunately doesn't work, when the launched non-compliant application
// launches another one that is compliant and there is any delay in between (bnc:#343359)
return false;
#endif
}
}
if (silent_arg) {
*silent_arg = silent;
}
if (wmclass_arg) {
*wmclass_arg = wmclass;
}
return true;
}
ForkingProcessRunner::ForkingProcessRunner()
: KProcessRunner()
{
}
#include "moc_kprocessrunner_p.cpp"
@@ -0,0 +1,178 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KPROCESSRUNNER_P_H
#define KPROCESSRUNNER_P_H
#include "applicationlauncherjob.h"
#include "config-kiogui.h"
#include "kiogui_export.h"
#include <KProcess>
#if HAVE_X11
#include <KStartupInfo>
#endif
#include <QObject>
#include <memory>
namespace KIOGuiPrivate
{
bool checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg);
}
/**
* @internal (exported for KIO GUI job unit tests)
* This class runs a KService or a shell command, using QProcess internally.
* It creates a startup notification and finishes it on success or on error (for the taskbar)
* It also shows an error message if necessary (e.g. "program not found").
*/
class KIOGUI_EXPORT KProcessRunner : public QObject
{
Q_OBJECT
public:
enum LaunchMode {
Forking,
SystemdAsScope,
SystemdAsService,
};
Q_ENUM(LaunchMode)
/**
* Run a KService (application desktop file) to open @p urls.
* @param service the service to run
* @param urls the list of URLs, can be empty
* @param flags various flags
* @param suggestedFileName see KRun::setSuggestedFileName
* @param asn Application startup notification id, if any (otherwise "")
* @param serviceEntryPath the KService entryPath(), passed as an argument
* because in some cases it could become an empty string, e.g. if an
* ApplicationLauncherJob is created from a @c KServiceAction, the
* ApplicationLauncherJob will call KService::setExec() which clears the
* entryPath() of the KService
*/
static KProcessRunner *fromApplication(const KService::Ptr &service,
const QString &serviceEntryPath,
const QList<QUrl> &urls,
KIO::ApplicationLauncherJob::RunFlags flags = {},
const QString &suggestedFileName = {},
const QByteArray &asn = {});
/**
* Run a shell command
* @param cmd must be a shell command. No need to append "&" to it.
* @param desktopName name of the desktop file, if known.
* @param execName the name of the executable, if known.
* @param asn Application startup notification id, if any (otherwise "").
* @param workingDirectory the working directory for the started process. The default
* (if passing an empty string) is the user's document path.
* This allows a command like "kwrite file.txt" to find file.txt from the right place.
*/
static KProcessRunner *fromCommand(const QString &cmd,
const QString &desktopName,
const QString &execName,
const QByteArray &asn,
const QString &workingDirectory,
const QProcessEnvironment &environment);
/**
* Run an executable with arguments (without invoking a shell, by starting a new process).
*
* @note: Starting from 5.92, if an actual executable named @p executable cannot be found
* in PATH, this will return a nullptr.
*
* @param executable the name of (or full path to) the executable, mandatory
* @param args the arguments to pass to the executable
* @param desktopName name of the desktop file, if known.
* @param asn Application startup notification id, if any (otherwise "").
* @param workingDirectory the working directory for the started process. The default
* (if passing an empty string) is the user's document path.
* This allows a command like "kwrite file.txt" to find file.txt from the right place.
*/
static KProcessRunner *fromExecutable(const QString &executable,
const QStringList &args,
const QString &desktopName,
const QByteArray &asn,
const QString &workingDirectory,
const QProcessEnvironment &environment);
/**
* Blocks until the process has started. Only exists for KRun via Command/ApplicationLauncherJob, will disappear in KF6.
*/
virtual bool waitForStarted(int timeout = 30000) = 0;
~KProcessRunner() override;
static int instanceCount(); // for the unittest
Q_SIGNALS:
/**
* @brief Emitted on error. In that case, finished() is not emitted.
* @param errorString the error message
*/
void error(const QString &errorString);
/**
* @brief emitted when the process was successfully started
* @param pid PID of the process that was started
*/
void processStarted(qint64 pid);
protected:
KProcessRunner();
virtual void startProcess() = 0;
void setPid(qint64 pid);
void terminateStartupNotification();
QString name() const;
QString resolveServiceAlias() const;
static QString escapeUnitName(const QString &input);
void emitDelayedError(const QString &errorMsg);
std::unique_ptr<KProcess> m_process;
QString m_executable; // can be a full path
QString m_desktopName;
QString m_desktopFilePath;
QString m_description;
QString m_cmd;
qint64 m_pid = 0;
KService::Ptr m_service;
QString m_serviceEntryPath;
bool m_waitingForXdgToken = false;
QList<QUrl> m_urls;
#if HAVE_X11
KStartupInfoId m_startupId;
#endif
private:
void initFromDesktopName(const QString &desktopName,
const QString &execName,
const QByteArray &asn,
const QString &workingDirectory,
const QProcessEnvironment &environment);
void init(const KService::Ptr &service, const QString &serviceEntryPath, const QString &userVisibleName, const QByteArray &asn);
Q_DISABLE_COPY(KProcessRunner)
};
class ForkingProcessRunner : public KProcessRunner
{
Q_OBJECT
public:
explicit ForkingProcessRunner();
void startProcess() override;
bool waitForStarted(int timeout) override;
protected Q_SLOTS:
void slotProcessExited(int, QProcess::ExitStatus);
void slotProcessError(QProcess::ProcessError error);
virtual void slotProcessStarted();
};
#endif
@@ -0,0 +1,173 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2021 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "kterminallauncherjob.h"
#include <KConfigGroup>
#include <KLocalizedString>
#include <KService>
#include <KSharedConfig>
#include <KShell>
#include <QProcessEnvironment>
class KTerminalLauncherJobPrivate
{
public:
QString m_workingDirectory;
QString m_command; // "ls"
QString m_fullCommand; // "xterm -e ls"
QString m_desktopName;
QByteArray m_startupId;
QProcessEnvironment m_environment{QProcessEnvironment::InheritFromParent};
};
KTerminalLauncherJob::KTerminalLauncherJob(const QString &command, QObject *parent)
: KJob(parent)
, d(new KTerminalLauncherJobPrivate)
{
d->m_command = command;
}
KTerminalLauncherJob::~KTerminalLauncherJob() = default;
void KTerminalLauncherJob::setWorkingDirectory(const QString &workingDirectory)
{
d->m_workingDirectory = workingDirectory;
}
void KTerminalLauncherJob::setStartupId(const QByteArray &startupId)
{
d->m_startupId = startupId;
}
void KTerminalLauncherJob::setProcessEnvironment(const QProcessEnvironment &environment)
{
d->m_environment = environment;
}
void KTerminalLauncherJob::start()
{
determineFullCommand();
if (error()) {
emitDelayedResult();
} else {
auto *subjob = new KIO::CommandLauncherJob(d->m_fullCommand, this);
subjob->setDesktopName(d->m_desktopName);
subjob->setWorkingDirectory(d->m_workingDirectory);
subjob->setStartupId(d->m_startupId);
subjob->setProcessEnvironment(d->m_environment);
connect(subjob, &KJob::result, this, [this, subjob] {
// NB: must go through emitResult otherwise we don't get correctly finished!
// TODO KF6: maybe change the base to KCompositeJob so we can get rid of this nonesense
if (subjob->error()) {
setError(subjob->error());
setErrorText(subjob->errorText());
}
emitResult();
});
subjob->start();
}
}
void KTerminalLauncherJob::emitDelayedResult()
{
// Use delayed invocation so the caller has time to connect to the signal
QMetaObject::invokeMethod(this, &KTerminalLauncherJob::emitResult, Qt::QueuedConnection);
}
#ifndef Q_OS_WIN
// helper function to help scope service so that not the entire determineFullCommand has access to it (it is not
// always not null!)
static KServicePtr serviceFromConfig(bool fallbackToKonsoleService)
{
const KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
const QString terminalExec = confGroup.readEntry("TerminalApplication");
const QString terminalService = confGroup.readEntry("TerminalService");
KServicePtr service;
if (!terminalService.isEmpty()) {
service = KService::serviceByStorageId(terminalService);
} else if (!terminalExec.isEmpty()) {
service = new KService(QStringLiteral("terminal"), terminalExec, QStringLiteral("utilities-terminal"));
}
if (!service && fallbackToKonsoleService) {
service = KService::serviceByStorageId(QStringLiteral("org.kde.konsole"));
}
return service;
}
#endif
// This sets m_fullCommand, but also (when possible) m_desktopName
void KTerminalLauncherJob::determineFullCommand(bool fallbackToKonsoleService /* allow synthesizing no konsole */)
{
const QString workingDir = d->m_workingDirectory;
#ifndef Q_OS_WIN
QString exec;
if (KServicePtr service = serviceFromConfig(fallbackToKonsoleService); service) {
d->m_desktopName = service->desktopEntryName();
exec = service->exec();
} else {
// konsole not found by desktop file, let's see what PATH has for us
auto useIfAvailable = [&exec](const QString &terminalApp) {
const bool found = !QStandardPaths::findExecutable(terminalApp).isEmpty();
if (found) {
exec = terminalApp;
}
return found;
};
if (!useIfAvailable(QStringLiteral("konsole")) && !useIfAvailable(QStringLiteral("xterm"))) {
setError(KJob::UserDefinedError);
setErrorText(i18n("No terminal emulator found"));
return;
}
}
if (!d->m_command.isEmpty()) {
if (exec == QLatin1String("konsole")) {
exec += QLatin1String(" --noclose");
} else if (exec == QLatin1String("xterm")) {
exec += QLatin1String(" -hold");
}
}
if (exec.startsWith(QLatin1String("konsole")) && !workingDir.isEmpty()) {
exec += QLatin1String(" --workdir %1").arg(KShell::quoteArg(workingDir));
}
if (!d->m_command.isEmpty()) {
exec += QLatin1String(" -e ") + d->m_command;
}
#else
const QString windowsTerminal = QStringLiteral("wt.exe");
const QString pwsh = QStringLiteral("pwsh.exe");
const QString powershell = QStringLiteral("powershell.exe"); // Powershell is used as fallback
const bool hasWindowsTerminal = !QStandardPaths::findExecutable(windowsTerminal).isEmpty();
const bool hasPwsh = !QStandardPaths::findExecutable(pwsh).isEmpty();
QString exec;
if (hasWindowsTerminal) {
exec = windowsTerminal;
if (!workingDir.isEmpty()) {
exec += QLatin1String(" --startingDirectory %1").arg(KShell::quoteArg(workingDir));
}
if (!d->m_command.isEmpty()) {
// Command and NoExit flag will be added later
exec += QLatin1Char(' ') + (hasPwsh ? pwsh : powershell);
}
} else {
exec = hasPwsh ? pwsh : powershell;
}
if (!d->m_command.isEmpty()) {
exec += QLatin1String(" -NoExit -Command ") + d->m_command;
}
#endif
d->m_fullCommand = exec;
}
QString KTerminalLauncherJob::fullCommand() const
{
return d->m_fullCommand;
}
#include "moc_kterminallauncherjob.cpp"
@@ -0,0 +1,90 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2021 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KTERMINALLAUNCHERJOB_H
#define KTERMINALLAUNCHERJOB_H
#include <KIO/CommandLauncherJob>
#include <memory>
class KTerminalLauncherJobPrivate;
/**
* @class KTerminalLauncherJob kterminallauncherjob.h <KTerminalLauncherJob>
*
* @brief KTerminalLauncherJob starts a terminal application,
* either for the user to use interactively, or to execute a command.
*
* It creates a startup notification and finishes it on success or on error (for the taskbar).
* It also emits an error message if necessary (e.g. "program not found").
*
* The job finishes when the application is successfully started.
* For error handling, either connect to the result() signal, or for a simple messagebox on error,
* you can do
* @code
* job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
* @endcode
*
* @since 5.83
*/
class KIOGUI_EXPORT KTerminalLauncherJob : public KJob
{
Q_OBJECT
public:
/**
* Creates a KTerminalLauncherJob.
* @param command the command to execute in a terminal, can be empty.
* @param parent the parent QObject
*/
explicit KTerminalLauncherJob(const QString &command, QObject *parent = nullptr);
/**
* Destructor
*
* Note that jobs auto-delete themselves after emitting result
*/
~KTerminalLauncherJob() override;
/**
* Sets the working directory from which to run the command.
* @param workingDirectory path of a local directory
*/
void setWorkingDirectory(const QString &workingDirectory);
/**
* Sets the platform-specific startup id of the command launch.
* @param startupId startup id, if any (otherwise "").
* For X11, this would be the id for the Startup Notification protocol.
* For Wayland, this would be the token for the XDG Activation protocol.
*/
void setStartupId(const QByteArray &startupId);
/**
* Can be used to pass environment variables to the child process.
* @param environment set of environment variables to pass to the child process
* @see QProcessEnvironment
*/
void setProcessEnvironment(const QProcessEnvironment &environment);
/**
* Starts the job.
* You must call this, after having called all the necessary setters.
*/
void start() override;
private:
friend class KTerminalLauncherJobTest;
void determineFullCommand(bool fallbackToKonsoleService = true); // for the unittest
QString fullCommand() const; // for the unittest
KIOGUI_NO_EXPORT void emitDelayedResult();
friend class KTerminalLauncherJobPrivate;
std::unique_ptr<KTerminalLauncherJobPrivate> d;
};
#endif // KTERMINALLAUNCHERJOB_H
@@ -0,0 +1,499 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2000 Yves Arrouye <yves@realnames.com>
SPDX-FileCopyrightText: 2000, 2010 Dawit Alemayehu <adawit at kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kurifilter.h"
#include "kurifilterdata_p.h"
#include <KService>
#include <kio/global.h>
#include <KPluginFactory>
#include <KPluginMetaData>
#include <QHashIterator>
#include <QHostAddress>
#include <QHostInfo>
#include <QIcon>
#include "kurifilterplugin_p.h"
QString KUriFilterDataPrivate::lookupIconNameFor(const QUrl &url, KUriFilterData::UriTypes type)
{
QString iconName;
switch (type) {
case KUriFilterData::NetProtocol:
iconName = KIO::iconNameForUrl(url);
break;
case KUriFilterData::Executable: {
QString exeName = url.path();
exeName.remove(0, exeName.lastIndexOf(QLatin1Char('/')) + 1); // strip path if given
KService::Ptr service = KService::serviceByDesktopName(exeName);
if (service && service->icon() != QLatin1String("unknown")) {
iconName = service->icon();
}
// Try to find an icon with the same name as the binary (useful for non-kde apps)
else if (!QIcon::fromTheme(exeName).isNull()) {
iconName = exeName;
} else {
// not found, use default
iconName = QStringLiteral("system-run");
}
break;
}
case KUriFilterData::Help: {
iconName = QStringLiteral("khelpcenter");
break;
}
case KUriFilterData::Shell: {
iconName = QStringLiteral("konsole");
break;
}
case KUriFilterData::Error:
case KUriFilterData::Blocked: {
iconName = QStringLiteral("error");
break;
}
default:
break;
}
return iconName;
}
class Q_DECL_HIDDEN KUriFilterSearchProvider::KUriFilterSearchProviderPrivate
{
public:
KUriFilterSearchProviderPrivate()
{
}
KUriFilterSearchProviderPrivate(const KUriFilterSearchProviderPrivate &other)
: desktopEntryName(other.desktopEntryName)
, iconName(other.iconName)
, name(other.name)
, keys(other.keys)
{
}
QString desktopEntryName;
QString iconName;
QString name;
QStringList keys;
};
KUriFilterSearchProvider::KUriFilterSearchProvider()
: d(new KUriFilterSearchProvider::KUriFilterSearchProviderPrivate)
{
}
KUriFilterSearchProvider::KUriFilterSearchProvider(const KUriFilterSearchProvider &other)
: d(new KUriFilterSearchProvider::KUriFilterSearchProviderPrivate(*(other.d)))
{
}
KUriFilterSearchProvider::~KUriFilterSearchProvider() = default;
QString KUriFilterSearchProvider::desktopEntryName() const
{
return d->desktopEntryName;
}
QString KUriFilterSearchProvider::iconName() const
{
return d->iconName;
}
QString KUriFilterSearchProvider::name() const
{
return d->name;
}
QStringList KUriFilterSearchProvider::keys() const
{
return d->keys;
}
QString KUriFilterSearchProvider::defaultKey() const
{
if (d->keys.isEmpty()) {
return QString();
}
return d->keys.first();
}
KUriFilterSearchProvider &KUriFilterSearchProvider::operator=(const KUriFilterSearchProvider &other)
{
d->desktopEntryName = other.d->desktopEntryName;
d->iconName = other.d->iconName;
d->keys = other.d->keys;
d->name = other.d->name;
return *this;
}
void KUriFilterSearchProvider::setDesktopEntryName(const QString &desktopEntryName)
{
d->desktopEntryName = desktopEntryName;
}
void KUriFilterSearchProvider::setIconName(const QString &iconName)
{
d->iconName = iconName;
}
void KUriFilterSearchProvider::setName(const QString &name)
{
d->name = name;
}
void KUriFilterSearchProvider::setKeys(const QStringList &keys)
{
d->keys = keys;
}
KUriFilterData::KUriFilterData()
: d(new KUriFilterDataPrivate(QUrl(), QString()))
{
}
KUriFilterData::KUriFilterData(const QUrl &url)
: d(new KUriFilterDataPrivate(url, url.toString()))
{
}
KUriFilterData::KUriFilterData(const QString &url)
: d(new KUriFilterDataPrivate(QUrl::fromUserInput(url), url))
{
}
KUriFilterData::KUriFilterData(const KUriFilterData &other)
: d(new KUriFilterDataPrivate(*other.d))
{
}
KUriFilterData::~KUriFilterData() = default;
QUrl KUriFilterData::uri() const
{
return d->url;
}
QString KUriFilterData::errorMsg() const
{
return d->errMsg;
}
KUriFilterData::UriTypes KUriFilterData::uriType() const
{
return d->uriType;
}
QString KUriFilterData::absolutePath() const
{
return d->absPath;
}
bool KUriFilterData::hasAbsolutePath() const
{
return !d->absPath.isEmpty();
}
QString KUriFilterData::argsAndOptions() const
{
return d->args;
}
bool KUriFilterData::hasArgsAndOptions() const
{
return !d->args.isEmpty();
}
bool KUriFilterData::checkForExecutables() const
{
return d->checkForExecs;
}
QString KUriFilterData::typedString() const
{
return d->typedString;
}
QString KUriFilterData::searchTerm() const
{
return d->searchTerm;
}
QChar KUriFilterData::searchTermSeparator() const
{
return d->searchTermSeparator;
}
QString KUriFilterData::searchProvider() const
{
return d->searchProvider;
}
QStringList KUriFilterData::preferredSearchProviders() const
{
return d->searchProviderList;
}
KUriFilterSearchProvider KUriFilterData::queryForSearchProvider(const QString &provider) const
{
const KUriFilterSearchProvider *searchProvider = d->searchProviderMap.value(provider);
if (searchProvider) {
return *(searchProvider);
}
return KUriFilterSearchProvider();
}
QString KUriFilterData::queryForPreferredSearchProvider(const QString &provider) const
{
const KUriFilterSearchProvider *searchProvider = d->searchProviderMap.value(provider);
if (searchProvider) {
return (searchProvider->defaultKey() % searchTermSeparator() % searchTerm());
}
return QString();
}
QStringList KUriFilterData::allQueriesForSearchProvider(const QString &provider) const
{
const KUriFilterSearchProvider *searchProvider = d->searchProviderMap.value(provider);
if (searchProvider) {
return searchProvider->keys();
}
return QStringList();
}
QString KUriFilterData::iconNameForPreferredSearchProvider(const QString &provider) const
{
const KUriFilterSearchProvider *searchProvider = d->searchProviderMap.value(provider);
if (searchProvider) {
return searchProvider->iconName();
}
return QString();
}
QStringList KUriFilterData::alternateSearchProviders() const
{
return d->alternateSearchProviders;
}
QString KUriFilterData::alternateDefaultSearchProvider() const
{
return d->alternateDefaultSearchProvider;
}
QString KUriFilterData::defaultUrlScheme() const
{
return d->defaultUrlScheme;
}
KUriFilterData::SearchFilterOptions KUriFilterData::searchFilteringOptions() const
{
return d->searchFilterOptions;
}
QString KUriFilterData::iconName()
{
auto foundProvider = d->searchProviderMap.constFind(searchProvider());
if (foundProvider != d->searchProviderMap.cend() && !foundProvider.value()->iconName().isEmpty()) {
return foundProvider.value()->iconName();
}
if (d->wasModified) {
d->iconName = KUriFilterDataPrivate::lookupIconNameFor(d->url, d->uriType);
d->wasModified = false;
}
return d->iconName;
}
void KUriFilterData::setData(const QUrl &url)
{
d->setData(url, url.toString());
}
void KUriFilterData::setData(const QString &url)
{
d->setData(QUrl(url), url);
}
bool KUriFilterData::setAbsolutePath(const QString &absPath)
{
// Since a malformed URL could possibly be a relative
// URL we tag it as a possible local resource...
if ((d->url.scheme().isEmpty() || d->url.isLocalFile())) {
d->absPath = absPath;
return true;
}
return false;
}
void KUriFilterData::setCheckForExecutables(bool check)
{
d->checkForExecs = check;
}
void KUriFilterData::setAlternateSearchProviders(const QStringList &providers)
{
d->alternateSearchProviders = providers;
}
void KUriFilterData::setAlternateDefaultSearchProvider(const QString &provider)
{
d->alternateDefaultSearchProvider = provider;
}
void KUriFilterData::setDefaultUrlScheme(const QString &scheme)
{
d->defaultUrlScheme = scheme;
}
void KUriFilterData::setSearchFilteringOptions(SearchFilterOptions options)
{
d->searchFilterOptions = options;
}
KUriFilterData &KUriFilterData::operator=(const QUrl &url)
{
d->setData(url, url.toString());
return *this;
}
KUriFilterData &KUriFilterData::operator=(const QString &url)
{
d->setData(QUrl(url), url);
return *this;
}
/******************************* KUriFilter ******************************/
class KUriFilterPrivate
{
public:
KUriFilterPrivate()
{
}
~KUriFilterPrivate()
{
qDeleteAll(pluginList);
pluginList.clear();
}
QList<KUriFilterPlugin *> pluginList;
};
class KUriFilterSingleton
{
public:
KUriFilter instance;
};
Q_GLOBAL_STATIC(KUriFilterSingleton, m_self)
KUriFilter *KUriFilter::self()
{
return &m_self()->instance;
}
KUriFilter::KUriFilter()
: d(new KUriFilterPrivate())
{
QList<KPluginMetaData> plugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/urifilters"));
const QString prefKey = QStringLiteral("X-KDE-InitialPreference");
// Sort the plugins by order of priority
std::sort(plugins.begin(), plugins.end(), [prefKey](const KPluginMetaData &a, const KPluginMetaData &b) {
return a.value(prefKey, 0) > b.value(prefKey, 0);
});
for (const KPluginMetaData &pluginMetaData : std::as_const(plugins)) {
if (auto plugin = KPluginFactory::instantiatePlugin<KUriFilterPlugin>(pluginMetaData).plugin) {
d->pluginList << plugin;
}
}
}
KUriFilter::~KUriFilter() = default;
bool KUriFilter::filterUri(KUriFilterData &data, const QStringList &filters)
{
bool filtered = false;
for (KUriFilterPlugin *plugin : std::as_const(d->pluginList)) {
// If no specific filters were requested, iterate through all the plugins.
// Otherwise, only use available filters.
if (filters.isEmpty() || filters.contains(plugin->objectName())) {
if (plugin->filterUri(data)) {
filtered = true;
}
}
}
return filtered;
}
bool KUriFilter::filterUri(QUrl &uri, const QStringList &filters)
{
KUriFilterData data(uri);
bool filtered = filterUri(data, filters);
if (filtered) {
uri = data.uri();
}
return filtered;
}
bool KUriFilter::filterUri(QString &uri, const QStringList &filters)
{
KUriFilterData data(uri);
bool filtered = filterUri(data, filters);
if (filtered) {
uri = data.uri().toString();
}
return filtered;
}
QUrl KUriFilter::filteredUri(const QUrl &uri, const QStringList &filters)
{
KUriFilterData data(uri);
filterUri(data, filters);
return data.uri();
}
QString KUriFilter::filteredUri(const QString &uri, const QStringList &filters)
{
KUriFilterData data(uri);
filterUri(data, filters);
return data.uri().toString();
}
bool KUriFilter::filterSearchUri(KUriFilterData &data, SearchFilterTypes types)
{
QStringList filters;
if (types & WebShortcutFilter) {
filters << QStringLiteral("kurisearchfilter");
}
if (types & NormalTextFilter) {
filters << QStringLiteral("kuriikwsfilter");
}
return filterUri(data, filters);
}
QStringList KUriFilter::pluginNames() const
{
QStringList res;
res.reserve(d->pluginList.size());
std::transform(d->pluginList.constBegin(), d->pluginList.constEnd(), std::back_inserter(res), [](const KUriFilterPlugin *plugin) {
return plugin->objectName();
});
return res;
}
@@ -0,0 +1,835 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2000-2001, 2003, 2010 Dawit Alemayehu <adawit at kde.org>
Original author
SPDX-FileCopyrightText: 2000 Yves Arrouye <yves@realnames.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KURIFILTER_H
#define KURIFILTER_H
#include "kiogui_export.h"
#include <QHash>
#include <QObject>
#include <QPair>
#include <QStringList>
#include <QUrl>
#include <memory>
#ifdef Q_OS_WIN
#undef ERROR
#endif
class KUriFilterPrivate;
class KUriFilterDataPrivate;
class QHostInfo;
/**
* @class KUriFilterSearchProvider kurifilter.h <KUriFilter>
*
* Class that holds information about a search provider.
*
*/
class KIOGUI_EXPORT KUriFilterSearchProvider
{
public:
/**
* Default constructor.
*/
KUriFilterSearchProvider();
/**
* Copy constructor.
*/
KUriFilterSearchProvider(const KUriFilterSearchProvider &);
/**
* Destructor.
*/
virtual ~KUriFilterSearchProvider();
/**
* Returns the desktop filename of the search provider without any extension.
*
* For example, if the desktop filename of the search provider was
* "foobar.desktop", this function will return "foobar".
*/
QString desktopEntryName() const;
/**
* Returns the descriptive name of the search provider, e.g.\ "Google News".
*
* This name comes from the "Name=" property entry in the desktop file that
* contains the search provider's information.
*/
QString name() const;
/**
* Returns the icon name associated with the search provider when available.
*/
virtual QString iconName() const;
/**
* Returns all the web shortcut keys associated with this search provider.
*
* @see defaultKey
*/
QStringList keys() const;
/**
* Returns the default web shortcut key for this search provider.
*
* Right now this is the same as doing keys().first(), it might however
* change based on what the backend plugins do.
*
* @see keys
*/
QString defaultKey() const;
/**
* Assignment operator.
*/
KUriFilterSearchProvider &operator=(const KUriFilterSearchProvider &);
protected:
void setDesktopEntryName(const QString &);
void setIconName(const QString &);
void setKeys(const QStringList &);
void setName(const QString &);
private:
friend class KUriFilterPlugin;
class KUriFilterSearchProviderPrivate;
std::unique_ptr<KUriFilterSearchProviderPrivate> const d;
};
/**
* @class KUriFilterData kurifilter.h <KUriFilter>
*
* This class is a basic messaging class used to exchange filtering information
* between the filter plugins and the application requesting the filtering
* service.
*
* Use this object if you require a more detailed information about the URI you
* want to filter. Any application can create an instance of this class and send
* it to KUriFilter to have the plugins fill out all possible information about
* the URI.
*
* On successful filtering you can use @ref uriType() to determine what type
* of resource the request was filtered into. See @ref KUriFilter::UriTypes for
* details. If an error is encountered, then @ref KUriFilter::Error is returned.
* You can use @ref errorMsg to obtain the error information.
*
* The functions in this class are not reentrant.
*
* \b Example
*
* Here is a basic example of how this class is used with @ref KUriFilter:
* \code
* KUriFilterData filterData (QLatin1String("kde.org"));
* bool filtered = KUriFilter::self()->filterUri(filterData);
* \endcode
*
* If you are only interested in getting the list of preferred search providers,
* then you can do the following:
*
* \code
* KUriFilterData data;
* data.setData("<text-to-search-for>");
* data.setSearchFilteringOption(KUriFilterData::RetrievePreferredSearchProvidersOnly);
* bool filtered = KUriFilter::self()->filterSearchUri(data, KUriFilter::NormalTextFilter);
* \endcode
*
* @short A class for exchanging filtering information.
* @author Dawit Alemayehu <adawit at kde.org>
*/
class KIOGUI_EXPORT KUriFilterData
{
public:
/**
* Describes the type of the URI that was filtered.
*/
enum UriTypes {
NetProtocol = 0, ///< Any network protocol: http, ftp, nttp, pop3, etc...
LocalFile, ///< A local file whose executable flag is not set
LocalDir, ///< A local directory
Executable, ///< A local file whose executable flag is set
Help, ///< A man or info page
Shell, ///< A shell executable (ex: echo "Test..." >> ~/testfile)
Blocked, ///< A URI that should be blocked/filtered (ex: ad filtering)
Error, ///< An incorrect URI (ex: "~johndoe" when user johndoe does not exist in that system)
Unknown, ///< A URI that is not identified. Default value when a KUriFilterData is first created.
};
/**
* This enum describes the search filtering options to be used.
*
* @li SearchFilterOptionNone
* No search filter options are set and normal filtering is performed
* on the input data.
* @li RetrieveSearchProvidersOnly
* If set, the list of all available search providers are returned without
* any input filtering. This flag only applies when used in conjunction
* with the @ref KUriFilter::NormalTextFilter flag.
* @li RetrievePreferredSearchProvidersOnly
* If set, the list of preferred search providers are returned without
* any input filtering. This flag only applies when used in conjunction
* with the @ref KUriFilter::NormalTextFilter flag.
* @li RetrieveAvailableSearchProvidersOnly
* Same as doing RetrievePreferredSearchProvidersOnly | RetrieveSearchProvidersOnly,
* where all available search providers are returned if no preferred ones
* ones are available. No input filtering will be performed.
*
* @see setSearchFilteringOptions
* @see KUriFilter::filterSearchUri
* @see SearchFilterOptions
*/
enum SearchFilterOption {
SearchFilterOptionNone = 0x0,
RetrieveSearchProvidersOnly = 0x01,
RetrievePreferredSearchProvidersOnly = 0x02,
RetrieveAvailableSearchProvidersOnly = (RetrievePreferredSearchProvidersOnly | RetrieveSearchProvidersOnly),
};
/**
* Stores a combination of #SearchFilterOption values.
*/
Q_DECLARE_FLAGS(SearchFilterOptions, SearchFilterOption)
/**
* Default constructor.
*
* Creates a UriFilterData object.
*/
KUriFilterData();
/**
* Creates a KUriFilterData object from the given URL.
*
* @param url is the URL to be filtered.
*/
explicit KUriFilterData(const QUrl &url);
/**
* Creates a KUriFilterData object from the given string.
*
* @param url is the string to be filtered.
*/
explicit KUriFilterData(const QString &url);
/**
* Copy constructor.
*
* Creates a KUriFilterData object from another KURIFilterData object.
*
* @param other the uri filter data to be copied.
*/
KUriFilterData(const KUriFilterData &other);
/**
* Destructor.
*/
~KUriFilterData();
/**
* Returns the filtered or the original URL.
*
* If one of the plugins successfully filtered the original input, this
* function returns it. Otherwise, it will return the input itself.
*
* @return the filtered or original url.
*/
QUrl uri() const;
/**
* Returns an error message.
*
* This functions returns the error message set by the plugin whenever the
* uri type is set to KUriFilterData::ERROR. Otherwise, it returns a nullptr
* string.
*
* @return the error message or a nullptr when there is none.
*/
QString errorMsg() const;
/**
* Returns the URI type.
*
* This method always returns KUriFilterData::UNKNOWN if the given URL was
* not filtered.
*
* @return the type of the URI
*/
UriTypes uriType() const;
/**
* Returns the absolute path if one has already been set.
*
* @return the absolute path, or QString()
*
* @see hasAbsolutePath()
*/
QString absolutePath() const;
/**
* Checks whether the supplied data had an absolute path.
*
* @return true if the supplied data has an absolute path
*
* @see absolutePath()
*/
bool hasAbsolutePath() const;
/**
* Returns the command line options and arguments for a local resource
* when present.
*
* @return options and arguments when present, otherwise QString()
*/
QString argsAndOptions() const;
/**
* Checks whether the current data is a local resource with command line
* options and arguments.
*
* @return true if the current data has command line options and arguments
*/
bool hasArgsAndOptions() const;
/**
* @return true if the filters should attempt to check whether the
* supplied uri is an executable. False otherwise.
*/
bool checkForExecutables() const;
/**
* The string as typed by the user, before any URL processing is done.
*/
QString typedString() const;
/**
* Returns the search term portion of the typed string.
*
* If the @ref typedString was not filtered by a search filter plugin, this
* function returns an empty string.
*
* @see typedString
*/
QString searchTerm() const;
/**
* Returns the character that is used to separate the search term from the
* keyword.
*
* If @ref typedString was not filtered by a search filter plugin, this
* function returns a null character.
*
* @see typedString
*/
QChar searchTermSeparator() const;
/**
* Returns the name of the search service provider, e.g.\ Google.
*
* If @ref typedString was not filtered by a search filter plugin, this
* function returns an empty string.
*
* @see typedString
*/
QString searchProvider() const;
/**
* Returns a list of the names of preferred or available search providers.
*
* This function returns the list of providers marked as preferred whenever
* the input data, i.e. @ref typedString, is successfully filtered.
*
* If no default search provider has been selected prior to a filter request,
* this function will return an empty list. To avoid this problem you must
* either set an alternate default search provider using @ref setAlternateDefaultSearchProvider
* or set one of the @ref SearchFilterOption flags if you are only interested
* in getting the list of providers and not filtering the input.
*
* Additionally, you can also provide alternate search providers in case
* there are no preferred ones already selected.
*
* You can use @ref queryForPreferredServiceProvider to obtain the query
* associated with the list of search providers returned by this function.
*
* @see setAlternateSearchProviders
* @see setAlternateDefaultSearchProvider
* @see setSearchFilteringOption
* @see queryForPreferredServiceProvider
*/
QStringList preferredSearchProviders() const;
/**
* Returns information about @p provider.
*
* You can use this function to obtain the more information about the search
* providers returned by @ref preferredSearchProviders.
*
* @see preferredSearchProviders
* @see KUriFilterSearchProvider
*/
KUriFilterSearchProvider queryForSearchProvider(const QString &provider) const;
/**
* Returns the web shortcut url for the given preferred search provider.
*
* You can use this function to obtain the query for the preferred search
* providers returned by @ref preferredSearchProviders.
*
* The query returned by this function is in web shortcut format, i.e.
* "gg:foo bar", and must be re-filtered through KUriFilter to obtain a
* valid url.
*
* @see preferredSearchProviders
*/
QString queryForPreferredSearchProvider(const QString &provider) const;
/**
* Returns all the query urls for the given search provider.
*
* Use this function to obtain all the different queries that can be used
* for the given provider. For example, if a search engine provider named
* "foobar" has web shortcuts named "foobar", "foo" and "bar", then this
* function, unlike @ref queryForPreferredSearchProvider, will return a
* a query for each and every web shortcut.
*
* @see queryForPreferredSearchProvider
*/
QStringList allQueriesForSearchProvider(const QString &provider) const;
/**
* Returns the icon associated with the given preferred search provider.
*
* You can use this function to obtain the icon names associated with the
* preferred search providers returned by @ref preferredSearchProviders.
*
* @see preferredSearchProviders
*/
QString iconNameForPreferredSearchProvider(const QString &provider) const;
/**
* Returns the list of alternate search providers.
*
* This function returns an empty list if @ref setAlternateSearchProviders
* was not called to set the alternate search providers to be when no
* preferred providers have been chosen by the user through the search
* configuration module.
*
* @see setAlternatteSearchProviders
* @see preferredSearchProviders
*/
QStringList alternateSearchProviders() const;
/**
* Returns the search provider to use when a default provider is not available.
*
* This function returns an empty string if @ref setAlternateDefaultSearchProvider
* was not called to set the default search provider to be used when none has been
* chosen by the user through the search configuration module.
*
* @see setAlternateDefaultSearchProvider
*/
QString alternateDefaultSearchProvider() const;
/**
* Returns the default protocol to use when filtering potentially valid url inputs.
*
* By default this function will return an empty string.
*
* @see setDefaultUrlScheme
*/
QString defaultUrlScheme() const;
/**
* Returns the specified search filter options.
*
* By default this function returns @ref SearchFilterOptionNone.
*
* @see setSearchFilteringOptions
*/
SearchFilterOptions searchFilteringOptions() const;
/**
* The name of the icon that matches the current filtered URL.
*
* This function returns a null string by default and when no icon is found
* for the filtered URL.
*/
QString iconName();
/**
* Check whether the provided uri is executable or not.
*
* Setting this to false ensures that typing the name of an executable does
* not start that application. This is useful in the location bar of a
* browser. The default value is true.
*/
void setCheckForExecutables(bool check);
/**
* Same as above except the argument is a URL.
*
* Use this function to set the string to be filtered when you construct an
* empty filter object.
*
* @param url the URL to be filtered.
*/
void setData(const QUrl &url);
/**
* Sets the URL to be filtered.
*
* Use this function to set the string to be
* filtered when you construct an empty filter
* object.
*
* @param url the string to be filtered.
*/
void setData(const QString &url);
/**
* Sets the absolute path to be used whenever the supplied data is a
* relative local URL.
*
* NOTE: This function should only be used for local resources, i.e. the
* "file:/" protocol. It is useful for specifying the absolute path in
* cases where the actual URL might be relative. If deriving the path from
* a QUrl, make sure you set the argument for this function to the result
* of calling path () instead of url ().
*
* @param abs_path the absolute path to the local resource.
*
* @return true if absolute path is successfully set. Otherwise, false.
*/
bool setAbsolutePath(const QString &abs_path);
/**
* Sets a list of search providers to use in case no preferred search
* providers are available.
*
* The list of preferred search providers set using this function will only
* be used if the default and favorite search providers have not yet been
* selected by the user. Otherwise, the providers specified through this
* function will be ignored.
*
* @see alternateSearchProviders
* @see preferredSearchProviders
*/
void setAlternateSearchProviders(const QStringList &providers);
/**
* Sets the search provider to use in case no default provider is available.
*
* The default search provider set using this function will only be used if
* the default and favorite search providers have not yet been selected by
* the user. Otherwise, the default provider specified by through function
* will be ignored.
*
* @see alternateDefaultSearchProvider
* @see preferredSearchProviders
*/
void setAlternateDefaultSearchProvider(const QString &provider);
/**
* Sets the default scheme used when filtering potentially valid url inputs.
*
* Use this function to change the default protocol used when filtering
* potentially valid url inputs. The default protocol is http.
*
* If the scheme is specified without a separator, then "://" will be used
* as the separator by default. For example, if the default url scheme was
* simply set to "ftp", then a potentially valid url input such as "kde.org"
* will be filtered to "ftp://kde.org".
*
* @see defaultUrlScheme
*/
void setDefaultUrlScheme(const QString &);
/**
* Sets the options used by search filter plugins to filter requests.
*
* The default search filter option is @ref SearchFilterOptionNone. See
* @ref SearchFilterOption for the description of the other flags.
*
* It is important to note that the options set through this function can
* prevent any filtering from being performed by search filter plugins.
* As such, @ref uriTypes can return KUriFilterData::Unknown and @ref uri
* can return an invalid url even though the filtering request returned
* a successful response.
*
* @see searchFilteringOptions
*/
void setSearchFilteringOptions(SearchFilterOptions options);
/**
* Overloaded assignment operator.
*
* This function allows you to easily assign a QUrl
* to a KUriFilterData object.
*
* @return an instance of a KUriFilterData object.
*/
KUriFilterData &operator=(const QUrl &url);
/**
* Overloaded assignment operator.
*
* This function allows you to easily assign a QString to a KUriFilterData
* object.
*
* @return an instance of a KUriFilterData object.
*/
KUriFilterData &operator=(const QString &url);
private:
friend class KUriFilterPlugin;
std::unique_ptr<KUriFilterDataPrivate> d;
};
/**
* @class KUriFilter kurifilter.h <KUriFilter>
*
* KUriFilter applies a number of filters to a URI and returns a filtered version if any
* filter matches.
* A simple example is "kde.org" to "http://www.kde.org", which is commonplace in web browsers.
*
* The filters are implemented as plugins in @ref KUriFilterPlugin subclasses.
*
* KUriFilter is a singleton object: obtain the instance by calling
* @p KUriFilter::self() and use the public member functions to
* perform the filtering.
*
* \b Example
*
* To simply filter a given string:
*
* \code
* QString url("kde.org");
* bool filtered = KUriFilter::self()->filteredUri( url );
* \endcode
*
* You can alternatively use a QUrl:
*
* \code
* QUrl url("kde.org");
* bool filtered = KUriFilter::self()->filterUri( url );
* \endcode
*
* If you have a constant string or a constant URL, simply invoke the
* corresponding function to obtain the filtered string or URL instead
* of a boolean flag:
*
* \code
* QString filteredText = KUriFilter::self()->filteredUri( "kde.org" );
* \endcode
*
* All of the above examples should result in "kde.org" being filtered into
* "http://kde.org".
*
* You can also restrict the filters to be used by supplying the name of the
* filters you want to use. By default all available filters are used.
*
* To use specific filters, add the names of the filters you want to use to a
* QStringList and invoke the appropriate filtering function.
*
* The examples below show the use of specific filters. KDE ships with the
* following filter plugins by default:
*
* kshorturifilter:
* This is used for filtering potentially valid url inputs such as "kde.org"
* Additionally it filters shell variables and shortcuts such as $HOME and
* ~ as well as man and info page shortcuts, # and ## respectively.
*
* kuriikwsfilter:
* This is used for filtering normal input text into a web search url using the
* configured fallback search engine selected by the user.
*
* kurisearchfilter:
* This is used for filtering KDE webshortcuts. For example "gg:KDE" will be
* converted to a url for searching the work "KDE" using the Google search
* engine.
*
* localdomainfilter:
* This is used for doing a DNS lookup to determine whether the input is a valid
* local address.
*
* fixuphosturifilter:
* This is used to append "www." to the host name of a pre filtered http url
* if the original url cannot be resolved.
*
* \code
* QString text ("kde.org");
* bool filtered = KUriFilter::self()->filterUri(text, QLatin1String("kshorturifilter"));
* \endcode
*
* The above code should result in "kde.org" being filtered into "http://kde.org".
*
* \code
* QStringList list;
* list << QLatin1String("kshorturifilter") << QLatin1String("localdomainfilter");
* bool filtered = KUriFilter::self()->filterUri( text, list );
* \endcode
*
* Additionally if you only want to do search related filtering, you can use the
* search specific function, @ref filterSearchUri, that is available in KDE
* 4.5 and higher. For example, to search for a given input on the web you
* can do the following:
*
* KUriFilterData filterData ("foo");
* bool filtered = KUriFilter::self()->filterSearchUri(filterData, KUriFilterData::NormalTextFilter);
*
* KUriFilter converts all filtering requests to use @ref KUriFilterData
* internally. The use of this bi-directional class allows you to send specific
* instructions to the filter plugins as well as receive detailed information
* about the filtered request from them. See the documentation of KUriFilterData
* class for more examples and details.
*
* All functions in this class are thread safe and reentrant.
*
* @short Filters the given input into a valid url whenever possible.
*/
class KIOGUI_EXPORT KUriFilter
{
public:
/**
* This enum describes the types of search plugin filters available.
*
* @li NormalTextFilter The plugin used to filter normal text, e.g. "some term to search".
* @li WebShortcutFilter The plugin used to filter web shortcuts, e.g. gg:KDE.
*
* @see SearchFilterTypes
*/
enum SearchFilterType {
NormalTextFilter = 0x01,
WebShortcutFilter = 0x02,
};
/**
* Stores a combination of #SearchFilterType values.
*/
Q_DECLARE_FLAGS(SearchFilterTypes, SearchFilterType)
/**
* Destructor
*/
~KUriFilter();
/**
* Returns an instance of KUriFilter.
*/
static KUriFilter *self();
/**
* Filters @p data using the specified @p filters.
*
* If no named filters are specified, the default, then all the
* URI filter plugins found will be used.
*
* @param data object that contains the URI to be filtered.
* @param filters specify the list of filters to be used.
*
* @return a boolean indicating whether the URI has been changed
*/
bool filterUri(KUriFilterData &data, const QStringList &filters = QStringList());
/**
* Filters the URI given by the URL.
*
* The given URL is filtered based on the specified list of filters.
* If the list is empty all available filters would be used.
*
* @param uri the URI to filter.
* @param filters specify the list of filters to be used.
*
* @return a boolean indicating whether the URI has been changed
*/
bool filterUri(QUrl &uri, const QStringList &filters = QStringList());
/**
* Filters a string representing a URI.
*
* The given URL is filtered based on the specified list of filters.
* If the list is empty all available filters would be used.
*
* @param uri The URI to filter.
* @param filters specify the list of filters to be used.
*
* @return a boolean indicating whether the URI has been changed
*/
bool filterUri(QString &uri, const QStringList &filters = QStringList());
/**
* Returns the filtered URI.
*
* The given URL is filtered based on the specified list of filters.
* If the list is empty all available filters would be used.
*
* @param uri The URI to filter.
* @param filters specify the list of filters to be used.
*
* @return the filtered URI or null if it cannot be filtered
*/
QUrl filteredUri(const QUrl &uri, const QStringList &filters = QStringList());
/**
* Return a filtered string representation of a URI.
*
* The given URL is filtered based on the specified list of filters.
* If the list is empty all available filters would be used.
*
* @param uri the URI to filter.
* @param filters specify the list of filters to be used.
*
* @return the filtered URI or null if it cannot be filtered
*/
QString filteredUri(const QString &uri, const QStringList &filters = QStringList());
/**
* Filter @p data using the criteria specified by @p types.
*
* The search filter type can be individual value of @ref SearchFilterTypes
* or a combination of those types using the bitwise OR operator.
*
* You can also use the flags from @ref KUriFilterData::SearchFilterOption
* to alter the filtering mechanisms of the search filter providers.
*
* @param data object that contains the URI to be filtered.
* @param types the search filters used to filter the request.
* @return true if the specified @p data was successfully filtered.
*
* @see KUriFilterData::setSearchFilteringOptions
*/
bool filterSearchUri(KUriFilterData &data, SearchFilterTypes types);
/**
* Return a list of the names of all loaded plugins.
*
* @return a QStringList of plugin names
*/
QStringList pluginNames() const;
protected:
/**
* Constructor.
*
* Creates a KUriFilter object and calls loads all available URI filter plugins.
*/
KUriFilter();
private:
std::unique_ptr<KUriFilterPrivate> const d;
friend class KUriFilterSingleton;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(KUriFilterData::SearchFilterOptions)
Q_DECLARE_OPERATORS_FOR_FLAGS(KUriFilter::SearchFilterTypes)
#endif
@@ -0,0 +1,94 @@
/*
SPDX-FileCopyrightText: 2000-2001, 2003, 2010 Dawit Alemayehu <adawit at kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kurifilter.h"
#include <QMap>
class KUriFilterDataPrivate
{
public:
explicit KUriFilterDataPrivate(const QUrl &u, const QString &typedUrl)
: checkForExecs(true)
, wasModified(true)
, uriType(KUriFilterData::Unknown)
, searchFilterOptions(KUriFilterData::SearchFilterOptionNone)
, url(u.adjusted(QUrl::NormalizePathSegments))
, typedString(typedUrl)
{
}
~KUriFilterDataPrivate()
{
}
static QString lookupIconNameFor(const QUrl &url, KUriFilterData::UriTypes type);
void setData(const QUrl &u, const QString &typedUrl)
{
checkForExecs = true;
wasModified = true;
uriType = KUriFilterData::Unknown;
searchFilterOptions = KUriFilterData::SearchFilterOptionNone;
url = u.adjusted(QUrl::NormalizePathSegments);
typedString = typedUrl;
errMsg.clear();
iconName.clear();
absPath.clear();
args.clear();
searchTerm.clear();
searchProvider.clear();
searchTermSeparator = QChar();
alternateDefaultSearchProvider.clear();
alternateSearchProviders.clear();
searchProviderMap.clear();
defaultUrlScheme.clear();
}
KUriFilterDataPrivate(KUriFilterDataPrivate *data)
{
wasModified = data->wasModified;
checkForExecs = data->checkForExecs;
uriType = data->uriType;
searchFilterOptions = data->searchFilterOptions;
url = data->url;
typedString = data->typedString;
errMsg = data->errMsg;
iconName = data->iconName;
absPath = data->absPath;
args = data->args;
searchTerm = data->searchTerm;
searchTermSeparator = data->searchTermSeparator;
searchProvider = data->searchProvider;
alternateDefaultSearchProvider = data->alternateDefaultSearchProvider;
alternateSearchProviders = data->alternateSearchProviders;
searchProviderMap = data->searchProviderMap;
defaultUrlScheme = data->defaultUrlScheme;
}
bool checkForExecs;
bool wasModified;
KUriFilterData::UriTypes uriType;
KUriFilterData::SearchFilterOptions searchFilterOptions;
QUrl url;
QString typedString;
QString errMsg;
QString iconName;
QString absPath;
QString args;
QString searchTerm;
QString searchProvider;
QString alternateDefaultSearchProvider;
QString defaultUrlScheme;
QChar searchTermSeparator;
QStringList alternateSearchProviders;
QStringList searchProviderList;
QMap<QString, KUriFilterSearchProvider *> searchProviderMap;
};
@@ -0,0 +1,75 @@
/*
SPDX-FileCopyrightText: 2000-2001, 2003, 2010 Dawit Alemayehu <adawit at kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kurifilterplugin_p.h"
#include "kurifilterdata_p.h"
#include <QHostInfo>
#include <hostinfo.h>
KUriFilterPlugin::KUriFilterPlugin(QObject *parent, const KPluginMetaData &data)
: QObject(parent)
, d(nullptr)
{
setObjectName(data.pluginId());
}
KUriFilterPlugin::~KUriFilterPlugin() = default;
void KUriFilterPlugin::setFilteredUri(KUriFilterData &data, const QUrl &uri) const
{
data.d->url = uri.adjusted(QUrl::NormalizePathSegments);
data.d->wasModified = true;
// qDebug() << "Got filtered to:" << uri;
}
void KUriFilterPlugin::setErrorMsg(KUriFilterData &data, const QString &errmsg) const
{
data.d->errMsg = errmsg;
}
void KUriFilterPlugin::setUriType(KUriFilterData &data, KUriFilterData::UriTypes type) const
{
data.d->uriType = type;
data.d->wasModified = true;
}
void KUriFilterPlugin::setArguments(KUriFilterData &data, const QString &args) const
{
data.d->args = args;
}
void KUriFilterPlugin::setSearchProvider(KUriFilterData &data, KUriFilterSearchProvider *provider, const QString &term, const QChar &separator) const
{
if (provider) {
data.d->searchProviderMap.insert(provider->name(), provider);
} else {
data.d->searchProviderMap.remove(data.d->searchProvider);
}
data.d->searchProvider = provider ? provider->name() : QString();
data.d->searchTerm = term;
data.d->searchTermSeparator = separator;
}
void KUriFilterPlugin::setSearchProviders(KUriFilterData &data, const QList<KUriFilterSearchProvider *> &providers) const
{
data.d->searchProviderList.reserve(data.d->searchProviderList.size() + providers.size());
for (KUriFilterSearchProvider *searchProvider : providers) {
data.d->searchProviderList << searchProvider->name();
data.d->searchProviderMap.insert(searchProvider->name(), searchProvider);
}
}
QString KUriFilterPlugin::iconNameFor(const QUrl &url, KUriFilterData::UriTypes type) const
{
return KUriFilterDataPrivate::lookupIconNameFor(url, type);
}
QHostInfo KUriFilterPlugin::resolveName(const QString &hostname, unsigned long timeout) const
{
return KIO::HostInfo::lookupHost(hostname, timeout);
}
#include "moc_kurifilterplugin_p.cpp"
@@ -0,0 +1,107 @@
/*
SPDX-FileCopyrightText: 2000-2001, 2003, 2010 Dawit Alemayehu <adawit at kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#include "kiogui_export.h"
#include "kurifilter.h"
#include <KPluginMetaData>
/**
* @class KUriFilterPlugin kurifilter.h <KUriFilter>
*
* Base class for URI filter plugins.
*
* This class applies a single filter to a URI. All plugins designed to provide
* URI filtering service should inherit from this abstract class and provide a
* concrete implementation.
*
* All inheriting classes need to implement the pure virtual function
* @ref filterUri.
*
* @short Abstract class for URI filter plugins.
*/
class KIOGUI_EXPORT KUriFilterPlugin : public QObject
{
Q_OBJECT
public:
/**
* Constructs a filter plugin with a given name
*
* @param parent the parent object, or @c nullptr for no parent
* @param name the name of the plugin, mandatory
*/
explicit KUriFilterPlugin(QObject *parent, const KPluginMetaData &data);
~KUriFilterPlugin() override;
public:
/**
* Filters a URI.
*
* @param data the URI data to be filtered.
* @return A boolean indicating whether the URI has been changed.
*/
virtual bool filterUri(KUriFilterData &data) const = 0;
protected:
/**
* Sets the URL in @p data to @p uri.
*/
void setFilteredUri(KUriFilterData &data, const QUrl &uri) const;
/**
* Sets the error message in @p data to @p errormsg.
*/
void setErrorMsg(KUriFilterData &data, const QString &errmsg) const;
/**
* Sets the URI type in @p data to @p type.
*/
void setUriType(KUriFilterData &data, KUriFilterData::UriTypes type) const;
/**
* Sets the arguments and options string in @p data to @p args if any were
* found during filtering.
*/
void setArguments(KUriFilterData &data, const QString &args) const;
/**
* Sets the name of the search provider, the search term and keyword/term
* separator in @p data.
*/
void setSearchProvider(KUriFilterData &data, KUriFilterSearchProvider *provider, const QString &term, const QChar &separator) const;
/**
* Sets the information about the search @p providers in @p data.
*/
void setSearchProviders(KUriFilterData &data, const QList<KUriFilterSearchProvider *> &providers) const;
/**
* Returns the icon name for the given @p url and URI @p type.
*/
QString iconNameFor(const QUrl &url, KUriFilterData::UriTypes type) const;
/**
* Performs a DNS lookup for @p hostname and returns the result.
*
* This function uses the KIO DNS cache to speed up the
* lookup. It also avoids doing a reverse lookup if the given
* host name is already an ip address.
*
* \note All uri filter plugins that need to perform a hostname
* lookup should use this function.
*
* @param hostname the hostname to lookup.
* @param timeout the amount of time in msecs to wait for the lookup.
* @return the result of the host name lookup.
*/
QHostInfo resolveName(const QString &hostname, unsigned long timeout) const;
private:
class KUriFilterPluginPrivate *const d;
};
@@ -0,0 +1,244 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-FileCopyrightText: 2023 g10 Code GmbH
SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "openfilemanagerwindowjob.h"
#include "openfilemanagerwindowjob_p.h"
#ifdef WITH_QTDBUS
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#endif
#if defined(Q_OS_WINDOWS)
#include <QDir>
#include <shlobj.h>
#include <vector>
#endif
#include <QGuiApplication>
#include <KWindowSystem>
#include "config-kiogui.h"
#if HAVE_WAYLAND
#include <KWaylandExtras>
#endif
#include <KIO/OpenUrlJob>
namespace KIO
{
class OpenFileManagerWindowJobPrivate
{
public:
OpenFileManagerWindowJobPrivate(OpenFileManagerWindowJob *qq)
: q(qq)
, strategy(nullptr)
{
}
~OpenFileManagerWindowJobPrivate() = default;
#ifdef WITH_QTDBUS
void createDBusStrategy()
{
strategy = std::make_unique<OpenFileManagerWindowDBusStrategy>();
}
#endif
#if defined(Q_OS_WINDOWS)
void createWindowsShellStrategy()
{
strategy = std::make_unique<OpenFileManagerWindowWindowsShellStrategy>();
}
#endif
void createKRunStrategy()
{
strategy = std::make_unique<OpenFileManagerWindowKRunStrategy>(q);
}
OpenFileManagerWindowJob *const q;
QList<QUrl> highlightUrls;
QByteArray startupId;
std::unique_ptr<AbstractOpenFileManagerWindowStrategy> strategy;
};
OpenFileManagerWindowJob::OpenFileManagerWindowJob(QObject *parent)
: KJob(parent)
, d(new OpenFileManagerWindowJobPrivate(this))
{
#ifdef WITH_QTDBUS
d->createDBusStrategy();
#elif defined(Q_OS_WINDOWS)
d->createWindowsShellStrategy();
#else
d->createKRunStrategy();
#endif
connect(d->strategy.get(), &AbstractOpenFileManagerWindowStrategy::finished, this, [this](int result) {
if (result == KJob::NoError) {
emitResult();
} else {
#ifdef WITH_QTDBUS
// DBus strategy failed, fall back to KRun strategy
d->strategy = std::make_unique<OpenFileManagerWindowKRunStrategy>(this);
d->strategy->start(d->highlightUrls, d->startupId);
connect(d->strategy.get(), &KIO::AbstractOpenFileManagerWindowStrategy::finished, this, [this](int result) {
setError(result);
emitResult();
});
#else
setError(result);
emitResult();
#endif
}
});
}
OpenFileManagerWindowJob::~OpenFileManagerWindowJob() = default;
QList<QUrl> OpenFileManagerWindowJob::highlightUrls() const
{
return d->highlightUrls;
}
void OpenFileManagerWindowJob::setHighlightUrls(const QList<QUrl> &highlightUrls)
{
d->highlightUrls = highlightUrls;
}
QByteArray OpenFileManagerWindowJob::startupId() const
{
return d->startupId;
}
void OpenFileManagerWindowJob::setStartupId(const QByteArray &startupId)
{
d->startupId = startupId;
}
void OpenFileManagerWindowJob::start()
{
if (d->highlightUrls.isEmpty()) {
setError(NoValidUrlsError);
emitResult();
return;
}
d->strategy->start(d->highlightUrls, d->startupId);
}
OpenFileManagerWindowJob *highlightInFileManager(const QList<QUrl> &urls, const QByteArray &asn)
{
auto *job = new OpenFileManagerWindowJob();
job->setHighlightUrls(urls);
job->setStartupId(asn);
job->start();
return job;
}
#ifdef WITH_QTDBUS
void OpenFileManagerWindowDBusStrategy::start(const QList<QUrl> &urls, const QByteArray &asn)
{
// see the spec at: https://www.freedesktop.org/wiki/Specifications/file-manager-interface/
auto runWithToken = [this, urls](const QByteArray &asn) {
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.FileManager1"),
QStringLiteral("/org/freedesktop/FileManager1"),
QStringLiteral("org.freedesktop.FileManager1"),
QStringLiteral("ShowItems"));
msg << QUrl::toStringList(urls) << QString::fromUtf8(asn);
QDBusPendingReply<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [urls, asn, this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<void> reply = *watcher;
watcher->deleteLater();
Q_EMIT finished(reply.isError() ? KJob::UserDefinedError : KJob::NoError);
});
};
if (asn.isEmpty()) {
#if HAVE_WAYLAND
if (KWindowSystem::isPlatformWayland()) {
auto window = qGuiApp->focusWindow();
if (!window && !qGuiApp->allWindows().isEmpty()) {
window = qGuiApp->allWindows().constFirst();
}
const int launchedSerial = KWaylandExtras::lastInputSerial(window);
QObject::connect(
KWaylandExtras::self(),
&KWaylandExtras::xdgActivationTokenArrived,
this,
[launchedSerial, runWithToken](int serial, const QString &token) {
if (serial == launchedSerial) {
runWithToken(token.toUtf8());
}
},
Qt::SingleShotConnection);
KWaylandExtras::requestXdgActivationToken(window, launchedSerial, {});
} else {
runWithToken({});
}
#else
runWithToken({});
#endif
} else {
runWithToken(asn);
}
}
#endif
void OpenFileManagerWindowKRunStrategy::start(const QList<QUrl> &urls, const QByteArray &asn)
{
KIO::OpenUrlJob *urlJob = new KIO::OpenUrlJob(urls.at(0).adjusted(QUrl::RemoveFilename), QStringLiteral("inode/directory"));
urlJob->setUiDelegate(m_job->uiDelegate());
urlJob->setStartupId(asn);
QObject::connect(urlJob, &KJob::result, this, [this](KJob *urlJob) {
if (urlJob->error()) {
Q_EMIT finished(OpenFileManagerWindowJob::LaunchFailedError);
} else {
Q_EMIT finished(KJob::NoError);
}
});
urlJob->start();
}
#if defined(Q_OS_WINDOWS)
void OpenFileManagerWindowWindowsShellStrategy::start(const QList<QUrl> &urls, const QByteArray &asn)
{
Q_UNUSED(asn);
LPITEMIDLIST dir = ILCreateFromPathW(QDir::toNativeSeparators(urls.at(0).adjusted(QUrl::RemoveFilename).toLocalFile()).toStdWString().data());
std::vector<LPCITEMIDLIST> items;
for (const auto &url : urls) {
LPITEMIDLIST item = ILCreateFromPathW(QDir::toNativeSeparators(url.toLocalFile()).toStdWString().data());
items.push_back(item);
}
auto result = SHOpenFolderAndSelectItems(dir, items.size(), items.data(), 0);
if (SUCCEEDED(result)) {
Q_EMIT finished(KJob::NoError);
} else {
Q_EMIT finished(OpenFileManagerWindowJob::LaunchFailedError);
}
ILFree(dir);
for (auto &item : items) {
ILFree(const_cast<LPITEMIDLIST>(item));
}
}
#endif
} // namespace KIO
#include "moc_openfilemanagerwindowjob.cpp"
@@ -0,0 +1,117 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef OPENFILEMANAGERWINDOWJOB_H
#define OPENFILEMANAGERWINDOWJOB_H
#include "kiogui_export.h"
#include <KJob>
#include <QList>
#include <QUrl>
#include <memory>
namespace KIO
{
class OpenFileManagerWindowJobPrivate;
/**
* @class KIO::OpenFileManagerWindowJob openfilemanagerwindowjob.h <KIO/OpenFileManagerWindowJob>
*
* @brief Open a File Manager Window
*
* Using this job you can open a file manager window and highlight specific
* files within a folder. This can be useful if you downloaded a file and want
* to present it to the user without the user having to manually search the
* file in its parent folder. This can also be used for a "Show in Parent Folder"
* functionality.
*
* On Linux, this job will use the org.freedesktop.FileManager1 interface to highlight
* the files and/or folders. If this fails, the parent directory of the first URL
* will be opened in the default file manager instead.
*
* Note that this job is really only about highlighting certain items
* which means if you, for example, pass it just a URL to a folder it will
* not open this particular folder but instead highlight it within its parent folder.
*
* If you just want to open a folder, use OpenUrlJob instead.
*
* @since 5.24
*/
class KIOGUI_EXPORT OpenFileManagerWindowJob : public KJob
{
Q_OBJECT
public:
/**
* Creates an OpenFileManagerWindowJob
*/
explicit OpenFileManagerWindowJob(QObject *parent = nullptr);
/**
* Destroys the OpenFileManagerWindowJob
*/
~OpenFileManagerWindowJob() override;
/**
* Errors the job may emit
*/
enum Errors {
NoValidUrlsError = KJob::UserDefinedError, ///< No valid URLs to highlight have been specified
LaunchFailedError, ///< Failed to launch the file manager
};
/**
* The files and/or folders to highlight
*/
QList<QUrl> highlightUrls() const;
/**
* Set the files and/or folders to highlight
*/
void setHighlightUrls(const QList<QUrl> &highlightUrls);
/**
* The Startup ID
*/
QByteArray startupId() const;
/**
* Sets the platform-specific startup id of the file manager launch.
* @param startupId startup id, if any (otherwise "").
* For X11, this would be the id for the Startup Notification protocol.
* For Wayland, this would be the token for the XDG Activation protocol.
*/
void setStartupId(const QByteArray &startupId);
/**
* Starts the job
*/
void start() override;
private:
friend class AbstractOpenFileManagerWindowStrategy;
friend class OpenFileManagerWindowDBusStrategy;
friend class OpenFileManagerWindowKRunStrategy;
std::unique_ptr<OpenFileManagerWindowJobPrivate> const d;
};
/**
* Convenience method for creating a job to highlight a certain file or folder.
*
* It will create a job for a given URL(s) and automatically start it.
*
* @since 5.24
*/
KIOGUI_EXPORT OpenFileManagerWindowJob *highlightInFileManager(const QList<QUrl> &urls, const QByteArray &asn = QByteArray());
} // namespace KIO
#endif // OPENFILEMANAGERWINDOWJOB_H
@@ -0,0 +1,74 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef OPENFILEMANAGERWINDOWJOB_P_H
#define OPENFILEMANAGERWINDOWJOB_P_H
#include <KJob>
#include "openfilemanagerwindowjob.h"
namespace KIO
{
class AbstractOpenFileManagerWindowStrategy : public QObject
{
Q_OBJECT
public:
explicit AbstractOpenFileManagerWindowStrategy()
: QObject()
{
}
virtual ~AbstractOpenFileManagerWindowStrategy()
{
}
virtual void start(const QList<QUrl> &urls, const QByteArray &asn) = 0;
Q_SIGNALS:
void finished(int error);
};
#ifdef WITH_QTDBUS
class OpenFileManagerWindowDBusStrategy : public AbstractOpenFileManagerWindowStrategy
{
public:
explicit OpenFileManagerWindowDBusStrategy()
: AbstractOpenFileManagerWindowStrategy()
{
}
void start(const QList<QUrl> &urls, const QByteArray &asn) override;
};
#endif
class OpenFileManagerWindowKRunStrategy : public AbstractOpenFileManagerWindowStrategy
{
public:
explicit OpenFileManagerWindowKRunStrategy(OpenFileManagerWindowJob *job)
: AbstractOpenFileManagerWindowStrategy()
, m_job(job)
{
}
void start(const QList<QUrl> &urls, const QByteArray &asn) override;
private:
OpenFileManagerWindowJob *m_job;
};
#if defined(Q_OS_WINDOWS)
class OpenFileManagerWindowWindowsShellStrategy : public AbstractOpenFileManagerWindowStrategy
{
public:
explicit OpenFileManagerWindowWindowsShellStrategy()
: AbstractOpenFileManagerWindowStrategy()
{
}
void start(const QList<QUrl> &urls, const QByteArray &asn) override;
};
#endif
}
#endif // OPENFILEMANAGERWINDOWJOB_P_H
@@ -0,0 +1,30 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 Ahmad Samir <a.samirh78@gmail.com>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "openorexecutefileinterface.h"
using namespace KIO;
class KIO::OpenOrExecuteFileInterfacePrivate
{
};
OpenOrExecuteFileInterface::OpenOrExecuteFileInterface(QObject *parent)
: QObject(parent)
{
}
OpenOrExecuteFileInterface::~OpenOrExecuteFileInterface() = default;
void OpenOrExecuteFileInterface::promptUserOpenOrExecute(KJob *job, const QString &mimetype)
{
Q_UNUSED(job)
Q_UNUSED(mimetype)
Q_EMIT canceled();
}
#include "moc_openorexecutefileinterface.cpp"
@@ -0,0 +1,85 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 Ahmad Samir <a.samirh78@gmail.com>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef OPENOREXECUTEFILEINTERFACE_H
#define OPENOREXECUTEFILEINTERFACE_H
#include <QObject>
#include <kiogui_export.h>
class KJob;
namespace KIO
{
class OpenOrExecuteFileInterfacePrivate;
/**
* @class OpenOrExecuteFileInterface openorexecutefileinterface.h <KIO/OpenOrExecuteFileInterface>
* @brief The OpenOrExecuteFileInterface class allows OpenUrlJob to ask
* the user about how to handle various types of executable files, basically
* whether to run/execute the file, or in the case of text-based ones (shell
* scripts and .desktop files) open them as text.
*
* This extension mechanism for jobs is similar to KIO::JobUiDelegateExtension,
* OpenWithHandlerInterface and UntrustedProgramHandlerInterface.
*
* @since 5.73
*/
class KIOGUI_EXPORT OpenOrExecuteFileInterface : public QObject
{
Q_OBJECT
protected:
/**
* Constructor
*/
explicit OpenOrExecuteFileInterface(QObject *parent = nullptr);
/**
* Destructor
*/
~OpenOrExecuteFileInterface() override;
public:
/**
* Show a dialog to ask the user how to handle various types of executable
* files, basically whether to run/execute the file, or in the case of text-based
* ones (shell scripts and .desktop files) open them as text.
*
* @param job the job calling this. This is useful if you need to
* get any of its properties
* @param mimetype the MIME type of the file being handled
*
* Implementations of this method must emit either executeFile or canceled.
*
* The default implementation in this base class simply emits canceled().
* Any application using KIO::JobUiDelegate (from KIOWidgets) will benefit
* from an automatically registered subclass which implements this method,
* which in turn uses ExecutableFileOpenDialog (from KIOWidgets).
*/
virtual void promptUserOpenOrExecute(KJob *job, const QString &mimetype);
Q_SIGNALS:
/**
* Emitted by promptUserOpenOrExecute() once the user chooses an action.
* @param enable \c true if the user selected to execute/run the file or
* \c false if the user selected to open the file as text (the latter is
* only valid for shell scripts and .desktop files)
*/
void executeFile(bool enable);
/**
* Emitted by promptUserOpenOrExecute() if user selects cancel.
*/
void canceled();
private:
QScopedPointer<OpenOrExecuteFileInterfacePrivate> d;
};
}
#endif // OPENOREXECUTEFILEINTERFACE_H
@@ -0,0 +1,721 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "openurljob.h"
#include "commandlauncherjob.h"
#include "desktopexecparser.h"
#include "global.h"
#include "job.h" // for buildErrorString
#include "jobuidelegatefactory.h"
#include "kiogui_debug.h"
#include "openorexecutefileinterface.h"
#include "openwithhandlerinterface.h"
#include "untrustedprogramhandlerinterface.h"
#include <KApplicationTrader>
#include <KAuthorized>
#include <KConfigGroup>
#include <KDesktopFile>
#include <KLocalizedString>
#include <KSandbox>
#include <KUrlAuthorized>
#include <QFileInfo>
#include <KProtocolManager>
#include <KSharedConfig>
#include <QDesktopServices>
#include <QHostInfo>
#include <QMimeDatabase>
#include <QOperatingSystemVersion>
#include <mimetypefinderjob.h>
// For unit test purposes, to test both code paths in externalBrowser()
KIOGUI_EXPORT bool openurljob_force_use_browserapp_kdeglobals = false;
class KIO::OpenUrlJobPrivate
{
public:
explicit OpenUrlJobPrivate(const QUrl &url, OpenUrlJob *qq)
: m_url(url)
, q(qq)
{
q->setCapabilities(KJob::Killable);
}
void emitAccessDenied();
void runUrlWithMimeType();
QString externalBrowser() const;
bool runExternalBrowser(const QString &exe);
void useSchemeHandler();
QUrl m_url;
KIO::OpenUrlJob *const q;
QString m_suggestedFileName;
QByteArray m_startupId;
QString m_mimeTypeName;
KService::Ptr m_preferredService;
bool m_deleteTemporaryFile = false;
bool m_runExecutables = false;
bool m_showOpenOrExecuteDialog = false;
bool m_externalBrowserEnabled = true;
bool m_followRedirections = true;
private:
void executeCommand();
void handleBinaries(const QMimeType &mimeType);
void handleBinariesHelper(const QString &localPath, bool isNativeBinary);
void handleDesktopFiles();
void handleScripts();
void openInPreferredApp();
void runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName);
void showOpenWithDialog();
void showOpenOrExecuteFileDialog(std::function<void(bool)> dialogFinished);
void showUntrustedProgramWarningDialog(const QString &filePath);
void startService(const KService::Ptr &service, const QList<QUrl> &urls);
void startService(const KService::Ptr &service)
{
startService(service, {m_url});
}
};
KIO::OpenUrlJob::OpenUrlJob(const QUrl &url, QObject *parent)
: KCompositeJob(parent)
, d(new OpenUrlJobPrivate(url, this))
{
}
KIO::OpenUrlJob::OpenUrlJob(const QUrl &url, const QString &mimeType, QObject *parent)
: KCompositeJob(parent)
, d(new OpenUrlJobPrivate(url, this))
{
d->m_mimeTypeName = mimeType;
}
KIO::OpenUrlJob::~OpenUrlJob()
{
}
void KIO::OpenUrlJob::setDeleteTemporaryFile(bool b)
{
d->m_deleteTemporaryFile = b;
}
void KIO::OpenUrlJob::setSuggestedFileName(const QString &suggestedFileName)
{
d->m_suggestedFileName = suggestedFileName;
}
void KIO::OpenUrlJob::setStartupId(const QByteArray &startupId)
{
d->m_startupId = startupId;
}
void KIO::OpenUrlJob::setRunExecutables(bool allow)
{
d->m_runExecutables = allow;
}
void KIO::OpenUrlJob::setShowOpenOrExecuteDialog(bool b)
{
d->m_showOpenOrExecuteDialog = b;
}
void KIO::OpenUrlJob::setEnableExternalBrowser(bool b)
{
d->m_externalBrowserEnabled = b;
}
void KIO::OpenUrlJob::setFollowRedirections(bool b)
{
d->m_followRedirections = b;
}
void KIO::OpenUrlJob::start()
{
if (!d->m_url.isValid() || d->m_url.scheme().isEmpty()) {
const QString error = !d->m_url.isValid() ? d->m_url.errorString() : d->m_url.toDisplayString();
setError(KIO::ERR_MALFORMED_URL);
setErrorText(i18n("Malformed URL\n%1", error));
emitResult();
return;
}
if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), d->m_url)) {
d->emitAccessDenied();
return;
}
auto qtOpenUrl = [this]() {
if (!QDesktopServices::openUrl(d->m_url)) {
// Is this an actual error, or USER_CANCELED?
setError(KJob::UserDefinedError);
setErrorText(i18n("Failed to open %1", d->m_url.toDisplayString()));
}
emitResult();
};
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
if (d->m_externalBrowserEnabled) {
// For Windows and MacOS, the mimetypes handling is different, so use QDesktopServices
qtOpenUrl();
return;
}
#endif
if (d->m_externalBrowserEnabled && KSandbox::isInside()) {
// Use the function from QDesktopServices as it handles portals correctly
// Note that it falls back to "normal way" if the portal service isn't running.
qtOpenUrl();
return;
}
// If we know the MIME type, proceed
if (!d->m_mimeTypeName.isEmpty()) {
d->runUrlWithMimeType();
return;
}
if (d->m_url.scheme().startsWith(QLatin1String("http"))) {
if (d->m_externalBrowserEnabled) {
const QString externalBrowser = d->externalBrowser();
if (!externalBrowser.isEmpty() && d->runExternalBrowser(externalBrowser)) {
return;
}
}
} else {
if (KIO::DesktopExecParser::hasSchemeHandler(d->m_url)) {
d->useSchemeHandler();
return;
}
}
auto *job = new KIO::MimeTypeFinderJob(d->m_url, this);
job->setFollowRedirections(d->m_followRedirections);
job->setSuggestedFileName(d->m_suggestedFileName);
connect(job, &KJob::result, this, [job, this]() {
const int errCode = job->error();
if (errCode) {
setError(errCode);
setErrorText(job->errorText());
emitResult();
} else {
d->m_suggestedFileName = job->suggestedFileName();
d->m_mimeTypeName = job->mimeType();
d->runUrlWithMimeType();
}
});
job->start();
}
bool KIO::OpenUrlJob::doKill()
{
return true;
}
QString KIO::OpenUrlJobPrivate::externalBrowser() const
{
if (!m_externalBrowserEnabled) {
return QString();
}
if (!openurljob_force_use_browserapp_kdeglobals) {
KService::Ptr externalBrowser = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/https"));
if (!externalBrowser) {
externalBrowser = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/http"));
}
if (externalBrowser) {
return externalBrowser->storageId();
}
}
const QString browserApp = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("General")).readEntry("BrowserApplication");
return browserApp;
}
bool KIO::OpenUrlJobPrivate::runExternalBrowser(const QString &exec)
{
if (exec.startsWith(QLatin1Char('!'))) {
// Literal command
const QString command = QStringView(exec).mid(1) + QLatin1String(" %u");
KService::Ptr service(new KService(QString(), command, QString()));
startService(service);
return true;
} else {
// Name of desktop file
KService::Ptr service = KService::serviceByStorageId(exec);
if (service) {
startService(service);
return true;
}
}
return false;
}
void KIO::OpenUrlJobPrivate::useSchemeHandler()
{
// look for an application associated with x-scheme-handler/<protocol>
const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + m_url.scheme());
if (service) {
startService(service);
return;
}
// fallback, look for associated helper protocol
Q_ASSERT(KProtocolInfo::isHelperProtocol(m_url.scheme()));
const auto exec = KProtocolInfo::exec(m_url.scheme());
if (exec.isEmpty()) {
// use default MIME type opener for file
m_mimeTypeName = KProtocolManager::defaultMimetype(m_url);
runUrlWithMimeType();
} else {
KService::Ptr servicePtr(new KService(QString(), exec, QString()));
startService(servicePtr);
}
}
void KIO::OpenUrlJobPrivate::startService(const KService::Ptr &service, const QList<QUrl> &urls)
{
KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service, q);
job->setUrls(urls);
job->setRunFlags(m_deleteTemporaryFile ? KIO::ApplicationLauncherJob::DeleteTemporaryFiles : KIO::ApplicationLauncherJob::RunFlags{});
job->setSuggestedFileName(m_suggestedFileName);
job->setStartupId(m_startupId);
q->addSubjob(job);
job->start();
}
void KIO::OpenUrlJobPrivate::runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName)
{
if (urlStr.isEmpty()) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("The desktop entry file\n%1\nis of type Link but has no URL=... entry.", filePath));
q->emitResult();
return;
}
m_url = QUrl::fromUserInput(urlStr);
m_mimeTypeName.clear();
// X-KDE-LastOpenedWith holds the service desktop entry name that
// should be preferred for opening this URL if possible.
// This is used by the Recent Documents menu for instance.
if (!optionalServiceName.isEmpty()) {
m_preferredService = KService::serviceByDesktopName(optionalServiceName);
}
// Restart from scratch with the target of the link
q->start();
}
void KIO::OpenUrlJobPrivate::emitAccessDenied()
{
q->setError(KIO::ERR_ACCESS_DENIED);
q->setErrorText(KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, m_url.toDisplayString()));
q->emitResult();
}
// was: KRun::isExecutable (minus application/x-desktop MIME type).
// Feel free to make public if needed.
static bool isBinary(const QMimeType &mimeType)
{
// - Binaries could be e.g.:
// - application/x-executable
// - application/x-sharedlib e.g. /usr/bin/ls, see
// https://gitlab.freedesktop.org/xdg/shared-mime-info/-/issues/11
//
// - MIME types that inherit application/x-executable _and_ text/plain are scripts, these are
// handled by handleScripts()
return (mimeType.inherits(QStringLiteral("application/x-executable")) || mimeType.inherits(QStringLiteral("application/x-ms-dos-executable")));
}
// Helper function that returns whether a file is a text-based script
// e.g. ".sh", ".csh", ".py", ".js"
static bool isTextScript(const QMimeType &mimeType)
{
return (mimeType.inherits(QStringLiteral("application/x-executable")) && mimeType.inherits(QStringLiteral("text/plain")));
}
// Helper function that returns whether a file has the execute bit set or not.
static bool hasExecuteBit(const QString &fileName)
{
return QFileInfo(fileName).isExecutable();
}
bool KIO::OpenUrlJob::isExecutableFile(const QUrl &url, const QString &mimetypeString)
{
if (!url.isLocalFile()) {
return false;
}
QMimeDatabase db;
QMimeType mimeType = db.mimeTypeForName(mimetypeString);
return (isBinary(mimeType) || isTextScript(mimeType)) && hasExecuteBit(url.toLocalFile());
}
// Handle native binaries (.e.g. /usr/bin/*); and .exe files
void KIO::OpenUrlJobPrivate::handleBinaries(const QMimeType &mimeType)
{
if (!KAuthorized::authorize(KAuthorized::SHELL_ACCESS)) {
emitAccessDenied();
return;
}
const bool isLocal = m_url.isLocalFile();
// Don't run remote executables
if (!isLocal) {
q->setError(KJob::UserDefinedError);
q->setErrorText(
i18n("The executable file \"%1\" is located on a remote filesystem. "
"For safety reasons it will not be started.",
m_url.toDisplayString()));
q->emitResult();
return;
}
const QString localPath = m_url.toLocalFile();
bool isNativeBinary = true;
#ifndef Q_OS_WIN
isNativeBinary = !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable"));
#endif
if (m_showOpenOrExecuteDialog) {
auto dialogFinished = [this, localPath, isNativeBinary](bool shouldExecute) {
// shouldExecute is always true if we get here, because for binaries the
// dialog only offers Execute/Cancel
Q_UNUSED(shouldExecute)
handleBinariesHelper(localPath, isNativeBinary);
};
// Ask the user for confirmation before executing this binary (for binaries
// the dialog will only show Execute/Cancel)
showOpenOrExecuteFileDialog(dialogFinished);
return;
}
handleBinariesHelper(localPath, isNativeBinary);
}
void KIO::OpenUrlJobPrivate::handleBinariesHelper(const QString &localPath, bool isNativeBinary)
{
if (!m_runExecutables) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("For security reasons, launching executables is not allowed in this context."));
q->emitResult();
return;
}
// For local .exe files, open in the default app (e.g. WINE)
if (!isNativeBinary) {
openInPreferredApp();
return;
}
// Native binaries
if (!hasExecuteBit(localPath)) {
// Show untrustedProgram dialog for local, native executables without the execute bit
showUntrustedProgramWarningDialog(localPath);
return;
}
// Local executable with execute bit, proceed
executeCommand();
}
// For local, native executables (i.e. not shell scripts) without execute bit,
// show a prompt asking the user if he wants to run the program.
void KIO::OpenUrlJobPrivate::showUntrustedProgramWarningDialog(const QString &filePath)
{
auto *untrustedProgramHandler = KIO::delegateExtension<KIO::UntrustedProgramHandlerInterface *>(q);
if (!untrustedProgramHandler) {
// No way to ask the user to make it executable
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("The program \"%1\" needs to have executable permission before it can be launched.", filePath));
q->emitResult();
return;
}
QObject::connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, q, [=, this](bool result) {
if (result) {
QString errorString;
if (untrustedProgramHandler->setExecuteBit(filePath, errorString)) {
executeCommand();
} else {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("Unable to make file \"%1\" executable.\n%2.", filePath, errorString));
q->emitResult();
}
} else {
q->setError(KIO::ERR_USER_CANCELED);
q->emitResult();
}
});
untrustedProgramHandler->showUntrustedProgramWarning(q, m_url.fileName());
}
void KIO::OpenUrlJobPrivate::executeCommand()
{
// Execute the URL as a command. This is how we start scripts and executables
KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(m_url.toLocalFile(), QStringList());
job->setStartupId(m_startupId);
job->setWorkingDirectory(m_url.adjusted(QUrl::RemoveFilename).toLocalFile());
q->addSubjob(job);
job->start();
// TODO implement deleting the file if tempFile==true
// CommandLauncherJob doesn't support that, unlike ApplicationLauncherJob
// We'd have to do it in KProcessRunner.
}
void KIO::OpenUrlJobPrivate::runUrlWithMimeType()
{
// Tell the app, in case it wants us to stop here
Q_EMIT q->mimeTypeFound(m_mimeTypeName);
if (q->error() == KJob::KilledJobError) {
q->emitResult();
return;
}
// Support for preferred service setting, see setPreferredService
if (m_preferredService && m_preferredService->hasMimeType(m_mimeTypeName)) {
startService(m_preferredService);
return;
}
// Scripts and executables
QMimeDatabase db;
const QMimeType mimeType = db.mimeTypeForName(m_mimeTypeName);
// .desktop files
if (mimeType.inherits(QStringLiteral("application/x-desktop"))) {
handleDesktopFiles();
return;
}
// Scripts (e.g. .sh, .csh, .py, .js)
if (isTextScript(mimeType)) {
handleScripts();
return;
}
// Binaries (e.g. /usr/bin/{konsole,ls}) and .exe files
if (isBinary(mimeType)) {
handleBinaries(mimeType);
return;
}
// General case: look up associated application
openInPreferredApp();
}
void KIO::OpenUrlJobPrivate::handleDesktopFiles()
{
// Open remote .desktop files in the default (text editor) app
if (!m_url.isLocalFile()) {
openInPreferredApp();
return;
}
if (m_url.fileName() == QLatin1String(".directory") || m_mimeTypeName == QLatin1String("application/x-theme")) {
// We cannot execute these files, open in the default app
m_mimeTypeName = QStringLiteral("text/plain");
openInPreferredApp();
return;
}
const QString filePath = m_url.toLocalFile();
KDesktopFile cfg(filePath);
KConfigGroup cfgGroup = cfg.desktopGroup();
if (!cfgGroup.hasKey("Type")) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("The desktop entry file %1 has no Type=... entry.", filePath));
q->emitResult();
openInPreferredApp();
return;
}
if (cfg.hasLinkType()) {
runLink(filePath, cfg.readUrl(), cfg.desktopGroup().readEntry("X-KDE-LastOpenedWith"));
return;
}
if ((cfg.hasApplicationType() || cfg.readType() == QLatin1String("Service"))) { // kio_settings lets users run Type=Service desktop files
KService::Ptr service(new KService(filePath));
if (!service->exec().isEmpty()) {
if (m_showOpenOrExecuteDialog) { // Show the openOrExecute dialog
auto dialogFinished = [this, filePath, service](bool shouldExecute) {
if (shouldExecute) { // Run the file
startService(service, {});
return;
}
// The user selected "open"
openInPreferredApp();
};
showOpenOrExecuteFileDialog(dialogFinished);
return;
}
if (m_runExecutables) {
startService(service, {});
return;
}
} // exec is not empty
} // type Application or Service
// Fallback to opening in the default app
openInPreferredApp();
}
void KIO::OpenUrlJobPrivate::handleScripts()
{
// Executable scripts of any type can run arbitrary shell commands
if (!KAuthorized::authorize(KAuthorized::SHELL_ACCESS)) {
emitAccessDenied();
return;
}
const bool isLocal = m_url.isLocalFile();
const QString localPath = m_url.toLocalFile();
if (!isLocal || !hasExecuteBit(localPath)) {
// Open remote scripts or ones without the execute bit, with the default application
openInPreferredApp();
return;
}
if (m_showOpenOrExecuteDialog) {
auto dialogFinished = [this](bool shouldExecute) {
if (shouldExecute) {
executeCommand();
} else {
openInPreferredApp();
}
};
showOpenOrExecuteFileDialog(dialogFinished);
return;
}
if (m_runExecutables) { // Local executable script, proceed
executeCommand();
} else { // Open in the default (text editor) app
openInPreferredApp();
}
}
void KIO::OpenUrlJobPrivate::openInPreferredApp()
{
KService::Ptr service = KApplicationTrader::preferredService(m_mimeTypeName);
if (service) {
// If file mimetype is set to xdg-open or kde-open, the file will be opened in endless loop
// In these cases, showOpenWithDialog instead
const QStringList disallowedWrappers = {QStringLiteral("xdg-open"), QStringLiteral("kde-open")};
if (disallowedWrappers.contains(service.data()->exec())) {
showOpenWithDialog();
return;
}
startService(service);
} else {
// Avoid directly opening partial downloads and incomplete files
// This is done here in the off chance the user actually has a default handler for it
if (m_mimeTypeName == QLatin1String("application/x-partial-download")) {
q->setError(KJob::UserDefinedError);
q->setErrorText(
i18n("This file is incomplete and should not be opened.\n"
"Check your open applications and the notification area for any pending tasks or downloads."));
q->emitResult();
return;
}
showOpenWithDialog();
}
}
void KIO::OpenUrlJobPrivate::showOpenWithDialog()
{
if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("You are not authorized to select an application to open this file."));
q->emitResult();
return;
}
auto *openWithHandler = KIO::delegateExtension<KIO::OpenWithHandlerInterface *>(q);
if (!openWithHandler || QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows) {
// As KDE on windows doesn't know about the windows default applications, offers will be empty in nearly all cases.
// So we use QDesktopServices::openUrl to let windows decide how to open the file.
// It's also our fallback if there's no handler to show an open-with dialog.
if (!QDesktopServices::openUrl(m_url)) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("Failed to open the file."));
}
q->emitResult();
return;
}
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
q->setError(KIO::ERR_USER_CANCELED);
q->emitResult();
});
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
startService(service);
});
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
q->emitResult();
});
openWithHandler->promptUserForApplication(q, {m_url}, m_mimeTypeName);
}
void KIO::OpenUrlJobPrivate::showOpenOrExecuteFileDialog(std::function<void(bool)> dialogFinished)
{
QMimeDatabase db;
QMimeType mimeType = db.mimeTypeForName(m_mimeTypeName);
auto *openOrExecuteFileHandler = KIO::delegateExtension<KIO::OpenOrExecuteFileInterface *>(q);
if (!openOrExecuteFileHandler) {
// No way to ask the user whether to execute or open
if (isTextScript(mimeType) || mimeType.inherits(QStringLiteral("application/x-desktop"))) { // Open text-based ones in the default app
openInPreferredApp();
} else {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("The program \"%1\" could not be launched.", m_url.toDisplayString(QUrl::PreferLocalFile)));
q->emitResult();
}
return;
}
QObject::connect(openOrExecuteFileHandler, &KIO::OpenOrExecuteFileInterface::canceled, q, [this]() {
q->setError(KIO::ERR_USER_CANCELED);
q->emitResult();
});
QObject::connect(openOrExecuteFileHandler, &KIO::OpenOrExecuteFileInterface::executeFile, q, [this, dialogFinished](bool shouldExecute) {
m_runExecutables = shouldExecute;
dialogFinished(shouldExecute);
});
openOrExecuteFileHandler->promptUserOpenOrExecute(q, m_mimeTypeName);
}
void KIO::OpenUrlJob::slotResult(KJob *job)
{
// This is only used for the final application/launcher job, so we're done when it's done
const int errCode = job->error();
if (errCode) {
setError(errCode);
// We're a KJob, not a KIO::Job, so build the error string here
setErrorText(KIO::buildErrorString(errCode, job->errorText()));
}
emitResult();
}
#include "moc_openurljob.cpp"
@@ -0,0 +1,177 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KIO_OPENURLJOB_H
#define KIO_OPENURLJOB_H
#include "applicationlauncherjob.h"
#include "kiogui_export.h"
#include <KCompositeJob>
#include <QScopedPointer>
class QUrl;
namespace KIO
{
class OpenUrlJobPrivate;
/**
* @class OpenUrlJob openurljob.h <KIO/OpenUrlJob>
*
* @brief OpenUrlJob finds out the right way to "open" a URL.
* This includes finding out its MIME type, and then the associated application,
* or running desktop files, executables, etc.
* It also honours the "use this webbrowser for all http(s) URLs" setting.
*
* For the "Open With" dialog functionality to work, make sure to set
* KIO::JobUiDelegate as the delegate for this job (in widgets applications).
* @code
* // Since 5.98 use:
* job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window));
* // For older releases use:
* job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window));
* @endcode
*
* @since 5.71
*/
class KIOGUI_EXPORT OpenUrlJob : public KCompositeJob
{
Q_OBJECT
public:
/**
* @brief Creates an OpenUrlJob in order to open a URL.
* @param url the URL of the file/directory to open
*/
explicit OpenUrlJob(const QUrl &url, QObject *parent = nullptr);
/**
* @brief Creates an OpenUrlJob for the case where the MIME type is already known.
* @param url the URL of the file/directory to open
* @param mimeType the type of file/directory. See QMimeType.
*/
explicit OpenUrlJob(const QUrl &url, const QString &mimeType, QObject *parent = nullptr);
/**
* Destructor
*
* Note that by default jobs auto-delete themselves after emitting result.
*/
~OpenUrlJob() override;
/**
* Specifies that the URL passed to the application will be deleted when it exits (if the URL is a local file)
*/
void setDeleteTemporaryFile(bool b);
/**
* Sets the file name to use in the case of downloading the file to a tempfile,
* in order to give it to a non-URL-aware application.
* Some apps rely on the extension to determine the MIME type of the file.
* Usually the file name comes from the URL, but in the case of the
* HTTP Content-Disposition header, we need to override the file name.
* @param suggestedFileName the file name
*/
void setSuggestedFileName(const QString &suggestedFileName);
/**
* Sets the platform-specific startup id of the application launch.
* @param startupId startup id, if any (otherwise "").
* For X11, this would be the id for the Startup Notification protocol.
* For Wayland, this would be the token for the XDG Activation protocol.
*/
void setStartupId(const QByteArray &startupId);
/**
* Set this to true if this class should allow the user to run executables.
* Unlike KF5's KRun, this setting is OFF by default here for security reasons.
* File managers can enable this, but e.g. web browsers, mail clients etc. shouldn't.
*/
void setRunExecutables(bool allow);
/**
* Set this to @c true if this class should show a dialog to ask the user about how
* to handle various types of executable files; note that executing/running remote
* files is disallowed as that is not secure (in the case of remote shell scripts
* and .desktop files, they are always opened as text in the default application):
* - For native binaries: whether to execute or cancel
* - For .exe files: whether to execute or cancel, ("execute" on Linux in this
* context means running the file with the default application (e.g. WINE))
* - For executable shell scripts: whether to execute the file or open it as
* text in the default application; note that if the file doesn't have the
* execute bit, it'll always be opened as text
* - For .desktop files: whether to run the file or open it as text in the default
* application; note that if the .desktop file is located in a non-standard
* location (on Linux standard locations are /usr/share/applications or
* ~/.local/share/applications) and does not have the execute bit, another dialog
* (see UntrustedProgramHandlerInterface) will be launched to ask the user whether
* they trust running the application (the one the .desktop file launches based on
* the Exec line)
*
* Note that the dialog, ExecutableFileOpenDialog (from KIOWidgets), provides an option
* to remember the last value used and not ask again, if that is set, then the dialog will
* not be shown.
*
* When set to @c true this will take precedence over setRunExecutables (the latter can be
* used to allow running executables without first asking the user for confirmation).
*
* @since 5.73
*/
void setShowOpenOrExecuteDialog(bool b);
/**
* Sets whether the external webbrowser setting should be honoured.
* This is enabled by default.
* This should only be disabled in webbrowser applications.
* @param b whether to let the external browser handle the URL or not
*/
void setEnableExternalBrowser(bool b);
/**
* Sets whether the job should follow URL redirections.
* This is enabled by default.
* @param b whether to follow redirections or not.
*/
void setFollowRedirections(bool b);
/**
* Starts the job.
* You must call this, after having called all the needed setters.
* This is a GUI job, never use exec(), it would block user interaction.
*/
void start() override;
/**
* Returns whether the @p url of @p mimetype is executable.
* To be executable the file must pass the following rules:
* -# Must reside on the local filesystem.
* -# Must be marked as executable for the user by the filesystem.
* -# The MIME type must inherit application/x-executable, application/x-executable-script
*/
static bool isExecutableFile(const QUrl &url, const QString &mimetypeName);
Q_SIGNALS:
/**
* Emitted when the MIME type is determined.
* This can be used for special cases like webbrowsers
* who want to embed the URL in some cases, rather than starting a different
* application. In that case they can kill the job.
*/
void mimeTypeFound(const QString &mimeType);
protected:
bool doKill() override;
private:
void slotResult(KJob *job) override;
friend class OpenUrlJobPrivate;
QScopedPointer<OpenUrlJobPrivate> d;
};
} // namespace KIO
#endif // OPENURLJOB_H
@@ -0,0 +1,35 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "openwithhandlerinterface.h"
#include "kiocoredebug.h"
#include <QFile>
#include <QSaveFile>
using namespace KIO;
class KIO::OpenWithHandlerInterfacePrivate
{
};
OpenWithHandlerInterface::OpenWithHandlerInterface(QObject *parent)
: QObject(parent)
{
}
OpenWithHandlerInterface::~OpenWithHandlerInterface() = default;
void OpenWithHandlerInterface::promptUserForApplication(KJob *job, const QList<QUrl> &urls, const QString &mimeType)
{
Q_UNUSED(job)
Q_UNUSED(urls)
Q_UNUSED(mimeType)
Q_EMIT canceled();
}
#include "moc_openwithhandlerinterface.cpp"
@@ -0,0 +1,86 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef OPENWITHHANDLERINTERFACE_H
#define OPENWITHHANDLERINTERFACE_H
#include <KService>
#include <QObject>
#include <kiogui_export.h>
class QString;
class KJob;
namespace KIO
{
class OpenWithHandlerInterfacePrivate;
/**
* @class OpenWithHandlerInterface openwithhandlerinterface.h <KIO/OpenWithHandlerInterface>
* @brief The OpenWithHandlerInterface class allows OpenUrlJob to
* prompt the user about which application to use to open URLs that do not
* have an associated application (via the "Open With" dialog).
*
* This extension mechanism for jobs is similar to KIO::JobUiDelegateExtension
* and UntrustedProgramHandlerInterface.
*
* @since 5.71
*/
class KIOGUI_EXPORT OpenWithHandlerInterface : public QObject
{
Q_OBJECT
protected:
/**
* Constructor
*/
explicit OpenWithHandlerInterface(QObject *parent = nullptr);
/**
* Destructor
*/
~OpenWithHandlerInterface() override;
public:
/**
* Show the "Open With" dialog.
* @param job the job calling this. Useful to get all its properties
* @param urls the URLs to open
* @param mimeType the MIME type of the URLs, if known. Can be empty otherwise.
*
* Implementations of this method must emit either serviceSelected or canceled.
*
* The default implementation in this base class simply emits canceled().
* Any application using KIO::JobUiDelegate (from KIOWidgets) will benefit from an
* automatically registered subclass which implements this method using KOpenWithDialog.
*/
virtual void promptUserForApplication(KJob *job, const QList<QUrl> &urls, const QString &mimeType);
Q_SIGNALS:
/**
* Emitted by promptUserForApplication() once the user chooses an application.
* @param service the application chosen by the user
*/
void serviceSelected(const KService::Ptr &service);
/**
* Emitted by promptUserForApplication() if the user canceled the application selection dialog.
*/
void canceled();
/**
* Emitted by promptUserForApplication() if it fully handled it including launching the app.
* This is a special case for the native Windows open-with dialog.
*/
void handled();
private:
QScopedPointer<OpenWithHandlerInterfacePrivate> d;
};
}
#endif // OPENWITHHANDLERINTERFACE_H
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,215 @@
// -*- c++ -*-
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
SPDX-FileCopyrightText: 2001 Malte Starostik <malte.starostik@t-online.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KIO_PREVIEWJOB_H
#define KIO_PREVIEWJOB_H
#include "kiogui_export.h"
#include <kfileitem.h>
#include <kio/job.h>
class QPixmap;
class KPluginMetaData;
namespace KIO
{
class PreviewJobPrivate;
/*!
* @class KIO::PreviewJob previewjob.h <KIO/PreviewJob>
*
* This class catches a preview (thumbnail) for files.
* @short KIO Job to get a thumbnail picture
*/
class KIOGUI_EXPORT PreviewJob : public KIO::Job
{
Q_OBJECT
public:
/**
* Specifies the type of scaling that is applied to the generated preview.
* For HiDPI, pixel density scaling, @see setDevicePixelRatio
*
*/
enum ScaleType {
/**
* The original size of the preview will be returned. Most previews
* will return a size of 256 x 256 pixels.
*/
Unscaled,
/**
* The preview will be scaled to the size specified when constructing
* the PreviewJob. The aspect ratio will be kept.
*/
Scaled,
/**
* The preview will be scaled to the size specified when constructing
* the PreviewJob. The result will be cached for later use. Per default
* ScaledAndCached is set.
*/
ScaledAndCached,
};
/**
* @param items List of files to create previews for.
* @param size Desired size of the preview.
* @param enabledPlugins If non-zero it defines the list of plugins that
* are considered for generating the preview. If
* enabledPlugins is zero the plugins specified in the
* KConfigGroup "PreviewSettings" are used.
*/
PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins = nullptr);
~PreviewJob() override;
/**
* Sets the scale type for the generated preview. Per default
* PreviewJob::ScaledAndCached is set.
* @see PreviewJob::ScaleType
*/
void setScaleType(ScaleType type);
/**
* @return The scale type for the generated preview.
* @see PreviewJob::ScaleType
*/
ScaleType scaleType() const;
/**
* Removes an item from preview processing. Use this if you passed
* an item to filePreview and want to delete it now.
*
* @param url the url of the item that should be removed from the preview queue
*/
void removeItem(const QUrl &url);
/**
* If @p ignoreSize is true, then the preview is always
* generated regardless of the settings
**/
void setIgnoreMaximumSize(bool ignoreSize = true);
/**
* Sets the sequence index given to the thumb creators.
* Use the sequence index, it is possible to create alternative
* icons for the same item. For example it may allow iterating through
* the items of a directory, or the frames of a video.
*
**/
void setSequenceIndex(int index);
/**
* Returns the currently set sequence index
*
**/
int sequenceIndex() const;
/**
* Returns the index at which the thumbs of a ThumbSequenceCreator start
* wrapping around ("looping"). Fractional values may be returned if the
* ThumbSequenceCreator supports sub-integer precision, but frontends
* supporting only integer sequence indices may choose to round it down.
*
* @see ThumbSequenceCreator::sequenceIndexWraparoundPoint()
* @since 5.80
*/
float sequenceIndexWraparoundPoint() const;
/**
* Determines whether the ThumbCreator in use is a ThumbSequenceCreator.
*
* @since 5.80
*/
bool handlesSequences() const;
/**
* Request preview to use the device pixel ratio @p dpr.
* The returned thumbnail may not respect the device pixel ratio requested.
* Use QPixmap::devicePixelRatio to check, or paint as necessary.
*
* @since 5.84
*/
void setDevicePixelRatio(qreal dpr);
/**
* Returns a list of all available preview plugins. The list
* contains the basenames of the plugins' .desktop files (no path,
* no .desktop).
* @return the list of all available plugins
*/
static QStringList availablePlugins();
/**
* Returns all plugins that are considered when a preview is generated
* The result is internally cached, meaning any further method call will not reload the plugins
* @since 5.90
*/
static QList<KPluginMetaData> availableThumbnailerPlugins();
/**
* Returns a list of plugins that should be enabled by default, which is all plugins
* Minus the plugins specified in an internal blacklist
* @return the list of plugins that should be enabled by default
* @since 5.40
*/
static QStringList defaultPlugins();
/**
* Returns a list of all supported MIME types. The list can
* contain entries like text/ * (without the space).
* @return the list of MIME types
*/
static QStringList supportedMimeTypes();
Q_SIGNALS:
/**
* Emitted when a thumbnail picture for @p item has been successfully
* retrieved.
* @param item the file of the preview
* @param preview the preview image
*/
void gotPreview(const KFileItem &item, const QPixmap &preview);
/**
* Emitted when a thumbnail for @p item could not be created,
* either because a ThumbCreator for its MIME type does not
* exist, or because something went wrong.
* @param item the file that failed
*/
void failed(const KFileItem &item);
protected Q_SLOTS:
void slotResult(KJob *job) override;
private:
Q_DECLARE_PRIVATE(PreviewJob)
public:
/**
* Sets a default device Pixel Ratio used for Previews
* @see setDevicePixelRatio
*
* Defaults to 1
*
* @since 5.84
*/
static void setDefaultDevicePixelRatio(qreal devicePixelRatio);
};
/**
* Creates a PreviewJob to generate a preview image for the given items.
* @param items List of files to create previews for.
* @param size Desired size of the preview.
* @param enabledPlugins If non-zero it defines the list of plugins that
* are considered for generating the preview. If
* enabledPlugins is zero the plugins specified in the
* KConfigGroup "PreviewSettings" are used.
*/
KIOGUI_EXPORT PreviewJob *filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins = nullptr);
}
#endif
@@ -0,0 +1,151 @@
/*
SPDX-FileCopyrightText: 2024 Akseli Lahtinen <akselmo@akselmo.dev>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "global.h"
#include "standardthumbnailjob_p.h"
#include <KMacroExpander>
#include <QImage>
#include <QProcess>
#include <QTemporaryFile>
class ThumbnailerExpander : public KMacroExpanderBase
{
public:
explicit ThumbnailerExpander(const QString execString, const int width, const QString inputFile, const QString outputFile)
: KMacroExpanderBase(QLatin1Char('%'))
, m_width(width)
, m_execString(execString)
, m_inputFile(inputFile)
, m_outputFile(outputFile)
{
QString newString(execString);
expandMacros(newString);
auto fullCmd = QProcess::splitCommand(newString);
m_binary = fullCmd.first();
fullCmd.removeFirst();
m_args = fullCmd;
}
QString binary()
{
return m_binary;
}
QStringList args()
{
return m_args;
}
private:
int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override;
const int m_width;
const QString m_execString;
const QString m_inputFile;
const QString m_outputFile;
QString m_binary;
QStringList m_args;
};
int ThumbnailerExpander::expandEscapedMacro(const QString &str, int pos, QStringList &ret)
{
uint option = str[pos + 1].unicode();
switch (option) {
case 's':
ret << QString::number(m_width);
break;
case 'i':
case 'u':
ret << QStringLiteral(R"("%1")").arg(m_inputFile);
break;
case 'o':
ret << QStringLiteral(R"("%1")").arg(m_outputFile);
break;
case '%':
ret = QStringList(QStringLiteral("%"));
break;
default:
return -2; // subst with same and skip
}
return 2;
}
class Q_DECL_HIDDEN KIO::StandardThumbnailJob::Private
{
public:
explicit Private(const QString &execString, int width, const QString &inputFile, const QString &outputFolder)
: m_execString(execString)
, m_width(width)
, m_inputFile(inputFile)
, m_outputFolder(outputFolder)
{
}
~Private()
{
}
QString m_execString;
int m_width;
QString m_inputFile;
QString m_outputFolder;
QProcess *m_proc;
QTemporaryFile *m_tempFile;
};
KIO::StandardThumbnailJob::StandardThumbnailJob(const QString &execString, int width, const QString &inputFile, const QString &outputFolder)
: d(new Private(execString, width, inputFile, outputFolder))
{
setAutoDelete(true);
}
KIO::StandardThumbnailJob::~StandardThumbnailJob() = default;
bool KIO::StandardThumbnailJob::StandardThumbnailJob::doKill()
{
d->m_proc->kill();
return true;
}
void KIO::StandardThumbnailJob::StandardThumbnailJob::start()
{
// Prepare the command
d->m_tempFile = new QTemporaryFile(QStringLiteral("%1/XXXXXX.png").arg(d->m_outputFolder));
if (!d->m_tempFile->open()) {
setErrorText(QStringLiteral("Standard Thumbnail Job had an error: could not open temporary file"));
setError(KIO::ERR_CANNOT_OPEN_FOR_WRITING);
emitResult();
}
d->m_tempFile->setAutoRemove(false);
ThumbnailerExpander thumbnailer(d->m_execString, d->m_width, d->m_inputFile, d->m_tempFile->fileName());
// Emit data on command exit
d->m_proc = new QProcess();
connect(d->m_proc, &QProcess::finished, this, [=, this](const int exitCode, const QProcess::ExitStatus /* exitStatus */) {
d->m_proc->deleteLater();
if (exitCode != 0) {
setErrorText(QStringLiteral("Standard Thumbnail Job failed with exit code: %1 ").arg(exitCode));
setError(KIO::ERR_CANNOT_LAUNCH_PROCESS);
emitResult();
// clean temp file
d->m_tempFile->remove();
return;
}
Q_EMIT data(this, QImage(d->m_tempFile->fileName()));
emitResult();
// clean temp file
d->m_tempFile->remove();
});
connect(d->m_proc, &QProcess::errorOccurred, this, [=, this](const QProcess::ProcessError error) {
d->m_proc->deleteLater();
setErrorText(QStringLiteral("Standard Thumbnail Job had an error: %1").arg(error));
setError(KIO::ERR_CANNOT_LAUNCH_PROCESS);
emitResult();
// clean temp file
d->m_tempFile->remove();
});
d->m_proc->start(thumbnailer.binary(), thumbnailer.args());
}
@@ -0,0 +1,32 @@
/*
SPDX-FileCopyrightText: 2024 Akseli Lahtinen <akselmo@akselmo.dev>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#include <kio/job.h>
namespace KIO
{
class StandardThumbnailJob : public KIO::Job
{
Q_OBJECT
public:
StandardThumbnailJob(const QString &execString, int width, const QString &inputFile, const QString &outputFile);
~StandardThumbnailJob() override;
void start() override;
bool doKill() override;
Q_SIGNALS:
void data(KIO::Job *job, const QImage &thumb);
private:
class Private;
std::unique_ptr<Private> const d; /// @internal
Q_DISABLE_COPY(StandardThumbnailJob)
};
}
@@ -0,0 +1,86 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 Henri Chain <henri.chain@enioka.com>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef DBUSTYPES_H
#define DBUSTYPES_H
#include <QDBusArgument>
struct QVariantMultiItem {
QString key;
QVariant value;
};
Q_DECLARE_METATYPE(QVariantMultiItem)
using QVariantMultiMap = QList<QVariantMultiItem>;
Q_DECLARE_METATYPE(QVariantMultiMap)
inline QDBusArgument &operator<<(QDBusArgument &argument, const QVariantMultiItem &item)
{
argument.beginStructure();
argument << item.key << QDBusVariant(item.value);
argument.endStructure();
return argument;
}
inline const QDBusArgument &operator>>(const QDBusArgument &argument, QVariantMultiItem &item)
{
argument.beginStructure();
argument >> item.key >> item.value;
argument.endStructure();
return argument;
}
struct ExecCommand {
QString path;
QStringList argv;
bool ignoreFailure;
};
Q_DECLARE_METATYPE(ExecCommand)
using ExecCommandList = QList<ExecCommand>;
Q_DECLARE_METATYPE(ExecCommandList)
inline QDBusArgument &operator<<(QDBusArgument &argument, const ExecCommand &execCommand)
{
argument.beginStructure();
argument << execCommand.path << execCommand.argv << execCommand.ignoreFailure;
argument.endStructure();
return argument;
}
inline const QDBusArgument &operator>>(const QDBusArgument &argument, ExecCommand &execCommand)
{
argument.beginStructure();
argument >> execCommand.path >> execCommand.argv >> execCommand.ignoreFailure;
argument.endStructure();
return argument;
}
struct TransientAux {
QString name;
QVariantMultiMap properties;
};
Q_DECLARE_METATYPE(TransientAux)
using TransientAuxList = QList<TransientAux>;
Q_DECLARE_METATYPE(TransientAuxList)
inline QDBusArgument &operator<<(QDBusArgument &argument, const TransientAux &aux)
{
argument.beginStructure();
argument << aux.name << aux.properties;
argument.endStructure();
return argument;
}
inline const QDBusArgument &operator>>(const QDBusArgument &argument, TransientAux &aux)
{
argument.beginStructure();
argument >> aux.name >> aux.properties;
argument.endStructure();
return argument;
}
#endif // DBUSTYPES_H
@@ -0,0 +1,22 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface" direction="in" type="s"/>
<arg name="property" direction="in" type="s"/>
<arg name="value" direction="out" type="v"/>
</method>
<method name="GetAll">
<arg name="interface" direction="in" type="s"/>
<arg name="properties" direction="out" type="a{sv}"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
</method>
<signal name="PropertiesChanged">
<arg type="s" name="interface"/>
<arg type="a{sv}" name="changed_properties"/>
<arg type="as" name="invalidated_properties"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap" />
</signal>
</interface>
</node>
@@ -0,0 +1,28 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.systemd1.Manager">
<method name="StartTransientUnit">
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMultiMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In3" value="TransientAuxList"/>
<arg name="name" type="s" direction="in"/>
<arg name="mode" type="s" direction="in"/>
<arg name="properties" type="a(sv)" direction="in"/>
<arg name="aux" type="a(sa(sv))" direction="in"/>
</method>
<method name="Subscribe">
</method>
<method name="Unsubscribe">
</method>
<signal name="UnitNew">
<arg name="name" type="s"/>
<arg name="path" type="o"/>
</signal>
<signal name="JobRemoved">
<arg name="id" type="u"/>
<arg name="job" type="o"/>
<arg name="unit" type="s"/>
<arg name="result" type="s"/>
</signal>
</interface>
</node>
@@ -0,0 +1,8 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.systemd1.Unit">
<method name="Unref">
</method>
</interface>
</node>
@@ -0,0 +1,78 @@
#include "kiogui_debug.h"
#include "managerinterface.h"
#include "scopedprocessrunner_p.h"
#include "systemdprocessrunner_p.h"
#include <sys/eventfd.h>
#include <unistd.h>
using namespace org::freedesktop;
ScopedProcessRunner::ScopedProcessRunner()
: ForkingProcessRunner()
{
}
void ScopedProcessRunner::startProcess()
{
std::function oldModifier = m_process->childProcessModifier();
int efd = eventfd(0, EFD_CLOEXEC);
m_process->setChildProcessModifier([efd, oldModifier]() {
// wait for the parent process to be done registering the transient unit
eventfd_read(efd, nullptr);
if (oldModifier)
oldModifier();
});
// actually start
ForkingProcessRunner::startProcess();
m_process->setChildProcessModifier(oldModifier);
Q_ASSERT(m_process->processId());
// As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/
const QString serviceName = QStringLiteral("app-%1-%2.scope").arg(escapeUnitName(resolveServiceAlias()), QUuid::createUuid().toString(QUuid::Id128));
const auto manager = new systemd1::Manager(systemdService, systemdPath, QDBusConnection::sessionBus(), this);
// Ask systemd for a new transient service
const auto startReply =
manager->StartTransientUnit(serviceName,
QStringLiteral("fail"), // mode defines what to do in the case of a name conflict, in this case, just do nothing
{// Properties of the transient service unit
{QStringLiteral("Slice"), QStringLiteral("app.slice")},
{QStringLiteral("Description"), m_description},
{QStringLiteral("SourcePath"), m_desktopFilePath},
{QStringLiteral("PIDs"), QVariant::fromValue(QList<uint>{static_cast<uint>(m_process->processId())})}},
{} // aux is currently unused and should be passed as empty array.
);
m_transientUnitStartupwatcher = new QDBusPendingCallWatcher(startReply, this);
connect(m_transientUnitStartupwatcher, &QDBusPendingCallWatcher::finished, [serviceName, efd](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<QDBusObjectPath> reply = *watcher;
watcher->deleteLater();
if (reply.isError()) {
qCWarning(KIO_GUI) << "Failed to register new cgroup:" << serviceName << reply.error().name() << reply.error().message();
} else {
qCDebug(KIO_GUI) << "Successfully registered new cgroup:" << serviceName;
}
// release child and close the eventfd
eventfd_write(efd, 1);
close(efd);
});
}
bool ScopedProcessRunner::waitForStarted(int timeout)
{
if (m_process->state() == QProcess::NotRunning || m_waitingForXdgToken || !m_transientUnitStartupwatcher->isFinished()) {
QEventLoop loop;
QObject::connect(m_process.get(), &QProcess::stateChanged, &loop, &QEventLoop::quit);
QObject::connect(m_transientUnitStartupwatcher, &QDBusPendingCallWatcher::finished, &loop, &QEventLoop::quit);
QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
loop.exec();
}
return m_process->waitForStarted(timeout);
}
#include "moc_scopedprocessrunner_p.cpp"
@@ -0,0 +1,19 @@
#ifndef SCOPEDPROCESSRUNNER_P_H
#define SCOPEDPROCESSRUNNER_P_H
#include "kprocessrunner_p.h"
class QDBusPendingCallWatcher;
class ScopedProcessRunner : public ForkingProcessRunner
{
Q_OBJECT
public:
explicit ScopedProcessRunner();
void startProcess() override;
bool waitForStarted(int timeout) override;
private:
QDBusPendingCallWatcher *m_transientUnitStartupwatcher;
};
#endif // SCOPEDPROCESSRUNNER_H
@@ -0,0 +1,252 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 Henri Chain <henri.chain@enioka.com>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "kiogui_debug.h"
#include "systemdprocessrunner_p.h"
#include "managerinterface.h"
#include "propertiesinterface.h"
#include "unitinterface.h"
#include <QTimer>
#include <algorithm>
#include <mutex>
#include <signal.h>
using namespace org::freedesktop;
using namespace Qt::Literals::StringLiterals;
KProcessRunner::LaunchMode calculateLaunchMode()
{
// overrides for unit test purposes. These are considered internal, private and may change in the future.
if (Q_UNLIKELY(qEnvironmentVariableIntValue("_KDE_APPLICATIONS_AS_SERVICE"))) {
return KProcessRunner::SystemdAsService;
}
if (Q_UNLIKELY(qEnvironmentVariableIntValue("_KDE_APPLICATIONS_AS_SCOPE"))) {
return KProcessRunner::SystemdAsScope;
}
if (Q_UNLIKELY(qEnvironmentVariableIntValue("_KDE_APPLICATIONS_AS_FORKING"))) {
return KProcessRunner::Forking;
}
QDBusConnection bus = QDBusConnection::sessionBus();
auto queryVersionMessage = QDBusMessage::createMethodCall(systemdService, systemdPath, u"org.freedesktop.DBus.Properties"_s, u"Get"_s);
queryVersionMessage << u"org.freedesktop.systemd1.Manager"_s << u"Version"_s;
QDBusReply<QDBusVariant> reply = bus.call(queryVersionMessage);
QVersionNumber systemdVersion = QVersionNumber::fromString(reply.value().variant().toString());
if (systemdVersion.isNull()) {
qCWarning(KIO_GUI) << "Failed to determine systemd version, falling back to extremely legacy forking mode.";
return KProcessRunner::Forking;
}
if (systemdVersion.majorVersion() < 250) { // first version with ExitType=cgroup, which won't cleanup when the first process exits
return KProcessRunner::SystemdAsScope;
}
return KProcessRunner::SystemdAsService;
}
KProcessRunner::LaunchMode SystemdProcessRunner::modeAvailable()
{
static std::once_flag launchModeCalculated;
static KProcessRunner::LaunchMode launchMode = Forking;
std::call_once(launchModeCalculated, [] {
launchMode = calculateLaunchMode();
qCDebug(KIO_GUI) << "Launching processes via" << launchMode;
qDBusRegisterMetaType<QVariantMultiItem>();
qDBusRegisterMetaType<QVariantMultiMap>();
qDBusRegisterMetaType<TransientAux>();
qDBusRegisterMetaType<TransientAuxList>();
qDBusRegisterMetaType<ExecCommand>();
qDBusRegisterMetaType<ExecCommandList>();
});
return launchMode;
}
SystemdProcessRunner::SystemdProcessRunner()
: KProcessRunner()
{
}
bool SystemdProcessRunner::waitForStarted(int timeout)
{
if (m_pid || m_exited) {
return true;
}
QEventLoop loop;
bool success = false;
loop.connect(this, &KProcessRunner::processStarted, this, [&loop, &success]() {
loop.quit();
success = true;
});
QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
QObject::connect(this, &KProcessRunner::error, &loop, &QEventLoop::quit);
loop.exec();
return success;
}
static QStringList prepareEnvironment(const QProcessEnvironment &environment)
{
QProcessEnvironment allowedEnvironment = environment.inheritsFromParent() ? QProcessEnvironment::systemEnvironment() : environment;
auto allowedBySystemd = [](const QChar c) {
return c.isDigit() || c.isLetter() || c == u'_';
};
for (const auto variables = allowedEnvironment.keys(); const auto &variable : variables) {
if (!std::ranges::all_of(variable, allowedBySystemd)) {
qCWarning(KIO_GUI) << "Not passing environment variable" << variable << "to systemd because its name contains illegal characters";
allowedEnvironment.remove(variable);
}
}
return allowedEnvironment.toStringList();
}
// systemd performs substitution of $ variables, we don't want this
// $ should be replaced with $$
static QStringList escapeArguments(const QStringList &in)
{
QStringList escaped = in;
std::transform(escaped.begin(), escaped.end(), escaped.begin(), [](QString &item) {
return item.replace(QLatin1Char('$'), QLatin1String("$$"));
});
return escaped;
}
void SystemdProcessRunner::startProcess()
{
// As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/
m_serviceName = QStringLiteral("app-%1@%2.service").arg(escapeUnitName(resolveServiceAlias()), QUuid::createUuid().toString(QUuid::Id128));
// Watch for new services
m_manager = new systemd1::Manager(systemdService, systemdPath, QDBusConnection::sessionBus(), this);
m_manager->Subscribe();
connect(m_manager, &systemd1::Manager::UnitNew, this, &SystemdProcessRunner::handleUnitNew);
// Watch for service creation job error
connect(m_manager,
&systemd1::Manager::JobRemoved,
this,
[this](uint jobId, const QDBusObjectPath &jobPath, const QString &unitName, const QString &result) {
Q_UNUSED(jobId)
if (jobPath.path() == m_jobPath && unitName == m_serviceName && result != QLatin1String("done")) {
qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << ", result " << result;
// result=failed is not a fatal error, service is actually created in this case
if (result != QLatin1String("failed")) {
systemdError(result);
}
}
});
const QStringList argv = escapeArguments(m_process->program());
// Ask systemd for a new transient service
const auto startReply =
m_manager->StartTransientUnit(m_serviceName,
QStringLiteral("fail"), // mode defines what to do in the case of a name conflict, in this case, just do nothing
{
// Properties of the transient service unit
{QStringLiteral("Type"), QStringLiteral("simple")},
{QStringLiteral("ExitType"), QStringLiteral("cgroup")},
{QStringLiteral("Slice"), QStringLiteral("app.slice")},
{QStringLiteral("Description"), m_description},
{QStringLiteral("SourcePath"), m_desktopFilePath},
{QStringLiteral("AddRef"), true}, // Asks systemd to avoid garbage collecting the service if it immediately crashes,
// so we can be notified (see https://github.com/systemd/systemd/pull/3984)
{QStringLiteral("Environment"), prepareEnvironment(m_process->processEnvironment())},
{QStringLiteral("WorkingDirectory"), m_process->workingDirectory()},
{QStringLiteral("ExecStart"), QVariant::fromValue(ExecCommandList{{m_process->program().first(), argv, false}})},
},
{} // aux is currently unused and should be passed as empty array.
);
connect(new QDBusPendingCallWatcher(startReply, this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<QDBusObjectPath> reply = *watcher;
watcher->deleteLater();
if (reply.isError()) {
qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << reply.error().name() << reply.error().message();
return systemdError(reply.error().message());
}
qCDebug(KIO_GUI) << "Successfully asked systemd to launch process as service:" << m_serviceName;
m_jobPath = reply.argumentAt<0>().path();
});
}
void SystemdProcessRunner::handleProperties(QDBusPendingCallWatcher *watcher)
{
const QDBusPendingReply<QVariantMap> reply = *watcher;
watcher->deleteLater();
if (reply.isError()) {
qCWarning(KIO_GUI) << "Failed to get properties for service:" << m_serviceName << reply.error().name() << reply.error().message();
return systemdError(reply.error().message());
}
qCDebug(KIO_GUI) << "Successfully retrieved properties for service:" << m_serviceName;
if (m_exited) {
return;
}
const auto properties = reply.argumentAt<0>();
if (!m_pid) {
setPid(properties[QStringLiteral("ExecMainPID")].value<quint32>());
return;
}
const auto activeState = properties[QStringLiteral("ActiveState")].toString();
if (activeState != QLatin1String("inactive") && activeState != QLatin1String("failed")) {
return;
}
m_exited = true;
// ExecMainCode/Status correspond to si_code/si_status in the siginfo_t structure
// ExecMainCode is the signal code: CLD_EXITED (1) means normal exit
// ExecMainStatus is the process exit code in case of normal exit, otherwise it is the signal number
const auto signalCode = properties[QStringLiteral("ExecMainCode")].value<qint32>();
const auto exitCodeOrSignalNumber = properties[QStringLiteral("ExecMainStatus")].value<qint32>();
const auto exitStatus = signalCode == CLD_EXITED ? QProcess::ExitStatus::NormalExit : QProcess::ExitStatus::CrashExit;
qCDebug(KIO_GUI) << m_serviceName << "pid=" << m_pid << "exitCode=" << exitCodeOrSignalNumber << "exitStatus=" << exitStatus;
terminateStartupNotification();
deleteLater();
systemd1::Unit unitInterface(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
connect(new QDBusPendingCallWatcher(unitInterface.Unref(), this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<> reply = *watcher;
watcher->deleteLater();
if (reply.isError()) {
qCWarning(KIO_GUI) << "Failed to unref service:" << m_serviceName << reply.error().name() << reply.error().message();
return systemdError(reply.error().message());
}
qCDebug(KIO_GUI) << "Successfully unref'd service:" << m_serviceName;
});
}
void SystemdProcessRunner::handleUnitNew(const QString &newName, const QDBusObjectPath &newPath)
{
if (newName != m_serviceName) {
return;
}
qCDebug(KIO_GUI) << "Successfully launched process as service:" << m_serviceName;
// Get PID (and possibly exit code) from systemd service properties
m_servicePath = newPath.path();
m_serviceProperties = new DBus::Properties(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
auto propReply = m_serviceProperties->GetAll(QString());
connect(new QDBusPendingCallWatcher(propReply, this), &QDBusPendingCallWatcher::finished, this, &SystemdProcessRunner::handleProperties);
// Watch for status change
connect(m_serviceProperties, &DBus::Properties::PropertiesChanged, this, [this]() {
if (m_exited) {
return;
}
qCDebug(KIO_GUI) << "Got PropertiesChanged signal:" << m_serviceName;
// We need to look at the full list of properties rather than only those which changed
auto reply = m_serviceProperties->GetAll(QString());
connect(new QDBusPendingCallWatcher(reply, this), &QDBusPendingCallWatcher::finished, this, &SystemdProcessRunner::handleProperties);
});
}
void SystemdProcessRunner::systemdError(const QString &message)
{
Q_EMIT error(message);
deleteLater();
}
#include "moc_systemdprocessrunner_p.cpp"
@@ -0,0 +1,42 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 Henri Chain <henri.chain@enioka.com>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef SYSTEMDPROCESSRUNNER_H
#define SYSTEMDPROCESSRUNNER_H
#include "kprocessrunner_p.h"
class OrgFreedesktopSystemd1ManagerInterface;
class OrgFreedesktopDBusPropertiesInterface;
class QDBusObjectPath;
class QDBusPendingCallWatcher;
const auto systemdService = QStringLiteral("org.freedesktop.systemd1");
const auto systemdPath = QStringLiteral("/org/freedesktop/systemd1");
class SystemdProcessRunner : public KProcessRunner
{
Q_OBJECT
public:
explicit SystemdProcessRunner();
void startProcess() override;
bool waitForStarted(int timeout) override;
static KProcessRunner::LaunchMode modeAvailable();
private:
void handleProperties(QDBusPendingCallWatcher *watcher);
void handleUnitNew(const QString &newName, const QDBusObjectPath &newPath);
void systemdError(const QString &error);
bool m_exited = false;
QString m_serviceName;
QString m_servicePath;
QString m_jobPath;
OrgFreedesktopSystemd1ManagerInterface *m_manager = nullptr;
OrgFreedesktopDBusPropertiesInterface *m_serviceProperties = nullptr;
};
#endif // SYSTEMDPROCESSRUNNER_H
@@ -0,0 +1,144 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2000 Malte Starostik <malte@kde.org>
SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "thumbnailcreator.h"
#include <QImage>
namespace KIO
{
class ThumbnailRequestPrivate
{
public:
QUrl url;
QSize targetSize;
QString mimeType;
qreal dpr;
float sequenceIndex;
};
ThumbnailRequest::~ThumbnailRequest() = default;
ThumbnailRequest::ThumbnailRequest(const ThumbnailRequest &other)
{
d = std::make_unique<ThumbnailRequestPrivate>(*other.d);
}
ThumbnailRequest &ThumbnailRequest::operator=(const ThumbnailRequest &other)
{
ThumbnailRequest temp(other);
std::swap(*d, *temp.d);
return *this;
}
ThumbnailRequest::ThumbnailRequest(const QUrl &url, const QSize &targetSize, const QString &mimeType, qreal dpr, float sequenceIndex)
: d(new ThumbnailRequestPrivate)
{
d->url = url;
d->targetSize = targetSize;
d->mimeType = mimeType;
d->dpr = dpr;
d->sequenceIndex = sequenceIndex;
}
QUrl ThumbnailRequest::url() const
{
return d->url;
}
QSize ThumbnailRequest::targetSize() const
{
return d->targetSize;
}
QString ThumbnailRequest::mimeType() const
{
return d->mimeType;
}
qreal ThumbnailRequest::devicePixelRatio() const
{
return d->dpr;
}
float ThumbnailRequest::sequenceIndex() const
{
return d->sequenceIndex;
}
class ThumbnailResultPrivate
{
public:
QImage image;
float sequenceIndexWraparoundPoint = -1;
};
ThumbnailResult::ThumbnailResult()
: d(new ThumbnailResultPrivate)
{
}
ThumbnailResult::~ThumbnailResult() = default;
ThumbnailResult::ThumbnailResult(const ThumbnailResult &other)
{
d = std::make_unique<ThumbnailResultPrivate>(*other.d);
}
ThumbnailResult &ThumbnailResult::operator=(const ThumbnailResult &other)
{
ThumbnailResult temp(other);
std::swap(*d, *temp.d);
return *this;
}
ThumbnailResult ThumbnailResult::pass(const QImage &image)
{
ThumbnailResult response;
response.d->image = image;
return response;
}
ThumbnailResult ThumbnailResult::fail()
{
ThumbnailResult response;
return response;
}
float ThumbnailResult::sequenceIndexWraparoundPoint() const
{
return d->sequenceIndexWraparoundPoint;
}
void ThumbnailResult::setSequenceIndexWraparoundPoint(float wraparoundPoint)
{
d->sequenceIndexWraparoundPoint = wraparoundPoint;
}
QImage ThumbnailResult::image() const
{
return d->image;
}
bool ThumbnailResult::isValid() const
{
return !d->image.isNull();
}
ThumbnailCreator::ThumbnailCreator(QObject *parent, const QVariantList &args)
: QObject(parent)
{
Q_UNUSED(args);
}
ThumbnailCreator::~ThumbnailCreator() = default;
};
#include "moc_thumbnailcreator.cpp"
@@ -0,0 +1,250 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2000 Malte Starostik <malte@kde.org>
SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef _THUMBNAILCREATOR_H_
#define _THUMBNAILCREATOR_H_
#include "kiogui_export.h"
#include <QObject>
#include <QUrl>
#include <memory>
class QString;
class QImage;
namespace KIO
{
class ThumbnailCreatorPrivate;
class ThumbnailRequestPrivate;
class ThumbnailResultPrivate;
/**
* Encapsulates the input data for a thumbnail request.
* This includes the URL of the target file as well as additional
* data such as the target size
*
* @since 5.100
*
*/
class KIOGUI_EXPORT ThumbnailRequest
{
public:
/**
* Contruct a new ThumbnailRequest for a given file.
*
* @param url URL of the relevant file.
* @param targetSize A size hint for the result image.
* The actual result size may be different. This already
* accounts for highdpi scaling, i.e. if a 500x500px thumbnail
* with a DPR of 2 is requested 1000x1000 is passed here.
* @param mimeType The MIME type of the target file.
* @param dpr The device pixle ratio for this request. This can
* be used to adjust the level of detail rendered. For example
* a thumbnail for text of size 1000x1000 and DPR 1 should have
* the name number of text lines as for a request of size 2000x2000
* and DPR 2.
* @param sequenceIndex If the thumbnailer supports sequences this
* determines which sequence frame is used. Pass 0 otherwise.
*
*/
explicit ThumbnailRequest(const QUrl &url, const QSize &targetSize, const QString &mimeType, qreal dpr, float sequenceIndex);
ThumbnailRequest(const ThumbnailRequest &);
ThumbnailRequest &operator=(const ThumbnailRequest &);
~ThumbnailRequest();
/**
* URL of the relevant file
*/
QUrl url() const;
/**
* The target thumbnail size
*/
QSize targetSize() const;
/**
* The target file's MIME type
*/
QString mimeType() const;
/**
* The device Pixel Ratio used for thumbnail creation
*/
qreal devicePixelRatio() const;
/**
* If the thumb-creator can create a sequence of thumbnails,
* it should use this to decide what sequence item to use.
*
* If the value is zero, the standard thumbnail should be created.
*
* This can be used for example to create thumbnails for different
* timeframes in videos(For example 0m, 10m, 20m, ...).
*
* If the thumb-creator supports a high granularity, like a video,
* the sub-integer precision coming from the float should be respected.
*
* If the end of the sequence is reached, the sequence should start
* from the beginning.
*/
float sequenceIndex() const;
private:
std::unique_ptr<ThumbnailRequestPrivate> d;
};
/**
* Encapsulates the output of a thumbnail request.
* It contains information on whether the request was successful and,
* if successful, the requested thumbnail
*
* To create a result use KIO::ThumbnailResult::pass(image) or KIO::ThumbnailResult::fail()
*
* @since 5.100
*/
class KIOGUI_EXPORT ThumbnailResult
{
public:
ThumbnailResult(const ThumbnailResult &);
ThumbnailResult &operator=(const ThumbnailResult &);
~ThumbnailResult();
/**
* The requested thumbnail.
*
* If the request failed the image is null
*/
QImage image() const;
/**
* Whether the request was successful.
*/
bool isValid() const;
/**
* Returns the point at which this thumb-creator's sequence indices
* will wrap around (loop).
*
* Usually, the frontend will call setSequenceIndex() with indices
* that increase indefinitely with time, e.g. as long as the user
* keeps hovering a video file. Most thumb-creators however only
* want to display a finite sequence of thumbs, after which their
* sequence repeats.
*
* This method can return the sequence index at which this
* thumb-creator's sequence starts wrapping around to the start
* again ("looping"). The frontend may use this to generate only
* thumbs up to this index, and then use cached versions for the
* repeating sequence instead.
*
* Like sequenceIndex(), fractional values can be used if the
* wraparound does not happen at an integer position, but
* frontends handling only integer sequence indices may choose
* to round it down.
*
* By default, this method returns a negative index, which signals
* the frontend that it can't rely on this fixed-length sequence.
*
*/
float sequenceIndexWraparoundPoint() const;
/**
* Sets the point at which this thumb-creator's sequence indices
* will wrap around.
*
* @see sequenceIndexWraparoundPoint()
*/
void setSequenceIndexWraparoundPoint(float wraparoundPoint);
/**
* Create a successful result with a given image
*/
static ThumbnailResult pass(const QImage &image);
/**
* Create an error result, i.e. the thumbnail creation failed
*/
static ThumbnailResult fail();
private:
KIOGUI_NO_EXPORT ThumbnailResult();
std::unique_ptr<ThumbnailResultPrivate> d;
};
/**
* @class ThumbnailCreator thumbnailcreator.h <KIO/ThumbnailCreator>
*
* Base class for thumbnail generator plugins.
*
* KIO::PreviewJob, via the "thumbnail" KIO worker, uses instances of this class
* to generate the thumbnail previews.
*
* To add support for a new document type, subclass KIO::ThumbnailCreator and implement
* create() to generate a thumbnail for a given path.
*
* Compile your ThumbCreator as a plugin; for example, the relevant CMake code
* for a thumbnailer for the "foo" filetype might look like
* \code
* kcoreaddons_add_plugin(foothumbnail SOURCES foothumbnail.cpp INSTALL_NAMESPACE "kf6/thumbcreator")
* target_link_libraries(foothumbnail PRIVATE KF5::KIOGui)
* \endcode
*
* You also need a JSON file containing the metadata:
* \code
* {
* "CacheThumbnail": true,
* "KPlugin": {
* "MimeTypes": [
* "image/x-foo"
* ],
* "Name": "Foo Documents"
* }
* }
* \endcode
*
* MIME types can also use
* simple wildcards, like
* \htmlonly "text/&#42;".\endhtmlonly\latexonly text/$\ast$.\endlatexonly
*
* If the thumbnail creation is cheap (such as text previews), you can set
* \code
* "CacheThumbnail": false
* \endcode
* in metadata to prevent your thumbnails from being cached on disk.
*
* You can also use the "ThumbnailerVersion" optional property in the .desktop
* file, like
* \code
* "ThumbnailerVersion": 5
* \endcode
* When this is incremented (or defined when it previously was not), all the
* previously-cached thumbnails for this creator will be discarded. You should
* increase the version if and only if old thumbnails need to be regenerated.
*
* @since 5.100
*/
class KIOGUI_EXPORT ThumbnailCreator : public QObject
{
Q_OBJECT
public:
explicit ThumbnailCreator(QObject *parent, const QVariantList &args);
virtual ~ThumbnailCreator();
/**
* Creates a thumbnail for a given request
*/
virtual ThumbnailResult create(const ThumbnailRequest &request) = 0;
private:
void *d; // Placeholder
};
}
#endif
@@ -0,0 +1,91 @@
/*
SPDX-FileCopyrightText: 2015 Gregor Mi <codestruct@posteo.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef URLUTIL_P_H
#define URLUTIL_P_H
#include <QUrl>
namespace KIO
{
namespace UrlUtil
{
/**
* Given that @p lastUrl is a child of @p currentUrl
* or put in other words:
* @p currentUrl is a parent in the hierarchy of @p lastUrl,
* then an URL 'currentUrl'/'childitem' is returned where
* 'childitem' is the first item in the hierarchy down to
* @p lastUrl. An example will illustrate this:
\verbatim
lastUrl : "/home/test/data/documents/muh/"
currentUrl : "/home/test/"
returns : "/home/test/data/"
\endverbatim
* In case @p currentUrl is a child of @p lastUrl, an invalid
* URL is returned:
\verbatim
lastUrl : "/home/"
currentUrl : "/home/test/"
returns : "" (invalid url)
\endverbatim
* In case both URLs are equal, an invalid URL is returned:
\verbatim
lastUrl : "/home/test/"
currentUrl : "/home/test/"
returns : "" (invalid url)
\endverbatim
*/
static QUrl firstChildUrl(const QUrl &lastUrl, const QUrl &currentUrl)
{
const QUrl adjustedLastUrl = lastUrl.adjusted(QUrl::StripTrailingSlash);
const QUrl adjustedCurrentUrl = currentUrl.adjusted(QUrl::StripTrailingSlash);
if (!adjustedCurrentUrl.isParentOf(adjustedLastUrl)) {
return QUrl();
}
const QString childPath = adjustedLastUrl.path();
const QString parentPath = adjustedCurrentUrl.path();
// if the parent path is root "/"
// one char more is a valid path, otherwise "/" and another char are needed.
const int minIndex = (parentPath == QLatin1String("/")) ? 1 : 2;
// e.g. this would just be ok:
// childPath = /a len=2
// parentPath = / len=1
// childPath = /home/a len=7
// parentPath = /home len=5
if (childPath.length() < (parentPath.length() + minIndex)) {
return QUrl();
}
const int idx2 = childPath.indexOf(QLatin1Char('/'), parentPath.length() + minIndex);
// parentPath = /home
// childPath = /home/a
// idx = -1
// => len2 = 7
//
// childPath = /homa/a/b
// idx = 7
// => len2 = 7
const int len2 = (idx2 < 0) ? childPath.length() : idx2;
const QString path3 = childPath.left(len2);
QUrl res = lastUrl; // keeps the scheme (e.g. file://)
res.setPath(path3);
return res;
}
}
}
#endif