File indexing completed on 2024-03-24 15:33:19

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2014, 2020 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "applicationlauncherjobtest.h"
0009 #include "applicationlauncherjob.h"
0010 #include <kprocessrunner_p.h>
0011 
0012 #include "kiotesthelper.h" // createTestFile etc.
0013 #include "mockcoredelegateextensions.h"
0014 #include "mockguidelegateextensions.h"
0015 
0016 #include <KConfigGroup>
0017 #include <KDesktopFile>
0018 #include <KJobUiDelegate>
0019 #include <KService>
0020 
0021 #ifdef Q_OS_UNIX
0022 #include <signal.h> // kill
0023 #endif
0024 
0025 #include <QRegularExpression>
0026 #include <QStandardPaths>
0027 #include <QTemporaryDir>
0028 #include <QTest>
0029 
0030 QTEST_GUILESS_MAIN(ApplicationLauncherJobTest)
0031 
0032 void ApplicationLauncherJobTest::initTestCase()
0033 {
0034     QStandardPaths::setTestModeEnabled(true);
0035     m_tempService = createTempService();
0036 }
0037 
0038 void ApplicationLauncherJobTest::cleanupTestCase()
0039 {
0040     std::for_each(m_filesToRemove.cbegin(), m_filesToRemove.cend(), [](const QString &f) {
0041         QFile::remove(f);
0042     });
0043 }
0044 
0045 static const char s_tempServiceName[] = "applicationlauncherjobtest_service.desktop";
0046 
0047 static void createSrcFile(const QString path)
0048 {
0049     QFile srcFile(path);
0050     QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString()));
0051     srcFile.write("Hello world\n");
0052 }
0053 
0054 void ApplicationLauncherJobTest::startProcess_data()
0055 {
0056     QTest::addColumn<bool>("tempFile");
0057     QTest::addColumn<bool>("useExec");
0058     QTest::addColumn<int>("numFiles");
0059 
0060     QTest::newRow("1_file_exec") << false << true << 1;
0061     QTest::newRow("1_file_waitForStarted") << false << false << 1;
0062     QTest::newRow("1_tempfile_exec") << true << true << 1;
0063     QTest::newRow("1_tempfile_waitForStarted") << true << false << 1;
0064 
0065     QTest::newRow("2_files_exec") << false << true << 2;
0066     QTest::newRow("2_files_waitForStarted") << false << false << 2;
0067     QTest::newRow("2_tempfiles_exec") << true << true << 2;
0068     QTest::newRow("2_tempfiles_waitForStarted") << true << false << 2;
0069 }
0070 
0071 void ApplicationLauncherJobTest::startProcess()
0072 {
0073     QFETCH(bool, tempFile);
0074     QFETCH(bool, useExec);
0075     QFETCH(int, numFiles);
0076 
0077     // Given a service desktop file and a number of source files
0078     QTemporaryDir tempDir;
0079     const QString srcDir = tempDir.path();
0080     QList<QUrl> urls;
0081     for (int i = 0; i < numFiles; ++i) {
0082         const QString srcFile = srcDir + "/srcfile" + QString::number(i + 1);
0083         createSrcFile(srcFile);
0084         QVERIFY(QFile::exists(srcFile));
0085         urls.append(QUrl::fromLocalFile(srcFile));
0086     }
0087 
0088     // When running a ApplicationLauncherJob
0089     KService::Ptr servicePtr(new KService(m_tempService));
0090     KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this);
0091     job->setUrls(urls);
0092     if (tempFile) {
0093         job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
0094     }
0095     if (useExec) {
0096         QVERIFY2(job->exec(), qPrintable(job->errorString()));
0097     } else {
0098         job->start();
0099         QVERIFY(job->waitForStarted());
0100     }
0101     const QVector<qint64> pids = job->pids();
0102 
0103     // Then the service should be executed (which copies the source file to "dest")
0104     QCOMPARE(pids.count(), numFiles);
0105     QVERIFY(!pids.contains(0));
0106     for (int i = 0; i < numFiles; ++i) {
0107         const QString dest = srcDir + "/dest_srcfile" + QString::number(i + 1);
0108         QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest));
0109         QVERIFY(QFile::exists(srcDir + "/srcfile" + QString::number(i + 1))); // if tempfile is true, kioexec will delete it... in 3 minutes.
0110         QVERIFY(QFile::remove(dest)); // cleanup
0111     }
0112 
0113 #ifdef Q_OS_UNIX
0114     // Kill the running kioexec processes
0115     for (qint64 pid : pids) {
0116         ::kill(pid, SIGTERM);
0117     }
0118 #endif
0119 
0120     // The kioexec processes that are waiting for 3 minutes and got killed above,
0121     // will now trigger KProcessRunner::slotProcessError, KProcessRunner::slotProcessExited and delete the KProcessRunner.
0122     // We wait for that to happen otherwise it gets confusing to see that output from later tests.
0123     QTRY_COMPARE(KProcessRunner::instanceCount(), 0);
0124 }
0125 
0126 void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile_data()
0127 {
0128     QTest::addColumn<bool>("withHandler");
0129     QTest::addColumn<bool>("handlerRetVal");
0130     QTest::addColumn<bool>("useExec");
0131     QTest::addColumn<bool>("serviceAction");
0132 
0133     QTest::newRow("no_handler_exec") << false << false << true << false;
0134     QTest::newRow("handler_false_exec") << true << false << true << false;
0135     QTest::newRow("handler_true_exec") << true << true << true << false;
0136     QTest::newRow("no_handler_waitForStarted") << false << false << false << false;
0137     QTest::newRow("handler_false_waitForStarted") << true << false << false << false;
0138     QTest::newRow("handler_true_waitForStarted") << true << true << false << false;
0139 
0140     // Test the ctor that takes a KServiceAction
0141     QTest::newRow("serviceaction_no_handler_exec") << false << false << true << true;
0142     QTest::newRow("serviceaction_handler_false_exec") << true << false << true << true;
0143     QTest::newRow("serviceaction_handler_true_exec") << true << true << true << true;
0144     QTest::newRow("serviceaction_no_handler_waitForStarted") << false << false << false << true;
0145     QTest::newRow("serviceaction_handler_false_waitForStarted") << true << false << false << true;
0146     QTest::newRow("serviceaction_handler_true_waitForStarted") << true << true << false << true;
0147 }
0148 
0149 void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile()
0150 {
0151     QFETCH(bool, useExec);
0152     QFETCH(bool, withHandler);
0153     QFETCH(bool, handlerRetVal);
0154     QFETCH(bool, serviceAction);
0155 
0156     // Given a .desktop file in a temporary directory (outside the trusted paths)
0157     QTemporaryDir tempDir;
0158     const QString srcDir = tempDir.path();
0159     const QString desktopFilePath = srcDir + "/shouldfail.desktop";
0160     writeTempServiceDesktopFile(desktopFilePath);
0161     m_filesToRemove.append(desktopFilePath);
0162 
0163     const QString srcFile = srcDir + "/srcfile";
0164     createSrcFile(srcFile);
0165     const QList<QUrl> urls{QUrl::fromLocalFile(srcFile)};
0166     KService::Ptr servicePtr(new KService(desktopFilePath));
0167 
0168     KIO::ApplicationLauncherJob *job =
0169         !serviceAction ? new KIO::ApplicationLauncherJob(servicePtr, this) : new KIO::ApplicationLauncherJob(servicePtr->actions().at(0), this);
0170 
0171     job->setUrls(urls);
0172     job->setUiDelegate(new KJobUiDelegate);
0173     MockUntrustedProgramHandler *handler = withHandler ? new MockUntrustedProgramHandler(job->uiDelegate()) : nullptr;
0174     if (handler) {
0175         handler->setRetVal(handlerRetVal);
0176     }
0177     bool success;
0178     if (useExec) {
0179         success = job->exec();
0180     } else {
0181         job->start();
0182         success = job->waitForStarted();
0183     }
0184     if (!withHandler) {
0185         QVERIFY(!success);
0186         QCOMPARE(job->error(), KJob::UserDefinedError);
0187         QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file."));
0188     } else {
0189         if (handlerRetVal) {
0190             QVERIFY(success);
0191             // check that the handler was called (before any event loop deletes the job...)
0192             QCOMPARE(handler->m_calls.count(), 1);
0193             QCOMPARE(handler->m_calls.at(0), QStringLiteral("KRunUnittestService"));
0194 
0195             const QString dest = srcDir + (!serviceAction ? QLatin1String{"/dest_srcfile"} : QLatin1String{"/actionDest_srcfile"});
0196             QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest));
0197 
0198             // The actual shell process will race against the deletion of the QTemporaryDir,
0199             // so don't be surprised by stderr like getcwd: cannot access parent directories: No such file or directory
0200             QTest::qWait(50); // this helps a bit
0201         } else {
0202             QVERIFY(!success);
0203             QCOMPARE(job->error(), KIO::ERR_USER_CANCELED);
0204         }
0205     }
0206 }
0207 
0208 void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable_data()
0209 {
0210     QTest::addColumn<bool>("tempFile");
0211     QTest::addColumn<bool>("fullPath");
0212 
0213     QTest::newRow("file") << false << false;
0214     QTest::newRow("tempFile") << true << false;
0215     QTest::newRow("file_fullPath") << false << true;
0216     QTest::newRow("tempFile_fullPath") << true << true;
0217 }
0218 
0219 void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable()
0220 {
0221     QFETCH(bool, tempFile);
0222     QFETCH(bool, fullPath);
0223 
0224     const QString desktopFilePath =
0225         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/non_existing_executable.desktop");
0226     KDesktopFile file(desktopFilePath);
0227     KConfigGroup group = file.desktopGroup();
0228     group.writeEntry("Name", "KRunUnittestService");
0229     group.writeEntry("Type", "Service");
0230     if (fullPath) {
0231         group.writeEntry("Exec", "/usr/bin/does_not_exist %f %d/dest_%n");
0232     } else {
0233         group.writeEntry("Exec", "does_not_exist %f %d/dest_%n");
0234     }
0235     file.sync();
0236 
0237     KService::Ptr servicePtr(new KService(desktopFilePath));
0238     KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this);
0239     job->setUrls({QUrl::fromLocalFile(desktopFilePath)}); // just to have one URL as argument, as the desktop file expects
0240     if (tempFile) {
0241         job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
0242     }
0243     QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Could not find the program '.*'")); // from KProcessRunner
0244     QVERIFY(!job->exec());
0245     QCOMPARE(job->error(), KJob::UserDefinedError);
0246     if (fullPath) {
0247         QCOMPARE(job->errorString(), QStringLiteral("Could not find the program '/usr/bin/does_not_exist'"));
0248     } else {
0249         QCOMPARE(job->errorString(), QStringLiteral("Could not find the program 'does_not_exist'"));
0250     }
0251     QFile::remove(desktopFilePath);
0252 }
0253 
0254 void ApplicationLauncherJobTest::shouldFailOnInvalidService()
0255 {
0256     const QString desktopFilePath =
0257         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop");
0258     KDesktopFile file(desktopFilePath);
0259     KConfigGroup group = file.desktopGroup();
0260     group.writeEntry("Name", "KRunUnittestService");
0261     group.writeEntry("Type", "NoSuchType");
0262     group.writeEntry("Exec", "does_not_exist");
0263     file.sync();
0264 
0265     QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The desktop entry file \".*\" has Type.*\"NoSuchType\" instead of \"Application\" or \"Service\""));
0266     KService::Ptr servicePtr(new KService(desktopFilePath));
0267     KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this);
0268     QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The desktop entry file.*is not valid")); // from KProcessRunner
0269     QVERIFY(!job->exec());
0270     QCOMPARE(job->error(), KJob::UserDefinedError);
0271     const QString expectedError = QStringLiteral("The desktop entry file\n%1\nis not valid.").arg(desktopFilePath);
0272     QCOMPARE(job->errorString(), expectedError);
0273 
0274     QFile::remove(desktopFilePath);
0275 }
0276 
0277 void ApplicationLauncherJobTest::shouldFailOnServiceWithNoExec()
0278 {
0279     const QString desktopFilePath =
0280         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop");
0281     KDesktopFile file(desktopFilePath);
0282     KConfigGroup group = file.desktopGroup();
0283     group.writeEntry("Name", "KRunUnittestServiceNoExec");
0284     group.writeEntry("Type", "Service");
0285     file.sync();
0286 
0287     QTest::ignoreMessage(QtWarningMsg, qPrintable(QString("No Exec field in \"%1\"").arg(desktopFilePath))); // from KService
0288     KService::Ptr servicePtr(new KService(desktopFilePath));
0289     KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this);
0290     QTest::ignoreMessage(QtWarningMsg, QRegularExpression("No Exec field in .*")); // from KProcessRunner
0291     QVERIFY(!job->exec());
0292     QCOMPARE(job->error(), KJob::UserDefinedError);
0293     QCOMPARE(job->errorString(), QStringLiteral("No Exec field in %1").arg(desktopFilePath));
0294 
0295     QFile::remove(desktopFilePath);
0296 }
0297 
0298 void ApplicationLauncherJobTest::shouldFailOnExecutableWithoutPermissions()
0299 {
0300 #ifdef Q_OS_UNIX
0301     // Given an executable shell script that copies "src" to "dest" (we'll cheat with the MIME type to treat it like a native binary)
0302     QTemporaryDir tempDir;
0303     const QString dir = tempDir.path();
0304     const QString scriptFilePath = dir + QStringLiteral("/script.sh");
0305     QFile scriptFile(scriptFilePath);
0306     QVERIFY(scriptFile.open(QIODevice::WriteOnly));
0307     scriptFile.write("#!/bin/sh\ncp src dest");
0308     scriptFile.close();
0309     // Note that it's missing executable permissions
0310 
0311     const QString desktopFilePath =
0312         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop");
0313     KDesktopFile file(desktopFilePath);
0314     KConfigGroup group = file.desktopGroup();
0315     group.writeEntry("Name", "KRunUnittestServiceNoPermission");
0316     group.writeEntry("Type", "Service");
0317     group.writeEntry("Exec", scriptFilePath);
0318     file.sync();
0319 
0320     KService::Ptr servicePtr(new KService(desktopFilePath));
0321     KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this);
0322     QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The program .* is missing executable permissions.")); // from KProcessRunner
0323     QVERIFY(!job->exec());
0324     QCOMPARE(job->error(), KJob::UserDefinedError);
0325     QCOMPARE(job->errorString(), QStringLiteral("The program '%1' is missing executable permissions.").arg(scriptFilePath));
0326 
0327     QFile::remove(desktopFilePath);
0328 #else
0329     QSKIP("This test is not run on Windows");
0330 #endif
0331 }
0332 
0333 void ApplicationLauncherJobTest::showOpenWithDialog_data()
0334 {
0335     QTest::addColumn<bool>("withHandler");
0336     QTest::addColumn<bool>("handlerRetVal");
0337     QTest::addColumn<bool>("nullService");
0338 
0339     for (bool nullService : {false, true}) {
0340         const char *nullServiceStr = nullService ? "pass_null_service" : "default_ctor";
0341         QTest::addRow("without_handler_%s", nullServiceStr) << false << false << nullService;
0342         QTest::addRow("false_canceled_%s", nullServiceStr) << true << false << nullService;
0343         QTest::addRow("true_service_selected_%s", nullServiceStr) << true << true << nullService;
0344     }
0345 }
0346 
0347 void ApplicationLauncherJobTest::showOpenWithDialog()
0348 {
0349 #ifdef Q_OS_UNIX
0350     QFETCH(bool, withHandler);
0351     QFETCH(bool, handlerRetVal);
0352     QFETCH(bool, nullService);
0353 
0354     // Given a local text file (we could test multiple files, too...)
0355     QTemporaryDir tempDir;
0356     const QString srcDir = tempDir.path();
0357     const QString srcFile = srcDir + QLatin1String("/file.txt");
0358     createSrcFile(srcFile);
0359 
0360     KIO::ApplicationLauncherJob *job = nullService ? new KIO::ApplicationLauncherJob(KService::Ptr(), this) : new KIO::ApplicationLauncherJob(this);
0361     job->setUrls({QUrl::fromLocalFile(srcFile)});
0362     job->setUiDelegate(new KJobUiDelegate);
0363     MockOpenWithHandler *openWithHandler = withHandler ? new MockOpenWithHandler(job->uiDelegate()) : nullptr;
0364     KService::Ptr service = KService::serviceByDesktopName(QString(s_tempServiceName).remove(".desktop"));
0365     QVERIFY(service);
0366     if (withHandler) {
0367         openWithHandler->m_chosenService = handlerRetVal ? service : KService::Ptr{};
0368     }
0369 
0370     const bool success = job->exec();
0371 
0372     // Then --- it depends on what the user says via the handler
0373     if (withHandler) {
0374         QCOMPARE(openWithHandler->m_urls.count(), 1);
0375         QCOMPARE(openWithHandler->m_mimeTypes.count(), 1);
0376         QCOMPARE(openWithHandler->m_mimeTypes.at(0), QStringLiteral("text/plain")); // the job doesn't have the information
0377         if (handlerRetVal) {
0378             QVERIFY2(success, qPrintable(job->errorString()));
0379             // If the user chose a service, it should be executed (it writes to "dest")
0380             const QString dest = srcDir + "/dest_file.txt";
0381             QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest));
0382         } else {
0383             QVERIFY(!success);
0384             QCOMPARE(job->error(), KIO::ERR_USER_CANCELED);
0385         }
0386     } else {
0387         QVERIFY(!success);
0388         QCOMPARE(job->error(), KJob::UserDefinedError);
0389     }
0390 #else
0391     QSKIP("Test skipped on Windows because the code ends up in QDesktopServices::openUrl");
0392 #endif
0393 }
0394 
0395 void ApplicationLauncherJobTest::writeTempServiceDesktopFile(const QString &filePath)
0396 {
0397     if (QFile::exists(filePath)) {
0398         return;
0399     }
0400 
0401     KDesktopFile file(filePath);
0402     KConfigGroup group = file.desktopGroup();
0403     group.writeEntry("Name", "KRunUnittestService");
0404     group.writeEntry("Type", "Service");
0405 #ifdef Q_OS_WIN
0406     group.writeEntry("Exec", "copy.exe %f %d/dest_%n");
0407 #else
0408     group.writeEntry("Exec", "cd %d ; cp %f %d/dest_%n"); // cd is just to show that we can't do QFile::exists(binary)
0409 #endif
0410 
0411     // Add a desktop Action
0412     group.writeEntry("Actions", "SubServiceAction");
0413 
0414     KConfigGroup actGroup = file.actionGroup(QStringLiteral("SubServiceAction"));
0415     actGroup.writeEntry("Name", "ServiceActionTest");
0416 #ifdef Q_OS_WIN
0417     actGroup.writeEntry("Exec", "copy.exe %f %d/actionDest_%n");
0418 #else
0419     actGroup.writeEntry("Exec", "cd %d ; cp %f %d/actionDest_%n"); // cd is just to show that we can't do QFile::exists(binary)
0420 #endif
0421 
0422     file.sync();
0423 }
0424 
0425 QString ApplicationLauncherJobTest::createTempService()
0426 {
0427     const QString fileName = s_tempServiceName;
0428     const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + fileName;
0429     writeTempServiceDesktopFile(fakeService);
0430     m_filesToRemove.append(fakeService);
0431     return fakeService;
0432 }
0433 
0434 #include "moc_applicationlauncherjobtest.cpp"