diff --git a/autotests/applicationlauncherjobtest.cpp b/autotests/applicationlauncherjobtest.cpp index 73646c86..7583d88a 100644 --- a/autotests/applicationlauncherjobtest.cpp +++ b/autotests/applicationlauncherjobtest.cpp @@ -1,366 +1,441 @@ /* This file is part of the KDE libraries Copyright (c) 2014, 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "applicationlauncherjobtest.h" #include "applicationlauncherjob.h" #include #include +#include #include "kiotesthelper.h" // createTestFile etc. #include #include #include #ifdef Q_OS_UNIX #include // kill #endif #include #include #include #include QTEST_GUILESS_MAIN(ApplicationLauncherJobTest) namespace KIO { KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface); +KIOGUI_EXPORT void setDefaultOpenWithHandler(KIO::OpenWithHandlerInterface *iface); } class TestUntrustedProgramHandler : public KIO::UntrustedProgramHandlerInterface { public: void showUntrustedProgramWarning(KJob *job, const QString &programName) override { Q_UNUSED(job) m_calls << programName; Q_EMIT result(m_retVal); } void setRetVal(bool b) { m_retVal = b; } QStringList m_calls; bool m_retVal = false; }; static TestUntrustedProgramHandler s_handler; +class TestOpenWithHandler : public KIO::OpenWithHandlerInterface +{ +public: + void promptUserForApplication(KJob *job, const QList &url, const QString &mimeType) override + { + Q_UNUSED(job); + m_urls << url; + m_mimeTypes << mimeType; + if (m_chosenService) { + Q_EMIT serviceSelected(m_chosenService); + } else { + Q_EMIT canceled(); + } + } + QList m_urls; + QStringList m_mimeTypes; + KService::Ptr m_chosenService; +}; +static TestOpenWithHandler s_openWithHandler; + void ApplicationLauncherJobTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); } void ApplicationLauncherJobTest::cleanupTestCase() { std::for_each(m_filesToRemove.begin(), m_filesToRemove.end(), [](const QString & f) { QFile::remove(f); }); } static const char s_tempServiceName[] = "applicationlauncherjobtest_service.desktop"; static void createSrcFile(const QString path) { QFile srcFile(path); QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); srcFile.write("Hello world\n"); } void ApplicationLauncherJobTest::startProcess_data() { QTest::addColumn("tempFile"); QTest::addColumn("useExec"); QTest::addColumn("numFiles"); QTest::newRow("1_file_exec") << false << true << 1; QTest::newRow("1_file_waitForStarted") << false << false << 1; QTest::newRow("1_tempfile_exec") << true << true << 1; QTest::newRow("1_tempfile_waitForStarted") << true << false << 1; QTest::newRow("2_files_exec") << false << true << 2; QTest::newRow("2_files_waitForStarted") << false << false << 2; QTest::newRow("2_tempfiles_exec") << true << true << 2; QTest::newRow("2_tempfiles_waitForStarted") << true << false << 2; } void ApplicationLauncherJobTest::startProcess() { QFETCH(bool, tempFile); QFETCH(bool, useExec); QFETCH(int, numFiles); // Given a service desktop file and a number of source files const QString path = createTempService(); QTemporaryDir tempDir; const QString srcDir = tempDir.path(); QList urls; for (int i = 0; i < numFiles; ++i) { const QString srcFile = srcDir + "/srcfile" + QString::number(i + 1); createSrcFile(srcFile); QVERIFY(QFile::exists(srcFile)); urls.append(QUrl::fromLocalFile(srcFile)); } // When running a ApplicationLauncherJob KService::Ptr servicePtr(new KService(path)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls(urls); if (tempFile) { job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); } if (useExec) { QVERIFY2(job->exec(), qPrintable(job->errorString())); } else { job->start(); QVERIFY(job->waitForStarted()); } const QVector pids = job->pids(); // Then the service should be executed (which copies the source file to "dest") QCOMPARE(pids.count(), numFiles); QVERIFY(!pids.contains(0)); for (int i = 0; i < numFiles; ++i) { const QString dest = srcDir + "/dest_srcfile" + QString::number(i + 1); QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); QVERIFY(QFile::exists(srcDir + "/srcfile" + QString::number(i + 1))); // if tempfile is true, kioexec will delete it... in 3 minutes. QVERIFY(QFile::remove(dest)); // cleanup } #ifdef Q_OS_UNIX // Kill the running kioexec processes for (qint64 pid : pids) { ::kill(pid, SIGTERM); } #endif // The kioexec processes that are waiting for 3 minutes and got killed above, // will now trigger KProcessRunner::slotProcessError, KProcessRunner::slotProcessExited and delete the KProcessRunner. // We wait for that to happen otherwise it gets confusing to see that output from later tests. QTRY_COMPARE(KProcessRunner::instanceCount(), 0); } void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile_data() { QTest::addColumn("withHandler"); QTest::addColumn("handlerRetVal"); QTest::addColumn("useExec"); QTest::newRow("no_handler_exec") << false << false << true; QTest::newRow("handler_false_exec") << true << false << true; QTest::newRow("handler_true_exec") << true << true << true; QTest::newRow("no_handler_waitForStarted") << false << false << false; QTest::newRow("handler_false_waitForStarted") << true << false << false; QTest::newRow("handler_true_waitForStarted") << true << true << false; } void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile() { QFETCH(bool, useExec); QFETCH(bool, withHandler); QFETCH(bool, handlerRetVal); // Given a .desktop file in a temporary directory (outside the trusted paths) QTemporaryDir tempDir; const QString srcDir = tempDir.path(); const QString desktopFilePath = srcDir + "/shouldfail.desktop"; writeTempServiceDesktopFile(desktopFilePath); m_filesToRemove.append(desktopFilePath); const QString srcFile = srcDir + "/srcfile"; createSrcFile(srcFile); const QList urls{QUrl::fromLocalFile(srcFile)}; KService::Ptr servicePtr(new KService(desktopFilePath)); s_handler.m_calls.clear(); s_handler.setRetVal(handlerRetVal); KIO::setDefaultUntrustedProgramHandler(withHandler ? &s_handler : nullptr); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls(urls); bool success; if (useExec) { success = job->exec(); } else { job->start(); success = job->waitForStarted(); } if (!withHandler) { QVERIFY(!success); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file.")); } else { if (handlerRetVal) { QVERIFY(success); + const QString dest = srcDir + "/dest_srcfile"; + QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); + // The actual shell process will race against the deletion of the QTemporaryDir, // so don't be surprised by stderr like getcwd: cannot access parent directories: No such file or directory QTest::qWait(50); // this helps a bit } else { QVERIFY(!success); QCOMPARE(job->error(), KIO::ERR_USER_CANCELED); } } if (withHandler) { // check that the handler was called QCOMPARE(s_handler.m_calls.count(), 1); QCOMPARE(s_handler.m_calls.at(0), QStringLiteral("KRunUnittestService")); } } void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable_data() { QTest::addColumn("tempFile"); QTest::addColumn("fullPath"); QTest::newRow("file") << false << false; QTest::newRow("tempFile") << true << false; QTest::newRow("file_fullPath") << false << true; QTest::newRow("tempFile_fullPath") << true << true; } void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable() { QFETCH(bool, tempFile); QFETCH(bool, fullPath); const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/non_existing_executable.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "Service"); if (fullPath) { group.writeEntry("Exec", "/usr/bin/does_not_exist %f %d/dest_%n"); } else { group.writeEntry("Exec", "does_not_exist %f %d/dest_%n"); } file.sync(); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls({QUrl::fromLocalFile(desktopFilePath)}); // just to have one URL as argument, as the desktop file expects if (tempFile) { job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); } QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); if (fullPath) { QCOMPARE(job->errorString(), QStringLiteral("Could not find the program '/usr/bin/does_not_exist'")); } else { QCOMPARE(job->errorString(), QStringLiteral("Could not find the program 'does_not_exist'")); } QFile::remove(desktopFilePath); } void ApplicationLauncherJobTest::shouldFailOnInvalidService() { const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "NoSuchType"); group.writeEntry("Exec", "does_not_exist"); file.sync(); QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The desktop entry file \".*\" has Type.*\"NoSuchType\" instead of \"Application\" or \"Service\"")); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("The desktop entry file\n%1\nis not valid.").arg(desktopFilePath)); QFile::remove(desktopFilePath); } void ApplicationLauncherJobTest::shouldFailOnServiceWithNoExec() { const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestServiceNoExec"); group.writeEntry("Type", "Service"); file.sync(); QTest::ignoreMessage(QtWarningMsg, qPrintable(QString("No Exec field in \"%1\"").arg(desktopFilePath))); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("No Exec field in %1").arg(desktopFilePath)); QFile::remove(desktopFilePath); } void ApplicationLauncherJobTest::shouldFailOnExecutableWithoutPermissions() { #ifdef Q_OS_UNIX // Given an executable shell script that copies "src" to "dest" (we'll cheat with the mimetype to treat it like a native binary) QTemporaryDir tempDir; const QString dir = tempDir.path(); const QString scriptFilePath = dir + QStringLiteral("/script.sh"); QFile scriptFile(scriptFilePath); QVERIFY(scriptFile.open(QIODevice::WriteOnly)); scriptFile.write("#!/bin/sh\ncp src dest"); scriptFile.close(); // Note that it's missing executable permissions const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestServiceNoPermission"); group.writeEntry("Type", "Service"); group.writeEntry("Exec", scriptFilePath); file.sync(); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("The program '%1' is missing executable permissions.").arg(scriptFilePath)); QFile::remove(desktopFilePath); #else QSKIP("This test is not run on Windows"); #endif } +void ApplicationLauncherJobTest::showOpenWithDialog_data() +{ + QTest::addColumn("handlerRetVal"); + + QTest::newRow("false_canceled") << false; + QTest::newRow("true_service_selected") << true; +} + +void ApplicationLauncherJobTest::showOpenWithDialog() +{ +#ifdef Q_OS_UNIX + QFETCH(bool, handlerRetVal); + + // Given a local text file (we could test multiple files, too...) + QTemporaryDir tempDir; + const QString srcDir = tempDir.path(); + const QString srcFile = srcDir + QLatin1String("/file.txt"); + createSrcFile(srcFile); + + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(this); + job->setUrls({QUrl::fromLocalFile(srcFile)}); + + KService::Ptr service = KService::serviceByDesktopName(QString(s_tempServiceName).remove(".desktop")); + QVERIFY(service); + s_openWithHandler.m_urls.clear(); + s_openWithHandler.m_mimeTypes.clear(); + s_openWithHandler.m_chosenService = handlerRetVal ? service : KService::Ptr{}; + KIO::setDefaultOpenWithHandler(&s_openWithHandler); + + const bool success = job->exec(); + + // Then --- it depends on what the user says via the handler + + QCOMPARE(s_openWithHandler.m_urls.count(), 1); + QCOMPARE(s_openWithHandler.m_mimeTypes.count(), 1); + QCOMPARE(s_openWithHandler.m_mimeTypes.at(0), QString()); // the job doesn't have the information + if (handlerRetVal) { + QVERIFY2(success, qPrintable(job->errorString())); + // If the user chose a service, it should be executed (it writes to "dest") + const QString dest = srcDir + "/dest_file.txt"; + QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); + } else { + QVERIFY(!success); + QCOMPARE(job->error(), KIO::ERR_USER_CANCELED); + } +#else + QSKIP("Test skipped on Windows because the code ends up in QDesktopServices::openUrl") +#endif +} + void ApplicationLauncherJobTest::writeTempServiceDesktopFile(const QString &filePath) { if (!QFile::exists(filePath)) { KDesktopFile file(filePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "Service"); #ifdef Q_OS_WIN group.writeEntry("Exec", "copy.exe %f %d/dest_%n"); #else group.writeEntry("Exec", "cd %d ; cp %f %d/dest_%n"); // cd is just to show that we can't do QFile::exists(binary) #endif file.sync(); } } QString ApplicationLauncherJobTest::createTempService() { const QString fileName = s_tempServiceName; const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + fileName; writeTempServiceDesktopFile(fakeService); m_filesToRemove.append(fakeService); return fakeService; } diff --git a/autotests/applicationlauncherjobtest.h b/autotests/applicationlauncherjobtest.h index 17a105cf..54b7b8f5 100644 --- a/autotests/applicationlauncherjobtest.h +++ b/autotests/applicationlauncherjobtest.h @@ -1,58 +1,61 @@ /* This file is part of the KDE libraries Copyright (c) 2014, 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef APPLICATIONLAUNCHERJOBTEST_H #define APPLICATIONLAUNCHERJOBTEST_H #include #include class ApplicationLauncherJobTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void startProcess_data(); void startProcess(); void shouldFailOnNonExecutableDesktopFile_data(); void shouldFailOnNonExecutableDesktopFile(); void shouldFailOnNonExistingExecutable_data(); void shouldFailOnNonExistingExecutable(); void shouldFailOnInvalidService(); void shouldFailOnServiceWithNoExec(); void shouldFailOnExecutableWithoutPermissions(); + void showOpenWithDialog_data(); + void showOpenWithDialog(); + private: QString createTempService(); void writeTempServiceDesktopFile(const QString &filePath); QStringList m_filesToRemove; }; #endif /* APPLICATIONLAUNCHERJOBTEST_H */ diff --git a/src/gui/applicationlauncherjob.cpp b/src/gui/applicationlauncherjob.cpp index 7be2b4f5..22e8f12a 100644 --- a/src/gui/applicationlauncherjob.cpp +++ b/src/gui/applicationlauncherjob.cpp @@ -1,239 +1,283 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "applicationlauncherjob.h" #include "kprocessrunner_p.h" #include "untrustedprogramhandlerinterface.h" #include "kiogui_debug.h" +#include "openwithhandlerinterface.h" #include "../core/global.h" #include #include #include #include // KF6 TODO: Remove static KIO::UntrustedProgramHandlerInterface *s_untrustedProgramHandler = nullptr; + +extern KIO::OpenWithHandlerInterface *s_openWithHandler; // defined in openurljob.cpp + namespace KIO { // Hidden API because in KF6 we'll just check if the job's uiDelegate implements UntrustedProgramHandlerInterface. KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface) { s_untrustedProgramHandler = iface; } // For OpenUrlJob KIO::UntrustedProgramHandlerInterface *defaultUntrustedProgramHandler() { return s_untrustedProgramHandler; } } #include class KIO::ApplicationLauncherJobPrivate { public: - explicit ApplicationLauncherJobPrivate(const KService::Ptr &service) - : m_service(service) {} + explicit ApplicationLauncherJobPrivate(KIO::ApplicationLauncherJob *job, const KService::Ptr &service) + : m_service(service), q(job) {} - void slotStarted(KIO::ApplicationLauncherJob *q, KProcessRunner *processRunner) { + void slotStarted(KProcessRunner *processRunner) { m_pids.append(processRunner->pid()); if (--m_numProcessesPending == 0) { q->emitResult(); } } + + void showOpenWithDialog(); + KService::Ptr m_service; QList m_urls; KIO::ApplicationLauncherJob::RunFlags m_runFlags; QString m_suggestedFileName; QByteArray m_startupId; QVector m_pids; QVector m_processRunners; int m_numProcessesPending = 0; + KIO::ApplicationLauncherJob *q; }; KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KService::Ptr &service, QObject *parent) - : KJob(parent), d(new ApplicationLauncherJobPrivate(service)) + : KJob(parent), d(new ApplicationLauncherJobPrivate(this, service)) { } 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(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 &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->showOpenWithDialog(); + return; + } 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_service->entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(d->m_service->entryPath())) { // 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(); } if (!s_untrustedProgramHandler) { emitUnauthorizedError(); return; } connect(s_untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [this](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 (s_untrustedProgramHandler->makeServiceFileExecutable(d->m_service->entryPath(), 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(); } }); s_untrustedProgramHandler->showUntrustedProgramWarning(this, d->m_service->name()); return; } proceedAfterSecurityChecks(); } void KIO::ApplicationLauncherJob::proceedAfterSecurityChecks() { if (d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles()) { // 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 = new KProcessRunner(d->m_service, { d->m_urls.at(i) }, d->m_runFlags, d->m_suggestedFileName, QByteArray()); d->m_processRunners.push_back(processRunner); connect(processRunner, &KProcessRunner::processStarted, this, [this, processRunner]() { - d->slotStarted(this, processRunner); + d->slotStarted(processRunner); }); } d->m_urls = { d->m_urls.at(0) }; } else { d->m_numProcessesPending = 1; } auto *processRunner = new KProcessRunner(d->m_service, 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, processRunner]() { - d->slotStarted(this, processRunner); + d->slotStarted(processRunner); }); } // 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(), [](KProcessRunner *r) { return r->waitForStarted(); }); for (KProcessRunner *r : qAsConst(d->m_processRunners)) { qApp->sendPostedEvents(r); // so slotStarted gets called } return ret; } qint64 KIO::ApplicationLauncherJob::pid() const { return d->m_pids.at(0); } QVector KIO::ApplicationLauncherJob::pids() const { return d->m_pids; } + +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; + } + + QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() { + q->setError(KIO::ERR_USER_CANCELED); + q->emitResult(); + }); + + QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) { + Q_ASSERT(service); + m_service = service; + q->start(); + }); + + QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() { + q->emitResult(); + }); + + s_openWithHandler->promptUserForApplication(q, m_urls, QString() /* mimetype name unknown */); +} diff --git a/src/gui/applicationlauncherjob.h b/src/gui/applicationlauncherjob.h index 9964ed37..c3d5f3ba 100644 --- a/src/gui/applicationlauncherjob.h +++ b/src/gui/applicationlauncherjob.h @@ -1,168 +1,174 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIO_APPLICATIONLAUNCHERJOB_H #define KIO_APPLICATIONLAUNCHERJOB_H #include "kiogui_export.h" #include #include #include class KRun; // KF6 REMOVE class ApplicationLauncherJobTest; // KF6 REMOVE namespace KIO { class ApplicationLauncherJobPrivate; /** * @class ApplicationLauncherJob applicationlauncherjob.h * * @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 do * @code * 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 those files. * * @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); + /** + * Creates an ApplicationLauncherJob which will prompt the user for which application to use (via the "open with" dialog). + * @param parent the parent QObject + */ + 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 &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 mimetype 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 startup notification id of the application launch. * @param startupId startup notification id, if any (otherwise ""). */ 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(). */ QVector pids() const; private: friend class ::KRun; // KF6 REMOVE friend class ::ApplicationLauncherJobTest; // KF6 REMOVE /** * Blocks until the process has started. Only exists for KRun, will disappear in KF6. */ bool waitForStarted(); void emitUnauthorizedError(); void proceedAfterSecurityChecks(); friend class ApplicationLauncherJobPrivate; QScopedPointer d; }; } // namespace KIO #endif diff --git a/src/gui/openurljob.cpp b/src/gui/openurljob.cpp index b7bdfa9c..93818d15 100644 --- a/src/gui/openurljob.cpp +++ b/src/gui/openurljob.cpp @@ -1,651 +1,655 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "openurljob.h" #include "openwithhandlerinterface.h" #include "global.h" #include "job.h" // for buildErrorString #include "commandlauncherjob.h" #include "desktopexecparser.h" #include "untrustedprogramhandlerinterface.h" #include "kiogui_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -static KIO::OpenWithHandlerInterface *s_openWithHandler = nullptr; +KIO::OpenWithHandlerInterface *s_openWithHandler = nullptr; namespace KIO { // Hidden API because in KF6 we'll just check if the job's uiDelegate implements OpenWithHandlerInterface. KIOGUI_EXPORT void setDefaultOpenWithHandler(KIO::OpenWithHandlerInterface *iface) { s_openWithHandler = iface; } } 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(); void determineLocalMimeType(); void statFile(); void scanFileWithGet(); 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_externalBrowserEnabled = true; bool m_followRedirections = true; private: void executeCommand(); bool handleExecutables(const QMimeType &mimeType); void runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName); void showOpenWithDialog(); void startService(const KService::Ptr &service, const QList &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::setEnableExternalBrowser(bool b) { d->m_externalBrowserEnabled = b; } void KIO::OpenUrlJob::setFollowRedirections(bool b) { d->m_followRedirections = b; } static bool checkNeedPortalSupport() { return !(QStandardPaths::locate(QStandardPaths::RuntimeLocation, QLatin1String("flatpak-info")).isEmpty() || qEnvironmentVariableIsSet("SNAP")); } 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; } if (d->m_externalBrowserEnabled && checkNeedPortalSupport()) { // 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. 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(); return; } // If we know the mimetype, proceed if (!d->m_mimeTypeName.isEmpty()) { d->runUrlWithMimeType(); return; } if (d->m_externalBrowserEnabled && d->m_url.scheme().startsWith(QLatin1String("http"))) { const QString externalBrowser = d->externalBrowser(); if (!externalBrowser.isEmpty() && d->runExternalBrowser(externalBrowser)) { return; } } if (KIO::DesktopExecParser::hasSchemeHandler(d->m_url)) { d->useSchemeHandler(); return; } if (!KProtocolManager::supportsListing(d->m_url)) { // No support for listing => it can't be a directory (example: http) d->scanFileWithGet(); return; } // It may be a directory or a file, let's use stat to find out d->statFile(); } bool KIO::OpenUrlJob::doKill() { return true; } QString KIO::OpenUrlJobPrivate::externalBrowser() const { if (!m_externalBrowserEnabled) { return QString(); } const QString browserApp = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("BrowserApplication"); if (!browserApp.isEmpty()) { return browserApp; } // If a default browser isn't set in kdeglobals, fall back to mimeapps.list KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); const KConfigGroup defaultApps(profile, "Default Applications"); QString externalBrowser = defaultApps.readEntry("x-scheme-handler/https"); if (externalBrowser.isEmpty()) { externalBrowser = defaultApps.readEntry("x-scheme-handler/http"); } return externalBrowser; } bool KIO::OpenUrlJobPrivate::runExternalBrowser(const QString &exec) { if (exec.startsWith(QLatin1Char('!'))) { // Literal command const QString command = exec.midRef(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/ 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 mimetype opener for file m_mimeTypeName = KProtocolManager::defaultMimetype(m_url); runUrlWithMimeType(); } else { KService::Ptr service(new KService(QString(), exec, QString())); startService(service); } } void KIO::OpenUrlJobPrivate::statFile() { Q_ASSERT(m_mimeTypeName.isEmpty()); KIO::StatJob *job = KIO::statDetails(m_url, KIO::StatJob::SourceSide, KIO::StatBasic, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QObject::connect(job, &KJob::result, q, [=]() { const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { q->setError(errCode); q->setErrorText(KIO::buildErrorString(errCode, job->errorText())); } q->emitResult(); return; } if (m_followRedirections) { // Update our URL in case of a redirection m_url = job->url(); } const KIO::UDSEntry entry = job->statResult(); const QString localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); if (!localPath.isEmpty()) { m_url = QUrl::fromLocalFile(localPath); } // mimetype already known? (e.g. print:/manager) m_mimeTypeName = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); if (!m_mimeTypeName.isEmpty()) { runUrlWithMimeType(); return; } if (entry.isDir()) { m_mimeTypeName = QStringLiteral("inode/directory"); runUrlWithMimeType(); } else { // it's a file // Start the timer. Once we get the timer event this // protocol server is back in the pool and we can reuse it. // This gives better performance than starting a new slave QTimer::singleShot(0, q, [this] { scanFileWithGet(); }); } }); } void KIO::OpenUrlJobPrivate::startService(const KService::Ptr &service, const QList &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); job->setUiDelegate(q->uiDelegate()); q->addSubjob(job); job->start(); } static QMimeType fixupMimeType(const QString &mimeType, const QString &fileName) { QMimeDatabase db; QMimeType mime = db.mimeTypeForName(mimeType); if ((!mime.isValid() || mime.isDefault()) && !fileName.isEmpty()) { mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchExtension); } return mime; } void KIO::OpenUrlJobPrivate::scanFileWithGet() { Q_ASSERT(m_mimeTypeName.isEmpty()); // First, let's check for well-known extensions // Not over HTTP and not when there is a query in the URL, in any case. if (!m_url.hasQuery() && !m_url.scheme().startsWith(QLatin1String("http"))) { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(m_url); if (!mime.isDefault()) { //qDebug() << "Scanfile: MIME TYPE is " << mime.name(); m_mimeTypeName = mime.name(); runUrlWithMimeType(); return; } } // No mimetype found, and the URL is not local (or fast mode not allowed). // We need to apply the 'KIO' method, i.e. either asking the server or // getting some data out of the file, to know what mimetype it is. if (!KProtocolManager::supportsReading(m_url)) { qCWarning(KIO_GUI) << "#### NO SUPPORT FOR READING!"; q->setError(KIO::ERR_CANNOT_READ); q->setErrorText(m_url.toDisplayString()); q->emitResult(); return; } //qDebug() << this << "Scanning file" << url; KIO::TransferJob *job = KIO::get(m_url, KIO::NoReload /*reload*/, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QObject::connect(job, &KJob::result, q, [=]() { const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { q->setError(errCode); q->setErrorText(job->errorText()); } q->emitResult(); } // if the job succeeded, we certainly hope it emitted mimetype()... }); QObject::connect(job, QOverload::of(&KIO::TransferJob::mimetype), q, [=](KIO::Job *, const QString &mimetype) { if (m_followRedirections) { // Update our URL in case of a redirection m_url = job->url(); } if (mimetype.isEmpty()) { qCWarning(KIO_GUI) << "get() didn't emit a mimetype! Probably a kioslave bug, please check the implementation of" << m_url.scheme(); } m_mimeTypeName = mimetype; // If the current mime-type is the default mime-type, then attempt to // determine the "real" mimetype from the file name (bug #279675) const QMimeType mime = fixupMimeType(m_mimeTypeName, m_suggestedFileName.isEmpty() ? m_url.fileName() : m_suggestedFileName); const QString mimeName = mime.name(); if (mime.isValid() && mimeName != m_mimeTypeName) { m_mimeTypeName = mimeName; } if (m_suggestedFileName.isEmpty()) { m_suggestedFileName = job->queryMetaData(QStringLiteral("content-disposition-filename")); } job->putOnHold(); KIO::Scheduler::publishSlaveOnHold(); runUrlWithMimeType(); }); } 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. Feel free to make public if needed. static bool isExecutableMime(const QMimeType &mimeType) { return (mimeType.inherits(QLatin1String("application/x-desktop")) || mimeType.inherits(QLatin1String("application/x-executable")) || /* See https://bugs.freedesktop.org/show_bug.cgi?id=97226 */ mimeType.inherits(QLatin1String("application/x-sharedlib")) || mimeType.inherits(QLatin1String("application/x-ms-dos-executable")) || mimeType.inherits(QLatin1String("application/x-shellscript"))); } // Helper function that returns whether a file has the execute bit set or not. static bool hasExecuteBit(const QString &fileName) { return QFileInfo(fileName).isExecutable(); } namespace KIO { extern KIO::UntrustedProgramHandlerInterface *defaultUntrustedProgramHandler(); } // Return true if handled in any way (success or error) // Return false if the caller should proceed bool KIO::OpenUrlJobPrivate::handleExecutables(const QMimeType &mimeType) { if (!KAuthorized::authorize(QStringLiteral("shell_access"))) { emitAccessDenied(); return true; // handled } // Check whether file is an executable script #ifdef Q_OS_WIN const bool isNativeBinary = !mimeType.inherits(QStringLiteral("text/plain")); #else const bool isNativeBinary = !mimeType.inherits(QStringLiteral("text/plain")) && !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable")); #endif if (!m_url.isLocalFile() || !m_runExecutables) { if (isNativeBinary) { // Show warning for executables that aren't scripts q->setError(KJob::UserDefinedError); q->setErrorText(i18n("The file \"%1\" is an executable program. " "For safety it will not be started.", m_url.toDisplayString())); q->emitResult(); return true; // handled } // Let scripts be open as text files, if remote, or no exec allowed return false; } const QString localPath = m_url.toLocalFile(); // For executables that aren't scripts and without execute bit, // show prompt asking user if he wants to run the program. if (!hasExecuteBit(localPath)) { if (!isNativeBinary) { // Don't try to run scripts/exes without execute bit, instead // open them with default application return false; } KIO::UntrustedProgramHandlerInterface *untrustedProgramHandler = defaultUntrustedProgramHandler(); 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.", localPath)); q->emitResult(); return true; } QObject::connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, q, [=](bool result) { if (result) { QString errorString; if (untrustedProgramHandler->setExecuteBit(localPath, errorString)) { executeCommand(); } else { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Unable to make file \"%1\" executable.\n%2.", localPath, errorString)); q->emitResult(); } } else { q->setError(KIO::ERR_USER_CANCELED); q->emitResult(); } }); untrustedProgramHandler->showUntrustedProgramWarning(q, m_url.fileName()); return true; } // Local executable with execute bit, proceed executeCommand(); return true; // handled } 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()); job->setUiDelegate(q->uiDelegate()); 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; } // Local desktop file if (m_url.isLocalFile() && m_mimeTypeName == QLatin1String("application/x-desktop")) { if (m_url.fileName() == QLatin1String(".directory")) { // We cannot execute a .directory file. Open with a text editor instead. m_mimeTypeName = QStringLiteral("text/plain"); } else { 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(); return; } if ((cfg.hasApplicationType() || cfg.readType() == QLatin1String("Service")) // for kio_settings && !cfgGroup.readEntry("Exec").isEmpty() && m_runExecutables) { KService::Ptr service(new KService(filePath)); startService(service, {}); return; } else if (cfg.hasLinkType()) { runLink(filePath, cfg.readUrl(), cfg.desktopGroup().readEntry("X-KDE-LastOpenedWith")); return; } } } // Scripts and executables QMimeDatabase db; const QMimeType mimeType = db.mimeTypeForName(m_mimeTypeName); if (isExecutableMime(mimeType)) { if (handleExecutables(mimeType)) { return; } } // General case: look up associated application KService::Ptr service = KApplicationTrader::preferredService(m_mimeTypeName); if (service) { startService(service); } else { 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; } if (!s_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(s_openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() { q->setError(KIO::ERR_USER_CANCELED); q->emitResult(); }); QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) { startService(service); }); + QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() { + q->emitResult(); + }); + s_openWithHandler->promptUserForApplication(q, {m_url}, 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); setErrorText(KIO::buildErrorString(errCode, job->errorText())); } emitResult(); } diff --git a/src/gui/openwithhandlerinterface.cpp b/src/gui/openwithhandlerinterface.cpp index 64fdd600..d7bbd174 100644 --- a/src/gui/openwithhandlerinterface.cpp +++ b/src/gui/openwithhandlerinterface.cpp @@ -1,41 +1,41 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "openwithhandlerinterface.h" #include #include #include "kiocoredebug.h" using namespace KIO; class KIO::OpenWithHandlerInterfacePrivate {}; OpenWithHandlerInterface::OpenWithHandlerInterface() = default; OpenWithHandlerInterface::~OpenWithHandlerInterface() = default; -void OpenWithHandlerInterface::promptUserForApplication(OpenUrlJob *job, const QList &urls, const QString &mimeType) +void OpenWithHandlerInterface::promptUserForApplication(KJob *job, const QList &urls, const QString &mimeType) { Q_UNUSED(job) Q_UNUSED(urls) Q_UNUSED(mimeType) Q_EMIT canceled(); } diff --git a/src/gui/openwithhandlerinterface.h b/src/gui/openwithhandlerinterface.h index d56d3c36..d8acca24 100644 --- a/src/gui/openwithhandlerinterface.h +++ b/src/gui/openwithhandlerinterface.h @@ -1,91 +1,98 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef OPENWITHHANDLERINTERFACE_H #define OPENWITHHANDLERINTERFACE_H #include #include #include class QString; +class KJob; + namespace KIO { -class OpenUrlJob; class OpenWithHandlerInterfacePrivate; /** * @class OpenWithHandlerInterface openwithhandlerinterface.h * @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 */ OpenWithHandlerInterface(); /** * 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 mimeType 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(KIO::OpenUrlJob *job, const QList &urls, const QString &mimeType); + virtual void promptUserForApplication(KJob *job, const QList &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 d; }; } #endif // OPENWITHHANDLERINTERFACE_H diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index ee013a89..6c988b9e 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,227 +1,223 @@ project(KIOWidgets) #include (ConfigureChecks.cmake) find_package(ACL) set(HAVE_LIBACL ${ACL_FOUND}) set(HAVE_POSIX_ACL ${ACL_FOUND}) set_package_properties(ACL PROPERTIES DESCRIPTION "LibACL" URL "ftp://oss.sgi.com/projects/xfs/cmd_tars" TYPE RECOMMENDED PURPOSE "Support for manipulating access control lists") configure_file(config-kiowidgets.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kiowidgets.h) set(kiowidgets_SRCS accessmanager.cpp accessmanagerreply_p.cpp fileundomanager.cpp kacleditwidget.cpp kpropertiesdialog.cpp kurlrequesterdialog.cpp kurlcombobox.cpp kfileitemactions.cpp delegateanimationhandler.cpp imagefilter.cpp kfileitemdelegate.cpp kdesktopfileactions.cpp kopenwithdialog.cpp kfile.cpp pastedialog.cpp paste.cpp clipboardupdater.cpp kabstractfileitemactionplugin.cpp koverlayiconplugin.cpp kbuildsycocaprogressdialog.cpp kurlrequester.cpp krun.cpp sslui.cpp kurlpixmapprovider.cpp pixmaploader.cpp thumbsequencecreator.cpp thumbcreator.cpp kshellcompletion.cpp kurlcompletion.cpp kurifilter.cpp dropjob.cpp openfilemanagerwindowjob.cpp pastejob.cpp previewjob.cpp renamedialog.cpp ksslcertificatebox.cpp kdynamicjobtracker.cpp ksslinfodialog.cpp joburlcache.cpp skipdialog.cpp jobuidelegate.cpp kdirlister.cpp kdirmodel.cpp executablefileopendialog.cpp dndpopupmenuplugin.cpp kurifiltersearchprovideractions.cpp renamefiledialog.cpp widgetsuntrustedprogramhandler.cpp widgetsopenwithhandler.cpp ) -if (WIN32) - list(APPEND kiowidgets_SRCS - krun_win.cpp - ) -else() +if (NOT WIN32) list(APPEND kiowidgets_SRCS kautomount.cpp ) endif() ecm_qt_declare_logging_category(kiowidgets_SRCS HEADER kio_widgets_debug.h IDENTIFIER KIO_WIDGETS CATEGORY_NAME kf5.kio.widgets DESCRIPTION "KIOWidgets (KIO)" EXPORT KIO ) ecm_qt_export_logging_category( IDENTIFIER category CATEGORY_NAME kf5.kio.kdirmodel DESCRIPTION "KDirModel (KIO)" EXPORT KIO ) qt5_add_dbus_adaptor(kiowidgets_SRCS org.kde.kio.FileUndoManager.xml fileundomanager_p.h KIO::FileUndoManagerPrivate fileundomanager_adaptor KIOFileUndoManagerAdaptor) qt5_add_dbus_interface(kiowidgets_SRCS org.kde.kuiserver.xml kuiserver_interface) ki18n_wrap_ui(kiowidgets_SRCS checksumswidget.ui certificateparty.ui sslinfo.ui kpropertiesdesktopadvbase.ui kpropertiesdesktopbase.ui ) add_library(KF5KIOWidgets ${kiowidgets_SRCS}) add_library(KF5::KIOWidgets ALIAS KF5KIOWidgets) ecm_generate_export_header(KF5KIOWidgets BASE_NAME KIOWidgets GROUP_BASE_NAME KF VERSION ${KF5_VERSION} DEPRECATED_BASE_VERSION 0 DEPRECATION_VERSIONS 4.0 4.1 4.3 4.4 4.5 4.6 4.7 5.0 5.4 5.6 5.25 5.31 5.32 5.64 5.66 5.71 EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT} ) # TODO: add support for EXCLUDE_DEPRECATED_BEFORE_AND_AT to all KIO libs # needs fixing of undeprecated API being still implemented using own deprecated API target_include_directories(KF5KIOWidgets INTERFACE "$") target_link_libraries(KF5KIOWidgets PUBLIC KF5::KIOGui KF5::KIOCore KF5::JobWidgets KF5::Service Qt5::Network # SSL KF5::Completion # KUrlCompletion uses KCompletion KF5::WidgetsAddons # keditlistwidget PRIVATE Qt5::Concurrent Qt5::DBus KF5::I18n KF5::IconThemes # KIconLoader KF5::WindowSystem # KStartupInfo KF5::ConfigWidgets # KColorScheme ) if(ACL_FOUND) target_link_libraries(KF5KIOWidgets PRIVATE ${ACL_LIBS}) endif() set_target_properties(KF5KIOWidgets PROPERTIES VERSION ${KIO_VERSION_STRING} SOVERSION ${KIO_SOVERSION} EXPORT_NAME KIOWidgets ) # Headers not prefixed with KIO/ ecm_generate_headers(KIOWidgets_HEADERS HEADER_NAMES KPropertiesDialog KUrlRequesterDialog KUrlComboBox KFileItemActions KFileItemDelegate KAutoMount KDesktopFileActions KOpenWithDialog KAbstractFileItemActionPlugin KOverlayIconPlugin KBuildSycocaProgressDialog KFile KUrlRequester KRun KUrlPixmapProvider KSslCertificateBox KSslInfoDialog KDirLister KDirModel KShellCompletion KUrlCompletion KUriFilter REQUIRED_HEADERS KIOWidgets_HEADERS ) # Headers prefixed with KIO/ ecm_generate_headers(KIOWidgets_CamelCase_HEADERS HEADER_NAMES AccessManager SslUi ThumbSequenceCreator ThumbCreator DropJob DndPopupMenuPlugin OpenFileManagerWindowJob PasteJob PreviewJob RenameDialog SkipDialog JobUiDelegate FileUndoManager Paste PixmapLoader KUriFilterSearchProviderActions # KF6: fix and move to non-KIO prefixed install folder RenameFileDialog PREFIX KIO REQUIRED_HEADERS KIO_namespaced_widgets_HEADERS ) install(FILES ${KIOWidgets_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets/KIO COMPONENT Devel) install(TARGETS KF5KIOWidgets EXPORT KF5KIOTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES org.kde.kio.FileUndoManager.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} RENAME kf5_org.kde.kio.FileUndoManager.xml) install(FILES ${KIO_namespaced_widgets_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets/kio COMPONENT Devel) install(FILES ${KIOWidgets_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kiowidgets_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets COMPONENT Devel) install(FILES kfileitemactionplugin.desktop kpropertiesdialogplugin.desktop kurifilterplugin.desktop konqpopupmenuplugin.desktop kiodndpopupmenuplugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR} ) # make available to ecm_add_qch in parent folder set(KIOWidgets_QCH_SOURCES ${KIOWidgets_HEADERS} ${KIO_namespaced_widgets_HEADERS} PARENT_SCOPE) include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KIOWidgets LIB_NAME KF5KIOWidgets DEPS "KIOGui KIOCore KBookmarks KXmlGui Solid" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/widgets/kfileitemactions.cpp b/src/widgets/kfileitemactions.cpp index 259ca771..74569e75 100644 --- a/src/widgets/kfileitemactions.cpp +++ b/src/widgets/kfileitemactions.cpp @@ -1,836 +1,837 @@ /* This file is part of the KDE project Copyright (C) 1998-2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfileitemactions.h" #include "kfileitemactions_p.h" -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool KIOSKAuthorizedAction(const KConfigGroup &cfg) { if (!cfg.hasKey("X-KDE-AuthorizeAction")) { return true; } const QStringList list = cfg.readEntry("X-KDE-AuthorizeAction", QStringList()); for (QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!KAuthorized::authorize((*it).trimmed())) { return false; } } return true; } static bool mimeTypeListContains(const QStringList &list, const KFileItem &item) { const QString itemMimeType = item.mimetype(); for (const QString &i : list) { if (i == itemMimeType || i == QLatin1String("all/all")) { return true; } if (item.isFile() && (i == QLatin1String("allfiles") || i == QLatin1String("all/allfiles") || i == QLatin1String("application/octet-stream"))) { return true; } if (item.currentMimeType().inherits(i)) { return true; } const int iSlashPos = i.indexOf(QLatin1Char(QLatin1Char('/'))); Q_ASSERT(iSlashPos > 0); const QStringRef iSubType = i.midRef(iSlashPos+1); if (iSubType == QLatin1String("*")) { const int itemSlashPos = itemMimeType.indexOf(QLatin1Char('/')); Q_ASSERT(itemSlashPos > 0); const QStringRef iTopLevelType = i.midRef(0, iSlashPos); const QStringRef itemTopLevelType = itemMimeType.midRef(0, itemSlashPos); if (itemTopLevelType == iTopLevelType) { return true; } } } return false; } // This helper class stores the .desktop-file actions and the servicemenus // in order to support X-KDE-Priority and X-KDE-Submenu. namespace KIO { class PopupServices { public: ServiceList &selectList(const QString &priority, const QString &submenuName); ServiceList builtin; ServiceList user, userToplevel, userPriority; QMap userSubmenus, userToplevelSubmenus, userPrioritySubmenus; }; ServiceList &PopupServices::selectList(const QString &priority, const QString &submenuName) { // we use the categories .desktop entry to define submenus // if none is defined, we just pop it in the main menu if (submenuName.isEmpty()) { if (priority == QLatin1String("TopLevel")) { return userToplevel; } else if (priority == QLatin1String("Important")) { return userPriority; } } else if (priority == QLatin1String("TopLevel")) { return userToplevelSubmenus[submenuName]; } else if (priority == QLatin1String("Important")) { return userPrioritySubmenus[submenuName]; } else { return userSubmenus[submenuName]; } return user; } } // namespace //// KFileItemActionsPrivate::KFileItemActionsPrivate(KFileItemActions *qq) : QObject(), q(qq), m_executeServiceActionGroup(static_cast(nullptr)), m_runApplicationActionGroup(static_cast(nullptr)), m_parentWidget(nullptr), m_config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals) { QObject::connect(&m_executeServiceActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotExecuteService); QObject::connect(&m_runApplicationActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotRunApplication); } KFileItemActionsPrivate::~KFileItemActionsPrivate() { } int KFileItemActionsPrivate::insertServicesSubmenus(const QMap &submenus, QMenu *menu, bool isBuiltin) { int count = 0; QMap::ConstIterator it; for (it = submenus.begin(); it != submenus.end(); ++it) { if (it.value().isEmpty()) { //avoid empty sub-menus continue; } QMenu *actionSubmenu = new QMenu(menu); actionSubmenu->setTitle(it.key()); actionSubmenu->setIcon(QIcon::fromTheme(it.value().first().icon())); actionSubmenu->menuAction()->setObjectName(QStringLiteral("services_submenu")); // for the unittest menu->addMenu(actionSubmenu); count += insertServices(it.value(), actionSubmenu, isBuiltin); } return count; } int KFileItemActionsPrivate::insertServices(const ServiceList &list, QMenu *menu, bool isBuiltin) { int count = 0; for (const KServiceAction &serviceAction : list) { if (serviceAction.isSeparator()) { const QList actions = menu->actions(); if (!actions.isEmpty() && !actions.last()->isSeparator()) { menu->addSeparator(); } continue; } if (isBuiltin || !serviceAction.noDisplay()) { QAction *act = new QAction(q); act->setObjectName(QStringLiteral("menuaction")); // for the unittest QString text = serviceAction.text(); text.replace(QLatin1Char('&'), QLatin1String("&&")); act->setText(text); if (!serviceAction.icon().isEmpty()) { act->setIcon(QIcon::fromTheme(serviceAction.icon())); } act->setData(QVariant::fromValue(serviceAction)); m_executeServiceActionGroup.addAction(act); menu->addAction(act); // Add to toplevel menu ++count; } } return count; } void KFileItemActionsPrivate::slotExecuteService(QAction *act) { KServiceAction serviceAction = act->data().value(); if (KAuthorized::authorizeAction(serviceAction.name())) { KDesktopFileActions::executeService(m_props.urlList(), serviceAction); } } //// KFileItemActions::KFileItemActions(QObject *parent) : QObject(parent), d(new KFileItemActionsPrivate(this)) { } KFileItemActions::~KFileItemActions() { delete d; } void KFileItemActions::setItemListProperties(const KFileItemListProperties &itemListProperties) { d->m_props = itemListProperties; d->m_mimeTypeList.clear(); const KFileItemList items = d->m_props.items(); KFileItemList::const_iterator kit = items.constBegin(); const KFileItemList::const_iterator kend = items.constEnd(); for (; kit != kend; ++kit) { if (!d->m_mimeTypeList.contains((*kit).mimetype())) { d->m_mimeTypeList << (*kit).mimetype(); } } } int KFileItemActions::addServiceActionsTo(QMenu *mainMenu) { const KFileItemList items = d->m_props.items(); const KFileItem &firstItem = items.first(); const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items const bool isLocal = !firstItem.localPath().isEmpty(); const bool isSingleLocal = items.count() == 1 && isLocal; const QList urlList = d->m_props.urlList(); KIO::PopupServices s; // 1 - Look for builtin and user-defined services if (isSingleLocal && d->m_props.mimeType() == QLatin1String("application/x-desktop")) { // get builtin services, like mount/unmount const QString path = firstItem.localPath(); s.builtin = KDesktopFileActions::builtinServices(QUrl::fromLocalFile(path)); KDesktopFile desktopFile(path); KConfigGroup cfg = desktopFile.desktopGroup(); const QString priority = cfg.readEntry("X-KDE-Priority"); const QString submenuName = cfg.readEntry("X-KDE-Submenu"); #if 0 if (cfg.readEntry("Type") == "Link") { d->m_url = cfg.readEntry("URL"); // TODO: Do we want to make all the actions apply on the target // of the .desktop file instead of the .desktop file itself? } #endif ServiceList &list = s.selectList(priority, submenuName); list = KDesktopFileActions::userDefinedServices(path, desktopFile, true /*isLocal*/); } // 2 - Look for "servicemenus" bindings (user-defined services) // first check the .directory if this is a directory if (d->m_props.isDirectory() && isSingleLocal) { QString dotDirectoryFile = QUrl::fromLocalFile(firstItem.localPath()).path().append(QLatin1String("/.directory")); if (QFile::exists(dotDirectoryFile)) { const KDesktopFile desktopFile(dotDirectoryFile); const KConfigGroup cfg = desktopFile.desktopGroup(); if (KIOSKAuthorizedAction(cfg)) { const QString priority = cfg.readEntry("X-KDE-Priority"); const QString submenuName = cfg.readEntry("X-KDE-Submenu"); ServiceList &list = s.selectList(priority, submenuName); list += KDesktopFileActions::userDefinedServices(dotDirectoryFile, desktopFile, true); } } } const KConfigGroup showGroup = d->m_config.group("Show"); const QMimeDatabase db; const KService::List entries = KServiceTypeTrader::self()->query(QStringLiteral("KonqPopupMenu/Plugin")); KService::List::const_iterator eEnd = entries.end(); for (KService::List::const_iterator it2 = entries.begin(); it2 != eEnd; ++it2) { QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + (*it2)->entryPath()); KDesktopFile desktopFile(file); const KConfigGroup cfg = desktopFile.desktopGroup(); if (!KIOSKAuthorizedAction(cfg)) { continue; } if (cfg.hasKey("X-KDE-ShowIfRunning")) { const QString app = cfg.readEntry("X-KDE-ShowIfRunning"); if (QDBusConnection::sessionBus().interface()->isServiceRegistered(app)) { continue; } } if (cfg.hasKey("X-KDE-ShowIfDBusCall")) { QString calldata = cfg.readEntry("X-KDE-ShowIfDBusCall"); const QStringList parts = calldata.split(QLatin1Char(' ')); const QString &app = parts.at(0); const QString &obj = parts.at(1); QString interface = parts.at(2); QString method; int pos = interface.lastIndexOf(QLatin1Char('.')); if (pos != -1) { method = interface.mid(pos + 1); interface.truncate(pos); } //if (!QDBus::sessionBus().busService()->nameHasOwner(app)) // continue; //app does not exist so cannot send call QDBusMessage reply = QDBusInterface(app, obj, interface). call(method, QUrl::toStringList(urlList)); if (reply.arguments().count() < 1 || reply.arguments().at(0).type() != QVariant::Bool || !reply.arguments().at(0).toBool()) { continue; } } if (cfg.hasKey("X-KDE-Protocol")) { const QString theProtocol = cfg.readEntry("X-KDE-Protocol"); if (theProtocol.startsWith(QLatin1Char('!'))) { const QStringRef excludedProtocol = theProtocol.midRef(1); if (excludedProtocol == protocol) { continue; } } else if (protocol != theProtocol) { continue; } } else if (cfg.hasKey("X-KDE-Protocols")) { const QStringList protocols = cfg.readEntry("X-KDE-Protocols", QStringList()); if (!protocols.contains(protocol)) { continue; } } else if (protocol == QLatin1String("trash")) { // Require servicemenus for the trash to ask for protocol=trash explicitly. // Trashed files aren't supposed to be available for actions. // One might want a servicemenu for trash.desktop itself though. continue; } if (cfg.hasKey("X-KDE-Require")) { const QStringList capabilities = cfg.readEntry("X-KDE-Require", QStringList()); if (capabilities.contains(QLatin1String("Write")) && !d->m_props.supportsWriting()) { continue; } } if (cfg.hasKey("X-KDE-RequiredNumberOfUrls")) { const QStringList requiredNumberOfUrls = cfg.readEntry("X-KDE-RequiredNumberOfUrls", QStringList()); bool matchesAtLeastOneCriterion = false; for (const QString &criterion : requiredNumberOfUrls) { const int number = criterion.toInt(); if (number < 1) { continue; } if (urlList.count() == number) { matchesAtLeastOneCriterion = true; break; } } if (!matchesAtLeastOneCriterion) { continue; } } if (cfg.hasKey("Actions") || cfg.hasKey("X-KDE-GetActionMenu")) { // Like KService, we support ServiceTypes, X-KDE-ServiceTypes, and MimeType. const QStringList types = cfg.readEntry("ServiceTypes", QStringList()) << cfg.readEntry("X-KDE-ServiceTypes", QStringList()) << cfg.readXdgListEntry("MimeType"); if (types.isEmpty()) { continue; } const QStringList excludeTypes = cfg.readEntry("ExcludeServiceTypes", QStringList()); const bool ok = std::all_of(items.constBegin(), items.constEnd(), [&types, &excludeTypes](const KFileItem &i) { return mimeTypeListContains(types, i) && !mimeTypeListContains(excludeTypes, i); }); if (ok) { const QString priority = cfg.readEntry("X-KDE-Priority"); const QString submenuName = cfg.readEntry("X-KDE-Submenu"); ServiceList &list = s.selectList(priority, submenuName); const ServiceList userServices = KDesktopFileActions::userDefinedServices(*(*it2), isLocal, urlList); for (const KServiceAction &action : userServices) { if (showGroup.readEntry(action.name(), true)) { list += action; } } } } } QMenu *actionMenu = mainMenu; int userItemCount = 0; if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() > 1) { // we have more than one item, so let's make a submenu actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu); actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic"))); actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest mainMenu->addMenu(actionMenu); } userItemCount += d->insertServicesSubmenus(s.userPrioritySubmenus, actionMenu, false); userItemCount += d->insertServices(s.userPriority, actionMenu, false); // see if we need to put a separator between our priority items and our regular items if (userItemCount > 0 && (s.user.count() > 0 || s.userSubmenus.count() > 0 || s.builtin.count() > 0) && !actionMenu->actions().constLast()->isSeparator()) { actionMenu->addSeparator(); } userItemCount += d->insertServicesSubmenus(s.userSubmenus, actionMenu, false); userItemCount += d->insertServices(s.user, actionMenu, false); userItemCount += d->insertServices(s.builtin, mainMenu, true); userItemCount += d->insertServicesSubmenus(s.userToplevelSubmenus, mainMenu, false); userItemCount += d->insertServices(s.userToplevel, mainMenu, false); return userItemCount; } int KFileItemActions::addPluginActionsTo(QMenu *mainMenu) { QString commonMimeType = d->m_props.mimeType(); if (commonMimeType.isEmpty() && d->m_props.isFile()) { commonMimeType = QStringLiteral("application/octet-stream"); } QStringList addedPlugins; int itemCount = 0; const KConfigGroup showGroup = d->m_config.group("Show"); const KService::List fileItemPlugins = KMimeTypeTrader::self()->query(commonMimeType, QStringLiteral("KFileItemAction/Plugin"), QStringLiteral("exist Library")); for(const auto &service : fileItemPlugins) { if (!showGroup.readEntry(service->desktopEntryName(), true)) { // The plugin has been disabled continue; } KAbstractFileItemActionPlugin *abstractPlugin = service->createInstance(); if (abstractPlugin) { abstractPlugin->setParent(mainMenu); auto actions = abstractPlugin->actions(d->m_props, d->m_parentWidget); itemCount += actions.count(); mainMenu->addActions(actions); addedPlugins.append(service->desktopEntryName()); } } const QMimeDatabase db; const auto jsonPlugins = KPluginLoader::findPlugins(QStringLiteral("kf5/kfileitemaction"), [&db, commonMimeType](const KPluginMetaData& metaData) { if (!metaData.serviceTypes().contains(QLatin1String("KFileItemAction/Plugin"))) { return false; } auto mimeType = db.mimeTypeForName(commonMimeType); const QStringList list = metaData.mimeTypes(); for (const auto &supportedMimeType : list) { if (mimeType.inherits(supportedMimeType)) { return true; } } return false; }); for (const auto &jsonMetadata : jsonPlugins) { // The plugin has been disabled if (!showGroup.readEntry(jsonMetadata.pluginId(), true)) { continue; } // The plugin also has a .desktop file and has already been added. if (addedPlugins.contains(jsonMetadata.pluginId())) { continue; } KPluginFactory *factory = KPluginLoader(jsonMetadata.fileName()).factory(); if (!factory) { continue; } KAbstractFileItemActionPlugin* abstractPlugin = factory->create(); if (abstractPlugin) { abstractPlugin->setParent(this); auto actions = abstractPlugin->actions(d->m_props, d->m_parentWidget); itemCount += actions.count(); mainMenu->addActions(actions); addedPlugins.append(jsonMetadata.pluginId()); } } return itemCount; } // static KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint) { if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) { return KService::List(); } const KService::List firstOffers = KMimeTypeTrader::self()->query(mimeTypeList.first(), QStringLiteral("Application"), traderConstraint); QList rankings; QStringList serviceList; // This section does two things. First, it determines which services are common to all the given mimetypes. // Second, it ranks them based on their preference level in the associated applications list. // The more often a service appear near the front of the list, the LOWER its score. rankings.reserve(firstOffers.count()); serviceList.reserve(firstOffers.count()); for (int i = 0; i < firstOffers.count(); ++i) { KFileItemActionsPrivate::ServiceRank tempRank; tempRank.service = firstOffers[i]; tempRank.score = i; rankings << tempRank; serviceList << tempRank.service->storageId(); } for (int j = 1; j < mimeTypeList.count(); ++j) { QStringList subservice; // list of services that support this mimetype const KService::List offers = KMimeTypeTrader::self()->query(mimeTypeList[j], QStringLiteral("Application"), traderConstraint); subservice.reserve(offers.count()); for (int i = 0; i != offers.count(); ++i) { const QString serviceId = offers[i]->storageId(); subservice << serviceId; const int idPos = serviceList.indexOf(serviceId); if (idPos != -1) { rankings[idPos].score += i; } // else: we ignore the services that didn't support the previous mimetypes } // Remove services which supported the previous mimetypes but don't support this one for (int i = 0; i < serviceList.count(); ++i) { if (!subservice.contains(serviceList[i])) { serviceList.removeAt(i); rankings.removeAt(i); --i; } } // Nothing left -> there is no common application for these mimetypes if (rankings.isEmpty()) { return KService::List(); } } std::sort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank); KService::List result; result.reserve(rankings.size()); for (const KFileItemActionsPrivate::ServiceRank &tempRank : qAsConst(rankings)) { result << tempRank.service; } return result; } // KMimeTypeTrader::preferredService doesn't take a constraint static KService::Ptr preferredService(const QString &mimeType, const QString &constraint) { const KService::List services = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("Application"), constraint); return !services.isEmpty() ? services.first() : KService::Ptr(); } void KFileItemActions::addOpenWithActionsTo(QMenu *topMenu, const QString &traderConstraint) { if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) { return; } d->m_traderConstraint = traderConstraint; KService::List offers = associatedApplications(d->m_mimeTypeList, traderConstraint); //// Ok, we have everything, now insert const KFileItemList items = d->m_props.items(); const KFileItem &firstItem = items.first(); const bool isLocal = firstItem.url().isLocalFile(); // "Open With..." for folders is really not very useful, especially for remote folders. // (media:/something, or trash:/, or ftp://...) if (!d->m_props.isDirectory() || isLocal) { QAction *runAct = new QAction(this); const QStringList serviceIdList = d->listPreferredServiceIds(d->m_mimeTypeList, traderConstraint); //qDebug() << "serviceIdList=" << serviceIdList; // When selecting files with multiple mimetypes, offer either "open with " // or a generic (if there are any apps associated). if (d->m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty() && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated" if (serviceIdList.count() == 1) { const KService::Ptr app = preferredService(d->m_mimeTypeList.first(), traderConstraint); runAct->setText(i18n("&Open with %1", app->name())); runAct->setIcon(QIcon::fromTheme(app->icon())); // Remove that app from the offers list (#242731) for (int i = 0; i < offers.count(); ++i) { if (offers[i]->storageId() == app->storageId()) { offers.removeAt(i); break; } } } else { runAct->setText(i18n("&Open")); } d->m_traderConstraint = traderConstraint; d->m_fileOpenList = d->m_props.items(); QObject::connect(runAct, &QAction::triggered, d, &KFileItemActionsPrivate::slotRunPreferredApplications); topMenu->addAction(runAct); } if (!offers.isEmpty()) { QMenu *menu = topMenu; // Show the top app inline for files, but not folders if (!d->m_props.isDirectory()) { QAction *act = d->createAppAction(offers.takeFirst(), true); menu->addAction(act); } // If there are still more apps, show them in a sub-menu if (!offers.isEmpty()) { // submenu 'open with' menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu); menu->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); menu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // for the unittest topMenu->addMenu(menu); // Add other apps to the sub-menu KService::List::ConstIterator it = offers.constBegin(); for (; it != offers.constEnd(); it++) { QAction *act = d->createAppAction(*it, // no submenu -> prefix single offer menu == topMenu); menu->addAction(act); } topMenu->addSeparator(); } QString openWithActionName; if (menu != topMenu) { // submenu menu->addSeparator(); openWithActionName = i18nc("@action:inmenu Open With", "&Other Application..."); } else { openWithActionName = i18nc("@title:menu", "&Open With..."); } QAction *openWithAct = new QAction(this); openWithAct->setText(openWithActionName); openWithAct->setObjectName(QStringLiteral("openwith_browse")); // for the unittest QObject::connect(openWithAct, &QAction::triggered, d, &KFileItemActionsPrivate::slotOpenWithDialog); menu->addAction(openWithAct); menu->addSeparator(); } else { // no app offers -> Open With... QAction *act = new QAction(this); act->setText(i18nc("@title:menu", "&Open With...")); act->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); act->setObjectName(QStringLiteral("openwith")); // for the unittest QObject::connect(act, &QAction::triggered, d, &KFileItemActionsPrivate::slotOpenWithDialog); topMenu->addAction(act); } } } void KFileItemActionsPrivate::slotRunPreferredApplications() { const KFileItemList fileItems = m_fileOpenList; const QStringList mimeTypeList = listMimeTypes(fileItems); const QStringList serviceIdList = listPreferredServiceIds(mimeTypeList, m_traderConstraint); for (const QString& serviceId : serviceIdList) { KFileItemList serviceItems; for (const KFileItem &item : fileItems) { const KService::Ptr serv = preferredService(item.mimetype(), m_traderConstraint); const QString preferredServiceId = serv ? serv->storageId() : QString(); if (preferredServiceId == serviceId) { serviceItems << item; } } if (serviceId.isEmpty()) { // empty means: no associated app for this mimetype openWithByMime(serviceItems); continue; } - const KService::Ptr servicePtr = KService::serviceByStorageId(serviceId); - if (!servicePtr) { - KRun::displayOpenWithDialog(serviceItems.urlList(), m_parentWidget); - continue; - } - + const KService::Ptr servicePtr = KService::serviceByStorageId(serviceId); // can be nullptr KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr); job->setUrls(serviceItems.urlList()); job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); job->start(); } } void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList, const QString &traderConstraint) { d->m_fileOpenList = fileOpenList; d->m_traderConstraint = traderConstraint; d->slotRunPreferredApplications(); } void KFileItemActionsPrivate::openWithByMime(const KFileItemList &fileItems) { const QStringList mimeTypeList = listMimeTypes(fileItems); for (const QString& mimeType : mimeTypeList) { KFileItemList mimeItems; for (const KFileItem &item : fileItems) { if (item.mimetype() == mimeType) { mimeItems << item; } } - KRun::displayOpenWithDialog(mimeItems.urlList(), m_parentWidget); + // Show Open With dialog + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(); + job->setUrls(mimeItems.urlList()); + job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); + job->start(); } } void KFileItemActionsPrivate::slotRunApplication(QAction *act) { // Is it an application, from one of the "Open With" actions? KService::Ptr app = act->data().value(); Q_ASSERT(app); if (app) { KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(app); job->setUrls(m_props.urlList()); job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); job->start(); } } void KFileItemActionsPrivate::slotOpenWithDialog() { // The item 'Other...' or 'Open With...' has been selected emit q->openWithDialogAboutToBeShown(); - KRun::displayOpenWithDialog(m_props.urlList(), m_parentWidget); + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(); + job->setUrls(m_props.urlList()); + job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); + job->start(); } QStringList KFileItemActionsPrivate::listMimeTypes(const KFileItemList &items) { QStringList mimeTypeList; for (const KFileItem &item : items) { if (!mimeTypeList.contains(item.mimetype())) { mimeTypeList << item.mimetype(); } } return mimeTypeList; } QStringList KFileItemActionsPrivate::listPreferredServiceIds(const QStringList &mimeTypeList, const QString &traderConstraint) { QStringList serviceIdList; serviceIdList.reserve(mimeTypeList.size()); for (const QString &mimeType : mimeTypeList) { const KService::Ptr serv = preferredService(mimeType, traderConstraint); const QString newOffer = serv ? serv->storageId() : QString(); serviceIdList << newOffer; } serviceIdList.removeDuplicates(); return serviceIdList; } QAction *KFileItemActionsPrivate::createAppAction(const KService::Ptr &service, bool singleOffer) { QString actionName(service->name().replace(QLatin1Char('&'), QLatin1String("&&"))); if (singleOffer) { actionName = i18n("Open &with %1", actionName); } else { actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName); } QAction *act = new QAction(q); act->setObjectName(QStringLiteral("openwith")); // for the unittest act->setIcon(QIcon::fromTheme(service->icon())); act->setText(actionName); act->setData(QVariant::fromValue(service)); m_runApplicationActionGroup.addAction(act); return act; } QAction *KFileItemActions::preferredOpenWithAction(const QString &traderConstraint) { const KService::List offers = associatedApplications(d->m_mimeTypeList, traderConstraint); if (offers.isEmpty()) { return nullptr; } return d->createAppAction(offers.first(), true); } void KFileItemActions::setParentWidget(QWidget *widget) { d->m_parentWidget = widget; } diff --git a/src/widgets/krun.cpp b/src/widgets/krun.cpp index b831a1e3..3d2da672 100644 --- a/src/widgets/krun.cpp +++ b/src/widgets/krun.cpp @@ -1,1089 +1,1094 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Torben Weis Copyright (C) 2006 David Faure Copyright (C) 2009 Michael Pyne This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "krun.h" #include "krun_p.h" #include "kio_widgets_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kio/job.h" #include "kio/global.h" #include "kio/scheduler.h" #include "kopenwithdialog.h" #include "krecentdocument.h" #include "kdesktopfileactions.h" #include #include "kprocessrunner_p.h" // for KIOGuiPrivate::checkStartupNotify #include "applicationlauncherjob.h" #include "jobuidelegate.h" #include "widgetsuntrustedprogramhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#ifdef Q_OS_WIN +#include "widgetsopenwithhandler_win.cpp" // displayNativeOpenWithDialog +#endif + KRunPrivate::KRunPrivate(KRun *parent) : q(parent), m_showingDialog(false) { } void KRunPrivate::startTimer() { m_timer->start(0); } // --------------------------------------------------------------------------- static KService::Ptr schemeService(const QString &protocol) { return KMimeTypeTrader::self()->preferredService(QLatin1String("x-scheme-handler/") + protocol); } static bool checkNeedPortalSupport() { return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, QLatin1String("flatpak-info")).isEmpty() || qEnvironmentVariableIsSet("SNAP"); } qint64 KRunPrivate::runCommandLauncherJob(KIO::CommandLauncherJob *job, QWidget *widget) { QObject *receiver = widget ? static_cast(widget) : static_cast(qApp); QObject::connect(job, &KJob::result, receiver, [widget](KJob *job) { if (job->error()) { QEventLoopLocker locker; KMessageBox::sorry(widget, job->errorString()); } }); job->start(); job->waitForStarted(); return job->error() ? 0 : job->pid(); } // --------------------------------------------------------------------------- // Helper function that returns whether a file has the execute bit set or not. static bool hasExecuteBit(const QString &fileName) { QFileInfo file(fileName); return file.isExecutable(); } bool KRun::isExecutableFile(const QUrl &url, const QString &mimetype) { if (!url.isLocalFile()) { return false; } // While isExecutable performs similar check to this one, some users depend on // this method not returning true for application/x-desktop QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName(mimetype); if (!mimeType.inherits(QStringLiteral("application/x-executable")) && !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable")) && !mimeType.inherits(QStringLiteral("application/x-executable-script")) && !mimeType.inherits(QStringLiteral("application/x-sharedlib"))) { return false; } if (!hasExecuteBit(url.toLocalFile()) && !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable"))) { return false; } return true; } void KRun::handleInitError(int kioErrorCode, const QString &errorMsg) { Q_UNUSED(kioErrorCode); d->m_showingDialog = true; KMessageBox::error(d->m_window, errorMsg); d->m_showingDialog = false; } void KRun::handleError(KJob *job) { Q_ASSERT(job); if (job) { d->m_showingDialog = true; job->uiDelegate()->showErrorMessage(); d->m_showingDialog = false; } } #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 31) bool KRun::runUrl(const QUrl &url, const QString &mimetype, QWidget *window, bool tempFile, bool runExecutables, const QString &suggestedFileName, const QByteArray &asn) { RunFlags flags = tempFile ? KRun::DeleteTemporaryFiles : RunFlags(); if (runExecutables) { flags |= KRun::RunExecutables; } return runUrl(url, mimetype, window, flags, suggestedFileName, asn); } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) // This is called by foundMimeType, since it knows the mimetype of the URL bool KRun::runUrl(const QUrl &u, const QString &_mimetype, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { const bool runExecutables = flags.testFlag(KRun::RunExecutables); const bool tempFile = flags.testFlag(KRun::DeleteTemporaryFiles); KIO::OpenUrlJob *job = new KIO::OpenUrlJob(u, _mimetype); job->setSuggestedFileName(suggestedFileName); job->setStartupId(asn); job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); job->setDeleteTemporaryFile(tempFile); job->setRunExecutables(runExecutables); job->start(); return true; } #endif +#if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) bool KRun::displayOpenWithDialog(const QList &lst, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) { KMessageBox::sorry(window, i18n("You are not authorized to select an application to open this file.")); return false; } #ifdef Q_OS_WIN KConfigGroup cfgGroup(KSharedConfig::openConfig(), QStringLiteral("KOpenWithDialog Settings")); if (cfgGroup.readEntry("Native", true)) { - return KRunPrivate::displayNativeOpenWithDialog(lst, window, tempFiles, - suggestedFileName, asn); + return displayNativeOpenWithDialog(lst, window); } #endif // TODO : pass the mimetype as a parameter, to show it (comment field) in the dialog ! // Note KOpenWithDialog::setMimeTypeFromUrls already guesses the mimetype if lst.size() == 1 KOpenWithDialog dialog(lst, QString(), QString(), window); dialog.setWindowModality(Qt::WindowModal); if (dialog.exec()) { KService::Ptr service = dialog.service(); if (!service) { //qDebug() << "No service set, running " << dialog.text(); service = KService::Ptr(new KService(QString() /*name*/, dialog.text(), QString() /*icon*/)); } const RunFlags flags = tempFiles ? KRun::DeleteTemporaryFiles : RunFlags(); return KRun::runApplication(*service, lst, window, flags, suggestedFileName, asn); } return false; } +#endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 0) void KRun::shellQuote(QString &_str) { // Credits to Walter, says Bernd G. :) if (_str.isEmpty()) { // Don't create an explicit empty parameter return; } const QChar q = QLatin1Char('\''); _str.replace(q, QLatin1String("'\\''")).prepend(q).append(q); } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 0) QStringList KRun::processDesktopExec(const KService &_service, const QList &_urls, bool tempFiles, const QString &suggestedFileName) { KIO::DesktopExecParser parser(_service, _urls); parser.setUrlsAreTempFiles(tempFiles); parser.setSuggestedFileName(suggestedFileName); return parser.resultingArguments(); } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 0) QString KRun::binaryName(const QString &execLine, bool removePath) { return removePath ? KIO::DesktopExecParser::executableName(execLine) : KIO::DesktopExecParser::executablePath(execLine); } #endif // This code is also used in klauncher. // TODO: port klauncher to KIOGuiPrivate::checkStartupNotify once this lands // TODO: then deprecate this method, and remove in KF6 bool KRun::checkStartupNotify(const QString & /*binName*/, const KService *service, bool *silent_arg, QByteArray *wmclass_arg) { return KIOGuiPrivate::checkStartupNotify(service, silent_arg, wmclass_arg); } #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 6) bool KRun::run(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { const RunFlags flags = tempFiles ? KRun::DeleteTemporaryFiles : RunFlags(); return runApplication(_service, _urls, window, flags, suggestedFileName, asn) != 0; } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) qint64 KRun::runApplication(const KService &service, const QList &urls, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { KService::Ptr servicePtr(new KService(service)); // clone // QTBUG-59017 Calling winId() on an embedded widget will break interaction // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using // its parent window instead if (window) { window = window->window(); } KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr); job->setUrls(urls); if (flags & DeleteTemporaryFiles) { job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); } job->setSuggestedFileName(suggestedFileName); job->setStartupId(asn); job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); job->start(); job->waitForStarted(); return job->error() ? 0 : job->pid(); } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) qint64 KRun::runService(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { return runApplication(_service, _urls, window, tempFiles ? RunFlags(DeleteTemporaryFiles) : RunFlags(), suggestedFileName, asn); } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) bool KRun::run(const QString &_exec, const QList &_urls, QWidget *window, const QString &_name, const QString &_icon, const QByteArray &asn) { KService::Ptr service(new KService(_name, _exec, _icon)); return runApplication(*service, _urls, window, RunFlags{}, QString(), asn); } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) bool KRun::runCommand(const QString &cmd, QWidget *window, const QString &workingDirectory) { if (cmd.isEmpty()) { qCWarning(KIO_WIDGETS) << "Command was empty, nothing to run"; return false; } const QStringList args = KShell::splitArgs(cmd); if (args.isEmpty()) { qCWarning(KIO_WIDGETS) << "Command could not be parsed."; return false; } const QString &bin = args.first(); return KRun::runCommand(cmd, bin, bin /*iconName*/, window, QByteArray(), workingDirectory); } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) bool KRun::runCommand(const QString &cmd, const QString &execName, const QString &iconName, QWidget *window, const QByteArray &asn) { return runCommand(cmd, execName, iconName, window, asn, QString()); } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) bool KRun::runCommand(const QString &cmd, const QString &execName, const QString &iconName, QWidget *window, const QByteArray &asn, const QString &workingDirectory) { auto *job = new KIO::CommandLauncherJob(cmd); job->setExecutable(execName); job->setIcon(iconName); job->setStartupId(asn); job->setWorkingDirectory(workingDirectory); if (window) { window = window->window(); } return KRunPrivate::runCommandLauncherJob(job, window); } #endif KRun::KRun(const QUrl &url, QWidget *window, bool showProgressInfo, const QByteArray &asn) : d(new KRunPrivate(this)) { d->m_timer = new QTimer(this); d->m_timer->setObjectName(QStringLiteral("KRun::timer")); d->m_timer->setSingleShot(true); d->init(url, window, showProgressInfo, asn); } void KRunPrivate::init(const QUrl &url, QWidget *window, bool showProgressInfo, const QByteArray &asn) { m_bFault = false; m_bAutoDelete = true; m_bProgressInfo = showProgressInfo; m_bFinished = false; m_job = nullptr; m_strURL = url; m_bScanFile = false; m_bIsDirectory = false; m_runExecutables = true; m_followRedirections = true; m_window = window; m_asn = asn; q->setEnableExternalBrowser(true); // Start the timer. This means we will return to the event // loop and do initialization afterwards. // Reason: We must complete the constructor before we do anything else. m_bCheckPrompt = false; m_bInit = true; q->connect(m_timer, &QTimer::timeout, q, &KRun::slotTimeout); startTimer(); //qDebug() << "new KRun" << q << url << "timer=" << m_timer; } void KRun::init() { //qDebug() << "INIT called"; if (!d->m_strURL.isValid() || d->m_strURL.scheme().isEmpty()) { const QString error = !d->m_strURL.isValid() ? d->m_strURL.errorString() : d->m_strURL.toString(); handleInitError(KIO::ERR_MALFORMED_URL, i18n("Malformed URL\n%1", error)); qCWarning(KIO_WIDGETS) << "Malformed URL:" << error; d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), d->m_strURL)) { QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, d->m_strURL.toDisplayString()); handleInitError(KIO::ERR_ACCESS_DENIED, msg); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } if (d->m_externalBrowserEnabled && checkNeedPortalSupport()) { // use the function from QDesktopServices as it handles portals correctly d->m_bFault = !QDesktopServices::openUrl(d->m_strURL); d->m_bFinished = true; d->startTimer(); return; } if (!d->m_externalBrowser.isEmpty() && d->m_strURL.scheme().startsWith(QLatin1String("http"))) { if (d->runExternalBrowser(d->m_externalBrowser)) { return; } } else if (d->m_strURL.isLocalFile() && (d->m_strURL.host().isEmpty() || (d->m_strURL.host() == QLatin1String("localhost")) || (d->m_strURL.host().compare(QHostInfo::localHostName(), Qt::CaseInsensitive) == 0))) { const QString localPath = d->m_strURL.toLocalFile(); if (!QFile::exists(localPath)) { handleInitError(KIO::ERR_DOES_NOT_EXIST, i18n("Unable to run the command specified. " "The file or folder %1 does not exist.", localPath.toHtmlEscaped())); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(d->m_strURL); //qDebug() << "MIME TYPE is " << mime.name(); if (mime.isDefault() && !QFileInfo(localPath).isReadable()) { // Unknown mimetype because the file is unreadable, no point in showing an open-with dialog (#261002) const QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, localPath); handleInitError(KIO::ERR_ACCESS_DENIED, msg); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } else { mimeTypeDetermined(mime.name()); return; } } else if (KIO::DesktopExecParser::hasSchemeHandler(d->m_strURL)) { // looks for an application associated with x-scheme-handler/ const KService::Ptr service = schemeService(d->m_strURL.scheme()); if (service) { // if there's one... if (runApplication(*service, QList() << d->m_strURL, d->m_window, RunFlags{}, QString(), d->m_asn)) { d->m_bFinished = true; d->startTimer(); return; } } else { // fallback, look for associated helper protocol Q_ASSERT(KProtocolInfo::isHelperProtocol(d->m_strURL.scheme())); const auto exec = KProtocolInfo::exec(d->m_strURL.scheme()); if (exec.isEmpty()) { // use default mimetype opener for file mimeTypeDetermined(KProtocolManager::defaultMimetype(d->m_strURL)); return; } else { if (run(exec, QList() << d->m_strURL, d->m_window, QString(), QString(), d->m_asn)) { d->m_bFinished = true; d->startTimer(); return; } } } } #if 0 // removed for KF5 (for portability). Reintroduce a bool or flag if useful. // Did we already get the information that it is a directory ? if ((d->m_mode & QT_STAT_MASK) == QT_STAT_DIR) { mimeTypeDetermined("inode/directory"); return; } #endif // Let's see whether it is a directory if (!KProtocolManager::supportsListing(d->m_strURL)) { // No support for listing => it can't be a directory (example: http) if (!KProtocolManager::supportsReading(d->m_strURL)) { // No support for reading files either => we can't do anything (example: mailto URL, with no associated app) handleInitError(KIO::ERR_UNSUPPORTED_ACTION, i18n("Could not find any application or handler for %1", d->m_strURL.toDisplayString())); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } scanFile(); return; } //qDebug() << "Testing directory (stating)"; // It may be a directory or a file, let's stat KIO::JobFlags flags = d->m_bProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::StatJob *job = KIO::statDetails(d->m_strURL, KIO::StatJob::SourceSide, KIO::StatBasic, flags); KJobWidgets::setWindow(job, d->m_window); connect(job, &KJob::result, this, &KRun::slotStatResult); d->m_job = job; //qDebug() << "Job" << job << "is about stating" << d->m_strURL; } KRun::~KRun() { //qDebug() << this; d->m_timer->stop(); killJob(); //qDebug() << this << "done"; delete d; } bool KRunPrivate::runExternalBrowser(const QString &_exec) { QList urls; urls.append(m_strURL); if (_exec.startsWith(QLatin1Char('!'))) { // Literal command const QString exec = _exec.midRef(1) + QLatin1String(" %u"); if (KRun::run(exec, urls, m_window, QString(), QString(), m_asn)) { m_bFinished = true; startTimer(); return true; } } else { KService::Ptr service = KService::serviceByStorageId(_exec); if (service && KRun::runApplication(*service, urls, m_window, KRun::RunFlags{}, QString(), m_asn)) { m_bFinished = true; startTimer(); return true; } } return false; } void KRunPrivate::showPrompt() { ExecutableFileOpenDialog *dialog = new ExecutableFileOpenDialog(promptMode(), q->window()); dialog->setAttribute(Qt::WA_DeleteOnClose); QObject::connect(dialog, &ExecutableFileOpenDialog::finished, q, [this, dialog](int result){ onDialogFinished(result, dialog->isDontAskAgainChecked()); }); dialog->show(); } bool KRunPrivate::isPromptNeeded() { if (m_strURL == QUrl(QStringLiteral("remote:/x-wizard_service.desktop"))) { return false; } const QMimeDatabase db; const QMimeType mime = db.mimeTypeForUrl(m_strURL); const bool isFileExecutable = (KRun::isExecutableFile(m_strURL, mime.name()) || mime.inherits(QStringLiteral("application/x-desktop"))); if (isFileExecutable) { KConfigGroup cfgGroup(KSharedConfig::openConfig(QStringLiteral("kiorc")), "Executable scripts"); const QString value = cfgGroup.readEntry("behaviourOnLaunch", "alwaysAsk"); if (value == QLatin1String("alwaysAsk")) { return true; } else { q->setRunExecutables(value == QLatin1String("execute")); } } return false; } ExecutableFileOpenDialog::Mode KRunPrivate::promptMode() { const QMimeDatabase db; const QMimeType mime = db.mimeTypeForUrl(m_strURL); if (mime.inherits(QStringLiteral("text/plain"))) { return ExecutableFileOpenDialog::OpenOrExecute; } #ifndef Q_OS_WIN if (mime.inherits(QStringLiteral("application/x-ms-dos-executable"))) { return ExecutableFileOpenDialog::OpenAsExecute; } #endif return ExecutableFileOpenDialog::OnlyExecute; } void KRunPrivate::onDialogFinished(int result, bool isDontAskAgainSet) { if (result == ExecutableFileOpenDialog::Rejected) { m_bFinished = true; m_bInit = false; startTimer(); return; } q->setRunExecutables(result == ExecutableFileOpenDialog::ExecuteFile); if (isDontAskAgainSet) { QString output = result == ExecutableFileOpenDialog::OpenFile ? QStringLiteral("open") : QStringLiteral("execute"); KConfigGroup cfgGroup(KSharedConfig::openConfig(QStringLiteral("kiorc")), "Executable scripts"); cfgGroup.writeEntry("behaviourOnLaunch", output); } startTimer(); } void KRun::scanFile() { //qDebug() << d->m_strURL; // First, let's check for well-known extensions // Not when there is a query in the URL, in any case. if (!d->m_strURL.hasQuery()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(d->m_strURL); if (!mime.isDefault() || d->m_strURL.isLocalFile()) { //qDebug() << "Scanfile: MIME TYPE is " << mime.name(); mimeTypeDetermined(mime.name()); return; } } // No mimetype found, and the URL is not local (or fast mode not allowed). // We need to apply the 'KIO' method, i.e. either asking the server or // getting some data out of the file, to know what mimetype it is. if (!KProtocolManager::supportsReading(d->m_strURL)) { qCWarning(KIO_WIDGETS) << "#### NO SUPPORT FOR READING!"; d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } //qDebug() << this << "Scanning file" << d->m_strURL; KIO::JobFlags flags = d->m_bProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::TransferJob *job = KIO::get(d->m_strURL, KIO::NoReload /*reload*/, flags); KJobWidgets::setWindow(job, d->m_window); connect(job, &KJob::result, this, &KRun::slotScanFinished); connect(job, QOverload::of(&KIO::TransferJob::mimetype), this, &KRun::slotScanMimeType); d->m_job = job; //qDebug() << "Job" << job << "is about getting from" << d->m_strURL; } // When arriving in that method there are 6 possible states: // must_show_prompt, must_init, must_scan_file, found_dir, done+error or done+success. void KRun::slotTimeout() { if (d->m_bCheckPrompt) { d->m_bCheckPrompt = false; if (d->isPromptNeeded()) { d->showPrompt(); return; } } if (d->m_bInit) { d->m_bInit = false; init(); return; } if (d->m_bFault) { emit error(); } if (d->m_bFinished) { emit finished(); } else { if (d->m_bScanFile) { d->m_bScanFile = false; scanFile(); return; } else if (d->m_bIsDirectory) { d->m_bIsDirectory = false; mimeTypeDetermined(QStringLiteral("inode/directory")); return; } } if (d->m_bAutoDelete) { deleteLater(); return; } } void KRun::slotStatResult(KJob *job) { d->m_job = nullptr; const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { qCWarning(KIO_WIDGETS) << this << "ERROR" << job->error() << job->errorString(); handleError(job); //qDebug() << this << " KRun returning from showErrorDialog, starting timer to delete us"; d->m_bFault = true; } d->m_bFinished = true; // will emit the error and autodelete this d->startTimer(); } else { //qDebug() << "Finished"; KIO::StatJob *statJob = qobject_cast(job); if (!statJob) { qFatal("Fatal Error: job is a %s, should be a StatJob", typeid(*job).name()); } // Update our URL in case of a redirection setUrl(statJob->url()); const KIO::UDSEntry entry = statJob->statResult(); const mode_t mode = entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE); if ((mode & QT_STAT_MASK) == QT_STAT_DIR) { d->m_bIsDirectory = true; // it's a dir } else { d->m_bScanFile = true; // it's a file } d->m_localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); // mimetype already known? (e.g. print:/manager) const QString knownMimeType = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); if (!knownMimeType.isEmpty()) { mimeTypeDetermined(knownMimeType); d->m_bFinished = true; } // We should have found something assert(d->m_bScanFile || d->m_bIsDirectory); // Start the timer. Once we get the timer event this // protocol server is back in the pool and we can reuse it. // This gives better performance than starting a new slave d->startTimer(); } } void KRun::slotScanMimeType(KIO::Job *, const QString &mimetype) { if (mimetype.isEmpty()) { qCWarning(KIO_WIDGETS) << "get() didn't emit a mimetype! Probably a kioslave bug, please check the implementation of" << url().scheme(); } mimeTypeDetermined(mimetype); d->m_job = nullptr; } void KRun::slotScanFinished(KJob *job) { d->m_job = nullptr; const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { qCWarning(KIO_WIDGETS) << this << "ERROR (stat):" << job->error() << ' ' << job->errorString(); handleError(job); d->m_bFault = true; } d->m_bFinished = true; // will emit the error and autodelete this d->startTimer(); } } void KRun::mimeTypeDetermined(const QString &mimeType) { // foundMimeType reimplementations might show a dialog box; // make sure some timer doesn't kill us meanwhile (#137678, #156447) Q_ASSERT(!d->m_showingDialog); d->m_showingDialog = true; foundMimeType(mimeType); d->m_showingDialog = false; // We cannot assume that we're finished here. Some reimplementations // start a KIO job and call setFinished only later. } void KRun::foundMimeType(const QString &type) { //qDebug() << "Resulting mime type is " << type; QMimeDatabase db; KIO::TransferJob *job = qobject_cast(d->m_job); if (job) { // Update our URL in case of a redirection if (d->m_followRedirections) { setUrl(job->url()); } job->putOnHold(); KIO::Scheduler::publishSlaveOnHold(); d->m_job = nullptr; } Q_ASSERT(!d->m_bFinished); // Support for preferred service setting, see setPreferredService if (!d->m_preferredService.isEmpty()) { //qDebug() << "Attempting to open with preferred service: " << d->m_preferredService; KService::Ptr serv = KService::serviceByDesktopName(d->m_preferredService); if (serv && serv->hasMimeType(type)) { QList lst; lst.append(d->m_strURL); if (KRun::runApplication(*serv, lst, d->m_window, RunFlags{}, QString(), d->m_asn)) { setFinished(true); return; } /// Note: if that service failed, we'll go to runUrl below to /// maybe find another service, even though an error dialog box was /// already displayed. That's good if runUrl tries another service, /// but it's not good if it tries the same one :} } } // Resolve .desktop files from media:/, remote:/, applications:/ etc. QMimeType mime = db.mimeTypeForName(type); if (!mime.isValid()) { qCWarning(KIO_WIDGETS) << "Unknown mimetype" << type; } else if (mime.inherits(QStringLiteral("application/x-desktop")) && !d->m_localPath.isEmpty()) { d->m_strURL = QUrl::fromLocalFile(d->m_localPath); } KRun::RunFlags runFlags; if (d->m_runExecutables) { runFlags |= KRun::RunExecutables; } if (!KRun::runUrl(d->m_strURL, type, d->m_window, runFlags, d->m_suggestedFileName, d->m_asn)) { d->m_bFault = true; } setFinished(true); } void KRun::killJob() { if (d->m_job) { //qDebug() << this << "m_job=" << d->m_job; d->m_job->kill(); d->m_job = nullptr; } } void KRun::abort() { if (d->m_bFinished) { return; } //qDebug() << this << "m_showingDialog=" << d->m_showingDialog; killJob(); // If we're showing an error message box, the rest will be done // after closing the msgbox -> don't autodelete nor emit signals now. if (d->m_showingDialog) { return; } d->m_bFault = true; d->m_bFinished = true; d->m_bInit = false; d->m_bScanFile = false; // will emit the error and autodelete this d->startTimer(); } QWidget *KRun::window() const { return d->m_window; } bool KRun::hasError() const { return d->m_bFault; } bool KRun::hasFinished() const { return d->m_bFinished; } bool KRun::autoDelete() const { return d->m_bAutoDelete; } void KRun::setAutoDelete(bool b) { d->m_bAutoDelete = b; } void KRun::setEnableExternalBrowser(bool b) { d->m_externalBrowserEnabled = b; if (d->m_externalBrowserEnabled) { d->m_externalBrowser = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("BrowserApplication"); // If a default browser isn't set in kdeglobals, fall back to mimeapps.list if (!d->m_externalBrowser.isEmpty()) { return; } KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); KConfigGroup defaultApps(profile, "Default Applications"); d->m_externalBrowser = defaultApps.readEntry("x-scheme-handler/https"); if (d->m_externalBrowser.isEmpty()) { d->m_externalBrowser = defaultApps.readEntry("x-scheme-handler/http"); } } else { d->m_externalBrowser.clear(); } } void KRun::setPreferredService(const QString &desktopEntryName) { d->m_preferredService = desktopEntryName; } void KRun::setRunExecutables(bool b) { d->m_runExecutables = b; } void KRun::setSuggestedFileName(const QString &fileName) { d->m_suggestedFileName = fileName; } void KRun::setShowScriptExecutionPrompt(bool showPrompt) { d->m_bCheckPrompt = showPrompt; } void KRun::setFollowRedirections(bool followRedirections) { d->m_followRedirections = followRedirections; } QString KRun::suggestedFileName() const { return d->m_suggestedFileName; } bool KRun::isExecutable(const QString &mimeTypeName) { QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName(mimeTypeName); return (mimeType.inherits(QLatin1String("application/x-desktop")) || mimeType.inherits(QLatin1String("application/x-executable")) || /* See https://bugs.freedesktop.org/show_bug.cgi?id=97226 */ mimeType.inherits(QLatin1String("application/x-sharedlib")) || mimeType.inherits(QLatin1String("application/x-ms-dos-executable")) || mimeType.inherits(QLatin1String("application/x-shellscript"))); } void KRun::setUrl(const QUrl &url) { d->m_strURL = url; } QUrl KRun::url() const { return d->m_strURL; } void KRun::setError(bool error) { d->m_bFault = error; } void KRun::setProgressInfo(bool progressInfo) { d->m_bProgressInfo = progressInfo; } bool KRun::progressInfo() const { return d->m_bProgressInfo; } void KRun::setFinished(bool finished) { d->m_bFinished = finished; if (finished) { d->startTimer(); } } void KRun::setJob(KIO::Job *job) { d->m_job = job; } KIO::Job *KRun::job() { return d->m_job; } #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 4) QTimer &KRun::timer() { return *d->m_timer; } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 1) void KRun::setDoScanFile(bool scanFile) { d->m_bScanFile = scanFile; } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 1) bool KRun::doScanFile() const { return d->m_bScanFile; } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 1) void KRun::setIsDirecory(bool isDirectory) { d->m_bIsDirectory = isDirectory; } #endif bool KRun::isDirectory() const { return d->m_bIsDirectory; } #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 1) void KRun::setInitializeNextAction(bool initialize) { d->m_bInit = initialize; } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 1) bool KRun::initializeNextAction() const { return d->m_bInit; } #endif bool KRun::isLocalFile() const { return d->m_strURL.isLocalFile(); } #include "moc_krun.cpp" diff --git a/src/widgets/krun.h b/src/widgets/krun.h index 8d4bfa6f..c1dadb7e 100644 --- a/src/widgets/krun.h +++ b/src/widgets/krun.h @@ -1,792 +1,807 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2006 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KRUN_H #define KRUN_H #include "kiowidgets_export.h" #include #include #include class KService; class KJob; class QTimer; class KRunPrivate; namespace KIO { class Job; } /** * @class KRun krun.h * * To open files with their associated applications in KDE, use KRun. * * It can execute any desktop entry, as well as any file, using * the default application or another application "bound" to the file type * (or URL protocol). * * In that example, the mimetype of the file is not known by the application, * so a KRun instance must be created. It will determine the mimetype by itself. * If the mimetype is known, or if you even know the service (application) to * use for this file, use one of the static methods. * * By default KRun uses auto deletion. It causes the KRun instance to delete * itself when the it finished its task. If you allocate the KRun * object on the stack you must disable auto deletion, otherwise it will crash. * * This respects the "shell_access", "openwith" and "run_desktop_files" Kiosk * action restrictions (see KAuthorized::authorize()). * * @short Opens files with their associated applications in KDE */ class KIOWIDGETS_EXPORT KRun : public QObject { Q_OBJECT public: /** * @param url the URL of the file or directory to 'run' * * @param window * The top-level widget of the app that invoked this object. * It is used to make sure private information like passwords * are properly handled per application. * * @param showProgressInfo * Whether to show progress information when determining the * type of the file (i.e. when using KIO::stat and KIO::mimetype) * Before you set this to false to avoid a dialog box, think about * a very slow FTP server... * It is always better to provide progress info in such cases. * * @param asn * Application startup notification id, if available (otherwise ""). * * @deprecated since 5.71, use KIO::OpenUrlJob(url) (except for KRun subclasses, for now) */ KRun(const QUrl &url, QWidget *window, bool showProgressInfo = true, const QByteArray &asn = QByteArray()); /** * Destructor. Don't call it yourself, since a KRun object auto-deletes * itself. */ virtual ~KRun(); /** * Abort this KRun. This kills any jobs launched by it, * and leads to deletion if auto-deletion is on. * This is much safer than deleting the KRun (in case it's * currently showing an error dialog box, for instance) */ void abort(); /** * Returns true if the KRun instance has an error. * @return true when an error occurred * @see error() */ bool hasError() const; /** * Returns true if the KRun instance has finished. * @return true if the KRun instance has finished * @see finished() */ bool hasFinished() const; /** * Checks whether auto delete is activated. * Auto-deletion causes the KRun instance to delete itself * when it finished its task. * By default auto deletion is on. * @return true if auto deletion is on, false otherwise */ bool autoDelete() const; /** * Enables or disabled auto deletion. * Auto deletion causes the KRun instance to delete itself * when it finished its task. If you allocate the KRun * object on the stack you must disable auto deletion. * By default auto deletion is on. * @param b true to enable auto deletion, false to disable */ void setAutoDelete(bool b); /** * Set the preferred service for opening this URL, after * its mimetype will have been found by KRun. IMPORTANT: the service is * only used if its configuration says it can handle this mimetype. * This is used for instance for the X-KDE-LastOpenedWith key in * the recent documents list, or for the app selection in * KParts::BrowserOpenOrSaveQuestion. * @param desktopEntryName the desktopEntryName of the service, e.g. "kate". */ void setPreferredService(const QString &desktopEntryName); /** * Sets whether executables, .desktop files or shell scripts should * be run by KRun. This is enabled by default. * @param b whether to run executable files or not. * @see isExecutable() */ void setRunExecutables(bool b); /** * Sets whether KRun should follow URLs redirections. * This is enabled by default * @param b whether to follow redirections or not. * @since 5.55 */ void setFollowRedirections(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 enable the external browser or not. */ void setEnableExternalBrowser(bool b); /** * 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 mimetype 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. */ void setSuggestedFileName(const QString &fileName); /** * Sets whether a prompt should be shown before executing scripts or desktop files. * If enabled, KRun uses the "kiorc" configuration file to decide whether to open the * file, execute it or show a prompt. * @since 5.4 */ void setShowScriptExecutionPrompt(bool showPrompt); /** * Suggested file name given by the server (e.g. HTTP content-disposition) */ QString suggestedFileName() const; /** * Associated window, as passed to the constructor * @since 4.9.3 */ QWidget *window() const; #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 6) /** * Open a list of URLs with a certain service (application). * * @param service the service to run * @param urls the list of URLs, can be empty (app launched * without argument) * @param window The top-level widget of the app that invoked this object. * @param tempFiles if true and urls are local files, they will be deleted * when the application exits. * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * * @deprecated since 5.6. Since 5.71 use ApplicationLauncherJob, otherwise runApplication instead. * @code * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); * job->setUrls(urls); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * if (tempFiles) { * job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); * } * job->setSuggestedFileName(suggestedFileName); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 6, "Use KIO::ApplicationLauncherJob, see API docs for a code sample") static bool run(const KService &service, const QList &urls, QWidget *window, bool tempFiles = false, const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Open a list of URLs with a certain service (application). * * Prefer runApplication(), unless you need to wait for the application * to register to D-Bus before this method returns (but that should rather * be done with D-Bus activation). * * @param service the service to run * @param urls the list of URLs, can be empty (app launched * without argument) * @param window The top-level widget of the app that invoked this object. * @param tempFiles if true and urls are local files, they will be deleted * when the application exits. * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return 0 on error, the process ID on success * @since 5.6 * @deprecated since 5.71, use ApplicationLauncherJob instead. * @code * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); * job->setUrls(urls); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * if (tempFiles) { * job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); * } * job->setSuggestedFileName(suggestedFileName); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::ApplicationLauncherJob, see API docs for a code sample") static qint64 runService(const KService &service, const QList &urls, QWidget *window, bool tempFiles = false, const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * @see RunFlags */ enum RunFlag { DeleteTemporaryFiles = 0x1, ///< the URLs passed to the service will be deleted when it exits (if the URLs are local files) RunExecutables = 0x2, ///< Whether to run URLs that are executable scripts or binaries @see isExecutableFile() @since 5.31 }; /** * Stores a combination of #RunFlag values. * @deprecated since 5.71, see porting instructions in the respective methods */ Q_DECLARE_FLAGS(RunFlags, RunFlag) #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Run an application (known from its .desktop file, i.e. as a KService) * * If you need to wait for the application to register to D-Bus, use D-Bus activation instead. * * If you don't need the prompt for asking the user whether to add the executable bit for * desktop files or binaries that don't have it, you can use KIO::ApplicationLauncherJob from KIOGui directly. * * @param service the service to run * @param urls the list of URLs, can be empty (app launched * without argument) * @param window The top-level widget of the app that invoked this object. * @param flags various flags * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return 0 on error, the process ID on success * @since 5.24 * * @deprecated since 5.71, use ApplicationLauncherJob instead. * @code * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); * job->setUrls(urls); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setSuggestedFileName(suggestedFileName); * job->setRunFlags(flags); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::ApplicationLauncherJob, see API docs for a code sample") static qint64 runApplication(const KService &service, const QList &urls, QWidget *window, RunFlags flags = RunFlags(), const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Open a list of URLs with an executable. * * @param exec the name of the executable, for example * "/usr/bin/netscape %u". * Don't forget to include the %u if you know that the applications * supports URLs. Otherwise, non-local urls will first be downloaded * to a temp file (using kioexec). * @param urls the list of URLs to open, can be empty (app launched without argument) * @param window The top-level widget of the app that invoked this object. * @param name the logical name of the application, for example * "Netscape 4.06". * @param icon the icon which should be used by the application. * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * * @deprecated since 5.71, use KIO::ApplicationLauncherJob with a temporary KService * @code * KService::Ptr service(new KService(name, exec, icon)); * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); * job->setUrls(urls); * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::ApplicationLauncherJob with a temporary KService, see API docs for a code sample") static bool run(const QString &exec, const QList &urls, QWidget *window, const QString &name = QString(), const QString &icon = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 31) /** * Open the given URL. * * This function is used after the mime type * is found out. It will search for all services which can handle * the mime type and call run() afterwards. * @param url the URL to open * @param mimetype the mime type of the resource * @param window The top-level widget of the app that invoked this object. * @param tempFile if true and url is a local file, it will be deleted * when the launched application exits. * @param runExecutables if false then local .desktop files, * executables and shell scripts will not be run. * See also isExecutable(). * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * @deprecated since 5.31. Since 5.71 use OpenUrlJob, otherwise runUrl() with RunFlags. * @code * KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimetype); * job->setSuggestedFileName(suggestedFileName); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setRunExecutables(runExecutables); * job->setDeleteTemporaryFile(...); // depending on the old RunFlags * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 31, "Use KIO::OpenUrlJob, see API docs for a code sample") static bool runUrl(const QUrl &url, const QString &mimetype, QWidget *window, bool tempFile = false, bool runExecutables = true, const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Open the given URL. * * This function can be used after the mime type has been found out. * It will search for all services which can handle the mime type and call run() afterwards. * * @param url The URL to open. * @param mimetype The mime type of the resource. * @param window The top-level widget of the app that invoked this object. * @param flags Various run flags. * @param suggestedFileName See setSuggestedFileName() * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * @since 5.31 * @deprecated since 5.71, use KIO::OpenUrlJob: * @code * KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimetype); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setRunExecutables(runExecutables); * job->setDeleteTemporaryFile(...); // depending on the old RunFlags * job->setSuggestedFileName(suggestedFileName); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::OpenUrlJob, see API docs for a code sample") static bool runUrl(const QUrl &url, const QString &mimetype, QWidget *window, RunFlags flags, const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Run the given shell command and notifies KDE of the starting * of the application. If the program to be called doesn't exist, * an error box will be displayed. * * Use only when you know the full command line. Otherwise use the other * static methods, or KRun's constructor. * * @param cmd must be a shell command. You must not append "&" * to it, since the function will do that for you. * @param window The top-level widget of the app that invoked this object. * @param workingDirectory directory to use for relative paths, so that * a command like "kwrite file.txt" finds file.txt from the right place * * @return @c true on success, @c false on error * @deprecated since 5.71, use KIO::CommandLauncherJob * @code * KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(cmd); * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setWorkingDirectory(workingDirectory); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::CommandLauncherJob, see API docs for a code sample") static bool runCommand(const QString &cmd, QWidget *window, const QString &workingDirectory = QString()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Same as the other runCommand(), but it also takes the name of the * binary, to display an error message in case it couldn't find it. * * @param cmd must be a shell command. You must not append "&" * to it, since the function will do that for you. * @param execName the name of the executable * @param icon icon for app starting notification * @param window The top-level widget of the app that invoked this object. * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * @deprecated since 5.71, use KIO::CommandLauncherJob * @code * KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(cmd); * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setWorkingDirectory(workingDirectory); * job->setExecutable(execName); * job->setIcon(icon); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::CommandLauncherJob, see API docs for a code sample") static bool runCommand(const QString &cmd, const QString &execName, const QString &icon, QWidget *window, const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Overload that also takes a working directory, so that a command like * "kwrite file.txt" finds file.txt from the right place. * @param workingDirectory the working directory for the started process. The default * (if passing an empty string) is the user's document path. * @since 4.4 * @deprecated since 5.71, use KIO::CommandLauncherJob instead * @code * KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(cmd); * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setWorkingDirectory(workingDirectory); * job->setExecutable(execName); * job->setIcon(icon); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::CommandLauncherJob, see API docs for a code sample") static bool runCommand(const QString &cmd, const QString &execName, const QString &icon, QWidget *window, const QByteArray &asn, const QString &workingDirectory); #endif +#if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Display the Open-With dialog for those URLs, and run the chosen application. - * @param lst the list of applications to run + * @param lst the list of URLs to open * @param window The top-level widget of the app that invoked this object. * @param tempFiles if true and lst are local files, they will be deleted * when the application exits. * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return false if the dialog was canceled + * @deprecated since 5.71, use KIO::ApplicationLauncherJob with no service argument (or a null service) + * @code + * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(); + * job->setUrls(urls); + * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); + * if (tempFiles) { + * job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); + * } + * job->setSuggestedFileName(suggestedFileName); + * job->setStartupId(asn); + * job->start(); + * @endcode */ + KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::OpenUrlJob, see API docs for a code sample") static bool displayOpenWithDialog(const QList &lst, QWidget *window, bool tempFiles = false, const QString &suggestedFileName = QString(), - const QByteArray &asn = QByteArray()); // TODO deprecate and provide RunFlags() overload + const QByteArray &asn = QByteArray()); +#endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 0) /** * Quotes a string for the shell. * An empty string will @em not be quoted. * * @param str the string to quote. The quoted string will be written here * * @deprecated Since 4.0, use KShell::quoteArg() instead. @em Note that this function * behaves differently for empty arguments and returns the result * differently. */ KIOWIDGETS_DEPRECATED_VERSION(4, 0, "Use KShell::quoteArg(...)") static void shellQuote(QString &str); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 0) /** * Processes a Exec= line as found in .desktop files. * @param _service the service to extract information from. * @param _urls The urls the service should open. * @param tempFiles if true and urls are local files, they will be deleted * when the application exits. * @param suggestedFileName see setSuggestedFileName * * @return a list of arguments suitable for QProcess. * @deprecated since 5.0, use KIO::DesktopExecParser */ KIOWIDGETS_DEPRECATED_VERSION(5, 0, "Use KIO::DesktopExecParser") static QStringList processDesktopExec(const KService &_service, const QList &_urls, bool tempFiles = false, const QString &suggestedFileName = QString()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 0) /** * Given a full command line (e.g. the Exec= line from a .desktop file), * extract the name of the binary being run. * @param execLine the full command line * @param removePath if true, remove a (relative or absolute) path. E.g. /usr/bin/ls becomes ls. * @return the name of the executable to run * @deprecated since 5.0, use KIO::DesktopExecParser::executableName if removePath was true, * or KIO::DesktopExecParser::executablePath if removePath was false. */ KIOWIDGETS_DEPRECATED_VERSION(5, 0, "See API docs") static QString binaryName(const QString &execLine, bool removePath); #endif /** * Returns whether @p mimeType refers to an executable program instead * of a data file. */ static bool isExecutable(const QString &mimeType); /** * 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 or application/x-sharedlib. * To allow a script to run when the above rules are satisfied add the entry * @code * X-KDE-IsAlso=application/x-executable-script * @endcode * to the mimetype's desktop file. */ static bool isExecutableFile(const QUrl &url, const QString &mimetype); /** * @internal */ static bool checkStartupNotify(const QString &binName, const KService *service, bool *silent_arg, QByteArray *wmclass_arg); Q_SIGNALS: /** * Emitted when the operation finished. * This signal is emitted in all cases of completion, whether successful or with error. * @see hasFinished() */ void finished(); /** * Emitted when the operation had an error. * @see hasError() */ void error(); protected Q_SLOTS: /** * All following protected slots are used by subclasses of KRun! */ /** * This slot is called whenever the internal timer fired, * in order to move on to the next step. */ void slotTimeout(); /** * This slot is called when the scan job is finished. */ void slotScanFinished(KJob *); /** * This slot is called when the scan job has found out * the mime type. */ void slotScanMimeType(KIO::Job *, const QString &type); /** * Call this from subclasses when you have determined the mimetype. * It will call foundMimeType, but also sets up protection against deletion during message boxes. * @since 4.0.2 */ void mimeTypeDetermined(const QString &mimeType); /** * This slot is called when the 'stat' job has finished. */ virtual void slotStatResult(KJob *); protected: /** * All following protected methods are used by subclasses of KRun! */ /** * Initializes the krun object. */ virtual void init(); /** * Start scanning a file. */ virtual void scanFile(); /** * Called if the mimetype has been detected. The function runs * the application associated with this mimetype. * Reimplement this method to implement a different behavior, * like opening the component for displaying the URL embedded. * * Important: call setFinished(true) once you are done! * Usually at the end of the foundMimeType reimplementation, but if the * reimplementation is asynchronous (e.g. uses KIO jobs) then * it can be called later instead. */ virtual void foundMimeType(const QString &type); /** * Kills the file scanning job. */ virtual void killJob(); /** * Called when KRun detects an error during the init phase. * * The default implementation shows a message box. * @since 5.0 */ virtual void handleInitError(int kioErrorCode, const QString &errorMsg); /** * Called when a KIO job started by KRun gives an error. * * The default implementation shows a message box. */ virtual void handleError(KJob *job); /** * Sets the url. */ void setUrl(const QUrl &url); /** * Returns the url. */ QUrl url() const; /** * Sets whether an error has occurred. */ void setError(bool error); /** * Sets whether progress information shall be shown. */ void setProgressInfo(bool progressInfo); /** * Returns whether progress information are shown. */ bool progressInfo() const; /** * Marks this 'KRun' instance as finished. */ void setFinished(bool finished); /** * Sets the job. */ void setJob(KIO::Job *job); /** * Returns the job. */ KIO::Job *job(); #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 4) /** * Returns the timer object. * @deprecated Since 4.4. setFinished(true) now takes care of the timer().start(0), * so this can be removed. */ KIOWIDGETS_DEPRECATED_VERSION(4, 4, "See API docs") QTimer &timer(); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * Indicate that the next action is to scan the file. * @deprecated Since 4.1. Not useful in public API */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") void setDoScanFile(bool scanFile); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * Returns whether the file shall be scanned. * @deprecated Since 4.1. Not useful in public API */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") bool doScanFile() const; #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * Sets whether it is a directory. * @deprecated Since 4.1. Typo in the name, and not useful as a public method */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") void setIsDirecory(bool isDirectory); #endif /** * Returns whether it is a directory. */ bool isDirectory() const; #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * @deprecated Since 4.1. Not useful in public API */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") void setInitializeNextAction(bool initialize); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * @deprecated Since 4.1. Not useful in public API */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") bool initializeNextAction() const; #endif /** * Returns whether it is a local file. */ bool isLocalFile() const; private: friend class KRunPrivate; KRunPrivate *const d; }; #endif diff --git a/src/widgets/widgetsopenwithhandler.cpp b/src/widgets/widgetsopenwithhandler.cpp index f4147438..3f512d01 100644 --- a/src/widgets/widgetsopenwithhandler.cpp +++ b/src/widgets/widgetsopenwithhandler.cpp @@ -1,55 +1,73 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kopenwithdialog.h" #include "openurljob.h" #include "widgetsopenwithhandler.h" +#include #include #include -#include +#include #include +#ifdef Q_OS_WIN +#include "widgetsopenwithhandler_win.cpp" // displayNativeOpenWithDialog +#endif + KIO::WidgetsOpenWithHandler::WidgetsOpenWithHandler() : KIO::OpenWithHandlerInterface() { } KIO::WidgetsOpenWithHandler::~WidgetsOpenWithHandler() = default; -void KIO::WidgetsOpenWithHandler::promptUserForApplication(KIO::OpenUrlJob *job, const QList &urls, const QString &mimeType) +void KIO::WidgetsOpenWithHandler::promptUserForApplication(KJob *job, const QList &urls, const QString &mimeType) { QWidget *parentWidget = job ? KJobWidgets::window(job) : qApp->activeWindow(); +#ifdef Q_OS_WIN + KConfigGroup cfgGroup(KSharedConfig::openConfig(), QStringLiteral("KOpenWithDialog Settings")); + if (cfgGroup.readEntry("Native", true)) { + // Implemented in applicationlauncherjob_win.cpp + if (displayNativeOpenWithDialog(urls, parentWidget)) { + Q_EMIT handled(); + return; + } else { + // Some error happened with the Windows-specific code. Fallback to the KDE one... + } + } +#endif + KOpenWithDialog *dialog = new KOpenWithDialog(urls, mimeType, QString(), QString(), parentWidget); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &QDialog::accepted, this, [=]() { KService::Ptr service = dialog->service(); if (!service) { service = KService::Ptr(new KService(QString() /*name*/, dialog->text(), QString() /*icon*/)); } Q_EMIT serviceSelected(service); }); connect(dialog, &QDialog::rejected, this, [this]() { Q_EMIT canceled(); }); dialog->show(); } diff --git a/src/widgets/widgetsopenwithhandler.h b/src/widgets/widgetsopenwithhandler.h index 94d97832..e93efc80 100644 --- a/src/widgets/widgetsopenwithhandler.h +++ b/src/widgets/widgetsopenwithhandler.h @@ -1,50 +1,50 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef WIDGETSOPENWITHHANDLER_H #define WIDGETSOPENWITHHANDLER_H #include "openwithhandlerinterface.h" class QDialog; class QWidget; namespace KIO { // TODO KF6: Make KIO::JobUiDelegate inherit from WidgetsOpenWithHandler // (or even merge the two classes) // so that setDelegate(new KIO::JobUiDelegate) provides both dialog boxes on error // and the open-with dialog. class WidgetsOpenWithHandler : public OpenWithHandlerInterface { public: WidgetsOpenWithHandler(); ~WidgetsOpenWithHandler() override; - void promptUserForApplication(KIO::OpenUrlJob *job, const QList &urls, const QString &mimeType) override; + void promptUserForApplication(KJob *job, const QList &urls, const QString &mimeType) override; private: // Note: no d pointer because not exported at this point }; } #endif // WIDGETSOPENWITHHANDLER_H diff --git a/src/widgets/krun_win.cpp b/src/widgets/widgetsopenwithhandler_win.cpp similarity index 91% rename from src/widgets/krun_win.cpp rename to src/widgets/widgetsopenwithhandler_win.cpp index 72adf5a4..2364614e 100644 --- a/src/widgets/krun_win.cpp +++ b/src/widgets/widgetsopenwithhandler_win.cpp @@ -1,101 +1,96 @@ /* This file is part of the KDE libraries Copyright (C) 2008 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "krun.h" -#include "krun_p.h" - #include #include #include // TODO move to a shared lib static int runDll(WId windowId, const QString &libraryName, const QByteArray &functionName, const QString &arguments) { HMODULE libHandle = LoadLibraryW((LPCWSTR)libraryName.utf16()); if (!libHandle) { return 0; } typedef int (WINAPI * FunctionType)(HWND, HMODULE, LPCWSTR, int); #ifdef _WIN32_WCE QString functionNamestr = QString(functionName); FunctionType function = (FunctionType)GetProcAddressW(libHandle, functionNamestr.utf16()); #else FunctionType function = (FunctionType)GetProcAddress(libHandle, functionName.constData()); #endif if (!function) { return 0; } int result = function((HWND)windowId, libHandle, (LPCWSTR)arguments.utf16(), SW_SHOW); FreeLibrary(libHandle); return result; } static int runDll(WId windowId, const QString &libraryName, const QByteArray &functionName, const QByteArray &arguments) { HMODULE libHandle = LoadLibraryW((LPCWSTR)libraryName.utf16()); if (!libHandle) { return 0; } typedef int (WINAPI * FunctionType)(HWND, HMODULE, LPCSTR, int); #ifdef _WIN32_WCE QString functionNamestr = QString(functionName); FunctionType function = (FunctionType)GetProcAddressW(libHandle, functionNamestr.utf16()); #else FunctionType function = (FunctionType)GetProcAddress(libHandle, functionName.constData()); #endif if (!function) { return 0; } int result = function((HWND)windowId, libHandle, (LPCSTR)arguments.constData(), SW_SHOW); FreeLibrary(libHandle); return result; } // TODO move to a shared lib static int runDll(QWidget *parent, const QString &libraryName, const QByteArray &functionName, const QString &arguments) { return runDll(parent ? parent->winId() : 0, libraryName, functionName, arguments); } // Windows implementation using "OpenAs_RunDLL" entry -bool KRunPrivate::displayNativeOpenWithDialog(const QList &lst, QWidget *window, bool tempFiles, - const QString &suggestedFileName, const QByteArray &asn) +static bool displayNativeOpenWithDialog(const QList &lst, QWidget *window) { Q_UNUSED(tempFiles); Q_UNUSED(suggestedFileName); - Q_UNUSED(asn); QStringList fnames; for (const QUrl &url : lst) { - fnames += QDir::toNativeSeparators(url.path()); + fnames += url.isLocalFile() ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString(); } int result = runDll(window, QLatin1String("shell32.dll"), "OpenAs_RunDLLW", fnames.join(QLatin1Char(' '))); return result == 0; }