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:
@@ -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
|
||||
* <link rel="shortcut icon" href="another_favicon.ico" />
|
||||
* 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/*".\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 ¤tUrl)
|
||||
{
|
||||
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
|
||||
Reference in New Issue
Block a user