File indexing completed on 2024-04-28 05:27:04

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 <KConfigGroup>
0012 #include <KParts/PartLoader>
0013 #include <KProtocolManager>
0014 #include <KService>
0015 #include <KSharedConfig>
0016 #include <QDebug>
0017 #include <QFileInfo>
0018 #include <QMimeDatabase>
0019 #include <QStandardPaths>
0020 #include <QXmlStreamReader>
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     // We do not do any sorting, because KParts uses the order in which the entries are saved
0227     const auto partOfferList = KParts::PartLoader::partsForMimeType(name());
0228     for (const auto &metaData : partOfferList) {
0229         servicesIds.append(metaData.pluginId());
0230     }
0231     return servicesIds;
0232 }
0233 
0234 void MimeTypeData::getMyServiceOffers() const
0235 {
0236     m_appServices = getAppOffers();
0237     m_embedParts = getPartOffers();
0238     m_bFullInit = true;
0239 }
0240 
0241 QStringList MimeTypeData::appServices() const
0242 {
0243     if (!m_bFullInit) {
0244         getMyServiceOffers();
0245     }
0246     return m_appServices;
0247 }
0248 
0249 QStringList MimeTypeData::embedParts() const
0250 {
0251     if (!m_bFullInit) {
0252         getMyServiceOffers();
0253     }
0254     return m_embedParts;
0255 }
0256 
0257 bool MimeTypeData::isMimeTypeDirty() const
0258 {
0259     Q_ASSERT(!m_isGroup);
0260     if (m_bNewItem) {
0261         return true;
0262     }
0263 
0264     if (!m_mimetype.isValid()) {
0265         qWarning() << "MimeTypeData for" << name() << "says 'not new' but is without a mimetype? Should not happen.";
0266         return true;
0267     }
0268 
0269     if (m_mimetype.comment() != m_comment) {
0270         qDebug() << "Mimetype Comment Dirty: old=" << m_mimetype.comment() << "m_comment=" << m_comment;
0271         return true;
0272     }
0273     if (m_userSpecifiedIconModified) {
0274         qDebug() << "m_userSpecifiedIcon has changed. Now set to" << m_userSpecifiedIcon;
0275         return true;
0276     }
0277 
0278     QStringList storedPatterns = m_mimetype.globPatterns();
0279     storedPatterns.sort(); // see ctor
0280     if (storedPatterns != m_patterns) {
0281         qDebug() << "Mimetype Patterns Dirty: old=" << storedPatterns << "m_patterns=" << m_patterns;
0282         return true;
0283     }
0284 
0285     if (readAutoEmbed() != m_autoEmbed) {
0286         return true;
0287     }
0288     return false;
0289 }
0290 
0291 bool MimeTypeData::isServiceListDirty() const
0292 {
0293     return !m_isGroup && (m_appServicesModified || m_embedServicesModified);
0294 }
0295 
0296 bool MimeTypeData::isDirty() const
0297 {
0298     if (m_bNewItem) {
0299         qDebug() << "New item, need to save it";
0300         return true;
0301     }
0302 
0303     if (!m_isGroup) {
0304         if (isServiceListDirty()) {
0305             return true;
0306         }
0307         if (isMimeTypeDirty()) {
0308             return true;
0309         }
0310     } else { // is a group
0311         if (readAutoEmbed() != m_autoEmbed) {
0312             return true;
0313         }
0314     }
0315 
0316     if (m_askSave != AskSaveDefault) {
0317         return true;
0318     }
0319 
0320     // nothing seems to have changed, it's not dirty.
0321     return false;
0322 }
0323 
0324 bool MimeTypeData::sync()
0325 {
0326     if (m_isGroup) {
0327         writeAutoEmbed();
0328         return false;
0329     }
0330 
0331     if (m_askSave != AskSaveDefault) {
0332         KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals);
0333         if (!config->isConfigWritable(true)) {
0334             return false;
0335         }
0336         KConfigGroup cg = config->group("Notification Messages");
0337         if (m_askSave == AskSaveYes) {
0338             // Ask
0339             cg.deleteEntry(QStringLiteral("askSave") + name());
0340             cg.deleteEntry(QStringLiteral("askEmbedOrSave") + name());
0341         } else {
0342             // Do not ask, open
0343             cg.writeEntry(QStringLiteral("askSave") + name(), QStringLiteral("no"));
0344             cg.writeEntry(QStringLiteral("askEmbedOrSave") + name(), QStringLiteral("no"));
0345         }
0346     }
0347 
0348     writeAutoEmbed();
0349 
0350     bool needUpdateMimeDb = false;
0351     if (isMimeTypeDirty()) {
0352         MimeTypeWriter mimeTypeWriter(name());
0353         mimeTypeWriter.setComment(m_comment);
0354         if (!m_userSpecifiedIcon.isEmpty()) {
0355             mimeTypeWriter.setIconName(m_userSpecifiedIcon);
0356         }
0357         mimeTypeWriter.setPatterns(m_patterns);
0358         if (!mimeTypeWriter.write()) {
0359             return false;
0360         }
0361         m_userSpecifiedIconModified = false;
0362         needUpdateMimeDb = true;
0363     }
0364 
0365     syncServices();
0366 
0367     return needUpdateMimeDb;
0368 }
0369 
0370 static const char s_DefaultApplications[] = "Default Applications";
0371 static const char s_AddedAssociations[] = "Added Associations";
0372 static const char s_RemovedAssociations[] = "Removed Associations";
0373 
0374 void MimeTypeData::syncServices()
0375 {
0376     if (!m_bFullInit) {
0377         return;
0378     }
0379 
0380     KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation);
0381 
0382     if (!profile->isConfigWritable(true)) { // warn user if mimeapps.list is root-owned (#155126/#94504)
0383         return;
0384     }
0385 
0386     const QStringList oldAppServices = getAppOffers();
0387     if (oldAppServices != m_appServices) {
0388         // Save the default application according to mime-apps-spec 1.0
0389         KConfigGroup defaultApp(profile, s_DefaultApplications);
0390         saveDefaultApplication(defaultApp, m_appServices);
0391         // Save preferred services
0392         KConfigGroup addedApps(profile, s_AddedAssociations);
0393         saveServices(addedApps, m_appServices);
0394         KConfigGroup removedApps(profile, s_RemovedAssociations);
0395         saveRemovedServices(removedApps, m_appServices, oldAppServices);
0396     }
0397 
0398     KSharedConfig::Ptr kpartsProfile = KSharedConfig::openConfig(QStringLiteral("kpartsrc"), KConfig::NoGlobals);
0399     const QStringList oldPartServices = getPartOffers();
0400     if (oldPartServices != m_embedParts) {
0401         KConfigGroup addedParts(kpartsProfile, "Added KDE Part Associations");
0402         if (m_embedParts.isEmpty()) {
0403             addedParts.deleteEntry(name());
0404         } else {
0405             addedParts.writeXdgListEntry(name(), m_embedParts);
0406         }
0407         KConfigGroup removedParts(kpartsProfile, "Removed KDE Part Associations");
0408         saveRemovedServices(removedParts, m_embedParts, oldPartServices);
0409     }
0410 
0411     // Clean out any kde-mimeapps.list which would take precedence any cancel our changes.
0412     const QString desktops = QString::fromLocal8Bit(qgetenv("XDG_CURRENT_DESKTOP"));
0413     const auto desktopsSplit = desktops.split(QLatin1Char(':'), Qt::SkipEmptyParts);
0414     for (const QString &desktop : desktopsSplit) {
0415         const QString file =
0416             QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + desktop.toLower() + QLatin1String("-mimeapps.list");
0417         if (QFileInfo::exists(file)) {
0418             qDebug() << "Cleaning up" << file;
0419             KConfig conf(file, KConfig::NoGlobals);
0420             KConfigGroup(&conf, s_DefaultApplications).deleteEntry(name());
0421             KConfigGroup(&conf, s_AddedAssociations).deleteEntry(name());
0422             KConfigGroup(&conf, s_RemovedAssociations).deleteEntry(name());
0423         }
0424     }
0425 
0426     m_appServicesModified = false;
0427     m_embedServicesModified = false;
0428 }
0429 
0430 static QStringList collectStorageIds(const QStringList &services)
0431 {
0432     QStringList storageIds;
0433 
0434     for (const QString &service : services) {
0435         KService::Ptr pService = KService::serviceByStorageId(service);
0436         if (!pService) {
0437             qWarning() << "service with storage id" << service << "not found";
0438             continue; // Where did that one go?
0439         }
0440 
0441         storageIds.append(pService->storageId());
0442     }
0443 
0444     return storageIds;
0445 }
0446 
0447 void MimeTypeData::saveRemovedServices(KConfigGroup &config, const QStringList &services, const QStringList &oldServices)
0448 {
0449     QStringList removedServiceList = config.readXdgListEntry(name());
0450 
0451     for (const QString &service : services) {
0452         // If removedServiceList.contains(service), then it was previously removed but has been added back
0453         removedServiceList.removeAll(service);
0454     }
0455     for (const QString &oldService : oldServices) {
0456         if (!services.contains(oldService)) {
0457             // The service was in m_appServices (or m_embedServices) but has been removed
0458             removedServiceList.append(oldService);
0459         }
0460     }
0461     if (removedServiceList.isEmpty()) {
0462         config.deleteEntry(name());
0463     } else {
0464         config.writeXdgListEntry(name(), removedServiceList);
0465     }
0466 }
0467 
0468 void MimeTypeData::saveServices(KConfigGroup &config, const QStringList &services)
0469 {
0470     if (services.isEmpty()) {
0471         config.deleteEntry(name());
0472     } else {
0473         config.writeXdgListEntry(name(), collectStorageIds(services));
0474     }
0475 }
0476 
0477 void MimeTypeData::saveDefaultApplication(KConfigGroup &config, const QStringList &services)
0478 {
0479     if (services.isEmpty()) {
0480         config.deleteEntry(name());
0481         return;
0482     }
0483 
0484     const QStringList storageIds = collectStorageIds(services);
0485     if (!storageIds.isEmpty()) {
0486         const QString firstStorageId = storageIds.at(0);
0487         config.writeXdgListEntry(name(), {firstStorageId});
0488     }
0489 }
0490 
0491 void MimeTypeData::refresh()
0492 {
0493     if (m_isGroup) {
0494         return;
0495     }
0496     QMimeDatabase db;
0497     m_mimetype = db.mimeTypeForName(name());
0498     if (m_mimetype.isValid()) {
0499         if (m_bNewItem) {
0500             qDebug() << "OK, created" << name();
0501             m_bNewItem = false; // if this was a new mimetype, we just created it
0502         }
0503         if (!isMimeTypeDirty()) {
0504             // Update from the xml, in case something was changed from out of this kcm
0505             // (e.g. using KOpenWithDialog, or keditfiletype + kcmshell filetypes)
0506             initFromQMimeType();
0507         }
0508         if (!m_appServicesModified && !m_embedServicesModified) {
0509             m_bFullInit = false; // refresh services too
0510         }
0511     }
0512 }
0513 
0514 void MimeTypeData::getAskSave(bool &_askSave)
0515 {
0516     if (m_askSave == AskSaveYes) {
0517         _askSave = true;
0518     }
0519     if (m_askSave == AskSaveNo) {
0520         _askSave = false;
0521     }
0522 }
0523 
0524 void MimeTypeData::setAskSave(bool _askSave)
0525 {
0526     m_askSave = _askSave ? AskSaveYes : AskSaveNo;
0527 }
0528 
0529 bool MimeTypeData::canUseGroupSetting() const
0530 {
0531     // "Use group settings" isn't available for zip, tar etc.; those have a builtin default...
0532     if (!m_mimetype.isValid()) { // e.g. new mimetype
0533         return true;
0534     }
0535     const bool hasLocalProtocolRedirect = !KProtocolManager::protocolForArchiveMimetype(name()).isEmpty();
0536     return !hasLocalProtocolRedirect;
0537 }
0538 
0539 void MimeTypeData::setPatterns(const QStringList &p)
0540 {
0541     m_patterns = p;
0542     // Sort them, since update-mime-database doesn't respect order (order of globs file != order of xml),
0543     // and this code says things like if (m_mimetype.patterns() == m_patterns).
0544     // We could also sort in KMimeType::setPatterns but this would just slow down the
0545     // normal use case (anything else than this KCM) for no good reason.
0546     m_patterns.sort();
0547 }
0548 
0549 bool MimeTypeData::matchesFilter(const QString &filter) const
0550 {
0551     if (name().contains(filter, Qt::CaseInsensitive)) {
0552         return true;
0553     }
0554 
0555     if (m_comment.contains(filter, Qt::CaseInsensitive)) {
0556         return true;
0557     }
0558 
0559     if (!m_patterns.filter(filter, Qt::CaseInsensitive).isEmpty()) {
0560         return true;
0561     }
0562 
0563     return false;
0564 }
0565 
0566 void MimeTypeData::setAppServices(const QStringList &dsl)
0567 {
0568     if (!m_bFullInit) {
0569         getMyServiceOffers(); // so that m_bFullInit is true
0570     }
0571     m_appServices = dsl;
0572     m_appServicesModified = true;
0573 }
0574 
0575 void MimeTypeData::setEmbedParts(const QStringList &dsl)
0576 {
0577     if (!m_bFullInit) {
0578         getMyServiceOffers(); // so that m_bFullInit is true
0579     }
0580     m_embedParts = dsl;
0581     m_embedServicesModified = true;
0582 }
0583 
0584 QString MimeTypeData::icon() const
0585 {
0586     if (!m_userSpecifiedIcon.isEmpty()) {
0587         return m_userSpecifiedIcon;
0588     }
0589     if (m_mimetype.isValid()) {
0590         return m_mimetype.iconName();
0591     }
0592     return QString();
0593 }