1 /*******************************************************************
2  * reportinterface.cpp
3  * SPDX-FileCopyrightText: 2009, 2010, 2011 Dario Andres Rodriguez <andresbajotierra@gmail.com>
4  * SPDX-FileCopyrightText: 2009 George Kiagiadakis <gkiagia@users.sourceforge.net>
5  * SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
6  *
7  * SPDX-License-Identifier: GPL-2.0-or-later
8  *
9  ******************************************************************/
10 
11 #include "reportinterface.h"
12 
13 #include <KLocalizedString>
14 
15 #include "applicationdetailsexamples.h"
16 #include "backtracegenerator.h"
17 #include "bugzillalib.h"
18 #include "config-drkonqi.h"
19 #include "crashedapplication.h"
20 #include "debuggermanager.h"
21 #include "drkonqi.h"
22 #include "parser/backtraceparser.h"
23 #include "productmapping.h"
24 #include "systeminformation.h"
25 
26 // Max size a report may have. This is enforced in bugzilla, hardcoded, and
27 // cannot be queried through the API, so handle this client-side in a hardcoded
28 // fashion as well.
29 static int s_maxReportSize = 65535;
30 
ReportInterface(QObject * parent)31 ReportInterface::ReportInterface(QObject *parent)
32     : QObject(parent)
33     , m_duplicate(0)
34 {
35     m_bugzillaManager = new BugzillaManager(KDE_BUGZILLA_URL, this);
36 
37     m_productMapping = new ProductMapping(DrKonqi::crashedApplication(), m_bugzillaManager, this);
38 
39     m_appDetailsExamples = new ApplicationDetailsExamples(this);
40 
41     // Information the user can provide about the crash
42     m_userRememberCrashSituation = false;
43     m_reproducible = ReproducibleUnsure;
44     m_provideActionsApplicationDesktop = false;
45     m_provideUnusualBehavior = false;
46     m_provideApplicationConfigurationDetails = false;
47 
48     // Do not attach the bug report to any other existent report (create a new one)
49     m_attachToBugNumber = 0;
50 }
51 
setBugAwarenessPageData(bool rememberSituation,Reproducible reproducible,bool actions,bool unusual,bool configuration)52 void ReportInterface::setBugAwarenessPageData(bool rememberSituation, Reproducible reproducible, bool actions, bool unusual, bool configuration)
53 {
54     // Save the information the user can provide about the crash from the assistant page
55     m_userRememberCrashSituation = rememberSituation;
56     m_reproducible = reproducible;
57     m_provideActionsApplicationDesktop = actions;
58     m_provideUnusualBehavior = unusual;
59     m_provideApplicationConfigurationDetails = configuration;
60 }
61 
isBugAwarenessPageDataUseful() const62 bool ReportInterface::isBugAwarenessPageDataUseful() const
63 {
64     // Determine if the assistant should proceed, considering the amount of information
65     // the user can provide
66     int rating = selectedOptionsRating();
67 
68     // Minimum information required even for a good backtrace.
69     bool useful = m_userRememberCrashSituation && (rating >= 2 || (m_reproducible == ReproducibleSometimes || m_reproducible == ReproducibleEverytime));
70     return useful;
71 }
72 
selectedOptionsRating() const73 int ReportInterface::selectedOptionsRating() const
74 {
75     // Check how many information the user can provide and generate a rating
76     int rating = 0;
77     if (m_provideActionsApplicationDesktop) {
78         rating += 3;
79     }
80     if (m_provideApplicationConfigurationDetails) {
81         rating += 2;
82     }
83     if (m_provideUnusualBehavior) {
84         rating += 1;
85     }
86     return rating;
87 }
88 
backtrace() const89 QString ReportInterface::backtrace() const
90 {
91     return m_backtrace;
92 }
93 
setBacktrace(const QString & backtrace)94 void ReportInterface::setBacktrace(const QString &backtrace)
95 {
96     m_backtrace = backtrace;
97 }
98 
firstBacktraceFunctions() const99 QStringList ReportInterface::firstBacktraceFunctions() const
100 {
101     return m_firstBacktraceFunctions;
102 }
103 
setFirstBacktraceFunctions(const QStringList & functions)104 void ReportInterface::setFirstBacktraceFunctions(const QStringList &functions)
105 {
106     m_firstBacktraceFunctions = functions;
107 }
108 
title() const109 QString ReportInterface::title() const
110 {
111     return m_reportTitle;
112 }
113 
setTitle(const QString & text)114 void ReportInterface::setTitle(const QString &text)
115 {
116     m_reportTitle = text;
117 }
118 
setDetailText(const QString & text)119 void ReportInterface::setDetailText(const QString &text)
120 {
121     m_reportDetailText = text;
122 }
123 
setPossibleDuplicates(const QStringList & list)124 void ReportInterface::setPossibleDuplicates(const QStringList &list)
125 {
126     m_possibleDuplicates = list;
127 }
128 
generateReportFullText(DrKonqiStamp stamp,Backtrace inlineBacktrace) const129 QString ReportInterface::generateReportFullText(DrKonqiStamp stamp, Backtrace inlineBacktrace) const
130 {
131     // Note: no translations should be done in this function's strings
132 
133     const CrashedApplication *crashedApp = DrKonqi::crashedApplication();
134     const SystemInformation *sysInfo = DrKonqi::systemInformation();
135 
136     QString report;
137 
138     // Program name and versions
139     report.append(QStringLiteral("Application: %1 (%2)\n").arg(crashedApp->fakeExecutableBaseName(), crashedApp->version()));
140     if (sysInfo->compiledSources()) {
141         report.append(QStringLiteral(" (Compiled from sources)\n"));
142     } else {
143         report.append(QLatin1Char('\n'));
144     }
145     report.append(QStringLiteral("Qt Version: %1\n").arg(sysInfo->qtVersion()));
146     report.append(QStringLiteral("Frameworks Version: %1\n").arg(sysInfo->frameworksVersion()));
147 
148     report.append(QStringLiteral("Operating System: %1\n").arg(sysInfo->operatingSystem()));
149     report.append(QStringLiteral("Windowing System: %1\n").arg(sysInfo->windowSystem()));
150 
151     // LSB output or manually selected distro
152     if (!sysInfo->distributionPrettyName().isEmpty()) {
153         report.append(QStringLiteral("Distribution: %1\n").arg(sysInfo->distributionPrettyName()));
154     } else if (!sysInfo->bugzillaPlatform().isEmpty() && sysInfo->bugzillaPlatform() != QLatin1String("unspecified")) {
155         report.append(QStringLiteral("Distribution (Platform): %1\n").arg(sysInfo->bugzillaPlatform()));
156     }
157 
158     report.append(QStringLiteral("DrKonqi: %1 [%2]\n").arg(QString::fromLatin1(PROJECT_VERSION), DrKonqi::backendClassName()));
159     report.append(QLatin1Char('\n'));
160 
161     // Details of the crash situation
162     if (isBugAwarenessPageDataUseful()) {
163         report.append(QStringLiteral("-- Information about the crash:\n"));
164         if (!m_reportDetailText.isEmpty()) {
165             report.append(m_reportDetailText.trimmed());
166         } else {
167             // If the user manual reports this crash, he/she should know what to put in here.
168             // This message is the only one translated in this function
169             report.append(xi18nc("@info/plain",
170                                  "<placeholder>In detail, tell us what you were doing "
171                                  " when the application crashed.</placeholder>"));
172         }
173         report.append(QLatin1String("\n\n"));
174     }
175 
176     // Crash reproducibility
177     switch (m_reproducible) {
178     case ReproducibleUnsure:
179         report.append(QStringLiteral("The reporter is unsure if this crash is reproducible.\n\n"));
180         break;
181     case ReproducibleNever:
182         report.append(QStringLiteral("The crash does not seem to be reproducible.\n\n"));
183         break;
184     case ReproducibleSometimes:
185         report.append(QStringLiteral("The crash can be reproduced sometimes.\n\n"));
186         break;
187     case ReproducibleEverytime:
188         report.append(QStringLiteral("The crash can be reproduced every time.\n\n"));
189         break;
190     }
191 
192     // Backtrace
193     switch (inlineBacktrace) {
194     case Backtrace::Complete:
195         report.append(QStringLiteral("-- Backtrace:\n"));
196         break;
197     case Backtrace::Reduced:
198         report.append(QStringLiteral("-- Backtrace (Reduced):\n"));
199         break;
200     case Backtrace::Exclude:
201         report.append(QStringLiteral("The backtrace was excluded and likely attached as a file.\n"));
202         break;
203     }
204     if (!m_backtrace.isEmpty()) {
205         switch (inlineBacktrace) {
206         case Backtrace::Complete:
207             report.append(m_backtrace.trimmed() + QLatin1Char('\n'));
208             break;
209         case Backtrace::Reduced:
210             report.append(DrKonqi::debuggerManager()->backtraceGenerator()->parser()->simplifiedBacktrace() + QLatin1Char('\n'));
211             break;
212         case Backtrace::Exclude:
213             report.append(QStringLiteral("The backtrace is attached as a comment due to length constraints\n"));
214             break;
215         }
216     } else {
217         report.append(QStringLiteral("A useful backtrace could not be generated\n"));
218     }
219 
220     // Possible duplicates (selected by the user)
221     if (!m_possibleDuplicates.isEmpty()) {
222         report.append(QLatin1Char('\n'));
223         QString duplicatesString;
224         for (const QString &dupe : std::as_const(m_possibleDuplicates)) {
225             duplicatesString += QLatin1String("bug ") + dupe + QLatin1String(", ");
226         }
227         duplicatesString = duplicatesString.left(duplicatesString.length() - 2) + QLatin1Char('.');
228         report.append(QStringLiteral("The reporter indicates this bug may be a duplicate of or related to %1\n").arg(duplicatesString));
229     }
230 
231     // Several possible duplicates (by bugzilla query)
232     if (!m_allPossibleDuplicatesByQuery.isEmpty()) {
233         report.append(QLatin1Char('\n'));
234         QString duplicatesString;
235         int count = m_allPossibleDuplicatesByQuery.count();
236         for (int i = 0; i < count && i < 5; i++) {
237             duplicatesString += QLatin1String("bug ") + m_allPossibleDuplicatesByQuery.at(i) + QLatin1String(", ");
238         }
239         duplicatesString = duplicatesString.left(duplicatesString.length() - 2) + QLatin1Char('.');
240         report.append(QStringLiteral("Possible duplicates by query: %1\n").arg(duplicatesString));
241     }
242 
243     switch (stamp) {
244     case DrKonqiStamp::Include: {
245         report.append(QLatin1String("\nReported using DrKonqi"));
246         const QString product = m_productMapping->bugzillaProduct();
247         const QString originalProduct = m_productMapping->bugzillaProductOriginal();
248         if (!originalProduct.isEmpty()) {
249             report.append(
250                 QStringLiteral(
251                     "\nThis report was filed against '%1' because the product '%2' could not be located in Bugzilla. Add it to drkonqi's mappings file!")
252                     .arg(product, originalProduct));
253         }
254         break;
255     }
256     case DrKonqiStamp::Exclude:
257         break;
258     }
259 
260     return report;
261 }
262 
generateAttachmentComment() const263 QString ReportInterface::generateAttachmentComment() const
264 {
265     // Note: no translations should be done in this function's strings
266 
267     const CrashedApplication *crashedApp = DrKonqi::crashedApplication();
268     const SystemInformation *sysInfo = DrKonqi::systemInformation();
269 
270     QString comment;
271 
272     // Program name and versions
273     comment.append(QStringLiteral("%1 (%2) using Qt %4\n\n").arg(crashedApp->fakeExecutableBaseName(), crashedApp->version(), sysInfo->qtVersion()));
274 
275     // Details of the crash situation
276     if (isBugAwarenessPageDataUseful()) {
277         comment.append(QStringLiteral("%1\n\n").arg(m_reportDetailText.trimmed()));
278     }
279 
280     // Backtrace (only 6 lines)
281     comment.append(QStringLiteral("-- Backtrace (Reduced):\n"));
282     QString reducedBacktrace = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->simplifiedBacktrace();
283     comment.append(reducedBacktrace.trimmed());
284 
285     return comment;
286 }
287 
newBugReportTemplate() const288 Bugzilla::NewBug ReportInterface::newBugReportTemplate() const
289 {
290     const SystemInformation *sysInfo = DrKonqi::systemInformation();
291 
292     Bugzilla::NewBug bug;
293     bug.product = m_productMapping->bugzillaProduct();
294     bug.component = m_productMapping->bugzillaComponent();
295     bug.version = m_productMapping->bugzillaVersion();
296     bug.op_sys = sysInfo->bugzillaOperatingSystem();
297     if (sysInfo->compiledSources()) {
298         bug.platform = QLatin1String("Compiled Sources");
299     } else {
300         bug.platform = sysInfo->bugzillaPlatform();
301     }
302     bug.keywords = QStringList{QStringLiteral("drkonqi")};
303     bug.priority = QLatin1String("NOR");
304     bug.severity = QLatin1String("crash");
305     bug.summary = m_reportTitle;
306 
307     return bug;
308 }
309 
sendBugReport()310 void ReportInterface::sendBugReport()
311 {
312     if (m_attachToBugNumber > 0) {
313         // We are going to attach the report to an existent one
314         connect(m_bugzillaManager, &BugzillaManager::addMeToCCFinished, this, &ReportInterface::attachBacktraceWithReport);
315         connect(m_bugzillaManager, &BugzillaManager::addMeToCCError, this, &ReportInterface::sendReportError);
316         // First add the user to the CC list, then attach
317         m_bugzillaManager->addMeToCC(m_attachToBugNumber);
318     } else {
319         // Creating a new bug report
320         bool attach = false;
321         Bugzilla::NewBug report = newBugReportTemplate();
322         report.description = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Complete);
323 
324         // If the report is too long try to reduce it, try to not include the
325         // backtrace and eventually give up.
326         // Bugzilla has a hard-limit on the server side, if we cannot strip the
327         // report down enough the submission will simply not work.
328         // Exhausting the cap with just user input is nigh impossible, so we'll
329         // forego handling of the report being too long even without without
330         // backtrace.
331         // https://bugs.kde.org/show_bug.cgi?id=248807
332         if (report.description.size() >= s_maxReportSize) {
333             report.description = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Reduced);
334             attach = true;
335         }
336         if (report.description.size() >= s_maxReportSize) {
337             report.description = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Exclude);
338             attach = true;
339         }
340         Q_ASSERT(!report.description.isEmpty());
341 
342         connect(m_bugzillaManager, &BugzillaManager::sendReportErrorInvalidValues, this, &ReportInterface::sendUsingDefaultProduct);
343         connect(m_bugzillaManager, &BugzillaManager::reportSent, this, [=](int bugId) {
344             if (attach) {
345                 m_attachToBugNumber = bugId;
346                 attachBacktrace(QStringLiteral("DrKonqi auto-attaching complete backtrace."));
347             } else {
348                 Q_EMIT reportSent(bugId);
349             }
350         });
351         connect(m_bugzillaManager, &BugzillaManager::sendReportError, this, &ReportInterface::sendReportError);
352         m_bugzillaManager->sendReport(report);
353     }
354 }
355 
sendUsingDefaultProduct() const356 void ReportInterface::sendUsingDefaultProduct() const
357 {
358     // Fallback function: if some of the custom values fail, we need to reset all the fields to the default
359     //(and valid) bugzilla values; and try to resend
360     Bugzilla::NewBug bug = newBugReportTemplate();
361     bug.product = QLatin1String("kde");
362     bug.component = QLatin1String("general");
363     bug.platform = QLatin1String("unspecified");
364     bug.description = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Complete);
365     m_bugzillaManager->sendReport(bug);
366 }
367 
attachBacktraceWithReport()368 void ReportInterface::attachBacktraceWithReport()
369 {
370     attachBacktrace(generateAttachmentComment());
371 }
372 
attachBacktrace(const QString & comment)373 void ReportInterface::attachBacktrace(const QString &comment)
374 {
375     // The user was added to the CC list, proceed with the attachment
376     connect(m_bugzillaManager, &BugzillaManager::attachToReportSent, this, &ReportInterface::attachSent);
377     connect(m_bugzillaManager, &BugzillaManager::attachToReportError, this, &ReportInterface::sendReportError);
378 
379     QString reportText = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Complete);
380     QString filename = getSuggestedKCrashFilename(DrKonqi::crashedApplication());
381     QLatin1String summary("New crash information added by DrKonqi");
382 
383     // Attach the report. The comment of the attachment also includes the bug description
384     m_bugzillaManager->attachTextToReport(reportText, filename, summary, m_attachToBugNumber, comment);
385 }
386 
attachSent(int attachId)387 void ReportInterface::attachSent(int attachId)
388 {
389     Q_UNUSED(attachId);
390 
391     // The bug was attached, consider it "sent"
392     Q_EMIT reportSent(m_attachToBugNumber);
393 }
394 
relatedBugzillaProducts() const395 QStringList ReportInterface::relatedBugzillaProducts() const
396 {
397     return m_productMapping->relatedBugzillaProducts();
398 }
399 
isWorthReporting() const400 bool ReportInterface::isWorthReporting() const
401 {
402     if (DrKonqi::ignoreQuality()) {
403         return true;
404     }
405 
406     // Evaluate if the provided information is useful enough to enable the automatic report
407     bool needToReport = false;
408 
409     if (!m_userRememberCrashSituation) {
410         // This should never happen... but...
411         return false;
412     }
413 
414     int rating = selectedOptionsRating();
415 
416     BacktraceParser::Usefulness use = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->backtraceUsefulness();
417 
418     switch (use) {
419     case BacktraceParser::ReallyUseful: {
420         // Perfect backtrace: require at least one option or a 100%-50% reproducible crash
421         needToReport = (rating >= 2) || (m_reproducible == ReproducibleEverytime || m_reproducible == ReproducibleSometimes);
422         break;
423     }
424     case BacktraceParser::MayBeUseful: {
425         // Not perfect backtrace: require at least two options or a 100% reproducible crash
426         needToReport = (rating >= 3) || (m_reproducible == ReproducibleEverytime);
427         break;
428     }
429     case BacktraceParser::ProbablyUseless:
430         // Bad backtrace: require at least two options and always reproducible (strict)
431         needToReport = (rating >= 5) && (m_reproducible == ReproducibleEverytime);
432         break;
433     case BacktraceParser::Useless:
434     case BacktraceParser::InvalidUsefulness: {
435         needToReport = false;
436     }
437     }
438 
439     return needToReport;
440 }
441 
setAttachToBugNumber(uint bugNumber)442 void ReportInterface::setAttachToBugNumber(uint bugNumber)
443 {
444     // If bugNumber>0, the report is going to be attached to bugNumber
445     m_attachToBugNumber = bugNumber;
446 }
447 
attachToBugNumber() const448 uint ReportInterface::attachToBugNumber() const
449 {
450     return m_attachToBugNumber;
451 }
452 
setDuplicateId(uint duplicate)453 void ReportInterface::setDuplicateId(uint duplicate)
454 {
455     m_duplicate = duplicate;
456 }
457 
duplicateId() const458 uint ReportInterface::duplicateId() const
459 {
460     return m_duplicate;
461 }
462 
setPossibleDuplicatesByQuery(const QStringList & list)463 void ReportInterface::setPossibleDuplicatesByQuery(const QStringList &list)
464 {
465     m_allPossibleDuplicatesByQuery = list;
466 }
467 
bugzillaManager() const468 BugzillaManager *ReportInterface::bugzillaManager() const
469 {
470     return m_bugzillaManager;
471 }
472 
appDetailsExamples() const473 ApplicationDetailsExamples *ReportInterface::appDetailsExamples() const
474 {
475     return m_appDetailsExamples;
476 }
477 
productMapping() const478 ProductMapping *ReportInterface::productMapping() const
479 {
480     return m_productMapping;
481 }
482