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 }