File indexing completed on 2024-03-24 03:59:38
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2008 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 "kmimeassociations_p.h" 0009 #include "ksycoca_p.h" 0010 #include "setupxdgdirs.h" 0011 #include <KConfigGroup> 0012 #include <KDesktopFile> 0013 #include <QDebug> 0014 #include <QDir> 0015 #include <QMimeDatabase> 0016 #include <QMimeType> 0017 #include <QSignalSpy> 0018 #include <QTemporaryDir> 0019 #include <QTemporaryFile> 0020 #include <QTest> 0021 #include <kapplicationtrader.h> 0022 #include <kbuildsycoca_p.h> 0023 #include <kservicefactory_p.h> 0024 #include <ksycoca.h> 0025 0026 using namespace Qt::StringLiterals; 0027 0028 // We need a factory that returns the same KService::Ptr every time it's asked for a given service. 0029 // Otherwise the changes to the service's serviceTypes by KMimeAssociationsTest have no effect 0030 class FakeServiceFactory : public KServiceFactory 0031 { 0032 public: 0033 FakeServiceFactory(KSycoca *db) 0034 : KServiceFactory(db) 0035 { 0036 } 0037 ~FakeServiceFactory() override; 0038 0039 KService::Ptr findServiceByMenuId(const QString &name) override 0040 { 0041 // qDebug() << name; 0042 KService::Ptr result = m_cache.value(name); 0043 if (!result) { 0044 result = KServiceFactory::findServiceByMenuId(name); 0045 m_cache.insert(name, result); 0046 } 0047 // qDebug() << name << result.data(); 0048 return result; 0049 } 0050 KService::Ptr findServiceByDesktopPath(const QString &name) override 0051 { 0052 KService::Ptr result = m_cache.value(name); // yeah, same cache, I don't care :) 0053 if (!result) { 0054 result = KServiceFactory::findServiceByDesktopPath(name); 0055 m_cache.insert(name, result); 0056 } 0057 return result; 0058 } 0059 0060 private: 0061 QMap<QString, KService::Ptr> m_cache; 0062 }; 0063 0064 static QString menusDir() 0065 { 0066 return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String{"/menus"}; 0067 } 0068 0069 // Helper method for all the trader tests, comes from kmimetypetest.cpp 0070 static bool offerListHasService(const KService::List &offers, const QString &entryPath, bool expected /* if set, show error if not found */) 0071 { 0072 // ksycoca resolves to canonical paths, so do it here as well 0073 const QString realPath = QFileInfo(entryPath).canonicalFilePath(); 0074 Q_ASSERT(!realPath.isEmpty()); 0075 0076 bool found = false; 0077 for (const KService::Ptr &serv : offers) { 0078 if (serv->entryPath() == realPath) { 0079 if (found) { // should be there only once 0080 qWarning("ERROR: %s was found twice in the list", qPrintable(realPath)); 0081 return false; // make test fail 0082 } 0083 found = true; 0084 } 0085 } 0086 if (!found && expected) { 0087 qWarning() << "ERROR:" << realPath << "not found in offer list. Here's the full list:"; 0088 for (const KService::Ptr &serv : offers) { 0089 qDebug() << serv->entryPath(); 0090 } 0091 } 0092 return found; 0093 } 0094 0095 static void writeAppDesktopFile(const QString &path, const QStringList &mimeTypes, int initialPreference = 1) 0096 { 0097 KDesktopFile file(path); 0098 KConfigGroup group = file.desktopGroup(); 0099 group.writeEntry("Name", "FakeApplication"); 0100 group.writeEntry("Type", "Application"); 0101 group.writeEntry("Exec", "ls"); 0102 group.writeEntry("OnlyShowIn", "KDE;UDE"); 0103 group.writeEntry("NotShowIn", "GNOME"); 0104 group.writeEntry("InitialPreference", initialPreference); 0105 group.writeXdgListEntry("MimeType", mimeTypes); 0106 } 0107 0108 static void writeNonKDEAppDesktopFile(const QString &path, const QStringList &mimeTypes) // bug 427469 0109 { 0110 KDesktopFile file(path); 0111 KConfigGroup group = file.desktopGroup(); 0112 group.writeEntry("Name", "FakeApplication"); 0113 group.writeEntry("Type", "Application"); 0114 group.writeEntry("Exec", "ls"); 0115 group.writeEntry("NotShowIn", "KDE"); 0116 group.writeXdgListEntry("MimeType", mimeTypes); 0117 } 0118 0119 /** 0120 * This unit test verifies the parsing of mimeapps.list files, both directly 0121 * and via kbuildsycoca (and making trader queries). 0122 */ 0123 class KMimeAssociationsTest : public QObject 0124 { 0125 Q_OBJECT 0126 private Q_SLOTS: 0127 void initTestCase() 0128 { 0129 setupXdgDirs(); 0130 QStandardPaths::setTestModeEnabled(true); 0131 // The Plasma bit makes no sense, but this is just to test that this is treated as a colon-separated list 0132 qputenv("XDG_CURRENT_DESKTOP", "KDE:Plasma"); 0133 0134 m_localConfig = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/'); 0135 QDir(m_localConfig).removeRecursively(); 0136 QVERIFY(QDir().mkpath(m_localConfig)); 0137 m_localApps = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/'); 0138 QDir(m_localApps).removeRecursively(); 0139 QVERIFY(QDir().mkpath(m_localApps)); 0140 QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/'); 0141 QDir(cacheDir).removeRecursively(); 0142 0143 QDir(menusDir()).removeRecursively(); 0144 QDir(menusDir()).mkpath(QStringLiteral(".")); 0145 QFile::copy(QFINDTESTDATA("test-applications.menu"), menusDir() + QLatin1String("/applications.menu")); 0146 0147 // Create fake application (associated with text/plain in mimeapps.list) 0148 fakeTextApplication = m_localApps + QLatin1String{"faketextapplication.desktop"}; 0149 writeAppDesktopFile(fakeTextApplication, QStringList() << QStringLiteral("text/plain")); 0150 0151 // Create fake application (associated with text/plain in mimeapps.list) 0152 fakeTextApplicationPrefixed = m_localApps + QLatin1String{"fakepfx/faketextapplicationpfx.desktop"}; 0153 writeAppDesktopFile(fakeTextApplicationPrefixed, QStringList() << QStringLiteral("text/plain")); 0154 0155 // A fake "default" application for text/plain (high initial preference, but not in mimeapps.list) 0156 fakeDefaultTextApplication = m_localApps + QLatin1String{"fakedefaulttextapplication.desktop"}; 0157 writeAppDesktopFile(fakeDefaultTextApplication, QStringList() << QStringLiteral("text/plain"), 9); 0158 0159 // An app (like emacs) listing explicitly the derived mimetype (c-src); not in mimeapps.list 0160 // This interacted badly with mimeapps.list listing another app for text/plain, but the 0161 // lookup found this app first, due to c-src. The fix: ignoring derived mimetypes when 0162 // the base mimetype is already listed. 0163 // 0164 // Also include aliases (msword), to check they don't cancel each other out. 0165 fakeCSrcApplication = m_localApps + QLatin1String{"fakecsrcmswordapplication.desktop"}; 0166 writeAppDesktopFile(fakeCSrcApplication, 0167 QStringList() << QStringLiteral("text/plain") << QStringLiteral("text/c-src") << QStringLiteral("application/vnd.ms-word") 0168 << QStringLiteral("application/msword"), 0169 8); 0170 0171 fakeJpegApplication = m_localApps + QLatin1String{"fakejpegapplication.desktop"}; 0172 writeAppDesktopFile(fakeJpegApplication, QStringList() << QStringLiteral("image/jpeg")); 0173 0174 fakeArkApplication = m_localApps + QLatin1String{"fakearkapplication.desktop"}; 0175 writeAppDesktopFile(fakeArkApplication, QStringList() << QStringLiteral("application/zip")); 0176 0177 fakeHtmlApplication = m_localApps + QLatin1String{"fakehtmlapplication.desktop"}; 0178 writeAppDesktopFile(fakeHtmlApplication, QStringList() << QStringLiteral("text/html")); 0179 0180 fakeHtmlApplicationPrefixed = m_localApps + QLatin1String{"fakepfx/fakehtmlapplicationpfx.desktop"}; 0181 writeAppDesktopFile(fakeHtmlApplicationPrefixed, QStringList() << QStringLiteral("text/html")); 0182 0183 fakeOktetaApplication = m_localApps + QLatin1String{"fakeoktetaapplication.desktop"}; 0184 writeAppDesktopFile(fakeOktetaApplication, QStringList() << QStringLiteral("application/octet-stream")); 0185 0186 const QString fakeGnomeRoller = m_localApps + QLatin1String{"fake.org.gnome.FileRoller.desktop"}; 0187 writeNonKDEAppDesktopFile(fakeGnomeRoller, QStringList() << QStringLiteral("application/x-7z-compressed")); 0188 0189 const QString fakeNautilus = m_localApps + QLatin1String{"fake.org.gnome.Nautilus.desktop"}; 0190 writeNonKDEAppDesktopFile(fakeNautilus, QStringList() << QStringLiteral("application/x-7z-compressed")); 0191 0192 // Update ksycoca in ~/.qttest after creating the above 0193 runKBuildSycoca(); 0194 0195 // Create factory on the heap and don't delete it. This must happen after 0196 // Sycoca is built, in case it did not exist before. 0197 // It registers to KSycoca, which deletes it at end of program execution. 0198 KServiceFactory *factory = new FakeServiceFactory(KSycoca::self()); 0199 KSycocaPrivate::self()->m_serviceFactory = factory; 0200 QCOMPARE(KSycocaPrivate::self()->serviceFactory(), factory); 0201 0202 // For debugging: print all services and their storageId 0203 #if 0 0204 const KService::List lst = KService::allServices(); 0205 QVERIFY(!lst.isEmpty()); 0206 for (const KService::Ptr &serv : lst) { 0207 qDebug() << serv->entryPath() << serv->storageId() /*<< serv->desktopEntryName()*/; 0208 } 0209 #endif 0210 0211 KService::Ptr fakeApplicationService = KService::serviceByStorageId(QStringLiteral("faketextapplication.desktop")); 0212 QVERIFY(fakeApplicationService); 0213 0214 m_mimeAppsFileContents = 0215 "[Added Associations]\n" 0216 "image/jpeg=fakejpegapplication.desktop;\n" 0217 "text/html=fakehtmlapplication.desktop;fakehtmlapplicationpfx.desktop;\n" 0218 "text/plain=fakepfx-faketextapplicationpfx.desktop;gvim.desktop;wine.desktop;idontexist.desktop;\n" 0219 // test alias resolution 0220 "application/x-pdf=fakejpegapplication.desktop;\n" 0221 // test x-scheme-handler (#358159) (missing trailing ';' as per xdg-mime bug...) 0222 "x-scheme-handler/mailto=faketextapplication.desktop\n" 0223 // test association with octet-stream (#425154) 0224 "application/octet-stream=fakeoktetaapplication.desktop\n" 0225 // test a non-kde app (#427469) 0226 "application/x-7z-compressed=fake.org.gnome.FileRoller.desktop;\n" 0227 "[Added KParts/ReadOnlyPart Associations]\n" 0228 "text/plain=katepart.desktop;\n" 0229 "[Removed Associations]\n" 0230 "image/jpeg=firefox.desktop;\n" 0231 "text/html=gvim.desktop;abiword.desktop;\n" 0232 "[Default Applications]\n" 0233 "text/plain=faketextapplication.desktop;second-faketextapplicationpfx.desktop\n"; 0234 // Expected results 0235 preferredApps[QStringLiteral("image/jpeg")] << QStringLiteral("fakejpegapplication.desktop"); 0236 preferredApps[QStringLiteral("application/pdf")] << QStringLiteral("fakejpegapplication.desktop"); 0237 preferredApps[QStringLiteral("text/plain")] << QStringLiteral("faketextapplication.desktop") << QStringLiteral("second-faketextapplicationpfx.desktop") 0238 << QStringLiteral("fakepfx-faketextapplicationpfx.desktop") << QStringLiteral("gvim.desktop"); 0239 preferredApps[QStringLiteral("text/x-csrc")] << QStringLiteral("faketextapplication.desktop") 0240 << QStringLiteral("fakepfx-faketextapplicationpfx.desktop") << QStringLiteral("gvim.desktop"); 0241 preferredApps[QStringLiteral("text/html")] << QStringLiteral("fakehtmlapplication.desktop") << QStringLiteral("fakepfx-fakehtmlapplicationpfx.desktop"); 0242 preferredApps[QStringLiteral("application/msword")] << QStringLiteral("fakecsrcmswordapplication.desktop"); 0243 preferredApps[QStringLiteral("x-scheme-handler/mailto")] << QStringLiteral("faketextapplication.desktop"); 0244 preferredApps[QStringLiteral("text/x-python")] << QStringLiteral("faketextapplication.desktop"); 0245 preferredApps[QStringLiteral("application/x-7z-compressed")] << QStringLiteral("fake.org.gnome.FileRoller.desktop"); 0246 removedApps[QStringLiteral("application/x-7z-compressed")] << QStringLiteral("fake.org.gnome.Nautilus.desktop"); 0247 removedApps[QStringLiteral("image/jpeg")] << QStringLiteral("firefox.desktop"); 0248 removedApps[QStringLiteral("text/html")] << QStringLiteral("gvim.desktop") << QStringLiteral("abiword.desktop"); 0249 0250 // Clean-up non-existing apps 0251 removeNonExisting(preferredApps); 0252 removeNonExisting(removedApps); 0253 } 0254 0255 void cleanupTestCase() 0256 { 0257 QFile::remove(m_localConfig + QLatin1String{"/mimeapps.list"}); 0258 runKBuildSycoca(); 0259 } 0260 0261 void testDefaultInitialPreference() 0262 { 0263 KService::Ptr service = KApplicationTrader::preferredService(QStringLiteral("text/plain")); 0264 QCOMPARE(service->entryPath(), fakeDefaultTextApplication); 0265 } 0266 0267 void testParseSingleFile() 0268 { 0269 KOfferHash offerHash; 0270 KMimeAssociations parser(offerHash, KSycocaPrivate::self()->serviceFactory()); 0271 0272 QTemporaryDir tempDir; 0273 QVERIFY(tempDir.isValid()); 0274 0275 QFile tempFile(tempDir.path() + QLatin1String{"/mimeapps.list"}); 0276 QVERIFY(tempFile.open(QIODevice::WriteOnly)); 0277 tempFile.write(m_mimeAppsFileContents); 0278 const QString fileName = tempFile.fileName(); 0279 tempFile.close(); 0280 0281 // QTest::ignoreMessage(QtDebugMsg, "findServiceByDesktopPath: idontexist.desktop not found"); 0282 parser.parseMimeAppsList(fileName, 100); 0283 0284 for (auto it = preferredApps.cbegin(), endIt = preferredApps.cend(); it != endIt; ++it) { 0285 const QString mime = it.key(); 0286 // The data for derived types and aliases isn't for this test (which only looks at mimeapps.list) 0287 if (mime == QLatin1String("text/x-csrc") // 0288 || mime == QLatin1String("text/x-python") // 0289 || mime == QLatin1String("application/msword")) { 0290 continue; 0291 } 0292 const QList<KServiceOffer> offers = offerHash.offersFor(mime); 0293 for (const QString &service : it.value()) { 0294 KService::Ptr serv = KService::serviceByStorageId(service); 0295 if (serv && !offersContains(offers, serv)) { 0296 qDebug() << "expected offer" << serv->entryPath() << "not in offers for" << mime << ":"; 0297 for (const KServiceOffer &offer : offers) { 0298 qDebug() << offer.service()->storageId(); 0299 } 0300 QFAIL("offer does not have servicetype"); 0301 } 0302 } 0303 } 0304 0305 for (auto it = removedApps.cbegin(), end = removedApps.cend(); it != end; ++it) { 0306 const QString mime = it.key(); 0307 const QList<KServiceOffer> offers = offerHash.offersFor(mime); 0308 for (const QString &service : it.value()) { 0309 KService::Ptr serv = KService::serviceByStorageId(service); 0310 if (serv && offersContains(offers, serv)) { 0311 // qDebug() << serv.data() << serv->entryPath() << "does not have" << mime; 0312 QFAIL("offer should not have servicetype"); 0313 } 0314 } 0315 } 0316 } 0317 0318 void testGlobalAndLocalFiles() 0319 { 0320 KOfferHash offerHash; 0321 KMimeAssociations parser(offerHash, KSycocaPrivate::self()->serviceFactory()); 0322 0323 // Write global file 0324 QTemporaryDir tempDirGlobal; 0325 QVERIFY(tempDirGlobal.isValid()); 0326 0327 QFile tempFileGlobal(tempDirGlobal.path() + QLatin1String{"/mimeapps.list"}); 0328 QVERIFY(tempFileGlobal.open(QIODevice::WriteOnly)); 0329 QByteArray globalAppsFileContents = 0330 "[Added Associations]\n" 0331 "image/jpeg=firefox.desktop;\n" // removed by local config 0332 "text/html=firefox.desktop;\n" // mdv 0333 "image/png=fakejpegapplication.desktop;\n"; 0334 tempFileGlobal.write(globalAppsFileContents); 0335 const QString globalFileName = tempFileGlobal.fileName(); 0336 tempFileGlobal.close(); 0337 0338 // We didn't keep it, so we need to write the local file again 0339 QTemporaryDir tempDir; 0340 QVERIFY(tempDir.isValid()); 0341 0342 QFile tempFile(tempDir.path() + QLatin1String{"/mimeapps.list"}); 0343 QVERIFY(tempFile.open(QIODevice::WriteOnly)); 0344 tempFile.write(m_mimeAppsFileContents); 0345 const QString fileName = tempFile.fileName(); 0346 tempFile.close(); 0347 0348 parser.parseMimeAppsList(globalFileName, 1000); 0349 parser.parseMimeAppsList(fileName, 1050); // += 50 is correct. 0350 0351 QList<KServiceOffer> offers = offerHash.offersFor(QStringLiteral("image/jpeg")); 0352 std::stable_sort(offers.begin(), offers.end()); // like kbuildservicefactory.cpp does 0353 const QStringList expectedJpegApps = preferredApps[QStringLiteral("image/jpeg")]; 0354 QCOMPARE(assembleOffers(offers), expectedJpegApps); 0355 0356 offers = offerHash.offersFor(QStringLiteral("text/html")); 0357 std::stable_sort(offers.begin(), offers.end()); 0358 QStringList textHtmlApps = preferredApps[QStringLiteral("text/html")]; 0359 if (KService::serviceByStorageId(QStringLiteral("firefox.desktop"))) { 0360 textHtmlApps.append(QStringLiteral("firefox.desktop")); 0361 } 0362 qDebug() << assembleOffers(offers); 0363 QCOMPARE(assembleOffers(offers), textHtmlApps); 0364 0365 offers = offerHash.offersFor(QStringLiteral("image/png")); 0366 std::stable_sort(offers.begin(), offers.end()); 0367 QCOMPARE(assembleOffers(offers), QStringList() << QStringLiteral("fakejpegapplication.desktop")); 0368 } 0369 0370 void testSetupRealFile() 0371 { 0372 writeToMimeApps(m_mimeAppsFileContents); 0373 0374 // Test a trader query 0375 KService::List offers = KApplicationTrader::queryByMimeType(QStringLiteral("image/jpeg")); 0376 QVERIFY(!offers.isEmpty()); 0377 QCOMPARE(offers.first()->storageId(), QStringLiteral("fakejpegapplication.desktop")); 0378 0379 // Now the generic variant of the above test: 0380 // for each mimetype, check that the preferred apps are as specified 0381 for (auto it = preferredApps.cbegin(), endIt = preferredApps.cend(); it != endIt; ++it) { 0382 const QString mime = it.key(); 0383 const KService::List offers = KApplicationTrader::queryByMimeType(mime); 0384 const QStringList offerIds = assembleServices(offers, it.value().count()); 0385 if (offerIds != it.value()) { 0386 qDebug() << "offers for" << mime << ":"; 0387 for (int i = 0; i < offers.count(); ++i) { 0388 qDebug() << " " << i << ":" << offers[i]->storageId(); 0389 } 0390 qDebug() << " Expected:" << it.value(); 0391 const QStringList expectedPreferredServices = it.value(); 0392 for (int i = 0; i < expectedPreferredServices.count(); ++i) { 0393 qDebug() << mime << i << expectedPreferredServices[i]; 0394 // QCOMPARE(expectedPreferredServices[i], offers[i]->storageId()); 0395 } 0396 } 0397 QCOMPARE(offerIds, it.value()); 0398 } 0399 for (auto it = removedApps.constBegin(), endIt = removedApps.constEnd(); it != endIt; ++it) { 0400 const QString mime = it.key(); 0401 const KService::List offers = KApplicationTrader::queryByMimeType(mime); 0402 const QStringList offerIds = assembleServices(offers); 0403 for (const QString &service : it.value()) { 0404 const QString error = QStringLiteral("Offers for %1 should not contain %2").arg(mime, service); 0405 QVERIFY2(!offerIds.contains(service), qPrintable(error)); 0406 } 0407 } 0408 } 0409 0410 void testMultipleInheritance() 0411 { 0412 // application/x-shellscript inherits from both text/plain and application/x-executable 0413 KService::List offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/x-shellscript")); 0414 QVERIFY(offerListHasService(offers, fakeTextApplication, true)); 0415 } 0416 0417 void testRemoveAssociationFromParent() 0418 { 0419 // I removed kate from text/plain, and it would still appear in text/x-java. 0420 0421 // First, let's check our fake app is associated with text/plain 0422 KService::List offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain")); 0423 QVERIFY(offerListHasService(offers, fakeTextApplication, true)); 0424 0425 writeToMimeApps( 0426 QByteArray("[Removed Associations]\n" 0427 "text/plain=faketextapplication.desktop;\n")); 0428 0429 offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain")); 0430 QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); 0431 0432 offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/x-java")); 0433 QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); 0434 } 0435 0436 void testRemovedImplicitAssociation() // remove (implicit) assoc from derived mimetype 0437 { 0438 // #164584: Removing ark from opendocument.text didn't work 0439 const QString opendocument = QStringLiteral("application/vnd.oasis.opendocument.text"); 0440 0441 // [sanity checking of s-m-i installation] 0442 QMimeType mime = QMimeDatabase().mimeTypeForName(opendocument); 0443 QVERIFY(mime.isValid()); 0444 if (!mime.inherits(QStringLiteral("application/zip"))) { 0445 // CentOS patches out the application/zip inheritance from application/vnd.oasis.opendocument.text!! Grmbl. 0446 QSKIP("Broken distro where application/vnd.oasis.opendocument.text doesn't inherit from application/zip"); 0447 } 0448 0449 KService::List offers = KApplicationTrader::queryByMimeType(opendocument); 0450 QVERIFY(offerListHasService(offers, fakeArkApplication, true)); 0451 0452 writeToMimeApps( 0453 QByteArray("[Removed Associations]\n" 0454 "application/vnd.oasis.opendocument.text=fakearkapplication.desktop;\n")); 0455 0456 offers = KApplicationTrader::queryByMimeType(opendocument); 0457 QVERIFY(!offerListHasService(offers, fakeArkApplication, false)); 0458 0459 offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/zip")); 0460 QVERIFY(offerListHasService(offers, fakeArkApplication, true)); 0461 } 0462 0463 void testRemovedImplicitAssociation178560() 0464 { 0465 // #178560: Removing ark from interface/x-winamp-skin didn't work 0466 // Using application/x-kns (another zip-derived mimetype) nowadays. 0467 const QString mime = QStringLiteral("application/x-kns"); 0468 0469 // That mimetype comes from kcoreaddons, let's make sure it's properly installed 0470 { 0471 QMimeDatabase db; 0472 QMimeType mime = db.mimeTypeForName(QStringLiteral("application/x-kns")); 0473 QVERIFY(mime.isValid()); 0474 QCOMPARE(mime.name(), QStringLiteral("application/x-kns")); 0475 QVERIFY(mime.inherits(QStringLiteral("application/zip"))); 0476 } 0477 0478 KService::List offers = KApplicationTrader::queryByMimeType(mime); 0479 QVERIFY(offerListHasService(offers, fakeArkApplication, true)); 0480 0481 writeToMimeApps( 0482 QByteArray("[Removed Associations]\n" 0483 "application/x-kns=fakearkapplication.desktop;\n")); 0484 0485 offers = KApplicationTrader::queryByMimeType(mime); 0486 QVERIFY(!offerListHasService(offers, fakeArkApplication, false)); 0487 0488 offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/zip")); 0489 QVERIFY(offerListHasService(offers, fakeArkApplication, true)); 0490 } 0491 0492 // remove assoc from a mime which is both a parent and a derived mimetype 0493 void testRemovedMiddleAssociation() 0494 { 0495 // More tricky: x-theme inherits x-desktop inherits text/plain, 0496 // if we remove an association for x-desktop then x-theme shouldn't 0497 // get it from text/plain... 0498 0499 KService::List offers; 0500 writeToMimeApps( 0501 QByteArray("[Removed Associations]\n" 0502 "application/x-desktop=faketextapplication.desktop;\n")); 0503 0504 offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain")); 0505 QVERIFY(offerListHasService(offers, fakeTextApplication, true)); 0506 0507 offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/x-desktop")); 0508 QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); 0509 0510 offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/x-theme")); 0511 QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); 0512 } 0513 0514 void testCorrectFallbackOrder() 0515 { 0516 // Verify that a "more specific type" is used before a "less-specifc" 0517 // application/ecmascript inherits from both application/x-executable and text/plain 0518 // In this example we should use both of these first which has an app set compared to the less 0519 // specifc application/octet-stream via application/x-executable 0520 auto queryOnlyDirect = [](const QString &mimeType) { 0521 return KApplicationTrader::queryByMimeType(mimeType, [&mimeType](const KService::Ptr &service) { 0522 return service->mimeTypes().contains(mimeType); 0523 }); 0524 }; 0525 const auto offers = assembleServices(KApplicationTrader::queryByMimeType(u"text/javascript"_s)); 0526 const auto textOffers = assembleServices(queryOnlyDirect(u"text/plain"_s)); 0527 const auto appOffers = assembleServices(queryOnlyDirect(u"application/x-executable"_s)); 0528 auto octetOffers = assembleServices(KApplicationTrader::queryByMimeType(u"application/octet-stream"_s)); 0529 // Apps that only support octet-stream but not text/plain nor application/x-executable 0530 octetOffers.removeIf([&textOffers, &appOffers](const QString &app) { 0531 return textOffers.contains(app) || appOffers.contains(app); 0532 }); 0533 QCOMPARE(offers.mid(offers.count() - octetOffers.size(), octetOffers.size()), octetOffers); 0534 } 0535 0536 private: 0537 typedef QMap<QString /*mimetype*/, QStringList> ExpectedResultsMap; 0538 0539 void runKBuildSycoca() 0540 { 0541 // Wait for notifyDatabaseChanged DBus signal 0542 // (The real KCM code simply does the refresh in a slot, asynchronously) 0543 QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged); 0544 0545 KBuildSycoca builder; 0546 QVERIFY(builder.recreate()); 0547 if (spy.isEmpty()) { 0548 spy.wait(); 0549 } 0550 } 0551 0552 void writeToMimeApps(const QByteArray &contents) 0553 { 0554 QString mimeAppsPath = m_localConfig + QLatin1String{"/mimeapps.list"}; 0555 QFile mimeAppsFile(mimeAppsPath); 0556 QVERIFY(mimeAppsFile.open(QIODevice::WriteOnly)); 0557 mimeAppsFile.write(contents); 0558 mimeAppsFile.close(); 0559 0560 runKBuildSycoca(); 0561 } 0562 0563 static bool offersContains(const QList<KServiceOffer> &offers, KService::Ptr serv) 0564 { 0565 for (const KServiceOffer &offer : offers) { 0566 if (offer.service()->storageId() == serv->storageId()) { 0567 return true; 0568 } 0569 } 0570 return false; 0571 } 0572 static QStringList assembleOffers(const QList<KServiceOffer> &offers) 0573 { 0574 QStringList lst; 0575 for (const KServiceOffer &offer : offers) { 0576 lst.append(offer.service()->storageId()); 0577 } 0578 return lst; 0579 } 0580 static QStringList assembleServices(const QList<KService::Ptr> &services, int maxCount = -1) 0581 { 0582 QStringList lst; 0583 for (const KService::Ptr &service : services) { 0584 lst.append(service->storageId()); 0585 if (maxCount > -1 && lst.count() == maxCount) { 0586 break; 0587 } 0588 } 0589 return lst; 0590 } 0591 0592 void removeNonExisting(ExpectedResultsMap &erm) 0593 { 0594 for (auto it = erm.begin(), endIt = erm.end(); it != endIt; ++it) { 0595 QMutableStringListIterator serv_it(it.value()); 0596 while (serv_it.hasNext()) { 0597 if (!KService::serviceByStorageId(serv_it.next())) { 0598 // qDebug() << "removing non-existing entry" << serv_it.value(); 0599 serv_it.remove(); 0600 } 0601 } 0602 } 0603 } 0604 QString m_localApps; 0605 QString m_localConfig; 0606 QByteArray m_mimeAppsFileContents; 0607 QString fakeTextApplication; 0608 QString fakeTextApplicationPrefixed; 0609 QString fakeDefaultTextApplication; 0610 QString fakeCSrcApplication; 0611 QString fakeJpegApplication; 0612 QString fakeHtmlApplication; 0613 QString fakeHtmlApplicationPrefixed; 0614 QString fakeArkApplication; 0615 QString fakeOktetaApplication; 0616 0617 ExpectedResultsMap preferredApps; 0618 ExpectedResultsMap removedApps; 0619 }; 0620 0621 FakeServiceFactory::~FakeServiceFactory() 0622 { 0623 } 0624 0625 QTEST_GUILESS_MAIN(KMimeAssociationsTest) 0626 0627 #include "kmimeassociationstest.moc"