File indexing completed on 2024-10-06 03:39:09
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"