File indexing completed on 2024-04-07 07:24:54

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