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