1 /*******************************************************************
2  * productmapping.cpp
3  * SPDX-FileCopyrightText: 2009 Dario Andres Rodriguez <andresbajotierra@gmail.com>
4  * SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
5  *
6  * SPDX-License-Identifier: GPL-2.0-or-later
7  *
8  ******************************************************************/
9 
10 #include "productmapping.h"
11 
12 #include "drkonqi_debug.h"
13 #include <KConfig>
14 #include <KConfigGroup>
15 #include <QStandardPaths>
16 
17 #include "bugzillalib.h"
18 #include "crashedapplication.h"
19 
ProductMapping(const CrashedApplication * crashedApp,BugzillaManager * bzManager,QObject * parent)20 ProductMapping::ProductMapping(const CrashedApplication *crashedApp, BugzillaManager *bzManager, QObject *parent)
21     : QObject(parent)
22     , m_crashedAppPtr(crashedApp)
23     , m_bugzillaManagerPtr(bzManager)
24     , m_bugzillaProductDisabled(false)
25     , m_bugzillaVersionDisabled(false)
26 
27 {
28     // Default "fallback" values
29     m_bugzillaProduct = crashedApp->fakeExecutableBaseName();
30     m_bugzillaComponent = QStringLiteral("general");
31     m_bugzillaVersionString = QStringLiteral("unspecified");
32     m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct;
33 
34     if (!crashedApp->productName().isEmpty()) {
35         const auto l = crashedApp->productName().split(QLatin1Char('/'), Qt::SkipEmptyParts);
36         if (l.size() == 2) {
37             m_bugzillaProduct = l[0];
38             m_bugzillaComponent = l[1];
39         } else {
40             m_bugzillaProduct = crashedApp->productName();
41         }
42         m_hasExternallyProvidedProductName = true;
43     }
44 
45     map(crashedApp->fakeExecutableBaseName());
46 
47     // Get valid versions
48     connect(m_bugzillaManagerPtr, &BugzillaManager::productInfoFetched, this, &ProductMapping::checkProductInfo);
49     // Holding the connection so we can easily disconnect in the fallback logic.
50     m_productInfoErrorConnection = connect(m_bugzillaManagerPtr, &BugzillaManager::productInfoError, this, &ProductMapping::fallBackToKDE);
51 
52     m_bugzillaManagerPtr->fetchProductInfo(m_bugzillaProduct);
53 }
54 
map(const QString & appName)55 void ProductMapping::map(const QString &appName)
56 {
57     mapUsingInternalFile(appName);
58     getRelatedProductsUsingInternalFile(m_bugzillaProduct);
59 }
60 
mapUsingInternalFile(const QString & appName)61 void ProductMapping::mapUsingInternalFile(const QString &appName)
62 {
63     KConfig mappingsFile(QString::fromLatin1("mappings"), KConfig::NoGlobals, QStandardPaths::AppDataLocation);
64     const KConfigGroup mappings = mappingsFile.group("Mappings");
65     if (mappings.hasKey(appName)) {
66         if (m_hasExternallyProvidedProductName) {
67             qCWarning(DRKONQI_LOG) << "Mapping found despite product information being provided by the application. Consider removing the mapping entry"
68                                    << appName;
69         }
70         QString mappingString = mappings.readEntry(appName);
71         if (!mappingString.isEmpty()) {
72             QStringList list = mappingString.split(QLatin1Char('|'), Qt::SkipEmptyParts);
73             if (list.count() == 2) {
74                 m_bugzillaProduct = list.at(0);
75                 m_bugzillaComponent = list.at(1);
76                 m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct;
77             } else {
78                 qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Sections found " << list.count();
79             }
80         } else {
81             qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty "
82                                       "(or there was an error when reading)";
83         }
84     }
85 }
86 
getRelatedProductsUsingInternalFile(const QString & bugzillaProduct)87 void ProductMapping::getRelatedProductsUsingInternalFile(const QString &bugzillaProduct)
88 {
89     // ProductGroup ->  kontact=kdepim
90     // Groups -> kdepim=kontact|kmail|korganizer|akonadi|pimlibs..etc
91 
92     KConfig mappingsFile(QString::fromLatin1("mappings"), KConfig::NoGlobals, QStandardPaths::AppDataLocation);
93     const KConfigGroup productGroup = mappingsFile.group("ProductGroup");
94 
95     // Get groups of the application
96     QStringList groups;
97     if (productGroup.hasKey(bugzillaProduct)) {
98         QString group = productGroup.readEntry(bugzillaProduct);
99         if (group.isEmpty()) {
100             qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty "
101                                       "(or there was an error when reading)";
102             return;
103         }
104         groups = group.split(QLatin1Char('|'), Qt::SkipEmptyParts);
105     }
106 
107     // All KDE apps use the KDE Platform (basic libs)
108     groups << QLatin1String("kdeplatform");
109 
110     // Add the product itself
111     m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct;
112 
113     // Get related products of each related group
114     for (const QString &group : std::as_const(groups)) {
115         const KConfigGroup bzGroups = mappingsFile.group("BZGroups");
116         if (bzGroups.hasKey(group)) {
117             QString bzGroup = bzGroups.readEntry(group);
118             if (!bzGroup.isEmpty()) {
119                 const QStringList relatedGroups = bzGroup.split(QLatin1Char('|'), Qt::SkipEmptyParts);
120                 if (!relatedGroups.isEmpty()) {
121                     m_relatedBugzillaProducts.append(relatedGroups);
122                 }
123             } else {
124                 qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty "
125                                           "(or there was an error when reading)";
126             }
127         }
128     }
129 }
130 
checkProductInfo(const Bugzilla::Product::Ptr product)131 void ProductMapping::checkProductInfo(const Bugzilla::Product::Ptr product)
132 {
133     // check whether the product itself is disabled for new reports,
134     // which usually means that product/application is unmaintained.
135     m_bugzillaProductDisabled = !product->isActive();
136 
137     // check whether the product on bugzilla contains the expected component
138     if (!product->componentNames().contains(m_bugzillaComponent)) {
139         m_bugzillaComponent = QLatin1String("general");
140     }
141 
142     // find the appropriate version to use on bugzilla
143     const QString version = m_crashedAppPtr->version();
144     const QStringList &allVersions = product->allVersions();
145 
146     if (allVersions.contains(version)) {
147         // The version the crash application provided is a valid bugzilla version: use it !
148         m_bugzillaVersionString = version;
149     } else if (version.endsWith(QLatin1String(".00"))) {
150         // check if there is a version on bugzilla with just ".0"
151         const QString shorterVersion = version.left(version.size() - 1);
152         if (allVersions.contains(shorterVersion)) {
153             m_bugzillaVersionString = shorterVersion;
154         }
155     } else if (!allVersions.contains(m_bugzillaVersionString)) {
156         // No good match found, make sure the default is sound...
157         // If our hardcoded fallback is not in bugzilla it was likely
158         // renamed so we'll find the version with the lowest id instead
159         // and that should technically have been the "default" version.
160         Bugzilla::ProductVersion *lowestVersion = nullptr;
161         const QList<Bugzilla::ProductVersion *> versions = product->versions();
162         for (const auto &version : versions) {
163             if (!lowestVersion || lowestVersion->id() > version->id()) {
164                 lowestVersion = version;
165             }
166         }
167         if (lowestVersion) {
168             m_bugzillaVersionString = lowestVersion->name();
169         }
170     }
171 
172     // check whether that versions is disabled for new reports, which
173     // usually means that version is outdated and not supported anymore.
174     const QStringList &inactiveVersions = product->inactiveVersions();
175     m_bugzillaVersionDisabled = inactiveVersions.contains(m_bugzillaVersionString);
176 
177     Q_EMIT resolved();
178 }
179 
fallBackToKDE()180 void ProductMapping::fallBackToKDE()
181 {
182     // Fall back to the generic kde product when we couldn't find an explicit mapping.
183     // This is in an effort to make it as easy as possible to file a bug, unfortunately it means someone will
184     // have to triage it accordingly.
185     // Disconnect to safe-guard against infinite loop should kde also fail for some reason....
186     //   An argument could be made that we should raise a user error if this fails again,
187     //   'kde' not resolving shouldn't ever happen and points at a huge problem somewhere.
188     disconnect(m_productInfoErrorConnection);
189     m_bugzillaProductOriginal = m_bugzillaProduct;
190     m_bugzillaProduct = QStringLiteral("kde");
191     m_bugzillaManagerPtr->fetchProductInfo(m_bugzillaProduct);
192 
193     Q_EMIT resolved();
194 }
195 
relatedBugzillaProducts() const196 QStringList ProductMapping::relatedBugzillaProducts() const
197 {
198     return m_relatedBugzillaProducts;
199 }
200 
bugzillaProduct() const201 QString ProductMapping::bugzillaProduct() const
202 {
203     return m_bugzillaProduct;
204 }
205 
bugzillaComponent() const206 QString ProductMapping::bugzillaComponent() const
207 {
208     return m_bugzillaComponent;
209 }
210 
bugzillaVersion() const211 QString ProductMapping::bugzillaVersion() const
212 {
213     return m_bugzillaVersionString;
214 }
215 
bugzillaProductDisabled() const216 bool ProductMapping::bugzillaProductDisabled() const
217 {
218     return m_bugzillaProductDisabled;
219 }
220 
bugzillaVersionDisabled() const221 bool ProductMapping::bugzillaVersionDisabled() const
222 {
223     return m_bugzillaVersionDisabled;
224 }
225 
bugzillaProductOriginal() const226 QString ProductMapping::bugzillaProductOriginal() const
227 {
228     return m_bugzillaProductOriginal;
229 }
230