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 }