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

0001 /*******************************************************************
0002  * bugzillalib.cpp
0003  * SPDX-FileCopyrightText: 2009, 2011 Dario Andres Rodriguez <andresbajotierra@gmail.com>
0004  * SPDX-FileCopyrightText: 2012 George Kiagiadakis <kiagiadakis.george@gmail.com>
0005  * SPDX-FileCopyrightText: 2019 Harald Sitter <sitter@kde.org>
0006  *
0007  * SPDX-License-Identifier: GPL-2.0-or-later
0008  *
0009  ******************************************************************/
0010 
0011 #include "bugzillalib.h"
0012 
0013 #include <QReadWriteLock>
0014 #include <QRegularExpression>
0015 
0016 #include "drkonqi_debug.h"
0017 #include "libbugzilla/bugzilla.h"
0018 #include "libbugzilla/clients/commentclient.h"
0019 #include "libbugzilla/connection.h"
0020 
0021 static const char showBugUrl[] = "show_bug.cgi?id=%1";
0022 
0023 // Extra filter rigging. We don't want to leak secrets via qdebug, so install
0024 // a message handler which does nothing more than replace secrets in debug
0025 // messages with placeholders.
0026 // This is used as a global static (since message handlers are meant to be
0027 // static) and is slightly synchronizing across threads WRT the filter hash.
0028 struct QMessageFilterContainer {
0029     QMessageFilterContainer();
0030     ~QMessageFilterContainer();
0031     void insert(const QString &needle, const QString &replace);
0032     void clear();
0033 
0034     QString filter(const QString &msg);
0035 
0036     // Message handler is called across threads. Synchronize for good measure.
0037     QReadWriteLock lock;
0038     QtMessageHandler handler;
0039 
0040 private:
0041     QHash<QString, QString> filters;
0042 };
0043 
0044 Q_GLOBAL_STATIC(QMessageFilterContainer, s_messageFilter)
0045 
0046 QMessageFilterContainer::QMessageFilterContainer()
0047 {
0048     handler = qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) {
0049         s_messageFilter->handler(type, context, s_messageFilter->filter(msg));
0050     });
0051 }
0052 
0053 QMessageFilterContainer::~QMessageFilterContainer()
0054 {
0055     qInstallMessageHandler(handler);
0056 }
0057 
0058 void QMessageFilterContainer::insert(const QString &needle, const QString &replace)
0059 {
0060     if (needle.isEmpty()) {
0061         return;
0062     }
0063 
0064     QWriteLocker locker(&lock);
0065     filters[needle] = replace;
0066 }
0067 
0068 QString QMessageFilterContainer::filter(const QString &msg)
0069 {
0070     QReadLocker locker(&lock);
0071     QString filteredMsg = msg;
0072     for (auto it = filters.constBegin(); it != filters.constEnd(); ++it) {
0073         filteredMsg.replace(it.key(), it.value());
0074     }
0075     return filteredMsg;
0076 }
0077 
0078 void QMessageFilterContainer::clear()
0079 {
0080     QWriteLocker locker(&lock);
0081     filters.clear();
0082 }
0083 
0084 BugzillaManager::BugzillaManager(const QString &bugTrackerUrl, QObject *parent)
0085     : QObject(parent)
0086     , m_bugTrackerUrl(bugTrackerUrl.isEmpty() ? KDE_BUGZILLA_URL : bugTrackerUrl)
0087 {
0088     Q_ASSERT(bugTrackerUrl.endsWith(QLatin1Char('/')));
0089     Bugzilla::setConnection(new Bugzilla::HTTPConnection(QUrl(m_bugTrackerUrl + QStringLiteral("rest"))));
0090 }
0091 
0092 void BugzillaManager::lookupVersion()
0093 {
0094     KJob *job = Bugzilla::version();
0095     connect(job, &KJob::finished, this, [this](KJob *job) {
0096         try {
0097             QString version = Bugzilla::version(job);
0098             setFeaturesForVersion(version);
0099             Q_EMIT bugzillaVersionFound();
0100         } catch (Bugzilla::Exception &e) {
0101             // Version detection problems simply mean we'll not mark the version
0102             // found and the UI will not allow reporting.
0103             qCWarning(DRKONQI_LOG) << e.whatString();
0104             Q_EMIT bugzillaVersionError(e.whatString());
0105         }
0106     });
0107 }
0108 
0109 void BugzillaManager::setFeaturesForVersion(const QString &version)
0110 {
0111     // A procedure to change Dr Konqi behaviour automatically when Bugzilla
0112     // software versions change.
0113     //
0114     // Changes should be added to Dr Konqi AHEAD of when the corresponding
0115     // Bugzilla software changes are released into bugs.kde.org, so that
0116     // Dr Konqi can continue to operate smoothly, without bug reports and a
0117     // reactive KDE software release.
0118     //
0119     // If Bugzilla announces a change to its software that affects Dr Konqi,
0120     // add executable code to implement the change automatically when the
0121     // Bugzilla software version changes. It goes at the end of this procedure
0122     // and elsewhere in this class (BugzillaManager) and/or other classes where
0123     // the change should actually be implemented.
0124 
0125     const int nVersionParts = 3;
0126     QStringList digits = version.split(QRegularExpression(QStringLiteral("[._-]")), Qt::SkipEmptyParts);
0127     while (digits.count() < nVersionParts) {
0128         digits << QLatin1String("0");
0129     }
0130     if (digits.count() > nVersionParts) {
0131         qCWarning(DRKONQI_LOG)
0132             << QStringLiteral("Current Bugzilla version %1 has more than %2 parts. Check that this is not a problem.").arg(version).arg(nVersionParts);
0133     }
0134 
0135     qCDebug(DRKONQI_LOG) << "VERSION" << version;
0136 }
0137 
0138 void BugzillaManager::tryLogin(const QString &username, const QString &password)
0139 {
0140     m_username = username;
0141     m_password = password;
0142     refreshToken();
0143 }
0144 
0145 void BugzillaManager::refreshToken()
0146 {
0147     Q_ASSERT(!m_username.isEmpty());
0148     Q_ASSERT(!m_password.isEmpty());
0149     m_logged = false;
0150 
0151     // Rest token and qdebug filters
0152     Bugzilla::connection().setToken(QString());
0153     s_messageFilter->clear();
0154     s_messageFilter->insert(m_password, QStringLiteral("PASSWORD"));
0155 
0156     KJob *job = Bugzilla::login(m_username, m_password);
0157     connect(job, &KJob::finished, this, [this](KJob *job) {
0158         try {
0159             auto details = Bugzilla::login(job);
0160             m_token = details.token;
0161             if (m_token.isEmpty()) {
0162                 throw Bugzilla::RuntimeException(QStringLiteral("Did not receive a token"));
0163             }
0164 
0165             s_messageFilter->insert(m_token, QStringLiteral("TOKEN"));
0166             Bugzilla::connection().setToken(m_token);
0167             m_logged = true;
0168 
0169             Q_EMIT loginFinished(true);
0170         } catch (Bugzilla::Exception &e) {
0171             qCWarning(DRKONQI_LOG) << e.whatString();
0172             // Version detection problems simply mean we'll not mark the version
0173             // found and the UI will not allow reporting.
0174             Q_EMIT loginError(e.whatString());
0175         }
0176     });
0177 }
0178 
0179 bool BugzillaManager::getLogged() const
0180 {
0181     return m_logged;
0182 }
0183 
0184 QString BugzillaManager::getUsername() const
0185 {
0186     return m_username;
0187 }
0188 
0189 void BugzillaManager::fetchBugReport(int bugnumber, QObject *jobOwner)
0190 {
0191     Bugzilla::BugSearch search;
0192     search.id = bugnumber;
0193 
0194     Bugzilla::BugClient client;
0195     auto job = m_searchJob = client.search(search);
0196     connect(job, &KJob::finished, this, [this, client, jobOwner](KJob *job) {
0197         try {
0198             auto list = client.search(job);
0199             if (list.size() != 1) {
0200                 throw Bugzilla::RuntimeException(QStringLiteral("Unexpected bug amount returned: %1").arg(list.size()));
0201             }
0202             auto bug = list.at(0);
0203             m_searchJob = nullptr;
0204             Q_EMIT bugReportFetched(bug, jobOwner);
0205         } catch (Bugzilla::Exception &e) {
0206             qCWarning(DRKONQI_LOG) << e.whatString();
0207             Q_EMIT bugReportError(e.whatString(), jobOwner);
0208         }
0209     });
0210 }
0211 
0212 void BugzillaManager::fetchComments(const Bugzilla::Bug::Ptr &bug, QObject *jobOwner)
0213 {
0214     Bugzilla::CommentClient client;
0215     auto job = client.getFromBug(bug->id());
0216     connect(job, &KJob::finished, this, [this, client, jobOwner](KJob *job) {
0217         try {
0218             auto comments = client.getFromBug(job);
0219             Q_EMIT commentsFetched(comments, jobOwner);
0220         } catch (Bugzilla::Exception &e) {
0221             qCWarning(DRKONQI_LOG) << e.whatString();
0222             Q_EMIT commentsError(e.whatString(), jobOwner);
0223         }
0224     });
0225 }
0226 
0227 // TODO: This would kinda benefit from an actual pagination class,
0228 // currently this implicitly relies on the caller to handle offsets correctly.
0229 // Fortunately we only have one caller so it makes no difference.
0230 void BugzillaManager::searchBugs(const QStringList &products, const QString &severity, const QString &comment, int offset)
0231 {
0232     Bugzilla::BugSearch search;
0233     search.products = products;
0234     search.severity = severity;
0235     search.longdesc = comment;
0236     // Order descendingly by bug_id. This allows us to offset through the results
0237     // from newest to oldest.
0238     // The UI will later order our data anyway, so the order at which we receive
0239     // the data is not important for the UI (outside the fact that we want
0240     // to step through pages of data)
0241     search.order << QStringLiteral("bug_id DESC");
0242     search.limit = 25;
0243     search.offset = offset;
0244 
0245     stopCurrentSearch();
0246 
0247     Bugzilla::BugClient client;
0248     auto job = m_searchJob = Bugzilla::BugClient().search(search);
0249     connect(job, &KJob::finished, this, [this, client](KJob *job) {
0250         try {
0251             auto list = client.search(job);
0252             m_searchJob = nullptr;
0253             Q_EMIT searchFinished(list);
0254         } catch (Bugzilla::Exception &e) {
0255             qCWarning(DRKONQI_LOG) << e.whatString();
0256             Q_EMIT searchError(e.whatString());
0257         }
0258     });
0259 }
0260 
0261 void BugzillaManager::sendReport(const Bugzilla::NewBug &bug)
0262 {
0263     auto job = Bugzilla::BugClient().create(bug);
0264     connect(job, &KJob::finished, this, [this](KJob *job) {
0265         try {
0266             int id = Bugzilla::BugClient().create(job);
0267             Q_ASSERT(id > 0);
0268             Q_EMIT reportSent(id);
0269         } catch (Bugzilla::Exception &e) {
0270             qCWarning(DRKONQI_LOG) << e.whatString();
0271             Q_EMIT sendReportError(e.whatString());
0272         }
0273     });
0274 }
0275 
0276 void BugzillaManager::attachTextToReport(const QString &text, const QString &filename, const QString &summary, int bugId, const QString &comment)
0277 {
0278     Bugzilla::NewAttachment attachment;
0279     attachment.ids = QList<int>{bugId};
0280     attachment.data = text;
0281     attachment.file_name = filename;
0282     attachment.summary = summary;
0283     attachment.comment = comment;
0284     attachment.content_type = QLatin1String("text/plain");
0285 
0286     auto job = Bugzilla::AttachmentClient().createAttachment(bugId, attachment);
0287     connect(job, &KJob::finished, this, [this, bugId](KJob *job) {
0288         try {
0289             const QList<int> attachmentIds = Bugzilla::AttachmentClient().createAttachment(job);
0290             Q_ASSERT(attachmentIds.size() == 1); // NB: attachmentIds are not bug ids!
0291             Q_EMIT attachToReportSent(bugId);
0292         } catch (Bugzilla::Exception &e) {
0293             qCWarning(DRKONQI_LOG) << e.whatString();
0294             Q_EMIT attachToReportError(e.whatString());
0295         }
0296     });
0297 }
0298 
0299 void BugzillaManager::addMeToCC(int bugId)
0300 {
0301     Bugzilla::BugUpdate update;
0302     Q_ASSERT(!m_username.isEmpty());
0303     update.cc->add << m_username;
0304 
0305     auto job = Bugzilla::BugClient().update(bugId, update);
0306     connect(job, &KJob::finished, this, [this](KJob *job) {
0307         try {
0308             const auto bugId = Bugzilla::BugClient().update(job);
0309             Q_ASSERT(bugId != 0);
0310             Q_EMIT addMeToCCFinished(bugId);
0311         } catch (Bugzilla::Exception &e) {
0312             qCWarning(DRKONQI_LOG) << e.whatString();
0313             Q_EMIT addMeToCCError(e.whatString());
0314         }
0315     });
0316 }
0317 
0318 void BugzillaManager::fetchProductInfo(const QString &product)
0319 {
0320     auto job = Bugzilla::ProductClient().get(product);
0321     connect(job, &KJob::finished, this, [this](KJob *job) {
0322         try {
0323             auto ptr = Bugzilla::ProductClient().get(job);
0324             Q_ASSERT(ptr);
0325             Q_EMIT productInfoFetched(ptr);
0326         } catch (Bugzilla::Exception &e) {
0327             qCWarning(DRKONQI_LOG) << e.whatString();
0328             // This doesn't have a string because it is actually not used for
0329             // anything...
0330             Q_EMIT productInfoError();
0331         }
0332     });
0333 }
0334 
0335 QString BugzillaManager::urlForBug(int bug_number) const
0336 {
0337     return QString(m_bugTrackerUrl) + QString::fromLatin1(showBugUrl).arg(bug_number);
0338 }
0339 
0340 void BugzillaManager::stopCurrentSearch()
0341 {
0342     if (m_searchJob) { // Stop previous searchJob
0343         m_searchJob->disconnect();
0344         m_searchJob->kill();
0345         m_searchJob = nullptr;
0346     }
0347 }