diff --git a/src/bugzillaintegration/bugzillalib.cpp b/src/bugzillaintegration/bugzillalib.cpp index b8d216d2..6ba7931c 100644 --- a/src/bugzillaintegration/bugzillalib.cpp +++ b/src/bugzillaintegration/bugzillalib.cpp @@ -1,366 +1,370 @@ /******************************************************************* * bugzillalib.cpp * Copyright 2009, 2011 Dario Andres Rodriguez * Copyright 2012 George Kiagiadakis * Copyright 2019 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "bugzillalib.h" #include #include "libbugzilla/clients/commentclient.h" #include "libbugzilla/connection.h" #include "libbugzilla/bugzilla.h" #include "drkonqi_debug.h" static const char showBugUrl[] = "show_bug.cgi?id=%1"; // Extra filter rigging. We don't want to leak secrets via qdebug, so install // a message handler which does nothing more than replace secrets in debug // messages with placeholders. // This is used as a global static (since message handlers are meant to be // static) and is slightly synchronizing across threads WRT the filter hash. struct QMessageFilterContainer { QMessageFilterContainer(); ~QMessageFilterContainer(); void insert(const QString &needle, const QString &replace); void clear(); QString filter(const QString &msg); // Message handler is called across threads. Synchronize for good measure. QReadWriteLock lock; QtMessageHandler handler; private: QHash filters; }; Q_GLOBAL_STATIC(QMessageFilterContainer, s_messageFilter) QMessageFilterContainer::QMessageFilterContainer() { handler = qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) { s_messageFilter->handler(type, context, s_messageFilter->filter(msg)); }); } QMessageFilterContainer::~QMessageFilterContainer() { qInstallMessageHandler(handler); } void QMessageFilterContainer::insert(const QString &needle, const QString &replace) { if (needle.isEmpty()) { return; } QWriteLocker locker(&lock); filters[needle] = replace; } QString QMessageFilterContainer::filter(const QString &msg) { QReadLocker locker(&lock); QString filteredMsg = msg; for (auto it = filters.constBegin(); it != filters.constEnd(); ++it) { filteredMsg.replace(it.key(), it.value()); } return filteredMsg; } void QMessageFilterContainer::clear() { QWriteLocker locker(&lock); filters.clear(); } BugzillaManager::BugzillaManager(const QString &bugTrackerUrl, QObject *parent) : QObject(parent) , m_bugTrackerUrl(bugTrackerUrl) , m_logged(false) , m_searchJob(nullptr) { Q_ASSERT(bugTrackerUrl.endsWith(QLatin1Char('/'))); Bugzilla::setConnection(new Bugzilla::HTTPConnection(QUrl(m_bugTrackerUrl + QStringLiteral("rest")))); } void BugzillaManager::lookupVersion() { KJob *job = Bugzilla::version(); connect(job, &KJob::finished, this, [this](KJob *job) { try { QString version = Bugzilla::version(job); setFeaturesForVersion(version); emit bugzillaVersionFound(); } catch (Bugzilla::Exception &e) { // Version detection problems simply mean we'll not mark the version // found and the UI will not allow reporting. qCWarning(DRKONQI_LOG) << e.whatString(); emit bugzillaVersionError(e.whatString()); } }); } void BugzillaManager::setFeaturesForVersion(const QString& version) { // A procedure to change Dr Konqi behaviour automatically when Bugzilla // software versions change. // // Changes should be added to Dr Konqi AHEAD of when the corresponding // Bugzilla software changes are released into bugs.kde.org, so that // Dr Konqi can continue to operate smoothly, without bug reports and a // reactive KDE software release. // // If Bugzilla announces a change to its software that affects Dr Konqi, // add executable code to implement the change automatically when the // Bugzilla software version changes. It goes at the end of this procedure // and elsewhere in this class (BugzillaManager) and/or other classes where // the change should actually be implemented. const int nVersionParts = 3; QString seps = QLatin1String("[._-]"); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList digits = version.split(QRegExp(seps), QString::SkipEmptyParts); +#else + QStringList digits = version.split(QRegExp(seps), Qt::SkipEmptyParts); +#endif while (digits.count() < nVersionParts) { digits << QLatin1String("0"); } if (digits.count() > nVersionParts) { qCWarning(DRKONQI_LOG) << QStringLiteral("Current Bugzilla version %1 has more than %2 parts. Check that this is not a problem.").arg(version).arg(nVersionParts); } qCDebug(DRKONQI_LOG) << "VERSION" << version; } void BugzillaManager::tryLogin(const QString &username, const QString &password) { m_username = username; m_password = password; refreshToken(); } void BugzillaManager::refreshToken() { Q_ASSERT(!m_username.isEmpty()); Q_ASSERT(!m_password.isEmpty()); m_logged = false; // Rest token and qdebug filters Bugzilla::connection().setToken(QString()); s_messageFilter->clear(); s_messageFilter->insert(m_password, QStringLiteral("PASSWORD")); KJob *job = Bugzilla::login(m_username, m_password); connect(job, &KJob::finished, this, [this](KJob *job) { try { auto details = Bugzilla::login(job); m_token = details.token; if (m_token.isEmpty()) { throw Bugzilla::RuntimeException(QStringLiteral("Did not receive a token")); } s_messageFilter->insert(m_token, QStringLiteral("TOKEN")); Bugzilla::connection().setToken(m_token); m_logged = true; emit loginFinished(true); } catch (Bugzilla::Exception &e) { qCWarning(DRKONQI_LOG) << e.whatString(); // Version detection problems simply mean we'll not mark the version // found and the UI will not allow reporting. emit loginError(e.whatString()); } }); } bool BugzillaManager::getLogged() const { return m_logged; } QString BugzillaManager::getUsername() const { return m_username; } void BugzillaManager::fetchBugReport(int bugnumber, QObject *jobOwner) { Bugzilla::BugSearch search; search.id = bugnumber; Bugzilla::BugClient client; auto job = m_searchJob = client.search(search); connect(job, &KJob::finished, this, [this, &client, jobOwner](KJob *job) { try { auto list = client.search(job); if (list.size() != 1) { throw Bugzilla::RuntimeException(QStringLiteral("Unexpected bug amount returned: %1").arg(list.size())); } auto bug = list.at(0); m_searchJob = nullptr; emit bugReportFetched(bug, jobOwner); } catch (Bugzilla::Exception &e) { qCWarning(DRKONQI_LOG) << e.whatString(); emit bugReportError(e.whatString(), jobOwner); } }); } void BugzillaManager::fetchComments(const Bugzilla::Bug::Ptr &bug, QObject *jobOwner) { Bugzilla::CommentClient client; auto job = client.getFromBug(bug->id()); connect(job, &KJob::finished, this, [this, &client, jobOwner](KJob *job) { try { auto comments = client.getFromBug(job); emit commentsFetched(comments, jobOwner); } catch (Bugzilla::Exception &e) { qCWarning(DRKONQI_LOG) << e.whatString(); emit commentsError(e.whatString(), jobOwner); } }); } // TODO: This would kinda benefit from an actual pagination class, // currently this implicitly relies on the caller to handle offests correctly. // Fortunately we only have one caller so it makes no difference. void BugzillaManager::searchBugs(const QStringList &products, const QString &severity, const QString &comment, int offset) { Bugzilla::BugSearch search; search.products = products; search.severity = severity; search.longdesc = comment; // Order descendingly by bug_id. This allows us to offset through the results // from newest to oldest. // The UI will later order our data anyway, so the order at which we receive // the data is not important for the UI (outside the fact that we want // to step through pages of data) search.order << QStringLiteral("bug_id DESC"); search.limit = 25; search.offset = offset; stopCurrentSearch(); Bugzilla::BugClient client; auto job = m_searchJob = Bugzilla::BugClient().search(search); connect(job, &KJob::finished, this, [this, &client](KJob *job) { try { auto list = client.search(job); m_searchJob = nullptr; emit searchFinished(list); } catch (Bugzilla::Exception &e) { qCWarning(DRKONQI_LOG) << e.whatString(); emit searchError(e.whatString()); } }); } void BugzillaManager::sendReport(const Bugzilla::NewBug &bug) { auto job = Bugzilla::BugClient().create(bug); connect(job, &KJob::finished, this, [this](KJob *job) { try { int id = Bugzilla::BugClient().create(job); Q_ASSERT(id > 0); emit reportSent(id); } catch (Bugzilla::Exception &e) { qCWarning(DRKONQI_LOG) << e.whatString(); emit sendReportError(e.whatString()); } }); } void BugzillaManager::attachTextToReport(const QString & text, const QString & filename, const QString & summary, int bugId, const QString & comment) { Bugzilla::NewAttachment attachment; attachment.ids = QList { bugId }; attachment.data = text; attachment.file_name = filename; attachment.summary = summary; attachment.comment = comment; attachment.content_type = QLatin1String("text/plain"); auto job = Bugzilla::AttachmentClient().createAttachment(bugId, attachment); connect(job, &KJob::finished, this, [this](KJob *job) { try { QList ids = Bugzilla::AttachmentClient().createAttachment(job); Q_ASSERT(ids.size() == 1); emit attachToReportSent(ids.at(0)); } catch (Bugzilla::Exception &e) { qCWarning(DRKONQI_LOG) << e.whatString(); emit attachToReportError(e.whatString()); } }); } void BugzillaManager::addMeToCC(int bugId) { Bugzilla::BugUpdate update; Q_ASSERT(!m_username.isEmpty()); update.cc->add << m_username; auto job = Bugzilla::BugClient().update(bugId, update); connect(job, &KJob::finished, this, [this](KJob *job) { try { const auto bugId = Bugzilla::BugClient().update(job); Q_ASSERT(bugId != 0); emit addMeToCCFinished(bugId); } catch (Bugzilla::Exception &e) { qCWarning(DRKONQI_LOG) << e.whatString(); emit addMeToCCError(e.whatString()); } }); } void BugzillaManager::fetchProductInfo(const QString &product) { auto job = Bugzilla::ProductClient().get(product); connect(job, &KJob::finished, this, [this](KJob *job) { try { auto ptr = Bugzilla::ProductClient().get(job); Q_ASSERT(ptr); productInfoFetched(ptr); } catch (Bugzilla::Exception &e) { qCWarning(DRKONQI_LOG) << e.whatString(); // This doesn't have a string because it is actually not used for // anything... emit productInfoError(); } }); } QString BugzillaManager::urlForBug(int bug_number) const { return QString(m_bugTrackerUrl) + QString::fromLatin1(showBugUrl).arg(bug_number); } void BugzillaManager::stopCurrentSearch() { if (m_searchJob) { //Stop previous searchJob m_searchJob->disconnect(); m_searchJob->kill(); m_searchJob = nullptr; } } diff --git a/src/bugzillaintegration/productmapping.cpp b/src/bugzillaintegration/productmapping.cpp index bf17ad59..3e585154 100644 --- a/src/bugzillaintegration/productmapping.cpp +++ b/src/bugzillaintegration/productmapping.cpp @@ -1,198 +1,210 @@ /******************************************************************* * productmapping.cpp * Copyright 2009 Dario Andres Rodriguez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "productmapping.h" #include #include #include "drkonqi_debug.h" #include #include "bugzillalib.h" #include "crashedapplication.h" ProductMapping::ProductMapping(const CrashedApplication * crashedApp, BugzillaManager * bzManager, QObject * parent) : QObject(parent) , m_crashedAppPtr(crashedApp) , m_bugzillaManagerPtr(bzManager) , m_bugzillaProductDisabled(false) , m_bugzillaVersionDisabled(false) { //Default "fallback" values m_bugzillaProduct = crashedApp->fakeExecutableBaseName(); m_bugzillaComponent = QStringLiteral("general"); m_bugzillaVersionString = QStringLiteral("unspecified"); m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct; map(crashedApp->fakeExecutableBaseName()); //Get valid versions connect(m_bugzillaManagerPtr, &BugzillaManager::productInfoFetched, this, &ProductMapping::checkProductInfo); m_bugzillaManagerPtr->fetchProductInfo(m_bugzillaProduct); } void ProductMapping::map(const QString & appName) { mapUsingInternalFile(appName); getRelatedProductsUsingInternalFile(m_bugzillaProduct); } void ProductMapping::mapUsingInternalFile(const QString & appName) { KConfig mappingsFile(QString::fromLatin1("mappings"), KConfig::NoGlobals, QStandardPaths::DataLocation); const KConfigGroup mappings = mappingsFile.group("Mappings"); if (mappings.hasKey(appName)) { QString mappingString = mappings.readEntry(appName); if (!mappingString.isEmpty()) { +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList list = mappingString.split(QLatin1Char('|'), QString::SkipEmptyParts); +#else + QStringList list = mappingString.split(QLatin1Char('|'), Qt::SkipEmptyParts); +#endif if (list.count()==2) { m_bugzillaProduct = list.at(0); m_bugzillaComponent = list.at(1); m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct; } else { qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Sections found " << list.count(); } } else { qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty " "(or there was an error when reading)"; } } } void ProductMapping::getRelatedProductsUsingInternalFile(const QString & bugzillaProduct) { //ProductGroup -> kontact=kdepim //Groups -> kdepim=kontact|kmail|korganizer|akonadi|pimlibs..etc KConfig mappingsFile(QString::fromLatin1("mappings"), KConfig::NoGlobals, QStandardPaths::DataLocation); const KConfigGroup productGroup = mappingsFile.group("ProductGroup"); //Get groups of the application QStringList groups; if (productGroup.hasKey(bugzillaProduct)) { QString group = productGroup.readEntry(bugzillaProduct); if (group.isEmpty()) { qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty " "(or there was an error when reading)"; return; } +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) groups = group.split(QLatin1Char('|'), QString::SkipEmptyParts); +#else + groups = group.split(QLatin1Char('|'), Qt::SkipEmptyParts); +#endif } //All KDE apps use the KDE Platform (basic libs) groups << QLatin1String("kdeplatform"); //Add the product itself m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct; //Get related products of each related group Q_FOREACH( const QString & group, groups ) { const KConfigGroup bzGroups = mappingsFile.group("BZGroups"); if (bzGroups.hasKey(group)) { QString bzGroup = bzGroups.readEntry(group); if (!bzGroup.isEmpty()) { +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList relatedGroups = bzGroup.split(QLatin1Char('|'), QString::SkipEmptyParts); +#else + QStringList relatedGroups = bzGroup.split(QLatin1Char('|'), Qt::SkipEmptyParts); +#endif if (relatedGroups.size()>0) { m_relatedBugzillaProducts.append(relatedGroups); } } else { qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty " "(or there was an error when reading)"; } } } } void ProductMapping::checkProductInfo(const Bugzilla::Product::Ptr product) { // check whether the product itself is disabled for new reports, // which usually means that product/application is unmaintained. m_bugzillaProductDisabled = !product->isActive(); // check whether the product on bugzilla contains the expected component if (!product->componentNames().contains(m_bugzillaComponent)) { m_bugzillaComponent = QLatin1String("general"); } // find the appropriate version to use on bugzilla const QString version = m_crashedAppPtr->version(); const QStringList &allVersions = product->allVersions(); if (allVersions.contains(version)) { //The version the crash application provided is a valid bugzilla version: use it ! m_bugzillaVersionString = version; } else if (version.endsWith(QLatin1String(".00"))) { //check if there is a version on bugzilla with just ".0" const QString shorterVersion = version.left(version.size() - 1); if (allVersions.contains(shorterVersion)) { m_bugzillaVersionString = shorterVersion; } } else if (!allVersions.contains(m_bugzillaVersionString)) { // No good match found, make sure the default is sound... // If our hardcoded fallback is not in bugzilla it was likely // renamed so we'll find the version with the lowest id instead // and that should technically have been the "default" version. Bugzilla::ProductVersion *lowestVersion = nullptr; for (const auto &version : product->versions()) { if (!lowestVersion || lowestVersion->id() > version->id()) { lowestVersion = version; } } if (lowestVersion) { m_bugzillaVersionString = lowestVersion->name(); } } // check whether that versions is disabled for new reports, which // usually means that version is outdated and not supported anymore. const QStringList &inactiveVersions = product->inactiveVersions(); m_bugzillaVersionDisabled = inactiveVersions.contains(m_bugzillaVersionString); } QStringList ProductMapping::relatedBugzillaProducts() const { return m_relatedBugzillaProducts; } QString ProductMapping::bugzillaProduct() const { return m_bugzillaProduct; } QString ProductMapping::bugzillaComponent() const { return m_bugzillaComponent; } QString ProductMapping::bugzillaVersion() const { return m_bugzillaVersionString; } bool ProductMapping::bugzillaProductDisabled() const { return m_bugzillaProductDisabled; } bool ProductMapping::bugzillaVersionDisabled() const { return m_bugzillaVersionDisabled; } diff --git a/src/debugger.cpp b/src/debugger.cpp index 7441b458..82d4d4a0 100644 --- a/src/debugger.cpp +++ b/src/debugger.cpp @@ -1,166 +1,170 @@ /* Copyright (C) 2009 George Kiagiadakis This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "debugger.h" #include #include #include #include #include #include "crashedapplication.h" #include "drkonqi.h" #include "drkonqi_debug.h" //static QList Debugger::availableInternalDebuggers(const QString & backend) { return availableDebuggers(QStringLiteral("debuggers/internal"), backend); } //static QList Debugger::availableExternalDebuggers(const QString & backend) { return availableDebuggers(QStringLiteral("debuggers/external"), backend); } bool Debugger::isValid() const { return m_config; } bool Debugger::isInstalled() const { QString tryexec = tryExec(); if(tryexec.isEmpty()) { qCDebug(DRKONQI_LOG) << "tryExec of" << codeName() << "is empty!"; return false; } // Find for executable in PATH and in our application path return !QStandardPaths::findExecutable(tryexec).isEmpty() || !QStandardPaths::findExecutable(tryexec, {QCoreApplication::applicationDirPath()}).isEmpty(); } QString Debugger::displayName() const { return isValid() ? m_config->group("General").readEntry("Name") : QString(); } QString Debugger::codeName() const { //fall back to the "TryExec" string if "CodeName" is not specified. //for most debuggers those strings should be the same return isValid() ? m_config->group("General").readEntry("CodeName", tryExec()) : QString(); } QString Debugger::tryExec() const { return isValid() ? m_config->group("General").readEntry("TryExec") : QString(); } QStringList Debugger::supportedBackends() const { return isValid() ? m_config->group("General").readEntry("Backends") +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) .split(QLatin1Char('|'), QString::SkipEmptyParts) : QStringList(); +#else + .split(QLatin1Char('|'), Qt::SkipEmptyParts) : QStringList(); +#endif } void Debugger::setUsedBackend(const QString & backendName) { if (supportedBackends().contains(backendName)) { m_backend = backendName; } } QString Debugger::command() const { return (isValid() && m_config->hasGroup(m_backend)) ? m_config->group(m_backend).readPathEntry("Exec", QString()) : QString(); } QString Debugger::backtraceBatchCommands() const { return (isValid() && m_config->hasGroup(m_backend)) ? m_config->group(m_backend).readPathEntry("BatchCommands", QString()) : QString(); } bool Debugger::runInTerminal() const { return (isValid() && m_config->hasGroup(m_backend)) ? m_config->group(m_backend).readEntry("Terminal", false) : false; } QString Debugger::backendValueOfParameter(const QString &key) const { return (isValid() && m_config->hasGroup(m_backend)) ? m_config->group(m_backend).readEntry(key, QString()) : QString(); } //static void Debugger::expandString(QString & str, ExpandStringUsage usage, const QString & tempFile) { const CrashedApplication *appInfo = DrKonqi::crashedApplication(); const QHash map = { { QLatin1String("progname"), appInfo->name() }, { QLatin1String("execname"), appInfo->fakeExecutableBaseName() }, { QLatin1String("execpath"), appInfo->executable().absoluteFilePath() }, { QLatin1String("signum"), QString::number(appInfo->signalNumber()) }, { QLatin1String("signame"), appInfo->signalName() }, { QLatin1String("pid"), QString::number(appInfo->pid()) }, { QLatin1String("tempfile"), tempFile }, { QLatin1String("thread"), QString::number(appInfo->thread()) }, }; if (usage == ExpansionUsageShell) { str = KMacroExpander::expandMacrosShellQuote(str, map); } else { str = KMacroExpander::expandMacros(str, map); } } //static QList Debugger::availableDebuggers(const QString & path, const QString & backend) { const QStringList debuggerDirs { // Search from application path, this helps when deploying an application // as binary blob (e.g. Windows exe). QCoreApplication::applicationDirPath() + QLatin1Char('/') + path, // Search in default path QStandardPaths::locate(QStandardPaths::AppDataLocation, path, QStandardPaths::LocateDirectory) }; QHash result; for (const auto &debuggerDir : qAsConst(debuggerDirs)) { const QStringList debuggers = QDir(debuggerDir).entryList(QDir::Files); for (const auto &debuggerFile : qAsConst(debuggers)) { Debugger debugger; debugger.m_config = KSharedConfig::openConfig(debuggerDir + QLatin1Char('/') + debuggerFile); if (result.contains(debugger.codeName())) { continue; // Already found in a higher priority location } if (debugger.supportedBackends().contains(backend)) { debugger.setUsedBackend(backend); result.insert(debugger.codeName(), debugger); } } } return result.values(); }