File indexing completed on 2024-04-21 16:12:15

0001 /*******************************************************************
0002  * productmapping.cpp
0003  * SPDX-FileCopyrightText: 2009 Dario Andres Rodriguez <andresbajotierra@gmail.com>
0004  * SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  *
0008  ******************************************************************/
0009 
0010 #include "productmapping.h"
0011 
0012 #include "drkonqi_debug.h"
0013 #include <KConfig>
0014 #include <KConfigGroup>
0015 #include <QStandardPaths>
0016 
0017 #include "bugzillalib.h"
0018 #include "crashedapplication.h"
0019 
0020 ProductMapping::ProductMapping(const CrashedApplication *crashedApp, BugzillaManager *bzManager, QObject *parent)
0021     : QObject(parent)
0022     , m_crashedAppPtr(crashedApp)
0023     , m_bugzillaManagerPtr(bzManager)
0024     , m_bugzillaProductDisabled(false)
0025     , m_bugzillaVersionDisabled(false)
0026 
0027 {
0028     // Default "fallback" values
0029     m_bugzillaProduct = crashedApp->fakeExecutableBaseName();
0030     m_bugzillaComponent = QStringLiteral("general");
0031     m_bugzillaVersionString = QStringLiteral("unspecified");
0032     m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct;
0033 
0034     if (!crashedApp->productName().isEmpty()) {
0035         const auto l = crashedApp->productName().split(QLatin1Char('/'), Qt::SkipEmptyParts);
0036         if (l.size() == 2) {
0037             m_bugzillaProduct = l[0];
0038             m_bugzillaComponent = l[1];
0039         } else {
0040             m_bugzillaProduct = crashedApp->productName();
0041         }
0042         m_hasExternallyProvidedProductName = true;
0043     }
0044 
0045     map(crashedApp->fakeExecutableBaseName());
0046 
0047     // Get valid versions
0048     connect(m_bugzillaManagerPtr, &BugzillaManager::productInfoFetched, this, &ProductMapping::checkProductInfo);
0049     // Holding the connection so we can easily disconnect in the fallback logic.
0050     m_productInfoErrorConnection = connect(m_bugzillaManagerPtr, &BugzillaManager::productInfoError, this, &ProductMapping::fallBackToKDE);
0051 
0052     m_bugzillaManagerPtr->fetchProductInfo(m_bugzillaProduct);
0053 }
0054 
0055 void ProductMapping::map(const QString &appName)
0056 {
0057     mapUsingInternalFile(appName);
0058     getRelatedProductsUsingInternalFile(m_bugzillaProduct);
0059 }
0060 
0061 void ProductMapping::mapUsingInternalFile(const QString &appName)
0062 {
0063     KConfig mappingsFile(QString::fromLatin1("mappings"), KConfig::NoGlobals, QStandardPaths::AppDataLocation);
0064     const KConfigGroup mappings = mappingsFile.group("Mappings");
0065     if (mappings.hasKey(appName)) {
0066         if (m_hasExternallyProvidedProductName) {
0067             qCWarning(DRKONQI_LOG) << "Mapping found despite product information being provided by the application. Consider removing the mapping entry"
0068                                    << appName;
0069         }
0070         QString mappingString = mappings.readEntry(appName);
0071         if (!mappingString.isEmpty()) {
0072             QStringList list = mappingString.split(QLatin1Char('|'), Qt::SkipEmptyParts);
0073             if (list.count() == 2) {
0074                 m_bugzillaProduct = list.at(0);
0075                 m_bugzillaComponent = list.at(1);
0076                 m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct;
0077             } else {
0078                 qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Sections found " << list.count();
0079             }
0080         } else {
0081             qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty "
0082                                       "(or there was an error when reading)";
0083         }
0084     }
0085 }
0086 
0087 void ProductMapping::getRelatedProductsUsingInternalFile(const QString &bugzillaProduct)
0088 {
0089     // ProductGroup ->  kontact=kdepim
0090     // Groups -> kdepim=kontact|kmail|korganizer|akonadi|pimlibs..etc
0091 
0092     KConfig mappingsFile(QString::fromLatin1("mappings"), KConfig::NoGlobals, QStandardPaths::AppDataLocation);
0093     const KConfigGroup productGroup = mappingsFile.group("ProductGroup");
0094 
0095     // Get groups of the application
0096     QStringList groups;
0097     if (productGroup.hasKey(bugzillaProduct)) {
0098         QString group = productGroup.readEntry(bugzillaProduct);
0099         if (group.isEmpty()) {
0100             qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty "
0101                                       "(or there was an error when reading)";
0102             return;
0103         }
0104         groups = group.split(QLatin1Char('|'), Qt::SkipEmptyParts);
0105     }
0106 
0107     // All KDE apps use the KDE Platform (basic libs)
0108     groups << QLatin1String("kdeplatform");
0109 
0110     // Add the product itself
0111     m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct;
0112 
0113     // Get related products of each related group
0114     for (const QString &group : std::as_const(groups)) {
0115         const KConfigGroup bzGroups = mappingsFile.group("BZGroups");
0116         if (bzGroups.hasKey(group)) {
0117             QString bzGroup = bzGroups.readEntry(group);
0118             if (!bzGroup.isEmpty()) {
0119                 const QStringList relatedGroups = bzGroup.split(QLatin1Char('|'), Qt::SkipEmptyParts);
0120                 if (!relatedGroups.isEmpty()) {
0121                     m_relatedBugzillaProducts.append(relatedGroups);
0122                 }
0123             } else {
0124                 qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty "
0125                                           "(or there was an error when reading)";
0126             }
0127         }
0128     }
0129 }
0130 
0131 void ProductMapping::checkProductInfo(const Bugzilla::Product::Ptr product)
0132 {
0133     // check whether the product itself is disabled for new reports,
0134     // which usually means that product/application is unmaintained.
0135     m_bugzillaProductDisabled = !product->isActive();
0136 
0137     // check whether the product on bugzilla contains the expected component
0138     if (!product->componentNames().contains(m_bugzillaComponent)) {
0139         m_bugzillaComponent = QLatin1String("general");
0140     }
0141 
0142     // find the appropriate version to use on bugzilla
0143     const QString version = m_crashedAppPtr->version();
0144     const QStringList &allVersions = product->allVersions();
0145 
0146     if (allVersions.contains(version)) {
0147         // The version the crash application provided is a valid bugzilla version: use it !
0148         m_bugzillaVersionString = version;
0149     } else if (version.endsWith(QLatin1String(".00"))) {
0150         // check if there is a version on bugzilla with just ".0"
0151         const QString shorterVersion = version.left(version.size() - 1);
0152         if (allVersions.contains(shorterVersion)) {
0153             m_bugzillaVersionString = shorterVersion;
0154         }
0155     } else if (!allVersions.contains(m_bugzillaVersionString)) {
0156         // No good match found, make sure the default is sound...
0157         // If our hardcoded fallback is not in bugzilla it was likely
0158         // renamed so we'll find the version with the lowest id instead
0159         // and that should technically have been the "default" version.
0160         Bugzilla::ProductVersion *lowestVersion = nullptr;
0161         const QList<Bugzilla::ProductVersion *> versions = product->versions();
0162         for (const auto &version : versions) {
0163             if (!lowestVersion || lowestVersion->id() > version->id()) {
0164                 lowestVersion = version;
0165             }
0166         }
0167         if (lowestVersion) {
0168             m_bugzillaVersionString = lowestVersion->name();
0169         }
0170     }
0171 
0172     // check whether that versions is disabled for new reports, which
0173     // usually means that version is outdated and not supported anymore.
0174     const QStringList &inactiveVersions = product->inactiveVersions();
0175     m_bugzillaVersionDisabled = inactiveVersions.contains(m_bugzillaVersionString);
0176 
0177     Q_EMIT resolved();
0178 }
0179 
0180 void ProductMapping::fallBackToKDE()
0181 {
0182     // Fall back to the generic kde product when we couldn't find an explicit mapping.
0183     // This is in an effort to make it as easy as possible to file a bug, unfortunately it means someone will
0184     // have to triage it accordingly.
0185     // Disconnect to safe-guard against infinite loop should kde also fail for some reason....
0186     //   An argument could be made that we should raise a user error if this fails again,
0187     //   'kde' not resolving shouldn't ever happen and points at a huge problem somewhere.
0188     disconnect(m_productInfoErrorConnection);
0189     m_bugzillaProductOriginal = m_bugzillaProduct;
0190     m_bugzillaProduct = QStringLiteral("kde");
0191     m_bugzillaManagerPtr->fetchProductInfo(m_bugzillaProduct);
0192 
0193     Q_EMIT resolved();
0194 }
0195 
0196 QStringList ProductMapping::relatedBugzillaProducts() const
0197 {
0198     return m_relatedBugzillaProducts;
0199 }
0200 
0201 QString ProductMapping::bugzillaProduct() const
0202 {
0203     return m_bugzillaProduct;
0204 }
0205 
0206 QString ProductMapping::bugzillaComponent() const
0207 {
0208     return m_bugzillaComponent;
0209 }
0210 
0211 QString ProductMapping::bugzillaVersion() const
0212 {
0213     return m_bugzillaVersionString;
0214 }
0215 
0216 bool ProductMapping::bugzillaProductDisabled() const
0217 {
0218     return m_bugzillaProductDisabled;
0219 }
0220 
0221 bool ProductMapping::bugzillaVersionDisabled() const
0222 {
0223     return m_bugzillaVersionDisabled;
0224 }
0225 
0226 QString ProductMapping::bugzillaProductOriginal() const
0227 {
0228     return m_bugzillaProductOriginal;
0229 }