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

0001 /*******************************************************************
0002  * reportinterface.cpp
0003  * SPDX-FileCopyrightText: 2009, 2010, 2011 Dario Andres Rodriguez <andresbajotierra@gmail.com>
0004  * SPDX-FileCopyrightText: 2009 George Kiagiadakis <gkiagia@users.sourceforge.net>
0005  * SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
0006  *
0007  * SPDX-License-Identifier: GPL-2.0-or-later
0008  *
0009  ******************************************************************/
0010 
0011 #include "reportinterface.h"
0012 
0013 #include <chrono>
0014 
0015 #include <KIO/TransferJob>
0016 #include <KLocalizedString>
0017 #include <KUserFeedback/Provider>
0018 
0019 #include "backtracegenerator.h"
0020 #include "bugzillalib.h"
0021 #include "config-drkonqi.h"
0022 #include "crashedapplication.h"
0023 #include "debuggermanager.h"
0024 #include "drkonqi.h"
0025 #include "drkonqi_debug.h"
0026 #include "parser/backtraceparser.h"
0027 #include "productmapping.h"
0028 #include "systeminformation.h"
0029 
0030 // Max size a report may have. This is enforced in bugzilla, hardcoded, and
0031 // cannot be queried through the API, so handle this client-side in a hardcoded
0032 // fashion as well.
0033 static const int s_maxReportSize = 65535;
0034 
0035 ReportInterface::ReportInterface(QObject *parent)
0036     : QObject(parent)
0037     , m_duplicate(0)
0038 {
0039     m_bugzillaManager = new BugzillaManager(KDE_BUGZILLA_URL, this);
0040 
0041     m_productMapping = new ProductMapping(DrKonqi::crashedApplication(), m_bugzillaManager, this);
0042 
0043     // Information the user can provide about the crash
0044     m_userRememberCrashSituation = false;
0045     m_reproducible = ReproducibleUnsure;
0046     m_provideActionsApplicationDesktop = false;
0047     m_provideUnusualBehavior = false;
0048     m_provideApplicationConfigurationDetails = false;
0049 
0050     // Do not attach the bug report to any other existent report (create a new one)
0051     m_attachToBugNumber = 0;
0052 
0053     connect(&m_sentryBeacon, &SentryBeacon::eventSent, this, [this] {
0054         m_sentryEventSent = true;
0055         maybeDone();
0056     });
0057     connect(&m_sentryBeacon, &SentryBeacon::userFeedbackSent, this, [this] {
0058         m_sentryUserFeedbackSent = true;
0059         maybeDone();
0060     });
0061     if (KUserFeedback::Provider provider; provider.isEnabled() && provider.telemetryMode() == KUserFeedback::Provider::DetailedUsageStatistics
0062         && !DrKonqi::isTestingBugzilla() && qgetenv("DRKONQI_KDE_BUGZILLA_URL").isEmpty() && !DrKonqi::crashedApplication()->hasDeletedFiles()) {
0063         metaObject()->invokeMethod(this, [this] {
0064             // Send crash event ASAP, if applicable. Trace quality doesn't matter for it.
0065             sendCrashEvent();
0066         });
0067     }
0068 }
0069 
0070 void ReportInterface::setBugAwarenessPageData(bool rememberSituation, Reproducible reproducible, bool actions, bool unusual, bool configuration)
0071 {
0072     // Save the information the user can provide about the crash from the assistant page
0073     m_userRememberCrashSituation = rememberSituation;
0074     m_reproducible = reproducible;
0075     m_provideActionsApplicationDesktop = actions;
0076     m_provideUnusualBehavior = unusual;
0077     m_provideApplicationConfigurationDetails = configuration;
0078 }
0079 
0080 bool ReportInterface::isBugAwarenessPageDataUseful() const
0081 {
0082     // Determine if the assistant should proceed, considering the amount of information
0083     // the user can provide
0084     int rating = selectedOptionsRating();
0085 
0086     // Minimum information required even for a good backtrace.
0087     bool useful = m_userRememberCrashSituation && (rating >= 2 || (m_reproducible == ReproducibleSometimes || m_reproducible == ReproducibleEverytime));
0088     return useful;
0089 }
0090 
0091 int ReportInterface::selectedOptionsRating() const
0092 {
0093     // Check how many information the user can provide and generate a rating
0094     int rating = 0;
0095     if (m_provideActionsApplicationDesktop) {
0096         rating += 3;
0097     }
0098     if (m_provideApplicationConfigurationDetails) {
0099         rating += 2;
0100     }
0101     if (m_provideUnusualBehavior) {
0102         rating += 1;
0103     }
0104     return rating;
0105 }
0106 
0107 QString ReportInterface::backtrace() const
0108 {
0109     return m_backtrace;
0110 }
0111 
0112 void ReportInterface::setBacktrace(const QString &backtrace)
0113 {
0114     m_backtrace = backtrace;
0115     Q_EMIT backtraceChanged();
0116 }
0117 
0118 QStringList ReportInterface::firstBacktraceFunctions() const
0119 {
0120     return m_firstBacktraceFunctions;
0121 }
0122 
0123 void ReportInterface::setFirstBacktraceFunctions(const QStringList &functions)
0124 {
0125     m_firstBacktraceFunctions = functions;
0126 }
0127 
0128 QString ReportInterface::title() const
0129 {
0130     return m_reportTitle;
0131 }
0132 
0133 void ReportInterface::setTitle(const QString &text)
0134 {
0135     m_reportTitle = text;
0136     Q_EMIT titleChanged();
0137 }
0138 
0139 void ReportInterface::setDetailText(const QString &text)
0140 {
0141     m_reportDetailText = text;
0142     Q_EMIT detailTextChanged();
0143 }
0144 
0145 void ReportInterface::setPossibleDuplicates(const QStringList &list)
0146 {
0147     m_possibleDuplicates = list;
0148 }
0149 
0150 QString ReportInterface::generateReportFullText(DrKonqiStamp stamp, Backtrace inlineBacktrace) const
0151 {
0152     // Note: no translations should be done in this function's strings
0153 
0154     const CrashedApplication *crashedApp = DrKonqi::crashedApplication();
0155     const SystemInformation *sysInfo = DrKonqi::systemInformation();
0156 
0157     QString report;
0158 
0159     // Program name and versions
0160     report.append(QStringLiteral("Application: %1 (%2)\n").arg(crashedApp->fakeExecutableBaseName(), crashedApp->version()));
0161     if (sysInfo->compiledSources()) {
0162         report.append(QStringLiteral(" (Compiled from sources)\n"));
0163     } else {
0164         report.append(QLatin1Char('\n'));
0165     }
0166     report.append(QStringLiteral("Qt Version: %1\n").arg(sysInfo->qtVersion()));
0167     report.append(QStringLiteral("Frameworks Version: %1\n").arg(sysInfo->frameworksVersion()));
0168 
0169     report.append(QStringLiteral("Operating System: %1\n").arg(sysInfo->operatingSystem()));
0170     report.append(QStringLiteral("Windowing System: %1\n").arg(sysInfo->windowSystem()));
0171 
0172     // LSB output or manually selected distro
0173     if (!sysInfo->distributionPrettyName().isEmpty()) {
0174         report.append(QStringLiteral("Distribution: %1\n").arg(sysInfo->distributionPrettyName()));
0175     } else if (!sysInfo->bugzillaPlatform().isEmpty() && sysInfo->bugzillaPlatform() != QLatin1String("unspecified")) {
0176         report.append(QStringLiteral("Distribution (Platform): %1\n").arg(sysInfo->bugzillaPlatform()));
0177     }
0178 
0179     report.append(QStringLiteral("DrKonqi: %1 [%2]\n").arg(QString::fromLatin1(PROJECT_VERSION), DrKonqi::backendClassName()));
0180     report.append(QLatin1Char('\n'));
0181 
0182     // Details of the crash situation
0183     if (isBugAwarenessPageDataUseful()) {
0184         report.append(QStringLiteral("-- Information about the crash:\n"));
0185         if (!m_reportDetailText.isEmpty()) {
0186             report.append(m_reportDetailText.trimmed());
0187         } else {
0188             // If the user manual reports this crash, he/she should know what to put in here.
0189             // This message is the only one translated in this function
0190             report.append(xi18nc("@info/plain",
0191                                  "<placeholder>In detail, tell us what you were doing "
0192                                  " when the application crashed.</placeholder>"));
0193         }
0194         report.append(QLatin1String("\n\n"));
0195     }
0196 
0197     // Crash reproducibility
0198     switch (m_reproducible) {
0199     case ReproducibleUnsure:
0200         report.append(QStringLiteral("The reporter is unsure if this crash is reproducible.\n\n"));
0201         break;
0202     case ReproducibleNever:
0203         report.append(QStringLiteral("The crash does not seem to be reproducible.\n\n"));
0204         break;
0205     case ReproducibleSometimes:
0206         report.append(QStringLiteral("The crash can be reproduced sometimes.\n\n"));
0207         break;
0208     case ReproducibleEverytime:
0209         report.append(QStringLiteral("The crash can be reproduced every time.\n\n"));
0210         break;
0211     }
0212 
0213     // Backtrace
0214     switch (inlineBacktrace) {
0215     case Backtrace::Complete:
0216         report.append(QStringLiteral("-- Backtrace:\n"));
0217         break;
0218     case Backtrace::Reduced:
0219         report.append(QStringLiteral("-- Backtrace (Reduced):\n"));
0220         break;
0221     case Backtrace::Exclude:
0222         report.append(QStringLiteral("The backtrace was excluded and likely attached as a file.\n"));
0223         break;
0224     }
0225     if (!m_backtrace.isEmpty()) {
0226         switch (inlineBacktrace) {
0227         case Backtrace::Complete:
0228             report.append(m_backtrace.trimmed() + QLatin1Char('\n'));
0229             break;
0230         case Backtrace::Reduced:
0231             report.append(DrKonqi::debuggerManager()->backtraceGenerator()->parser()->simplifiedBacktrace() + QLatin1Char('\n'));
0232             break;
0233         case Backtrace::Exclude:
0234             report.append(QStringLiteral("The backtrace is attached as a comment due to length constraints\n"));
0235             break;
0236         }
0237     } else {
0238         report.append(QStringLiteral("A useful backtrace could not be generated\n"));
0239     }
0240 
0241     // Possible duplicates (selected by the user)
0242     if (!m_possibleDuplicates.isEmpty()) {
0243         report.append(QLatin1Char('\n'));
0244         QString duplicatesString;
0245         for (const QString &dupe : std::as_const(m_possibleDuplicates)) {
0246             duplicatesString += QLatin1String("bug ") + dupe + QLatin1String(", ");
0247         }
0248         duplicatesString = duplicatesString.left(duplicatesString.length() - 2) + QLatin1Char('.');
0249         report.append(QStringLiteral("The reporter indicates this bug may be a duplicate of or related to %1\n").arg(duplicatesString));
0250     }
0251 
0252     // Several possible duplicates (by bugzilla query)
0253     if (!m_allPossibleDuplicatesByQuery.isEmpty()) {
0254         report.append(QLatin1Char('\n'));
0255         QString duplicatesString;
0256         int count = m_allPossibleDuplicatesByQuery.count();
0257         for (int i = 0; i < count && i < 5; i++) {
0258             duplicatesString += QLatin1String("bug ") + m_allPossibleDuplicatesByQuery.at(i) + QLatin1String(", ");
0259         }
0260         duplicatesString = duplicatesString.left(duplicatesString.length() - 2) + QLatin1Char('.');
0261         report.append(QStringLiteral("Possible duplicates by query: %1\n").arg(duplicatesString));
0262     }
0263 
0264     switch (stamp) {
0265     case DrKonqiStamp::Include: {
0266         report.append(QLatin1String("\nReported using DrKonqi"));
0267         const QString product = m_productMapping->bugzillaProduct();
0268         const QString originalProduct = m_productMapping->bugzillaProductOriginal();
0269         if (!originalProduct.isEmpty()) {
0270             report.append(
0271                 QStringLiteral(
0272                     "\nThis report was filed against '%1' because the product '%2' could not be located in Bugzilla. Add it to drkonqi's mappings file!")
0273                     .arg(product, originalProduct));
0274         }
0275         break;
0276     }
0277     case DrKonqiStamp::Exclude:
0278         break;
0279     }
0280 
0281     return report;
0282 }
0283 
0284 QString ReportInterface::generateAttachmentComment() const
0285 {
0286     // Note: no translations should be done in this function's strings
0287 
0288     const CrashedApplication *crashedApp = DrKonqi::crashedApplication();
0289     const SystemInformation *sysInfo = DrKonqi::systemInformation();
0290 
0291     QString comment;
0292 
0293     // Program name and versions
0294     comment.append(QStringLiteral("%1 (%2) using Qt %4\n\n").arg(crashedApp->fakeExecutableBaseName(), crashedApp->version(), sysInfo->qtVersion()));
0295 
0296     // Details of the crash situation
0297     if (isBugAwarenessPageDataUseful()) {
0298         comment.append(QStringLiteral("%1\n\n").arg(m_reportDetailText.trimmed()));
0299     }
0300 
0301     // Backtrace (only 6 lines)
0302     comment.append(QStringLiteral("-- Backtrace (Reduced):\n"));
0303     QString reducedBacktrace = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->simplifiedBacktrace();
0304     comment.append(reducedBacktrace.trimmed());
0305 
0306     return comment;
0307 }
0308 
0309 Bugzilla::NewBug ReportInterface::newBugReportTemplate() const
0310 {
0311     const SystemInformation *sysInfo = DrKonqi::systemInformation();
0312 
0313     Bugzilla::NewBug bug;
0314     bug.product = m_productMapping->bugzillaProduct();
0315     bug.component = m_productMapping->bugzillaComponent();
0316     bug.version = m_productMapping->bugzillaVersion();
0317     bug.op_sys = sysInfo->bugzillaOperatingSystem();
0318     if (sysInfo->compiledSources()) {
0319         bug.platform = QLatin1String("Compiled Sources");
0320     } else {
0321         bug.platform = sysInfo->bugzillaPlatform();
0322     }
0323     bug.keywords = QStringList{QStringLiteral("drkonqi")};
0324     bug.priority = QLatin1String("NOR");
0325     bug.severity = QLatin1String("crash");
0326     bug.summary = m_reportTitle;
0327 
0328     return bug;
0329 }
0330 
0331 void ReportInterface::sendCrashEvent()
0332 {
0333 #if WITH_SENTRY
0334     if (DrKonqi::debuggerManager()->backtraceGenerator()->state() == BacktraceGenerator::Loaded) {
0335         m_sentryBeacon.sendEvent();
0336         return;
0337     }
0338     static bool connected = false;
0339     if (!connected) {
0340         connected = true;
0341         connect(DrKonqi::debuggerManager()->backtraceGenerator(), &BacktraceGenerator::done, this, [this] {
0342             m_sentryBeacon.sendEvent();
0343         });
0344     }
0345     if (DrKonqi::debuggerManager()->backtraceGenerator()->state() != BacktraceGenerator::Loading) {
0346         DrKonqi::debuggerManager()->backtraceGenerator()->start();
0347     }
0348 #endif
0349 }
0350 
0351 void ReportInterface::sendCrashComment()
0352 {
0353 #if WITH_SENTRY
0354     m_sentryBeacon.sendUserFeedback(m_reportTitle + QLatin1Char('\n') + m_reportDetailText + QLatin1Char('\n') + DrKonqi::kdeBugzillaURL()
0355                                     + QLatin1String("show_bug.cgi?id=%1").arg(QString::number(m_sentReport)));
0356 #endif
0357 }
0358 
0359 void ReportInterface::sendBugReport()
0360 {
0361     sendCrashEvent();
0362 
0363     if (m_attachToBugNumber > 0) {
0364         // We are going to attach the report to an existent one
0365         connect(m_bugzillaManager, &BugzillaManager::addMeToCCFinished, this, &ReportInterface::attachBacktraceWithReport);
0366         connect(m_bugzillaManager, &BugzillaManager::addMeToCCError, this, &ReportInterface::sendReportError);
0367         // First add the user to the CC list, then attach
0368         m_bugzillaManager->addMeToCC(m_attachToBugNumber);
0369     } else {
0370         // Creating a new bug report
0371         bool attach = false;
0372         Bugzilla::NewBug report = newBugReportTemplate();
0373         report.description = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Complete);
0374 
0375         // If the report is too long try to reduce it, try to not include the
0376         // backtrace and eventually give up.
0377         // Bugzilla has a hard-limit on the server side, if we cannot strip the
0378         // report down enough the submission will simply not work.
0379         // Exhausting the cap with just user input is nigh impossible, so we'll
0380         // forego handling of the report being too long even without without
0381         // backtrace.
0382         // https://bugs.kde.org/show_bug.cgi?id=248807
0383         if (report.description.size() >= s_maxReportSize) {
0384             report.description = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Reduced);
0385             attach = true;
0386         }
0387         if (report.description.size() >= s_maxReportSize) {
0388             report.description = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Exclude);
0389             attach = true;
0390         }
0391         Q_ASSERT(!report.description.isEmpty());
0392 
0393         connect(m_bugzillaManager, &BugzillaManager::sendReportErrorInvalidValues, this, &ReportInterface::sendUsingDefaultProduct);
0394         connect(m_bugzillaManager, &BugzillaManager::reportSent, this, [=](int bugId) {
0395             if (attach) {
0396                 m_attachToBugNumber = bugId;
0397                 attachBacktrace(QStringLiteral("DrKonqi auto-attaching complete backtrace."));
0398             } else {
0399                 m_sentReport = bugId;
0400                 sendCrashComment();
0401                 maybeDone();
0402             }
0403         });
0404         connect(m_bugzillaManager, &BugzillaManager::sendReportError, this, &ReportInterface::sendReportError);
0405         m_bugzillaManager->sendReport(report);
0406     }
0407 }
0408 
0409 void ReportInterface::sendUsingDefaultProduct() const
0410 {
0411     // Fallback function: if some of the custom values fail, we need to reset all the fields to the default
0412     //(and valid) bugzilla values; and try to resend
0413     Bugzilla::NewBug bug = newBugReportTemplate();
0414     bug.product = QLatin1String("kde");
0415     bug.component = QLatin1String("general");
0416     bug.platform = QLatin1String("unspecified");
0417     bug.description = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Complete);
0418     m_bugzillaManager->sendReport(bug);
0419 }
0420 
0421 void ReportInterface::attachBacktraceWithReport()
0422 {
0423     attachBacktrace(generateAttachmentComment());
0424 }
0425 
0426 void ReportInterface::attachBacktrace(const QString &comment)
0427 {
0428     // The user was added to the CC list, proceed with the attachment
0429     connect(m_bugzillaManager, &BugzillaManager::attachToReportSent, this, &ReportInterface::attachSent);
0430     connect(m_bugzillaManager, &BugzillaManager::attachToReportError, this, &ReportInterface::sendReportError);
0431 
0432     QString reportText = generateReportFullText(ReportInterface::DrKonqiStamp::Include, ReportInterface::Backtrace::Complete);
0433     QString filename = getSuggestedKCrashFilename(DrKonqi::crashedApplication());
0434     QLatin1String summary("New crash information added by DrKonqi");
0435 
0436     // Attach the report. The comment of the attachment also includes the bug description
0437     m_bugzillaManager->attachTextToReport(reportText, filename, summary, m_attachToBugNumber, comment);
0438 }
0439 
0440 void ReportInterface::attachSent(int attachId)
0441 {
0442     Q_UNUSED(attachId);
0443 
0444     // The bug was attached, consider it "sent"
0445     m_sentReport = attachId;
0446     sendCrashComment();
0447     maybeDone();
0448 }
0449 
0450 QStringList ReportInterface::relatedBugzillaProducts() const
0451 {
0452     return m_productMapping->relatedBugzillaProducts();
0453 }
0454 
0455 bool ReportInterface::isWorthReporting() const
0456 {
0457     if (DrKonqi::ignoreQuality()) {
0458         return true;
0459     }
0460 
0461     // Evaluate if the provided information is useful enough to enable the automatic report
0462     bool needToReport = false;
0463 
0464     if (!m_userRememberCrashSituation) {
0465         // This should never happen... but...
0466         return false;
0467     }
0468 
0469     int rating = selectedOptionsRating();
0470 
0471     BacktraceParser::Usefulness use = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->backtraceUsefulness();
0472 
0473     switch (use) {
0474     case BacktraceParser::ReallyUseful: {
0475         // Perfect backtrace: require at least one option or a 100%-50% reproducible crash
0476         needToReport = (rating >= 2) || (m_reproducible == ReproducibleEverytime || m_reproducible == ReproducibleSometimes);
0477         break;
0478     }
0479     case BacktraceParser::MayBeUseful: {
0480         // Not perfect backtrace: require at least two options or a 100% reproducible crash
0481         needToReport = (rating >= 3) || (m_reproducible == ReproducibleEverytime);
0482         break;
0483     }
0484     case BacktraceParser::ProbablyUseless:
0485         // Bad backtrace: require at least two options and always reproducible (strict)
0486         needToReport = (rating >= 5) && (m_reproducible == ReproducibleEverytime);
0487         break;
0488     case BacktraceParser::Useless:
0489     case BacktraceParser::InvalidUsefulness: {
0490         needToReport = false;
0491     }
0492     }
0493 
0494     return needToReport;
0495 }
0496 
0497 void ReportInterface::setAttachToBugNumber(uint bugNumber)
0498 {
0499     // If bugNumber>0, the report is going to be attached to bugNumber
0500     m_attachToBugNumber = bugNumber;
0501     Q_EMIT attachToBugNumberChanged();
0502 }
0503 
0504 uint ReportInterface::attachToBugNumber() const
0505 {
0506     return m_attachToBugNumber;
0507 }
0508 
0509 void ReportInterface::setDuplicateId(uint duplicate)
0510 {
0511     m_duplicate = duplicate;
0512     Q_EMIT duplicateIdChanged();
0513 }
0514 
0515 uint ReportInterface::duplicateId() const
0516 {
0517     return m_duplicate;
0518 }
0519 
0520 void ReportInterface::setPossibleDuplicatesByQuery(const QStringList &list)
0521 {
0522     m_allPossibleDuplicatesByQuery = list;
0523 }
0524 
0525 BugzillaManager *ReportInterface::bugzillaManager() const
0526 {
0527     return m_bugzillaManager;
0528 }
0529 
0530 ProductMapping *ReportInterface::productMapping() const
0531 {
0532     return m_productMapping;
0533 }
0534 
0535 void ReportInterface::maybeDone()
0536 {
0537     if (m_sentReport != 0 && m_sentryEventSent && m_sentryUserFeedbackSent) {
0538         Q_EMIT done();
0539     }
0540 };