File indexing completed on 2024-04-28 16:44:27
0001 /* This file is part of the KDE project 0002 SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org> 0003 SPDX-FileCopyrightText: 2003, 2007 David Faure <faure@kde.org> 0004 0005 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only 0006 */ 0007 0008 #include "mimetypedata.h" 0009 #include "mimetypewriter.h" 0010 #include <KApplicationTrader> 0011 #include <KParts/PartLoader> 0012 #include <QFileInfo> 0013 #include <QMimeDatabase> 0014 #include <QStandardPaths> 0015 #include <QXmlStreamReader> 0016 #include <kconfiggroup.h> 0017 #include <kprotocolmanager.h> 0018 #include <kservice.h> 0019 #include <ksharedconfig.h> 0020 #include <qdebug.h> 0021 0022 MimeTypeData::MimeTypeData(const QString &major) 0023 : m_askSave(AskSaveDefault) 0024 , m_bNewItem(false) 0025 , m_bFullInit(true) 0026 , m_isGroup(true) 0027 , m_appServicesModified(false) 0028 , m_embedServicesModified(false) 0029 , m_userSpecifiedIconModified(false) 0030 , m_major(major) 0031 { 0032 m_autoEmbed = readAutoEmbed(); 0033 } 0034 0035 MimeTypeData::MimeTypeData(const QMimeType &mime) 0036 : m_mimetype(mime) 0037 , m_askSave(AskSaveDefault) 0038 , // TODO: the code for initializing this is missing. FileTypeDetails initializes the checkbox instead... 0039 m_bNewItem(false) 0040 , m_bFullInit(false) 0041 , m_isGroup(false) 0042 , m_appServicesModified(false) 0043 , m_embedServicesModified(false) 0044 , m_userSpecifiedIconModified(false) 0045 { 0046 const QString mimeName = m_mimetype.name(); 0047 Q_ASSERT(!mimeName.isEmpty()); 0048 const int index = mimeName.indexOf(QLatin1Char('/')); 0049 if (index != -1) { 0050 m_major = mimeName.left(index); 0051 m_minor = mimeName.mid(index + 1); 0052 } else { 0053 m_major = mimeName; 0054 } 0055 initFromQMimeType(); 0056 } 0057 0058 MimeTypeData::MimeTypeData(const QString &mimeName, bool) 0059 : m_askSave(AskSaveDefault) 0060 , m_bNewItem(true) 0061 , m_bFullInit(false) 0062 , m_isGroup(false) 0063 , m_appServicesModified(false) 0064 , m_embedServicesModified(false) 0065 , m_userSpecifiedIconModified(false) 0066 { 0067 const int index = mimeName.indexOf(QLatin1Char('/')); 0068 if (index != -1) { 0069 m_major = mimeName.left(index); 0070 m_minor = mimeName.mid(index + 1); 0071 } else { 0072 m_major = mimeName; 0073 } 0074 m_autoEmbed = UseGroupSetting; 0075 // all the rest is empty by default 0076 } 0077 0078 void MimeTypeData::initFromQMimeType() 0079 { 0080 m_comment = m_mimetype.comment(); 0081 setPatterns(m_mimetype.globPatterns()); 0082 m_autoEmbed = readAutoEmbed(); 0083 0084 // Parse XML file to find out if the user specified a custom icon name 0085 QString file = name().toLower() + QLatin1String(".xml"); 0086 QStringList mimeFiles = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("mime/") + file); 0087 if (mimeFiles.isEmpty()) { 0088 // This is for shared-mime-info < 1.3 that did not lowecase mime names 0089 file = name() + QLatin1String(".xml"); 0090 mimeFiles = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("mime/") + file); 0091 if (mimeFiles.isEmpty()) { 0092 qWarning() << "No file found for" << file << ", even though the file appeared in a directory listing."; 0093 qWarning() << "Either it was just removed, or the directory doesn't have executable permission..."; 0094 qWarning() << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("mime"), QStandardPaths::LocateDirectory); 0095 return; 0096 } 0097 } 0098 0099 // Reverse iterator to get global first, then local. 0100 for (auto rIt = mimeFiles.crbegin(); rIt != mimeFiles.crend(); ++rIt) { 0101 const QString fullPath = *rIt; 0102 QFile qfile(fullPath); 0103 if (!qfile.open(QFile::ReadOnly)) { 0104 continue; 0105 } 0106 0107 QXmlStreamReader xml(&qfile); 0108 if (xml.readNextStartElement()) { 0109 if (xml.name() != QLatin1String("mime-type")) { 0110 continue; 0111 } 0112 const QString mimeName = xml.attributes().value(QLatin1String("type")).toString(); 0113 if (mimeName.isEmpty()) { 0114 continue; 0115 } 0116 if (QString::compare(mimeName, name(), Qt::CaseInsensitive) != 0) { 0117 qWarning() << "Got name" << mimeName << "in file" << file << "expected" << name(); 0118 } 0119 0120 while (xml.readNextStartElement()) { 0121 const auto tag = xml.name(); 0122 if (tag == QLatin1String("icon")) { 0123 m_userSpecifiedIcon = xml.attributes().value(QLatin1String("name")).toString(); 0124 } 0125 xml.skipCurrentElement(); 0126 } 0127 } 0128 } 0129 } 0130 0131 MimeTypeData::AutoEmbed MimeTypeData::readAutoEmbed() const 0132 { 0133 const KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals); 0134 const QString key = QStringLiteral("embed-") + name(); 0135 const KConfigGroup group(config, "EmbedSettings"); 0136 if (m_isGroup) { 0137 // embedding is false by default except for image/*, multipart/* and inode/* (hardcoded in konq) 0138 const bool defaultValue = (m_major == QLatin1String("image") || m_major == QLatin1String("multipart") || m_major == QLatin1String("inode")); 0139 return group.readEntry(key, defaultValue) ? Yes : No; 0140 } else { 0141 if (group.hasKey(key)) { 0142 return group.readEntry(key, false) ? Yes : No; 0143 } 0144 // TODO if ( !mimetype.property( "X-KDE-LocalProtocol" ).toString().isEmpty() ) 0145 // TODO return MimeTypeData::Yes; // embed by default for zip, tar etc. 0146 return MimeTypeData::UseGroupSetting; 0147 } 0148 } 0149 0150 void MimeTypeData::writeAutoEmbed() 0151 { 0152 KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals); 0153 if (!config->isConfigWritable(true)) { 0154 return; 0155 } 0156 0157 const QString key = QStringLiteral("embed-") + name(); 0158 KConfigGroup group(config, "EmbedSettings"); 0159 if (m_isGroup) { 0160 group.writeEntry(key, m_autoEmbed == Yes); 0161 } else { 0162 if (m_autoEmbed == UseGroupSetting) { 0163 group.deleteEntry(key); 0164 } else { 0165 group.writeEntry(key, m_autoEmbed == Yes); 0166 } 0167 } 0168 } 0169 0170 bool MimeTypeData::isEssential() const 0171 { 0172 // Keep in sync with KMimeType::checkEssentialMimeTypes 0173 const QString n = name(); 0174 if (n == QLatin1String("application/octet-stream")) { 0175 return true; 0176 } 0177 if (n == QLatin1String("inode/directory")) { 0178 return true; 0179 } 0180 if (n == QLatin1String("inode/blockdevice")) { 0181 return true; 0182 } 0183 if (n == QLatin1String("inode/chardevice")) { 0184 return true; 0185 } 0186 if (n == QLatin1String("inode/socket")) { 0187 return true; 0188 } 0189 if (n == QLatin1String("inode/fifo")) { 0190 return true; 0191 } 0192 if (n == QLatin1String("application/x-shellscript")) { 0193 return true; 0194 } 0195 if (n == QLatin1String("application/x-executable")) { 0196 return true; 0197 } 0198 if (n == QLatin1String("application/x-desktop")) { 0199 return true; 0200 } 0201 return false; 0202 } 0203 0204 void MimeTypeData::setUserSpecifiedIcon(const QString &icon) 0205 { 0206 if (icon == m_userSpecifiedIcon) { 0207 return; 0208 } 0209 m_userSpecifiedIcon = icon; 0210 m_userSpecifiedIconModified = true; 0211 } 0212 0213 QStringList MimeTypeData::getAppOffers() const 0214 { 0215 QStringList serviceIds; 0216 const KService::List offerList = KApplicationTrader::queryByMimeType(name()); 0217 for (const auto &servicePtr : offerList) { 0218 serviceIds.append(servicePtr->storageId()); 0219 } 0220 return serviceIds; 0221 } 0222 0223 QStringList MimeTypeData::getPartOffers() const 0224 { 0225 QStringList servicesIds; 0226 const auto partOfferList = KParts::PartLoader::partsForMimeType(name()); 0227 for (const auto &metaData : partOfferList) { 0228 servicesIds.append(metaData.pluginId()); 0229 } 0230 return servicesIds; 0231 } 0232 0233 void MimeTypeData::getMyServiceOffers() const 0234 { 0235 m_appServices = getAppOffers(); 0236 m_embedServices = getPartOffers(); 0237 m_bFullInit = true; 0238 } 0239 0240 QStringList MimeTypeData::appServices() const 0241 { 0242 if (!m_bFullInit) { 0243 getMyServiceOffers(); 0244 } 0245 return m_appServices; 0246 } 0247 0248 QStringList MimeTypeData::embedServices() const 0249 { 0250 if (!m_bFullInit) { 0251 getMyServiceOffers(); 0252 } 0253 return m_embedServices; 0254 } 0255 0256 bool MimeTypeData::isMimeTypeDirty() const 0257 { 0258 Q_ASSERT(!m_isGroup); 0259 if (m_bNewItem) { 0260 return true; 0261 } 0262 0263 if (!m_mimetype.isValid()) { 0264 qWarning() << "MimeTypeData for" << name() << "says 'not new' but is without a mimetype? Should not happen."; 0265 return true; 0266 } 0267 0268 if (m_mimetype.comment() != m_comment) { 0269 qDebug() << "Mimetype Comment Dirty: old=" << m_mimetype.comment() << "m_comment=" << m_comment; 0270 return true; 0271 } 0272 if (m_userSpecifiedIconModified) { 0273 qDebug() << "m_userSpecifiedIcon has changed. Now set to" << m_userSpecifiedIcon; 0274 return true; 0275 } 0276 0277 QStringList storedPatterns = m_mimetype.globPatterns(); 0278 storedPatterns.sort(); // see ctor 0279 if (storedPatterns != m_patterns) { 0280 qDebug() << "Mimetype Patterns Dirty: old=" << storedPatterns << "m_patterns=" << m_patterns; 0281 return true; 0282 } 0283 0284 if (readAutoEmbed() != m_autoEmbed) { 0285 return true; 0286 } 0287 return false; 0288 } 0289 0290 bool MimeTypeData::isServiceListDirty() const 0291 { 0292 return !m_isGroup && (m_appServicesModified || m_embedServicesModified); 0293 } 0294 0295 bool MimeTypeData::isDirty() const 0296 { 0297 if (m_bNewItem) { 0298 qDebug() << "New item, need to save it"; 0299 return true; 0300 } 0301 0302 if (!m_isGroup) { 0303 if (isServiceListDirty()) { 0304 return true; 0305 } 0306 if (isMimeTypeDirty()) { 0307 return true; 0308 } 0309 } else { // is a group 0310 if (readAutoEmbed() != m_autoEmbed) { 0311 return true; 0312 } 0313 } 0314 0315 if (m_askSave != AskSaveDefault) { 0316 return true; 0317 } 0318 0319 // nothing seems to have changed, it's not dirty. 0320 return false; 0321 } 0322 0323 bool MimeTypeData::sync() 0324 { 0325 if (m_isGroup) { 0326 writeAutoEmbed(); 0327 return false; 0328 } 0329 0330 if (m_askSave != AskSaveDefault) { 0331 KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals); 0332 if (!config->isConfigWritable(true)) { 0333 return false; 0334 } 0335 KConfigGroup cg = config->group("Notification Messages"); 0336 if (m_askSave == AskSaveYes) { 0337 // Ask 0338 cg.deleteEntry(QStringLiteral("askSave") + name()); 0339 cg.deleteEntry(QStringLiteral("askEmbedOrSave") + name()); 0340 } else { 0341 // Do not ask, open 0342 cg.writeEntry(QStringLiteral("askSave") + name(), QStringLiteral("no")); 0343 cg.writeEntry(QStringLiteral("askEmbedOrSave") + name(), QStringLiteral("no")); 0344 } 0345 } 0346 0347 writeAutoEmbed(); 0348 0349 bool needUpdateMimeDb = false; 0350 if (isMimeTypeDirty()) { 0351 MimeTypeWriter mimeTypeWriter(name()); 0352 mimeTypeWriter.setComment(m_comment); 0353 if (!m_userSpecifiedIcon.isEmpty()) { 0354 mimeTypeWriter.setIconName(m_userSpecifiedIcon); 0355 } 0356 mimeTypeWriter.setPatterns(m_patterns); 0357 if (!mimeTypeWriter.write()) { 0358 return false; 0359 } 0360 m_userSpecifiedIconModified = false; 0361 needUpdateMimeDb = true; 0362 } 0363 0364 syncServices(); 0365 0366 return needUpdateMimeDb; 0367 } 0368 0369 static const char s_DefaultApplications[] = "Default Applications"; 0370 static const char s_AddedAssociations[] = "Added Associations"; 0371 static const char s_RemovedAssociations[] = "Removed Associations"; 0372 0373 void MimeTypeData::syncServices() 0374 { 0375 if (!m_bFullInit) { 0376 return; 0377 } 0378 0379 KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); 0380 0381 if (!profile->isConfigWritable(true)) { // warn user if mimeapps.list is root-owned (#155126/#94504) 0382 return; 0383 } 0384 0385 const QStringList oldAppServices = getAppOffers(); 0386 if (oldAppServices != m_appServices) { 0387 // Save the default application according to mime-apps-spec 1.0 0388 KConfigGroup defaultApp(profile, s_DefaultApplications); 0389 saveDefaultApplication(defaultApp, m_appServices); 0390 // Save preferred services 0391 KConfigGroup addedApps(profile, s_AddedAssociations); 0392 saveServices(addedApps, m_appServices); 0393 KConfigGroup removedApps(profile, s_RemovedAssociations); 0394 saveRemovedServices(removedApps, m_appServices, oldAppServices); 0395 } 0396 0397 const QStringList oldPartServices = getPartOffers(); 0398 if (oldPartServices != m_embedServices) { 0399 // Handle removed services 0400 KConfigGroup addedParts(profile, "Added KDE Service Associations"); 0401 saveServices(addedParts, m_embedServices); 0402 KConfigGroup removedParts(profile, "Removed KDE Service Associations"); 0403 saveRemovedServices(removedParts, m_embedServices, oldPartServices); 0404 } 0405 0406 // Clean out any kde-mimeapps.list which would take precedence any cancel our changes. 0407 const QString desktops = QString::fromLocal8Bit(qgetenv("XDG_CURRENT_DESKTOP")); 0408 const auto desktopsSplit = desktops.split(QLatin1Char(':'), Qt::SkipEmptyParts); 0409 for (const QString &desktop : desktopsSplit) { 0410 const QString file = 0411 QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + desktop.toLower() + QLatin1String("-mimeapps.list"); 0412 if (QFileInfo::exists(file)) { 0413 qDebug() << "Cleaning up" << file; 0414 KConfig conf(file, KConfig::NoGlobals); 0415 KConfigGroup(&conf, s_DefaultApplications).deleteEntry(name()); 0416 KConfigGroup(&conf, s_AddedAssociations).deleteEntry(name()); 0417 KConfigGroup(&conf, s_RemovedAssociations).deleteEntry(name()); 0418 } 0419 } 0420 0421 m_appServicesModified = false; 0422 m_embedServicesModified = false; 0423 } 0424 0425 static QStringList collectStorageIds(const QStringList &services) 0426 { 0427 QStringList storageIds; 0428 0429 for (const QString &service : services) { 0430 KService::Ptr pService = KService::serviceByStorageId(service); 0431 if (!pService) { 0432 qWarning() << "service with storage id" << service << "not found"; 0433 continue; // Where did that one go? 0434 } 0435 0436 storageIds.append(pService->storageId()); 0437 } 0438 0439 return storageIds; 0440 } 0441 0442 void MimeTypeData::saveRemovedServices(KConfigGroup &config, const QStringList &services, const QStringList &oldServices) 0443 { 0444 QStringList removedServiceList = config.readXdgListEntry(name()); 0445 0446 for (const QString &service : services) { 0447 // If removedServiceList.contains(service), then it was previously removed but has been added back 0448 removedServiceList.removeAll(service); 0449 } 0450 for (const QString &oldService : oldServices) { 0451 if (!services.contains(oldService)) { 0452 // The service was in m_appServices (or m_embedServices) but has been removed 0453 removedServiceList.append(oldService); 0454 } 0455 } 0456 if (removedServiceList.isEmpty()) { 0457 config.deleteEntry(name()); 0458 } else { 0459 config.writeXdgListEntry(name(), removedServiceList); 0460 } 0461 } 0462 0463 void MimeTypeData::saveServices(KConfigGroup &config, const QStringList &services) 0464 { 0465 if (services.isEmpty()) { 0466 config.deleteEntry(name()); 0467 } else { 0468 config.writeXdgListEntry(name(), collectStorageIds(services)); 0469 } 0470 } 0471 0472 void MimeTypeData::saveDefaultApplication(KConfigGroup &config, const QStringList &services) 0473 { 0474 if (services.isEmpty()) { 0475 config.deleteEntry(name()); 0476 return; 0477 } 0478 0479 const QStringList storageIds = collectStorageIds(services); 0480 if (!storageIds.isEmpty()) { 0481 const QString firstStorageId = storageIds.at(0); 0482 config.writeXdgListEntry(name(), {firstStorageId}); 0483 } 0484 } 0485 0486 void MimeTypeData::refresh() 0487 { 0488 if (m_isGroup) { 0489 return; 0490 } 0491 QMimeDatabase db; 0492 m_mimetype = db.mimeTypeForName(name()); 0493 if (m_mimetype.isValid()) { 0494 if (m_bNewItem) { 0495 qDebug() << "OK, created" << name(); 0496 m_bNewItem = false; // if this was a new mimetype, we just created it 0497 } 0498 if (!isMimeTypeDirty()) { 0499 // Update from the xml, in case something was changed from out of this kcm 0500 // (e.g. using KOpenWithDialog, or keditfiletype + kcmshell filetypes) 0501 initFromQMimeType(); 0502 } 0503 if (!m_appServicesModified && !m_embedServicesModified) { 0504 m_bFullInit = false; // refresh services too 0505 } 0506 } 0507 } 0508 0509 void MimeTypeData::getAskSave(bool &_askSave) 0510 { 0511 if (m_askSave == AskSaveYes) { 0512 _askSave = true; 0513 } 0514 if (m_askSave == AskSaveNo) { 0515 _askSave = false; 0516 } 0517 } 0518 0519 void MimeTypeData::setAskSave(bool _askSave) 0520 { 0521 m_askSave = _askSave ? AskSaveYes : AskSaveNo; 0522 } 0523 0524 bool MimeTypeData::canUseGroupSetting() const 0525 { 0526 // "Use group settings" isn't available for zip, tar etc.; those have a builtin default... 0527 if (!m_mimetype.isValid()) { // e.g. new mimetype 0528 return true; 0529 } 0530 const bool hasLocalProtocolRedirect = !KProtocolManager::protocolForArchiveMimetype(name()).isEmpty(); 0531 return !hasLocalProtocolRedirect; 0532 } 0533 0534 void MimeTypeData::setPatterns(const QStringList &p) 0535 { 0536 m_patterns = p; 0537 // Sort them, since update-mime-database doesn't respect order (order of globs file != order of xml), 0538 // and this code says things like if (m_mimetype.patterns() == m_patterns). 0539 // We could also sort in KMimeType::setPatterns but this would just slow down the 0540 // normal use case (anything else than this KCM) for no good reason. 0541 m_patterns.sort(); 0542 } 0543 0544 bool MimeTypeData::matchesFilter(const QString &filter) const 0545 { 0546 if (name().contains(filter, Qt::CaseInsensitive)) { 0547 return true; 0548 } 0549 0550 if (m_comment.contains(filter, Qt::CaseInsensitive)) { 0551 return true; 0552 } 0553 0554 if (!m_patterns.filter(filter, Qt::CaseInsensitive).isEmpty()) { 0555 return true; 0556 } 0557 0558 return false; 0559 } 0560 0561 void MimeTypeData::setAppServices(const QStringList &dsl) 0562 { 0563 if (!m_bFullInit) { 0564 getMyServiceOffers(); // so that m_bFullInit is true 0565 } 0566 m_appServices = dsl; 0567 m_appServicesModified = true; 0568 } 0569 0570 void MimeTypeData::setEmbedServices(const QStringList &dsl) 0571 { 0572 if (!m_bFullInit) { 0573 getMyServiceOffers(); // so that m_bFullInit is true 0574 } 0575 m_embedServices = dsl; 0576 m_embedServicesModified = true; 0577 } 0578 0579 QString MimeTypeData::icon() const 0580 { 0581 if (!m_userSpecifiedIcon.isEmpty()) { 0582 return m_userSpecifiedIcon; 0583 } 0584 if (m_mimetype.isValid()) { 0585 return m_mimetype.iconName(); 0586 } 0587 return QString(); 0588 }